From 1a81192cc3beaf7cbc207f6c6e5a379042592838 Mon Sep 17 00:00:00 2001 From: Nicole Tietz-Sokolskaya <me@ntietz.com> Date: Fri, 31 Jan 2025 19:48:32 +0000 Subject: [PATCH] feat: add raw message storage and display (#2) Reviewed-on: https://git.kittencollective.com/nicole/midi-keys/pulls/2 --- src/midi.rs | 14 ++++- src/parser.rs | 9 ++-- src/ui.rs | 141 ++++++++++++++++++++++++++++++-------------------- 3 files changed, 103 insertions(+), 61 deletions(-) diff --git a/src/midi.rs b/src/midi.rs index 1fd9a68..2ed2725 100644 --- a/src/midi.rs +++ b/src/midi.rs @@ -1,7 +1,19 @@ pub mod daemon; #[derive(PartialEq, Eq, Debug, Clone)] -pub enum Message { +pub struct Message { + pub raw: Vec<u8>, + pub parsed: ParsedMessage, +} + +impl Message { + pub fn new(raw: Vec<u8>, parsed: ParsedMessage) -> Self { + Message { raw, parsed } + } +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum ParsedMessage { Voice(VoiceMessage), System(SystemCommon), Realtime(SystemRealtime), diff --git a/src/parser.rs b/src/parser.rs index 8d98516..3e63a22 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3,9 +3,10 @@ use nom::{ IResult, }; -use crate::midi::{Message, SystemCommon, SystemRealtime, VoiceCategory, VoiceMessage}; +use crate::midi::{Message, ParsedMessage, SystemCommon, SystemRealtime, VoiceCategory, VoiceMessage}; pub fn parse_message(bytes: &[u8]) -> IResult<&[u8], Message> { + let raw = Vec::from(bytes); let (bytes, status_byte) = take(1usize)(bytes)?; let status_byte = status_byte[0]; @@ -14,13 +15,13 @@ pub fn parse_message(bytes: &[u8]) -> IResult<&[u8], Message> { if status_byte < 0xF0 { let (bytes, vm) = parse_voice_message(status_byte, bytes)?; - Ok((bytes, Message::Voice(vm))) + Ok((bytes, Message::new(raw, ParsedMessage::Voice(vm)))) } else if status_byte < 0xf8 { let (bytes, sc) = parse_system_common(status_byte, bytes)?; - Ok((bytes, Message::System(sc))) + Ok((bytes, Message::new(raw, ParsedMessage::System(sc)))) } else { let sr = parse_system_realtime(status_byte); - Ok((bytes, Message::Realtime(sr))) + Ok((bytes, Message::new(raw, ParsedMessage::Realtime(sr)))) } } diff --git a/src/ui.rs b/src/ui.rs index 200a62e..b5bc571 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -11,7 +11,7 @@ use crossbeam::{channel::Receiver, select}; use egui::{mutex::Mutex, Color32, Frame, Grid, RichText, Rounding, ScrollArea, SelectableLabel}; use midir::{MidiInput, MidiInputPort}; -use crate::midi::{daemon::CTM, Message, VoiceCategory, VoiceMessage}; +use crate::midi::{daemon::CTM, Message, ParsedMessage, VoiceCategory, VoiceMessage}; /// State used to display the UI. It's intended to be shared between the /// renderer and the daemon which updates the state. @@ -22,6 +22,7 @@ pub struct DisplayState { pub selected_ports: HashMap<String, bool>, pub max_messages: usize, pub only_note_on_off: Arc<AtomicBool>, + pub show_raw: Arc<AtomicBool>, } impl DisplayState { @@ -31,7 +32,8 @@ impl DisplayState { 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)), + only_note_on_off: Arc::new(AtomicBool::new(true)), + show_raw: Arc::new(AtomicBool::new(true)), } } @@ -159,7 +161,7 @@ impl eframe::App for MidiKeysApp { let conn_id = port.id(); - let selected = self.state.selected_ports.get(&conn_id).unwrap_or(&false); + let selected = self.state.selected_ports.get(&conn_id).unwrap_or(&true); if ui .add(SelectableLabel::new(*selected, &port_name)) @@ -171,73 +173,80 @@ impl eframe::App for MidiKeysApp { }); let mut only_note_on_off = self.state.only_note_on_off.load(Ordering::Relaxed); + let mut show_raw = self.state.show_raw.load(Ordering::Relaxed); egui::TopBottomPanel::top("filter_panel").show(ctx, |ui| { - ui.checkbox(&mut only_note_on_off, "Only note on/off"); + ui.horizontal_wrapped(|ui| { + ui.checkbox(&mut only_note_on_off, "Only note on/off"); + ui.checkbox(&mut show_raw, "Display raw bytes"); + }); }); self.state .only_note_on_off .store(only_note_on_off, Ordering::Relaxed); + self.state.show_raw.store(show_raw, Ordering::Relaxed); egui::CentralPanel::default().show(ctx, |ui| { - ScrollArea::vertical().show(ui, |ui| { - for (idx, (conn, _ts, msg)) in messages.iter().rev().enumerate() { - if only_note_on_off - && !matches!( - msg, - Message::Voice(VoiceMessage { - category: VoiceCategory::NoteOn { .. } - | VoiceCategory::NoteOff { .. }, - .. - }) - ) - { - 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); + ScrollArea::vertical() + .max_width(f32::INFINITY) + .show(ui, |ui| { + ui.vertical_centered_justified(|ui| { + for (idx, (conn, _ts, msg)) in messages.iter().rev().enumerate() { + if only_note_on_off + && !matches!( + &msg.parsed, + ParsedMessage::Voice(VoiceMessage { + category: VoiceCategory::NoteOn { .. } + | VoiceCategory::NoteOff { .. }, + .. + }) + ) + { + continue; + } + if !self + .state + .selected_ports + .get(conn.as_str()) + .unwrap_or(&true) + { + continue; + } + let port = match ports.iter().find(|p| &p.id() == conn) { + Some(p) => p, + None => continue, + }; + ui.set_width(ui.available_width()); + let mut frame = Frame::default() + .inner_margin(4.0) + .outer_margin(4.0) + .stroke((1.0, Color32::BLACK)) + .rounding(Rounding::same(2.0)) + .begin(ui); - 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()); + let port_name = self + .midi_in + .port_name(port) + .unwrap_or("(disconnected_device)".into()); - frame.content_ui.label(RichText::new(port_name).strong()); + frame.content_ui.label(RichText::new(port_name).strong()); - display_midi_message(idx, msg, &mut frame.content_ui); + display_midi_message(idx, msg, &mut frame.content_ui, show_raw); + frame.end(ui); + } + }); - frame.end(ui); - } - }); - - ctx.request_repaint(); + ctx.request_repaint(); + }); }); } } -fn display_midi_message(idx: usize, msg: &Message, ui: &mut egui::Ui) { - match msg { - Message::Voice(vm) => { +fn display_midi_message(idx: usize, msg: &Message, ui: &mut egui::Ui, raw: bool) { + match &msg.parsed { + ParsedMessage::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", @@ -280,16 +289,36 @@ fn display_midi_message(idx: usize, msg: &Message, ui: &mut egui::Ui) { VoiceCategory::Unknown => ("Unknown", vec![]), }; - ui.label(name); - ui.end_row(); + let voice_label = format!("Voice:{}", name); + + ui.label(RichText::new(voice_label).italics()); + ui.label(format!("channel = {}", vm.channel)); for (name, value) in fields { ui.label(format!("{name} = {value}")); } }); } - Message::System(_system_common) => {} - Message::Realtime(_system_realtime) => {} + ParsedMessage::System(_system_common) => {} + ParsedMessage::Realtime(_system_realtime) => {} + } + + if raw { + ui.horizontal_top(|ui| { + ui.label("Bytes:"); + Frame::none().show(ui, |ui| { + ui.spacing_mut().item_spacing = (0.0, 0.0).into(); + for byte in &msg.raw { + Frame::none() + .inner_margin(2.0) + .stroke((0.5, Color32::GRAY)) + .outer_margin(0.0) + .show(ui, |ui| { + ui.label(format!("{:02X}", byte)); + }); + } + }); + }); } }