Something went wrong. Try again.
YAML-based live configuration of MicroFreak datyoma.tngl.io/mfctl
microfreak
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305use super::*;
#[test]fn port_selection_prefers_exact_names_and_rejects_ambiguity() { let names = ["Other", "Micro-Freak MIDI", "Micro-Freak MIDI extra"].map(String::from); assert_eq!(select_port(&names, Some("Micro-Freak MIDI")).unwrap(), 1); assert_eq!(select_port(&names, Some("EXTRA")).unwrap(), 2); for requested in [None, Some("micro"), Some("missing")] { let error = select_port(&names, requested).unwrap_err().to_string(); assert!(error.contains("--port NAME")); assert!(error.contains("Micro-Freak MIDI extra")); } assert_eq!(select_port(&names[..2], None).unwrap(), 1); assert!( select_port(&[], None) .unwrap_err() .to_string() .contains("(none)") );}
#[test]fn requests_use_firmware_ids_and_midi_safe_numeric_writes() { assert_eq!( query(127, 0x41, 0x070a), [0xf0, 0, 0x20, 0x6b, 7, 1, 127, 3, 0x41, 0, 7, 10, 0xf7,] ); let packet = write_packet(12, param::global::CHORD_OFFSET3, StoredValue(65535)); assert_eq!(&packet[..9], &[0xf0, 0, 0x20, 0x6b, 7, 1, 12, 22, 0x53]); assert_eq!(&packet[9..packet.len() - 1], b"synth param 1802 65535"); assert_eq!(packet.last(), Some(&0xf7)); assert!(packet[1..packet.len() - 1].iter().all(|byte| *byte < 128));}
fn reply(raw: u16) -> [u8; 15] { let [high, low] = raw.to_be_bytes(); [ 0xf0, 0, 0x20, 0x6b, 7, 0x7f, 99, 5, 0x40, ((high >> 7) << 4) | ((low >> 7) << 3), 1, 1, high & 127, low & 127, 0xf7, ]}
#[test]fn parameter_replies_restore_both_msbs_and_ignore_independent_counters() { for raw in [0, 127, 128, 255, 32767, 32768, 65535] { let mut bytes = reply(raw); assert_eq!( parameter_reply(&bytes, param::filter::CUTOFF_HZ), Some(StoredValue(raw)) ); bytes[5] = 1; bytes[6] = 0; assert_eq!( parameter_reply(&bytes, param::filter::CUTOFF_HZ), Some(StoredValue(raw)) ); assert_eq!(parameter_reply(&bytes, param::filter::RESONANCE), None); }}
#[test]fn malformed_parameter_replies_are_not_accepted() { let bytes = reply(0xffff); for length in 0..bytes.len() { assert_eq!( parameter_reply(&bytes[..length], param::filter::CUTOFF_HZ), None ); } for (index, value) in [ (0, 0), (4, 8), (5, 2), (7, 4), (8, 0x41), (12, 128), (14, 0), ] { let mut bad = bytes; bad[index] = value; assert_eq!( parameter_reply(&bad, param::filter::CUTOFF_HZ), None, "byte {index}" ); }}
#[test]fn identity_requires_microfreak_and_firmware_five() { let good = [ 0xf0, 0x7e, 0x7f, 6, 2, 0, 0x20, 0x6b, 6, 0, 6, 1, 5, 0, 0, 1, 0xf7, ]; assert!(identity_reply(&good).unwrap().validate().is_ok()); let mut old = good; old[12] = 4; assert!( identity_reply(&old) .unwrap() .validate() .unwrap_err() .to_string() .contains("firmware 5") ); let mut other = good; other[8] = 7; assert!( identity_reply(&other) .unwrap() .validate() .unwrap_err() .to_string() .contains("not an Arturia") ); for length in 0..15 { assert!(identity_reply(&good[..length]).is_none()); } let mut bad = good; bad[13] = 128; assert!(identity_reply(&bad).is_none());}
#[test]fn wake_response_requires_the_expected_header_and_seven_bit_data() { let mut bytes = [0; 45]; bytes[..5].copy_from_slice(PREFIX); bytes[5] = 1; bytes[7..9].copy_from_slice(&[35, 0x52]); bytes[44] = 0xf7; assert_eq!(wake_reply(&bytes), Some(())); assert_eq!(wake_reply(&bytes[..44]), None); bytes[30] = 128; assert_eq!(wake_reply(&bytes), None);}
#[test]fn receive_skips_stale_and_unrelated_messages() { let (sender, receiver) = mpsc::channel(); let sent = Instant::now(); sender .send((sent - Duration::from_secs(1), vec![1])) .unwrap(); sender.send((sent, vec![2])).unwrap(); sender.send((sent, vec![1])).unwrap(); let value = receive(&receiver, sent, Duration::from_secs(1), |bytes| { (bytes == [1]).then_some(42) }) .unwrap(); assert_eq!(value, 42);}
#[test]fn receive_distinguishes_deadline_from_disconnection() { let (sender, receiver) = mpsc::channel::<Reply>(); let error = receive(&receiver, Instant::now(), Duration::ZERO, |_| Some(())).unwrap_err(); assert_eq!(error.kind(), io::ErrorKind::TimedOut); drop(sender); let error = receive(&receiver, Instant::now(), Duration::from_secs(1), |_| { Some(()) }) .unwrap_err(); assert_eq!(error.kind(), io::ErrorKind::BrokenPipe);}
#[derive(Default)]struct FakeDevice { registers: Registers, reads: Vec<ParamDescriptor>, writes: Vec<(ParamDescriptor, StoredValue)>, fail_read: Option<ParamDescriptor>, fail_write: bool, ignore_writes: bool, side_effect: Option<(ParamDescriptor, StoredValue)>,}
impl RegisterReader for FakeDevice { fn read(&mut self, parameter: ParamDescriptor) -> io::Result<StoredValue> { self.reads.push(parameter); if self.fail_read == Some(parameter) { return Err(io::Error::other("read failed")); } Ok(self.registers[¶meter]) }}
impl RegisterWriter for FakeDevice { fn write(&mut self, parameter: ParamDescriptor, value: StoredValue) -> io::Result<()> { self.writes.push((parameter, value)); if self.fail_write { return Err(io::Error::other("write failed")); } if !self.ignore_writes { self.registers.insert(parameter, value); } if let Some((parameter, value)) = self.side_effect { self.registers.insert(parameter, value); } Ok(()) }}
fn targets() -> Registers { [ (param::filter::CUTOFF_HZ, StoredValue(200)), (param::filter::RESONANCE, StoredValue(300)), ] .into()}
#[test]fn apply_reads_every_baseline_writes_only_differences_then_verifies_every_target() { let targets = targets(); let mut device = FakeDevice { registers: targets.clone(), ..Default::default() }; device .registers .insert(param::filter::CUTOFF_HZ, StoredValue(100)); apply_registers(&mut device, &targets).unwrap(); assert_eq!( device.writes, [(param::filter::CUTOFF_HZ, StoredValue(200))] ); assert_eq!( device.reads, [ param::filter::CUTOFF_HZ, param::filter::RESONANCE, param::filter::CUTOFF_HZ, param::filter::RESONANCE ] ); assert_eq!(device.registers, targets);}
#[test]fn baseline_failure_prevents_all_writes() { let mut device = FakeDevice { registers: targets(), fail_read: Some(param::filter::RESONANCE), ..Default::default() }; device .registers .insert(param::filter::CUTOFF_HZ, StoredValue(100)); let error = apply_registers(&mut device, &targets()).unwrap_err(); assert!(error.to_string().contains("parameter 0x0102")); assert!(device.writes.is_empty());}
#[test]fn apply_detects_ignored_writes_and_side_effects_on_initially_matching_targets() { for side_effect in [false, true] { let mut device = FakeDevice { registers: targets(), ..Default::default() }; device .registers .insert(param::filter::CUTOFF_HZ, StoredValue(100)); if side_effect { device.side_effect = Some((param::filter::RESONANCE, StoredValue(0))); } else { device.ignore_writes = true; } let error = apply_registers(&mut device, &targets()) .unwrap_err() .to_string(); assert!(error.contains("read-back mismatch")); assert!(error.contains(if side_effect { "0x0102" } else { "0x0101" })); assert!(error.contains("partially changed")); }}
#[test]fn apply_stops_at_the_first_failed_write_with_parameter_context() { let mut device = FakeDevice { registers: targets(), fail_write: true, ..Default::default() }; device .registers .values_mut() .for_each(|value| *value = StoredValue(0)); let error = apply_registers(&mut device, &targets()).unwrap_err(); assert!(error.to_string().contains("parameter 0x0101: write failed")); assert_eq!(device.writes.len(), 1); assert_eq!(device.reads.len(), 2);}