//! MicroFreak live control I/O. No preset saves or sequence/audio transfer. use std::{ error::Error, io, sync::mpsc::{self, Receiver, RecvTimeoutError}, thread, time::{Duration, Instant}, }; use midir::{Ignore, MidiIO, MidiInput, MidiInputConnection, MidiOutput, MidiOutputConnection}; use mfctl_core::{ parameter::{ self as param, ParamDescriptor, RegisterError, RegisterReader, RegisterWriter, Registers, StoredValue, }, patch::{self, Metadata}, }; #[cfg(test)] mod tests; const IDENTITY_REQUEST: &[u8] = &[0xf0, 0x7e, 0x7f, 6, 1, 0xf7]; const PREFIX: &[u8] = &[0xf0, 0, 0x20, 0x6b, 7]; type Reply = (Instant, Vec); struct Client { // Drop the input first: midir stops its callback before releasing the receiver. _input: MidiInputConnection<()>, output: MidiOutputConnection, replies: Receiver, sequence: u8, timeout: Duration, } impl Client { fn connect(port: Option<&str>, timeout: Duration) -> Result> { let mut input = MidiInput::new("mfctl input")?; input.ignore(Ignore::None); let output = MidiOutput::new("mfctl output")?; let input_port = find_port(&input, port)?; let output_port = find_port(&output, port)?; let (sender, replies) = mpsc::channel(); let input = input.connect( &input_port, "mfctl receive", move |_, bytes, ()| { if bytes.first() == Some(&0xf0) { // Own the bytes; callback buffers are borrowed from midir. // A closed receiver only means the operation is shutting down. let _ = sender.send((Instant::now(), bytes.to_vec())); } }, (), )?; let output = output.connect(&output_port, "mfctl query")?; Ok(Self { _input: input, output, replies, sequence: 0, timeout, }) } fn next_sequence(&mut self) -> u8 { let sequence = self.sequence; self.sequence = (sequence + 1) & 0x7f; sequence } fn exchange(&mut self, packet: &[u8], decode: impl Fn(&[u8]) -> Option) -> io::Result { let sent = Instant::now(); self.output.send(packet).map_err(io::Error::other)?; receive(&self.replies, sent, self.timeout, decode) } fn identify(&mut self) -> io::Result<()> { let identity = match self.exchange(IDENTITY_REQUEST, identity_reply) { Err(error) if error.kind() == io::ErrorKind::TimedOut => { // The demonstrated wake-up is a read-only preset-header query, // not a preset load/save, service command or transfer. let sequence = self.next_sequence(); self.exchange(&query(sequence, 0x19, 0x0000), wake_reply)?; thread::sleep(Duration::from_millis(30)); self.exchange(IDENTITY_REQUEST, identity_reply)? } result => result?, }; identity.validate() } } impl RegisterReader for Client { fn read(&mut self, parameter: ParamDescriptor) -> io::Result { let mut attempt = 0; loop { attempt += 1; let sequence = self.next_sequence(); match self.exchange(&query(sequence, 0x41, parameter.id), |bytes| { parameter_reply(bytes, parameter) }) { Err(error) if error.kind() == io::ErrorKind::TimedOut && attempt < 3 => continue, result => return result, } } } } impl RegisterWriter for Client { fn write(&mut self, parameter: ParamDescriptor, value: StoredValue) -> io::Result<()> { let sequence = self.next_sequence(); self.output .send(&write_packet(sequence, parameter, value)) .map_err(io::Error::other)?; // Allow the synth to settle before read-back or another write. thread::sleep(Duration::from_millis(30)); Ok(()) } } /// Resolve and encode the complete desired patch before MIDI access. Metadata /// emits no writes. Failures after writing may leave partial changes; no rollback. pub fn apply(source: &str, port: Option<&str>, timeout: Duration) -> Result<(), Box> { let patch = patch::parse(source)?; let mut targets = patch.spec.registers(); targets.remove(¶m::global::SAMPLE_HASH); let mut client = Client::connect(port, timeout)?; client.identify()?; apply_registers(&mut client, &targets) } /// Read all supported live controls and render compact YAML. The optional port /// is a midir port name (or unique substring), not an ALSA RawMIDI hw: address. pub fn get(port: Option<&str>, timeout: Duration) -> Result> { let mut client = Client::connect(port, timeout)?; client.identify()?; patch::capture(&mut client, &Metadata::default()) } fn find_port(midi: &M, requested: Option<&str>) -> Result> { let ports = midi.ports(); let names = ports .iter() .map(|port| midi.port_name(port)) .collect::, _>>()?; Ok(ports[select_port(&names, requested)?].clone()) } fn select_port(names: &[String], requested: Option<&str>) -> io::Result { let exact = requested.is_some_and(|wanted| names.iter().any(|name| name == wanted)); let matches: Vec<_> = names .iter() .enumerate() .filter(|(_, name)| match requested { Some(wanted) if exact => name.as_str() == wanted, Some(wanted) => name.to_lowercase().contains(&wanted.to_lowercase()), None => name .to_lowercase() .replace([' ', '-', '_'], "") .contains("microfreak"), }) .map(|(index, _)| index) .collect(); if let [index] = matches.as_slice() { return Ok(*index); } let available = if names.is_empty() { " (none)".into() } else { names .iter() .map(|name| format!(" {name}")) .collect::>() .join("\n") }; Err(io::Error::other(format!( "expected one matching MIDI port, found {}; select one with --port NAME. Available ports:\n{available}", matches.len(), ))) } fn receive( replies: &Receiver, sent: Instant, timeout: Duration, decode: impl Fn(&[u8]) -> Option, ) -> io::Result { loop { let elapsed = sent.elapsed(); if elapsed >= timeout { return Err(io::Error::new( io::ErrorKind::TimedOut, "timed out waiting for MIDI reply", )); } let (arrived, bytes) = replies .recv_timeout(timeout - elapsed) .map_err(|error| match error { RecvTimeoutError::Timeout => { io::Error::new(io::ErrorKind::TimedOut, "timed out waiting for MIDI reply") } RecvTimeoutError::Disconnected => { io::Error::new(io::ErrorKind::BrokenPipe, "MIDI input disconnected") } })?; if arrived >= sent && let Some(value) = decode(&bytes) { return Ok(value); } } } fn query(sequence: u8, opcode: u8, id: u16) -> [u8; 13] { [ 0xf0, 0, 0x20, 0x6b, 7, 1, sequence, 3, opcode, 0, (id >> 8) as u8, id as u8, 0xf7, ] } struct Identity { microfreak: bool, version: [u8; 4], } impl Identity { fn validate(self) -> io::Result<()> { if !self.microfreak { return Err(io::Error::other( "selected MIDI device is not an Arturia MicroFreak", )); } if self.version[0] != 5 { return Err(io::Error::other(format!( "MicroFreak firmware 5 required (device reports {}.{}.{}.{})", self.version[0], self.version[1], self.version[2], self.version[3], ))); } Ok(()) } } fn identity_reply(bytes: &[u8]) -> Option { if !matches!(bytes.len(), 15 | 17) || bytes[..2] != [0xf0, 0x7e] || bytes[3..5] != [6, 2] || bytes.last() != Some(&0xf7) || bytes[1..bytes.len() - 1].iter().any(|byte| *byte >= 128) { return None; } let version = bytes.len() - 5; Some(Identity { microfreak: bytes.len() == 17 && bytes[5..12] == [0, 0x20, 0x6b, 6, 0, 6, 1], version: [ bytes[version], bytes[version + 1], bytes[version + 2], bytes[version + 3], ], }) } // Device reply counters are independent: match opcode and parameter ID, not // an assumed echo of the request counter. Exchanges are serialized and receive // discards messages that arrived before the request was sent. fn parameter_reply(bytes: &[u8], parameter: ParamDescriptor) -> Option { if bytes.len() != 15 || &bytes[..5] != PREFIX || !matches!(bytes[5], 1 | 0x7f) || bytes[7..9] != [5, 0x40] || bytes[14] != 0xf7 || bytes[5..14].iter().any(|byte| *byte >= 128) || u16::from_be_bytes([bytes[10], bytes[11]]) != parameter.id { return None; } let high = bytes[12] | ((bytes[9] & 0x10) << 3); let low = bytes[13] | ((bytes[9] & 0x08) << 4); Some(StoredValue(u16::from_be_bytes([high, low]))) } fn wake_reply(bytes: &[u8]) -> Option<()> { (bytes.len() == 45 && &bytes[..5] == PREFIX && bytes[5] == 1 && bytes[7..9] == [35, 0x52] && bytes[44] == 0xf7 && bytes[6..44].iter().all(|byte| *byte < 128)) .then_some(()) } /// Baselines must all succeed before the first write. Verify every target, /// including initially matching registers: earlier writes can have side effects. fn apply_registers( reader: &mut impl RegisterWriter, targets: &Registers, ) -> Result<(), Box> { let baseline = targets .keys() .map(|¶meter| reader.value::(parameter)) .collect::, Box>>()?; for ((¶meter, &target), initial) in targets.iter().zip(baseline) { if initial != target.0 { reader .write(parameter, target) .map_err(|source| RegisterError { parameter, source: source.into(), })?; } } for (¶meter, &target) in targets { let actual = reader.value::(parameter)?; if actual != target.0 { return Err(io::Error::other(format!( "patch read-back mismatch at 0x{:04X}: expected 0x{:04X}, got 0x{actual:04X}; device may be partially changed", parameter.id, target.0 )).into()); } } Ok(()) } /// Full-register numeric opcode 53; never send raw MSBs as SysEx data. fn write_packet(sequence: u8, parameter: ParamDescriptor, value: StoredValue) -> Vec { let payload = format!("synth param {} {}", parameter.id, value.0); let mut packet = vec![ 0xf0, 0, 0x20, 0x6b, 7, 1, sequence, payload.len() as u8, 0x53, ]; packet.extend_from_slice(payload.as_bytes()); packet.push(0xf7); packet }