//! Host-independent controls. Levels in dB, times in milliseconds, frequencies in Hz. 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, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct Settings { pub source: Source, /// 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, 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, } impl Default for Settings { fn default() -> Self { Self { source: Source::Audio, level: -9., split: 90., dynamics: 0., drive: 0., octave: 0, glide: 0., phase: 0., gate: -42., keep: 0., output: 0., retrigger: true, snap: true, bypass: false, } } } impl Settings { pub fn validate(&self) -> Result<(), &'static str> { let finite = [ self.level, self.split, self.dynamics, self.drive, self.glide, self.phase, self.gate, self.keep, self.output, ] .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) && (-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); if ok { Ok(()) } else { Err("setting out of range") } } }