parent
a96285501d
commit
1a81192cc3
3 changed files with 103 additions and 61 deletions
14
src/midi.rs
14
src/midi.rs
|
@ -1,7 +1,19 @@
|
||||||
pub mod daemon;
|
pub mod daemon;
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
#[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),
|
Voice(VoiceMessage),
|
||||||
System(SystemCommon),
|
System(SystemCommon),
|
||||||
Realtime(SystemRealtime),
|
Realtime(SystemRealtime),
|
||||||
|
|
|
@ -3,9 +3,10 @@ use nom::{
|
||||||
IResult,
|
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> {
|
pub fn parse_message(bytes: &[u8]) -> IResult<&[u8], Message> {
|
||||||
|
let raw = Vec::from(bytes);
|
||||||
let (bytes, status_byte) = take(1usize)(bytes)?;
|
let (bytes, status_byte) = take(1usize)(bytes)?;
|
||||||
let status_byte = status_byte[0];
|
let status_byte = status_byte[0];
|
||||||
|
|
||||||
|
@ -14,13 +15,13 @@ pub fn parse_message(bytes: &[u8]) -> IResult<&[u8], Message> {
|
||||||
|
|
||||||
if status_byte < 0xF0 {
|
if status_byte < 0xF0 {
|
||||||
let (bytes, vm) = parse_voice_message(status_byte, bytes)?;
|
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 {
|
} else if status_byte < 0xf8 {
|
||||||
let (bytes, sc) = parse_system_common(status_byte, bytes)?;
|
let (bytes, sc) = parse_system_common(status_byte, bytes)?;
|
||||||
Ok((bytes, Message::System(sc)))
|
Ok((bytes, Message::new(raw, ParsedMessage::System(sc))))
|
||||||
} else {
|
} else {
|
||||||
let sr = parse_system_realtime(status_byte);
|
let sr = parse_system_realtime(status_byte);
|
||||||
Ok((bytes, Message::Realtime(sr)))
|
Ok((bytes, Message::new(raw, ParsedMessage::Realtime(sr))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
141
src/ui.rs
141
src/ui.rs
|
@ -11,7 +11,7 @@ use crossbeam::{channel::Receiver, select};
|
||||||
use egui::{mutex::Mutex, Color32, Frame, Grid, RichText, Rounding, ScrollArea, SelectableLabel};
|
use egui::{mutex::Mutex, Color32, Frame, Grid, RichText, Rounding, ScrollArea, SelectableLabel};
|
||||||
use midir::{MidiInput, MidiInputPort};
|
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
|
/// State used to display the UI. It's intended to be shared between the
|
||||||
/// renderer and the daemon which updates the state.
|
/// renderer and the daemon which updates the state.
|
||||||
|
@ -22,6 +22,7 @@ pub struct DisplayState {
|
||||||
pub selected_ports: HashMap<String, bool>,
|
pub selected_ports: HashMap<String, bool>,
|
||||||
pub max_messages: usize,
|
pub max_messages: usize,
|
||||||
pub only_note_on_off: Arc<AtomicBool>,
|
pub only_note_on_off: Arc<AtomicBool>,
|
||||||
|
pub show_raw: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DisplayState {
|
impl DisplayState {
|
||||||
|
@ -31,7 +32,8 @@ impl DisplayState {
|
||||||
midi_messages: Arc::new(Mutex::new(VecDeque::new())),
|
midi_messages: Arc::new(Mutex::new(VecDeque::new())),
|
||||||
selected_ports: HashMap::new(),
|
selected_ports: HashMap::new(),
|
||||||
max_messages: 10_000_usize,
|
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 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
|
if ui
|
||||||
.add(SelectableLabel::new(*selected, &port_name))
|
.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 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| {
|
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
|
self.state
|
||||||
.only_note_on_off
|
.only_note_on_off
|
||||||
.store(only_note_on_off, Ordering::Relaxed);
|
.store(only_note_on_off, Ordering::Relaxed);
|
||||||
|
self.state.show_raw.store(show_raw, Ordering::Relaxed);
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
ScrollArea::vertical().show(ui, |ui| {
|
ScrollArea::vertical()
|
||||||
for (idx, (conn, _ts, msg)) in messages.iter().rev().enumerate() {
|
.max_width(f32::INFINITY)
|
||||||
if only_note_on_off
|
.show(ui, |ui| {
|
||||||
&& !matches!(
|
ui.vertical_centered_justified(|ui| {
|
||||||
msg,
|
for (idx, (conn, _ts, msg)) in messages.iter().rev().enumerate() {
|
||||||
Message::Voice(VoiceMessage {
|
if only_note_on_off
|
||||||
category: VoiceCategory::NoteOn { .. }
|
&& !matches!(
|
||||||
| VoiceCategory::NoteOff { .. },
|
&msg.parsed,
|
||||||
..
|
ParsedMessage::Voice(VoiceMessage {
|
||||||
})
|
category: VoiceCategory::NoteOn { .. }
|
||||||
)
|
| VoiceCategory::NoteOff { .. },
|
||||||
{
|
..
|
||||||
continue;
|
})
|
||||||
}
|
)
|
||||||
if !self
|
{
|
||||||
.state
|
continue;
|
||||||
.selected_ports
|
}
|
||||||
.get(conn.as_str())
|
if !self
|
||||||
.unwrap_or(&false)
|
.state
|
||||||
{
|
.selected_ports
|
||||||
continue;
|
.get(conn.as_str())
|
||||||
}
|
.unwrap_or(&true)
|
||||||
let mut frame = Frame::default()
|
{
|
||||||
.inner_margin(4.0)
|
continue;
|
||||||
.stroke((1.0, Color32::BLACK))
|
}
|
||||||
.rounding(Rounding::same(2.0))
|
let port = match ports.iter().find(|p| &p.id() == conn) {
|
||||||
.begin(ui);
|
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) {
|
let port_name = self
|
||||||
Some(p) => p,
|
.midi_in
|
||||||
None => continue,
|
.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) {
|
fn display_midi_message(idx: usize, msg: &Message, ui: &mut egui::Ui, raw: bool) {
|
||||||
match msg {
|
match &msg.parsed {
|
||||||
Message::Voice(vm) => {
|
ParsedMessage::Voice(vm) => {
|
||||||
Grid::new(format!("message_grid_{idx}")).show(ui, |ui| {
|
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 {
|
let (name, fields) = match vm.category {
|
||||||
VoiceCategory::NoteOff { note, velocity } => (
|
VoiceCategory::NoteOff { note, velocity } => (
|
||||||
"NoteOff",
|
"NoteOff",
|
||||||
|
@ -280,16 +289,36 @@ fn display_midi_message(idx: usize, msg: &Message, ui: &mut egui::Ui) {
|
||||||
VoiceCategory::Unknown => ("Unknown", vec![]),
|
VoiceCategory::Unknown => ("Unknown", vec![]),
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.label(name);
|
let voice_label = format!("Voice:{}", name);
|
||||||
ui.end_row();
|
|
||||||
|
ui.label(RichText::new(voice_label).italics());
|
||||||
|
ui.label(format!("channel = {}", vm.channel));
|
||||||
|
|
||||||
for (name, value) in fields {
|
for (name, value) in fields {
|
||||||
ui.label(format!("{name} = {value}"));
|
ui.label(format!("{name} = {value}"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Message::System(_system_common) => {}
|
ParsedMessage::System(_system_common) => {}
|
||||||
Message::Realtime(_system_realtime) => {}
|
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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue