From 44c28183de02cd8a20f62a37c7322fae47227d84 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 2 Jan 2025 17:20:37 +0100 Subject: [PATCH] wip: zoom lock --- src/clock/clock_tui.rs | 8 +++--- src/groovebox.rs | 20 +++++++------- src/midi/midi_editor.rs | 29 ++++----------------- src/midi/midi_note.rs | 6 ++--- src/midi/midi_status.rs | 14 ++++++++++ src/midi/midi_view.rs | 52 ++++++++++++++++++++++++++++++++----- src/piano.rs | 4 +-- src/piano/piano_h.rs | 51 ++++++++++++------------------------ src/piano/piano_h_notes.rs | 2 +- src/pool/phrase_selector.rs | 6 ++--- src/sequencer.rs | 4 +-- 11 files changed, 107 insertions(+), 89 deletions(-) diff --git a/src/clock/clock_tui.rs b/src/clock/clock_tui.rs index 2584c064..31e0be75 100644 --- a/src/clock/clock_tui.rs +++ b/src/clock/clock_tui.rs @@ -33,8 +33,10 @@ impl<'a> TransportView<'a> { render!(Tui: (self: TransportView<'a>) => Outer( Style::default().fg(TuiTheme::g(255)).bg(TuiTheme::g(0)) ).enclose(row!( - BeatStats::new(self.compact, self.clock), " ", - PlayPause { compact: self.compact, playing: self.clock.is_rolling() }, " ", + BeatStats::new(self.compact, self.clock), + " ", + PlayPause { compact: self.compact, playing: self.clock.is_rolling() }, + " ", OutputStats::new(self.compact, self.clock), ))); @@ -87,7 +89,7 @@ impl OutputStats { format!("{:.0}Hz", rate) }, buffer_size: format!("{chunk}"), - latency: format!("{:.3}ms", chunk as f64 / rate * 1000.), + latency: format!("{:.1}ms", chunk as f64 / rate * 1000.), } } } diff --git a/src/groovebox.rs b/src/groovebox.rs index 54a9b237..72ca8522 100644 --- a/src/groovebox.rs +++ b/src/groovebox.rs @@ -109,7 +109,8 @@ render!(Tui: (self: Groovebox) => { .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)); + let selectors = Bsp::e(ClipSelected::play_phrase(&self.player), ClipSelected::next_phrase(&self.player)); + let edit_clip = MidiEditClip(&self.editor); self.size.of(Bsp::s( Fill::x(Fixed::y(if self.pool.visible { 3 } else { 1 }, lay!( Align::w(Meter("L/", self.sampler.input_meter[0])), @@ -139,7 +140,10 @@ render!(Tui: (self: Groovebox) => { Fixed::x(pool_w, Align::e(Fill::y(PoolView(&self.pool)))), Fill::xy(Bsp::e( Fixed::x(sampler_w, Push::y(3, sampler)), - Bsp::s(selector, &self.editor), + Bsp::s( + lay!(Align::w(edit_clip), Align::e(selectors)), + &self.editor + ), )), ), ) @@ -232,13 +236,7 @@ command!(|self: GrooveboxCommand, state: Groovebox|match self { _ => cmd.delegate(&mut state.pool, Self::Pool)? } }, - Self::Editor(cmd) => { - cmd.delegate(&mut state.editor, Self::Editor)? - }, - Self::Clock(cmd) => { - cmd.delegate(state, Self::Clock)? - }, - Self::Sampler(cmd) => { - cmd.delegate(&mut state.sampler, Self::Sampler)? - }, + 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)?, }); diff --git a/src/midi/midi_editor.rs b/src/midi/midi_editor.rs index deb2a508..84bc0662 100644 --- a/src/midi/midi_editor.rs +++ b/src/midi/midi_editor.rs @@ -48,8 +48,6 @@ render!(Tui: (self: MidiEditor) => { Fill::xy(Bsp::b(&self.size, &self.mode)) }); -impl MidiView for MidiEditor {} - impl TimeRange for MidiEditor { fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } @@ -79,7 +77,7 @@ impl MidiViewMode for MidiEditor { fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize) { self.mode.buffer_size(phrase) } - fn redraw (&mut self) { + fn redraw (&self) { self.mode.redraw() } fn phrase (&self) -> &Option>> { @@ -126,23 +124,6 @@ impl MidiEditor { } } -pub trait MidiViewMode: HasSize + MidiRange + MidiPoint + Debug + Send + Sync { - fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize); - fn redraw (&mut self); - fn phrase (&self) -> &Option>>; - fn phrase_mut (&mut self) -> &mut Option>>; - fn set_phrase (&mut self, phrase: Option<&Arc>>) { - *self.phrase_mut() = phrase.cloned(); - self.redraw(); - } -} - -impl Content for Box { - fn content (&self) -> impl Content { - Some(&(*self)) - } -} - impl std::fmt::Debug for MidiEditor { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("MidiEditor") @@ -191,10 +172,10 @@ impl MidiEditor { (kexp!(Right), &|s: &Self|SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length())), (kexp!(Char('d')), &|s: &Self|SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length())), (kexp!(Char('z')), &|s: &Self|SetTimeLock(!s.time_lock().get())), - (kexp!(Char('-')), &|s: &Self|SetTimeZoom(Note::next(s.time_zoom().get()))), - (kexp!(Char('_')), &|s: &Self|SetTimeZoom(Note::next(s.time_zoom().get()))), - (kexp!(Char('=')), &|s: &Self|SetTimeZoom(Note::prev(s.time_zoom().get()))), - (kexp!(Char('+')), &|s: &Self|SetTimeZoom(Note::prev(s.time_zoom().get()))), + (kexp!(Char('-')), &|s: &Self|SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { Note::next(s.time_zoom().get()) })), + (kexp!(Char('_')), &|s: &Self|SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { Note::next(s.time_zoom().get()) })), + (kexp!(Char('=')), &|s: &Self|SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { Note::prev(s.time_zoom().get()) })), + (kexp!(Char('+')), &|s: &Self|SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { Note::prev(s.time_zoom().get()) })), (kexp!(Enter), &|s: &Self|PutNote), (kexp!(Ctrl-Enter), &|s: &Self|AppendNote), (kexp!(Char(',')), &|s: &Self|SetNoteLength(Note::prev(s.note_len()))), // TODO: no 3plet diff --git a/src/midi/midi_note.rs b/src/midi/midi_note.rs index b6b370cf..e49d19df 100644 --- a/src/midi/midi_note.rs +++ b/src/midi/midi_note.rs @@ -41,16 +41,16 @@ impl Note { ]; /// Returns the next shorter length pub fn prev (pulses: usize) -> usize { - for i in 1..=16 { let length = Note::DURATIONS[16-i].0; if length < pulses { return length } } + for (length, _) in Self::DURATIONS.iter().rev() { if *length < pulses { return *length } } pulses } /// Returns the next longer length pub fn next (pulses: usize) -> usize { - for (length, _) in &Note::DURATIONS { if *length > pulses { return *length } } + for (length, _) in Self::DURATIONS.iter() { if *length > pulses { return *length } } pulses } pub fn pulses_to_name (pulses: usize) -> &'static str { - for (length, name) in &Note::DURATIONS { if *length == pulses { return name } } + for (length, name) in Self::DURATIONS.iter() { if *length == pulses { return name } } "" } } diff --git a/src/midi/midi_status.rs b/src/midi/midi_status.rs index ae0af245..8fc8a74d 100644 --- a/src/midi/midi_status.rs +++ b/src/midi/midi_status.rs @@ -1,5 +1,19 @@ use crate::*; +pub struct MidiEditClip<'a>(pub &'a MidiEditor); +render!(Tui: (self: MidiEditClip<'a>) => { + let (color, name, length, looped) = if let Some(phrase) = self.0.phrase().as_ref().map(|p|p.read().unwrap()) { + (phrase.color, phrase.name.clone(), phrase.length, phrase.looped) + } else { + (ItemPalette::from(TuiTheme::g(64)), String::new(), 0, false) + }; + Fixed::y(1, row!( + Field(color, "Edit", name.to_string()), + Field(color, "Length", length.to_string()), + Field(color, "Loop", looped.to_string()) + )) +}); + pub struct MidiEditStatus<'a>(pub &'a MidiEditor); render!(Tui: (self: MidiEditStatus<'a>) => { let (color, name, length, looped) = if let Some(phrase) = self.0.phrase().as_ref().map(|p|p.read().unwrap()) { diff --git a/src/midi/midi_view.rs b/src/midi/midi_view.rs index 0d617adb..47573902 100644 --- a/src/midi/midi_view.rs +++ b/src/midi/midi_view.rs @@ -1,7 +1,15 @@ use crate::*; -pub trait MidiView: MidiRange + MidiPoint + HasSize { - /// Make sure cursor is within range +pub trait MidiViewMode: HasSize + MidiRange + MidiPoint + Debug + Send + Sync { + fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize); + fn redraw (&self); + fn phrase (&self) -> &Option>>; + fn phrase_mut (&mut self) -> &mut Option>>; + fn set_phrase (&mut self, phrase: Option<&Arc>>) { + *self.phrase_mut() = phrase.cloned(); + self.redraw(); + } + /// Make sure cursor is within note range fn autoscroll (&self) { let note_point = self.note_point().min(127); let note_lo = self.note_lo().get(); @@ -12,11 +20,43 @@ pub trait MidiView: MidiRange + MidiPoint + HasSize { self.note_lo().set((note_lo + note_point).saturating_sub(note_hi)); } } - /// Make sure range is within display + /// Make sure time range is within display fn autozoom (&self) { - let time_len = self.time_len().get(); - let time_axis = self.time_axis().get(); - let time_zoom = self.time_zoom().get(); + if self.time_lock().get() { + let time_len = self.time_len().get(); + let time_axis = self.time_axis().get(); + let time_zoom = self.time_zoom().get(); + loop { + let time_zoom = self.time_zoom().get(); + let time_area = time_axis * time_zoom; + if time_area > time_len { + let next_time_zoom = Note::prev(time_zoom); + if next_time_zoom <= 1 { + break + } + let next_time_area = time_axis * next_time_zoom; + if next_time_area >= time_len { + self.time_zoom().set(next_time_zoom); + } else { + break + } + } else if time_area < time_len { + let prev_time_zoom = Note::next(time_zoom); + if prev_time_zoom > 384 { + break + } + let prev_time_area = time_axis * prev_time_zoom; + if prev_time_area <= time_len { + self.time_zoom().set(prev_time_zoom); + } else { + break + } + } + } + if time_zoom != self.time_zoom().get() { + self.redraw() + } + } //while time_len.div_ceil(time_zoom) > time_axis { //println!("\r{time_len} {time_zoom} {time_axis}"); //time_zoom = Note::next(time_zoom); diff --git a/src/piano.rs b/src/piano.rs index 6590f04a..92a9485a 100644 --- a/src/piano.rs +++ b/src/piano.rs @@ -11,7 +11,7 @@ mod piano_h_time; pub(crate) use self::piano_h_time::*; pub struct PianoHorizontal { phrase: Option>>, /// Buffer where the whole phrase is rerendered on change - buffer: BigBuffer, + buffer: Arc>, /// Size of actual notes area size: Measure, /// The display window @@ -34,7 +34,7 @@ impl PianoHorizontal { keys_width: 5, size, range, - buffer: Default::default(), + buffer: RwLock::new(Default::default()).into(), point: MidiPointModel::default(), phrase: phrase.cloned(), color: phrase.as_ref() diff --git a/src/piano/piano_h.rs b/src/piano/piano_h.rs index b3dd5894..53427c66 100644 --- a/src/piano/piano_h.rs +++ b/src/piano/piano_h.rs @@ -5,37 +5,19 @@ pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iter (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) } -render!(Tui: (self: PianoHorizontal) => { - let (color, name, length, looped) = if let Some(phrase) = self.phrase().as_ref().map(|p|p.read().unwrap()) { - (phrase.color, phrase.name.clone(), phrase.length, phrase.looped) - } else { - (ItemPalette::from(TuiTheme::g(64)), String::new(), 0, false) - }; - let field = move|x, y|row!( - Tui::fg_bg(color.lighter.rgb, color.darker.rgb, Tui::bold(true, x)), - Tui::fg_bg(color.lightest.rgb, color.dark.rgb, format!(" {y} ")), - ); - Bsp::s( - Fixed::y(1, row!( - field(" Edit ", name.to_string()), " ", - field(" Length ", length.to_string()), " ", - field(" Loop ", looped.to_string()) - )), - Bsp::s( - Fixed::y(1, Bsp::e( - Fixed::x(self.keys_width, ""), - Fill::x(PianoHorizontalTimeline(self)), - )), - Fill::xy(Bsp::e( - Fixed::x(self.keys_width, PianoHorizontalKeys(self)), - Fill::xy(self.size.of(lay!( - Fill::xy(PianoHorizontalNotes(self)), - Fill::xy(PianoHorizontalCursor(self)), - ))), - )), - ) - ) -}); +render!(Tui: (self: PianoHorizontal) => Bsp::s( + Fixed::y(1, Bsp::e( + Fixed::x(self.keys_width, ""), + Fill::x(PianoHorizontalTimeline(self)), + )), + Fill::xy(Bsp::e( + Fixed::x(self.keys_width, PianoHorizontalKeys(self)), + Fill::xy(self.size.of(lay!( + Fill::xy(PianoHorizontalNotes(self)), + Fill::xy(PianoHorizontalCursor(self)), + ))), + )), +)); impl PianoHorizontal { /// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄ @@ -131,7 +113,7 @@ impl MidiViewMode for PianoHorizontal { fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize) { (phrase.length / self.range.time_zoom().get(), 128) } - fn redraw (&mut self) { + fn redraw (&self) { let buffer = if let Some(phrase) = self.phrase.as_ref() { let phrase = phrase.read().unwrap(); let buf_size = self.buffer_size(&phrase); @@ -145,7 +127,7 @@ impl MidiViewMode for PianoHorizontal { } else { Default::default() }; - self.buffer = buffer + *self.buffer.write().unwrap() = buffer } fn set_phrase (&mut self, phrase: Option<&Arc>>) { *self.phrase_mut() = phrase.cloned(); @@ -157,9 +139,10 @@ impl MidiViewMode for PianoHorizontal { impl std::fmt::Debug for PianoHorizontal { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + let buffer = self.buffer.read().unwrap(); f.debug_struct("PianoHorizontal") .field("time_zoom", &self.range.time_zoom) - .field("buffer", &format!("{}x{}", self.buffer.width, self.buffer.height)) + .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) .finish() } } diff --git a/src/piano/piano_h_notes.rs b/src/piano/piano_h_notes.rs index 3bc42f6c..ce26f488 100644 --- a/src/piano/piano_h_notes.rs +++ b/src/piano/piano_h_notes.rs @@ -9,7 +9,7 @@ render!(Tui: |self: PianoHorizontalNotes<'a>, render|{ let note_lo = self.0.note_lo().get(); let note_hi = self.0.note_hi(); let note_point = self.0.note_point(); - let source = &self.0.buffer; + let source = self.0.buffer.read().unwrap(); let [x0, y0, w, h] = render.area().xywh(); if h as usize != note_axis { panic!("area height mismatch: {h} <> {note_axis}"); diff --git a/src/pool/phrase_selector.rs b/src/pool/phrase_selector.rs index a4aa5601..b3f240f6 100644 --- a/src/pool/phrase_selector.rs +++ b/src/pool/phrase_selector.rs @@ -1,16 +1,16 @@ use crate::*; -pub struct PhraseSelector { +pub struct ClipSelected { pub(crate) title: &'static str, pub(crate) name: String, pub(crate) color: ItemPalette, pub(crate) time: String, } -render!(Tui: (self: PhraseSelector) => +render!(Tui: (self: ClipSelected) => Field(self.color, self.title, format!("{} {}", self.time, self.name))); -impl PhraseSelector { +impl ClipSelected { // beats elapsed pub fn play_phrase (state: &T) -> Self { diff --git a/src/sequencer.rs b/src/sequencer.rs index 3e07e3de..c0eb9759 100644 --- a/src/sequencer.rs +++ b/src/sequencer.rs @@ -59,8 +59,8 @@ render!(Tui: (self: SequencerTui) => { let toolbar = Tui::when(self.transport, TransportView::new(true, &self.clock)); let play_queue = Tui::when(self.selectors, row!( - PhraseSelector::play_phrase(&self.player), - PhraseSelector::next_phrase(&self.player), + ClipSelected::play_phrase(&self.player), + ClipSelected::next_phrase(&self.player), )); Min::y(15, with_size(with_status(col!(