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)
|
- **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)
|
- **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`
|
- **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
|
- **96kHz 24-bit WAV file support** for high-quality audio processing
|
||||||
- **Multiple output formats** with configurable sample rates and bit depths
|
- **Multiple output formats** with configurable sample rates and bit depths
|
||||||
- **Minimum Phase Transform (MPT)** option for reduced latency IRs
|
- **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).
|
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
|
### IR Visualization
|
||||||
|
|
||||||
Generate frequency response and waveform plots of your IRs:
|
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 |
|
| `--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 |
|
| `--cut-slope` | Filter slope in dB/octave (12, 24, 36, ...; default 12) | 12 | No |
|
||||||
| `--plot-ir` | Generate frequency response and waveform plot | false | 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
|
## 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
|
- **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
|
- **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
|
### Output Format Options
|
||||||
- **Sample Rates:** 44.1kHz (CD), 48kHz (studio), 88.2kHz, 96kHz (high-res)
|
- **Sample Rates:** 44.1kHz (CD), 48kHz (studio), 88.2kHz, 96kHz (high-res)
|
||||||
- **Bit Depths:** 16-bit (CD), 24-bit (studio), 32-bit (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",
|
Name: "plot-ir",
|
||||||
Usage: "Plot the generated regular IR waveform to ir_plot.png",
|
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 {
|
Action: func(c *cli.Context) error {
|
||||||
// Read sweep WAV file
|
// Read sweep WAV file
|
||||||
@@ -118,8 +126,19 @@ func main() {
|
|||||||
recordedFiltered = convolve.CascadeHighcut(recordedFiltered, recSampleRate, highcutHz, cutSlope)
|
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...")
|
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.Printf("Deconvolution result: %d samples", len(ir))
|
||||||
|
|
||||||
log.Println("Trimming silence...")
|
log.Println("Trimming silence...")
|
||||||
@@ -204,7 +223,12 @@ func main() {
|
|||||||
if c.Bool("mpt") {
|
if c.Bool("mpt") {
|
||||||
log.Println("Generating minimum phase transform...")
|
log.Println("Generating minimum phase transform...")
|
||||||
// Use the original 96kHz IR for MPT generation
|
// 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)
|
originalIR = convolve.TrimSilence(originalIR, 1e-5)
|
||||||
mptIR := convolve.MinimumPhaseTransform(originalIR)
|
mptIR := convolve.MinimumPhaseTransform(originalIR)
|
||||||
mptIR = convolve.Normalize(mptIR, c.Float64("normalize"))
|
mptIR = convolve.Normalize(mptIR, c.Float64("normalize"))
|
||||||
|
|||||||
@@ -453,3 +453,90 @@ func CascadeHighcut(data []float64, sampleRate int, cutoffHz float64, slopeDb in
|
|||||||
}
|
}
|
||||||
return out
|
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