//! Host-independent controls. Levels in dB, times in milliseconds, amounts in percent. use serde::{Deserialize, Serialize}; /// Bands: the fundamental, low harmonics, mid harmonics and the top. pub const BANDS: usize = 4; pub const GAIN_MIN: f32 = -24.; pub const GAIN_MAX: f32 = 24.; /// Highest harmonic a band edge can be set to. pub const TOP_MAX: u32 = 48; #[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)] #[serde(deny_unknown_fields)] pub struct Settings { pub source: Source, /// Highest harmonic in the bottom band. 1 keeps the fundamental on its own. pub low: u32, /// Highest harmonic in the second band. pub mid: u32, /// Highest harmonic in the third band; everything above is the top band. pub high: u32, /// Share of the compressed signal, per band, against the dry band. pub depth: f32, /// Scales every attack and release time. pub time: f32, /// Amount of upward compression: quiet bands are lifted. pub upward: f32, /// Amount of downward compression: loud bands are pushed down. pub downward: f32, /// Gain on the level the compressors see, not on the audio: moves the band levels against /// the thresholds. More drive means more squash; at Depth 0 it changes nothing. pub drive: f32, /// Gain of each band after its compressor, bottom band first. pub gains: [f32; BANDS], /// Retune time when the pitch slides or bends. pub smooth: f32, /// Detection threshold in dBFS for note starts (Audio source). pub gate: f32, /// Round detected pitch to the nearest semitone. Off by default: the level window is one /// period of the pitch in use, and a detuned note then gets a window that does not fit. pub snap: bool, pub output: f32, pub bypass: bool, } impl Default for Settings { fn default() -> Self { Self { source: Source::Audio, low: 1, mid: 4, high: 16, depth: 50., time: 100., upward: 100., downward: 100., drive: 0., gains: [0.; BANDS], smooth: 10., gate: -42., snap: false, output: 0., bypass: false, } } } impl Settings { pub fn validate(&self) -> Result<(), &'static str> { let finite = [ self.depth, self.time, self.upward, self.downward, self.drive, self.smooth, self.gate, self.output, ] .iter() .chain(self.gains.iter()) .all(|v| v.is_finite()); if !finite { return Err("non-finite setting"); } let ok = (1..=8).contains(&self.low) && (2..=16).contains(&self.mid) && (3..=TOP_MAX).contains(&self.high) && self.gains.iter().all(|g| (GAIN_MIN..=GAIN_MAX).contains(g)) && (0.0..=100.).contains(&self.depth) && (10.0..=1000.).contains(&self.time) && (0.0..=100.).contains(&self.upward) && (0.0..=100.).contains(&self.downward) && (-24.0..=24.).contains(&self.drive) && (0.0..=100.).contains(&self.smooth) && (-60.0..=-20.).contains(&self.gate) && (-24.0..=24.).contains(&self.output); if ok { Ok(()) } else { Err("setting out of range") } } /// Highest harmonic of the bottom three bands, in rising order. Host automation can set /// the edges in any order, so a lower edge pushes the next one up instead of failing. pub fn tops(&self) -> [u32; BANDS - 1] { let low = self.low.clamp(1, 8); let mid = self.mid.max(low + 1); let high = self.high.max(mid + 1); [low, mid, high] } /// Text for the harmonics of `band`, like "1", "2–4" or "17+". pub fn band_label(&self, band: usize) -> String { let t = self.tops(); let first = if band == 0 { 1 } else { t[band - 1] + 1 }; match band { 3 => format!("{first}+"), _ if first == t[band] => format!("{first}"), _ => format!("{first}–{}", t[band]), } } }