Something went wrong. Try again.
YAML-based live configuration of MicroFreak datyoma.tngl.io/mfctl
microfreak
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484use super::*;use crate::parameter::{self as param, RegisterCodec, RegisterReader, StoredValue};use std::io;
#[test]fn empty_spec_resolves_the_complete_default_patch() { let patch = parse("spec: {}").unwrap(); assert_eq!(patch.spec, Spec::default()); assert_eq!(patch.metadata, Metadata::default()); assert_eq!(patch.api_version, ApiVersion::V5); assert_eq!(patch.kind, Kind::Patch); assert_eq!( yaml::render_patch(&patch.spec, &Spec::default(), &patch.metadata).unwrap(), "spec: {}\n" );}
#[test]fn author_boundary_rejects_missing_null_unknown_and_wrongly_typed_fields() { for source in [ "{}", "spec: null", "spec: []", "spec: {filter: null}", "spec: {envelope: {sustain: null}}", "spec: {typo: 1}", "spec: {filter: {cutoff: 100}}", "spec: {chordOffset1: 7}", "spec: {chordOffsets: null}", "spec: {chordOffsets: [7, null]}", "spec: {chordOffsets: 7}", "spec: {keyboard: {hold: true}}", "spec: {oscillator: {type: BasicWaves, detune: 20}}", "apiVersion: microfreak/v4\nspec: {}", "kind: Preset\nspec: {}", "metadata: {extra: x}\nspec: {}", ] { assert!( matches!(parse(source), Err(ParseError::Yaml(_))), "{source}" ); }}
#[test]fn keyboard_hold_is_not_an_author_field_or_live_parameter() { let schema = serde_json::to_value(schemars::schema_for!(Patch)).unwrap(); assert!( schema["$defs"]["Keyboard"]["properties"] .get("hold") .is_none() ); assert!( param::DESCRIPTORS .iter() .all(|parameter| parameter.id != 0x0303) ); let mut registers = Spec::default().registers(); assert!(registers.keys().all(|parameter| parameter.id != 0x0303)); let yaml = capture(&mut registers, &Metadata::default()).unwrap(); assert_eq!(yaml, "spec: {}\n");}
#[test]fn numeric_and_metadata_validation_runs_recursively() { for source in [ "spec: {filter: {resonance: 101}}", "spec: {envelope: {attackMs: 10001}}", "spec: {keyboard: {octave: -4}}", "spec: {chordOffsets: [0, 4, 7, 12]}", "spec: {chordOffsets: [127]}", "spec: {modulations: {pitch: {sources: {lfo: -101}}}}", "spec: {oscillator: {type: Wavetable, table: 17}}", "spec: {oscillator: {type: Sample, sample: {index: 0}}}", "metadata: {name: ''}\nspec: {}", "metadata: {name: 'café'}\nspec: {}", ] { assert!( matches!(parse(source), Err(ParseError::Validation(_))), "{source}" ); }}
#[test]fn chord_offsets_preserve_empty_zero_and_saved_inactive_chords() { assert_eq!(Spec::default().global.chord_offsets.0, [0, 0, 0]); let parameters = [ param::global::CHORD_OFFSET1, param::global::CHORD_OFFSET2, param::global::CHORD_OFFSET3, ]; for mode in ["Off", "Chord", "Unison"] { for (offsets, expected, raw) in [ ("[]", vec![], [32640, 32640, 32640]), ("[7]", vec![7], [17280, 32640, 32640]), ("[4, 7]", vec![4, 7], [16896, 17280, 32640]), ("[0, 0, 0]", vec![0, 0, 0], [16384; 3]), ("[-128, -12, 126]", vec![-128, -12, 126], [0, 14848, 32512]), ] { let patch = parse(&format!( "spec: {{chordMode: {mode}, chordOffsets: {offsets}}}" )) .unwrap(); assert_eq!(patch.spec.global.chord_offsets.0, expected); let mut registers = patch.spec.registers(); for (parameter, value) in parameters.into_iter().zip(raw) { assert_eq!(registers[¶meter], StoredValue(value)); } let (snapshot, defaults) = mapping::snapshot(&mut registers).unwrap(); assert_eq!(snapshot, patch.spec); let rendered = yaml::render_patch(&snapshot, &defaults, &patch.metadata).unwrap(); assert_eq!(parse(&rendered).unwrap().spec, patch.spec); assert_eq!(rendered.contains("chordOffsets:"), expected != [0, 0, 0]); } }}
#[test]fn chord_capture_stops_at_the_first_sentinel_and_canonicalizes_padding() { let mut registers = Spec::default().registers(); registers.insert(param::global::CHORD_OFFSET2, StoredValue(32767)); registers.insert(param::global::CHORD_OFFSET3, StoredValue(17280)); let (snapshot, _) = mapping::snapshot(&mut registers).unwrap(); assert_eq!(snapshot.global.chord_offsets.0, [0]); let canonical = snapshot.registers(); assert_eq!(canonical[¶m::global::CHORD_OFFSET2], StoredValue(32640)); assert_eq!(canonical[¶m::global::CHORD_OFFSET3], StoredValue(32640)); registers.insert(param::global::CHORD_OFFSET1, StoredValue(32768)); assert!( mapping::snapshot(&mut registers) .unwrap_err() .to_string() .contains("0x0708") );}
#[test]fn yaml_merges_resolve_before_typed_deserialization_and_explicit_values_win() { let source = "spec:\n filter: &controls {resonance: 35}\n envelope:\n <<: *controls\n resonance: 40\n"; // A merged unknown field must not evade the typed boundary. assert!(parse(source).is_err()); let source = "spec:\n arp: &controls {spice: 35, dice: 20}\n filter: {resonance: 40}\n <<: {arp: {spice: 90}}\n"; let patch = parse(source).unwrap(); assert_eq!(patch.spec.arp.spice.0, 35.0); assert_eq!(patch.spec.arp.dice.0, 20.0); let patch = parse("spec: {filter: {<<: &base {resonance: 30}, resonance: 45}}").unwrap(); assert_eq!(patch.spec.filter.resonance.0, 45.0);}
#[test]fn lfo_rate_is_an_exclusive_choice_and_omission_is_synced() { assert!(matches!( parse("spec: {lfo: {}}").unwrap().spec.lfo.rate, lfo::Rate::Sync(_) )); assert!(matches!( parse("spec: {lfo: {rateHz: 2}}").unwrap().spec.lfo.rate, lfo::Rate::Free(_) )); for fields in [ "rateHz: 2, rateDivision: 1/16", "rateHz: null", "rateDivision: invalid", "rate: 2", ] { assert!( parse(&format!("spec: {{lfo: {{{fields}}}}}")).is_err(), "{fields}" ); }}
#[test]fn glide_is_an_exclusive_choice_and_validates_physical_units() { assert_eq!( parse("spec: {keyboard: {}}").unwrap().spec.keyboard.glide, glide::Glide::default() ); for fields in [ "glideTimeMs: 1, glideDivision: off", "glideTimeMs: 1, glideMsPerOctave: 1", "glideDivision: off, glideMsPerOctave: 0", "glideTimeMs: null", "glideDivision: invalid", "glide: 25", "glideMode: Sync", ] { assert!( parse(&format!("spec: {{keyboard: {{{fields}}}}}")).is_err(), "{fields}" ); } for fields in [ "glideTimeMs: -1", "glideTimeMs: 10001", "glideMsPerOctave: -1", "glideMsPerOctave: 10001", ] { assert!( matches!( parse(&format!("spec: {{keyboard: {{{fields}}}}}")), Err(ParseError::Validation(_)) ), "{fields}" ); }}
#[test]fn glide_snapshots_decode_the_selected_mode_and_preserve_off_modes() { for (mode, field, value) in [ (0, "glideTimeMs", "0"), (0, "glideTimeMs", "1250"), (1, "glideDivision", "off"), (1, "glideDivision", "1/32T"), (1, "glideDivision", "1/1"), (2, "glideMsPerOctave", "0"), (2, "glideMsPerOctave", "500"), ] { let patch = parse(&format!( "spec: {{keyboard: {{{field}: {value}, octave: 1}}}}" )) .unwrap(); let mut registers = patch.spec.registers(); assert_eq!( registers[¶m::keyboard::GLIDE_MODE], param::encode_bin(mode, 2) ); let (snapshot, defaults) = mapping::snapshot(&mut registers).unwrap(); assert_eq!(snapshot.registers(), registers); let rendered = yaml::render_patch(&snapshot, &defaults, &Metadata::default()).unwrap(); assert_eq!( parse(&rendered).unwrap().spec.registers(), snapshot.registers(), "{rendered}" ); if mode != 0 { assert!(rendered.contains(field), "{rendered}"); } }}
#[test]fn sample_models_require_a_sample_object_and_other_models_can_carry_sample_identity() { for model in ["Sample", "Scan Grains", "Cloud Grains", "Hit Grains"] { assert!(parse(&format!("spec: {{oscillator: {{type: '{model}'}}}}")).is_err()); assert!( parse(&format!( "spec: {{oscillator: {{type: '{model}', sample: null}}}}" )) .is_err() ); let patch = parse(&format!( "spec: {{oscillator: {{type: '{model}', sample: {{}}}}}}" )) .unwrap(); assert_eq!(patch.spec.oscillator.sample.unwrap().index.0, 1); } let patch = parse("spec: {oscillator: {type: BasicWaves, sample: {hash: 65535}}}").unwrap(); assert_eq!( patch.spec.registers()[¶m::global::SAMPLE_HASH], StoredValue(65535) );}
#[test]fn cycling_envelope_modes_only_accept_their_own_controls() { for (mode, field) in [("Env", "sustain"), ("Run", "holdMs"), ("Loop", "holdMs")] { let patch = parse(&format!( "spec: {{cyclingEnvelope: {{mode: {mode}, {field}: 50}}}}" )) .unwrap(); assert!( patch .spec .registers() .contains_key(¶m::cycling_envelope::HOLD_SUSTAIN) ); let wrong = if field == "sustain" { "holdMs" } else { "sustain" }; assert!( parse(&format!( "spec: {{cyclingEnvelope: {{mode: {mode}, {wrong}: 50}}}}" )) .is_err() ); }}
#[test]fn vocoder_forbids_active_oscillator_type_assignment_but_not_unused_slots() { for slot in ["assign1", "assign2", "assign3"] { let source = format!( "spec: {{oscillator: {{type: Vocoder}}, modulations: {{{slot}: {{target: oscillator.type, sources: {{lfo: 10}}}}}}}}" ); assert!(matches!(parse(&source), Err(ParseError::Validation(_)))); assert!(parse(&source.replace("lfo: 10", "lfo: 0")).is_ok()); assert!(parse(&source.replace("Vocoder", "BasicWaves")).is_ok()); }}
#[test]fn compact_yaml_preserves_contextual_tags_and_required_default_objects() { for controls in [ "{}", "{oscillator: {type: SuperWave}}", "{oscillator: {type: BasicWaves, morph: 40}}", "{oscillator: {type: Sample, sample: {}}}", "{oscillator: {type: BasicWaves, sample: {}}}", "{cyclingEnvelope: {mode: Env}}", "{cyclingEnvelope: {mode: Loop, holdMs: 100}}", "{cyclingEnvelope: {mode: Run, amount: 20}}", "{lfo: {rateHz: 0.16700050545902942}}", "{lfo: {rateDivision: 1/8}}", "{keyboard: {glideTimeMs: 250}}", "{keyboard: {glideDivision: off}}", "{keyboard: {glideDivision: 1/8T}}", "{keyboard: {glideMsPerOctave: 0}}", "{keyboard: {glideMsPerOctave: 500}}", "{keyboard: {glideDivision: off, octave: 1}}", ] { let patch = parse(&format!( "metadata: {{name: 'A spaced name', category: Pad}}\nspec: {controls}" )) .unwrap(); let registers = patch.spec.registers(); let defaults = mapping::context_defaults( registers[¶m::oscillator::TYPE], registers[¶m::cycling_envelope::MODE], registers[¶m::lfo::SYNC], registers[¶m::keyboard::GLIDE_MODE], ) .unwrap(); let rendered = yaml::render_patch(&patch.spec, &defaults, &patch.metadata).unwrap(); let restored = parse(&rendered).unwrap(); assert_eq!(restored.spec.registers(), registers, "{rendered}"); assert_eq!(restored.metadata, patch.metadata); }}
#[test]fn every_oscillator_model_has_a_stable_contextual_snapshot() { for model in 1..=22 { let spec = mapping::context_defaults( param::encode_bin(model, 22), StoredValue(16384), true.encode(), StoredValue(0), ) .unwrap(); let mut registers = spec.registers(); let (snapshot, defaults) = mapping::snapshot(&mut registers).unwrap(); assert_eq!(snapshot, spec, "model {model}"); assert_eq!(defaults, spec, "model {model}"); assert_eq!(snapshot.registers(), registers); } assert!( mapping::context_defaults( StoredValue(0), StoredValue(0), StoredValue(0), StoredValue(0) ) .is_err() );}
#[test]fn snapshot_reads_only_the_active_lfo_rate_and_rejects_invalid_stored_controls() { for rate in ["rateHz: 2", "rateDivision: 1/8"] { let patch = parse(&format!("spec: {{lfo: {{{rate}}}}}")).unwrap(); let mut registers = patch.spec.registers(); let inactive = if rate.starts_with("rateHz") { param::lfo::RATE_DIVISION } else { param::lfo::RATE_HZ }; assert!(!registers.contains_key(&inactive)); let (snapshot, _) = mapping::snapshot(&mut registers).unwrap(); assert_eq!(snapshot.registers(), registers); registers.insert(param::filter::RESONANCE, StoredValue(32768)); let error = mapping::snapshot(&mut registers).unwrap_err(); assert!(error.to_string().contains("0x0102")); }}
#[test]fn assignment_self_references_drop_the_whole_column() { let mut registers = Spec::default().registers(); for parameter in [ param::modulations::ASSIGN1_TARGET, param::modulations::ASSIGN2_TARGET, param::modulations::ASSIGN3_TARGET, ] { registers.insert(parameter, StoredValue(parameter.id)); } assert_eq!( mapping::snapshot(&mut registers).unwrap().0, Spec::default() ); for parameter in [ param::modulations::assign1::LFO, param::modulations::assign2::KEY, param::modulations::assign3::PRESSURE, ] { registers.insert(parameter, value::Bipolar(65.0).encode()); } assert_eq!( mapping::snapshot(&mut registers).unwrap().0, Spec::default() );}
#[test]fn snapshot_rejects_selector_changes_during_capture() { struct ChangingReader { registers: param::Registers, selector: param::ParamDescriptor, seen: bool, } impl RegisterReader for ChangingReader { fn read(&mut self, parameter: param::ParamDescriptor) -> io::Result<StoredValue> { let raw = self.registers[¶meter]; if parameter == self.selector { if self.seen { return Ok(StoredValue(raw.0 + 1)); } self.seen = true; } Ok(raw) } } for selector in [ param::oscillator::TYPE, param::cycling_envelope::MODE, param::lfo::SYNC, ] { let mut reader = ChangingReader { registers: Spec::default().registers(), selector, seen: false, }; assert!( mapping::snapshot(&mut reader) .unwrap_err() .to_string() .contains("changed during capture") ); }}
#[test]fn generated_schema_agrees_with_representative_author_documents() { let schema = serde_json::to_value(schemars::schema_for!(Patch)).unwrap(); let validator = jsonschema::validator_for(&schema).unwrap(); for source in [ "spec: {}", "spec: {oscillator: {type: Sample, sample: {index: 128, hash: 65535}}}", "spec: {lfo: {rateHz: 2}, cyclingEnvelope: {mode: Env, sustain: 25}}", "spec: {filter: {resonance: 101}}", "spec: {oscillator: {type: Sample}}", "spec: {envelope: null}", "spec: {chordOffsets: []}", "spec: {chordOffsets: [4, 7]}", "spec: {chordOffsets: [-128, 0, 126]}", "spec: {chordOffsets: [127]}", "spec: {chordOffsets: [0, 4, 7, 12]}", "spec: {chordOffsets: [null]}", "spec: {chordOffset1: 7}", "spec: {lfo: {rateHz: 2, rateDivision: 1/16}}", ] { let document: serde_json::Value = serde_yaml::from_str(source).unwrap(); assert_eq!( validator.is_valid(&document), parse(source).is_ok(), "{source}" ); }}