use std::{collections::HashMap, time::Duration}; use crossbeam::{ channel::{Receiver, Sender}, select, }; use midir::{MidiInput, MidiInputConnection, MidiInputPort}; use crate::parser::parse_message; use super::Message; #[derive(PartialEq, Eq, Debug)] pub enum Category { All, Drums, WindSynth, Piano, Ignore, } /// Architecture for the MIDI connectivity: /// /// State is what's held by the daemon, and is used in an event loop. /// - queues are passed in which are what we use to route messages /// - this state can only be accessed directly by the event loop, as it owns the state /// - does a `select!` (crossbeam) on the timer tick and on other channels /// - each tick of the timer (on a timer rx channel), call `state.refresh_ports()` and assign /// ports if they're mapped automatically (drums / windsynth / ignore) for anything that /// isn't yet mapped (users can override in UI) /// - listens to a queue of routing updates ("set connectionid to drums / windsynth / ignore") /// from the UI /// - receive from the message_receive queue and route the messages /// /// ideal would be to do the port refresh on a separate thread. but! it's relatively cheap (250 /// microseconds per refresh) so it's not a big deal type ConnectionId = String; type Timestamp = u64; type CTM = (ConnectionId, Timestamp, Message); type MidiSend = crossbeam::channel::Sender; type MidiRecv = crossbeam::channel::Receiver; pub fn run_daemon(routes: Vec<(Category, MidiSend)>) { let mut state = State::new(); let mut conn_mapping: HashMap = HashMap::new(); let messages_q = state.message_queue(); let conn_mapping_q = state.connection_mapping_queue(); let ticker = crossbeam::channel::tick(Duration::from_millis(250)); loop { select! { recv(ticker) -> m => match m { Ok(_ts) => { state.refresh_ports(); } Err(e) => { println!("borken {e:?}"); } }, recv(conn_mapping_q) -> m => match m { Ok((conn_id, category)) => { conn_mapping.insert(conn_id, category); }, Err(e) => { println!("borken3 {e:?}"); } }, recv(messages_q) -> m => match m { Ok(ctm) => { let (cid, _ts, _msg) = &ctm; let msg_cat = conn_mapping.get(cid).unwrap_or(&Category::Ignore); for (route_cat, q) in routes.iter() { if route_cat == msg_cat || *route_cat == Category::All { let _ = q.send(ctm.clone()); } } } Err(e) => { println!("borken2 {e:?}"); } }, } } } pub struct State { midi: MidiInput, ports: Vec, connections: HashMap)>>, message_receive: Receiver, message_send: Sender, conn_map_receive: Receiver<(ConnectionId, Category)>, conn_map_send: Sender<(ConnectionId, Category)>, } impl State { pub fn new() -> State { let (msg_tx, msg_rx) = crossbeam::channel::unbounded(); let (conn_tx, conn_rx) = crossbeam::channel::unbounded(); State { midi: MidiInput::new("midi-keys daemon").expect("could not connect to system MIDI"), ports: vec![], connections: HashMap::new(), message_receive: msg_rx, message_send: msg_tx, conn_map_receive: conn_rx, conn_map_send: conn_tx, } } pub fn message_queue(&self) -> Receiver { self.message_receive.clone() } pub fn connection_mapping_queue(&self) -> Receiver<(ConnectionId, Category)> { self.conn_map_receive.clone() } pub fn refresh_ports(&mut self) { let ports = self.midi.ports(); for port in ports.iter() { self.refresh_port(port); } self.ports = ports; } pub fn refresh_port(&mut self, port: &MidiInputPort) { let connected = self.connections.contains_key(&port.id()); let name = match self.midi.port_name(port) { Ok(s) => s, Err(_) => { // this case happens if we're using a MidiInputPort which has been disconnected, // so we will remove the device from our connections list. we can try this even // if it's not already in the map, as this is idempotent. self.connections.remove(&port.id()); return; } }; if connected { return; } let midi = MidiInput::new("midi-keys").expect("could not connect to system MIDI"); let send_queue = self.message_send.clone(); if let Ok(conn) = midi.connect( &port, &name, |timestamp, bytes, (conn_id, send_queue)| { handle_message(timestamp, bytes, &conn_id, &send_queue); }, (port.id(), send_queue), ) { let category = guess_catgory(&name); println!("guessing category:"); println!(" name> {name}"); println!(" id> {}", port.id()); println!(" cat> {category:?}"); let _ = self.conn_map_send.send((port.id(), category)); // TODO error handler self.connections.insert(port.id(), conn); } } } fn guess_catgory(name: &str) -> Category { if name.contains("TravelSax2") || name.contains("AE-20") { Category::WindSynth } else if name.contains("MultiPad") { Category::Drums } else { Category::Ignore } } fn handle_message(ts: Timestamp, bytes: &[u8], conn_id: &ConnectionId, tx: &Sender) { if let Ok((_rem, msg)) = parse_message(bytes) { let _ = tx.send((conn_id.clone(), ts, msg)); } } // TODO: chain of handlers? -> handle event as keypress, send to UI