From 3cae2d310c263d771859c5f37c201016e96f4ccd Mon Sep 17 00:00:00 2001 From: Nicole Tietz-Sokolskaya <me@ntietz.com> Date: Fri, 3 Jan 2025 14:25:28 -0500 Subject: [PATCH 1/5] routes --- src/bin/main.rs | 26 +++++++++++++++++--- src/midi/daemon.rs | 61 ++++++++++++++++++++++++++++++++++++++-------- src/ui.rs | 4 +-- 3 files changed, 74 insertions(+), 17 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index a6b8d89..f17b5ac 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -13,19 +13,37 @@ fn main() { //enigo.text("echo \"hello world\"").unwrap(); //enigo.key(Key::Return, Direction::Press).unwrap(); - let (send, receive) = crossbeam::channel::unbounded(); + let (dbg_send, dbg_recv) = crossbeam::channel::unbounded(); + let (ws_send, ws_recv) = crossbeam::channel::unbounded(); + let (drum_send, drum_recv) = crossbeam::channel::unbounded(); let all = Category::All; std::thread::spawn(move || { - midi_keys::midi::daemon::run_daemon(vec![(all, send)]); + midi_keys::midi::daemon::run_daemon(vec![ + (all, dbg_send), + (Category::WindSynth, ws_send), + (Category::Drums, drum_send), + ]); }); std::thread::spawn(move || loop { - match receive.recv() { - Ok(m) => println!("got: {m:?}"), + match dbg_recv.recv() { + Ok(m) => println!("debug: {m:?}"), Err(err) => println!("err: {err:?}"), } }); + std::thread::spawn(move || loop { + match ws_recv.recv() { + Ok(m) => println!("windsynth: {m:?}"), + Err(err) => println!("err(ws): {err:?}"), + } + }); + std::thread::spawn(move || loop { + match drum_recv.recv() { + Ok(m) => println!("drum: {m:?}"), + Err(err) => println!("err(drum): {err:?}"), + } + }); midi_keys::ui::run(); } diff --git a/src/midi/daemon.rs b/src/midi/daemon.rs index a703b2c..f232c1f 100644 --- a/src/midi/daemon.rs +++ b/src/midi/daemon.rs @@ -10,6 +10,7 @@ use crate::parser::parse_message; use super::Message; +#[derive(PartialEq, Eq, Debug)] pub enum Category { All, Drums, @@ -36,7 +37,6 @@ pub enum Category { type ConnectionId = String; type Timestamp = u64; -type TimestampedMessage = (Timestamp, Message); type CTM = (ConnectionId, Timestamp, Message); type MidiSend = crossbeam::channel::Sender<CTM>; @@ -44,25 +44,39 @@ type MidiRecv = crossbeam::channel::Receiver<CTM>; pub fn run_daemon(routes: Vec<(Category, MidiSend)>) { let mut state = State::new(); - let messages = state.message_queue(); + 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) => { - println!("refreshing ports at {ts:?}"); + Ok(_ts) => { state.refresh_ports(); } Err(e) => { println!("borken {e:?}"); } }, - recv(messages) -> m => match m { + 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) => { - for (_cat, q) in routes.iter() { - q.send(ctm.clone()); + 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) => { @@ -80,18 +94,24 @@ pub struct State { 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 (tx, rx) = crossbeam::channel::unbounded(); + 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: rx, - message_send: tx, + message_receive: msg_rx, + message_send: msg_tx, + conn_map_receive: conn_rx, + conn_map_send: conn_tx, } } @@ -99,6 +119,10 @@ impl State { 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() { @@ -136,11 +160,28 @@ impl State { }, (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)); diff --git a/src/ui.rs b/src/ui.rs index 552d06b..870bd5c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -156,9 +156,7 @@ impl eframe::App for MidiKeysApp { } } -struct MidiDaemon { - -} +struct MidiDaemon {} pub fn launch_midi_daemon(target_field: Arc<Mutex<Vec<MidiInputPort>>>) -> JoinHandle<()> { let daemon_handle = std::thread::spawn(move || midi_daemon(target_field)); -- 2.34.1 From 40acf31efc2345ec5b844d53fa092c3962545f51 Mon Sep 17 00:00:00 2001 From: Nicole Tietz-Sokolskaya <me@ntietz.com> Date: Wed, 8 Jan 2025 22:29:23 -0500 Subject: [PATCH 2/5] display all events in the ui --- Cargo.toml | 2 +- src/bin/main.rs | 31 ++++-- src/midi/daemon.rs | 100 +++++++++++-------- src/ui.rs | 236 +++++++++++++++++++++++++-------------------- 4 files changed, 209 insertions(+), 160 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 970c60e..b6add1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,4 @@ nom = "7.1.3" thiserror = "1.0.63" [lints.clippy] -unwrap_used = "deny" +#unwrap_used = "deny" diff --git a/src/bin/main.rs b/src/bin/main.rs index f17b5ac..de8176d 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -4,7 +4,12 @@ use std::{ }; use anyhow::{anyhow, Result}; -use midi_keys::{log::load_raw_log, midi::daemon::Category, parser::parse_message}; +use midi_keys::{ + log::load_raw_log, + midi::daemon::Category, + parser::parse_message, + ui::{display_state_daemon, DisplayQueues, DisplayState}, +}; //use enigo::{Direction, Enigo, Key, Keyboard, Settings}; use midir::{MidiInput, MidiInputPort}; @@ -16,15 +21,21 @@ fn main() { let (dbg_send, dbg_recv) = crossbeam::channel::unbounded(); let (ws_send, ws_recv) = crossbeam::channel::unbounded(); let (drum_send, drum_recv) = crossbeam::channel::unbounded(); - let all = Category::All; - std::thread::spawn(move || { - midi_keys::midi::daemon::run_daemon(vec![ - (all, dbg_send), - (Category::WindSynth, ws_send), - (Category::Drums, drum_send), - ]); - }); + let (msgs_send, msgs_recv) = crossbeam::channel::unbounded(); + + let (_handle, ports_recv) = midi_keys::midi::daemon::run_daemon(vec![ + (Category::All, dbg_send), + (Category::All, msgs_send), + (Category::WindSynth, ws_send), + (Category::Drums, drum_send), + ]); + + let queues = DisplayQueues::new(msgs_recv, ports_recv); + let state = DisplayState::new(); + + let _handle = display_state_daemon(queues, state.clone()); + println!("started daemon"); std::thread::spawn(move || loop { match dbg_recv.recv() { @@ -45,7 +56,7 @@ fn main() { } }); - midi_keys::ui::run(); + midi_keys::ui::run(state); } pub fn run() -> Result<()> { diff --git a/src/midi/daemon.rs b/src/midi/daemon.rs index f232c1f..1389d03 100644 --- a/src/midi/daemon.rs +++ b/src/midi/daemon.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, time::Duration}; +use std::{collections::HashMap, thread::JoinHandle, time::Duration}; use crossbeam::{ channel::{Receiver, Sender}, @@ -35,56 +35,60 @@ pub enum Category { /// 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); +pub type ConnectionId = String; +pub type Timestamp = u64; +pub 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)>) { +pub fn run_daemon( + routes: Vec<(Category, Sender<CTM>)>, +) -> (JoinHandle<usize>, Receiver<Vec<MidiInputPort>>) { let mut state = State::new(); + let ports_receiver = state.ports_queue(); - let mut conn_mapping: HashMap<ConnectionId, Category> = HashMap::new(); + let handle = std::thread::spawn(move || { + 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)); + 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); + loop { + select! { + recv(ticker) -> m => match m { + Ok(_ts) => { + state.refresh_ports(); + } + Err(e) => { + println!("borken {e:?}"); + } }, - 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()); + 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:?}"); - } - }, + Err(e) => { + println!("borken2 {e:?}"); + } + }, + } } - } + }); + + (handle, ports_receiver) } pub struct State { @@ -97,12 +101,16 @@ pub struct State { conn_map_receive: Receiver<(ConnectionId, Category)>, conn_map_send: Sender<(ConnectionId, Category)>, + + ports_receive: Receiver<Vec<MidiInputPort>>, + ports_send: Sender<Vec<MidiInputPort>>, } impl State { pub fn new() -> State { let (msg_tx, msg_rx) = crossbeam::channel::unbounded(); let (conn_tx, conn_rx) = crossbeam::channel::unbounded(); + let (ports_tx, ports_rx) = crossbeam::channel::unbounded(); State { midi: MidiInput::new("midi-keys daemon").expect("could not connect to system MIDI"), @@ -112,6 +120,8 @@ impl State { message_send: msg_tx, conn_map_receive: conn_rx, conn_map_send: conn_tx, + ports_receive: ports_rx, + ports_send: ports_tx, } } @@ -123,11 +133,17 @@ impl State { self.conn_map_receive.clone() } + pub fn ports_queue(&self) -> Receiver<Vec<MidiInputPort>> { + self.ports_receive.clone() + } + pub fn refresh_ports(&mut self) { let ports = self.midi.ports(); for port in ports.iter() { self.refresh_port(port); } + // TODO: don't ignore + let _ = self.ports_send.send(ports.clone()); self.ports = ports; } diff --git a/src/ui.rs b/src/ui.rs index 870bd5c..f97b437 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,102 +1,120 @@ -use std::{sync::Arc, thread::JoinHandle}; +use std::{ + collections::VecDeque, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + thread::JoinHandle, +}; -use egui::{mutex::Mutex, Color32, Frame, Rounding, SelectableLabel}; +use crossbeam::{channel::Receiver, select}; +use egui::{mutex::Mutex, Color32, Frame, Rounding, ScrollArea, SelectableLabel}; use midir::{MidiInput, MidiInputPort}; -use crate::midi::Message; +use crate::midi::{daemon::CTM, Message, VoiceCategory, VoiceMessage}; + +/// State used to display the UI. It's intended to be shared between the +/// renderer and the daemon which updates the state. +#[derive(Clone)] +pub struct DisplayState { + pub midi_input_ports: Arc<Mutex<Vec<MidiInputPort>>>, + pub midi_messages: Arc<Mutex<VecDeque<CTM>>>, + pub max_messages: usize, + pub only_note_on_off: Arc<AtomicBool>, +} + +impl DisplayState { + pub fn new() -> DisplayState { + DisplayState { + midi_input_ports: Arc::new(Mutex::new(vec![])), + midi_messages: Arc::new(Mutex::new(VecDeque::new())), + max_messages: 10_000_usize, + only_note_on_off: Arc::new(AtomicBool::new(false)), + } + } + + pub fn set_ports(&self, updated_ports: Vec<MidiInputPort>) { + let mut ports = self.midi_input_ports.lock(); + *ports = updated_ports; + } + + pub fn store_message(&self, message: CTM) { + let mut messages = self.midi_messages.lock(); + messages.push_back(message); + if messages.len() > self.max_messages { + messages.pop_front(); + } + } +} + +/// Queues we receive from to refresh and update the UI. +#[derive(Debug, Clone)] +pub struct DisplayQueues { + pub messages: Receiver<CTM>, + pub ports: Receiver<Vec<MidiInputPort>>, + // TODO: conn mapping +} + +impl DisplayQueues { + pub fn new(messages: Receiver<CTM>, ports: Receiver<Vec<MidiInputPort>>) -> DisplayQueues { + DisplayQueues { messages, ports } + } +} + +pub fn display_state_daemon(queues: DisplayQueues, state: DisplayState) -> JoinHandle<()> { + std::thread::spawn(move || loop { + select! { + recv(queues.messages) -> m => match m { + Ok(ctm) => { + state.store_message(ctm); + } + Err(e) => { + println!("borken {e:?}"); + } + }, + + recv(queues.ports) -> ports => match ports { + Ok(ports) => { + state.set_ports(ports) + } + Err(e) => { + println!("borken {e:?}"); + } + } + + + } + }) +} /// Launches the UI and runs it until it's done executing. -pub fn run() { +/// +/// Accepts a VecDeque as input, which is used as shared state to provide the +/// messages +pub fn run(state: DisplayState) { let native_options = eframe::NativeOptions::default(); // TODO: don't ignore result let _ = eframe::run_native( "Midi Keys", native_options, - Box::new(|cc| Ok(Box::new(MidiKeysApp::new(cc)))), + Box::new(|cc| Ok(Box::new(MidiKeysApp::new(cc, state.clone())))), ); } -const MAX_MESSAGES: usize = 1_000; -type Timestamp = u64; - -struct MidiMessageBuffer { - messages: Vec<(Timestamp, Message)>, - start: usize, - max: usize, -} - -impl MidiMessageBuffer { - pub fn new(max: usize) -> MidiMessageBuffer { - Self { - messages: vec![], - start: 0, - max, - } - } - - pub fn insert(&mut self, m: (Timestamp, Message)) { - if self.messages.len() < self.max { - self.messages.push(m); - } else { - self.messages[self.start] = m; - self.start = (self.start + 1) % self.messages.len(); - } - } - - pub fn iter(&self) -> BufferIter { - BufferIter { - elems: self.messages.as_slice(), - start: self.start, - index: 0, - } - } -} - -struct BufferIter<'a> { - elems: &'a [(Timestamp, Message)], - start: usize, - index: usize, -} - -impl<'a> Iterator for BufferIter<'a> { - type Item = (Timestamp, Message); - - fn next(&mut self) -> Option<Self::Item> { - if self.index >= self.elems.len() { - None - } else { - let elem = self.elems[(self.start + self.index) % self.elems.len()].clone(); - self.index += 1; - Some(elem) - } - } -} - struct MidiKeysApp { - midi_input_ports: Arc<Mutex<Vec<MidiInputPort>>>, - midi_messages: Arc<Mutex<MidiMessageBuffer>>, midi_in: MidiInput, + state: DisplayState, } impl MidiKeysApp { - fn new(_cc: &eframe::CreationContext<'_>) -> Self { + fn new(_cc: &eframe::CreationContext<'_>, state: DisplayState) -> Self { // this is where to hook in for customizing eguji, like fonts and visuals. let midi_in: MidiInput = MidiInput::new("midi-keys").expect("could not connect to system MIDI"); - let midi_input_ports = Arc::new(Mutex::new(Vec::new())); - let midi_messages = Arc::new(Mutex::new(MidiMessageBuffer::new(MAX_MESSAGES))); - - // TODO: have a way to shut down the midi daemon? - let _ = launch_midi_daemon(midi_input_ports.clone()); - - MidiKeysApp { - midi_input_ports, - midi_messages, - midi_in, - } + MidiKeysApp { midi_in, state } } } @@ -104,7 +122,8 @@ impl eframe::App for MidiKeysApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { ctx.set_theme(egui::Theme::Light); - let ports = self.midi_input_ports.lock().clone(); + let ports = self.state.midi_input_ports.lock().clone(); + let messages = self.state.midi_messages.lock().clone(); egui::TopBottomPanel::top("menu_bar_panel").show(ctx, |ui| { egui::menu::bar(ui, |ui| { @@ -134,42 +153,45 @@ impl eframe::App for MidiKeysApp { } }); + let mut only_note_on_off = self.state.only_note_on_off.load(Ordering::Relaxed); + + egui::TopBottomPanel::top("filter_panel").show(ctx, |ui| { + ui.checkbox(&mut only_note_on_off, "Only note on/off"); + }); + + self.state + .only_note_on_off + .store(only_note_on_off, Ordering::Relaxed); + egui::CentralPanel::default().show(ctx, |ui| { - for port in ports.iter() { - let mut frame = Frame::default() - .inner_margin(4.0) - .stroke((1.0, Color32::BLACK)) - .rounding(Rounding::same(2.0)) - .begin(ui); + ScrollArea::vertical().show(ui, |ui| { + for (_conn, ts, msg) in messages.iter().rev() { + if only_note_on_off + && !matches!( + msg, + Message::Voice(VoiceMessage { + category: VoiceCategory::NoteOn { .. } + | VoiceCategory::NoteOff { .. }, + .. + }) + ) + { + continue; + } + let mut frame = Frame::default() + .inner_margin(4.0) + .stroke((1.0, Color32::BLACK)) + .rounding(Rounding::same(2.0)) + .begin(ui); - let port_name = self - .midi_in - .port_name(port) - .unwrap_or("unknown".to_string()); - frame.content_ui.label(port_name); + let s = format!("{ts}: {:?}", msg); + frame.content_ui.label(s); - frame.end(ui); - } + frame.end(ui); + } + }); ctx.request_repaint(); }); } } - -struct MidiDaemon {} - -pub fn launch_midi_daemon(target_field: Arc<Mutex<Vec<MidiInputPort>>>) -> JoinHandle<()> { - let daemon_handle = std::thread::spawn(move || midi_daemon(target_field)); - - daemon_handle -} - -pub fn midi_daemon(target_field: Arc<Mutex<Vec<MidiInputPort>>>) { - let midi_in: MidiInput = MidiInput::new("midi-keys").expect("could not connect to system MIDI"); - - loop { - *target_field.lock() = midi_in.ports(); - - std::thread::sleep(std::time::Duration::from_millis(100)); - } -} -- 2.34.1 From 6bc347851dad6695323a6ac283c729797e36d1ac Mon Sep 17 00:00:00 2001 From: Nicole Tietz-Sokolskaya <me@ntietz.com> Date: Fri, 10 Jan 2025 19:12:01 -0500 Subject: [PATCH 3/5] fix clippy lints --- src/midi/daemon.rs | 10 ++++++++-- src/ui.rs | 6 ++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/midi/daemon.rs b/src/midi/daemon.rs index 1389d03..03fd07a 100644 --- a/src/midi/daemon.rs +++ b/src/midi/daemon.rs @@ -169,10 +169,10 @@ impl State { let send_queue = self.message_send.clone(); if let Ok(conn) = midi.connect( - &port, + port, &name, |timestamp, bytes, (conn_id, send_queue)| { - handle_message(timestamp, bytes, &conn_id, &send_queue); + handle_message(timestamp, bytes, conn_id, send_queue); }, (port.id(), send_queue), ) { @@ -188,6 +188,12 @@ impl State { } } +impl Default for State { + fn default() -> Self { + State::new() + } +} + fn guess_catgory(name: &str) -> Category { if name.contains("TravelSax2") || name.contains("AE-20") { Category::WindSynth diff --git a/src/ui.rs b/src/ui.rs index f97b437..f3c6b9c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -47,6 +47,12 @@ impl DisplayState { } } +impl Default for DisplayState { + fn default() -> Self { + DisplayState::new() + } +} + /// Queues we receive from to refresh and update the UI. #[derive(Debug, Clone)] pub struct DisplayQueues { -- 2.34.1 From 914db6dbe0c350bb47ee7927ff924a6caa94f8c5 Mon Sep 17 00:00:00 2001 From: Nicole Tietz-Sokolskaya <me@ntietz.com> Date: Fri, 10 Jan 2025 19:13:15 -0500 Subject: [PATCH 4/5] fix final clippy lint --- src/midi/daemon.rs | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/midi/daemon.rs b/src/midi/daemon.rs index 03fd07a..c3181d7 100644 --- a/src/midi/daemon.rs +++ b/src/midi/daemon.rs @@ -1,3 +1,19 @@ +//! 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 + use std::{collections::HashMap, thread::JoinHandle, time::Duration}; use crossbeam::{ @@ -19,21 +35,6 @@ pub enum Category { 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 pub type ConnectionId = String; pub type Timestamp = u64; -- 2.34.1 From 9d9c82e410836362f34f44aa44ba00a50e9ffffb Mon Sep 17 00:00:00 2001 From: Nicole Tietz-Sokolskaya <me@ntietz.com> Date: Sun, 12 Jan 2025 22:20:19 -0500 Subject: [PATCH 5/5] fix up UI more --- src/midi/daemon.rs | 27 ++------- src/ui.rs | 139 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 136 insertions(+), 30 deletions(-) diff --git a/src/midi/daemon.rs b/src/midi/daemon.rs index c3181d7..cdaf67d 100644 --- a/src/midi/daemon.rs +++ b/src/midi/daemon.rs @@ -47,10 +47,7 @@ pub fn run_daemon( let ports_receiver = state.ports_queue(); let handle = std::thread::spawn(move || { - 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 { @@ -63,18 +60,10 @@ pub fn run_daemon( 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); + let msg_cat = state.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()); @@ -96,13 +85,11 @@ pub struct State { midi: MidiInput, ports: Vec<MidiInputPort>, connections: HashMap<String, MidiInputConnection<(ConnectionId, Sender<CTM>)>>, + conn_mapping: HashMap<ConnectionId, Category>, message_receive: Receiver<CTM>, message_send: Sender<CTM>, - conn_map_receive: Receiver<(ConnectionId, Category)>, - conn_map_send: Sender<(ConnectionId, Category)>, - ports_receive: Receiver<Vec<MidiInputPort>>, ports_send: Sender<Vec<MidiInputPort>>, } @@ -110,17 +97,15 @@ pub struct State { impl State { pub fn new() -> State { let (msg_tx, msg_rx) = crossbeam::channel::unbounded(); - let (conn_tx, conn_rx) = crossbeam::channel::unbounded(); let (ports_tx, ports_rx) = crossbeam::channel::unbounded(); State { midi: MidiInput::new("midi-keys daemon").expect("could not connect to system MIDI"), ports: vec![], connections: HashMap::new(), + conn_mapping: HashMap::new(), message_receive: msg_rx, message_send: msg_tx, - conn_map_receive: conn_rx, - conn_map_send: conn_tx, ports_receive: ports_rx, ports_send: ports_tx, } @@ -130,10 +115,6 @@ impl State { self.message_receive.clone() } - pub fn connection_mapping_queue(&self) -> Receiver<(ConnectionId, Category)> { - self.conn_map_receive.clone() - } - pub fn ports_queue(&self) -> Receiver<Vec<MidiInputPort>> { self.ports_receive.clone() } @@ -183,7 +164,7 @@ impl State { println!(" id> {}", port.id()); println!(" cat> {category:?}"); - let _ = self.conn_map_send.send((port.id(), category)); // TODO error handler + self.conn_mapping.insert(port.id(), category); self.connections.insert(port.id(), conn); } } diff --git a/src/ui.rs b/src/ui.rs index f3c6b9c..200a62e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,5 +1,5 @@ use std::{ - collections::VecDeque, + collections::{HashMap, VecDeque}, sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -8,7 +8,7 @@ use std::{ }; use crossbeam::{channel::Receiver, select}; -use egui::{mutex::Mutex, Color32, Frame, Rounding, ScrollArea, SelectableLabel}; +use egui::{mutex::Mutex, Color32, Frame, Grid, RichText, Rounding, ScrollArea, SelectableLabel}; use midir::{MidiInput, MidiInputPort}; use crate::midi::{daemon::CTM, Message, VoiceCategory, VoiceMessage}; @@ -19,6 +19,7 @@ use crate::midi::{daemon::CTM, Message, VoiceCategory, VoiceMessage}; pub struct DisplayState { pub midi_input_ports: Arc<Mutex<Vec<MidiInputPort>>>, pub midi_messages: Arc<Mutex<VecDeque<CTM>>>, + pub selected_ports: HashMap<String, bool>, pub max_messages: usize, pub only_note_on_off: Arc<AtomicBool>, } @@ -28,6 +29,7 @@ impl DisplayState { DisplayState { midi_input_ports: Arc::new(Mutex::new(vec![])), midi_messages: Arc::new(Mutex::new(VecDeque::new())), + selected_ports: HashMap::new(), max_messages: 10_000_usize, only_note_on_off: Arc::new(AtomicBool::new(false)), } @@ -149,13 +151,22 @@ impl eframe::App for MidiKeysApp { egui::SidePanel::left("instrument_panel").show(ctx, |ui| { ui.heading("Connections"); - for (idx, port) in ports.iter().enumerate() { + for port in ports.iter() { let port_name = self .midi_in .port_name(port) .unwrap_or("unknown".to_string()); - ui.add(SelectableLabel::new(idx == 0, port_name)); + let conn_id = port.id(); + + let selected = self.state.selected_ports.get(&conn_id).unwrap_or(&false); + + if ui + .add(SelectableLabel::new(*selected, &port_name)) + .clicked() + { + self.state.selected_ports.insert(conn_id, !selected); + } } }); @@ -171,7 +182,7 @@ impl eframe::App for MidiKeysApp { egui::CentralPanel::default().show(ctx, |ui| { ScrollArea::vertical().show(ui, |ui| { - for (_conn, ts, msg) in messages.iter().rev() { + for (idx, (conn, _ts, msg)) in messages.iter().rev().enumerate() { if only_note_on_off && !matches!( msg, @@ -184,14 +195,32 @@ impl eframe::App for MidiKeysApp { { continue; } + if !self + .state + .selected_ports + .get(conn.as_str()) + .unwrap_or(&false) + { + continue; + } let mut frame = Frame::default() .inner_margin(4.0) .stroke((1.0, Color32::BLACK)) .rounding(Rounding::same(2.0)) .begin(ui); - let s = format!("{ts}: {:?}", msg); - frame.content_ui.label(s); + let port = match ports.iter().find(|p| &p.id() == conn) { + Some(p) => p, + None => continue, + }; + let port_name = self + .midi_in + .port_name(port) + .unwrap_or("(disconnected_device)".into()); + + frame.content_ui.label(RichText::new(port_name).strong()); + + display_midi_message(idx, msg, &mut frame.content_ui); frame.end(ui); } @@ -201,3 +230,99 @@ impl eframe::App for MidiKeysApp { }); } } + +fn display_midi_message(idx: usize, msg: &Message, ui: &mut egui::Ui) { + match msg { + Message::Voice(vm) => { + Grid::new(format!("message_grid_{idx}")).show(ui, |ui| { + let voice_label = format!("Voice (channel={})", vm.channel); + ui.label(RichText::new(voice_label).italics()); + + let (name, fields) = match vm.category { + VoiceCategory::NoteOff { note, velocity } => ( + "NoteOff", + vec![ + ("note", note_name(note)), + ("velocity", format!("{}", velocity)), + ], + ), + VoiceCategory::NoteOn { note, velocity } => ( + "NoteOn", + vec![ + ("note", note_name(note)), + ("velocity", format!("{}", velocity)), + ], + ), + VoiceCategory::AfterTouch { note, pressure } => ( + "AfterTouch", + vec![ + ("note", note_name(note)), + ("pressure", format!("{}", pressure)), + ], + ), + VoiceCategory::ControlChange { controller, value } => ( + "ControlChange", + vec![ + ("controller", format!("{}", controller)), + ("value", format!("{}", value)), + ], + ), + VoiceCategory::ProgramChange { value } => { + ("ProgramChange", vec![("value", format!("{}", value))]) + } + VoiceCategory::ChannelPressure { pressure } => ( + "ChannelPressure", + vec![("pressure", format!("{}", pressure))], + ), + VoiceCategory::PitchWheel { value } => { + ("PitchWheel", vec![("value", format!("{}", value))]) + } + VoiceCategory::Unknown => ("Unknown", vec![]), + }; + + ui.label(name); + ui.end_row(); + + for (name, value) in fields { + ui.label(format!("{name} = {value}")); + } + }); + } + Message::System(_system_common) => {} + Message::Realtime(_system_realtime) => {} + } +} + +fn note_name(midi_note: u8) -> String { + let octave = ((midi_note as i32) - 12) / 12; + let note = match midi_note % 12 { + 0 => "C", + 1 => "C#", + 2 => "D", + 3 => "Eb", + 4 => "E", + 5 => "F#", + 6 => "F#", + 7 => "G", + 8 => "Ab", + 9 => "A", + 10 => "Bb", + 11 => "B", + _ => panic!("somehow we broke modular arithmetic rules i guess"), + }; + + format!("{note}{octave}") +} + +#[cfg(test)] +mod tests { + use crate::ui::note_name; + + #[test] + pub fn names_of_notes() { + assert_eq!(note_name(60), "C4"); + assert_eq!(note_name(36), "C2"); + assert_eq!(note_name(57), "A3"); + assert_eq!(note_name(35), "B1"); + } +} -- 2.34.1