summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Cargo.lock22
-rw-r--r--Cargo.toml2
-rw-r--r--config.rhai19
-rw-r--r--src/main.rs307
5 files changed, 264 insertions, 88 deletions
diff --git a/.gitignore b/.gitignore
index ce8c7e3..2800a93 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@ TAGS
*~
*.out
scratch
+*.mid
+*.wav
diff --git a/Cargo.lock b/Cargo.lock
index ede03a6..551905d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 21b6607..65e2294 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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));
}
}