Files
valhallir-deconvolver/main.go
Bastian Bührig 1954312833 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
2025-07-11 16:10:01 +02:00

280 lines
9.0 KiB
Go

package main
import (
"fmt"
"log"
"os"
"valhallir-deconvolver/pkg/convolve"
"valhallir-deconvolver/pkg/plot"
"valhallir-deconvolver/pkg/wav"
"github.com/urfave/cli/v2"
)
func main() {
app := &cli.App{
Name: "valhallir-deconvolver",
Usage: "Deconvolve sweep and recorded WAV files to create impulse responses",
Version: "v1.1.0",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "sweep",
Usage: "Path to the sweep WAV file (96kHz 24bit)",
Required: true,
},
&cli.StringFlag{
Name: "recorded",
Usage: "Path to the recorded WAV file (96kHz 24bit)",
Required: true,
},
&cli.StringFlag{
Name: "output",
Usage: "Path to the output IR WAV file (96kHz 24bit)",
Required: true,
},
&cli.BoolFlag{
Name: "mpt",
Usage: "Generate minimum phase transform IR in addition to regular IR",
},
&cli.IntFlag{
Name: "sample-rate",
Usage: "Output sample rate (44, 48, 88, 96 kHz)",
Value: 96000,
},
&cli.IntFlag{
Name: "bit-depth",
Usage: "Output bit depth (16, 24, 32 bit)",
Value: 24,
},
&cli.Float64Flag{
Name: "normalize",
Usage: "Normalize output to this peak value (0.0-1.0, default 0.95)",
Value: 0.95,
},
&cli.Float64Flag{
Name: "trim-threshold",
Usage: "Silence threshold for trimming (0.0-1.0, default 0.001)",
Value: 0.001,
},
&cli.Float64Flag{
Name: "length-ms",
Usage: "Optional: Output IR length in milliseconds (will trim or zero-pad as needed)",
},
&cli.Float64Flag{
Name: "fade-ms",
Usage: "Fade-out duration in milliseconds to apply at the end of the IR (default 5)",
Value: 5.0,
},
&cli.Float64Flag{
Name: "highcut",
Usage: "High-cut filter (low-pass) cutoff frequency in Hz (applied to recorded sweep, optional)",
},
&cli.Float64Flag{
Name: "lowcut",
Usage: "Low-cut filter (high-pass) cutoff frequency in Hz (applied to recorded sweep, optional)",
},
&cli.IntFlag{
Name: "cut-slope",
Usage: "Cut filter slope in dB/octave (12, 24, 36, 48, ...; default 12)",
Value: 12,
},
&cli.BoolFlag{
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
sweepData, err := wav.ReadWAVFile(c.String("sweep"))
if err != nil {
return err
}
// Read recorded WAV file
recordedData, err := wav.ReadWAVFile(c.String("recorded"))
if err != nil {
return err
}
log.Printf("Sweep: %d samples, %d channels", len(sweepData.PCMData), sweepData.Channels)
log.Printf("Recorded: %d samples, %d channels", len(recordedData.PCMData), recordedData.Channels)
// Optionally filter the recorded sweep
recordedFiltered := recordedData.PCMData
recSampleRate := recordedData.SampleRate
highcutHz := c.Float64("highcut")
lowcutHz := c.Float64("lowcut")
cutSlope := c.Int("cut-slope")
if cutSlope < 12 || cutSlope%12 != 0 {
return fmt.Errorf("cut-slope must be a positive multiple of 12 (got %d)", cutSlope)
}
if lowcutHz > 0 {
log.Printf("Applying low-cut (high-pass) filter to recorded sweep: %.2f Hz, slope: %d dB/oct", lowcutHz, cutSlope)
recordedFiltered = convolve.CascadeLowcut(recordedFiltered, recSampleRate, lowcutHz, cutSlope)
}
if highcutHz > 0 {
log.Printf("Applying high-cut (low-pass) filter to recorded sweep: %.2f Hz, slope: %d dB/oct", 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...")
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...")
ir = convolve.TrimSilence(ir, 1e-5)
log.Printf("After trimming: %d samples", len(ir))
log.Println("Normalizing...")
ir = convolve.Normalize(ir, c.Float64("normalize"))
log.Printf("Final IR: %d samples", len(ir))
// Validate output format options
sampleRate := c.Int("sample-rate")
bitDepth := c.Int("bit-depth")
// Validate sample rate
validSampleRates := []int{44100, 48000, 88200, 96000}
validSampleRate := false
for _, sr := range validSampleRates {
if sampleRate == sr {
validSampleRate = true
break
}
}
if !validSampleRate {
return fmt.Errorf("invalid sample rate: %d. Valid options: %v", sampleRate, validSampleRates)
}
// Validate bit depth
validBitDepths := []int{16, 24, 32}
validBitDepth := false
for _, bd := range validBitDepths {
if bitDepth == bd {
validBitDepth = true
break
}
}
if !validBitDepth {
return fmt.Errorf("invalid bit depth: %d. Valid options: %v", bitDepth, validBitDepths)
}
// Resample IR to target sample rate if different from input (96kHz)
targetSampleRate := sampleRate
if targetSampleRate != 96000 {
log.Printf("Resampling IR from 96kHz to %dHz...", targetSampleRate)
ir = convolve.Resample(ir, 96000, targetSampleRate)
log.Printf("Resampled IR: %d samples", len(ir))
}
// Trim or pad IR to requested length if --length-ms is set
lengthMs := c.Float64("length-ms")
if lengthMs > 0 {
targetSamples := int(float64(targetSampleRate) * lengthMs / 1000.0)
log.Printf("Trimming or padding IR to %d samples (%.2f ms)...", targetSamples, lengthMs)
ir = convolve.TrimOrPad(ir, targetSamples)
}
// Apply fade-out
fadeMs := c.Float64("fade-ms")
fadeSamples := int(float64(targetSampleRate) * fadeMs / 1000.0)
if fadeSamples > 0 {
log.Printf("Applying linear fade-out: %d samples (%.2f ms)...", fadeSamples, fadeMs)
ir = convolve.FadeOutLinear(ir, fadeSamples)
}
// Write regular IR
log.Printf("Writing IR to: %s (%dHz, %d-bit WAV)", c.String("output"), sampleRate, bitDepth)
if err := wav.WriteWAVFileWithOptions(c.String("output"), ir, sampleRate, bitDepth); err != nil {
return err
}
// Plot IR waveform if requested
if c.Bool("plot-ir") {
log.Printf("Plotting IR waveform to ir_plot.png...")
err := plot.PlotIR(ir, sampleRate, c.String("output"))
if err != nil {
return fmt.Errorf("failed to plot IR: %v", err)
}
log.Printf("IR plot saved as ir_plot.png")
}
// Generate MPT IR if requested
if c.Bool("mpt") {
log.Println("Generating minimum phase transform...")
// Use the original 96kHz IR for MPT generation
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"))
log.Printf("MPT IR: %d samples", len(mptIR))
// Resample MPT IR to target sample rate if different from input (96kHz)
if targetSampleRate != 96000 {
log.Printf("Resampling MPT IR from 96kHz to %dHz...", targetSampleRate)
mptIR = convolve.Resample(mptIR, 96000, targetSampleRate)
log.Printf("Resampled MPT IR: %d samples", len(mptIR))
}
// Trim or pad MPT IR to requested length if --length-ms is set
if lengthMs > 0 {
targetSamples := int(float64(targetSampleRate) * lengthMs / 1000.0)
log.Printf("Trimming or padding MPT IR to %d samples (%.2f ms)...", targetSamples, lengthMs)
mptIR = convolve.TrimOrPad(mptIR, targetSamples)
}
// Apply fade-out to MPT IR
if fadeSamples > 0 {
log.Printf("Applying linear fade-out to MPT IR: %d samples (%.2f ms)...", fadeSamples, fadeMs)
mptIR = convolve.FadeOutLinear(mptIR, fadeSamples)
}
// Generate MPT output filename
outputPath := c.String("output")
if len(outputPath) > 4 && outputPath[len(outputPath)-4:] == ".wav" {
outputPath = outputPath[:len(outputPath)-4]
}
mptOutputPath := outputPath + "_mpt.wav"
log.Printf("Writing MPT IR to: %s (%dHz, %d-bit WAV)", mptOutputPath, sampleRate, bitDepth)
if err := wav.WriteWAVFileWithOptions(mptOutputPath, mptIR, sampleRate, bitDepth); err != nil {
return err
}
log.Println("Minimum phase transform IR generated successfully!")
}
log.Println("Impulse response generated successfully!")
return nil
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}