//! Host parameters shared by every tracked plugin: Note mode's note, the key lock and //! Follow bends. Plugins nest [`PitchParams`] in their own parameter struct. use common_dsp::tracker::{Pitch, Scale, NOTE_MAX, NOTE_MIN}; use common_dsp::note_name; use nih_plug::prelude::*; use std::sync::Arc; #[derive(Enum, Debug, PartialEq, Eq, Clone, Copy)] pub enum ScaleParam { #[id = "free"] #[name = "Off"] Free, #[id = "minor"] Minor, #[id = "major"] Major, #[id = "root"] #[name = "Root only"] Root, } impl From for Scale { fn from(s: ScaleParam) -> Self { match s { ScaleParam::Free => Scale::Free, ScaleParam::Minor => Scale::Minor, ScaleParam::Major => Scale::Major, ScaleParam::Root => Scale::Root, } } } impl From for ScaleParam { fn from(s: Scale) -> Self { match s { Scale::Free => ScaleParam::Free, Scale::Minor => ScaleParam::Minor, Scale::Major => ScaleParam::Major, Scale::Root => ScaleParam::Root, } } } #[derive(Params)] pub struct PitchParams { /// The note played in Note mode; its pitch class is the key root. #[id = "note"] pub note: IntParam, #[id = "scale"] pub scale: EnumParam, #[id = "bends"] pub bends: BoolParam, } impl Default for PitchParams { fn default() -> Self { let d = Pitch::default(); Self { note: IntParam::new( "Note", d.note as i32, IntRange::Linear { min: NOTE_MIN as i32, max: NOTE_MAX as i32, }, ) .with_value_to_string(Arc::new(|v| note_name(v.clamp(0, 127) as u8))) .with_string_to_value(Arc::new(|s| parse_note(s).map(i32::from))), scale: EnumParam::new("Key lock", ScaleParam::Free), bends: BoolParam::new("Follow bends", d.bends), } } } impl PitchParams { pub fn pitch(&self) -> Pitch { Pitch { note: (self.note.value().clamp(NOTE_MIN as i32, NOTE_MAX as i32)) as u8, scale: self.scale.value().into(), bends: self.bends.value(), } } /// Pushes editor changes to the host, one gesture per change. pub fn apply(&self, setter: &ParamSetter, old: &Pitch, new: &Pitch) { if old.note != new.note { setter.begin_set_parameter(&self.note); setter.set_parameter(&self.note, new.note as i32); setter.end_set_parameter(&self.note); } if old.scale != new.scale { setter.begin_set_parameter(&self.scale); setter.set_parameter(&self.scale, new.scale.into()); setter.end_set_parameter(&self.scale); } if old.bends != new.bends { setter.begin_set_parameter(&self.bends); setter.set_parameter(&self.bends, new.bends); setter.end_set_parameter(&self.bends); } } } /// Reads a note name such as "F1" within the Note range. pub fn parse_note(text: &str) -> Option { common_dsp::parse_note(text).filter(|n| (NOTE_MIN..=NOTE_MAX).contains(n)) } #[cfg(test)] mod tests { use super::*; #[test] fn note_names_round_trip() { for n in NOTE_MIN..=NOTE_MAX { assert_eq!(parse_note(¬e_name(n)), Some(n)); } assert_eq!(parse_note("f1"), Some(29)); assert_eq!(parse_note("H2"), None); } #[test] fn scale_maps_both_ways() { for s in [Scale::Free, Scale::Minor, Scale::Major, Scale::Root] { assert_eq!(Scale::from(ScaleParam::from(s)), s); } } }