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 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
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
pub mod log;
|
||||
pub mod midi;
|
||||
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)]
|
||||
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)]
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue