summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock88
-rw-r--r--Cargo.toml1
-rw-r--r--config.rhai8
-rw-r--r--src/main.rs343
4 files changed, 324 insertions, 116 deletions
diff --git a/Cargo.lock b/Cargo.lock
index a448e66..5b86c97 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,6 +3,18 @@
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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -175,12 +187,32 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -481,6 +513,7 @@ dependencies = [
"cc",
"cpal",
"libc",
+ "rhai",
]
[[package]]
@@ -546,6 +579,32 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -585,6 +644,23 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -637,6 +713,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -648,6 +730,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
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<Note>,
}
@@ -24,67 +29,63 @@ static NOTE_INFO: Mutex<NoteInfo> = Mutex::new(NoteInfo {
pitch_bend: 0,
notes: vec![],
pedal_down: false,
+ presets: [0; CHANNEL_COUNT],
});
static SOUNDFONT: Mutex<Option<soundfont::SoundFont>> = Mutex::new(None);
-fn playmidi_main() -> Result<(), String> {
+fn get_midi_device(idx: i32) -> Result<midi_input::Device, String> {
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::<usize>() {
- 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::<usize>() {
+ 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<cpal::Stream, String> {
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);
}
}
_ => {}