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:
35
README.md
35
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)
|
||||
|
||||
28
main.go
28
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"))
|
||||
|
||||
@@ -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
BIN
valhallir-deconvolver
Executable file
Binary file not shown.
Reference in New Issue
Block a user