diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index e037b5ad..1a962cc1 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -23,7 +23,6 @@ mod phrase_length; pub(crate) use phrase_length::*; mod phrase_rename; pub(crate) use phrase_rename::*; mod phrase_list; pub(crate) use phrase_list::*; mod phrase_player; pub(crate) use phrase_player::*; -mod phrase_select; pub(crate) use phrase_select::*; mod port_select; pub(crate) use port_select::*; //////////////////////////////////////////////////////// diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 13ee7904..b72fde0a 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -21,10 +21,6 @@ impl TryFrom<&Arc>> for SequencerTui { phrases.phrases.push(phrase.clone()); phrases.phrase.store(1, Ordering::Relaxed); - let mut editor = PhraseEditorModel::default(); - editor.show_phrase(Some(phrase.clone())); - editor.edit_mode = PhraseEditMode::Note; - let mut player = PhrasePlayerModel::from(&clock); player.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase))); @@ -32,7 +28,7 @@ impl TryFrom<&Arc>> for SequencerTui { clock, phrases, player, - editor, + editor: PhraseEditorModel::from(&phrase), jack: jack.clone(), size: Measure::new(), cursor: (0, 0), @@ -79,20 +75,20 @@ impl Audio for SequencerTui { fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { // Start profiling cycle let t0 = self.perf.get_t0(); - // Update transport clock - if ClockAudio(self).process(client, scope) == Control::Quit { + if ClockAudio(self) + .process(client, scope) == Control::Quit + { 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 PlayerAudio(&mut self.player, &mut self.note_buf, &mut self.midi_buf) + .process(client, scope) == Control::Quit + { return Control::Quit } + // End profiling cycle + self.perf.update(t0, scope); // Update sequencer playhead indicator //self.now().set(0.); @@ -105,37 +101,105 @@ impl Audio for SequencerTui { //self.now().set(now); //} //} - // End profiling cycle - self.perf.update(t0, scope); - Control::Continue } } -render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1, - Tui::fill_xy(SequencerStatusBar::from(self)), - Tui::split_right(false, if self.size.w() > 60 { 20 } else if self.size.w() > 40 { 15 } else { 10 }, - Tui::fixed_x(20, Tui::split_down(false, 4, col!([ - PhraseSelector::play_phrase(&self.player), - PhraseSelector::next_phrase(&self.player), - ]), Tui::split_up(false, 2, - PhraseSelector::edit_phrase(&self.editor.phrase), - PhraseListView::from(self), - ))), - col!([ - Tui::fixed_y(2, TransportView::from(( - self, - self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten(), - if let SequencerFocus::Transport(_) = self.focus { - true - } else { - false - } +render!(|self: SequencerTui|{ + + let content = lay!([self.size, Tui::split_up(false, 1, + Tui::fill_xy(SequencerStatusBar::from(self)), + Tui::split_right(false, if self.size.w() > 60 { 20 } else if self.size.w() > 40 { 15 } else { 10 }, + Tui::fixed_x(20, Tui::split_down(false, 4, col!([ + PhraseSelector::play_phrase(&self.player), + PhraseSelector::next_phrase(&self.player), + ]), Tui::split_up(false, 2, + PhraseSelector::edit_phrase(&self.editor.phrase.read().unwrap()), + PhraseListView::from(self), ))), - PhraseView::from(self) - ]), - ) -)])); + col!([ + Tui::fixed_y(2, TransportView::from(( + self, + self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten(), + if let SequencerFocus::Transport(_) = self.focus { + true + } else { + false + } + ))), + PhraseView::from(self) + ]), + ) + )]); + + pub struct PhraseSelector { + pub(crate) title: &'static str, + pub(crate) name: String, + pub(crate) color: ItemPalette, + pub(crate) time: String, + } + + // TODO: Display phrases always in order of appearance + render!(|self: PhraseSelector|Tui::fixed_y(2, col!([ + lay!(move|add|{ + add(&Tui::push_x(1, Tui::fg(TuiTheme::g(240), self.title)))?; + add(&Tui::bg(self.color.base.rgb, Tui::fill_x(Tui::inset_x(1, Tui::fill_x(Tui::at_e( + Tui::fg(self.color.lightest.rgb, &self.time)))))))?; + Ok(()) + }), + Tui::bg(self.color.base.rgb, + Tui::fg(self.color.lightest.rgb, + Tui::bold(true, self.name.clone()))), + ]))); + + impl PhraseSelector { + // beats elapsed + pub fn play_phrase (state: &T) -> Self { + let (name, color) = if let Some((_, Some(phrase))) = state.play_phrase() { + let Phrase { ref name, color, .. } = *phrase.read().unwrap(); + (name.clone(), color) + } else { + ("".to_string(), ItemPalette::from(TuiTheme::g(64))) + }; + let time = if let Some(elapsed) = state.pulses_since_start_looped() { + format!("+{:>}", state.clock().timebase.format_beats_0(elapsed)) + } else { + String::from("") + }; + Self { title: "Now:", time, name, color, } + } + // beats until switchover + pub fn next_phrase (state: &T) -> Self { + let (time, name, color) = if let Some((t, Some(phrase))) = state.next_phrase() { + let Phrase { ref name, color, .. } = *phrase.read().unwrap(); + let time = { + let target = t.pulse.get(); + let current = state.clock().playhead.pulse.get(); + if target > current { + let remaining = target - current; + format!("-{:>}", state.clock().timebase.format_beats_0(remaining)) + } else { + String::new() + } + }; + (time, name.clone(), color) + } else { + ("".into(), "".into(), TuiTheme::g(64).into()) + }; + Self { title: "Next:", time, name, color, } + } + pub fn edit_phrase (phrase: &Option>>) -> Self { + let (time, name, color) = if let Some(phrase) = phrase { + let phrase = phrase.read().unwrap(); + (format!("{}", phrase.length), phrase.name.clone(), phrase.color) + } else { + ("".to_string(), "".to_string(), ItemPalette::from(TuiTheme::g(64))) + }; + Self { title: "Editing:", time, name, color } + } + } + content +}); impl HasClock for SequencerTui { fn clock (&self) -> &ClockModel { @@ -266,20 +330,13 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option SequencerCommand::Focus(FocusCommand::Set(PhraseEditor)), } - // Esc: toggle between scrolling and editing - key!(Esc) => - Editor(SetEditMode(match state.editor.edit_mode { - PhraseEditMode::Scroll => PhraseEditMode::Note, - PhraseEditMode::Note => PhraseEditMode::Scroll, - })), - // Enqueue currently edited phrase key!(Char('q')) => Enqueue(Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())), // E: Toggle between editing currently playing or other phrase key!(Char('e')) => if let Some((_, Some(playing_phrase))) = state.player.play_phrase() { - let editing_phrase = state.editor.phrase.as_ref().map(|p|p.read().unwrap().clone()); + let editing_phrase = state.editor.phrase.read().unwrap().map(|p|p.read().unwrap().clone()); let selected_phrase = state.phrases.phrase().clone(); if Some(selected_phrase.read().unwrap().clone()) != editing_phrase { Editor(Show(Some(selected_phrase))) diff --git a/crates/tek/src/tui/engine_output.rs b/crates/tek/src/tui/engine_output.rs index 5fac2e11..7ab69bb7 100644 --- a/crates/tek/src/tui/engine_output.rs +++ b/crates/tek/src/tui/engine_output.rs @@ -1,19 +1,6 @@ use crate::*; use ratatui::buffer::Cell; -/// Every struct that has [Content]<[Tui]> is a renderable [Render]<[Tui]>. -//impl> Render for C { - //fn min_size (&self, to: [u16;2]) -> Perhaps { - //self.content().min_size(to) - //} - //fn render (&self, to: &mut TuiOutput) -> Usually<()> { - //match self.min_size(to.area().wh().into())? { - //Some(wh) => to.render_in(to.area().clip(wh).into(), &self.content()), - //None => Ok(()) - //} - //} -//} - pub struct TuiOutput { pub buffer: Buffer, pub area: [u16;4] @@ -86,22 +73,6 @@ impl TuiOutput { } } -//impl Area for Rect { - //fn x (&self) -> u16 { self.x } - //fn y (&self) -> u16 { self.y } - //fn w (&self) -> u16 { self.width } - //fn h (&self) -> u16 { self.height } -//} - -pub fn half_block (lower: bool, upper: bool) -> Option { - match (lower, upper) { - (true, true) => Some('█'), - (true, false) => Some('▄'), - (false, true) => Some('▀'), - _ => None - } -} - #[derive(Default)] pub struct BigBuffer { pub width: usize, @@ -126,6 +97,12 @@ impl BigBuffer { } } +impl From<(usize, usize)> for BigBuffer { // cuteness overload + fn from ((width, height): (usize, usize)) -> Self { + Self::new(width, height) + } +} + pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) { for row in 0..area.h() { let y = area.y() + row; @@ -138,6 +115,22 @@ pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut C } } +//impl Area for Rect { + //fn x (&self) -> u16 { self.x } + //fn y (&self) -> u16 { self.y } + //fn w (&self) -> u16 { self.width } + //fn h (&self) -> u16 { self.height } +//} + +pub fn half_block (lower: bool, upper: bool) -> Option { + match (lower, upper) { + (true, true) => Some('█'), + (true, false) => Some('▄'), + (false, true) => Some('▀'), + _ => None + } +} + impl Render for () { fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { Ok(None) diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 9672b742..f2546128 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -9,15 +9,15 @@ pub trait HasEditor { /// Contains state for viewing and editing a phrase pub struct PhraseEditorModel { /// Phrase being played - pub(crate) phrase: Option>>, + pub(crate) phrase: Arc>>>>, + /// Renders the phrase pub(crate) view_mode: Box, - pub(crate) edit_mode: PhraseEditMode, // 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: usize, + pub(crate) note_len: Arc, /// Notes currently held at input pub(crate) notes_in: Arc>, /// Notes currently held at output @@ -32,32 +32,18 @@ pub struct PhraseEditorModel { pub(crate) size: Measure, } -#[derive(Copy, Clone, Debug)] -pub enum PhraseEditMode { - Note, - Scroll, +impl From<&Arc>> for PhraseEditorModel { + fn from (phrase: &Arc>) -> Self { + Self::from(Some(phrase.clone())) + } } -pub trait PhraseViewMode: Debug + Send + Sync { - fn show (&mut self, phrase: Option<&Phrase>, note_len: usize); - fn time_zoom (&self) -> Option; - fn set_time_zoom (&mut self, time_zoom: Option); - fn buffer_width (&self, phrase: &Phrase) -> usize; - fn buffer_height (&self, phrase: &Phrase) -> usize; - fn render_keys (&self, - to: &mut TuiOutput, color: Color, point: Option, range: (usize, usize)); - fn render_notes (&self, - to: &mut TuiOutput, time_start: usize, note_hi: usize); - fn render_cursor ( - &self, - to: &mut TuiOutput, - time_point: usize, - time_start: usize, - note_point: usize, - note_len: usize, - note_hi: usize, - note_lo: usize, - ); +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 { @@ -77,21 +63,23 @@ impl std::fmt::Debug for PhraseEditorModel { impl Default for PhraseEditorModel { fn default () -> Self { + let phrase = Arc::new(RwLock::new(None)); Self { - phrase: None, - note_len: 24, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - now: Pulse::default().into(), size: Measure::new(), - edit_mode: PhraseEditMode::Scroll, - note_lo: 0.into(), - note_point: 0.into(), + 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: Arc::from(AtomicUsize::from(24)), + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), view_mode: Box::new(PianoHorizontal { + phrase, buffer: Default::default(), - time_zoom: Some(24), + time_zoom: 24, + time_lock: true, note_zoom: PhraseViewNoteZoom::N(1) }), } @@ -101,17 +89,18 @@ impl Default for PhraseEditorModel { impl PhraseEditorModel { /// Put note at current position pub fn put_note (&mut self) { - if let Some(phrase) = &self.phrase { + 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 + self.note_len) % phrase.length; + 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.show(Some(&phrase), self.note_len); + self.view_mode.show(Some(&phrase), note_len); } } /// Move time cursor forward by current note length @@ -134,119 +123,14 @@ impl PhraseEditorModel { } } -#[derive(Clone, Debug)] -pub enum PhraseCommand { - // TODO: 1-9 seek markers that by default start every 8th of the phrase - AppendNote, - PutNote, - SetNoteCursor(usize), - SetNoteLength(usize), - SetNoteScroll(usize), - SetTimeCursor(usize), - SetTimeScroll(usize), - SetTimeZoom(Option), - Show(Option>>), - SetEditMode(PhraseEditMode), - ToggleDirection, -} - -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(); - let length = state.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - let note_len = state.note_len; - Some(match from.event() { - key!(Char('`')) => ToggleDirection, - key!(Esc) => SetEditMode(PhraseEditMode::Scroll), - key!(Char('z')) => SetTimeZoom(None), - key!(Char('-')) => SetTimeZoom(time_zoom.map(next_note_length)), - key!(Char('_')) => SetTimeZoom(time_zoom.map(next_note_length)), - key!(Char('=')) => SetTimeZoom(time_zoom.map(prev_note_length)), - key!(Char('+')) => SetTimeZoom(time_zoom.map(prev_note_length)), - key!(Char('a')) => AppendNote, - key!(Char('s')) => PutNote, - // TODO: no triplet/dotted - key!(Char(',')) => SetNoteLength(prev_note_length(state.note_len)), - key!(Char('.')) => SetNoteLength(next_note_length(state.note_len)), - // TODO: with triplet/dotted - key!(Char('<')) => SetNoteLength(prev_note_length(state.note_len)), - key!(Char('>')) => SetNoteLength(next_note_length(state.note_len)), - // TODO: '/' set triplet, '?' set dotted - _ => match state.edit_mode { - PhraseEditMode::Scroll => match from.event() { - key!(Char('e')) => SetEditMode(PhraseEditMode::Note), - - key!(Up) => SetNoteScroll(note_lo + 1), - key!(Down) => SetNoteScroll(note_lo.saturating_sub(1)), - key!(PageUp) => SetNoteScroll(note_lo + 3), - key!(PageDown) => SetNoteScroll(note_lo.saturating_sub(3)), - - key!(Left) => SetTimeScroll(time_start.saturating_sub(note_len)), - key!(Right) => SetTimeScroll(time_start + note_len), - key!(Shift-Left) => SetTimeScroll(time_point.saturating_sub(time_zoom.unwrap())), - key!(Shift-Right) => SetTimeScroll((time_point + time_zoom.unwrap()) % length), - _ => return None - }, - PhraseEditMode::Note => match from.event() { - key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll), - - key!(Up) => SetNoteCursor(note_point + 1), - key!(Down) => SetNoteCursor(note_point.saturating_sub(1)), - key!(PageUp) => SetNoteCursor(note_point + 3), - key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)), - - key!(Shift-Up) => SetNoteScroll(note_lo + 1), - key!(Shift-Down) => SetNoteScroll(note_lo.saturating_sub(1)), - key!(Shift-PageUp) => SetNoteScroll(note_point + 3), - key!(Shift-PageDown) => SetNoteScroll(note_point.saturating_sub(3)), - - key!(Left) => SetTimeCursor(time_point.saturating_sub(note_len)), - key!(Right) => SetTimeCursor((time_point + note_len) % length), - - key!(Shift-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom.unwrap())), - key!(Shift-Right) => SetTimeCursor((time_point + time_zoom.unwrap()) % length), - - _ => return None - }, - } - }) - } -} - -impl Command for PhraseCommand { - fn execute (self, state: &mut PhraseEditorModel) -> Perhaps { - use PhraseCommand::*; - match self { - Show(phrase) => { state.show_phrase(phrase); }, - SetEditMode(mode) => { state.edit_mode = mode; } - PutNote => { state.put_note(); }, - AppendNote => { state.put_note(); - state.time_cursor_advance(); }, - SetTimeZoom(zoom) => { state.view_mode.set_time_zoom(zoom); - state.show_phrase(state.phrase.clone()); }, - SetTimeScroll(time) => { state.time_start.store(time, Ordering::Relaxed); }, - SetTimeCursor(time) => { state.time_point.store(time, Ordering::Relaxed); }, - SetNoteLength(time) => { state.note_len = time; }, - SetNoteScroll(note) => { state.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); - if note < start { - state.note_lo.store(note, Ordering::Relaxed); - } - }, - - _ => todo!("{:?}", self) - } - Ok(None) - } +pub trait PhraseViewMode: 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>>>>; } pub struct PhraseView<'a> { @@ -255,7 +139,7 @@ pub struct PhraseView<'a> { time_start: usize, time_point: usize, note_len: usize, - phrase: &'a Option>>, + phrase: Arc>>>>, view_mode: &'a Box, now: &'a Arc, size: &'a Measure, @@ -269,7 +153,7 @@ render!(|self: PhraseView<'a>|{ .map(|p|p.read().unwrap().color.clone()) .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); Tui::bg(bg, Tui::split_up(false, 2, - Tui::bg(fg.dark.rgb, lay!([ + Tui::bg(fg.dark.rgb, col!([ PhraseTimeline(&self, fg), PhraseViewStats(&self, fg), ])), @@ -297,8 +181,8 @@ impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { 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, - phrase: &editor.phrase, + note_len: editor.note_len.load(Ordering::Relaxed), + phrase: editor.phrase.clone(), view_mode: &editor.view_mode, size: &editor.size, now: &editor.now, @@ -363,3 +247,112 @@ render!(|self: PhraseCursor<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|Ok( self.0.note_range.0, ) )))); + +#[derive(Clone, Debug)] +pub enum PhraseCommand { + // TODO: 1-9 seek markers that by default start every 8th of the phrase + AppendNote, + PutNote, + SetNoteCursor(usize), + SetNoteLength(usize), + SetNoteScroll(usize), + SetTimeCursor(usize), + SetTimeScroll(usize), + SetTimeZoom(usize), + SetTimeZoomLock(bool), + Show(Option>>), + ToggleDirection, +} + +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(); + let length = state.phrase.read().unwrap().map(|p|p.read().unwrap().length).unwrap_or(1); + let note_len = state.note_len.load(Ordering::Relaxed); + Some(match from.event() { + key!(Char('`')) => ToggleDirection, + key!(Char('z')) => SetTimeZoomLock(!state.view_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)), + key!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)), + key!(Char('a')) => AppendNote, + key!(Char('s')) => PutNote, + // TODO: no triplet/dotted + key!(Char(',')) => SetNoteLength(prev_note_length(note_len)), + key!(Char('.')) => SetNoteLength(next_note_length(note_len)), + // TODO: with triplet/dotted + key!(Char('<')) => SetNoteLength(prev_note_length(note_len)), + key!(Char('>')) => SetNoteLength(next_note_length(note_len)), + // TODO: '/' set triplet, '?' set dotted + _ => match from.event() { + key!(Up) => SetNoteCursor(note_point + 1), + key!(Down) => SetNoteCursor(note_point.saturating_sub(1)), + key!(PageUp) => SetNoteCursor(note_point + 3), + key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)), + key!(Left) => SetTimeCursor(time_point.saturating_sub(note_len)), + key!(Right) => SetTimeCursor((time_point + note_len) % length), + key!(Shift-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)), + key!(Shift-Right) => SetTimeCursor((time_point + time_zoom) % length), + + key!(Ctrl-Up) => SetNoteScroll(note_lo + 1), + key!(Ctrl-Down) => SetNoteScroll(note_lo.saturating_sub(1)), + key!(Ctrl-PageUp) => SetNoteScroll(note_point + 3), + key!(Ctrl-PageDown) => SetNoteScroll(note_point.saturating_sub(3)), + key!(Ctrl-Left) => SetTimeScroll(time_start.saturating_sub(note_len)), + key!(Ctrl-Right) => SetTimeScroll(time_start + note_len), + + + + _ => return None + }, + }) + } +} + +impl Command for PhraseCommand { + fn execute (self, state: &mut PhraseEditorModel) -> Perhaps { + use PhraseCommand::*; + match self { + Show(phrase) => { + state.show_phrase(phrase); + }, + PutNote => { + state.put_note(); + }, + AppendNote => { + state.put_note(); + state.time_cursor_advance(); + }, + SetTimeZoom(zoom) => { + state.view_mode.set_time_zoom(zoom); + state.show_phrase(state.phrase.clone()); + }, + SetTimeZoomLock(lock) => { + state.view_mode.set_zoom_lock(lock); + state.show_phrase(state.phrase.clone()); + }, + 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); }, + SetNoteCursor(note) => { + let note = 127.min(note); + let start = state.note_lo.load(Ordering::Relaxed); + state.note_point.store(note, Ordering::Relaxed); + if note < start { + state.note_lo.store(note, Ordering::Relaxed); + } + }, + + _ => todo!("{:?}", self) + } + Ok(None) + } +} diff --git a/crates/tek/src/tui/phrase_select.rs b/crates/tek/src/tui/phrase_select.rs deleted file mode 100644 index c23c234f..00000000 --- a/crates/tek/src/tui/phrase_select.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::*; - -pub struct PhraseSelector { - pub(crate) title: &'static str, - pub(crate) name: String, - pub(crate) color: ItemPalette, - pub(crate) time: String, -} - -// TODO: Display phrases always in order of appearance -render!(|self: PhraseSelector|Tui::fixed_y(1, lay!(move|add|{ - add(&Tui::push_x(1, Tui::fg(TuiTheme::g(240), self.title)))?; - add(&Tui::bg(self.color.base.rgb, Tui::fill_x(Tui::inset_x(1, Tui::fill_x(Tui::at_e(Tui::fg(self.color.lightest.rgb, &self.time)))))))?; - Ok(()) -}))); -impl PhraseSelector { - // beats elapsed - pub fn play_phrase (state: &T) -> Self { - let (name, color) = if let Some((_, Some(phrase))) = state.play_phrase() { - let Phrase { ref name, color, .. } = *phrase.read().unwrap(); - (name.clone(), color) - } else { - ("".to_string(), ItemPalette::from(TuiTheme::g(64))) - }; - let time = if let Some(elapsed) = state.pulses_since_start_looped() { - format!("+{:>}", state.clock().timebase.format_beats_0(elapsed)) - } else { - String::from("") - }; - Self { title: "Now:", time, name, color, } - } - // beats until switchover - pub fn next_phrase (state: &T) -> Self { - let (time, name, color) = if let Some((t, Some(phrase))) = state.next_phrase() { - let Phrase { ref name, color, .. } = *phrase.read().unwrap(); - let time = { - let target = t.pulse.get(); - let current = state.clock().playhead.pulse.get(); - if target > current { - let remaining = target - current; - format!("-{:>}", state.clock().timebase.format_beats_0(remaining)) - } else { - String::new() - } - }; - (time, name.clone(), color) - } else { - ("".into(), "".into(), TuiTheme::g(64).into()) - }; - Self { title: "Next:", time, name, color, } - } - pub fn edit_phrase (phrase: &Option>>) -> Self { - let (time, name, color) = if let Some(phrase) = phrase { - let phrase = phrase.read().unwrap(); - (format!("{}", phrase.length), phrase.name.clone(), phrase.color) - } else { - ("".to_string(), "".to_string(), ItemPalette::from(TuiTheme::g(64))) - }; - Self { title: "Editing:", time, name, color } - } -} diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index 82b5322b..0a73e619 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -1,12 +1,251 @@ use crate::*; use super::*; - pub struct PianoHorizontal { - pub(crate) time_zoom: Option, + 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, } +#[derive(Copy, Clone, Debug)] +pub enum PhraseViewNoteZoom { + N(usize), + Half, + Octant, +} +render!(|self: PianoHorizontal|{ + let bg = if self.focused { TuiTheme::g(32) } else { Color::Reset }; + 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(fg.dark.rgb, PianoHorizontalTimeline { + start: "TIMELINE".into() + }), + Split::right(false, 5, PianoHorizontalKeys { + color: ItemPalette::random(), + note_lo: 0, + note_hi: 0, + note_point: None + }, lay!([ + PianoHorizontalNotes { + source: &self.buffer, + time_start: 0, + note_hi: 0, + }, + PianoHorizontalCursor { + time_zoom: 0, + time_point: 0, + time_start: 0, + note_point: 0, + note_len: 0, + note_hi: 0, + note_lo: 0, + }, + ])), + )) +}); +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, + note_hi: usize, + note_point: Option, +} +render!(|self: PianoHorizontalKeys|render(|to|Ok({ + let [x, y0, _, _] = to.area().xywh(); + let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0))); + let off_style = Some(Style::default().fg(TuiTheme::g(160))); + let on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(self.color.base.rgb).bold()); + for (y, note) in (self.note_lo..=self.note_hi).rev().enumerate().map(|(y, n)|(y0 + y as u16, n)) { + let key = match note % 12 { + 11 => "████▌", + 10 => " ", + 9 => "████▌", + 8 => " ", + 7 => "████▌", + 6 => " ", + 5 => "████▌", + 4 => "████▌", + 3 => " ", + 2 => "████▌", + 1 => " ", + 0 => "████▌", + _ => unreachable!(), + }; + to.blit(&key, x, y, key_style); + + if Some(note) == self.note_point { + to.blit(&format!("{:<5}", to_note_name(note)), x, y, on_style) + } else { + to.blit(&to_note_name(note), x, y, off_style) + }; + } +}))); +pub struct PianoHorizontalCursor { + time_zoom: usize, + time_point: usize, + time_start: usize, + note_point: usize, + note_len: usize, + note_hi: usize, + note_lo: usize, +} +render!(|self: PianoHorizontalCursor|render(|to|Ok({ + let [x0, y0, w, _] = to.area().xywh(); + let style = Some(Style::default().fg(Color::Rgb(0,255,0))); + for (y, note) in (self.note_lo..=self.note_hi).rev().enumerate() { + if note == self.note_point { + for x in 0..w { + let time_1 = self.time_start + x as usize * self.time_zoom; + let time_2 = time_1 + self.time_zoom; + if time_1 <= self.time_point && self.time_point < time_2 { + to.blit(&"█", x0 + x as u16, y0 + y as u16, style); + let tail = self.note_len as u16 / self.time_zoom as u16; + for x_tail in (x0 + x + 1)..(x0 + x + tail) { + to.blit(&"▂", x_tail, y0 + y as u16, style); + } + break + } + } + break + } + } +}))); +pub struct PianoHorizontalNotes<'a> { + source: &'a BigBuffer, + time_start: usize, + note_hi: usize, +} +render!(|self: PianoHorizontalNotes<'a>|render(|to|Ok({ + let [x0, y0, w, h] = to.area().xywh(); + let target = &mut to.buffer; + for (x, target_x) in (x0..x0+w).enumerate() { + for (y, target_y) in (y0..y0+h).enumerate() { + if y > self.note_hi { + break + } + let source_x = self.time_start + x; + let source_y = self.note_hi - y; + // TODO: enable loop rollover: + //let source_x = (time_start + x) % source.width.max(1); + //let source_y = (note_hi - y) % source.height.max(1); + if source_x < self.source.width && source_y < self.source.height { + let target_cell = target.get_mut(target_x, target_y); + if let Some(source_cell) = self.source.get(source_x, source_y) { + *target_cell = source_cell.clone(); + } + } + } + } +}))); +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) { + for (y, note) in (0..127).rev().enumerate() { + for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { + let cell = buf.get_mut(x, y).unwrap(); + cell.set_bg(phrase.color.darkest.rgb); + cell.set_fg(phrase.color.darker.rgb); + cell.set_char(if time % 384 == 0 { + '│' + } else if time % 96 == 0 { + '╎' + } else if time % note_len == 0 { + '┊' + } else if (127 - note) % 12 == 1 { + '=' + } else { + '·' + }); + } + } + } + /// Draw the piano roll background using full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_fg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize) { + let style = Style::default().fg(phrase.color.lightest.rgb);//.bg(Color::Rgb(0, 0, 0)); + let mut notes_on = [false;128]; + for (x, time_start) in (0..phrase.length).step_by(zoom).enumerate() { + for (y, note) in (0..127).rev().enumerate() { + let cell = buf.get_mut(x, note).unwrap(); + if notes_on[note] { + cell.set_char('▂'); + cell.set_style(style); + } + } + + let time_end = time_start + zoom; + for time in time_start..time_end { + for event in phrase.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + let cell = buf.get_mut(x, note).unwrap(); + cell.set_char('█'); + cell.set_style(style); + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} + } + } + } + + } + } +} +impl PhraseViewMode for PianoHorizontal { + fn phrase (&self) -> &Arc>>>> { + &self.phrase + } + fn time_zoom (&self) -> usize { + self.time_zoom + } + fn set_time_zoom (&mut self, time_zoom: usize) { + self.time_zoom = time_zoom; + self.redraw() + } + fn time_zoom_lock (&self) -> bool { + self.time_lock + } + fn set_time_zoom_lock (&mut self, time_lock: bool) { + self.time_lock = time_lock; + self.redraw() + } + /// 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) + } + fn redraw (&mut self) { + let buffer = if let Some(phrase) = &*self.phrase().read().unwrap() { + let phrase = phrase.read().unwrap(); + let mut buffer = BigBuffer::from(self.buffer_size(&phrase)); + let note_len = self.note_len.load(Ordering::Relaxed); + PianoHorizontal::draw_bg(&mut buffer, &phrase, self.time_zoom(), note_len); + PianoHorizontal::draw_fg(&mut buffer, &phrase, self.time_zoom()); + buffer + } else { + Default::default() + }; + 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") @@ -17,191 +256,34 @@ impl std::fmt::Debug for PianoHorizontal { } } -#[derive(Copy, Clone, Debug)] -pub enum PhraseViewNoteZoom { - N(usize), - Half, - Octant, -} - -impl PhraseViewMode for PianoHorizontal { - fn time_zoom (&self) -> Option { - self.time_zoom - } - fn set_time_zoom (&mut self, time_zoom: Option) { - self.time_zoom = time_zoom - } - fn show (&mut self, phrase: Option<&Phrase>, note_len: usize) { - if let Some(phrase) = phrase { - self.buffer = BigBuffer::new(self.buffer_width(phrase), self.buffer_height(phrase)); - draw_piano_horizontal_bg(&mut self.buffer, phrase, self.time_zoom.unwrap(), note_len); - draw_piano_horizontal_fg(&mut self.buffer, phrase, self.time_zoom.unwrap()); - } else { - self.buffer = Default::default(); - } - } - fn buffer_width (&self, phrase: &Phrase) -> usize { - phrase.length / self.time_zoom.unwrap() - } - /// Determine the required height to render the phrase. - fn buffer_height (&self, phrase: &Phrase) -> usize { - match self.note_zoom { - PhraseViewNoteZoom::Half => 64, - PhraseViewNoteZoom::N(n) => 128*n, - _ => unimplemented!() - } - } - fn render_notes ( - &self, - target: &mut TuiOutput, - time_start: usize, - note_hi: usize, - ) { - let [x0, y0, w, h] = target.area().xywh(); - let source = &self.buffer; - let target = &mut target.buffer; - for (x, target_x) in (x0..x0+w).enumerate() { - for (y, target_y) in (y0..y0+h).enumerate() { - if y > note_hi { - break - } - let source_x = time_start + x; - let source_y = note_hi - y; - // TODO: enable loop rollover: - //let source_x = (time_start + x) % source.width.max(1); - //let source_y = (note_hi - y) % source.height.max(1); - if source_x < source.width && source_y < source.height { - let target_cell = target.get_mut(target_x, target_y); - if let Some(source_cell) = source.get(source_x, source_y) { - *target_cell = source_cell.clone(); - } - } - } - } - } - fn render_keys ( - &self, - to: &mut TuiOutput, - color: Color, - point: Option, - (note_lo, note_hi): (usize, usize) - ) { - let [x, y0, _, _] = to.area().xywh(); - let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0))); - let note_off_style = Some(Style::default().fg(TuiTheme::g(160))); - let note_on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(color).bold()); - for (y, note) in (note_lo..=note_hi).rev().enumerate().map(|(y, n)|(y0 + y as u16, n)) { - let key = match note % 12 { - 11 => "████▌", - 10 => " ", - 9 => "████▌", - 8 => " ", - 7 => "████▌", - 6 => " ", - 5 => "████▌", - 4 => "████▌", - 3 => " ", - 2 => "████▌", - 1 => " ", - 0 => "████▌", - _ => unreachable!(), - }; - to.blit(&key, x, y, key_style); - - if Some(note) == point { - to.blit(&format!("{:<5}", to_note_name(note)), x, y, note_on_style) - } else { - to.blit(&to_note_name(note), x, y, note_off_style) - }; - } - } - fn render_cursor ( - &self, - to: &mut TuiOutput, - time_point: usize, - time_start: usize, - note_point: usize, - note_len: usize, - note_hi: usize, - note_lo: usize, - ) { - let time_zoom = self.time_zoom.unwrap(); - let [x0, y0, w, h] = to.area().xywh(); - let style = Some(Style::default().fg(Color::Rgb(0,255,0))); - for (y, note) in (note_lo..=note_hi).rev().enumerate() { - if note == note_point { - for x in 0..w { - let time_1 = time_start + x as usize * time_zoom; - let time_2 = time_1 + time_zoom; - if time_1 <= time_point && time_point < time_2 { - to.blit(&"█", x0 + x as u16, y0 + y as u16, style); - let tail = note_len as u16 / time_zoom as u16; - for x_tail in (x0 + x + 1)..(x0 + x + tail) { - to.blit(&"▂", x_tail, y0 + y as u16, style); - } - break - } - } - break - } - } - } -} - -/// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄ -fn draw_piano_horizontal_bg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize, note_len: usize) { - for (y, note) in (0..127).rev().enumerate() { - for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { - let cell = buf.get_mut(x, y).unwrap(); - cell.set_bg(phrase.color.darkest.rgb); - cell.set_fg(phrase.color.darker.rgb); - cell.set_char(if time % 384 == 0 { - '│' - } else if time % 96 == 0 { - '╎' - } else if time % note_len == 0 { - '┊' - } else if (127 - note) % 12 == 1 { - '=' - } else { - '·' - }); - } - } -} - -/// Draw the piano roll background using full blocks on note on and half blocks on legato: █▄ █▄ █▄ -fn draw_piano_horizontal_fg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize) { - let style = Style::default().fg(phrase.color.lightest.rgb);//.bg(Color::Rgb(0, 0, 0)); - let mut notes_on = [false;128]; - for (x, time_start) in (0..phrase.length).step_by(zoom).enumerate() { - - for (y, note) in (0..127).rev().enumerate() { - let cell = buf.get_mut(x, note).unwrap(); - if notes_on[note] { - cell.set_char('▂'); - cell.set_style(style); - } - } - - let time_end = time_start + zoom; - for time in time_start..time_end { - for event in phrase.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - let cell = buf.get_mut(x, note).unwrap(); - cell.set_char('█'); - cell.set_style(style); - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } - } - } - - } -} + //fn render_cursor ( + //&self, + //to: &mut TuiOutput, + //time_point: usize, + //time_start: usize, + //note_point: usize, + //note_len: usize, + //note_hi: usize, + //note_lo: usize, + //) { + //let time_zoom = self.time_zoom; + //let [x0, y0, w, h] = to.area().xywh(); + //let style = Some(Style::default().fg(Color::Rgb(0,255,0))); + //for (y, note) in (note_lo..=note_hi).rev().enumerate() { + //if note == note_point { + //for x in 0..w { + //let time_1 = time_start + x as usize * time_zoom; + //let time_2 = time_1 + time_zoom; + //if time_1 <= time_point && time_point < time_2 { + //to.blit(&"█", x0 + x as u16, y0 + y as u16, style); + //let tail = note_len as u16 / time_zoom as u16; + //for x_tail in (x0 + x + 1)..(x0 + x + tail) { + //to.blit(&"▂", x_tail, y0 + y as u16, style); + //} + //break + //} + //} + //break + //} + //} + //} diff --git a/crates/tek/src/tui/status_bar.rs b/crates/tek/src/tui/status_bar.rs index ff6660ed..1939baa0 100644 --- a/crates/tek/src/tui/status_bar.rs +++ b/crates/tek/src/tui/status_bar.rs @@ -71,10 +71,7 @@ impl From<&SequencerTui> for SequencerStatusBar { //PhrasePlay => " TO PLAY ", //PhraseNext => " UP NEXT ", PhraseList => " PHRASES ", - PhraseEditor => match state.editor.edit_mode { - PhraseEditMode::Note => " EDIT MIDI ", - PhraseEditMode::Scroll => " VIEW MIDI ", - }, + PhraseEditor => " EDIT MIDI ", }, help: match state.focused() { Transport(PlayPause) => &[ @@ -101,14 +98,10 @@ impl From<&SequencerTui> for SequencerStatusBar { ("", "⏎", " play"), ("", "e", " edit"), ], - PhraseEditor => match state.editor.edit_mode { - PhraseEditMode::Note => &[ - ("", "✣", " cursor"), - ], - PhraseEditMode::Scroll => &[ - ("", "✣", " scroll"), - ], - }, + PhraseEditor => &[ + ("", "✣", " cursor"), + ("", "Ctrl-✣", " scroll"), + ], _ => default_help, } }