Something went wrong. Try again.
YAML-based live configuration of MicroFreak datyoma.tngl.io/mfctl
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357//! 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<u8>);
struct Client { // Drop the input first: midir stops its callback before releasing the receiver. _input: MidiInputConnection<()>, output: MidiOutputConnection, replies: Receiver<Reply>, sequence: u8, timeout: Duration,}
impl Client { fn connect(port: Option<&str>, timeout: Duration) -> Result<Self, Box<dyn Error>> { 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<T>(&mut self, packet: &[u8], decode: impl Fn(&[u8]) -> Option<T>) -> io::Result<T> { 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<StoredValue> { 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<dyn Error>> { 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<String, Box<dyn Error>> { let mut client = Client::connect(port, timeout)?; client.identify()?; patch::capture(&mut client, &Metadata::default())}
fn find_port<M: MidiIO>(midi: &M, requested: Option<&str>) -> Result<M::Port, Box<dyn Error>> { let ports = midi.ports(); let names = ports .iter() .map(|port| midi.port_name(port)) .collect::<Result<Vec<_>, _>>()?; Ok(ports[select_port(&names, requested)?].clone())}
fn select_port(names: &[String], requested: Option<&str>) -> io::Result<usize> { 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::<Vec<_>>() .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<T>( replies: &Receiver<Reply>, sent: Instant, timeout: Duration, decode: impl Fn(&[u8]) -> Option<T>,) -> io::Result<T> { 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<Identity> { 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<StoredValue> { 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<dyn Error>> { let baseline = targets .keys() .map(|¶meter| reader.value::<u16>(parameter)) .collect::<Result<Vec<_>, Box<dyn Error>>>()?; 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::<u16>(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<u8> { 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}