Compare commits

..

2 Commits

12 changed files with 198 additions and 2109 deletions

21
.gitignore vendored
View File

@@ -42,24 +42,3 @@ ehthumbs.db
# 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

View File

@@ -1,77 +1,5 @@
# 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

238
README.md
View File

@@ -9,16 +9,11 @@ A CLI tool for processing WAV files to generate impulse responses (IR) from swee
- **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
- **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
- **Modular design** with separate packages for WAV I/O and convolution
- **Robust error handling** and validation
## Installation
@@ -34,24 +29,6 @@ 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):
@@ -60,131 +37,6 @@ 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:
@@ -235,53 +87,6 @@ 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).
**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:
@@ -332,9 +137,9 @@ Generate IRs in different sample rates and bit depths:
| Flag | Description | Default | Required |
|------|-------------|---------|----------|
| `--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 |
| `--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 |
| `--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 |
@@ -345,10 +150,6 @@ Generate IRs in different sample rates and bit depths:
| `--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
@@ -381,11 +182,9 @@ Generate IRs in different sample rates and bit depths:
- 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
- You can apply a Butterworth low-cut (high-pass) and/or high-cut (low-pass) filter to the recorded sweep before IR extraction
- 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
@@ -399,19 +198,6 @@ 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)
@@ -463,18 +249,6 @@ 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:
@@ -553,4 +327,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.2.0
See [CHANGELOG.md](./CHANGELOG.md) for version history and details. Current version: v1.0.0

8
go.mod
View File

@@ -10,25 +10,18 @@ require (
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
@@ -52,7 +45,6 @@ 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

131
go.sum
View File

@@ -1,68 +1,32 @@
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=
@@ -73,29 +37,18 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5uk
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=
@@ -104,8 +57,6 @@ 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=
@@ -142,97 +93,18 @@ 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=
@@ -243,6 +115,3 @@ 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=

1295
main.go

File diff suppressed because it is too large Load Diff

View File

@@ -285,25 +285,11 @@ 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)
@@ -356,71 +342,13 @@ func FadeOutLinear(data []float64, fadeSamples int) []float64 {
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
// Biquad coefficients
w0 := 2 * math.Pi * cutoffHz / float64(sampleRate)
cosw0 := math.Cos(w0)
sinw0 := math.Sin(w0)
@@ -434,26 +362,35 @@ func ApplyLowpassButterworth(data []float64, sampleRate int, cutoffHz float64) [
a1 := -2 * cosw0
a2 := 1 - alpha
// Normalize coefficients
// Normalize
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)
// Apply filter (Direct Form I)
out := make([]float64, len(data))
var x1, x2, y1, y2 float64
for i := 0; i < len(data); i++ {
x0 := data[i]
y0 := b0*x0 + b1*x1 + b2*x2 - a1*y1 - a2*y2
out[i] = y0
x2 = x1
x1 = x0
y2 = y1
y1 = y0
}
return out
}
// 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
// Biquad coefficients
w0 := 2 * math.Pi * cutoffHz / float64(sampleRate)
cosw0 := math.Cos(w0)
sinw0 := math.Sin(w0)
@@ -467,235 +404,52 @@ func ApplyHighpassButterworth(data []float64, sampleRate int, cutoffHz float64)
a1 := -2 * cosw0
a2 := 1 - alpha
// Normalize coefficients
// Normalize
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)
// Apply filter (Direct Form I)
out := make([]float64, len(data))
var x1, x2, y1, y2 float64
for i := 0; i < len(data); i++ {
x0 := data[i]
y0 := b0*x0 + b1*x1 + b2*x2 - a1*y1 - a2*y2
out[i] = y0
x2 = x1
x1 = x0
y2 = y1
y1 = y0
}
return out
}
// 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)
// slopeDb: 12, 24, 36, ... (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
out := data
for i := 0; i < n; i++ {
out = ApplyHighpassButterworth(out, sampleRate, cutoffHz)
}
// 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)
// slopeDb: 12, 24, 36, ... (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
out := data
for i := 0; i < n; i++ {
out = ApplyLowpassButterworth(out, sampleRate, cutoffHz)
}
// 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)
}

View File

@@ -1,209 +0,0 @@
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
}

View File

@@ -36,15 +36,6 @@ 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
testdata/ir.wav vendored Normal file

Binary file not shown.

BIN
testdata/ir_mpt.wav vendored Normal file

Binary file not shown.

Binary file not shown.