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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user