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