- 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
280 lines
9.0 KiB
Go
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)
|
|
}
|
|
}
|