use super::value::Percent; use super::value::integer_control; use crate::parameter::bin_enum; use crate::parameter::{DecodeError, RegisterCodec, StoredValue}; use mfctl_derive::author; #[author] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct Arp { #[garde(skip)] /// true enables the arpeggiator when sequencer is false. pub enabled: bool, /// Octave range; 1 uses the held-note pitches only. pub range: ArpRange, /// Arpeggiator/sequencer step length as a tempo division. Active when sync is true; T means triplet. pub division: Division, /// Arpeggiator step rate in beats per minute; active when sync is false. pub rate_bpm: ArpRateBpm, /// Delays alternate steps: 50% gives straight timing, 75% gives maximum swing. pub swing: Swing, #[garde(skip)] /// true uses division for tempo-synchronized steps; false uses rateBpm. pub sync: bool, /// Intensity of Dice variation in gates, triggers, octaves, and velocity (0–100%). Requires an active arpeggio or sequence. Not saved with the preset. pub spice: Percent, /// Random variation of gates and triggers (0–100%); spice sets its intensity. Requires an active arpeggio or sequence. Not saved with the preset. pub dice: Percent, /// Pattern repeats a semi-random note order that changes when the held notes change. pub direction: Direction, #[garde(skip)] /// true selects the sequencer; false selects the arpeggiator. pub sequencer: bool, } impl Default for Arp { fn default() -> Self { Self { enabled: false, range: ArpRange(1), division: Division::default(), rate_bpm: ArpRateBpm(120.0), swing: Swing(50), sync: false, spice: Percent::default(), dice: Percent::default(), direction: Direction::default(), sequencer: false, } } } bin_enum! { pub enum Direction { #[default] Up, Order, Random, Pattern, } } bin_enum! { pub enum Division { #[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, #[serde(rename = "1/16")] #[default] Sixteenth, #[serde(rename = "1/16T")] SixteenthTriplet, #[serde(rename = "1/32")] ThirtySecond, #[serde(rename = "1/32T")] ThirtySecondTriplet, } } integer_control!(ArpRange, u8, 1, 4); #[author] #[serde(transparent)] #[garde(transparent)] pub struct ArpRateBpm(#[garde(range(min = 30.0, max = 240.0))] pub f64); impl RegisterCodec for ArpRateBpm { fn encode(&self) -> StoredValue { StoredValue(((self.0 - 30.0) * 150.0).round() as u16) } fn decode(raw: StoredValue) -> Result { // Higher registers exceed the supported 240 BPM author range. if raw.0 > 31500 { return Err(DecodeError::new::(raw)); } Ok(Self(30.0 + f64::from(raw.0) / 150.0)) } } integer_control!(Swing, u8, 50, 75); #[cfg(test)] mod tests { use super::*; use crate::parameter::{RegisterCodec, StoredValue}; use crate::patch::value::tests::check_integer_controls; use garde::Validate; #[test] fn integer_controls_round_trip_their_entire_author_ranges() { check_integer_controls!(ArpRange: 1 => 4, Swing: 50 => 75); } #[test] fn rate_codec_matches_firmware() { for (bpm, raw) in [(30.0, 0), (120.0, 13500), (240.0, 31500)] { assert_eq!(ArpRateBpm(bpm).encode(), StoredValue(raw)); assert_eq!(ArpRateBpm::decode(StoredValue(raw)).unwrap().0, bpm); } assert!(ArpRateBpm::decode(StoredValue(31501)).is_err()); } }