Something went wrong. Try again.
YAML-based live configuration of MicroFreak datyoma.tngl.io/mfctl
microfreak
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251use 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<Rate>")] /// 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<Self> would also swallow invalid rate values. fn deserialize_or_default<'de, D: serde::Deserializer<'de>>( deserializer: D, ) -> Result<Self, D::Error> { 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<M: serde::de::MapAccess<'de>>(self, map: M) -> Result<Rate, M::Error> { 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<Self, DecodeError> { 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<Self, DecodeError> { Ok(Self::from_raw(unsigned::<Self>(raw)?)) }}