fixes filter on MPT-transition. Add a high quality filter wir anti-aliasing
This commit is contained in:
@@ -285,11 +285,25 @@ func realSlice(in []complex128) []float64 {
|
||||
}
|
||||
|
||||
// Resample resamples audio data from one sample rate to another using linear interpolation
|
||||
// with proper anti-aliasing for downsampling
|
||||
func Resample(data []float64, fromSampleRate, toSampleRate int) []float64 {
|
||||
if fromSampleRate == toSampleRate {
|
||||
return data
|
||||
}
|
||||
|
||||
// For downsampling, apply anti-aliasing filter to prevent aliasing
|
||||
// Filter out frequencies above the target Nyquist frequency
|
||||
if toSampleRate < fromSampleRate {
|
||||
// Calculate target Nyquist frequency (slightly below to ensure clean cutoff)
|
||||
targetNyquist := float64(toSampleRate) / 2.0
|
||||
// Use 90% of Nyquist as cutoff to ensure clean anti-aliasing
|
||||
antiAliasCutoff := targetNyquist * 0.9
|
||||
|
||||
// Apply steep low-pass filter to prevent aliasing
|
||||
// Use 48 dB/octave slope for clean anti-aliasing
|
||||
data = CascadeHighcut(data, fromSampleRate, antiAliasCutoff, 48)
|
||||
}
|
||||
|
||||
// Calculate the resampling ratio
|
||||
ratio := float64(toSampleRate) / float64(fromSampleRate)
|
||||
newLength := int(float64(len(data)) * ratio)
|
||||
@@ -342,13 +356,71 @@ func FadeOutLinear(data []float64, fadeSamples int) []float64 {
|
||||
return out
|
||||
}
|
||||
|
||||
// biquadFilterState holds the state for a biquad filter
|
||||
type biquadFilterState struct {
|
||||
x1, x2 float64 // input history (intermediate values in DF2T)
|
||||
}
|
||||
|
||||
// applyBiquadDF2T applies a biquad filter using Direct Form II Transposed (more numerically stable)
|
||||
// This form is preferred for cascaded filters as it reduces numerical errors
|
||||
// Uses high-precision calculations and proper state management for maximum quality
|
||||
func applyBiquadDF2T(data []float64, b0, b1, b2, a1, a2 float64, state *biquadFilterState) []float64 {
|
||||
out := make([]float64, len(data))
|
||||
|
||||
// Initialize state if nil
|
||||
if state == nil {
|
||||
state = &biquadFilterState{}
|
||||
}
|
||||
|
||||
// Direct Form II Transposed implementation with high precision
|
||||
// This structure minimizes numerical errors in cascaded filters
|
||||
for i := 0; i < len(data); i++ {
|
||||
x := data[i]
|
||||
// Compute intermediate value (w[n] = x[n] - a1*w[n-1] - a2*w[n-2])
|
||||
w := x - a1*state.x1 - a2*state.x2
|
||||
// Compute output (y[n] = b0*w[n] + b1*w[n-1] + b2*w[n-2])
|
||||
y := b0*w + b1*state.x1 + b2*state.x2
|
||||
out[i] = y
|
||||
// Update state (shift delay line)
|
||||
state.x2 = state.x1
|
||||
state.x1 = w
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// warmupFilter applies a filter with warm-up to avoid transients
|
||||
// This is especially important for high-quality filtering
|
||||
func warmupFilter(data []float64, b0, b1, b2, a1, a2 float64) []float64 {
|
||||
if len(data) == 0 {
|
||||
return data
|
||||
}
|
||||
|
||||
// Use first sample value for warm-up to avoid transients
|
||||
warmupValue := data[0]
|
||||
state := &biquadFilterState{}
|
||||
|
||||
// Warm up the filter with a few samples of the first value
|
||||
// This initializes the filter state properly
|
||||
warmupSamples := 10
|
||||
warmupData := make([]float64, warmupSamples)
|
||||
for i := range warmupData {
|
||||
warmupData[i] = warmupValue
|
||||
}
|
||||
_ = applyBiquadDF2T(warmupData, b0, b1, b2, a1, a2, state)
|
||||
|
||||
// Now apply filter to actual data with warmed-up state
|
||||
return applyBiquadDF2T(data, b0, b1, b2, a1, a2, state)
|
||||
}
|
||||
|
||||
// ApplyLowpassButterworth applies a 2nd-order Butterworth low-pass filter to the data.
|
||||
// cutoffHz: cutoff frequency in Hz, sampleRate: sample rate in Hz.
|
||||
// Uses Direct Form II Transposed for better numerical stability.
|
||||
func ApplyLowpassButterworth(data []float64, sampleRate int, cutoffHz float64) []float64 {
|
||||
if cutoffHz <= 0 || cutoffHz >= float64(sampleRate)/2 {
|
||||
return data
|
||||
}
|
||||
// Biquad coefficients
|
||||
// Biquad coefficients for Butterworth low-pass
|
||||
w0 := 2 * math.Pi * cutoffHz / float64(sampleRate)
|
||||
cosw0 := math.Cos(w0)
|
||||
sinw0 := math.Sin(w0)
|
||||
@@ -362,35 +434,26 @@ func ApplyLowpassButterworth(data []float64, sampleRate int, cutoffHz float64) [
|
||||
a1 := -2 * cosw0
|
||||
a2 := 1 - alpha
|
||||
|
||||
// Normalize
|
||||
// Normalize coefficients
|
||||
b0 /= a0
|
||||
b1 /= a0
|
||||
b2 /= a0
|
||||
a1 /= a0
|
||||
a2 /= a0
|
||||
|
||||
// Apply filter (Direct Form I)
|
||||
out := make([]float64, len(data))
|
||||
var x1, x2, y1, y2 float64
|
||||
for i := 0; i < len(data); i++ {
|
||||
x0 := data[i]
|
||||
y0 := b0*x0 + b1*x1 + b2*x2 - a1*y1 - a2*y2
|
||||
out[i] = y0
|
||||
x2 = x1
|
||||
x1 = x0
|
||||
y2 = y1
|
||||
y1 = y0
|
||||
}
|
||||
return out
|
||||
// Apply filter using Direct Form II Transposed
|
||||
state := &biquadFilterState{}
|
||||
return applyBiquadDF2T(data, b0, b1, b2, a1, a2, state)
|
||||
}
|
||||
|
||||
// ApplyHighpassButterworth applies a 2nd-order Butterworth high-pass filter to the data.
|
||||
// cutoffHz: cutoff frequency in Hz, sampleRate: sample rate in Hz.
|
||||
// Uses Direct Form II Transposed for better numerical stability.
|
||||
func ApplyHighpassButterworth(data []float64, sampleRate int, cutoffHz float64) []float64 {
|
||||
if cutoffHz <= 0 || cutoffHz >= float64(sampleRate)/2 {
|
||||
return data
|
||||
}
|
||||
// Biquad coefficients
|
||||
// Biquad coefficients for Butterworth high-pass
|
||||
w0 := 2 * math.Pi * cutoffHz / float64(sampleRate)
|
||||
cosw0 := math.Cos(w0)
|
||||
sinw0 := math.Sin(w0)
|
||||
@@ -404,53 +467,149 @@ func ApplyHighpassButterworth(data []float64, sampleRate int, cutoffHz float64)
|
||||
a1 := -2 * cosw0
|
||||
a2 := 1 - alpha
|
||||
|
||||
// Normalize
|
||||
// Normalize coefficients
|
||||
b0 /= a0
|
||||
b1 /= a0
|
||||
b2 /= a0
|
||||
a1 /= a0
|
||||
a2 /= a0
|
||||
|
||||
// Apply filter (Direct Form I)
|
||||
out := make([]float64, len(data))
|
||||
var x1, x2, y1, y2 float64
|
||||
for i := 0; i < len(data); i++ {
|
||||
x0 := data[i]
|
||||
y0 := b0*x0 + b1*x1 + b2*x2 - a1*y1 - a2*y2
|
||||
out[i] = y0
|
||||
x2 = x1
|
||||
x1 = x0
|
||||
y2 = y1
|
||||
y1 = y0
|
||||
}
|
||||
return out
|
||||
// Apply filter using Direct Form II Transposed
|
||||
state := &biquadFilterState{}
|
||||
return applyBiquadDF2T(data, b0, b1, b2, a1, a2, state)
|
||||
}
|
||||
|
||||
// CascadeLowcut applies the low-cut (high-pass) filter multiple times for steeper slopes.
|
||||
// slopeDb: 12, 24, 36, ... (dB/octave)
|
||||
// Uses Linkwitz-Riley design principles with high-quality implementation for maximum precision.
|
||||
// Features: proper warm-up, high-precision coefficients, optimized cascade structure.
|
||||
// slopeDb: 12, 24, 36, 48, ... (dB/octave)
|
||||
func CascadeLowcut(data []float64, sampleRate int, cutoffHz float64, slopeDb int) []float64 {
|
||||
if slopeDb < 12 {
|
||||
slopeDb = 12
|
||||
}
|
||||
n := slopeDb / 12
|
||||
out := data
|
||||
for i := 0; i < n; i++ {
|
||||
out = ApplyHighpassButterworth(out, sampleRate, cutoffHz)
|
||||
if cutoffHz <= 0 || cutoffHz >= float64(sampleRate)/2 {
|
||||
return data
|
||||
}
|
||||
|
||||
n := slopeDb / 12
|
||||
if n == 0 {
|
||||
return data
|
||||
}
|
||||
|
||||
// High-quality filter implementation with proper coefficient calculation
|
||||
// Use high precision for coefficient calculation
|
||||
w0 := 2.0 * math.Pi * cutoffHz / float64(sampleRate)
|
||||
|
||||
// Pre-calculate trigonometric functions once for precision
|
||||
cosw0 := math.Cos(w0)
|
||||
sinw0 := math.Sin(w0)
|
||||
|
||||
// Butterworth Q factor (1/sqrt(2) ≈ 0.7071067811865476 for maximally flat response)
|
||||
const butterworthQ = 0.7071067811865476
|
||||
alpha := sinw0 / (2.0 * butterworthQ)
|
||||
|
||||
// High-pass Butterworth coefficients
|
||||
b0 := (1.0 + cosw0) / 2.0
|
||||
b1 := -(1.0 + cosw0)
|
||||
b2 := (1.0 + cosw0) / 2.0
|
||||
a0 := 1.0 + alpha
|
||||
a1 := -2.0 * cosw0
|
||||
a2 := 1.0 - alpha
|
||||
|
||||
// Normalize coefficients for Direct Form II Transposed
|
||||
// This ensures proper scaling and numerical stability
|
||||
b0 /= a0
|
||||
b1 /= a0
|
||||
b2 /= a0
|
||||
a1 /= a0
|
||||
a2 /= a0
|
||||
|
||||
// Apply cascaded filters with proper warm-up for each stage
|
||||
// This ensures clean filtering without transients, especially important for MPT
|
||||
out := make([]float64, len(data))
|
||||
copy(out, data)
|
||||
|
||||
for stage := 0; stage < n; stage++ {
|
||||
// Use warm-up for first stage to avoid transients
|
||||
// Subsequent stages benefit from the already-filtered signal
|
||||
if stage == 0 {
|
||||
out = warmupFilter(out, b0, b1, b2, a1, a2)
|
||||
} else {
|
||||
// For subsequent stages, use fresh state but no warm-up needed
|
||||
// as the signal is already filtered
|
||||
state := &biquadFilterState{}
|
||||
filtered := applyBiquadDF2T(out, b0, b1, b2, a1, a2, state)
|
||||
copy(out, filtered)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// CascadeHighcut applies the high-cut (low-pass) filter multiple times for steeper slopes.
|
||||
// slopeDb: 12, 24, 36, ... (dB/octave)
|
||||
// Uses Linkwitz-Riley design principles with high-quality implementation for maximum precision.
|
||||
// Features: proper warm-up, high-precision coefficients, optimized cascade structure.
|
||||
// slopeDb: 12, 24, 36, 48, ... (dB/octave)
|
||||
func CascadeHighcut(data []float64, sampleRate int, cutoffHz float64, slopeDb int) []float64 {
|
||||
if slopeDb < 12 {
|
||||
slopeDb = 12
|
||||
}
|
||||
n := slopeDb / 12
|
||||
out := data
|
||||
for i := 0; i < n; i++ {
|
||||
out = ApplyLowpassButterworth(out, sampleRate, cutoffHz)
|
||||
if cutoffHz <= 0 || cutoffHz >= float64(sampleRate)/2 {
|
||||
return data
|
||||
}
|
||||
|
||||
n := slopeDb / 12
|
||||
if n == 0 {
|
||||
return data
|
||||
}
|
||||
|
||||
// High-quality filter implementation with proper coefficient calculation
|
||||
// Use high precision for coefficient calculation
|
||||
w0 := 2.0 * math.Pi * cutoffHz / float64(sampleRate)
|
||||
|
||||
// Pre-calculate trigonometric functions once for precision
|
||||
cosw0 := math.Cos(w0)
|
||||
sinw0 := math.Sin(w0)
|
||||
|
||||
// Butterworth Q factor (1/sqrt(2) ≈ 0.7071067811865476 for maximally flat response)
|
||||
const butterworthQ = 0.7071067811865476
|
||||
alpha := sinw0 / (2.0 * butterworthQ)
|
||||
|
||||
// Low-pass Butterworth coefficients
|
||||
b0 := (1.0 - cosw0) / 2.0
|
||||
b1 := 1.0 - cosw0
|
||||
b2 := (1.0 - cosw0) / 2.0
|
||||
a0 := 1.0 + alpha
|
||||
a1 := -2.0 * cosw0
|
||||
a2 := 1.0 - alpha
|
||||
|
||||
// Normalize coefficients for Direct Form II Transposed
|
||||
// This ensures proper scaling and numerical stability
|
||||
b0 /= a0
|
||||
b1 /= a0
|
||||
b2 /= a0
|
||||
a1 /= a0
|
||||
a2 /= a0
|
||||
|
||||
// Apply cascaded filters with proper warm-up for each stage
|
||||
// This ensures clean filtering without transients, especially important for MPT
|
||||
out := make([]float64, len(data))
|
||||
copy(out, data)
|
||||
|
||||
for stage := 0; stage < n; stage++ {
|
||||
// Use warm-up for first stage to avoid transients
|
||||
// Subsequent stages benefit from the already-filtered signal
|
||||
if stage == 0 {
|
||||
out = warmupFilter(out, b0, b1, b2, a1, a2)
|
||||
} else {
|
||||
// For subsequent stages, use fresh state but no warm-up needed
|
||||
// as the signal is already filtered
|
||||
state := &biquadFilterState{}
|
||||
filtered := applyBiquadDF2T(out, b0, b1, b2, a1, a2, state)
|
||||
copy(out, filtered)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user