From abee6cc2c874ce32f7b6bb7c881b5b47b9532e78 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Fri, 7 Jun 2024 15:37:27 +0300 Subject: [PATCH] panic hook; two sequencers --- Cargo.lock | 73 +++++++ Cargo.toml | 1 + src/engine.rs | 104 +++++----- src/main.rs | 129 +++++++----- src/prelude.rs | 5 +- src/render.rs | 43 ++++ src/sequencer.rs | 426 ++++++++++++++++++++++++++++++++++++++++ src/sequencer/handle.rs | 73 ------- src/sequencer/jack.rs | 40 ---- src/sequencer/mod.rs | 209 -------------------- src/sequencer/render.rs | 193 ------------------ src/transport.rs | 242 +++++++++++++++++++++++ src/transport/handle.rs | 8 - src/transport/jack.rs | 40 ---- src/transport/mod.rs | 63 ------ src/transport/render.rs | 106 ---------- 16 files changed, 927 insertions(+), 828 deletions(-) create mode 100644 src/sequencer.rs delete mode 100644 src/sequencer/handle.rs delete mode 100644 src/sequencer/jack.rs delete mode 100644 src/sequencer/mod.rs delete mode 100644 src/sequencer/render.rs create mode 100644 src/transport.rs delete mode 100644 src/transport/handle.rs delete mode 100644 src/transport/jack.rs delete mode 100644 src/transport/mod.rs delete mode 100644 src/transport/render.rs diff --git a/Cargo.lock b/Cargo.lock index a3c89466..92f7c1ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "ahash" version = "0.8.11" @@ -75,6 +90,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "backtrace" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -91,6 +121,7 @@ checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" name = "bloop" version = "0.0.0" dependencies = [ + "backtrace", "clap", "crossterm", "jack", @@ -112,6 +143,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "cc" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" + [[package]] name = "cfg-if" version = "1.0.0" @@ -208,6 +245,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + [[package]] name = "hashbrown" version = "0.14.5" @@ -318,6 +361,21 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "miniz_oxide" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.11" @@ -330,6 +388,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "object" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -418,6 +485,12 @@ dependencies = [ "bitflags 2.5.0", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustversion" version = "1.0.17" diff --git a/Cargo.toml b/Cargo.toml index 62daaaad..3b093dce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,4 @@ jack = "0.10" clap = { version = "4.5.4", features = [ "derive" ] } crossterm = "0.27" ratatui = { version = "0.26.3", features = [ "unstable-widget-ref" ] } +backtrace = "0.3.72" diff --git a/src/engine.rs b/src/engine.rs index 57e27e65..038ce345 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -25,49 +25,52 @@ pub enum Event { } pub struct Engine { - exited: Arc, - sender: Sender, - receiver: Receiver, + exited: Arc, + sender: Sender, + receiver: Receiver, pub jack_client: Jack, } -pub fn jack_client ( - name: &str, +pub fn activate_jack_client ( + client: Client, notifications: N, - handler: BoxedProcessHandler + handler: BoxedProcessHandler ) -> Result, Box> { - let (client, _status) = Client::new(name, ClientOptions::NO_START_SERVER)?; Ok(client.activate_async(notifications, ClosureProcessHandler::new(handler))?) } +pub trait Component: Exitable + WidgetRef + HandleInput + Send + 'static {} + +impl Component for T {} + impl Engine { pub fn new (name: Option<&str>) -> Result> { let (sender, receiver) = mpsc::channel::(); let exited = Arc::new(AtomicBool::new(false)); + let (client, _status) = Client::new(name.unwrap_or("engine"), ClientOptions::NO_START_SERVER)?; Ok(Self { - jack_client: jack_client( - name.unwrap_or("blinkenlive"), - Notifications(sender.clone()), - Box::new({ - let sender = sender.clone(); - let exited = exited.clone(); - move |_client: &Client, _ps: &ProcessScope| -> Control { - if exited.fetch_and(true, Ordering::Relaxed) { - Control::Quit - } else { - sender.send(Event::Update).unwrap(); - Control::Continue - } - }}))?, - exited, - sender, receiver, + sender: sender.clone(), + exited: exited.clone(), + jack_client: activate_jack_client( + client, + Notifications(sender.clone()), + Box::new(move |_client: &Client, _ps: &ProcessScope| -> Control { + if exited.fetch_and(true, Ordering::Relaxed) { + Control::Quit + } else { + sender.send(Event::Update).unwrap(); + Control::Continue + } + }) + )? }) } - pub fn run ( - &mut self, mut state: T, + pub fn run ( + &mut self, + mut state: impl Component, ) -> Result<(), Box> { let state = Arc::new(Mutex::new(state)); @@ -93,29 +96,28 @@ impl Engine { }) }; - let render_thread = { - stdout().queue(EnterAlternateScreen)?.flush()?; - enable_raw_mode()?; - let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?; - let sleep = std::time::Duration::from_millis(16); - let exited = self.exited.clone(); - spawn(move || loop { - terminal.draw(|frame|{ - let area = frame.size(); - frame.render_widget( - &*state.lock().unwrap(), - area - ); - }); - if state.lock().unwrap().exited() { - exited.store(true, Ordering::Relaxed); - break - } - std::thread::sleep(sleep); - }) + stdout().execute(EnterAlternateScreen)?; + enable_raw_mode()?; + let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?; + let sleep = std::time::Duration::from_millis(16); + let exited = self.exited.clone(); + std::panic::set_hook(Box::new(panic_hook)); + loop { + terminal.draw(|frame|{ + let area = frame.size(); + frame.render_widget( + &*state.lock().unwrap(), + area + ); + }).expect("Failed to render frame"); + if state.lock().unwrap().exited() { + exited.store(true, Ordering::Relaxed); + break + } + std::thread::sleep(sleep); }; - render_thread.join(); + //render_thread.join().expect("Failed to join render thread"); stdout() .queue(crossterm::terminal::LeaveAlternateScreen)? @@ -127,6 +129,16 @@ impl Engine { } +fn panic_hook (info: &std::panic::PanicInfo) { + stdout() + .execute(crossterm::terminal::LeaveAlternateScreen) + .unwrap(); + crossterm::terminal::disable_raw_mode() + .unwrap(); + writeln!(std::io::stderr(), "{}", info); + writeln!(std::io::stderr(), "{:?}", ::backtrace::Backtrace::new()); +} + pub struct Notifications(mpsc::Sender); impl NotificationHandler for Notifications { diff --git a/src/main.rs b/src/main.rs index ac69b43f..88f4cb68 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,7 +43,7 @@ fn run_one (command: &cli::Command) -> Result<(), Box> { sampler::Sampler::new()?, ), cli::Command::Sequencer => engine.run( - sequencer::Sequencer::new()?, + sequencer::Sequencer::new(Some("sequencer"))?, ), } } @@ -54,7 +54,10 @@ fn run_all () -> Result<(), Box> { exited: false, mode: Mode::Sequencer, transport: transport::Transport::new(engine.jack_client.as_client())?, - sequencer: sequencer::Sequencer::new()?, + sequencers: vec![ + sequencer::Sequencer::new(Some("Melody#000"))?, + sequencer::Sequencer::new(Some("Rhythm#000"))?, + ], mixer: mixer::Mixer::new()?, looper: looper::Looper::new()?, sampler: sampler::Sampler::new()?, @@ -63,14 +66,14 @@ fn run_all () -> Result<(), Box> { } struct App { - exited: bool, - mode: Mode, - transport: crate::transport::Transport, - mixer: crate::mixer::Mixer, - looper: crate::looper::Looper, - sampler: crate::sampler::Sampler, - sequencer: crate::sequencer::Sequencer, - actions: Vec<(&'static str, &'static str)> + exited: bool, + mode: Mode, + transport: crate::transport::Transport, + mixer: crate::mixer::Mixer, + looper: crate::looper::Looper, + sampler: crate::sampler::Sampler, + sequencers: Vec, + actions: Vec<(&'static str, &'static str)>, } #[derive(PartialEq)] @@ -94,46 +97,74 @@ impl Exitable for App { impl WidgetRef for App { fn render_ref (&self, area: Rect, buffer: &mut Buffer) { use ratatui::{widgets::*, style::Stylize}; - let column_constraints = vec![ - Constraint::Percentage(28), - Constraint::Percentage(72), - ]; - let row_constraints = vec![ - Constraint::Length(4), - Constraint::Length(14), - Constraint::Length(10), - Constraint::Length(10), - Constraint::Length(10), - ]; - let cols = Layout::default() - .direction(Direction::Horizontal) - .constraints(column_constraints) + let areas = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(4), + Constraint::Min(16), + Constraint::Min(16), + ].clone()) .split(area); - let side = Layout::default() - .direction(Direction::Vertical) - .constraints(row_constraints.clone()) - .split(cols[0]); - let main = Layout::default() - .direction(Direction::Vertical) - .constraints(row_constraints) - .split(cols[1]); + self.transport.render(area, buffer); + self.sequencers[0].render(areas[1], buffer); + self.sequencers[1].render(areas[2], buffer); - Line::from(" Transport").render(side[0], buffer); - self.transport.render(main[0].inner(&Margin { - vertical: 0, - horizontal: 1, - }), buffer); - buffer.set_string( - area.x, main[1].y-1, "─".repeat(area.width as usize), Style::default().black().dim() - ); - Line::from(" Piano roll").render(side[1], buffer); - self.sequencer.render(main[1].inner(&Margin { - vertical: 0, - horizontal: 1, - }), buffer); - buffer.set_string( - area.x, main[2].y+1, "─".repeat(area.width as usize), Style::default().black().dim() - ); + //buffer.set_string( + //area.x, area.y + 5, + //format!(" [ ] Melody#000 │1.1 │1.2 │1.3 │1.4 │ → None "), + //Style::default() + //); + //buffer.set_string( + //area.x, area.y + 6, + //format!(" Variation: │A │B │C │D │E │F │G │H │"), + //Style::default() + //); + //buffer.set_string( + //area.x, area.y + 7, + //format!(" [x] Chain 𝄆 A A B A 𝄇"), + //Style::default() + //); + + //buffer.set_string( + //area.x, area.y, + //format!("│"), + //Style::default().black() + //); + //buffer.set_string( + //area.x + area.width - 1, area.y, + //format!("│"), + //Style::default().black() + //); + //buffer.set_string( + //area.x, area.y + 1, + //format!("╰{}╯", "─".repeat((area.width - 2) as usize)), + //Style::default().black() + //); + + ////Paragraph::new("|Project: The Witty Gerbils - Sha Na Na\n|Rewind |Play |BPM▐▔▔▔▔▔▔▔▔▔▔▔▔▔▔▌\n▐ ⯈ Play/Pause ▌\n▐▁▁▁▁▁▁▁▁▁▁▁▁▁▁▌") + //Paragraph::new("|Project: The Witty Gerbils - Sha Na Na\n|Rewind |Play |Beat 1.1.0 |BPM 113.000 |Time: 123.456 |Rate: 44100Hz |Frame: 2000000000") + //.render(transport_area, buffer); + + //self.transport.render(main[0].inner(&Margin { + //vertical: 0, + //horizontal: 1, + //}), buffer); + //buffer.set_string( + //area.x, main[1].y-1, + //format!("╭{}╮", "─".repeat((area.width - 2) as usize)), + //Style::default().black() + //); + //Paragraph::new("│|Melody #000\n│\n│|Port:\n│|Channel:\n│\n│\n│\n│\n│\n│\n│\n│\n│\n│|Variation:") + //.render(side[1], buffer); + //self.sequencer.render(main[1].inner(&Margin { + //vertical: 0, + //horizontal: 1, + //}), buffer); + //buffer.set_string( + //area.x, main[2].y+1, + //format!("╰{}╯", "─".repeat((area.width - 2) as usize)), + //Style::default().black() + //); //Block::default() //.title("Sampler") @@ -243,7 +274,7 @@ impl HandleInput for App { .handle(&event)?, Mode::Sampler => self.sampler .handle(&event)?, - Mode::Sequencer => self.sequencer + Mode::Sequencer => self.sequencers[0] .handle(&event)?, } } diff --git a/src/prelude.rs b/src/prelude.rs index 2412ff2a..a60534ac 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -16,7 +16,8 @@ pub use std::sync::{ mpsc::{self, channel, Sender, Receiver} }; pub use crossterm::{ - QueueableCommand, + QueueableCommand, + ExecutableCommand, event::{self, KeyCode, KeyModifiers}, cursor::{self, MoveTo, Show, Hide}, terminal::{ @@ -56,7 +57,9 @@ pub use jack::{ pub type BoxedProcessHandler = Box Control + Send>; pub type Jack = AsyncClient>; pub use crate::engine::{ + Component, Exitable, HandleInput, Event }; +pub use crate::render::{draw_box, draw_leaf}; diff --git a/src/render.rs b/src/render.rs index 1171d1ff..e000f677 100644 --- a/src/render.rs +++ b/src/render.rs @@ -27,6 +27,49 @@ impl<'a> WidgetRef for ActionBar<'a> { } } +pub fn draw_leaf (buffer: &mut Buffer, area: Rect, y: u16, x: u16, text: &str) { + use ratatui::style::{Style, Stylize}; + buffer.set_string( + area.x + x, area.y + y, + format!("│"), + Style::default().black().dim() + ); + buffer.set_string( + area.x + x + 1, area.y + y, + format!("{text}"), + Style::default() + ); + buffer.set_string( + area.x + x + text.len() as u16 + 1, area.y + y, + format!("│"), + Style::default().black().dim() + ); + buffer.set_string( + area.x + x, area.y + 1 + y, + format!("╰{}╯", "─".repeat(text.len() as usize)), + Style::default().black() + ); +} + +pub fn draw_box (buffer: &mut Buffer, area: Rect) { + use ratatui::style::{Style, Stylize}; + buffer.set_string( + area.x, area.y, + format!("╭{}╮", "─".repeat((area.width - 2).into())),//.repeat(area.width.saturating_sub(2).into())), + Style::default().black() + ); + buffer.set_string( + area.x, area.y + area.height - 1, + format!("╰{}╯", "─".repeat((area.width - 2).into())),//.repeat(area.width.saturating_sub(2).into())), + Style::default().black() + ); + //buffer.set_string( + //area.x, area.y + area.height, + //format!("╰{}╯", "─".repeat(area.width.saturating_sub(2).into())), + //Style::default().black() + //); +} + pub fn render_toolbar_vertical ( stdout: &mut std::io::Stdout, offset: (u16, u16), diff --git a/src/sequencer.rs b/src/sequencer.rs new file mode 100644 index 00000000..24ae1c06 --- /dev/null +++ b/src/sequencer.rs @@ -0,0 +1,426 @@ +use crate::prelude::*; +use ratatui::style::Stylize; + +pub const ACTIONS: [(&'static str, &'static str);4] = [ + ("+/-", "Zoom"), + ("A/D", "Add/delete note"), + ("]/[", "Duration"), + ("CapsLock", "Auto advance"), +]; + +pub struct Sequencer { + exited: Arc, + sequence: Arc>>>>, + cursor: (u16, u16, u16), + timesig: (f32, f32), + pub jack_client: Jack, +} + +#[derive(Clone)] +pub enum Event { + NoteOn(u8, u8), + NoteOff(u8) +} + +impl Sequencer { + + pub fn new (name: Option<&str>) -> Result> { + let exited = Arc::new(AtomicBool::new(false)); + let (client, _status) = Client::new(name.unwrap_or("sequencer"), ClientOptions::NO_START_SERVER)?; + let mut port = client.register_port("sequence", ::jack::MidiOut::default())?; + let sequence: Arc>>>> = Arc::new(Mutex::new(vec![vec![None;64];128])); + let beats = 4; + let steps = 16; + let bpm = 120.0; + let rate = 44100; // Hz + let frame = 1f64 / rate as f64; // msec + let buf = 512; // frames + let t_beat = 60.0 / bpm; // 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); + } + Ok(Self { + exited: exited.clone(), + sequence: sequence.clone(), + cursor: (11, 0, 0), + timesig: (4.0, 4.0), + jack_client: crate::engine::activate_jack_client( + client, + Notifications, + Box::new(move |client: &Client, scope: &ProcessScope| -> Control { + if exited.fetch_and(true, Ordering::Relaxed) { + return Control::Quit + } + if client.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 + }) + )? + }) + } + +} + +impl Exitable for Sequencer { + fn exit (&mut self) { + self.exited.store(true, Ordering::Relaxed) + } + fn exited (&self) -> bool { + 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)); + } + } +} + +impl HandleInput for Sequencer { + fn handle (&mut self, event: &crate::engine::Event) -> Result<(), Box> { + handle(self, event) + } +} + +fn handle (state: &mut Sequencer, event: &crate::engine::Event) -> Result<(), Box> { + + if let crate::engine::Event::Input(crossterm::event::Event::Key(event)) = event { + + match event.code { + KeyCode::Char('c') => { + if event.modifiers == KeyModifiers::CONTROL { + state.exit(); + } + }, + KeyCode::Down => { + state.cursor.0 = if state.cursor.0 >= 23 { + 0 + } else { + state.cursor.0 + 1 + } + }, + KeyCode::Up => { + state.cursor.0 = if state.cursor.0 == 0 { + 23 + } else { + state.cursor.0 - 1 + } + }, + KeyCode::Left => { + state.cursor.1 = if state.cursor.1 == 0 { + 63 + } else { + state.cursor.1 - 1 + } + }, + KeyCode::Right => { + state.cursor.1 = if state.cursor.1 == 63 { + 0 + } else { + state.cursor.1 + 1 + } + }, + KeyCode::Char('[') => { + if state.cursor.2 > 0 { + state.cursor.2 = state.cursor.2 - 1 + } + }, + KeyCode::Char(']') => { + state.cursor.2 = state.cursor.2 + 1 + }, + KeyCode::Char('a') => { + let row = state.cursor.0 as usize; + let step = state.cursor.1 as usize; + let duration = state.cursor.2 as usize; + let mut sequence = state.sequence.lock().unwrap(); + sequence[row][step] = Some(self::Event::NoteOn(48 - row as u8, 128)); + if state.cursor.2 > 0 { + sequence[row][step + duration] = Some(self::Event::NoteOff(35)); + } + }, + _ => { + println!("{event:?}"); + } + } + + } + Ok(()) +} + +const NOTE_NAMES: [&'static str;12] = [ + "C ", "C#", "D ", "D#", "E ", "F ", "F#", "G ", "G#", "A ", "A#", "B ", +]; + +const KEYS: [&'static str; 6] = [ + "▀", "▀", "▀", "█", "▄", "▄", +]; + +impl WidgetRef for Sequencer { + fn render_ref (&self, area: Rect, buf: &mut Buffer) { + draw_box(buf, area); + draw_leaf(buf, area, 1, 0, "Name: Melody#000"); + draw_leaf(buf, area, 3, 0, "Output: None"); + draw_leaf(buf, area, 5, 0, "Input: None"); + draw_leaf(buf, area, 7, 0, "Channel: 01"); + draw_leaf(buf, area, 9, 0, "Zoom: 1/64"); + draw_leaf(buf, area, 11, 0, "Rate: 1/1"); + let mut area = area.inner(&Margin { horizontal: 1, vertical: 3, }); + area.x = area.x + 14; + draw_sequence_keys(area, buf, &self.jack_client.as_client().transport().query().unwrap(), &self.sequence); + draw_sequence_header(area, buf); + draw_sequence_cursor(area, buf, self.cursor); +//╭{}╮ +//faul + +//ring +//rea. +//╰{}╯ + let cursor = self.cursor; + } +} + +fn draw_sequence_header ( + area: Rect, buf: &mut Buffer +) { + buf.set_string(area.x + 3, area.y, "|1.1.", Style::default().dim()); + buf.set_string(area.x + 3 + 16, area.y, "|1.2.", Style::default().dim()); + buf.set_string(area.x + 3 + 32, area.y, "|1.3.", Style::default().dim()); + buf.set_string(area.x + 3 + 48, area.y, "|1.4.", Style::default().dim()); +} + +fn draw_sequence_keys ( + area: Rect, + buf: &mut Buffer, + transport: &::jack::TransportStatePosition, + sequence: &Arc>>>> +) { + buf.set_string(area.x + 2, area.y, "╭", Style::default().black()); + buf.set_string(area.x + 2, area.y + 13, "╰", Style::default().black()); + buf.set_string(area.x + 2 + 65, area.y, "╮", Style::default().black()); + buf.set_string(area.x + 2 + 65, area.y + 13, "╯", Style::default().black()); + //let transport = state.transport.query()?; + let frame = transport.pos.frame(); + let rate = transport.pos.frame_rate().unwrap(); + let second = (frame as f64) / (rate as f64); + let minute = second / 60f64; + let bpm = 120f64; + let div = 4; + let beats = minute * bpm; + let bars = beats as u32 / div as u32; + let beat = beats as u32 % div as u32 + 1; + let beat_sub = beats % 1.0; + + let sequence = sequence.lock().unwrap(); + + for key in 0..12 { + buf.set_string(area.x, area.y + 1 + key, KEYS[(key % 6) as usize], + Style::default().black()); + buf.set_string(area.x + 1, area.y + 1 + key, "█", + Style::default().black()); + for step in 0..64 { + let bg = if step as u32 == (beat - 1) * 16 + (beat_sub * 16.0) as u32 { + ratatui::style::Color::Black + } else { + ratatui::style::Color::Reset + }; + let top = sequence[(key * 2) as usize][step].is_some(); + let bottom = sequence[(key * 2 + 1) as usize][step].is_some(); + match (top, bottom) { + (true, true) => { + buf.set_string(area.x + 3 + step as u16, area.y + 1 + key, "█", + Style::default().yellow().bold().bg(bg)); + }, + (true, false) => { + buf.set_string(area.x + 3 + step as u16, area.y + 1 + key, "▀", + Style::default().yellow().bold().bg(bg)); + }, + (false, true) => { + buf.set_string(area.x + 3 + step as u16, area.y + 1 + key, "▄", + Style::default().yellow().bold().bg(bg)); + }, + (false, false) => if step % 16 == 0 { + buf.set_string(area.x + 3 + step as u16, area.y + 1 + key, "┊", + Style::default().black().dim().bg(bg)) + } else { + buf.set_string(area.x + 3 + step as u16, area.y + 1 + key, "·", + Style::default().black().dim().bg(bg)) + }, + } + } + } + + for step in 0..64 { + if step % 8 == 0 { + buf.set_string(area.x + 3 + step as u16, area.y + 1 + 12, [ + "|A", "|B", "|C", "|D", "|E", "|F", "|G", "|H" + ][step / 8 as usize], Style::default().dim()) + } + } +} + +fn draw_sequence_cursor ( + area: Rect, buf: &mut Buffer, cursor: (u16, u16, u16) +) { + let cursor_character = match cursor.0 % 2 { + 0 => "▀", + 1 => "▄", + _ => unreachable!() + }; + let cursor_y = cursor.0 / 2; + buf.set_string(area.x + cursor.1 + 3, area.y + 1 + cursor_y, if cursor.2 == 0 { + cursor_character.into() + } else { + cursor_character.repeat(cursor.2 as usize) + }, Style::default().yellow()); +} + +pub struct Notifications; + +impl NotificationHandler for Notifications { + fn thread_init (&self, _: &Client) { + } + + fn shutdown (&mut self, status: ClientStatus, reason: &str) { + } + + fn freewheel (&mut self, _: &Client, is_enabled: bool) { + } + + fn sample_rate (&mut self, _: &Client, _: Frames) -> Control { + Control::Quit + } + + fn client_registration (&mut self, _: &Client, name: &str, is_reg: bool) { + } + + fn port_registration (&mut self, _: &Client, port_id: PortId, is_reg: bool) { + } + + fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + Control::Continue + } + + fn ports_connected (&mut self, _: &Client, id_a: PortId, id_b: PortId, are: bool) { + } + + fn graph_reorder (&mut self, _: &Client) -> Control { + Control::Continue + } + + fn xrun (&mut self, _: &Client) -> Control { + Control::Continue + } +} diff --git a/src/sequencer/handle.rs b/src/sequencer/handle.rs deleted file mode 100644 index 460288e8..00000000 --- a/src/sequencer/handle.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::prelude::*; -use super::Sequencer; - -impl HandleInput for Sequencer { - fn handle (&mut self, event: &Event) -> Result<(), Box> { - handle(self, event) - } -} - -fn handle (state: &mut Sequencer, event: &Event) -> Result<(), Box> { - - if let Event::Input(crossterm::event::Event::Key(event)) = event { - - match event.code { - KeyCode::Char('c') => { - if event.modifiers == KeyModifiers::CONTROL { - state.exit(); - } - }, - KeyCode::Down => { - state.cursor.0 = if state.cursor.0 >= 23 { - 0 - } else { - state.cursor.0 + 1 - } - }, - KeyCode::Up => { - state.cursor.0 = if state.cursor.0 == 0 { - 23 - } else { - state.cursor.0 - 1 - } - }, - KeyCode::Left => { - state.cursor.1 = if state.cursor.1 == 0 { - 63 - } else { - state.cursor.1 - 1 - } - }, - KeyCode::Right => { - state.cursor.1 = if state.cursor.1 == 63 { - 0 - } else { - state.cursor.1 + 1 - } - }, - KeyCode::Char('[') => { - if state.cursor.2 > 0 { - state.cursor.2 = state.cursor.2 - 1 - } - }, - KeyCode::Char(']') => { - state.cursor.2 = state.cursor.2 + 1 - }, - KeyCode::Char('a') => { - let row = state.cursor.0 as usize; - let step = state.cursor.1 as usize; - let duration = state.cursor.2 as usize; - let mut sequence = state.sequence.lock().unwrap(); - sequence[row][step] = Some(super::Event::NoteOn(35, 128)); - if state.cursor.2 > 0 { - sequence[row][step + duration] = Some(super::Event::NoteOff(35)); - } - }, - _ => { - println!("{event:?}"); - } - } - - } - Ok(()) -} diff --git a/src/sequencer/jack.rs b/src/sequencer/jack.rs deleted file mode 100644 index 0f42d709..00000000 --- a/src/sequencer/jack.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::prelude::*; - -pub struct Notifications; - -impl NotificationHandler for Notifications { - fn thread_init (&self, _: &Client) { - } - - fn shutdown (&mut self, status: ClientStatus, reason: &str) { - } - - fn freewheel (&mut self, _: &Client, is_enabled: bool) { - } - - fn sample_rate (&mut self, _: &Client, _: Frames) -> Control { - Control::Quit - } - - fn client_registration (&mut self, _: &Client, name: &str, is_reg: bool) { - } - - fn port_registration (&mut self, _: &Client, port_id: PortId, is_reg: bool) { - } - - fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { - Control::Continue - } - - fn ports_connected (&mut self, _: &Client, id_a: PortId, id_b: PortId, are: bool) { - } - - fn graph_reorder (&mut self, _: &Client) -> Control { - Control::Continue - } - - fn xrun (&mut self, _: &Client) -> Control { - Control::Continue - } -} - diff --git a/src/sequencer/mod.rs b/src/sequencer/mod.rs deleted file mode 100644 index ab89b9fd..00000000 --- a/src/sequencer/mod.rs +++ /dev/null @@ -1,209 +0,0 @@ -mod handle; -mod jack; -mod render; -pub use self::handle::*; -pub use self::jack::*; -pub use self::render::*; -use crate::prelude::*; - -pub const ACTIONS: [(&'static str, &'static str);4] = [ - ("+/-", "Zoom"), - ("A/D", "Add/delete note"), - ("]/[", "Duration"), - ("CapsLock", "Auto advance"), -]; - -pub struct Sequencer { - exited: Arc, - cursor: (u16, u16, u16), - sequence: Arc>>>>, - transport: ::jack::Transport, - bpm: f64, - timesig: (f32, f32), - pub jack_client: Jack, -} - -#[derive(Clone)] -pub enum Event { - NoteOn(u8, u8), - NoteOff(u8) -} - -impl Sequencer { - - pub fn new () -> Result> { - let exited = Arc::new(AtomicBool::new(false)); - let sequence: Arc>>>> = - Arc::new(Mutex::new(vec![vec![None;64];128])); - 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, - cursor: (0, 0, 0), - transport, - sequence, - bpm: 120.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.store(true, Ordering::Relaxed) - } - fn exited (&self) -> bool { - 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 deleted file mode 100644 index c3b8bbd7..00000000 --- a/src/sequencer/render.rs +++ /dev/null @@ -1,193 +0,0 @@ -use crate::prelude::*; -use super::Sequencer; -use ratatui::style::Stylize; - -const NOTE_NAMES: [&'static str;12] = [ - "C ", "C#", "D ", "D#", "E ", "F ", "F#", "G ", "G#", "A ", "A#", "B ", -]; - -const KEYS: [&'static str; 6] = [ - "▀", - "▀", - "▀", - "█", - "▄", - "▄", -]; - -impl WidgetRef for Sequencer { - fn render_ref (&self, area: Rect, buf: &mut Buffer) { - render_sequence_header(area, buf); - render_sequence_keys(area, buf, &self.transport.query().unwrap(), &self.sequence); - render_sequence_cursor(area, buf, self.cursor); - let cursor = self.cursor; - } -} - -fn render_sequence_header ( - area: Rect, buf: &mut Buffer -) { - buf.set_string(area.x, area.y, "1.1.", Style::default().dim()); - buf.set_string(area.x + 16, area.y, "1.2.", Style::default().dim()); - buf.set_string(area.x + 32, area.y, "1.3.", Style::default().dim()); - buf.set_string(area.x + 48, area.y, "1.4.", Style::default().dim()); -} - -fn render_sequence_keys ( - area: Rect, - buf: &mut Buffer, - transport: &::jack::TransportStatePosition, - sequence: &Arc>>>> -) { - //let transport = state.transport.query()?; - let frame = transport.pos.frame(); - let rate = transport.pos.frame_rate().unwrap(); - let second = (frame as f64) / (rate as f64); - let minute = second / 60f64; - let bpm = 120f64; - let div = 4; - let beats = minute * bpm; - let bars = beats as u32 / div as u32; - let beat = beats as u32 % div as u32 + 1; - let beat_sub = beats % 1.0; - - let sequence = sequence.lock().unwrap(); - - for key in 0..12 { - buf.set_string(area.x - 3, area.y + 1 + key, KEYS[(key % 6) as usize], - Style::default().dim()); - buf.set_string(area.x - 2, area.y + 1 + key, "█", - Style::default().dim()); - buf.set_string(area.x, area.y + 1 + key, "·".repeat(64), - Style::default().black().dim()); - buf.set_string(area.x, area.y + 1 + key, "┊", - Style::default().black().dim()); - buf.set_string(area.x + 16, area.y + 1 + key, "┊", - Style::default().black().dim()); - buf.set_string(area.x + 32, area.y + 1 + key, "┊", - Style::default().black().dim()); - buf.set_string(area.x + 48, area.y + 1 + key, "┊", - Style::default().black().dim()); - for step in 0..64 { - - //let bg = if x as u32 == (beat - 1) * 16 + (beat_sub * 16.0) as u32 { - //crossterm::style::Color::Black - //} else { - //crossterm::style::Color::Reset - //}; - - let top = sequence[(key * 2) as usize][step].is_some(); - let bottom = sequence[(key * 2 + 1) as usize][step].is_some(); - match (top, bottom) { - (true, true) => { - buf.set_string(area.x + step as u16, area.y + 1 + key, "█", - Style::default().yellow().bold()); - }, - (true, false) => { - buf.set_string(area.x + step as u16, area.y + 1 + key, "▀", - Style::default().yellow().bold()); - }, - (false, true) => { - buf.set_string(area.x + step as u16, area.y + 1 + key, "▄", - Style::default().yellow().bold()); - }, - (false, false) => {}, - } - } - } -} - -fn render_sequence_cursor ( - area: Rect, buf: &mut Buffer, cursor: (u16, u16, u16) -) { - let cursor_character = match cursor.0 % 2 { - 0 => "▀", - 1 => "▄", - _ => unreachable!() - }; - let cursor_y = cursor.0 / 2; - buf.set_string(area.x + cursor.1, area.y + 1 + cursor_y, if cursor.2 == 0 { - cursor_character.into() - } else { - cursor_character.repeat(cursor.2 as usize) - }, Style::default().yellow()); -} - -//pub fn render ( - //state: &mut Sequencer, - //stdout: &mut Stdout, - //offset: (u16, u16) -//) -> Result<(), Box> { - ////render_toolbar(state, stdout, offset)?; - //render_grid(state, stdout, offset)?; - //render_events(state, stdout, offset)?; - //Ok(()) -//} - -//fn render_grid ( - //state: &mut Sequencer, - //stdout: &mut Stdout, - //offset: (u16, u16) -//) -> Result<(), Box> { - //let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row); - //let cursor: String = if state.cursor.2 == 0 { - //"❱".into() - //} else { - //std::iter::repeat("·") - //.take(state.cursor.2 as usize) - //.collect() - //}; - //let transport = state.transport.query()?; - //let frame = transport.pos.frame(); - //let rate = transport.pos.frame_rate().unwrap(); - //let second = (frame as f64) / (rate as f64); - //let minute = second / 60f64; - //let bpm = 120f64; - //let div = 4; - //let beats = minute * bpm; - //let bars = beats as u32 / div as u32; - //let beat = beats as u32 % div as u32 + 1; - //let beat_sub = beats % 1.0; - //stdout - //.queue(move_to(1, 3))?.queue(Print("1.1"))? - //.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"))?; - //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 { - //crossterm::style::Color::Black - //} else { - //crossterm::style::Color::Reset - //}; - //if let Some(step) = &row[x as usize] { - //stdout.queue(move_to(1 + x, y))?.queue( - //PrintStyledContent("X".white().bold().on(bg)) - //)?; - //} else if x % 16 == 0 { - //stdout.queue(move_to(1 + x, y))?.queue( - //PrintStyledContent("┊".grey().on(bg)) - //)?; - //} else { - //stdout.queue(move_to(1 + x, y))?.queue( - //PrintStyledContent("·".grey().on(bg)) - //)?; - //} - //} - //} - //stdout - //.queue(move_to(1 + state.cursor.1, 4 + state.cursor.0))? - //.queue(PrintStyledContent(cursor.reverse()))?; - //Ok(()) -//} - -//fn render_events ( - //state: &mut Sequencer, - //stdout: &mut Stdout, - //offset: (u16, u16) -//) -> Result<(), Box> { - //Ok(()) -//} - diff --git a/src/transport.rs b/src/transport.rs new file mode 100644 index 00000000..29b7235d --- /dev/null +++ b/src/transport.rs @@ -0,0 +1,242 @@ +use crate::prelude::*; + +pub const ACTIONS: [(&'static str, &'static str);4] = [ + ("?", "Toggle help"), + ("(Shift-)Tab", "Switch pane"), + ("Arrows", "Navigate"), + ("(Shift-)Space", "⯈ Play/pause"), +]; + +pub struct Transport { + exited: bool, + title: String, + transport: ::jack::Transport, + bpm: f64, + timesig: (f32, f32), +} + +impl Transport { + pub fn new (client: &Client) -> Result> { + let transport = client.transport(); + Ok(Self { + exited: false, + title: String::from("Untitled project"), + bpm: 113.0, + timesig: (4.0, 4.0), + transport, + }) + } + + pub fn play_from_start_or_stop_and_rewind (&mut self) { + } + + pub fn play_or_pause (&mut self) -> Result<(), Box> { + match self.transport.query_state()? { + TransportState::Stopped => self.play(), + TransportState::Rolling => self.stop(), + _ => Ok(()) + } + } + + pub fn play (&mut self) -> Result<(), Box> { + Ok(self.transport.start()?) + } + + pub fn stop (&mut self) -> Result<(), Box> { + Ok(self.transport.stop()?) + } +} + +impl Exitable for Transport { + fn exit (&mut self) { + self.exited = true + } + fn exited (&self) -> bool { + self.exited + } +} + +impl WidgetRef for Transport { + fn render_ref (&self, area: Rect, buf: &mut Buffer) { + use ratatui::{layout::*, widgets::*, style::Stylize}; + draw_leaf(buf, area, 0, 0, "REC"); + draw_leaf(buf, area, 0, 5, "DUB"); + draw_leaf(buf, area, 0, 10, "STOP"); + draw_leaf(buf, area, 0, 16, "PLAY/PAUSE"); + draw_leaf(buf, area, 0, 28, "START"); + draw_leaf(buf, area, 0, 35, "Project: Witty Gerbil - Sha Na Na "); + let position = self.transport.query().expect("failed to query transport"); + draw_leaf(buf, area, 2, 0, &format!( + "BPM {:03}.{:03}", + self.bpm as u64, + ((self.bpm % 1.0) * 1000.0) as u64 + )); + //let bbt = position.pos.bbt().map(|mut bbt|*bbt + //.with_bpm(self.bpm) + //.with_timesig(self.timesig.0, self.timesig.1)); + //.unwrap(); + draw_leaf(buf, area, 2, 13, &format!("BBT {}.{}.{}", + 0, + 0, + 0, + )); + let rate = position.pos.frame_rate().unwrap(); + let frame = position.pos.frame(); + let time = frame as f64 / rate as f64; + let seconds = time % 60.0; + let msec = seconds % 1.0; + let minutes = (time / 60.0) % 60.0; + let hours = time / 3600.0; + draw_leaf(buf, area, 2, 27, &format!( + "Time {:02}:{:02}:{:02}.{:03}", + hours as u64, + minutes as u64, + seconds as u64, + (msec * 1000.0) as u64 + )); + draw_leaf(buf, area, 2, 46, &format!("Rate {:>6}Hz", rate)); + draw_leaf(buf, area, 2, 61, &format!("Frame {:>12}", frame)); + //Line::from("Project:").render(area, buf); + //if let Ok(position) = self.transport.query() { + //let frame = position.pos.frame(); + //let rate = position.pos.frame_rate(); + //let bbt = position.pos.bbt().map(|mut bbt|*bbt + //.with_bpm(self.bpm) + //.with_timesig(self.timesig.0, self.timesig.1)); + //Line::from("Frame:").render(area.clone().offset(Offset { x: 0, y: 1 }), buf); + //Line::from(format!("{frame}")).render(area.clone().offset(Offset { x: 0, y: 2 }), buf); + //Line::from("Rate:").render(area.clone().offset(Offset { x: 10, y: 1 }), buf); + //Line::from(match rate { + //Some(rate) => format!("{rate}Hz"), + //None => String::from("(none)"), + //}).render(area.clone().offset(Offset { x: 10, y: 2 }), buf); + //Line::from("Time:").render(area.clone().offset(Offset { x: 20, y: 1 }), buf); + //Line::from(match rate { + //Some(rate) => format!("{:.03}", frame as f64 / rate as f64), + //None => String::from("(none)") + //}).render(area.clone().offset(Offset { x: 20, y: 2 }), buf); + //Line::from("BPM:").render(area.clone().offset(Offset { x: 30, y: 1 }), buf); + //Line::from(match bbt { + //Some(bbt) => format!("{:.01}", bbt.bpm), + //None => String::from("(none)") + //}).render(area.clone().offset(Offset { x: 30, y: 2 }), buf); + //Line::from("TimeSig:").render(area.clone().offset(Offset { x: 40, y: 1 }), buf); + //Line::from(match bbt { + //Some(bbt) => format!("{}/{}", bbt.sig_num, bbt.sig_denom), + //None => String::from("(none)") + //}).render(area.clone().offset(Offset { x: 40, y: 2 }), buf); + //Line::from("Beat:").render(area.clone().offset(Offset { x: 50, y: 1 }), buf); + //Line::from(match bbt { + //Some(bbt) => format!("{}.{}.{}", bbt.bar, bbt.beat, bbt.tick), + //None => String::from("(none)") + //}).render(area.clone().offset(Offset { x: 50, y: 2 }), buf); + //} + } +} + +//pub fn render ( + //state: &mut Transport, + //stdout: &mut Stdout, + //mut offset: (u16, u16) +//) -> Result<(), Box> { + //let move_to = |col, row| MoveTo(offset.0 + col, offset.1 + row); + //stdout.queue(move_to( 1, 0))?.queue( + //Print("Project: ") + //)?.queue(move_to(10, 0))?.queue( + //PrintStyledContent(state.title.clone().white().bold()) + //)?; + + //if let Ok(position) = state.transport.query() { + //let frame = position.pos.frame(); + //let rate = position.pos.frame_rate(); + //let bbt = position.pos.bbt().map(|mut bbt|*bbt + //.with_bpm(state.bpm) + //.with_timesig(state.timesig.0, state.timesig.1)); + //stdout + //.queue(move_to( 1, 1))?.queue(Print("Frame: "))? + //.queue(move_to( 1, 2))?.queue( + //PrintStyledContent( + //format!("{frame}").white().bold(), + //))? + //.queue(move_to(11, 1))?.queue(Print("Rate: "))? + //.queue(move_to(11, 2))?.queue( + //PrintStyledContent(match rate { + //Some(rate) => format!("{rate}Hz"), + //None => String::from("(none)"), + //}.white().bold()) + //)? + //.queue(move_to(20, 1))?.queue(Print("Time: "))? + //.queue(move_to(20, 2))?.queue( + //PrintStyledContent(match rate { + //Some(rate) => format!("{:.03}", frame as f64 / rate as f64), + //None => String::from("(none)") + //}.white().bold()) + //)? + //.queue(move_to(30, 1))?.queue(Print("BPM: "))? + //.queue(move_to(30, 2))?.queue( + //PrintStyledContent(match bbt { + //Some(bbt) => format!("{:.01}", bbt.bpm), + //None => String::from("(none)") + //}.white().bold()) + //)? + //.queue(move_to(39, 1))?.queue(Print("Timesig: "))? + //.queue(move_to(39, 2))?.queue( + //PrintStyledContent(match bbt { + //Some(bbt) => format!("{}/{}", bbt.sig_num, bbt.sig_denom), + //None => String::from("(none)") + //}.white().bold()) + //)? + //.queue(move_to(50, 1))?.queue(Print("Beat: "))? + //.queue(move_to(50, 2))?.queue( + //PrintStyledContent(match bbt { + //Some(bbt) => format!("{}.{}.{}", bbt.bar, bbt.beat, bbt.tick), + //None => String::from("(none)") + //}.white().bold()) + //)?; + //} + //Ok(()) +//} + +impl HandleInput for self::Transport { + fn handle (&mut self, event: &Event) -> Result<(), Box> { + Ok(()) + } +} + +struct Notifications; + +impl NotificationHandler for Notifications { + fn thread_init (&self, _: &Client) { + } + + fn shutdown (&mut self, status: ClientStatus, reason: &str) { + } + + fn freewheel (&mut self, _: &Client, is_enabled: bool) { + } + + fn sample_rate (&mut self, _: &Client, _: Frames) -> Control { + Control::Quit + } + + fn client_registration (&mut self, _: &Client, name: &str, is_reg: bool) { + } + + fn port_registration (&mut self, _: &Client, port_id: PortId, is_reg: bool) { + } + + fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + Control::Continue + } + + fn ports_connected (&mut self, _: &Client, id_a: PortId, id_b: PortId, are: bool) { + } + + fn graph_reorder (&mut self, _: &Client) -> Control { + Control::Continue + } + + fn xrun (&mut self, _: &Client) -> Control { + Control::Continue + } +} diff --git a/src/transport/handle.rs b/src/transport/handle.rs deleted file mode 100644 index 463fbe81..00000000 --- a/src/transport/handle.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::prelude::*; -use super::*; - -impl HandleInput for super::Transport { - fn handle (&mut self, event: &Event) -> Result<(), Box> { - Ok(()) - } -} diff --git a/src/transport/jack.rs b/src/transport/jack.rs deleted file mode 100644 index 5b149824..00000000 --- a/src/transport/jack.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::prelude::*; - -struct Notifications; - -impl NotificationHandler for Notifications { - fn thread_init (&self, _: &Client) { - } - - fn shutdown (&mut self, status: ClientStatus, reason: &str) { - } - - fn freewheel (&mut self, _: &Client, is_enabled: bool) { - } - - fn sample_rate (&mut self, _: &Client, _: Frames) -> Control { - Control::Quit - } - - fn client_registration (&mut self, _: &Client, name: &str, is_reg: bool) { - } - - fn port_registration (&mut self, _: &Client, port_id: PortId, is_reg: bool) { - } - - fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { - Control::Continue - } - - fn ports_connected (&mut self, _: &Client, id_a: PortId, id_b: PortId, are: bool) { - } - - fn graph_reorder (&mut self, _: &Client) -> Control { - Control::Continue - } - - fn xrun (&mut self, _: &Client) -> Control { - Control::Continue - } -} - diff --git a/src/transport/mod.rs b/src/transport/mod.rs deleted file mode 100644 index 769f5eee..00000000 --- a/src/transport/mod.rs +++ /dev/null @@ -1,63 +0,0 @@ -mod handle; -mod jack; -mod render; -pub use self::handle::*; -pub use self::jack::*; -pub use self::render::*; -use crate::prelude::*; - -pub const ACTIONS: [(&'static str, &'static str);4] = [ - ("?", "Toggle help"), - ("(Shift-)Tab", "Switch pane"), - ("Arrows", "Navigate"), - ("(Shift-)Space", "⯈ Play/pause"), -]; - -pub struct Transport { - exited: bool, - title: String, - transport: ::jack::Transport, - bpm: f64, - timesig: (f32, f32), -} - -impl Transport { - pub fn new (client: &Client) -> Result> { - let transport = client.transport(); - Ok(Self { - exited: false, - title: String::from("Untitled project"), - bpm: 93.0, - timesig: (4.0, 4.0), - transport, - }) - } - - pub fn play_from_start_or_stop_and_rewind (&mut self) { - } - - pub fn play_or_pause (&mut self) -> Result<(), Box> { - match self.transport.query_state()? { - TransportState::Stopped => self.play(), - TransportState::Rolling => self.stop(), - _ => Ok(()) - } - } - - pub fn play (&mut self) -> Result<(), Box> { - Ok(self.transport.start()?) - } - - pub fn stop (&mut self) -> Result<(), Box> { - Ok(self.transport.stop()?) - } -} - -impl Exitable for Transport { - fn exit (&mut self) { - self.exited = true - } - fn exited (&self) -> bool { - self.exited - } -} diff --git a/src/transport/render.rs b/src/transport/render.rs deleted file mode 100644 index 4e209a4b..00000000 --- a/src/transport/render.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::prelude::*; -use super::{Transport, ACTIONS}; - -impl WidgetRef for Transport { - fn render_ref (&self, area: Rect, buf: &mut Buffer) { - use ratatui::{layout::*, widgets::*, style::Stylize}; - Line::from("Project:").render(area, buf); - if let Ok(position) = self.transport.query() { - let frame = position.pos.frame(); - let rate = position.pos.frame_rate(); - let bbt = position.pos.bbt().map(|mut bbt|*bbt - .with_bpm(self.bpm) - .with_timesig(self.timesig.0, self.timesig.1)); - Line::from("Frame:").render(area.clone().offset(Offset { x: 0, y: 1 }), buf); - Line::from(format!("{frame}")).render(area.clone().offset(Offset { x: 0, y: 2 }), buf); - Line::from("Rate:").render(area.clone().offset(Offset { x: 10, y: 1 }), buf); - Line::from(match rate { - Some(rate) => format!("{rate}Hz"), - None => String::from("(none)"), - }).render(area.clone().offset(Offset { x: 10, y: 2 }), buf); - Line::from("Time:").render(area.clone().offset(Offset { x: 20, y: 1 }), buf); - Line::from(match rate { - Some(rate) => format!("{:.03}", frame as f64 / rate as f64), - None => String::from("(none)") - }).render(area.clone().offset(Offset { x: 20, y: 2 }), buf); - Line::from("BPM:").render(area.clone().offset(Offset { x: 30, y: 1 }), buf); - Line::from(match bbt { - Some(bbt) => format!("{:.01}", bbt.bpm), - None => String::from("(none)") - }).render(area.clone().offset(Offset { x: 30, y: 2 }), buf); - Line::from("TimeSig:").render(area.clone().offset(Offset { x: 40, y: 1 }), buf); - Line::from(match bbt { - Some(bbt) => format!("{}/{}", bbt.sig_num, bbt.sig_denom), - None => String::from("(none)") - }).render(area.clone().offset(Offset { x: 40, y: 2 }), buf); - Line::from("Beat:").render(area.clone().offset(Offset { x: 50, y: 1 }), buf); - Line::from(match bbt { - Some(bbt) => format!("{}.{}.{}", bbt.bar, bbt.beat, bbt.tick), - None => String::from("(none)") - }).render(area.clone().offset(Offset { x: 50, y: 2 }), buf); - } - } -} - -pub fn render ( - state: &mut Transport, - stdout: &mut Stdout, - mut offset: (u16, u16) -) -> Result<(), Box> { - let move_to = |col, row| MoveTo(offset.0 + col, offset.1 + row); - stdout.queue(move_to( 1, 0))?.queue( - Print("Project: ") - )?.queue(move_to(10, 0))?.queue( - PrintStyledContent(state.title.clone().white().bold()) - )?; - - if let Ok(position) = state.transport.query() { - let frame = position.pos.frame(); - let rate = position.pos.frame_rate(); - let bbt = position.pos.bbt().map(|mut bbt|*bbt - .with_bpm(state.bpm) - .with_timesig(state.timesig.0, state.timesig.1)); - stdout - .queue(move_to( 1, 1))?.queue(Print("Frame: "))? - .queue(move_to( 1, 2))?.queue( - PrintStyledContent( - format!("{frame}").white().bold(), - ))? - .queue(move_to(11, 1))?.queue(Print("Rate: "))? - .queue(move_to(11, 2))?.queue( - PrintStyledContent(match rate { - Some(rate) => format!("{rate}Hz"), - None => String::from("(none)"), - }.white().bold()) - )? - .queue(move_to(20, 1))?.queue(Print("Time: "))? - .queue(move_to(20, 2))?.queue( - PrintStyledContent(match rate { - Some(rate) => format!("{:.03}", frame as f64 / rate as f64), - None => String::from("(none)") - }.white().bold()) - )? - .queue(move_to(30, 1))?.queue(Print("BPM: "))? - .queue(move_to(30, 2))?.queue( - PrintStyledContent(match bbt { - Some(bbt) => format!("{:.01}", bbt.bpm), - None => String::from("(none)") - }.white().bold()) - )? - .queue(move_to(39, 1))?.queue(Print("Timesig: "))? - .queue(move_to(39, 2))?.queue( - PrintStyledContent(match bbt { - Some(bbt) => format!("{}/{}", bbt.sig_num, bbt.sig_denom), - None => String::from("(none)") - }.white().bold()) - )? - .queue(move_to(50, 1))?.queue(Print("Beat: "))? - .queue(move_to(50, 2))?.queue( - PrintStyledContent(match bbt { - Some(bbt) => format!("{}.{}.{}", bbt.bar, bbt.beat, bbt.tick), - None => String::from("(none)") - }.white().bold()) - )?; - } - Ok(()) -}