From 87b3ef15b80b5420c8f65baa848a100deb6e2dda Mon Sep 17 00:00:00 2001 From: pommicket Date: Wed, 5 Oct 2022 21:20:34 -0400 Subject: start rhai --- Cargo.lock | 88 ++++++++++++++++ Cargo.toml | 1 + config.rhai | 8 ++ src/main.rs | 343 ++++++++++++++++++++++++++++++++++++++++-------------------- 4 files changed, 324 insertions(+), 116 deletions(-) create mode 100644 config.rhai diff --git a/Cargo.lock b/Cargo.lock index a448e66..5b86c97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e6e951cfbb2db8de1828d49073a113a29fd7117b1596caa781a258c7e38d72" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "alsa" version = "0.6.0" @@ -174,12 +186,32 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "glob" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "jni" version = "0.19.0" @@ -481,6 +513,7 @@ dependencies = [ "cc", "cpal", "libc", + "rhai", ] [[package]] @@ -545,6 +578,32 @@ version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +[[package]] +name = "rhai" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eec3a3db30f591ece18c66b3db4c9fa26f3bce20bc821c50550968361f84333" +dependencies = [ + "ahash", + "bitflags", + "instant", + "num-traits", + "rhai_codegen", + "smallvec", + "smartstring", +] + +[[package]] +name = "rhai_codegen" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36791b0b801159db25130fd46ac726d2751c070260bba3a4a0a3eeb6231bb82a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -584,6 +643,23 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stdweb" version = "0.1.3" @@ -636,6 +712,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "walkdir" version = "2.3.2" @@ -647,6 +729,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.83" diff --git a/Cargo.toml b/Cargo.toml index b9989e5..11c2457 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,4 @@ cc = "1.0" [dependencies] cpal = "0.14" libc = "0.2" +rhai = "1.10" diff --git a/config.rhai b/config.rhai new file mode 100644 index 0000000..3b86af7 --- /dev/null +++ b/config.rhai @@ -0,0 +1,8 @@ +const PM_DEVICE_ID = 0; + +fn pm_note_played(channel, note, vel) { + pm_play_note(0, note, vel); +} + +pm_load_soundfont("/etc/alternatives/default-GM.sf3"); +pm_load_preset(0, 299); diff --git a/src/main.rs b/src/main.rs index bce1b12..4c5ebd5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ extern crate cpal; +extern crate rhai; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use std::io::Write; @@ -7,6 +8,9 @@ use std::sync::Mutex; mod midi_input; mod soundfont; +const NOTE_FALLOFF: f32 = 0.1; // falloff when note is released +const CHANNEL_COUNT: usize = 16; + struct Note { key: u8, req: soundfont::SamplesRequest, @@ -15,8 +19,9 @@ struct Note { } struct NoteInfo { - pitch_bend: i16, // in cents + pitch_bend: i32, // in cents pedal_down: bool, + presets: [usize; CHANNEL_COUNT], notes: Vec, } @@ -24,67 +29,63 @@ static NOTE_INFO: Mutex = Mutex::new(NoteInfo { pitch_bend: 0, notes: vec![], pedal_down: false, + presets: [0; CHANNEL_COUNT], }); static SOUNDFONT: Mutex> = Mutex::new(None); -fn playmidi_main() -> Result<(), String> { +fn get_midi_device(idx: i32) -> Result { let mut device_mgr = midi_input::DeviceManager::new()?; device_mgr.set_quiet(true); let devices = device_mgr.list()?; - for (index, device) in (&devices).into_iter().enumerate() { - print!("{:3} | ", index + 1); - let mut first = true; - for line in device.name.lines() { - if !first { - print!(" | "); - } - println!("{}", line); - first = false; - } - println!(" -----------------"); - } - print!("Select a device (default {}): ", devices.default + 1); - if std::io::stdout().flush().is_err() { - //who cares - } - - let device_id; - { - let mut buf = String::new(); - std::io::stdin() - .read_line(&mut buf) - .expect("error reading stdin"); - let s = buf.trim(); - if s.is_empty() { - device_id = &devices[devices.default].id; - } else { - match s.parse::() { - Ok(idx) if idx >= 1 && idx <= devices.len() => { - device_id = &devices[idx - 1].id; + + let device_idx = match idx { + 0 => devices.default, + i if i >= 1 => { + (i - 1) as usize + }, + _ => { + // user selects device + for (index, device) in (&devices).into_iter().enumerate() { + print!("{:3} | ", index + 1); + let mut first = true; + for line in device.name.lines() { + if !first { + print!(" | "); + } + println!("{}", line); + first = false; } - _ => { - return Err(format!("Bad device ID: {}", s)); + println!(" -----------------"); + } + print!("Select a device (default {}): ", devices.default + 1); + if std::io::stdout().flush().is_err() { + //who cares + } + + let mut buf = String::new(); + std::io::stdin() + .read_line(&mut buf) + .expect("error reading stdin"); + let s = buf.trim(); + if s.is_empty() { + devices.default + } else { + match s.parse::() { + Ok(idx) if idx >= 1 && idx <= devices.len() => { + idx - 1 + } + _ => { + return Err(format!("Bad device ID: {}", s)); + } } } } - } - let mut midi_device = device_mgr - .open(device_id) - .expect("error opening MIDI device"); - - let mut sf = soundfont::SoundFont::open("/etc/alternatives/default-GM.sf3")?; - - for i in 0..sf.preset_count() { - println!("{}. {}", i, sf.preset_name(i).unwrap()); - } - - //sf._debug_preset_zones(125); - //sf._debug_instrument_zones(148); + }; + let device_id = &devices[device_idx].id; + Ok(device_mgr.open(device_id)?) +} - // let result = playmidi_main(); - // if let Err(s) = result { - // eprintln!("Error: {}", s); - // } +fn get_audio_stream() -> Result { let host = cpal::default_host(); let audio_device = host .default_output_device() @@ -108,19 +109,6 @@ fn playmidi_main() -> Result<(), String> { let supp_config: cpal::SupportedStreamConfig = chosen_config.with_max_sample_rate(); if supp_config.channels() != 2 {} let config = supp_config.into(); - let preset = 299; - - { - use std::time::Instant; - let now = Instant::now(); - sf.load_samples_for_preset(preset).expect("oh no"); - println!("Loaded in {:?}", now.elapsed()); - } - - { - let mut sflock = SOUNDFONT.lock().expect("couldn't lock soundfont."); - *sflock = Some(sf); - } let stream = audio_device .build_output_stream( @@ -146,69 +134,192 @@ fn playmidi_main() -> Result<(), String> { } }, move |err| { - println!("audio stream error: {}", err); + eprintln!("audio stream error: {}", err); }, - ) - .expect("couldn't build output stream"); - stream.play().expect("couldn't play stream"); + ).map_err(|e| format!("{}", e))?; + Ok(stream) +} + +#[must_use] +fn check_channel(channel: usize) -> bool { + if channel >= CHANNEL_COUNT { + eprintln!("channel {} out of range", channel); + false + } else { + true + } +} + +fn play_note(channel: i64, note: i64, vel: i64) { + let channel = channel as usize; + let note = note as u8; + let vel = vel as u8; + + if !check_channel(channel) { + return; + } + + let mut note_info = NOTE_INFO + .lock() + .expect("couldn't lock notes"); + let mut maybe_sf = SOUNDFONT.lock().expect("couldn't lock soundfont."); + note_info.notes.retain(|n| n.key != note); + let preset = note_info.presets[channel]; + if let Some(sf) = maybe_sf.as_mut() { + match sf.request(preset, note, vel) { + Ok(mut req) => { + req.set_volume(0.1); + note_info.notes.push(Note { + key: note, + req, + down: true, + kill: false, + }); + } + Err(e) => eprintln!("get samples error: {}", e), + } + } +} + +fn release_note(note: u8) { + let mut note_info = NOTE_INFO + .lock() + .expect("couldn't lock notes"); + let pedal_down = note_info.pedal_down; + if let Some(n) = note_info.notes.iter_mut().find(|n| n.key == note) { + n.down = false; + if !pedal_down { + n.req.set_falloff(NOTE_FALLOFF); + } + } +} - let note_falloff = 0.1; // falloff when note is released +fn set_pedal_down(down: bool) { + let mut note_info = NOTE_INFO + .lock() + .expect("couldn't lock notes"); + note_info.pedal_down = down; + if down { + // disable falloff for all notes + for note in note_info.notes.iter_mut() { + note.req.set_falloff(1.0); + } + } else { + // start falloff for all non-down notes + for note in note_info.notes.iter_mut() { + if !note.down { + note.req.set_falloff(NOTE_FALLOFF); + } + } + } +} + +fn set_pitch_bend(amount: i32) { + let mut note_info = NOTE_INFO + .lock() + .expect("couldn't lock notes"); + note_info.pitch_bend = amount; +} + +fn load_soundfont(filename: &str) { + if let Ok(sf) = soundfont::SoundFont::open(filename) { + for i in 0..sf.preset_count() { + println!("{}. {}", i, sf.preset_name(i).unwrap()); + } + let mut sflock = SOUNDFONT.lock().expect("couldn't lock soundfont."); + *sflock = Some(sf); + } else { + eprintln!("Couldn't open soundfont: {}", filename); + } +} + +fn load_preset(channel: i64, preset: i64) { + if !check_channel(channel as usize) { + return; + } + let preset = preset as usize; + let channel = channel as usize; + let mut note_info = NOTE_INFO.lock().expect("couldn't lock notes"); + let mut soundfont = SOUNDFONT.lock().expect("couldn't lock soundfont."); + 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); + } + } + } + +} + +fn call_fn_if_exists(engine: &rhai::Engine, ast: &rhai::AST, name: &str, args: impl rhai::FuncArgs) { + let mut scope = rhai::Scope::new(); + match engine.call_fn::<()>(&mut scope, &ast, name, args) { + Ok(_) => {}, + Err(e) => eprintln!("Warning: rhai error: {}", e), + } +} + +// @TODO change this to -> () and handle errors +fn playmidi_main() -> Result<(), String> { + let mut engine = rhai::Engine::new(); + engine.register_fn("pm_load_soundfont", load_soundfont); + engine.register_fn("pm_load_preset", load_preset); + engine.register_fn("pm_play_note", play_note); + 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 mut midi_device; + { + let mut idx = -1; + for (name, _, value) in ast.iter_literal_variables(true, true) { + if name == "PM_DEVICE_ID" { + match value.as_int() { + Ok(i) => match i.try_into() { + Ok(i) => idx = i, + Err(_) => eprintln!("PM_DEVICE_ID {} too large.", i), + }, + Err(t) => eprintln!("Warning: PM_DEVICE_ID should be integer, not {}.", t), + } + } + } + midi_device = get_midi_device(idx)?; + } + + // without this, top-level statements will be executed each time a function is called + ast.clear_statements(); + + let ast = ast; // de-mutablify + +// 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))?; + while midi_device.is_connected() { while let Some(event) = midi_device.read_event() { - let mut note_info = NOTE_INFO - .lock() - .map_err(|_| "couldn't lock notes".to_string())?; use midi_input::Event::*; match event { - NoteOn { note, vel, .. } => { - let mut maybe_sf = SOUNDFONT.lock().expect("couldn't lock soundfont."); - note_info.notes.retain(|n| n.key != note); - if let Some(sf) = maybe_sf.as_mut() { - match sf.request(preset, note, vel) { - Ok(mut req) => { - req.set_volume(0.1); - note_info.notes.push(Note { - key: note, - req, - down: true, - kill: false, - }); - } - Err(e) => eprintln!("get samples error: {}", e), - } - } - } - NoteOff { note, .. } => { - let pedal_down = note_info.pedal_down; - if let Some(n) = note_info.notes.iter_mut().find(|n| n.key == note) { - n.down = false; - if !pedal_down { - n.req.set_falloff(note_falloff); - } - } - } - PitchBend { amount, .. } => { - note_info.pitch_bend = amount / 128; - } + NoteOn { channel, note, vel } => + call_fn_if_exists(&engine, &ast, "pm_note_played", (channel as i64, note as i64, vel as i64)), + NoteOff { note, .. } => release_note(note), + PitchBend { amount, .. } => set_pitch_bend(amount as i32 / 128), ControlChange { controller, value, .. } => { if controller == 64 { // oddly, a value of 0 means "down" - note_info.pedal_down = value < 127; - if note_info.pedal_down { - // disable falloff for all notes - for note in note_info.notes.iter_mut() { - note.req.set_falloff(1.0); - } - } else { - // start falloff for all non-down notes - for note in note_info.notes.iter_mut() { - if !note.down { - note.req.set_falloff(note_falloff); - } - } - } + set_pedal_down(value < 127); } } _ => {} -- cgit v1.2.3