use crate::parameter::{self as param, DecodeError, RegisterCodec, StoredValue, bin_enum}; use mfctl_derive::author; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::{ oscillator::{self, Oscillator}, value::Bipolar, }; /// Stable modulation destinations. Oscillator axes keep their panel identities /// even though each model has different author parameter names. Discriminants /// are complete firmware parameter IDs, not enum-bin indices. #[derive( Clone, Copy, Debug, PartialEq, Deserialize, Serialize, JsonSchema, strum_macros::FromRepr, )] #[repr(u16)] pub enum Target { #[serde(rename = "oscillator.type")] OscillatorType = param::oscillator::TYPE.id, #[serde(rename = "oscillator.wave")] OscillatorWave = param::oscillator::WAVE.id, #[serde(rename = "oscillator.timbre")] OscillatorTimbre = param::oscillator::TIMBRE.id, #[serde(rename = "oscillator.shape")] OscillatorShape = param::oscillator::SHAPE.id, #[serde(rename = "sampleIndex")] SampleIndex = param::SAMPLE_INDEX.id, #[serde(rename = "filter.cutoffHz")] FilterCutoffHz = param::filter::CUTOFF_HZ.id, #[serde(rename = "filter.resonance")] FilterResonance = param::filter::RESONANCE.id, #[serde(rename = "cyclingEnvelope.riseMs")] CyclingEnvelopeRiseMs = param::cycling_envelope::RISE_MS.id, #[serde(rename = "cyclingEnvelope.fallMs")] CyclingEnvelopeFallMs = param::cycling_envelope::FALL_MS.id, #[serde(rename = "cyclingEnvelope.sustain")] CyclingEnvelopeSustain = param::cycling_envelope::HOLD_SUSTAIN.id, #[serde(rename = "cyclingEnvelope.amount")] CyclingEnvelopeAmount = param::cycling_envelope::AMOUNT.id, #[serde(rename = "keyboard.glide")] KeyboardGlide = param::keyboard::GLIDE.id, #[serde(rename = "arp.range")] ArpRange = param::arp::RANGE.id, #[serde(rename = "arp.rateBpm")] ArpRateBpm = param::arp::RATE_BPM.id, #[serde(rename = "lfo.shape")] LfoShape = param::lfo::SHAPE.id, #[serde(rename = "lfo.rateDivision")] LfoRateDivision = param::lfo::RATE_DIVISION.id, #[serde(rename = "lfo.rateHz")] LfoRateHz = param::lfo::RATE_HZ.id, #[serde(rename = "envelope.attackMs")] EnvelopeAttackMs = param::envelope::ATTACK_MS.id, #[serde(rename = "envelope.decayReleaseMs")] EnvelopeDecayReleaseMs = param::envelope::DECAY_RELEASE_MS.id, #[serde(rename = "envelope.sustain")] EnvelopeSustain = param::envelope::SUSTAIN.id, #[serde(rename = "envelope.legato")] EnvelopeLegato = param::envelope::LEGATO.id, #[serde(rename = "unisonSpread")] GlobalUnisonSpread = param::global::UNISON_SPREAD.id, #[serde(rename = "cyclingEnvelope->pitch")] CyclingEnvelopePitch = param::modulations::pitch::CYCLING_ENVELOPE.id, #[serde(rename = "envelope->pitch")] EnvelopePitch = param::modulations::pitch::ENVELOPE.id, #[serde(rename = "lfo->pitch")] LfoPitch = param::modulations::pitch::LFO.id, #[serde(rename = "pressure->pitch")] PressurePitch = param::modulations::pitch::PRESSURE.id, #[serde(rename = "key->pitch")] KeyPitch = param::modulations::pitch::KEY.id, #[serde(rename = "cyclingEnvelope->wave")] CyclingEnvelopeWave = param::modulations::wave::CYCLING_ENVELOPE.id, #[serde(rename = "envelope->wave")] EnvelopeWave = param::modulations::wave::ENVELOPE.id, #[serde(rename = "lfo->wave")] LfoWave = param::modulations::wave::LFO.id, #[serde(rename = "pressure->wave")] PressureWave = param::modulations::wave::PRESSURE.id, #[serde(rename = "key->wave")] KeyWave = param::modulations::wave::KEY.id, #[serde(rename = "cyclingEnvelope->timbre")] CyclingEnvelopeTimbre = param::modulations::timbre::CYCLING_ENVELOPE.id, #[serde(rename = "envelope->timbre")] EnvelopeTimbre = param::modulations::timbre::ENVELOPE.id, #[serde(rename = "lfo->timbre")] LfoTimbre = param::modulations::timbre::LFO.id, #[serde(rename = "pressure->timbre")] PressureTimbre = param::modulations::timbre::PRESSURE.id, #[serde(rename = "key->timbre")] KeyTimbre = param::modulations::timbre::KEY.id, #[serde(rename = "cyclingEnvelope->cutoff")] CyclingEnvelopeCutoff = param::modulations::cutoff::CYCLING_ENVELOPE.id, #[serde(rename = "envelope->cutoff")] EnvelopeCutoff = param::modulations::cutoff::ENVELOPE.id, #[serde(rename = "lfo->cutoff")] LfoCutoff = param::modulations::cutoff::LFO.id, #[serde(rename = "pressure->cutoff")] PressureCutoff = param::modulations::cutoff::PRESSURE.id, #[serde(rename = "key->cutoff")] KeyCutoff = param::modulations::cutoff::KEY.id, #[serde(rename = "cyclingEnvelope->assign1")] CyclingEnvelopeAssign1 = param::modulations::assign1::CYCLING_ENVELOPE.id, #[serde(rename = "envelope->assign1")] EnvelopeAssign1 = param::modulations::assign1::ENVELOPE.id, #[serde(rename = "lfo->assign1")] LfoAssign1 = param::modulations::assign1::LFO.id, #[serde(rename = "pressure->assign1")] PressureAssign1 = param::modulations::assign1::PRESSURE.id, #[serde(rename = "key->assign1")] KeyAssign1 = param::modulations::assign1::KEY.id, #[serde(rename = "cyclingEnvelope->assign2")] CyclingEnvelopeAssign2 = param::modulations::assign2::CYCLING_ENVELOPE.id, #[serde(rename = "envelope->assign2")] EnvelopeAssign2 = param::modulations::assign2::ENVELOPE.id, #[serde(rename = "lfo->assign2")] LfoAssign2 = param::modulations::assign2::LFO.id, #[serde(rename = "pressure->assign2")] PressureAssign2 = param::modulations::assign2::PRESSURE.id, #[serde(rename = "key->assign2")] KeyAssign2 = param::modulations::assign2::KEY.id, #[serde(rename = "cyclingEnvelope->assign3")] CyclingEnvelopeAssign3 = param::modulations::assign3::CYCLING_ENVELOPE.id, #[serde(rename = "envelope->assign3")] EnvelopeAssign3 = param::modulations::assign3::ENVELOPE.id, #[serde(rename = "lfo->assign3")] LfoAssign3 = param::modulations::assign3::LFO.id, #[serde(rename = "pressure->assign3")] PressureAssign3 = param::modulations::assign3::PRESSURE.id, #[serde(rename = "key->assign3")] KeyAssign3 = param::modulations::assign3::KEY.id, } impl RegisterCodec for Target { fn encode(&self) -> StoredValue { StoredValue(*self as u16) } fn decode(raw: StoredValue) -> Result { Self::from_repr(raw.0).ok_or_else(|| DecodeError::new::(raw)) } } #[author] #[derive(Default)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct Sources { /// Cycling-envelope modulation amount (−100% to 100%). pub cycling_envelope: Bipolar, /// Main-envelope modulation amount (−100% to 100%). pub envelope: Bipolar, /// LFO modulation amount (−100% to 100%). pub lfo: Bipolar, /// Pressure modulation amount (−100% to 100%). pub pressure: Bipolar, /// Key/Arp modulation amount (−100% to 100%). pub key: Bipolar, } #[author] #[derive(Default)] #[serde(default, deny_unknown_fields)] pub struct Destination { /// Modulation amounts routed to this destination. pub sources: Sources, } #[author] #[serde(default, deny_unknown_fields)] pub struct Assignment1 { #[garde(skip)] /// Destination controlled by this assignment slot. pub target: Target, #[garde(dive)] /// Modulation amounts routed to this destination. pub sources: Sources, } impl Default for Assignment1 { fn default() -> Self { Self { target: Target::OscillatorShape, sources: Sources::default(), } } } #[author] #[serde(default, deny_unknown_fields)] pub struct Assignment2 { #[garde(skip)] /// Destination controlled by this assignment slot. pub target: Target, #[garde(dive)] /// Modulation amounts routed to this destination. pub sources: Sources, } impl Default for Assignment2 { fn default() -> Self { Self { target: Target::CyclingEnvelopeAmount, sources: Sources::default(), } } } #[author] #[serde(default, deny_unknown_fields)] pub struct Assignment3 { #[garde(skip)] /// Destination controlled by this assignment slot. pub target: Target, #[garde(dive)] /// Modulation amounts routed to this destination. pub sources: Sources, } impl Default for Assignment3 { fn default() -> Self { Self { target: Target::EnvelopeCutoff, sources: Sources::default(), } } } bin_enum! { pub enum KeySource { #[default] Linear, Random, } } #[author] #[derive(Default)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct Modulations { /// Linear follows note pitch; Random produces a new random value for each played note. Applies to the Key/Arp matrix source. pub key_source: KeySource, /// Modulation amounts routed to pitch. pub pitch: Destination, /// Modulation amounts routed to the oscillator Wave destination. pub wave: Destination, /// Modulation amounts routed to the oscillator Timbre destination. pub timbre: Destination, /// Modulation amounts routed to filter cutoff. pub cutoff: Destination, /// First assignable modulation slot. pub assign1: Assignment1, /// Second assignable modulation slot. pub assign2: Assignment2, /// Third assignable modulation slot. pub assign3: Assignment3, } impl Modulations { pub fn validate_for(&self, oscillator: &Oscillator) -> garde::Result { if matches!(oscillator.model, oscillator::Model::Vocoder(_)) && [ (&self.assign1.target, &self.assign1.sources), (&self.assign2.target, &self.assign2.sources), (&self.assign3.target, &self.assign3.sources), ] .iter() .any(|(target, sources)| { **target == Target::OscillatorType && **sources != Sources::default() }) { return Err(garde::Error::new( "Vocoder does not permit oscillator.type modulation", )); } Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::parameter::{RegisterCodec, StoredValue}; #[test] fn assignment_targets_are_sparse_parameter_ids_not_bins() { assert_eq!(Target::FilterCutoffHz.encode(), StoredValue(0x0101)); assert_eq!( Target::decode(StoredValue(0x0a02)).unwrap(), Target::LfoPitch ); assert!(Target::decode(StoredValue(0x0902)).is_err()); assert!(Target::decode(StoredValue(0xffff)).is_err()); } }