Compare commits
2 Commits
279038c566
...
73cff7ea08
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73cff7ea08 | ||
|
|
1954312833 |
173
README.md
173
README.md
@@ -10,10 +10,14 @@ A CLI tool for processing WAV files to generate impulse responses (IR) from swee
|
||||
- **Optional low-cut and high-cut filtering:** Apply Butterworth filters to the recorded sweep before IR extraction (--lowcut, --highcut, --cut-slope)
|
||||
- **Automatic fade-out:** Linear fade-out at the end of the IR to avoid clicks (default 5 ms, configurable with --fade-ms)
|
||||
- **IR Visualization:** Generate frequency response and waveform plots with `--plot-ir`
|
||||
- **Automatic Phase Correction:** Detects and corrects phase-inverted recorded sweeps for consistent IR polarity
|
||||
- **Manual Phase Inversion:** Use `--force-invert-phase` to explicitly invert the recorded sweep for testing or manual override
|
||||
- **96kHz 24-bit WAV file support** for high-quality audio processing
|
||||
- **Multiple output formats** with configurable sample rates and bit depths
|
||||
- **Minimum Phase Transform (MPT)** option for reduced latency IRs
|
||||
- **Automatic silence trimming** and normalization
|
||||
- **Batch processing:** Process entire directories of recorded files automatically
|
||||
- **Cabpack generation:** Create complete cabpack structures with IRs in multiple formats organized in a directory tree
|
||||
- **Modular design** with separate packages for WAV I/O, convolution, and visualization
|
||||
- **Robust error handling** and validation
|
||||
|
||||
@@ -30,6 +34,24 @@ go build -o valhallir-deconvolver
|
||||
|
||||
## Usage
|
||||
|
||||
### Default Mode (No Options)
|
||||
|
||||
If you run the tool without any command-line options, it automatically creates a cabpack:
|
||||
|
||||
```sh
|
||||
./valhallir-deconvolver
|
||||
```
|
||||
|
||||
This will:
|
||||
- Use the current directory as the recorded directory
|
||||
- Look for `sweep.wav` in the current directory as the sweep file
|
||||
- Create output in an `IRs` folder one directory level up from the current directory
|
||||
- Automatically enable cabpack mode
|
||||
- Process all WAV files in the current directory (excluding the sweep file)
|
||||
- Use `selection.txt` and `ambient.txt` from the current directory if present
|
||||
|
||||
**Perfect for:** Simply placing the tool in a folder with recorded WAV files and running it to create a complete cabpack in the parent directory's `IRs` folder.
|
||||
|
||||
### Basic IR Generation
|
||||
|
||||
Generate a standard impulse response from sweep and recorded files (any WAV format):
|
||||
@@ -38,6 +60,105 @@ Generate a standard impulse response from sweep and recorded files (any WAV form
|
||||
./valhallir-deconvolver --sweep sweep.wav --recorded recorded.wav --output ir.wav
|
||||
```
|
||||
|
||||
### Batch Processing (Directory Mode)
|
||||
|
||||
Process all WAV files in a directory of recorded files. The tool will automatically detect if `--recorded` is a directory and process all WAV files found in it (including subdirectories):
|
||||
|
||||
```sh
|
||||
./valhallir-deconvolver --sweep sweep.wav --recorded ./recordings/ --output ./ir_output/
|
||||
```
|
||||
|
||||
This will:
|
||||
- Find all `.wav` files in the `./recordings/` directory (recursively)
|
||||
- Process each recorded file with the same sweep file
|
||||
- Generate IR files in the `./ir_output/` directory
|
||||
- Use the original recorded filename for each output IR (e.g., `recorded1.wav` → `recorded1.wav`)
|
||||
|
||||
**Note:** If `--output` is a directory, it will be created automatically if it doesn't exist. If `--output` is a file path, single-file mode is used.
|
||||
|
||||
**Example with options:**
|
||||
```sh
|
||||
./valhallir-deconvolver \
|
||||
--sweep sweep.wav \
|
||||
--recorded ./recordings/ \
|
||||
--output ./ir_output/ \
|
||||
--mpt \
|
||||
--length-ms 50 \
|
||||
--sample-rate 48000 \
|
||||
--bit-depth 24
|
||||
```
|
||||
|
||||
This processes all WAV files in `./recordings/` and generates both regular and MPT IRs in `./ir_output/`.
|
||||
|
||||
### Cabpack Generation
|
||||
|
||||
Generate a complete cabpack with IRs in multiple formats organized in a structured directory tree:
|
||||
|
||||
**Simple mode (default - no options needed):**
|
||||
```sh
|
||||
./valhallir-deconvolver
|
||||
```
|
||||
This uses:
|
||||
- Current directory as recorded directory
|
||||
- `sweep.wav` in current directory as sweep file
|
||||
- `IRs` folder one directory level up from current directory as output
|
||||
- Automatically enables cabpack mode
|
||||
- Excludes the sweep file from processing
|
||||
|
||||
**Explicit mode:**
|
||||
```sh
|
||||
./valhallir-deconvolver \
|
||||
--sweep sweep.wav \
|
||||
--recorded ./recordings/ \
|
||||
--output ./cabpack_output/ \
|
||||
--cabpack
|
||||
```
|
||||
|
||||
Both modes create a cabpack structure with:
|
||||
- **Format folders** named like `V2-1960STV 96000Hz-24bit 500ms` (extracted from WAV filenames)
|
||||
- **9 different formats** covering various sample rates, bit depths, and lengths:
|
||||
- 44100Hz, 16bit, 170ms
|
||||
- 44100Hz, 24bit, 170ms
|
||||
- 44100Hz, 24bit, 500ms
|
||||
- 48000Hz, 16bit, 170ms
|
||||
- 48000Hz, 24bit, 170ms
|
||||
- 48000Hz, 24bit, 500ms
|
||||
- 48000Hz, 24bit, 1370ms
|
||||
- 96000Hz, 24bit, 500ms
|
||||
- 96000Hz, 24bit, 1370ms
|
||||
- **Subfolders** in each format: `ambient mics`, `close mics`, `mixees`, `selection`
|
||||
- **RAW and MPT IRs** in `close mics/RAW` and `close mics/MPT` respectively
|
||||
- **IR visualizations** in the `plots` folder (96000Hz format)
|
||||
- **Automatic file organization** using `selection.txt` and `ambient.txt` files
|
||||
|
||||
The cabpack base name is automatically extracted from WAV filenames. For example, `V2-1960STV-d-SM7B-A1.wav` will create a cabpack named `V2-1960STV`.
|
||||
|
||||
#### Automatic File Organization
|
||||
|
||||
You can automatically populate the `selection` and `ambient mics` folders by placing text files in the recorded directory:
|
||||
|
||||
- **`selection.txt`**: Lists filenames (one per line) to **copy** from `close mics` to `selection` folders. Both RAW and MPT versions are copied.
|
||||
- **`ambient.txt`**: Lists filenames (one per line) to **move** from `close mics` to `ambient mics` folders. Both RAW and MPT versions are moved.
|
||||
|
||||
**Example `selection.txt`:**
|
||||
```
|
||||
V2-1960STV-d-SM7B-A1.wav
|
||||
V2-1960STV-d-SM7B-A2.wav
|
||||
V2-1960STV-d-SM7B-A3.wav
|
||||
```
|
||||
|
||||
**Example `ambient.txt`:**
|
||||
```
|
||||
V2-1960STV-d-SM7B-B1.wav
|
||||
V2-1960STV-d-SM7B-B2.wav
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- If `selection.txt` or `ambient.txt` are missing, those operations are skipped (no error).
|
||||
- Files are processed for all format folders automatically.
|
||||
- The `.wav` extension is automatically added if missing in the list files.
|
||||
- The `mixees` folder is left empty for manual filling.
|
||||
|
||||
### With Minimum Phase Transform
|
||||
|
||||
Generate both regular and minimum phase IRs:
|
||||
@@ -88,6 +209,31 @@ You can control the filter steepness (slope) with `--cut-slope` (in dB/octave, d
|
||||
|
||||
This applies a 40 Hz low-cut and 18 kHz high-cut, both with a 24 dB/octave slope (steeper than the default 12).
|
||||
|
||||
### Automatic Phase Correction
|
||||
|
||||
Valhallir Deconvolver automatically detects and corrects phase-inverted recorded sweeps to ensure consistent IR polarity. This is especially important when mixing multiple IRs later.
|
||||
|
||||
By default, the tool:
|
||||
- **Detects phase inversion** by analyzing the correlation between sweep and recorded signals
|
||||
- **Automatically corrects** phase-inverted recorded sweeps
|
||||
- **Ensures consistent polarity** across all generated IRs
|
||||
|
||||
If you know your recorded sweep has the correct phase, you can disable automatic correction:
|
||||
|
||||
```sh
|
||||
./valhallir-deconvolver --sweep sweep.wav --recorded recorded.wav --output ir.wav --no-phase-correction
|
||||
```
|
||||
|
||||
### Forcing Phase Inversion (Testing/Manual Override)
|
||||
|
||||
You can force the recorded sweep to be inverted (regardless of automatic detection) using:
|
||||
|
||||
```sh
|
||||
./valhallir-deconvolver --sweep sweep.wav --recorded recorded.wav --output ir.wav --force-invert-phase
|
||||
```
|
||||
|
||||
This is useful for testing or if you know your recorded sweep is out of phase and want to override the automatic detection.
|
||||
|
||||
### IR Visualization
|
||||
|
||||
Generate frequency response and waveform plots of your IRs:
|
||||
@@ -156,9 +302,9 @@ Generate IRs in different sample rates and bit depths:
|
||||
|
||||
| Flag | Description | Default | Required |
|
||||
|------|-------------|---------|----------|
|
||||
| `--sweep` | Path to sweep WAV file (any format) | - | Yes |
|
||||
| `--recorded` | Path to recorded WAV file (any format) | - | Yes |
|
||||
| `--output` | Path to output IR WAV file | - | Yes |
|
||||
| `--sweep` | Path to sweep WAV file (any format) | `sweep.wav` in current directory | No |
|
||||
| `--recorded` | Path to recorded WAV file or directory containing WAV files | Current directory | No |
|
||||
| `--output` | Path to output IR WAV file or directory for batch processing | `IRs` folder one directory level up from recorded directory | No |
|
||||
| `--mpt` | Generate minimum phase transform IR | false | No |
|
||||
| `--sample-rate` | Output sample rate (44, 48, 88, 96 kHz) | 96000 | No |
|
||||
| `--bit-depth` | Output bit depth (16, 24, 32 bit) | 24 | No |
|
||||
@@ -170,6 +316,9 @@ Generate IRs in different sample rates and bit depths:
|
||||
| `--highcut` | High-cut filter (low-pass) cutoff frequency in Hz (recorded sweep) | - | No |
|
||||
| `--cut-slope` | Filter slope in dB/octave (12, 24, 36, ...; default 12) | 12 | No |
|
||||
| `--plot-ir` | Generate frequency response and waveform plot | false | No |
|
||||
| `--no-phase-correction` | Disable automatic phase correction | false | No |
|
||||
| `--force-invert-phase` | Force inversion of the recorded sweep (manual override) | false | No |
|
||||
| `--cabpack` | Generate a cabpack with IRs in multiple formats organized in a directory tree | false | No |
|
||||
|
||||
## File Requirements
|
||||
|
||||
@@ -225,6 +374,12 @@ Generate IRs in different sample rates and bit depths:
|
||||
- **Automatic File Naming:** Plots are saved with the same base name as the IR file
|
||||
- **High-Quality Output:** PNG format suitable for documentation and sharing
|
||||
|
||||
### Phase Correction
|
||||
- **Automatic Detection:** Analyzes correlation between sweep and recorded signals to detect phase inversion
|
||||
- **Smart Correction:** Uses the first 100ms of signals for reliable phase analysis
|
||||
- **Consistent Polarity:** Ensures all IRs have the same phase polarity for easy mixing
|
||||
- **Optional Disable:** Use `--no-phase-correction` if you know the phase is correct
|
||||
|
||||
### Output Format Options
|
||||
- **Sample Rates:** 44.1kHz (CD), 48kHz (studio), 88.2kHz, 96kHz (high-res)
|
||||
- **Bit Depths:** 16-bit (CD), 24-bit (studio), 32-bit (high-res)
|
||||
@@ -276,6 +431,18 @@ Generate IRs in different sample rates and bit depths:
|
||||
--mpt
|
||||
```
|
||||
|
||||
### Cabpack Generation
|
||||
```sh
|
||||
# Generate a complete cabpack with all formats
|
||||
./valhallir-deconvolver \
|
||||
--sweep sweep.wav \
|
||||
--recorded ./recordings/ \
|
||||
--output ./cabpack_output/ \
|
||||
--cabpack
|
||||
```
|
||||
|
||||
This will process all WAV files in `./recordings/` and create a complete cabpack structure with IRs in 9 different formats, organized in folders with RAW and MPT versions, plus visualization plots.
|
||||
|
||||
## CI/CD & Multi-Platform Builds
|
||||
|
||||
This project includes a Dagger pipeline for building binaries for multiple platforms:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -62,8 +62,9 @@ func PlotIR(ir []float64, sampleRate int, irFileName string) error {
|
||||
}
|
||||
}
|
||||
fmt.Printf("[PlotIR] minDb in plotted range: %.2f dB at %.2f Hz\n", minDb, minDbFreq)
|
||||
irBaseName := filepath.Base(irFileName)
|
||||
p := plot.New()
|
||||
p.Title.Text = "IR Frequency Response (dB, 2048-sample window)"
|
||||
p.Title.Text = fmt.Sprintf("IR Frequency Response: %s", irBaseName)
|
||||
p.X.Label.Text = "Frequency (Hz)"
|
||||
p.Y.Label.Text = "Magnitude (dB)"
|
||||
p.X.Scale = plot.LogScale{}
|
||||
@@ -111,7 +112,7 @@ func PlotIR(ir []float64, sampleRate int, irFileName string) error {
|
||||
|
||||
// --- Time-aligned waveform plot ---
|
||||
p2 := plot.New()
|
||||
p2.Title.Text = "IR Waveform (Time Aligned)"
|
||||
p2.Title.Text = fmt.Sprintf("IR Waveform: %s", irBaseName)
|
||||
p2.X.Label.Text = "Time (ms)"
|
||||
p2.Y.Label.Text = "Amplitude"
|
||||
// Prepare waveform data (only first 10ms)
|
||||
|
||||
@@ -36,6 +36,15 @@ func toMono(data []float64, channels int) []float64 {
|
||||
|
||||
// ReadWAVFile reads a WAV file and returns its PCM data as float64 (resampled to 96kHz mono)
|
||||
func ReadWAVFile(filePath string) (*WAVData, error) {
|
||||
// Check if path is a directory
|
||||
info, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to access file %s: %w", filePath, err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil, fmt.Errorf("path %s is a directory, not a WAV file", filePath)
|
||||
}
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file %s: %w", filePath, err)
|
||||
|
||||
BIN
valhallir-deconvolver
Executable file
BIN
valhallir-deconvolver
Executable file
Binary file not shown.
Reference in New Issue
Block a user