From 36fafbf1441de7c614b8f21a874986ae3ca66803 Mon Sep 17 00:00:00 2001 From: Nicole Tietz-Sokolskaya <me@ntietz.com> Date: Sun, 10 Nov 2024 16:15:01 -0500 Subject: [PATCH] can parse entire wind synth file --- assets/windsynth.log | Bin 0 -> 23935 bytes src/bin/main.rs | 57 +++++++++++++++++++++++----- src/lib.rs | 1 + src/log.rs | 51 +++++++++++++++++++++++++ src/midi.rs | 8 ++-- src/parser.rs | 88 +++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 187 insertions(+), 18 deletions(-) create mode 100644 assets/windsynth.log create mode 100644 src/log.rs diff --git a/assets/windsynth.log b/assets/windsynth.log new file mode 100644 index 0000000000000000000000000000000000000000..2df753f19803900024a96c6781393f23695abd31 GIT binary patch literal 23935 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 EAD&iKj{pDw literal 0 HcmV?d00001 diff --git a/src/bin/main.rs b/src/bin/main.rs index 7d67708..a90e2b4 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -1,13 +1,17 @@ -use std::io::stdin; +use std::{ + fs::File, + io::{stdin, Read, Write}, +}; use anyhow::{anyhow, Result}; -use enigo::{Direction, Enigo, Key, Keyboard, Settings}; +use midi_keys::{log::load_raw_log, parser::parse_message}; +//use enigo::{Direction, Enigo, Key, Keyboard, Settings}; use midir::{MidiInput, MidiInputPort}; fn main() { - let mut enigo = Enigo::new(&Settings::default()).unwrap(); - enigo.text("echo \"hello world\"").unwrap(); - enigo.key(Key::Return, Direction::Press).unwrap(); + //let mut enigo = Enigo::new(&Settings::default()).unwrap(); + //enigo.text("echo \"hello world\"").unwrap(); + //enigo.key(Key::Return, Direction::Press).unwrap(); match run() { Ok(_) => {} @@ -20,10 +24,23 @@ fn main() { fn run() -> Result<()> { let midi_in = MidiInput::new("keyboard")?; - let midi_device = find_first_midi_device(&midi_in)?; + let midi_device = match find_first_midi_device(&midi_in) { + Ok(m) => m, + Err(e) => { + println!("error: {}", e); + return replay_file("assets/windsynth.log"); + } + }; let port_name = midi_in.port_name(&midi_device)?; - let _midi_connection = match midi_in.connect(&midi_device, &port_name, handle_midi_event, ()) { + let output_file = File::options().append(true).create(true).open("midi.log")?; + + let _midi_connection = match midi_in.connect( + &midi_device, + &port_name, + handle_midi_event, + Some(output_file), + ) { Ok(m) => m, Err(err) => return Err(anyhow!("failed to connect to device: {}", err)), }; @@ -35,9 +52,31 @@ fn run() -> Result<()> { Ok(()) } -fn handle_midi_event(timestamp: u64, message: &[u8], _extra_data: &mut ()) { +fn replay_file(filename: &str) -> Result<()> { + let entries = load_raw_log(filename)?; + for entry in entries { + handle_midi_event(entry.ts, &entry.message, &mut None); + } + Ok(()) +} + +fn handle_midi_event(timestamp: u64, message: &[u8], file: &mut Option<File>) { + if let Some(file) = file { + let ts_buf = timestamp.to_be_bytes(); + let len_buf = message.len().to_be_bytes(); + + file.write(&ts_buf).unwrap(); + file.write(&len_buf).unwrap(); + file.write(message).unwrap(); + } + let hex_msg = hex::encode(message); - println!("{timestamp} > {hex_msg}"); + let msg = match parse_message(message) { + Ok((_n, msg)) => format!("{:?}", msg), + Err(_) => "failed to parse message".to_string(), + }; + + println!("{timestamp} > {hex_msg} - {msg}"); } /// Finds the first MIDI input port which corresponds to a physical MIDI device diff --git a/src/lib.rs b/src/lib.rs index 66200c2..535cd1e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,3 @@ +pub mod log; pub mod midi; pub mod parser; diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..fea66b2 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,51 @@ +use anyhow::{anyhow, Result}; +use std::{fs::File, io::Read}; + +pub struct LogEntry { + pub ts: u64, + pub message: Vec<u8>, +} + +pub fn load_raw_log(filename: &str) -> Result<Vec<LogEntry>> { + let mut file = File::options() + .append(true) + .create(true) + .read(true) + .open(filename)?; + + let mut ts_buf: [u8; 8] = [0_u8; 8]; + let mut len_buf: [u8; 8] = [0_u8; 8]; + + let mut entries = vec![]; + + loop { + match file.read(&mut ts_buf) { + Ok(0) => return Ok(entries), + Ok(8) => {} + Ok(n) => return Err(anyhow!("got {} of expected 8 bytes for timestamp", n)), + Err(e) => return Err(e.into()), + }; + file.read_exact(&mut len_buf)?; + + let ts = u64::from_be_bytes(ts_buf); + let len = usize::from_be_bytes(len_buf); + + let mut message = vec![0_u8; len]; + file.read_exact(message.as_mut_slice())?; + + entries.push(LogEntry { ts, message }); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + pub fn reads_entries_from_windsynth_log() { + let filename = "assets/windsynth.log"; + let entries = load_raw_log(filename).unwrap(); + + assert_eq!(1260, entries.len()); + } +} diff --git a/src/midi.rs b/src/midi.rs index 233d310..d7a0e9e 100644 --- a/src/midi.rs +++ b/src/midi.rs @@ -1,5 +1,3 @@ - - #[derive(PartialEq, Eq, Debug, Clone)] pub enum Message { Voice(VoiceMessage), @@ -23,10 +21,10 @@ pub enum VoiceCategory { NoteOff { note: u8, velocity: u8 }, NoteOn { note: u8, velocity: u8 }, AfterTouch, - ControlChange, - ProgramChange, + ControlChange { controller: u8, value: u8 }, + ProgramChange { value: u8 }, ChannelPressure, - PitchWheel, + PitchWheel { value: u16 }, } #[derive(PartialEq, Eq, Debug, Clone)] diff --git a/src/parser.rs b/src/parser.rs index 7cfebe4..c7f040a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,6 +1,6 @@ use nom::{bytes::complete::take, IResult}; -use crate::midi::{Message, VoiceCategory, VoiceMessage}; +use crate::midi::{Message, SystemMessage, VoiceCategory, VoiceMessage}; pub fn parse_message(bytes: &[u8]) -> IResult<&[u8], Message> { let (bytes, status_byte) = take(1usize)(bytes)?; @@ -10,18 +10,35 @@ pub fn parse_message(bytes: &[u8]) -> IResult<&[u8], Message> { let (bytes, vm) = parse_voice_message(status_byte, bytes)?; Ok((bytes, Message::Voice(vm))) } else { - todo!() + let (bytes, sm) = parse_status_message(status_byte, bytes)?; + Ok((bytes, Message::System(sm))) } } +fn parse_status_message(_status_byte: u8, bytes: &[u8]) -> IResult<&[u8], SystemMessage> { + return Err(nom::Err::Error(nom::error::Error { + input: bytes, + code: nom::error::ErrorKind::Fail, + })); +} + pub fn parse_voice_message(status_byte: u8, remainder: &[u8]) -> IResult<&[u8], VoiceMessage> { let category_nibble = 0xf0 & status_byte; let channel = 0x0f & status_byte; + println!("category_nibble = {:#x}", category_nibble); let (remainder, category) = match category_nibble { 0x80 => parse_voice_note(remainder, true)?, 0x90 => parse_voice_note(remainder, false)?, - _ => todo!(), + 0xb0 => parse_control_change(remainder)?, + 0xc0 => parse_program_change(remainder)?, + 0xe0 => parse_pitch_wheel(remainder)?, + _ => { + return Err(nom::Err::Error(nom::error::Error { + input: remainder, + code: nom::error::ErrorKind::Fail, + })) + } }; Ok((remainder, VoiceMessage::new(category, channel))) @@ -42,9 +59,50 @@ pub fn parse_voice_note(bytes: &[u8], off: bool) -> IResult<&[u8], VoiceCategory Ok((remainder, category)) } +pub fn generic_error(bytes: &[u8]) -> nom::Err<nom::error::Error<&[u8]>> { + nom::Err::Error(nom::error::Error { + input: bytes, + code: nom::error::ErrorKind::Fail, + }) +} + +pub fn parse_pitch_wheel(bytes: &[u8]) -> IResult<&[u8], VoiceCategory> { + if bytes.len() < 2 { + return Err(generic_error(bytes)); + } + + let (db1, db2) = (bytes[0], bytes[1]); + let value = ((db1 as u16) << 7) | db2 as u16; + + Ok((&bytes[2..], VoiceCategory::PitchWheel { value })) +} + +pub fn parse_control_change(bytes: &[u8]) -> IResult<&[u8], VoiceCategory> { + if bytes.len() < 2 { + return Err(generic_error(bytes)); + } + + let controller = bytes[0]; + let value = bytes[1]; + + Ok(( + &bytes[2..], + VoiceCategory::ControlChange { controller, value }, + )) +} + +pub fn parse_program_change(bytes: &[u8]) -> IResult<&[u8], VoiceCategory> { + if bytes.len() < 1 { + return Err(generic_error(bytes)); + } + + let value = bytes[0]; + + Ok((&bytes[1..], VoiceCategory::ProgramChange { value })) +} #[cfg(test)] mod tests { - use crate::midi::VoiceMessage; + use crate::{log::load_raw_log, midi::VoiceMessage}; use super::*; @@ -62,4 +120,26 @@ mod tests { assert_eq!(parsed, expected,); assert_eq!(remainder.len(), 0); } + + #[test] + fn parse_log_from_windsynth() { + // I played a few notes on my Roland AE-20 and saved it to a log file. + // This test just reads that in and ensures that everything parses. It + // doesn't check for *correct* parsing, but this is sufficient for much + // of our needs. + + let filename = "assets/windsynth.log"; + let entries = load_raw_log(filename).unwrap(); + + assert_eq!(1260, entries.len()); + for entry in entries { + let parsed = parse_message(&entry.message); + assert!( + parsed.is_ok(), + "failed to parse message: {:?}; {:?}", + &entry.message, + parsed + ); + } + } }