first parsing of MIDI: parse note on/off

This commit is contained in:
Nicole Tietz-Sokolskaya 2024-09-09 10:18:50 -04:00
parent 56234e4e07
commit 7983bdab0b
5 changed files with 109 additions and 8 deletions

View file

@ -5,5 +5,7 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.86" anyhow = "1.0.86"
hex = "0.4.3"
midir = "0.10.0" midir = "0.10.0"
nom = "7.1.3"
thiserror = "1.0.63" thiserror = "1.0.63"

View file

@ -1,5 +1,4 @@
use std::io::stdin; use std::io::stdin;
use std::io::Read;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use midir::{MidiInput, MidiInputPort}; use midir::{MidiInput, MidiInputPort};
@ -32,7 +31,8 @@ fn run() -> Result<()> {
} }
fn handle_midi_event(timestamp: u64, message: &[u8], _extra_data: &mut ()) { 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 /// 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> { fn find_first_midi_device(midi_in: &MidiInput) -> Result<MidiInputPort> {
let ports = midi_in.ports(); 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| { let mut physical_ports = ports.iter().filter(|p| {
midi_in midi_in
.port_name(p) .port_name(p)
@ -51,9 +56,3 @@ fn find_first_midi_device(midi_in: &MidiInput) -> Result<MidiInputPort> {
.cloned() .cloned()
.ok_or(anyhow!("no physical MIDI devices found")) .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/

2
src/lib.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod midi;
pub mod parser;

33
src/midi.rs Normal file
View file

@ -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 {}

65
src/parser.rs Normal file
View file

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