From 9f739fe04039765e985d7a91cfcbe09bd2af8105 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 28 Dec 2024 14:03:12 +0100 Subject: [PATCH] add groovebox app its own copy of sequencer innards --- crates/cli/src/cli_groovebox.rs | 4 +- crates/tek/src/groovebox.rs | 161 +++++++++++++++++----- crates/tek/src/piano_h.rs | 9 +- crates/tek/src/sequencer.rs | 10 +- crates/tek/src/status/status_groovebox.rs | 8 +- crates/tek/src/transport.rs | 99 +++++++++++++ crates/tek/src/tui.rs | 99 ------------- 7 files changed, 240 insertions(+), 150 deletions(-) diff --git a/crates/cli/src/cli_groovebox.rs b/crates/cli/src/cli_groovebox.rs index 002777c8..49631ec1 100644 --- a/crates/cli/src/cli_groovebox.rs +++ b/crates/cli/src/cli_groovebox.rs @@ -34,8 +34,8 @@ impl GrooveboxCli { let jack = jack.read().unwrap(); let midi_in = jack.register_port("i", MidiIn::default())?; let midi_out = jack.register_port("o", MidiOut::default())?; - app.sequencer.player.midi_ins.push(midi_in); - app.sequencer.player.midi_outs.push(midi_out); + app.player.midi_ins.push(midi_in); + app.player.midi_outs.push(midi_out); Ok(app) })?)?; Ok(()) diff --git a/crates/tek/src/groovebox.rs b/crates/tek/src/groovebox.rs index ad2316a5..64a444fe 100644 --- a/crates/tek/src/groovebox.rs +++ b/crates/tek/src/groovebox.rs @@ -1,20 +1,31 @@ use crate::*; use super::*; use KeyCode::{Char, Delete, Tab, Up, Down, Left, Right}; +use ClockCommand::{Play, Pause}; +use GrooveboxCommand::*; +use PhraseCommand::*; +use PhrasePoolCommand::*; pub struct GrooveboxTui { + _jack: Arc>, + pub clock: ClockModel, + pub phrases: PoolModel, + pub player: MidiPlayer, + pub editor: MidiEditorModel, pub size: Measure, - pub sequencer: SequencerTui, + pub status: bool, + pub note_buf: Vec, + pub midi_buf: Vec>>, + pub perf: PerfModel, pub sampler: SamplerTui, - pub split: u16, - pub focus: GrooveboxFocus } from_jack!(|jack|GrooveboxTui { - let mut sequencer = SequencerTui::try_from(jack)?; - sequencer.status = false; - sequencer.transport = false; - sequencer.selectors = false; + let clock = ClockModel::from(jack); + let phrase = Arc::new(RwLock::new(MidiClip::new( + "New", true, 4 * clock.timebase.ppq.get() as usize, + None, Some(ItemColor::random().into()) + ))); let midi_in_1 = jack.read().unwrap().register_port("in1", MidiIn::default())?; let midi_out = jack.read().unwrap().register_port("out", MidiOut::default())?; let midi_in_2 = jack.read().unwrap().register_port("in2", MidiIn::default())?; @@ -23,25 +34,25 @@ from_jack!(|jack|GrooveboxTui { let audio_out_1 = jack.read().unwrap().register_port("out1", AudioOut::default())?; let audio_out_2 = jack.read().unwrap().register_port("out2", AudioOut::default())?; Self { - sequencer, - sampler: SamplerTui::try_from(jack)?, - split: 16, - focus: GrooveboxFocus::Sequencer, - size: Measure::new(), + _jack: jack.clone(), + phrases: PoolModel::from(&phrase), + editor: MidiEditorModel::from(&phrase), + player: MidiPlayer::from((&clock, &phrase)), + sampler: SamplerTui::try_from(jack)?, + size: Measure::new(), + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + status: true, + clock, } }); - -pub enum GrooveboxFocus { - Sequencer, - Sampler -} - audio!(|self:GrooveboxTui,_client,_process|Control::Continue); -has_clock!(|self:GrooveboxTui|&self.sequencer.clock); +has_clock!(|self:GrooveboxTui|&self.clock); render!(|self:GrooveboxTui|{ let w = self.size.w(); let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - let pool_w = if self.sequencer.phrases.visible { phrase_w } else { 0 }; + let pool_w = if self.phrases.visible { phrase_w } else { 0 }; let sampler_w = 24; Fill::wh(lay!([ &self.size, @@ -49,21 +60,21 @@ render!(|self:GrooveboxTui|{ Tui::shrink_y(2, col!([ Fixed::h(2, row!([ Fixed::wh(5, 2, PlayPause(self.clock().is_rolling())), - Fixed::h(2, TransportView::from((self, self.sequencer.player.play_phrase().as_ref().map(|(_,p)| + Fixed::h(2, TransportView::from((self, self.player.play_phrase().as_ref().map(|(_,p)| p.as_ref().map(|p|p.read().unwrap().color) ).flatten().clone(), true))), ])), Tui::push_x(sampler_w, Fixed::h(1, row!([ - PhraseSelector::play_phrase(&self.sequencer.player), - PhraseSelector::next_phrase(&self.sequencer.player), + PhraseSelector::play_phrase(&self.player), + PhraseSelector::next_phrase(&self.player), ]))), row!([ Tui::pull_y(1, Tui::shrink_y(0, Fill::h(Fixed::w(sampler_w, &self.sampler)))), Tui::split_n(false, 1, - MidiEditStatus(&self.sequencer.editor), + MidiEditStatus(&self.editor), Tui::split_w(false, pool_w, - Tui::pull_y(1, Fill::h(Align::e(PoolView(&self.sequencer.phrases)))), - Fill::wh(&self.sequencer.editor) + Tui::pull_y(1, Fill::h(Align::e(PoolView(&self.phrases)))), + Fill::wh(&self.editor) ) ), ]), @@ -72,23 +83,99 @@ render!(|self:GrooveboxTui|{ }); pub enum GrooveboxCommand { - Sequencer(SequencerCommand), + History(isize), + Clock(ClockCommand), + Pool(PoolCommand), + Editor(PhraseCommand), + Enqueue(Option>>), Sampler(SamplerCommand), } handle!(|self: GrooveboxTui, input|GrooveboxCommand::execute_with_state(self, input)); -input_to_command!(GrooveboxCommand: |state: GrooveboxTui,input|match input.event() { - key_pat!(Up) | key_pat!(Down) | key_pat!(Left) | key_pat!(Right) | - key_pat!(Shift-Char('L')) => - SamplerCommand::input_to_command(&state.sampler, input).map(Self::Sampler)?, - _ => - SequencerCommand::input_to_command(&state.sequencer, input).map(Self::Sequencer)?, +input_to_command!(GrooveboxCommand: |state: GrooveboxTui, input|match input.event() { + // TODO: k: toggle on-screen keyboard + key_pat!(Ctrl-Char('k')) => { + todo!("keyboard") + }, + + // Transport: Play/pause + key_pat!(Char(' ')) => Clock( + if state.clock().is_stopped() { Play(None) } else { Pause(None) } + ), + + // Transport: Play from start or rewind to start + key_pat!(Shift-Char(' ')) => Clock( + if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } + ), + + // Tab: Toggle visibility of phrase pool column + key_pat!(Tab) => Pool(PoolCommand::Show(!state.phrases.visible)), + // q: Enqueue currently edited phrase + key_pat!(Char('q')) => Enqueue(Some(state.phrases.phrase().clone())), + // 0: Enqueue phrase 0 (stop all) + key_pat!(Char('0')) => Enqueue(Some(state.phrases.phrases()[0].clone())), + + // e: Toggle between editing currently playing or other phrase + key_pat!(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() { + let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone()); + let selected = state.phrases.phrase().clone(); + Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing { + selected + } else { + playing.clone() + }))) + } else { + return None + }, + + // For the rest, use the default keybindings of the components. + // The ones defined above supersede them. + _ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) { + Editor(command) + } else if let Some(command) = PoolCommand::input_to_command(&state.phrases, input) { + Pool(command) + } else { + return None + } }); command!(|self:GrooveboxCommand,state:GrooveboxTui|match self { - GrooveboxCommand::Sequencer(command) => - command.execute(&mut state.sequencer)?.map(GrooveboxCommand::Sequencer), - GrooveboxCommand::Sampler(command) => - command.execute(&mut state.sampler)?.map(GrooveboxCommand::Sampler), + Self::Pool(cmd) => { + let mut default = |cmd: PoolCommand|cmd + .execute(&mut state.phrases) + .map(|x|x.map(Pool)); + match cmd { + // autoselect: automatically load selected phrase in editor + PoolCommand::Select(_) => { + let undo = default(cmd)?; + state.editor.set_phrase(Some(state.phrases.phrase())); + undo + }, + // update color in all places simultaneously + PoolCommand::Phrase(SetColor(index, _)) => { + let undo = default(cmd)?; + state.editor.set_phrase(Some(state.phrases.phrase())); + undo + }, + _ => default(cmd)? + } + }, + Self::Editor(cmd) => { + let default = ||cmd.execute(&mut state.editor).map(|x|x.map(Editor)); + match cmd { + _ => default()? + } + }, + Self::Clock(cmd) => cmd.execute(state)?.map(Clock), + Self::Enqueue(phrase) => { + state.player.enqueue_next(phrase.as_ref()); + None + }, + Self::History(delta) => { + todo!("undo/redo") + }, + Self::Sampler(command) => { + todo!("sampler") + } }); diff --git a/crates/tek/src/piano_h.rs b/crates/tek/src/piano_h.rs index 78ff8645..be9cb76e 100644 --- a/crates/tek/src/piano_h.rs +++ b/crates/tek/src/piano_h.rs @@ -29,6 +29,8 @@ pub struct PianoHorizontal { point: MidiPointModel, /// The highlight color palette color: ItemPalette, + /// Width of the keyboard + keys_width: u16, } impl PianoHorizontal { @@ -46,7 +48,8 @@ impl PianoHorizontal { phrase: phrase.cloned(), size, range, - color + color, + keys_width: 5 } } } @@ -85,11 +88,11 @@ render!(|self: PianoHorizontal|{ ])), Tui::inset_xy(0, 1, Fill::wh(Bsp::s( Fixed::h(1, Bsp::e( - Fixed::w(keys_width, ""), + Fixed::w(self.keys_width, ""), Fill::w(timeline()), )), Bsp::e( - Fixed::w(keys_width, keys()), + Fixed::w(self.keys_width, keys()), Fill::wh(lay!([ &self.size, Fill::wh(lay!([ diff --git a/crates/tek/src/sequencer.rs b/crates/tek/src/sequencer.rs index 7e6bfc60..791279c9 100644 --- a/crates/tek/src/sequencer.rs +++ b/crates/tek/src/sequencer.rs @@ -93,7 +93,7 @@ handle!(|self:SequencerTui,input|SequencerCommand::execute_with_state(self, #[derive(Clone, Debug)] pub enum SequencerCommand { History(isize), Clock(ClockCommand), - Phrases(PoolCommand), + Pool(PoolCommand), Editor(PhraseCommand), Enqueue(Option>>), } @@ -113,7 +113,7 @@ input_to_command!(SequencerCommand: |state: SequencerTui, input|match input // Shift-U: redo key_pat!(Char('U')) => History( 1), // Tab: Toggle visibility of phrase pool column - key_pat!(Tab) => Phrases(PoolCommand::Show(!state.phrases.visible)), + key_pat!(Tab) => Pool(PoolCommand::Show(!state.phrases.visible)), // q: Enqueue currently edited phrase key_pat!(Char('q')) => Enqueue(Some(state.phrases.phrase().clone())), // 0: Enqueue phrase 0 (stop all) @@ -135,16 +135,16 @@ input_to_command!(SequencerCommand: |state: SequencerTui, input|match input _ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) { Editor(command) } else if let Some(command) = PoolCommand::input_to_command(&state.phrases, input) { - Phrases(command) + Pool(command) } else { return None } }); command!(|self: SequencerCommand, state: SequencerTui|match self { - Self::Phrases(cmd) => { + Self::Pool(cmd) => { let mut default = |cmd: PoolCommand|cmd .execute(&mut state.phrases) - .map(|x|x.map(Phrases)); + .map(|x|x.map(Pool)); match cmd { // autoselect: automatically load selected phrase in editor PoolCommand::Select(_) => { diff --git a/crates/tek/src/status/status_groovebox.rs b/crates/tek/src/status/status_groovebox.rs index e92e99cf..a00b30cd 100644 --- a/crates/tek/src/status/status_groovebox.rs +++ b/crates/tek/src/status/status_groovebox.rs @@ -9,14 +9,14 @@ pub struct GrooveboxStatus { pub(crate) playing: bool, } from!(|state:&GrooveboxTui|GrooveboxStatus = { - let samples = state.sequencer.clock.chunk.load(Relaxed); - let rate = state.sequencer.clock.timebase.sr.get(); + let samples = state.clock.chunk.load(Relaxed); + let rate = state.clock.timebase.sr.get(); let buffer = samples as f64 / rate; let width = state.size.w(); Self { width, - playing: state.sequencer.clock.is_rolling(), - cpu: state.sequencer.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), + playing: state.clock.is_rolling(), + cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), size: format!("{}x{}│", width, state.size.h()), } }); diff --git a/crates/tek/src/transport.rs b/crates/tek/src/transport.rs index 32d64d66..629217d1 100644 --- a/crates/tek/src/transport.rs +++ b/crates/tek/src/transport.rs @@ -273,3 +273,102 @@ fn to_seek_command (input: &TuiInput) -> Option { _ => return None, }) } + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +//struct Field(&'static str, String); + +//render!(|self: Field|{ + //Tui::to_east("│", Tui::to_east( + //Tui::bold(true, self.0), + //Tui::bg(Color::Rgb(0, 0, 0), self.1.as_str()), + //)) +//}); + +//pub struct TransportView { + //pub(crate) state: Option, + //pub(crate) selected: Option, + //pub(crate) focused: bool, + //pub(crate) bpm: f64, + //pub(crate) sync: f64, + //pub(crate) quant: f64, + //pub(crate) beat: String, + //pub(crate) msu: String, +//} + ////)?; + ////match *state { + ////Some(TransportState::Rolling) => { + ////add(&row!( + ////"│", + ////TuiStyle::fg("▶ PLAYING", Color::Rgb(0, 255, 0)), + ////format!("│0 (0)"), + ////format!("│00m00s000u"), + ////format!("│00B 0b 00/00") + ////))?; + ////add(&row!("│Now ", row!( + ////format!("│0 (0)"), //sample(chunk) + ////format!("│00m00s000u"), //msu + ////format!("│00B 0b 00/00"), //bbt + ////)))?; + ////}, + ////_ => { + ////add(&row!("│", TuiStyle::fg("⏹ STOPPED", Color::Rgb(255, 128, 0))))?; + ////add(&"")?; + ////} + ////} + ////Ok(()) + ////}).fill_x().bg(Color::Rgb(40, 50, 30)) +////}); + +//impl<'a, T: HasClock> From<&'a T> for TransportView where Option: From<&'a T> { + //fn from (state: &'a T) -> Self { + //let selected = state.into(); + //Self { + //selected, + //focused: selected.is_some(), + //state: Some(state.clock().transport.query_state().unwrap()), + //bpm: state.clock().bpm().get(), + //sync: state.clock().sync.get(), + //quant: state.clock().quant.get(), + //beat: state.clock().playhead.format_beat(), + //msu: state.clock().playhead.usec.format_msu(), + //} + //} +//} + + //row!( + ////selected.wrap(TransportFocus::PlayPause, &play_pause.fixed_xy(10, 3)), + //row!( + //col!( + //Field("SR ", format!("192000")), + //Field("BUF ", format!("1024")), + //Field("LEN ", format!("21300")), + //Field("CPU ", format!("00.0%")) + //), + //col!( + //Field("PUL ", format!("000000000")), + //Field("PPQ ", format!("96")), + //Field("BBT ", format!("00B0b00p")) + //), + //col!( + //Field("SEC ", format!("000000.000")), + //Field("BPM ", format!("000.000")), + //Field("MSU ", format!("00m00s00u")) + //), + //), + //selected.wrap(TransportFocus::Bpm, &Outset::X(1u16, { + //row! { + //"BPM ", + //format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0) + //} + //})), + //selected.wrap(TransportFocus::Sync, &Outset::X(1u16, row! { + //"SYNC ", pulses_to_name(*sync as usize) + //})), + //selected.wrap(TransportFocus::Quant, &Outset::X(1u16, row! { + //"QUANT ", pulses_to_name(*quant as usize) + //})), + //selected.wrap(TransportFocus::Clock, &{ + //row!("B" , beat.as_str(), " T", msu.as_str()).outset_x(1) + //}).align_e().fill_x(), + //).fill_x().bg(Color::Rgb(40, 50, 30)) diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index 737ef479..8cd25285 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -151,102 +151,3 @@ impl Tui { buffer } } - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -//struct Field(&'static str, String); - -//render!(|self: Field|{ - //Tui::to_east("│", Tui::to_east( - //Tui::bold(true, self.0), - //Tui::bg(Color::Rgb(0, 0, 0), self.1.as_str()), - //)) -//}); - -//pub struct TransportView { - //pub(crate) state: Option, - //pub(crate) selected: Option, - //pub(crate) focused: bool, - //pub(crate) bpm: f64, - //pub(crate) sync: f64, - //pub(crate) quant: f64, - //pub(crate) beat: String, - //pub(crate) msu: String, -//} - ////)?; - ////match *state { - ////Some(TransportState::Rolling) => { - ////add(&row!( - ////"│", - ////TuiStyle::fg("▶ PLAYING", Color::Rgb(0, 255, 0)), - ////format!("│0 (0)"), - ////format!("│00m00s000u"), - ////format!("│00B 0b 00/00") - ////))?; - ////add(&row!("│Now ", row!( - ////format!("│0 (0)"), //sample(chunk) - ////format!("│00m00s000u"), //msu - ////format!("│00B 0b 00/00"), //bbt - ////)))?; - ////}, - ////_ => { - ////add(&row!("│", TuiStyle::fg("⏹ STOPPED", Color::Rgb(255, 128, 0))))?; - ////add(&"")?; - ////} - ////} - ////Ok(()) - ////}).fill_x().bg(Color::Rgb(40, 50, 30)) -////}); - -//impl<'a, T: HasClock> From<&'a T> for TransportView where Option: From<&'a T> { - //fn from (state: &'a T) -> Self { - //let selected = state.into(); - //Self { - //selected, - //focused: selected.is_some(), - //state: Some(state.clock().transport.query_state().unwrap()), - //bpm: state.clock().bpm().get(), - //sync: state.clock().sync.get(), - //quant: state.clock().quant.get(), - //beat: state.clock().playhead.format_beat(), - //msu: state.clock().playhead.usec.format_msu(), - //} - //} -//} - - //row!( - ////selected.wrap(TransportFocus::PlayPause, &play_pause.fixed_xy(10, 3)), - //row!( - //col!( - //Field("SR ", format!("192000")), - //Field("BUF ", format!("1024")), - //Field("LEN ", format!("21300")), - //Field("CPU ", format!("00.0%")) - //), - //col!( - //Field("PUL ", format!("000000000")), - //Field("PPQ ", format!("96")), - //Field("BBT ", format!("00B0b00p")) - //), - //col!( - //Field("SEC ", format!("000000.000")), - //Field("BPM ", format!("000.000")), - //Field("MSU ", format!("00m00s00u")) - //), - //), - //selected.wrap(TransportFocus::Bpm, &Outset::X(1u16, { - //row! { - //"BPM ", - //format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0) - //} - //})), - //selected.wrap(TransportFocus::Sync, &Outset::X(1u16, row! { - //"SYNC ", pulses_to_name(*sync as usize) - //})), - //selected.wrap(TransportFocus::Quant, &Outset::X(1u16, row! { - //"QUANT ", pulses_to_name(*quant as usize) - //})), - //selected.wrap(TransportFocus::Clock, &{ - //row!("B" , beat.as_str(), " T", msu.as_str()).outset_x(1) - //}).align_e().fill_x(), - //).fill_x().bg(Color::Rgb(40, 50, 30))