//! Host-independent controls. Gains in dB, times in milliseconds. use serde::{Deserialize, Serialize}; /// Editable harmonics: the fundamental is harmonic 1. pub const HARMONICS: usize = 32; pub const GAIN_MIN: f32 = -24.; pub const GAIN_MAX: f32 = 12.; #[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, /// Gain per harmonic, index 0 = fundamental. pub gains: [f32; HARMONICS], /// dB per octave of harmonic number, added on top of `gains`. Harmonic 1 is the pivot. pub tilt: f32, /// Positive lifts odd harmonics and lowers even ones by half this amount each. pub odd_even: f32, /// Gain of the regions halfway between harmonics: noise, beating, inharmonic grit. pub between: f32, /// Bandwidth of every band as a percentage of the fundamental. pub width: f32, /// Number of harmonics that are shaped; higher ones pass unchanged. pub harmonics: u32, /// 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. pub snap: bool, pub mix: f32, pub output: f32, pub bypass: bool, } impl Default for Settings { fn default() -> Self { Self { source: Source::Audio, gains: [0.; HARMONICS], tilt: 0., odd_even: 0., between: 0., width: 20., harmonics: 24, smooth: 10., gate: -42., snap: true, mix: 100., output: 0., bypass: false, } } } impl Settings { pub fn validate(&self) -> Result<(), &'static str> { let finite = [ self.tilt, self.odd_even, self.between, self.width, self.smooth, self.gate, self.mix, self.output, ] .iter() .chain(self.gains.iter()) .all(|v| v.is_finite()); if !finite { return Err("non-finite setting"); } let ok = self.gains.iter().all(|g| (GAIN_MIN..=GAIN_MAX).contains(g)) && (-12.0..=12.).contains(&self.tilt) && (-12.0..=12.).contains(&self.odd_even) && (-24.0..=12.).contains(&self.between) && (5.0..=50.).contains(&self.width) && (8..=HARMONICS as u32).contains(&self.harmonics) && (0.0..=100.).contains(&self.smooth) && (-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") } } /// Gain applied to harmonic `k` (1-based), with tilt and odd/even included. pub fn effective(&self, k: usize) -> f32 { if k == 0 || k > self.harmonics as usize || k > HARMONICS { return 0.; } let parity = if k % 2 == 1 { 0.5 } else { -0.5 }; (self.gains[k - 1] + self.tilt * (k as f32).log2() + self.odd_even * parity) .clamp(-30., 18.) } }