//! Host-independent controls. Levels in dB, harmonic positions as multiples of the fundamental. 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. Midi, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] pub enum Curve { /// tanh: smooth, odd harmonics, rounded top. #[default] Soft, /// Clipping with a narrow knee: dense, square-like. Hard, /// Sine wavefolder: drive folds the wave back, adding moving upper harmonics. Fold, /// Asymmetric: the negative half clips earlier, adding even harmonics. Tube, } impl Curve { pub const ALL: [Curve; 4] = [Curve::Soft, Curve::Hard, Curve::Fold, Curve::Tube]; pub fn name(self) -> &'static str { match self { Curve::Soft => "Soft", Curve::Hard => "Hard", Curve::Fold => "Fold", Curve::Tube => "Tube", } } /// Transfer function; maps 0 to 0. #[inline] pub fn shape(self, u: f32) -> f32 { match self { Curve::Soft => u.tanh(), Curve::Hard => { let u = u.clamp(-8., 8.); u / (1. + u.abs().powi(8)).powf(0.125) } // Scaled so full drive gives about five folds; more is mostly aliasing noise. Curve::Fold => (std::f32::consts::FRAC_PI_2 * 0.4 * u.clamp(-64., 64.)).sin(), Curve::Tube => { if u >= 0. { u.tanh() } else { 0.6 * (u / 0.6).tanh() } } } } } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct Settings { pub source: Source, /// Gain into the shaper after level normalisation. pub drive: f32, pub curve: Curve, /// Asymmetry in percent: shifts the curve's operating point, adding even harmonics. pub bias: f32, /// How far the input level is normalised per note before the shaper, in percent. At 100 % /// a quiet patch and a loud patch are driven the same. pub normalize: f32, /// Highpass in front of the shaper at this multiple of the fundamental; 0 is off. pub floor: f32, /// Harmonic that `push_gain` boosts or cuts before the shaper. pub push_harmonic: u32, pub push_gain: f32, /// Lowpass after the shaper at this harmonic number; 0 is off. pub bright: f32, /// Matches output loudness to the input, so Drive changes character, not level. pub auto_gain: bool, /// Detection threshold in dBFS for note starts (Audio source). pub gate: f32, pub mix: f32, pub output: f32, pub bypass: bool, } impl Default for Settings { fn default() -> Self { Self { source: Source::Audio, drive: 12., curve: Curve::Soft, bias: 0., normalize: 100., floor: 0., push_harmonic: 3, push_gain: 0., bright: 0., auto_gain: true, gate: -42., mix: 100., output: 0., bypass: false, } } } impl Settings { pub fn validate(&self) -> Result<(), &'static str> { let finite = [ self.drive, self.bias, self.normalize, self.floor, self.push_gain, self.bright, self.gate, self.mix, self.output, ] .iter() .all(|v| v.is_finite()); if !finite { return Err("non-finite setting"); } let ok = (0.0..=36.).contains(&self.drive) && (0.0..=100.).contains(&self.bias) && (0.0..=100.).contains(&self.normalize) && (self.floor == 0. || (0.25..=2.).contains(&self.floor)) && (1..=16).contains(&self.push_harmonic) && (-12.0..=12.).contains(&self.push_gain) && (self.bright == 0. || (4.0..=64.).contains(&self.bright)) && (-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") } } }