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