Compare commits
6 Commits
v1.0.0
...
279038c566
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
279038c566 | ||
| 80ab0f0922 | |||
| bb9a93a4f0 | |||
| a113550aee | |||
|
|
8cacf243d8 | ||
|
|
3ed43bda8b |
26
.gitignore
vendored
26
.gitignore
vendored
@@ -38,4 +38,28 @@ Thumbs.db
|
|||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
|
|
||||||
# Dagger cache
|
# 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
|
||||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,5 +1,33 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [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
|
## [v1.0.0] - 2024-06-09
|
||||||
### Added
|
### Added
|
||||||
- Initial public release: Valhallir Deconvolver
|
- Initial public release: Valhallir Deconvolver
|
||||||
|
|||||||
92
README.md
92
README.md
@@ -7,11 +7,14 @@ A CLI tool for processing WAV files to generate impulse responses (IR) from swee
|
|||||||
- **Fast FFT-based deconvolution** for accurate IR extraction
|
- **Fast FFT-based deconvolution** for accurate IR extraction
|
||||||
- **Automatic input conversion:** Accepts any WAV sample rate, bit depth, or channel count
|
- **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 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`
|
||||||
- **96kHz 24-bit WAV file support** for high-quality audio processing
|
- **96kHz 24-bit WAV file support** for high-quality audio processing
|
||||||
- **Multiple output formats** with configurable sample rates and bit depths
|
- **Multiple output formats** with configurable sample rates and bit depths
|
||||||
- **Minimum Phase Transform (MPT)** option for reduced latency IRs
|
- **Minimum Phase Transform (MPT)** option for reduced latency IRs
|
||||||
- **Automatic silence trimming** and normalization
|
- **Automatic silence trimming** and normalization
|
||||||
- **Modular design** with separate packages for WAV I/O and convolution
|
- **Modular design** with separate packages for WAV I/O, convolution, and visualization
|
||||||
- **Robust error handling** and validation
|
- **Robust error handling** and validation
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -57,6 +60,52 @@ 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).
|
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).
|
||||||
|
|
||||||
|
### 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 and filename information
|
||||||
|
- **Professional Layout:** Clean, publication-ready visualization
|
||||||
|
|
||||||
### Different Output Formats
|
### Different Output Formats
|
||||||
|
|
||||||
Generate IRs in different sample rates and bit depths:
|
Generate IRs in different sample rates and bit depths:
|
||||||
@@ -116,6 +165,11 @@ Generate IRs in different sample rates and bit depths:
|
|||||||
| `--normalize` | Normalize output to peak value (0.0-1.0) | 0.95 | 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 |
|
| `--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 |
|
| `--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 |
|
||||||
|
|
||||||
## File Requirements
|
## File Requirements
|
||||||
|
|
||||||
@@ -143,6 +197,15 @@ 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 `--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
|
- 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 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)
|
||||||
|
|
||||||
### Deconvolution Process
|
### Deconvolution Process
|
||||||
1. **FFT-based deconvolution** of recorded signal by sweep signal
|
1. **FFT-based deconvolution** of recorded signal by sweep signal
|
||||||
2. **Regularization** to prevent division by zero
|
2. **Regularization** to prevent division by zero
|
||||||
@@ -155,6 +218,13 @@ Generate IRs in different sample rates and bit depths:
|
|||||||
- **Maintains frequency response** while optimizing phase characteristics
|
- **Maintains frequency response** while optimizing phase characteristics
|
||||||
- **Suitable for real-time applications** like guitar amp modeling
|
- **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
|
||||||
|
|
||||||
### Output Format Options
|
### Output Format Options
|
||||||
- **Sample Rates:** 44.1kHz (CD), 48kHz (studio), 88.2kHz, 96kHz (high-res)
|
- **Sample Rates:** 44.1kHz (CD), 48kHz (studio), 88.2kHz, 96kHz (high-res)
|
||||||
- **Bit Depths:** 16-bit (CD), 24-bit (studio), 32-bit (high-res)
|
- **Bit Depths:** 16-bit (CD), 24-bit (studio), 32-bit (high-res)
|
||||||
@@ -218,19 +288,35 @@ The pipeline is defined in [`ci/dagger.go`](./ci/dagger.go). It outputs binaries
|
|||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
1. Install the Dagger Go SDK:
|
1. Install the Dagger Go SDK and dependencies:
|
||||||
```sh
|
```sh
|
||||||
go install dagger.io/dagger@latest
|
go install dagger.io/dagger@latest
|
||||||
|
go get github.com/joho/godotenv
|
||||||
go mod tidy
|
go mod tidy
|
||||||
```
|
```
|
||||||
2. Run the pipeline:
|
2. Build for all platforms:
|
||||||
```sh
|
```sh
|
||||||
go run ci/dagger.go
|
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
|
### 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`.
|
- 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.
|
- The pipeline requires Docker or a compatible container runtime.
|
||||||
|
- For Gitea upload, ensure your `.env` file is present and correct.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
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.
|
// 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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"dagger.io/dagger"
|
"dagger.io/dagger"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
releaseTag := flag.String("release", "", "(optional) Release tag to upload binaries to Gitea")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
|
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -30,6 +49,8 @@ func main() {
|
|||||||
|
|
||||||
src := client.Host().Directory(".")
|
src := client.Host().Directory(".")
|
||||||
|
|
||||||
|
var builtBinaries []string
|
||||||
|
|
||||||
for _, p := range platforms {
|
for _, p := range platforms {
|
||||||
binName := fmt.Sprintf("valhallir-deconvolver-%s-%s", p.OS, p.Arch)
|
binName := fmt.Sprintf("valhallir-deconvolver-%s-%s", p.OS, p.Arch)
|
||||||
if p.OS == "windows" {
|
if p.OS == "windows" {
|
||||||
@@ -50,5 +71,146 @@ func main() {
|
|||||||
panic(fmt.Sprintf("export failed for %s/%s: %v", p.OS, p.Arch, err))
|
panic(fmt.Sprintf("export failed for %s/%s: %v", p.OS, p.Arch, err))
|
||||||
}
|
}
|
||||||
fmt.Printf("Built and exported %s\n", outPath)
|
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
|
module valhallir-deconvolver
|
||||||
|
|
||||||
go 1.24.1
|
go 1.24.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dagger.io/dagger v0.18.12
|
dagger.io/dagger v0.18.12
|
||||||
github.com/go-audio/audio v1.0.0
|
github.com/go-audio/audio v1.0.0
|
||||||
github.com/go-audio/wav v1.1.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/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12
|
||||||
github.com/urfave/cli/v2 v2.27.7
|
github.com/urfave/cli/v2 v2.27.7
|
||||||
gonum.org/v1/gonum v0.13.0
|
gonum.org/v1/gonum v0.13.0
|
||||||
|
gonum.org/v1/plot v0.10.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
git.sr.ht/~sbinet/gg v0.3.1 // indirect
|
||||||
github.com/99designs/gqlgen v0.17.75 // indirect
|
github.com/99designs/gqlgen v0.17.75 // indirect
|
||||||
github.com/Khan/genqlient v0.8.1 // indirect
|
github.com/Khan/genqlient v0.8.1 // indirect
|
||||||
github.com/adrg/xdg v0.5.3 // 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/v4 v4.3.0 // indirect
|
||||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||||
github.com/go-audio/riff v1.0.0 // 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/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.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/google/uuid v1.6.0 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // 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/sdk/metric v1.36.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.6.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/net v0.41.0 // indirect
|
||||||
golang.org/x/sync v0.15.0 // indirect
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
golang.org/x/sys v0.33.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 h1:s7v8aHlzDUogZ/jW92lHC+gljCNRML+0mosfh13R4vs=
|
||||||
dagger.io/dagger v0.18.12/go.mod h1:azlZ24m2br95t0jQHUBpL5SiafeqtVDLl1Itlq6GO+4=
|
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 h1:GwHJsptXWLHeY7JO8b7YueUI4w9Pom6wJTICosDtQuI=
|
||||||
github.com/99designs/gqlgen v0.17.75/go.mod h1:p7gbTpdnHyl70hmSpM8XG8GiKwmCv+T5zkdY8U8bLog=
|
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 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
|
||||||
github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU=
|
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 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
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 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
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 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
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 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
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 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4=
|
||||||
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
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 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA=
|
||||||
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
|
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 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g=
|
||||||
github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE=
|
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.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 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
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 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
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 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
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/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 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
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 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
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 h1:dd7vnTDfjtwCETZDrRe+GPYNLA1jBtbZeyfyE8eZCyk=
|
||||||
github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12/go.mod h1:i/KKcxEWEO8Yyl11DYafRPKOPVYTrhxiTRigjtEEXZU=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
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 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
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 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
||||||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
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/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 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
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 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
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.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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
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 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
|
||||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
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 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
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 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
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 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
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 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
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 h1:a0T3bh+7fhRyqeNbiC3qVHYmkiQgit3wnNan/2c0HMM=
|
||||||
gonum.org/v1/gonum v0.13.0/go.mod h1:/WPYRckkfWrhWefxyYTfrTtQR0KH4iyHNuzxqXAKyAU=
|
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 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
|
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=
|
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=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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=
|
||||||
|
|||||||
69
main.go
69
main.go
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"valhallir-deconvolver/pkg/convolve"
|
"valhallir-deconvolver/pkg/convolve"
|
||||||
|
"valhallir-deconvolver/pkg/plot"
|
||||||
"valhallir-deconvolver/pkg/wav"
|
"valhallir-deconvolver/pkg/wav"
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
@@ -15,7 +16,7 @@ func main() {
|
|||||||
app := &cli.App{
|
app := &cli.App{
|
||||||
Name: "valhallir-deconvolver",
|
Name: "valhallir-deconvolver",
|
||||||
Usage: "Deconvolve sweep and recorded WAV files to create impulse responses",
|
Usage: "Deconvolve sweep and recorded WAV files to create impulse responses",
|
||||||
Version: "v1.0.0",
|
Version: "v1.1.0",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "sweep",
|
Name: "sweep",
|
||||||
@@ -60,6 +61,28 @@ func main() {
|
|||||||
Name: "length-ms",
|
Name: "length-ms",
|
||||||
Usage: "Optional: Output IR length in milliseconds (will trim or zero-pad as needed)",
|
Usage: "Optional: Output IR length in milliseconds (will trim or zero-pad as needed)",
|
||||||
},
|
},
|
||||||
|
&cli.Float64Flag{
|
||||||
|
Name: "fade-ms",
|
||||||
|
Usage: "Fade-out duration in milliseconds to apply at the end of the IR (default 5)",
|
||||||
|
Value: 5.0,
|
||||||
|
},
|
||||||
|
&cli.Float64Flag{
|
||||||
|
Name: "highcut",
|
||||||
|
Usage: "High-cut filter (low-pass) cutoff frequency in Hz (applied to recorded sweep, optional)",
|
||||||
|
},
|
||||||
|
&cli.Float64Flag{
|
||||||
|
Name: "lowcut",
|
||||||
|
Usage: "Low-cut filter (high-pass) cutoff frequency in Hz (applied to recorded sweep, optional)",
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "cut-slope",
|
||||||
|
Usage: "Cut filter slope in dB/octave (12, 24, 36, 48, ...; default 12)",
|
||||||
|
Value: 12,
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "plot-ir",
|
||||||
|
Usage: "Plot the generated regular IR waveform to ir_plot.png",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
// Read sweep WAV file
|
// Read sweep WAV file
|
||||||
@@ -77,8 +100,26 @@ func main() {
|
|||||||
log.Printf("Sweep: %d samples, %d channels", len(sweepData.PCMData), sweepData.Channels)
|
log.Printf("Sweep: %d samples, %d channels", len(sweepData.PCMData), sweepData.Channels)
|
||||||
log.Printf("Recorded: %d samples, %d channels", len(recordedData.PCMData), recordedData.Channels)
|
log.Printf("Recorded: %d samples, %d channels", len(recordedData.PCMData), recordedData.Channels)
|
||||||
|
|
||||||
|
// Optionally filter the recorded sweep
|
||||||
|
recordedFiltered := recordedData.PCMData
|
||||||
|
recSampleRate := recordedData.SampleRate
|
||||||
|
highcutHz := c.Float64("highcut")
|
||||||
|
lowcutHz := c.Float64("lowcut")
|
||||||
|
cutSlope := c.Int("cut-slope")
|
||||||
|
if cutSlope < 12 || cutSlope%12 != 0 {
|
||||||
|
return fmt.Errorf("cut-slope must be a positive multiple of 12 (got %d)", cutSlope)
|
||||||
|
}
|
||||||
|
if lowcutHz > 0 {
|
||||||
|
log.Printf("Applying low-cut (high-pass) filter to recorded sweep: %.2f Hz, slope: %d dB/oct", lowcutHz, cutSlope)
|
||||||
|
recordedFiltered = convolve.CascadeLowcut(recordedFiltered, recSampleRate, lowcutHz, cutSlope)
|
||||||
|
}
|
||||||
|
if highcutHz > 0 {
|
||||||
|
log.Printf("Applying high-cut (low-pass) filter to recorded sweep: %.2f Hz, slope: %d dB/oct", highcutHz, cutSlope)
|
||||||
|
recordedFiltered = convolve.CascadeHighcut(recordedFiltered, recSampleRate, highcutHz, cutSlope)
|
||||||
|
}
|
||||||
|
|
||||||
log.Println("Performing deconvolution...")
|
log.Println("Performing deconvolution...")
|
||||||
ir := convolve.Deconvolve(sweepData.PCMData, recordedData.PCMData)
|
ir := convolve.Deconvolve(sweepData.PCMData, recordedFiltered)
|
||||||
log.Printf("Deconvolution result: %d samples", len(ir))
|
log.Printf("Deconvolution result: %d samples", len(ir))
|
||||||
|
|
||||||
log.Println("Trimming silence...")
|
log.Println("Trimming silence...")
|
||||||
@@ -135,12 +176,30 @@ func main() {
|
|||||||
ir = convolve.TrimOrPad(ir, targetSamples)
|
ir = convolve.TrimOrPad(ir, targetSamples)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply fade-out
|
||||||
|
fadeMs := c.Float64("fade-ms")
|
||||||
|
fadeSamples := int(float64(targetSampleRate) * fadeMs / 1000.0)
|
||||||
|
if fadeSamples > 0 {
|
||||||
|
log.Printf("Applying linear fade-out: %d samples (%.2f ms)...", fadeSamples, fadeMs)
|
||||||
|
ir = convolve.FadeOutLinear(ir, fadeSamples)
|
||||||
|
}
|
||||||
|
|
||||||
// Write regular IR
|
// Write regular IR
|
||||||
log.Printf("Writing IR to: %s (%dHz, %d-bit WAV)", c.String("output"), sampleRate, bitDepth)
|
log.Printf("Writing IR to: %s (%dHz, %d-bit WAV)", c.String("output"), sampleRate, bitDepth)
|
||||||
if err := wav.WriteWAVFileWithOptions(c.String("output"), ir, sampleRate, bitDepth); err != nil {
|
if err := wav.WriteWAVFileWithOptions(c.String("output"), ir, sampleRate, bitDepth); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plot IR waveform if requested
|
||||||
|
if c.Bool("plot-ir") {
|
||||||
|
log.Printf("Plotting IR waveform to ir_plot.png...")
|
||||||
|
err := plot.PlotIR(ir, sampleRate, c.String("output"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to plot IR: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("IR plot saved as ir_plot.png")
|
||||||
|
}
|
||||||
|
|
||||||
// Generate MPT IR if requested
|
// Generate MPT IR if requested
|
||||||
if c.Bool("mpt") {
|
if c.Bool("mpt") {
|
||||||
log.Println("Generating minimum phase transform...")
|
log.Println("Generating minimum phase transform...")
|
||||||
@@ -165,6 +224,12 @@ func main() {
|
|||||||
mptIR = convolve.TrimOrPad(mptIR, targetSamples)
|
mptIR = convolve.TrimOrPad(mptIR, targetSamples)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply fade-out to MPT IR
|
||||||
|
if fadeSamples > 0 {
|
||||||
|
log.Printf("Applying linear fade-out to MPT IR: %d samples (%.2f ms)...", fadeSamples, fadeMs)
|
||||||
|
mptIR = convolve.FadeOutLinear(mptIR, fadeSamples)
|
||||||
|
}
|
||||||
|
|
||||||
// Generate MPT output filename
|
// Generate MPT output filename
|
||||||
outputPath := c.String("output")
|
outputPath := c.String("output")
|
||||||
if len(outputPath) > 4 && outputPath[len(outputPath)-4:] == ".wav" {
|
if len(outputPath) > 4 && outputPath[len(outputPath)-4:] == ".wav" {
|
||||||
|
|||||||
@@ -322,3 +322,134 @@ func Resample(data []float64, fromSampleRate, toSampleRate int) []float64 {
|
|||||||
|
|
||||||
return result
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyLowpassButterworth applies a 2nd-order Butterworth low-pass filter to the data.
|
||||||
|
// cutoffHz: cutoff frequency in Hz, sampleRate: sample rate in Hz.
|
||||||
|
func ApplyLowpassButterworth(data []float64, sampleRate int, cutoffHz float64) []float64 {
|
||||||
|
if cutoffHz <= 0 || cutoffHz >= float64(sampleRate)/2 {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
// Biquad coefficients
|
||||||
|
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
|
||||||
|
b0 /= a0
|
||||||
|
b1 /= a0
|
||||||
|
b2 /= a0
|
||||||
|
a1 /= a0
|
||||||
|
a2 /= a0
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func ApplyHighpassButterworth(data []float64, sampleRate int, cutoffHz float64) []float64 {
|
||||||
|
if cutoffHz <= 0 || cutoffHz >= float64(sampleRate)/2 {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
// Biquad coefficients
|
||||||
|
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
|
||||||
|
b0 /= a0
|
||||||
|
b1 /= a0
|
||||||
|
b2 /= a0
|
||||||
|
a1 /= a0
|
||||||
|
a2 /= a0
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// slopeDb: 12, 24, 36, ... (dB/octave)
|
||||||
|
func CascadeLowcut(data []float64, sampleRate int, cutoffHz float64, slopeDb int) []float64 {
|
||||||
|
if slopeDb < 12 {
|
||||||
|
slopeDb = 12
|
||||||
|
}
|
||||||
|
n := slopeDb / 12
|
||||||
|
out := data
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
out = ApplyHighpassButterworth(out, sampleRate, cutoffHz)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// CascadeHighcut applies the high-cut (low-pass) filter multiple times for steeper slopes.
|
||||||
|
// slopeDb: 12, 24, 36, ... (dB/octave)
|
||||||
|
func CascadeHighcut(data []float64, sampleRate int, cutoffHz float64, slopeDb int) []float64 {
|
||||||
|
if slopeDb < 12 {
|
||||||
|
slopeDb = 12
|
||||||
|
}
|
||||||
|
n := slopeDb / 12
|
||||||
|
out := data
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
out = ApplyLowpassButterworth(out, sampleRate, cutoffHz)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
211
pkg/plot/plot.go
Normal file
211
pkg/plot/plot.go
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
package plot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/cmplx"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"image/png"
|
||||||
|
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"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
|
||||||
|
func PlotIR(ir []float64, sampleRate int, irFileName string) 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 (dB, 2048-sample window)"
|
||||||
|
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 (Time Aligned)"
|
||||||
|
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
|
||||||
|
logoPath := "assets/logo.png"
|
||||||
|
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
|
||||||
|
f, err := os.Open(logoPath)
|
||||||
|
if err == nil {
|
||||||
|
defer f.Close()
|
||||||
|
logoImg, err := png.Decode(f)
|
||||||
|
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")
|
||||||
|
|
||||||
|
f, err = os.Create(plotFileName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
_, err = vgimg.PngCanvas{Canvas: img}.WriteTo(f)
|
||||||
|
return err
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user