From 92459b5f823b1ce8c12830b1352965455704fd1d Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 2 Jan 2025 15:32:49 +0100 Subject: [PATCH] transport compact mode --- src/arranger.rs | 17 ++++--- src/clock/clock_tui.rs | 92 +++++++++++++++++++++++++++----------- src/groovebox.rs | 42 ++++------------- src/sampler.rs | 4 ++ src/sampler/sample_list.rs | 34 ++++++++++++++ src/sequencer.rs | 28 ++++++------ 6 files changed, 134 insertions(+), 83 deletions(-) create mode 100644 src/sampler/sample_list.rs diff --git a/src/arranger.rs b/src/arranger.rs index 418abbf9..15f85078 100644 --- a/src/arranger.rs +++ b/src/arranger.rs @@ -105,14 +105,13 @@ impl ArrangerTui { } } render!(Tui: (self: ArrangerTui) => { - let play = PlayPause(self.clock.is_rolling()); - let pool_size = if self.phrases.visible { self.splits[1] } else { 0 }; - let with_pool = |x|Bsp::w(Fixed::x(pool_size, PoolView(&self.phrases)), x); - let status = ArrangerStatus::from(self); - let with_editbar = |x|Bsp::n(Fixed::y(1, MidiEditStatus(&self.editor)), x); - let with_status = |x|Bsp::n(Fixed::y(2, status), x); - let with_size = |x|lay!(&self.size, x); - let arranger = ||{ + let pool_size = if self.phrases.visible { self.splits[1] } else { 0 }; + let with_pool = |x|Bsp::w(Fixed::x(pool_size, PoolView(&self.phrases)), x); + let status = ArrangerStatus::from(self); + let with_editbar = |x|Bsp::n(Fixed::y(1, MidiEditStatus(&self.editor)), x); + let with_status = |x|Bsp::n(Fixed::y(2, status), x); + let with_size = |x|lay!(&self.size, x); + let arranger = ||{ let color = self.color; lay!( Fill::xy(Tui::bg(color.darkest.rgb, "")), @@ -121,7 +120,7 @@ render!(Tui: (self: ArrangerTui) => { ) }; with_size(with_status(with_editbar(with_pool(col!( - TransportView(&self.clock), + TransportView::new(true, &self.clock), Fill::x(Fixed::y(20, arranger())), Fill::xy(&self.editor), ))))) diff --git a/src/clock/clock_tui.rs b/src/clock/clock_tui.rs index 035e5eb7..c3a6d747 100644 --- a/src/clock/clock_tui.rs +++ b/src/clock/clock_tui.rs @@ -11,7 +11,10 @@ pub struct TransportTui { has_clock!(|self: TransportTui|&self.clock); audio!(|self: TransportTui, client, scope|ClockAudio(self).process(client, scope)); handle!(|self: TransportTui, from|TransportCommand::execute_with_state(self, from)); -render!(Tui: (self: TransportTui) => TransportView(&self.clock)); +render!(Tui: (self: TransportTui) => TransportView { + compact: false, + clock: &self.clock +}); impl TransportTui { pub fn new (jack: &Arc>) -> Usually { Ok(Self { @@ -21,25 +24,43 @@ impl TransportTui { } } -pub struct TransportView<'a>(pub &'a Clock); +pub struct TransportView<'a> { pub compact: bool, pub clock: &'a Clock } +impl<'a> TransportView<'a> { + pub fn new (compact: bool, clock: &'a Clock) -> Self { + Self { compact, clock } + } +} render!(Tui: (self: TransportView<'a>) => Outer( Style::default().fg(TuiTheme::g(255)).bg(TuiTheme::g(0)) ).enclose(row!( - BeatStats::new(self.0), " ", - PlayPause(self.0.is_rolling()), " ", - OutputStats::new(self.0), + BeatStats::new(self.compact, self.clock), " ", + PlayPause { compact: self.compact, playing: self.clock.is_rolling() }, " ", + OutputStats::new(self.compact, self.clock), ))); -pub struct PlayPause(pub bool); -render!(Tui: (self: PlayPause) => Tui::bg( - if self.0{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)}, - Fixed::x(5, Tui::either(self.0, - Tui::fg(Color::Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - Tui::fg(Color::Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))))); +struct Field<'a>(ItemPalette, &'a str, &'a str); +render!(Tui: (self: Field<'a>) => row!( + Tui::bg(self.0.darkest.rgb, Tui::fg(self.0.darker.rgb, "▐")), + Tui::bg(self.0.darker.rgb, Tui::fg(self.0.lighter.rgb, + Tui::bold(true, format!("{}", self.1)))), + Tui::bg(self.0.darkest.rgb, Tui::fg(self.0.darker.rgb, "▌")), + Tui::bg(self.0.darkest.rgb, Tui::fg(self.0.lightest.rgb, + Tui::bold(true, format!("{} ", self.2)))))); -pub struct BeatStats { bpm: String, beat: String, time: String, } +pub struct PlayPause { pub compact: bool, pub playing: bool } +render!(Tui: (self: PlayPause) => Tui::bg( + if self.playing{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)}, + Tui::either(self.compact, + Thunk::new(||Fixed::x(9, Tui::either(self.playing, + Tui::fg(Color::Rgb(0, 255, 0), " PLAYING "), + Tui::fg(Color::Rgb(255, 128, 0), " STOPPED ")))), + Thunk::new(||Fixed::x(5, Tui::either(self.playing, + Tui::fg(Color::Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), + Tui::fg(Color::Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))))))); + +pub struct BeatStats { compact: bool, bpm: String, beat: String, time: String, } impl BeatStats { - fn new (clock: &Clock) -> Self { + fn new (compact: bool, clock: &Clock) -> Self { let (beat, time) = clock.started.read().unwrap().as_ref().map(|started|{ let current_usec = clock.global.usec.get() - started.usec.get(); ( @@ -47,32 +68,49 @@ impl BeatStats { format!("{:.3}s", current_usec/1000000.) ) }).unwrap_or_else(||("-.-.--".to_string(), "-.---s".to_string())); - Self { bpm: format!("{:.3}", clock.timebase.bpm.get()), beat, time } + Self { compact, bpm: format!("{:.3}", clock.timebase.bpm.get()), beat, time } } } -render!(Tui: (self: BeatStats) => col!( - Bsp::e(Tui::fg(TuiTheme::g(255), &self.bpm), " BPM"), - Bsp::e("Beat ", Tui::fg(TuiTheme::g(255), &self.beat)), - Bsp::e("Time ", Tui::fg(TuiTheme::g(255), &self.time)), -)); +render!(Tui: (self: BeatStats) => Tui::either(self.compact, + row!( + Field(TuiTheme::g(128).into(), "BPM", &self.bpm), + Field(TuiTheme::g(128).into(), "Beat", &self.beat), + Field(TuiTheme::g(128).into(), "Time", &self.time), + ), + col!( + Bsp::e(Tui::fg(TuiTheme::g(255), &self.bpm), " BPM"), + Bsp::e("Beat ", Tui::fg(TuiTheme::g(255), &self.beat)), + Bsp::e("Time ", Tui::fg(TuiTheme::g(255), &self.time)), + ))); -pub struct OutputStats { sample_rate: String, buffer_size: String, latency: String, } +pub struct OutputStats { compact: bool, sample_rate: String, buffer_size: String, latency: String, } impl OutputStats { - fn new (clock: &Clock) -> Self { + fn new (compact: bool, clock: &Clock) -> Self { let rate = clock.timebase.sr.get(); let chunk = clock.chunk.load(Relaxed); Self { - sample_rate: format!("{:.1}Hz", rate), + compact, + sample_rate: if compact { + format!("{:.1}kHz", rate / 1000.) + } else { + format!("{:.0}Hz", rate) + }, buffer_size: format!("{chunk}"), latency: format!("{}", chunk as f64 / rate * 1000.), } } } -render!(Tui: (self: OutputStats) => col!( - Bsp::e(Tui::fg(TuiTheme::g(255), format!("{}", self.sample_rate)), " sample rate"), - Bsp::e(Tui::fg(TuiTheme::g(255), format!("{}", self.buffer_size)), " sample buffer"), - Bsp::e(Tui::fg(TuiTheme::g(255), format!("{:.3}ms", self.latency)), " latency"), -)); +render!(Tui: (self: OutputStats) => Tui::either(self.compact, + row!( + Field(TuiTheme::g(128).into(), "SR", &self.sample_rate), + Field(TuiTheme::g(128).into(), "Buf", &self.buffer_size), + Field(TuiTheme::g(128).into(), "Lat", &self.latency), + ), + col!( + Bsp::e(Tui::fg(TuiTheme::g(255), format!("{}", self.sample_rate)), " sample rate"), + Bsp::e(Tui::fg(TuiTheme::g(255), format!("{}", self.buffer_size)), " sample buffer"), + Bsp::e(Tui::fg(TuiTheme::g(255), format!("{:.3}ms", self.latency)), " latency"), + ))); #[derive(Clone, Debug, PartialEq)] pub enum TransportCommand { diff --git a/src/groovebox.rs b/src/groovebox.rs index 442dbb4d..6196f87e 100644 --- a/src/groovebox.rs +++ b/src/groovebox.rs @@ -108,11 +108,16 @@ render!(Tui: (self: Groovebox) => { let color = self.player.play_phrase().as_ref() .and_then(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)) .clone(); + let sampler = Align::w(Fill::y(SampleList::new(&self.sampler, &self.editor))); + let selector = Bsp::e(PhraseSelector::play_phrase(&self.player), PhraseSelector::next_phrase(&self.player)); self.size.of(Bsp::s( - Fill::x(Fixed::y(3, lay!( + Fill::x(Fixed::y(if self.pool.visible { 3 } else { 1 }, lay!( Align::w(Meter("L/", self.sampler.input_meter[0])), - Align::x(TransportView(&self.player.clock)), Align::e(Meter("R/", self.sampler.input_meter[1])), + Align::x(Tui::bg(TuiTheme::g(32), TransportView::new( + !self.pool.visible, + &self.player.clock + ))), ))), Bsp::n( Bsp::s( @@ -135,12 +140,9 @@ render!(Tui: (self: Groovebox) => { Bsp::w( Fixed::x(pool_w, Align::e(Fill::y(PoolView(&self.pool)))), Fill::xy(Bsp::e( - Fixed::x(sampler_w, Align::w(Fill::y(GrooveboxSamples(self)))), + Fixed::x(sampler_w, sampler), Bsp::s( - Fill::x(Align::c(Bsp::e( - PhraseSelector::play_phrase(&self.player), - PhraseSelector::next_phrase(&self.player), - ))), + selector, Bsp::n( MidiEditStatus(&self.editor), &self.editor, @@ -152,32 +154,6 @@ render!(Tui: (self: Groovebox) => { )) }); -// TODO move this to sampler -struct GrooveboxSamples<'a>(&'a Groovebox); -render!(Tui: (self: GrooveboxSamples<'a>) => { - let note_lo = self.0.editor.note_lo().load(Relaxed); - let note_pt = self.0.editor.note_point(); - let note_hi = self.0.editor.note_hi(); - Fill::xy(Tui::map(move||(note_lo..=note_hi).rev(), move|note, i| { - let mut bg = if note == note_pt { TuiTheme::g(64) } else { Color::Reset }; - let mut fg = TuiTheme::g(160); - if let Some((index, _)) = self.0.sampler.recording { - if note == index { - bg = Color::Rgb(64,16,0); - fg = Color::Rgb(224,64,32) - } - } else if self.0.sampler.mapped[note].is_some() { - fg = TuiTheme::g(224); - } - let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a)))); - offset(Tui::bg(bg, if let Some(sample) = &self.0.sampler.mapped[note] { - Tui::fg(fg, format!("{note:3} ?????? ")) - } else { - Tui::fg(fg, format!("{note:3} (none) ")) - })) - })) -}); - pub enum GrooveboxCommand { History(isize), Clock(ClockCommand), diff --git a/src/sampler.rs b/src/sampler.rs index d11aa888..3e0ec2e7 100644 --- a/src/sampler.rs +++ b/src/sampler.rs @@ -28,6 +28,10 @@ pub use self::sampler_tui::SamplerTui; pub mod sample_import; pub(crate) use self::sample_import::*; +pub mod sample_list; +pub(crate) use self::sample_list::*; +pub use self::sample_list::SampleList; + pub mod sample_viewer; pub(crate) use self::sample_viewer::*; pub use self::sample_viewer::SampleViewer; diff --git a/src/sampler/sample_list.rs b/src/sampler/sample_list.rs new file mode 100644 index 00000000..253db240 --- /dev/null +++ b/src/sampler/sample_list.rs @@ -0,0 +1,34 @@ +use crate::*; + +pub struct SampleList<'a>(&'a Sampler, &'a MidiEditor); + +impl<'a> SampleList<'a> { + pub fn new (sampler: &'a Sampler, editor: &'a MidiEditor) -> Self { + Self(sampler, editor) + } +} + +render!(Tui: (self: SampleList<'a>) => { + let Self(sampler, editor) = self; + let note_lo = editor.note_lo().load(Relaxed); + let note_pt = editor.note_point(); + let note_hi = editor.note_hi(); + Fill::xy(Tui::map(move||(note_lo..=note_hi).rev(), move|note, i| { + let mut bg = if note == note_pt { TuiTheme::g(64) } else { Color::Reset }; + let mut fg = TuiTheme::g(160); + if let Some((index, _)) = sampler.recording { + if note == index { + bg = Color::Rgb(64,16,0); + fg = Color::Rgb(224,64,32) + } + } else if sampler.mapped[note].is_some() { + fg = TuiTheme::g(224); + } + let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a)))); + offset(Tui::bg(bg, if let Some(sample) = &sampler.mapped[note] { + Tui::fg(fg, format!("{note:3} ?????? ")) + } else { + Tui::fg(fg, format!("{note:3} (none) ")) + })) + })) +}); diff --git a/src/sequencer.rs b/src/sequencer.rs index 530660e7..3e07e3de 100644 --- a/src/sequencer.rs +++ b/src/sequencer.rs @@ -10,7 +10,7 @@ pub struct SequencerTui { pub transport: bool, pub selectors: bool, pub clock: Clock, - pub phrases: PoolModel, + pub pool: PoolModel, pub player: MidiPlayer, pub editor: MidiEditor, pub size: Measure, @@ -29,7 +29,7 @@ from_jack!(|jack|SequencerTui { _jack: jack.clone(), transport: true, selectors: true, - phrases: PoolModel::from(&phrase), + pool: PoolModel::from(&phrase), editor: MidiEditor::from(&phrase), player: MidiPlayer::from((&clock, &phrase)), size: Measure::new(), @@ -43,8 +43,8 @@ from_jack!(|jack|SequencerTui { render!(Tui: (self: SequencerTui) => { let w = self.size.w(); let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - let pool_w = if self.phrases.visible { phrase_w } else { 0 }; - let pool = Pull::y(1, Fill::y(Align::e(PoolView(&self.phrases)))); + let pool_w = if self.pool.visible { phrase_w } else { 0 }; + let pool = Pull::y(1, Fill::y(Align::e(PoolView(&self.pool)))); let with_pool = move|x|Bsp::w(Fixed::x(pool_w, pool), x); let status = SequencerStatus::from(self); let with_status = |x|Bsp::n(Fixed::x(if self.status { 2 } else { 0 }, status), x); @@ -56,7 +56,7 @@ render!(Tui: (self: SequencerTui) => { p.as_ref().map(|p|p.read().unwrap().color) ).flatten().clone(); - let toolbar = Tui::when(self.transport, TransportView(&self.clock)); + let toolbar = Tui::when(self.transport, TransportView::new(true, &self.clock)); let play_queue = Tui::when(self.selectors, row!( PhraseSelector::play_phrase(&self.player), @@ -88,7 +88,7 @@ audio!(|self:SequencerTui, client, scope|{ }); has_size!(|self:SequencerTui|&self.size); has_clock!(|self:SequencerTui|&self.clock); -has_phrases!(|self:SequencerTui|self.phrases.phrases); +has_phrases!(|self:SequencerTui|self.pool.phrases); has_editor!(|self:SequencerTui|self.editor); handle!(|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input)); #[derive(Clone, Debug)] pub enum SequencerCommand { @@ -114,15 +114,15 @@ input_to_command!(SequencerCommand: |state: SequencerTui, input|match input // Shift-U: redo key_pat!(Char('U')) => Cmd::History( 1), // Tab: Toggle visibility of phrase pool column - key_pat!(Tab) => Cmd::Pool(PoolCommand::Show(!state.phrases.visible)), + key_pat!(Tab) => Cmd::Pool(PoolCommand::Show(!state.pool.visible)), // q: Enqueue currently edited phrase - key_pat!(Char('q')) => Cmd::Enqueue(Some(state.phrases.phrase().clone())), + key_pat!(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())), // 0: Enqueue phrase 0 (stop all) - key_pat!(Char('0')) => Cmd::Enqueue(Some(state.phrases.phrases()[0].clone())), + key_pat!(Char('0')) => Cmd::Enqueue(Some(state.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(); + let selected = state.pool.phrase().clone(); Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing { selected } else { @@ -135,7 +135,7 @@ input_to_command!(SequencerCommand: |state: SequencerTui, input|match input // The ones defined above supersede them. _ => if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { Cmd::Editor(command) - } else if let Some(command) = PoolCommand::input_to_command(&state.phrases, input) { + } else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { Cmd::Pool(command) } else { return None @@ -144,19 +144,19 @@ input_to_command!(SequencerCommand: |state: SequencerTui, input|match input command!(|self: SequencerCommand, state: SequencerTui|match self { Self::Pool(cmd) => { let mut default = |cmd: PoolCommand|cmd - .execute(&mut state.phrases) + .execute(&mut state.pool) .map(|x|x.map(Cmd::Pool)); match cmd { // autoselect: automatically load selected phrase in editor PoolCommand::Select(_) => { let undo = default(cmd)?; - state.editor.set_phrase(Some(state.phrases.phrase())); + state.editor.set_phrase(Some(state.pool.phrase())); undo }, // update color in all places simultaneously PoolCommand::Phrase(SetColor(index, _)) => { let undo = default(cmd)?; - state.editor.set_phrase(Some(state.phrases.phrase())); + state.editor.set_phrase(Some(state.pool.phrase())); undo }, _ => default(cmd)?