diff --git a/crates/tek/src/midi.rs b/crates/tek/src/midi.rs index 240d102f..9e8ff123 100644 --- a/crates/tek/src/midi.rs +++ b/crates/tek/src/midi.rs @@ -7,8 +7,6 @@ pub(crate) mod midi_out; pub(crate) use midi_out::*; pub(crate) mod midi_phrase; pub(crate) use midi_phrase::*; pub(crate) mod midi_play; pub(crate) use midi_play::*; pub(crate) mod midi_rec; pub(crate) use midi_rec::*; -pub(crate) mod midi_scene; pub(crate) use midi_scene::*; -pub(crate) mod midi_track; pub(crate) use midi_track::*; /// Add "all notes off" to the start of a buffer. pub fn all_notes_off (output: &mut [Vec>]) { diff --git a/crates/tek/src/midi/midi_scene.rs b/crates/tek/src/midi/midi_scene.rs deleted file mode 100644 index fe0825dc..00000000 --- a/crates/tek/src/midi/midi_scene.rs +++ /dev/null @@ -1,96 +0,0 @@ -use crate::*; - -pub trait HasScenes { - fn scenes (&self) -> &Vec; - fn scenes_mut (&mut self) -> &mut Vec; - fn scene_add (&mut self, name: Option<&str>, color: Option) -> Usually<&mut S>; - fn scene_del (&mut self, index: usize) { - self.scenes_mut().remove(index); - } - 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)] -pub enum ArrangerSceneCommand { - Add, - Delete(usize), - RandomColor, - Play(usize), - Swap(usize, usize), - SetSize(usize), - SetZoom(usize), -} - -//impl Command for ArrangerSceneCommand { - //fn execute (self, state: &mut T) -> Perhaps { - //match self { - //Self::Delete(index) => { state.scene_del(index); }, - //_ => todo!() - //} - //Ok(None) - //} -//} - -pub trait ArrangerSceneApi: Sized { - fn name (&self) -> &Arc>; - fn clips (&self) -> &Vec>>>; - fn color (&self) -> ItemPalette; - - fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> { - let mut total = 0; - if factor == 0 { - scenes.iter().map(|scene|{ - let pulses = scene.pulses().max(PPQ); - total = total + pulses; - (pulses, total - pulses) - }).collect() - } else { - (0..=scenes.len()).map(|i|{ - (factor*PPQ, factor*PPQ*i) - }).collect() - } - } - - fn longest_name (scenes: &[Self]) -> usize { - scenes.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) - } - - /// Returns the pulse length of the longest phrase in the scene - fn pulses (&self) -> usize { - self.clips().iter().fold(0, |a, p|{ - a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) - }) - } - - /// Returns true if all phrases in the scene are - /// currently playing on the given collection of tracks. - fn is_playing (&self, tracks: &[T]) -> bool { - self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate() - .all(|(track_index, clip)|match clip { - Some(clip) => tracks - .get(track_index) - .map(|track|{ - if let Some((_, Some(phrase))) = track.player().play_phrase() { - *phrase.read().unwrap() == *clip.read().unwrap() - } else { - false - } - }) - .unwrap_or(false), - None => true - }) - } - - fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None } - } - -} diff --git a/crates/tek/src/midi/midi_track.rs b/crates/tek/src/midi/midi_track.rs deleted file mode 100644 index 0a4fc7e6..00000000 --- a/crates/tek/src/midi/midi_track.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::*; - -pub trait HasTracks: Send + Sync { - fn tracks (&self) -> &Vec; - fn tracks_mut (&mut self) -> &mut Vec; -} - -impl HasTracks for Vec { - fn tracks (&self) -> &Vec { - self - } - fn tracks_mut (&mut self) -> &mut Vec { - self - } -} - -pub trait ArrangerTracksApi: HasTracks { - fn track_add (&mut self, name: Option<&str>, color: Option)-> Usually<&mut T>; - fn track_del (&mut self, index: usize); - fn track_default_name (&self) -> String { - format!("Track {}", self.tracks().len() + 1) - } -} - -#[derive(Clone, Debug)] -pub enum ArrangerTrackCommand { - Add, - Delete(usize), - RandomColor, - Stop, - Swap(usize, usize), - SetSize(usize), - SetZoom(usize), -} - -pub trait ArrangerTrackApi: HasPlayer + Send + Sync + Sized { - /// Name of track - fn name (&self) -> &Arc>; - /// Preferred width of track column - fn width (&self) -> usize; - /// Preferred width of track column - fn width_mut (&mut self) -> &mut usize; - /// Identifying color of track - fn color (&self) -> ItemPalette; - - fn longest_name (tracks: &[Self]) -> usize { - tracks.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) - } - - const MIN_WIDTH: usize = 3; - - fn width_inc (&mut self) { - *self.width_mut() += 1; - } - - fn width_dec (&mut self) { - if self.width() > Self::MIN_WIDTH { - *self.width_mut() -= 1; - } - } -} - -/// Hosts the JACK callback for a collection of tracks -pub struct TracksAudio<'a, T: ArrangerTrackApi, H: HasTracks>( - // Track collection - pub &'a mut H, - /// Note buffer - pub &'a mut Vec, - /// Note chunk buffer - pub &'a mut Vec>>, - /// Marker - pub PhantomData, -); - -impl<'a, T: ArrangerTrackApi, H: HasTracks> Audio for TracksAudio<'a, T, H> { - #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - let model = &mut self.0; - let note_buffer = &mut self.1; - let output_buffer = &mut self.2; - for track in model.tracks_mut().iter_mut() { - if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit { - return Control::Quit - } - } - Control::Continue - } -} diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 20c27cd4..bd05a5f9 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -1,4 +1,6 @@ use crate::*; +use ClockCommand::{Play, Pause}; +use KeyCode::{Char, Down, Right, Delete}; /// Root view for standalone `tek_arranger` pub struct ArrangerTui { pub jack: Arc>, @@ -42,10 +44,6 @@ from_jack!(|jack| ArrangerTui Self { note_buf: vec![], perf: PerfModel::default(), }); -has_clock!(|self: ArrangerTui|&self.clock); -has_phrases!(|self: ArrangerTui|self.phrases.phrases); -has_editor!(|self: ArrangerTui|self.editor); -handle!(|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input)); render!(|self: ArrangerTui|{ let arranger = ||lay!(|add|{ let color = self.color; @@ -112,10 +110,15 @@ audio!(|self: ArrangerTui, client, scope|{ self.perf.update(t0, scope); return Control::Continue }); +has_clock!(|self: ArrangerTui|&self.clock); +has_phrases!(|self: ArrangerTui|self.phrases.phrases); +has_editor!(|self: ArrangerTui|self.editor); +handle!(|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input)); #[derive(Clone, Debug)] pub enum ArrangerCommand { Undo, Redo, Clear, + StopAll, Color(ItemPalette), Clock(ClockCommand), Scene(ArrangerSceneCommand), @@ -126,35 +129,60 @@ audio!(|self: ArrangerTui, client, scope|{ Phrases(PhrasesCommand), Editor(PhraseCommand), } -input_to_command!(ArrangerCommand: |state:ArrangerTui,input|to_arranger_command(state, input)?); -fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option { - use ArrangerSelection::*; - use ArrangerCommand as Cmd; - use KeyCode::Char; +input_to_command!(ArrangerCommand: |state:ArrangerTui,input|{ + use ArrangerSelection as Selected; + use ArrangerCommand::*; // WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor - Some(match input.event() { - key_pat!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))), - key_pat!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)), - key_pat!(Char('+')) => Cmd::Zoom(0), // TODO - key_pat!(Char('=')) => Cmd::Zoom(0), // TODO - key_pat!(Char('_')) => Cmd::Zoom(0), // TODO - key_pat!(Char('-')) => Cmd::Zoom(0), // TODO - key_pat!(Char('`')) => { todo!("toggle state mode") }, - key_pat!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add), - key_pat!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add), + match input.event() { + // Transport: Play/pause + key_pat!(Char(' ')) => + Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), + // Transport: Play from start or rewind to start + key_pat!(Shift-Char(' ')) => + Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), + // TODO: u: undo + key_pat!(Char('u')) => + { todo!("undo") }, + // TODO: Shift-U: redo + key_pat!(Char('U')) => + { todo!("redo") }, + // TODO: k: toggle on-screen keyboard + key_pat!(Ctrl-Char('k')) => + { todo!("keyboard") }, + key_pat!(Char('e')) => + Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))), + key_pat!(Char('l')) => + Clip(ArrangerClipCommand::SetLoop(false)), + key_pat!(Ctrl-Char('a')) => + Scene(ArrangerSceneCommand::Add), + key_pat!(Ctrl-Char('t')) => + Track(ArrangerTrackCommand::Add), + key_pat!(Char('0')) => match state.selected() { + Selected::Mix => StopAll, + Selected::Track(t) => return None, + Selected::Scene(s) => return None, + Selected::Clip(t, s) => return None, + }, + key_pat!(Char('q')) => match state.selected() { + Selected::Mix => return None, + Selected::Track(t) => return None, + Selected::Scene(s) => return None, + Selected::Clip(t, s) => return None, + }, _ => match state.selected() { - Mix => to_arranger_mix_command(input)?, - Track(t) => to_arranger_track_command(input, t)?, - Scene(s) => to_arranger_scene_command(input, s)?, - Clip(t, s) => to_arranger_clip_command(input, t, s)?, + Selected::Mix => to_arranger_mix_command(input)?, + Selected::Track(t) => to_arranger_track_command(input, t)?, + Selected::Scene(s) => to_arranger_scene_command(input, s)?, + Selected::Clip(t, s) => to_arranger_clip_command(input, t, s)?, } - }) -} + } +}); fn to_arranger_mix_command (input: &TuiInput) -> Option { - use KeyCode::{Char, Down, Right, Delete}; use ArrangerCommand as Cmd; use ArrangerSelection as Select; Some(match input.event() { + // 0: Enqueue phrase 0 (stop all) + key_pat!(Char('0')) => Cmd::StopAll, key_pat!(Char('s')) => Cmd::Select(Select::Scene(0)), key_pat!(Char('d')) => Cmd::Select(Select::Track(0)), key_pat!(Char(',')) => Cmd::Zoom(0), diff --git a/crates/tek/src/tui/arranger_scene.rs b/crates/tek/src/tui/arranger_scene.rs index 4f0139be..db61f32a 100644 --- a/crates/tek/src/tui/arranger_scene.rs +++ b/crates/tek/src/tui/arranger_scene.rs @@ -1,4 +1,99 @@ use crate::*; + +pub trait HasScenes { + fn scenes (&self) -> &Vec; + fn scenes_mut (&mut self) -> &mut Vec; + fn scene_add (&mut self, name: Option<&str>, color: Option) -> Usually<&mut S>; + fn scene_del (&mut self, index: usize) { + self.scenes_mut().remove(index); + } + 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)] +pub enum ArrangerSceneCommand { + Add, + Delete(usize), + RandomColor, + Play(usize), + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), +} + +//impl Command for ArrangerSceneCommand { + //fn execute (self, state: &mut T) -> Perhaps { + //match self { + //Self::Delete(index) => { state.scene_del(index); }, + //_ => todo!() + //} + //Ok(None) + //} +//} + +pub trait ArrangerSceneApi: Sized { + fn name (&self) -> &Arc>; + fn clips (&self) -> &Vec>>>; + fn color (&self) -> ItemPalette; + + fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> { + let mut total = 0; + if factor == 0 { + scenes.iter().map(|scene|{ + let pulses = scene.pulses().max(PPQ); + total = total + pulses; + (pulses, total - pulses) + }).collect() + } else { + (0..=scenes.len()).map(|i|{ + (factor*PPQ, factor*PPQ*i) + }).collect() + } + } + + fn longest_name (scenes: &[Self]) -> usize { + scenes.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) + } + + /// Returns the pulse length of the longest phrase in the scene + fn pulses (&self) -> usize { + self.clips().iter().fold(0, |a, p|{ + a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) + }) + } + + /// Returns true if all phrases in the scene are + /// currently playing on the given collection of tracks. + fn is_playing (&self, tracks: &[T]) -> bool { + self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate() + .all(|(track_index, clip)|match clip { + Some(clip) => tracks + .get(track_index) + .map(|track|{ + if let Some((_, Some(phrase))) = track.player().play_phrase() { + *phrase.read().unwrap() == *clip.read().unwrap() + } else { + false + } + }) + .unwrap_or(false), + None => true + }) + } + + fn clip (&self, index: usize) -> Option<&Arc>> { + match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None } + } + +} pub fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option { use KeyCode::{Char, Up, Down, Right, Enter, Delete}; use ArrangerCommand as Cmd; @@ -18,6 +113,7 @@ pub fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option return None }) } + impl HasScenes for ArrangerTui { fn scenes (&self) -> &Vec { &self.scenes diff --git a/crates/tek/src/tui/arranger_track.rs b/crates/tek/src/tui/arranger_track.rs index 74077109..cdb63b23 100644 --- a/crates/tek/src/tui/arranger_track.rs +++ b/crates/tek/src/tui/arranger_track.rs @@ -1,18 +1,104 @@ use crate::*; +use KeyCode::{Char, Down, Left, Right, Delete}; + +pub trait HasTracks: Send + Sync { + fn tracks (&self) -> &Vec; + fn tracks_mut (&mut self) -> &mut Vec; +} + +impl HasTracks for Vec { + fn tracks (&self) -> &Vec { + self + } + fn tracks_mut (&mut self) -> &mut Vec { + self + } +} + +pub trait ArrangerTracksApi: HasTracks { + fn track_add (&mut self, name: Option<&str>, color: Option)-> Usually<&mut T>; + fn track_del (&mut self, index: usize); + fn track_default_name (&self) -> String { + format!("Track {}", self.tracks().len() + 1) + } +} + +#[derive(Clone, Debug)] +pub enum ArrangerTrackCommand { + Add, + Delete(usize), + RandomColor, + Stop, + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), +} + +pub trait ArrangerTrackApi: HasPlayer + Send + Sync + Sized { + /// Name of track + fn name (&self) -> &Arc>; + /// Preferred width of track column + fn width (&self) -> usize; + /// Preferred width of track column + fn width_mut (&mut self) -> &mut usize; + /// Identifying color of track + fn color (&self) -> ItemPalette; + + fn longest_name (tracks: &[Self]) -> usize { + tracks.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) + } + + const MIN_WIDTH: usize = 3; + + fn width_inc (&mut self) { + *self.width_mut() += 1; + } + + fn width_dec (&mut self) { + if self.width() > Self::MIN_WIDTH { + *self.width_mut() -= 1; + } + } +} + +/// Hosts the JACK callback for a collection of tracks +pub struct TracksAudio<'a, T: ArrangerTrackApi, H: HasTracks>( + // Track collection + pub &'a mut H, + /// Note buffer + pub &'a mut Vec, + /// Note chunk buffer + pub &'a mut Vec>>, + /// Marker + pub PhantomData, +); + +impl<'a, T: ArrangerTrackApi, H: HasTracks> Audio for TracksAudio<'a, T, H> { + #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + let model = &mut self.0; + let note_buffer = &mut self.1; + let output_buffer = &mut self.2; + for track in model.tracks_mut().iter_mut() { + if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit { + return Control::Quit + } + } + Control::Continue + } +} pub fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option { - use KeyCode::{Char, Down, Left, Right, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerTrackCommand as Track; + use ArrangerCommand::*; + use ArrangerSelection as Selected; + use ArrangerTrackCommand as Tracks; Some(match input.event() { - key_pat!(Char('s')) => Cmd::Select(Select::Clip(t, 0)), - key_pat!(Char('a')) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }), - key_pat!(Char('d')) => Cmd::Select(Select::Track(t + 1)), - key_pat!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)), - key_pat!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)), - key_pat!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)), - key_pat!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)), - key_pat!(Delete) => Cmd::Track(Track::Delete(t)), + key_pat!(Char('s')) => Select(Selected::Clip(t, 0)), + key_pat!(Char('a')) => Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix }), + key_pat!(Char('d')) => Select(Selected::Track(t + 1)), + key_pat!(Char(',')) => Track(Tracks::Swap(t, t - 1)), + key_pat!(Char('.')) => Track(Tracks::Swap(t, t + 1)), + key_pat!(Char('<')) => Track(Tracks::Swap(t, t - 1)), + key_pat!(Char('>')) => Track(Tracks::Swap(t, t + 1)), + key_pat!(Delete) => Track(Tracks::Delete(t)), //key_pat!(Char('c')) => Cmd::Track(Track::Color(t, ItemPalette::random())), _ => return None })