diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 468a0794..218d8571 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -66,31 +66,17 @@ impl Audio for SequencerTui { // Start profiling cycle let t0 = self.perf.get_t0(); // Update transport clock - if ClockAudio(self) - .process(client, scope) == Control::Quit - { + if Control::Quit == ClockAudio(self).process(client, scope) { return Control::Quit } // Update MIDI sequencer - if PlayerAudio(&mut self.player, &mut self.note_buf, &mut self.midi_buf) - .process(client, scope) == Control::Quit - { + if Control::Quit == PlayerAudio( + &mut self.player, &mut self.note_buf, &mut self.midi_buf + ).process(client, scope) { return Control::Quit } // End profiling cycle self.perf.update(t0, scope); - - // Update sequencer playhead indicator - //self.now().set(0.); - //if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase { - //let phrase = phrase.read().unwrap(); - //if *playing.read().unwrap() == *phrase { - //let pulse = self.current().pulse.get(); - //let start = started_at.pulse.get(); - //let now = (pulse - start) % phrase.length as f64; - //self.now().set(now); - //} - //} Control::Continue } } diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 7c95c640..96518b3f 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -6,201 +6,6 @@ pub trait HasEditor { fn editor_entered (&self) -> bool; } -/// Contains state for viewing and editing a phrase -pub struct PhraseEditorModel { - /// Phrase being played - pub(crate) phrase: Arc>>>>, - /// Renders the phrase - pub(crate) view_mode: Box, - // Lowest note displayed - pub(crate) note_lo: AtomicUsize, - /// Note coordinate of cursor - pub(crate) note_point: AtomicUsize, - /// Length of note that will be inserted, in pulses - pub(crate) note_len: Arc, - /// Notes currently held at input - pub(crate) notes_in: Arc>, - /// Notes currently held at output - pub(crate) notes_out: Arc>, - /// Earliest time displayed - pub(crate) time_start: AtomicUsize, - /// Time coordinate of cursor - pub(crate) time_point: AtomicUsize, - /// Current position of global playhead - pub(crate) now: Arc, - /// Width and height of notes area at last render - pub(crate) size: Measure, -} - -impl From<&Arc>> for PhraseEditorModel { - fn from (phrase: &Arc>) -> Self { - Self::from(Some(phrase.clone())) - } -} - -impl From>>> for PhraseEditorModel { - fn from (phrase: Option>>) -> Self { - let model = Self::default(); - *model.phrase.write().unwrap() = phrase; - model - } -} - -impl std::fmt::Debug for PhraseEditorModel { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("PhraseEditorModel") - .field("note_axis", &format!("{} {}", - self.note_lo.load(Ordering::Relaxed), - self.note_point.load(Ordering::Relaxed), - )) - .field("time_axis", &format!("{} {}", - self.time_start.load(Ordering::Relaxed), - self.time_point.load(Ordering::Relaxed), - )) - .finish() - } -} - -impl Default for PhraseEditorModel { - fn default () -> Self { - let phrase = Arc::new(RwLock::new(None)); - let note_len = Arc::from(AtomicUsize::from(24)); - Self { - size: Measure::new(), - phrase: phrase.clone(), - now: Pulse::default().into(), - time_start: 0.into(), - time_point: 0.into(), - note_lo: 0.into(), - note_point: 0.into(), - note_len: note_len.clone(), - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - view_mode: Box::new(PianoHorizontal { - phrase: Arc::new(RwLock::new(None)), - buffer: Default::default(), - time_zoom: 24, - time_lock: true, - note_zoom: PhraseViewNoteZoom::N(1), - focused: true, - note_len - }), - } - } -} - -impl PhraseEditorModel { - /// Select which pattern to display. This pre-renders it to the buffer at full resolution. - pub fn show_phrase (&mut self, phrase: Option>>) { - *self.view_mode.phrase().write().unwrap() = if phrase.is_some() { - phrase.clone() - } else { - None - }; - self.view_mode.redraw(); - } - /// Put note at current position - pub fn put_note (&mut self, advance: bool) { - if let Some(phrase) = &*self.phrase.read().unwrap() { - let note_len = self.note_len.load(Ordering::Relaxed); - let time = self.time_point.load(Ordering::Relaxed); - let note = self.note_point.load(Ordering::Relaxed); - let mut phrase = phrase.write().unwrap(); - let key: u7 = u7::from(note as u8); - let vel: u7 = 100.into(); - let start = time; - let end = (start + note_len) % phrase.length; - phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); - phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); - self.view_mode.redraw(); - if advance { - let point = self.time_point.load(Ordering::Relaxed); - let length = phrase.length; - let forward = |time|(time + note_len) % length; - self.time_point.store(forward(point), Ordering::Relaxed); - } - } - } -} - -render!(|self: PhraseEditorModel|self.view_mode); - -pub trait PhraseViewMode: Render + Debug + Send + Sync { - fn time_zoom (&self) -> usize; - fn set_time_zoom (&mut self, time_zoom: usize); - fn time_zoom_lock (&self) -> bool; - fn set_time_zoom_lock (&mut self, time_zoom: bool); - fn buffer_size (&self, phrase: &Phrase) -> (usize, usize); - fn redraw (&mut self); - fn phrase (&self) -> &Arc>>>>; -} - -impl PhraseViewMode for PhraseEditorModel { - fn time_zoom (&self) -> usize { - self.view_mode.time_zoom() - } - fn set_time_zoom (&mut self, time_zoom: usize) { - self.view_mode.set_time_zoom(time_zoom) - } - fn time_zoom_lock (&self) -> bool { - self.view_mode.time_zoom_lock() - } - fn set_time_zoom_lock (&mut self, time_lock: bool) { - self.view_mode.set_time_zoom_lock(time_lock); - } - fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { - self.view_mode.buffer_size(phrase) - } - fn redraw (&mut self) { - self.view_mode.redraw() - } - fn phrase (&self) -> &Arc>>>> { - self.view_mode.phrase() - } -} - -pub struct PhraseView<'a> { - note_point: usize, - note_range: (usize, usize), - time_start: usize, - time_point: usize, - note_len: usize, - phrase: Arc>>>>, - view_mode: &'a Box, - now: &'a Arc, - size: &'a Measure, - focused: bool, - entered: bool, -} - -impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { - fn from (state: &'a T) -> Self { - let editor = state.editor(); - let height = editor.size.h(); - let note_point = editor.note_point.load(Ordering::Relaxed); - let mut note_lo = editor.note_lo.load(Ordering::Relaxed); - let mut note_hi = 127.min((note_lo + height).saturating_sub(2)); - if note_point > note_hi { - note_lo += note_point - note_hi; - note_hi = note_point; - editor.note_lo.store(note_lo, Ordering::Relaxed); - } - Self { - note_point, - note_range: (note_lo, note_hi), - time_start: editor.time_start.load(Ordering::Relaxed), - time_point: editor.time_point.load(Ordering::Relaxed), - note_len: editor.note_len.load(Ordering::Relaxed), - phrase: editor.phrase.clone(), - view_mode: &editor.view_mode, - size: &editor.size, - now: &editor.now, - focused: state.editor_focused(), - entered: state.editor_entered(), - } - } -} - #[derive(Clone, Debug)] pub enum PhraseCommand { // TODO: 1-9 seek markers that by default start every 8th of the phrase @@ -220,18 +25,18 @@ pub enum PhraseCommand { impl InputToCommand for PhraseCommand { fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option { use PhraseCommand::*; - use KeyCode::{Char, Esc, Up, Down, PageUp, PageDown, Left, Right}; - let note_lo = state.note_lo.load(Ordering::Relaxed); - let note_point = state.note_point.load(Ordering::Relaxed); - let time_start = state.time_start.load(Ordering::Relaxed); - let time_point = state.time_point.load(Ordering::Relaxed); - let time_zoom = state.view_mode.time_zoom(); + use KeyCode::{Char, Up, Down, PageUp, PageDown, Left, Right}; + let note_lo = state.range.note_lo.load(Ordering::Relaxed); + let note_point = state.point.note_point.load(Ordering::Relaxed); + let time_start = state.range.time_start.load(Ordering::Relaxed); + let time_point = state.point.time_point.load(Ordering::Relaxed); + let time_zoom = state.mode.time_zoom(); let length = state.phrase().read().unwrap().as_ref() .map(|p|p.read().unwrap().length).unwrap_or(1); - let note_len = state.note_len.load(Ordering::Relaxed); + let note_len = state.point.note_len.load(Ordering::Relaxed); Some(match from.event() { key!(Char('`')) => ToggleDirection, - key!(Char('z')) => SetTimeZoomLock(!state.view_mode.time_zoom_lock()), + key!(Char('z')) => SetTimeZoomLock(!state.mode.time_zoom_lock()), key!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)), key!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)), key!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)), @@ -271,21 +76,21 @@ impl Command for PhraseCommand { fn execute (self, state: &mut PhraseEditorModel) -> Perhaps { use PhraseCommand::*; match self { - Show(phrase) => { state.show_phrase(phrase); }, + Show(phrase) => { state.set_phrase(phrase); }, PutNote => { state.put_note(false); }, AppendNote => { state.put_note(true); }, - SetTimeZoom(zoom) => { state.view_mode.set_time_zoom(zoom); }, - SetTimeZoomLock(lock) => { state.view_mode.set_time_zoom_lock(lock); }, - SetTimeScroll(time) => { state.time_start.store(time, Ordering::Relaxed); }, - SetTimeCursor(time) => { state.time_point.store(time, Ordering::Relaxed); }, - SetNoteLength(time) => { state.note_len.store(time, Ordering::Relaxed); }, - SetNoteScroll(note) => { state.note_lo.store(note, Ordering::Relaxed); }, + SetTimeZoom(zoom) => { state.mode.set_time_zoom(zoom); }, + SetTimeZoomLock(lock) => { state.mode.set_time_zoom_lock(lock); }, + SetTimeScroll(time) => { state.range.time_start.store(time, Ordering::Relaxed); }, + SetTimeCursor(time) => { state.point.time_point.store(time, Ordering::Relaxed); }, + SetNoteLength(time) => { state.point.note_len.store(time, Ordering::Relaxed); }, + SetNoteScroll(note) => { state.range.note_lo.store(note, Ordering::Relaxed); }, SetNoteCursor(note) => { let note = 127.min(note); - let start = state.note_lo.load(Ordering::Relaxed); - state.note_point.store(note, Ordering::Relaxed); + let start = state.range.note_lo.load(Ordering::Relaxed); + state.point.note_point.store(note, Ordering::Relaxed); if note < start { - state.note_lo.store(note, Ordering::Relaxed); + state.range.note_lo.store(note, Ordering::Relaxed); } }, @@ -294,3 +99,191 @@ impl Command for PhraseCommand { Ok(None) } } + +/// Contains state for viewing and editing a phrase +pub struct PhraseEditorModel { + /// Phrase being played + pub phrase: Arc>>>>, + /// Renders the phrase + pub mode: Box, + /// The display window + pub range: PhraseEditorRange, + /// The note cursor + pub point: PhraseEditorPoint, +} + +impl Default for PhraseEditorModel { + fn default () -> Self { + let phrase = Arc::new(RwLock::new(None)); + let range = PhraseEditorRange::default(); + let point = PhraseEditorPoint::default(); + let mode = PianoHorizontal::new(&phrase, &range, &point); + Self { phrase, mode: Box::new(mode), range, point } + } +} + +render!(|self: PhraseEditorModel|self.mode); + +pub trait PhraseViewMode: Render + Debug + Send + Sync { + fn time_zoom (&self) -> usize; + fn set_time_zoom (&mut self, time_zoom: usize); + fn time_zoom_lock (&self) -> bool; + fn set_time_zoom_lock (&mut self, time_zoom: bool); + fn buffer_size (&self, phrase: &Phrase) -> (usize, usize); + fn redraw (&mut self); + fn phrase (&self) -> &Arc>>>>; + fn set_phrase (&mut self, phrase: Option>>) { + *self.phrase().write().unwrap() = phrase; + self.redraw(); + } +} + +impl PhraseViewMode for PhraseEditorModel { + fn time_zoom (&self) -> usize { + self.mode.time_zoom() + } + fn set_time_zoom (&mut self, time_zoom: usize) { + self.mode.set_time_zoom(time_zoom) + } + fn time_zoom_lock (&self) -> bool { + self.mode.time_zoom_lock() + } + fn set_time_zoom_lock (&mut self, time_lock: bool) { + self.mode.set_time_zoom_lock(time_lock); + } + fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { + self.mode.buffer_size(phrase) + } + fn redraw (&mut self) { + self.mode.redraw() + } + fn phrase (&self) -> &Arc>>>> { + self.mode.phrase() + } +} + +#[derive(Debug, Clone)] +pub struct PhraseEditorRange { + /// Earliest time displayed + pub time_start: Arc, + /// Time step + pub time_zoom: Arc, + // Lowest note displayed + pub note_lo: Arc, +} + +impl Default for PhraseEditorRange { + fn default () -> Self { + Self { + time_start: Arc::new(0.into()), + time_zoom: Arc::new(24.into()), + note_lo: Arc::new(0.into()), + } + } +} + +#[derive(Debug, Clone)] +pub struct PhraseEditorPoint { + /// Time coordinate of cursor + pub time_point: Arc, + /// Note coordinate of cursor + pub note_point: Arc, + /// Length of note that will be inserted, in pulses + pub note_len: Arc, +} + +impl Default for PhraseEditorPoint { + fn default () -> Self { + Self { + time_point: Arc::new(0.into()), + note_point: Arc::new(0.into()), + note_len: Arc::new(24.into()), + } + } +} + +impl PhraseEditorModel { + /// Put note at current position + pub fn put_note (&mut self, advance: bool) { + if let Some(phrase) = &*self.phrase.read().unwrap() { + let note_len = self.point.note_len.load(Ordering::Relaxed); + let time = self.point.time_point.load(Ordering::Relaxed); + let note = self.point.note_point.load(Ordering::Relaxed); + let mut phrase = phrase.write().unwrap(); + let key: u7 = u7::from(note as u8); + let vel: u7 = 100.into(); + let start = time; + let end = (start + note_len) % phrase.length; + phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); + phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); + self.mode.redraw(); + if advance { + let point = self.point.time_point.load(Ordering::Relaxed); + let length = phrase.length; + let forward = |time|(time + note_len) % length; + self.point.time_point.store(forward(point), Ordering::Relaxed); + } + } + } +} + +impl From<&Arc>> for PhraseEditorModel { + fn from (phrase: &Arc>) -> Self { + Self::from(Some(phrase.clone())) + } +} + +impl From>>> for PhraseEditorModel { + fn from (phrase: Option>>) -> Self { + let model = Self::default(); + *model.phrase.write().unwrap() = phrase; + model + } +} + +impl std::fmt::Debug for PhraseEditorModel { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("PhraseEditorModel") + .field("range", &self.range) + .field("point", &self.point) + .finish() + } +} + +fn autoscroll_notes ( + range: &PhraseEditorRange, point: &PhraseEditorPoint, height: usize +) -> (usize, (usize, usize)) { + let note_point = point.note_point.load(Ordering::Relaxed); + let mut note_lo = range.note_lo.load(Ordering::Relaxed); + let mut note_hi = 127.min((note_lo + height).saturating_sub(2)); + if note_point > note_hi { + note_lo += note_point - note_hi; + note_hi = note_point; + range.note_lo.store(note_lo, Ordering::Relaxed); + } + (note_point, (note_lo, note_hi)) +} + +//impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { + //fn from (state: &'a T) -> Self { + //let editor = state.editor(); + //let (note_point, note_range) = autoscroll_notes( + //&editor.range, + //&editor.point, + //editor.size.h() + //); + //Self { + //note_point, + //note_range, + //time_start: editor.range.time_start.load(Ordering::Relaxed), + //time_point: editor.point.time_point.load(Ordering::Relaxed), + //note_len: editor.point.note_len.load(Ordering::Relaxed), + //phrase: editor.phrase.clone(), + //mode: &editor.mode, + //size: &editor.size, + //now: &editor.now, + //focused: state.editor_focused(), + //entered: state.editor_entered(), + //} + //} +//} diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index 0a73e619..cc2a510e 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -1,28 +1,42 @@ use crate::*; use super::*; + +/// A phrase, rendered as a horizontal piano roll. pub struct PianoHorizontal { - pub(crate) phrase: Arc>>>>, - pub(crate) time_lock: bool, - pub(crate) time_zoom: usize, - pub(crate) note_zoom: PhraseViewNoteZoom, - pub(crate) note_len: Arc, - pub(crate) buffer: BigBuffer, - pub(crate) focused: bool, + phrase: Arc>>>>, + time_lock: bool, + time_zoom: Arc, + note_len: Arc, + buffer: BigBuffer, + /// Width and height of notes area at last render + size: Measure, } -#[derive(Copy, Clone, Debug)] -pub enum PhraseViewNoteZoom { - N(usize), - Half, - Octant, + +impl PianoHorizontal { + pub fn new ( + phrase: &Arc>>>>, + range: &PhraseEditorRange, + point: &PhraseEditorPoint, + ) -> Self { + Self { + phrase: phrase.clone(), + buffer: Default::default(), + time_lock: true, + time_zoom: range.time_zoom.clone(), + note_len: point.note_len.clone(), + size: Measure::new() + } + } } + render!(|self: PianoHorizontal|{ - let bg = if self.focused { TuiTheme::g(32) } else { Color::Reset }; + let bg = TuiTheme::g(32); let fg = self.phrase().read().unwrap() .as_ref().map(|p|p.read().unwrap().color) .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); - Tui::bg(bg, Tui::split_up(false, 2, + Tui::bg(bg, Tui::split_down(false, 1, Tui::bg(fg.dark.rgb, PianoHorizontalTimeline { - start: "TIMELINE".into() + start: "|0".into() }), Split::right(false, 5, PianoHorizontalKeys { color: ItemPalette::random(), @@ -30,6 +44,7 @@ render!(|self: PianoHorizontal|{ note_hi: 0, note_point: None }, lay!([ + self.size, PianoHorizontalNotes { source: &self.buffer, time_start: 0, @@ -47,12 +62,14 @@ render!(|self: PianoHorizontal|{ ])), )) }); + pub struct PianoHorizontalTimeline { start: String } render!(|self: PianoHorizontalTimeline|{ Tui::fg(TuiTheme::g(224), Tui::push_x(5, self.start.as_str())) }); + pub struct PianoHorizontalKeys { color: ItemPalette, note_lo: usize, @@ -89,6 +106,7 @@ render!(|self: PianoHorizontalKeys|render(|to|Ok({ }; } }))); + pub struct PianoHorizontalCursor { time_zoom: usize, time_point: usize, @@ -119,6 +137,7 @@ render!(|self: PianoHorizontalCursor|render(|to|Ok({ } } }))); + pub struct PianoHorizontalNotes<'a> { source: &'a BigBuffer, time_start: usize, @@ -146,6 +165,7 @@ render!(|self: PianoHorizontalNotes<'a>|render(|to|Ok({ } } }))); + impl PianoHorizontal { /// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄ fn draw_bg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize, note_len: usize) { @@ -204,15 +224,16 @@ impl PianoHorizontal { } } } + impl PhraseViewMode for PianoHorizontal { fn phrase (&self) -> &Arc>>>> { &self.phrase } fn time_zoom (&self) -> usize { - self.time_zoom + self.time_zoom.load(Ordering::Relaxed) } fn set_time_zoom (&mut self, time_zoom: usize) { - self.time_zoom = time_zoom; + self.time_zoom.store(time_zoom, Ordering::Relaxed); self.redraw() } fn time_zoom_lock (&self) -> bool { @@ -224,13 +245,7 @@ impl PhraseViewMode for PianoHorizontal { } /// Determine the required space to render the phrase. fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { - let width = phrase.length / self.time_zoom(); - let height = match self.note_zoom { - PhraseViewNoteZoom::Half => 64, - PhraseViewNoteZoom::N(n) => 128*n, - _ => unimplemented!() - }; - (width, height) + (phrase.length / self.time_zoom(), 128) } fn redraw (&mut self) { let buffer = if let Some(phrase) = &*self.phrase().read().unwrap() { @@ -246,11 +261,11 @@ impl PhraseViewMode for PianoHorizontal { self.buffer = buffer } } + impl std::fmt::Debug for PianoHorizontal { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("PianoHorizontal") .field("time_zoom", &self.time_zoom) - .field("note_zoom", &self.note_zoom) .field("buffer", &format!("{}x{}", self.buffer.width, self.buffer.height)) .finish() } @@ -287,3 +302,14 @@ impl std::fmt::Debug for PianoHorizontal { //} //} //} + // Update sequencer playhead indicator + //self.now().set(0.); + //if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase { + //let phrase = phrase.read().unwrap(); + //if *playing.read().unwrap() == *phrase { + //let pulse = self.current().pulse.get(); + //let start = started_at.pulse.get(); + //let now = (pulse - start) % phrase.length as f64; + //self.now().set(now); + //} + //}