Add automatic phase correction with cross-correlation detection and manual override

- Implement cross-correlation-based phase inversion detection
- Add --force-invert-phase flag for manual override and testing
- Add --no-phase-correction flag to disable automatic detection
- Update README with comprehensive documentation
- Improve phase detection sensitivity and add detailed logging
- Ensure consistent IR polarity for easier mixing of multiple IRs
This commit is contained in:
Bastian Bührig
2025-07-11 16:10:01 +02:00
parent 279038c566
commit 1954312833
4 changed files with 148 additions and 2 deletions

View File

@@ -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)

28
main.go
View File

@@ -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"))

View File

@@ -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)
}

BIN
valhallir-deconvolver Executable file

Binary file not shown.