can parse entire wind synth file

This commit is contained in:
Nicole Tietz-Sokolskaya 2024-11-10 16:15:01 -05:00
parent a3daeddd6d
commit 36fafbf144
6 changed files with 187 additions and 18 deletions

BIN
assets/windsynth.log Normal file

Binary file not shown.

View file

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

View file

@ -1,2 +1,3 @@
pub mod log;
pub mod midi;
pub mod parser;

51
src/log.rs Normal file
View file

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

View file

@ -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)]

View file

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