Implement playback; I think it works!

This commit is contained in:
Kelvin Ly 2019-12-21 13:55:52 -05:00
parent c07837c3e2
commit 21e719c4d2
3 changed files with 194 additions and 9 deletions

47
Cargo.lock generated
View File

@ -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"

View File

@ -9,3 +9,5 @@ edition = "2018"
[dependencies]
midir = "0.5"
pancurses = "0.16"
time = "0.1"
timer = "0.2"

View File

@ -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<MidiMessage> {
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<Mutex<Instant>>,
ts_offset: u64,
msg_buf: Arc<Mutex<Vec<(u64, Vec<u8>)>>>,
log_msgs: Box<dyn FnMut(u64, Vec<u8>) + Send>,
}
struct Looper {
@ -35,7 +82,10 @@ struct Looper {
msg_buf: Arc<Mutex<Vec<(u64, Vec<u8>)>>>,
// for either playback or recording
start_time: Arc<Mutex<Instant>>,
playback_thread: Option<std::thread::Thread>,
recording_length: u64,
playback_structs: Option<timer::Guard>,
playback_timer: timer::Timer,
playback_cb: Arc<Mutex<dyn FnMut(u64, MidiMessage) + Send + 'static>>,
}
impl Looper {
@ -43,7 +93,10 @@ impl Looper {
const RECORDING: usize = 1;
const PLAYBACK: usize = 2;
fn new() -> Result<Looper, Box<dyn Error>> {
fn new<F, G>(mut record_cb: F, playback_cb: G) -> Result<Looper, Box<dyn Error>>
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();
}