can parse entire wind synth file
This commit is contained in:
parent
a3daeddd6d
commit
36fafbf144
6 changed files with 187 additions and 18 deletions
BIN
assets/windsynth.log
Normal file
BIN
assets/windsynth.log
Normal file
Binary file not shown.
|
@ -1,13 +1,17 @@
|
||||||
use std::io::stdin;
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{stdin, Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
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};
|
use midir::{MidiInput, MidiInputPort};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut enigo = Enigo::new(&Settings::default()).unwrap();
|
//let mut enigo = Enigo::new(&Settings::default()).unwrap();
|
||||||
enigo.text("echo \"hello world\"").unwrap();
|
//enigo.text("echo \"hello world\"").unwrap();
|
||||||
enigo.key(Key::Return, Direction::Press).unwrap();
|
//enigo.key(Key::Return, Direction::Press).unwrap();
|
||||||
|
|
||||||
match run() {
|
match run() {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
|
@ -20,10 +24,23 @@ fn main() {
|
||||||
fn run() -> Result<()> {
|
fn run() -> Result<()> {
|
||||||
let midi_in = MidiInput::new("keyboard")?;
|
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 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,
|
Ok(m) => m,
|
||||||
Err(err) => return Err(anyhow!("failed to connect to device: {}", err)),
|
Err(err) => return Err(anyhow!("failed to connect to device: {}", err)),
|
||||||
};
|
};
|
||||||
|
@ -35,9 +52,31 @@ fn run() -> Result<()> {
|
||||||
Ok(())
|
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);
|
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
|
/// Finds the first MIDI input port which corresponds to a physical MIDI device
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
|
pub mod log;
|
||||||
pub mod midi;
|
pub mod midi;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
|
|
51
src/log.rs
Normal file
51
src/log.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
Voice(VoiceMessage),
|
Voice(VoiceMessage),
|
||||||
|
@ -23,10 +21,10 @@ pub enum VoiceCategory {
|
||||||
NoteOff { note: u8, velocity: u8 },
|
NoteOff { note: u8, velocity: u8 },
|
||||||
NoteOn { note: u8, velocity: u8 },
|
NoteOn { note: u8, velocity: u8 },
|
||||||
AfterTouch,
|
AfterTouch,
|
||||||
ControlChange,
|
ControlChange { controller: u8, value: u8 },
|
||||||
ProgramChange,
|
ProgramChange { value: u8 },
|
||||||
ChannelPressure,
|
ChannelPressure,
|
||||||
PitchWheel,
|
PitchWheel { value: u16 },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use nom::{bytes::complete::take, IResult};
|
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> {
|
pub fn parse_message(bytes: &[u8]) -> IResult<&[u8], Message> {
|
||||||
let (bytes, status_byte) = take(1usize)(bytes)?;
|
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)?;
|
let (bytes, vm) = parse_voice_message(status_byte, bytes)?;
|
||||||
Ok((bytes, Message::Voice(vm)))
|
Ok((bytes, Message::Voice(vm)))
|
||||||
} else {
|
} 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> {
|
pub fn parse_voice_message(status_byte: u8, remainder: &[u8]) -> IResult<&[u8], VoiceMessage> {
|
||||||
let category_nibble = 0xf0 & status_byte;
|
let category_nibble = 0xf0 & status_byte;
|
||||||
let channel = 0x0f & status_byte;
|
let channel = 0x0f & status_byte;
|
||||||
|
|
||||||
|
println!("category_nibble = {:#x}", category_nibble);
|
||||||
let (remainder, category) = match category_nibble {
|
let (remainder, category) = match category_nibble {
|
||||||
0x80 => parse_voice_note(remainder, true)?,
|
0x80 => parse_voice_note(remainder, true)?,
|
||||||
0x90 => parse_voice_note(remainder, false)?,
|
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)))
|
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))
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::midi::VoiceMessage;
|
use crate::{log::load_raw_log, midi::VoiceMessage};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -62,4 +120,26 @@ mod tests {
|
||||||
assert_eq!(parsed, expected,);
|
assert_eq!(parsed, expected,);
|
||||||
assert_eq!(remainder.len(), 0);
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue