diff --git a/README.md b/README.md index af83cc0..5e43f72 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ A CLI tool for processing WAV files to generate impulse responses (IR) from swee - **Optional low-cut and high-cut filtering:** Apply Butterworth filters to the recorded sweep before IR extraction (--lowcut, --highcut, --cut-slope) - **Automatic fade-out:** Linear fade-out at the end of the IR to avoid clicks (default 5 ms, configurable with --fade-ms) - **IR Visualization:** Generate frequency response and waveform plots with `--plot-ir` +- **Automatic Phase Correction:** Detects and corrects phase-inverted recorded sweeps for consistent IR polarity +- **Manual Phase Inversion:** Use `--force-invert-phase` to explicitly invert the recorded sweep for testing or manual override - **96kHz 24-bit WAV file support** for high-quality audio processing - **Multiple output formats** with configurable sample rates and bit depths - **Minimum Phase Transform (MPT)** option for reduced latency IRs @@ -88,6 +90,31 @@ You can control the filter steepness (slope) with `--cut-slope` (in dB/octave, d This applies a 40 Hz low-cut and 18 kHz high-cut, both with a 24 dB/octave slope (steeper than the default 12). +### Automatic Phase Correction + +Valhallir Deconvolver automatically detects and corrects phase-inverted recorded sweeps to ensure consistent IR polarity. This is especially important when mixing multiple IRs later. + +By default, the tool: +- **Detects phase inversion** by analyzing the correlation between sweep and recorded signals +- **Automatically corrects** phase-inverted recorded sweeps +- **Ensures consistent polarity** across all generated IRs + +If you know your recorded sweep has the correct phase, you can disable automatic correction: + +```sh +./valhallir-deconvolver --sweep sweep.wav --recorded recorded.wav --output ir.wav --no-phase-correction +``` + +### Forcing Phase Inversion (Testing/Manual Override) + +You can force the recorded sweep to be inverted (regardless of automatic detection) using: + +```sh +./valhallir-deconvolver --sweep sweep.wav --recorded recorded.wav --output ir.wav --force-invert-phase +``` + +This is useful for testing or if you know your recorded sweep is out of phase and want to override the automatic detection. + ### IR Visualization Generate frequency response and waveform plots of your IRs: @@ -170,6 +197,8 @@ Generate IRs in different sample rates and bit depths: | `--highcut` | High-cut filter (low-pass) cutoff frequency in Hz (recorded sweep) | - | No | | `--cut-slope` | Filter slope in dB/octave (12, 24, 36, ...; default 12) | 12 | No | | `--plot-ir` | Generate frequency response and waveform plot | false | No | +| `--no-phase-correction` | Disable automatic phase correction | false | No | +| `--force-invert-phase` | Force inversion of the recorded sweep (manual override) | false | No | ## File Requirements @@ -225,6 +254,12 @@ Generate IRs in different sample rates and bit depths: - **Automatic File Naming:** Plots are saved with the same base name as the IR file - **High-Quality Output:** PNG format suitable for documentation and sharing +### Phase Correction +- **Automatic Detection:** Analyzes correlation between sweep and recorded signals to detect phase inversion +- **Smart Correction:** Uses the first 100ms of signals for reliable phase analysis +- **Consistent Polarity:** Ensures all IRs have the same phase polarity for easy mixing +- **Optional Disable:** Use `--no-phase-correction` if you know the phase is correct + ### Output Format Options - **Sample Rates:** 44.1kHz (CD), 48kHz (studio), 88.2kHz, 96kHz (high-res) - **Bit Depths:** 16-bit (CD), 24-bit (studio), 32-bit (high-res) diff --git a/main.go b/main.go index 5f4f521..ad613c7 100644 --- a/main.go +++ b/main.go @@ -83,6 +83,14 @@ func main() { Name: "plot-ir", Usage: "Plot the generated regular IR waveform to ir_plot.png", }, + &cli.BoolFlag{ + Name: "no-phase-correction", + Usage: "Disable automatic phase correction (use if you know the phase is correct)", + }, + &cli.BoolFlag{ + Name: "force-invert-phase", + Usage: "Force inversion of the recorded sweep (for testing/manual override)", + }, }, Action: func(c *cli.Context) error { // Read sweep WAV file @@ -118,8 +126,19 @@ func main() { recordedFiltered = convolve.CascadeHighcut(recordedFiltered, recSampleRate, highcutHz, cutSlope) } + // Force phase inversion if requested + if c.Bool("force-invert-phase") { + log.Printf("Forcing phase inversion of recorded sweep (manual override)") + recordedFiltered = convolve.InvertPhase(recordedFiltered) + } + log.Println("Performing deconvolution...") - ir := convolve.Deconvolve(sweepData.PCMData, recordedFiltered) + var ir []float64 + if c.Bool("no-phase-correction") { + ir = convolve.Deconvolve(sweepData.PCMData, recordedFiltered) + } else { + ir = convolve.DeconvolveWithPhaseCorrection(sweepData.PCMData, recordedFiltered) + } log.Printf("Deconvolution result: %d samples", len(ir)) log.Println("Trimming silence...") @@ -204,7 +223,12 @@ func main() { if c.Bool("mpt") { log.Println("Generating minimum phase transform...") // Use the original 96kHz IR for MPT generation - originalIR := convolve.Deconvolve(sweepData.PCMData, recordedData.PCMData) + var originalIR []float64 + if c.Bool("no-phase-correction") { + originalIR = convolve.Deconvolve(sweepData.PCMData, recordedData.PCMData) + } else { + originalIR = convolve.DeconvolveWithPhaseCorrection(sweepData.PCMData, recordedData.PCMData) + } originalIR = convolve.TrimSilence(originalIR, 1e-5) mptIR := convolve.MinimumPhaseTransform(originalIR) mptIR = convolve.Normalize(mptIR, c.Float64("normalize")) diff --git a/pkg/convolve/convolve.go b/pkg/convolve/convolve.go index 4257580..29922b1 100644 --- a/pkg/convolve/convolve.go +++ b/pkg/convolve/convolve.go @@ -453,3 +453,90 @@ func CascadeHighcut(data []float64, sampleRate int, cutoffHz float64, slopeDb in } return out } + +// min returns the minimum of three integers +func min(a, b, c int) int { + if a <= b && a <= c { + return a + } + if b <= a && b <= c { + return b + } + return c +} + +// DetectPhaseInversion detects if the recorded sweep is phase-inverted compared to the sweep +// by computing the normalized cross-correlation over a range of lags +func DetectPhaseInversion(sweep, recorded []float64) bool { + if len(sweep) == 0 || len(recorded) == 0 { + return false + } + + windowSize := min(len(sweep), len(recorded), 9600) // 100ms at 96kHz + sweepWindow := sweep[:windowSize] + recordedWindow := recorded[:windowSize] + + maxLag := 500 // +/- 500 samples (~5ms) + bestCorr := 0.0 + bestLag := 0 + + for lag := -maxLag; lag <= maxLag; lag++ { + var corr, sweepSum, recordedSum, sweepSumSq, recordedSumSq float64 + count := 0 + for i := 0; i < windowSize; i++ { + j := i + lag + if j < 0 || j >= windowSize { + continue + } + corr += sweepWindow[i] * recordedWindow[j] + sweepSum += sweepWindow[i] + recordedSum += recordedWindow[j] + sweepSumSq += sweepWindow[i] * sweepWindow[i] + recordedSumSq += recordedWindow[j] * recordedWindow[j] + count++ + } + if count == 0 { + continue + } + sweepMean := sweepSum / float64(count) + recordedMean := recordedSum / float64(count) + sweepVar := sweepSumSq/float64(count) - sweepMean*sweepMean + recordedVar := recordedSumSq/float64(count) - recordedMean*recordedMean + if sweepVar <= 0 || recordedVar <= 0 { + continue + } + corrCoeff := (corr/float64(count) - sweepMean*recordedMean) / math.Sqrt(sweepVar*recordedVar) + if math.Abs(corrCoeff) > math.Abs(bestCorr) { + bestCorr = corrCoeff + bestLag = lag + } + } + + log.Printf("[deconvolve] Phase cross-correlation: best lag = %d, coeff = %.4f", bestLag, bestCorr) + return bestCorr < 0.0 +} + +// InvertPhase inverts the phase of the audio data by negating all samples +func InvertPhase(data []float64) []float64 { + inverted := make([]float64, len(data)) + for i, sample := range data { + inverted[i] = -sample + } + return inverted +} + +// DeconvolveWithPhaseCorrection extracts the impulse response with automatic phase correction +func DeconvolveWithPhaseCorrection(sweep, recorded []float64) []float64 { + // Detect if recorded sweep is phase-inverted + isInverted := DetectPhaseInversion(sweep, recorded) + + if isInverted { + log.Printf("[deconvolve] Detected phase inversion in recorded sweep, correcting...") + recorded = InvertPhase(recorded) + } else { + log.Printf("[deconvolve] Phase alignment verified") + } + + // Perform normal deconvolution + return Deconvolve(sweep, recorded) +} diff --git a/valhallir-deconvolver b/valhallir-deconvolver new file mode 100755 index 0000000..7c110e7 Binary files /dev/null and b/valhallir-deconvolver differ