190 lines
6.1 KiB
Rust
190 lines
6.1 KiB
Rust
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<CTM>;
|
|
type MidiRecv = crossbeam::channel::Receiver<CTM>;
|
|
|
|
pub fn run_daemon(routes: Vec<(Category, MidiSend)>) {
|
|
let mut state = State::new();
|
|
|
|
let mut conn_mapping: HashMap<ConnectionId, Category> = 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<MidiInputPort>,
|
|
connections: HashMap<String, MidiInputConnection<(ConnectionId, Sender<CTM>)>>,
|
|
|
|
message_receive: Receiver<CTM>,
|
|
message_send: Sender<CTM>,
|
|
|
|
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<CTM> {
|
|
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<CTM>) {
|
|
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
|