From 21e719c4d24d582ad5d1b4a027bb706148cc2a82 Mon Sep 17 00:00:00 2001 From: Kelvin Ly Date: Sat, 21 Dec 2019 13:55:52 -0500 Subject: [PATCH] Implement playback; I think it works! --- Cargo.lock | 47 ++++++++++++++++ Cargo.toml | 2 + src/main.rs | 154 +++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 194 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0138827..548f911 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,11 @@ dependencies = [ "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "autocfg" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "bitflags" version = "0.3.3" @@ -40,6 +45,16 @@ name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "chrono" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "core-foundation" version = "0.2.3" @@ -100,6 +115,8 @@ version = "0.1.0" dependencies = [ "midir 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "pancurses 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "timer 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -138,6 +155,23 @@ dependencies = [ "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "num-integer" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "pancurses" version = "0.16.1" @@ -179,6 +213,14 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "timer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "void" version = "1.0.2" @@ -233,10 +275,12 @@ dependencies = [ [metadata] "checksum alsa 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b4a0d4ebc8b23041c5de9bc9aee13b4bad844a589479701f31a5934cfe4aeb32" "checksum alsa-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b0edcbbf9ef68f15ae1b620f722180b82a98b6f0628d30baa6b8d2a5abc87d58" +"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" "checksum bitflags 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "32866f4d103c4e438b1db1158aa1b1a80ee078e5d77a59a2f906fd62a577389c" "checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" "checksum cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)" = "f52a465a666ca3d838ebbf08b241383421412fe7ebb463527bba275526d89f76" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +"checksum chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01" "checksum core-foundation 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "25bfd746d203017f7d5cbd31ee5d8e17f94b6521c7af77ece6c9e4b2d4b16c67" "checksum core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "065a5d7ffdcbc8fa145d6f0746f3555025b9097a9e9cda59f7467abae670c78d" "checksum coremidi 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ae2b8d4679b3a2a92b843d4c8e4a721be038ce49fce701c46296b2713f24c814" @@ -247,11 +291,14 @@ dependencies = [ "checksum midir 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e653b919aca8b5f2697854f1819b33bfe49bcb3378abb0dbd834ce43ede9c7b1" "checksum ncurses 5.99.0 (registry+https://github.com/rust-lang/crates.io-index)" = "15699bee2f37e9f8828c7b35b2bc70d13846db453f2d507713b758fabe536b82" "checksum nix 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a2c5afeb0198ec7be8569d666644b574345aad2e95a53baf3a532da3e0f3fb32" +"checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09" +"checksum num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c81ffc11c212fa327657cb19dd85eb7419e163b5b076bede2bdb5c974c07e4" "checksum pancurses 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d3058bc37c433096b2ac7afef1c5cdfae49ede0a4ffec3dfc1df1df0959d0ff0" "checksum pdcurses-sys 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "084dd22796ff60f1225d4eb6329f33afaf4c85419d51d440ab6b8c6f4529166b" "checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" "checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" +"checksum timer 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "31d42176308937165701f50638db1c31586f183f1aab416268216577aec7306b" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" diff --git a/Cargo.toml b/Cargo.toml index ddb9e70..f7b925e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,5 @@ edition = "2018" [dependencies] midir = "0.5" pancurses = "0.16" +time = "0.1" +timer = "0.2" diff --git a/src/main.rs b/src/main.rs index 51277fe..393a1d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,62 @@ extern crate midir; extern crate pancurses; +extern crate time; +extern crate timer; use std::error::Error; use std::thread; use std::time::{Duration, Instant}; use std::sync::{Arc, Mutex}; use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::mpsc::channel; use midir::os::unix::{VirtualInput, VirtualOutput}; use pancurses::{initscr, endwin, noecho, Input}; +#[derive(Clone, Copy, Debug)] +enum MidiMessage { + NoteOn(u8, u8, u8), + NoteOff(u8, u8, u8), + PitchBend(u8, i16), +} + +impl MidiMessage { + fn from_slice(s: &[u8]) -> Option { + if s.len() == 0 { + return None; + } + let channel = s[0] & 0x0f; + match s[0] & 0xf0 { + 0x80 => { + if s.len() == 3 { + Some(MidiMessage::NoteOff(channel, s[1], s[2])) + } else { + None + } + }, + 0x90 => { + if s.len() == 3 { + Some(MidiMessage::NoteOn(channel, s[1], s[2])) + } else { + None + } + }, + 0xE0 => { + if s.len() == 3 { + let lsb = s[2] as i16; + let msb = s[1] as i16; + Some(MidiMessage::PitchBend(channel, (msb << 8) | lsb)) + } else { + None + } + }, + _ => None + } + } +} + + #[derive(Clone, Copy, Debug)] enum LooperState { Idle, @@ -24,6 +70,7 @@ struct LooperInputCallback { outer_start: Arc>, ts_offset: u64, msg_buf: Arc)>>>, + log_msgs: Box) + Send>, } struct Looper { @@ -35,7 +82,10 @@ struct Looper { msg_buf: Arc)>>>, // for either playback or recording start_time: Arc>, - playback_thread: Option, + recording_length: u64, + playback_structs: Option, + playback_timer: timer::Timer, + playback_cb: Arc>, } impl Looper { @@ -43,7 +93,10 @@ impl Looper { const RECORDING: usize = 1; const PLAYBACK: usize = 2; - fn new() -> Result> { + fn new(mut record_cb: F, playback_cb: G) -> Result> + where F: 'static + FnMut(u64, MidiMessage) + Send, + G: 'static + FnMut(u64, MidiMessage) + Send + { let input = midir::MidiInput::new("midi looper")?; let output = midir::MidiOutput::new("midi looper")?; @@ -59,11 +112,11 @@ impl Looper { |ts, data, me| { { let mut out = me.output.lock().unwrap(); - out.send(data); + out.send(data).unwrap(); } + let ts_millis = ts/1000; if me.state.load(Ordering::Acquire) == Looper::RECORDING { let msg_time = Instant::now(); - let ts_millis = ts/1000; let mut buf = me.msg_buf.lock().unwrap(); @@ -78,12 +131,18 @@ impl Looper { } buf.push((ts_millis - me.ts_offset, data.to_vec())); } + (me.log_msgs)(ts_millis - me.ts_offset, data.to_vec()); }, LooperInputCallback { output: virtual_output.clone(), state: cur_state.clone(), outer_start: start_time.clone(), ts_offset: 0, msg_buf: msg_buf.clone(), + log_msgs: Box::new(move |ts, msg| { + if let Some(msg) = MidiMessage::from_slice(msg.as_slice()) { + record_cb(ts, msg); + } + }), })?; Ok(Looper { @@ -93,7 +152,10 @@ impl Looper { cur_state: cur_state.clone(), msg_buf: msg_buf.clone(), start_time: start_time.clone(), - playback_thread: None, + playback_structs: None, + playback_timer: timer::Timer::new(), + playback_cb: Arc::new(Mutex::new(playback_cb)), + recording_length: 0, }) } @@ -108,7 +170,60 @@ impl Looper { *start_time = now; }, Looper::PLAYBACK => { - // TODO + let now = Instant::now(); + if self.msg_buf.lock().unwrap().len() == 0 { + // empty buffer, no messages to send + return Ok(()); + } + let mut start = self.start_time.lock().unwrap(); + self.recording_length = now.duration_since(*start).as_millis() as u64; + *start = now; + + // setup/start playback thread + let mut cur_tick: u64 = 0; + let mut cur_idx = 0; + let playback_buf = self.msg_buf.clone(); + let record_len = self.recording_length; + let output_mutex = self.virtual_output.clone(); + let playback_cb = self.playback_cb.clone(); + let playback_guard = self.playback_timer.schedule_repeating( + time::Duration::milliseconds(1), + move || { + // TODO optimize this so locks happen after + // the messages are sent; this would remove + // timing jitter caused by lock contention + let buf = playback_buf.lock().unwrap(); + let (ref ts1, ref msg1) = &buf[cur_idx]; + let mut ts = ts1; + let mut msg = msg1; + if *ts == cur_tick { + let mut output = output_mutex.lock().unwrap(); + while *ts == cur_tick { + output.send(msg.as_slice()).unwrap(); + if let Some(msg) = MidiMessage::from_slice(msg) { + let mut cb = playback_cb.lock().unwrap(); + (&mut *cb)(*ts, msg); + } + cur_idx += 1; + if cur_idx == buf.len() { + cur_idx = 0; + break; + } + let (ts_next, msg_next) = &buf[cur_idx]; + ts = ts_next; + msg = msg_next; + } + } + cur_tick += 1; + if cur_tick >= record_len { + cur_tick = 0; + cur_idx = 0; + } + }); + self.playback_structs = Some(playback_guard); + }, + Looper::IDLE => { + self.playback_structs = None; }, _ => () } @@ -128,9 +243,8 @@ impl Looper { } fn main() { - let mut looper = Looper::new().unwrap(); - let window = initscr(); + window.printw("============================\n"); window.printw("======== MIDI LOOPER =======\n"); window.printw("============================\n"); @@ -145,7 +259,20 @@ fn main() { window.printw("exit = Esc/Ctrl-C\n"); window.refresh(); window.keypad(true); + window.nodelay(true); noecho(); + + let (tx, rx) = channel(); + let tx_record = tx.clone(); + let tx_playback = tx.clone(); + let mut looper = Looper::new(move |ts, msg| { + tx_record.send((ts, msg)).unwrap(); + }, move |ts, msg| { + tx_playback.send((ts, msg)).unwrap(); + }).unwrap(); + // TODO turn off delay, and sleep for 1 millisecond between getch calls + // TODO call refresh once every 16 frames + let mut count = 0; loop { match window.getch() { Some(Input::Character('\x1b')) => break, @@ -168,10 +295,19 @@ fn main() { }, Some(input) => { window.addstr(&format!("{:?}", input)); - window.refresh(); }, None => () } + while let Ok((ts, msg)) = rx.try_recv() { + window.mvprintw(5, 0, format!("{:?} {}.{:03}", msg, ts/1000, ts%1000)); + } + + count = (count + 1) % 16; + if count == 0 { + window.mv(12, 0); + window.refresh(); + } + thread::sleep(Duration::from_millis(1)); } endwin(); }