From faac61180b74a94a2317a4d124fb62809d916713 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Mon, 3 Jun 2024 17:53:10 +0300 Subject: [PATCH] wip: assigning steps to frames --- Cargo.lock | 217 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/main.rs | 4 +- src/sequencer/handle.rs | 5 +- src/sequencer/mod.rs | 172 +++++++++++++++++++++++++++++-- src/sequencer/render.rs | 3 +- 6 files changed, 389 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7916e2ac..a3c89466 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,24 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "anstream" version = "0.6.14" @@ -76,6 +94,22 @@ dependencies = [ "clap", "crossterm", "jack", + "ratatui", +] + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", ] [[package]] @@ -130,6 +164,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "crossterm" version = "0.27.0" @@ -155,6 +202,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "either" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "heck" version = "0.5.0" @@ -167,6 +230,21 @@ version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "jack" version = "0.10.0" @@ -231,6 +309,15 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown", +] + [[package]] name = "mio" version = "0.8.11" @@ -243,6 +330,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + [[package]] name = "parking_lot" version = "0.12.2" @@ -266,6 +359,12 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pkg-config" version = "0.3.30" @@ -290,6 +389,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ratatui" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" +dependencies = [ + "bitflags 2.5.0", + "cassowary", + "compact_str", + "crossterm", + "itertools", + "lru", + "paste", + "stability", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + [[package]] name = "redox_syscall" version = "0.5.1" @@ -299,6 +418,18 @@ dependencies = [ "bitflags 2.5.0", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "scopeguard" version = "1.2.0" @@ -341,12 +472,50 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "stability" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7993a8e3a9e88a00351486baae9522c91b123a088f76469e5bd5cc17198ea87" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "2.0.60" @@ -364,12 +533,40 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-truncate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" +dependencies = [ + "itertools", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" + [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -536,3 +733,23 @@ name = "windows_x86_64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 148a4ac8..68b99266 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" jack = "0.10" clap = { version = "4.5.4", features = [ "derive" ] } crossterm = "0.27" +ratatui = "0.26.3" diff --git a/src/main.rs b/src/main.rs index 247fa252..f264d894 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,7 +72,7 @@ fn run_one (command: &cli::Command) -> Result<(), Box> { ), cli::Command::Sequencer => engine.run( - &mut sequencer::Sequencer::new(engine.jack_client.as_client())?, + &mut sequencer::Sequencer::new()?, |state, stdout, mut offset| { let (w, h) = render::render_toolbar_vertical(stdout, offset, &sequencer::ACTIONS)?; offset.0 = offset.0 + w + 2; @@ -122,7 +122,7 @@ fn run_all () -> Result<(), Box> { exited: false, mode: Mode::Sequencer, transport: transport::Transport::new(engine.jack_client.as_client())?, - sequencer: sequencer::Sequencer::new(engine.jack_client.as_client())?, + sequencer: sequencer::Sequencer::new()?, mixer: mixer::Mixer::new()?, looper: looper::Looper::new()?, sampler: sampler::Sampler::new()?, diff --git a/src/sequencer/handle.rs b/src/sequencer/handle.rs index 39f4bc8b..40d05dd1 100644 --- a/src/sequencer/handle.rs +++ b/src/sequencer/handle.rs @@ -51,9 +51,10 @@ pub fn handle (state: &mut Sequencer, event: &Event) -> Result<(), Box 0 { - state.sequence[row][step + duration] = Some(super::Event::NoteOff(35)); + sequence[row][step + duration] = Some(super::Event::NoteOff(35)); } }, _ => { diff --git a/src/sequencer/mod.rs b/src/sequencer/mod.rs index f70a750e..2997267f 100644 --- a/src/sequencer/mod.rs +++ b/src/sequencer/mod.rs @@ -14,12 +14,13 @@ pub const ACTIONS: [(&'static str, &'static str);4] = [ ]; pub struct Sequencer { - exited: bool, + exited: Arc, cursor: (u16, u16, u16), - sequence: Vec>>, + sequence: Arc>>>>, transport: ::jack::Transport, bpm: f64, timesig: (f32, f32), + pub jack_client: Jack, } #[derive(Clone)] @@ -29,24 +30,179 @@ pub enum Event { } impl Sequencer { - pub fn new (client: &Client) -> Result> { + + pub fn new () -> Result> { + let exited = Arc::new(AtomicBool::new(false)); + let sequence: Arc>>>> = Arc::new(Mutex::new(vec![vec![None;64];4])); + let (client, _status) = Client::new( + "blinkenlive-sequencer", + ClientOptions::NO_START_SERVER + )?; let transport = client.transport(); + let mut port = client.register_port("sequence", ::jack::MidiOut::default())?; + let beats = 4; + let steps = 16; + let bpm = 120; + let rate = 44100; // Hz + let frame = 1f64 / rate as f64; // msec + let buf = 512; // frames + let t_beat = 60.0 / bpm as f64; // msec + let t_loop = t_beat * beats as f64; // msec + let t_step = t_beat / steps as f64; // msec + let mut step_frames = vec![]; + for step in 0..beats*steps { + let step_index = (step as f64 * t_step / frame) as usize; + step_frames.push(step_index); + } + let loop_frames = (t_loop*rate as f64) as usize; + let mut frame_steps: Vec> = vec![None;loop_frames]; + for (index, frame) in step_frames.iter().enumerate() { + frame_steps[*frame] = Some(index); + } + let handler: BoxedProcessHandler = Box::new({ + let transport = client.transport(); + let exited = exited.clone(); + let sequence = sequence.clone(); + move |client: &Client, scope: &ProcessScope| -> Control { + if exited.fetch_and(true, Ordering::Relaxed) { + return Control::Quit + } + if transport.query_state().unwrap() == TransportState::Rolling { + let chunk_start = scope.last_frame_time(); + let chunk_size = scope.n_frames(); + let chunk_end = chunk_start + chunk_size; + let start_looped = chunk_start as usize % loop_frames; + let end_looped = chunk_end as usize % loop_frames; + let mut writer = port.writer(scope); + let sequence = sequence.lock().unwrap(); + for frame in 0..chunk_size { + let value = frame_steps[(start_looped + frame as usize) % loop_frames]; + if let Some(step) = value { + for track in sequence.iter() { + for event in track[step].iter() { + writer.write(&::jack::RawMidi { + time: frame as u32, + bytes: &match event { + Event::NoteOn(pitch, velocity) => [ + 0b10010000, + *pitch, + *velocity + ], + Event::NoteOff(pitch) => [ + 0b10000000, + *pitch, + 0b00000000 + ], + } + }).unwrap() + } + } + } + } + } + Control::Continue + } + }); Ok(Self { - exited: false, + exited, cursor: (0, 0, 0), transport, - sequence: vec![vec![None;64];4], + sequence, bpm: 120.0, - timesig: (4.0, 4.0) + timesig: (4.0, 4.0), + jack_client: client.activate_async( + self::jack::Notifications, + ClosureProcessHandler::new(handler) + )? }) } + } impl Exitable for Sequencer { fn exit (&mut self) { - self.exited = true + self.exited.store(true, Ordering::Relaxed) } fn exited (&self) -> bool { - self.exited + self.exited.fetch_and(true, Ordering::Relaxed) + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_midi_frames () { + let beats = 4; + let steps = 16; + let bpm = 120; + let rate = 44100; // Hz + let frame = 1f64 / rate as f64; // msec + let buf = 512; // frames + let t_beat = 60.0 / bpm as f64; // msec + let t_loop = t_beat * beats as f64; // msec + let t_step = t_beat / steps as f64; // msec + + let assign = |chunk: usize| { + let start = chunk * buf; // frames + let end = (chunk + 1) * buf; // frames + println!("{chunk}: {start} .. {end}"); + let mut steps: Vec<(usize, usize, f64)> = vec![]; + for frame_index in start..end { + let frame_msec = frame_index as f64 * frame; + let offset = (frame_msec * 1000.0) % (t_step * 1000.0); + if offset < 0.1 { + let time = frame_index - start; + let step_index = (frame_msec % t_loop / t_step) as usize; + println!("{chunk}: {frame_index} ({time}) -> {step_index} ({frame_msec} % {t_step} = {offset})"); + steps.push((time, step_index, offset)); + } + } + steps + }; + + for chunk in 0..10 { + let chunk = assign(chunk); + //println!("{chunk} {:#?}", assign(chunk)); + } + } + + #[test] + fn test_midi_frames_2 () { + let beats = 4; + let steps = 16; + let bpm = 120; + let rate = 44100; // Hz + let frame = 1f64 / rate as f64; // msec + let buf = 512; // frames + let t_beat = 60.0 / bpm as f64; // msec + let t_loop = t_beat * beats as f64; // msec + let t_step = t_beat / steps as f64; // msec + let mut step_frames = vec![]; + for step in 0..beats*steps { + let step_index = (step as f64 * t_step / frame) as usize; + step_frames.push(step_index); + } + let loop_frames = (t_loop*rate as f64) as usize; + let mut frame_steps: Vec> = vec![None;loop_frames]; + for (index, frame) in step_frames.iter().enumerate() { + println!("{index} {frame}"); + frame_steps[*frame] = Some(index); + } + let assign = |chunk: usize| { + let (start, end) = (chunk * buf, (chunk + 1) * buf); // frames + let (start_looped, end_looped) = (start % loop_frames, end % loop_frames); + println!("{chunk}: {start} .. {end} ({start_looped} .. {end_looped})"); + let mut steps: Vec> = vec![None;buf]; + for frame in 0..buf { + let value = frame_steps[(start_looped + frame) as usize % loop_frames]; + if value.is_some() { println!("{frame:03} = {value:?}, ") }; + steps[frame as usize] = value; + } + steps + }; + for chunk in 0..1000 { + let chunk = assign(chunk); + //println!("{chunk} {:#?}", assign(chunk)); + } } } diff --git a/src/sequencer/render.rs b/src/sequencer/render.rs index 05675f5f..367f4b3e 100644 --- a/src/sequencer/render.rs +++ b/src/sequencer/render.rs @@ -41,7 +41,8 @@ fn render_grid ( .queue(move_to(17, 3))?.queue(Print("1.2"))? .queue(move_to(33, 3))?.queue(Print("1.3"))? .queue(move_to(49, 3))?.queue(Print("1.4"))?; - for (index, row) in state.sequence.iter().enumerate() { + let sequence = state.sequence.lock().unwrap(); + for (index, row) in sequence.iter().enumerate() { let y = index as u16 + 4; for x in 0u16..64 { let bg = if x as u32 == (beat - 1) * 16 + (beat_sub * 16.0) as u32 {