Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
074643577d | ||
|
|
73cff7ea08 | ||
|
|
1954312833 | ||
|
|
279038c566 | ||
| 80ab0f0922 | |||
| bb9a93a4f0 | |||
| a113550aee | |||
|
|
8cacf243d8 | ||
|
|
3ed43bda8b |
26
.gitignore
vendored
26
.gitignore
vendored
@@ -38,4 +38,28 @@ Thumbs.db
|
||||
ehthumbs.db
|
||||
|
||||
# Dagger cache
|
||||
.dagger/
|
||||
.dagger/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
|
||||
# Test files - ignore all except recorded.wav and sweep.wav
|
||||
testfiles/*
|
||||
!testfiles/recorded.wav
|
||||
!testfiles/sweep.wav
|
||||
|
||||
# Generated IR files and plots
|
||||
*.wav
|
||||
*.flac
|
||||
*.png
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.tiff
|
||||
*.tif
|
||||
*.pdf
|
||||
*.svg
|
||||
*.eps
|
||||
|
||||
# But allow the specific test files
|
||||
!testfiles/recorded.wav
|
||||
!testfiles/sweep.wav
|
||||
72
CHANGELOG.md
72
CHANGELOG.md
@@ -1,5 +1,77 @@
|
||||
# Changelog
|
||||
|
||||
## [v1.2.1] - 2024-12-20
|
||||
### Added
|
||||
- **High-Quality Filter Implementation**: Direct Form II Transposed filter structure for improved numerical stability
|
||||
- **Filter Warm-Up**: Proper filter initialization to avoid transients, especially important for MPT transforms
|
||||
- **Anti-Aliasing in Resampling**: Automatic anti-aliasing filter when downsampling to prevent aliasing artifacts
|
||||
|
||||
### Fixed
|
||||
- **MPT Filter Application**: MPT IRs now correctly use the filtered recorded sweep (same as RAW IRs)
|
||||
- **Filter Response at Lower Sample Rates**: Fixed filter response accuracy when resampling to 48kHz and other lower sample rates
|
||||
- **Resampling Artifacts**: Added proper anti-aliasing to prevent frequency aliasing when downsampling
|
||||
|
||||
### Changed
|
||||
- **Filter Quality**: Improved filter implementation with high-precision coefficient calculation and proper state management
|
||||
- **Filter Artifacts**: Reduced artifacts in steep filters (48+ dB/octave) through better numerical stability
|
||||
|
||||
### Technical Improvements
|
||||
- **Direct Form II Transposed**: More numerically stable filter structure for cascaded filters
|
||||
- **Filter Warm-Up**: Pre-initialization of filter state to eliminate transients
|
||||
- **Anti-Aliasing**: Steep low-pass filter (48 dB/octave) applied before downsampling to prevent aliasing
|
||||
|
||||
## [v1.2.0] - 2024-12-20
|
||||
### Added
|
||||
- **Cabpack Generation**: Complete cabpack creation with IRs in 9 different formats organized in structured directory trees
|
||||
- **Default Mode**: Run without command-line options for automatic cabpack generation from current directory
|
||||
- **Executable Directory Detection**: Automatic detection of binary location for double-click support in Finder/Explorer
|
||||
- **Automatic File Organization**: Support for `selection.txt` and `ambient.txt` files to automatically organize IRs
|
||||
- **Mixes Folder Support**: Automatic conversion of ready-to-use IRs from `mixes` folder to all cabpack formats
|
||||
- **IR Filename in Plots**: IR filenames now displayed in plot titles for better identification
|
||||
- **Cabpack Defaults**: Automatic defaults for cabpack mode (fade-ms: 10, lowcut: 40Hz)
|
||||
- **Plots for Mixes**: Automatic plot generation for mix IRs in `plots/mixes` folder
|
||||
- **Logo in Plots**: Valhallir logo automatically displayed in upper-left corner of all generated plots (embedded in binary)
|
||||
|
||||
### Changed
|
||||
- **Optional Command-Line Flags**: All main flags (`--sweep`, `--recorded`, `--output`) are now optional with intelligent defaults
|
||||
- **Default Sweep File**: Changed default sweep filename to `sweep.wav` (simpler naming)
|
||||
- **Output Directory**: IRs folder created one directory level up from recorded directory by default
|
||||
- **Directory Structure**: Added `mixes` subfolder to each format folder (only created if `mixes` folder exists)
|
||||
- **Conditional Folder Creation**: `selection`, `ambient mics`, and `mixes` folders are only created if the corresponding files/folders exist in the recorded directory
|
||||
|
||||
### Technical Improvements
|
||||
- **Smart Path Detection**: Uses executable directory when double-clicked, current directory when run from command line
|
||||
- **Enhanced Logging**: Better progress information and path logging for debugging
|
||||
- **Improved Error Handling**: Graceful handling of missing optional files (selection.txt, ambient.txt, mixes folder)
|
||||
|
||||
## [v1.1.0] - 2024-12-19
|
||||
### Added
|
||||
- **IR Visualization**: New `--plot-ir` flag to generate frequency response and waveform plots
|
||||
- **Professional Plotting**: Frequency response (dB vs Hz) and time-aligned waveform visualization
|
||||
- **Valhallir Branding**: Logo and filename information in generated plots
|
||||
- **Modular Architecture**: Separated plotting logic into dedicated `pkg/plot` package
|
||||
- **Enhanced File Management**: Plots are saved in the same directory as IR files with matching names
|
||||
- **Improved Documentation**: Updated README with plotting features and usage examples
|
||||
- **Linear Fade-Out**: New `--fade-ms` option to apply linear fade-out to IRs (default 5ms)
|
||||
- **High/Low-Cut Filtering**: New `--highcut` and `--lowcut` options for frequency filtering
|
||||
- **Configurable Filter Slopes**: New `--cut-slope` option for filter steepness (12, 24, 36, 48 dB/oct)
|
||||
- **Audio Resampling**: Automatic resampling between different sample rates (44.1, 48, 88.2, 96 kHz)
|
||||
- **Enhanced Audio Processing**: Cascade filtering for steeper slopes and better frequency response
|
||||
- **Assets Integration**: Added Valhallir logo for professional plot branding
|
||||
|
||||
### Changed
|
||||
- **Package Structure**: Refactored codebase with separate packages for audio processing and visualization
|
||||
- **Plot File Naming**: Plots now use the same base name as IR files (e.g., `ir.png` instead of `ir_plot.png`)
|
||||
- **Repository Organization**: Updated `.gitignore` to exclude generated files while preserving essential test files
|
||||
- **Enhanced Error Handling**: Improved validation and error messages for all new features
|
||||
- **Better Logging**: More detailed progress information during processing
|
||||
|
||||
### Technical Improvements
|
||||
- **Clean Architecture**: Separated concerns between audio processing (`pkg/convolve`) and visualization (`pkg/plot`)
|
||||
- **Better Maintainability**: Modular design makes it easier to extend and modify features
|
||||
- **Enhanced Audio Quality**: Improved filtering algorithms with cascade implementation
|
||||
- **Professional Output**: Better IR quality with fade-out and advanced filtering options
|
||||
|
||||
## [v1.0.0] - 2024-06-09
|
||||
### Added
|
||||
- Initial public release: Valhallir Deconvolver
|
||||
|
||||
299
README.md
299
README.md
@@ -7,11 +7,18 @@ A CLI tool for processing WAV files to generate impulse responses (IR) from swee
|
||||
- **Fast FFT-based deconvolution** for accurate IR extraction
|
||||
- **Automatic input conversion:** Accepts any WAV sample rate, bit depth, or channel count
|
||||
- **Optional output IR length:** Specify output IR length in milliseconds with --length-ms
|
||||
- **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
|
||||
- **Modular design** with separate packages for WAV I/O and convolution
|
||||
- **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
|
||||
|
||||
## Installation
|
||||
@@ -27,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 a `cabpack` 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 `cabpack` folder.
|
||||
|
||||
### Basic IR Generation
|
||||
|
||||
Generate a standard impulse response from sweep and recorded files (any WAV format):
|
||||
@@ -35,6 +60,131 @@ 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
|
||||
- `cabpack` 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: `close mics` (always), plus `ambient mics`, `selection`, and `mixes` (only if corresponding files/folders exist)
|
||||
- **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
|
||||
- **Mixes folder support**: Convert ready-to-use IRs from `mixes` folder to all formats
|
||||
|
||||
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` is missing, the `selection` folder is not created in any format folder.
|
||||
- If `ambient.txt` is missing, the `ambient mics` folder is not created in any format folder.
|
||||
- If the `mixes` folder is missing, the `mixes` subfolder is not created in any format folder.
|
||||
- Files are processed for all format folders automatically.
|
||||
- The `.wav` extension is automatically added if missing in the list files.
|
||||
|
||||
#### Mixes Folder Support
|
||||
|
||||
If a `mixes` folder exists in the recorded directory, it will be processed automatically:
|
||||
|
||||
- **Ready-to-use IRs**: Place pre-processed IR files (already deconvolved) in the `mixes` folder
|
||||
- **Automatic conversion**: Each IR file is converted to all 9 cabpack formats
|
||||
- **Format storage**: Converted IRs are stored in the `mixes` subfolder of each format folder (only created if `mixes` folder exists)
|
||||
- **Plot generation**: IR visualizations are generated in the `plots/mixes` folder (only created if `mixes` folder exists)
|
||||
|
||||
**Note:** If the `mixes` folder is missing in the recorded directory, the `mixes` subfolder is not created in any format folder, and no mix processing occurs.
|
||||
|
||||
**Example structure:**
|
||||
```
|
||||
recorded/
|
||||
├── sweep.wav
|
||||
├── recorded1.wav
|
||||
├── recorded2.wav
|
||||
└── mixes/
|
||||
├── mix1.wav
|
||||
└── mix2.wav
|
||||
```
|
||||
|
||||
This will create converted versions of `mix1.wav` and `mix2.wav` in all format folders under the `mixes` subfolder, plus plots in `plots/mixes/`.
|
||||
|
||||
### With Minimum Phase Transform
|
||||
|
||||
Generate both regular and minimum phase IRs:
|
||||
@@ -57,6 +207,81 @@ Trim or zero-pad the output IR to a specific length (in milliseconds):
|
||||
|
||||
This will ensure the output IR is exactly 100 ms long (trimming or zero-padding as needed).
|
||||
|
||||
### Fade-Out to Avoid Clicks
|
||||
|
||||
By default, a 5 ms linear fade-out is applied to the end of the IR to avoid clicks. You can change the fade duration:
|
||||
|
||||
```sh
|
||||
./valhallir-deconvolver --sweep sweep.wav --recorded recorded.wav --output ir.wav --fade-ms 10
|
||||
```
|
||||
|
||||
This applies a 10 ms fade-out at the end of the IR.
|
||||
|
||||
### Filtering the Recorded Sweep
|
||||
|
||||
You can apply a low-cut (high-pass) and/or high-cut (low-pass) filter to the recorded sweep before IR extraction. This is useful for removing rumble, DC, or high-frequency noise:
|
||||
|
||||
```sh
|
||||
./valhallir-deconvolver --sweep sweep.wav --recorded recorded.wav --output ir.wav --lowcut 40 --highcut 18000
|
||||
```
|
||||
|
||||
This applies a 40 Hz low-cut (high-pass) and 18 kHz high-cut (low-pass) filter to the recorded sweep.
|
||||
|
||||
You can control the filter steepness (slope) with `--cut-slope` (in dB/octave, default 12). For example:
|
||||
|
||||
```sh
|
||||
./valhallir-deconvolver --sweep sweep.wav --recorded recorded.wav --output ir.wav --lowcut 40 --highcut 18000 --cut-slope 24
|
||||
```
|
||||
|
||||
This applies a 40 Hz low-cut and 18 kHz high-cut, both with a 24 dB/octave slope (steeper than the default 12).
|
||||
|
||||
**Filter Implementation:** The tool uses Linkwitz-Riley design principles with Direct Form II Transposed filter structures for optimal numerical stability and minimal artifacts, even at very steep slopes (48+ dB/octave). The filters are designed to provide clean cutoffs without introducing unwanted artifacts in the frequency response.
|
||||
|
||||
### 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:
|
||||
|
||||
```sh
|
||||
./valhallir-deconvolver --sweep sweep.wav --recorded recorded.wav --output ir.wav --plot-ir
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `ir.wav` - The impulse response file
|
||||
- `ir.png` - A professional plot showing frequency response and waveform
|
||||
|
||||
The plot includes:
|
||||
- **Frequency Response:** dB vs Hz with log frequency scale (20Hz-20kHz)
|
||||
- **Waveform:** Time-domain view of the first 10ms of the IR
|
||||
- **Valhallir Branding:** Logo displayed in upper-left corner and filename information
|
||||
- **Professional Layout:** Clean, publication-ready visualization
|
||||
|
||||
**Note:** The logo is embedded directly in the binary, so no external assets folder is required. The logo will always be available regardless of where the binary is located or how it's executed.
|
||||
|
||||
### Different Output Formats
|
||||
|
||||
Generate IRs in different sample rates and bit depths:
|
||||
@@ -107,15 +332,23 @@ 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 | `cabpack` 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 |
|
||||
| `--normalize` | Normalize output to peak value (0.0-1.0) | 0.95 | No |
|
||||
| `--trim-threshold` | Silence threshold for trimming (0.0-1.0) | 0.001 | No |
|
||||
| `--length-ms` | Output IR length in milliseconds (trim or zero-pad) | - | No |
|
||||
| `--fade-ms` | Fade-out duration in milliseconds at end of IR (default 5) | 5 | No |
|
||||
| `--lowcut` | Low-cut filter (high-pass) cutoff frequency in Hz (recorded sweep) | - | No |
|
||||
| `--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
|
||||
|
||||
@@ -143,6 +376,17 @@ Generate IRs in different sample rates and bit depths:
|
||||
- If `--length-ms` is set, the output IR (and MPT IR) will be trimmed or zero-padded to the specified length in milliseconds
|
||||
- If not set, the full IR is used
|
||||
|
||||
### Fade-Out
|
||||
- By default, a 5 ms linear fade-out is applied to the end of the IR (and MPT IR) to avoid clicks
|
||||
- You can change the fade duration with `--fade-ms`
|
||||
|
||||
### Filtering
|
||||
- You can apply a low-cut (high-pass) and/or high-cut (low-pass) filter to the recorded sweep before IR extraction
|
||||
- Uses Linkwitz-Riley design principles with Direct Form II Transposed structures for optimal stability
|
||||
- Use `--lowcut` and/or `--highcut` to specify cutoff frequencies in Hz
|
||||
- Use `--cut-slope` to control the filter steepness (12 dB/octave = gentle, 24+ = steeper)
|
||||
- The filter implementation is designed to provide clean cutoffs without artifacts, even at very steep slopes (48+ dB/octave)
|
||||
|
||||
### Deconvolution Process
|
||||
1. **FFT-based deconvolution** of recorded signal by sweep signal
|
||||
2. **Regularization** to prevent division by zero
|
||||
@@ -155,6 +399,19 @@ Generate IRs in different sample rates and bit depths:
|
||||
- **Maintains frequency response** while optimizing phase characteristics
|
||||
- **Suitable for real-time applications** like guitar amp modeling
|
||||
|
||||
### IR Visualization
|
||||
- **Frequency Response Plot:** Shows magnitude response in dB vs Hz with log frequency scale
|
||||
- **Waveform Plot:** Displays the first 10ms of the IR in the time domain
|
||||
- **Professional Layout:** Clean, publication-ready plots with Valhallir branding
|
||||
- **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)
|
||||
@@ -206,6 +463,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:
|
||||
@@ -218,19 +487,35 @@ The pipeline is defined in [`ci/dagger.go`](./ci/dagger.go). It outputs binaries
|
||||
|
||||
### Usage
|
||||
|
||||
1. Install the Dagger Go SDK:
|
||||
1. Install the Dagger Go SDK and dependencies:
|
||||
```sh
|
||||
go install dagger.io/dagger@latest
|
||||
go get github.com/joho/godotenv
|
||||
go mod tidy
|
||||
```
|
||||
2. Run the pipeline:
|
||||
2. Build for all platforms:
|
||||
```sh
|
||||
go run ci/dagger.go
|
||||
```
|
||||
3. (Optional) Upload binaries to a Gitea release:
|
||||
- Create a `.env` file in the project root with:
|
||||
```
|
||||
GITEA_TOKEN=your_token
|
||||
GITEA_URL=https://your.gitea.server
|
||||
GITEA_OWNER=youruser
|
||||
GITEA_REPO=yourrepo
|
||||
```
|
||||
- Run:
|
||||
```sh
|
||||
go run ci/dagger.go --release v1.0.0
|
||||
```
|
||||
- This will create (if needed) and upload all binaries to the specified release tag on your Gitea instance.
|
||||
- **The pipeline will also create and push a local git tag for the release if it does not already exist.**
|
||||
|
||||
### Troubleshooting Dagger
|
||||
- If you see `could not import dagger.io/dagger`, make sure you have installed the Dagger Go SDK and run `go mod tidy`.
|
||||
- The pipeline requires Docker or a compatible container runtime.
|
||||
- For Gitea upload, ensure your `.env` file is present and correct.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -268,4 +553,4 @@ The pipeline is defined in [`ci/dagger.go`](./ci/dagger.go). It outputs binaries
|
||||
|
||||
## Changelog
|
||||
|
||||
See [CHANGELOG.md](./CHANGELOG.md) for version history and details. Current version: v1.0.0
|
||||
See [CHANGELOG.md](./CHANGELOG.md) for version history and details. Current version: v1.2.0
|
||||
4
ci/.env.example
Normal file
4
ci/.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
GITEA_TOKEN=your_token
|
||||
GITEA_URL=https://your.gitea.server
|
||||
GITEA_OWNER=youruser
|
||||
GITEA_REPO=yourrepo
|
||||
164
ci/dagger.go
164
ci/dagger.go
@@ -1,16 +1,35 @@
|
||||
// Dagger pipeline for multi-platform builds.
|
||||
// Requires: go install dagger.io/dagger@latest && go mod tidy
|
||||
// Usage:
|
||||
//
|
||||
// go run ci/dagger.go [--release v1.0.0]
|
||||
//
|
||||
// If --release is provided, uploads all built binaries in dist/ to the specified Gitea release.
|
||||
// Requires .env file with GITEA_TOKEN, GITEA_URL, GITEA_OWNER, GITEA_REPO.
|
||||
// Requires: go install dagger.io/dagger@latest && go get github.com/joho/godotenv && go mod tidy
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"dagger.io/dagger"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
releaseTag := flag.String("release", "", "(optional) Release tag to upload binaries to Gitea")
|
||||
flag.Parse()
|
||||
|
||||
ctx := context.Background()
|
||||
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
|
||||
if err != nil {
|
||||
@@ -30,6 +49,8 @@ func main() {
|
||||
|
||||
src := client.Host().Directory(".")
|
||||
|
||||
var builtBinaries []string
|
||||
|
||||
for _, p := range platforms {
|
||||
binName := fmt.Sprintf("valhallir-deconvolver-%s-%s", p.OS, p.Arch)
|
||||
if p.OS == "windows" {
|
||||
@@ -50,5 +71,146 @@ func main() {
|
||||
panic(fmt.Sprintf("export failed for %s/%s: %v", p.OS, p.Arch, err))
|
||||
}
|
||||
fmt.Printf("Built and exported %s\n", outPath)
|
||||
builtBinaries = append(builtBinaries, outPath)
|
||||
}
|
||||
|
||||
if *releaseTag != "" {
|
||||
fmt.Printf("\nUploading binaries to Gitea release: %s\n", *releaseTag)
|
||||
// Load .env
|
||||
err := godotenv.Load("ci/.env")
|
||||
if err != nil {
|
||||
panic("Error loading .env file: " + err.Error())
|
||||
}
|
||||
giteaToken := os.Getenv("GITEA_TOKEN")
|
||||
giteaURL := os.Getenv("GITEA_URL")
|
||||
giteaOwner := os.Getenv("GITEA_OWNER")
|
||||
giteaRepo := os.Getenv("GITEA_REPO")
|
||||
if giteaToken == "" || giteaURL == "" || giteaOwner == "" || giteaRepo == "" {
|
||||
panic("GITEA_TOKEN, GITEA_URL, GITEA_OWNER, GITEA_REPO must be set in .env")
|
||||
}
|
||||
|
||||
// 1. Get or create the release
|
||||
releaseID, err := getOrCreateRelease(giteaURL, giteaOwner, giteaRepo, *releaseTag, giteaToken)
|
||||
if err != nil {
|
||||
panic("Failed to get or create release: " + err.Error())
|
||||
}
|
||||
|
||||
// 2. Upload each binary as an asset
|
||||
for _, bin := range builtBinaries {
|
||||
fmt.Printf("Uploading %s...\n", bin)
|
||||
err := uploadAsset(giteaURL, giteaOwner, giteaRepo, releaseID, bin, giteaToken)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to upload %s: %v", bin, err))
|
||||
}
|
||||
fmt.Printf("Uploaded %s\n", bin)
|
||||
}
|
||||
fmt.Println("All binaries uploaded to Gitea release.")
|
||||
|
||||
// 3. Tag the release locally and push the tag
|
||||
if err := tagAndPush(*releaseTag); err != nil {
|
||||
panic("Failed to tag and push: " + err.Error())
|
||||
}
|
||||
fmt.Printf("Tagged and pushed %s\n", *releaseTag)
|
||||
}
|
||||
}
|
||||
|
||||
func getOrCreateRelease(url, owner, repo, tag, token string) (int, error) {
|
||||
// Try to get the release by tag
|
||||
api := fmt.Sprintf("%s/api/v1/repos/%s/%s/releases/tags/%s", strings.TrimRight(url, "/"), owner, repo, tag)
|
||||
req, _ := http.NewRequest("GET", api, nil)
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == 200 {
|
||||
// Parse JSON to get ID
|
||||
type releaseResp struct{ ID int }
|
||||
var r releaseResp
|
||||
io.ReadAll(resp.Body) // ignore body for now, parse below
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
dec.Decode(&r)
|
||||
return r.ID, nil
|
||||
}
|
||||
// If not found, create it
|
||||
if resp.StatusCode == 404 {
|
||||
api = fmt.Sprintf("%s/api/v1/repos/%s/%s/releases", strings.TrimRight(url, "/"), owner, repo)
|
||||
body := strings.NewReader(fmt.Sprintf(`{"tag_name":"%s","name":"%s"}`, tag, tag))
|
||||
req, _ = http.NewRequest("POST", api, body)
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == 201 {
|
||||
type releaseResp struct{ ID int }
|
||||
var r releaseResp
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
dec.Decode(&r)
|
||||
return r.ID, nil
|
||||
}
|
||||
return 0, fmt.Errorf("failed to create release: %s", resp.Status)
|
||||
}
|
||||
return 0, fmt.Errorf("failed to get release: %s", resp.Status)
|
||||
}
|
||||
|
||||
func uploadAsset(url, owner, repo string, releaseID int, filePath, token string) error {
|
||||
api := fmt.Sprintf("%s/api/v1/repos/%s/%s/releases/%d/assets?name=%s", strings.TrimRight(url, "/"), owner, repo, releaseID, filepath.Base(filePath))
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var b bytes.Buffer
|
||||
w := multipart.NewWriter(&b)
|
||||
f, err := w.CreateFormFile("attachment", filepath.Base(filePath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(f, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Close()
|
||||
|
||||
req, _ := http.NewRequest("POST", api, &b)
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
req.Header.Set("Content-Type", w.FormDataContentType())
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 201 {
|
||||
return fmt.Errorf("upload failed: %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tagAndPush(tag string) error {
|
||||
// Check if tag exists
|
||||
cmd := exec.Command("git", "tag", "--list", tag)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(string(out)) == tag {
|
||||
// Tag already exists
|
||||
return nil
|
||||
}
|
||||
// Create tag
|
||||
cmd = exec.Command("git", "tag", tag)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Push tag
|
||||
cmd = exec.Command("git", "push", "origin", tag)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
11
go.mod
11
go.mod
@@ -1,26 +1,34 @@
|
||||
module valhallir-deconvolver
|
||||
|
||||
go 1.24.1
|
||||
go 1.24.5
|
||||
|
||||
require (
|
||||
dagger.io/dagger v0.18.12
|
||||
github.com/go-audio/audio v1.0.0
|
||||
github.com/go-audio/wav v1.1.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12
|
||||
github.com/urfave/cli/v2 v2.27.7
|
||||
gonum.org/v1/gonum v0.13.0
|
||||
gonum.org/v1/plot v0.10.1
|
||||
)
|
||||
|
||||
require (
|
||||
git.sr.ht/~sbinet/gg v0.3.1 // indirect
|
||||
github.com/99designs/gqlgen v0.17.75 // indirect
|
||||
github.com/Khan/genqlient v0.8.1 // indirect
|
||||
github.com/adrg/xdg v0.5.3 // indirect
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/go-audio/riff v1.0.0 // indirect
|
||||
github.com/go-fonts/liberation v0.3.0 // indirect
|
||||
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-pdf/fpdf v0.6.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
@@ -44,6 +52,7 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
|
||||
golang.org/x/image v0.6.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
|
||||
133
go.sum
133
go.sum
@@ -1,32 +1,68 @@
|
||||
dagger.io/dagger v0.18.12 h1:s7v8aHlzDUogZ/jW92lHC+gljCNRML+0mosfh13R4vs=
|
||||
dagger.io/dagger v0.18.12/go.mod h1:azlZ24m2br95t0jQHUBpL5SiafeqtVDLl1Itlq6GO+4=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
|
||||
git.sr.ht/~sbinet/gg v0.3.1 h1:LNhjNn8DerC8f9DHLz6lS0YYul/b602DUxDgGkd/Aik=
|
||||
git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
|
||||
github.com/99designs/gqlgen v0.17.75 h1:GwHJsptXWLHeY7JO8b7YueUI4w9Pom6wJTICosDtQuI=
|
||||
github.com/99designs/gqlgen v0.17.75/go.mod h1:p7gbTpdnHyl70hmSpM8XG8GiKwmCv+T5zkdY8U8bLog=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
|
||||
github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU=
|
||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
|
||||
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4=
|
||||
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
||||
github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA=
|
||||
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
|
||||
github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g=
|
||||
github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE=
|
||||
github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ=
|
||||
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
|
||||
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
|
||||
github.com/go-fonts/latin-modern v0.3.0 h1:CIDlMm0djMO3XIKHVz2na9lFKt3kdC/YCy7k7lLpyjE=
|
||||
github.com/go-fonts/latin-modern v0.3.0/go.mod h1:ysEQXnuT/sCDOAONxC7ImeEDVINbltClhasMAqEtRK0=
|
||||
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
|
||||
github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
|
||||
github.com/go-fonts/liberation v0.3.0 h1:3BI2iaE7R/s6uUUtzNCjo3QijJu3aS4wmrMgfSpYQ+8=
|
||||
github.com/go-fonts/liberation v0.3.0/go.mod h1:jdJ+cqF+F4SUL2V+qxBth8fvBpBDS7yloUL5Fi8GTGY=
|
||||
github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
|
||||
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
|
||||
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9 h1:NxXI5pTAtpEaU49bpLpQoDsu1zrteW/vxzTz8Cd2UAs=
|
||||
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9/go.mod h1:gWuR/CrFDDeVRFQwHPvsv9soJVB/iqymhuZQuJ3a9OM=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
|
||||
github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8=
|
||||
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
@@ -35,18 +71,31 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12 h1:dd7vnTDfjtwCETZDrRe+GPYNLA1jBtbZeyfyE8eZCyk=
|
||||
github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12/go.mod h1:i/KKcxEWEO8Yyl11DYafRPKOPVYTrhxiTRigjtEEXZU=
|
||||
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
|
||||
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
||||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||
@@ -55,6 +104,8 @@ github.com/vektah/gqlparser/v2 v2.5.28 h1:bIulcl3LF69ba6EiZVGD88y4MkM+Jxrf3P2MX8
|
||||
github.com/vektah/gqlparser/v2 v2.5.28/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
@@ -91,18 +142,97 @@ go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9f
|
||||
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
|
||||
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
||||
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
|
||||
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
|
||||
gonum.org/v1/gonum v0.13.0 h1:a0T3bh+7fhRyqeNbiC3qVHYmkiQgit3wnNan/2c0HMM=
|
||||
gonum.org/v1/gonum v0.13.0/go.mod h1:/WPYRckkfWrhWefxyYTfrTtQR0KH4iyHNuzxqXAKyAU=
|
||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
|
||||
gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4=
|
||||
gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
|
||||
@@ -113,3 +243,6 @@ google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9x
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
|
||||
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@@ -285,11 +285,25 @@ func realSlice(in []complex128) []float64 {
|
||||
}
|
||||
|
||||
// Resample resamples audio data from one sample rate to another using linear interpolation
|
||||
// with proper anti-aliasing for downsampling
|
||||
func Resample(data []float64, fromSampleRate, toSampleRate int) []float64 {
|
||||
if fromSampleRate == toSampleRate {
|
||||
return data
|
||||
}
|
||||
|
||||
// For downsampling, apply anti-aliasing filter to prevent aliasing
|
||||
// Filter out frequencies above the target Nyquist frequency
|
||||
if toSampleRate < fromSampleRate {
|
||||
// Calculate target Nyquist frequency (slightly below to ensure clean cutoff)
|
||||
targetNyquist := float64(toSampleRate) / 2.0
|
||||
// Use 90% of Nyquist as cutoff to ensure clean anti-aliasing
|
||||
antiAliasCutoff := targetNyquist * 0.9
|
||||
|
||||
// Apply steep low-pass filter to prevent aliasing
|
||||
// Use 48 dB/octave slope for clean anti-aliasing
|
||||
data = CascadeHighcut(data, fromSampleRate, antiAliasCutoff, 48)
|
||||
}
|
||||
|
||||
// Calculate the resampling ratio
|
||||
ratio := float64(toSampleRate) / float64(fromSampleRate)
|
||||
newLength := int(float64(len(data)) * ratio)
|
||||
@@ -322,3 +336,366 @@ func Resample(data []float64, fromSampleRate, toSampleRate int) []float64 {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FadeOutLinear applies a linear fade-out to the last fadeSamples of the data.
|
||||
// fadeSamples is the number of samples over which to fade to zero.
|
||||
func FadeOutLinear(data []float64, fadeSamples int) []float64 {
|
||||
if fadeSamples <= 0 || len(data) == 0 {
|
||||
return data
|
||||
}
|
||||
if fadeSamples > len(data) {
|
||||
fadeSamples = len(data)
|
||||
}
|
||||
out := make([]float64, len(data))
|
||||
copy(out, data)
|
||||
start := len(data) - fadeSamples
|
||||
for i := start; i < len(data); i++ {
|
||||
fade := float64(len(data)-i) / float64(fadeSamples)
|
||||
out[i] *= fade
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// biquadFilterState holds the state for a biquad filter
|
||||
type biquadFilterState struct {
|
||||
x1, x2 float64 // input history (intermediate values in DF2T)
|
||||
}
|
||||
|
||||
// applyBiquadDF2T applies a biquad filter using Direct Form II Transposed (more numerically stable)
|
||||
// This form is preferred for cascaded filters as it reduces numerical errors
|
||||
// Uses high-precision calculations and proper state management for maximum quality
|
||||
func applyBiquadDF2T(data []float64, b0, b1, b2, a1, a2 float64, state *biquadFilterState) []float64 {
|
||||
out := make([]float64, len(data))
|
||||
|
||||
// Initialize state if nil
|
||||
if state == nil {
|
||||
state = &biquadFilterState{}
|
||||
}
|
||||
|
||||
// Direct Form II Transposed implementation with high precision
|
||||
// This structure minimizes numerical errors in cascaded filters
|
||||
for i := 0; i < len(data); i++ {
|
||||
x := data[i]
|
||||
// Compute intermediate value (w[n] = x[n] - a1*w[n-1] - a2*w[n-2])
|
||||
w := x - a1*state.x1 - a2*state.x2
|
||||
// Compute output (y[n] = b0*w[n] + b1*w[n-1] + b2*w[n-2])
|
||||
y := b0*w + b1*state.x1 + b2*state.x2
|
||||
out[i] = y
|
||||
// Update state (shift delay line)
|
||||
state.x2 = state.x1
|
||||
state.x1 = w
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// warmupFilter applies a filter with warm-up to avoid transients
|
||||
// This is especially important for high-quality filtering
|
||||
func warmupFilter(data []float64, b0, b1, b2, a1, a2 float64) []float64 {
|
||||
if len(data) == 0 {
|
||||
return data
|
||||
}
|
||||
|
||||
// Use first sample value for warm-up to avoid transients
|
||||
warmupValue := data[0]
|
||||
state := &biquadFilterState{}
|
||||
|
||||
// Warm up the filter with a few samples of the first value
|
||||
// This initializes the filter state properly
|
||||
warmupSamples := 10
|
||||
warmupData := make([]float64, warmupSamples)
|
||||
for i := range warmupData {
|
||||
warmupData[i] = warmupValue
|
||||
}
|
||||
_ = applyBiquadDF2T(warmupData, b0, b1, b2, a1, a2, state)
|
||||
|
||||
// Now apply filter to actual data with warmed-up state
|
||||
return applyBiquadDF2T(data, b0, b1, b2, a1, a2, state)
|
||||
}
|
||||
|
||||
// ApplyLowpassButterworth applies a 2nd-order Butterworth low-pass filter to the data.
|
||||
// cutoffHz: cutoff frequency in Hz, sampleRate: sample rate in Hz.
|
||||
// Uses Direct Form II Transposed for better numerical stability.
|
||||
func ApplyLowpassButterworth(data []float64, sampleRate int, cutoffHz float64) []float64 {
|
||||
if cutoffHz <= 0 || cutoffHz >= float64(sampleRate)/2 {
|
||||
return data
|
||||
}
|
||||
// Biquad coefficients for Butterworth low-pass
|
||||
w0 := 2 * math.Pi * cutoffHz / float64(sampleRate)
|
||||
cosw0 := math.Cos(w0)
|
||||
sinw0 := math.Sin(w0)
|
||||
Q := 1.0 / math.Sqrt(2) // Butterworth Q
|
||||
alpha := sinw0 / (2 * Q)
|
||||
|
||||
b0 := (1 - cosw0) / 2
|
||||
b1 := 1 - cosw0
|
||||
b2 := (1 - cosw0) / 2
|
||||
a0 := 1 + alpha
|
||||
a1 := -2 * cosw0
|
||||
a2 := 1 - alpha
|
||||
|
||||
// Normalize coefficients
|
||||
b0 /= a0
|
||||
b1 /= a0
|
||||
b2 /= a0
|
||||
a1 /= a0
|
||||
a2 /= a0
|
||||
|
||||
// Apply filter using Direct Form II Transposed
|
||||
state := &biquadFilterState{}
|
||||
return applyBiquadDF2T(data, b0, b1, b2, a1, a2, state)
|
||||
}
|
||||
|
||||
// ApplyHighpassButterworth applies a 2nd-order Butterworth high-pass filter to the data.
|
||||
// cutoffHz: cutoff frequency in Hz, sampleRate: sample rate in Hz.
|
||||
// Uses Direct Form II Transposed for better numerical stability.
|
||||
func ApplyHighpassButterworth(data []float64, sampleRate int, cutoffHz float64) []float64 {
|
||||
if cutoffHz <= 0 || cutoffHz >= float64(sampleRate)/2 {
|
||||
return data
|
||||
}
|
||||
// Biquad coefficients for Butterworth high-pass
|
||||
w0 := 2 * math.Pi * cutoffHz / float64(sampleRate)
|
||||
cosw0 := math.Cos(w0)
|
||||
sinw0 := math.Sin(w0)
|
||||
Q := 1.0 / math.Sqrt(2) // Butterworth Q
|
||||
alpha := sinw0 / (2 * Q)
|
||||
|
||||
b0 := (1 + cosw0) / 2
|
||||
b1 := -(1 + cosw0)
|
||||
b2 := (1 + cosw0) / 2
|
||||
a0 := 1 + alpha
|
||||
a1 := -2 * cosw0
|
||||
a2 := 1 - alpha
|
||||
|
||||
// Normalize coefficients
|
||||
b0 /= a0
|
||||
b1 /= a0
|
||||
b2 /= a0
|
||||
a1 /= a0
|
||||
a2 /= a0
|
||||
|
||||
// Apply filter using Direct Form II Transposed
|
||||
state := &biquadFilterState{}
|
||||
return applyBiquadDF2T(data, b0, b1, b2, a1, a2, state)
|
||||
}
|
||||
|
||||
// CascadeLowcut applies the low-cut (high-pass) filter multiple times for steeper slopes.
|
||||
// Uses Linkwitz-Riley design principles with high-quality implementation for maximum precision.
|
||||
// Features: proper warm-up, high-precision coefficients, optimized cascade structure.
|
||||
// slopeDb: 12, 24, 36, 48, ... (dB/octave)
|
||||
func CascadeLowcut(data []float64, sampleRate int, cutoffHz float64, slopeDb int) []float64 {
|
||||
if slopeDb < 12 {
|
||||
slopeDb = 12
|
||||
}
|
||||
if cutoffHz <= 0 || cutoffHz >= float64(sampleRate)/2 {
|
||||
return data
|
||||
}
|
||||
|
||||
n := slopeDb / 12
|
||||
if n == 0 {
|
||||
return data
|
||||
}
|
||||
|
||||
// High-quality filter implementation with proper coefficient calculation
|
||||
// Use high precision for coefficient calculation
|
||||
w0 := 2.0 * math.Pi * cutoffHz / float64(sampleRate)
|
||||
|
||||
// Pre-calculate trigonometric functions once for precision
|
||||
cosw0 := math.Cos(w0)
|
||||
sinw0 := math.Sin(w0)
|
||||
|
||||
// Butterworth Q factor (1/sqrt(2) ≈ 0.7071067811865476 for maximally flat response)
|
||||
const butterworthQ = 0.7071067811865476
|
||||
alpha := sinw0 / (2.0 * butterworthQ)
|
||||
|
||||
// High-pass Butterworth coefficients
|
||||
b0 := (1.0 + cosw0) / 2.0
|
||||
b1 := -(1.0 + cosw0)
|
||||
b2 := (1.0 + cosw0) / 2.0
|
||||
a0 := 1.0 + alpha
|
||||
a1 := -2.0 * cosw0
|
||||
a2 := 1.0 - alpha
|
||||
|
||||
// Normalize coefficients for Direct Form II Transposed
|
||||
// This ensures proper scaling and numerical stability
|
||||
b0 /= a0
|
||||
b1 /= a0
|
||||
b2 /= a0
|
||||
a1 /= a0
|
||||
a2 /= a0
|
||||
|
||||
// Apply cascaded filters with proper warm-up for each stage
|
||||
// This ensures clean filtering without transients, especially important for MPT
|
||||
out := make([]float64, len(data))
|
||||
copy(out, data)
|
||||
|
||||
for stage := 0; stage < n; stage++ {
|
||||
// Use warm-up for first stage to avoid transients
|
||||
// Subsequent stages benefit from the already-filtered signal
|
||||
if stage == 0 {
|
||||
out = warmupFilter(out, b0, b1, b2, a1, a2)
|
||||
} else {
|
||||
// For subsequent stages, use fresh state but no warm-up needed
|
||||
// as the signal is already filtered
|
||||
state := &biquadFilterState{}
|
||||
filtered := applyBiquadDF2T(out, b0, b1, b2, a1, a2, state)
|
||||
copy(out, filtered)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// CascadeHighcut applies the high-cut (low-pass) filter multiple times for steeper slopes.
|
||||
// Uses Linkwitz-Riley design principles with high-quality implementation for maximum precision.
|
||||
// Features: proper warm-up, high-precision coefficients, optimized cascade structure.
|
||||
// slopeDb: 12, 24, 36, 48, ... (dB/octave)
|
||||
func CascadeHighcut(data []float64, sampleRate int, cutoffHz float64, slopeDb int) []float64 {
|
||||
if slopeDb < 12 {
|
||||
slopeDb = 12
|
||||
}
|
||||
if cutoffHz <= 0 || cutoffHz >= float64(sampleRate)/2 {
|
||||
return data
|
||||
}
|
||||
|
||||
n := slopeDb / 12
|
||||
if n == 0 {
|
||||
return data
|
||||
}
|
||||
|
||||
// High-quality filter implementation with proper coefficient calculation
|
||||
// Use high precision for coefficient calculation
|
||||
w0 := 2.0 * math.Pi * cutoffHz / float64(sampleRate)
|
||||
|
||||
// Pre-calculate trigonometric functions once for precision
|
||||
cosw0 := math.Cos(w0)
|
||||
sinw0 := math.Sin(w0)
|
||||
|
||||
// Butterworth Q factor (1/sqrt(2) ≈ 0.7071067811865476 for maximally flat response)
|
||||
const butterworthQ = 0.7071067811865476
|
||||
alpha := sinw0 / (2.0 * butterworthQ)
|
||||
|
||||
// Low-pass Butterworth coefficients
|
||||
b0 := (1.0 - cosw0) / 2.0
|
||||
b1 := 1.0 - cosw0
|
||||
b2 := (1.0 - cosw0) / 2.0
|
||||
a0 := 1.0 + alpha
|
||||
a1 := -2.0 * cosw0
|
||||
a2 := 1.0 - alpha
|
||||
|
||||
// Normalize coefficients for Direct Form II Transposed
|
||||
// This ensures proper scaling and numerical stability
|
||||
b0 /= a0
|
||||
b1 /= a0
|
||||
b2 /= a0
|
||||
a1 /= a0
|
||||
a2 /= a0
|
||||
|
||||
// Apply cascaded filters with proper warm-up for each stage
|
||||
// This ensures clean filtering without transients, especially important for MPT
|
||||
out := make([]float64, len(data))
|
||||
copy(out, data)
|
||||
|
||||
for stage := 0; stage < n; stage++ {
|
||||
// Use warm-up for first stage to avoid transients
|
||||
// Subsequent stages benefit from the already-filtered signal
|
||||
if stage == 0 {
|
||||
out = warmupFilter(out, b0, b1, b2, a1, a2)
|
||||
} else {
|
||||
// For subsequent stages, use fresh state but no warm-up needed
|
||||
// as the signal is already filtered
|
||||
state := &biquadFilterState{}
|
||||
filtered := applyBiquadDF2T(out, b0, b1, b2, a1, a2, state)
|
||||
copy(out, filtered)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
209
pkg/plot/plot.go
Normal file
209
pkg/plot/plot.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package plot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"math"
|
||||
"math/cmplx"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mjibson/go-dsp/fft"
|
||||
"gonum.org/v1/plot"
|
||||
"gonum.org/v1/plot/font"
|
||||
"gonum.org/v1/plot/plotter"
|
||||
"gonum.org/v1/plot/vg"
|
||||
"gonum.org/v1/plot/vg/draw"
|
||||
"gonum.org/v1/plot/vg/vgimg"
|
||||
)
|
||||
|
||||
// PlotIR plots the frequency response (magnitude in dB vs. frequency in Hz) of the IR to a PNG file
|
||||
// logoData is the embedded logo image data (can be nil if not available)
|
||||
func PlotIR(ir []float64, sampleRate int, irFileName string, logoData []byte) error {
|
||||
if len(ir) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Use only the first 8192 samples of the IR for plotting
|
||||
windowLen := 8192
|
||||
if len(ir) < windowLen {
|
||||
windowLen = len(ir)
|
||||
}
|
||||
irWin := ir[:windowLen]
|
||||
X := fft.FFTReal(irWin)
|
||||
// Plot from 20 Hz up to 20kHz, include every bin
|
||||
var plotPts plotter.XYs
|
||||
var minDb float64 = 1e9
|
||||
var maxDb float64 = -1e9
|
||||
var minDbFreq float64
|
||||
freqBins := windowLen / 2
|
||||
for i := 1; i < freqBins; i++ {
|
||||
freq := float64(i) * float64(sampleRate) / float64(windowLen)
|
||||
if freq < 20.0 {
|
||||
continue
|
||||
}
|
||||
if freq > 20000.0 {
|
||||
break
|
||||
}
|
||||
mag := cmplx.Abs(X[i])
|
||||
if mag < 1e-12 {
|
||||
mag = 1e-12
|
||||
}
|
||||
db := 20 * math.Log10(mag)
|
||||
plotPts = append(plotPts, plotter.XY{X: freq, Y: db})
|
||||
if db < minDb {
|
||||
minDb = db
|
||||
minDbFreq = freq
|
||||
}
|
||||
if db > maxDb {
|
||||
maxDb = db
|
||||
}
|
||||
}
|
||||
fmt.Printf("[PlotIR] minDb in plotted range: %.2f dB at %.2f Hz\n", minDb, minDbFreq)
|
||||
p := plot.New()
|
||||
p.Title.Text = "IR Frequency Response"
|
||||
p.X.Label.Text = "Frequency (Hz)"
|
||||
p.Y.Label.Text = "Magnitude (dB)"
|
||||
p.X.Scale = plot.LogScale{}
|
||||
p.X.Tick.Marker = plot.TickerFunc(func(min, max float64) []plot.Tick {
|
||||
ticks := []float64{20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000}
|
||||
labels := []string{"20", "50", "100", "200", "500", "1k", "2k", "5k", "10k", "20k"}
|
||||
var result []plot.Tick
|
||||
for i, v := range ticks {
|
||||
if v >= min && v <= max {
|
||||
result = append(result, plot.Tick{Value: v, Label: labels[i]})
|
||||
}
|
||||
}
|
||||
return result
|
||||
})
|
||||
line, err := plotter.NewLine(plotPts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Set line color to blue
|
||||
line.Color = color.RGBA{R: 30, G: 100, B: 220, A: 255}
|
||||
p.Add(line)
|
||||
// Find minimum dB value between 20 Hz and 50 Hz for y-axis anchor
|
||||
minDb2050 := 1e9
|
||||
for i := 1; i < freqBins; i++ {
|
||||
freq := float64(i) * float64(sampleRate) / float64(windowLen)
|
||||
if freq < 20.0 {
|
||||
continue
|
||||
}
|
||||
if freq > 50.0 {
|
||||
break
|
||||
}
|
||||
mag := cmplx.Abs(X[i])
|
||||
if mag < 1e-12 {
|
||||
mag = 1e-12
|
||||
}
|
||||
db := 20 * math.Log10(mag)
|
||||
if db < minDb2050 {
|
||||
minDb2050 = db
|
||||
}
|
||||
}
|
||||
p.Y.Min = minDb2050
|
||||
p.Y.Max = math.Ceil(maxDb)
|
||||
p.X.Min = 20.0
|
||||
p.X.Max = 20000.0
|
||||
|
||||
// --- Time-aligned waveform plot ---
|
||||
p2 := plot.New()
|
||||
p2.Title.Text = "IR Waveform"
|
||||
p2.X.Label.Text = "Time (ms)"
|
||||
p2.Y.Label.Text = "Amplitude"
|
||||
// Prepare waveform data (only first 10ms)
|
||||
var pts plotter.XYs
|
||||
maxTimeMs := 10.0
|
||||
for i := 0; i < windowLen; i++ {
|
||||
t := float64(i) * 1000.0 / float64(sampleRate) // ms
|
||||
if t > maxTimeMs {
|
||||
break
|
||||
}
|
||||
pts = append(pts, plotter.XY{X: t, Y: irWin[i]})
|
||||
}
|
||||
wline, err := plotter.NewLine(pts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wline.Color = color.RGBA{R: 30, G: 100, B: 220, A: 255}
|
||||
p2.Add(wline)
|
||||
p2.X.Min = 0
|
||||
p2.X.Max = maxTimeMs
|
||||
// Y range auto
|
||||
|
||||
// --- Compose both plots vertically ---
|
||||
const width = 6 * vg.Inch
|
||||
const height = 8 * vg.Inch // increased height for frequency diagram
|
||||
img := vgimg.New(width, height+1*vg.Inch) // extra space for logo/headline
|
||||
dc := draw.New(img)
|
||||
|
||||
// Draw logo at the top left, headline to the right, IR filename below
|
||||
// Logo is embedded in the binary
|
||||
logoW := 2.4 * vg.Inch // doubled size
|
||||
logoH := 0.68 * vg.Inch // doubled size
|
||||
logoX := 0.3 * vg.Inch
|
||||
logoY := height + 0.2*vg.Inch // move logo down by an additional ~10px
|
||||
logoDrawn := false
|
||||
if len(logoData) > 0 {
|
||||
logoImg, err := png.Decode(bytes.NewReader(logoData))
|
||||
if err == nil {
|
||||
rect := vg.Rectangle{
|
||||
Min: vg.Point{X: logoX, Y: logoY},
|
||||
Max: vg.Point{X: logoX + logoW, Y: logoY + logoH},
|
||||
}
|
||||
dc.DrawImage(rect, logoImg)
|
||||
logoDrawn = true
|
||||
}
|
||||
}
|
||||
// Draw headline (bold, larger) to the right of the logo
|
||||
headline := "Valhallir Deconvolver IR Analysis"
|
||||
fntSize := vg.Points(14) // Same as IR filename
|
||||
if logoDrawn {
|
||||
headlineX := logoX + logoW + 0.3*vg.Inch
|
||||
headlineY := logoY + logoH - vg.Points(16) - vg.Points(5) // move headline up by ~10px
|
||||
boldFont := plot.DefaultFont
|
||||
boldFont.Weight = 3 // font.WeightBold is 3 in gonum/plot/font
|
||||
boldFace := font.DefaultCache.Lookup(boldFont, fntSize)
|
||||
dc.SetColor(color.Black)
|
||||
dc.FillString(boldFace, vg.Point{X: headlineX, Y: headlineY}, headline)
|
||||
// Draw IR filename below headline, left-aligned, standard font
|
||||
fileLabel := "IR-File: " + filepath.Base(irFileName)
|
||||
fileY := headlineY - fntSize - vg.Points(6)
|
||||
fileFace := font.DefaultCache.Lookup(plot.DefaultFont, vg.Points(10))
|
||||
dc.FillString(fileFace, vg.Point{X: headlineX, Y: fileY}, fileLabel)
|
||||
}
|
||||
|
||||
// Custom tile arrangement: frequency diagram gets more height, waveform gets less
|
||||
tiles := draw.Tiles{
|
||||
Rows: 2,
|
||||
Cols: 1,
|
||||
PadX: vg.Millimeter,
|
||||
PadY: 20 * vg.Millimeter, // more space between plots to emphasize frequency diagram
|
||||
PadTop: vg.Points(15), // move diagrams down by ~20px
|
||||
}
|
||||
|
||||
// Offset the plots down by 1 inch to make space for logo/headline
|
||||
imgPlots := vgimg.New(width, height)
|
||||
dcPlots := draw.New(imgPlots)
|
||||
canvases := plot.Align([][]*plot.Plot{{p}, {p2}}, tiles, dcPlots)
|
||||
p.Draw(canvases[0][0])
|
||||
p2.Draw(canvases[1][0])
|
||||
dc.DrawImage(vg.Rectangle{Min: vg.Point{X: 0, Y: 0}, Max: vg.Point{X: width, Y: height}}, imgPlots.Image())
|
||||
|
||||
// Save as PNG in the same directory as the IR file
|
||||
irDir := filepath.Dir(irFileName)
|
||||
irBase := filepath.Base(irFileName)
|
||||
irNameWithoutExt := strings.TrimSuffix(irBase, filepath.Ext(irBase))
|
||||
plotFileName := filepath.Join(irDir, irNameWithoutExt+".png")
|
||||
|
||||
plotFile, err := os.Create(plotFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer plotFile.Close()
|
||||
_, err = vgimg.PngCanvas{Canvas: img}.WriteTo(plotFile)
|
||||
return err
|
||||
}
|
||||
@@ -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