From 9cd6e9f195d9699b93600b6d21e34405a444b53b Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 14 Jan 2025 15:39:28 +0100 Subject: [PATCH] unify edn_view entrypoint --- input/examples/edn-keys.rs | 1 + input/src/edn_keymap.rs | 14 +- midi/examples/midi-import.rs | 19 +- midi/{edn => src}/midi-view.edn | 0 midi/src/midi_editor.rs | 18 +- .../midi_editor_keys.edn} | 0 midi/{edn => src}/piano-view-h.edn | 0 midi/{edn => src}/piano-view-v.edn | 0 midi/{edn => src}/pool-keys.edn | 0 output/examples/edn-view.rs | 30 +- output/src/edn_view.rs | 57 +- output/src/lib.rs | 65 +- tek/src/arranger-view.edn | 2 +- tek/src/groovebox-keys.edn | 0 tek/src/lib.rs | 938 +++++++++--------- tek/src/sequencer-keys.edn | 0 16 files changed, 603 insertions(+), 541 deletions(-) rename midi/{edn => src}/midi-view.edn (100%) rename midi/{edn/midi-keys.edn => src/midi_editor_keys.edn} (100%) rename midi/{edn => src}/piano-view-h.edn (100%) rename midi/{edn => src}/piano-view-v.edn (100%) rename midi/{edn => src}/pool-keys.edn (100%) create mode 100644 tek/src/groovebox-keys.edn create mode 100644 tek/src/sequencer-keys.edn diff --git a/input/examples/edn-keys.rs b/input/examples/edn-keys.rs index e69de29b..edc5c930 100644 --- a/input/examples/edn-keys.rs +++ b/input/examples/edn-keys.rs @@ -0,0 +1 @@ +fn main () {} diff --git a/input/src/edn_keymap.rs b/input/src/edn_keymap.rs index 49396959..dd5ca46c 100644 --- a/input/src/edn_keymap.rs +++ b/input/src/edn_keymap.rs @@ -4,7 +4,9 @@ use EdnItem::*; pub struct EdnKeymap(pub Vec>); impl EdnKeymap { - pub fn command , E: EdnCommand, I: EdnInput> (&self, state: &C, input: &I) -> Option { + pub fn command , E: EdnCommand, I: EdnInput> ( + &self, state: &C, input: &I + ) -> Option { for item in self.0.iter() { if let Exp(items) = item { match items.as_slice() { @@ -20,3 +22,13 @@ impl EdnKeymap { None } } + +impl> From for EdnKeymap { + fn from (source: T) -> Self { + Self(EdnItem::::read_all(source.as_ref()).expect("failed to load keymap")) + } +} + +#[cfg(test)] #[test] fn test_edn_keymap () { + let keymap = EdnKeymap::from(""); +} diff --git a/midi/examples/midi-import.rs b/midi/examples/midi-import.rs index 9aa894be..802bf5b4 100644 --- a/midi/examples/midi-import.rs +++ b/midi/examples/midi-import.rs @@ -1,18 +1,19 @@ -use tek::*; +use tek_midi::*; +use std::sync::*; -struct ExampleClips(Vec>>); +struct ExampleClips(Arc>>>>); impl HasClips for ExampleClips { - fn phrases (&self) -> &Vec>> { - &self.0 + fn clips (&self) -> RwLockReadGuard<'_, Vec>>> { + self.0.read().unwrap() } - fn phrases_mut (&mut self) -> &mut Vec>> { - &mut self.0 + fn clips_mut (&self) -> RwLockWriteGuard<'_, Vec>>> { + self.0.write().unwrap() } } -fn main () -> Usually<()> { - let mut phrases = ExampleClips(vec![]); - PoolClipCommand::Import(0, String::from("./example.mid")).execute(&mut phrases)?; +fn main () -> Result<(), Box> { + let mut clips = ExampleClips(Arc::new(vec![].into())); + PoolClipCommand::Import(0, String::from("./example.mid")).execute(&mut clips)?; Ok(()) } diff --git a/midi/edn/midi-view.edn b/midi/src/midi-view.edn similarity index 100% rename from midi/edn/midi-view.edn rename to midi/src/midi-view.edn diff --git a/midi/src/midi_editor.rs b/midi/src/midi_editor.rs index c50efcab..3ea0f8a4 100644 --- a/midi/src/midi_editor.rs +++ b/midi/src/midi_editor.rs @@ -7,11 +7,25 @@ pub trait HasEditor { fn editor_h (&self) -> usize { 0 } } #[macro_export] macro_rules! has_editor { + (|$self:ident: $Struct:ident|{ + editor = $e0:expr; + editor_w = $e1:expr; + editor_h = $e2:expr; + is_editing = $e3:expr; + }) => { + impl HasEditor for $Struct { + fn editor (&$self) -> &Option { &$e0 } + fn editor_mut (&mut $self) -> &Option { &mut $e0 } + fn editor_w (&$self) -> usize { $e1 } + fn editor_h (&$self) -> usize { $e2 } + fn is_editing (&$self) -> bool { $e3 } + } + }; (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? { fn editor (&$self) -> &MidiEditor { &$cb } } - } + }; } /// Contains state for viewing and editing a clip pub struct MidiEditor { @@ -32,7 +46,7 @@ impl Default for MidiEditor { mode: PianoHorizontal::new(None), size: Measure::new(), keymap: EdnKeymap( - EdnItem::::read_all(include_str!("../edn/midi-keys.edn")) + EdnItem::::read_all(include_str!("midi_editor_keys.edn")) .expect("failed to load keymap for MidiEditor") ) } diff --git a/midi/edn/midi-keys.edn b/midi/src/midi_editor_keys.edn similarity index 100% rename from midi/edn/midi-keys.edn rename to midi/src/midi_editor_keys.edn diff --git a/midi/edn/piano-view-h.edn b/midi/src/piano-view-h.edn similarity index 100% rename from midi/edn/piano-view-h.edn rename to midi/src/piano-view-h.edn diff --git a/midi/edn/piano-view-v.edn b/midi/src/piano-view-v.edn similarity index 100% rename from midi/edn/piano-view-v.edn rename to midi/src/piano-view-v.edn diff --git a/midi/edn/pool-keys.edn b/midi/src/pool-keys.edn similarity index 100% rename from midi/edn/pool-keys.edn rename to midi/src/pool-keys.edn diff --git a/output/examples/edn-view.rs b/output/examples/edn-view.rs index b1601f5b..c38c4966 100644 --- a/output/examples/edn-view.rs +++ b/output/examples/edn-view.rs @@ -1,7 +1,8 @@ -use tek::*; +use tek_tui::{*, tek_input::*, tek_output::*}; use tek_edn::*; use std::sync::{Arc, RwLock}; use crossterm::event::{*, KeyCode::*}; +use crate::ratatui::style::Color; const EDN: &'static [&'static str] = &[ include_str!("edn01.edn"), @@ -25,26 +26,15 @@ fn main () -> Usually<()> { Ok(()) } -pub struct Example(usize, Measure); +#[derive(Debug)] pub struct Example(usize, Measure); -impl EdnViewData for &Example { - fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> { - use EdnItem::*; - match item { - Nil => Box::new(()), - Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), - //Sym(name) => self.get_sym(name), TODO - Sym(":title") => Tui::bg(Color::Rgb(60,10,10), Push::y(1, - Align::n(format!("Example {}/{}:", self.0 + 1, EDN.len())))).boxed(), - Sym(":code") => Tui::bg(Color::Rgb(10,60,10), Push::y(2, - Align::n(format!("{}", EDN[self.0])))).boxed(), - Sym(":hello-world") => "Hello world!".boxed(), - Sym(":hello") => Tui::bg(Color::Rgb(10, 100, 10), "Hello").boxed(), - Sym(":world") => Tui::bg(Color::Rgb(100, 10, 10), "world").boxed(), - _ => panic!("no content for {item:?}") - } - } -} +edn_provide_content!(|self: Example|{ + ":title" => Tui::bg(Color::Rgb(60,10,10), Push::y(1, Align::n(format!("Example {}/{}:", self.0 + 1, EDN.len())))).boxed(), + ":code" => Tui::bg(Color::Rgb(10,60,10), Push::y(2, Align::n(format!("{}", EDN[self.0])))).boxed(), + ":hello-world" => "Hello world!".boxed(), + ":hello" => Tui::bg(Color::Rgb(10, 100, 10), "Hello").boxed(), + ":world" => Tui::bg(Color::Rgb(100, 10, 10), "world").boxed(), +}); impl Content for Example { fn content (&self) -> impl Render { diff --git a/output/src/edn_view.rs b/output/src/edn_view.rs index 0936848e..9667a5e0 100644 --- a/output/src/edn_view.rs +++ b/output/src/edn_view.rs @@ -1,11 +1,42 @@ use crate::*; use std::marker::PhantomData; use EdnItem::*; + +#[macro_export] macro_rules! edn_view { + ($Output:ty: |$self:ident: $App:ty| $content:expr; { + $( $type:ty { $($sym:literal => $value:expr),* } );* + }) => { + impl Content<$Output> for $App { + fn content(&$self) -> impl Render<$Output> { $content } + } + $( + impl<'a> EdnProvide<'a, $type> for $App { + fn get > (&'a $self, edn: &'a EdnItem) -> Option<$type> { + Some(match edn.to_ref() { + $(EdnItem::Sym($sym) => $value,)* + _ => return None + }) + } + } + )* + } +} + /// Implements `EdnProvide` for content components and expressions #[macro_export] macro_rules! edn_provide_content { - (|$self:ident:$Stat:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => { - impl<'a> EdnProvide<'a, Box + 'a>> for $State { - fn get > (&'a $self, edn: &'a EdnItem) -> Option + 'a>> { + (|$self:ident:$State:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => { + impl<'a, E: Output> EdnProvide<'a, Box + 'a>> for $State { + fn get > (&'a $self, edn: &'a EdnItem) -> Option + 'a>> { + Some(match edn.to_ref() { + $(EdnItem::Sym($pat) => $expr),*, + _ => return None + }) + } + } + }; + ($Output:ty: |$self:ident:$State:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => { + impl<'a> EdnProvide<'a, Box + 'a>> for $State { + fn get > (&'a $self, edn: &'a EdnItem) -> Option + 'a>> { Some(match edn.to_ref() { $(EdnItem::Sym($pat) => $expr),*, _ => return None @@ -15,21 +46,23 @@ use EdnItem::*; } } /// Renders from EDN source and context. -#[derive(Default)] -pub enum EdnView<'a, E: Output, T: EdnViewData<'a, E> + std::fmt::Debug> { - #[default] - Inert, - _Unused(PhantomData<&'a E>), +#[derive(Default)] pub enum EdnView<'a, E: Output, T: EdnViewData<'a, E> + std::fmt::Debug> { + #[default] Inert, Ok(T, EdnItem<&'a str>), //render: BoxBox + Send + Sync + 'a> + Send + Sync + 'a> - Err(String) + Err(String), + _Unused(PhantomData<&'a E>), } impl<'a, E: Output, T: EdnViewData<'a, E> + std::fmt::Debug> std::fmt::Debug for EdnView<'a, E, T> { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { match self { - Self::Inert | Self::_Unused(_) => write!(f, "EdnView::Inert"), - Self::Ok(state, view) => write!(f, "EdnView::Ok(state={state:?} view={view:?}"), - Self::Err(error) => write!(f, "EdnView::Err({error})"), + Self::Inert | Self::_Unused(_) => + write!(f, "EdnView::Inert"), + Self::Ok(state, view) => + write!(f, "EdnView::Ok(state={state:?} view={view:?}"), + Self::Err(error) => + write!(f, "EdnView::Err({error})"), + _ => unreachable!() } } } diff --git a/output/src/lib.rs b/output/src/lib.rs index 7f0831cb..7fc745c7 100644 --- a/output/src/lib.rs +++ b/output/src/lib.rs @@ -66,42 +66,35 @@ pub type Perhaps = Result, Box>; #[cfg(test)] #[test] fn test_layout () -> Usually<()> { use ::tek_tui::{*, tek_output::*}; let area: [u16;4] = [10, 10, 20, 20]; - let unit = (); - - assert_eq!(Content::::layout(&unit, area), [20, 20, 0, 0]); - - assert_eq!(Content::::layout(&Fill::<()>::x(unit), area), [10, 20, 20, 0]); - assert_eq!(Render::::layout(&Fill::<()>::x(unit), area), [10, 20, 20, 0]); - - assert_eq!(Fill::<()>::y(unit).layout(area), [20, 10, 0, 20]); - assert_eq!(Fill::<()>::xy(unit).layout(area), area); - - assert_eq!(Fixed::::x(4, unit).layout(area), [18, 20, 4, 0]); - assert_eq!(Fixed::::y(4, unit).layout(area), [20, 18, 0, 4]); - assert_eq!(Fixed::::xy(4, 4, unit).layout(area), [18, 18, 4, 4]); - - let four = ||Fixed::::xy(4, 4, unit); - - assert_eq!(Align::nw(four()).layout(area), [10, 10, 4, 4]); - assert_eq!(Align::n(four()).layout(area), [18, 10, 4, 4]); - assert_eq!(Align::ne(four()).layout(area), [26, 10, 4, 4]); - assert_eq!(Align::e(four()).layout(area), [26, 18, 4, 4]); - assert_eq!(Align::se(four()).layout(area), [26, 26, 4, 4]); - assert_eq!(Align::s(four()).layout(area), [18, 26, 4, 4]); - assert_eq!(Align::sw(four()).layout(area), [10, 26, 4, 4]); - assert_eq!(Align::w(four()).layout(area), [10, 18, 4, 4]); - - let two_by_four = ||Fixed::::xy(4, 2, unit); - - assert_eq!(Align::nw(two_by_four()).layout(area), [10, 10, 4, 2]); - assert_eq!(Align::n(two_by_four()).layout(area), [18, 10, 4, 2]); - assert_eq!(Align::ne(two_by_four()).layout(area), [26, 10, 4, 2]); - assert_eq!(Align::e(two_by_four()).layout(area), [26, 19, 4, 2]); - assert_eq!(Align::se(two_by_four()).layout(area), [26, 28, 4, 2]); - assert_eq!(Align::s(two_by_four()).layout(area), [18, 28, 4, 2]); - assert_eq!(Align::sw(two_by_four()).layout(area), [10, 28, 4, 2]); - assert_eq!(Align::w(two_by_four()).layout(area), [10, 19, 4, 2]); - + fn test (area: [u16;4], item: &impl Content, expected: [u16;4]) { + assert_eq!(Content::layout(item, area), expected); + assert_eq!(Render::layout(item, area), expected); + }; + test(area, &(), [20, 20, 0, 0]); + test(area, &Fill::xy(()), area); + test(area, &Fill::x(()), [10, 20, 20, 0]); + test(area, &Fill::y(()), [20, 10, 0, 20]); + test(area, &Fixed::x(4, unit), [18, 20, 4, 0]); + test(area, &Fixed::y(4, unit), [20, 18, 0, 4]); + test(area, &Fixed::xy(4, 4, unit), [18, 18, 4, 4]); + let four = ||Fixed::::xy(4, 4, unit); + test(area, &Align::nw(four()), [10, 10, 4, 4]); + test(area, &Align::n(four()), [18, 10, 4, 4]); + test(area, &Align::ne(four()), [26, 10, 4, 4]); + test(area, &Align::e(four()), [26, 18, 4, 4]); + test(area, &Align::se(four()), [26, 26, 4, 4]); + test(area, &Align::s(four()), [18, 26, 4, 4]); + test(area, &Align::sw(four()), [10, 26, 4, 4]); + test(area, &Align::w(four()), [10, 18, 4, 4]); + let two_by_four = ||Fixed::::xy(4, 2, unit); + test(area, &Align::nw(two_by_four()), [10, 10, 4, 2]); + test(area, &Align::n(two_by_four()), [18, 10, 4, 2]); + test(area, &Align::ne(two_by_four()), [26, 10, 4, 2]); + test(area, &Align::e(two_by_four()), [26, 19, 4, 2]); + test(area, &Align::se(two_by_four()), [26, 28, 4, 2]); + test(area, &Align::s(two_by_four()), [18, 28, 4, 2]); + test(area, &Align::sw(two_by_four()), [10, 28, 4, 2]); + test(area, &Align::w(two_by_four()), [10, 19, 4, 2]); Ok(()) } diff --git a/tek/src/arranger-view.edn b/tek/src/arranger-view.edn index c26febe0..850bd21b 100644 --- a/tek/src/arranger-view.edn +++ b/tek/src/arranger-view.edn @@ -1,3 +1,3 @@ (bsp/s (fixed/y 2 :toolbar) (fill/x (align/c (bsp/w :pool - (bsp/n :outputs (bsp/n :inputs (bsp/n :tracks :scenes))))))) + (bsp/s :outputs (bsp/s :inputs (bsp/s :tracks :scenes))))))) diff --git a/tek/src/groovebox-keys.edn b/tek/src/groovebox-keys.edn new file mode 100644 index 00000000..e69de29b diff --git a/tek/src/lib.rs b/tek/src/lib.rs index 52bcfbd1..60e0deda 100644 --- a/tek/src/lib.rs +++ b/tek/src/lib.rs @@ -62,74 +62,434 @@ impl HasSampler for App { fn sampler_mut (&mut self) -> &mut Option { &mut self.sampler } fn sample_index (&self) -> usize { self.editor.as_ref().map(|e|e.note_point()).unwrap_or(0) } } -impl HasEditor for App { - fn editor (&self) -> &Option { &self.editor } - fn editor_mut (&mut self) -> &Option { &mut self.editor } +has_editor!(|self: App|{ + editor = self.editor; + editor_w = { + let size = self.size.w(); + let editor = self.editor.as_ref().expect("missing editor"); + let time_len = editor.time_len().get(); + let time_zoom = editor.time_zoom().get().max(1); + (5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) + }; + editor_h = 15; + is_editing = self.editing.load(Relaxed); +}); +edn_view!(TuiOut: |self: App| self.size.of(EdnView::from_source(self, self.edn.as_ref())); { + bool {}; + usize {}; + isize {}; + Color {}; + Selection {}; + Arc> {}; + Option>> {}; + + u16 { + ":sidebar-w" => self.sidebar_w(), + ":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 } } }; + + Box + 'a> { + ":editor" => (&self.editor).boxed(), + ":pool" => self.pool.as_ref().map(|pool|PoolView(self.compact(), pool)).boxed(), + ":sample" => self.view_sample(self.is_editing()).boxed(), + ":sampler" => self.view_sampler(self.is_editing(), &self.editor).boxed(), + ":status" => self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())).boxed(), + ":toolbar" => ClockView::new(true, &self.clock).boxed(), + ":tracks" => self.row(self.w(), 3, self.track_header(), self.track_cells()).boxed(), + ":inputs" => self.row(self.w(), 3, self.input_header(), self.input_cells()).boxed(), + ":outputs" => self.row(self.w(), 3, self.output_header(), self.output_cells()).boxed(), + ":scenes" => self.row(self.w(), self.size.h().saturating_sub(9) as u16, + self.scene_header(), self.scene_cells(self.is_editing())).boxed() }}); + +//content!(TuiOut:|self: App|self.size.of(EdnView::from_source(self, self.edn.as_ref()))); +//edn_provide!(bool: |self: App| { _ => return None }); +//edn_provide!(usize: |self: App| { _ => return None }); +//edn_provide!(isize: |self: App| { _ => return None }); +//edn_provide!(Color: |self: App| { _ => return None }); +//edn_provide!(Selection: |self: App| { _ => return None }); +//edn_provide!(Arc>: |self: App| { _ => return None }); +//edn_provide!(Option>>: |self: App| { _ => return None }); +//edn_provide!('a: Box + 'a>: |self: App|{ + //":editor" => (&self.editor).boxed(), + //":pool" => self.pool.as_ref().map(|pool|PoolView(self.compact(), pool)).boxed(), + //":sample" => self.view_sample(self.is_editing()).boxed(), + //":sampler" => self.view_sampler(self.is_editing(), &self.editor).boxed(), + //":status" => self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())).boxed(), + //":toolbar" => ClockView::new(true, &self.clock).boxed(), + //":tracks" => self.row(self.w(), 3, self.track_header(), self.track_cells()).boxed(), + //":inputs" => self.row(self.w(), 3, self.input_header(), self.input_cells()).boxed(), + //":outputs" => self.row(self.w(), 3, self.output_header(), self.output_cells()).boxed(), + //":scenes" => self.row(self.w(), self.size.h().saturating_sub(9) as u16, + //self.scene_header(), self.scene_cells(self.is_editing())).boxed() }); +//edn_provide!(u16: |self: App|{ + //":sidebar-w" => self.sidebar_w(), + //":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 } + //} +//}); +impl App { + pub fn sequencer ( + jack: &Arc>, pool: MidiPool, editor: MidiEditor, + player: Option, midi_froms: &[PortConnection], midi_tos: &[PortConnection], + ) -> Self { + Self { + edn: include_str!("./sequencer-view.edn").to_string(), + jack: jack.clone(), + pool: Some(pool), + editor: Some(editor), + player: player, + editing: false.into(), + midi_buf: vec![vec![];65536], + color: ItemPalette::random(), + ..Default::default() + } + } + pub fn groovebox ( + jack: &Arc>, pool: MidiPool, editor: MidiEditor, + player: Option, midi_froms: &[PortConnection], midi_tos: &[PortConnection], + sampler: Sampler, audio_froms: &[&[PortConnection]], audio_tos: &[&[PortConnection]], + ) -> Self { + Self { + edn: include_str!("./groovebox-view.edn").to_string(), + sampler: Some(sampler), + ..Self::sequencer( + jack, pool, editor, + player, midi_froms, midi_tos + ) + } + } + pub fn arranger ( + jack: &Arc>, pool: MidiPool, editor: MidiEditor, + midi_froms: &[PortConnection], midi_tos: &[PortConnection], + sampler: Sampler, audio_froms: &[&[PortConnection]], audio_tos: &[&[PortConnection]], + scenes: usize, tracks: usize, track_width: usize, + ) -> Self { + let mut arranger = Self { + edn: include_str!("./arranger-view.edn").to_string(), + ..Self::groovebox( + jack, pool, editor, + None, midi_froms, midi_tos, + sampler, audio_froms, audio_tos + ) + }; + arranger.scenes_add(scenes); + arranger.tracks_add(tracks, track_width, &[], &[]); + arranger + } + fn compact (&self) -> bool { false } fn is_editing (&self) -> bool { self.editing.load(Relaxed) } - fn editor_h (&self) -> usize { 15 } - 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 editor (&self) -> impl Content + '_ { &self.editor } + fn w (&self) -> u16 { self.tracks_sizes(self.is_editing(), self.editor_w()).last().map(|x|x.3 as u16).unwrap_or(0) } + fn pool (&self) -> impl Content + use<'_> { self.pool.as_ref().map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))) } + 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 + } + 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 clip (&self) -> Option>> { + self.scene()?.clips.get(self.selected().track()?)?.clone() + } + fn toggle_loop (&mut self) { + if let Some(clip) = self.clip() { + clip.write().unwrap().toggle_loop() + } + } + //fn randomize_color (&mut self) { + //match self.selected { + //Selection::Mix => { self.color = ItemPalette::random() }, + //Selection::Track(t) => { self.tracks[t].color = ItemPalette::random() }, + //Selection::Scene(s) => { self.scenes[s].color = ItemPalette::random() }, + //Selection::Clip(t, s) => if let Some(clip) = &self.scenes[s].clips[t] { + //clip.write().unwrap().color = ItemPalette::random(); + //} + //} + //} + fn track_add ( + &mut self, + name: Option<&str>, + color: Option + ) -> Usually<&mut Track> { + let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into()); + let track = Track { + width: (name.len() + 2).max(9), + color: color.unwrap_or_else(ItemPalette::random), + player: MidiPlayer::from(self.clock()), + name, + ..Default::default() + }; + self.tracks_mut().push(track); + let len = self.tracks().len(); + let index = len - 1; + for scene in self.scenes_mut().iter_mut() { + while scene.clips.len() < len { + scene.clips.push(None); + } + } + Ok(&mut self.tracks_mut()[index]) + } + 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); + } + Ok(()) + } + fn track_del (&mut self, index: usize) { + self.tracks_mut().remove(index); + for scene in self.scenes_mut().iter_mut() { + scene.clips.remove(index); + } + } + fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut Scene> + { + let scene = Scene { + 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_mut().push(scene); + let index = self.scenes().len() - 1; + Ok(&mut self.scenes_mut()[index]) + } + 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(()) + } + fn scene_cells <'a> (&'a self, editing: bool) -> BoxThunk<'a, TuiOut> { + let tracks = move||self.tracks_sizes(self.is_editing(), self.editor_w()); + let scenes = ||self.scenes_sizes(self.is_editing(), 2, 15); + let selected_track = self.selected().track(); + 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 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(||self.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::new(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 activate (&mut self) -> Usually<()> { + let selected = self.selected().clone(); + match selected { + Selection::Scene(s) => { + let mut clips = vec![]; + for (t, _) in self.tracks().iter().enumerate() { + clips.push(self.scenes()[s].clips[t].clone()); + } + for (t, track) in self.tracks_mut().iter_mut().enumerate() { + if track.player.play_clip.is_some() || clips[t].is_some() { + track.player.enqueue_next(clips[t].as_ref()); + } + } + if self.clock().is_stopped() { + self.clock().play_from(Some(0))?; + } + }, + Selection::Clip(t, s) => { + let clip = self.scenes()[s].clips[t].clone(); + self.tracks_mut()[t].player.enqueue_next(clip.as_ref()); + }, + _ => {} + } + Ok(()) } } -edn_provide!(u16: |self: App|{ - ":sidebar-w" => self.sidebar_w(), - ":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 } +handle!(TuiIn: |self: App, input| Ok(None)); +#[derive(Clone, Debug)] pub enum AppCommand { + Clear, + Clip(ClipCommand), + Clock(ClockCommand), + Color(ItemPalette), + Compact(Option), + Editor(MidiEditCommand), + Enqueue(Option>>), + History(isize), + Pool(PoolCommand), + Sampler(SamplerCommand), + Scene(SceneCommand), + Select(Selection), + StopAll, + Track(TrackCommand), + Zoom(Option), +} +edn_command!(AppCommand: |state: App| { + ("clear" [] Self::Clear) + ("stop-all" [] Self::StopAll) + ("compact" [c: bool ] Self::Compact(c)) + ("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default())) + ("history" [d: isize] Self::History(d.unwrap_or(0))) + ("zoom" [z: usize] Self::Zoom(z)) + ("select" [s: Selection] Self::Select(s.expect("no selection"))) + ("enqueue" [c: Arc>] Self::Enqueue(c)) + ("clock" [a, ..b] Self::Clock(ClockCommand::from_edn(state, &a.to_ref(), b))) + ("track" [a, ..b] Self::Track(TrackCommand::from_edn(state, &a.to_ref(), b))) + ("scene" [a, ..b] Self::Scene(SceneCommand::from_edn(state, &a.to_ref(), b))) + ("clip" [a, ..b] Self::Clip(ClipCommand::from_edn(state, &a.to_ref(), b))) + ("pool" [a, ..b] Self::Pool(PoolCommand::from_edn(state.pool.as_ref().expect("no pool"), &a.to_ref(), b))) + ("editor" [a, ..b] Self::Editor(MidiEditCommand::from_edn(state.editor.as_ref().expect("no editor"), &a.to_ref(), b))) + ("sampler" [a, ..b] Self::Sampler(SamplerCommand::from_edn(state.sampler.as_ref().expect("no sampler"), &a.to_ref(), b))) +}); +command!(|self: AppCommand, state: App|match self { + Self::Clear => { todo!() }, + Self::Zoom(_) => { todo!(); }, + Self::History(delta) => { todo!("undo/redo") }, + Self::Select(s) => { state.selected = s; None }, + Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, + Self::Scene(cmd) => match cmd { + SceneCommand::Add => { state.scene_add(None, None)?; None } + SceneCommand::Del(index) => { state.scene_del(index); None }, + SceneCommand::SetColor(index, color) => { + let old = state.scenes[index].color; + state.scenes[index].color = color; + Some(SceneCommand::SetColor(index, old)) + }, + SceneCommand::Enqueue(scene) => { + for track in 0..state.tracks.len() { + state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); + } + None + }, + _ => None + }.map(Self::Scene), + Self::Track(cmd) => match cmd { + TrackCommand::Add => { state.track_add(None, None)?; None }, + TrackCommand::Del(index) => { state.track_del(index); None }, + TrackCommand::Stop(track) => { state.tracks[track].player.enqueue_next(None); None }, + TrackCommand::SetColor(index, color) => { + let old = state.tracks[index].color; + state.tracks[index].color = color; + Some(TrackCommand::SetColor(index, old)) + }, + _ => None + }.map(Self::Track), + Self::Clip(cmd) => match cmd { + ClipCommand::Get(track, scene) => { todo!() }, + ClipCommand::Put(track, scene, clip) => { + let old = state.scenes[scene].clips[track].clone(); + state.scenes[scene].clips[track] = clip; + Some(ClipCommand::Put(track, scene, old)) + }, + ClipCommand::Enqueue(track, scene) => { + state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); + None + }, + _ => None + }.map(Self::Clip), + Self::Editor(cmd) => + state.editor.as_mut().map(|editor|cmd.delegate(editor, Self::Editor)).transpose()?.flatten(), + Self::Sampler(cmd) => + state.sampler.as_mut().map(|sampler|cmd.delegate(sampler, Self::Sampler)).transpose()?.flatten(), + Self::Enqueue(clip) => + state.player.as_mut().map(|player|{player.enqueue_next(clip.as_ref());None}).flatten(), + 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) => if let Some(pool) = state.pool.as_mut() { + let undo = cmd.clone().delegate(pool, Self::Pool)?; + if let Some(editor) = state.editor.as_mut() { + match cmd { + // autoselect: automatically load selected clip in editor + // autocolor: update color in all places simultaneously + PoolCommand::Select(_) | PoolCommand::Clip(PoolClipCommand::SetColor(_, _)) => + editor.set_clip(pool.clip().as_ref()), + _ => {} + } + }; + undo + } else { + None + }, + Self::Compact(compact) => match compact { + Some(compact) => { + if state.compact != compact { + state.compact = compact; + Some(Self::Compact(Some(!compact))) + } else { + None + } + }, + None => { + state.compact = !state.compact; + Some(Self::Compact(Some(!state.compact))) + } } }); -edn_provide!(bool: |self: App| { _ => return None }); -edn_provide!(usize: |self: App| { _ => return None }); -edn_provide!(isize: |self: App| { _ => return None }); -edn_provide!(Color: |self: App| { _ => return None }); -edn_provide!(Selection: |self: App| { _ => return None }); -edn_provide!(Arc>: |self: App| { _ => return None }); -edn_provide!(Option>>: |self: App| { _ => return None }); -edn_provide!('a: Box + 'a>: |self: App|{ - ":editor" => (&self.editor).boxed(), - ":pool" => self.pool.as_ref().map(|pool|PoolView(self.compact(), pool)).boxed(), - ":sample" => self.view_sample(self.is_editing()).boxed(), - ":sampler" => self.view_sampler(self.is_editing(), &self.editor).boxed(), - ":status" => self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())).boxed(), - ":toolbar" => ClockView::new(true, &self.clock).boxed(), - ":tracks" => self.row(self.w(), 3, self.track_header(), self.track_cells()).boxed(), - ":inputs" => self.row(self.w(), 3, self.input_header(), self.input_cells()).boxed(), - ":outputs" => self.row(self.w(), 3, self.output_header(), self.output_cells()).boxed(), - ":scenes" => self.row(self.w(), self.size.h().saturating_sub(9) as u16, - self.scene_header(), self.scene_cells(self.is_editing())).boxed(), -}); -content!(TuiOut: |self: App| self.size.of(EdnView::from_source(self, self.edn.as_ref()))); -handle!(TuiIn: |self: App, input| Ok(None)); -#[derive(Debug, Default)] struct Meter<'a>(pub &'a str, pub f32); -content!(TuiOut: |self: Meter<'a>| col!( - Field(TuiTheme::g(128).into(), self.0, format!("{:>+9.3}", self.1)), - Fixed::xy(if self.1 >= 0.0 { 13 } - else if self.1 >= -1.0 { 12 } - else if self.1 >= -2.0 { 11 } - else if self.1 >= -3.0 { 10 } - else if self.1 >= -4.0 { 9 } - else if self.1 >= -6.0 { 8 } - else if self.1 >= -9.0 { 7 } - else if self.1 >= -12.0 { 6 } - else if self.1 >= -15.0 { 5 } - else if self.1 >= -20.0 { 4 } - else if self.1 >= -25.0 { 3 } - else if self.1 >= -30.0 { 2 } - else if self.1 >= -40.0 { 1 } - else { 0 }, 1, Tui::bg(if self.1 >= 0.0 { Color::Red } - else if self.1 >= -3.0 { Color::Yellow } - else { Color::Green }, ())))); -#[derive(Debug, Default)] struct Meters<'a>(pub &'a[f32]); -content!(TuiOut: |self: Meters<'a>| col!( - format!("L/{:>+9.3}", self.0[0]), - format!("R/{:>+9.3}", self.0[1]) -)); /// Represents the current user selection in the arranger #[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { /// The whole mix is selected @@ -209,6 +569,25 @@ impl Track { } } } +#[derive(Clone, Debug)] pub enum TrackCommand { + Add, + Del(usize), + Stop(usize), + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), + SetColor(usize, ItemPalette), +} +edn_command!(TrackCommand: |state: App| { + ("add" [] Self::Add) + ("size" [a: usize] Self::SetSize(a.unwrap())) + ("zoom" [a: usize] Self::SetZoom(a.unwrap())) + ("color" [a: usize] Self::SetColor(a.unwrap(), ItemPalette::random())) + ("del" [a: usize] Self::Del(a.unwrap())) + ("stop" [a: usize] Self::Stop(a.unwrap())) + ("swap" [a: usize, b: usize] Self::Swap(a.unwrap(), b.unwrap())) +}); +command!(|self: TrackCommand, state: App|match self { _ => todo!("track command") }); impl HasTracks for App { fn midi_ins (&self) -> &Vec> { &self.midi_ins } fn midi_outs (&self) -> &Vec> { &self.midi_outs } @@ -449,202 +828,6 @@ pub trait HasScenes: HasSelection + HasEditor + Send + Sync { }).into() } } -impl App { - pub fn sequencer ( - jack: &Arc>, pool: MidiPool, editor: MidiEditor, - player: Option, midi_froms: &[PortConnection], midi_tos: &[PortConnection], - ) -> Self { - Self { - edn: include_str!("./sequencer-view.edn").to_string(), - jack: jack.clone(), - pool: Some(pool), - editor: Some(editor), - player: player, - editing: false.into(), - midi_buf: vec![vec![];65536], - color: ItemPalette::random(), - ..Default::default() - } - } - pub fn groovebox ( - jack: &Arc>, pool: MidiPool, editor: MidiEditor, - player: Option, midi_froms: &[PortConnection], midi_tos: &[PortConnection], - sampler: Sampler, audio_froms: &[&[PortConnection]], audio_tos: &[&[PortConnection]], - ) -> Self { - Self { - edn: include_str!("./groovebox-view.edn").to_string(), - sampler: Some(sampler), - ..Self::sequencer( - jack, pool, editor, - player, midi_froms, midi_tos - ) - } - } - pub fn arranger ( - jack: &Arc>, pool: MidiPool, editor: MidiEditor, - midi_froms: &[PortConnection], midi_tos: &[PortConnection], - sampler: Sampler, audio_froms: &[&[PortConnection]], audio_tos: &[&[PortConnection]], - scenes: usize, tracks: usize, track_width: usize, - ) -> Self { - let mut arranger = Self { - edn: include_str!("./arranger-view.edn").to_string(), - ..Self::groovebox( - jack, pool, editor, - None, midi_froms, midi_tos, - sampler, audio_froms, audio_tos - ) - }; - arranger.scenes_add(scenes); - arranger.tracks_add(tracks, track_width, &[], &[]); - arranger - } - fn compact (&self) -> bool { false } - fn editor (&self) -> impl Content + '_ { &self.editor } - fn w (&self) -> u16 { self.tracks_sizes(self.is_editing(), self.editor_w()).last().map(|x|x.3 as u16).unwrap_or(0) } - fn pool (&self) -> impl Content + use<'_> { self.pool.as_ref().map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))) } - 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 is_editing (&self) -> bool { - self.editing.load(Relaxed) - } - 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 - } -} -#[derive(Clone, Debug)] pub enum AppCommand { - Clear, - Clip(ClipCommand), - Clock(ClockCommand), - Color(ItemPalette), - Compact(Option), - Editor(MidiEditCommand), - Enqueue(Option>>), - History(isize), - Pool(PoolCommand), - Sampler(SamplerCommand), - Scene(SceneCommand), - Select(Selection), - StopAll, - Track(TrackCommand), - Zoom(Option), -} -edn_command!(AppCommand: |state: App| { - ("clear" [] Self::Clear) - ("stop-all" [] Self::StopAll) - ("compact" [c: bool ] Self::Compact(c)) - ("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default())) - ("history" [d: isize] Self::History(d.unwrap_or(0))) - ("zoom" [z: usize] Self::Zoom(z)) - ("select" [s: Selection] Self::Select(s.expect("no selection"))) - ("enqueue" [c: Arc>] Self::Enqueue(c)) - ("clock" [a, ..b] Self::Clock(ClockCommand::from_edn(state, &a.to_ref(), b))) - ("track" [a, ..b] Self::Track(TrackCommand::from_edn(state, &a.to_ref(), b))) - ("scene" [a, ..b] Self::Scene(SceneCommand::from_edn(state, &a.to_ref(), b))) - ("clip" [a, ..b] Self::Clip(ClipCommand::from_edn(state, &a.to_ref(), b))) - ("pool" [a, ..b] Self::Pool(PoolCommand::from_edn(state.pool.as_ref().expect("no pool"), &a.to_ref(), b))) - ("editor" [a, ..b] Self::Editor(MidiEditCommand::from_edn(state.editor.as_ref().expect("no editor"), &a.to_ref(), b))) - ("sampler" [a, ..b] Self::Sampler(SamplerCommand::from_edn(state.sampler.as_ref().expect("no sampler"), &a.to_ref(), b))) -}); -command!(|self: AppCommand, state: App|match self { - Self::Clear => { todo!() }, - Self::Zoom(_) => { todo!(); }, - Self::History(delta) => { todo!("undo/redo") }, - Self::Select(s) => { state.selected = s; None }, - Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, - Self::Scene(cmd) => match cmd { - SceneCommand::Add => { state.scene_add(None, None)?; None } - SceneCommand::Del(index) => { state.scene_del(index); None }, - SceneCommand::SetColor(index, color) => { - let old = state.scenes[index].color; - state.scenes[index].color = color; - Some(SceneCommand::SetColor(index, old)) - }, - SceneCommand::Enqueue(scene) => { - for track in 0..state.tracks.len() { - state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); - } - None - }, - _ => None - }.map(Self::Scene), - Self::Track(cmd) => match cmd { - TrackCommand::Add => { state.track_add(None, None)?; None }, - TrackCommand::Del(index) => { state.track_del(index); None }, - TrackCommand::Stop(track) => { state.tracks[track].player.enqueue_next(None); None }, - TrackCommand::SetColor(index, color) => { - let old = state.tracks[index].color; - state.tracks[index].color = color; - Some(TrackCommand::SetColor(index, old)) - }, - _ => None - }.map(Self::Track), - Self::Clip(cmd) => match cmd { - ClipCommand::Get(track, scene) => { todo!() }, - ClipCommand::Put(track, scene, clip) => { - let old = state.scenes[scene].clips[track].clone(); - state.scenes[scene].clips[track] = clip; - Some(ClipCommand::Put(track, scene, old)) - }, - ClipCommand::Enqueue(track, scene) => { - state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); - None - }, - _ => None - }.map(Self::Clip), - Self::Editor(cmd) => - state.editor.as_mut().map(|editor|cmd.delegate(editor, Self::Editor)).transpose()?.flatten(), - Self::Sampler(cmd) => - state.sampler.as_mut().map(|sampler|cmd.delegate(sampler, Self::Sampler)).transpose()?.flatten(), - Self::Enqueue(clip) => - state.player.as_mut().map(|player|{player.enqueue_next(clip.as_ref());None}).flatten(), - 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) => if let Some(pool) = state.pool.as_mut() { - let undo = cmd.clone().delegate(pool, Self::Pool)?; - if let Some(editor) = state.editor.as_mut() { - match cmd { - // autoselect: automatically load selected clip in editor - // autocolor: update color in all places simultaneously - PoolCommand::Select(_) | PoolCommand::Clip(PoolClipCommand::SetColor(_, _)) => - editor.set_clip(pool.clip().as_ref()), - _ => {} - } - }; - undo - } else { - None - }, - Self::Compact(compact) => match compact { - Some(compact) => { - if state.compact != compact { - state.compact = compact; - Some(Self::Compact(Some(!compact))) - } else { - None - } - }, - None => { - state.compact = !state.compact; - Some(Self::Compact(Some(!state.compact))) - } - } -}); #[derive(Clone, Debug)] pub enum ClipCommand { Get(usize, usize), Put(usize, usize, Option>>), @@ -654,45 +837,20 @@ command!(|self: AppCommand, state: App|match self { SetColor(usize, usize, ItemPalette), } edn_command!(ClipCommand: |state: App| { - ("get" [a: usize - ,b: usize] Self::Get(a.unwrap(), b.unwrap())) - - ("put" [a: usize - ,b: usize - ,c: Option>>] Self::Put(a.unwrap(), b.unwrap(), c.unwrap())) - - ("enqueue" [a: usize - ,b: usize] Self::Enqueue(a.unwrap(), b.unwrap())) - - ("edit" [a: Option>>] Self::Edit(a.unwrap())) - - ("loop" [a: usize - ,b: usize - ,c: bool] Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())) - - ("color" [a: usize - ,b: usize] Self::SetColor(a.unwrap(), b.unwrap(), ItemPalette::random())) + ("get" [a: usize ,b: usize] + Self::Get(a.unwrap(), b.unwrap())) + ("put" [a: usize, b: usize, c: Option>>] + Self::Put(a.unwrap(), b.unwrap(), c.unwrap())) + ("enqueue" [a: usize, b: usize] + Self::Enqueue(a.unwrap(), b.unwrap())) + ("edit" [a: Option>>] + Self::Edit(a.unwrap())) + ("loop" [a: usize, b: usize, c: bool] + Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())) + ("color" [a: usize, b: usize] + Self::SetColor(a.unwrap(), b.unwrap(), ItemPalette::random())) }); command!(|self: ClipCommand, state: App|match self { _ => todo!("clip command") }); -#[derive(Clone, Debug)] pub enum TrackCommand { - Add, - Del(usize), - Stop(usize), - Swap(usize, usize), - SetSize(usize), - SetZoom(usize), - SetColor(usize, ItemPalette), -} -edn_command!(TrackCommand: |state: App| { - ("add" [] Self::Add) - ("size" [a: usize] Self::SetSize(a.unwrap())) - ("zoom" [a: usize] Self::SetZoom(a.unwrap())) - ("color" [a: usize] Self::SetColor(a.unwrap(), ItemPalette::random())) - ("del" [a: usize] Self::Del(a.unwrap())) - ("stop" [a: usize] Self::Stop(a.unwrap())) - ("swap" [a: usize, b: usize] Self::Swap(a.unwrap(), b.unwrap())) -}); -command!(|self: TrackCommand, state: App|match self { _ => todo!("track command") }); audio!(|self: App, client, scope|{ // Start profiling cycle let t0 = self.perf.get_t0(); @@ -815,170 +973,30 @@ fn help_tag <'a> (before: &'a str, key: &'a str, after: &'a str) -> impl Content let hi = TuiTheme::orange(); Tui::bold(true, row!(Tui::fg(lo, before), Tui::fg(hi, key), Tui::fg(lo, after))) } -impl Arrangement for T where T: - HasEditor + HasTracks + HasScenes + HasSelection + HasClock + HasJack {} -pub trait Arrangement: HasEditor + HasTracks + HasScenes + HasSelection + HasClock + HasJack { - fn clip (&self) -> Option>> { - self.scene()?.clips.get(self.selected().track()?)?.clone() - } - fn toggle_loop (&mut self) { - if let Some(clip) = self.clip() { - clip.write().unwrap().toggle_loop() - } - } - //fn randomize_color (&mut self) { - //match self.selected { - //Selection::Mix => { self.color = ItemPalette::random() }, - //Selection::Track(t) => { self.tracks[t].color = ItemPalette::random() }, - //Selection::Scene(s) => { self.scenes[s].color = ItemPalette::random() }, - //Selection::Clip(t, s) => if let Some(clip) = &self.scenes[s].clips[t] { - //clip.write().unwrap().color = ItemPalette::random(); - //} - //} - //} - fn track_add ( - &mut self, - name: Option<&str>, - color: Option - ) -> Usually<&mut Track> { - let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into()); - let track = Track { - width: (name.len() + 2).max(9), - color: color.unwrap_or_else(ItemPalette::random), - player: MidiPlayer::from(self.clock()), - name, - ..Default::default() - }; - self.tracks_mut().push(track); - let len = self.tracks().len(); - let index = len - 1; - for scene in self.scenes_mut().iter_mut() { - while scene.clips.len() < len { - scene.clips.push(None); - } - } - Ok(&mut self.tracks_mut()[index]) - } - 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); - } - Ok(()) - } - fn track_del (&mut self, index: usize) { - self.tracks_mut().remove(index); - for scene in self.scenes_mut().iter_mut() { - scene.clips.remove(index); - } - } - fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut Scene> - { - let scene = Scene { - 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_mut().push(scene); - let index = self.scenes().len() - 1; - Ok(&mut self.scenes_mut()[index]) - } - 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(()) - } - fn scene_cells <'a> (&'a self, editing: bool) -> BoxThunk<'a, TuiOut> { - let tracks = move||self.tracks_sizes(self.is_editing(), self.editor_w()); - let scenes = ||self.scenes_sizes(self.is_editing(), 2, 15); - let selected_track = self.selected().track(); - 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 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(||self.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::new(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 activate (&mut self) -> Usually<()> { - let selected = self.selected().clone(); - match selected { - Selection::Scene(s) => { - let mut clips = vec![]; - for (t, _) in self.tracks().iter().enumerate() { - clips.push(self.scenes()[s].clips[t].clone()); - } - for (t, track) in self.tracks_mut().iter_mut().enumerate() { - if track.player.play_clip.is_some() || clips[t].is_some() { - track.player.enqueue_next(clips[t].as_ref()); - } - } - if self.clock().is_stopped() { - self.clock().play_from(Some(0))?; - } - }, - Selection::Clip(t, s) => { - let clip = self.scenes()[s].clips[t].clone(); - self.tracks_mut()[t].player.enqueue_next(clip.as_ref()); - }, - _ => {} - } - Ok(()) - } -} +#[derive(Debug, Default)] struct Meter<'a>(pub &'a str, pub f32); +content!(TuiOut: |self: Meter<'a>| col!( + Field(TuiTheme::g(128).into(), self.0, format!("{:>+9.3}", self.1)), + Fixed::xy(if self.1 >= 0.0 { 13 } + else if self.1 >= -1.0 { 12 } + else if self.1 >= -2.0 { 11 } + else if self.1 >= -3.0 { 10 } + else if self.1 >= -4.0 { 9 } + else if self.1 >= -6.0 { 8 } + else if self.1 >= -9.0 { 7 } + else if self.1 >= -12.0 { 6 } + else if self.1 >= -15.0 { 5 } + else if self.1 >= -20.0 { 4 } + else if self.1 >= -25.0 { 3 } + else if self.1 >= -30.0 { 2 } + else if self.1 >= -40.0 { 1 } + else { 0 }, 1, Tui::bg(if self.1 >= 0.0 { Color::Red } + else if self.1 >= -3.0 { Color::Yellow } + else { Color::Green }, ())))); +#[derive(Debug, Default)] struct Meters<'a>(pub &'a[f32]); +content!(TuiOut: |self: Meters<'a>| col!( + format!("L/{:>+9.3}", self.0[0]), + format!("R/{:>+9.3}", self.0[1]) +)); #[cfg(test)] fn test_tek () { // TODO } diff --git a/tek/src/sequencer-keys.edn b/tek/src/sequencer-keys.edn new file mode 100644 index 00000000..e69de29b