use crate::parameter::unsigned; use crate::parameter::{self, DecodeError, RegisterCodec, StoredValue, bin_enum}; use mfctl_derive::author; use serde::Deserialize; use std::fmt; #[author] #[derive(Default)] #[serde(default, deny_unknown_fields)] pub struct Lfo { /// risingSaw rises then drops; random is stepped; rndGlide glides between random values. pub shape: Shape, /// off runs continuously; on restarts on every note trigger; legato restarts only when notes do not overlap. pub retrigger: Retrigger, #[serde(flatten, deserialize_with = "Rate::deserialize_or_default")] #[schemars(with = "Option")] /// Choose rateHz for free running or rateDivision for tempo sync. Omission defaults to synchronized 1/16. pub rate: Rate, } bin_enum! { #[serde(rename_all = "camelCase")] pub enum Shape { #[default] Sine, Triangle, RisingSaw, Square, Random, RndGlide, } } bin_enum! { #[serde(rename_all = "camelCase")] pub enum Retrigger { #[default] Off, On, Legato, } } #[author] #[schemars(extend("default" = Self::default()))] pub enum Rate { /// Free-running LFO rate in hertz. #[serde(rename = "rateHz")] Free(LfoHz), /// Tempo-synchronized LFO rate division. #[serde(rename = "rateDivision")] Sync(Division), } impl Default for Rate { fn default() -> Self { Self::Sync(Division::default()) } } impl Rate { // Flattened enums require a variant even for an empty map. Default only // omission: flattening Option would also swallow invalid rate values. fn deserialize_or_default<'de, D: serde::Deserializer<'de>>( deserializer: D, ) -> Result { struct RateVisitor; impl<'de> serde::de::Visitor<'de> for RateVisitor { type Value = Rate; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("rateHz or rateDivision") } fn visit_map>(self, map: M) -> Result { let fields = serde_yaml::Mapping::deserialize( serde::de::value::MapAccessDeserializer::new(map), )?; if fields.is_empty() { Ok(Rate::default()) } else if fields.len() == 1 { Rate::deserialize(serde::de::value::MapAccessDeserializer::new( serde::de::value::MapDeserializer::new(fields.into_iter()), )) .map_err(serde::de::Error::custom) } else { Err(serde::de::Error::custom( "choose rateHz or rateDivision, not both", )) } } } deserializer.deserialize_struct("Rate", &["rateHz", "rateDivision"], RateVisitor) } } #[author] #[derive(Default, strum_macros::FromRepr)] #[repr(u16)] #[schemars(extend("default" = Self::default()))] pub enum Division { #[serde(rename = "8/1")] EightBars, #[serde(rename = "4/1")] FourBars, #[serde(rename = "2/1")] TwoBars, #[serde(rename = "1/1")] Whole, #[serde(rename = "1/2")] Half, #[serde(rename = "1/2T")] HalfTriplet, #[serde(rename = "1/4")] Quarter, #[serde(rename = "1/4T")] QuarterTriplet, #[serde(rename = "1/8")] Eighth, #[serde(rename = "1/8T")] EighthTriplet, #[default] #[serde(rename = "1/16")] Sixteenth, #[serde(rename = "1/16T")] SixteenthTriplet, #[serde(rename = "1/32")] ThirtySecond, } impl RegisterCodec for Division { fn encode(&self) -> StoredValue { parameter::encode_bin(*self as u16, Self::ThirtySecond as u16) } fn decode(raw: StoredValue) -> Result { let maximum = Self::ThirtySecond as i32; // Presets can contain negative stored values. Interpret the register as // signed before scaling and clamp the resulting division index. let index = ((i32::from(raw.0 as i16) * (maximum + 1)) >> 15).clamp(0, maximum); Ok(Self::from_repr(index as u16).unwrap()) } } #[cfg(test)] mod tests { use super::*; #[test] fn divisions_round_trip_and_negative_registers_clamp_to_eight_bars() { for index in 0..=12 { let division = Division::from_repr(index).unwrap(); assert_eq!(Division::decode(division.encode()).unwrap(), division); } for raw in [0x8000, 0xB557, 0xB71D, 0xFFFF] { assert_eq!( Division::decode(StoredValue(raw)).unwrap(), Division::EightBars ); } assert_eq!( Division::decode(StoredValue(32767)).unwrap(), Division::ThirtySecond ); } #[test] fn free_rate_codec_preserves_every_register() { for raw in 0..=32767 { let raw = StoredValue(raw); assert_eq!(LfoHz::decode(raw).unwrap().encode(), raw); } assert!(LfoHz::decode(StoredValue(32768)).is_err()); } #[test] fn normalized_curve_reproduces_firmware_anchor_increments() { for (raw, increment) in [ (0, 89478), (256, 93857), (8192, 496547), (16384, 3218783), (24576, 21423474), (32512, 134912637), ] { let hz = LfoHz::decode(StoredValue(raw)).unwrap().0; assert_eq!((hz * 4294967296.0 / 3000.0).floor() as u32, increment); } assert!((LfoHz::from_raw(32768).0 - 100.0).abs() < 1e-12); } #[test] fn free_rate_uses_nominal_physical_hz_and_preserves_the_default_register() { use garde::Validate; assert!((LfoHz::decode(StoredValue(0)).unwrap().0 - 0.0625).abs() < 1e-6); assert!((LfoHz::decode(StoredValue(32767)).unwrap().0 - 100.0).abs() < 0.03); assert_eq!(LfoHz::default().encode(), StoredValue(4864)); assert_eq!(LfoHz(100.0).encode(), StoredValue(32767)); assert!(LfoHz(0.0625).validate().is_ok()); assert!(LfoHz(100.0).validate().is_ok()); assert!(LfoHz(0.06).validate().is_err()); assert!(LfoHz(106.0).validate().is_err()); let rate = LfoHz::decode(LfoHz(80.0).encode()).unwrap().0; assert!((rate - 80.0).abs() < 0.01); } } /// Nominal free-running frequency, not the firmware's miscalibrated OLED scale. #[author] #[serde(transparent)] #[garde(transparent)] pub struct LfoHz(#[garde(range(min = 0.0625, max = 100.0))] pub f64); impl LfoHz { // Firmware bug: OLED conversion 0x0800e458 normalizes the penultimate LFO // table anchor to 100 Hz instead of the final anchor, overstating the rate. // Use the underlying exponential, normalized between 1/16 Hz and 100 Hz: // min + (max - min) * (2000^(raw/32768) - 1) / (2000 - 1). // Quantizing this curve to phase increments at nominal 3000 updates/s // reproduces all 129 firmware anchors exactly. Runtime linearly interpolates // those anchors; the smooth curve differs between them by at most ~0.044%. fn from_raw(raw: u16) -> Self { Self( 0.0625 + (100.0 - 0.0625) * (2000.0_f64.ln() * f64::from(raw) / 32768.0).exp_m1() / 1999.0, ) } } impl Default for LfoHz { fn default() -> Self { Self::from_raw(4864) } } impl RegisterCodec for LfoHz { fn encode(&self) -> StoredValue { StoredValue( (32768.0 * ((self.0 - 0.0625) * 1999.0 / (100.0 - 0.0625)).ln_1p() / 2000.0_f64.ln()) .round() .min(32767.0) as u16, ) } fn decode(raw: StoredValue) -> Result { Ok(Self::from_raw(unsigned::(raw)?)) } }