diff --git a/.gitea/workflows/check.yml b/.gitea/workflows/check.yml index 7ad0da7..67956bd 100644 --- a/.gitea/workflows/check.yml +++ b/.gitea/workflows/check.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - plugin: [rivet, patina, anvil, crucible, lathe, temper, weld, knurl, planish, braze] + plugin: [rivet, patina, anvil, crucible, lathe, temper, weld, knurl, planish, braze, quench] steps: - uses: https://github.com/actions/checkout@v4 with: @@ -64,6 +64,7 @@ jobs: cargo run --manifest-path tools/editor-check/Cargo.toml --locked -- target/editor/knurl.png --plugin knurl cargo run --manifest-path tools/editor-check/Cargo.toml --locked -- target/editor/planish.png --plugin planish cargo run --manifest-path tools/editor-check/Cargo.toml --locked -- target/editor/braze.png --plugin braze + cargo run --manifest-path tools/editor-check/Cargo.toml --locked -- target/editor/quench.png --plugin quench - uses: https://github.com/actions/upload-artifact@v3-node20 with: name: editor-previews @@ -97,6 +98,7 @@ jobs: python3 scripts/build-plugin.py --plugin knurl --target x86_64-pc-windows-gnu python3 scripts/build-plugin.py --plugin planish --target x86_64-pc-windows-gnu python3 scripts/build-plugin.py --plugin braze --target x86_64-pc-windows-gnu + python3 scripts/build-plugin.py --plugin quench --target x86_64-pc-windows-gnu python3 scripts/package-windows.py - uses: https://github.com/actions/upload-artifact@v3-node20 with: diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index e7714c9..e2285ad 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -1,7 +1,7 @@ name: Independent plugin release on: push: - tags: ["rivet-v*", "patina-v*", "anvil-v*", "crucible-v*", "lathe-v*", "temper-v*", "weld-v*", "knurl-v*", "planish-v*", "braze-v*"] + tags: ["rivet-v*", "patina-v*", "anvil-v*", "crucible-v*", "lathe-v*", "temper-v*", "weld-v*", "knurl-v*", "planish-v*", "braze-v*", "quench-v*"] permissions: contents: read diff --git a/Cargo.lock b/Cargo.lock index 7b11ef2..c229024 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -389,6 +389,25 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +[[package]] +name = "quench-core" +version = "0.1.0" +dependencies = [ + "common-dsp", + "serde", + "serde_json", +] + +[[package]] +name = "quench-ui" +version = "0.1.0" +dependencies = [ + "common-dsp", + "common-ui", + "egui", + "quench-core", +] + [[package]] name = "quote" version = "1.0.47" diff --git a/Cargo.toml b/Cargo.toml index 55bf3c6..d23524d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["plugins/common/ui", "plugins/common/dsp", "plugins/anvil/core", "plugins/anvil/ui", "plugins/lathe/core", "plugins/lathe/ui", "plugins/temper/core", "plugins/temper/ui", "plugins/weld/core", "plugins/weld/ui", "plugins/knurl/core", "plugins/knurl/ui", "plugins/planish/core", "plugins/planish/ui", "plugins/braze/core", "plugins/braze/ui", "plugins/crucible/core", "plugins/crucible/ui", "plugins/rivet/core", "plugins/rivet/midi", "plugins/rivet/preview", "plugins/rivet/app", "plugins/patina/core", "plugins/patina/render", "plugins/patina/ui"] +members = ["plugins/common/ui", "plugins/common/dsp", "plugins/anvil/core", "plugins/anvil/ui", "plugins/lathe/core", "plugins/lathe/ui", "plugins/temper/core", "plugins/temper/ui", "plugins/weld/core", "plugins/weld/ui", "plugins/knurl/core", "plugins/knurl/ui", "plugins/planish/core", "plugins/planish/ui", "plugins/braze/core", "plugins/braze/ui", "plugins/quench/core", "plugins/quench/ui", "plugins/crucible/core", "plugins/crucible/ui", "plugins/rivet/core", "plugins/rivet/midi", "plugins/rivet/preview", "plugins/rivet/app", "plugins/patina/core", "plugins/patina/render", "plugins/patina/ui"] exclude = ["plugins/rivet/plugin", "tools/editor-check", "plugins/patina/plugin", "plugins/anvil/plugin", "plugins/crucible/plugin", "plugins/lathe/plugin", "plugins/temper/plugin", "plugins/weld/plugin", "plugins/knurl/plugin", "plugins/planish/plugin"] [workspace.package] @@ -31,3 +31,5 @@ opt-level = 2 [profile.dev.package.weld-core] opt-level = 2 +[profile.dev.package.quench-core] +opt-level = 2 diff --git a/README.md b/README.md index bf6da56..10ec66d 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ note. Each plugin is a separate product with its own version, guide and release. | **[Knurl](docs/knurl/README.md)** | Gives every cycle of the note its own level, timing and grit: in-tune subharmonic growl, rattle and fry on every note | Bass | ≈ 71 ms | [Guide](docs/knurl/README.md) · [Changes](docs/knurl/CHANGELOG.md) | | **[Planish](docs/planish/README.md)** | Upward and downward compression (OTT) in bands set by harmonic number: band 2 holds harmonics 2–4 on every note | Bass | ≈ 67 ms | [Guide](docs/planish/README.md) · [Changes](docs/planish/CHANGELOG.md) | | **[Braze](docs/braze/README.md)** | Adds a detuned copy whose beating stays on the tempo grid on every note: reese movement in time | Bass | ≈ 67 ms | [Guide](docs/braze/README.md) · [Changes](docs/braze/CHANGELOG.md) | +| **[Quench](docs/quench/README.md)** | Clips the top as hard as you like while the sub passes clean: the low end keeps its level under a loud master | Master or bass bus, last | < 1 ms (oversampling) | [Guide](docs/quench/README.md) · [Changes](docs/quench/CHANGELOG.md) | Plugins with latency report it to the host, so FL Studio compensates the delay. Anvil, Lathe, Temper, Knurl, Planish and Braze detect pitch from the audio. They can also take the @@ -41,7 +42,8 @@ is struck on; a **crucible** melts material down to be cast again; a **lathe** turns a piece so each cut lands the same all the way round; **tempering** sets how hard and how tough a metal ends up; a **weld** fuses two pieces into one; **knurling** presses a ridged grip pattern into a surface; **planishing** -hammers a surface smooth and even; **brazing** joins two metals with a filler. +hammers a surface smooth and even; **brazing** joins two metals with a filler; **quenching** hardens hot metal in one +fast step. The suite is the **foundry**: the workshop where the bass gets cast, shaped and finished. Hosts list the plugins under the vendor name *Bass Foundry*. @@ -68,6 +70,10 @@ finished. Hosts list the plugins under the vendor name *Bass Foundry*. Planish editor
Planish: harmonic OTT Braze editor
Braze: tempo-locked beating + + Quench editor
Quench: sub-safe clipper + + The previews are rendered headless from the real editor code by @@ -126,7 +132,7 @@ cargo run --manifest-path tools/editor-check/Cargo.toml -- target/editor/lathe.p ``` Plugin names for the scripts are `rivet`, `patina`, `anvil`, `crucible`, -`lathe`, `temper`, `weld`, `knurl`, `planish` and `braze`. Builds go to `dist//.clap`. No +`lathe`, `temper`, `weld`, `knurl`, `planish`, `braze` and `quench`. Builds go to `dist//.clap`. No script installs anything into a DAW. [Development and testing](docs/development.md) has the full command list. @@ -166,6 +172,7 @@ plugins/ knurl/ core · ui · plugin planish/ core · ui · plugin braze/ core · ui · plugin + quench/ core · ui · plugin common/ ui/ Shared look: palette, embedded fonts, knobs, panels, meters dsp/ Shared filters, pitch tracking, oversampling, meters, WAV files diff --git a/docs/chain.md b/docs/chain.md index e37a19c..87d61c8 100644 --- a/docs/chain.md +++ b/docs/chain.md @@ -8,7 +8,8 @@ in the chain changes the audio, the pitch detection after it can go wrong. ```text bass track: synth → Lathe → Temper → Planish → Braze → Knurl → Patina → Weld -bass bus: Anvil → (bus compression, clipping) → Crucible +bass bus: Anvil → (bus compression) → Quench → Crucible +master: … → Quench (last, or before a true-peak limiter) ``` - **Lathe and Temper before Knurl.** They hear the plain synth, where the pitch @@ -31,6 +32,10 @@ bass bus: Anvil → (bus compression, clipping) → Crucible that layer late. - **Anvil on the bus, in MIDI mode when Knurl is in the chain.** For Kick lock, send the kick's track to Anvil as a sidechain. +- **Quench last on the bus or the master.** It clips the top and leaves the sub + alone, so put it where you would put a clipper. On the bass bus after Anvil, + set its Split at or a little above Anvil's Split: Anvil's sub then passes + Quench untouched. Quench does not detect pitch, so it can sit after Knurl. - **Crucible at the end of the chain you want to resample.** ## Knurl and pitch detection @@ -53,7 +58,8 @@ and route the bass's MIDI notes to it. ## Latency Each tracked plugin adds its own lookahead: Anvil 80 ms, Lathe, Temper, -Planish and Braze about 67 ms, Knurl about 71 ms, Patina about 45 ms. A full chain is +Planish and Braze about 67 ms, Knurl about 71 ms, Patina about 45 ms, Quench +under 1 ms. A full chain is about 400 ms. FL Studio compensates this for playback and rendering, but playing a MIDI keyboard through the full chain is not practical. Write with a shorter chain and add the diff --git a/docs/development.md b/docs/development.md index 637ebe2..209f0fb 100644 --- a/docs/development.md +++ b/docs/development.md @@ -25,7 +25,7 @@ Patina has `render` for offline WAV evaluation. The shared crates are: - `common-ui`: palette, embedded fonts, header, panels, knobs, meters. - `common-dsp`: filters, YIN pitch tracking, the note tracker used by Anvil, - Lathe, Temper, Knurl, Planish and Braze, 4× oversampling, meters and WAV files. + Lathe, Temper, Knurl, Planish and Braze, 4× oversampling (also Quench's), meters and WAV files. The wrappers stay separate workspaces, each with its own lockfile, so the NIH-plug Git dependency stays out of the root workspace. Host identities and @@ -45,7 +45,7 @@ python3 -m unittest discover -s scripts/tests -v ``` Products: `rivet`, `patina`, `anvil`, `crucible`, `lathe`, `temper`, `weld`, -`knurl`, `planish`, `braze`. +`knurl`, `planish`, `braze`, `quench`. `check-plugin.py` runs formatting, tests and clippy for one product's packages and its wrapper, without compiling the other products. Run it for every product @@ -146,6 +146,7 @@ identify plugins by CLAP ID, so the vendor name does not affect saved projects. | Knurl | (new) | `io.narl.bass-foundry.knurl` | | Planish | (new) | `io.narl.bass-foundry.planish` | | Braze | (new) | `io.narl.bass-foundry.braze` | +| Quench | (new) | `io.narl.bass-foundry.quench` | CLAP IDs use the form `io.narl.bass-foundry.`. Builds tagged before this scheme used `io.storyboard.*` IDs; hosts treat the new IDs as different diff --git a/docs/ideas.md b/docs/ideas.md index 6468b50..9f1e8fe 100644 --- a/docs/ideas.md +++ b/docs/ideas.md @@ -263,8 +263,8 @@ one period of the note, so heavy settings do not ripple at f0. See [Planish](pla Patina already cut fixed formants on every note, but it also cut a boosted harmonic as hard (5.7 dB), at a position that moved with the note. Open: MIDI as the pitch source. -3. Planish (done, released 0.1.0), then Quench and Ballast: they fill the mix - and master gap. +3. Planish (done, released 0.1.0), then Quench (done, unreleased) and Ballast: + they fill the mix and master gap. - In parallel, from the research findings, in this order: Weld delay spread (done, Weld 0.3.0); the fatness lane, as the first Assay piece (prototype `scripts/fatness.py`); Anvil Ghost harmonics and Phone listen @@ -328,6 +328,29 @@ Quenching hardens hot metal in one fast step. Quench clips only above a split frequency and adds the clean sub back with its own headroom, so the master can be clipped hard without distorting the low end. Pairs with Anvil's split. +Built (unreleased 0.1.0, see [Quench](quench/README.md)). Design checks, made +in a prototype before the plugin: + +- Splitting and giving the top the instantaneous room, clamp(top, −C − sub, + C − sub), is exactly a plain clip of the sum: the error is the same signal. + The sub-safe part has to come from somewhere else. +- Feeding the clip error back through 1 − lowpass diverges: the complement of + an LR4 lowpass peaks at 1.5× near the split. +- What works: clip the top symmetrically at the ceiling minus the sub's held + peak (no pumping at the sub's rate, the sum stays under the ceiling), then + high-pass the clip error at the split so its products below the split go. + Two clip-and-high-pass passes and an oversampled hard clip hold the ceiling. + Measured: sub level +0.04 dB against −2.2 dB for a plain clip; low-band + products −35 dB against −16 dB. Cost: 0.5–1.2 dB less RMS at the same + ceiling. +- Where it does not help: a top whose fundamental lies within an octave of the + split is partly in the sub band (saw at 98 Hz over a 49 Hz sub: about the same + as a plain clip). +- Next ideas: link the channels' sub peak and offer mono below the split + (with Gauge); give the top the room on the sub's far side too (asymmetric) + with the error still high-passed, and measure whether that wins back the + loudness without pumping; a true-peak ceiling. + ### Scribe — audio to MIDI (problem 18) A scribe marks lines on metal. Scribe runs the note tracker on a bass and diff --git a/docs/quench/CHANGELOG.md b/docs/quench/CHANGELOG.md new file mode 100644 index 0000000..e1912e1 --- /dev/null +++ b/docs/quench/CHANGELOG.md @@ -0,0 +1,16 @@ +# Quench changelog + +## Unreleased (0.1.0) + +- Introduce Quench, a clipper that clips the top and lets the sub pass clean. +- A Linkwitz–Riley split at **Split** (40–300 Hz). The top is clipped at the + ceiling minus the sub's held peak; **Hold** sets how long that peak holds. +- What the clip removes is high-passed at Split, so its products stay out of + the low band. Two correction passes and an oversampled hard clip hold the + ceiling; a safety clip catches the downsampler's ringing. +- **Drive**, **Ceiling**, **Soft** (knee of the top's clip), **Bypass**. +- Listen to **Quench**, a **Plain** clipper at the same settings, or the + **Delta** that Quench takes away. Not saved. +- Editor: how much the low band changed for Quench and for a plain clip, how + far the top is clipped, the sub's peak and the room it leaves, and a scope. +- 4× oversampling; 37 samples of latency. diff --git a/docs/quench/README.md b/docs/quench/README.md new file mode 100644 index 0000000..88372fb --- /dev/null +++ b/docs/quench/README.md @@ -0,0 +1,104 @@ +# Quench + +Quench clips the top of the mix as hard as you like and lets the sub pass clean. +Current version **0.1.0** (unreleased). + +Riddim and dubstep masters are clipped hard. A plain clipper clips the sum of +the sub and everything above it. Near the sub's peaks only one side of the top +gets clipped, so the clipping takes energy out at the sub's rate: the sub loses +level, and the low end fills with products that were not in the mix. Quench +splits the signal at a frequency you set, never clips the sub, and clips only +the top, in the room the sub leaves under the ceiling. + +![Quench](assets/editor.png) + +## Install on Windows + +Copy `Quench.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 Quench last on the master or on the bass bus. Keep the +font license with redistributed packages. + +## Work with it + +1. Put Quench where you would put a clipper: last on the master, or last on + the bass bus. Before a true-peak limiter is fine. +2. Set **Ceiling**, the highest output peak. +3. Set **Split** above the highest sub note, or at Anvil's Split when Anvil + makes the sub. Everything below Split passes without clipping. +4. Raise **Drive** until the top clips as hard as you want. The panel shows + how far the top is clipped. +5. Press **PLAIN** to hear a plain clipper at the same settings, then + **QUENCH** again. **DELTA** plays only what Quench takes away. The listen + choice is not saved and goes back to QUENCH when the editor closes. + +The panel shows: + +- **Low end changed by**: how much the band below Split differs from the + input, against the sub's level, for Quench and for a plain clip of the same + signal. Lower is cleaner. It reads "—" when no sub plays. +- **Top clipped by**: how far the top's loudest peak went past its room. +- **Sub peak · room left**: the sub's peak after Drive and the room it leaves + the top under the ceiling. +- The scope: the output (grey), the sub band (blue), and the sub plus or minus + the top's room (thin lines). The output stays between the thin lines. + +Other controls: + +- **Hold** keeps the room the sub's peak needs for this long. With a hold + longer than half a period of the lowest note (17 ms at 30 Hz), the top's + clip level stays steady through the sub's cycle and the top does not pump + with the sub. Shorter holds give the top a little more room between sub + peaks, and that room then changes at the sub's rate. +- **Soft** bends the top's clip: 0 % is a hard clip, 100 % a soft curve from + the start. The ceiling stays a hard limit. +- **Bypass** crossfades to the input, delayed by the latency. + +When the sub alone reaches the ceiling, the top has no room left. The panel +then warns: lower Drive or the sub. A safety clip still holds the ceiling, and +that clip does cut the sub. + +### How it works + +A Linkwitz–Riley crossover splits the signal at Split. The sub passes. The top +is clipped symmetrically at the ceiling minus the sub's held peak, so the sum +of sub and top never goes over the ceiling. + +Clipping the top also makes products below Split, for example difference +tones between its partials. Quench high-passes what the clip removed at Split, +so those products do not reach the low band. High-passing lets a few peaks +overshoot. Two correction passes clip those again and high-pass the correction, +and a final hard clip at the ceiling takes the small rest. All of this runs at +4× oversampling. A safety clip after the downsampler catches the filters' +ringing; it reads "idle" or a few hundredths of a dB. + +Measured on a 50 Hz sub at −10.5 dBFS under a top of five unrelated partials +peaking about 6 dB over the ceiling: + +| | Sub level | Low-band products | +| --- | --- | --- | +| Plain clip | −2.2 dB | −16 dB | +| Quench | +0.04 dB | −35 dB | + +Low-band products are everything below 110 Hz except the sub, against the +sub's level. + +Quench pays for this in loudness. The top gets the room on the sub's quieter +side only, so at the same ceiling Quench measured 0.5–1.2 dB lower in RMS +than a plain clip. Add Drive to make up for it. + +## Timing and limits + +Latency is the oversampling filters: 37 samples (0.8 ms at 48 kHz), reported +to the host for delay compensation. + +The split is a slope, not a wall. A top whose own fundamental lies near +Split, for example a distorted patch an octave above a 49 Hz sub, is partly in +the sub band. That part passes unclipped and takes up room, and Quench helps +less there: on a saw top at 98 Hz it changed the low end about as much as a +plain clip. Keep the top's lowest partial at least an octave above Split. + +Quench keeps the ceiling on sample peaks. True peaks between samples can +still go a little over it: put a true-peak limiter after Quench if the master +needs one. Each channel is clipped on its own; Quench does not link the +channels or turn the sub to mono. diff --git a/docs/quench/assets/editor.png b/docs/quench/assets/editor.png new file mode 100644 index 0000000..3a24348 Binary files /dev/null and b/docs/quench/assets/editor.png differ diff --git a/plugins/catalog.json b/plugins/catalog.json index 292fab7..9580817 100644 --- a/plugins/catalog.json +++ b/plugins/catalog.json @@ -165,5 +165,22 @@ "braze-core", "braze-ui" ] + }, + "quench": { + "name": "Quench", + "description": "Clips the top as hard as you like while the sub passes clean", + "manifest": "plugins/quench/plugin/Cargo.toml", + "library": "quench", + "documentation": "docs/quench", + "clap_id": "io.narl.bass-foundry.quench", + "licenses": { + "plugins/common/ui/assets/OFL.txt": "licenses/Roboto-OFL.txt" + }, + "packages": [ + "common-ui", + "common-dsp", + "quench-core", + "quench-ui" + ] } } diff --git a/plugins/quench/core/Cargo.toml b/plugins/quench/core/Cargo.toml new file mode 100644 index 0000000..9c1a85a --- /dev/null +++ b/plugins/quench/core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "quench-core" +version = "0.1.0" +edition.workspace = true +license.workspace = true + +[dependencies] +common-dsp = { path = "../../common/dsp" } +serde = { workspace = true } + +[dev-dependencies] +serde_json = { workspace = true } diff --git a/plugins/quench/core/src/lib.rs b/plugins/quench/core/src/lib.rs new file mode 100644 index 0000000..794c43c --- /dev/null +++ b/plugins/quench/core/src/lib.rs @@ -0,0 +1,422 @@ +//! A clipper that leaves the sub alone. +//! +//! A plain clipper clips the sum of sub and top. Near the sub's peaks only one side of the top +//! gets clipped, so the clipping takes energy out at the sub's rate: the sub loses level and +//! the low band fills with products that were not in the mix. +//! +//! Quench splits the signal with a Linkwitz–Riley crossover. The sub passes untouched. The +//! top is clipped symmetrically at the room the sub leaves under the ceiling: the ceiling +//! minus the sub's held peak. Holding the peak through the sub's cycle keeps the top's clip +//! level steady, so the top is not pumped at the sub's rate. What the clip removes is then +//! high-passed at the split, so its products below the split do not reach the low band. +//! High-passing lets a few peaks overshoot; two correction passes clip those and high-pass the +//! correction again. A hard clip at the ceiling, still oversampled, takes the small rest, and a +//! safety clip after the downsampler catches its ringing. +//! +//! Everything runs at 4× oversampling. Plain clipping of the same signal runs alongside, for +//! the Plain monitor and for the meter that compares how much each changes the low band. +mod settings; +use common_dsp::oversample::Oversampler; +use common_dsp::svf::{Svf, SvfCoef}; +use common_dsp::{coefficient, db_to_gain, gain_to_db}; +pub use settings::{Settings, CEILING_MIN, DRIVE_MAX, HOLD_MAX, HOLD_MIN, SPLIT_MAX, SPLIT_MIN}; + +const Q: f32 = std::f32::consts::FRAC_1_SQRT_2; +const FACTOR: usize = Oversampler::FACTOR; +/// Correction passes after the top's clip. +pub const PASSES: usize = 2; +/// Points in the scope, each the extremes of `SCOPE_STEP` samples. +pub const SCOPE: usize = 256; +const SCOPE_STEP: usize = 8; +const SCOPE_LEN: usize = SCOPE * SCOPE_STEP; +/// Averaging time of the low-band meter. +const METER_MS: f32 = 300.; +const SMOOTH_MS: f32 = 20.; +const BYPASS_MS: f32 = 10.; +pub const SILENT: f32 = -120.; +/// Below this sub energy (−50 dB RMS) the low-band meter has nothing to compare against. +const SUB_FLOOR: f32 = 1e-5; + +/// What the output plays. Monitors are not saved. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Monitor { + #[default] + Quench, + /// A plain clipper at the same Drive, Ceiling and Soft, for comparison. + Plain, + /// What Quench takes away: its output minus the unclipped input. + Delta, +} + +#[derive(Clone, Copy, Debug)] +pub struct AnalyzerFrame { + /// Peak after Drive, in dBFS, since the last frame. + pub input_peak: f32, + pub output_peak: f32, + /// Peak of the sub band after Drive. + pub sub_peak: f32, + /// How far the top's loudest peak went past its clip level, in dB. 0 when nothing clipped. + pub top_clip: f32, + /// How far the final safety clip reached, in dB. 0 when it did nothing. + pub safety: f32, + /// Change of the low band below the split, against the sub, in dB: Quench and a plain clip + /// of the same signal. NaN without a sub. + pub low_quench: f32, + pub low_plain: f32, + pub ceiling: f32, + pub latency: u32, + pub sample_rate: f32, + /// Latest output of the selected monitor (oldest first), as extremes per point. + pub scope_min: [f32; SCOPE], + pub scope_max: [f32; SCOPE], + /// The sub band and the room it leaves the top, at the same points. + pub scope_sub: [f32; SCOPE], + pub scope_room: [f32; SCOPE], +} +impl Default for AnalyzerFrame { + fn default() -> Self { + Self { + input_peak: SILENT, + output_peak: SILENT, + sub_peak: SILENT, + top_clip: 0., + safety: 0., + low_quench: f32::NAN, + low_plain: f32::NAN, + ceiling: Settings::default().ceiling, + latency: Oversampler::latency() as u32, + sample_rate: 48000., + scope_min: [0.; SCOPE], + scope_max: [0.; SCOPE], + scope_sub: [0.; SCOPE], + scope_room: [0.; SCOPE], + } + } +} + +/// Clips `h` to ±`limit`. `knee` (0–1) is the share of the range below `limit` that bends: +/// 0 is a hard clip. Never exceeds `limit`. +#[inline] +pub fn shape(h: f32, limit: f32, knee: f32) -> f32 { + let a = limit * (1. - knee); + let m = h.abs(); + if m <= a { + return h; + } + let w = limit - a; + let y = if w <= 1e-9 { + limit + } else { + a + w * ((m - a) / w).tanh() + }; + y.min(limit).copysign(h) +} + +/// One band of a Linkwitz–Riley crossover: two Butterworth stages. +#[inline] +fn lr4(f: &mut [Svf; 2], c: &SvfCoef, v: f32) -> f32 { + let v = f[0].process(c, v); + f[1].process(c, v) +} + +#[derive(Clone, Copy)] +struct Coefs { + low: SvfCoef, + high: SvfCoef, + /// Held-peak length in oversampled samples, and its release per sample after that. + hold: usize, + release: f32, + knee: f32, +} +impl Coefs { + fn new(rate: f32, s: &Settings) -> Self { + let fast = rate * FACTOR as f32; + Self { + low: SvfCoef::lowpass(fast, s.split, Q), + high: SvfCoef::highpass(fast, s.split, Q), + hold: (s.hold * 0.001 * fast) as usize, + release: 1. - coefficient(fast, s.hold), + knee: s.soft * 0.01, + } + } +} + +struct Channel { + os: Oversampler, + low: [Svf; 2], + high: [Svf; 2], + /// High-passes what the top's clip removed. + error: Svf, + passes: [Svf; PASSES], + env: f32, + hold_left: usize, + meter_quench: [Svf; 2], + meter_plain: [Svf; 2], + /// The input delayed by the latency, for bypass. + dry: Vec, +} +impl Channel { + fn new(latency: usize) -> Self { + Self { + os: Oversampler::new(), + low: [Svf::default(); 2], + high: [Svf::default(); 2], + error: Svf::default(), + passes: [Svf::default(); PASSES], + env: 0., + hold_left: 0, + meter_quench: [Svf::default(); 2], + meter_plain: [Svf::default(); 2], + dry: vec![0.; latency + 1], + } + } + fn reset(&mut self) { + self.os.reset(); + for f in self + .low + .iter_mut() + .chain(self.high.iter_mut()) + .chain(self.passes.iter_mut()) + .chain(self.meter_quench.iter_mut()) + .chain(self.meter_plain.iter_mut()) + { + f.reset(); + } + self.error.reset(); + self.env = 0.; + self.hold_left = 0; + self.dry.fill(0.); + } +} + +/// Peaks and energies gathered between two analyzer frames. +#[derive(Clone, Copy, Default)] +struct Gather { + input: f32, + output: f32, + sub: f32, + /// Largest ratio of the top to its room. + top: f32, + safety: f32, +} + +pub struct Engine { + rate: f32, + channels: usize, + settings: Settings, + coefs: Coefs, + channel: [Channel; 2], + drive: f32, + ceiling: f32, + bypass: f32, + primed: bool, + smooth: f32, + fade: f32, + meter: f32, + dry_at: usize, + energy_quench: f32, + energy_plain: f32, + energy_sub: f32, + gather: Gather, + scope: [[f32; 4]; SCOPE_LEN], + scope_at: usize, +} +impl Engine { + pub fn new(rate: f32, channels: usize) -> Result { + if !(8000.0..=384000.).contains(&rate) { + return Err("unsupported sample rate"); + } + if !(1..=2).contains(&channels) { + return Err("unsupported channel count"); + } + let settings = Settings::default(); + let latency = Oversampler::latency(); + Ok(Self { + rate, + channels, + settings, + coefs: Coefs::new(rate, &settings), + channel: [Channel::new(latency), Channel::new(latency)], + drive: 1., + ceiling: db_to_gain(settings.ceiling), + bypass: 0., + primed: false, + smooth: coefficient(rate, SMOOTH_MS), + fade: coefficient(rate, BYPASS_MS), + meter: coefficient(rate * FACTOR as f32, METER_MS), + dry_at: 0, + energy_quench: 0., + energy_plain: 0., + energy_sub: 0., + gather: Gather::default(), + scope: [[0.; 4]; SCOPE_LEN], + scope_at: 0, + }) + } + pub fn channels(&self) -> usize { + self.channels + } + pub fn latency_samples(&self) -> u32 { + Oversampler::latency() as u32 + } + pub fn settings(&self) -> Settings { + self.settings + } + pub fn set_settings(&mut self, s: Settings) -> Result<(), &'static str> { + s.validate()?; + if s != self.settings || !self.primed { + self.coefs = Coefs::new(self.rate, &s); + } + self.settings = s; + if !self.primed { + // The first settings after a start take effect at once instead of ramping. + self.drive = db_to_gain(s.drive); + self.ceiling = db_to_gain(s.ceiling); + self.bypass = if s.bypass { 1. } else { 0. }; + self.primed = true; + } + Ok(()) + } + pub fn reset(&mut self) { + for c in &mut self.channel { + c.reset(); + } + self.dry_at = 0; + self.energy_quench = 0.; + self.energy_plain = 0.; + self.energy_sub = 0.; + self.gather = Gather::default(); + self.scope = [[0.; 4]; SCOPE_LEN]; + self.scope_at = 0; + self.primed = false; + let s = self.settings; + let _ = self.set_settings(s); + } + + pub fn process_sample(&mut self, x: [f32; 2], monitor: Monitor) -> [f32; 2] { + let s = self.settings; + self.drive += self.smooth * (db_to_gain(s.drive) - self.drive); + self.ceiling += self.smooth * (db_to_gain(s.ceiling) - self.ceiling); + self.bypass += self.fade * (if s.bypass { 1. } else { 0. } - self.bypass); + let (c, k, drive) = (self.ceiling, self.coefs, self.drive); + let mut out = [0.; 2]; + let mut point = [f32::MAX, f32::MIN, 0., 0.]; + let n = self.channel[0].dry.len(); + for (ch, &input) in x.iter().enumerate().take(self.channels) { + let chan = &mut self.channel[ch]; + let driven = input * drive; + self.gather.input = self.gather.input.max(driven.abs()); + let up = chan.os.up(driven); + let mut picked = [0.; FACTOR]; + for (j, &v) in up.iter().enumerate() { + let low = lr4(&mut chan.low, &k.low, v); + let high = lr4(&mut chan.high, &k.high, v); + // The sub's held peak decides the top's room. + let a = low.abs(); + if a >= chan.env { + chan.env = a; + chan.hold_left = k.hold; + } else if chan.hold_left > 0 { + chan.hold_left -= 1; + } else { + chan.env = a.max(chan.env * k.release); + } + let room = (c - chan.env).max(0.); + let top = shape(high, room, k.knee); + let reference = low + high; + let mut y = reference + chan.error.process(&k.high, top - high); + for p in &mut chan.passes { + y += p.process(&k.high, y.clamp(-c, c) - y); + } + // What the passes leave over the ceiling is small; clipping it here, still + // oversampled, leaves the safety clip after the downsampler almost idle. + let y = y.clamp(-c, c); + let plain = shape(reference, c, k.knee); + let lq = lr4(&mut chan.meter_quench, &k.low, y - reference); + let lp = lr4(&mut chan.meter_plain, &k.low, plain - reference); + self.energy_quench += self.meter * (lq * lq - self.energy_quench); + self.energy_plain += self.meter * (lp * lp - self.energy_plain); + self.energy_sub += self.meter * (low * low - self.energy_sub); + self.gather.sub = self.gather.sub.max(a); + if high.abs() > room { + let ratio = high.abs() / room.max(c * 1e-3); + self.gather.top = self.gather.top.max(ratio); + } + picked[j] = match monitor { + Monitor::Quench => y, + Monitor::Plain => plain, + Monitor::Delta => y - reference, + }; + if ch == 0 { + point[0] = point[0].min(picked[j]); + point[1] = point[1].max(picked[j]); + point[2] = low; + point[3] = room; + } + } + let mut o = chan.os.down(picked); + if monitor != Monitor::Delta && o.abs() > c { + self.gather.safety = self.gather.safety.max(o.abs() / c); + o = o.clamp(-c, c); + } + let dry = chan.dry[(self.dry_at + 1) % n]; + chan.dry[self.dry_at] = input; + o = if self.bypass >= 1. - 1e-6 { + dry + } else { + o + self.bypass * (dry - o) + }; + self.gather.output = self.gather.output.max(o.abs()); + out[ch] = o; + } + self.dry_at = (self.dry_at + 1) % n; + self.scope[self.scope_at] = point; + self.scope_at = (self.scope_at + 1) % SCOPE_LEN; + out + } + + /// Readings since the last call. Never allocates. + pub fn take_analyzer(&mut self) -> AnalyzerFrame { + let g = std::mem::take(&mut self.gather); + let db = |v: f32| if v > 0. { gain_to_db(v) } else { SILENT }; + let relative = |e: f32| { + if self.energy_sub > SUB_FLOOR { + 10. * (e.max(1e-20) / self.energy_sub).log10() + } else { + f32::NAN + } + }; + let mut f = AnalyzerFrame { + input_peak: db(g.input), + output_peak: db(g.output), + sub_peak: db(g.sub), + top_clip: if g.top > 1. { gain_to_db(g.top) } else { 0. }, + safety: if g.safety > 1. { + gain_to_db(g.safety) + } else { + 0. + }, + low_quench: relative(self.energy_quench), + low_plain: relative(self.energy_plain), + ceiling: self.settings.ceiling, + latency: self.latency_samples(), + sample_rate: self.rate, + ..AnalyzerFrame::default() + }; + for i in 0..SCOPE { + let (mut lo, mut hi) = (f32::MAX, f32::MIN); + let mut last = [0.; 4]; + for j in 0..SCOPE_STEP { + let v = self.scope[(self.scope_at + i * SCOPE_STEP + j) % SCOPE_LEN]; + lo = lo.min(v[0]); + hi = hi.max(v[1]); + last = v; + } + f.scope_min[i] = if lo <= hi { lo } else { 0. }; + f.scope_max[i] = if lo <= hi { hi } else { 0. }; + f.scope_sub[i] = last[2]; + f.scope_room[i] = last[3]; + } + f + } +} diff --git a/plugins/quench/core/src/settings.rs b/plugins/quench/core/src/settings.rs new file mode 100644 index 0000000..a51b9a5 --- /dev/null +++ b/plugins/quench/core/src/settings.rs @@ -0,0 +1,58 @@ +//! Host-independent controls. Levels in dB, times in milliseconds, amounts in percent. +use serde::{Deserialize, Serialize}; + +pub const DRIVE_MAX: f32 = 24.; +pub const CEILING_MIN: f32 = -12.; +pub const SPLIT_MIN: f32 = 40.; +pub const SPLIT_MAX: f32 = 300.; +pub const HOLD_MIN: f32 = 10.; +pub const HOLD_MAX: f32 = 300.; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct Settings { + /// Gain into the clipper. + pub drive: f32, + /// Highest output peak, in dBFS. + pub ceiling: f32, + /// Everything below this frequency is the sub: it passes without clipping. + pub split: f32, + /// How long the sub's peak keeps its headroom reserved. Longer than half a period of the + /// lowest note keeps the top's clip level steady through the sub's cycle. + pub hold: f32, + /// Knee of the top's clipper: 0 % is a hard clip, 100 % a soft curve from the start. + pub soft: f32, + pub bypass: bool, +} +impl Default for Settings { + fn default() -> Self { + Self { + drive: 0., + ceiling: -0.3, + split: 120., + hold: 60., + soft: 0., + bypass: false, + } + } +} +impl Settings { + pub fn validate(&self) -> Result<(), &'static str> { + let finite = [self.drive, self.ceiling, self.split, self.hold, self.soft] + .iter() + .all(|v| v.is_finite()); + if !finite { + return Err("non-finite setting"); + } + let ok = (0.0..=DRIVE_MAX).contains(&self.drive) + && (CEILING_MIN..=0.).contains(&self.ceiling) + && (SPLIT_MIN..=SPLIT_MAX).contains(&self.split) + && (HOLD_MIN..=HOLD_MAX).contains(&self.hold) + && (0.0..=100.).contains(&self.soft); + if ok { + Ok(()) + } else { + Err("setting out of range") + } + } +} diff --git a/plugins/quench/core/tests/dsp.rs b/plugins/quench/core/tests/dsp.rs new file mode 100644 index 0000000..ad5663f --- /dev/null +++ b/plugins/quench/core/tests/dsp.rs @@ -0,0 +1,300 @@ +use quench_core::{shape, Engine, Monitor, Settings}; +use std::f64::consts::TAU; + +const RATE: f32 = 48000.; +/// Partials of the top: not harmonics of the sub, so their clipping products can be told +/// apart from the sub's own harmonics. +const TOPS: [f64; 5] = [437., 611., 1013., 1597., 2399.]; + +fn top(t: f64) -> f64 { + TOPS.iter() + .enumerate() + .map(|(i, f)| (TAU * f * t + i as f64).sin()) + .sum::() + / 4.2 +} +/// Mono render of `seconds` of `signal`, returned as the left channel. +fn render( + engine: &mut Engine, + seconds: f32, + monitor: Monitor, + signal: impl Fn(f64) -> f64, +) -> Vec { + let n = (seconds * RATE) as usize; + (0..n) + .map(|i| { + let v = signal(i as f64 / RATE as f64) as f32; + engine.process_sample([v, v], monitor)[0] + }) + .collect() +} +fn engine(s: Settings) -> Engine { + let mut e = Engine::new(RATE, 2).unwrap(); + e.set_settings(s).unwrap(); + e +} +/// Amplitude of `hz` over a whole number of its periods at the end of `x`. +fn amplitude(x: &[f32], hz: f64) -> f64 { + let n = (RATE as f64 / hz * (0.5 * hz).floor()) as usize; + let x = &x[x.len() - n..]; + let (mut re, mut im) = (0., 0.); + for (i, &v) in x.iter().enumerate() { + let a = TAU * hz * i as f64 / RATE as f64; + re += v as f64 * a.cos(); + im += v as f64 * a.sin(); + } + 2. * (re * re + im * im).sqrt() / n as f64 +} +/// Hann-windowed spectrum magnitude of the last half second, 2 Hz bins up to `top_bin`. +fn spectrum(x: &[f32], top_bin: usize) -> Vec { + let n = RATE as usize / 2; + let x = &x[x.len() - n..]; + let w: Vec = (0..n) + .map(|i| 0.5 - 0.5 * (TAU * i as f64 / n as f64).cos()) + .collect(); + let norm: f64 = w.iter().sum(); + (0..=top_bin) + .map(|b| { + let (mut re, mut im) = (0., 0.); + for i in 0..n { + let a = TAU * b as f64 * i as f64 / n as f64; + re += x[i] as f64 * w[i] * a.cos(); + im += x[i] as f64 * w[i] * a.sin(); + } + 2. * (re * re + im * im).sqrt() / norm + }) + .collect() +} +/// Everything below 110 Hz except the 50 Hz sub, against the sub, in dB. +fn low_junk(x: &[f32]) -> f64 { + let s = spectrum(x, 55); + let junk: f64 = s + .iter() + .enumerate() + .filter(|(b, _)| !(22..=28).contains(b) && *b > 0) + .map(|(_, v)| v * v) + .sum(); + let sub: f64 = s[22..=28].iter().map(|v| v * v).sum(); + 10. * (junk / sub).log10() +} +fn db(r: f64) -> f64 { + 20. * r.log10() +} + +#[test] +fn below_the_ceiling_the_signal_passes_unchanged() { + let mut e = engine(Settings::default()); + let signal = |t: f64| 0.3 * (TAU * 50. * t).sin() + 0.2 * (TAU * 1000. * t).sin(); + let out = render(&mut e, 1., Monitor::Quench, signal); + for (hz, a) in [(50., 0.3), (1000., 0.2)] { + let got = db(amplitude(&out, hz) / a); + assert!(got.abs() < 0.02, "{hz} Hz: {got:.3} dB"); + } + let f = e.take_analyzer(); + assert_eq!(f.top_clip, 0.); + assert_eq!(f.safety, 0.); + let mut e = engine(Settings::default()); + let delta = render(&mut e, 0.2, Monitor::Delta, signal); + assert!(delta.iter().all(|v| v.abs() < 1e-4)); +} + +#[test] +fn the_output_never_passes_the_ceiling() { + let mut seed = 1u32; + let noise: Vec = (0..RATE as usize) + .map(|_| { + seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); + (seed >> 8) as f64 / (1u32 << 24) as f64 * 2. - 1. + }) + .collect(); + for ceiling in [-6., -0.3, 0.] { + for soft in [0., 50., 100.] { + for monitor in [Monitor::Quench, Monitor::Plain] { + let mut e = engine(Settings { + drive: 24., + ceiling, + soft, + ..Settings::default() + }); + let out = render(&mut e, 1., monitor, |t| { + let i = ((t * RATE as f64) as usize).min(noise.len() - 1); + 0.4 * (TAU * 41. * t).sin() + 0.3 * top(t) + 0.2 * noise[i] + }); + let limit = 10f32.powf(ceiling / 20.) * (1. + 1e-6); + let peak = out.iter().fold(0f32, |m, v| m.max(v.abs())); + assert!( + peak <= limit, + "{ceiling} dB, soft {soft}, {monitor:?}: {peak}" + ); + } + } + } +} + +#[test] +fn the_sub_keeps_its_level_and_the_low_band_stays_clean() { + // A quiet sub under a top that peaks far over the ceiling: the hard case for a clipper. + let signal = |t: f64| 0.3 * (TAU * 50. * t).sin() + 2. * top(t); + let mut results = Vec::new(); + for monitor in [Monitor::Quench, Monitor::Plain] { + let mut e = engine(Settings::default()); + let out = render(&mut e, 1.5, monitor, signal); + let sub = db(amplitude(&out, 50.) / 0.3); + results.push((sub, low_junk(&out))); + } + let [(q_sub, q_junk), (p_sub, p_junk)] = [results[0], results[1]]; + assert!(q_sub.abs() < 0.2, "Quench changes the sub by {q_sub:.2} dB"); + assert!( + p_sub < -0.8, + "a plain clip should squash the sub: {p_sub:.2} dB" + ); + assert!( + q_junk < p_junk - 10., + "low-band products: Quench {q_junk:.1} dB, plain {p_junk:.1} dB" + ); +} + +#[test] +fn the_meter_shows_how_much_less_the_low_band_changes() { + let mut e = engine(Settings::default()); + render(&mut e, 1.5, Monitor::Quench, |t| { + 0.3 * (TAU * 50. * t).sin() + 2. * top(t) + }); + let f = e.take_analyzer(); + assert!( + f.low_quench < f.low_plain - 10., + "Quench {:.1} dB, plain {:.1} dB", + f.low_quench, + f.low_plain + ); + assert!(f.top_clip > 6.); + // The oversampled clip leaves the safety clip after the downsampler almost idle. + assert!(f.safety < 0.2, "{}", f.safety); + let mut e = engine(Settings::default()); + render(&mut e, 0.5, Monitor::Quench, |t| { + 0.5 * (TAU * 1000. * t).sin() + }); + assert!( + e.take_analyzer().low_quench.is_nan(), + "no sub, nothing to compare" + ); +} + +#[test] +fn a_sub_over_the_ceiling_leaves_no_room_and_is_still_limited() { + let mut e = engine(Settings { + ceiling: -6., + ..Settings::default() + }); + let out = render(&mut e, 1., Monitor::Quench, |t| { + 0.8 * (TAU * 50. * t).sin() + 0.3 * top(t) + }); + let f = e.take_analyzer(); + assert!(f.sub_peak > -6., "{}", f.sub_peak); + assert!(f.safety > 0.); + let peak = out.iter().fold(0f32, |m, v| m.max(v.abs())); + assert!(peak <= 0.5012 * (1. + 1e-6)); + let room = f.scope_room.iter().fold(0f32, |m, v| m.max(*v)); + assert_eq!(room, 0., "the held sub peak is over the ceiling"); +} + +#[test] +fn the_held_peak_keeps_the_tops_room_steady_through_the_cycle() { + let room_ripple = |hold: f32, hz: f64| { + let mut e = engine(Settings { + hold, + ..Settings::default() + }); + render(&mut e, 1., Monitor::Quench, |t| { + 0.5 * (TAU * hz * t).sin() + 0.3 * top(t) + }); + let f = e.take_analyzer(); + let (lo, hi) = f + .scope_room + .iter() + .fold((f32::MAX, f32::MIN), |(a, b), v| (a.min(*v), b.max(*v))); + (hi - lo) / hi + }; + // 30 Hz: the sub peaks every 16.7 ms. + let held = room_ripple(60., 30.); + let short = room_ripple(10., 30.); + assert!(held < 0.01, "{held}"); + assert!(short > 0.1, "{short}"); +} + +#[test] +fn bypass_is_the_input_delayed_by_the_latency() { + let mut e = engine(Settings { + bypass: true, + drive: 12., + ..Settings::default() + }); + let latency = e.latency_samples() as usize; + let x: Vec = (0..4800) + .map(|i| (0.9 * (TAU * 50. * i as f64 / RATE as f64).sin()) as f32 + 0.5) + .collect(); + let out: Vec = x + .iter() + .map(|&v| e.process_sample([v, v], Monitor::Quench)[0]) + .collect(); + for i in latency..x.len() { + assert_eq!(out[i], x[i - latency]); + } +} + +#[test] +fn mono_works_and_the_shape_never_passes_its_limit() { + let mut e = Engine::new(44100., 1).unwrap(); + e.set_settings(Settings { + drive: 12., + ..Settings::default() + }) + .unwrap(); + for i in 0..44100 { + let v = (TAU * 55. * i as f64 / 44100.).sin() as f32; + let o = e.process_sample([v, 0.], Monitor::Quench); + assert!(o[0].abs() <= 0.9661 && o[1] == 0.); + } + for knee in [0., 0.3, 1.] { + for h in [-5., -1., -0.2, 0., 0.5, 0.99, 3.] { + let y = shape(h, 0.8, knee); + assert!(y.abs() <= 0.8 && y * h >= 0.); + } + } + assert_eq!(shape(0.5, 0.8, 0.), 0.5); + assert_eq!(shape(0.5, 0., 0.), 0.); +} + +#[test] +fn settings_are_validated_and_serialise() { + assert!(Settings::default().validate().is_ok()); + for bad in [ + Settings { + drive: -1., + ..Settings::default() + }, + Settings { + ceiling: 0.5, + ..Settings::default() + }, + Settings { + split: 20., + ..Settings::default() + }, + Settings { + hold: f32::NAN, + ..Settings::default() + }, + ] { + assert!(bad.validate().is_err()); + assert!(Engine::new(RATE, 2).unwrap().set_settings(bad).is_err()); + } + let s = Settings { + drive: 9., + soft: 40., + ..Settings::default() + }; + let json = serde_json::to_string(&s).unwrap(); + assert_eq!(serde_json::from_str::(&json).unwrap(), s); + assert!(serde_json::from_str::(r#"{"drive":0,"oops":1}"#).is_err()); +} diff --git a/plugins/quench/core/tests/realtime.rs b/plugins/quench/core/tests/realtime.rs new file mode 100644 index 0000000..9d23878 --- /dev/null +++ b/plugins/quench/core/tests/realtime.rs @@ -0,0 +1,65 @@ +use quench_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_settings_monitors_and_analysis_do_not_allocate_or_free() { + let mut engine = Engine::new(48000., 2).unwrap(); + let mut settings = Settings { + drive: 12., + ..Settings::default() + }; + engine.set_settings(settings).unwrap(); + let monitors = [Monitor::Quench, Monitor::Plain, Monitor::Delta]; + CHECK.with(|v| v.set(true)); + for i in 0..96000 { + if i % 4800 == 0 { + let step = i / 4800; + settings.split = 60. + 20. * (step % 6) as f32; + settings.hold = 20. + 40. * (step % 3) as f32; + settings.soft = 25. * (step % 5) as f32; + settings.ceiling = -3. * (step % 3) as f32; + settings.bypass = step % 7 == 6; + engine.set_settings(settings).unwrap(); + } + let t = i as f32 / 48000.; + let x = (std::f32::consts::TAU * 45. * t).sin() * 0.5 + + (std::f32::consts::TAU * 733. * t).sin() * 0.5; + std::hint::black_box(engine.process_sample([x, -x], monitors[(i / 9600) % 3])); + if i % 1600 == 0 { + 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/plugins/quench/plugin/Cargo.lock b/plugins/quench/plugin/Cargo.lock new file mode 100644 index 0000000..0c34360 --- /dev/null +++ b/plugins/quench/plugin/Cargo.lock @@ -0,0 +1,2381 @@ +# 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.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ff2a8126fff0f8c839080d619675a585e61720bb12d2baceae45769fd66f155" +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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "common-dsp" +version = "0.1.0" + +[[package]] +name = "common-ui" +version = "0.1.0" +dependencies = [ + "egui", +] + +[[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.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a" +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.5", +] + +[[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-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d5490aaf8f1d7cf7688dfa9b0ce07900e168852c45cd2c03f534dfd27cfd0b" +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 = "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 = "quench" +version = "0.1.0" +dependencies = [ + "clap-sys", + "crossbeam-queue", + "nih_plug", + "nih_plug_egui", + "quench-core", + "quench-ui", + "serde_json", +] + +[[package]] +name = "quench-core" +version = "0.1.0" +dependencies = [ + "common-dsp", + "serde", +] + +[[package]] +name = "quench-ui" +version = "0.1.0" +dependencies = [ + "common-dsp", + "common-ui", + "egui", + "quench-core", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "580c875845b41ecb1564ad1ae4703044cef774145e3d7a94dc2b3fa7c97868fd" +dependencies = [ + "bitflags 2.13.2", +] + +[[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 = "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 = "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.18+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec6e78bd70db9420d8d212fa9dc97f116448a1ceff5d00eabb991d407e21396" +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 = "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.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c" +dependencies = [ + "cfg-if", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e14e0327cb5368ad60ccb22d04a9827a693c95a926d8d125e9fa39e0baec501" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d179596ff5ec0f6de4db64d54011bde6289cc49603d3a6e4ce3f329207ab88" +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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3b93199df0ab874cdf0c6a94f0a0dc3423fe41f1cafb62b8bd7b1deb277aef" +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/quench/plugin/Cargo.toml b/plugins/quench/plugin/Cargo.toml new file mode 100644 index 0000000..076cbef --- /dev/null +++ b/plugins/quench/plugin/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "quench" +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" } +quench-core = { path = "../core" } +quench-ui = { path = "../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/quench/plugin/src/lib.rs b/plugins/quench/plugin/src/lib.rs new file mode 100644 index 0000000..b8e3a61 --- /dev/null +++ b/plugins/quench/plugin/src/lib.rs @@ -0,0 +1,204 @@ +//! 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::{QuenchParams, FLOATS}; +use quench_core::{AnalyzerFrame, Engine, Monitor}; +use std::sync::{ + atomic::{AtomicU8, Ordering}, + Arc, +}; + +struct Shared { + analyzer: ArrayQueue, + /// The editor's listen choice: 0 Quench, 1 Plain, 2 Delta. + monitor: AtomicU8, +} +fn encode(m: Monitor) -> u8 { + match m { + Monitor::Quench => 0, + Monitor::Plain => 1, + Monitor::Delta => 2, + } +} +fn decode(v: u8) -> Monitor { + match v { + 1 => Monitor::Plain, + 2 => Monitor::Delta, + _ => Monitor::Quench, + } +} +struct EditorData { + ui: quench_ui::State, + gestures: [bool; FLOATS], +} +pub struct Quench { + params: Arc, + shared: Arc, + engine: Option, + publish_count: usize, + publish_interval: usize, +} +impl Default for Quench { + fn default() -> Self { + Self { + params: Arc::new(QuenchParams::default()), + shared: Arc::new(Shared { + analyzer: ArrayQueue::new(2), + monitor: AtomicU8::new(0), + }), + engine: None, + publish_count: 0, + publish_interval: 1600, + } + } +} +impl Plugin for Quench { + const NAME: &'static str = "Quench"; + const VENDOR: &'static str = "Bass Foundry"; + 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 = false; + 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(), + EditorData { + ui: quench_ui::State::default(), + gestures: [false; FLOATS], + }, + |ctx, data| { + data.ui.monitor = Monitor::Quench; + quench_ui::style(ctx); + }, + move |ctx, setter, data| { + while let Some(frame) = shared.analyzer.pop() { + data.ui.accept_frame(frame); + } + let before = params.settings(); + let mut edited = before; + nih_plug_egui::resizable_window::ResizableWindow::new("quench-editor") + .min_size(egui::vec2( + quench_ui::WIDTH as f32 - 80., + quench_ui::HEIGHT as f32 - 40., + )) + .show(ctx, ¶ms.editor, |ui| { + quench_ui::show(ui, &mut edited, &mut data.ui) + }); + params.apply( + setter, + &before, + &edited, + data.ui.gesture_active, + &mut data.gestures, + ); + // The monitor is a listen choice only: never saved and never left on after + // the editor closes. + shared + .monitor + .store(encode(data.ui.monitor), Ordering::Release); + ctx.request_repaint_after(std::time::Duration::from_millis(40)); + }, + ) + } + 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.shared.monitor.store(0, Ordering::Release); + self.publish_interval = (config.sample_rate / 30.) as usize; + self.engine = Some(engine); + true + } + fn reset(&mut self) { + if let Some(e) = self.engine.as_mut() { + e.reset(); + } + self.publish_count = 0; + } + 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 monitor = if self.params.editor.is_open() { + decode(self.shared.monitor.load(Ordering::Acquire)) + } else { + Monitor::Quench + }; + 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); + for (c, value) in sample.into_iter().enumerate() { + *value = out[c]; + } + self.publish_count += 1; + if self.publish_count >= self.publish_interval { + let _ = self.shared.analyzer.force_push(engine.take_analyzer()); + self.publish_count = 0; + } + } + // The oversampling filters must drain after input stops. + ProcessStatus::KeepAlive + } +} +impl ClapPlugin for Quench { + const CLAP_ID: &'static str = "io.narl.bass-foundry.quench"; + const CLAP_DESCRIPTION: Option<&'static str> = + Some("Clips the top as hard as you like while the sub passes clean"); + const CLAP_MANUAL_URL: Option<&'static str> = None; + const CLAP_SUPPORT_URL: Option<&'static str> = None; + const CLAP_FEATURES: &'static [ClapFeature] = &[ + ClapFeature::AudioEffect, + ClapFeature::Distortion, + ClapFeature::Limiter, + ClapFeature::Mastering, + ClapFeature::Stereo, + ClapFeature::Mono, + ]; +} +nih_export_clap!(Quench); diff --git a/plugins/quench/plugin/src/params.rs b/plugins/quench/plugin/src/params.rs new file mode 100644 index 0000000..ec94683 --- /dev/null +++ b/plugins/quench/plugin/src/params.rs @@ -0,0 +1,108 @@ +use nih_plug::prelude::*; +use nih_plug_egui::EguiState; +use quench_core::{Settings, CEILING_MIN, DRIVE_MAX, HOLD_MAX, HOLD_MIN, SPLIT_MAX, SPLIT_MIN}; +use std::sync::Arc; + +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 skewed(name: &str, v: f32, min: f32, max: f32, unit: &'static str) -> FloatParam { + FloatParam::new( + name, + v, + FloatRange::Skewed { + min, + max, + factor: FloatRange::skew_factor(-1.), + }, + ) + .with_unit(unit) +} +/// Continuous controls, in the order of the gesture slots. +pub const FLOATS: usize = 5; + +#[derive(Params)] +pub struct QuenchParams { + #[persist = "editor"] + pub editor: Arc, + #[id = "drive"] + pub drive: FloatParam, + #[id = "ceiling"] + pub ceiling: FloatParam, + #[id = "split"] + pub split: FloatParam, + #[id = "hold"] + pub hold: FloatParam, + #[id = "soft"] + pub soft: FloatParam, + #[id = "bypass"] + pub bypass: BoolParam, +} +impl Default for QuenchParams { + fn default() -> Self { + let d = Settings::default(); + Self { + editor: EguiState::from_size(quench_ui::WIDTH, quench_ui::HEIGHT), + drive: linear("Drive", d.drive, 0., DRIVE_MAX, " dB"), + ceiling: linear("Ceiling", d.ceiling, CEILING_MIN, 0., " dB"), + split: skewed("Split", d.split, SPLIT_MIN, SPLIT_MAX, " Hz"), + hold: skewed("Hold", d.hold, HOLD_MIN, HOLD_MAX, " ms"), + soft: linear("Soft", d.soft, 0., 100., " %"), + bypass: BoolParam::new("Bypass", false).make_bypass(), + } + } +} +impl QuenchParams { + pub fn settings(&self) -> Settings { + Settings { + drive: self.drive.value(), + ceiling: self.ceiling.value(), + split: self.split.value(), + hold: self.hold.value(), + soft: self.soft.value(), + bypass: self.bypass.value(), + } + } + fn floats(&self) -> [&FloatParam; FLOATS] { + [ + &self.drive, + &self.ceiling, + &self.split, + &self.hold, + &self.soft, + ] + } + fn values(s: &Settings) -> [f32; FLOATS] { + [s.drive, s.ceiling, s.split, s.hold, s.soft] + } + /// Pushes every changed editor value to the host. Continuous controls hold one gesture + /// while the pointer is down; switches are set in a single gesture. + pub fn apply( + &self, + setter: &ParamSetter, + old: &Settings, + new: &Settings, + dragging: bool, + open: &mut [bool; FLOATS], + ) { + let pairs = Self::values(old).into_iter().zip(Self::values(new)); + for (i, (p, (a, b))) in self.floats().into_iter().zip(pairs).enumerate() { + if a != b { + if !open[i] { + open[i] = true; + setter.begin_set_parameter(p); + } + setter.set_parameter(p, b); + } + if !dragging && open[i] { + setter.end_set_parameter(p); + open[i] = false; + } + } + if new.bypass != old.bypass { + setter.begin_set_parameter(&self.bypass); + setter.set_parameter(&self.bypass, new.bypass); + setter.end_set_parameter(&self.bypass); + } + } +} diff --git a/plugins/quench/plugin/tests/host.rs b/plugins/quench/plugin/tests/host.rs new file mode 100644 index 0000000..39347e4 --- /dev/null +++ b/plugins/quench/plugin/tests/host.rs @@ -0,0 +1,205 @@ +//! 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 = &quench::clap_entry; + assert!(entry.init.unwrap()(c"quench.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).name), c"Quench"); + assert_eq!( + CStr::from_ptr((*descriptor).id), + c"io.narl.bass-foundry.quench" + ); + 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("soft".into(), ParamValue::F32(40.)); + envelope.params.insert("hold".into(), ParamValue::F32(100.)); + envelope.params.insert("split".into(), ParamValue::F32(90.)); + 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() + ); + for rate in [44100., 48000., 96000.] { + assert!(p.activate.unwrap()(plugin, rate, 1, 512)); + let delay = (*latency).get.unwrap()(plugin) as usize; + // Only the 4× oversampling filters: under a millisecond at every rate. + assert_eq!(delay, 37); + assert!(p.start_processing.unwrap()(plugin)); + let total = delay + 9600; + let mut position = 0; + let (mut energy_in, mut energy_out) = (0f64, 0f64); + while position < total { + let frames = [1, 127, 512, 31][position % 4].min(total - position); + // A 1 kHz tone well under the ceiling passes unchanged. + let tone = |n: usize| { + 0.25 * (std::f64::consts::TAU * 1000. * n as f64 / rate).sin() as f32 + }; + let mut input_left = [0f32; 512]; + let mut input_right = [0f32; 512]; + for i in 0..frames { + input_left[i] = tone(position + i); + input_right[i] = -tone(position + i); + } + 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 n = position + i; + if n < delay + 4800 { + // The tone's onset rings in the filters, but never past the ceiling. + assert!(left[i].abs() <= 0.9661, "{}", left[i]); + } else if n > delay + 4800 { + energy_in += (tone(n - delay) as f64).powi(2); + energy_out += (left[i] as f64).powi(2) + (right[i] as f64).powi(2); + } + } + position += frames; + } + let ratio_db = 10. * (energy_out / (2. * energy_in)).log10(); + assert!(ratio_db.abs() < 0.2, "{ratio_db}"); + p.stop_processing.unwrap()(plugin); + p.deactivate.unwrap()(plugin); + } + p.destroy.unwrap()(plugin); + entry.deinit.unwrap()(); + } +} diff --git a/plugins/quench/ui/Cargo.toml b/plugins/quench/ui/Cargo.toml new file mode 100644 index 0000000..ed19faf --- /dev/null +++ b/plugins/quench/ui/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "quench-ui" +version = "0.1.0" +edition.workspace = true +license.workspace = true + +[dependencies] +egui = "0.31.1" +quench-core = { path = "../core" } +common-ui = { path = "../../common/ui" } +common-dsp = { path = "../../common/dsp" } diff --git a/plugins/quench/ui/src/lib.rs b/plugins/quench/ui/src/lib.rs new file mode 100644 index 0000000..207ea1e --- /dev/null +++ b/plugins/quench/ui/src/lib.rs @@ -0,0 +1,327 @@ +//! Quench editor, shared by the CLAP wrapper and the headless renderer. +use common_ui::*; +use egui::{self, Color32, FontId, Pos2, Rect, RichText, Sense, Stroke, Vec2}; +use quench_core::{ + AnalyzerFrame, Monitor, Settings, CEILING_MIN, DRIVE_MAX, HOLD_MAX, HOLD_MIN, SCOPE, SPLIT_MAX, + SPLIT_MIN, +}; + +pub use common_ui::style; +pub const WIDTH: u32 = 980; +pub const HEIGHT: u32 = 640; + +pub struct State { + pub analyzer: AnalyzerFrame, + /// What the output plays. Not saved: the wrapper resets it when the editor opens. + pub monitor: Monitor, + /// True while a pointer button is held, so knob drags become one host gesture. + pub gesture_active: bool, + pub help: String, +} +impl Default for State { + fn default() -> Self { + Self { + analyzer: AnalyzerFrame::default(), + monitor: Monitor::Quench, + gesture_active: false, + help: DEFAULT_HELP.into(), + } + } +} +const DEFAULT_HELP: &str = + "Put Quench last on the master or the bass bus. Raise Drive until the top clips as hard as you want."; +impl State { + pub fn accept_frame(&mut self, frame: AnalyzerFrame) { + self.analyzer = frame; + } +} +fn hint(r: &egui::Response, state: &mut State, text: &str) { + if r.hovered() || r.has_focus() { + state.help = text.into(); + } +} +#[allow(clippy::too_many_arguments)] +fn knob( + ui: &mut egui::Ui, + state: &mut State, + name: &str, + value: &mut f32, + range: std::ops::RangeInclusive, + default: f32, + unit: &str, + help: &str, +) { + ui.vertical_centered(|ui| { + let r = rotary(ui, name, value, range, default, unit, 72., ACCENT); + hint(&r, state, help); + }); +} + +/// The selected output with the ceiling, the sub band, and the room the sub leaves the top. +fn scope(ui: &mut egui::Ui, settings: &Settings, frame: &AnalyzerFrame, height: f32) { + let (rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), height), Sense::hover()); + let p = ui.painter_at(rect); + p.rect_filled(rect, 2., SCREEN); + p.text( + rect.left_top() + Vec2::new(10., 8.), + egui::Align2::LEFT_TOP, + "OUTPUT · SUB · ROOM FOR THE TOP", + FontId::proportional(11.), + MUTED, + ); + p.text( + rect.right_top() + Vec2::new(-12., 8.), + egui::Align2::RIGHT_TOP, + format!( + "{:.0} ms", + SCOPE as f32 * 8. / frame.sample_rate.max(1.) * 1000. + ), + FontId::proportional(11.), + MUTED, + ); + let area = Rect::from_min_max( + rect.left_top() + Vec2::new(12., 28.), + rect.right_bottom() - Vec2::new(12., 10.), + ); + let mid = area.center().y; + let scale = area.height() * 0.5 / 1.08; + let y = |v: f32| mid - v.clamp(-1.08, 1.08) * scale; + let x = |i: usize| area.left() + area.width() * i as f32 / (SCOPE - 1) as f32; + p.line_segment( + [Pos2::new(area.left(), mid), Pos2::new(area.right(), mid)], + Stroke::new(1.0_f32, Color32::from_rgb(32, 35, 40)), + ); + let c = 10f32.powf(settings.ceiling / 20.); + for v in [c, -c] { + p.line_segment( + [Pos2::new(area.left(), y(v)), Pos2::new(area.right(), y(v))], + Stroke::new(1.0_f32, ACCENT.gamma_multiply(0.6)), + ); + } + p.text( + Pos2::new(area.right() - 4., y(c) - 3.), + egui::Align2::RIGHT_BOTTOM, + format!("CEILING {:.1} dB", settings.ceiling), + FontId::proportional(10.), + ACCENT, + ); + for i in 0..SCOPE { + let (lo, hi) = (frame.scope_min[i], frame.scope_max[i]); + let top = y(hi); + let bottom = y(lo).max(top + 1.); + p.line_segment( + [Pos2::new(x(i), top), Pos2::new(x(i), bottom)], + Stroke::new(2.0_f32, STEEL.gamma_multiply(0.55)), + ); + } + // Where the output may go: the sub plus or minus the top's room. + for sign in [1., -1.] { + let line: Vec = (0..SCOPE) + .map(|i| Pos2::new(x(i), y(frame.scope_sub[i] + sign * frame.scope_room[i]))) + .collect(); + p.add(egui::Shape::line(line, Stroke::new(1.0_f32, MUTED))); + } + let sub: Vec = (0..SCOPE) + .map(|i| Pos2::new(x(i), y(frame.scope_sub[i]))) + .collect(); + p.add(egui::Shape::line(sub, Stroke::new(2.0_f32, FOCUS))); + p.rect_stroke( + rect, + 2., + Stroke::new(1.0_f32, EDGE), + egui::StrokeKind::Inside, + ); +} + +pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) { + backdrop(ui); + state.help = DEFAULT_HELP.into(); + state.gesture_active = ui.input(|i| i.pointer.any_down()); + ui.add_space(2.); + header(ui, "QUENCH", "SUB-SAFE CLIPPER", |ui| { + let r = ui + .selectable_label(settings.bypass, RichText::new("BYPASS").size(13.)) + .on_hover_text("Crossfade to the input, delayed by the latency."); + if r.clicked() { + settings.bypass = !settings.bypass; + } + ui.separator(); + for (monitor, label, text) in [ + ( + Monitor::Delta, + "DELTA", + "Hear what Quench takes away. Not saved.", + ), + ( + Monitor::Plain, + "PLAIN", + "Hear a plain clipper at the same settings, to compare the low end. Not saved.", + ), + (Monitor::Quench, "QUENCH", "Hear Quench."), + ] { + let r = ui + .selectable_label(state.monitor == monitor, RichText::new(label).size(13.)) + .on_hover_text(text); + if r.clicked() { + state.monitor = monitor; + } + } + caption(ui, "LISTEN"); + }); + ui.add_space(6.); + let frame = state.analyzer; + ui.horizontal_top(|ui| { + panel(ui, |ui| { + ui.set_width(250.); + ui.set_min_height(TOP); + ui.vertical(|ui| readings(ui, settings, &frame)); + }); + ui.add_space(4.); + panel(ui, |ui| { + ui.set_width(ui.available_width()); + scope(ui, settings, &frame, TOP); + }); + }); + ui.add_space(6.); + panel(ui, |ui| { + ui.set_width(ui.available_width()); + ui.vertical(|ui| controls(ui, settings, state)); + }); + ui.separator(); + ui.label(RichText::new(&state.help).color(MUTED)); +} +const TOP: f32 = 330.; + +fn readings(ui: &mut egui::Ui, settings: &Settings, frame: &AnalyzerFrame) { + caption(ui, "LOW END CHANGED BY"); + let has_sub = frame.low_quench.is_finite(); + ui.label( + RichText::new(if has_sub { + format!("{:.0} dB", frame.low_quench) + } else { + "—".into() + }) + .font(FontId::new(44., egui::FontFamily::Name("medium".into()))) + .color(if has_sub { TEXT } else { DIM }), + ); + ui.label( + RichText::new(if has_sub { + format!("plain clip: {:.0} dB", frame.low_plain) + } else { + "no sub playing".into() + }) + .monospace() + .size(13.) + .color(MUTED), + ); + ui.add_space(12.); + readout( + ui, + "TOP CLIPPED BY", + if frame.top_clip >= 40. { + "no room left".into() + } else if frame.top_clip > 0. { + format!("{:.1} dB", frame.top_clip) + } else { + "not clipping".into() + }, + ACCENT, + ); + let room = settings.ceiling - frame.sub_peak; + readout( + ui, + "SUB PEAK · ROOM LEFT", + if frame.sub_peak > -119. { + format!("{:.1} dBFS · {:.1} dB", frame.sub_peak, room.max(0.)) + } else { + "—".into() + }, + FOCUS, + ); + if frame.sub_peak > settings.ceiling - 1. { + ui.label( + RichText::new( + "The sub alone reaches the ceiling: the top has no room and the safety clip \ + cuts the sub. Lower Drive or the sub.", + ) + .size(12.) + .color(ACCENT), + ); + ui.add_space(6.); + } + ui.add_space(4.); + meter(ui, "IN", frame.input_peak, STEEL, 150.); + meter(ui, "OUT", frame.output_peak, ACCENT, 150.); + ui.add_space(6.); + ui.label( + RichText::new(format!( + "safety clip {} · {:.1} ms latency", + if frame.safety > 0. { + format!("{:.2} dB", frame.safety) + } else { + "idle".into() + }, + frame.latency as f32 / frame.sample_rate.max(1.) * 1000. + )) + .size(12.) + .color(MUTED), + ); +} + +fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) { + let d = Settings::default(); + ui.columns(5, |c| { + knob( + &mut c[0], + state, + "Drive", + &mut settings.drive, + 0. ..=DRIVE_MAX, + d.drive, + " dB", + "Gain into the clipper. The sub rises with it: watch the room it leaves.", + ); + knob( + &mut c[1], + state, + "Ceiling", + &mut settings.ceiling, + CEILING_MIN..=0., + d.ceiling, + " dB", + "Highest output peak. The sub keeps its level; the top gets what is left.", + ); + knob( + &mut c[2], + state, + "Split", + &mut settings.split, + SPLIT_MIN..=SPLIT_MAX, + d.split, + " Hz", + "Below this the sub passes without clipping. Set it above the highest sub note, \ + or at Anvil's Split.", + ); + knob( + &mut c[3], + state, + "Hold", + &mut settings.hold, + HOLD_MIN..=HOLD_MAX, + d.hold, + " ms", + "How long the sub's peak keeps its room. Longer than half a period of the lowest \ + note keeps the top from pumping with the sub.", + ); + knob( + &mut c[4], + state, + "Soft", + &mut settings.soft, + 0. ..=100., + d.soft, + " %", + "Knee of the top's clip. 0 % is a hard clip, 100 % a soft curve.", + ); + }); +} diff --git a/tools/editor-check/Cargo.lock b/tools/editor-check/Cargo.lock index 50d1f37..a97da10 100644 --- a/tools/editor-check/Cargo.lock +++ b/tools/editor-check/Cargo.lock @@ -186,6 +186,8 @@ dependencies = [ "patina-ui", "planish-core", "planish-ui", + "quench-core", + "quench-ui", "rivet-app", "rivet-core", "rivet-midi", @@ -513,6 +515,24 @@ version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" +[[package]] +name = "quench-core" +version = "0.1.0" +dependencies = [ + "common-dsp", + "serde", +] + +[[package]] +name = "quench-ui" +version = "0.1.0" +dependencies = [ + "common-dsp", + "common-ui", + "egui", + "quench-core", +] + [[package]] name = "quote" version = "1.0.47" diff --git a/tools/editor-check/Cargo.toml b/tools/editor-check/Cargo.toml index a5fe513..9986c3f 100644 --- a/tools/editor-check/Cargo.toml +++ b/tools/editor-check/Cargo.toml @@ -29,5 +29,7 @@ planish-core = { path = "../../plugins/planish/core" } planish-ui = { path = "../../plugins/planish/ui" } braze-core = { path = "../../plugins/braze/core" } braze-ui = { path = "../../plugins/braze/ui" } +quench-core = { path = "../../plugins/quench/core" } +quench-ui = { path = "../../plugins/quench/ui" } weld-core = { path = "../../plugins/weld/core" } weld-ui = { path = "../../plugins/weld/ui" } diff --git a/tools/editor-check/src/main.rs b/tools/editor-check/src/main.rs index 07e12a7..8fbff15 100644 --- a/tools/editor-check/src/main.rs +++ b/tools/editor-check/src/main.rs @@ -12,13 +12,13 @@ fn main() { .unwrap_or("rivet"); if ![ "rivet", "patina", "anvil", "crucible", "lathe", "temper", "weld", "knurl", "planish", - "braze", + "braze", "quench", ] .contains(&plugin) { eprintln!( - "Use --plugin rivet, patina, anvil, crucible, lathe, temper, weld, knurl, planish \ - or braze" + "Use --plugin rivet, patina, anvil, crucible, lathe, temper, weld, knurl, planish, \ + braze or quench" ); std::process::exit(2); } @@ -32,7 +32,10 @@ fn main() { let knurl = plugin == "knurl"; let planish = plugin == "planish"; let braze = plugin == "braze"; - let (width, height) = if braze { + let quench = plugin == "quench"; + let (width, height) = if quench { + (quench_ui::WIDTH, quench_ui::HEIGHT) + } else if braze { (braze_ui::WIDTH, braze_ui::HEIGHT) } else if planish { (planish_ui::WIDTH, planish_ui::HEIGHT) @@ -229,6 +232,27 @@ fn main() { } braze_state.accept_frame(engine.take_analyzer()); } + let mut quench_settings = quench_core::Settings { + drive: 9., + ..Default::default() + }; + let mut quench_state = quench_ui::State::default(); + if quench { + // A 49 Hz sub under a distorted saw top two octaves up, driven 9 dB into the clipper. + let mut engine = quench_core::Engine::new(48000., 2).unwrap(); + engine.set_settings(quench_settings).unwrap(); + let mut saw = common_dsp::Saw::new(48000., 196.); + for i in 0..48000 { + let t = i as f32 / 48000.; + let sub = 0.18 * (std::f32::consts::TAU * 49. * t).sin(); + let top = (saw.sample() * 3.).tanh() * 0.3; + engine.process_sample([sub + top, sub + top], quench_core::Monitor::Quench); + if i % 1600 == 0 { + quench_state.accept_frame(engine.take_analyzer()); + } + } + quench_state.accept_frame(engine.take_analyzer()); + } let mut weld_settings = weld_core::Settings::default(); let mut weld_state = weld_ui::State::default(); if weld { @@ -358,7 +382,9 @@ fn main() { let session_motif = session.motifs[0].id; shared.replace(session).unwrap(); let ctx = egui::Context::default(); - if braze { + if quench { + quench_ui::style(&ctx); + } else if braze { braze_ui::style(&ctx); } else if planish { planish_ui::style(&ctx); @@ -399,7 +425,9 @@ fn main() { }, |ui| { egui::CentralPanel::default().show(ui, |ui| { - if braze { + if quench { + quench_ui::show(ui, &mut quench_settings, &mut quench_state); + } else if braze { braze_ui::show(ui, &mut braze_settings, &mut braze_state); } else if planish { planish_ui::show(ui, &mut planish_settings, &mut planish_state); diff --git a/website/src/data/plugins.ts b/website/src/data/plugins.ts index ac96105..01dd806 100644 --- a/website/src/data/plugins.ts +++ b/website/src/data/plugins.ts @@ -199,6 +199,21 @@ const COPY: Record = { latency: '≈ 67 ms', tracksNotes: true, }, + quench: { + role: 'Sub-safe clipper', + metal: 'Quenching hardens hot metal in one fast step.', + problem: + 'Hard-clipped masters clip the sub with the top: near the sub’s peaks only one side of the top clips, so the sub loses level and the low end fills with products that were not in the mix.', + points: [ + 'The sub below Split passes unclipped; the top is clipped in the room the sub leaves under the ceiling.', + 'What the clip removes is high-passed, so its products stay out of the low band.', + 'Compare against a plain clipper, or hear only what Quench takes away.', + 'A meter shows how much the low end changed, for Quench and for a plain clip.', + ], + loadOn: 'Master or bass bus, last', + latency: '< 1 ms', + tracksNotes: false, + }, }; export interface Plugin extends Copy {