diff --git a/.gitea/workflows/check.yml b/.gitea/workflows/check.yml index a546b90..0a91601 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] + plugin: [rivet, patina, anvil, crucible, lathe, temper, weld, knurl] steps: - uses: https://github.com/actions/checkout@v4 with: @@ -61,6 +61,7 @@ jobs: cargo run --manifest-path tools/editor-check/Cargo.toml --locked -- target/editor/lathe.png --plugin lathe cargo run --manifest-path tools/editor-check/Cargo.toml --locked -- target/editor/temper.png --plugin temper cargo run --manifest-path tools/editor-check/Cargo.toml --locked -- target/editor/weld.png --plugin weld + cargo run --manifest-path tools/editor-check/Cargo.toml --locked -- target/editor/knurl.png --plugin knurl - uses: https://github.com/actions/upload-artifact@v3-node20 with: name: editor-previews @@ -91,6 +92,7 @@ jobs: python3 scripts/build-plugin.py --plugin lathe --target x86_64-pc-windows-gnu python3 scripts/build-plugin.py --plugin temper --target x86_64-pc-windows-gnu python3 scripts/build-plugin.py --plugin weld --target x86_64-pc-windows-gnu + python3 scripts/build-plugin.py --plugin knurl --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 e25073a..9ea3625 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*"] + tags: ["rivet-v*", "patina-v*", "anvil-v*", "crucible-v*", "lathe-v*", "temper-v*", "weld-v*", "knurl-v*"] permissions: contents: read diff --git a/Cargo.lock b/Cargo.lock index b014de6..121bd91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -170,6 +170,25 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "knurl-core" +version = "0.1.0" +dependencies = [ + "common-dsp", + "serde", + "serde_json", +] + +[[package]] +name = "knurl-ui" +version = "0.1.0" +dependencies = [ + "common-dsp", + "common-ui", + "egui", + "knurl-core", +] + [[package]] name = "lathe-core" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 7b1f391..9f38650 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [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/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"] +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/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"] [workspace.package] edition = "2021" @@ -24,5 +24,8 @@ opt-level = 2 opt-level = 2 [profile.dev.package.temper-core] opt-level = 2 +[profile.dev.package.knurl-core] +opt-level = 2 + [profile.dev.package.weld-core] opt-level = 2 diff --git a/README.md b/README.md index 07e5ba9..ba3021d 100644 --- a/README.md +++ b/README.md @@ -25,16 +25,18 @@ note. Each plugin is a separate product with its own version, guide and release. | **[Lathe](docs/lathe/README.md)** | EQ with one band on each harmonic of the played note: a cut on harmonic 3 stays on harmonic 3 | Bass, after the synth | ≈ 67 ms | [Guide](docs/lathe/README.md) · [Changes](docs/lathe/CHANGELOG.md) | | **[Temper](docs/temper/README.md)** | Distortion that drives every note the same: level and tone filters follow the note | Bass | ≈ 67 ms + oversampling | [Guide](docs/temper/README.md) · [Changes](docs/temper/CHANGELOG.md) | | **[Weld](docs/weld/README.md)** | Phase-aligns one bass layer to another layer through the sidechain, so the layers sum in the mids instead of cancelling | One layer's track, other layer as sidechain | none | [Guide](docs/weld/README.md) · [Changes](docs/weld/CHANGELOG.md) | +| **[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) | Plugins with latency report it to the host, so FL Studio compensates the delay. -Anvil, Lathe and Temper detect pitch from the audio. They can also take the +Anvil, Lathe, Temper and Knurl detect pitch from the audio. They can also take the pitch from MIDI. The names come from metalwork: a **rivet** joins pieces; **patina** changes a surface and keeps its character; an **anvil** is the solid base that everything 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. +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. 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*. @@ -55,7 +57,7 @@ finished. Hosts list the plugins under the vendor name *Bass Foundry*. Weld editor
Weld: layer phase alignment - + Knurl editor
Knurl: cycle texture @@ -115,7 +117,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` and `weld`. Builds go to `dist//.clap`. No +`lathe`, `temper`, `weld` and `knurl`. Builds go to `dist//.clap`. No script installs anything into a DAW. [Development and testing](docs/development.md) has the full command list. @@ -152,6 +154,7 @@ plugins/ lathe/ core · ui · plugin temper/ core · ui · plugin weld/ core · ui · plugin + knurl/ 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/development.md b/docs/development.md index 4466d70..7c256e0 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 and Temper, 4× oversampling, meters and WAV files. + Lathe, Temper and Knurl, 4× oversampling, 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 @@ -44,7 +44,8 @@ python3 scripts/check-plugin.py --plugin python3 -m unittest discover -s scripts/tests -v ``` -Products: `rivet`, `patina`, `anvil`, `crucible`, `lathe`, `temper`, `weld`. +Products: `rivet`, `patina`, `anvil`, `crucible`, `lathe`, `temper`, `weld`, +`knurl`. `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 @@ -142,6 +143,7 @@ identify plugins by CLAP ID, so the vendor name does not affect saved projects. | Lathe | (new) | `io.storyboard.harmonic-eq` | | Temper | (new) | `io.storyboard.tracked-distortion` | | Weld | (new) | `io.storyboard.layer-align` | +| Knurl | (new) | `io.storyboard.cycle-texture` | The `io.storyboard.` prefix is historical and stays, because changing a CLAP ID breaks existing projects. Rivet keeps `story-session-v1` and its session diff --git a/docs/ideas.md b/docs/ideas.md index a794618..5209880 100644 --- a/docs/ideas.md +++ b/docs/ideas.md @@ -75,6 +75,14 @@ delay), so the layers sum instead of cancel. A display shows how much level the cancellation cost and how much Weld recovered. Anvil solves the sub band; Weld solves the mids. See [Weld](weld/README.md). +### Knurl — cycle texture (problems 11, 13) + +Follows the played note and treats every cycle on its own: a repeating level +pattern (in-tune subharmonics), random level and timing per cycle, and a noise +burst per cycle tuned to the note. Amounts are counted in cycles, so the texture +is the same on every note. Slow per-harmonic drift is postponed. See +[Knurl](knurl/README.md). + ## Ideas ### Ballast — switch-up loudness leveller (problem 2) @@ -113,27 +121,11 @@ bars 2–4". No name yet. ## Texture ideas -These ideas target problems 11–15. Most of them reuse the note tracker in +These ideas target problems 11–15; Knurl, the first of them, is released above. +Most of them reuse the note tracker in `common-dsp`, so a texture setting means the same thing on every note, the same way Lathe and Temper work. -### Knurl — pitch-synchronous grit (problems 11, 13) - -Knurling presses a ridged pattern into metal. Knurl cuts the signal into single -pitch periods with the note tracker and changes each period on its own: - -- level and timing jitter per cycle; -- short noise bursts locked to the cycle; -- period doubling and tripling (a cycle pattern such as "loud, quiet, quiet"), - which gives the subharmonic rattle and growl of riddim basses; -- slow random drift of each harmonic's level and phase, which brings movement - into a printed resample without widening it. - -Rates are in cycles, not in Hz or tempo, so the grit has the same grain on F1 -and on C2. A pattern editor sets 1–16 cycles. Everything is mono-safe. Latency -is the tracker lookahead, about 67 ms. Recommended first texture build: it is -new, small, and reuses Lathe's tracking. - ### Anneal — tone and grit split (problem 12) Annealing changes how a metal behaves without changing its shape. Anneal diff --git a/docs/knurl/CHANGELOG.md b/docs/knurl/CHANGELOG.md new file mode 100644 index 0000000..342bb47 --- /dev/null +++ b/docs/knurl/CHANGELOG.md @@ -0,0 +1,14 @@ +# Knurl changelog + +## 0.1.0 + +- Introduce Knurl, texture per cycle of the played note. +- Step patterns over 1–16 cycles make subharmonics in tune with every note: + Off, Double, Triple, Fry, Gallop, Stutter, or a drawn pattern. +- Random level and timing per cycle, and a noise burst per cycle tuned to the + note (Tone, Position, Length), switchable per step. +- Same every note or Free random sequences. +- Detect pitch and note starts from the audio with about 71 ms of latency, or + follow MIDI notes sent to the effect. +- Editor shows the last eight cycles overlaid and the pattern with the playing + step. diff --git a/docs/knurl/README.md b/docs/knurl/README.md new file mode 100644 index 0000000..e770550 --- /dev/null +++ b/docs/knurl/README.md @@ -0,0 +1,86 @@ +# Knurl + +Knurl gives every cycle of the note its own level, timing and grit. Current +version **0.1.0**. + +Knurling presses a ridged pattern into metal. Riddim and dubstep basses get their +character the same way, from surface texture: rattle, growl, fry, grit. Usually +that texture comes from long chains of distortion, comb filters, formants and +resampling, and it changes from note to note. Knurl works on single cycles of the +wave instead. It follows the played note and treats each cycle on its own, and +every amount is counted in cycles, not in Hz or tempo. So the texture has the +same grain on F1 and on C2. + +![Knurl](assets/editor.png) + +## Install on Windows + +Copy `Knurl.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 Knurl in a mixer slot of the bass. Keep the font license with +redistributed packages. + +## Work with it + +1. Play the bass. The note readout shows what Knurl tracks; the scope overlays + the last eight cycles of the output, so the texture is visible cycle by cycle. +2. Choose a **Pattern**. The pattern sets the level of each cycle and repeats: + - **Double** (0, −9 dB): every second cycle quieter. This makes a sub-octave + growl, exactly in tune, about 8 dB under the fundamental. + - **Triple** (0, −12, −12 dB): a growl an octave and a fifth down. + - **Fry** (0, −14, −5, −14 dB, the default): an uneven vocal-fry rattle. + - **Gallop** and **Stutter**: rhythmic patterns over six and eight cycles. + - **Off**: every cycle at full level. +3. Draw your own pattern on the bars: drag a bar to set the level of that cycle, + double-click to reset it. **Steps** sets the length (1–16 cycles). +4. Add surface: + - **Level** gives every cycle a random level (standard deviation in dB). + - **Time** moves every cycle a little in time (percent of a period): rattle + and scrape. + - **Grit** adds a short noise burst inside every cycle. **Tone** sets its + lowest frequency in harmonics of the note, **Position** where in the cycle + it sits, and **Length** how long it is. The squares under the pattern bars + switch the burst per step. +5. **Edge** sets how soft each step change is. Low values are sharper and + buzzier; high values smoother. + +**Random**: **Same every note** (default) restarts every random sequence at each +note, so identical notes sound identical, which keeps a riddim line tight. +**Free** lets the sequences run on, so every note differs. + +**Mix** blends with the delayed dry signal; **Output** trims the level. + +### Pitch from MIDI + +For basses whose fundamental is unclear (heavy FM, formant or noise patches), +select **MIDI** and route the bass's MIDI notes to Knurl. In FL Studio, set the +bass channel's MIDI output port and Knurl's input port to the same number. +**Gate** sets the input level that counts as a note in AUDIO mode; **Snap to +semitone** rounds the detected pitch (turn it off for slides). + +## Timing and limits + +Knurl delays the audio by about 71 ms: 67 ms of pitch-detection lookahead plus +4 ms of room for the timing jitter. The delay is reported to the host for delay +compensation, so the cycle clock knows every note before its attack leaves the +plugin. The cycle clock restarts at every note start, so the pattern always +begins with step 1 on the attack. Pitch range is 30–500 Hz, one note at a time. +Between notes the effects fade out over 20 ms, so tails stay clean. + +Knurl works per cycle of the tracked pitch; it does not look for cycle borders in +the waveform. With a stable pitch that is the same thing. With heavy pitch +modulation inside a note the pattern follows the tracked pitch, which can lag a +fast bend. Both channels get the same treatment, so the effect is mono-safe. + +## How it works + +A note tracker (shared with Anvil, Lathe and Temper) detects pitch and note +starts ahead of the delayed audio. A cycle clock starts at 0 on every note and +advances by f0 / sample rate. Each cycle gets its step gain and its random level +and time offset from a hash of the cycle number, so the result is deterministic. +Step changes are raised-cosine crossfades over the last **Edge** of a cycle; +level jitter crossfades over half a cycle and time jitter over the whole cycle, +read from the delay line with cubic interpolation. The grit is white noise +through a state-variable highpass at **Tone** × f0, shaped by a sin² window and +scaled to the input level. There is no nonlinearity, so no oversampling is +needed. diff --git a/docs/knurl/assets/editor.png b/docs/knurl/assets/editor.png new file mode 100644 index 0000000..ee529b5 Binary files /dev/null and b/docs/knurl/assets/editor.png differ diff --git a/docs/releases.md b/docs/releases.md index 8cd64cb..2a2415b 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -14,6 +14,7 @@ workflows are in `.gitea/workflows/`. A plugin's version is the `version` in | Lathe | `lathe-v0.1.0` | `lathe-windows-x86_64-0.1.0.zip` | | Temper | `temper-v0.1.0` | `temper-windows-x86_64-0.1.0.zip` | | Weld | `weld-v0.1.0` | `weld-windows-x86_64-0.1.0.zip` | +| Knurl | `knurl-v0.1.0` | `knurl-windows-x86_64-0.1.0.zip` | ## Make a release diff --git a/plugins/catalog.json b/plugins/catalog.json index d00b722..cd62aac 100644 --- a/plugins/catalog.json +++ b/plugins/catalog.json @@ -114,5 +114,21 @@ "weld-core", "weld-ui" ] + }, + "knurl": { + "name": "Knurl", + "description": "Give every cycle of the note its own level, timing and grit", + "manifest": "plugins/knurl/plugin/Cargo.toml", + "library": "knurl", + "documentation": "docs/knurl", + "clap_id": "io.storyboard.cycle-texture", + "licenses": { + "plugins/common/ui/assets/OFL.txt": "licenses/Roboto-OFL.txt" + }, + "packages": [ + "common-ui", + "knurl-core", + "knurl-ui" + ] } } diff --git a/plugins/knurl/core/Cargo.toml b/plugins/knurl/core/Cargo.toml new file mode 100644 index 0000000..562b805 --- /dev/null +++ b/plugins/knurl/core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "knurl-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/knurl/core/src/lib.rs b/plugins/knurl/core/src/lib.rs new file mode 100644 index 0000000..5fb3d3f --- /dev/null +++ b/plugins/knurl/core/src/lib.rs @@ -0,0 +1,406 @@ +//! Knurl: texture per cycle of the played note. A cycle clock follows the tracked pitch and +//! restarts at every note; each cycle gets its own step gain (a repeating pattern that +//! makes subharmonics), a random level and timing, and a noise burst tuned to the note. +//! Every amount is counted in cycles, so the texture has the same grain on every note. +//! +//! The audio path is delayed by the tracker lookahead plus the timing-jitter headroom, so +//! the clock already knows each note when its attack leaves the plugin. Allocation-free +//! after `new`; mono-safe (both channels get the same treatment). +mod settings; +use common_dsp::svf::{Svf, SvfCoef}; +use common_dsp::tracker::{PitchFollower, Tracker, TrackerConfig}; +use common_dsp::{coefficient, db_to_gain, hz_to_midi}; +pub use settings::{Seed, Settings, Source, GRIT_OFF, PATTERNS, STEPS, STEP_MIN}; + +/// Cycles kept for the editor's scope, and points per cycle. +pub const SCOPE_CYCLES: usize = 8; +pub const SCOPE_POINTS: usize = 64; +/// Timing jitter can move a cycle by at most this share of the lowest period. +const JITTER_HEADROOM: f32 = 0.125; +/// Effects fade in and out with the note over this time. +const FADE_MS: f32 = 20.; +/// The grit filter is retuned this often, in samples. +const RETUNE: usize = 16; +/// Level follower for scaling the grit to the input. +const LEVEL_MS: f32 = 30.; + +#[derive(Clone, Copy, Debug)] +pub struct AnalyzerFrame { + /// Fundamental the clock follows, 0 before the first note. + pub hz: f32, + /// Sounding note, rounded; None between notes. + pub note: Option, + /// Pattern step of the newest cycle. + pub step: usize, + /// Output of the last `SCOPE_CYCLES` cycles, oldest first, each resampled to + /// `SCOPE_POINTS` points. + pub scope: [[f32; SCOPE_POINTS]; SCOPE_CYCLES], + /// True when the scope holds cycles of a sounding note. + pub scope_valid: bool, + pub input_db: f32, + pub output_db: f32, + pub latency: u32, + pub sample_rate: f32, +} +impl Default for AnalyzerFrame { + fn default() -> Self { + Self { + hz: 0., + note: None, + step: 0, + scope: [[0.; SCOPE_POINTS]; SCOPE_CYCLES], + scope_valid: false, + input_db: -120., + output_db: -120., + latency: 0, + sample_rate: 48000., + } + } +} + +/// Deterministic per-cycle randomness: a hash of (stream, note seed, cycle). +fn hash(stream: u64, seed: u64, cycle: u64) -> u64 { + let mut z = stream + .wrapping_mul(0x9E37_79B9_7F4A_7C15) + .wrapping_add(seed.wrapping_mul(0xBF58_476D_1CE4_E5B9)) + .wrapping_add(cycle.wrapping_mul(0x94D0_49BB_1331_11EB)); + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) +} +fn uniform(stream: u64, seed: u64, cycle: u64) -> f32 { + (hash(stream, seed, cycle) >> 40) as f32 / (1u64 << 24) as f32 +} +/// Approximately normal, clamped to ±3 (sum of four uniforms, unit variance). +fn normal(stream: u64, seed: u64, cycle: u64) -> f32 { + let s: f32 = (0..4).map(|i| uniform(stream, seed, cycle * 4 + i)).sum(); + ((s - 2.) * 3f32.sqrt()).clamp(-3., 3.) +} +/// Raised-cosine weight of the next cycle's value: 0 until `1 − width` of the cycle, then +/// rising to 1 at the boundary. +fn edge(frac: f32, width: f32) -> f32 { + let t = ((frac - (1. - width)) / width).clamp(0., 1.); + 0.5 - 0.5 * (std::f32::consts::PI * t).cos() +} + +const LEVEL: u64 = 1; +const TIME: u64 = 2; +const NOISE: u64 = 3; + +/// What the clock needs, delayed by the jitter headroom so it lines up with the audio. +#[derive(Clone, Copy, Default)] +struct Pitch { + hz: f32, + start: bool, + sounding: bool, +} + +pub struct Engine { + rate: f32, + channels: usize, + settings: Settings, + /// Tracker lookahead. + lookahead: usize, + /// Largest timing offset in samples, either way. + headroom: usize, + tracker: Tracker, + pitch: PitchFollower, + pitch_line: Vec, + pitch_at: usize, + audio: Vec<[f32; 2]>, + audio_at: usize, + // Cycle clock. + phase: f64, + cycle: u64, + note_seed: u64, + notes: u64, + noise_at: u64, + amount: f32, + fade: f32, + // Grit. + grit_coef: SvfCoef, + grit_filter: Svf, + countdown: usize, + level: f32, + level_coef: f32, + // Smoothed controls. + smooth: f32, + mix: f32, + output: f32, + bypass: f32, + fresh: bool, + // Editor. + scope: [[f32; SCOPE_POINTS]; SCOPE_CYCLES], + scope_valid: bool, + meter_in: f64, + meter_out: f64, + meter_count: usize, +} + +impl Engine { + pub fn new(rate: f32, channels: usize) -> Result { + if !(rate.is_finite() && (8000.0..=768000.).contains(&rate)) { + return Err("unsupported sample rate"); + } + if !(1..=2).contains(&channels) { + return Err("unsupported channel count"); + } + let tracker = Tracker::new(rate, TrackerConfig::BASS); + let lookahead = tracker.latency(); + let headroom = (JITTER_HEADROOM * rate / TrackerConfig::BASS.min_hz).ceil() as usize; + let settings = Settings::default(); + let mut e = Self { + rate, + channels, + settings, + lookahead, + headroom, + tracker, + pitch: PitchFollower::default(), + pitch_line: vec![Pitch::default(); headroom + 1], + pitch_at: 0, + // Reads reach `lookahead + 2·headroom` back, plus the interpolation taps. + audio: vec![[0.; 2]; lookahead + 2 * headroom + 4], + audio_at: 0, + phase: 0., + cycle: 0, + note_seed: 0, + notes: 0, + noise_at: 0, + amount: 0., + fade: coefficient(rate, FADE_MS), + grit_coef: SvfCoef::IDENTITY, + grit_filter: Svf::default(), + countdown: 0, + level: 0., + level_coef: coefficient(rate, LEVEL_MS), + smooth: coefficient(rate, 10.), + mix: 1., + output: 1., + bypass: 0., + fresh: true, + scope: [[0.; SCOPE_POINTS]; SCOPE_CYCLES], + scope_valid: false, + meter_in: 0., + meter_out: 0., + meter_count: 0, + }; + e.set_settings(settings)?; + e.fresh = true; + Ok(e) + } + pub fn latency_samples(&self) -> u32 { + (self.lookahead + self.headroom) as u32 + } + pub fn channels(&self) -> usize { + self.channels + } + pub fn settings(&self) -> Settings { + self.settings + } + pub fn set_settings(&mut self, s: Settings) -> Result<(), &'static str> { + s.validate()?; + self.tracker.set(s.source == Source::Midi, s.gate, false); + self.settings = s; + if std::mem::take(&mut self.fresh) { + self.mix = s.mix * 0.01; + self.output = db_to_gain(s.output); + self.bypass = f32::from(s.bypass); + } + Ok(()) + } + pub fn reset(&mut self) { + self.audio.fill([0.; 2]); + self.pitch_line.fill(Pitch::default()); + self.pitch = PitchFollower::default(); + self.tracker.reset(); + self.grit_filter.reset(); + self.phase = 0.; + self.cycle = 0; + self.notes = 0; + self.note_seed = 0; + self.noise_at = 0; + self.amount = 0.; + self.level = 0.; + self.countdown = 0; + self.scope_valid = false; + self.fresh = true; + } + pub fn note_on(&mut self, note: u8) { + self.tracker.note_on(note); + } + pub fn note_off(&mut self, note: u8) { + self.tracker.note_off(note); + } + /// The audio `back` samples (fractional) before the newest one, cubic Hermite. + #[inline] + fn read(&self, back: f32) -> [f32; 2] { + let len = self.audio.len(); + let whole = back.floor(); + let t = back - whole; + let base = whole as usize; + let at = |back: usize| self.audio[(self.audio_at + len - back % len) % len]; + // y0 is one sample newer than y1; y2 and y3 are older. + let (newer, p1, p2, p3) = (at(base.max(1) - 1), at(base), at(base + 1), at(base + 2)); + let mut out = [0.; 2]; + for c in 0..2 { + let (y0, y1, y2, y3) = (newer[c], p1[c], p2[c], p3[c]); + let c1 = 0.5 * (y2 - y0); + let c2 = y0 - 2.5 * y1 + 2. * y2 - 0.5 * y3; + let c3 = 0.5 * (y3 - y0) + 1.5 * (y1 - y2); + out[c] = ((c3 * t + c2) * t + c1) * t + y1; + } + out + } + pub fn process_sample(&mut self, x: [f32; 2]) -> [f32; 2] { + let mono = if self.channels == 2 { + 0.5 * (x[0] + x[1]) + } else { + x[0] + }; + let (pitch, snap) = (&mut self.pitch, self.settings.snap); + self.tracker.process(mono, |e| pitch.event(snap, e)); + let (hz, jump) = self.pitch.step(1.); + // Tracker events are timed for `lookahead`; the audio leaves `headroom` later. + let len = self.pitch_line.len(); + self.pitch_line[self.pitch_at] = Pitch { + hz, + start: jump, + sounding: self.tracker.sounding(), + }; + self.pitch_at = (self.pitch_at + 1) % len; + let p = self.pitch_line[self.pitch_at]; + + let len = self.audio.len(); + self.audio_at = (self.audio_at + 1) % len; + self.audio[self.audio_at] = x; + let centre = (self.lookahead + self.headroom) as f32; + let dry = self.read(centre); + + // Cycle clock. + if p.start { + self.phase = 0.; + self.cycle = 0; + self.notes += 1; + self.noise_at = 0; + self.note_seed = match self.settings.seed { + Seed::Same => 0, + Seed::Free => self.notes, + }; + self.countdown = 0; + } + let active = p.sounding && p.hz > 0.; + self.amount += (f32::from(active) - self.amount) * self.fade; + let s = self.settings; + let frac = self.phase.fract() as f32; + let c = self.cycle; + let seed = self.note_seed; + let step = |k: u64| (k % s.steps as u64) as usize; + + // Pattern and level jitter, crossfaded into the next cycle. + let w = edge(frac, s.edge * 0.01); + let jitter_w = edge(frac, 0.5); + let pattern = |k: u64| db_to_gain(s.pattern[step(k)]); + let wobble = |k: u64| db_to_gain(s.level_jitter * normal(LEVEL, seed, k)); + let gain = (pattern(c) * (1. - w) + pattern(c + 1) * w) + * (wobble(c) * (1. - jitter_w) + wobble(c + 1) * jitter_w); + let gain = 1. + self.amount * (gain - 1.); + + // Timing jitter: an offset per cycle, blended across the whole cycle. + let period = if p.hz > 0. { self.rate / p.hz } else { 0. }; + let shift = |k: u64| normal(TIME, seed, k) * s.time_jitter * 0.01 * period; + let tw = edge(frac, 1.); + let offset = (shift(c) * (1. - tw) + shift(c + 1) * tw) * self.amount; + let offset = offset.clamp(-(self.headroom as f32), self.headroom as f32); + let moved = if offset == 0. { + dry + } else { + self.read(centre + offset) + }; + + // Grit: noise above `tone` harmonics, in a sin² window at `position` of each cycle. + let mut grit = 0.; + if s.grit > GRIT_OFF && p.hz > 0. && s.grit_steps[step(c)] { + if self.countdown == 0 { + let f = (s.tone * p.hz).min(0.45 * self.rate); + self.grit_coef = SvfCoef::highpass(self.rate, f, std::f32::consts::FRAC_1_SQRT_2); + self.countdown = RETUNE; + } + self.countdown -= 1; + self.noise_at = self.noise_at.wrapping_add(1); + let n = 2. * uniform(NOISE, seed, self.noise_at) - 1.; + let hp = self.grit_filter.process(&self.grit_coef, n * 3f32.sqrt()); + let d = (frac - s.position * 0.01).rem_euclid(1.); + let l = s.length * 0.01; + if d < l { + let window = (std::f32::consts::PI * d / l).sin().powi(2); + grit = hp * window * self.level.sqrt() * db_to_gain(s.grit) * self.amount; + } + } + let m = if self.channels == 2 { + 0.5 * (dry[0] + dry[1]) + } else { + dry[0] + }; + self.level += (m * m - self.level) * self.level_coef; + // Advance the clock. + if p.hz > 0. { + self.phase += (p.hz / self.rate) as f64; + let whole = self.phase.floor() as u64; + if whole > 0 { + self.phase -= whole as f64; + self.cycle += whole; + self.scope + .rotate_left(whole.min(SCOPE_CYCLES as u64) as usize); + self.scope[SCOPE_CYCLES - 1] = [0.; SCOPE_POINTS]; + } + } + + self.mix += (s.mix * 0.01 - self.mix) * self.smooth; + self.output += (db_to_gain(s.output) - self.output) * self.smooth; + self.bypass += (f32::from(s.bypass) - self.bypass) * self.smooth; + let mut out = [0.; 2]; + for ch in 0..self.channels { + let wet = moved[ch] * gain + grit; + let mixed = (self.mix * wet + (1. - self.mix) * dry[ch]) * self.output; + out[ch] = mixed * (1. - self.bypass) + dry[ch] * self.bypass; + } + let o = if self.channels == 2 { + 0.5 * (out[0] + out[1]) + } else { + out[0] + }; + if active { + let i = ((frac * SCOPE_POINTS as f32) as usize).min(SCOPE_POINTS - 1); + self.scope[SCOPE_CYCLES - 1][i] = o; + self.scope_valid = true; + } + self.meter_in += (m * m) as f64; + self.meter_out += (o * o) as f64; + self.meter_count += 1; + out + } + /// Editor data; levels are averaged since the previous call. + pub fn take_analyzer(&mut self) -> AnalyzerFrame { + let n = self.meter_count.max(1) as f64; + let db = |p: f64| (10. * (p / n).max(1e-12).log10()).max(-120.) as f32; + let mut f = AnalyzerFrame { + hz: self.pitch.hz(), + note: None, + step: (self.cycle % self.settings.steps as u64) as usize, + scope: self.scope, + scope_valid: self.scope_valid, + input_db: db(self.meter_in), + output_db: db(self.meter_out), + latency: self.latency_samples(), + sample_rate: self.rate, + }; + if self.tracker.sounding() && f.hz > 0. { + f.note = Some(hz_to_midi(f.hz).round().clamp(0., 127.) as u8); + } + self.meter_in = 0.; + self.meter_out = 0.; + self.meter_count = 0; + f + } +} diff --git a/plugins/knurl/core/src/settings.rs b/plugins/knurl/core/src/settings.rs new file mode 100644 index 0000000..ff5c5a3 --- /dev/null +++ b/plugins/knurl/core/src/settings.rs @@ -0,0 +1,166 @@ +//! Host-independent controls. Gains in dB; amounts relative to one cycle in percent. +use serde::{Deserialize, Serialize}; + +/// Pattern steps: one per cycle, repeating. +pub const STEPS: usize = 16; +pub const STEP_MIN: f32 = -24.; +/// Grit at or below this level is off. +pub const GRIT_OFF: f32 = -60.; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +pub enum Source { + /// Pitch and note starts are detected from the incoming bass. + #[default] + Audio, + /// Notes come from MIDI note input. + Midi, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +pub enum Seed { + /// Every note gets the same random sequence, so identical notes sound identical. + #[default] + Same, + /// The random sequences keep running from note to note. + Free, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct Settings { + pub source: Source, + /// Gain per pattern step, 0 down to `STEP_MIN`. + pub pattern: [f32; STEPS], + /// Steps that get a grit burst. + pub grit_steps: [bool; STEPS], + /// Pattern length in cycles, 1..=STEPS. + pub steps: usize, + /// Crossfade into the next step, as a share of a cycle. + pub edge: f32, + /// Random level per cycle, standard deviation in dB. + pub level_jitter: f32, + /// Random timing per cycle, standard deviation in percent of a period. + pub time_jitter: f32, + /// Grit burst level relative to the input, dB; `GRIT_OFF` or lower is off. + pub grit: f32, + /// Grit highpass, in harmonics of the note. + pub tone: f32, + /// Where in the cycle the grit burst sits, percent. + pub position: f32, + /// Grit burst length, percent of a cycle. + pub length: f32, + pub seed: Seed, + /// Detection threshold in dBFS for note starts (Audio source). + pub gate: f32, + /// Round detected pitch to the nearest semitone. + pub snap: bool, + pub mix: f32, + pub output: f32, + pub bypass: bool, +} + +/// Named step patterns: (name, help, gains). Missing steps are 0 dB. +pub const PATTERNS: [(&str, &str, &[f32]); 6] = [ + ("Off", "Every cycle at full level.", &[0.]), + ( + "Double", + "Every second cycle quieter: a sub-octave growl.", + &[0., -9.], + ), + ( + "Triple", + "One loud cycle, two quiet: a growl an octave and a fifth down.", + &[0., -12., -12.], + ), + ( + "Fry", + "Uneven four-cycle pattern: vocal-fry rattle.", + &[0., -14., -5., -14.], + ), + ( + "Gallop", + "Loud, quiet, loud, then three quiet: a rolling gallop.", + &[0., -18., 0., -18., -18., -18.], + ), + ( + "Stutter", + "Bursts of cycles with gaps: a gated stutter.", + &[0., 0., -24., -24., 0., -24., -24., -24.], + ), +]; + +impl Settings { + /// Applies a named pattern: its gains, its length, grit on every step. + pub fn apply_pattern(&mut self, gains: &[f32]) { + self.pattern = [0.; STEPS]; + for (p, g) in self.pattern.iter_mut().zip(gains) { + *p = *g; + } + self.steps = gains.len().clamp(1, STEPS); + } +} + +impl Default for Settings { + fn default() -> Self { + let mut s = Self { + source: Source::Audio, + pattern: [0.; STEPS], + grit_steps: [true; STEPS], + steps: 1, + edge: 15., + level_jitter: 1., + time_jitter: 3., + grit: -9., + tone: 12., + position: 10., + length: 12., + seed: Seed::Same, + gate: -42., + snap: true, + mix: 100., + output: 0., + bypass: false, + }; + s.apply_pattern(PATTERNS[3].2); + s + } +} +impl Settings { + pub fn validate(&self) -> Result<(), &'static str> { + let finite = [ + self.edge, + self.level_jitter, + self.time_jitter, + self.grit, + self.tone, + self.position, + self.length, + self.gate, + self.mix, + self.output, + ] + .iter() + .chain(self.pattern.iter()) + .all(|v| v.is_finite()); + if !finite { + return Err("non-finite setting"); + } + let ok = self.pattern.iter().all(|g| (STEP_MIN..=0.).contains(g)) + && (1..=STEPS).contains(&self.steps) + && (5.0..=50.).contains(&self.edge) + && (0.0..=6.).contains(&self.level_jitter) + && (0.0..=10.).contains(&self.time_jitter) + && (GRIT_OFF..=0.).contains(&self.grit) + && (4.0..=64.).contains(&self.tone) + && (0.0..=100.).contains(&self.position) + && (2.0..=50.).contains(&self.length) + && (-60.0..=-20.).contains(&self.gate) + && (0.0..=100.).contains(&self.mix) + && (-12.0..=12.).contains(&self.output); + if ok { + Ok(()) + } else { + Err("setting out of range") + } + } +} diff --git a/plugins/knurl/core/tests/dsp.rs b/plugins/knurl/core/tests/dsp.rs new file mode 100644 index 0000000..2d1a27c --- /dev/null +++ b/plugins/knurl/core/tests/dsp.rs @@ -0,0 +1,232 @@ +use common_dsp::{midi_to_hz, Saw}; +use knurl_core::{Engine, Seed, Settings, Source, GRIT_OFF, PATTERNS}; + +const RATE: f32 = 48000.; + +/// A tanh saw bass at `note`, as the prototype used. +fn bass(note: u8, n: usize) -> Vec { + let mut saw = Saw::new(RATE, midi_to_hz(note as f32)); + (0..n).map(|_| (2.2 * saw.sample()).tanh() * 0.5).collect() +} +fn plain() -> Settings { + let mut s = Settings { + source: Source::Midi, + level_jitter: 0., + time_jitter: 0., + grit: GRIT_OFF, + ..Settings::default() + }; + s.apply_pattern(PATTERNS[0].2); + s +} +/// Runs one held note from sample 0 and returns the output, aligned to the input. +fn run(settings: Settings, note: u8, input: &[f32]) -> Vec { + let mut engine = Engine::new(RATE, 1).unwrap(); + engine.set_settings(settings).unwrap(); + let latency = engine.latency_samples() as usize; + engine.note_on(note); + let mut out: Vec = input + .iter() + .chain(std::iter::repeat_n(&0., latency)) + .map(|&x| engine.process_sample([x, 0.])[0]) + .collect(); + out.drain(..latency); + out +} +/// Level of the component at `hz` (single-bin DFT with a Hann window), dB. +fn level(x: &[f32], hz: f32) -> f64 { + let n = x.len(); + let (mut re, mut im) = (0f64, 0f64); + for (i, &v) in x.iter().enumerate() { + let w = 0.5 - 0.5 * (std::f64::consts::TAU * i as f64 / n as f64).cos(); + let a = std::f64::consts::TAU * hz as f64 * i as f64 / RATE as f64; + re += w * v as f64 * a.cos(); + im -= w * v as f64 * a.sin(); + } + 10. * (re * re + im * im).max(1e-30).log10() +} +fn subharmonic(out: &[f32], note: u8, divisor: f32) -> f64 { + let f = midi_to_hz(note as f32); + let seg = &out[4800..out.len() - 4800]; + level(seg, f / divisor) - level(seg, f) +} +fn max_step(x: &[f32]) -> f32 { + x.windows(2).map(|w| (w[1] - w[0]).abs()).fold(0., f32::max) +} + +#[test] +fn everything_off_is_the_delayed_input() { + let input = bass(29, 24000); + let out = run(plain(), 29, &input); + assert_eq!(out, input); +} + +#[test] +fn patterns_make_tuned_subharmonics_without_clicks() { + let input = bass(29, 48000); + let base = run(plain(), 29, &input); + assert!(subharmonic(&base, 29, 2.) < -50.); + for (pattern, divisor, expected) in [(1, 2., -8.2), (2, 3., -10.7)] { + let mut s = plain(); + s.apply_pattern(PATTERNS[pattern].2); + let out = run(s, 29, &input); + let sub = subharmonic(&out, 29, divisor); + assert!( + (sub - expected).abs() < 0.5, + "{}: {sub}", + PATTERNS[pattern].0 + ); + assert!( + max_step(&out) <= 1.05 * max_step(&input), + "{}", + PATTERNS[pattern].0 + ); + } +} + +#[test] +fn the_texture_is_the_same_on_every_note() { + let mut s = plain(); + s.apply_pattern(PATTERNS[1].2); + let low = run(s, 29, &bass(29, 48000)); + let high = run(s, 36, &bass(36, 48000)); + let (a, b) = (subharmonic(&low, 29, 2.), subharmonic(&high, 36, 2.)); + assert!((a - b).abs() < 0.3, "F1 {a} C2 {b}"); +} + +#[test] +fn same_seed_repeats_every_note_and_free_does_not() { + for (seed, identical) in [(Seed::Same, true), (Seed::Free, false)] { + let s = Settings { + source: Source::Midi, + seed, + ..Settings::default() + }; + let mut engine = Engine::new(RATE, 2).unwrap(); + engine.set_settings(s).unwrap(); + let latency = engine.latency_samples() as usize; + let note = bass(29, 12000); + let mut out = Vec::new(); + let starts = [0, 36000]; + for i in 0..72000 + latency { + if starts.contains(&i) { + engine.note_on(29); + } + if i == starts[0] + 12000 || i == starts[1] + 12000 { + engine.note_off(29); + } + let k = starts + .iter() + .filter(|&&s| i >= s) + .map(|&s| i - s) + .min() + .unwrap(); + let x = note.get(k).copied().unwrap_or(0.); + out.push(engine.process_sample([x, x])[0]); + } + let first = &out[latency..latency + 12000]; + let second = &out[starts[1] + latency..starts[1] + latency + 12000]; + let largest = first + .iter() + .zip(second) + .map(|(a, b)| (a - b).abs()) + .fold(0f32, f32::max); + if identical { + assert!(largest < 1e-4, "{seed:?}: {largest}"); + } else { + assert!(largest > 1e-2, "{seed:?}: {largest}"); + } + assert!(out.iter().all(|v| v.is_finite())); + } +} + +#[test] +fn grit_follows_the_input_level_and_stops_with_the_note() { + let mut s = plain(); + s.grit = -6.; + let input = bass(29, 24000); + let out = run(s, 29, &input); + let noise: f64 = out + .iter() + .zip(&input) + .skip(4800) + .map(|(o, i)| ((o - i) as f64).powi(2)) + .sum::() + / (out.len() - 4800) as f64; + let signal: f64 = input + .iter() + .skip(4800) + .map(|v| (*v as f64).powi(2)) + .sum::() + / (out.len() - 4800) as f64; + let ratio = 10. * (noise / signal).log10(); + // −6 dB bursts over 12 % of each cycle; the sin² window keeps 3/8 of the power: + // −6 + 10·log10(0.12 · 0.375) = −19.5 dB. + assert!((ratio + 19.5).abs() < 1.5, "{ratio}"); + + // Silence in, silence out: no grit without a note, and no NaN. + let mut engine = Engine::new(RATE, 2).unwrap(); + engine.set_settings(Settings::default()).unwrap(); + for _ in 0..48000 { + let out = engine.process_sample([0., 0.]); + assert_eq!(out, [0., 0.]); + } +} + +#[test] +fn audio_source_finds_the_note_and_reports_latency() { + let mut s = plain(); + s.source = Source::Audio; + s.apply_pattern(PATTERNS[1].2); + let mut engine = Engine::new(RATE, 1).unwrap(); + engine.set_settings(s).unwrap(); + let latency = engine.latency_samples() as usize; + // About 67 ms of lookahead plus 4.2 ms of jitter headroom. + assert!((latency as f32 / RATE - 0.071).abs() < 0.005, "{latency}"); + let input = bass(29, 48000); + let mut out: Vec = input + .iter() + .map(|&x| engine.process_sample([x, 0.])[0]) + .collect(); + out.drain(..latency); + let frame = engine.take_analyzer(); + assert_eq!(frame.note, Some(29)); + assert!(frame.scope_valid); + let sub = subharmonic(&out[..out.len() - 4800], 29, 2.); + assert!(sub > -14., "{sub}"); +} + +#[test] +fn invalid_settings_are_rejected() { + let mut engine = Engine::new(RATE, 2).unwrap(); + for bad in [ + Settings { + steps: 0, + ..Settings::default() + }, + Settings { + edge: 60., + ..Settings::default() + }, + Settings { + grit: f32::NAN, + ..Settings::default() + }, + Settings { + time_jitter: 11., + ..Settings::default() + }, + ] { + assert!(engine.set_settings(bad).is_err()); + } + let mut s = Settings::default(); + s.pattern[3] = -30.; + assert!(engine.set_settings(s).is_err()); + assert!(Engine::new(1000., 2).is_err()); + assert!(Engine::new(48000., 3).is_err()); + let json = serde_json::to_string(&Settings::default()).unwrap(); + assert_eq!( + serde_json::from_str::(&json).unwrap(), + Settings::default() + ); +} diff --git a/plugins/knurl/core/tests/realtime.rs b/plugins/knurl/core/tests/realtime.rs new file mode 100644 index 0000000..3dc85ba --- /dev/null +++ b/plugins/knurl/core/tests/realtime.rs @@ -0,0 +1,70 @@ +use knurl_core::{Engine, Seed, Settings, Source, PATTERNS}; +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 notes_patterns_grit_and_jitter_do_not_allocate_or_free() { + let mut engine = Engine::new(48000., 2).unwrap(); + let mut settings = Settings::default(); + engine.set_settings(settings).unwrap(); + CHECK.with(|v| v.set(true)); + for i in 0..96000 { + if i % 4800 == 0 { + settings.apply_pattern(PATTERNS[(i / 4800) % PATTERNS.len()].2); + settings.seed = if i > 48000 { Seed::Free } else { Seed::Same }; + settings.source = if (i / 4800) % 3 == 0 { + Source::Midi + } else { + Source::Audio + }; + settings.time_jitter = ((i / 4800) % 10) as f32; + engine.set_settings(settings).unwrap(); + engine.note_on(30 + (i / 4800 % 12) as u8); + } + if i % 4800 == 2400 { + engine.note_off(30 + (i / 4800 % 12) as u8); + } + let t = i as f32 / 48000.; + let x = if (i / 2400) % 2 == 0 { + (std::f32::consts::TAU * 55. * t).sin() * 0.5 + } else { + 0. + }; + std::hint::black_box(engine.process_sample([x, x])); + 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/knurl/plugin/Cargo.lock b/plugins/knurl/plugin/Cargo.lock new file mode 100644 index 0000000..a748606 --- /dev/null +++ b/plugins/knurl/plugin/Cargo.lock @@ -0,0 +1,2382 @@ +# 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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375989decec1a0fa424442bed520fa12ede43a48475ce8c7417d2c69b32b5f6b" +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 = "knurl" +version = "0.1.0" +dependencies = [ + "clap-sys", + "crossbeam-queue", + "knurl-core", + "knurl-ui", + "nih_plug", + "nih_plug_egui", + "serde", + "serde_json", +] + +[[package]] +name = "knurl-core" +version = "0.1.0" +dependencies = [ + "common-dsp", + "serde", +] + +[[package]] +name = "knurl-ui" +version = "0.1.0" +dependencies = [ + "common-dsp", + "common-ui", + "egui", + "knurl-core", +] + +[[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 = "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/knurl/plugin/Cargo.toml b/plugins/knurl/plugin/Cargo.toml new file mode 100644 index 0000000..9c94cc0 --- /dev/null +++ b/plugins/knurl/plugin/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "knurl" +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" } +knurl-core = { path = "../core" } +knurl-ui = { path = "../ui" } +crossbeam-queue = "0.3" +serde = { version = "1", features = ["derive"] } + +[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/knurl/plugin/src/lib.rs b/plugins/knurl/plugin/src/lib.rs new file mode 100644 index 0000000..f4da474 --- /dev/null +++ b/plugins/knurl/plugin/src/lib.rs @@ -0,0 +1,195 @@ +//! CLAP audio effect. All DSP state is owned by the processing thread. +mod params; +use crossbeam_queue::ArrayQueue; +use knurl_core::{AnalyzerFrame, Engine}; +use nih_plug::prelude::*; +use nih_plug_egui::{create_egui_editor, egui}; +use params::{KnurlParams, Pattern, GESTURES}; +use std::sync::Arc; + +struct EditorData { + ui: knurl_ui::State, + gestures: [bool; GESTURES], +} +pub struct Knurl { + params: Arc, + analyzer: Arc>, + engine: Option, + pattern: Pattern, + publish_count: usize, + publish_interval: usize, +} +impl Default for Knurl { + fn default() -> Self { + Self { + params: Arc::new(KnurlParams::default()), + analyzer: Arc::new(ArrayQueue::new(2)), + engine: None, + pattern: Pattern::default(), + publish_count: 0, + publish_interval: 1600, + } + } +} +impl Plugin for Knurl { + const NAME: &'static str = "Knurl"; + 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 MIDI_INPUT: MidiConfig = MidiConfig::Basic; + 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 analyzer = self.analyzer.clone(); + create_egui_editor( + params.editor.clone(), + EditorData { + ui: knurl_ui::State::default(), + gestures: [false; GESTURES], + }, + |ctx, _| knurl_ui::style(ctx), + move |ctx, setter, data| { + while let Some(frame) = analyzer.pop() { + data.ui.accept_frame(frame); + } + let current = params.pattern(); + let before = params.settings(¤t); + let mut edited = before; + nih_plug_egui::resizable_window::ResizableWindow::new("knurl-editor") + .min_size(egui::vec2( + knurl_ui::WIDTH as f32 - 80., + knurl_ui::HEIGHT as f32 - 40., + )) + .show(ctx, ¶ms.editor, |ui| { + knurl_ui::show(ui, &mut edited, &mut data.ui) + }); + params.apply( + setter, + &before, + &edited, + data.ui.gesture_active, + &mut data.gestures, + ); + // The scope follows the audio. + 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; + }; + let settings = self.params.settings(&self.pattern); + self.pattern = Pattern { + gains: settings.pattern, + grit: settings.grit_steps, + }; + if engine.set_settings(settings).is_err() { + return false; + } + context.set_latency_samples(engine.latency_samples()); + self.publish_interval = (config.sample_rate / 30.) as usize; + self.engine = Some(engine); + 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, + context: &mut impl ProcessContext, + ) -> ProcessStatus { + let Some(engine) = self.engine.as_mut() else { + return ProcessStatus::Normal; + }; + let settings = self.params.settings(&self.pattern); + self.pattern = Pattern { + gains: settings.pattern, + grit: settings.grit_steps, + }; + let _ = engine.set_settings(settings); + let mut next = context.next_event(); + for (index, mut sample) in buffer.iter_samples().enumerate() { + while let Some(event) = next { + if event.timing() as usize > index { + break; + } + match event { + NoteEvent::NoteOn { note, velocity, .. } if velocity > 0. => { + engine.note_on(note) + } + NoteEvent::NoteOn { note, .. } | NoteEvent::NoteOff { note, .. } => { + engine.note_off(note) + } + NoteEvent::Choke { note, .. } => engine.note_off(note), + _ => {} + } + next = context.next_event(); + } + let x = [ + *sample.get_mut(0).unwrap(), + if engine.channels() == 2 { + *sample.get_mut(1).unwrap() + } else { + 0. + }, + ]; + let out = engine.process_sample(x); + for (c, value) in sample.into_iter().enumerate() { + *value = out[c]; + } + self.publish_count += 1; + if self.publish_count >= self.publish_interval { + self.analyzer.force_push(engine.take_analyzer()); + self.publish_count = 0; + } + } + // The lookahead delay and filter tails must drain after input stops. + ProcessStatus::KeepAlive + } +} +impl ClapPlugin for Knurl { + const CLAP_ID: &'static str = "io.storyboard.cycle-texture"; + const CLAP_DESCRIPTION: Option<&'static str> = + Some("Texture per cycle of the played note: patterns, jitter and grit that fit every note"); + 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::Stereo, + ClapFeature::Mono, + ]; +} +nih_export_clap!(Knurl); diff --git a/plugins/knurl/plugin/src/params.rs b/plugins/knurl/plugin/src/params.rs new file mode 100644 index 0000000..0a2edab --- /dev/null +++ b/plugins/knurl/plugin/src/params.rs @@ -0,0 +1,255 @@ +use knurl_core::{Seed, Settings, Source, GRIT_OFF, STEPS, STEP_MIN}; +use nih_plug::prelude::*; +use nih_plug_egui::EguiState; +use std::sync::{Arc, RwLock}; + +#[derive(Enum, Debug, PartialEq, Eq, Clone, Copy)] +pub enum SourceParam { + #[id = "audio"] + Audio, + #[id = "midi"] + #[name = "MIDI"] + Midi, +} +#[derive(Enum, Debug, PartialEq, Eq, Clone, Copy)] +pub enum SeedParam { + #[id = "same"] + #[name = "Same every note"] + Same, + #[id = "free"] + Free, +} +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) +} +/// Continuous controls, in the order of the gesture slots. +const FLOATS: usize = 10; +pub const GESTURES: usize = FLOATS; + +/// The drawn pattern: step gains and grit switches. Saved with the project, not automation. +#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Pattern { + pub gains: [f32; STEPS], + pub grit: [bool; STEPS], +} +impl Default for Pattern { + fn default() -> Self { + let d = Settings::default(); + Self { + gains: d.pattern, + grit: d.grit_steps, + } + } +} + +#[derive(Params)] +pub struct KnurlParams { + #[persist = "editor"] + pub editor: Arc, + #[persist = "knurl-pattern"] + pub pattern: RwLock, + #[id = "source"] + pub source: EnumParam, + #[id = "seed"] + pub seed: EnumParam, + #[id = "steps"] + pub steps: IntParam, + #[id = "edge"] + pub edge: FloatParam, + #[id = "level-jitter"] + pub level_jitter: FloatParam, + #[id = "time-jitter"] + pub time_jitter: FloatParam, + #[id = "grit"] + pub grit: FloatParam, + #[id = "tone"] + pub tone: FloatParam, + #[id = "position"] + pub position: FloatParam, + #[id = "length"] + pub length: FloatParam, + #[id = "gate"] + pub gate: FloatParam, + #[id = "mix"] + pub mix: FloatParam, + #[id = "output"] + pub output: FloatParam, + #[id = "snap"] + pub snap: BoolParam, + #[id = "bypass"] + pub bypass: BoolParam, +} +impl Default for KnurlParams { + fn default() -> Self { + let d = Settings::default(); + Self { + editor: EguiState::from_size(knurl_ui::WIDTH, knurl_ui::HEIGHT), + pattern: RwLock::new(Pattern::default()), + source: EnumParam::new("Pitch source", SourceParam::Audio), + seed: EnumParam::new("Random", SeedParam::Same), + steps: IntParam::new( + "Steps", + d.steps as i32, + IntRange::Linear { + min: 1, + max: STEPS as i32, + }, + ), + edge: linear("Edge", d.edge, 5., 50., " %"), + level_jitter: linear("Level jitter", d.level_jitter, 0., 6., " dB"), + time_jitter: linear("Time jitter", d.time_jitter, 0., 10., " %"), + grit: linear("Grit", d.grit, GRIT_OFF, 0., " dB").with_value_to_string(Arc::new(|v| { + if v <= GRIT_OFF { + "Off".into() + } else { + format!("{v:.1}") + } + })), + tone: FloatParam::new( + "Tone", + d.tone, + FloatRange::Skewed { + min: 4., + max: 64., + factor: FloatRange::skew_factor(-1.), + }, + ) + .with_unit(" ×f0"), + position: linear("Position", d.position, 0., 100., " %"), + length: linear("Length", d.length, 2., 50., " %"), + gate: linear("Gate", d.gate, -60., -20., " dB"), + mix: linear("Mix", d.mix, 0., 100., " %"), + output: linear("Output", d.output, -12., 12., " dB"), + snap: BoolParam::new("Snap to semitone", d.snap), + bypass: BoolParam::new("Bypass", false).make_bypass(), + } + } +} +impl KnurlParams { + /// Current settings. The pattern falls back to `last` when the editor holds the lock, + /// so the audio thread never waits. + pub fn settings(&self, last: &Pattern) -> Settings { + let pattern = self.pattern.try_read().map(|p| *p).unwrap_or(*last); + Settings { + source: match self.source.value() { + SourceParam::Audio => Source::Audio, + SourceParam::Midi => Source::Midi, + }, + pattern: pattern.gains.map(|g| { + if g.is_finite() { + g.clamp(STEP_MIN, 0.) + } else { + 0. + } + }), + grit_steps: pattern.grit, + steps: self.steps.value().clamp(1, STEPS as i32) as usize, + edge: self.edge.value(), + level_jitter: self.level_jitter.value(), + time_jitter: self.time_jitter.value(), + grit: self.grit.value(), + tone: self.tone.value(), + position: self.position.value(), + length: self.length.value(), + seed: match self.seed.value() { + SeedParam::Same => Seed::Same, + SeedParam::Free => Seed::Free, + }, + gate: self.gate.value(), + snap: self.snap.value(), + mix: self.mix.value(), + output: self.output.value(), + bypass: self.bypass.value(), + } + } + pub fn pattern(&self) -> Pattern { + self.pattern.read().map(|p| *p).unwrap_or_default() + } + fn floats(&self) -> [&FloatParam; FLOATS] { + [ + &self.edge, + &self.level_jitter, + &self.time_jitter, + &self.grit, + &self.tone, + &self.position, + &self.length, + &self.gate, + &self.mix, + &self.output, + ] + } + /// 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. The pattern is + /// stored directly. + pub fn apply( + &self, + setter: &ParamSetter, + old: &Settings, + new: &Settings, + dragging: bool, + open: &mut [bool; FLOATS], + ) { + if new.pattern != old.pattern || new.grit_steps != old.grit_steps { + if let Ok(mut p) = self.pattern.write() { + *p = Pattern { + gains: new.pattern, + grit: new.grit_steps, + }; + } + } + let values = [ + (old.edge, new.edge), + (old.level_jitter, new.level_jitter), + (old.time_jitter, new.time_jitter), + (old.grit, new.grit), + (old.tone, new.tone), + (old.position, new.position), + (old.length, new.length), + (old.gate, new.gate), + (old.mix, new.mix), + (old.output, new.output), + ]; + for (i, (p, (a, b))) in self.floats().into_iter().zip(values).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; + } + } + fn switch(setter: &ParamSetter, p: &P, v: P::Plain) { + setter.begin_set_parameter(p); + setter.set_parameter(p, v); + setter.end_set_parameter(p); + } + if new.steps != old.steps { + switch(setter, &self.steps, new.steps as i32); + } + if new.snap != old.snap { + switch(setter, &self.snap, new.snap); + } + if new.bypass != old.bypass { + switch(setter, &self.bypass, new.bypass); + } + if new.source != old.source { + let v = match new.source { + Source::Audio => SourceParam::Audio, + Source::Midi => SourceParam::Midi, + }; + switch(setter, &self.source, v); + } + if new.seed != old.seed { + let v = match new.seed { + Seed::Same => SeedParam::Same, + Seed::Free => SeedParam::Free, + }; + switch(setter, &self.seed, v); + } + } +} diff --git a/plugins/knurl/plugin/tests/host.rs b/plugins/knurl/plugin/tests/host.rs new file mode 100644 index 0000000..9cfba7c --- /dev/null +++ b/plugins/knurl/plugin/tests/host.rs @@ -0,0 +1,227 @@ +//! 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 = &knurl::clap_entry; + assert!(entry.init.unwrap()(c"knurl.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"Knurl"); + assert_eq!( + CStr::from_ptr((*descriptor).id), + c"io.storyboard.cycle-texture" + ); + 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("edge".into(), ParamValue::F32(30.)); + envelope.params.insert("steps".into(), ParamValue::I32(3)); + envelope + .params + .insert("seed".into(), ParamValue::String("free".into())); + envelope + .params + .insert("source".into(), ParamValue::String("midi".into())); + // Pitch from MIDI and no note arrives, so the audio check below sees the dry input + // even with a pattern set. + envelope + .params + .insert("level-jitter".into(), ParamValue::F32(0.)); + envelope + .params + .insert("time-jitter".into(), ParamValue::F32(0.)); + envelope.params.insert("grit".into(), ParamValue::F32(-60.)); + let (gains, grit) = ([-6f32; 16], [true; 16]); + let mut pattern = serde_json::json!({ "gains": gains, "grit": grit }); + pattern["grit"][5] = false.into(); + envelope + .fields + .insert("knurl-pattern".into(), pattern.to_string()); + 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() + ); + let saved: serde_json::Value = + serde_json::from_str(&restored.fields["knurl-pattern"]).unwrap(); + assert_eq!(saved, pattern); + for rate in [44100., 48000., 96000.] { + assert!(p.activate.unwrap()(plugin, rate, 1, 512)); + let delay = (*latency).get.unwrap()(plugin) as usize; + // About 67 ms lookahead plus 4.2 ms of timing-jitter headroom. + assert!((delay as f64 / rate - 0.071).abs() < 0.01, "{delay}"); + 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); + // With no MIDI note and every effect off, a tone passes through 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 { + assert!(left[i].abs() < 1e-6, "output before the lookahead"); + } 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/knurl/ui/Cargo.toml b/plugins/knurl/ui/Cargo.toml new file mode 100644 index 0000000..b769231 --- /dev/null +++ b/plugins/knurl/ui/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "knurl-ui" +version = "0.1.0" +edition.workspace = true +license.workspace = true + +[dependencies] +egui = "0.31.1" +knurl-core = { path = "../core" } +common-ui = { path = "../../common/ui" } +common-dsp = { path = "../../common/dsp" } diff --git a/plugins/knurl/ui/src/lib.rs b/plugins/knurl/ui/src/lib.rs new file mode 100644 index 0000000..880a25e --- /dev/null +++ b/plugins/knurl/ui/src/lib.rs @@ -0,0 +1,501 @@ +//! Knurl 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 knurl_core::{ + AnalyzerFrame, Seed, Settings, Source, GRIT_OFF, PATTERNS, SCOPE_CYCLES, SCOPE_POINTS, STEPS, + STEP_MIN, +}; + +pub use common_ui::style; +pub const WIDTH: u32 = 1040; +pub const HEIGHT: u32 = 650; + +const GRID: Color32 = Color32::from_rgb(24, 26, 30); +const TOP: f32 = 316.; + +pub struct State { + pub analyzer: AnalyzerFrame, + /// 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(), + gesture_active: false, + help: DEFAULT_HELP.into(), + } + } +} +const DEFAULT_HELP: &str = + "Every cycle of the note gets its own level, timing and grit. Counted in cycles, so it fits every note."; +impl State { + pub fn accept_frame(&mut self, frame: AnalyzerFrame) { + let old = self.analyzer; + self.analyzer = frame; + self.analyzer.input_db = frame.input_db.max(old.input_db - 3.); + self.analyzer.output_db = frame.output_db.max(old.output_db - 3.); + // Keep the last scope between notes. + if !frame.scope_valid && old.scope_valid { + self.analyzer.scope = old.scope; + self.analyzer.scope_valid = true; + } + } +} +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, 58., ACCENT); + hint(&r, state, help); + }); +} + +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, "KNURL", "CYCLE TEXTURE", |ui| { + let r = ui + .selectable_label(settings.bypass, RichText::new("BYPASS").size(13.)) + .on_hover_text("Crossfade to the delayed dry signal."); + if r.clicked() { + settings.bypass = !settings.bypass; + } + ui.separator(); + for (source, label) in [(Source::Midi, "MIDI"), (Source::Audio, "AUDIO")] { + let r = ui.selectable_label(settings.source == source, RichText::new(label).size(13.)); + if r.clicked() { + settings.source = source; + } + } + caption(ui, "PITCH FROM"); + }); + ui.add_space(6.); + ui.horizontal_top(|ui| { + panel(ui, |ui| { + ui.set_width(210.); + ui.set_min_height(TOP); + ui.vertical(|ui| note_panel(ui, settings, state)); + }); + ui.add_space(4.); + panel(ui, |ui| { + ui.set_width(ui.available_width()); + ui.vertical(|ui| { + let frame = state.analyzer; + scope(ui, &frame, 140.); + ui.add_space(4.); + pattern_editor(ui, settings, state, TOP - 144.); + }); + }); + }); + 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)); +} + +fn note_panel(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) { + let frame = state.analyzer; + caption(ui, "NOTE"); + let (name, hz) = match frame.note { + Some(n) if frame.hz > 0. => (common_dsp::note_name(n), format!("{:.1} Hz", frame.hz)), + _ if frame.hz > 0. => ("—".into(), format!("holding {:.1} Hz", frame.hz)), + _ => ("—".into(), "no note yet".into()), + }; + ui.label( + RichText::new(name) + .font(FontId::new(54., egui::FontFamily::Name("medium".into()))) + .color(if frame.note.is_some() { TEXT } else { DIM }), + ); + ui.label(RichText::new(hz).size(13.).color(MUTED)); + if frame.hz > 0. { + ui.label( + RichText::new(format!("one cycle = {:.1} ms", 1000. / frame.hz)) + .size(12.) + .color(DIM), + ); + } + ui.add_space(10.); + meter(ui, "IN", frame.input_db, STEEL, 110.); + meter(ui, "OUT", frame.output_db, ACCENT, 110.); + ui.add_space(10.); + caption(ui, "RANDOM"); + ui.horizontal(|ui| { + for (seed, label, help) in [ + ( + Seed::Same, + "SAME EVERY NOTE", + "Every note gets the same random sequence: identical notes sound identical.", + ), + ( + Seed::Free, + "FREE", + "The random sequences keep running, so every note differs.", + ), + ] { + let r = ui.selectable_label(settings.seed == seed, RichText::new(label).size(12.)); + if r.clicked() { + settings.seed = seed; + } + hint(&r, state, help); + } + }); + ui.add_space(6.); + let r = ui.checkbox( + &mut settings.snap, + RichText::new("Snap to semitone").size(12.), + ); + hint( + &r, + state, + "Round the detected pitch. Turn it off for slides.", + ); +} + +/// The last cycles of the output, overlaid; the newest is brightest. +fn scope(ui: &mut egui::Ui, frame: &AnalyzerFrame, height: f32) { + let (outer, _) = + ui.allocate_exact_size(Vec2::new(ui.available_width(), height), Sense::hover()); + let p = ui.painter_at(outer); + p.rect_filled(outer, 2., SCREEN); + p.text( + outer.left_top() + Vec2::new(8., 5.), + egui::Align2::LEFT_TOP, + format!("LAST {SCOPE_CYCLES} CYCLES, OVERLAID"), + FontId::proportional(10.), + MUTED, + ); + let plot = Rect::from_min_max( + outer.left_top() + Vec2::new(10., 20.), + outer.right_bottom() - Vec2::new(10., 8.), + ); + for t in [0.25f32, 0.5, 0.75] { + let x = plot.left() + plot.width() * t; + p.line_segment( + [Pos2::new(x, plot.top()), Pos2::new(x, plot.bottom())], + Stroke::new(1.0_f32, GRID), + ); + } + p.line_segment( + [ + Pos2::new(plot.left(), plot.center().y), + Pos2::new(plot.right(), plot.center().y), + ], + Stroke::new(1.0_f32, EDGE), + ); + if !frame.scope_valid { + p.text( + plot.center(), + egui::Align2::CENTER_CENTER, + "play a note", + FontId::proportional(12.), + DIM, + ); + return; + } + let peak = frame + .scope + .iter() + .flatten() + .fold(1e-6f32, |m, v| m.max(v.abs())); + // The newest cycle is still being written; draw the complete ones. + for (age, cycle) in frame.scope[..SCOPE_CYCLES - 1].iter().enumerate() { + let alpha = 0.2 + 0.8 * age as f32 / (SCOPE_CYCLES - 2) as f32; + let color = if age == SCOPE_CYCLES - 2 { + ACCENT + } else { + STEEL.gamma_multiply(alpha * 0.6) + }; + let points: Vec = cycle + .iter() + .enumerate() + .map(|(i, v)| { + Pos2::new( + plot.left() + plot.width() * i as f32 / (SCOPE_POINTS - 1) as f32, + plot.center().y - plot.height() * 0.48 * v / peak, + ) + }) + .collect(); + p.add(egui::Shape::line( + points, + Stroke::new( + if age == SCOPE_CYCLES - 2 { + 2.0_f32 + } else { + 1.0 + }, + color, + ), + )); + } +} + +fn step_y(rect: Rect, db: f32) -> f32 { + rect.top() + rect.height() * (db / STEP_MIN).clamp(0., 1.) +} + +/// One bar per step: drag to set its level, double-click to reset; the square below a bar +/// switches its grit burst. +fn pattern_editor(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State, height: f32) { + let (outer, response) = ui.allocate_exact_size( + Vec2::new(ui.available_width(), height), + Sense::click_and_drag(), + ); + let p = ui.painter_at(outer); + p.rect_filled(outer, 2., SCREEN); + p.text( + outer.left_top() + Vec2::new(8., 5.), + egui::Align2::LEFT_TOP, + "PATTERN: LEVEL PER CYCLE", + FontId::proportional(10.), + MUTED, + ); + let bars = Rect::from_min_max( + outer.left_top() + Vec2::new(34., 22.), + Pos2::new(outer.right() - 10., outer.bottom() - 26.), + ); + let toggles_y = outer.bottom() - 16.; + let col = bars.width() / STEPS as f32; + let column = + |x: f32| (((x - bars.left()) / col).floor() as isize).clamp(0, STEPS as isize - 1) as usize; + if response.hovered() { + state.help = "Drag a bar to set the level of that cycle; double-click resets it. The squares switch grit per step.".into(); + } + if let Some(pos) = response.interact_pointer_pos() { + let k = column(pos.x); + if pos.y > bars.bottom() + 2. { + if response.clicked() { + settings.grit_steps[k] = !settings.grit_steps[k]; + } + } else if response.double_clicked() { + settings.pattern[k] = 0.; + } else if response.dragged() || response.clicked() { + let t = ((pos.y - bars.top()) / bars.height()).clamp(0., 1.); + let db = t * STEP_MIN; + settings.pattern[k] = if db > -0.5 { 0. } else { db.round() }; + if k >= settings.steps { + settings.steps = k + 1; + } + } + } + for db in [0f32, -6., -12., -18., -24.] { + let y = step_y(bars, db); + p.line_segment( + [Pos2::new(bars.left(), y), Pos2::new(bars.right(), y)], + Stroke::new(1.0_f32, GRID), + ); + p.text( + Pos2::new(outer.left() + 6., y), + egui::Align2::LEFT_CENTER, + if db == 0. { + "0".into() + } else { + format!("{db:.0}") + }, + FontId::proportional(10.), + DIM, + ); + } + let current = state.analyzer.step; + let playing = state.analyzer.note.is_some(); + for k in 0..STEPS { + let on = k < settings.steps; + let x0 = bars.left() + k as f32 * col + 3.; + let x1 = x0 + col - 6.; + let top = step_y(bars, settings.pattern[k]); + let color = if !on { + DIM.gamma_multiply(0.5) + } else if playing && k == current { + TEXT + } else { + ACCENT + }; + p.rect_filled( + Rect::from_min_max( + Pos2::new(x0, top.min(bars.bottom() - 2.)), + Pos2::new(x1, bars.bottom()), + ), + 1., + color.gamma_multiply(if on { 0.85 } else { 0.4 }), + ); + let grit = settings.grit_steps[k] && settings.grit > GRIT_OFF; + let square = Rect::from_center_size(Pos2::new((x0 + x1) / 2., toggles_y), Vec2::splat(9.)); + if settings.grit_steps[k] { + p.rect_filled(square, 1., if grit && on { FOCUS } else { DIM }); + } else { + p.rect_stroke( + square, + 1., + Stroke::new(1.0_f32, DIM), + egui::StrokeKind::Inside, + ); + } + } + p.text( + Pos2::new(outer.left() + 6., toggles_y), + egui::Align2::LEFT_CENTER, + "grit", + FontId::proportional(10.), + DIM, + ); +} + +fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) { + let d = Settings::default(); + ui.horizontal(|ui| { + caption(ui, "PATTERN"); + for (name, help, gains) in PATTERNS { + let selected = settings.steps == gains.len() + && settings.pattern[..gains.len()] + .iter() + .zip(gains) + .all(|(a, b)| a == b); + let r = ui.selectable_label(selected, RichText::new(name.to_uppercase()).size(13.)); + if r.clicked() { + settings.apply_pattern(gains); + } + hint(&r, state, help); + } + ui.add_space(24.); + caption(ui, "STEPS"); + let minus = ui.button(RichText::new("−").size(14.)); + if minus.clicked() && settings.steps > 1 { + settings.steps -= 1; + } + ui.label( + RichText::new(format!("{:>2}", settings.steps)) + .monospace() + .size(14.) + .color(TEXT), + ); + let plus = ui.button(RichText::new("+").size(14.)); + if plus.clicked() && settings.steps < STEPS { + settings.steps += 1; + } + let help = "Pattern length in cycles. 2 makes a sub-octave, 3 an octave and a fifth down."; + hint(&minus, state, help); + hint(&plus, state, help); + }); + ui.add_space(4.); + ui.columns(10, |c| { + knob( + &mut c[0], + state, + "Edge", + &mut settings.edge, + 5. ..=50., + d.edge, + " %", + "How soft each step change is, as a share of a cycle. Low is sharper and buzzier.", + ); + knob( + &mut c[1], + state, + "Level", + &mut settings.level_jitter, + 0. ..=6., + d.level_jitter, + " dB", + "Random level per cycle: a rough, living surface.", + ); + knob( + &mut c[2], + state, + "Time", + &mut settings.time_jitter, + 0. ..=10., + d.time_jitter, + " %", + "Random timing per cycle, in percent of a period: rattle and scrape.", + ); + knob( + &mut c[3], + state, + "Grit", + &mut settings.grit, + GRIT_OFF..=0., + d.grit, + " dB", + "Level of the noise burst in each cycle, relative to the input. Fully left is off.", + ); + knob( + &mut c[4], + state, + "Tone", + &mut settings.tone, + 4. ..=64., + d.tone, + " ×f0", + "Lowest frequency of the grit, in harmonics of the note.", + ); + knob( + &mut c[5], + state, + "Position", + &mut settings.position, + 0. ..=100., + d.position, + " %", + "Where in the cycle the grit burst sits.", + ); + knob( + &mut c[6], + state, + "Length", + &mut settings.length, + 2. ..=50., + d.length, + " %", + "Length of the grit burst, as a share of a cycle.", + ); + knob( + &mut c[7], + state, + "Gate", + &mut settings.gate, + -60. ..=-20., + d.gate, + " dB", + "Input level that counts as a note (AUDIO). Raise it if tails retrigger.", + ); + knob( + &mut c[8], + state, + "Mix", + &mut settings.mix, + 0. ..=100., + d.mix, + " %", + "Blend with the delayed dry signal.", + ); + knob( + &mut c[9], + state, + "Output", + &mut settings.output, + -12. ..=12., + d.output, + " dB", + "Output trim.", + ); + }); +} diff --git a/tools/editor-check/Cargo.lock b/tools/editor-check/Cargo.lock index d719d79..0877146 100644 --- a/tools/editor-check/Cargo.lock +++ b/tools/editor-check/Cargo.lock @@ -158,6 +158,8 @@ dependencies = [ "crucible-ui", "egui", "image", + "knurl-core", + "knurl-ui", "lathe-core", "lathe-ui", "patina-core", @@ -252,6 +254,24 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "knurl-core" +version = "0.1.0" +dependencies = [ + "common-dsp", + "serde", +] + +[[package]] +name = "knurl-ui" +version = "0.1.0" +dependencies = [ + "common-dsp", + "common-ui", + "egui", + "knurl-core", +] + [[package]] name = "lathe-core" version = "0.1.0" diff --git a/tools/editor-check/Cargo.toml b/tools/editor-check/Cargo.toml index 64b0127..7171e32 100644 --- a/tools/editor-check/Cargo.toml +++ b/tools/editor-check/Cargo.toml @@ -23,5 +23,7 @@ lathe-core = { path = "../../plugins/lathe/core" } lathe-ui = { path = "../../plugins/lathe/ui" } temper-core = { path = "../../plugins/temper/core" } temper-ui = { path = "../../plugins/temper/ui" } +knurl-core = { path = "../../plugins/knurl/core" } +knurl-ui = { path = "../../plugins/knurl/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 f735f97..3418123 100644 --- a/tools/editor-check/src/main.rs +++ b/tools/editor-check/src/main.rs @@ -11,11 +11,11 @@ fn main() { .map(|a| a[1].as_str()) .unwrap_or("rivet"); if ![ - "rivet", "patina", "anvil", "crucible", "lathe", "temper", "weld", + "rivet", "patina", "anvil", "crucible", "lathe", "temper", "weld", "knurl", ] .contains(&plugin) { - eprintln!("Use --plugin rivet, patina, anvil, crucible, lathe, temper or weld"); + eprintln!("Use --plugin rivet, patina, anvil, crucible, lathe, temper, weld or knurl"); std::process::exit(2); } let patina = plugin == "patina" || args.iter().any(|arg| arg == "--resonance"); @@ -25,7 +25,10 @@ fn main() { let lathe = plugin == "lathe"; let temper = plugin == "temper"; let weld = plugin == "weld"; - let (width, height) = if weld { + let knurl = plugin == "knurl"; + let (width, height) = if knurl { + (knurl_ui::WIDTH, knurl_ui::HEIGHT) + } else if weld { (weld_ui::WIDTH, weld_ui::HEIGHT) } else if temper { (temper_ui::WIDTH, temper_ui::HEIGHT) @@ -146,6 +149,22 @@ fn main() { } temper_state.accept_frame(engine.take_analyzer()); } + let mut knurl_settings = knurl_core::Settings::default(); + let mut knurl_state = knurl_ui::State::default(); + if knurl { + // An F1 saw bass with the default Fry pattern, jitter and grit. + let mut engine = knurl_core::Engine::new(48000., 2).unwrap(); + engine.set_settings(knurl_settings).unwrap(); + let mut saw = common_dsp::Saw::new(48000., 43.65); + for i in 0..36000 { + let x = (saw.sample() * 2.2).tanh() * 0.5; + engine.process_sample([x, x]); + if i % 1600 == 0 { + knurl_state.accept_frame(engine.take_analyzer()); + } + } + knurl_state.accept_frame(engine.take_analyzer()); + } let mut weld_settings = weld_core::Settings::default(); let mut weld_state = weld_ui::State::default(); if weld { @@ -275,7 +294,9 @@ fn main() { let session_motif = session.motifs[0].id; shared.replace(session).unwrap(); let ctx = egui::Context::default(); - if weld { + if knurl { + knurl_ui::style(&ctx); + } else if weld { weld_ui::style(&ctx); } else if temper { temper_ui::style(&ctx); @@ -310,7 +331,9 @@ fn main() { }, |ui| { egui::CentralPanel::default().show(ui, |ui| { - if weld { + if knurl { + knurl_ui::show(ui, &mut knurl_settings, &mut knurl_state); + } else if weld { weld_ui::show(ui, &mut weld_settings, &mut weld_state); } else if temper { temper_ui::show(ui, &mut temper_settings, &mut temper_state);