diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Cargo.lock | 22 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | config.rhai | 19 | ||||
-rw-r--r-- | src/main.rs | 307 |
5 files changed, 264 insertions, 88 deletions
@@ -4,3 +4,5 @@ TAGS *~ *.out scratch +*.mid +*.wav @@ -554,17 +554,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] -name = "playmidi" -version = "0.0.0" -dependencies = [ - "cc", - "chrono", - "cpal", - "libc", - "rhai", -] - -[[package]] name = "proc-macro-crate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -585,6 +574,17 @@ dependencies = [ ] [[package]] +name = "progmidi" +version = "0.0.0" +dependencies = [ + "cc", + "chrono", + "cpal", + "libc", + "rhai", +] + +[[package]] name = "quote" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1,5 +1,5 @@ [package] -name = "playmidi" +name = "progmidi" version = "0.0.0" edition = "2021" diff --git a/config.rhai b/config.rhai index 513e34f..07c4bb3 100644 --- a/config.rhai +++ b/config.rhai @@ -1,10 +1,17 @@ const PM_DEVICE_ID = 0; fn pm_note_played(channel, note, vel) { + print(channel); + if channel <= 4 { + channel = 0; + } pm_play_note(channel, note, vel); } fn pm_note_released(channel, note, vel) { + if channel <= 4 { + channel = 0; + } pm_release_note(channel, note); } @@ -26,10 +33,16 @@ fn pm_control_changed(channel, controller, value) { } print("setting metronome to " + bpm); pm_set_metronome(60, bpm, 1.0); + } else if controller == 51 { + if value == 127 { + pm_start_midi_recording(); + } else { + pm_stop_midi_recording(); + } } } pm_load_soundfont("/etc/alternatives/default-GM.sf3"); -pm_load_preset(0, 299); -pm_load_preset(9, 102); -pm_load_preset(16, 102); +pm_load_preset(-1, 299); // default = piano +pm_load_preset(9, 102); // drum pad +pm_load_preset(16, 102); // metronome diff --git a/src/main.rs b/src/main.rs index 2a0e8e8..c1b59b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,10 +6,12 @@ mod midi_input; mod soundfont; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -// use std::fs::File; +use std::fs::File; use soundfont::SoundFont; use std::io::Write; -use std::sync::Mutex; +use std::sync::{Mutex, MutexGuard}; +use std::time::{Instant, Duration}; +use std::ffi::c_int; const NOTE_FALLOFF: f32 = 0.1; // falloff when note is released const CHANNEL_COUNT: usize = 17; // 16 MIDI channels + metronome @@ -29,13 +31,14 @@ struct Metronome { volume: f32, } -// struct MidiRecording { -// file: File, -// last_event_time: std::time::Instant, -// } +struct MidiRecording { + data: Vec<u8>, + last_event_time: Instant, +} + struct NoteInfo { - // midi_out: Option<MidiRecording>, + midi_recording: Option<MidiRecording>, pitch_bend: i32, // in cents pedal_down: bool, presets: [usize; CHANNEL_COUNT], @@ -46,7 +49,7 @@ struct NoteInfo { } static NOTE_INFO: Mutex<NoteInfo> = Mutex::new(NoteInfo { - // midi_out: None, + midi_recording: None, pitch_bend: 0, // this is ridiculous. notes: [ @@ -81,23 +84,80 @@ static NOTE_INFO: Mutex<NoteInfo> = Mutex::new(NoteInfo { static SOUNDFONT: Mutex<Option<SoundFont>> = Mutex::new(None); -// fn write_u16_be(file: &mut File, n: u16) -> std::io::Result<()> { -// file.write(&mut u16::to_be_bytes(n))?; -// Ok(()) -// } +impl MidiRecording { + fn write(&mut self, byte: u8) { + self.data.push(byte); + } + fn write_vlq(&mut self, value: u32) { + if value < 0x80 { + self.write(value as u8); + } else if value < 0x4000 { + self.write((0x80 | value >> 7) as u8); + self.write((value & 0x7f) as u8); + } else if value < 0x200000 { + self.write((0x80 | value >> 14) as u8); + self.write((0x80 | value >> 7) as u8); + self.write((value & 0x7f) as u8); + } else { + self.write((0x80 | value >> 21) as u8); + self.write((0x80 | value >> 14) as u8); + self.write((0x80 | value >> 7) as u8); + self.write((value & 0x7f) as u8); + } + } + + fn write_event(&mut self, status: u8, data1: Option<u8>, data2: Option<u8>) { + // check data + if let Some(b) = data1 { + if b >= 128 { + return; + } + } + if let Some(b) = data2 { + if b >= 128 { + return; + } + } + + let now = Instant::now(); + let ms_elapsed = now.duration_since(self.last_event_time).as_millis(); + self.write_vlq(ms_elapsed as u32); + self.write(status); + if let Some(b) = data1 { + self.write(b); + if let Some(b2) = data2 { + self.write(b2); + } + } + self.last_event_time = now; + } +} + +fn flush_stdout() { + if std::io::stdout().flush().is_err() { + //who cares + } +} -fn lock_note_info<'a>() -> std::sync::MutexGuard<'a, NoteInfo> { +fn write_u16_be(file: &mut File, n: u16) -> std::io::Result<usize> { + file.write(&mut u16::to_be_bytes(n)) +} +fn write_u32_be(file: &mut File, n: u32) -> std::io::Result<usize> { + file.write(&mut u32::to_be_bytes(n)) +} + +fn lock_note_info<'a>() -> MutexGuard<'a, NoteInfo> { NOTE_INFO.lock().expect("couldn't lock notes") } -fn lock_note_info_and_soundfont<'a>() -> ( - std::sync::MutexGuard<'a, NoteInfo>, - std::sync::MutexGuard<'a, Option<SoundFont>>, -) { - ( - lock_note_info(), - SOUNDFONT.lock().expect("couldn't lock soundfont"), - ) +fn lock_soundfont<'a>() -> MutexGuard<'a, Option<SoundFont>> { + SOUNDFONT.lock().expect("couldn't lock soundfont") +} + +fn lock_note_info_and_soundfont<'a>() -> (MutexGuard<'a, NoteInfo>, MutexGuard<'a, Option<SoundFont>>) { + let note_info = lock_note_info(); + let soundfont = lock_soundfont(); + (note_info, soundfont) } fn get_midi_device(idx: i32) -> Result<midi_input::Device, String> { @@ -123,9 +183,7 @@ fn get_midi_device(idx: i32) -> Result<midi_input::Device, String> { println!(" -----------------"); } print!("Select a device (default {}): ", devices.default + 1); - if std::io::stdout().flush().is_err() { - //who cares - } + flush_stdout(); let mut buf = String::new(); std::io::stdin() @@ -229,6 +287,13 @@ fn play_note(channel: i64, note: i64, vel: i64) { } let (mut note_info, mut maybe_sf) = lock_note_info_and_soundfont(); + + if channel < 16 { + if let Some(recording) = note_info.midi_recording.as_mut() { + // note on event + recording.write_event(0b1001_0000 | (channel as u8), Some(note), Some(vel)); + } + } note_info.notes[channel].retain(|n| n.key != note); let preset = note_info.presets[channel]; if let Some(sf) = maybe_sf.as_mut() { @@ -253,7 +318,15 @@ fn release_note(channel: i64, note: i64) { } let note = note as u8; - let mut note_info = NOTE_INFO.lock().expect("couldn't lock notes"); + let mut note_info = lock_note_info(); + + if channel < 16 { + if let Some(recording) = note_info.midi_recording.as_mut() { + // note off event + recording.write_event(0b1000_0000 | (channel as u8), Some(note), Some(0)); + } + } + let pedal_down = note_info.pedal_down; let notes = &mut note_info.notes[channel]; if let Some(n) = notes.iter_mut().find(|n| n.key == note && n.down) { @@ -265,7 +338,7 @@ fn release_note(channel: i64, note: i64) { } fn set_pedal_down(down: bool) { - let mut note_info = NOTE_INFO.lock().expect("couldn't lock notes"); + let mut note_info = lock_note_info(); note_info.pedal_down = down; for channel in 0..CHANNEL_COUNT { let notes = &mut note_info.notes[channel]; @@ -287,7 +360,7 @@ fn set_pedal_down(down: bool) { fn set_pitch_bend(amount: f64) { let amount = amount as i32; - let mut note_info = NOTE_INFO.lock().expect("couldn't lock notes"); + let mut note_info = lock_note_info(); note_info.pitch_bend = amount; } @@ -296,7 +369,7 @@ fn load_soundfont(filename: &str) { for i in 0..sf.preset_count() { println!("{}. {}", i, sf.preset_name(i).unwrap()); } - let mut sflock = SOUNDFONT.lock().expect("couldn't lock soundfont."); + let mut sflock = lock_soundfont(); *sflock = Some(sf); } else { eprintln!("Couldn't open soundfont: {}", filename); @@ -305,17 +378,24 @@ fn load_soundfont(filename: &str) { fn load_preset(channel: i64, preset: i64) { let preset = preset as usize; - let channel = channel as usize; - if !check_channel(channel) { - return; + + let (mut note_info, mut soundfont) = lock_note_info_and_soundfont(); + if channel == -1 { + for p in note_info.presets.iter_mut() { + *p = preset; + } + } else { + let channel = channel as usize; + if !check_channel(channel) { + return; + } + note_info.presets[channel] = preset; } - let mut note_info = NOTE_INFO.lock().expect("couldn't lock notes"); - let mut soundfont = SOUNDFONT.lock().expect("couldn't lock soundfont."); + println!("{}",note_info.presets[0]); if let Some(sf) = soundfont.as_mut() { if preset >= sf.preset_count() { eprintln!("preset {} out of range", preset); } else { - note_info.presets[channel] = preset; if let Err(e) = sf.load_samples_for_preset(preset) { eprintln!("error loading preset {}: {}", preset, e); } @@ -325,7 +405,7 @@ fn load_preset(channel: i64, preset: i64) { fn set_volume(channel: i64, volume: f64) { let volume = if volume.is_nan() { 0.0 } else { volume as f32 }; - let mut note_info = NOTE_INFO.lock().expect("couldn't lock notes"); + let mut note_info = lock_note_info(); if channel == -1 { note_info.master_volume = volume as f32; return; @@ -346,13 +426,62 @@ fn set_metronome(key: i64, bpm: f64, volume: f64) { } } -// fn start_midi_recording() { -// let name = chrono::Local::now().format("%Y-%m-%d-%H-%M-%S.mid").to_string(); -// let file = File::create(name); -// let note_info = lock_notes(); -// -// } -// +fn start_midi_recording_err() -> std::io::Result<()> { + let mut note_info = lock_note_info(); + note_info.midi_recording = Some(MidiRecording { + data: vec![], + last_event_time: Instant::now(), + }); + Ok(()) +} + +fn start_midi_recording() { + if let Err(e) = start_midi_recording_err() { + eprintln!("Error starting MIDI recording: {}", e); + } +} + +fn stop_midi_recording_err() -> std::io::Result<()> { + let mut note_info = lock_note_info(); + if let Some(mut recording) = note_info.midi_recording.take() { + drop(note_info); + print!("Saving MIDI recording..."); + flush_stdout(); + // MIDI "track end" + // (people complain if we don't include this) + recording.write(0); + recording.write(0xff); + recording.write(0x2f); + recording.write(0x00); + + let name = chrono::Local::now().format("%Y-%m-%d-%H-%M-%S.mid").to_string(); + let mut file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&name)?; + file.write("MThd".as_bytes())?; + write_u32_be(&mut file, 6)?; // header size + write_u16_be(&mut file, 1)?; // <format> = 1 + write_u16_be(&mut file, 1)?; // <ntrks> = 1 + // tick = 1ms, quarter note = 600 ticks => 100bpm + write_u16_be(&mut file, 600)?; + file.write("MTrk".as_bytes())?; + + write_u32_be(&mut file, recording.data.len() as u32)?; // track size + file.write(&recording.data)?; + file.sync_all()?; + drop(file); + println!("\rMIDI recording saved to {}", name); + } + Ok(()) +} + +fn stop_midi_recording() { + if let Err(e) = stop_midi_recording_err() { + eprintln!("Error stopping MIDI recording: {}", e); + } +} + fn call_fn_if_exists( engine: &rhai::Engine, ast: &rhai::AST, @@ -371,9 +500,25 @@ fn call_fn_if_exists( } } -// @TODO change this to -> () and handle errors -fn playmidi_main() -> Result<(), String> { + +const SIGINT: c_int = 2; + +type SigHandler = extern "C" fn (c_int); + +extern "C" { + fn signal(signum: c_int, handler: SigHandler) -> SigHandler; +} + +extern "C" fn sig_handler(_signum: c_int) { + stop_midi_recording(); + std::process::exit(0); +} + +fn main() { + unsafe { signal(SIGINT, sig_handler) }; + let mut engine = rhai::Engine::new(); + engine.set_max_expr_depths(0, 0); engine.register_fn("pm_load_soundfont", load_soundfont); engine.register_fn("pm_load_preset", load_preset); engine.register_fn("pm_play_note", play_note); @@ -386,14 +531,32 @@ fn playmidi_main() -> Result<(), String> { engine.register_fn("pm_set_metronome", |key: i64, bpm: i64, volume: f64| { set_metronome(key, bpm as f64, volume); }); - // engine.register_fn("pm_start_midi_recording", start_midi_recording); + engine.register_fn("pm_start_midi_recording", start_midi_recording); + engine.register_fn("pm_stop_midi_recording", stop_midi_recording); let engine = engine; // de-multablify - let mut ast = engine - .compile_file("config.rhai".into()) - .map_err(|e| format!("{}", e))?; - engine.run_ast(&ast).map_err(|e| format!("{}", e))?; - + let args: Vec<String> = std::env::args().collect(); + let config_filename = match args.len() { + 0 | 1 => "config.rhai", + 2 => &args[1], + _ => { + eprintln!("Usage: {} [config file]", args[0]); + return; + } + }; + let mut ast = match engine.compile_file(config_filename.into()) { + Ok(x) => x, + Err(e) => { + eprintln!("Config error: {}", e); + return; + }, + }; + + if let Err(e) = engine.run_ast(&ast) { + eprintln!("Error running top-level statements in {}: {}", config_filename, e); + return; + } + let mut midi_device; { let mut idx = -1; @@ -408,26 +571,32 @@ fn playmidi_main() -> Result<(), String> { } } } - midi_device = get_midi_device(idx)?; + midi_device = match get_midi_device(idx) { + Ok(dev) => dev, + Err(e) => { + eprintln!("Error loading MIDI device: {}", e); + return; + } + }; } // without this, top-level statements will be executed each time a function is called ast.clear_statements(); let ast = ast; // de-mutablify + let stream = match get_audio_stream() { + Ok(s) => s, + Err(e) => { + eprintln!("Error loading audio stream: {}", e); + return; + } + }; + if let Err(e) = stream.play() { + eprintln!("Error starting audio stream: {}", e); + return; + } - // load_soundfont("/etc/alternatives/default-GM.sf3"); - // { - // use std::time::Instant; - // let now = Instant::now(); - // sf.load_samples_for_preset(preset).expect("oh no"); - // println!("Loaded in {:?}", now.elapsed()); - // } - - let stream = get_audio_stream()?; - stream.play().map_err(|e| format!("{}", e))?; - - let mut last_metronome_tick = std::time::Instant::now(); + let mut last_metronome_tick = Instant::now(); while midi_device.is_connected() { let metronome_time = last_metronome_tick.elapsed().as_secs_f32(); @@ -443,7 +612,7 @@ fn playmidi_main() -> Result<(), String> { metronome.key, (metronome.volume * 127.0) as i64, ); - last_metronome_tick = std::time::Instant::now(); + last_metronome_tick = Instant::now(); } while let Some(event) = midi_device.read_event() { @@ -484,14 +653,6 @@ fn playmidi_main() -> Result<(), String> { eprintln!("Error: {}", err); midi_device.clear_error(); } - std::thread::sleep(std::time::Duration::from_millis(1)); - } - - Ok(()) -} - -fn main() { - if let Err(e) = playmidi_main() { - eprintln!("Error: {:?}", e); + std::thread::sleep(Duration::from_millis(1)); } } |