Something went wrong. Try again.
YAML-based live configuration of MicroFreak datyoma.tngl.io/mfctl
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454use crate::parameter::unsigned;use std::error::Error;
use mfctl_derive::author;
use super::value::Percent;use crate::parameter::{ self, DecodeError, RegisterCodec, RegisterReader, Registers, StoredValue, bin_enum,};use parameter::oscillator as param;
/// Sample slot and archive identity; audio is not embedded or transferred.#[author]#[derive(Default)]#[serde(default, deny_unknown_fields)]pub struct Sample { /// Sample slot (1–128). pub index: SampleIndex, /// Full 16-bit archive sample identifier; zero clears it. #[garde(skip)] pub hash: u16,}
/// Oscillator model, controls, and optional sample selection.#[author]#[derive(Default)]#[schemars(extend( "unevaluatedProperties" = false, "if" = serde_json::json!({"properties": {"type": {"enum": ["Sample", "Scan Grains", "Cloud Grains", "Hit Grains"]}}}), "then" = serde_json::json!({"required": ["sample"], "properties": {"sample": {"type": "object"}}})))]pub struct Oscillator { #[serde(flatten)] #[garde(dive)] pub model: Model, /// Required for Sample, Scan Grains, Cloud Grains, and Hit Grains. #[serde(default, skip_serializing_if = "Option::is_none")] #[garde(dive, custom(|sample: &Option<Sample>, _: &()| { if self.model.uses_samples() && sample.is_none() { Err(garde::Error::new("sample is required for sample-based oscillators")) } else { Ok(()) } }))] pub sample: Option<Sample>,}
impl Oscillator { pub(crate) fn from_model_register(raw: StoredValue) -> Result<Self, DecodeError> { let model = Model::from_model_register(raw)?; let sample = model.uses_samples().then(Sample::default); Ok(Self { model, sample }) }
pub(crate) fn encode_controls(&self, registers: &mut Registers) { self.model.encode_controls(registers); let sample = self.sample.unwrap_or_default(); registers.insert( parameter::SAMPLE_INDEX, RegisterCodec::encode(&sample.index), ); registers.insert(parameter::global::SAMPLE_HASH, StoredValue(sample.hash)); }
pub(crate) fn read_controls( &mut self, reader: &mut impl RegisterReader, ) -> Result<(), Box<dyn Error>> { self.model.read_controls(reader)?; let index = reader.value(parameter::SAMPLE_INDEX)?; if let Some(sample) = &mut self.sample { sample.index = index; } else if index != SampleIndex::default() { self.sample = Some(Sample { index, ..Sample::default() }); } Ok(()) }}
impl Model { fn uses_samples(&self) -> bool { matches!( self, Self::Sample(_) | Self::ScanGrains(_) | Self::CloudGrains(_) | Self::HitGrains(_) ) }}
bin_enum! { pub enum Waveform { #[default] Saw, Square, Triangle, Sine, }}
bin_enum! { pub enum ChordType { #[serde(rename = "Oct")] Octave, #[serde(rename = "5th")] Fifth, #[serde(rename = "sus4")] SuspendedFourth, #[serde(rename = "m")] Minor, #[serde(rename = "m7")] MinorSeventh, #[serde(rename = "m9")] MinorNinth, #[serde(rename = "m11")] MinorEleventh, #[serde(rename = "6+9")] SixthNinth, #[serde(rename = "M9")] MajorNinth, #[serde(rename = "M7")] MajorSeventh, #[serde(rename = "M")] #[default] Major, }}
// Slots name the Wave/Timbre/Shape registers explicitly. Model numbers are firmware// IDs, not manual list positions (notably Vocoder). Defaults and codecs share this declaration.macro_rules! oscillator_controls { ( default $default:ident; $( $(#[$attr:meta])* $index:literal => $model:ident { $( $(#[$field_attr:meta])* $slot:ident => $field:ident: $field_type:ty = $field_default:expr ),+ $(,)? } ),+ $(,)? ) => { /// Digital oscillator parameters. Numeric model controls are knob percentages, /// except table numbers; they are not the model-specific screen units. #[author] #[serde(tag = "type", deny_unknown_fields)] #[schemars(extend("default" = Self::default()))] pub enum Model { $($(#[$attr])* $model(model::$model),)+ }
impl Default for Model { fn default() -> Self { Self::$default(model::$default::default()) } }
impl Model { pub(crate) fn from_model_register(raw: StoredValue) -> Result<Self, DecodeError> { match parameter::decode_bin::<Self>(raw, [$($index),+].len() as u16)? { $($index => Ok(Self::$model(model::$model::default())),)+ _ => Err(DecodeError::new::<Self>(raw)), } }
pub(crate) fn encode_controls(&self, registers: &mut Registers) { let model_count = [$($index),+].len() as u16; match self { $(Self::$model(controls) => { registers.insert(param::TYPE, parameter::encode_bin($index, model_count)); $(registers.insert(param::$slot, controls.$field.encode());)+ },)+ } }
pub(crate) fn read_controls(&mut self, reader: &mut impl RegisterReader) -> Result<(), Box<dyn Error>> { match self { $(Self::$model(controls) => { $(reader.field(param::$slot, &mut controls.$field)?;)+ },)+ } Ok(()) } }
pub mod model { use super::*; $( #[author] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct $model { $( $(#[$field_attr])* pub $field: $field_type, )+ }
impl Default for $model { fn default() -> Self { Self { $($field: $field_default,)+ } } } )+ } };}
oscillator_controls! { default BasicWaves; 1 => BasicWaves { /// Morphs from square to saw to two saws (0–100%). WAVE => morph: Percent = Percent(25.6), /// Pulse width and saw phase offset (0–100%). TIMBRE => sym: Percent = Percent(0.0), /// Sine sub-oscillator level (0–100%). SHAPE => sub: Percent = Percent(0.0), }, 2 => SuperWave { /// Waveform used by the detuned oscillator copies. WAVE => waveform: Waveform = Waveform::default(), /// Detuning between oscillator copies (0–100%). TIMBRE => detune: Percent = Percent(50.0), /// Level of the detuned copies (0–100%). SHAPE => volume: Percent = Percent(50.0), }, 3 => Wavetable { /// Wavetable number (1–16): factory tables in Wavetable, user tables in WaveUser. WAVE => table: WavetableIndex = WavetableIndex(1), /// Position within the wavetable (0–100%). TIMBRE => position: Percent = Percent(0.0), /// Chorus amount (0–100%). SHAPE => chorus: Percent = Percent(0.0), }, 4 => Harmo { /// Harmonic content (0–100%). WAVE => content: Percent = Percent(0.0), /// Base waveform, from sine to triangle (0–100%). TIMBRE => sculpting: Percent = Percent(0.0), /// Chorus amount (0–100%). SHAPE => chorus: Percent = Percent(25.0), }, 5 => KarplusStr { /// Bow excitation (0–100%): zero gives a plucked decay; higher values sustain the resonator. WAVE => bow: Percent = Percent(30.0), /// Position where the string is struck (0–100%). TIMBRE => position: Percent = Percent(0.0), /// Resonator decay and damping (0–100%). SHAPE => decay: Percent = Percent(60.0), }, #[serde(rename = "V.Analog")] 6 => VirtualAnalog { /// Detuning between the two waves (0–100%). WAVE => detune: Percent = Percent(49.8), /// Shape from narrow pulse through full square to hard-sync formants (0–100%). TIMBRE => shape: Percent = Percent(49.8), /// Wave morph from triangle to saw with an increasingly wide notch (0–100%). SHAPE => wave: Percent = Percent(0.0), }, 7 => Waveshaper { /// Waveshaper waveform (0–100%). WAVE => wave: Percent = Percent(49.8), /// Wavefolder amount (0–100%). TIMBRE => amount: Percent = Percent(49.8), /// Waveform asymmetry (0–100%). SHAPE => asym: Percent = Percent(0.0), }, #[serde(rename = "Two Op.FM")] 8 => TwoOperatorFm { /// Frequency ratio between the two FM oscillators (0–100% knob position). WAVE => ratio: Percent = Percent(30.6), /// FM modulation depth (0–100%). TIMBRE => amount: Percent = Percent(48.6), /// Feedback on the second FM oscillator (0–100%). SHAPE => feedback: Percent = Percent(0.0), }, 9 => Formant { /// Frequency ratio between the two formants (0–100% knob position). WAVE => interval: Percent = Percent(25.9), /// Formant frequency (0–100% knob position). TIMBRE => formant: Percent = Percent(85.5), /// Formant width and shape (0–100%). SHAPE => shape: Percent = Percent(81.2), }, 10 => Chords { // `type` is already the oscillator model discriminator. /// Chord voicing. WAVE => chord_type: ChordType = ChordType::default(), /// Chord inversion and transposition (0–100% knob position). TIMBRE => inv_transp: Percent = Percent(0.0), /// Chord waveform, from strings to wavetable sounds (0–100%). SHAPE => waveform: Percent = Percent(0.0), }, 11 => Speech { /// Speech sound bank and vowel selection (0–100% knob position). WAVE => category: Percent = Percent(100.0), /// Formant shift (0–100% knob position). TIMBRE => timbre: Percent = Percent(47.8), /// Word selection within the speech bank (0–100% knob position). SHAPE => word: Percent = Percent(35.7), }, 12 => Modal { /// Inharmonicity of the resonator, changing the apparent material (0–100%). WAVE => inharm: Percent = Percent(25.1), /// Brightness and density of the excitation (0–100%). TIMBRE => excitation: Percent = Percent(35.3), /// Resonator decay and damping (0–100%). SHAPE => decay: Percent = Percent(78.4), }, 13 => Noise { /// Noise source, from particles to white to metallic noise (0–100%). WAVE => source: Percent = Percent(33.3), /// Sample-rate reduction and noise pitch (0–100% knob position). TIMBRE => sample_rate: Percent = Percent(100.0), /// Mix of noise with sine, triangle, and square tones (0–100%). SHAPE => shape: Percent = Percent(50.0), }, 14 => Vocoder { /// Carrier waveform, from saw through pulse to noise (0–100%). WAVE => wave: Percent = Percent(0.0), /// Analysis frequency range (0–100% knob position). TIMBRE => freq_range: Percent = Percent(50.0), /// Filter-band width (0–100%). SHAPE => bandwidth: Percent = Percent(50.0), }, 15 => Bass { /// Cosine saturation (0–100%). WAVE => saturate: Percent = Percent(0.0), /// Wavefolding amount (0–100%). TIMBRE => fold: Percent = Percent(0.0), /// Noise modulation amount (0–100%). SHAPE => noise: Percent = Percent(0.0), }, #[serde(rename = "Sawx")] 16 => SawX { /// Saw modulation amount (0–100%). WAVE => saw_mod: Percent = Percent(0.0), /// Chorus amount (0–100%). TIMBRE => shape: Percent = Percent(0.0), /// Noise modulation amount (0–100%). SHAPE => noise: Percent = Percent(0.0), }, 17 => Harm { /// Partial spread (0–100%). WAVE => spread: Percent = Percent(0.0), /// Partial rectification (0–100%). TIMBRE => rectification: Percent = Percent(0.0), /// Noise modulation amount (0–100%); also adds clipping. SHAPE => noise: Percent = Percent(0.0), }, 18 => WaveUser { /// User wavetable number (1–16). WAVE => table: WavetableIndex = WavetableIndex(1), /// Position within the wavetable (0–100%). TIMBRE => position: Percent = Percent(0.0), /// Bit-depth reduction (0–100%). SHAPE => bitdepth: Percent = Percent(0.0), }, 19 => Sample { /// Sample playback start: 0% is the beginning, 100% is the end. WAVE => start: Percent = Percent(0.0), /// Length knob position (0–100%): the left half plays backward, the right half plays forward. TIMBRE => length: Percent = Percent(100.0), /// Loop crossfade position (0–100%); higher values shorten the loop toward its end. SHAPE => r#loop: Percent = Percent(0.0), }, #[serde(rename = "Scan Grains")] 20 => ScanGrains { /// Speed at which grains scan through the sample (0–100% knob position). WAVE => scan: Percent = Percent(75.0), /// Grain generation rate (0–100% knob position). TIMBRE => density: Percent = Percent(50.0), /// Random variation in grain density, size, and pitch (0–100%). SHAPE => chaos: Percent = Percent(0.0), }, #[serde(rename = "Cloud Grains")] 21 => CloudGrains { /// Grain start position within the sample (0–100%). WAVE => start: Percent = Percent(0.0), /// Grain generation rate (0–100% knob position). TIMBRE => density: Percent = Percent(50.0), /// Random variation in grain density, size, and pitch (0–100%). SHAPE => chaos: Percent = Percent(0.0), }, #[serde(rename = "Hit Grains")] 22 => HitGrains { /// Grain start position within the sample (0–100%). WAVE => start: Percent = Percent(0.0), /// Grain generation rate (0–100% knob position). TIMBRE => density: Percent = Percent(50.0), /// Grain size and envelope shape (0–100%). SHAPE => size: Percent = Percent(0.0), },}
#[author]#[serde(transparent)]#[garde(transparent)]pub struct SampleIndex(#[garde(range(min = 1, max = 128))] pub u8);
impl Default for SampleIndex { fn default() -> Self { Self(1) }}
impl RegisterCodec for SampleIndex { fn encode(&self) -> StoredValue { StoredValue((u16::from(self.0) - 1) * 258) }
fn decode(raw: StoredValue) -> Result<Self, DecodeError> { Ok(Self(1 + (unsigned::<Self>(raw)? / 256) as u8)) }}
#[author]#[serde(transparent)]#[garde(transparent)]pub struct WavetableIndex(#[garde(range(min = 1, max = 16))] pub u8);
impl Default for WavetableIndex { fn default() -> Self { Self(1) }}
impl RegisterCodec for WavetableIndex { fn encode(&self) -> StoredValue { StoredValue(u16::from(self.0) * 2048 - 1024) }
fn decode(raw: StoredValue) -> Result<Self, DecodeError> { Ok(Self(1 + (unsigned::<Self>(raw)? / 2048) as u8)) }}
#[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!(SampleIndex: 1 => 128, WavetableIndex: 1 => 16); assert_eq!(SampleIndex(128).encode(), StoredValue(32766)); assert_eq!(WavetableIndex(1).encode(), StoredValue(1024)); assert_eq!(WavetableIndex(16).encode(), StoredValue(31744)); }}