From 0c94c2af8f3614e1b8d8d4c6812aa753d109cf93 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 19 Nov 2024 00:13:12 +0100 Subject: [PATCH] wip: p.57, e=81 --- crates/tek_api/src/api_phrase.rs | 7 +- crates/tek_api/src/api_scene.rs | 7 + crates/tek_core/src/color.rs | 4 +- crates/tek_tui/src/lib.rs | 35 -- crates/tek_tui/src/tui_apis.rs | 561 +++--------------------------- crates/tek_tui/src/tui_command.rs | 189 +++++----- crates/tek_tui/src/tui_control.rs | 114 +++++- crates/tek_tui/src/tui_input.rs | 39 ++- crates/tek_tui/src/tui_menu.rs | 35 ++ crates/tek_tui/src/tui_model.rs | 243 +++++++++++++ crates/tek_tui/src/tui_view.rs | 105 ++++++ 11 files changed, 672 insertions(+), 667 deletions(-) diff --git a/crates/tek_api/src/api_phrase.rs b/crates/tek_api/src/api_phrase.rs index b43c3554..e93eaa2c 100644 --- a/crates/tek_api/src/api_phrase.rs +++ b/crates/tek_api/src/api_phrase.rs @@ -11,7 +11,7 @@ pub enum PhrasePoolCommand { Delete(usize), Duplicate(usize), Swap(usize, usize), - RandomColor(usize), + Color(usize, ItemColor), Import(usize, String), Export(usize, String), SetName(usize, String), @@ -38,10 +38,9 @@ impl Command for PhrasePoolCommand { //view.phrase += 1; }, Self::Swap(index, other) => { - //Self::MoveUp => { view.move_up() }, - //Self::MoveDown => { view.move_down() }, + model.phrases_mut().swap(index, other); }, - Self::RandomColor(index) => { + Self::Color(index, color) => { //view.phrase().write().unwrap().color = ItemColorTriplet::random(); }, Self::Import(index, path) => { diff --git a/crates/tek_api/src/api_scene.rs b/crates/tek_api/src/api_scene.rs index 5618da34..6459cc3d 100644 --- a/crates/tek_api/src/api_scene.rs +++ b/crates/tek_api/src/api_scene.rs @@ -10,6 +10,12 @@ pub trait HasScenes { fn scene_default_name (&self) -> String { format!("Scene {}", self.scenes().len() + 1) } + fn selected_scene (&self) -> Option<&S> { + None + } + fn selected_scene_mut (&mut self) -> Option<&mut S> { + None + } } #[derive(Clone, Debug)] @@ -86,6 +92,7 @@ pub trait ArrangerSceneApi: Sized { fn clip (&self, index: usize) -> Option<&Arc>> { match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None } } + } //impl ArrangerScene { diff --git a/crates/tek_core/src/color.rs b/crates/tek_core/src/color.rs index 26f9da6d..ed4d68b7 100644 --- a/crates/tek_core/src/color.rs +++ b/crates/tek_core/src/color.rs @@ -3,13 +3,13 @@ use rand::{thread_rng, distributions::uniform::UniformSampler}; pub use palette::{*, convert::*, okhsl::*}; /// A color in OKHSL and RGB representations. -#[derive(Debug, Default, Copy, Clone)] +#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemColor { pub okhsl: Okhsl, pub rgb: Color, } /// A color in OKHSL and RGB with lighter and darker variants. -#[derive(Debug, Default, Copy, Clone)] +#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemColorTriplet { pub base: ItemColor, pub light: ItemColor, diff --git a/crates/tek_tui/src/lib.rs b/crates/tek_tui/src/lib.rs index 4f94f952..9632a96d 100644 --- a/crates/tek_tui/src/lib.rs +++ b/crates/tek_tui/src/lib.rs @@ -29,38 +29,3 @@ submod! { tui_view tui_widget } - -fn content_with_menu_and_status <'a, A, S, C> ( - content: &'a A, - menu_bar: &'a Option>, - status_bar: &'a Option -) -> impl Widget + 'a -where - A: Widget, - S: Send + Sync + 'a, - C: Command -{ - let menus = menu_bar.as_ref().map_or_else( - ||&[] as &[Menu<_, _, _>], - |m|m.menus.as_slice() - ); - Either( - menu_bar.is_none(), - Either( - status_bar.is_none(), - widget(content), - Split::up( - 1, - widget(status_bar.as_ref().unwrap()), - widget(content) - ), - ), - Split::down( - 1, - row!(menu in menus.iter() => { - row!(" ", menu.title.as_str(), " ") - }), - widget(content) - ) - ) -} diff --git a/crates/tek_tui/src/tui_apis.rs b/crates/tek_tui/src/tui_apis.rs index ce985dd8..4a6d2b29 100644 --- a/crates/tek_tui/src/tui_apis.rs +++ b/crates/tek_tui/src/tui_apis.rs @@ -1,323 +1,5 @@ use crate::*; -impl HasPhrases for ArrangerTui { - fn phrases (&self) -> &Vec>> { - &self.phrases - } - fn phrases_mut (&mut self) -> &mut Vec>> { - &mut self.phrases - } -} - -impl HasPhrases for SequencerTui { - fn phrases (&self) -> &Vec>> { - &self.phrases - } - fn phrases_mut (&mut self) -> &mut Vec>> { - &mut self.phrases - } -} - -impl HasPhrases for PhrasesTui { - fn phrases (&self) -> &Vec>> { - &self.phrases - } - fn phrases_mut (&mut self) -> &mut Vec>> { - &mut self.phrases - } -} - -impl HasPhrase for SequencerTui { - fn reset (&self) -> bool { - self.reset - } - fn reset_mut (&mut self) -> &mut bool { - &mut self.reset - } - fn phrase (&self) -> &Option<(Instant, Option>>)> { - todo!() - } - fn phrase_mut (&self) -> &mut Option<(Instant, Option>>)> { - todo!() - } - fn next_phrase (&self) -> &Option<(Instant, Option>>)> { - todo!() - } - fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option>>)> { - todo!() - } -} - -impl ArrangerSceneApi for ArrangerScene { - fn name (&self) -> &Arc> { - &self.name - } - fn clips (&self) -> &Vec>>> { - &self.clips - } - fn color (&self) -> ItemColor { - self.color - } -} - -impl HasScenes for ArrangerTui { - fn scenes (&self) -> &Vec { - &self.scenes - } - fn scenes_mut (&mut self) -> &mut Vec { - &mut self.scenes - } - fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerScene> - { - let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string()); - let scene = ArrangerScene { - name: Arc::new(name.into()), - clips: vec![None;self.tracks().len()], - color: color.unwrap_or_else(||ItemColor::random()), - }; - self.scenes_mut().push(scene); - let index = self.scenes().len() - 1; - Ok(&mut self.scenes_mut()[index]) - } -} - -impl HasTracks for ArrangerTui { - fn tracks (&self) -> &Vec { - &self.tracks - } - fn tracks_mut (&mut self) -> &mut Vec { - &mut self.tracks - } -} - -impl ArrangerTracksApi for ArrangerTui { - fn track_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerTrack> - { - let name = name.map_or_else(||self.track_default_name(), |x|x.to_string()); - let track = ArrangerTrack { - width: name.len() + 2, - name: Arc::new(name.into()), - color: color.unwrap_or_else(||ItemColor::random()), - midi_ins: vec![], - midi_outs: vec![], - reset: true, - recording: false, - monitoring: false, - overdub: false, - play_phrase: None, - next_phrase: None, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - }; - self.tracks_mut().push(track); - let index = self.tracks().len() - 1; - Ok(&mut self.tracks_mut()[index]) - } - fn track_del (&mut self, index: usize) { - self.tracks_mut().remove(index); - for scene in self.scenes_mut().iter_mut() { - scene.clips.remove(index); - } - } -} - -impl ArrangerTrackApi for ArrangerTrack { - /// Name of track - fn name (&self) -> &Arc> { - &self.name - } - /// Preferred width of track column - fn width (&self) -> usize { - self.width - } - /// Preferred width of track column - fn width_mut (&mut self) -> &mut usize { - &mut self.width - } - /// Identifying color of track - fn color (&self) -> ItemColor { - self.color - } -} - -impl HasPhrase for ArrangerTrack { - fn reset (&self) -> bool { - self.reset - } - fn reset_mut (&mut self) -> &mut bool { - &mut self.reset - } - fn phrase (&self) -> &Option<(Instant, Option>>)> { - todo!() - } - fn phrase_mut (&self) -> &mut Option<(Instant, Option>>)> { - todo!() - } - fn next_phrase (&self) -> &Option<(Instant, Option>>)> { - todo!() - } - fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option>>)> { - todo!() - } -} - -impl MidiInputApi for ArrangerTrack { - fn midi_ins (&self) -> &Vec> { - todo!() - } - fn midi_ins_mut (&self) -> &mut Vec> { - todo!() - } - fn recording (&self) -> bool { - todo!() - } - fn recording_mut (&mut self) -> &mut bool { - todo!() - } - fn monitoring (&self) -> bool { - todo!() - } - fn monitoring_mut (&mut self) -> &mut bool { - todo!() - } - fn overdub (&self) -> bool { - todo!() - } - fn overdub_mut (&mut self) -> &mut bool { - todo!() - } - fn notes_in (&self) -> &Arc> { - todo!() - } -} - -impl MidiOutputApi for ArrangerTrack { - fn midi_outs (&self) -> &Vec> { - todo!() - } - fn midi_outs_mut (&mut self) -> &mut Vec> { - todo!() - } - fn midi_note (&mut self) -> &mut Vec { - todo!() - } - fn notes_out (&self) -> &Arc> { - todo!() - } -} - -impl ClockApi for ArrangerTrack { - fn timebase (&self) -> &Arc { - todo!() - } - fn quant (&self) -> &Quantize { - todo!() - } - fn sync (&self) -> &LaunchSync { - todo!() - } -} - -impl PlayheadApi for ArrangerTrack { - fn current (&self) -> &Instant { - todo!() - } - fn transport (&self) -> &Transport { - todo!() - } - fn playing (&self) -> &RwLock> { - todo!() - } - fn started (&self) -> &RwLock> { - todo!() - } -} - -impl PlayerApi for ArrangerTrack {} - - -/// General methods for arranger -impl ArrangerTui { - pub fn selected_scene (&self) -> Option<&ArrangerScene> { - self.selected.scene().map(|s|self.scenes().get(s)).flatten() - } - pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { - self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten() - } - pub fn selected_phrase (&self) -> Option>> { - self.selected_scene()?.clips.get(self.selected.track()?)?.clone() - } - - /// Focus the editor with the current phrase - pub fn show_phrase (&mut self) { - self.editor.show(self.selected_phrase().as_ref()); - } - - pub fn activate (&mut self) { - let selected = self.selected; - match selected { - ArrangerSelection::Scene(s) => { - for (t, track) in self.tracks_mut().iter_mut().enumerate() { - let clip = self.scenes()[s].clips[t].as_ref(); - if track.play_phrase.is_some() || clip.is_some() { - track.enqueue_next(clip); - } - } - // TODO make transport available here, so that - // activating a scene when stopped starts playback - //if self.is_stopped() { - //self.transport.toggle_play() - //} - }, - ArrangerSelection::Clip(t, s) => { - self.tracks_mut()[t].enqueue_next(self.scenes()[s].clips[t].as_ref()); - }, - _ => {} - } - } - - pub fn is_first_row (&self) -> bool { - let selected = self.selected; - selected.is_mix() || selected.is_track() - } - - pub fn is_last_row (&self) -> bool { - let selected = self.selected; - (self.scenes().len() == 0 && (selected.is_mix() || selected.is_track())) || match selected { - ArrangerSelection::Scene(s) => s == self.scenes().len() - 1, - ArrangerSelection::Clip(_, s) => s == self.scenes().len() - 1, - _ => false - } - } - - pub fn toggle_loop (&mut self) { - if let Some(phrase) = self.selected_phrase() { - phrase.write().unwrap().toggle_loop() - } - } - - pub fn randomize_color (&mut self) { - match self.selected { - ArrangerSelection::Mix => { - self.color = ItemColor::random_dark() - }, - ArrangerSelection::Track(t) => { - self.tracks_mut()[t].color = ItemColor::random() - }, - ArrangerSelection::Scene(s) => { - self.scenes_mut()[s].color = ItemColor::random() - }, - ArrangerSelection::Clip(t, s) => { - if let Some(phrase) = &self.scenes_mut()[s].clips[t] { - phrase.write().unwrap().color = ItemColorTriplet::random(); - } - } - } - } -} - impl PhrasesTui { pub fn new (phrases: Vec>>) -> Self { Self { @@ -329,71 +11,37 @@ impl PhrasesTui { phrases, } } - pub fn len (&self) -> usize { - self.phrases.len() - } - pub fn phrase (&self) -> &Arc> { - &self.phrases[self.phrase] - } - pub fn index_before (&self, index: usize) -> usize { - index.overflowing_sub(1).0.min(self.len() - 1) - } - pub fn index_after (&self, index: usize) -> usize { - (index + 1) % self.len() - } - pub fn index_of (&self, phrase: &Phrase) -> Option { - for i in 0..self.phrases.len() { - if *self.phrases[i].read().unwrap() == *phrase { return Some(i) } - } - return None - } - fn new_phrase (name: Option<&str>, color: Option) -> Arc> { - Arc::new(RwLock::new(Phrase::new( - String::from(name.unwrap_or("(new)")), true, 4 * PPQ, None, color - ))) - } - pub fn delete_selected (&mut self) { - if self.phrase > 0 { - self.phrases.remove(self.phrase); - self.phrase = self.phrase.min(self.phrases.len().saturating_sub(1)); - } - } - pub fn append_new (&mut self, name: Option<&str>, color: Option) { - self.phrases.push(Self::new_phrase(name, color)); - self.phrase = self.phrases.len() - 1; - } - pub fn insert_new (&mut self, name: Option<&str>, color: Option) { - self.phrases.insert(self.phrase + 1, Self::new_phrase(name, color)); - self.phrase += 1; - } - pub fn insert_dup (&mut self) { - let mut phrase = self.phrases[self.phrase].read().unwrap().duplicate(); - phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25); - self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase))); - self.phrase += 1; - } - pub fn move_up (&mut self) { - if self.phrase > 1 { - self.phrases.swap(self.phrase - 1, self.phrase); - self.phrase -= 1; - } - } - pub fn move_down (&mut self) { - if self.phrase < self.phrases.len().saturating_sub(1) { - self.phrases.swap(self.phrase + 1, self.phrase); - self.phrase += 1; +} +impl PhraseTui { + pub fn new () -> Self { + Self { + phrase: None, + note_len: 24, + notes_in: Arc::new(RwLock::new([false;128])), + notes_out: Arc::new(RwLock::new([false;128])), + keys: keys_vert(), + buffer: Default::default(), + focused: false, + entered: false, + mode: false, + now: Arc::new(0.into()), + width: 0.into(), + height: 0.into(), + note_axis: RwLock::new(FixedAxis { + start: 12, + point: Some(36), + clamp: Some(127) + }), + time_axis: RwLock::new(ScaledAxis { + start: 00, + point: Some(00), + clamp: Some(000), + scale: 24 + }), } } } - - - - - - - - //pub fn track_next (&mut self, last_track: usize) { //use ArrangerSelection::*; //*self = match self { @@ -572,142 +220,23 @@ impl PhrasesTui { //self.selected_scene()?.clips.get(self.selected.track()?)?.clone() //} -impl PhraseTui { - pub fn new () -> Self { - Self { - phrase: None, - note_len: 24, - notes_in: Arc::new(RwLock::new([false;128])), - notes_out: Arc::new(RwLock::new([false;128])), - keys: keys_vert(), - buffer: Default::default(), - focused: false, - entered: false, - mode: false, - now: Arc::new(0.into()), - width: 0.into(), - height: 0.into(), - note_axis: RwLock::new(FixedAxis { - start: 12, - point: Some(36), - clamp: Some(127) - }), - time_axis: RwLock::new(ScaledAxis { - start: 00, - point: Some(00), - clamp: Some(000), - scale: 24 - }), - } - } - pub fn time_cursor_advance (&self) { - let point = self.time_axis.read().unwrap().point; - let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - let forward = |time|(time + self.note_len) % length; - self.time_axis.write().unwrap().point = point.map(forward); - } + //pub fn is_first_row (&self) -> bool { + //let selected = self.selected; + //selected.is_mix() || selected.is_track() + //} - pub fn put (&mut self) { - if let (Some(phrase), Some(time), Some(note)) = ( - &self.phrase, - self.time_axis.read().unwrap().point, - self.note_axis.read().unwrap().point, - ) { - let mut phrase = phrase.write().unwrap(); - let key: u7 = u7::from((127 - note) as u8); - let vel: u7 = 100.into(); - let start = time; - let end = (start + self.note_len) % phrase.length; - phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); - phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); - self.buffer = Self::redraw(&phrase); - } - } - /// Select which pattern to display. This pre-renders it to the buffer at full resolution. - pub fn show (&mut self, phrase: Option<&Arc>>) { - if let Some(phrase) = phrase { - self.phrase = Some(phrase.clone()); - self.time_axis.write().unwrap().clamp = Some(phrase.read().unwrap().length); - self.buffer = Self::redraw(&*phrase.read().unwrap()); - } else { - self.phrase = None; - self.time_axis.write().unwrap().clamp = Some(0); - self.buffer = Default::default(); - } - } - fn redraw (phrase: &Phrase) -> BigBuffer { - let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65); - Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq); - Self::fill_seq_fg(&mut buf, &phrase); - buf - } - fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) { - for x in 0..buf.width { - // Only fill as far as phrase length - if x as usize >= length { break } - // Fill each row with background characters - for y in 0 .. buf.height { - buf.get_mut(x, y).map(|cell|{ - cell.set_char(if ppq == 0 { - '·' - } else if x % (4 * ppq) == 0 { - '│' - } else if x % ppq == 0 { - '╎' - } else { - '·' - }); - cell.set_fg(Color::Rgb(48, 64, 56)); - cell.modifier = Modifier::DIM; - }); - } - } - } - fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) { - let mut notes_on = [false;128]; - for x in 0..buf.width { - if x as usize >= phrase.length { - break - } - if let Some(notes) = phrase.notes.get(x as usize) { - if phrase.percussive { - for note in notes { - match note { - MidiMessage::NoteOn { key, .. } => - notes_on[key.as_int() as usize] = true, - _ => {} - } - } - } else { - for note in notes { - match note { - MidiMessage::NoteOn { key, .. } => - notes_on[key.as_int() as usize] = true, - MidiMessage::NoteOff { key, .. } => - notes_on[key.as_int() as usize] = false, - _ => {} - } - } - } - for y in 0..buf.height { - if y >= 64 { - break - } - if let Some(block) = half_block( - notes_on[y as usize * 2], - notes_on[y as usize * 2 + 1], - ) { - buf.get_mut(x, y).map(|cell|{ - cell.set_char(block); - cell.set_fg(Color::White); - }); - } - } - if phrase.percussive { - notes_on.fill(false); - } - } - } - } -} + //pub fn is_last_row (&self) -> bool { + //let selected = self.selected; + //(self.scenes().len() == 0 && (selected.is_mix() || selected.is_track())) || match selected { + //ArrangerSelection::Scene(s) => s == self.scenes().len() - 1, + //ArrangerSelection::Clip(_, s) => s == self.scenes().len() - 1, + //_ => false + //} + //} + //pub fn index_before (&self, index: usize) -> usize { + //index.overflowing_sub(1).0.min(self.len() - 1) + //} + //pub fn index_after (&self, index: usize) -> usize { + //(index + 1) % self.len() + //} diff --git a/crates/tek_tui/src/tui_command.rs b/crates/tek_tui/src/tui_command.rs index ce818dbb..93003764 100644 --- a/crates/tek_tui/src/tui_command.rs +++ b/crates/tek_tui/src/tui_command.rs @@ -7,79 +7,6 @@ pub enum TransportCommand { Playhead(PlayheadCommand), } -#[derive(Clone, Debug, PartialEq)] -pub enum SequencerCommand { - Focus(FocusCommand), - Undo, - Redo, - Clear, - Clock(ClockCommand), - Playhead(PlayheadCommand), - Phrases(PhrasesCommand), - Editor(PhraseCommand), -} - -#[derive(Clone, Debug)] -pub enum ArrangerCommand { - Focus(FocusCommand), - Undo, - Redo, - Clear, - Clock(ClockCommand), - Playhead(PlayheadCommand), - Scene(ArrangerSceneCommand), - Track(ArrangerTrackCommand), - Clip(ArrangerClipCommand), - Select(ArrangerSelection), - Zoom(usize), - Phrases(PhrasePoolCommand), - Editor(PhraseCommand), - EditPhrase(Option>>), -} - -#[derive(Clone, PartialEq, Debug)] -pub enum PhrasesCommand { - Select(usize), - Edit(PhrasePoolCommand), - Rename(PhraseRenameCommand), - Length(PhraseLengthCommand), -} - -#[derive(Clone, Debug, PartialEq)] -pub enum PhraseRenameCommand { - Begin, - Set(String), - Confirm, - Cancel, -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum PhraseLengthCommand { - Begin, - Next, - Prev, - Inc, - Dec, - Set(usize), - Cancel, -} - -#[derive(Clone, PartialEq, Debug)] -pub enum PhraseCommand { - // TODO: 1-9 seek markers that by default start every 8th of the phrase - ToggleDirection, - EnterEditMode, - ExitEditMode, - NoteAppend, - NoteSet, - NoteCursorSet(usize), - NoteLengthSet(usize), - NoteScrollSet(usize), - TimeCursorSet(usize), - TimeScrollSet(usize), - TimeZoomSet(usize), -} - impl Command for TransportCommand { fn execute (self, state: &mut T) -> Perhaps { use TransportCommand::{Focus, Clock, Playhead}; @@ -97,6 +24,18 @@ impl Command for TransportCommand { } } +#[derive(Clone, Debug, PartialEq)] +pub enum SequencerCommand { + Focus(FocusCommand), + Undo, + Redo, + Clear, + Clock(ClockCommand), + Playhead(PlayheadCommand), + Phrases(PhrasesCommand), + Editor(PhraseCommand), +} + impl Command for SequencerCommand where T: FocusGrid + PhrasesControl + PhraseControl + ClockApi + PlayheadApi @@ -113,6 +52,24 @@ where } } +#[derive(Clone, Debug)] +pub enum ArrangerCommand { + Focus(FocusCommand), + Undo, + Redo, + Clear, + Clock(ClockCommand), + Playhead(PlayheadCommand), + Scene(ArrangerSceneCommand), + Track(ArrangerTrackCommand), + Clip(ArrangerClipCommand), + Select(ArrangerSelection), + Zoom(usize), + Phrases(PhrasePoolCommand), + Editor(PhraseCommand), + EditPhrase(Option>>), +} + impl Command for ArrangerCommand where T: FocusGrid + ArrangerControl + HasPhrases + PhraseControl + ClockApi + PlayheadApi @@ -161,6 +118,16 @@ impl Command for ArrangerClipCommand { } } +#[derive(Clone, PartialEq, Debug)] +pub enum PhrasesCommand { + Select(usize), + Phrase(PhrasePoolCommand), + Rename(PhraseRenameCommand), + Length(PhraseLengthCommand), + MoveUp, + MoveDown, +} + impl Command for PhrasesCommand { fn execute (self, state: &mut T) -> Perhaps { use PhraseRenameCommand as Rename; @@ -169,8 +136,8 @@ impl Command for PhrasesCommand { Self::Select(phrase) => { state.phrase = phrase }, - Self::Edit(command) => { - return Ok(command.execute(&mut state)?.map(Self::Edit)) + Self::Phrase(command) => { + return Ok(command.execute(&mut state)?.map(Self::Phrase)) } Self::Rename(command) => match command { Rename::Begin => self.phrases_rename_begin(), @@ -185,6 +152,17 @@ impl Command for PhrasesCommand { } } +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum PhraseLengthCommand { + Begin, + Next, + Prev, + Inc, + Dec, + Set(usize), + Cancel, +} + impl Command for PhraseLengthCommand { fn execute (self, state: &mut T) -> Perhaps { use PhraseLengthFocus::*; @@ -229,29 +207,39 @@ impl Command for PhraseLengthCommand { } } -impl Command for PhraseRenameCommand { +#[derive(Clone, Debug, PartialEq)] +pub enum PhraseRenameCommand { + Begin, + Set(String), + Confirm, + Cancel, +} + +impl Command for PhraseRenameCommand +where + T: PhrasesControl +{ fn execute (self, state: &mut T) -> Perhaps { use PhraseRenameCommand::*; - if let Some(PhrasesMode::Rename(phrase, ref mut old_name)) = state.mode { + if let Some(PhrasesMode::Rename(phrase, ref mut old_name)) = state.phrases_mode() { match self { Set(s) => { - state.phrases[phrase].write().unwrap().name = s.into(); + state.phrases()[*phrase].write().unwrap().name = s.into(); return Ok(Some(Self::Set(old_name.clone()))) }, Confirm => { let old_name = old_name.clone(); - state.mode = None; + *state.phrases_mode_mut() = None; return Ok(Some(Self::Set(old_name))) }, Cancel => { - let mut phrase = state.phrases[phrase].write().unwrap(); - phrase.name = old_name.clone(); + state.phrases()[*phrase].write().unwrap().name = old_name.clone(); }, _ => unreachable!() }; Ok(None) } else if self == Begin { - self.phrases_rename_begin(); + self.phrase_rename_begin(); Ok(None) } else { unreachable!() @@ -259,7 +247,26 @@ impl Command for PhraseRenameCommand { } } -impl Command for PhraseCommand { +#[derive(Clone, PartialEq, Debug)] +pub enum PhraseCommand { + // TODO: 1-9 seek markers that by default start every 8th of the phrase + ToggleDirection, + EnterEditMode, + ExitEditMode, + NoteAppend, + NoteSet, + NoteCursorSet(usize), + NoteLengthSet(usize), + NoteScrollSet(usize), + TimeCursorSet(usize), + TimeScrollSet(usize), + TimeZoomSet(usize), +} + +impl Command for PhraseCommand +where + T: PhraseControl + FocusEnter +{ //fn translate (self, state: &PhraseTui) -> Self { //use PhraseCommand::*; //match self { @@ -277,10 +284,10 @@ impl Command for PhraseCommand { state.mode = !state.mode; }, EnterEditMode => { - state.entered = true; + state.focus_enter(); }, ExitEditMode => { - state.entered = false; + state.focus_exit(); }, TimeZoomOut => { let scale = state.time_axis().read().unwrap().scale; @@ -338,14 +345,12 @@ impl Command for PhraseCommand { axis.start_inc(3); axis.point_inc(3); }, - NoteAppend => { - if state.entered { - state.put(); - state.time_cursor_advance(); - } + NoteAppend => if state.focus_entered() { + state.put(); + state.time_cursor_advance(); }, - NoteSet => { - if state.entered { state.put(); } + NoteSet => if state.focus_entered() { + state.put(); }, _ => unreachable!() } diff --git a/crates/tek_tui/src/tui_control.rs b/crates/tek_tui/src/tui_control.rs index 1c641491..94eef920 100644 --- a/crates/tek_tui/src/tui_control.rs +++ b/crates/tek_tui/src/tui_control.rs @@ -14,33 +14,109 @@ impl SequencerControl for SequencerTui {} pub trait ArrangerControl { fn selected (&self) -> ArrangerSelection; + fn show_phrase (&mut self); + fn activate (&mut self); + fn selected_phrase (&self) -> Option>>; + fn toggle_loop (&mut self); + fn randomize_color (&mut self); } impl ArrangerControl for ArrangerTui { fn selected (&self) -> ArrangerSelection { self.selected } + fn show_phrase (&mut self) { + self.editor.show(self.selected_phrase().as_ref()); + } + fn activate (&mut self) { + if let ArrangerSelection::Scene(s) = self.selected { + for (t, track) in self.tracks_mut().iter_mut().enumerate() { + let clip = self.scenes()[s].clips[t].as_ref(); + if track.play_phrase.is_some() || clip.is_some() { + track.enqueue_next(clip); + } + } + // TODO make transport available here, so that + // activating a scene when stopped starts playback + //if self.is_stopped() { + //self.transport.toggle_play() + //} + } else if let ArrangerSelection::Clip(t, s) = self.selected { + self.tracks_mut()[t].enqueue_next(self.scenes()[s].clips[t].as_ref()); + }; + } + fn selected_phrase (&self) -> Option>> { + self.selected_scene()?.clips.get(self.selected.track()?)?.clone() + } + fn toggle_loop (&mut self) { + if let Some(phrase) = self.selected_phrase() { + phrase.write().unwrap().toggle_loop() + } + } + fn randomize_color (&mut self) { + match self.selected { + ArrangerSelection::Mix => { + self.color = ItemColor::random_dark() + }, + ArrangerSelection::Track(t) => { + self.tracks_mut()[t].color = ItemColor::random() + }, + ArrangerSelection::Scene(s) => { + self.scenes_mut()[s].color = ItemColor::random() + }, + ArrangerSelection::Clip(t, s) => { + if let Some(phrase) = &self.scenes_mut()[s].clips[t] { + phrase.write().unwrap().color = ItemColorTriplet::random(); + } + } + } + } } -pub trait PhrasesControl { +pub trait PhrasesControl: HasPhrases { + fn phrase_index (&self) -> usize; + fn phrase_index_mut (&mut self) -> &mut usize; fn phrases_mode (&self) -> &Option; fn phrases_mode_mut (&mut self) -> &mut Option; fn phrase_rename_begin (&mut self) { - *self.phrases_mode_mut() = Some(PhrasesMode::Rename( - self.phrase, - self.phrases[self.phrase].read().unwrap().name.clone() - )) + let name = self.phrases()[self.phrase_index()].read().unwrap().name.clone(); + *self.phrases_mode_mut() = Some( + PhrasesMode::Rename(self.phrase_index(), name) + ) } fn phrase_length_begin (&mut self) { - *self.phrases_mode_mut() = Some(PhrasesMode::Length( - self.phrase, - self.phrases[self.phrase].read().unwrap().length, - PhraseLengthFocus::Bar - )) + let length = self.phrases()[self.phrase_index()].read().unwrap().length; + *self.phrases_mode_mut() = Some( + PhrasesMode::Length(self.phrase_index(), length, PhraseLengthFocus::Bar) + ) + } + fn new_phrase (name: Option<&str>, color: Option) -> Arc> { + Arc::new(RwLock::new(Phrase::new( + String::from(name.unwrap_or("(new)")), true, 4 * PPQ, None, color + ))) + } + fn index_of (&self, phrase: &Phrase) -> Option { + for i in 0..self.phrases().len() { + if *self.phrases()[i].read().unwrap() == *phrase { return Some(i) } + } + return None + } + fn insert_dup (&mut self) { + let mut phrase = self.phrases()[self.phrase_index()].read().unwrap().duplicate(); + phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25); + let index = self.phrase_index() + 1; + self.phrases_mut().insert(index, Arc::new(RwLock::new(phrase))); + *self.phrase_index_mut() += 1; } } impl PhrasesControl for SequencerTui { + fn phrase_index (&self) -> usize { + self.view_phrase + } + fn phrase_index_mut (&mut self) -> &mut usize { + &mut self.view_phrase + } fn phrases_mode (&self) -> &Option { &self.phrases_mode } @@ -50,6 +126,12 @@ impl PhrasesControl for SequencerTui { } impl PhrasesControl for ArrangerTui { + fn phrase_index (&self) -> usize { + self.phrase + } + fn phrase_index_mut (&mut self) -> &mut usize { + &mut self.phrase + } fn phrases_mode (&self) -> &Option { &self.phrases_mode } @@ -59,6 +141,12 @@ impl PhrasesControl for ArrangerTui { } impl PhrasesControl for PhrasesTui { + fn phrase_index (&self) -> usize { + self.phrase + } + fn phrase_index_mut (&mut self) -> &mut usize { + &mut self.phrase + } fn phrases_mode (&self) -> &Option { &self.mode } @@ -73,6 +161,12 @@ pub trait PhraseControl { fn note_axis (&self) -> &RwLock>; fn note_len (&self) -> usize; fn note_len_mut (&mut self) -> &mut usize; + fn time_cursor_advance (&self) { + let point = self.time_axis().read().unwrap().point; + let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + let forward = |time|(time + self.note_len()) % length; + self.time_axis().write().unwrap().point = point.map(forward); + } } impl PhraseControl for SequencerTui { diff --git a/crates/tek_tui/src/tui_input.rs b/crates/tek_tui/src/tui_input.rs index 8fd4d2d3..f6c762fe 100644 --- a/crates/tek_tui/src/tui_input.rs +++ b/crates/tek_tui/src/tui_input.rs @@ -251,19 +251,42 @@ where impl InputToCommand for PhrasesCommand { fn input_to_command (state: &T, input: &TuiInput) -> Option { - use PhrasePoolCommand as Edit; + use PhrasePoolCommand as Phrase; use PhraseRenameCommand as Rename; use PhraseLengthCommand as Length; + let index = state.phrase(); + let count = state.phrases().len(); match input.event() { key!(KeyCode::Up) => Some(Self::Select(0)), key!(KeyCode::Down) => Some(Self::Select(0)), - key!(KeyCode::Char(',')) => Some(Self::Edit(Edit::Swap(0, 0))), - key!(KeyCode::Char('.')) => Some(Self::Edit(Edit::Swap(0, 0))), - key!(KeyCode::Delete) => Some(Self::Edit(Edit::Delete(0))), - key!(KeyCode::Char('a')) => Some(Self::Edit(Edit::Add(0))), - key!(KeyCode::Char('i')) => Some(Self::Edit(Edit::Add(0))), - key!(KeyCode::Char('d')) => Some(Self::Edit(Edit::Duplicate(0))), - key!(KeyCode::Char('c')) => Some(Self::Edit(Edit::RandomColor(0))), + key!(KeyCode::Char(',')) => { + if index > 1 { + Some(Self::Phrase(Phrase::Swap(index - 1, index))) + //index -= 1; + } else { + None + } + }, + key!(KeyCode::Char('.')) => { + if index < count.saturating_sub(1) { + Some(Self::Phrase(Phrase::Swap(index + 1, index))) + //index += 1; + } else { + None + } + }, + key!(KeyCode::Delete) => { + if index > 0 { + Some(Self::Delete(index)) + //index = index.min(count.saturating_sub(1)); + } else { + None + } + }, + key!(KeyCode::Char('a')) => Some(Self::Phrase(Phrase::Add(count))), + key!(KeyCode::Char('i')) => Some(Self::Phrase(Phrase::Add(index + 1))), + key!(KeyCode::Char('d')) => Some(Self::Phrase(Phrase::Duplicate(index))), + key!(KeyCode::Char('c')) => Some(Self::Phrase(Phrase::RandomColor(index))), key!(KeyCode::Char('n')) => Some(Self::Rename(Rename::Begin)), key!(KeyCode::Char('t')) => Some(Self::Length(Length::Begin)), _ => match state.phrases_mode() { diff --git a/crates/tek_tui/src/tui_menu.rs b/crates/tek_tui/src/tui_menu.rs index c7b7e813..7ae59332 100644 --- a/crates/tek_tui/src/tui_menu.rs +++ b/crates/tek_tui/src/tui_menu.rs @@ -1 +1,36 @@ use crate::*; + +fn content_with_menu_and_status <'a, A, S, C> ( + content: &'a A, + menu_bar: &'a Option>, + status_bar: &'a Option +) -> impl Widget + 'a +where + A: Widget, + S: Send + Sync + 'a, + C: Command +{ + let menus = menu_bar.as_ref().map_or_else( + ||&[] as &[Menu<_, _, _>], + |m|m.menus.as_slice() + ); + Either( + menu_bar.is_none(), + Either( + status_bar.is_none(), + widget(content), + Split::up( + 1, + widget(status_bar.as_ref().unwrap()), + widget(content) + ), + ), + Split::down( + 1, + row!(menu in menus.iter() => { + row!(" ", menu.title.as_str(), " ") + }), + widget(content) + ) + ) +} diff --git a/crates/tek_tui/src/tui_model.rs b/crates/tek_tui/src/tui_model.rs index 1f83754a..e5ff416c 100644 --- a/crates/tek_tui/src/tui_model.rs +++ b/crates/tek_tui/src/tui_model.rs @@ -78,6 +78,36 @@ pub struct SequencerTui { pub(crate) phrases_mode: Option, } +impl HasPhrases for SequencerTui { + fn phrases (&self) -> &Vec>> { + &self.phrases + } + fn phrases_mut (&mut self) -> &mut Vec>> { + &mut self.phrases + } +} + +impl HasPhrase for SequencerTui { + fn reset (&self) -> bool { + self.reset + } + fn reset_mut (&mut self) -> &mut bool { + &mut self.reset + } + fn phrase (&self) -> &Option<(Instant, Option>>)> { + todo!() + } + fn phrase_mut (&self) -> &mut Option<(Instant, Option>>)> { + todo!() + } + fn next_phrase (&self) -> &Option<(Instant, Option>>)> { + todo!() + } + fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option>>)> { + todo!() + } +} + /// Root view for standalone `tek_arranger` pub struct ArrangerTui { pub(crate) jack: Arc>, @@ -109,6 +139,43 @@ pub struct ArrangerTui { pub(crate) phrases_mode: Option, } +impl HasPhrases for ArrangerTui { + fn phrases (&self) -> &Vec>> { + &self.phrases + } + fn phrases_mut (&mut self) -> &mut Vec>> { + &mut self.phrases + } +} + +impl HasScenes for ArrangerTui { + fn scenes (&self) -> &Vec { + &self.scenes + } + fn scenes_mut (&mut self) -> &mut Vec { + &mut self.scenes + } + fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerScene> + { + let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string()); + let scene = ArrangerScene { + name: Arc::new(name.into()), + clips: vec![None;self.tracks().len()], + color: color.unwrap_or_else(||ItemColor::random()), + }; + self.scenes_mut().push(scene); + let index = self.scenes().len() - 1; + Ok(&mut self.scenes_mut()[index]) + } + fn selected_scene (&self) -> Option<&ArrangerScene> { + self.selected.scene().map(|s|self.scenes().get(s)).flatten() + } + fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { + self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten() + } +} + #[derive(Default, Debug, Clone)] pub struct ArrangerScene { /// Name of scene @@ -119,6 +186,59 @@ pub struct ArrangerScene { pub(crate) color: ItemColor, } +impl ArrangerSceneApi for ArrangerScene { + fn name (&self) -> &Arc> { + &self.name + } + fn clips (&self) -> &Vec>>> { + &self.clips + } + fn color (&self) -> ItemColor { + self.color + } +} + +impl HasTracks for ArrangerTui { + fn tracks (&self) -> &Vec { + &self.tracks + } + fn tracks_mut (&mut self) -> &mut Vec { + &mut self.tracks + } +} + +impl ArrangerTracksApi for ArrangerTui { + fn track_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerTrack> + { + let name = name.map_or_else(||self.track_default_name(), |x|x.to_string()); + let track = ArrangerTrack { + width: name.len() + 2, + name: Arc::new(name.into()), + color: color.unwrap_or_else(||ItemColor::random()), + midi_ins: vec![], + midi_outs: vec![], + reset: true, + recording: false, + monitoring: false, + overdub: false, + play_phrase: None, + next_phrase: None, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + }; + self.tracks_mut().push(track); + let index = self.tracks().len() - 1; + Ok(&mut self.tracks_mut()[index]) + } + fn track_del (&mut self, index: usize) { + self.tracks_mut().remove(index); + for scene in self.scenes_mut().iter_mut() { + scene.clips.remove(index); + } + } +} + #[derive(Debug)] pub struct ArrangerTrack { /// Name of track @@ -159,6 +279,120 @@ pub struct ArrangerTrack { pub(crate) phrases_mode: Option, } +impl ArrangerTrackApi for ArrangerTrack { + /// Name of track + fn name (&self) -> &Arc> { + &self.name + } + /// Preferred width of track column + fn width (&self) -> usize { + self.width + } + /// Preferred width of track column + fn width_mut (&mut self) -> &mut usize { + &mut self.width + } + /// Identifying color of track + fn color (&self) -> ItemColor { + self.color + } +} + +impl HasPhrase for ArrangerTrack { + fn reset (&self) -> bool { + self.reset + } + fn reset_mut (&mut self) -> &mut bool { + &mut self.reset + } + fn phrase (&self) -> &Option<(Instant, Option>>)> { + todo!() + } + fn phrase_mut (&self) -> &mut Option<(Instant, Option>>)> { + todo!() + } + fn next_phrase (&self) -> &Option<(Instant, Option>>)> { + todo!() + } + fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option>>)> { + todo!() + } +} + +impl MidiInputApi for ArrangerTrack { + fn midi_ins (&self) -> &Vec> { + todo!() + } + fn midi_ins_mut (&self) -> &mut Vec> { + todo!() + } + fn recording (&self) -> bool { + todo!() + } + fn recording_mut (&mut self) -> &mut bool { + todo!() + } + fn monitoring (&self) -> bool { + todo!() + } + fn monitoring_mut (&mut self) -> &mut bool { + todo!() + } + fn overdub (&self) -> bool { + todo!() + } + fn overdub_mut (&mut self) -> &mut bool { + todo!() + } + fn notes_in (&self) -> &Arc> { + todo!() + } +} + +impl MidiOutputApi for ArrangerTrack { + fn midi_outs (&self) -> &Vec> { + todo!() + } + fn midi_outs_mut (&mut self) -> &mut Vec> { + todo!() + } + fn midi_note (&mut self) -> &mut Vec { + todo!() + } + fn notes_out (&self) -> &Arc> { + todo!() + } +} + +impl ClockApi for ArrangerTrack { + fn timebase (&self) -> &Arc { + todo!() + } + fn quant (&self) -> &Quantize { + todo!() + } + fn sync (&self) -> &LaunchSync { + todo!() + } +} + +impl PlayheadApi for ArrangerTrack { + fn current (&self) -> &Instant { + todo!() + } + fn transport (&self) -> &Transport { + todo!() + } + fn playing (&self) -> &RwLock> { + todo!() + } + fn started (&self) -> &RwLock> { + todo!() + } +} + +impl PlayerApi for ArrangerTrack {} + pub struct PhrasesTui { /// Collection of phrases pub(crate) phrases: Vec>>, @@ -174,6 +408,15 @@ pub struct PhrasesTui { pub(crate) entered: bool, } +impl HasPhrases for PhrasesTui { + fn phrases (&self) -> &Vec>> { + &self.phrases + } + fn phrases_mut (&mut self) -> &mut Vec>> { + &mut self.phrases + } +} + /// Modes for phrase pool pub enum PhrasesMode { /// Renaming a pattern diff --git a/crates/tek_tui/src/tui_view.rs b/crates/tek_tui/src/tui_view.rs index 208d2a1f..077b01f7 100644 --- a/crates/tek_tui/src/tui_view.rs +++ b/crates/tek_tui/src/tui_view.rs @@ -767,3 +767,108 @@ pub(crate) fn keys_vert () -> Buffer { const NTH_OCTAVE: [&'static str; 11] = [ "-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", ]; + +impl PhraseTui { + pub fn put (&mut self) { + if let (Some(phrase), Some(time), Some(note)) = ( + &self.phrase, + self.time_axis.read().unwrap().point, + self.note_axis.read().unwrap().point, + ) { + let mut phrase = phrase.write().unwrap(); + let key: u7 = u7::from((127 - note) as u8); + let vel: u7 = 100.into(); + let start = time; + let end = (start + self.note_len) % phrase.length; + phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); + phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); + self.buffer = Self::redraw(&phrase); + } + } + /// Select which pattern to display. This pre-renders it to the buffer at full resolution. + pub fn show (&mut self, phrase: Option<&Arc>>) { + if let Some(phrase) = phrase { + self.phrase = Some(phrase.clone()); + self.time_axis.write().unwrap().clamp = Some(phrase.read().unwrap().length); + self.buffer = Self::redraw(&*phrase.read().unwrap()); + } else { + self.phrase = None; + self.time_axis.write().unwrap().clamp = Some(0); + self.buffer = Default::default(); + } + } + fn redraw (phrase: &Phrase) -> BigBuffer { + let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65); + Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq); + Self::fill_seq_fg(&mut buf, &phrase); + buf + } + fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) { + for x in 0..buf.width { + // Only fill as far as phrase length + if x as usize >= length { break } + // Fill each row with background characters + for y in 0 .. buf.height { + buf.get_mut(x, y).map(|cell|{ + cell.set_char(if ppq == 0 { + '·' + } else if x % (4 * ppq) == 0 { + '│' + } else if x % ppq == 0 { + '╎' + } else { + '·' + }); + cell.set_fg(Color::Rgb(48, 64, 56)); + cell.modifier = Modifier::DIM; + }); + } + } + } + fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) { + let mut notes_on = [false;128]; + for x in 0..buf.width { + if x as usize >= phrase.length { + break + } + if let Some(notes) = phrase.notes.get(x as usize) { + if phrase.percussive { + for note in notes { + match note { + MidiMessage::NoteOn { key, .. } => + notes_on[key.as_int() as usize] = true, + _ => {} + } + } + } else { + for note in notes { + match note { + MidiMessage::NoteOn { key, .. } => + notes_on[key.as_int() as usize] = true, + MidiMessage::NoteOff { key, .. } => + notes_on[key.as_int() as usize] = false, + _ => {} + } + } + } + for y in 0..buf.height { + if y >= 64 { + break + } + if let Some(block) = half_block( + notes_on[y as usize * 2], + notes_on[y as usize * 2 + 1], + ) { + buf.get_mut(x, y).map(|cell|{ + cell.set_char(block); + cell.set_fg(Color::White); + }); + } + } + if phrase.percussive { + notes_on.fill(false); + } + } + } + } +}