//! Host-independent controls. Levels in dB, times in milliseconds, frequencies in Hz. use common_dsp::tracker::{Pitch, PitchMode}; use serde::{Deserialize, Serialize}; #[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; the audio only supplies the band above the split. Midi, /// Note starts are detected, the pitch is the set note: nothing is guessed. Note, } impl Source { pub fn mode(self) -> PitchMode { match self { Source::Audio => PitchMode::Audio, Source::Midi => PitchMode::Midi, Source::Note => PitchMode::Note, } } pub fn from_mode(mode: PitchMode) -> Self { match mode { PitchMode::Audio => Source::Audio, PitchMode::Midi => Source::Midi, PitchMode::Note => Source::Note, } } } /// What happens to the original sub. #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] pub enum Mode { /// The band below the split is replaced by the clean sine (plus Keep of the original). #[default] Replace, /// The original sub stays, and the sine is added under it for weight. Enhance, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct Settings { pub source: Source, /// Note mode's note, the key lock and Follow bends. #[serde(default)] pub pitch: Pitch, #[serde(default)] pub mode: Mode, /// Peak level of the new sub in dBFS, the same for every note. pub level: f32, /// Crossover frequency: below it the original sub is replaced. pub split: f32, /// 0 % holds a constant level per note; 100 % follows the original low band's shape. pub dynamics: f32, /// Soft saturation of the sine, adding upper harmonics for small speakers. pub drive: f32, /// Ghost harmonics: adds the sub's harmonics 2–10 above `ghost_from`, so small /// speakers hear the note. Only harmonics the patch lacks are filled. 0 is off. #[serde(default)] pub ghost: f32, /// Lowest frequency Ghost harmonics reach down to, in Hz (never below `split`). #[serde(default = "default_ghost_from")] pub ghost_from: f32, pub octave: i32, /// Pitch slide time between overlapping notes. pub glide: f32, /// Start phase in degrees on every note start. pub phase: f32, /// Detection threshold in dBFS for note starts (Audio source). pub gate: f32, /// Amount of the original low band that is kept under the new sub. pub keep: f32, pub output: f32, /// Restart the sine at `phase` on every note, so each note and kick hit lines up the same. pub retrigger: bool, /// Round detected pitch to the nearest semitone. pub snap: bool, pub bypass: bool, /// Kick lock: on each kick in the sidechain, duck the sub and restart its phase. #[serde(default)] pub kick_lock: bool, /// How far the sub dips on each kick, in dB. #[serde(default = "default_kick_duck")] pub kick_duck: f32, /// Time for the sub to come back after a kick, in ms. #[serde(default = "default_kick_release")] pub kick_release: f32, /// When the sub restarts against the kick's start, in ms. Negative: before the kick. #[serde(default)] pub kick_offset: f32, } const GHOST_FROM: f32 = 300.; const KICK_DUCK: f32 = 6.; const KICK_RELEASE: f32 = 80.; /// Range of the kick offset, in ms either side of the kick. pub const KICK_OFFSET_MAX: f32 = 30.; fn default_kick_duck() -> f32 { KICK_DUCK } fn default_kick_release() -> f32 { KICK_RELEASE } fn default_ghost_from() -> f32 { GHOST_FROM } impl Default for Settings { fn default() -> Self { Self { source: Source::Audio, pitch: Pitch::default(), mode: Mode::Replace, level: -9., split: 90., dynamics: 0., drive: 0., ghost: 0., ghost_from: GHOST_FROM, octave: 0, glide: 0., phase: 0., gate: -42., keep: 0., output: 0., retrigger: true, snap: true, bypass: false, kick_lock: false, kick_duck: KICK_DUCK, kick_release: KICK_RELEASE, kick_offset: 0., } } } impl Settings { pub fn validate(&self) -> Result<(), &'static str> { self.pitch.validate()?; let finite = [ self.level, self.split, self.dynamics, self.drive, self.ghost, self.ghost_from, self.glide, self.phase, self.gate, self.keep, self.output, self.kick_duck, self.kick_release, self.kick_offset, ] .iter() .all(|v| v.is_finite()); if !finite { return Err("non-finite setting"); } let ok = (-30.0..=0.).contains(&self.level) && (40.0..=200.).contains(&self.split) && (0.0..=100.).contains(&self.dynamics) && (0.0..=100.).contains(&self.drive) && (0.0..=100.).contains(&self.ghost) && (150.0..=600.).contains(&self.ghost_from) && (-2..=1).contains(&self.octave) && (0.0..=200.).contains(&self.glide) && (0.0..=360.).contains(&self.phase) && (-60.0..=-20.).contains(&self.gate) && (0.0..=100.).contains(&self.keep) && (-12.0..=12.).contains(&self.output) && (0.0..=24.).contains(&self.kick_duck) && (10.0..=300.).contains(&self.kick_release) && (-KICK_OFFSET_MAX..=KICK_OFFSET_MAX).contains(&self.kick_offset); if ok { Ok(()) } else { Err("setting out of range") } } }