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] 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)); - } -}