use forge_core::{Settings, Source, ORDERS}; use knurl_core::{Seed, GRIT_OFF, STEPS, STEP_MIN}; use lathe_core::HARMONICS; use nih_plug::prelude::*; use nih_plug_egui::EguiState; use std::sync::{Arc, RwLock}; use temper_core::Curve; #[derive(Enum, Debug, PartialEq, Eq, Clone, Copy)] pub enum SourceParam { #[id = "audio"] Audio, #[id = "midi"] #[name = "MIDI"] Midi, } /// The six orders of `ORDERS`, in the same order. #[derive(Enum, Debug, PartialEq, Eq, Clone, Copy)] pub enum OrderParam { #[id = "lathe-temper-knurl"] #[name = "Lathe, Temper, Knurl"] LatheTemperKnurl, #[id = "temper-lathe-knurl"] #[name = "Temper, Lathe, Knurl"] TemperLatheKnurl, #[id = "lathe-knurl-temper"] #[name = "Lathe, Knurl, Temper"] LatheKnurlTemper, #[id = "temper-knurl-lathe"] #[name = "Temper, Knurl, Lathe"] TemperKnurlLathe, #[id = "knurl-lathe-temper"] #[name = "Knurl, Lathe, Temper"] KnurlLatheTemper, #[id = "knurl-temper-lathe"] #[name = "Knurl, Temper, Lathe"] KnurlTemperLathe, } #[derive(Enum, Debug, PartialEq, Eq, Clone, Copy)] pub enum CurveParam { #[id = "soft"] Soft, #[id = "hard"] Hard, #[id = "fold"] Fold, #[id = "tube"] Tube, } #[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) } fn on(name: &str) -> BoolParam { BoolParam::new(format!("{name} on"), true) } /// Knurl's drawn pattern: step gains and grit switches. #[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 = knurl_core::Settings::default(); Self { gains: d.pattern, grit: d.grit_steps, } } } /// Drawn data saved with the project, not exposed as automation lanes. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Drawn { pub gains: [f32; HARMONICS], pub pattern: Pattern, } impl Default for Drawn { fn default() -> Self { Self { gains: lathe_core::Settings::default().gains, pattern: Pattern::default(), } } } #[derive(Params)] pub struct LatheParams { #[persist = "gains"] pub gains: RwLock<[f32; HARMONICS]>, #[id = "on"] pub on: BoolParam, #[id = "tilt"] pub tilt: FloatParam, #[id = "odd-even"] pub odd_even: FloatParam, #[id = "between"] pub between: FloatParam, #[id = "width"] pub width: FloatParam, #[id = "smooth"] pub smooth: FloatParam, #[id = "mix"] pub mix: FloatParam, #[id = "output"] pub output: FloatParam, #[id = "harmonics"] pub harmonics: IntParam, #[id = "snap"] pub snap: BoolParam, } impl Default for LatheParams { fn default() -> Self { let d = lathe_core::Settings::default(); Self { gains: RwLock::new(d.gains), on: on("Lathe"), tilt: linear("Lathe tilt", d.tilt, -12., 12., " dB/oct"), odd_even: linear("Lathe odd/even", d.odd_even, -12., 12., " dB"), between: linear("Lathe between", d.between, -24., 12., " dB"), width: linear("Lathe width", d.width, 5., 50., " %"), smooth: linear("Lathe smooth", d.smooth, 0., 100., " ms"), mix: linear("Lathe mix", d.mix, 0., 100., " %"), output: linear("Lathe output", d.output, -12., 12., " dB"), harmonics: IntParam::new( "Lathe harmonics", d.harmonics as i32, IntRange::Linear { min: 8, max: HARMONICS as i32, }, ), snap: BoolParam::new("Lathe snap to semitone", d.snap), } } } #[derive(Params)] pub struct TemperParams { #[id = "on"] pub on: BoolParam, #[id = "curve"] pub curve: EnumParam, #[id = "drive"] pub drive: FloatParam, #[id = "bias"] pub bias: FloatParam, #[id = "normalize"] pub normalize: FloatParam, #[id = "floor"] pub floor: FloatParam, #[id = "push"] pub push_gain: FloatParam, #[id = "bright"] pub bright: FloatParam, #[id = "mix"] pub mix: FloatParam, #[id = "output"] pub output: FloatParam, #[id = "push-at"] pub push_harmonic: IntParam, #[id = "auto-gain"] pub auto_gain: BoolParam, } impl Default for TemperParams { fn default() -> Self { let d = temper_core::Settings::default(); Self { on: on("Temper"), curve: EnumParam::new("Temper curve", CurveParam::Soft), drive: linear("Temper drive", d.drive, 0., 36., " dB"), bias: linear("Temper bias", d.bias, 0., 100., " %"), normalize: linear("Temper normalize", d.normalize, 0., 100., " %"), floor: linear("Temper floor", d.floor, 0., 2., " ×f0"), push_gain: linear("Temper push", d.push_gain, -12., 12., " dB"), bright: linear("Temper bright", d.bright, 0., 64., " ×f0"), mix: linear("Temper mix", d.mix, 0., 100., " %"), output: linear("Temper output", d.output, -12., 12., " dB"), push_harmonic: IntParam::new( "Temper push at", d.push_harmonic as i32, IntRange::Linear { min: 1, max: 16 }, ), auto_gain: BoolParam::new("Temper auto gain", d.auto_gain), } } } #[derive(Params)] pub struct KnurlParams { #[persist = "pattern"] pub pattern: RwLock, #[id = "on"] pub on: BoolParam, #[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 = "mix"] pub mix: FloatParam, #[id = "output"] pub output: FloatParam, #[id = "snap"] pub snap: BoolParam, } impl Default for KnurlParams { fn default() -> Self { let d = knurl_core::Settings::default(); Self { pattern: RwLock::new(Pattern::default()), on: on("Knurl"), seed: EnumParam::new("Knurl random", SeedParam::Same), steps: IntParam::new( "Knurl steps", d.steps as i32, IntRange::Linear { min: 1, max: STEPS as i32, }, ), edge: linear("Knurl edge", d.edge, 5., 50., " %"), level_jitter: linear("Knurl level jitter", d.level_jitter, 0., 6., " dB"), time_jitter: linear("Knurl time jitter", d.time_jitter, 0., 10., " %"), grit: linear("Knurl 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( "Knurl tone", d.tone, FloatRange::Skewed { min: 4., max: 64., factor: FloatRange::skew_factor(-1.), }, ) .with_unit(" ×f0"), position: linear("Knurl position", d.position, 0., 100., " %"), length: linear("Knurl length", d.length, 2., 50., " %"), mix: linear("Knurl mix", d.mix, 0., 100., " %"), output: linear("Knurl output", d.output, -12., 12., " dB"), snap: BoolParam::new("Knurl snap to semitone", d.snap), } } } /// Continuous controls, in the order of the gesture slots. pub const FLOATS: usize = 25; #[derive(Params)] pub struct ForgeParams { #[persist = "editor"] pub editor: Arc, #[id = "source"] pub source: EnumParam, #[id = "gate"] pub gate: FloatParam, #[id = "order"] pub order: EnumParam, #[id = "bypass"] pub bypass: BoolParam, #[nested(id_prefix = "lathe", group = "Lathe")] pub lathe: LatheParams, #[nested(id_prefix = "temper", group = "Temper")] pub temper: TemperParams, #[nested(id_prefix = "knurl", group = "Knurl")] pub knurl: KnurlParams, } impl Default for ForgeParams { fn default() -> Self { let d = Settings::default(); Self { editor: EguiState::from_size(forge_ui::WIDTH, forge_ui::HEIGHT), source: EnumParam::new("Pitch source", SourceParam::Audio), gate: linear("Gate", d.gate, -60., -20., " dB"), order: EnumParam::new("Order", OrderParam::LatheTemperKnurl), bypass: BoolParam::new("Bypass", false).make_bypass(), lathe: LatheParams::default(), temper: TemperParams::default(), knurl: KnurlParams::default(), } } } const CURVES: [(CurveParam, Curve); 4] = [ (CurveParam::Soft, Curve::Soft), (CurveParam::Hard, Curve::Hard), (CurveParam::Fold, Curve::Fold), (CurveParam::Tube, Curve::Tube), ]; const ORDER_PARAMS: [OrderParam; 6] = [ OrderParam::LatheTemperKnurl, OrderParam::TemperLatheKnurl, OrderParam::LatheKnurlTemper, OrderParam::TemperKnurlLathe, OrderParam::KnurlLatheTemper, OrderParam::KnurlTemperLathe, ]; /// Every continuous control's value, in the order of `ForgeParams::floats`. fn float_values(s: &Settings) -> [f32; FLOATS] { let (l, t, k) = (&s.lathe, &s.temper, &s.knurl); [ s.gate, l.tilt, l.odd_even, l.between, l.width, l.smooth, l.mix, l.output, t.drive, t.bias, t.normalize, t.floor, t.push_gain, t.bright, t.mix, t.output, k.edge, k.level_jitter, k.time_jitter, k.grit, k.tone, k.position, k.length, k.mix, k.output, ] } fn switch(setter: &ParamSetter, p: &P, old: P::Plain, new: P::Plain) where P::Plain: PartialEq, { if old != new { setter.begin_set_parameter(p); setter.set_parameter(p, new); setter.end_set_parameter(p); } } impl ForgeParams { /// The drawn data. Falls back to defaults while the editor holds a lock. pub fn drawn(&self) -> Drawn { let mut d = Drawn::default(); if let Ok(g) = self.lathe.gains.read() { d.gains = *g; } if let Ok(p) = self.knurl.pattern.read() { d.pattern = *p; } d } /// Current settings. Drawn data falls back to `last` when the editor holds a lock, so the /// audio thread never waits. pub fn settings(&self, last: &Drawn) -> Settings { let gains = self .lathe .gains .try_read() .map(|g| *g) .unwrap_or(last.gains); let pattern = self .knurl .pattern .try_read() .map(|p| *p) .unwrap_or(last.pattern); let (l, t, k) = (&self.lathe, &self.temper, &self.knurl); let order = ORDER_PARAMS .iter() .position(|o| *o == self.order.value()) .unwrap_or(0); let curve = self.temper.curve.value(); let mut s = Settings { source: match self.source.value() { SourceParam::Audio => Source::Audio, SourceParam::Midi => Source::Midi, }, gate: self.gate.value(), order: ORDERS[order], lathe: lathe_core::Settings { gains: gains.map(|g| { if g.is_finite() { g.clamp(lathe_core::GAIN_MIN, lathe_core::GAIN_MAX) } else { 0. } }), tilt: l.tilt.value(), odd_even: l.odd_even.value(), between: l.between.value(), width: l.width.value(), harmonics: l.harmonics.value() as u32, smooth: l.smooth.value(), snap: l.snap.value(), mix: l.mix.value(), output: l.output.value(), bypass: !l.on.value(), ..lathe_core::Settings::default() }, temper: temper_core::Settings { drive: t.drive.value(), curve: CURVES .iter() .find(|(p, _)| *p == curve) .map_or(Curve::Soft, |(_, c)| *c), bias: t.bias.value(), normalize: t.normalize.value(), floor: t.floor.value(), push_harmonic: t.push_harmonic.value() as u32, push_gain: t.push_gain.value(), bright: t.bright.value(), auto_gain: t.auto_gain.value(), mix: t.mix.value(), output: t.output.value(), bypass: !t.on.value(), ..temper_core::Settings::default() }, knurl: knurl_core::Settings { pattern: pattern.gains.map(|g| { if g.is_finite() { g.clamp(STEP_MIN, 0.) } else { 0. } }), grit_steps: pattern.grit, steps: k.steps.value().clamp(1, STEPS as i32) as usize, edge: k.edge.value(), level_jitter: k.level_jitter.value(), time_jitter: k.time_jitter.value(), grit: k.grit.value(), tone: k.tone.value(), position: k.position.value(), length: k.length.value(), seed: match k.seed.value() { SeedParam::Same => Seed::Same, SeedParam::Free => Seed::Free, }, snap: k.snap.value(), mix: k.mix.value(), output: k.output.value(), bypass: !k.on.value(), ..knurl_core::Settings::default() }, bypass: self.bypass.value(), }; s.share(); s } fn floats(&self) -> [&FloatParam; FLOATS] { let (l, t, k) = (&self.lathe, &self.temper, &self.knurl); [ &self.gate, &l.tilt, &l.odd_even, &l.between, &l.width, &l.smooth, &l.mix, &l.output, &t.drive, &t.bias, &t.normalize, &t.floor, &t.push_gain, &t.bright, &t.mix, &t.output, &k.edge, &k.level_jitter, &k.time_jitter, &k.grit, &k.tone, &k.position, &k.length, &k.mix, &k.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. Drawn data is stored /// directly. pub fn apply( &self, setter: &ParamSetter, old: &Settings, new: &Settings, dragging: bool, open: &mut [bool; FLOATS], ) { if new.lathe.gains != old.lathe.gains { if let Ok(mut g) = self.lathe.gains.write() { *g = new.lathe.gains; } } if new.knurl.pattern != old.knurl.pattern || new.knurl.grit_steps != old.knurl.grit_steps { if let Ok(mut p) = self.knurl.pattern.write() { *p = Pattern { gains: new.knurl.pattern, grit: new.knurl.grit_steps, }; } } let (a, b) = (float_values(old), float_values(new)); for (i, p) in self.floats().into_iter().enumerate() { if a[i] != b[i] { if !open[i] { open[i] = true; setter.begin_set_parameter(p); } setter.set_parameter(p, b[i]); } if !dragging && open[i] { setter.end_set_parameter(p); open[i] = false; } } let (l, t, k) = (&self.lathe, &self.temper, &self.knurl); switch(setter, &self.bypass, old.bypass, new.bypass); switch(setter, &l.on, !old.lathe.bypass, !new.lathe.bypass); switch(setter, &t.on, !old.temper.bypass, !new.temper.bypass); switch(setter, &k.on, !old.knurl.bypass, !new.knurl.bypass); switch(setter, &l.snap, old.lathe.snap, new.lathe.snap); switch(setter, &k.snap, old.knurl.snap, new.knurl.snap); switch( setter, &t.auto_gain, old.temper.auto_gain, new.temper.auto_gain, ); switch( setter, &l.harmonics, old.lathe.harmonics as i32, new.lathe.harmonics as i32, ); switch( setter, &t.push_harmonic, old.temper.push_harmonic as i32, new.temper.push_harmonic as i32, ); switch( setter, &k.steps, old.knurl.steps as i32, new.knurl.steps as i32, ); let source = |s: Source| match s { Source::Audio => SourceParam::Audio, Source::Midi => SourceParam::Midi, }; switch(setter, &self.source, source(old.source), source(new.source)); let order = |s: &Settings| ORDER_PARAMS[ORDERS.iter().position(|o| *o == s.order).unwrap_or(0)]; switch(setter, &self.order, order(old), order(new)); let curve = |c: Curve| { CURVES .iter() .find(|(_, k)| *k == c) .map_or(CurveParam::Soft, |(p, _)| *p) }; switch( setter, &t.curve, curve(old.temper.curve), curve(new.temper.curve), ); let seed = |s: Seed| match s { Seed::Same => SeedParam::Same, Seed::Free => SeedParam::Free, }; switch(setter, &k.seed, seed(old.knurl.seed), seed(new.knurl.seed)); } }