diff --git a/Cargo.toml b/Cargo.toml index 1291649..9660a52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,5 +5,7 @@ edition = "2021" [dependencies] anyhow = "1.0.86" +hex = "0.4.3" midir = "0.10.0" +nom = "7.1.3" thiserror = "1.0.63" diff --git a/src/main.rs b/src/bin/main.rs similarity index 78% rename from src/main.rs rename to src/bin/main.rs index a6f105b..1934176 100644 --- a/src/main.rs +++ b/src/bin/main.rs @@ -1,5 +1,4 @@ use std::io::stdin; -use std::io::Read; use anyhow::{anyhow, Result}; use midir::{MidiInput, MidiInputPort}; @@ -32,7 +31,8 @@ fn run() -> Result<()> { } fn handle_midi_event(timestamp: u64, message: &[u8], _extra_data: &mut ()) { - println!("{timestamp} > {message:?}") + let hex_msg = hex::encode(message); + println!("{timestamp} > {hex_msg}"); } /// Finds the first MIDI input port which corresponds to a physical MIDI device @@ -40,6 +40,11 @@ fn handle_midi_event(timestamp: u64, message: &[u8], _extra_data: &mut ()) { fn find_first_midi_device(midi_in: &MidiInput) -> Result<MidiInputPort> { let ports = midi_in.ports(); + // "Midi Through" is automatically created by the snd-seq-dummy kernel module. + // We can ignore it, since it's not a physical MIDI device, which is what we + // are looking for. + // + // Source: https://www.reddit.com/r/linuxaudio/comments/jsrl31/comment/gc16qwu/ let mut physical_ports = ports.iter().filter(|p| { midi_in .port_name(p) @@ -51,9 +56,3 @@ fn find_first_midi_device(midi_in: &MidiInput) -> Result<MidiInputPort> { .cloned() .ok_or(anyhow!("no physical MIDI devices found")) } - -// "Midi Through" is automatically created bya the snd-seq-dummy kernel module. -// We can ignore it, since it's not a physical MIDI device, which is what we -// are looking for. -// -// Source: https://www.reddit.com/r/linuxaudio/comments/jsrl31/comment/gc16qwu/ diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..66200c2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +pub mod midi; +pub mod parser; diff --git a/src/midi.rs b/src/midi.rs new file mode 100644 index 0000000..233d310 --- /dev/null +++ b/src/midi.rs @@ -0,0 +1,33 @@ + + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum Message { + Voice(VoiceMessage), + System(SystemMessage), +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct VoiceMessage { + pub category: VoiceCategory, + pub channel: u8, +} + +impl VoiceMessage { + pub fn new(category: VoiceCategory, channel: u8) -> VoiceMessage { + VoiceMessage { category, channel } + } +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum VoiceCategory { + NoteOff { note: u8, velocity: u8 }, + NoteOn { note: u8, velocity: u8 }, + AfterTouch, + ControlChange, + ProgramChange, + ChannelPressure, + PitchWheel, +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum SystemMessage {} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..7cfebe4 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,65 @@ +use nom::{bytes::complete::take, IResult}; + +use crate::midi::{Message, VoiceCategory, VoiceMessage}; + +pub fn parse_message(bytes: &[u8]) -> IResult<&[u8], Message> { + let (bytes, status_byte) = take(1usize)(bytes)?; + let status_byte = status_byte[0]; + + if status_byte < 0xF0 { + let (bytes, vm) = parse_voice_message(status_byte, bytes)?; + Ok((bytes, Message::Voice(vm))) + } else { + todo!() + } +} + +pub fn parse_voice_message(status_byte: u8, remainder: &[u8]) -> IResult<&[u8], VoiceMessage> { + let category_nibble = 0xf0 & status_byte; + let channel = 0x0f & status_byte; + + let (remainder, category) = match category_nibble { + 0x80 => parse_voice_note(remainder, true)?, + 0x90 => parse_voice_note(remainder, false)?, + _ => todo!(), + }; + + Ok((remainder, VoiceMessage::new(category, channel))) +} + +pub fn parse_voice_note(bytes: &[u8], off: bool) -> IResult<&[u8], VoiceCategory> { + let (remainder, data) = take(2usize)(bytes)?; + + let note = data[0]; + let velocity = data[1]; + + let category = if velocity == 0 || off { + VoiceCategory::NoteOff { note, velocity } + } else { + VoiceCategory::NoteOn { note, velocity } + }; + + Ok((remainder, category)) +} + +#[cfg(test)] +mod tests { + use crate::midi::VoiceMessage; + + use super::*; + + #[test] + fn parses_note_on_message() { + let msg = [0x90, 0x24, 0x51]; + let (remainder, parsed) = parse_message(&msg).unwrap(); + + let note = 0x24; + let velocity = 0x51; + let expected = Message::Voice(VoiceMessage::new( + VoiceCategory::NoteOn { note, velocity }, + 0, + )); + assert_eq!(parsed, expected,); + assert_eq!(remainder.len(), 0); + } +}