diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml deleted file mode 100644 index 394e7b4..0000000 --- a/.github/workflows/check.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Check and build -on: [push, pull_request, workflow_dispatch] -permissions: - contents: read -jobs: - core: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - run: cargo fmt --all --check - - run: cargo test --workspace --locked - - run: cargo clippy --workspace --all-targets --locked -- -D warnings - plugin: - strategy: - matrix: - os: [windows-latest, ubuntu-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - name: Linux GUI and audio headers - if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install -y libasound2-dev libjack-jackd2-dev libgl1-mesa-dev libx11-dev libxcursor-dev libx11-xcb-dev libxcb1-dev libxcb-dri2-0-dev libxcb-icccm4-dev libxkbcommon-x11-dev - - run: cargo fmt --manifest-path plugins/motif-storyboard/Cargo.toml --check - - run: cargo test --manifest-path plugins/motif-storyboard/Cargo.toml --locked - - name: Windows CLAP - if: runner.os == 'Windows' - run: python scripts/build-plugin.py - - name: Linux CLAP and standalone - if: runner.os == 'Linux' - run: python3 scripts/build-plugin.py --standalone - - uses: actions/upload-artifact@v4 - with: - name: motif-storyboard-${{ runner.os }} - path: dist/ diff --git a/.gitignore b/.gitignore index 74c1cb9..7a3c255 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ venv/ *~ .DS_Store Thumbs.db + +# GitLab job-local Cargo cache. +/.cargo-ci/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..301d899 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,80 @@ +# Linux Docker runner, Windows deliverables. No Linux plugin artifacts. +image: rust:1-bookworm + +stages: [check, build] + +workflow: + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + - if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS && $CI_PIPELINE_SOURCE == "push"' + when: never + - if: '$CI_COMMIT_BRANCH || $CI_COMMIT_TAG' + +variables: + CARGO_HOME: "$CI_PROJECT_DIR/.cargo-ci" + CARGO_TERM_COLOR: always + WINDOWS_TARGET: x86_64-pc-windows-gnu + +default: + interruptible: true + cache: + key: + files: + - Cargo.lock + - plugins/resonance-compressor/Cargo.lock + prefix: rust-windows-v1 + paths: + - .cargo-ci/registry/ + - .cargo-ci/git/ + +check: + stage: check + before_script: + - apt-get update + - apt-get install -y --no-install-recommends python3 pkg-config libgl1-mesa-dev libx11-dev libxcursor-dev libx11-xcb-dev libxcb1-dev libxcb-dri2-0-dev libxcb-icccm4-dev libxkbcommon-x11-dev + - rustup component add rustfmt clippy + script: + - cargo fmt --manifest-path tools/editor-check/Cargo.toml -- --check + - cargo fmt --all -- --check + - cargo test --workspace --locked + - cargo clippy --workspace --all-targets --locked -- -D warnings + - cargo fmt --manifest-path plugins/motif-storyboard/Cargo.toml -- --check + - cargo test --manifest-path plugins/motif-storyboard/Cargo.toml --locked + - cargo clippy --manifest-path plugins/motif-storyboard/Cargo.toml --all-targets --locked -- -D warnings + - cargo fmt --manifest-path plugins/resonance-compressor/Cargo.toml -- --check + - cargo test --manifest-path plugins/resonance-compressor/Cargo.toml --locked + - cargo clippy --manifest-path plugins/resonance-compressor/Cargo.toml --all-targets --locked -- -D warnings + - mkdir -p target/editor + - cargo run --manifest-path tools/editor-check/Cargo.toml --locked -- target/editor/resonance-default.png --resonance + - cargo run --manifest-path tools/editor-check/Cargo.toml --locked -- target/editor/resonance-compact.png --resonance --compact + artifacts: + name: "editor-previews-$CI_COMMIT_SHORT_SHA" + expire_in: 7 days + paths: + - target/editor/*.png + +windows: + stage: build + needs: + - job: check + artifacts: false + variables: + CARGO_TARGET_DIR: "$CI_PROJECT_DIR/target/windows" + before_script: + - apt-get update + - apt-get install -y --no-install-recommends python3 gcc-mingw-w64-x86-64 + - rustup target add "$WINDOWS_TARGET" + script: + - python3 scripts/build-plugin.py --plugin motif-storyboard --target "$WINDOWS_TARGET" + - python3 scripts/build-plugin.py --plugin resonance-compressor --target "$WINDOWS_TARGET" + - python3 scripts/package-windows.py + artifacts: + name: "fl-plugins-windows-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA" + expire_in: 30 days + paths: + - dist/fl-plugins-windows-x86_64.zip + - dist/x86_64-pc-windows-gnu/*.clap + - dist/x86_64-pc-windows-gnu/*setup.md + - dist/x86_64-pc-windows-gnu/LICENSE + - dist/x86_64-pc-windows-gnu/Roboto-OFL.txt + - dist/x86_64-pc-windows-gnu/SHA256SUMS diff --git a/Cargo.lock b/Cargo.lock index b5b000b..db6153d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,52 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + [[package]] name = "crossbeam-queue" version = "0.3.14" @@ -17,18 +63,164 @@ version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" +[[package]] +name = "ecolor" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1" +dependencies = [ + "emath", +] + +[[package]] +name = "egui" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" +dependencies = [ + "ahash", + "bitflags", + "emath", + "epaint", + "nohash-hasher", + "profiling", +] + +[[package]] +name = "emath" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" + +[[package]] +name = "epaint" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fcc0f5a7c613afd2dee5e4b30c3e6acafb8ad6f0edb06068811f708a67c562" +dependencies = [ + "ab_glyph", + "ahash", + "ecolor", + "emath", + "epaint_default_fonts", + "nohash-hasher", + "parking_lot", + "profiling", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7e7a64c02cf7a5b51e745a9e45f60660a286f151c238b9d397b3e923f5082f" + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "memchr" version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -38,6 +230,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + [[package]] name = "quote" version = "1.0.47" @@ -47,6 +245,60 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "resonance-core" +version = "0.1.0" +dependencies = [ + "rustfft", + "serde", + "serde_json", +] + +[[package]] +name = "resonance-render" +version = "0.1.0" +dependencies = [ + "resonance-core", + "serde_json", +] + +[[package]] +name = "resonance-ui" +version = "0.1.0" +dependencies = [ + "egui", + "resonance-core", +] + +[[package]] +name = "rustfft" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.229" @@ -74,7 +326,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.9", ] [[package]] @@ -90,6 +342,12 @@ dependencies = [ "zmij", ] +[[package]] +name = "smallvec" +version = "1.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" + [[package]] name = "story-app" version = "0.3.0" @@ -123,6 +381,23 @@ dependencies = [ "story-midi", ] +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "3.0.9" @@ -134,12 +409,34 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + [[package]] name = "unicode-ident" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "windows-link" version = "0.2.1" @@ -155,6 +452,26 @@ dependencies = [ "windows-link", ] +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zmij" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index 8c6207c..e3f9f4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "2" -members = ["crates/story-core", "crates/story-midi", "crates/story-preview", "crates/story-app"] -exclude = ["plugins/motif-storyboard", "tools/editor-check"] +members = ["crates/story-core", "crates/story-midi", "crates/story-preview", "crates/story-app", "crates/resonance-core", "crates/resonance-render", "crates/resonance-ui"] +exclude = ["plugins/motif-storyboard", "tools/editor-check", "plugins/resonance-compressor"] [workspace.package] version = "0.3.0" diff --git a/README.md b/README.md index c195a29..ef1f202 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,13 @@ -# Motif Storyboard +# FL Studio plugins + +Two Rust CLAP plugins: **Motif Storyboard**, a melodic composition companion, +and **[Resonance Compressor](docs/resonance-compressor.md)**, a mixing effect +with spectral resonance control and a full native engineer panel. + +GitLab CI builds a Windows x86-64 ZIP containing both plugins, setup guides, +licenses, and checksums. Download it from the **windows** job artifacts. + +## Motif Storyboard A Rust composition companion for an experienced producer developing their own melodic ideas. Version 0.3 explores rhythm, space, contour, harmonic arrivals, and @@ -119,8 +128,7 @@ cargo run -p story-preview -- session saved-session.json /tmp/rendered-session ``` Windows needs the Rust MSVC build tools; Linux needs X11/OpenGL development -headers, plus ALSA/JACK headers for standalone. The CI workflow lists Ubuntu -packages. A MinGW cross-build needs the `x86_64-pc-windows-gnu` Rust target and +headers, plus ALSA/JACK headers for standalone. The GitLab pipeline installs its build dependencies. A MinGW cross-build needs the `x86_64-pc-windows-gnu` Rust target and `x86_64-w64-mingw32-gcc`. ```sh @@ -137,8 +145,9 @@ install into a DAW. The visual QA harness compiles the shared editor against the same egui version and software-renders it without a display. Interactive window embedding, FL Studio MIDI routing, and audible artistic evaluation still need manual testing. The native Linux window cannot access X11 in this environment. -CI is configured for Windows CLAP and Linux CLAP/standalone, but has not been -triggered remotely from this workspace. +The root GitLab pipeline checks both plugins and cross-builds Windows CLAPs. +Linux is used for CI execution and local development, with no Linux release +artifact. A live GitLab run still needs verification on the configured runner. ## Design and references diff --git a/RESONANCE_COMPRESSOR.md b/RESONANCE_COMPRESSOR.md new file mode 100644 index 0000000..0bee4f6 --- /dev/null +++ b/RESONANCE_COMPRESSOR.md @@ -0,0 +1,554 @@ +# Resonance compressor: research and proposed design + +Research date: **2026-11-05**. This document preserves the research and design +proposal. The first implementation is now in the repository; see the +[implementation and setup guide](docs/resonance-compressor.md). + +## 1. What we want to build + +A Rust audio effect for FL Studio that reduces objectionable spectral peaks +while retaining the source's intended tone, movement, and punch. The conventional +name for this category is **dynamic resonance suppressor**. “Resonance compressor” +is a useful working name: gain reduction varies with both frequency and time. + +The main application here is aggressive, harmonically rich riddim sound design, +with melodic parts and full mixes as additional evaluation material. A useful +result is a bass that keeps its vowel, metallic edge, and rhythm while a painful +ring becomes less distracting. Making every spectrum smooth is not the goal. + +The recommendation is an independently designed **STFT spectral attenuation +engine**, first delivered as a separate CLAP effect. Start with explicit controls, +conservative reduction, linked stereo, delta monitoring, and correct host latency. +Add transient protection after validating the basic detector. Defer automatic +harmonic protection, multiple resolution engines, and zero-latency processing. + +Throughout this document: + +- **Documented behavior** means a manufacturer, paper, or source code describes it. +- **Proposed design** means our engineering choice, not a disclosed commercial + algorithm or a claim of equivalent sound quality. +- **Listening hypothesis** means something to test with producers. + +No commercial binaries were reverse engineered or benchmarked. Product claims +below were researched from public primary sources; they are not listening results. + +## 2. What the reference tools actually offer + +### Quance — CodWaves + +This is the product referred to as “quance.” At the research date its official +page lists **€49.99 launch pricing**, a crossed-out **€79.99** price, and a sale +ending November 30, 2026. These are temporary advertised prices, not a guaranteed +future checkout price. Formats listed are VST3, AU, and AAX for Windows/macOS. + +Its useful workflow references include editable tame/protect regions, learn, +tone/transient protection, delta monitoring, and multiple processing grades. +The developer describes both multi-resolution studio processing and a separate +causal filter-bank audio path for zero added sample latency. +[Official product page](https://codwaves.com/quance/). + +The manual describes a detector based on neighboring spectral content and an +adaptive longer-term reference. Reduction uses a soft knee and temporal +smoothing. Its zero-latency audio path still uses an STFT for detection: detection +therefore reacts late even though audio is not delayed. This distinction is +important for our design. The manual also documents stereo linking, dry alignment, +and an optional reduction ceiling. These descriptions do not disclose all tuning +or implementation details. +[Quance manual, especially §§9 and 13](https://codwaves.com/assets/Quance-User-Manual.pdf). + +### Soothe2 — oeksound + +Soothe2 documents adaptive resonance reduction with frequency-dependent attack +and release. Sharpness controls the shape of cuts; selectivity controls which +resonances qualify. A frequency curve steers sensitivity rather than applying +ordinary static EQ. Delta, external sidechain detection, stereo options, and +quality controls support contextual audition. The manual warns that extreme +settings can damage the sound. + +Design lesson: keep **where to work**, **what qualifies**, **how deeply to cut**, +and **how quickly to react** understandable as separate decisions. This is an +interpretation of the workflow, not a reconstruction of Soothe2's DSP. +[Official Soothe2 manual](https://oeksound.com/manuals/soothe2/). + +### Soothe3 — the current oeksound reference + +Research should include Soothe3 rather than treating Soothe2 as the newest +version. Its manual describes a level-adaptive soft mode and an absolute-level +hard mode, a Detail control, maximum-cut limiting, and minimum-phase processing +by default with a linear-phase option. It also documents frequency-dependent +timing adjustments. These are useful references for control design and phase +tradeoffs; our first STFT processor would not reproduce these modes. +[Official Soothe3 manual](https://oeksound.com/manuals/soothe3/). + +Oeksound's product listing currently advertises **$259** for Soothe3. Price alone +does not establish comparative value or sound quality. +[Official product listing](https://oeksound.com/plugins/). + +### Useful lower-cost comparison points + +| Tool | Why compare it | What the comparison does not establish | +| --- | --- | --- | +| TDR Nova | Free parallel dynamic EQ with four dynamic bands: a practical manual corrective baseline | It is not the same workflow as automatic dense resonance suppression | +| NIH-plug Spectral Compressor | Public Rust example of FFT-domain dynamics and sidechain spectral processing | Many-band compression is not automatically a resonance detector | +| Static parametric EQ | Tests whether a simple fixed cut solves the problem adequately | A changing resonance may need different treatment over time | + +Sources: [Tokyo Dawn's Nova page](https://www.tokyodawn.net/tdr-nova/), +[Spectral Compressor's author README](https://github.com/robbert-vdh/nih-plug/blob/master/plugins/spectral_compressor/README.md). + +We should compare output against bypass and a sensible manual correction before +claiming an advantage over either commercial reference. + +## 3. The perceptual problem + +### A peak is observable; an unwanted peak is a judgment + +A spectral peak can be a fundamental, a formant, an instrument resonance, a room +mode, deliberate filter emphasis, or an analysis artifact. Height alone cannot +classify it as a defect. A stable peak may be the exact feature that makes a bass +recognizable. Noise-like sibilance can be objectionable without a narrow peak. + +For this project, use **excess relative to a local spectral reference** as an +operational detection rule, and expose decisions to the producer. It is not a +physical measurement of the source's resonator and not an emotion detector. + +### Frequency neighborhoods should reflect listening, not just FFT indices + +Human auditory filtering provides a reason to explore perceptual frequency +coordinates. Glasberg and Moore's research derives auditory-filter shapes from +notched-noise experiments; its scope is hearing, not a universal rule for desirable +EQ. An ERB-spaced or logarithmic neighborhood is a useful engineering hypothesis. +It should not be marketed as a complete model of perception. +[Glasberg and Moore, 1990](https://pubmed.ncbi.nlm.nih.gov/2228789/). + +### Relevant research on automatic resonance equalization + +Grachten, Deruty, and Tanguy describe a spectral resonance attenuator separately +from a neural network that predicts attenuation strength. Their detector compares +a smoothed spectral reference with a perceptually weighted spectrum on a +log-frequency representation. Their listening experiment involved 15 professional +participants and 150 excerpts, with substantial variation in preferred treatment. + +This supports trying a transparent, non-neural detector first and evaluating +preferred strength through listening. It does not demonstrate equivalence to +Soothe, success on riddim stems, or that their long analysis windows suit live +monitoring. Use the explicitly linked **2018 v1** when reproducing this account; +the unversioned arXiv record has subsequently changed. +[Paper, v1, §§2–3](https://arxiv.org/pdf/1807.08636v1). + +Corbach and colleagues study automated room-resonance suppression using measured +room behavior and parametric filtering. It is relevant to targeted filters, but +room-response identification is a different problem from deciding which peaks +to remove from an arbitrary musical recording. +[DAFx-09 paper](https://dafx.de/paper-archive/2009/papers/paper_28.pdf). + +### Consequences for riddim and melodic bass + +The following are project-specific listening hypotheses, not findings established +by the cited papers: + +- A resonant vowel sweep may be the musical gesture. Protect its body and reduce + only a distracting upper whistle instead of flattening the entire sweep. +- Distortion and multiband dynamics can change which partial dominates from note + to note. Evaluate after the intended processing chain as well as before it. +- A repeated metallic resonance can function as a hook. Persistence alone is + insufficient evidence that it should be removed. +- A snare's initial crack and its later ring need different treatment. Preserving + the attack while controlling the tail may retain impact. +- Cleaning a soloed bass can reduce its ability to cut through the mix. Make final + decisions against the drums and melodic context, at comparable listening level. + +The Devath reference work in this repository measured broad signal changes. It +did not establish which resonances were intentional or identify an ideal correction. +Use original stems and controlled synthetic examples for development; full mastered +references cannot isolate cause or provide a clean target. + +## 4. Architecture choices + +| Approach | Advantages | Main difficulties | Recommendation | +| --- | --- | --- | --- | +| STFT → gain surface → inverse STFT | Dense frequency control; straightforward delta and analysis; testable reconstruction | Window latency, time/frequency tradeoff, ringing and modulation artifacts | First implementation | +| Detector → a few tracked dynamic notches | Economical audio path; can target persistent rings | Peak tracking, filter stability, crossing peaks, phase movement | Later experiment | +| Dense causal filter bank | Audio path can have no added buffering delay | Finite band selectivity, detector lag, interacting filters, moving phase | Separate live-mode project | +| Multi-resolution spectral processing | Longer low-frequency observation and shorter high-frequency observation | Crossovers, alignment, phase, extra CPU and more validation | After single-resolution baseline | +| Learned detector or parameter estimator | Could model preferences or source categories | Dataset, labels, domain shift, inference cost, explainability | Defer | + +These are engineering tradeoffs. Zero audio delay does not imply instantaneous +detection. Minimum phase does not imply no phase change. Retaining FFT phase does +not make a time-varying processor equivalent to one fixed linear-phase filter. + +## 5. Proposed first DSP pipeline + +```text +main audio ── analysis window / FFT ──────────────── complex-bin gains + │ │ + └─ detector spectrum inverse FFT + local reference synthesis window + prominence overlap-add + sensitivity mask │ + knee + strength + cap wet audio + frequency/time smoothing │ + └───────────────────────────────┘ +main audio ── matching delay ── dry ── mix / delta / bypass ── output trim +``` + +This is our proposed baseline, not either manufacturer's internal block diagram. +An external sidechain can later replace the detector input without replacing the +main audio being filtered. Spectral ducking needs its own detection semantics; +simply feeding a resonance-only detector with a sidechain will not duck all its +musically relevant energy. + +### 5.1 Reconstruction before suppression + +Implement weighted overlap-add (WOLA), then demonstrate a unity-gain null against +the delayed input before adding detection. With analysis window `wa`, synthesis +window `ws`, and hop `H`, reconstruction requires a constant window-product sum: + +```text +sum_m wa[n - mH] * ws[n - mH] = C +``` + +Normalize by `C`, or use a corresponding reconstruction-normalization buffer. +Use a compatible periodic window/hop pair and explicitly test stream startup, +tail flushing, and arbitrary host block boundaries. The analysis window alone +is not sufficient when a synthesis window is also applied. +[Julius O. Smith, Weighted Overlap Add](https://ccrma.stanford.edu/~jos/sasp/Weighted_Overlap_Add.html). + +For the prototype, try a periodic square-root Hann pair with `H = N/4` and +the appropriate product-sum normalization. Use the FFT library's required +round-trip normalization separately. RealFFT does not normalize either transform; +a forward/inverse pair needs a total `1/N` factor. +[RealFFT scaling documentation](https://docs.rs/realfft/latest/realfft/#scaling). + +### 5.2 Detector and local reference + +The following equations define an original experimental detector. They are +starting points to measure and tune, not validated optimal settings. + +For channel `c`, frame `t`, bin `k`: + +```text +P[c,t,k] = calibrated_power(|X[c,t,k]|²) +L[c,t,k] = 10 * log10(max(P[c,t,k], epsilon)) +B[c,t,k] = robust_local_reference(L[c,t,*], frequency[k], neighborhood_width) +E[c,t,k] = max(0, L[c,t,k] - B[c,t,k]) +``` + +`calibrated_power` must define its units. Correct for FFT size, window gain, and +one-sided spectrum conventions before introducing any absolute-level threshold. +Do not interchange per-bin power, band energy, and power spectral density. + +Start with a log-frequency neighborhood and a bounded weighted mean baseline; +compare a median/percentile baseline offline. Excluding a small central region +can keep a strong peak from raising its own reference too much, but can also make +leakage look like multiple resonances. Define edge behavior near DC/Nyquist. +Precompute interpolation maps and neighbor lists outside the audio callback. + +Adding a constant gain to the signal shifts both `L` and `B`; their difference is +approximately level-invariant above the numerical floor. That is useful for a +relative mode. An absolute gate, saturation, and noise-floor handling break this +invariance and must be documented and tested. + +A smooth spectrum can still sound harsh, while harmonics can have large `E`. +Keep detection range and manual protection available from the first usable build. +Do not promise automatic separation of tone from unwanted resonance. + +### 5.3 Independent controls and reduction law + +Use these meanings consistently: + +- **Sensitivity:** local-prominence threshold `T` in dB. +- **Amount:** slope `a` of attenuation above that threshold. +- **Max cut:** explicit upper bound `Dmax` on the applied attenuation surface. +- **Detail:** neighborhood/reduction width, with the actual mapping documented. +- **Focus/protect curve:** steers sensitivity or reduction; label which it does. + +A simple continuous knee function for positive excess `u = E - T`, width `W > 0`: + +```text +K(u,W) = 0 if u <= -W/2 + (u + W/2)² / (2W) if -W/2 < u < W/2 + u if u >= W/2 + +Dtarget = clamp(a * K(E - T,W), 0, Dmax) +``` + +For `W = 0`, use `max(0,u)` explicitly. This is a knee on **relative prominence**, +not the complete static transfer curve of an ordinary broadband compressor. + +Apply smooth frequency limits and protection weights. Smooth reduction across +frequency to avoid isolated, rapidly appearing holes. Reapply the mask and cap +after operations that could spread or overshoot reduction into excluded regions. +Transition mask edits smoothly rather than abruptly changing gains. + +### 5.4 Temporal behavior + +Smooth attenuation in dB at the frame rate, with separate attack and release: + +```text +alpha = exp(-H / (sample_rate * tau_seconds)) +D[t,k] = alpha * D[t-1,k] + (1-alpha) * Dtarget[t,k] +``` + +Choose attack when the target attenuation increases and release when it decreases. +Handle zero time explicitly. The time constants describe this smoother, not total +response latency: windows, hop, and detector averaging also contribute. + +Compare frequency-scaled timing against uniform timing; do not assume one wins. +Very fast changing narrow cuts can produce audible movement and sidebands. +Very slow cuts can miss a short whistle or remain after it disappears. Inspect +step response and note transitions as well as steady-state attenuation. + +### 5.5 Apply the gain surface + +```text +G[t,k] = 10^(-D[t,k]/20) +Y[t,k] = G[t,k] * X[t,k] +``` + +Use amplitude gains on complex bins, not power ratios directly. Preserve the +real-spectrum constraints at DC and Nyquist and keep reconstruction scaling +consistent. Initially retain each bin's phase; evaluate ringing and transient +smear honestly. This is not a claimed minimum-phase implementation. + +`G <= 1` is a spectral attenuation constraint. It does **not** guarantee that +every output sample peak is lower than the input peak: changing frequency +components can change their time-domain cancellation. This is not a limiter. + +### 5.6 Stereo and monitoring + +Default to a shared attenuation surface for left and right. One proposed rule is +to compute both channels' target reductions and use their per-bin maximum before +smoothing. This avoids oppositely panned tones canceling in a summed detector. +Identical gains preserve per-bin L/R relationships, though tonal balance changes. + +Add dual-mono and mid/side only after linked stereo passes image and mono checks. +Use an explicit reversible M/S normalization and test it with no attenuation. + +For monitoring, define the signal taps precisely: + +```text +delta = delayed_dry - wet_before_output_trim +mixed = (1-mix) * delayed_dry + mix * wet +``` + +Use a linear dry/wet law so identical dry/wet signals remain unity at all mix +positions. Bypass keeps the same delay and crossfades to dry. Delta bypasses makeup +gain and loudness matching so the subtraction remains interpretable. It reveals +changed material, including phase/time changes; it is not proof that everything +heard there was unwanted. Restore normal monitoring when loading a preset. + +## 6. Resolution, latency, and phase + +For a length-`N` transform, nominal bin spacing is `sample_rate/N`. This is not +the full practical ability to distinguish neighboring tones: the window matters. +Zero-padding adds interpolated spectral samples, not more observation time. + +Calculated examples at **48 kHz**, with `H=N/4`: + +| FFT length | Observation length | Bin spacing | Hop duration | +| --- | --- | --- | --- | +| 1024 | 21.33 ms | 46.875 Hz | 5.33 ms | +| 2048 | 42.67 ms | 23.438 Hz | 10.67 ms | +| 4096 | 85.33 ms | 11.719 Hz | 21.33 ms | +| 8192 | 170.67 ms | 5.859 Hz | 42.67 ms | + +These are calculations, not measured plugin latencies. A causal implementation's +actual buffering schedule determines its reported delay. In the repository's +pinned NIH-plug revision, `StftHelper::latency_samples()` returns its active +window length. With that helper, `N=2048` reports 2048 samples, not `N/2` or `H`. +Report the actual helper delay to the host and verify it with an impulse. +[Pinned STFT helper source](https://github.com/robbert-vdh/nih-plug/blob/de421011f41a6d10fc8c7a6084e4f4dee0143683/src/util/stft.rs). + +Start with one fixed configuration: `N=2048`, `H=512` at 44.1/48 kHz. Consider +doubling both at 88.2/96 kHz to retain similar time/frequency behavior. This choice +is primarily for mid/high resonance work; it is not precise sub-bass surgery. +Increasing FFT length while retaining 75% overlap also increases time between +updates. Greater overlap can improve update rate at extra CPU cost. + +Do not advertise zero latency for this first engine. Keep quality changes out +of ordinary automation initially. Later mode switches need state transitions, +host latency updates, and aligned crossfades or a controlled processing restart. +Offline rendering should initially use the same DSP configuration as playback. + +Linear-phase-style spectral shaping can ring before transients. Minimum-phase +shaping changes phase and can complicate parallel blending. Neither removes the +time/frequency tradeoff. A future minimum-phase option needs its own validated +filter construction, not just a label on the existing gain surface. + +## 7. Protection features worth researching next + +| Feature | Candidate approach | Failure to evaluate | +| --- | --- | --- | +| Transient protection | Positive spectral flux or fast/slow energy comparison reduces suppression briefly; separate onset and tail | Missing a short painful peak, or ducking protection too often on distorted bass | +| Harmonic protection | Confidence-weighted fundamental/harmonic evidence relaxes reduction | Polyphonic, inharmonic, noisy, or rapidly changing tones; a harmful ring may itself be pitched | +| Learn | Aggregate prominence statistics over a representative passage and propose editable focus points | Learning the hook as a defect or treating one passage as the entire song | +| Fixed protect regions | User draws a smooth exclusion around the bass body or snare | Protecting a problem along with the desired tone | +| External spectral ducking | Derive a smooth masking/priority curve from a routed reference | Removing too much accompaniment, noisy sidechains, missing input routing | + +For a first protection feature, prefer an optional transient guard with a visible +effect and an off switch. Explicit protect regions are a more dependable initial +interface than promising perfect automatic harmonic preservation. + +If Learn is added, keep the result editable and undoable; show exactly which +parameters changed. It should suggest where to investigate rather than declare +that detected peaks are mistakes. + +## 8. Professional workflow + +1. Insert on the problem source or bus and loop the offending passage in context. +2. Restrict the processing range; protect the source's body or deliberate vowel. +3. Raise Amount until the problem is clearly affected, then back off. Use an + explicit modest Max cut as an initial experiment, not a universal preset. +4. Listen to delta briefly. If it carries the melody, vowel, or whole snare attack, + reassess what is being removed. This is a diagnostic, not an automatic verdict. +5. Adjust sensitivity/detail and timing. Check the quieter phrase and the next + note as well as the loudest part. +6. Compare bypass and processed sound at similar loudness in the whole mix. +7. Save A/B states and automation only when the choice improves the musical role. + +Useful starting tasks: upper-mid whistle on a growl, a moving metallic partial, +snare-tail ring, a bright melodic layer masking another part, and harsh buildup +on a bass bus. Evaluate these separately: one universal setting is unlikely. + +The first interface should expose Amount, Sensitivity, Detail, Max cut, Attack, +Release, frequency limits, linked stereo, Mix, output trim, Delta, and Bypass. +Show input spectrum and actual reduction as distinct traces, with a fixed +logarithmic frequency axis and readable reduction units. The UI should distinguish +frequency sensitivity from static EQ gain. + +Loudness matching can initially be a manual output-trim workflow. A later automatic +comparison feature should use bounded, slow compensation with silence handling; +avoid makeup gain that disguises excessive removal or boosts between phrases. + +## 9. Rust integration in this repository + +Suggested new components, without modifying the musical generator's DSP: + +```text +crates/resonance-core/ detector, gain law, smoothing, fixed DSP state +crates/resonance-render/ offline WAV processing and measurement harness +plugins/resonance-compressor/ independent NIH-plug CLAP effect + editor +``` + +Reuse the existing pinned NIH-plug wrapper strategy and packaging conventions, +but register a new plugin identifier and an **audio effect** layout with mono and +stereo input/output. Do not inherit the generator's instrument classification, +MIDI scheduling, or sound-generation path. External audio sidechain is a later +optional port with explicit host-routing tests. + +Candidate FFT implementation: `realfft` backed by `rustfft`. Plan transforms and +allocate all ring buffers, spectra, smoothing state, neighbor maps, and scratch +storage during initialization. RustFFT's convenient `process()` allocates scratch; +use the supplied-scratch methods in the processing path and query their required +sizes during setup. +[RustFFT API](https://docs.rs/rustfft/latest/rustfft/trait.Fft.html). + +Additional engineering requirements: + +- No heap allocation/deallocation, blocking locks, file I/O, or FFT planning in + the audio callback; prevent an overwritten shared buffer from freeing there. +- Bound detector work. A naive all-bin neighborhood scan is quadratic; use + precomputed bounded neighborhoods, running sums, or a coarser detector grid. +- Process arbitrary host buffer lengths through the ring-buffer scheduler. +- Smooth automatable parameters; keep latency-changing parameters nonautomatable + until the transition contract is implemented. +- Decimate analyzer data into preallocated UI buffers; rendering must not drive + processing or stall the callback. +- Reset histories and delay buffers consistently on reinitialization. Bound and + version state; default to normal monitoring, never unexpected delta playback. +- Sanitize invalid numerical state and handle silence/denormals without noise + bursts. Flush the delayed tail correctly in offline rendering. + +### Existing source-code reference and licensing distinction + +NIH-plug itself has an ISC license, but its **Spectral Compressor example is +GPL-3.0-or-later**, including in the locally cached revision used by this project. +The example's manifest and source header were checked. Treat it as a reference; +do not assume the framework license applies to every bundled plugin or copy that +implementation into an ISC-labeled plugin without resolving the differing terms. +[Framework license](https://github.com/robbert-vdh/nih-plug/blob/master/LICENSE), +[example manifest](https://github.com/robbert-vdh/nih-plug/blob/master/plugins/spectral_compressor/Cargo.toml). + +## 10. Validation before calling it useful + +### Deterministic DSP checks + +| Check | Required observation | +| --- | --- | +| Unity reconstruction | With attenuation disabled, null against delayed input within a declared floating-point tolerance, including startup/tail | +| Reported latency | Impulse delay agrees with host-reported samples at each supported sample rate | +| Dry/mix/bypass | Unity response at every Mix position when wet equals dry; crossfades and delta use aligned taps | +| Synthetic narrow ring | A gated tone over a shaped noise bed triggers localized reduction with predictable timing | +| Sweeping tone | No unstable hopping, clicks, or unexpectedly wide removal as a peak crosses bins | +| Harmonic stack | Fundamentals and intentional harmonics reveal the detector's false-positive behavior | +| Silence and fades | No gain bursts, NaNs, denormal CPU spikes, or stale reduction after silence | +| Gain scaling | Relative mode behaves similarly above its gate/floor after input-level changes | +| Stereo | In-phase, opposite-polarity, hard-panned, and decorrelated inputs retain expected linking behavior | +| Parameter motion | Extreme valid automation does not click, allocate, or exceed the defined attenuation cap | +| Block invariance | Equivalent stream output for fixed, tiny, irregular, and changing host buffer sizes | +| Real-time performance | Allocation counter stays zero during processing; worst-frame CPU tested with many instances | + +Measure the gain-surface bound separately from output sample/true-peak behavior. +Use swept and stepped resonances to measure attack, release, and detector lag; +steady-state plots alone are insufficient. Benchmark on named hardware with +stereo, sample rate, FFT size, overlap, host buffer size, and instance count stated. + +### Listening evaluation + +Use producer-owned dry stems, synthetic controls, and several source categories. +For each, compare bypass, a manual EQ/dynamic-EQ correction, our output, and +commercial references only when legitimately available. Randomize labels and +match listening level before judging. Include cases where bypass is preferred. + +Ask concrete questions: Is the whistle less distracting? Does the vowel still +move? Does the snare retain impact? Does the melodic layer retain its identity? +Can a producer achieve the result faster? Repeat on the complete arrangement. + +Record dulling, loss of body, pre-ringing, warbling, transient smear, image changes, +and the amount of useful adjustment. A visually flatter spectrum and a larger +reduction number are not success metrics. + +### FL Studio acceptance + +Verify discovery as an effect, insert routing, latency compensation, parallel +mixing, automation, save/reopen, sample-rate changes, render tails, and repeated +instance loading. Test mono/stereo channel layouts and multiple buffer sizes. +When sidechain support arrives, test absent/connected/changed routes explicitly. +Do not infer host compatibility solely from a successful Rust build. + +## 11. Implementation sequence and unresolved decisions + +1. **Reconstruction milestone:** offline identity processor, impulse/latency tests, + ring-buffer scheduling, and allocation checks. No suppression UI yet. +2. **Detector milestone:** relative prominence, explicit range/protection, knee, + separate Amount/Max cut, linked stereo, gain smoothing, and offline comparisons. +3. **Usable effect milestone:** CLAP audio wrapper, stable parameters and state, + analyzer/reduction display, delta, aligned bypass/mix, and FL Studio validation. +4. **Musical evaluation milestone:** transient guard, producer A/B sessions, tuned + defaults, CPU measurements, and a documented set of failures. +5. **Only then:** learn, harmonic evidence, sidechain ducking, additional quality + modes, minimum-phase filtering, or a separate low-latency engine. + +Open decisions require prototypes: the local baseline statistic and bandwidth, +detector grid, detail mapping, smoothing order, frequency-scaled timing, and the +acceptable CPU/latency envelope. No claim of matching Quance or Soothe is justified +until repeatable comparisons establish which tasks the new plugin handles well. + +The most valuable first outcome is a reliable, understandable suppressor that +helps the producer keep a sound's character while making a specific annoyance +easier to control. + + +## 12. First implementation + +Version 0.1 uses a complex RustFFT engine with preallocated scratch storage. +The initial mixing configuration is 4096 samples with a 512-sample hop at +44.1/48 kHz, scaling both for higher supported rates. This supersedes the +smaller experimental configuration proposed above. It implements linked stereo, +relative prominence, soft-knee bounded reduction, explicit focus/protect controls, +transient guard, aligned monitoring, and the full visible engineer panel. +The original research scope and unverified listening hypotheses still apply. + +The effect is independently implemented. The commercial references were not +reverse engineered. Tests establish reconstruction and scheduling behavior; +FL Studio acceptance and producer listening remain separate requirements. diff --git a/crates/resonance-core/Cargo.toml b/crates/resonance-core/Cargo.toml new file mode 100644 index 0000000..75b67c6 --- /dev/null +++ b/crates/resonance-core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "resonance-core" +version = "0.1.0" +edition = "2021" +license = "ISC" + +[dependencies] +rustfft = "6.4" +serde = { version = "1", features = ["derive"] } + +[dev-dependencies] +serde_json = "1" diff --git a/crates/resonance-core/src/lib.rs b/crates/resonance-core/src/lib.rs new file mode 100644 index 0000000..05585d3 --- /dev/null +++ b/crates/resonance-core/src/lib.rs @@ -0,0 +1,404 @@ +//! Independently designed spectral attenuation. No host or GUI dependencies. +mod settings; +use rustfft::{num_complex::Complex32, Fft, FftPlanner}; +pub use settings::{focus_offset, knee, Focus, Settings}; +use std::{f32::consts::PI, sync::Arc}; + +pub const GRID: usize = 512; +#[derive(Clone, Copy, Debug)] +pub struct AnalyzerFrame { + pub input: [f32; GRID], + pub reduction: [f32; GRID], + pub focus: [f32; GRID], + pub input_peak: f32, + pub output_peak: f32, + pub guard: f32, + pub sample_rate: f32, + pub latency: u32, + pub channels: usize, +} +impl Default for AnalyzerFrame { + fn default() -> Self { + Self { + input: [-120.; GRID], + reduction: [0.; GRID], + focus: [0.; GRID], + input_peak: 0., + output_peak: 0., + guard: 0., + sample_rate: 48000., + latency: 4096, + channels: 2, + } + } +} +pub fn frequency(index: usize) -> f32 { + 20. * 1000f32.powf(index as f32 / (GRID - 1) as f32) +} +#[derive(Clone, Copy, Default)] +pub struct Monitor { + pub delta: bool, +} + +pub struct Engine { + rate: f32, + channels: usize, + n: usize, + hop: usize, + pos: usize, + hop_pos: usize, + input: [Vec; 2], + output: [Vec; 2], + spectra: [Vec; 2], + window: Vec, + scratch: Vec, + forward: Arc>, + inverse: Arc>, + levels: [[f32; GRID]; 2], + target: [f32; GRID], + reduction: [f32; GRID], + curve: [f32; GRID], + mask: [f32; GRID], + map: [(usize, f32); GRID], + bin_map: Vec<(usize, f32)>, + kernel: [f32; 65], + radius: usize, + settings: Settings, + frame: AnalyzerFrame, + fresh: bool, + fast: f32, + slow: f32, + protect: f32, + sample_power: f32, + mix: f32, + gain: f32, + bypass: f32, + delta: f32, + mix_initialized: bool, + power_scale: f32, + in_peak: f32, + out_peak: f32, +} +impl Engine { + pub fn new(rate: f32, channels: usize) -> Result { + if ![44100., 48000., 88200., 96000., 176400., 192000.].contains(&rate) + || !(1..=2).contains(&channels) + { + return Err("Supported rates: 44.1/48/88.2/96/176.4/192 kHz; mono or stereo"); + } + let scale = if rate > 96000. { + 4 + } else if rate > 48000. { + 2 + } else { + 1 + }; + let n = 4096 * scale; + let hop = n / 8; + let mut planner = FftPlanner::new(); + let forward = planner.plan_fft_forward(n); + let inverse = planner.plan_fft_inverse(n); + let scratch_len = forward + .get_inplace_scratch_len() + .max(inverse.get_inplace_scratch_len()); + let window: Vec<_> = (0..n) + .map(|i| { + (0.5 - 0.5 * (2. * PI * i as f32 / n as f32).cos()) + .max(0.) + .sqrt() + }) + .collect(); + let power_scale = 2. / window.iter().sum::().powi(2); + let nyquist = rate * 0.5; + let map = std::array::from_fn(|i| { + let bin = (frequency(i).min(nyquist) * n as f32 / rate).min((n / 2 - 1) as f32); + (bin.floor() as usize, bin.fract()) + }); + let bin_map = (0..=n / 2) + .map(|k| { + let grid = ((k as f32 * rate / n as f32).max(20.) / 20.).log2() / 1000f32.log2() + * (GRID - 1) as f32; + let grid = grid.clamp(0., (GRID - 1) as f32); + ( + (grid.floor() as usize).min(GRID - 2), + if grid >= (GRID - 1) as f32 { + 1. + } else { + grid.fract() + }, + ) + }) + .collect(); + let mut engine = Self { + rate, + channels, + n, + hop, + pos: 0, + hop_pos: 0, + input: std::array::from_fn(|_| vec![0.; n]), + output: std::array::from_fn(|_| vec![0.; n]), + spectra: std::array::from_fn(|_| vec![Complex32::default(); n]), + window, + scratch: vec![Complex32::default(); scratch_len], + forward, + inverse, + levels: [[-120.; GRID]; 2], + target: [0.; GRID], + reduction: [0.; GRID], + curve: [0.; GRID], + mask: [0.; GRID], + map, + bin_map, + kernel: [0.; 65], + radius: 0, + settings: Settings::default(), + frame: AnalyzerFrame::default(), + fresh: false, + fast: 0., + slow: 0., + protect: 0., + sample_power: 0., + mix: 1., + gain: 1., + bypass: 0., + delta: 0., + mix_initialized: false, + power_scale, + in_peak: 0., + out_peak: 0., + }; + engine.rebuild(); + Ok(engine) + } + pub fn latency_samples(&self) -> u32 { + self.n as u32 + } + pub fn channels(&self) -> usize { + self.channels + } + pub fn settings(&self) -> Settings { + self.settings + } + pub fn set_settings(&mut self, settings: Settings) -> Result<(), &'static str> { + settings.validate()?; + if settings != self.settings { + let rebuild = settings.focus != self.settings.focus + || settings.low != self.settings.low + || settings.high != self.settings.high + || settings.cut_width != self.settings.cut_width; + self.settings = settings; + if rebuild { + self.rebuild(); + } + } + if !self.mix_initialized { + self.mix = settings.mix / 100.; + self.gain = 10f32.powf(settings.output / 20.); + self.bypass = f32::from(settings.bypass); + self.mix_initialized = true; + } + Ok(()) + } + fn rebuild(&mut self) { + let step = 1000f32.log2() / (GRID - 1) as f32; + self.radius = ((self.settings.cut_width / step * 1.3).ceil() as usize).clamp(1, 64); + let sigma = self.settings.cut_width / step / 2.355; + for j in 0..=self.radius { + self.kernel[j] = (-0.5 * (j as f32 / sigma).powi(2)).exp(); + } + for i in 0..GRID { + let f = frequency(i); + self.curve[i] = focus_offset(f, &self.settings.focus); + let low = (f / self.settings.low).log2() / 0.15; + let high = (self.settings.high.min(self.rate * 0.49) / f).log2() / 0.15; + let edge = low.min(high).clamp(0., 1.); + self.mask[i] = edge * edge * (3. - 2. * edge); + } + } + pub fn reset(&mut self) { + for channel in &mut self.input { + channel.fill(0.); + } + for channel in &mut self.output { + channel.fill(0.); + } + self.reduction.fill(0.); + self.pos = 0; + self.hop_pos = 0; + self.fast = 0.; + self.slow = 0.; + self.protect = 0.; + self.sample_power = 0.; + self.in_peak = 0.; + self.out_peak = 0.; + self.fresh = false; + self.frame = AnalyzerFrame { + sample_rate: self.rate, + latency: self.n as u32, + ..Default::default() + }; + self.mix_initialized = false; + self.delta = 0.; + } + pub fn take_analyzer(&mut self) -> Option { + if self.fresh { + self.fresh = false; + Some(self.frame) + } else { + None + } + } + /// Sample scheduling is independent of the caller's block partition. + pub fn process_sample(&mut self, sample: [f32; 2], monitor: Monitor) -> [f32; 2] { + let mut result = [0.; 2]; + let mut energy = 0f32; + let smooth = 1. - (-1. / (self.rate * 0.01)).exp(); + self.mix += (self.settings.mix / 100. - self.mix) * smooth; + self.gain += (10f32.powf(self.settings.output / 20.) - self.gain) * smooth; + self.bypass += (f32::from(self.settings.bypass) - self.bypass) * smooth; + self.delta += (f32::from(monitor.delta) - self.delta) * smooth; + for c in 0..self.channels { + let x = if sample[c].is_finite() { + sample[c].clamp(-64., 64.) + } else { + 0. + }; + let dry = self.input[c][self.pos]; + let wet = self.output[c][self.pos]; + self.output[c][self.pos] = 0.; + let x = if x.abs() < 1e-20 { 0. } else { x }; + self.input[c][self.pos] = x; + let mixed = (dry + (wet - dry) * self.mix) * self.gain; + let normal = mixed + (dry - mixed) * self.bypass; + let audible = normal + ((dry - wet) - normal) * self.delta; + result[c] = if audible.is_finite() { audible } else { 0. }; + self.in_peak = self.in_peak.max(x.abs()); + self.out_peak = self.out_peak.max(result[c].abs()); + energy = energy.max(x * x); + } + self.fast += (energy - self.fast) * (1. - (-1. / (self.rate * 0.001)).exp()); + self.slow += (energy - self.slow) * (1. - (-1. / (self.rate * 0.05)).exp()); + let onset = + ((10. * ((self.fast + 1e-20) / (self.slow + 1e-20)).log10() - 3.) / 9.).clamp(0., 1.); + self.protect = onset.max(self.protect * (-1. / (self.rate * 0.03)).exp()); + self.sample_power += energy; + self.pos = (self.pos + 1) % self.n; + self.hop_pos += 1; + if self.hop_pos == self.hop { + self.hop_pos = 0; + self.process_window(); + } + result + } + fn process_window(&mut self) { + for c in 0..self.channels { + for i in 0..self.n { + self.spectra[c][i] = + Complex32::new(self.input[c][(self.pos + i) % self.n] * self.window[i], 0.); + } + self.forward + .process_with_scratch(&mut self.spectra[c], &mut self.scratch); + for i in 0..GRID { + let (k, w) = self.map[i]; + let power = self.spectra[c][k].norm_sqr() * (1. - w) + + self.spectra[c][k + 1].norm_sqr() * w; + self.levels[c][i] = 10. * (power * self.power_scale).max(1e-20).log10(); + } + } + let awake = self.sample_power / self.hop as f32 > 1e-9; + self.sample_power = 0.; + let step = 1000f32.log2() / (GRID - 1) as f32; + let radius = (self.settings.neighborhood / step * 0.5).round() as usize; + self.target.fill(0.); + for c in 0..self.channels { + let mut prefix = [0.; GRID + 1]; + for i in 0..GRID { + prefix[i + 1] = prefix[i] + self.levels[c][i]; + } + for i in 0..GRID { + let low = i.saturating_sub(radius); + let high = (i + radius + 1).min(GRID); + let e0 = i.saturating_sub(1).max(low); + let e1 = (i + 2).min(high); + let count = (high - low) - (e1 - e0); + let baseline = + (prefix[high] - prefix[low] - (prefix[e1] - prefix[e0])) / count.max(1) as f32; + let excess = + self.levels[c][i] - baseline + self.curve[i] - self.settings.sensitivity; + let target = if awake { + knee(excess, self.settings.knee) * self.settings.amount * 0.02 + } else { + 0. + }; + self.target[i] = self.target[i].max(target.min(self.settings.max_cut)); + } + } + let attack = (-(self.hop as f32) / (self.rate * self.settings.attack * 0.001)).exp(); + let release = (-(self.hop as f32) / (self.rate * self.settings.release * 0.001)).exp(); + for i in 0..GRID { + let low = i.saturating_sub(self.radius); + let high = (i + self.radius + 1).min(GRID); + let mut sum = 0.; + let mut weight = 0.; + for j in low..high { + let w = self.kernel[i.abs_diff(j)]; + sum += self.target[j] * w; + weight += w; + } + let target = sum / weight; + let alpha = if target > self.reduction[i] { + attack + } else { + release + }; + self.reduction[i] = alpha * self.reduction[i] + (1. - alpha) * target; + if self.reduction[i] < 1e-12 { + self.reduction[i] = 0.; + } + self.frame.reduction[i] = (self.reduction[i] + * self.mask[i] + * (1. - self.protect * self.settings.transient_guard / 100.)) + .clamp(0., self.settings.max_cut); + self.frame.focus[i] = self.curve[i]; + self.frame.input[i] = self.levels[0][i].max(if self.channels == 2 { + self.levels[1][i] + } else { + -200. + }); + } + for c in 0..self.channels { + for k in 0..=self.n / 2 { + let (i, w) = self.bin_map[k]; + let d = self.frame.reduction[i] * (1. - w) + self.frame.reduction[i + 1] * w; + let f = k as f32 * self.rate / self.n as f32; + let d = if f >= self.settings.low && f <= self.settings.high.min(self.rate * 0.49) { + d + } else { + 0. + }; + let g = 10f32.powf(-d / 20.); + self.spectra[c][k] *= g; + if k > 0 && k < self.n / 2 { + self.spectra[c][self.n - k] *= g; + } + } + self.inverse + .process_with_scratch(&mut self.spectra[c], &mut self.scratch); + // Eight overlaps of a periodic Hann sum to four. + for i in 0..self.n { + self.output[c][(self.pos + i) % self.n] += + self.spectra[c][i].re * self.window[i] / (self.n as f32 * 4.); + } + } + self.frame.input_peak = self.in_peak; + self.frame.output_peak = self.out_peak; + self.frame.guard = self.protect; + self.frame.sample_rate = self.rate; + self.frame.latency = self.n as u32; + self.frame.channels = self.channels; + self.in_peak = 0.; + self.out_peak = 0.; + self.fresh = true; + } +} diff --git a/crates/resonance-core/src/settings.rs b/crates/resonance-core/src/settings.rs new file mode 100644 index 0000000..1eee50f --- /dev/null +++ b/crates/resonance-core/src/settings.rs @@ -0,0 +1,157 @@ +//! Stable, host-independent controls. Frequencies are Hz, times milliseconds. +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct Focus { + pub enabled: bool, + pub frequency: f32, + pub width: f32, + pub sensitivity: f32, +} +impl Default for Focus { + fn default() -> Self { + Self { + enabled: false, + frequency: 2000., + width: 0.5, + sensitivity: 0., + } + } +} +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct Settings { + pub amount: f32, + pub sensitivity: f32, + pub max_cut: f32, + pub neighborhood: f32, + pub cut_width: f32, + pub knee: f32, + pub attack: f32, + pub release: f32, + pub transient_guard: f32, + pub low: f32, + pub high: f32, + pub mix: f32, + pub output: f32, + pub bypass: bool, + pub focus: [Focus; 4], +} +impl Default for Settings { + fn default() -> Self { + Self { + amount: 35., + sensitivity: 6., + max_cut: 3., + neighborhood: 1., + cut_width: 0.08, + knee: 3., + attack: 15., + release: 120., + transient_guard: 50., + low: 120., + high: 12000., + mix: 100., + output: 0., + bypass: false, + focus: [Focus::default(); 4], + } + } +} +impl Settings { + pub fn validate(&self) -> Result<(), &'static str> { + for (v, low, high) in [ + (self.amount, 0., 100.), + (self.sensitivity, 0., 24.), + (self.max_cut, 0., 18.), + (self.neighborhood, 0.25, 2.), + (self.cut_width, 0.03, 0.5), + (self.knee, 0., 12.), + (self.attack, 1., 200.), + (self.release, 10., 1000.), + (self.transient_guard, 0., 100.), + (self.low, 20., 20000.), + (self.high, 20., 20000.), + (self.mix, 0., 100.), + (self.output, -12., 12.), + ] { + if !v.is_finite() || !(low..=high).contains(&v) { + return Err("Invalid resonance control value"); + } + } + if self.low >= self.high { + return Err("Low frequency must be below high frequency"); + } + for f in self.focus { + if !f.frequency.is_finite() + || !(20. ..=20000.).contains(&f.frequency) + || !f.width.is_finite() + || !(0.05..=2.).contains(&f.width) + || !f.sensitivity.is_finite() + || !(-18. ..=18.).contains(&f.sensitivity) + { + return Err("Invalid focus control"); + } + } + Ok(()) + } + pub const PRESETS: [&'static str; 4] = [ + "Gentle cleanup", + "Bass whistle", + "Snare tail", + "Bright melodic layer", + ]; + pub fn preset(index: usize) -> Self { + let mut s = Self::default(); + match index { + 1 => { + s.low = 700.; + s.high = 10000.; + s.sensitivity = 5.; + s.max_cut = 6.; + s.cut_width = 0.05; + s.attack = 8.; + } + 2 => { + s.low = 250.; + s.high = 6000.; + s.attack = 25.; + s.release = 180.; + s.transient_guard = 85.; + s.max_cut = 5.; + } + 3 => { + s.low = 1800.; + s.high = 15000.; + s.sensitivity = 4.; + s.max_cut = 4.; + s.cut_width = 0.14; + } + _ => {} + } + s + } +} +/// Soft knee on excess prominence, measured in dB. +pub fn knee(excess: f32, width: f32) -> f32 { + if width <= 0. { + excess.max(0.) + } else if excess <= -width * 0.5 { + 0. + } else if excess >= width * 0.5 { + excess + } else { + (excess + width * 0.5).powi(2) / (2. * width) + } +} +pub fn focus_offset(f: f32, focus: &[Focus; 4]) -> f32 { + focus + .iter() + .filter(|p| p.enabled) + .map(|p| { + let z = (f / p.frequency).log2() / p.width; + p.sensitivity * (-4. * std::f32::consts::LN_2 * z * z).exp() + }) + .sum() +} diff --git a/crates/resonance-core/tests/dsp.rs b/crates/resonance-core/tests/dsp.rs new file mode 100644 index 0000000..e8959c4 --- /dev/null +++ b/crates/resonance-core/tests/dsp.rs @@ -0,0 +1,207 @@ +use resonance_core::*; +use std::f32::consts::TAU; + +fn noise(i: usize) -> f32 { + let x = (i as u32).wrapping_mul(747796405).wrapping_add(2891336453); + let word = ((x >> ((x >> 28) + 4)) ^ x).wrapping_mul(277803737); + (((word >> 22) ^ word) as f32 / u32::MAX as f32 - 0.5) * 0.1 +} +#[test] +fn delayed_identity_and_all_mix_positions_at_all_rates() { + for rate in [44100., 48000., 88200., 96000., 176400., 192000.] { + for mix in [0., 37., 100.] { + let mut e = Engine::new(rate, 2).unwrap(); + e.set_settings(Settings { + amount: 0., + mix, + ..Default::default() + }) + .unwrap(); + let n = e.latency_samples() as usize; + let mut error = 0f64; + let mut power = 0f64; + for i in 0..n * 4 { + let x = if i < n * 2 { noise(i) } else { 0. }; + let y = e.process_sample([x, -x], Monitor::default()); + let expected = if i >= n && i < n * 3 { + noise(i - n) + } else { + 0. + }; + error += (y[0] as f64 - expected as f64).powi(2); + power += (expected as f64).powi(2); + assert!((y[0] + y[1]).abs() < 1e-6); + } + assert!( + 10. * (error / power).max(1e-30).log10() < -100., + "rate {rate}, mix {mix}: {error}" + ); + } + } +} +#[test] +fn impulse_latency_delta_and_reset() { + let mut e = Engine::new(48000., 1).unwrap(); + e.set_settings(Settings { + amount: 0., + ..Default::default() + }) + .unwrap(); + let latency = e.latency_samples() as usize; + for i in 0..latency * 2 { + let y = e.process_sample([if i == 0 { 1. } else { 0. }, 0.], Monitor::default())[0]; + assert!((y - if i == latency { 1. } else { 0. }).abs() < 1e-6); + } + e.reset(); + for _ in 0..latency * 2 { + assert_eq!(e.process_sample([0., 0.], Monitor { delta: true }), [0.; 2]); + } +} +#[test] +fn resonance_is_local_bounded_and_recovers_in_silence() { + let mut e = Engine::new(48000., 2).unwrap(); + e.set_settings(Settings { + amount: 100., + sensitivity: 2., + max_cut: 6., + transient_guard: 0., + attack: 1., + release: 20., + low: 500., + high: 9000., + ..Default::default() + }) + .unwrap(); + let mut frame = AnalyzerFrame::default(); + for i in 0..48000 { + let x = 0.3 * (TAU * 3000. * i as f32 / 48000.).sin() + noise(i); + e.process_sample([x, -x], Monitor::default()); + if let Some(f) = e.take_analyzer() { + frame = f; + } + } + let peak = frame.reduction.iter().copied().fold(0f32, f32::max); + assert!(peak > 3. && peak <= 6.); + for i in 0..GRID { + if frequency(i) < 500. || frequency(i) > 9000. { + assert_eq!(frame.reduction[i], 0.); + } + } + let ring = (0..GRID) + .min_by(|a, b| { + (frequency(*a) - 3000.) + .abs() + .total_cmp(&(frequency(*b) - 3000.).abs()) + }) + .unwrap(); + assert!(frame.reduction[ring] > 3.); + for _ in 0..48000 { + e.process_sample([0.; 2], Monitor::default()); + } + let f = e.take_analyzer().unwrap(); + assert!(f.reduction.iter().all(|d| *d < 0.001)); +} +#[test] +fn invalid_settings_do_not_mutate_and_numerical_inputs_stay_finite() { + let mut e = Engine::new(48000., 1).unwrap(); + let before = e.settings(); + assert!(e + .set_settings(Settings { + attack: f32::NAN, + ..before + }) + .is_err()); + assert_eq!(e.settings(), before); + assert!(Engine::new(8000., 2).is_err()); + for _ in 0..10000 { + let y = e.process_sample([f32::NAN, f32::INFINITY], Monitor::default()); + assert!(y.iter().all(|v| v.is_finite())); + } +} +#[test] +fn soft_knee_and_presets_are_valid() { + for i in 0..4 { + Settings::preset(i).validate().unwrap(); + } + assert_eq!(knee(-2., 3.), 0.); + assert_eq!(knee(5., 3.), 5.); + assert_eq!(knee(-1., 0.), 0.); + let p = Focus { + enabled: true, + frequency: 2000., + width: 1., + sensitivity: -12., + }; + assert_eq!(focus_offset(2000., &[p; 4]), -48.); +} + +#[test] +fn relative_detector_scales_and_manual_protection_reduces_false_positives() { + let s = Settings { + amount: 80., + sensitivity: 3., + max_cut: 12., + attack: 1., + transient_guard: 0., + ..Default::default() + }; + let mut loud = Engine::new(48000., 1).unwrap(); + loud.set_settings(s).unwrap(); + let mut quiet = Engine::new(48000., 1).unwrap(); + quiet.set_settings(s).unwrap(); + let mut protected = Engine::new(48000., 1).unwrap(); + let mut p = s; + p.focus[0] = Focus { + enabled: true, + frequency: 3000., + width: 0.5, + sensitivity: -18., + }; + protected.set_settings(p).unwrap(); + for i in 0..24000 { + let x = 0.3 * (TAU * 3000. * i as f32 / 48000.).sin() + noise(i); + loud.process_sample([x, 0.], Monitor::default()); + quiet.process_sample([x * 0.1, 0.], Monitor::default()); + protected.process_sample([x, 0.], Monitor::default()); + } + let a = loud.take_analyzer().unwrap(); + let b = quiet.take_analyzer().unwrap(); + let c = protected.take_analyzer().unwrap(); + for i in 0..GRID { + assert!((a.reduction[i] - b.reduction[i]).abs() < 0.02); + } + let k = (0..GRID) + .min_by(|a, b| { + (frequency(*a) - 3000.) + .abs() + .total_cmp(&(frequency(*b) - 3000.).abs()) + }) + .unwrap(); + assert!(c.reduction[k] < a.reduction[k] - 1.); +} +#[test] +fn settled_delta_and_bypass_have_precise_taps() { + let s = Settings { + attack: 1., + transient_guard: 0., + ..Default::default() + }; + let mut wet = Engine::new(48000., 1).unwrap(); + wet.set_settings(s).unwrap(); + let mut delta = Engine::new(48000., 1).unwrap(); + delta.set_settings(s).unwrap(); + let mut bypass = Engine::new(48000., 1).unwrap(); + bypass.set_settings(Settings { bypass: true, ..s }).unwrap(); + let latency = wet.latency_samples() as usize; + for i in 0..24000 { + let x = noise(i); + let a = wet.process_sample([x, 0.], Monitor::default())[0]; + let b = delta.process_sample([x, 0.], Monitor { delta: true })[0]; + let c = bypass.process_sample([x, 0.], Monitor::default())[0]; + if i > 12000 { + let expected = noise(i - latency); + assert!((a + b - expected).abs() < 1e-5); + assert!((c - expected).abs() < 1e-6); + } + } +} diff --git a/crates/resonance-core/tests/realtime.rs b/crates/resonance-core/tests/realtime.rs new file mode 100644 index 0000000..be0566d --- /dev/null +++ b/crates/resonance-core/tests/realtime.rs @@ -0,0 +1,54 @@ +use resonance_core::{Engine, Monitor, Settings}; +use std::{ + alloc::{GlobalAlloc, Layout, System}, + cell::Cell, +}; + +struct CheckedAllocator; +thread_local! { + static CHECK:Cell=const {Cell::new(false)}; + static OPERATIONS:Cell=const {Cell::new(0)}; +} +fn record() { + if CHECK.try_with(Cell::get).unwrap_or(false) { + let _ = OPERATIONS.try_with(|n| n.set(n.get() + 1)); + } +} +unsafe impl GlobalAlloc for CheckedAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + record(); + unsafe { System.alloc(layout) } + } + unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) { + record(); + unsafe { System.dealloc(pointer, layout) } + } + unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, size: usize) -> *mut u8 { + record(); + unsafe { System.realloc(pointer, layout, size) } + } +} +#[global_allocator] +static ALLOCATOR: CheckedAllocator = CheckedAllocator; + +#[test] +fn processing_automation_and_analysis_do_not_allocate_or_free() { + let mut engine = Engine::new(48000., 2).unwrap(); + let mut settings = Settings::preset(1); + engine.set_settings(settings).unwrap(); + CHECK.with(|v| v.set(true)); + for i in 0..48000 { + if i % 512 == 0 { + settings.focus[0].enabled = true; + settings.focus[0].frequency = 1000. + (i % 10000) as f32; + settings.focus[0].sensitivity = 6.; + settings.low = 120. + (i % 100) as f32; + engine.set_settings(settings).unwrap(); + } + std::hint::black_box(engine.process_sample([0.3, -0.2], Monitor { delta: i > 24000 })); + std::hint::black_box(engine.take_analyzer()); + } + engine.reset(); + CHECK.with(|v| v.set(false)); + assert_eq!(OPERATIONS.with(Cell::get), 0); +} diff --git a/crates/resonance-render/Cargo.toml b/crates/resonance-render/Cargo.toml new file mode 100644 index 0000000..b5f113f --- /dev/null +++ b/crates/resonance-render/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "resonance-render" +version = "0.1.0" +edition = "2021" +license = "ISC" + +[dependencies] +resonance-core = { path = "../resonance-core" } +serde_json = "1" diff --git a/crates/resonance-render/src/main.rs b/crates/resonance-render/src/main.rs new file mode 100644 index 0000000..5255a2a --- /dev/null +++ b/crates/resonance-render/src/main.rs @@ -0,0 +1,195 @@ +//! Bounded offline evaluation: PCM16/24/32 and IEEE float32 RIFF/WAVE input. +use resonance_core::{Engine, Monitor, Settings}; +use std::{ + env, fs, + io::{Read, Write}, + path::Path, + time::Instant, +}; +fn main() { + if let Err(e) = run() { + eprintln!("{e}"); + std::process::exit(1); + } +} +fn run() -> Result<(), Box> { + let args: Vec<_> = env::args().skip(1).collect(); + if args.is_empty() || args[0] == "--help" { + println!("resonance-render [settings.json] [--delta]\nresonance-render --demo \nresonance-render --benchmark\nOutput is float32 WAV, latency-trimmed with original length. Existing files are refused."); + return Ok(()); + } + if args[0] == "--benchmark" { + let mut e = Engine::new(48000., 2)?; + e.set_settings(Settings::preset(1))?; + let frames = 48000 * 5; + let start = Instant::now(); + for i in 0..frames { + let x = (i as f32 * 0.173).sin() * 0.3; + std::hint::black_box(e.process_sample([x, -x], Monitor::default())); + } + println!("Stereo 48 kHz / FFT 4096 / hop 512 / 5 seconds: {:.3}s ({:.1}% of one core, run --release for meaningful results)",start.elapsed().as_secs_f64(),start.elapsed().as_secs_f64()/5.*100.); + return Ok(()); + } + let (rate, channels, input, output) = if args[0] == "--demo" && args.len() == 2 { + let samples = (0..48000 * 3) + .map(|i| { + let t = i as f32 / 48000.; + let envelope = if t % 0.5 < 0.3 { 1. } else { 0.2 }; + [((t * 440. * std::f32::consts::TAU).sin() * 0.14 + + (t * 3000. * std::f32::consts::TAU).sin() * 0.28) + * envelope; 2] + }) + .collect(); + (48000., 2, samples, args[1].clone()) + } else if args.len() >= 2 { + let (r, c, x) = read_wave(Path::new(&args[0]))?; + (r, c, x, args[1].clone()) + } else { + return Err("Use --help for usage".into()); + }; + let settings = if args.get(2).is_some_and(|s| !s.starts_with("--")) { + let bytes = read_limit(Path::new(&args[2]), 64 * 1024)?; + serde_json::from_slice::(&bytes)? + } else { + Settings::default() + }; + settings.validate()?; + let mut engine = Engine::new(rate, channels)?; + engine.set_settings(settings)?; + let latency = engine.latency_samples() as usize; + let start = Instant::now(); + let mut rendered = Vec::with_capacity(input.len()); + let monitor = Monitor { + delta: args.iter().any(|a| a == "--delta"), + }; + for i in 0..input.len() + latency { + let sample = input.get(i).copied().unwrap_or([0.; 2]); + let out = engine.process_sample(sample, monitor); + if i >= latency { + rendered.push(out); + } + } + write_wave(Path::new(&output), rate as u32, channels, &rendered)?; + println!( + "Rendered {} frames, {} channel(s), {} Hz in {:.3}s; compensated {} samples → {}", + rendered.len(), + channels, + rate, + start.elapsed().as_secs_f64(), + latency, + output + ); + Ok(()) +} +fn read_limit(path: &Path, limit: usize) -> Result, Box> { + let mut bytes = Vec::new(); + fs::File::open(path)? + .take(limit as u64 + 1) + .read_to_end(&mut bytes)?; + if bytes.len() > limit { + return Err("Input exceeds size limit".into()); + } + Ok(bytes) +} +type Wave = (f32, usize, Vec<[f32; 2]>); +fn read_wave(path: &Path) -> Result> { + let b = read_limit(path, 256 * 1024 * 1024)?; + if b.len() < 12 || &b[..4] != b"RIFF" || &b[8..12] != b"WAVE" { + return Err("Expected RIFF/WAVE".into()); + } + let u16_at = |p| u16::from_le_bytes([b[p], b[p + 1]]); + let u32_at = |p| u32::from_le_bytes([b[p], b[p + 1], b[p + 2], b[p + 3]]); + let end = (u32_at(4) as usize) + .checked_add(8) + .ok_or("Invalid RIFF length")?; + if end > b.len() { + return Err("Truncated RIFF container".into()); + } + let mut pos = 12; + let mut format = None; + let mut data = None; + while pos + 8 <= end { + let len = u32_at(pos + 4) as usize; + let start = pos + 8; + let stop = start.checked_add(len).ok_or("Invalid chunk length")?; + if stop > end { + return Err("Truncated WAV chunk".into()); + } + match &b[pos..pos + 4] { + b"fmt " if len >= 16 => { + format = Some(( + u16_at(start), + u16_at(start + 2) as usize, + u32_at(start + 4), + u16_at(start + 12) as usize, + u16_at(start + 14) as usize, + )) + } + b"data" => data = Some((start, stop)), + _ => {} + } + pos = stop + (len % 2); + } + let (tag, channels, rate, align, bits) = format.ok_or("Missing WAV format")?; + let (start, stop) = data.ok_or("Missing WAV data")?; + if !(1..=2).contains(&channels) || !matches!((tag, bits), (1, 16) | (1, 24) | (1, 32) | (3, 32)) + { + return Err("Use mono/stereo PCM16/24/32 or float32 WAV".into()); + } + let size = bits / 8; + if align != channels * size || (stop - start) % align != 0 { + return Err("Invalid WAV block alignment".into()); + } + let mut samples = Vec::with_capacity((stop - start) / align); + for p in (start..stop).step_by(align) { + let mut frame = [0.; 2]; + for (c, value) in frame.iter_mut().enumerate().take(channels) { + let q = p + c * size; + *value = match (tag, bits) { + (3, 32) => f32::from_le_bytes(b[q..q + 4].try_into()?), + (1, 16) => i16::from_le_bytes(b[q..q + 2].try_into()?) as f32 / 32768., + (1, 24) => { + ((b[q] as i32 | ((b[q + 1] as i32) << 8) | ((b[q + 2] as i32) << 16)) << 8 >> 8) + as f32 + / 8388608. + } + _ => i32::from_le_bytes(b[q..q + 4].try_into()?) as f32 / 2147483648., + }; + if !value.is_finite() { + return Err("Non-finite WAV samples".into()); + } + } + samples.push(frame); + } + Ok((rate as f32, channels, samples)) +} +fn write_wave( + path: &Path, + rate: u32, + channels: usize, + samples: &[[f32; 2]], +) -> Result<(), Box> { + let bytes = (samples.len() * channels * 4) as u32; + let mut f = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(path)?; + f.write_all(b"RIFF")?; + f.write_all(&(36 + bytes).to_le_bytes())?; + f.write_all(b"WAVEfmt ")?; + f.write_all(&16u32.to_le_bytes())?; + f.write_all(&3u16.to_le_bytes())?; + f.write_all(&(channels as u16).to_le_bytes())?; + f.write_all(&rate.to_le_bytes())?; + f.write_all(&(rate * channels as u32 * 4).to_le_bytes())?; + f.write_all(&(channels as u16 * 4).to_le_bytes())?; + f.write_all(&32u16.to_le_bytes())?; + f.write_all(b"data")?; + f.write_all(&bytes.to_le_bytes())?; + for frame in samples { + for sample in frame.iter().take(channels) { + f.write_all(&sample.to_le_bytes())?; + } + } + Ok(()) +} diff --git a/crates/resonance-ui/Cargo.toml b/crates/resonance-ui/Cargo.toml new file mode 100644 index 0000000..8bab09c --- /dev/null +++ b/crates/resonance-ui/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "resonance-ui" +version = "0.1.0" +edition = "2021" +license = "ISC" + +[dependencies] +egui = "0.31.1" +resonance-core = { path = "../resonance-core" } diff --git a/crates/resonance-ui/assets/OFL.txt b/crates/resonance-ui/assets/OFL.txt new file mode 100644 index 0000000..9c48e05 --- /dev/null +++ b/crates/resonance-ui/assets/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/crates/resonance-ui/assets/RobotoCondensed-Medium.ttf b/crates/resonance-ui/assets/RobotoCondensed-Medium.ttf new file mode 100644 index 0000000..b6a65b9 Binary files /dev/null and b/crates/resonance-ui/assets/RobotoCondensed-Medium.ttf differ diff --git a/crates/resonance-ui/assets/RobotoCondensed-Regular.ttf b/crates/resonance-ui/assets/RobotoCondensed-Regular.ttf new file mode 100644 index 0000000..cdcdf12 Binary files /dev/null and b/crates/resonance-ui/assets/RobotoCondensed-Regular.ttf differ diff --git a/crates/resonance-ui/src/lib.rs b/crates/resonance-ui/src/lib.rs new file mode 100644 index 0000000..fc0af37 --- /dev/null +++ b/crates/resonance-ui/src/lib.rs @@ -0,0 +1,906 @@ +//! Shared native editor, also exercised by the headless renderer. +use egui::{self, Color32, FontId, Pos2, Rect, RichText, Sense, Stroke, Vec2}; +use resonance_core::{focus_offset, frequency, AnalyzerFrame, Focus, Settings, GRID}; +use std::time::{Duration, Instant}; + +const BG: Color32 = Color32::from_rgb(23, 20, 31); +const PANEL: Color32 = Color32::from_rgb(37, 33, 49); +const TEXT: Color32 = Color32::from_rgb(238, 234, 245); +const MUTED: Color32 = Color32::from_rgb(161, 151, 179); +const CYAN: Color32 = Color32::from_rgb(102, 215, 230); +const CORAL: Color32 = Color32::from_rgb(255, 136, 125); +const VIOLET: Color32 = Color32::from_rgb(174, 154, 247); +pub fn style(ctx: &egui::Context) { + let mut fonts = egui::FontDefinitions::default(); + fonts.font_data.insert( + "regular".into(), + egui::FontData::from_static(include_bytes!("../assets/RobotoCondensed-Regular.ttf")).into(), + ); + fonts.font_data.insert( + "medium".into(), + egui::FontData::from_static(include_bytes!("../assets/RobotoCondensed-Medium.ttf")).into(), + ); + fonts + .families + .get_mut(&egui::FontFamily::Proportional) + .unwrap() + .insert(0, "regular".into()); + fonts.families.insert( + egui::FontFamily::Name("medium".into()), + vec![ + "medium".into(), + "Ubuntu-Light".into(), + "NotoEmoji-Regular".into(), + "emoji-icon-font".into(), + ], + ); + ctx.set_fonts(fonts); + let mut s = (*ctx.style()).clone(); + s.visuals = egui::Visuals::dark(); + s.visuals.panel_fill = BG; + s.visuals.window_fill = PANEL; + s.visuals.override_text_color = Some(TEXT); + s.visuals.selection.bg_fill = Color32::from_rgb(80, 64, 112); + s.visuals.widgets.inactive.bg_fill = PANEL; + s.visuals.widgets.hovered.bg_fill = Color32::from_rgb(62, 50, 80); + s.visuals.widgets.active.bg_fill = Color32::from_rgb(81, 58, 102); + s.spacing.item_spacing = Vec2::new(10., 8.); + s.spacing.button_padding = Vec2::new(12., 6.); + s.text_styles + .insert(egui::TextStyle::Body, FontId::proportional(16.)); + s.text_styles + .insert(egui::TextStyle::Button, FontId::proportional(16.)); + ctx.set_style(s); +} +pub struct State { + pub delta: bool, + pub selected: Option, + pub preset: usize, + pub snapshots: [Settings; 2], + pub active: usize, + pub analyzer: AnalyzerFrame, + pub gesture_active: bool, + pub gestures: [bool; 30], + last_audio: Instant, + pub help: String, + drag: Option, +} +impl Default for State { + fn default() -> Self { + Self { + delta: false, + selected: None, + preset: 0, + snapshots: [Settings::default(); 2], + active: 0, + analyzer: AnalyzerFrame::default(), + gesture_active: false, + gestures: [false; 30], + last_audio: Instant::now(), + help: "Focus a troublesome region, then compare in the full mix.".into(), + drag: None, + } + } +} +impl State { + pub fn accept_frame(&mut self, frame: AnalyzerFrame) { + // Display-only smoothing; it never feeds the detector. + for i in 0..GRID { + self.analyzer.input[i] += 0.45 * (frame.input[i] - self.analyzer.input[i]); + self.analyzer.reduction[i] += 0.5 * (frame.reduction[i] - self.analyzer.reduction[i]); + } + self.analyzer.focus = frame.focus; + self.analyzer.input_peak = frame.input_peak; + self.analyzer.output_peak = frame.output_peak; + self.analyzer.guard = frame.guard; + self.analyzer.sample_rate = frame.sample_rate; + self.analyzer.latency = frame.latency; + self.analyzer.channels = frame.channels; + if frame.input_peak > 1e-6 + || frame.output_peak > 1e-6 + || frame.reduction.iter().any(|v| *v > 0.001) + { + self.last_audio = Instant::now(); + } + } +} +fn db(x: f32) -> f32 { + 20. * x.max(1e-6).log10() +} +fn note(ui: &mut egui::Ui, text: &str) { + ui.label(RichText::new(text).color(MUTED)); +} +#[allow(clippy::too_many_arguments)] // Explicit widget metadata keeps the control table readable. +fn control( + ui: &mut egui::Ui, + name: &str, + value: &mut f32, + range: std::ops::RangeInclusive, + unit: &str, + log: bool, + help: &str, + state: &mut State, +) { + ui.vertical(|ui| { + ui.label(RichText::new(name).color(TEXT)); + let previous = *value; + let response = ui.add( + egui::Slider::new(value, range) + .logarithmic(log) + .suffix(unit) + .clamping(egui::SliderClamping::Always) + .smart_aim(false), + ); + if response.dragged() && ui.input(|i| i.modifiers.shift) { + *value = previous + (*value - previous) * 0.1; + } + if response.double_clicked() { + let defaults = Settings::default(); + *value = match name { + "Amount" => defaults.amount, + "Sensitivity" => defaults.sensitivity, + "Max cut" => defaults.max_cut, + "Transient guard" => defaults.transient_guard, + "Attack" => defaults.attack, + "Release" => defaults.release, + "Neighborhood" => defaults.neighborhood, + "Cut width" => defaults.cut_width, + "Knee" => defaults.knee, + "Low frequency" => defaults.low, + "High frequency" => defaults.high, + "Mix" => defaults.mix, + _ => *value, + }; + } + if response.hovered() || response.has_focus() { + state.help = help.into(); + } + }); +} +#[allow(clippy::too_many_arguments)] +fn rotary( + ui: &mut egui::Ui, + name: &str, + value: &mut f32, + range: std::ops::RangeInclusive, + default: f32, + unit: &str, + size: f32, + color: Color32, +) { + let low = *range.start(); + let high = *range.end(); + let (rect, response) = ui.allocate_exact_size(Vec2::splat(size), Sense::click_and_drag()); + if response.drag_started() { + response.request_focus(); + } + if response.dragged() { + let delta = ui.input(|i| i.pointer.delta().y); + let fine = if ui.input(|i| i.modifiers.shift) { + 0.1 + } else { + 1. + }; + *value = (*value - delta * (high - low) / 180. * fine).clamp(low, high); + } + if response.double_clicked() { + *value = default; + } + if response.has_focus() { + let step = if ui.input(|i| i.modifiers.shift) { + 0.1 + } else { + 1. + }; + if ui.input(|i| i.key_pressed(egui::Key::ArrowUp) || i.key_pressed(egui::Key::ArrowRight)) { + *value = (*value + step).min(high); + } + if ui.input(|i| i.key_pressed(egui::Key::ArrowDown) || i.key_pressed(egui::Key::ArrowLeft)) + { + *value = (*value - step).max(low); + } + } + let center = rect.center(); + let radius = size * 0.45; + let p = ui.painter(); + p.circle_filled( + center + Vec2::new(0., 3.), + radius, + Color32::from_black_alpha(90), + ); + p.circle_filled(center, radius, Color32::from_rgb(20, 18, 29)); + p.circle_stroke( + center, + radius, + Stroke::new(1.0_f32, Color32::from_rgb(74, 65, 90)), + ); + p.circle_filled(center, radius - 6., Color32::from_rgb(43, 37, 56)); + p.circle_stroke( + center, + radius - 7., + Stroke::new(1.0_f32, Color32::from_rgb(60, 51, 77)), + ); + let start = std::f32::consts::PI * 0.75; + let span = std::f32::consts::PI * 1.5; + let normalized = (*value - low) / (high - low); + for (fraction, stroke) in [ + (1., Stroke::new(3.0_f32, Color32::from_rgb(59, 49, 76))), + (normalized, Stroke::new(3.0_f32, color)), + ] { + let points: Vec<_> = (0..=64) + .map(|i| { + let a = start + span * fraction * i as f32 / 64.; + center + Vec2::angled(a) * (radius - 3.) + }) + .collect(); + p.add(egui::Shape::line(points, stroke)); + } + let angle = start + span * normalized; + p.line_segment( + [ + center + Vec2::angled(angle) * (radius - 14.), + center + Vec2::angled(angle) * (radius - 8.), + ], + Stroke::new(2.0_f32, color), + ); + p.text( + center, + egui::Align2::CENTER_CENTER, + format!("{:.0}{}", *value, unit), + FontId::proportional(if size > 100. { 25. } else { 18. }), + color, + ); + if response.has_focus() { + p.circle_stroke(center, radius + 2., Stroke::new(1.0_f32, TEXT)); + } + ui.label( + RichText::new(name) + .size(if size > 100. { 20. } else { 17. }) + .color(TEXT), + ); + ui.add( + egui::DragValue::new(value) + .range(range) + .speed(0.1) + .suffix(unit), + ); +} +pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) { + ui.spacing_mut().slider_width = ((ui.available_width() - 100.) / 4. - 85.).max(95.); + let before = *settings; + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.label( + RichText::new("Resonance Compressor") + .font(FontId::new(30., egui::FontFamily::Name("medium".into()))), + ); + note(ui, "Keep the character. Control the ring."); + }); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.checkbox(&mut settings.bypass, "Bypass"); + if ui + .selectable_label( + state.delta, + RichText::new("Delta").color(if state.delta { CORAL } else { TEXT }), + ) + .on_hover_text("Hear delayed dry minus wet, before output trim.") + .clicked() + { + state.delta = !state.delta; + } + ui.separator(); + if ui.button("Copy A to B").clicked() { + state.snapshots[state.active] = *settings; + state.snapshots[1] = state.snapshots[0]; + if state.active == 1 { + *settings = state.snapshots[1]; + } + } + for i in (0..2).rev() { + if ui + .selectable_label(state.active == i, if i == 0 { "A" } else { "B" }) + .clicked() + && state.active != i + { + state.snapshots[state.active] = *settings; + *settings = state.snapshots[i]; + state.active = i; + } + } + egui::ComboBox::from_id_salt("resonance-preset") + .selected_text( + (0..4) + .find(|i| *settings == Settings::preset(*i)) + .map_or("Custom settings", |i| Settings::PRESETS[i]), + ) + .width(155.) + .show_ui(ui, |ui| { + for (i, name) in Settings::PRESETS.iter().enumerate() { + if ui.selectable_value(&mut state.preset, i, *name).clicked() { + *settings = Settings::preset(i); + state.delta = false; + } + } + }); + }); + }); + ui.add_space(8.); + ui.horizontal(|ui| { + let input = db(state.analyzer.input_peak); + let output = db(state.analyzer.output_peak); + ui.label(RichText::new(format!("Input {input:>5.1} dBFS")).color(CYAN)); + ui.label( + RichText::new(format!("Output {output:>5.1} dBFS")).color(if output >= 0. { + CORAL + } else { + TEXT + }), + ); + let max = state + .analyzer + .reduction + .iter() + .copied() + .fold(0f32, f32::max); + ui.label(RichText::new(format!("Reduction {max:.1} dB")).color(CORAL)); + ui.label( + RichText::new(format!( + "Transient guard {:.0}%", + state.analyzer.guard * 100. + )) + .color(MUTED), + ); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + note( + ui, + &format!( + "{:.1} ms latency • {}", + state.analyzer.latency as f32 / state.analyzer.sample_rate * 1000., + if state.analyzer.channels == 1 { + "Mono" + } else { + "Stereo linked" + } + ), + ); + }); + }); + ui.add_space(6.); + let body_height = (ui.available_height() - 35.).max(500.); + ui.horizontal_top(|ui| { + egui::Frame::new() + .fill(PANEL) + .corner_radius(8.) + .inner_margin(14.) + .show(ui, |ui| { + ui.set_width(156.); + ui.set_min_height(body_height - 28.); + ui.vertical_centered(|ui| { + ui.add_space(4.); + rotary( + ui, + "Amount", + &mut settings.amount, + 0. ..=100., + 35., + " %", + 126., + CYAN, + ); + ui.add_space(8.); + ui.separator(); + ui.add_space(12.); + ui.horizontal_top(|ui| { + ui.vertical_centered(|ui| { + ui.set_width(72.); + rotary( + ui, + "Mix", + &mut settings.mix, + 0. ..=100., + 100., + " %", + 64., + VIOLET, + ); + }); + ui.vertical_centered(|ui| { + ui.set_width(72.); + rotary( + ui, + "Output", + &mut settings.output, + -12. ..=12., + 0., + " dB", + 64., + CORAL, + ); + }); + }); + ui.add_space(10.); + note(ui, "Peak reduction"); + let max = state + .analyzer + .reduction + .iter() + .copied() + .fold(0f32, f32::max); + ui.label(RichText::new(format!("{max:.1} dB")).size(25.).color(CORAL)); + ui.add_space(12.); + ui.label( + RichText::new("Drag to adjust. Shift for fine steps. Double-click resets.") + .size(13.) + .color(MUTED), + ); + }); + }); + ui.add_space(4.); + ui.vertical(|ui| { + ui.set_width(ui.available_width()); + ui.spacing_mut().slider_width = ((ui.available_width() - 48.) / 4. - 103.).max(72.); + spectrum(ui, settings, state); + ui.add_space(4.); + ui.horizontal_wrapped(|ui| { + ui.label(RichText::new("Focus / protect").color(VIOLET)); + for i in 0..4 { + let f = settings.focus[i]; + if ui + .selectable_label( + state.selected == Some(i), + format!("{} {}", i + 1, if f.enabled { "●" } else { "○" }), + ) + .clicked() + { + state.selected = Some(i); + } + } + if let Some(i) = state.selected { + let f = &mut settings.focus[i]; + ui.checkbox(&mut f.enabled, "Enabled"); + ui.label("Hz"); + ui.add( + egui::DragValue::new(&mut f.frequency) + .range(20. ..=20000.) + .speed(5.), + ); + ui.label("Sensitivity"); + ui.add( + egui::DragValue::new(&mut f.sensitivity) + .range(-18. ..=18.) + .speed(0.1) + .suffix(" dB"), + ); + ui.label("Width"); + ui.add( + egui::DragValue::new(&mut f.width) + .range(0.05..=2.) + .speed(0.01) + .suffix(" oct"), + ); + if ui.small_button("Reset").clicked() { + f.sensitivity = 0.; + } + if ui.small_button("Remove").clicked() { + *f = Focus::default(); + state.selected = None; + } + } else { + note( + ui, + "Double-click the spectrum to place a point. Drag to tame or protect.", + ); + } + }); + ui.add_space(6.); + ui.separator(); + ui.add_space(4.); + egui::Grid::new("engine-controls") + .num_columns(4) + .spacing([16., 12.]) + .min_col_width((ui.available_width() - 48.) / 4.) + .show(ui, |ui| { + control( + ui, + "Sensitivity", + &mut settings.sensitivity, + 0. ..=24., + " dB", + false, + "Lower catches more peaks. Measured relative to the local spectral reference.", + state, + ); + control( + ui, + "Max cut", + &mut settings.max_cut, + 0. ..=18., + " dB", + false, + "Upper bound on spectral attenuation. This is not a limiter.", + state, + ); + control( + ui, + "Transient guard", + &mut settings.transient_guard, + 0. ..=100., + " %", + false, + "Relaxes reduction during attacks. Turn down to catch short resonances.", + state, + ); + control( + ui, + "Knee", + &mut settings.knee, + 0. ..=12., + " dB", + false, + "Softness of the transition into prominence reduction.", + state, + ); + ui.end_row(); + control( + ui, + "Attack", + &mut settings.attack, + 1. ..=200., + " ms", + true, + "Attenuation rise time, in addition to window and detection delay.", + state, + ); + control( + ui, + "Release", + &mut settings.release, + 10. ..=1000., + " ms", + true, + "Recovery time after a resonance subsides.", + state, + ); + control( + ui, + "Neighborhood", + &mut settings.neighborhood, + 0.25..=2., + " oct", + false, + "Width of the surrounding spectral reference. Wider includes more context.", + state, + ); + control( + ui, + "Cut width", + &mut settings.cut_width, + 0.03..=0.5, + " oct", + false, + "Frequency smoothing width of the attenuation surface.", + state, + ); + ui.end_row(); + }); + ui.add_space(6.); + egui::Grid::new("range-controls") + .num_columns(2) + .spacing([22., 8.]) + .show(ui, |ui| { + control( + ui, + "Low frequency", + &mut settings.low, + 20. ..=19999., + " Hz", + true, + "Lower processing boundary. Protect the source's body.", + state, + ); + control( + ui, + "High frequency", + &mut settings.high, + 21. ..=20000., + " Hz", + true, + "Upper processing boundary, also limited by Nyquist.", + state, + ); + }); + }); + }); + ui.separator(); + note(ui, &state.help); + if settings.low >= settings.high { + if settings.low != before.low { + settings.low = (settings.high - 1.).max(20.); + } else { + settings.high = (settings.low + 1.).min(20000.); + } + } + state.gesture_active = ui.input(|i| i.pointer.any_down()); + if state.last_audio.elapsed() < Duration::from_millis(300) { + ui.ctx().request_repaint_after(Duration::from_millis(33)); + } +} +fn spectrum(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) { + let height = (ui.available_height() - 360.).clamp(180., 480.); + let (outer, response) = ui.allocate_exact_size( + Vec2::new(ui.available_width(), height), + Sense::click_and_drag(), + ); + let p = ui.painter(); + p.rect_filled(outer, 6., Color32::from_rgb(15, 13, 22)); + let r = Rect::from_min_max( + outer.min + Vec2::new(46., 26.), + outer.max - Vec2::new(42., 26.), + ); + let x = |f: f32| r.left() + ((f.max(20.) / 20.).ln() / 1000f32.ln()).clamp(0., 1.) * r.width(); + let y = |d: f32| r.bottom() - (d + 100.).clamp(0., 100.) / 100. * r.height(); + let red_y = |d: f32| r.top() + d / 18. * r.height() * 0.75; + for (xx, level, color) in [ + (outer.left() + 5., state.analyzer.input_peak, CYAN), + ( + outer.right() - 9., + state.analyzer.output_peak, + if state.analyzer.output_peak >= 1. { + CORAL + } else { + VIOLET + }, + ), + ] { + let rail = Rect::from_min_max(Pos2::new(xx, r.top()), Pos2::new(xx + 4., r.bottom())); + p.rect_filled(rail, 2., Color32::from_rgb(38, 33, 48)); + let height = ((db(level) + 72.) / 72.).clamp(0., 1.) * rail.height(); + p.rect_filled( + Rect::from_min_max(Pos2::new(xx, rail.bottom() - height), rail.max), + 2., + color.gamma_multiply(0.65), + ); + } + let color_grid = Color32::from_rgb(58, 51, 70); + for f in [ + 20., 50., 100., 200., 500., 1000., 2000., 5000., 10000., 20000., + ] { + let xx = x(f); + p.line_segment( + [Pos2::new(xx, r.top()), Pos2::new(xx, r.bottom())], + Stroke::new(1.0_f32, color_grid), + ); + let label = if f >= 1000. { + format!("{}k", f / 1000.) + } else { + format!("{f:.0}") + }; + p.text( + Pos2::new(xx, r.bottom() + 7.), + egui::Align2::CENTER_TOP, + label, + FontId::proportional(12.), + MUTED, + ); + } + for d in [-96., -72., -48., -24., 0.] { + p.line_segment( + [Pos2::new(r.left(), y(d)), Pos2::new(r.right(), y(d))], + Stroke::new(1.0_f32, color_grid), + ); + p.text( + Pos2::new(r.left() - 7., y(d)), + egui::Align2::RIGHT_CENTER, + format!("{d:.0}"), + FontId::proportional(12.), + MUTED, + ); + } + for d in [0., 6., 12., 18.] { + p.text( + Pos2::new(r.right() + 7., red_y(d)), + egui::Align2::LEFT_CENTER, + format!("−{d:.0}"), + FontId::proportional(12.), + CORAL, + ); + } + p.text( + r.left_top() - Vec2::new(30., 19.), + egui::Align2::LEFT_TOP, + "dBFS / bin", + FontId::proportional(12.), + MUTED, + ); + p.text( + r.right_top() + Vec2::new(30., -19.), + egui::Align2::RIGHT_TOP, + "Cut dB", + FontId::proportional(12.), + CORAL, + ); + p.rect_filled( + Rect::from_min_max(r.min, Pos2::new(x(settings.low), r.bottom())), + 0., + Color32::from_black_alpha(90), + ); + p.rect_filled( + Rect::from_min_max(Pos2::new(x(settings.high), r.top()), r.max), + 0., + Color32::from_black_alpha(90), + ); + let signal: Vec<_> = (0..GRID) + .map(|i| Pos2::new(x(frequency(i)), y(state.analyzer.input[i]))) + .collect(); + let mut source_fill = egui::Mesh::default(); + for (i, point) in signal.iter().enumerate() { + source_fill.colored_vertex( + Pos2::new(point.x, r.bottom()), + Color32::from_rgba_unmultiplied(102, 215, 230, 7), + ); + source_fill.colored_vertex(*point, Color32::from_rgba_unmultiplied(102, 215, 230, 60)); + if i > 0 { + let j = (i * 2) as u32; + source_fill.add_triangle(j - 2, j - 1, j); + source_fill.add_triangle(j - 1, j, j + 1); + } + } + p.add(egui::Shape::mesh(source_fill)); + p.add(egui::Shape::line(signal, Stroke::new(1.5_f32, CYAN))); + let mut fill = Vec::with_capacity(GRID + 2); + fill.push(r.left_top()); + for i in 0..GRID { + fill.push(Pos2::new( + x(frequency(i)), + red_y(state.analyzer.reduction[i]), + )); + } + fill.push(r.right_top()); + // A triangle strip makes non-convex reduction envelopes render accurately. + let mut mesh = egui::Mesh::default(); + for i in 0..GRID { + let xx = x(frequency(i)); + let yy = red_y(state.analyzer.reduction[i]); + mesh.colored_vertex( + Pos2::new(xx, r.top()), + Color32::from_rgba_unmultiplied(255, 136, 125, 8), + ); + mesh.colored_vertex( + Pos2::new(xx, yy), + Color32::from_rgba_unmultiplied(255, 136, 125, 90), + ); + if i > 0 { + let j = (i * 2) as u32; + mesh.add_triangle(j - 2, j - 1, j); + mesh.add_triangle(j - 1, j, j + 1); + } + } + p.add(egui::Shape::mesh(mesh)); + p.add(egui::Shape::line( + fill[1..=GRID].to_vec(), + Stroke::new(1.6_f32, CORAL), + )); + let center = r.center().y; + let focus_y = |offset: f32| center - offset / 36. * r.height() * 0.65; + let points: Vec<_> = (0..GRID) + .map(|i| { + Pos2::new( + x(frequency(i)), + focus_y(focus_offset(frequency(i), &settings.focus)), + ) + }) + .collect(); + p.line_segment( + [Pos2::new(r.left(), center), Pos2::new(r.right(), center)], + Stroke::new(0.7_f32, VIOLET.gamma_multiply(0.35)), + ); + p.add(egui::Shape::line(points, Stroke::new(1.3_f32, VIOLET))); + for (i, f) in settings.focus.iter().enumerate().filter(|(_, f)| f.enabled) { + let pos = Pos2::new(x(f.frequency), focus_y(f.sensitivity)); + p.circle_filled(pos, if state.selected == Some(i) { 7. } else { 5. }, VIOLET); + p.text( + pos - Vec2::new(0., 12.), + egui::Align2::CENTER_BOTTOM, + format!("{}", i + 1), + FontId::proportional(12.), + TEXT, + ); + } + if state.analyzer.input_peak < 1e-6 { + p.text( + r.center() + Vec2::new(0., 42.), + egui::Align2::CENTER_CENTER, + "Play audio to inspect its resonances", + FontId::proportional(18.), + MUTED, + ); + } + if let Some(pos) = response.hover_pos() { + if r.contains(pos) { + let f = 20. * 1000f32.powf((pos.x - r.left()) / r.width()); + p.line_segment( + [Pos2::new(pos.x, r.top()), Pos2::new(pos.x, r.bottom())], + Stroke::new(0.7_f32, TEXT.gamma_multiply(0.3)), + ); + response.clone().on_hover_text(format!("{f:.0} Hz • Double-click to add focus; drag a node to tame/protect; wheel sets width")); + if response.double_clicked() { + if let Some(i) = settings.focus.iter().position(|p| !p.enabled) { + settings.focus[i] = Focus { + enabled: true, + frequency: f, + sensitivity: 0., + ..Default::default() + }; + state.selected = Some(i); + } + } + } + } + if response.drag_started() { + if let Some(pos) = response.interact_pointer_pos() { + state.drag = settings + .focus + .iter() + .enumerate() + .filter(|(_, f)| f.enabled) + .find(|(_, f)| { + Pos2::new(x(f.frequency), focus_y(f.sensitivity)).distance(pos) < 18. + }) + .map(|(i, _)| i); + if state.drag.is_some() { + state.selected = state.drag; + } else if (pos.x - x(settings.low)).abs() < 12. { + state.drag = Some(4); + } else if (pos.x - x(settings.high)).abs() < 12. { + state.drag = Some(5); + } + } + } + if response.dragged() { + if let (Some(i), Some(pos)) = (state.drag, response.interact_pointer_pos()) { + let fine = ui.input(|i| i.modifiers.shift); + let f = (20. * 1000f32.powf((pos.x - r.left()) / r.width())).clamp(20., 20000.); + if i < 4 { + let node = &mut settings.focus[i]; + let next = ((center - pos.y) / (r.height() * 0.65) * 36.).clamp(-18., 18.); + if fine { + node.frequency += (f - node.frequency) * 0.1; + node.sensitivity += (next - node.sensitivity) * 0.1; + } else { + node.frequency = f; + node.sensitivity = next; + } + } else if i == 4 { + settings.low = f.min(settings.high - 1.); + } else { + settings.high = f.max(settings.low + 1.); + } + } + } + if response.drag_stopped() { + state.drag = None; + } + if response.hovered() { + if let Some(i) = state.selected { + let scroll = ui.input(|i| i.smooth_scroll_delta.y); + if scroll != 0. { + settings.focus[i].width = + (settings.focus[i].width + scroll * 0.002).clamp(0.05, 2.); + } + } + } + for f in [settings.low, settings.high] { + p.line_segment( + [Pos2::new(x(f), r.top()), Pos2::new(x(f), r.bottom())], + Stroke::new(2.0_f32, MUTED), + ); + let handle = Rect::from_center_size(Pos2::new(x(f), r.center().y), Vec2::new(12., 30.)); + p.rect_filled(handle, 6., Color32::from_rgb(65, 77, 96)); + p.rect_stroke( + handle, + 6., + Stroke::new(1.0_f32, CYAN.gamma_multiply(0.65)), + egui::StrokeKind::Inside, + ); + } +} diff --git a/docs/resonance-compressor.md b/docs/resonance-compressor.md new file mode 100644 index 0000000..7a2f69f --- /dev/null +++ b/docs/resonance-compressor.md @@ -0,0 +1,91 @@ +# Resonance Compressor 0.1 + +A mixing effect for controlling local spectral peaks while retaining the intended +sound. Independently designed in Rust; no claim of matching Soothe or Quance. + +![Resonance Compressor](resonance-editor.png) + +## Install on Windows + +Download the Windows artifact from the GitLab pipeline's **windows** job and +extract the ZIP. Copy `Resonance Compressor.clap` into +`C:\Program Files\Common Files\CLAP\`. In FL Studio 2024.1 or newer, open +Options → Manage plugins, enable Verify plugins, and Find installed plugins. +Load Resonance Compressor in a mixer effect slot. Keep the font license with +redistributed packages. + +## Work with it + +Loop a troublesome passage in context. Restrict Low/High frequency, then adjust +Amount and Max cut. Sensitivity is a prominence threshold: lower catches more. +Neighborhood defines the spectral reference; Cut width smooths the attenuation. +Attack and Release smooth gain changes; Transient guard briefly relaxes cuts. + +Double-click the plot to add one of four focus points. Positive sensitivity +makes the detector more eager; negative values protect the region by raising +the threshold. Drag a point, wheel to change width, or use its numeric fields. +Range boundary handles constrain where processing may act. Focus is detector +weighting, not a static EQ boost and not an absolute protection guarantee. + +Cyan shows spectral input energy, coral the attenuation, violet detector focus. +The left scale is calibrated per-bin spectral power; the right scale is cut dB. +Delta plays delayed dry minus wet before output trim. Use it to inspect changes, +then return to normal monitoring to decide whether the music improved. +Mix is a linear blend of aligned signals. Bypass keeps the same latency. +Use Output for matched-level comparison. Spectral attenuation is not a limiter: +sample peaks can increase even when frequency gains never exceed unity. + +Four starting presets and A/B comparison help explore alternatives. All controls +are visible. The instrument-style sidebar holds Amount, Mix, and Output; the +spectrum includes filled energy traces, cut overlays, meter rails, and range +handles, using the plugin’s cyan/coral/violet palette. Host parameters and window size persist; Delta does not. The editor +supports numeric entry, keyboard focus, and direct graph editing. + +## Timing and limits + +Mixing only: 4096-sample analysis windows at 44.1/48 kHz, 8192 at 88.2/96 kHz, +16384 at 176.4/192 kHz, with eight overlaps. Delay is one window, about 85–93 ms. +The plugin reports it for host compensation. Supported layouts are mono and +linked stereo; supported rates are those six standard rates. + +The engine uses complex RustFFT with supplied scratch buffers, WOLA reconstruction, +a log-frequency prominence detector, bounded soft-knee cuts, and temporal/frequency +smoothing. Original bin phases are retained. This is not a minimum-phase filter +bank, a live monitoring effect, a limiter, or an automatic separation of tone and +unwanted resonance. No external sidechain, Learn, M/S, or pitch detector in v0.1. + +## Build and offline evaluation + +```sh +cargo test --workspace --locked +cargo run --release -p resonance-render -- input.wav processed.wav +cargo run --release -p resonance-render -- input.wav delta.wav --delta +cargo run --release -p resonance-render -- --demo /tmp/resonance-demo.wav +cargo run --release -p resonance-render -- --benchmark +python scripts/build-plugin.py --plugin resonance-compressor --target x86_64-pc-windows-gnu +``` + +The renderer accepts mono/stereo PCM16/24/32 and float32 RIFF/WAVE, capped at +256 MiB; WAV extensible/RF64 are not accepted. It writes float32 WAV, compensates +the processing delay, retains original duration, and refuses existing output. +An optional settings JSON uses the core's Settings structure. + +## CI and acceptance + +The root GitLab pipeline runs checks in a Linux Docker executor and cross-builds +**Windows binaries only** using MinGW. It needs an available untagged Docker +runner, outbound access to Debian/Rust/Cargo/Git dependencies, and no Windows +runner. The Windows job produces both CLAPs, setup guides, licenses, checksums, +and a ZIP. Runner execution must be verified on the actual GitLab instance; +local verification is not a remote pipeline run. + +Before calling host support complete, test FL Studio discovery as an effect, +automation, plugin delay compensation, parallel dry/wet alignment, repeated loading, +project restoration, mono/stereo, all supported sample rates, and render tails. +Judge through real patches with matched listening levels: preserve vowel motion, +snare punch, bass body, and stereo placement. Synthetic tests do not establish +artistic usefulness. + +Local benchmark: AMD Ryzen 7 PRO 3700U, optimized build, stereo 48 kHz, FFT +4096/hop 512: five seconds processed in 0.233 seconds (about 4.7% of one +core in that run). This is not a worst-case multi-instance guarantee. diff --git a/docs/resonance-editor.png b/docs/resonance-editor.png new file mode 100644 index 0000000..158d6ce Binary files /dev/null and b/docs/resonance-editor.png differ diff --git a/plugins/resonance-compressor/Cargo.lock b/plugins/resonance-compressor/Cargo.lock new file mode 100644 index 0000000..06c3c75 --- /dev/null +++ b/plugins/resonance-compressor/Cargo.lock @@ -0,0 +1,2435 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "addr2line" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e567177890eb1617b1f774005b66b26b2377afd138a2ca37aae7d8f0c81429d4" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.13.2", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.21", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "anymap3" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5dfbc6d8d2675589ccbe4d0fd61df2419075625f8c1a62325e718e2b0049f9" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atomic_float" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62af46d040ba9df09edc6528dae9d8e49f5f3e82f55b7d2ec31a733c38dbc49d" + +[[package]] +name = "atomic_refcell" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e4227379beff4205943696e6c3e0cd809bacdf3f0edd6e3dd153e2269571a4" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "backtrace" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f4220217f3288537b83684130f42fad43320b1c3b805f1910d683d52ded24f" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "baseview" +version = "0.1.0" +source = "git+https://github.com/RustAudio/baseview.git?rev=9a0b42c09d712777b2edb4c5e0cb6baf21e988f0#9a0b42c09d712777b2edb4c5e0cb6baf21e988f0" +dependencies = [ + "cocoa", + "core-foundation", + "keyboard-types", + "nix", + "objc", + "raw-window-handle 0.5.2", + "uuid", + "winapi", + "x11", + "x11rb", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.13.2", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "cc" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "clap-sys" +version = "0.5.0" +source = "git+https://github.com/micahrj/clap-sys.git?rev=25d7f53fdb6363ad63fbd80049cb7a42a97ac156#25d7f53fdb6363ad63fbd80049cb7a42a97ac156" + +[[package]] +name = "clipboard-win" +version = "5.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052af7878466feccda12ba7a6a67d019d712f296809559b47c12c8cc196d3a9a" +dependencies = [ + "error-code", +] + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics 0.22.3", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "copypasta" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6811e17f81fe246ef2bc553f76b6ee6ab41a694845df1d37e52a92b7bbd38a" +dependencies = [ + "clipboard-win", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "x11-clipboard", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "crossbeam" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e71406cd8807725f7ac2f999a4cdd32e98f829fdf65f528343cebf945e41df1e" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "ecolor" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "egui" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" +dependencies = [ + "ahash", + "bitflags 2.13.2", + "emath", + "epaint", + "nohash-hasher", + "profiling", +] + +[[package]] +name = "egui-baseview" +version = "0.5.0" +source = "git+https://github.com/BillyDM/egui-baseview.git?rev=ec70c3fe6b2f070dcacbc22924431edbe24bd1c0#ec70c3fe6b2f070dcacbc22924431edbe24bd1c0" +dependencies = [ + "baseview", + "copypasta", + "egui", + "egui_glow", + "keyboard-types", + "log", + "open", + "raw-window-handle 0.5.2", + "thiserror 2.0.21", +] + +[[package]] +name = "egui_glow" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "910906e3f042ea6d2378ec12a6fd07698e14ddae68aed2d819ffe944a73aab9e" +dependencies = [ + "ahash", + "bytemuck", + "egui", + "glow", + "log", + "memoffset 0.9.1", + "profiling", + "wasm-bindgen", + "web-sys", + "winit", +] + +[[package]] +name = "emath" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "epaint" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fcc0f5a7c613afd2dee5e4b30c3e6acafb8ad6f0edb06068811f708a67c562" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "nohash-hasher", + "parking_lot", + "profiling", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7e7a64c02cf7a5b51e745a9e45f60660a286f151c238b9d397b3e923f5082f" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.6", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "gimli" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1033caf0b349c518623b5396bfb2cf0bddf44f0306d543a250e5743297aafd10" + +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keyboard-types" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7668b7cff6a51fe61cdde64cd27c8a220786f399501b57ebe36f7d8112fd68" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "bitflags 2.13.2", + "libc", + "plain", + "redox_syscall 0.9.4", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "midi-consts" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2dd5c7f8aaf48a76e389068ab25ed80bdbc226b887f9013844c415698c9952" + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle 0.6.2", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nih_log" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0cdb52ef79af48ae110401c883bdb9c15e0306a99ab6ecf18bc52068b668e54" +dependencies = [ + "atty", + "log", + "once_cell", + "termcolor", + "time", + "windows", +] + +[[package]] +name = "nih_plug" +version = "0.0.0" +source = "git+https://github.com/robbert-vdh/nih-plug.git?rev=de421011f41a6d10fc8c7a6084e4f4dee0143683#de421011f41a6d10fc8c7a6084e4f4dee0143683" +dependencies = [ + "anyhow", + "anymap3", + "atomic_float", + "atomic_refcell", + "backtrace", + "bitflags 1.3.2", + "cfg-if", + "clap-sys", + "core-foundation", + "crossbeam", + "libc", + "log", + "midi-consts", + "nih_log", + "nih_plug_derive", + "objc", + "parking_lot", + "raw-window-handle 0.5.2", + "serde", + "serde_json", + "widestring", + "windows", +] + +[[package]] +name = "nih_plug_derive" +version = "0.1.0" +source = "git+https://github.com/robbert-vdh/nih-plug.git?rev=de421011f41a6d10fc8c7a6084e4f4dee0143683#de421011f41a6d10fc8c7a6084e4f4dee0143683" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "nih_plug_egui" +version = "0.0.0" +source = "git+https://github.com/robbert-vdh/nih-plug.git?rev=de421011f41a6d10fc8c7a6084e4f4dee0143683#de421011f41a6d10fc8c7a6084e4f4dee0143683" +dependencies = [ + "baseview", + "crossbeam", + "egui-baseview", + "nih_plug", + "parking_lot", + "raw-window-handle 0.5.2", + "serde", +] + +[[package]] +name = "nix" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num_threads" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabcc654086861f9e1bc5115258de62ef92c79b20f4615a1b0cc57c4e82027d4" +dependencies = [ + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2", + "objc2-contacts", + "objc2-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.2", + "block2", + "dispatch", + "libc", + "objc2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "object" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8c8b8e725ad26404d6896eb2f064294a0a7b4f1d9120210d85544684c4ca95" +dependencies = [ + "is-wsl", + "libc", +] + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.3", + "pin-project-lite", + "rustix 1.1.6", + "windows-sys 0.61.2", +] + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_syscall" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "resonance-compressor" +version = "0.1.0" +dependencies = [ + "clap-sys", + "crossbeam-queue", + "nih_plug", + "nih_plug_egui", + "resonance-core", + "resonance-ui", + "serde_json", +] + +[[package]] +name = "resonance-core" +version = "0.1.0" +dependencies = [ + "rustfft", + "serde", +] + +[[package]] +name = "resonance-ui" +version = "0.1.0" +dependencies = [ + "egui", + "resonance-core", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustfft" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tokio" +version = "1.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" + +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "android-activity", + "atomic-waker", + "bitflags 2.13.2", + "block2", + "bytemuck", + "calloop", + "cfg_aliases", + "concurrent-queue", + "core-foundation", + "core-graphics 0.23.2", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle 0.6.2", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-clipboard" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.6", + "x11rb-protocol", + "xcursor", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.13.2", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/plugins/resonance-compressor/Cargo.toml b/plugins/resonance-compressor/Cargo.toml new file mode 100644 index 0000000..070f3db --- /dev/null +++ b/plugins/resonance-compressor/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "resonance-compressor" +version = "0.1.0" +edition = "2021" +license = "ISC" + +[workspace] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +nih_plug = { git = "https://github.com/robbert-vdh/nih-plug.git", rev = "de421011f41a6d10fc8c7a6084e4f4dee0143683", default-features = false } +nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug.git", rev = "de421011f41a6d10fc8c7a6084e4f4dee0143683" } +resonance-core = { path = "../../crates/resonance-core" } +resonance-ui = { path = "../../crates/resonance-ui" } +crossbeam-queue = "0.3" + +[dev-dependencies] +clap-sys = { git = "https://github.com/micahrj/clap-sys.git", rev = "25d7f53fdb6363ad63fbd80049cb7a42a97ac156" } +serde_json = "1" + +[profile.release] +lto = "thin" +strip = "symbols" diff --git a/plugins/resonance-compressor/src/lib.rs b/plugins/resonance-compressor/src/lib.rs new file mode 100644 index 0000000..469ac39 --- /dev/null +++ b/plugins/resonance-compressor/src/lib.rs @@ -0,0 +1,202 @@ +//! CLAP audio effect. All DSP state is owned by the processing thread. +mod params; +use crossbeam_queue::ArrayQueue; +use nih_plug::prelude::*; +use nih_plug_egui::{create_egui_editor, egui}; +use params::{values, ResonanceParams}; +use resonance_core::{AnalyzerFrame, Engine, Monitor}; +use std::sync::{ + atomic::{AtomicBool, AtomicU64, Ordering}, + Arc, +}; + +struct Shared { + analyzer: ArrayQueue, + delta: AtomicBool, + reset: AtomicU64, +} +pub struct ResonanceCompressor { + params: Arc, + shared: Arc, + engine: Option, + publish_count: usize, + publish_interval: usize, +} +impl Default for ResonanceCompressor { + fn default() -> Self { + Self { + params: Arc::new(ResonanceParams::default()), + shared: Arc::new(Shared { + analyzer: ArrayQueue::new(3), + delta: AtomicBool::new(false), + reset: AtomicU64::new(0), + }), + engine: None, + publish_count: 0, + publish_interval: 1600, + } + } +} +impl Plugin for ResonanceCompressor { + const NAME: &'static str = "Resonance Compressor"; + const VENDOR: &'static str = "Storyboard"; + const URL: &'static str = ""; + const EMAIL: &'static str = ""; + const VERSION: &'static str = env!("CARGO_PKG_VERSION"); + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[ + AudioIOLayout { + main_input_channels: NonZeroU32::new(2), + main_output_channels: NonZeroU32::new(2), + ..AudioIOLayout::const_default() + }, + AudioIOLayout { + main_input_channels: NonZeroU32::new(1), + main_output_channels: NonZeroU32::new(1), + ..AudioIOLayout::const_default() + }, + ]; + const SAMPLE_ACCURATE_AUTOMATION: bool = true; + type SysExMessage = (); + type BackgroundTask = (); + fn params(&self) -> Arc { + self.params.clone() + } + fn editor(&mut self, _: AsyncExecutor) -> Option> { + let params = self.params.clone(); + let shared = self.shared.clone(); + create_egui_editor( + params.editor.clone(), + resonance_ui::State::default(), + |ctx, state| { + state.delta = false; + resonance_ui::style(ctx); + }, + move |ctx, setter, state| { + let epoch = shared.reset.load(Ordering::Acquire); + if epoch != state_epoch(ctx) { + state.delta = false; + store_epoch(ctx, epoch); + } + // Delta is never retained after editor close/reopen or a host reset. + shared.delta.store(state.delta, Ordering::Release); + while let Some(frame) = shared.analyzer.pop() { + state.accept_frame(frame); + } + let before = params.settings(); + let mut edited = before; + nih_plug_egui::resizable_window::ResizableWindow::new("resonance-editor") + .min_size(egui::vec2(1040., 700.)) + .show(ctx, ¶ms.editor, |ui| { + resonance_ui::show(ui, &mut edited, state) + }); + let old = values(&before); + let new = values(&edited); + let floats = params.floats(); + for i in 0..29 { + if new[i] != old[i] { + if !state.gestures[i] { + setter.begin_set_parameter(floats[i]); + state.gestures[i] = true; + } + setter.set_parameter(floats[i], new[i]); + } + if !state.gesture_active && state.gestures[i] { + setter.end_set_parameter(floats[i]); + state.gestures[i] = false; + } + } + if edited.bypass != before.bypass { + setter.begin_set_parameter(¶ms.bypass); + setter.set_parameter(¶ms.bypass, edited.bypass); + setter.end_set_parameter(¶ms.bypass); + } + shared.delta.store(state.delta, Ordering::Release); + }, + ) + } + fn initialize( + &mut self, + layout: &AudioIOLayout, + config: &BufferConfig, + context: &mut impl InitContext, + ) -> bool { + let Ok(mut engine) = Engine::new( + config.sample_rate, + layout.main_input_channels.map_or(0, |v| v.get() as usize), + ) else { + return false; + }; + if engine.set_settings(self.params.settings()).is_err() { + return false; + } + context.set_latency_samples(engine.latency_samples()); + self.publish_interval = (config.sample_rate / 30.) as usize; + self.engine = Some(engine); + self.shared.delta.store(false, Ordering::Release); + self.shared.reset.fetch_add(1, Ordering::AcqRel); + true + } + fn reset(&mut self) { + if let Some(e) = self.engine.as_mut() { + e.reset(); + } + self.publish_count = 0; + self.shared.delta.store(false, Ordering::Release); + self.shared.reset.fetch_add(1, Ordering::AcqRel); + } + fn process( + &mut self, + buffer: &mut Buffer, + _: &mut AuxiliaryBuffers, + _: &mut impl ProcessContext, + ) -> ProcessStatus { + let Some(engine) = self.engine.as_mut() else { + return ProcessStatus::Normal; + }; + let _ = engine.set_settings(self.params.settings()); + let delta = self.shared.delta.load(Ordering::Acquire) && self.params.editor.is_open(); + for mut sample in buffer.iter_samples() { + let x = [ + *sample.get_mut(0).unwrap(), + if engine.channels() == 2 { + *sample.get_mut(1).unwrap() + } else { + 0. + }, + ]; + let out = engine.process_sample(x, Monitor { delta }); + for (c, value) in sample.into_iter().enumerate() { + *value = out[c]; + } + self.publish_count += 1; + if self.publish_count >= self.publish_interval { + if let Some(frame) = engine.take_analyzer() { + self.shared.analyzer.force_push(frame); + } + self.publish_count = 0; + } + } + // The scheduler must drain its overlap buffers on silent input. + ProcessStatus::KeepAlive + } +} +fn state_epoch(ctx: &egui::Context) -> u64 { + ctx.data(|d| d.get_temp(egui::Id::new("reset-epoch")).unwrap_or(0)) +} +fn store_epoch(ctx: &egui::Context, e: u64) { + ctx.data_mut(|d| d.insert_temp(egui::Id::new("reset-epoch"), e)); +} +impl ClapPlugin for ResonanceCompressor { + const CLAP_ID: &'static str = "io.storyboard.resonance-compressor"; + const CLAP_DESCRIPTION: Option<&'static str> = + Some("Dynamic resonance attenuation with explicit tone protection"); + const CLAP_MANUAL_URL: Option<&'static str> = None; + const CLAP_SUPPORT_URL: Option<&'static str> = None; + const CLAP_FEATURES: &'static [ClapFeature] = &[ + ClapFeature::AudioEffect, + ClapFeature::Compressor, + ClapFeature::Stereo, + ClapFeature::Mono, + ]; +} +nih_export_clap!(ResonanceCompressor); diff --git a/plugins/resonance-compressor/src/params.rs b/plugins/resonance-compressor/src/params.rs new file mode 100644 index 0000000..9eb12c0 --- /dev/null +++ b/plugins/resonance-compressor/src/params.rs @@ -0,0 +1,188 @@ +use nih_plug::prelude::*; +use nih_plug_egui::EguiState; +use resonance_core::{Focus, Settings}; +use std::sync::Arc; +#[derive(Params)] +pub struct FocusParams { + #[id = "enabled"] + pub enabled: FloatParam, + #[id = "frequency"] + pub frequency: FloatParam, + #[id = "width"] + pub width: FloatParam, + #[id = "sensitivity"] + pub sensitivity: FloatParam, +} +fn linear(name: &str, v: f32, min: f32, max: f32, unit: &'static str) -> FloatParam { + FloatParam::new(name, v, FloatRange::Linear { min, max }).with_unit(unit) +} +fn log(name: &str, v: f32, min: f32, max: f32, unit: &'static str) -> FloatParam { + FloatParam::new( + name, + v, + FloatRange::Skewed { + min, + max, + factor: FloatRange::skew_factor(-2.), + }, + ) + .with_unit(unit) +} +impl Default for FocusParams { + fn default() -> Self { + Self { + enabled: linear("Enabled", 0., 0., 1., "").with_step_size(1.), + frequency: log("Frequency", 2000., 20., 20000., " Hz"), + width: linear("Width", 0.5, 0.05, 2., " oct"), + sensitivity: linear("Sensitivity", 0., -18., 18., " dB"), + } + } +} +#[derive(Params)] +pub struct ResonanceParams { + #[persist = "editor"] + pub editor: Arc, + #[id = "amount"] + pub amount: FloatParam, + #[id = "sensitivity"] + pub sensitivity: FloatParam, + #[id = "max-cut"] + pub max_cut: FloatParam, + #[id = "neighborhood"] + pub neighborhood: FloatParam, + #[id = "cut-width"] + pub cut_width: FloatParam, + #[id = "knee"] + pub knee: FloatParam, + #[id = "attack"] + pub attack: FloatParam, + #[id = "release"] + pub release: FloatParam, + #[id = "transient-guard"] + pub transient_guard: FloatParam, + #[id = "low"] + pub low: FloatParam, + #[id = "high"] + pub high: FloatParam, + #[id = "mix"] + pub mix: FloatParam, + #[id = "output"] + pub output: FloatParam, + #[id = "bypass"] + pub bypass: BoolParam, + #[nested(array, group = "Focus")] + pub focus: [FocusParams; 4], +} +impl Default for ResonanceParams { + fn default() -> Self { + Self { + editor: EguiState::from_size(1180, 760), + amount: linear("Amount", 35., 0., 100., " %"), + sensitivity: linear("Sensitivity", 6., 0., 24., " dB"), + max_cut: linear("Max cut", 3., 0., 18., " dB"), + neighborhood: linear("Neighborhood", 1., 0.25, 2., " oct"), + cut_width: linear("Cut width", 0.08, 0.03, 0.5, " oct"), + knee: linear("Knee", 3., 0., 12., " dB"), + attack: log("Attack", 15., 1., 200., " ms"), + release: log("Release", 120., 10., 1000., " ms"), + transient_guard: linear("Transient guard", 50., 0., 100., " %"), + low: log("Low frequency", 120., 20., 19999., " Hz"), + high: log("High frequency", 12000., 21., 20000., " Hz"), + mix: linear("Mix", 100., 0., 100., " %"), + output: linear("Output", 0., -12., 12., " dB"), + bypass: BoolParam::new("Bypass", false).make_bypass(), + focus: std::array::from_fn(|_| FocusParams::default()), + } + } +} +impl ResonanceParams { + pub fn floats(&self) -> [&FloatParam; 29] { + [ + &self.amount, + &self.sensitivity, + &self.max_cut, + &self.neighborhood, + &self.cut_width, + &self.knee, + &self.attack, + &self.release, + &self.transient_guard, + &self.low, + &self.high, + &self.mix, + &self.output, + &self.focus[0].enabled, + &self.focus[0].frequency, + &self.focus[0].width, + &self.focus[0].sensitivity, + &self.focus[1].enabled, + &self.focus[1].frequency, + &self.focus[1].width, + &self.focus[1].sensitivity, + &self.focus[2].enabled, + &self.focus[2].frequency, + &self.focus[2].width, + &self.focus[2].sensitivity, + &self.focus[3].enabled, + &self.focus[3].frequency, + &self.focus[3].width, + &self.focus[3].sensitivity, + ] + } + pub fn settings(&self) -> Settings { + let mut s = Settings { + amount: self.amount.value(), + sensitivity: self.sensitivity.value(), + max_cut: self.max_cut.value(), + neighborhood: self.neighborhood.value(), + cut_width: self.cut_width.value(), + knee: self.knee.value(), + attack: self.attack.value(), + release: self.release.value(), + transient_guard: self.transient_guard.value(), + low: self.low.value(), + high: self.high.value(), + mix: self.mix.value(), + output: self.output.value(), + bypass: self.bypass.value(), + focus: std::array::from_fn(|i| Focus { + enabled: self.focus[i].enabled.value() >= 0.5, + frequency: self.focus[i].frequency.value(), + width: self.focus[i].width.value(), + sensitivity: self.focus[i].sensitivity.value(), + }), + }; + // Independent automation of bounds may cross them; preserve a valid effective range. + if s.low >= s.high { + s.low = (s.high - 1.).max(20.); + } + s + } +} +pub fn values(s: &Settings) -> [f32; 29] { + let mut v = [0.; 29]; + v[..13].copy_from_slice(&[ + s.amount, + s.sensitivity, + s.max_cut, + s.neighborhood, + s.cut_width, + s.knee, + s.attack, + s.release, + s.transient_guard, + s.low, + s.high, + s.mix, + s.output, + ]); + for i in 0..4 { + v[13 + i * 4..17 + i * 4].copy_from_slice(&[ + f32::from(s.focus[i].enabled), + s.focus[i].frequency, + s.focus[i].width, + s.focus[i].sensitivity, + ]); + } + v +} diff --git a/plugins/resonance-compressor/tests/host.rs b/plugins/resonance-compressor/tests/host.rs new file mode 100644 index 0000000..690b491 --- /dev/null +++ b/plugins/resonance-compressor/tests/host.rs @@ -0,0 +1,199 @@ +//! Exercise the actual exported CLAP ABI without requiring a GUI or installed DAW. +use clap_sys::{ + audio_buffer::*, + events::*, + ext::{audio_ports::*, latency::*, state::*}, + factory::plugin_factory::*, + host::*, + process::*, + stream::*, + version::*, +}; +use std::{ + ffi::{c_char, c_void, CStr}, + io::{Cursor, Read}, + ptr, +}; + +unsafe extern "C" fn extension(_: *const clap_host, _: *const c_char) -> *const c_void { + ptr::null() +} +unsafe extern "C" fn request(_: *const clap_host) {} +unsafe extern "C" fn count(_: *const clap_input_events) -> u32 { + 0 +} +unsafe extern "C" fn event(_: *const clap_input_events, _: u32) -> *const clap_event_header { + ptr::null() +} +unsafe extern "C" fn push(_: *const clap_output_events, _: *const clap_event_header) -> bool { + true +} +unsafe extern "C" fn write(stream: *const clap_ostream, buffer: *const c_void, size: u64) -> i64 { + // SAFETY: The test owns the Vec for the entire synchronous save call. + let bytes = unsafe { &mut *((*stream).ctx as *mut Vec) }; + bytes.extend_from_slice(unsafe { + std::slice::from_raw_parts(buffer as *const u8, size as usize) + }); + size as i64 +} +unsafe extern "C" fn read(stream: *const clap_istream, buffer: *mut c_void, size: u64) -> i64 { + // SAFETY: The test owns the Cursor; the wrapper supplies writable stream storage. + let cursor = unsafe { &mut *((*stream).ctx as *mut Cursor>) }; + cursor + .read(unsafe { std::slice::from_raw_parts_mut(buffer as *mut u8, size as usize) }) + .unwrap() as i64 +} + +#[test] +fn effect_ports_state_latency_and_audio_lifecycle() { + // SAFETY: All callbacks, strings, streams, buffers, and the host outlive their + // synchronous ABI calls. Activation/processing/destruction follow CLAP order. + unsafe { + let entry = &resonance_compressor::clap_entry; + assert!(entry.init.unwrap()(c"resonance-compressor.clap".as_ptr())); + let factory = entry.get_factory.unwrap()(CLAP_PLUGIN_FACTORY_ID.as_ptr()) + as *const clap_plugin_factory; + assert!(!factory.is_null()); + let factory = &*factory; + assert_eq!(factory.get_plugin_count.unwrap()(factory), 1); + let descriptor = factory.get_plugin_descriptor.unwrap()(factory, 0); + assert!(!descriptor.is_null()); + assert_eq!( + CStr::from_ptr((*descriptor).id), + c"io.storyboard.resonance-compressor" + ); + assert!(factory.get_plugin_descriptor.unwrap()(factory, 1).is_null()); + let host = clap_host { + clap_version: CLAP_VERSION, + host_data: ptr::null_mut(), + name: c"Storyboard test host".as_ptr(), + vendor: c"Tests".as_ptr(), + url: c"".as_ptr(), + version: c"1.0".as_ptr(), + get_extension: Some(extension), + request_restart: Some(request), + request_process: Some(request), + request_callback: Some(request), + }; + let plugin = factory.create_plugin.unwrap()(factory, &host, (*descriptor).id); + assert!(!plugin.is_null()); + let p = &*plugin; + assert!(p.init.unwrap()(plugin)); + let ports = p.get_extension.unwrap()(plugin, CLAP_EXT_AUDIO_PORTS.as_ptr()) + as *const clap_plugin_audio_ports; + assert!(!ports.is_null()); + assert_eq!((*ports).count.unwrap()(plugin, false), 1); + assert_eq!((*ports).count.unwrap()(plugin, true), 1); + let latency = p.get_extension.unwrap()(plugin, CLAP_EXT_LATENCY.as_ptr()) + as *const clap_plugin_latency; + assert!(!latency.is_null()); + let state = + p.get_extension.unwrap()(plugin, CLAP_EXT_STATE.as_ptr()) as *const clap_plugin_state; + assert!(!state.is_null()); + let state = &*state; + let save = || { + let mut bytes = Vec::new(); + let stream = clap_ostream { + ctx: &mut bytes as *mut Vec as *mut c_void, + write: Some(write), + }; + assert!(state.save.unwrap()(plugin, &stream)); + bytes + }; + let mut envelope: nih_plug::prelude::PluginState = + serde_json::from_slice(&save()[8..]).unwrap(); + use nih_plug::wrapper::state::ParamValue; + envelope.params.insert("amount".into(), ParamValue::F32(0.)); + envelope + .params + .insert("frequency_1".into(), ParamValue::F32(2300.)); + envelope + .params + .insert("enabled_1".into(), ParamValue::F32(1.)); + envelope + .params + .insert("sensitivity_1".into(), ParamValue::F32(-12.)); + let encoded = serde_json::to_vec(&envelope).unwrap(); + let mut framed = (encoded.len() as u64).to_le_bytes().to_vec(); + framed.extend_from_slice(&encoded); + let mut bytes = Cursor::new(framed); + let stream = clap_istream { + ctx: &mut bytes as *mut Cursor> as *mut c_void, + read: Some(read), + }; + assert!(state.load.unwrap()(plugin, &stream)); + let restored: nih_plug::prelude::PluginState = + serde_json::from_slice(&save()[8..]).unwrap(); + assert_eq!( + serde_json::to_value(&restored.params).unwrap(), + serde_json::to_value(&envelope.params).unwrap() + ); + assert!(!restored.fields.contains_key("delta")); + for rate in [44100., 48000., 96000.] { + assert!(p.activate.unwrap()(plugin, rate, 1, 512)); + let delay = (*latency).get.unwrap()(plugin) as usize; + assert_eq!(delay, if rate > 48000. { 8192 } else { 4096 }); + assert!(p.start_processing.unwrap()(plugin)); + let mut position = 0; + while position < delay + 1024 { + let frames = [1, 127, 512, 31][position % 4].min(delay + 1024 - position); + let mut input_left = [0f32; 512]; + let mut input_right = [0f32; 512]; + for i in 0..frames { + let x = if position + i == 0 { 0.5 } else { 0. }; + input_left[i] = x; + input_right[i] = -x; + } + let mut left = [0f32; 512]; + let mut right = [0f32; 512]; + let mut in_channels = [input_left.as_mut_ptr(), input_right.as_mut_ptr()]; + let mut out_channels = [left.as_mut_ptr(), right.as_mut_ptr()]; + let incoming = clap_audio_buffer { + data32: in_channels.as_mut_ptr(), + data64: ptr::null_mut(), + channel_count: 2, + latency: 0, + constant_mask: 0, + }; + let mut outgoing_audio = clap_audio_buffer { + data32: out_channels.as_mut_ptr(), + data64: ptr::null_mut(), + channel_count: 2, + latency: 0, + constant_mask: 0, + }; + let input = clap_input_events { + ctx: ptr::null_mut(), + size: Some(count), + get: Some(event), + }; + let outgoing = clap_output_events { + ctx: ptr::null_mut(), + try_push: Some(push), + }; + let process = clap_process { + steady_time: position as i64, + frames_count: frames as u32, + transport: ptr::null(), + audio_inputs: &incoming, + audio_outputs: &mut outgoing_audio, + audio_inputs_count: 1, + audio_outputs_count: 1, + in_events: &input, + out_events: &outgoing, + }; + assert_ne!(p.process.unwrap()(plugin, &process), CLAP_PROCESS_ERROR); + for i in 0..frames { + let expected = if position + i == delay { 0.5 } else { 0. }; + assert!((left[i] - expected).abs() < 1e-6); + assert!((right[i] + expected).abs() < 1e-6); + } + position += frames; + } + p.stop_processing.unwrap()(plugin); + p.deactivate.unwrap()(plugin); + } + p.destroy.unwrap()(plugin); + entry.deinit.unwrap()(); + } +} diff --git a/scripts/build-plugin.py b/scripts/build-plugin.py index 1ec54b1..d8457f1 100644 --- a/scripts/build-plugin.py +++ b/scripts/build-plugin.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 """Build and package CLAP without modifying a DAW's plugin directories.""" import argparse +import os from pathlib import Path import shutil import subprocess root = Path(__file__).resolve().parents[1] parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("--plugin", choices=["motif-storyboard", "resonance-compressor"], default="motif-storyboard", help="Plugin to package") parser.add_argument("--target", help="Rust target triple; defaults to rustc's host") parser.add_argument("--standalone", action="store_true", help="Also build the standalone preview app") args = parser.parse_args() @@ -16,19 +18,30 @@ if target is None: target = next(line.removeprefix("host: ") for line in info.splitlines() if line.startswith("host: ")) if "windows" not in target and "linux" not in target: parser.error("This milestone packages Windows and Linux only.") -manifest = root / "plugins/motif-storyboard/Cargo.toml" +if args.standalone and args.plugin != "motif-storyboard": + parser.error("Standalone is available only for Motif Storyboard.") +manifest = root / "plugins" / args.plugin / "Cargo.toml" cmd = ["cargo", "build", "--manifest-path", str(manifest), "--release", "--locked", "--target", target, "--lib"] if args.standalone: cmd += ["--features", "standalone", "--bin", "motif-storyboard"] subprocess.run(cmd, cwd=root, check=True) -build = root / "plugins/motif-storyboard/target" / target / "release" +target_root = Path(os.environ.get("CARGO_TARGET_DIR", root / "plugins" / args.plugin / "target")) +if not target_root.is_absolute(): + target_root = root / target_root +build = target_root / target / "release" destination = root / "dist" / target destination.mkdir(parents=True, exist_ok=True) -library = build / ("motif_storyboard.dll" if "windows" in target else "libmotif_storyboard.so") -plugin = destination / "Motif Storyboard.clap" +stem = args.plugin.replace("-", "_") +library = build / (f"{stem}.dll" if "windows" in target else f"lib{stem}.so") +display = "Motif Storyboard" if args.plugin == "motif-storyboard" else "Resonance Compressor" +plugin = destination / f"{display}.clap" shutil.copy2(library, plugin) shutil.copy2(root / "LICENSE", destination) -shutil.copy2(root / "docs/fl-studio.md", destination / "FL-Studio-setup.md") +guide = "fl-studio.md" if args.plugin == "motif-storyboard" else "resonance-compressor.md" +shutil.copy2(root / "docs" / guide, destination / f"{args.plugin}-setup.md") +if args.plugin == "resonance-compressor": + shutil.copy2(root / "crates/resonance-ui/assets/OFL.txt", destination / "Roboto-OFL.txt") + shutil.copy2(root / "docs/resonance-editor.png", destination / "resonance-editor.png") if args.standalone: exe = "motif-storyboard.exe" if "windows" in target else "motif-storyboard" shutil.copy2(build / exe, destination) diff --git a/scripts/package-windows.py b/scripts/package-windows.py new file mode 100644 index 0000000..32db090 --- /dev/null +++ b/scripts/package-windows.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Validate and archive the two Windows CLAP deliverables; no Linux output.""" +import hashlib +from pathlib import Path +import struct +import zipfile + +root = Path(__file__).resolve().parents[1] +folder = root / "dist/x86_64-pc-windows-gnu" +names = [ + "Motif Storyboard.clap", "Resonance Compressor.clap", "LICENSE", + "motif-storyboard-setup.md", "resonance-compressor-setup.md", "Roboto-OFL.txt", "resonance-editor.png", +] +for name in names[:2]: + data = (folder / name).read_bytes() + if data[:2] != b"MZ": + raise SystemExit(f"{name}: expected Windows PE binary") + offset = struct.unpack_from("