diff --git a/assets/windsynth.log b/assets/windsynth.log new file mode 100644 index 0000000..2df753f Binary files /dev/null and b/assets/windsynth.log differ 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 + ); + } + } }