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