From 06b643e2b1399f2e1c90201c39c47b6522b135f8 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 11 Jan 2025 22:44:12 +0100 Subject: [PATCH] finally, flatten arranger --- sampler/src/sampler.rs | 4 +- tek/src/app.rs | 339 --------------- tek/src/arranger.rs | 525 ++++++++++++----------- tek/src/arranger/arranger_h.rs | 1 - tek/src/arranger/arranger_scene.rs | 118 ----- tek/src/arranger/arranger_select.rs | 56 --- tek/src/arranger/arranger_track.rs | 189 -------- tek/src/{arranger => }/arranger_keys.edn | 0 tek/src/audio.rs | 132 ++++++ tek/src/control.rs | 450 +++++++++++++++++++ tek/src/groovebox.rs | 115 ----- tek/src/lib.rs | 6 +- tek/src/sequencer.rs | 94 ---- tek/src/view.rs | 338 +++++++++++++++ 14 files changed, 1193 insertions(+), 1174 deletions(-) delete mode 100644 tek/src/arranger/arranger_h.rs delete mode 100644 tek/src/arranger/arranger_scene.rs delete mode 100644 tek/src/arranger/arranger_select.rs delete mode 100644 tek/src/arranger/arranger_track.rs rename tek/src/{arranger => }/arranger_keys.edn (100%) create mode 100644 tek/src/audio.rs create mode 100644 tek/src/control.rs create mode 100644 tek/src/view.rs diff --git a/sampler/src/sampler.rs b/sampler/src/sampler.rs index d8cf1241..3eda1b03 100644 --- a/sampler/src/sampler.rs +++ b/sampler/src/sampler.rs @@ -646,13 +646,13 @@ impl Content for AddSampleModal { handle!(TuiIn: |self: SamplerTui, input|SamplerTuiCommand::execute_with_state(self, input.event())); -pub enum SamplerTuiCommand { +#[derive(Clone, Debug)] pub enum SamplerTuiCommand { Import(FileBrowserCommand), Select(usize), Sample(SamplerCommand), } -pub enum SamplerCommand { +#[derive(Clone, Debug)] pub enum SamplerCommand { RecordBegin(u7), RecordCancel, RecordFinish, diff --git a/tek/src/app.rs b/tek/src/app.rs index 10a585da..fb995297 100644 --- a/tek/src/app.rs +++ b/tek/src/app.rs @@ -87,342 +87,3 @@ impl App { } } } -render!(TuiOut: (self: App) => self.size.of(EdnView::from_source(self, self.edn.as_ref()))); -audio!(|self: App, _client, _scope|Control::Continue); -impl EdnViewData for &App { - fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> { - use EdnItem::*; - let w = self.tracks_with_sizes().last().map(|x|x.3 as u16).unwrap_or(0); - match item { - Nil => Box::new(()), - Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), - Sym(":editor") => (&self.editor).boxed(), - Sym(":inputs") => self.input_row(w, 3).boxed(), - Sym(":outputs") => self.output_row(w, 3).boxed(), - Sym(":pool") => self.pool().boxed(), - Sym(":sample") => self.sample().boxed(), - Sym(":sampler") => self.sampler().boxed(), - Sym(":scenes") => self.scene_row(w, self.size.h().saturating_sub(9) as u16).boxed(), - Sym(":status") => self.status(0).boxed(), - Sym(":toolbar") => self.toolbar().boxed(), - Sym(":tracks") => self.track_row(w, 3).boxed(), - _ => panic!("no content for {item:?}") - } - } - fn get_unit (&self, item: EdnItem<&str>) -> u16 { - use EdnItem::*; - match item.to_str() { - ":sample-h" => if self.compact() { 0 } else { 5 }, - ":samples-w" => if self.compact() { 4 } else { 11 }, - ":samples-y" => if self.compact() { 1 } else { 0 }, - ":pool-w" => if self.compact() { 5 } else { - let w = self.size.w(); - if w > 60 { 20 } else if w > 40 { 15 } else { 10 } - }, - _ => 0 - } - } -} -impl App { - fn compact (&self) -> bool { false } - fn toolbar (&self) -> impl Content + use<'_> { - Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock)))) - } - fn status (&self, note_pt: usize) -> impl Content + use<'_> { - self.editor.as_ref() - .map(|e|Bsp::e(e.clip_status(), e.edit_status())) - } - fn pool (&self) -> impl Content + use<'_> { - self.pool.as_ref() - .map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))) - } - fn editor (&self) -> impl Content + '_ { - &self.editor - } - fn sample <'a> (&'a self) -> impl Content + 'a { - let compact = self.is_editing(); - if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) { - let note_pt = editor.note_point(); - let sample_h = if compact { 0 } else { 5 }; - return Some(Max::y(sample_h, Fill::xy(Bsp::a( - Fill::x(Align::w(Fixed::y(1, self.status(note_pt)))), - sampler.viewer(note_pt) - )))) - } - None - } - fn sampler (&self) -> impl Content + use<'_> { - let compact = self.is_editing(); - if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) { - let note_pt = editor.note_point(); - let w = if compact { 4 } else { 40 }; - let y = if compact { 1 } else { 0 }; - return Some(Fixed::x(w, Push::y(y, Fill::y(sampler.list(compact, editor))))) - } - None - } - - fn row <'a> ( - &'a self, w: u16, h: u16, a: impl Content + 'a, b: impl Content + 'a - ) -> impl Content + 'a { - Fixed::y(h, Bsp::e( - Fixed::xy(self.sidebar_w() as u16, h, a), - Fill::x(Align::c(Fixed::xy(w, h, b))) - )) - } - fn track_row (&self, w: u16, h: u16) -> impl Content + '_ { - self.row(w, h, track_header(&self), track_cells(&self)) - } - fn input_row (&self, w: u16, h: u16) -> impl Content + '_ { - self.row(w, h, input_header(&self), input_cells(&self)) - } - fn output_row (&self, w: u16, h: u16) -> impl Content + '_ { - self.row(w, h, output_header(&self), output_cells(&self)) - } - fn scene_row (&self, w: u16, h: u16) -> impl Content + '_ { - self.row(w, h, output_header(&self), output_cells(&self)) - } - - pub fn tracks_with_sizes (&self) - -> impl Iterator - { - tracks_with_sizes(self.tracks.iter(), match self.selected { - ArrangerSelection::Track(t) if self.is_editing() => Some(t), - ArrangerSelection::Clip(t, _) if self.is_editing() => Some(t), - _ => None - }, self.editor_w()) - } - pub fn scenes_with_sizes (&self, h: usize) - -> impl Iterator - { - scenes_with_sizes(self.scenes.iter(), &self.selected, self.is_editing(), 2, 15) - } - fn is_editing (&self) -> bool { - self.editing.load(Relaxed) - } - fn editor_w (&self) -> usize { - let editor = self.editor.as_ref().expect("missing editor"); - (5 + (editor.time_len().get() / editor.time_zoom().get())) - .min(self.size.w().saturating_sub(20)) - .max(16) - } - fn sidebar_w (&self) -> u16 { - let w = self.size.w(); - let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - let w = if self.is_editing() { 8 } else { w }; - w - } -} -pub fn scenes_with_sizes <'a>( - scenes: impl Iterator + 'a, - selected: &'a ArrangerSelection, - editing: bool, - scene_height: usize, - scene_larger: usize, -) -> impl Iterator + 'a { - let mut y = 0; - let (selected_track, selected_scene) = match selected { - ArrangerSelection::Clip(t, s) => (Some(t), Some(s)), - _ => (None, None) - }; - scenes.enumerate().map(move|(s, scene)|{ - let active = editing && selected_track.is_some() && selected_scene == Some(&s); - let height = if active { scene_larger } else { scene_height }; - let data = (s, scene, y, y + height); - y += height; - data - }) -} -pub fn tracks_with_sizes <'a> ( - tracks: impl Iterator, - active: Option, - bigger: usize -) -> impl Iterator { - let mut x = 0; - tracks.enumerate().map(move |(index, track)|{ - let width = if Some(index) == active { bigger } else { track.width.max(8) }; - let data = (index, track, x, x + width); - x += width; - data - }) -} -pub fn track_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - (||Tui::bg(TuiTheme::g(32), Tui::bold(true, Bsp::s( - row!( - Tui::fg(TuiTheme::g(128), "add "), - Tui::fg(TuiTheme::orange(), "t"), - Tui::fg(TuiTheme::g(128), "rack"), - ), - row!( - Tui::fg(TuiTheme::orange(), "a"), - Tui::fg(TuiTheme::g(128), "dd scene"), - ), - ))).boxed()).into() -} -pub fn track_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - let iter = ||state.tracks_with_sizes(); - (move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| { - let name = Push::x(1, &track.name); - let color = track.color(); - let fg = color.lightest.rgb; - let bg = color.base.rgb; - let active = state.selected.track() == Some(i); - let bfg = if active { Color::Rgb(255,255,255) } else { Color::Rgb(0,0,0) }; - let border = Style::default().fg(bfg).bg(bg); - Tui::bg(bg, map_east(x1 as u16, (x2 - x1) as u16, - Outer(border).enclose(Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::x(name))))) - )) - })).boxed()).into() -} -fn help_tag <'a>(before: &'a str, key: &'a str, after: &'a str) -> impl Content + 'a { - let lo = TuiTheme::g(128); - let hi = TuiTheme::orange(); - Tui::bold(true, row!(Tui::fg(lo, before), Tui::fg(hi, key), Tui::fg(lo, after))) -} -fn input_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - let fg = TuiTheme::g(224); - let bg = TuiTheme::g(64); - (move||Bsp::s(help_tag("midi ", "I", "ns"), state.midi_ins.get(0).map(|inp|Bsp::s( - Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(inp.name.clone())))), - inp.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, - Tui::fg_bg(fg, bg, connect.info()))))), - ))).boxed()).into() -} -fn input_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - (move||Align::x(Map::new(||state.tracks_with_sizes(), move|(_, track, x1, x2), i| { - let w = (x2 - x1) as u16; - let color: ItemPalette = track.color().dark.into(); - map_east(x1 as u16, w, Fixed::x(w, cell(color, Bsp::n( - rec_mon(color.base.rgb, false, false), - phat_hi(color.base.rgb, color.dark.rgb) - )))) - })).boxed()).into() -} -fn output_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - let fg = TuiTheme::g(224); - let bg = TuiTheme::g(64); - (move||Bsp::s(help_tag("midi ", "O", "uts"), state.midi_outs.get(0).map(|out|Bsp::s( - Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(out.name.clone())))), - out.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, - Tui::fg_bg(fg, bg, connect.info()))))), - ))).boxed()).into() -} -fn output_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - (move||Align::x(Map::new(||state.tracks_with_sizes(), move|(_, track, x1, x2), i| { - let w = (x2 - x1) as u16; - let color: ItemPalette = track.color().dark.into(); - map_east(x1 as u16, w, Fixed::x(w, cell(color, Bsp::n( - mute_solo(color.base.rgb, false, false), - phat_hi(color.dark.rgb, color.darker.rgb) - )))) - })).boxed()).into() -} -fn scene_headers <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - (||{ - let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); - let selected = state.selected.scene(); - Fill::y(Align::c(Map::new(||state.scenes_with_sizes(2), move|(_, scene, y1, y2), i| { - let h = (y2 - y1) as u16; - let name = format!("🭬{}", &scene.name); - let color = scene.color(); - let active = selected == Some(i); - let mid = if active { color.light } else { color.base }; - let top = Some(last_color.read().unwrap().base.rgb); - let cell = phat_sel_3( - active, - Tui::bold(true, name.clone()), - Tui::bold(true, name), - top, - mid.rgb, - Color::Rgb(0, 0, 0) - ); - *last_color.write().unwrap() = color; - map_south(y1 as u16, h + 1, Fixed::y(h + 1, cell)) - }))).boxed() - }).into() -} -fn scene_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - let editing = state.is_editing(); - let tracks = move||state.tracks_with_sizes(); - let scenes = ||state.scenes_with_sizes(2); - let selected_track = state.selected.track(); - let selected_scene = state.selected.scene(); - (move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| { - let w = (x2 - x1) as u16; - let color: ItemPalette = track.color().dark.into(); - let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); - let cells = Map::new(scenes, move|(_, scene, y1, y2), s| { - let h = (y2 - y1) as u16; - let color = scene.color(); - let (name, fg, bg) = if let Some(c) = &scene.clips[t] { - let c = c.read().unwrap(); - (c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb) - } else { - ("⏹ ".to_string(), TuiTheme::g(64), TuiTheme::g(32)) - }; - let last = last_color.read().unwrap().clone(); - let active = editing && selected_scene == Some(s) && selected_track == Some(t); - let editor = Thunk::new(||&state.editor); - let cell = Thunk::new(move||phat_sel_3( - selected_track == Some(t) && selected_scene == Some(s), - Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))), - Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))), - if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) { - None - } else { - Some(bg.into()) - }, - bg.into(), - bg.into(), - )); - let cell = Either(active, editor, cell); - *last_color.write().unwrap() = bg.into(); - map_south( - y1 as u16, - h + 1, - Fill::x(Fixed::y(h + 1, cell)) - ) - }); - let column = Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed()); - Fixed::x(w, map_east(x1 as u16, w, column)) - }))).boxed()).into() -} -fn cell_clip <'a> ( - scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16 -) -> impl Content + use<'a> { - scene.clips.get(index).map(|clip|clip.as_ref().map(|clip|{ - let clip = clip.read().unwrap(); - let mut bg = TuiTheme::border_bg(); - let name = clip.name.to_string(); - let max_w = name.len().min((w as usize).saturating_sub(2)); - let color = clip.color; - bg = color.dark.rgb; - if let Some((_, Some(ref playing))) = track.player.play_clip() { - if *playing.read().unwrap() == *clip { - bg = color.light.rgb - } - }; - Fixed::xy(w, h, &Tui::bg(bg, Push::x(1, Fixed::x(w, &name.as_str()[0..max_w])))); - })) -} -fn rec_mon (bg: Color, rec: bool, mon: bool) -> impl Content { - row!( - Tui::fg_bg(if rec { Color::Red } else { bg }, bg, "▐"), - Tui::fg_bg(if rec { Color::White } else { Color::Rgb(0,0,0) }, bg, "REC"), - Tui::fg_bg(if rec { Color::White } else { bg }, bg, "▐"), - Tui::fg_bg(if mon { Color::White } else { Color::Rgb(0,0,0) }, bg, "MON"), - Tui::fg_bg(if mon { Color::White } else { bg }, bg, "▌"), - ) -} -fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content { - row!( - Tui::fg_bg(if mute { Color::White } else { Color::Rgb(0,0,0) }, bg, "MUTE"), - Tui::fg_bg(if mute { Color::White } else { bg }, bg, "▐"), - Tui::fg_bg(if solo { Color::White } else { Color::Rgb(0,0,0) }, bg, "SOLO"), - ) -} -fn cell > (color: ItemPalette, field: T) -> impl Content { - Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field)) -} - -handle!(TuiIn: |self: App, input| Ok(None)); diff --git a/tek/src/arranger.rs b/tek/src/arranger.rs index f2b477ff..1ea57cd6 100644 --- a/tek/src/arranger.rs +++ b/tek/src/arranger.rs @@ -1,8 +1,4 @@ use crate::*; -mod arranger_scene; pub use self::arranger_scene::*; -mod arranger_select; pub use self::arranger_select::*; -mod arranger_track; pub use self::arranger_track::*; -mod arranger_h; use ClockCommand::{Play, Pause}; use self::ArrangerCommand as Cmd; /// Root view for standalone `tek_arranger` @@ -48,7 +44,7 @@ impl EdnViewData for &Arranger { impl Arranger { const EDN: &'static str = include_str!("arranger.edn"); pub const LEFT_SEP: char = '▎'; - pub const TRACK_MIN_WIDTH: usize = 4; + pub const TRACK_MIN_WIDTH: usize = 9; fn toolbar (&self) -> impl Content + use<'_> { Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock)))) @@ -115,8 +111,8 @@ impl Arranger { } fn play_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { (move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| { - //let color = track.color(); - let color: ItemPalette = track.color().dark.into(); + //let color = track.color; + let color: ItemPalette = track.color.dark.into(); let timebase = self.clock().timebase(); let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb, if let Some((_, Some(clip))) = track.player.play_clip().as_ref() { @@ -143,8 +139,8 @@ impl Arranger { } fn next_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { (move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| { - let color: ItemPalette = track.color(); - let color: ItemPalette = track.color().dark.into(); + let color: ItemPalette = track.color; + let color: ItemPalette = track.color.dark.into(); let current = &self.clock().playhead; let timebase = ¤t.timebase; let cell = Self::cell(color, Tui::bold(true, { @@ -204,7 +200,7 @@ impl Arranger { let iter = ||self.tracks_with_sizes(); (move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| { let name = Push::x(1, &track.name); - let color = track.color(); + let color = track.color; let fg = color.lightest.rgb; let bg = color.base.rgb; let active = self.selected.track() == Some(i); @@ -243,7 +239,7 @@ impl Arranger { fn input_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { (move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| { let w = (x2 - x1) as u16; - let color: ItemPalette = track.color().dark.into(); + let color: ItemPalette = track.color.dark.into(); map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n( Self::rec_mon(color.base.rgb, false, false), phat_hi(color.base.rgb, color.dark.rgb) @@ -287,7 +283,7 @@ impl Arranger { fn output_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { (move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| { let w = (x2 - x1) as u16; - let color: ItemPalette = track.color().dark.into(); + let color: ItemPalette = track.color.dark.into(); map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n( Self::mute_solo(color.base.rgb, false, false), phat_hi(color.dark.rgb, color.darker.rgb) @@ -317,7 +313,7 @@ impl Arranger { Fill::y(Align::c(Map::new(||self.scenes_with_sizes(2), move|(_, scene, y1, y2), i| { let h = (y2 - y1) as u16; let name = format!("🭬{}", &scene.name); - let color = scene.color(); + let color = scene.color; let active = selected == Some(i); let mid = if active { color.light } else { color.base }; let top = Some(last_color.read().unwrap().base.rgb); @@ -342,11 +338,11 @@ impl Arranger { let selected_scene = self.selected.scene(); (move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| { let w = (x2 - x1) as u16; - let color: ItemPalette = track.color().dark.into(); + let color: ItemPalette = track.color.dark.into(); let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); let cells = Map::new(scenes, move|(_, scene, y1, y2), s| { let h = (y2 - y1) as u16; - let color = scene.color(); + let color = scene.color; let (name, fg, bg) = if let Some(c) = &scene.clips[t] { let c = c.read().unwrap(); (c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb) @@ -559,46 +555,9 @@ pub fn phat_sel_3 > ( ) ) } -audio!(|self: Arranger, client, scope|{ - // Start profiling cycle - let t0 = self.perf.get_t0(); - // Update transport clock - //if Control::Quit == ClockAudio(self).process(client, scope) { - //return Control::Quit - //} - //// Update MIDI sequencers - //let tracks = &mut self.tracks; - //let note_buf = &mut self.note_buf; - //let midi_buf = &mut self.midi_buf; - //if Control::Quit == TracksAudio(tracks, note_buf, midi_buf).process(client, scope) { - //return Control::Quit - //} - // FIXME: one of these per playing track - //self.now.set(0.); - //if let ArrangerSelection::Clip(t, s) = self.selected { - //let clip = self.scenes.get(s).map(|scene|scene.clips.get(t)); - //if let Some(Some(Some(clip))) = clip { - //if let Some(track) = self.tracks().get(t) { - //if let Some((ref started_at, Some(ref playing))) = track.player.play_clip { - //let clip = clip.read().unwrap(); - //if *playing.read().unwrap() == *clip { - //let pulse = self.current().pulse.get(); - //let start = started_at.pulse.get(); - //let now = (pulse - start) % clip.length as f64; - //self.now.set(now); - //} - //} - //} - //} - //} - // End profiling cycle - self.perf.update(t0, scope); - return Control::Continue -}); has_clock!(|self: Arranger|&self.clock); has_clips!(|self: Arranger|self.pool.clips); has_editor!(|self: Arranger|self.editor); -handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event())); impl Arranger { pub fn activate (&mut self) -> Usually<()> { if let ArrangerSelection::Scene(s) = self.selected { @@ -636,213 +595,6 @@ impl Arranger { } } } -#[derive(Clone, Debug)] pub enum ArrangerCommand { - History(isize), - Color(ItemPalette), - Clock(ClockCommand), - Scene(ArrangerSceneCommand), - Track(ArrangerTrackCommand), - Clip(ArrangerClipCommand), - Select(ArrangerSelection), - Zoom(usize), - Phrases(PoolCommand), - Editor(MidiEditCommand), - StopAll, - Clear, -} -#[derive(Clone, Debug)] -pub enum ArrangerClipCommand { - Get(usize, usize), - Put(usize, usize, Option>>), - Enqueue(usize, usize), - Edit(Option>>), - SetLoop(usize, usize, bool), - SetColor(usize, usize, ItemPalette), -} -keymap!(KEYS_ARRANGER = |state: Arranger, input: Event| ArrangerCommand { - key(Char('u')) => Cmd::History(-1), - key(Char('U')) => Cmd::History(1), - // TODO: k: toggle on-screen keyboard - ctrl(key(Char('k'))) => { todo!("keyboard") }, - // Transport: Play/pause - key(Char(' ')) => Cmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), - // Transport: Play from start or rewind to start - shift(key(Char(' '))) => Cmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), - key(Char('e')) => Cmd::Editor(MidiEditCommand::Show(state.pool.clip().clone())), - ctrl(key(Char('a'))) => Cmd::Scene(ArrangerSceneCommand::Add), - ctrl(key(Char('A'))) => return None,//Cmd::Scene(ArrangerSceneCommand::Add), - ctrl(key(Char('t'))) => Cmd::Track(ArrangerTrackCommand::Add), - // Tab: Toggle visibility of clip pool column - key(Tab) => Cmd::Phrases(PoolCommand::Show(!state.pool.visible)), -}, { - use ArrangerSelection as Selected; - use ArrangerSceneCommand as Scene; - use ArrangerTrackCommand as Track; - use ArrangerClipCommand as Clip; - let t_len = state.tracks.len(); - let s_len = state.scenes.len(); - match state.selected { - Selected::Clip(t, s) => clip_keymap(state, input, t, s), - Selected::Scene(s) => scene_keymap(state, input, s), - Selected::Track(t) => track_keymap(state, input, t), - Selected::Mix => match input { - - kpat!(Delete) => Some(Cmd::Clear), - kpat!(Char('0')) => Some(Cmd::StopAll), - kpat!(Char('c')) => Some(Cmd::Color(ItemPalette::random())), - - kpat!(Up) => return None, - kpat!(Down) => Some( Cmd::Select(Selected::Scene(0))), - kpat!(Left) => return None, - kpat!(Right) => Some( Cmd::Select(Selected::Track(0))), - - _ => None - }, - } -}.or_else(||if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { - Some(Cmd::Editor(command)) -} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { - Some(Cmd::Phrases(command)) -} else { - None -})?); - -fn clip_keymap (state: &Arranger, input: &Event, t: usize, s: usize) -> Option { - use ArrangerSelection as Selected; - use ArrangerSceneCommand as Scene; - use ArrangerTrackCommand as Track; - use ArrangerClipCommand as Clip; - let t_len = state.tracks.len(); - let s_len = state.scenes.len(); - Some(match input { - - kpat!(Char('g')) => Cmd::Phrases(PoolCommand::Select(0)), - kpat!(Char('q')) => Cmd::Clip(Clip::Enqueue(t, s)), - kpat!(Char('l')) => Cmd::Clip(Clip::SetLoop(t, s, false)), - - kpat!(Enter) => if state.scenes[s].clips[t].is_none() { - // FIXME: get this clip from the pool (autoregister via intmut) - let (_, clip) = state.add_clip(); - Cmd::Clip(Clip::Put(t, s, Some(clip))) - } else { - return None - }, - kpat!(Delete) => Cmd::Clip(Clip::Put(t, s, None)), - kpat!(Char('p')) => Cmd::Clip(Clip::Put(t, s, state.pool.clip().clone())), - kpat!(Char(',')) => Cmd::Clip(Clip::Put(t, s, None)), - kpat!(Char('.')) => Cmd::Clip(Clip::Put(t, s, None)), - kpat!(Char('<')) => Cmd::Clip(Clip::Put(t, s, None)), - kpat!(Char('>')) => Cmd::Clip(Clip::Put(t, s, None)), - - kpat!(Up) => Cmd::Select(if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) }), - kpat!(Down) => Cmd::Select(Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1)))), - kpat!(Left) => Cmd::Select(if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) }), - kpat!(Right) => Cmd::Select(Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s)), - - _ => return None - }) -} -fn scene_keymap (state: &Arranger, input: &Event, s: usize) -> Option { - use ArrangerSelection as Selected; - use ArrangerSceneCommand as Scene; - use ArrangerTrackCommand as Track; - use ArrangerClipCommand as Clip; - let t_len = state.tracks.len(); - let s_len = state.scenes.len(); - Some(match input { - - kpat!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)), - kpat!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)), - kpat!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)), - kpat!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)), - kpat!(Char('q')) => Cmd::Scene(Scene::Enqueue(s)), - kpat!(Delete) => Cmd::Scene(Scene::Delete(s)), - kpat!(Char('c')) => Cmd::Scene(Scene::SetColor(s, ItemPalette::random())), - - kpat!(Up) => Cmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix }), - kpat!(Down) => Cmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1)))), - kpat!(Left) => return None, - kpat!(Right) => Cmd::Select(Selected::Clip(0, s)), - - _ => return None - }) -} -fn track_keymap (state: &Arranger, input: &Event, t: usize) -> Option { - use ArrangerSelection as Selected; - use ArrangerSceneCommand as Scene; - use ArrangerTrackCommand as Track; - use ArrangerClipCommand as Clip; - let t_len = state.tracks.len(); - let s_len = state.scenes.len(); - Some(match input { - - kpat!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)), - kpat!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)), - kpat!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)), - kpat!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)), - kpat!(Delete) => Cmd::Track(Track::Delete(t)), - kpat!(Char('c')) => Cmd::Track(Track::SetColor(t, ItemPalette::random())), - - kpat!(Up) => return None, - kpat!(Down) => Cmd::Select(Selected::Clip(t, 0)), - kpat!(Left) => Cmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix }), - kpat!(Right) => Cmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1)))), - - _ => return None - }) -} - -command!(|self: ArrangerCommand, state: Arranger|match self { - Self::Clear => { todo!() }, - Self::History(_) => { todo!() }, - Self::Zoom(_) => { todo!(); }, - Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, - Self::Clip(cmd) => cmd.delegate(state, Self::Clip)?, - Self::Scene(cmd) => cmd.delegate(state, Self::Scene)?, - Self::Track(cmd) => cmd.delegate(state, Self::Track)?, - Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, - Self::Select(selected) => { state.selected = selected; None }, - Self::StopAll => { - for track in 0..state.tracks.len() { state.tracks[track].player.enqueue_next(None); } - None - }, - Self::Color(palette) => { - let old = state.color; - state.color = palette; - Some(Self::Color(old)) - }, - Self::Phrases(cmd) => { - match cmd { - // autoselect: automatically load selected clip in editor - PoolCommand::Select(_) => { - let undo = cmd.delegate(&mut state.pool, Self::Phrases)?; - state.editor.set_clip(state.pool.clip().as_ref()); - undo - }, - // reload clip in editor to update color - PoolCommand::Phrase(MidiPoolCommand::SetColor(index, _)) => { - let undo = cmd.delegate(&mut state.pool, Self::Phrases)?; - state.editor.set_clip(state.pool.clip().as_ref()); - undo - }, - _ => cmd.delegate(&mut state.pool, Self::Phrases)? - } - }, -}); - -command!(|self: ArrangerClipCommand, state: Arranger|match self { - Self::Get(track, scene) => { todo!() }, - Self::Put(track, scene, clip) => { - let old = state.scenes[scene].clips[track].clone(); - state.scenes[scene].clips[track] = clip; - Some(Self::Put(track, scene, old)) - }, - Self::Enqueue(track, scene) => { - state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); - None - }, - _ => None -}); //pub struct ArrangerVCursor { //cols: Vec<(usize, usize)>, @@ -983,3 +735,258 @@ command!(|self: ArrangerClipCommand, state: Arranger|match self { //TuiTheme::g(32).into(), //TuiTheme::g(32).into(), //); +#[derive(PartialEq, Clone, Copy, Debug, Default)] +/// Represents the current user selection in the arranger +pub enum ArrangerSelection { + /// The whole mix is selected + #[default] Mix, + /// A track is selected. + Track(usize), + /// A scene is selected. + Scene(usize), + /// A clip (track × scene) is selected. + Clip(usize, usize), +} +/// Focus identification methods +impl ArrangerSelection { + pub fn is_mix (&self) -> bool { matches!(self, Self::Mix) } + pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) } + pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) } + pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) } + pub fn description ( + &self, + tracks: &[ArrangerTrack], + scenes: &[ArrangerScene], + ) -> Arc { + format!("Selected: {}", match self { + Self::Mix => "Everything".to_string(), + Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)) + .unwrap_or_else(||"T??".into()), + Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)) + .unwrap_or_else(||"S??".into()), + Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { + (Some(_), Some(scene)) => match scene.clip(*t) { + Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), + None => format!("T{t} S{s}: Empty") + }, + _ => format!("T{t} S{s}: Empty"), + } + }).into() + } + pub fn track (&self) -> Option { + use ArrangerSelection::*; + match self { + Clip(t, _) => Some(*t), + Track(t) => Some(*t), + _ => None + } + } + pub fn scene (&self) -> Option { + use ArrangerSelection::*; + match self { + Clip(_, s) => Some(*s), + Scene(s) => Some(*s), + _ => None + } + } +} +impl Arranger { + pub fn track_next_name (&self) -> Arc { + format!("Trk{:02}", self.tracks.len() + 1).into() + } + pub fn track_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerTrack> + { + let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into()); + let track = ArrangerTrack { + width: (name.len() + 2).max(9), + color: color.unwrap_or_else(ItemPalette::random), + player: MidiPlayer::from(&self.clock), + name, + }; + self.tracks.push(track); + let len = self.tracks.len(); + let index = len - 1; + for scene in self.scenes.iter_mut() { + while scene.clips.len() < len { + scene.clips.push(None); + } + } + Ok(&mut self.tracks[index]) + } + pub fn track_del (&mut self, index: usize) { + self.tracks.remove(index); + for scene in self.scenes.iter_mut() { + scene.clips.remove(index); + } + } + pub fn tracks_add ( + &mut self, + count: usize, + width: usize, + midi_from: &[PortConnection], + midi_to: &[PortConnection], + ) -> Usually<()> { + let jack = self.jack.clone(); + let track_color_1 = ItemColor::random(); + let track_color_2 = ItemColor::random(); + for i in 0..count { + let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); + let mut track = self.track_add(None, Some(color))?; + track.width = width; + let port = JackPort::::new(&jack, &format!("{}I", &track.name), midi_from)?; + track.player.midi_ins.push(port); + let port = JackPort::::new(&jack, &format!("{}O", &track.name), midi_to)?; + track.player.midi_outs.push(port); + } + // TODO: port per track: + //for connection in midi_from.iter() { + //let mut split = connection.as_ref().split("="); + //let number = split.next().unwrap().trim(); + //if let Ok(track) = number.parse::() { + //if track < 1 { + //panic!("Tracks start from 1") + //} + //if track > count { + //panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.") + //} + //if let Some(port) = split.next() { + //if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() { + ////jack.read().unwrap().client().connect_ports(port, &self.tracks[track-1].player.midi_ins[0])?; + //} else { + //panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names."); + //} + //} else { + //panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME") + //} + //} else { + //panic!("Failed to parse track number: {number}") + //} + //} + //for connection in midi_to.iter() { + //let mut split = connection.as_ref().split("="); + //let number = split.next().unwrap().trim(); + //if let Ok(track) = number.parse::() { + //if track < 1 { + //panic!("Tracks start from 1") + //} + //if track > count { + //panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.") + //} + //if let Some(port) = split.next() { + //if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() { + ////jack.read().unwrap().client().connect_ports(&self.tracks[track-1].player.midi_outs[0], port)?; + //} else { + //panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names."); + //} + //} else { + //panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME") + //} + //} else { + //panic!("Failed to parse track number: {number}") + //} + //} + Ok(()) + } +} +#[derive(Debug)] pub struct ArrangerTrack { + /// Name of track + pub name: Arc, + /// Preferred width of track column + pub width: usize, + /// Identifying color of track + pub color: ItemPalette, + /// MIDI player state + pub player: MidiPlayer, +} +has_clock!(|self:ArrangerTrack|self.player.clock()); +has_player!(|self:ArrangerTrack|self.player); +impl ArrangerTrack { + fn longest_name (tracks: &[Self]) -> usize { + tracks.iter().map(|s|s.name.len()).fold(0, usize::max) + } + fn width_inc (&mut self) { + self.width += 1; + } + fn width_dec (&mut self) { + if self.width > Arranger::TRACK_MIN_WIDTH { + self.width -= 1; + } + } +} +impl Arranger { + pub fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerScene> + { + let scene = ArrangerScene { + name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), + clips: vec![None;self.tracks.len()], + color: color.unwrap_or_else(ItemPalette::random), + }; + self.scenes.push(scene); + let index = self.scenes.len() - 1; + Ok(&mut self.scenes[index]) + } + pub fn scene_del (&mut self, index: usize) { + todo!("delete scene"); + } + fn scene_default_name (&self) -> Arc { + format!("Sc{:3>}", self.scenes.len() + 1).into() + } + pub fn selected_scene (&self) -> Option<&ArrangerScene> { + self.selected.scene().and_then(|s|self.scenes.get(s)) + } + pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { + self.selected.scene().and_then(|s|self.scenes.get_mut(s)) + } + pub fn scenes_add (&mut self, n: usize) -> Usually<()> { + let scene_color_1 = ItemColor::random(); + let scene_color_2 = ItemColor::random(); + for i in 0..n { + let _scene = self.scene_add(None, Some( + scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() + ))?; + } + Ok(()) + } +} +#[derive(Default, Debug, Clone)] pub struct ArrangerScene { + /// Name of scene + pub(crate) name: Arc, + /// Clips in scene, one per track + pub(crate) clips: Vec>>>, + /// Identifying color of scene + pub(crate) color: ItemPalette, +} +impl ArrangerScene { + pub fn longest_name (scenes: &[Self]) -> usize { + scenes.iter().map(|s|s.name.len()).fold(0, usize::max) + } + /// Returns the pulse length of the longest clip in the scene + pub 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 clips in the scene are + /// currently playing on the given collection of tracks. + pub fn is_playing (&self, tracks: &[ArrangerTrack]) -> bool { + self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() + .all(|(track_index, clip)|match clip { + Some(c) => tracks + .get(track_index) + .map(|track|{ + if let Some((_, Some(clip))) = track.player().play_clip() { + *clip.read().unwrap() == *c.read().unwrap() + } else { + false + } + }) + .unwrap_or(false), + None => true + }) + } + pub fn clip (&self, index: usize) -> Option<&Arc>> { + match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } + } +} diff --git a/tek/src/arranger/arranger_h.rs b/tek/src/arranger/arranger_h.rs deleted file mode 100644 index 70b786d1..00000000 --- a/tek/src/arranger/arranger_h.rs +++ /dev/null @@ -1 +0,0 @@ -// TODO diff --git a/tek/src/arranger/arranger_scene.rs b/tek/src/arranger/arranger_scene.rs deleted file mode 100644 index 4aba7fcf..00000000 --- a/tek/src/arranger/arranger_scene.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::*; -#[derive(Clone, Debug)] -pub enum ArrangerSceneCommand { - Add, - Delete(usize), - Swap(usize, usize), - SetSize(usize), - SetZoom(usize), - SetColor(usize, ItemPalette), - Enqueue(usize), -} -command!(|self: ArrangerSceneCommand, state: Arranger|match self { - Self::Add => { - state.scene_add(None, None)?; - None - } - Self::Delete(index) => { - state.scene_del(index); - None - }, - Self::SetColor(index, color) => { - let old = state.scenes[index].color; - state.scenes[index].color = color; - Some(Self::SetColor(index, old)) - }, - Self::Enqueue(scene) => { - for track in 0..state.tracks.len() { - state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); - } - None - }, - _ => None -}); -impl Arranger { - pub fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerScene> - { - let scene = ArrangerScene { - name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), - clips: vec![None;self.tracks.len()], - color: color.unwrap_or_else(ItemPalette::random), - }; - self.scenes.push(scene); - let index = self.scenes.len() - 1; - Ok(&mut self.scenes[index]) - } - pub fn scene_del (&mut self, index: usize) { - todo!("delete scene"); - } - fn scene_default_name (&self) -> Arc { - format!("Sc{:3>}", self.scenes.len() + 1).into() - } - pub fn selected_scene (&self) -> Option<&ArrangerScene> { - self.selected.scene().and_then(|s|self.scenes.get(s)) - } - pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { - self.selected.scene().and_then(|s|self.scenes.get_mut(s)) - } - pub fn scenes_add (&mut self, n: usize) -> Usually<()> { - let scene_color_1 = ItemColor::random(); - let scene_color_2 = ItemColor::random(); - for i in 0..n { - let _scene = self.scene_add(None, Some( - scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() - ))?; - } - Ok(()) - } -} -#[derive(Default, Debug, Clone)] pub struct ArrangerScene { - /// Name of scene - pub(crate) name: Arc, - /// Clips in scene, one per track - pub(crate) clips: Vec>>>, - /// Identifying color of scene - pub(crate) color: ItemPalette, -} -impl ArrangerScene { - pub fn name (&self) -> &Arc { - &self.name - } - pub fn clips (&self) -> &Vec>>> { - &self.clips - } - pub fn color (&self) -> ItemPalette { - self.color - } - pub fn longest_name (scenes: &[Self]) -> usize { - scenes.iter().map(|s|s.name.len()).fold(0, usize::max) - } - /// Returns the pulse length of the longest clip in the scene - pub 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 clips in the scene are - /// currently playing on the given collection of tracks. - pub fn is_playing (&self, tracks: &[ArrangerTrack]) -> bool { - self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate() - .all(|(track_index, clip)|match clip { - Some(c) => tracks - .get(track_index) - .map(|track|{ - if let Some((_, Some(clip))) = track.player().play_clip() { - *clip.read().unwrap() == *c.read().unwrap() - } else { - false - } - }) - .unwrap_or(false), - None => true - }) - } - pub fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None } - } -} diff --git a/tek/src/arranger/arranger_select.rs b/tek/src/arranger/arranger_select.rs deleted file mode 100644 index b129ed63..00000000 --- a/tek/src/arranger/arranger_select.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::*; -#[derive(PartialEq, Clone, Copy, Debug, Default)] -/// Represents the current user selection in the arranger -pub enum ArrangerSelection { - /// The whole mix is selected - #[default] Mix, - /// A track is selected. - Track(usize), - /// A scene is selected. - Scene(usize), - /// A clip (track × scene) is selected. - Clip(usize, usize), -} -/// Focus identification methods -impl ArrangerSelection { - pub fn is_mix (&self) -> bool { matches!(self, Self::Mix) } - pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) } - pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) } - pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) } - pub fn description ( - &self, - tracks: &[ArrangerTrack], - scenes: &[ArrangerScene], - ) -> Arc { - format!("Selected: {}", match self { - Self::Mix => "Everything".to_string(), - Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)) - .unwrap_or_else(||"T??".into()), - Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)) - .unwrap_or_else(||"S??".into()), - Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { - (Some(_), Some(scene)) => match scene.clip(*t) { - Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), - None => format!("T{t} S{s}: Empty") - }, - _ => format!("T{t} S{s}: Empty"), - } - }).into() - } - pub fn track (&self) -> Option { - use ArrangerSelection::*; - match self { - Clip(t, _) => Some(*t), - Track(t) => Some(*t), - _ => None - } - } - pub fn scene (&self) -> Option { - use ArrangerSelection::*; - match self { - Clip(_, s) => Some(*s), - Scene(s) => Some(*s), - _ => None - } - } -} diff --git a/tek/src/arranger/arranger_track.rs b/tek/src/arranger/arranger_track.rs deleted file mode 100644 index 4f0d9f47..00000000 --- a/tek/src/arranger/arranger_track.rs +++ /dev/null @@ -1,189 +0,0 @@ -use crate::*; -#[derive(Clone, Debug)] -pub enum ArrangerTrackCommand { - Add, - Delete(usize), - Stop(usize), - Swap(usize, usize), - SetSize(usize), - SetZoom(usize), - SetColor(usize, ItemPalette), -} -command!(|self: ArrangerTrackCommand, state: Arranger|match self { - Self::Add => { - state.track_add(None, None)?; - None - }, - Self::SetColor(index, color) => { - let old = state.tracks[index].color; - state.tracks[index].color = color; - Some(Self::SetColor(index, old)) - }, - Self::Stop(track) => { - state.tracks[track].player.enqueue_next(None); - None - }, - _ => None -}); -impl Arranger { - pub fn track_next_name (&self) -> Arc { - format!("Trk{:02}", self.tracks.len() + 1).into() - } - pub fn track_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerTrack> - { - let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into()); - let track = ArrangerTrack { - width: (name.len() + 2).max(9), - color: color.unwrap_or_else(ItemPalette::random), - player: MidiPlayer::from(&self.clock), - name, - }; - self.tracks.push(track); - let len = self.tracks.len(); - let index = len - 1; - for scene in self.scenes.iter_mut() { - while scene.clips.len() < len { - scene.clips.push(None); - } - } - Ok(&mut self.tracks[index]) - } - pub fn track_del (&mut self, index: usize) { - self.tracks.remove(index); - for scene in self.scenes.iter_mut() { - scene.clips.remove(index); - } - } - pub fn tracks_add ( - &mut self, - count: usize, - width: usize, - midi_from: &[PortConnection], - midi_to: &[PortConnection], - ) -> Usually<()> { - let jack = self.jack.clone(); - let track_color_1 = ItemColor::random(); - let track_color_2 = ItemColor::random(); - for i in 0..count { - let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); - let mut track = self.track_add(None, Some(color))?; - track.width = width; - let port = JackPort::::new(&jack, &format!("{}I", &track.name), midi_from)?; - track.player.midi_ins.push(port); - let port = JackPort::::new(&jack, &format!("{}O", &track.name), midi_to)?; - track.player.midi_outs.push(port); - } - // TODO: port per track: - //for connection in midi_from.iter() { - //let mut split = connection.as_ref().split("="); - //let number = split.next().unwrap().trim(); - //if let Ok(track) = number.parse::() { - //if track < 1 { - //panic!("Tracks start from 1") - //} - //if track > count { - //panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.") - //} - //if let Some(port) = split.next() { - //if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() { - ////jack.read().unwrap().client().connect_ports(port, &self.tracks[track-1].player.midi_ins[0])?; - //} else { - //panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names."); - //} - //} else { - //panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME") - //} - //} else { - //panic!("Failed to parse track number: {number}") - //} - //} - //for connection in midi_to.iter() { - //let mut split = connection.as_ref().split("="); - //let number = split.next().unwrap().trim(); - //if let Ok(track) = number.parse::() { - //if track < 1 { - //panic!("Tracks start from 1") - //} - //if track > count { - //panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.") - //} - //if let Some(port) = split.next() { - //if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() { - ////jack.read().unwrap().client().connect_ports(&self.tracks[track-1].player.midi_outs[0], port)?; - //} else { - //panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names."); - //} - //} else { - //panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME") - //} - //} else { - //panic!("Failed to parse track number: {number}") - //} - //} - Ok(()) - } -} -#[derive(Debug)] pub struct ArrangerTrack { - /// Name of track - pub name: Arc, - /// Preferred width of track column - pub width: usize, - /// Identifying color of track - pub color: ItemPalette, - /// MIDI player state - pub player: MidiPlayer, -} -has_clock!(|self:ArrangerTrack|self.player.clock()); -has_player!(|self:ArrangerTrack|self.player); -impl ArrangerTrack { - /// Name of track - pub 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 - pub fn color (&self) -> ItemPalette { - self.color - } - fn longest_name (tracks: &[Self]) -> usize { - tracks.iter().map(|s|s.name.len()).fold(0, usize::max) - } - fn width_inc (&mut self) { - *self.width_mut() += 1; - } - fn width_dec (&mut self) { - if self.width() > Arranger::TRACK_MIN_WIDTH { - *self.width_mut() -= 1; - } - } -} -/// Hosts the JACK callback for a collection of tracks -pub struct TracksAudio<'a>( - // Track collection - pub &'a mut [ArrangerTrack], - /// Note buffer - pub &'a mut Vec, - /// Note chunk buffer - pub &'a mut Vec>>, -); -impl Audio for TracksAudio<'_> { - #[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.iter_mut() { - if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit { - return Control::Quit - } - } - Control::Continue - } -} diff --git a/tek/src/arranger/arranger_keys.edn b/tek/src/arranger_keys.edn similarity index 100% rename from tek/src/arranger/arranger_keys.edn rename to tek/src/arranger_keys.edn diff --git a/tek/src/audio.rs b/tek/src/audio.rs new file mode 100644 index 00000000..33e3c4e2 --- /dev/null +++ b/tek/src/audio.rs @@ -0,0 +1,132 @@ +use crate::*; + +audio!(|self: Sequencer, client, scope|{ + // Start profiling cycle + let t0 = self.perf.get_t0(); + + // Update transport clock + if Control::Quit == ClockAudio(self).process(client, scope) { + return Control::Quit + } + + // Update MIDI sequencer + if Control::Quit == PlayerAudio( + &mut self.player, &mut self.note_buf, &mut self.midi_buf + ).process(client, scope) { + return Control::Quit + } + + // End profiling cycle + self.perf.update(t0, scope); + + Control::Continue +}); + +audio!(|self: Groovebox, client, scope|{ + // Start profiling cycle + let t0 = self.perf.get_t0(); + + // Update transport clock + if Control::Quit == ClockAudio(&mut self.player).process(client, scope) { + return Control::Quit + } + + // Update MIDI sequencer + if Control::Quit == PlayerAudio( + &mut self.player, &mut self.note_buf, &mut self.midi_buf + ).process(client, scope) { + return Control::Quit + } + + // Update sampler + if Control::Quit == SamplerAudio(&mut self.sampler).process(client, scope) { + return Control::Quit + } + + // TODO move these to editor and sampler: + for RawMidi { time, bytes } in self.player.midi_ins[0].port.iter(scope) { + if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { + match message { + MidiMessage::NoteOn { ref key, .. } => { + self.editor.set_note_point(key.as_int() as usize); + }, + MidiMessage::Controller { controller, value } => { + if let Some(sample) = &self.sampler.mapped[self.editor.note_point()] { + sample.write().unwrap().handle_cc(controller, value) + } + } + _ => {} + } + } + } + + // End profiling cycle + self.perf.update(t0, scope); + + Control::Continue +}); + +audio!(|self: Arranger, client, scope|{ + // Start profiling cycle + let t0 = self.perf.get_t0(); + + // Update transport clock + if Control::Quit == ClockAudio(self).process(client, scope) { + return Control::Quit + } + + //// Update MIDI sequencers + //let tracks = &mut self.tracks; + //let note_buf = &mut self.note_buf; + //let midi_buf = &mut self.midi_buf; + //if Control::Quit == TracksAudio(tracks, note_buf, midi_buf).process(client, scope) { + //return Control::Quit + //} + + // FIXME: one of these per playing track + //self.now.set(0.); + //if let ArrangerSelection::Clip(t, s) = self.selected { + //let clip = self.scenes.get(s).map(|scene|scene.clips.get(t)); + //if let Some(Some(Some(clip))) = clip { + //if let Some(track) = self.tracks().get(t) { + //if let Some((ref started_at, Some(ref playing))) = track.player.play_clip { + //let clip = clip.read().unwrap(); + //if *playing.read().unwrap() == *clip { + //let pulse = self.current().pulse.get(); + //let start = started_at.pulse.get(); + //let now = (pulse - start) % clip.length as f64; + //self.now.set(now); + //} + //} + //} + //} + //} + + // End profiling cycle + self.perf.update(t0, scope); + return Control::Continue +}); + +/// Hosts the JACK callback for a collection of tracks +pub struct TracksAudio<'a>( + // Track collection + pub &'a mut [ArrangerTrack], + /// Note buffer + pub &'a mut Vec, + /// Note chunk buffer + pub &'a mut Vec>>, +); + +impl Audio for TracksAudio<'_> { + #[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.iter_mut() { + if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit { + return Control::Quit + } + } + Control::Continue + } +} diff --git a/tek/src/control.rs b/tek/src/control.rs new file mode 100644 index 00000000..0ec430da --- /dev/null +++ b/tek/src/control.rs @@ -0,0 +1,450 @@ +use crate::*; +use ClockCommand::{Play, Pause}; +use KeyCode::{Tab, Char}; +use SequencerCommand as SeqCmd; +use GrooveboxCommand as GrvCmd; +use ArrangerCommand as ArrCmd; +use MidiEditCommand as EditCmd; +use MidiPoolCommand as PoolCmd; + +handle!(TuiIn: |self: App, input| Ok(None)); +handle!(TuiIn: |self: Sequencer, input|SequencerCommand::execute_with_state(self, input.event())); +handle!(TuiIn: |self: Groovebox, input|GrooveboxCommand::execute_with_state(self, input.event())); +handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event())); + +#[derive(Clone, Debug)] pub enum AppCommand { + Clear, + Clip(ArrangerClipCommand), + Clock(ClockCommand), + Color(ItemPalette), + Compact(bool), + Editor(MidiEditCommand), + Enqueue(Option>>), + History(isize), + Pool(PoolCommand), + Sampler(SamplerCommand), + Scene(ArrangerSceneCommand), + Select(ArrangerSelection), + StopAll, + Track(ArrangerTrackCommand), + Zoom(usize), +} +#[derive(Clone, Debug)] pub enum SequencerCommand { + Compact(bool), + History(isize), + Clock(ClockCommand), + Pool(PoolCommand), + Editor(MidiEditCommand), + Enqueue(Option>>), +} +#[derive(Clone, Debug)] pub enum GrooveboxCommand { + Compact(bool), + History(isize), + Clock(ClockCommand), + Pool(PoolCommand), + Editor(MidiEditCommand), + Enqueue(Option>>), + Sampler(SamplerCommand), +} +#[derive(Clone, Debug)] pub enum ArrangerCommand { + History(isize), + Color(ItemPalette), + Clock(ClockCommand), + Scene(ArrangerSceneCommand), + Track(ArrangerTrackCommand), + Clip(ArrangerClipCommand), + Select(ArrangerSelection), + Zoom(usize), + Pool(PoolCommand), + Editor(MidiEditCommand), + StopAll, + Clear, +} +#[derive(Clone, Debug)] pub enum ArrangerClipCommand { + Get(usize, usize), + Put(usize, usize, Option>>), + Enqueue(usize, usize), + Edit(Option>>), + SetLoop(usize, usize, bool), + SetColor(usize, usize, ItemPalette), +} +#[derive(Clone, Debug)] pub enum ArrangerSceneCommand { + Add, + Delete(usize), + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), + SetColor(usize, ItemPalette), + Enqueue(usize), +} +#[derive(Clone, Debug)] pub enum ArrangerTrackCommand { + Add, + Delete(usize), + Stop(usize), + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), + SetColor(usize, ItemPalette), +} + +command!(|self: SequencerCommand, state: Sequencer|match self { + Self::Enqueue(clip) => { + state.player.enqueue_next(clip.as_ref()); + None + }, + Self::Pool(cmd) => match cmd { + // autoselect: automatically load selected clip in editor + PoolCommand::Select(_) => { + let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + state.editor.set_clip(state.pool.clip().as_ref()); + undo + }, + // update color in all places simultaneously + PoolCommand::Phrase(PoolCmd::SetColor(index, _)) => { + let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + state.editor.set_clip(state.pool.clip().as_ref()); + undo + }, + _ => cmd.delegate(&mut state.pool, Self::Pool)? + }, + Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, + Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, + Self::History(delta) => { + todo!("undo/redo") + }, + Self::Compact(compact) => if state.compact != compact { + state.compact = compact; + Some(Self::Compact(!compact)) + } else { + None + }, +}); +command!(|self: GrooveboxCommand, state: Groovebox|match self { + Self::Enqueue(clip) => { + state.player.enqueue_next(clip.as_ref()); + None + }, + Self::Pool(cmd) => match cmd { + // autoselect: automatically load selected clip in editor + PoolCommand::Select(_) => { + let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + state.editor.set_clip(state.pool.clip().as_ref()); + undo + }, + // update color in all places simultaneously + PoolCommand::Phrase(PoolCmd::SetColor(index, _)) => { + let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + state.editor.set_clip(state.pool.clip().as_ref()); + undo + }, + _ => cmd.delegate(&mut state.pool, Self::Pool)? + }, + Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?, + Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, + Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, + Self::History(delta) => { todo!("undo/redo") }, + Self::Compact(compact) => if state.compact != compact { + state.compact = compact; + Some(Self::Compact(!compact)) + } else { + None + }, +}); +command!(|self: ArrangerCommand, state: Arranger|match self { + Self::Clear => { todo!() }, + Self::History(_) => { todo!() }, + Self::Zoom(_) => { todo!(); }, + Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, + Self::Clip(cmd) => cmd.delegate(state, Self::Clip)?, + Self::Scene(cmd) => cmd.delegate(state, Self::Scene)?, + Self::Track(cmd) => cmd.delegate(state, Self::Track)?, + Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, + Self::Select(selected) => { state.selected = selected; None }, + Self::StopAll => { + for track in 0..state.tracks.len() { state.tracks[track].player.enqueue_next(None); } + None + }, + Self::Color(palette) => { + let old = state.color; + state.color = palette; + Some(Self::Color(old)) + }, + Self::Pool(cmd) => { + match cmd { + // autoselect: automatically load selected clip in editor + PoolCommand::Select(_) => { + let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + state.editor.set_clip(state.pool.clip().as_ref()); + undo + }, + // reload clip in editor to update color + PoolCommand::Phrase(MidiPoolCommand::SetColor(index, _)) => { + let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + state.editor.set_clip(state.pool.clip().as_ref()); + undo + }, + _ => cmd.delegate(&mut state.pool, Self::Pool)? + } + }, +}); +command!(|self: ArrangerSceneCommand, state: Arranger|match self { + Self::Add => { + state.scene_add(None, None)?; + None + } + Self::Delete(index) => { + state.scene_del(index); + None + }, + Self::SetColor(index, color) => { + let old = state.scenes[index].color; + state.scenes[index].color = color; + Some(Self::SetColor(index, old)) + }, + Self::Enqueue(scene) => { + for track in 0..state.tracks.len() { + state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); + } + None + }, + _ => None +}); +command!(|self: ArrangerTrackCommand, state: Arranger|match self { + Self::Add => { + state.track_add(None, None)?; + None + }, + Self::SetColor(index, color) => { + let old = state.tracks[index].color; + state.tracks[index].color = color; + Some(Self::SetColor(index, old)) + }, + Self::Stop(track) => { + state.tracks[track].player.enqueue_next(None); + None + }, + _ => None +}); +command!(|self: ArrangerClipCommand, state: Arranger|match self { + Self::Get(track, scene) => { todo!() }, + Self::Put(track, scene, clip) => { + let old = state.scenes[scene].clips[track].clone(); + state.scenes[scene].clips[track] = clip; + Some(Self::Put(track, scene, old)) + }, + Self::Enqueue(track, scene) => { + state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); + None + }, + _ => None +}); +keymap!(KEYS_SEQUENCER = |state: Sequencer, input: Event| SequencerCommand { + // TODO: k: toggle on-screen keyboard + ctrl(key(Char('k'))) => { todo!("keyboard") }, + // Transport: Play/pause + key(Char(' ')) => SeqCmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), + // Transport: Play from start or rewind to start + shift(key(Char(' '))) => SeqCmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), + // u: undo + key(Char('u')) => SeqCmd::History(-1), + // Shift-U: redo + key(Char('U')) => SeqCmd::History( 1), + // Tab: Toggle compact mode + key(Tab) => SeqCmd::Compact(!state.compact), + // q: Enqueue currently edited clip + key(Char('q')) => SeqCmd::Enqueue(state.pool.clip().clone()), + // 0: Enqueue clip 0 (stop all) + key(Char('0')) => SeqCmd::Enqueue(Some(state.clips()[0].clone())), + // e: Toggle between editing currently playing or other clip + //key(Char('e')) => if let Some((_, Some(playing))) = state.player.play_clip() { + //let editing = state.editor.clip().as_ref().map(|p|p.read().unwrap().clone()); + //let selected = state.pool.clip().clone(); + //SeqCmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing { + //selected + //} else { + //playing.clone() + //}))) + //} else { + //return None + //} +}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { + SeqCmd::Editor(command) +} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { + SeqCmd::Pool(command) +} else { + return None +}); +keymap!(<'a> KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand { + // Tab: Toggle compact mode + key(Tab) => GrvCmd::Compact(!state.compact), + // q: Enqueue currently edited clip + key(Char('q')) => GrvCmd::Enqueue(state.pool.clip().clone()), + // 0: Enqueue clip 0 (stop all) + key(Char('0')) => GrvCmd::Enqueue(Some(state.pool.clips()[0].clone())), + // TODO: k: toggle on-screen keyboard + ctrl(key(Char('k'))) => todo!("keyboard"), + // Transport: Play from start or rewind to start + ctrl(key(Char(' '))) => GrvCmd::Clock( + if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } + ), + // Shift-R: toggle recording + shift(key(Char('R'))) => GrvCmd::Sampler(if state.sampler.recording.is_some() { + SamplerCommand::RecordFinish + } else { + SamplerCommand::RecordBegin(u7::from(state.editor.note_point() as u8)) + }), + // Shift-Del: delete sample + shift(key(Delete)) => GrvCmd::Sampler( + SamplerCommand::SetSample(u7::from(state.editor.note_point() as u8), None) + ), + // e: Toggle between editing currently playing or other clip + //shift(key(Char('e'))) => if let Some((_, Some(playing))) = state.player.play_clip() { + //let editing = state.editor.clip().as_ref().map(|p|p.read().unwrap().clone()); + //let selected = state.pool.clip().clone().map(|s|s.read().unwrap().clone()); + //GrvCmd::Editor(Show(if selected != editing { + //selected + //} else { + //Some(playing.clone()) + //})) + //} else { + //return None + //}, +}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { + GrvCmd::Editor(command) +} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { + GrvCmd::Pool(command) +} else { + return None +}); +keymap!(KEYS_ARRANGER = |state: Arranger, input: Event| ArrangerCommand { + key(Char('u')) => ArrCmd::History(-1), + key(Char('U')) => ArrCmd::History(1), + // TODO: k: toggle on-screen keyboard + ctrl(key(Char('k'))) => { todo!("keyboard") }, + // Transport: Play/pause + key(Char(' ')) => ArrCmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), + // Transport: Play from start or rewind to start + shift(key(Char(' '))) => ArrCmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), + key(Char('e')) => ArrCmd::Editor(MidiEditCommand::Show(state.pool.clip().clone())), + ctrl(key(Char('a'))) => ArrCmd::Scene(ArrangerSceneCommand::Add), + ctrl(key(Char('A'))) => return None,//ArrCmd::Scene(ArrangerSceneCommand::Add), + ctrl(key(Char('t'))) => ArrCmd::Track(ArrangerTrackCommand::Add), + // Tab: Toggle visibility of clip pool column + key(Tab) => ArrCmd::Pool(PoolCommand::Show(!state.pool.visible)), +}, { + use ArrangerSelection as Selected; + use ArrangerSceneCommand as Scene; + use ArrangerTrackCommand as Track; + use ArrangerClipCommand as Clip; + let t_len = state.tracks.len(); + let s_len = state.scenes.len(); + match state.selected { + Selected::Clip(t, s) => clip_keymap(state, input, t, s), + Selected::Scene(s) => scene_keymap(state, input, s), + Selected::Track(t) => track_keymap(state, input, t), + Selected::Mix => match input { + + kpat!(Delete) => Some(ArrCmd::Clear), + kpat!(Char('0')) => Some(ArrCmd::StopAll), + kpat!(Char('c')) => Some(ArrCmd::Color(ItemPalette::random())), + + kpat!(Up) => return None, + kpat!(Down) => Some( ArrCmd::Select(Selected::Scene(0))), + kpat!(Left) => return None, + kpat!(Right) => Some( ArrCmd::Select(Selected::Track(0))), + + _ => None + }, + } +}.or_else(||if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { + Some(ArrCmd::Editor(command)) +} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { + Some(ArrCmd::Pool(command)) +} else { + None +})?); + +fn clip_keymap (state: &Arranger, input: &Event, t: usize, s: usize) -> Option { + use ArrangerSelection as Selected; + use ArrangerSceneCommand as Scene; + use ArrangerTrackCommand as Track; + use ArrangerClipCommand as Clip; + let t_len = state.tracks.len(); + let s_len = state.scenes.len(); + Some(match input { + + kpat!(Char('g')) => ArrCmd::Pool(PoolCommand::Select(0)), + kpat!(Char('q')) => ArrCmd::Clip(Clip::Enqueue(t, s)), + kpat!(Char('l')) => ArrCmd::Clip(Clip::SetLoop(t, s, false)), + + kpat!(Enter) => if state.scenes[s].clips[t].is_none() { + // FIXME: get this clip from the pool (autoregister via intmut) + let (_, clip) = state.add_clip(); + ArrCmd::Clip(Clip::Put(t, s, Some(clip))) + } else { + return None + }, + kpat!(Delete) => ArrCmd::Clip(Clip::Put(t, s, None)), + kpat!(Char('p')) => ArrCmd::Clip(Clip::Put(t, s, state.pool.clip().clone())), + kpat!(Char(',')) => ArrCmd::Clip(Clip::Put(t, s, None)), + kpat!(Char('.')) => ArrCmd::Clip(Clip::Put(t, s, None)), + kpat!(Char('<')) => ArrCmd::Clip(Clip::Put(t, s, None)), + kpat!(Char('>')) => ArrCmd::Clip(Clip::Put(t, s, None)), + + kpat!(Up) => ArrCmd::Select(if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) }), + kpat!(Down) => ArrCmd::Select(Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1)))), + kpat!(Left) => ArrCmd::Select(if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) }), + kpat!(Right) => ArrCmd::Select(Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s)), + + _ => return None + }) +} +fn scene_keymap (state: &Arranger, input: &Event, s: usize) -> Option { + use ArrangerSelection as Selected; + use ArrangerSceneCommand as Scene; + use ArrangerTrackCommand as Track; + use ArrangerClipCommand as Clip; + let t_len = state.tracks.len(); + let s_len = state.scenes.len(); + Some(match input { + + kpat!(Char(',')) => ArrCmd::Scene(Scene::Swap(s, s - 1)), + kpat!(Char('.')) => ArrCmd::Scene(Scene::Swap(s, s + 1)), + kpat!(Char('<')) => ArrCmd::Scene(Scene::Swap(s, s - 1)), + kpat!(Char('>')) => ArrCmd::Scene(Scene::Swap(s, s + 1)), + kpat!(Char('q')) => ArrCmd::Scene(Scene::Enqueue(s)), + kpat!(Delete) => ArrCmd::Scene(Scene::Delete(s)), + kpat!(Char('c')) => ArrCmd::Scene(Scene::SetColor(s, ItemPalette::random())), + + kpat!(Up) => ArrCmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix }), + kpat!(Down) => ArrCmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1)))), + kpat!(Left) => return None, + kpat!(Right) => ArrCmd::Select(Selected::Clip(0, s)), + + _ => return None + }) +} +fn track_keymap (state: &Arranger, input: &Event, t: usize) -> Option { + use ArrangerSelection as Selected; + use ArrangerSceneCommand as Scene; + use ArrangerTrackCommand as Track; + use ArrangerClipCommand as Clip; + let t_len = state.tracks.len(); + let s_len = state.scenes.len(); + Some(match input { + + kpat!(Char(',')) => ArrCmd::Track(Track::Swap(t, t - 1)), + kpat!(Char('.')) => ArrCmd::Track(Track::Swap(t, t + 1)), + kpat!(Char('<')) => ArrCmd::Track(Track::Swap(t, t - 1)), + kpat!(Char('>')) => ArrCmd::Track(Track::Swap(t, t + 1)), + kpat!(Delete) => ArrCmd::Track(Track::Delete(t)), + kpat!(Char('c')) => ArrCmd::Track(Track::SetColor(t, ItemPalette::random())), + + kpat!(Up) => return None, + kpat!(Down) => ArrCmd::Select(Selected::Clip(t, 0)), + kpat!(Left) => ArrCmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix }), + kpat!(Right) => ArrCmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1)))), + + _ => return None + }) +} diff --git a/tek/src/groovebox.rs b/tek/src/groovebox.rs index e7120193..e6efe7d8 100644 --- a/tek/src/groovebox.rs +++ b/tek/src/groovebox.rs @@ -89,122 +89,7 @@ impl Groovebox { Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(self.sampler.list(self.compact, &self.editor)))) } } -audio!(|self: Groovebox, client, scope|{ - let t0 = self.perf.get_t0(); - if Control::Quit == ClockAudio(&mut self.player).process(client, scope) { - return Control::Quit - } - if Control::Quit == PlayerAudio( - &mut self.player, &mut self.note_buf, &mut self.midi_buf - ).process(client, scope) { - return Control::Quit - } - if Control::Quit == SamplerAudio(&mut self.sampler).process(client, scope) { - return Control::Quit - } - // TODO move these to editor and sampler: - for RawMidi { time, bytes } in self.player.midi_ins[0].port.iter(scope) { - if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { - match message { - MidiMessage::NoteOn { ref key, .. } => { - self.editor.set_note_point(key.as_int() as usize); - }, - MidiMessage::Controller { controller, value } => { - if let Some(sample) = &self.sampler.mapped[self.editor.note_point()] { - sample.write().unwrap().handle_cc(controller, value) - } - } - _ => {} - } - } - } - self.perf.update(t0, scope); - Control::Continue -}); has_clock!(|self: Groovebox|self.player.clock()); -pub enum GrooveboxCommand { - Compact(bool), - History(isize), - Clock(ClockCommand), - Pool(PoolCommand), - Editor(MidiEditCommand), - Enqueue(Option>>), - Sampler(SamplerCommand), -} -command!(|self: GrooveboxCommand, state: Groovebox|match self { - Self::Enqueue(clip) => { - state.player.enqueue_next(clip.as_ref()); - None - }, - Self::Pool(cmd) => match cmd { - // autoselect: automatically load selected clip in editor - PoolCommand::Select(_) => { - let undo = cmd.delegate(&mut state.pool, Self::Pool)?; - state.editor.set_clip(state.pool.clip().as_ref()); - undo - }, - // update color in all places simultaneously - PoolCommand::Phrase(SetColor(index, _)) => { - let undo = cmd.delegate(&mut state.pool, Self::Pool)?; - state.editor.set_clip(state.pool.clip().as_ref()); - undo - }, - _ => cmd.delegate(&mut state.pool, Self::Pool)? - }, - Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?, - Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, - Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, - Self::History(delta) => { todo!("undo/redo") }, - Self::Compact(compact) => if state.compact != compact { - state.compact = compact; - Some(Self::Compact(!compact)) - } else { - None - }, -}); -handle!(TuiIn: |self: Groovebox, input|GrooveboxCommand::execute_with_state(self, input.event())); -keymap!(<'a> KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand { - // Tab: Toggle compact mode - key(Tab) => Cmd::Compact(!state.compact), - // q: Enqueue currently edited clip - key(Char('q')) => Cmd::Enqueue(state.pool.clip().clone()), - // 0: Enqueue clip 0 (stop all) - key(Char('0')) => Cmd::Enqueue(Some(state.pool.clips()[0].clone())), - // TODO: k: toggle on-screen keyboard - ctrl(key(Char('k'))) => todo!("keyboard"), - // Transport: Play from start or rewind to start - ctrl(key(Char(' '))) => Cmd::Clock( - if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } - ), - // Shift-R: toggle recording - shift(key(Char('R'))) => Cmd::Sampler(if state.sampler.recording.is_some() { - SamplerCommand::RecordFinish - } else { - SamplerCommand::RecordBegin(u7::from(state.editor.note_point() as u8)) - }), - // Shift-Del: delete sample - shift(key(Delete)) => Cmd::Sampler( - SamplerCommand::SetSample(u7::from(state.editor.note_point() as u8), None) - ), - // e: Toggle between editing currently playing or other clip - //shift(key(Char('e'))) => if let Some((_, Some(playing))) = state.player.play_clip() { - //let editing = state.editor.clip().as_ref().map(|p|p.read().unwrap().clone()); - //let selected = state.pool.clip().clone().map(|s|s.read().unwrap().clone()); - //Cmd::Editor(Show(if selected != editing { - //selected - //} else { - //Some(playing.clone()) - //})) - //} else { - //return None - //}, -}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { - Cmd::Editor(command) -} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { - Cmd::Pool(command) -} else { - return None -}); ///// Status bar for sequencer app //#[derive(Clone)] diff --git a/tek/src/lib.rs b/tek/src/lib.rs index 5f4a75f3..1734cf94 100644 --- a/tek/src/lib.rs +++ b/tek/src/lib.rs @@ -10,7 +10,11 @@ pub type Usually = std::result::Result>; /// Standard optional result type. pub type Perhaps = std::result::Result, Box>; -pub mod app; pub use self::app::*; +pub mod app; pub use self::app::*; +pub mod view; pub use self::view::*; +pub mod control; pub use self::control::*; +pub mod audio; pub use self::audio::*; + pub mod arranger; pub use self::arranger::*; pub mod groovebox; pub use self::groovebox::*; pub mod mixer; pub use self::mixer::*; diff --git a/tek/src/sequencer.rs b/tek/src/sequencer.rs index 62037f6b..5478cf44 100644 --- a/tek/src/sequencer.rs +++ b/tek/src/sequencer.rs @@ -81,101 +81,7 @@ impl Sequencer { )) } } -audio!(|self:Sequencer, client, scope|{ - // Start profiling cycle - let t0 = self.perf.get_t0(); - // Update transport clock - if Control::Quit == ClockAudio(self).process(client, scope) { - return Control::Quit - } - // Update MIDI sequencer - if Control::Quit == PlayerAudio( - &mut self.player, &mut self.note_buf, &mut self.midi_buf - ).process(client, scope) { - return Control::Quit - } - // End profiling cycle - self.perf.update(t0, scope); - Control::Continue -}); has_size!(|self:Sequencer|&self.size); has_clock!(|self:Sequencer|&self.player.clock); has_clips!(|self:Sequencer|self.pool.clips); has_editor!(|self:Sequencer|self.editor); -handle!(TuiIn: |self:Sequencer,input|SequencerCommand::execute_with_state(self, input.event())); -#[derive(Clone, Debug)] pub enum SequencerCommand { - Compact(bool), - History(isize), - Clock(ClockCommand), - Pool(PoolCommand), - Editor(MidiEditCommand), - Enqueue(Option>>), -} -keymap!(KEYS_SEQUENCER = |state: Sequencer, input: Event| SequencerCommand { - // TODO: k: toggle on-screen keyboard - ctrl(key(Char('k'))) => { todo!("keyboard") }, - // Transport: Play/pause - key(Char(' ')) => Cmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), - // Transport: Play from start or rewind to start - shift(key(Char(' '))) => Cmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), - // u: undo - key(Char('u')) => Cmd::History(-1), - // Shift-U: redo - key(Char('U')) => Cmd::History( 1), - // Tab: Toggle compact mode - key(Tab) => Cmd::Compact(!state.compact), - // q: Enqueue currently edited clip - key(Char('q')) => Cmd::Enqueue(state.pool.clip().clone()), - // 0: Enqueue clip 0 (stop all) - key(Char('0')) => Cmd::Enqueue(Some(state.clips()[0].clone())), - // e: Toggle between editing currently playing or other clip - //key(Char('e')) => if let Some((_, Some(playing))) = state.player.play_clip() { - //let editing = state.editor.clip().as_ref().map(|p|p.read().unwrap().clone()); - //let selected = state.pool.clip().clone(); - //Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing { - //selected - //} else { - //playing.clone() - //}))) - //} else { - //return None - //} -}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { - Cmd::Editor(command) -} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { - Cmd::Pool(command) -} else { - return None -}); -command!(|self: SequencerCommand, state: Sequencer|match self { - Self::Enqueue(clip) => { - state.player.enqueue_next(clip.as_ref()); - None - }, - Self::Pool(cmd) => match cmd { - // autoselect: automatically load selected clip in editor - PoolCommand::Select(_) => { - let undo = cmd.delegate(&mut state.pool, Self::Pool)?; - state.editor.set_clip(state.pool.clip().as_ref()); - undo - }, - // update color in all places simultaneously - PoolCommand::Phrase(SetColor(index, _)) => { - let undo = cmd.delegate(&mut state.pool, Self::Pool)?; - state.editor.set_clip(state.pool.clip().as_ref()); - undo - }, - _ => cmd.delegate(&mut state.pool, Self::Pool)? - }, - Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, - Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, - Self::History(delta) => { - todo!("undo/redo") - }, - Self::Compact(compact) => if state.compact != compact { - state.compact = compact; - Some(Self::Compact(!compact)) - } else { - None - }, -}); diff --git a/tek/src/view.rs b/tek/src/view.rs new file mode 100644 index 00000000..a8c1135d --- /dev/null +++ b/tek/src/view.rs @@ -0,0 +1,338 @@ +use crate::*; +render!(TuiOut: (self: App) => self.size.of(EdnView::from_source(self, self.edn.as_ref()))); +audio!(|self: App, _client, _scope|Control::Continue); +impl EdnViewData for &App { + fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> { + use EdnItem::*; + let w = self.tracks_with_sizes().last().map(|x|x.3 as u16).unwrap_or(0); + match item { + Nil => Box::new(()), + Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), + Sym(":editor") => (&self.editor).boxed(), + Sym(":inputs") => self.input_row(w, 3).boxed(), + Sym(":outputs") => self.output_row(w, 3).boxed(), + Sym(":pool") => self.pool().boxed(), + Sym(":sample") => self.sample().boxed(), + Sym(":sampler") => self.sampler().boxed(), + Sym(":scenes") => self.scene_row(w, self.size.h().saturating_sub(9) as u16).boxed(), + Sym(":status") => self.status(0).boxed(), + Sym(":toolbar") => self.toolbar().boxed(), + Sym(":tracks") => self.track_row(w, 3).boxed(), + _ => panic!("no content for {item:?}") + } + } + fn get_unit (&self, item: EdnItem<&str>) -> u16 { + use EdnItem::*; + match item.to_str() { + ":sample-h" => if self.compact() { 0 } else { 5 }, + ":samples-w" => if self.compact() { 4 } else { 11 }, + ":samples-y" => if self.compact() { 1 } else { 0 }, + ":pool-w" => if self.compact() { 5 } else { + let w = self.size.w(); + if w > 60 { 20 } else if w > 40 { 15 } else { 10 } + }, + _ => 0 + } + } +} +impl App { + fn compact (&self) -> bool { false } + fn toolbar (&self) -> impl Content + use<'_> { + Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock)))) + } + fn status (&self, note_pt: usize) -> impl Content + use<'_> { + self.editor.as_ref() + .map(|e|Bsp::e(e.clip_status(), e.edit_status())) + } + fn pool (&self) -> impl Content + use<'_> { + self.pool.as_ref() + .map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))) + } + fn editor (&self) -> impl Content + '_ { + &self.editor + } + fn sample <'a> (&'a self) -> impl Content + 'a { + let compact = self.is_editing(); + if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) { + let note_pt = editor.note_point(); + let sample_h = if compact { 0 } else { 5 }; + return Some(Max::y(sample_h, Fill::xy(Bsp::a( + Fill::x(Align::w(Fixed::y(1, self.status(note_pt)))), + sampler.viewer(note_pt) + )))) + } + None + } + fn sampler (&self) -> impl Content + use<'_> { + let compact = self.is_editing(); + if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) { + let note_pt = editor.note_point(); + let w = if compact { 4 } else { 40 }; + let y = if compact { 1 } else { 0 }; + return Some(Fixed::x(w, Push::y(y, Fill::y(sampler.list(compact, editor))))) + } + None + } + + fn row <'a> ( + &'a self, w: u16, h: u16, a: impl Content + 'a, b: impl Content + 'a + ) -> impl Content + 'a { + Fixed::y(h, Bsp::e( + Fixed::xy(self.sidebar_w() as u16, h, a), + Fill::x(Align::c(Fixed::xy(w, h, b))) + )) + } + fn track_row (&self, w: u16, h: u16) -> impl Content + '_ { + self.row(w, h, track_header(&self), track_cells(&self)) + } + fn input_row (&self, w: u16, h: u16) -> impl Content + '_ { + self.row(w, h, input_header(&self), input_cells(&self)) + } + fn output_row (&self, w: u16, h: u16) -> impl Content + '_ { + self.row(w, h, output_header(&self), output_cells(&self)) + } + fn scene_row (&self, w: u16, h: u16) -> impl Content + '_ { + self.row(w, h, output_header(&self), output_cells(&self)) + } + + pub fn tracks_with_sizes (&self) + -> impl Iterator + { + tracks_with_sizes(self.tracks.iter(), match self.selected { + ArrangerSelection::Track(t) if self.is_editing() => Some(t), + ArrangerSelection::Clip(t, _) if self.is_editing() => Some(t), + _ => None + }, self.editor_w()) + } + pub fn scenes_with_sizes (&self, h: usize) + -> impl Iterator + { + scenes_with_sizes(self.scenes.iter(), &self.selected, self.is_editing(), 2, 15) + } + fn is_editing (&self) -> bool { + self.editing.load(Relaxed) + } + fn editor_w (&self) -> usize { + let editor = self.editor.as_ref().expect("missing editor"); + (5 + (editor.time_len().get() / editor.time_zoom().get())) + .min(self.size.w().saturating_sub(20)) + .max(16) + } + fn sidebar_w (&self) -> u16 { + let w = self.size.w(); + let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; + let w = if self.is_editing() { 8 } else { w }; + w + } +} +pub fn scenes_with_sizes <'a>( + scenes: impl Iterator + 'a, + selected: &'a ArrangerSelection, + editing: bool, + scene_height: usize, + scene_larger: usize, +) -> impl Iterator + 'a { + let mut y = 0; + let (selected_track, selected_scene) = match selected { + ArrangerSelection::Clip(t, s) => (Some(t), Some(s)), + _ => (None, None) + }; + scenes.enumerate().map(move|(s, scene)|{ + let active = editing && selected_track.is_some() && selected_scene == Some(&s); + let height = if active { scene_larger } else { scene_height }; + let data = (s, scene, y, y + height); + y += height; + data + }) +} +pub fn tracks_with_sizes <'a> ( + tracks: impl Iterator, + active: Option, + bigger: usize +) -> impl Iterator { + let mut x = 0; + tracks.enumerate().map(move |(index, track)|{ + let width = if Some(index) == active { bigger } else { track.width.max(8) }; + let data = (index, track, x, x + width); + x += width; + data + }) +} +pub fn track_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { + (||Tui::bg(TuiTheme::g(32), Tui::bold(true, Bsp::s( + row!( + Tui::fg(TuiTheme::g(128), "add "), + Tui::fg(TuiTheme::orange(), "t"), + Tui::fg(TuiTheme::g(128), "rack"), + ), + row!( + Tui::fg(TuiTheme::orange(), "a"), + Tui::fg(TuiTheme::g(128), "dd scene"), + ), + ))).boxed()).into() +} +pub fn track_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { + let iter = ||state.tracks_with_sizes(); + (move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| { + let name = Push::x(1, &track.name); + let color = track.color; + let fg = color.lightest.rgb; + let bg = color.base.rgb; + let active = state.selected.track() == Some(i); + let bfg = if active { Color::Rgb(255,255,255) } else { Color::Rgb(0,0,0) }; + let border = Style::default().fg(bfg).bg(bg); + Tui::bg(bg, map_east(x1 as u16, (x2 - x1) as u16, + Outer(border).enclose(Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::x(name))))) + )) + })).boxed()).into() +} +fn help_tag <'a>(before: &'a str, key: &'a str, after: &'a str) -> impl Content + 'a { + let lo = TuiTheme::g(128); + let hi = TuiTheme::orange(); + Tui::bold(true, row!(Tui::fg(lo, before), Tui::fg(hi, key), Tui::fg(lo, after))) +} +fn input_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { + let fg = TuiTheme::g(224); + let bg = TuiTheme::g(64); + (move||Bsp::s(help_tag("midi ", "I", "ns"), state.midi_ins.get(0).map(|inp|Bsp::s( + Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(inp.name.clone())))), + inp.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, + Tui::fg_bg(fg, bg, connect.info()))))), + ))).boxed()).into() +} +fn input_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { + (move||Align::x(Map::new(||state.tracks_with_sizes(), move|(_, track, x1, x2), i| { + let w = (x2 - x1) as u16; + let color: ItemPalette = track.color.dark.into(); + map_east(x1 as u16, w, Fixed::x(w, cell(color, Bsp::n( + rec_mon(color.base.rgb, false, false), + phat_hi(color.base.rgb, color.dark.rgb) + )))) + })).boxed()).into() +} +fn output_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { + let fg = TuiTheme::g(224); + let bg = TuiTheme::g(64); + (move||Bsp::s(help_tag("midi ", "O", "uts"), state.midi_outs.get(0).map(|out|Bsp::s( + Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(out.name.clone())))), + out.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, + Tui::fg_bg(fg, bg, connect.info()))))), + ))).boxed()).into() +} +fn output_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { + (move||Align::x(Map::new(||state.tracks_with_sizes(), move|(_, track, x1, x2), i| { + let w = (x2 - x1) as u16; + let color: ItemPalette = track.color.dark.into(); + map_east(x1 as u16, w, Fixed::x(w, cell(color, Bsp::n( + mute_solo(color.base.rgb, false, false), + phat_hi(color.dark.rgb, color.darker.rgb) + )))) + })).boxed()).into() +} +fn scene_headers <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { + (||{ + let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); + let selected = state.selected.scene(); + Fill::y(Align::c(Map::new(||state.scenes_with_sizes(2), move|(_, scene, y1, y2), i| { + let h = (y2 - y1) as u16; + let name = format!("🭬{}", &scene.name); + let color = scene.color; + let active = selected == Some(i); + let mid = if active { color.light } else { color.base }; + let top = Some(last_color.read().unwrap().base.rgb); + let cell = phat_sel_3( + active, + Tui::bold(true, name.clone()), + Tui::bold(true, name), + top, + mid.rgb, + Color::Rgb(0, 0, 0) + ); + *last_color.write().unwrap() = color; + map_south(y1 as u16, h + 1, Fixed::y(h + 1, cell)) + }))).boxed() + }).into() +} +fn scene_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { + let editing = state.is_editing(); + let tracks = move||state.tracks_with_sizes(); + let scenes = ||state.scenes_with_sizes(2); + let selected_track = state.selected.track(); + let selected_scene = state.selected.scene(); + (move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| { + let w = (x2 - x1) as u16; + let color: ItemPalette = track.color.dark.into(); + let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); + let cells = Map::new(scenes, move|(_, scene, y1, y2), s| { + let h = (y2 - y1) as u16; + let color = scene.color; + let (name, fg, bg) = if let Some(c) = &scene.clips[t] { + let c = c.read().unwrap(); + (c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb) + } else { + ("⏹ ".to_string(), TuiTheme::g(64), TuiTheme::g(32)) + }; + let last = last_color.read().unwrap().clone(); + let active = editing && selected_scene == Some(s) && selected_track == Some(t); + let editor = Thunk::new(||&state.editor); + let cell = Thunk::new(move||phat_sel_3( + selected_track == Some(t) && selected_scene == Some(s), + Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))), + Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))), + if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) { + None + } else { + Some(bg.into()) + }, + bg.into(), + bg.into(), + )); + let cell = Either(active, editor, cell); + *last_color.write().unwrap() = bg.into(); + map_south( + y1 as u16, + h + 1, + Fill::x(Fixed::y(h + 1, cell)) + ) + }); + let column = Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed()); + Fixed::x(w, map_east(x1 as u16, w, column)) + }))).boxed()).into() +} +fn cell_clip <'a> ( + scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16 +) -> impl Content + use<'a> { + scene.clips.get(index).map(|clip|clip.as_ref().map(|clip|{ + let clip = clip.read().unwrap(); + let mut bg = TuiTheme::border_bg(); + let name = clip.name.to_string(); + let max_w = name.len().min((w as usize).saturating_sub(2)); + let color = clip.color; + bg = color.dark.rgb; + if let Some((_, Some(ref playing))) = track.player.play_clip() { + if *playing.read().unwrap() == *clip { + bg = color.light.rgb + } + }; + Fixed::xy(w, h, &Tui::bg(bg, Push::x(1, Fixed::x(w, &name.as_str()[0..max_w])))); + })) +} +fn rec_mon (bg: Color, rec: bool, mon: bool) -> impl Content { + row!( + Tui::fg_bg(if rec { Color::Red } else { bg }, bg, "▐"), + Tui::fg_bg(if rec { Color::White } else { Color::Rgb(0,0,0) }, bg, "REC"), + Tui::fg_bg(if rec { Color::White } else { bg }, bg, "▐"), + Tui::fg_bg(if mon { Color::White } else { Color::Rgb(0,0,0) }, bg, "MON"), + Tui::fg_bg(if mon { Color::White } else { bg }, bg, "▌"), + ) +} +fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content { + row!( + Tui::fg_bg(if mute { Color::White } else { Color::Rgb(0,0,0) }, bg, "MUTE"), + Tui::fg_bg(if mute { Color::White } else { bg }, bg, "▐"), + Tui::fg_bg(if solo { Color::White } else { Color::Rgb(0,0,0) }, bg, "SOLO"), + ) +} +fn cell > (color: ItemPalette, field: T) -> impl Content { + Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field)) +}