Something went wrong. Try again.
YAML-based live configuration of MicroFreak datyoma.tngl.io/mfctl
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206//! Compact YAML rendering preserves contextual defaults and required discriminators.use super::{Metadata, Spec, cycling_envelope, glide, lfo, oscillator};use serde_yaml::{Mapping, Value};use std::error::Error;
type Result<T> = std::result::Result<T, Box<dyn Error>>;
fn compact(current: &mut Mapping, defaults: &Mapping) { current.retain(|key, value| { let Some(default) = defaults.get(key) else { return true; }; match (value, default) { (Value::Mapping(current), Value::Mapping(default)) => { compact(current, default); !current.is_empty() } (value, default) => value != default, } });}
// Required union tags and free LFO rates must survive contextual compaction:// omitting a free rate would select the synchronized default instead.fn retain_field(current: &mut Mapping, section: &str, tag: &str, value: Value, nondefault: bool) { if !nondefault && !current.contains_key(section) { return; } let fields = current .entry(Value::String(section.into())) .or_insert_with(|| Value::Mapping(Mapping::new())) .as_mapping_mut() .unwrap(); // Compaction removes default-valued tags; restore them before their controls. *fields = std::iter::once((Value::String(tag.into()), value)) .chain(std::mem::take(fields)) .collect();}
// Six significant digits preserve every float codec's register precision and// validation bounds. Round only the output, after comparing contextual defaults.fn round_floats(value: &mut Value) { match value { Value::Number(number) if number.is_f64() => { let rounded = format!("{:.5e}", number.as_f64().unwrap()) .parse::<f64>() .unwrap(); *number = rounded.into(); } Value::Sequence(values) => values.iter_mut().for_each(round_floats), Value::Mapping(fields) => fields.values_mut().for_each(round_floats), Value::Tagged(tagged) => round_floats(&mut tagged.value), _ => {} }}
pub(crate) fn render_patch(spec: &Spec, defaults: &Spec, metadata: &Metadata) -> Result<String> { let mut output = serde_yaml::to_value(spec)?; let baseline = serde_yaml::to_value(defaults)?; let oscillator_tag = output["oscillator"]["type"].clone(); let cycling_tag = output["cyclingEnvelope"]["mode"].clone(); let free_rate = matches!(spec.lfo.rate, lfo::Rate::Free(_)).then(|| output["lfo"]["rateHz"].clone()); let glide_choice = match spec.keyboard.glide { glide::Glide::Time(_) => None, glide::Glide::Sync(_) => Some("glideDivision"), glide::Glide::Rate(_) => Some("glideMsPerOctave"), } .map(|field| (field, output["keyboard"][field].clone())); let current = output.as_mapping_mut().unwrap(); compact(current, baseline.as_mapping().unwrap()); retain_field( current, "oscillator", "type", oscillator_tag, !matches!(spec.oscillator.model, oscillator::Model::BasicWaves(_)) || spec.oscillator.sample.is_some(), ); retain_field( current, "cyclingEnvelope", "mode", cycling_tag, !matches!( spec.cycling_envelope, cycling_envelope::CyclingEnvelope::Run(_) ), ); if let Some(rate) = free_rate { retain_field(current, "lfo", "rateHz", rate, true); } if let Some((field, value)) = glide_choice { retain_field(current, "keyboard", field, value, true); } if spec.oscillator.sample.is_some() && current .get("oscillator") .and_then(|v| v.get("sample")) .is_none() { current .entry(Value::String("oscillator".into())) .or_insert_with(|| Value::Mapping(Mapping::new())) .as_mapping_mut() .unwrap() .insert( Value::String("sample".into()), Value::Mapping(Mapping::new()), ); } let mut document = Mapping::new(); let defaults = Metadata::default(); let mut fields = Mapping::new(); if metadata.name != defaults.name { fields.insert( Value::String("name".into()), Value::String(metadata.name.clone()), ); } if metadata.category != defaults.category { fields.insert( Value::String("category".into()), serde_yaml::to_value(metadata.category)?, ); } if !metadata.tags.is_empty() { fields.insert( Value::String("tags".into()), serde_yaml::to_value(&metadata.tags)?, ); } if !fields.is_empty() { document.insert(Value::String("metadata".into()), Value::Mapping(fields)); } round_floats(&mut output); document.insert(Value::String("spec".into()), output); Ok(serde_yaml::to_string(&document)?)}
#[cfg(test)]mod tests { use super::*; use crate::{ parameter::{RegisterCodec, StoredValue}, patch::{ arp::ArpRateBpm, filter::FilterCutoffHz, glide::DurationMs, global::{MasterVolume, UnisonSpread}, lfo::LfoHz, value::{Bipolar, Percent}, }, }; use garde::Validate; use serde::{Serialize, de::DeserializeOwned};
fn check_roundtrips<T>() where T: RegisterCodec + Serialize + DeserializeOwned + Validate<Context = ()>, { for raw in 0..=u16::MAX { let Ok(decoded) = T::decode(StoredValue(raw)) else { continue; }; let mut value = serde_yaml::to_value(&decoded).unwrap(); round_floats(&mut value); let yaml = serde_yaml::to_string(&value).unwrap(); let rounded: T = serde_yaml::from_str(&yaml).unwrap(); assert!(rounded.validate().is_ok(), "raw {raw}: {yaml}"); // Quantized controls already canonicalize some registers on decode. assert_eq!(rounded.encode(), decoded.encode(), "raw {raw}: {yaml}"); } }
#[test] fn six_significant_digits_preserve_all_float_codecs() { check_roundtrips::<Percent>(); check_roundtrips::<Bipolar>(); check_roundtrips::<FilterCutoffHz>(); check_roundtrips::<LfoHz>(); check_roundtrips::<DurationMs>(); check_roundtrips::<ArpRateBpm>(); check_roundtrips::<MasterVolume>(); check_roundtrips::<UnisonSpread>(); }
#[test] fn rendering_rounds_nested_floats_without_changing_integers_or_metadata() { let patch = super::super::parse( "metadata: {name: '1.23456789'}\nspec:\n filter: {resonance: 68.55677968688009}\n envelope: {attackMs: 1234}\n modulations: {pitch: {sources: {lfo: -37.152099609375}}}\n", ) .unwrap(); let yaml = render_patch(&patch.spec, &Spec::default(), &patch.metadata).unwrap(); assert!(yaml.contains("resonance: 68.5568\n")); assert!(yaml.contains("lfo: -37.1521\n")); let restored = super::super::parse(&yaml).unwrap(); assert_eq!(restored.spec.registers(), patch.spec.registers()); assert_eq!(restored.metadata, patch.metadata); assert_eq!( restored.spec.envelope.attack_ms, patch.spec.envelope.attack_ms ); }}