use 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, _: &()| { if self.model.uses_samples() && sample.is_none() { Err(garde::Error::new("sample is required for sample-based oscillators")) } else { Ok(()) } }))] pub sample: Option, } impl Oscillator { pub(crate) fn from_model_register(raw: StoredValue) -> Result { 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> { 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 { match parameter::decode_bin::(raw, [$($index),+].len() as u16)? { $($index => Ok(Self::$model(model::$model::default())),)+ _ => Err(DecodeError::new::(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> { 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 { Ok(Self(1 + (unsigned::(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 { Ok(Self(1 + (unsigned::(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)); } }