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

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