diff --git a/README.md b/README.md index fd2c73e..7fb7c4f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ A CLI tool for processing WAV files to generate impulse responses (IR) from swee - **Fast FFT-based deconvolution** for accurate IR extraction - **Automatic input conversion:** Accepts any WAV sample rate, bit depth, or channel count - **Optional output IR length:** Specify output IR length in milliseconds with --length-ms +- **Optional low-cut and high-cut filtering:** Apply 2nd-order Butterworth filters to the recorded sweep before IR extraction (--lowcut, --highcut) - **Automatic fade-out:** Linear fade-out at the end of the IR to avoid clicks (default 5 ms, configurable with --fade-ms) - **96kHz 24-bit WAV file support** for high-quality audio processing - **Multiple output formats** with configurable sample rates and bit depths @@ -68,6 +69,16 @@ By default, a 5 ms linear fade-out is applied to the end of the IR to avoid clic This applies a 10 ms fade-out at the end of the IR. +### Filtering the Recorded Sweep + +You can apply a low-cut (high-pass) and/or high-cut (low-pass) filter to the recorded sweep before IR extraction. This is useful for removing rumble, DC, or high-frequency noise: + +```sh +./valhallir-deconvolver --sweep sweep.wav --recorded recorded.wav --output ir.wav --lowcut 40 --highcut 18000 +``` + +This applies a 40 Hz low-cut (high-pass) and 18 kHz high-cut (low-pass) filter to the recorded sweep. + ### Different Output Formats Generate IRs in different sample rates and bit depths: @@ -128,6 +139,8 @@ Generate IRs in different sample rates and bit depths: | `--trim-threshold` | Silence threshold for trimming (0.0-1.0) | 0.001 | No | | `--length-ms` | Output IR length in milliseconds (trim or zero-pad) | - | No | | `--fade-ms` | Fade-out duration in milliseconds at end of IR (default 5) | 5 | No | +| `--lowcut` | Low-cut filter (high-pass) cutoff frequency in Hz (recorded sweep) | - | No | +| `--highcut` | High-cut filter (low-pass) cutoff frequency in Hz (recorded sweep) | - | No | ## File Requirements @@ -159,6 +172,10 @@ Generate IRs in different sample rates and bit depths: - By default, a 5 ms linear fade-out is applied to the end of the IR (and MPT IR) to avoid clicks - You can change the fade duration with `--fade-ms` +### Filtering +- You can apply a 2nd-order Butterworth low-cut (high-pass) and/or high-cut (low-pass) filter to the recorded sweep before IR extraction +- Use `--lowcut` and/or `--highcut` to specify cutoff frequencies in Hz + ### Deconvolution Process 1. **FFT-based deconvolution** of recorded signal by sweep signal 2. **Regularization** to prevent division by zero diff --git a/main.go b/main.go index b38a332..d09e773 100644 --- a/main.go +++ b/main.go @@ -65,6 +65,14 @@ func main() { Usage: "Fade-out duration in milliseconds to apply at the end of the IR (default 5)", Value: 5.0, }, + &cli.Float64Flag{ + Name: "highcut", + Usage: "High-cut filter (low-pass) cutoff frequency in Hz (applied to recorded sweep, optional)", + }, + &cli.Float64Flag{ + Name: "lowcut", + Usage: "Low-cut filter (high-pass) cutoff frequency in Hz (applied to recorded sweep, optional)", + }, }, Action: func(c *cli.Context) error { // Read sweep WAV file @@ -82,8 +90,22 @@ func main() { log.Printf("Sweep: %d samples, %d channels", len(sweepData.PCMData), sweepData.Channels) log.Printf("Recorded: %d samples, %d channels", len(recordedData.PCMData), recordedData.Channels) + // Optionally filter the recorded sweep + recordedFiltered := recordedData.PCMData + recSampleRate := recordedData.SampleRate + highcutHz := c.Float64("highcut") + lowcutHz := c.Float64("lowcut") + if lowcutHz > 0 { + log.Printf("Applying low-cut (high-pass) filter to recorded sweep: %.2f Hz", lowcutHz) + recordedFiltered = convolve.ApplyHighpassButterworth(recordedFiltered, recSampleRate, lowcutHz) + } + if highcutHz > 0 { + log.Printf("Applying high-cut (low-pass) filter to recorded sweep: %.2f Hz", highcutHz) + recordedFiltered = convolve.ApplyLowpassButterworth(recordedFiltered, recSampleRate, highcutHz) + } + log.Println("Performing deconvolution...") - ir := convolve.Deconvolve(sweepData.PCMData, recordedData.PCMData) + ir := convolve.Deconvolve(sweepData.PCMData, recordedFiltered) log.Printf("Deconvolution result: %d samples", len(ir)) log.Println("Trimming silence...") diff --git a/pkg/convolve/convolve.go b/pkg/convolve/convolve.go index 50625db..aa17c71 100644 --- a/pkg/convolve/convolve.go +++ b/pkg/convolve/convolve.go @@ -341,3 +341,87 @@ func FadeOutLinear(data []float64, fadeSamples int) []float64 { } return out } + +// ApplyLowpassButterworth applies a 2nd-order Butterworth low-pass filter to the data. +// cutoffHz: cutoff frequency in Hz, sampleRate: sample rate in Hz. +func ApplyLowpassButterworth(data []float64, sampleRate int, cutoffHz float64) []float64 { + if cutoffHz <= 0 || cutoffHz >= float64(sampleRate)/2 { + return data + } + // Biquad coefficients + w0 := 2 * math.Pi * cutoffHz / float64(sampleRate) + cosw0 := math.Cos(w0) + sinw0 := math.Sin(w0) + Q := 1.0 / math.Sqrt(2) // Butterworth Q + alpha := sinw0 / (2 * Q) + + b0 := (1 - cosw0) / 2 + b1 := 1 - cosw0 + b2 := (1 - cosw0) / 2 + a0 := 1 + alpha + a1 := -2 * cosw0 + a2 := 1 - alpha + + // Normalize + 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 +} + +// ApplyHighpassButterworth applies a 2nd-order Butterworth high-pass filter to the data. +// cutoffHz: cutoff frequency in Hz, sampleRate: sample rate in Hz. +func ApplyHighpassButterworth(data []float64, sampleRate int, cutoffHz float64) []float64 { + if cutoffHz <= 0 || cutoffHz >= float64(sampleRate)/2 { + return data + } + // Biquad coefficients + w0 := 2 * math.Pi * cutoffHz / float64(sampleRate) + cosw0 := math.Cos(w0) + sinw0 := math.Sin(w0) + Q := 1.0 / math.Sqrt(2) // Butterworth Q + alpha := sinw0 / (2 * Q) + + b0 := (1 + cosw0) / 2 + b1 := -(1 + cosw0) + b2 := (1 + cosw0) / 2 + a0 := 1 + alpha + a1 := -2 * cosw0 + a2 := 1 - alpha + + // Normalize + 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 +} diff --git a/testdata/ir.wav b/testdata/ir.wav index 32bfda3..af46e9e 100644 Binary files a/testdata/ir.wav and b/testdata/ir.wav differ diff --git a/testdata/ir_mpt.wav b/testdata/ir_mpt.wav new file mode 100644 index 0000000..80cc469 Binary files /dev/null and b/testdata/ir_mpt.wav differ