From 005bb5fde87c0905fd409a76323611c231a11f93 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 2 Jan 2025 22:58:16 +0100 Subject: [PATCH] refactor groovebox into modules + accidental code golf --- src/groovebox.rs | 179 +---------------------------- src/groovebox/groovebox_audio.rs | 36 ++++++ src/groovebox/groovebox_command.rs | 91 +++++++++++++++ src/groovebox/groovebox_tui.rs | 86 ++++++++++++++ 4 files changed, 217 insertions(+), 175 deletions(-) create mode 100644 src/groovebox/groovebox_audio.rs create mode 100644 src/groovebox/groovebox_command.rs create mode 100644 src/groovebox/groovebox_tui.rs diff --git a/src/groovebox.rs b/src/groovebox.rs index 9b5c5d9c..d556ff9b 100644 --- a/src/groovebox.rs +++ b/src/groovebox.rs @@ -6,6 +6,10 @@ use GrooveboxCommand as Cmd; use MidiEditCommand::*; use PhrasePoolCommand::*; +mod groovebox_audio; pub use self::groovebox_audio::*; +mod groovebox_command; pub use self::groovebox_command::*; +mod groovebox_tui; pub use self::groovebox_tui::*; + pub struct Groovebox { _jack: Arc>, @@ -55,178 +59,3 @@ impl Groovebox { } } has_clock!(|self: Groovebox|self.player.clock()); -audio!(|self: Groovebox, client, scope|{ - let t0 = self.perf.get_t0(); - if Control::Quit == ClockAudio(&mut self.player).process(client, scope) { - return Control::Quit - } - if Control::Quit == PlayerAudio( - &mut self.player, &mut self.note_buf, &mut self.midi_buf - ).process(client, scope) { - return Control::Quit - } - if Control::Quit == SamplerAudio(&mut self.sampler).process(client, scope) { - return Control::Quit - } - // TODO move these to editor and sampler: - for RawMidi { time, bytes } in self.player.midi_ins[0].iter(scope) { - if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { - match message { - MidiMessage::NoteOn { ref key, .. } => { - self.editor.set_note_point(key.as_int() as usize); - }, - MidiMessage::Controller { controller, value } => { - if let Some(sample) = &self.sampler.mapped[self.editor.note_point()] { - sample.write().unwrap().handle_cc(controller, value) - } - } - _ => {} - } - } - } - self.perf.update(t0, scope); - Control::Continue -}); - -render!(Tui: (self: Groovebox) => self.size.of(Bsp::s( - self.toolbar_view(), - Bsp::s( - self.selector_view(), - Bsp::n(self.sample_view(), - Bsp::n(self.status_view(), - Bsp::w( - self.pool_view(), - Fill::xy(Bsp::e( - self.sampler_view(), - &self.editor - ))))))))); - -impl Groovebox { - fn toolbar_view (&self) -> impl Content + use<'_> { - Fill::x(Fixed::y(2, lay!( - Align::w(Meter("L/", self.sampler.input_meter[0])), - Align::e(Meter("R/", self.sampler.input_meter[1])), - Align::x(Tui::bg(TuiTheme::g(32), TransportView::new(true, &self.player.clock))), - ))) - } - fn status_view (&self) -> impl Content + use<'_> { - let note_pt = self.editor.note_point(); - lay!( - Align::w(Fixed::y(1, SamplerStatus(&self.sampler, note_pt))), - Align::x(Fixed::y(1, MidiEditStatus(&self.editor))), - ) - } - fn sampler_view (&self) -> impl Content + use<'_> { - let sampler_w = if !self.compact { 11 } else { 4 }; - let sampler_y = if self.compact { 1 } else { 0 }; - Fixed::x(sampler_w, Push::y(sampler_y, Fill::y( - SampleList::new(self.compact, &self.sampler, &self.editor)))) - } - fn sample_view (&self) -> impl Content + use<'_> { - let note_pt = self.editor.note_point(); - let sample_h = if self.compact { 0 } else { 5 }; - Max::y(sample_h, Fill::xy( - SampleViewer::from_sampler(&self.sampler, note_pt))) - } - fn pool_view (&self) -> impl Content + use<'_> { - let w = self.size.w(); - let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - Fixed::x(if self.compact { 5 } else { pool_w }, - PoolView(self.compact, &self.pool)) - } - fn selector_view (&self) -> impl Content + use<'_> { - lay!( - Align::w(MidiEditClip(&self.editor)), - Align::e(Bsp::e( - ClipSelected::play_phrase(&self.player), - ClipSelected::next_phrase(&self.player) - )) - ) - } -} - -pub enum GrooveboxCommand { - Compact(bool), - History(isize), - Clock(ClockCommand), - Pool(PoolCommand), - Editor(MidiEditCommand), - Enqueue(Option>>), - Sampler(SamplerCommand), -} - -handle!(|self: Groovebox, input|GrooveboxCommand::execute_with_state(self, input.event())); -keymap!(KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand { - // Tab: Toggle compact mode - key(Tab) => Cmd::Compact(!state.compact), - // q: Enqueue currently edited phrase - key(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())), - // 0: Enqueue phrase 0 (stop all) - key(Char('0')) => Cmd::Enqueue(Some(state.pool.phrases()[0].clone())), - // TODO: k: toggle on-screen keyboard - ctrl(key(Char('k'))) => todo!("keyboard"), - // Transport: Play from start or rewind to start - ctrl(key(Char(' '))) => Cmd::Clock( - if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } - ), - // Shift-R: toggle recording - shift(key(Char('R'))) => Cmd::Sampler(if state.sampler.recording.is_some() { - SamplerCommand::RecordFinish - } else { - SamplerCommand::RecordBegin(u7::from(state.editor.note_point() as u8)) - }), - // Shift-Del: delete sample - shift(key(Delete)) => Cmd::Sampler( - SamplerCommand::SetSample(u7::from(state.editor.note_point() as u8), None) - ), - // e: Toggle between editing currently playing or other phrase - shift(key(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.pool.phrase().clone(); - Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing { - selected - } else { - playing.clone() - }))) - } else { - return None - }, -}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { - Cmd::Editor(command) -} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { - Cmd::Pool(command) -} else { - return None -}); - -command!(|self: GrooveboxCommand, state: Groovebox|match self { - Self::Enqueue(phrase) => { - state.player.enqueue_next(phrase.as_ref()); - None - }, - Self::Pool(cmd) => match cmd { - // autoselect: automatically load selected phrase in editor - PoolCommand::Select(_) => { - let undo = cmd.delegate(&mut state.pool, Self::Pool)?; - state.editor.set_phrase(Some(state.pool.phrase())); - undo - }, - // update color in all places simultaneously - PoolCommand::Phrase(SetColor(index, _)) => { - let undo = cmd.delegate(&mut state.pool, Self::Pool)?; - state.editor.set_phrase(Some(state.pool.phrase())); - undo - }, - _ => cmd.delegate(&mut state.pool, Self::Pool)? - }, - Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?, - Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, - Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, - Self::History(delta) => { todo!("undo/redo") }, - Self::Compact(compact) => if state.compact != compact { - state.compact = compact; - Some(Self::Compact(!compact)) - } else { - None - }, -}); diff --git a/src/groovebox/groovebox_audio.rs b/src/groovebox/groovebox_audio.rs new file mode 100644 index 00000000..812e0fb6 --- /dev/null +++ b/src/groovebox/groovebox_audio.rs @@ -0,0 +1,36 @@ +use crate::*; +use super::*; + + +audio!(|self: Groovebox, client, scope|{ + let t0 = self.perf.get_t0(); + if Control::Quit == ClockAudio(&mut self.player).process(client, scope) { + return Control::Quit + } + if Control::Quit == PlayerAudio( + &mut self.player, &mut self.note_buf, &mut self.midi_buf + ).process(client, scope) { + return Control::Quit + } + if Control::Quit == SamplerAudio(&mut self.sampler).process(client, scope) { + return Control::Quit + } + // TODO move these to editor and sampler: + for RawMidi { time, bytes } in self.player.midi_ins[0].iter(scope) { + if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { + match message { + MidiMessage::NoteOn { ref key, .. } => { + self.editor.set_note_point(key.as_int() as usize); + }, + MidiMessage::Controller { controller, value } => { + if let Some(sample) = &self.sampler.mapped[self.editor.note_point()] { + sample.write().unwrap().handle_cc(controller, value) + } + } + _ => {} + } + } + } + self.perf.update(t0, scope); + Control::Continue +}); diff --git a/src/groovebox/groovebox_command.rs b/src/groovebox/groovebox_command.rs new file mode 100644 index 00000000..5ab59b3b --- /dev/null +++ b/src/groovebox/groovebox_command.rs @@ -0,0 +1,91 @@ +use crate::*; +use super::*; + +use self::GrooveboxCommand as Cmd; +use ClockCommand::{Play, Pause}; + +pub enum GrooveboxCommand { + Compact(bool), + History(isize), + Clock(ClockCommand), + Pool(PoolCommand), + Editor(MidiEditCommand), + Enqueue(Option>>), + Sampler(SamplerCommand), +} + +command!(|self: GrooveboxCommand, state: Groovebox|match self { + Self::Enqueue(phrase) => { + state.player.enqueue_next(phrase.as_ref()); + None + }, + Self::Pool(cmd) => match cmd { + // autoselect: automatically load selected phrase in editor + PoolCommand::Select(_) => { + let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + state.editor.set_phrase(Some(state.pool.phrase())); + undo + }, + // update color in all places simultaneously + PoolCommand::Phrase(SetColor(index, _)) => { + let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + state.editor.set_phrase(Some(state.pool.phrase())); + undo + }, + _ => cmd.delegate(&mut state.pool, Self::Pool)? + }, + Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?, + Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, + Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, + Self::History(delta) => { todo!("undo/redo") }, + Self::Compact(compact) => if state.compact != compact { + state.compact = compact; + Some(Self::Compact(!compact)) + } else { + None + }, +}); + +handle!(|self: Groovebox, input|GrooveboxCommand::execute_with_state(self, input.event())); +keymap!(KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand { + // Tab: Toggle compact mode + key(Tab) => Cmd::Compact(!state.compact), + // q: Enqueue currently edited phrase + key(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())), + // 0: Enqueue phrase 0 (stop all) + key(Char('0')) => Cmd::Enqueue(Some(state.pool.phrases()[0].clone())), + // TODO: k: toggle on-screen keyboard + ctrl(key(Char('k'))) => todo!("keyboard"), + // Transport: Play from start or rewind to start + ctrl(key(Char(' '))) => Cmd::Clock( + if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } + ), + // Shift-R: toggle recording + shift(key(Char('R'))) => Cmd::Sampler(if state.sampler.recording.is_some() { + SamplerCommand::RecordFinish + } else { + SamplerCommand::RecordBegin(u7::from(state.editor.note_point() as u8)) + }), + // Shift-Del: delete sample + shift(key(Delete)) => Cmd::Sampler( + SamplerCommand::SetSample(u7::from(state.editor.note_point() as u8), None) + ), + // e: Toggle between editing currently playing or other phrase + shift(key(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.pool.phrase().clone(); + Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing { + selected + } else { + playing.clone() + }))) + } else { + return None + }, +}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { + Cmd::Editor(command) +} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { + Cmd::Pool(command) +} else { + return None +}); diff --git a/src/groovebox/groovebox_tui.rs b/src/groovebox/groovebox_tui.rs new file mode 100644 index 00000000..f1b564a8 --- /dev/null +++ b/src/groovebox/groovebox_tui.rs @@ -0,0 +1,86 @@ +use crate::*; +use super::*; + +/* +render!(Tui: (self: Groovebox) => self.size.of( + self.toolbar_view() + .south(self.selector_view()) + .south(self.sample_view()) + .north(self.status_view()) + .north(self.pool_view()) + .west(Fill::xy(self.sampler_view().east(self.editor))))); +*/ + +/* +render!(Tui: (self: Groovebox) => self.size.of( + self.editor + .east_of(self.sample_view()) + .wrap(Fill::xy) + .west_of(self.pool_view()) + .north_of(self.status_view()) + .north_of(self.sample_view()) + .south_of(self.selector_view()) + .south_of(self.toolbar_view()) +*/ + +/* +render!(Tui: (self: Groovebox) => self.size.of( + self.toolbar_view().north_of( + self.selector_view().north_of( + self.sample_view().south_of( + self.status_view().south_of( + self.pool_view().east_of( + Fill::xy(self.sampler_view().west_of( + self.editor))))))))); +*/ + +render!(Tui: (self: Groovebox) => self.size.of( + Bsp::s(self.toolbar_view(), + Bsp::s(self.selector_view(), + Bsp::n(self.sample_view(), + Bsp::n(self.status_view(), + Bsp::w(self.pool_view(), Fill::xy(Bsp::e(self.sampler_view(), &self.editor))))))))); + +impl Groovebox { + fn toolbar_view (&self) -> impl Content + use<'_> { + Fill::x(Fixed::y(2, lay!( + Align::w(Meter("L/", self.sampler.input_meter[0])), + Align::e(Meter("R/", self.sampler.input_meter[1])), + Align::x(Tui::bg(TuiTheme::g(32), TransportView::new(true, &self.player.clock))), + ))) + } + fn status_view (&self) -> impl Content + use<'_> { + let note_pt = self.editor.note_point(); + lay!( + Align::w(Fixed::y(1, SamplerStatus(&self.sampler, note_pt))), + Align::x(Fixed::y(1, MidiEditStatus(&self.editor))), + ) + } + fn sampler_view (&self) -> impl Content + use<'_> { + let sampler_w = if !self.compact { 11 } else { 4 }; + let sampler_y = if self.compact { 1 } else { 0 }; + Fixed::x(sampler_w, Push::y(sampler_y, Fill::y( + SampleList::new(self.compact, &self.sampler, &self.editor)))) + } + fn sample_view (&self) -> impl Content + use<'_> { + let note_pt = self.editor.note_point(); + let sample_h = if self.compact { 0 } else { 5 }; + Max::y(sample_h, Fill::xy( + SampleViewer::from_sampler(&self.sampler, note_pt))) + } + fn pool_view (&self) -> impl Content + use<'_> { + let w = self.size.w(); + let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; + Fixed::x(if self.compact { 5 } else { pool_w }, + PoolView(self.compact, &self.pool)) + } + fn selector_view (&self) -> impl Content + use<'_> { + lay!( + Align::w(MidiEditClip(&self.editor)), + Align::e(Bsp::e( + ClipSelected::play_phrase(&self.player), + ClipSelected::next_phrase(&self.player) + )) + ) + } +}