From ed90196a60fcb5a65196dea06b85eae090112929 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 16 Jan 2025 11:59:48 +0100 Subject: [PATCH] wip: merge components --- tek/src/lib.rs | 250 +++++++++++++++++++++---------------------------- 1 file changed, 109 insertions(+), 141 deletions(-) diff --git a/tek/src/lib.rs b/tek/src/lib.rs index 6c5376dd..164bad0c 100644 --- a/tek/src/lib.rs +++ b/tek/src/lib.rs @@ -110,25 +110,11 @@ impl TekCli { jack, self.bpm, &midi_froms, &midi_tos, &audio_froms, &audio_tos, scenes, tracks, track_width, ))?)?, - //TekMode::Sampler => engine.run(&jack.activate_with(|jack|Ok( - //SamplerTui { - //cursor: (0, 0), - //editing: None, - //mode: None, - //note_lo: 36.into(), - //note_pt: 36.into(), - //size: Measure::new(), - //state: Sampler::new(jack, &"sampler", &midi_froms, - //&[&left_froms, &right_froms], - //&[&left_tos, &right_tos])?, - //color, - //} - //))?)?, _ => todo!() }) } } -#[derive(Default, Debug)] pub struct Tek { +#[derive(Default, Debug)] struct Tek { pub jack: Arc>, pub edn: String, pub clock: Clock, @@ -156,7 +142,6 @@ impl TekCli { has_size!(|self: Tek|&self.size); has_clock!(|self: Tek|self.clock); has_clips!(|self: Tek|self.pool.as_ref().expect("no clip pool").clips); -//has_editor!(|self: Tek|self.editor.as_ref().expect("no editor")); has_jack!(|self: Tek|&self.jack); has_sampler!(|self: Tek|{ sampler = self.sampler; @@ -201,22 +186,20 @@ edn_view!(TuiOut: |self: Tek| self.size.of(EdnView::from_source(self, self.edn.a // Provide components: Box + 'a> { ":editor" => (&self.editor).boxed(), - ":pool" => self.pool.as_ref().map(|pool|PoolView(self.compact(), pool)).boxed(), + ":pool" => self.view_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" => self.view_clock().boxed(),//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" => Outer(Style::default().fg(TuiTheme::g(0))).enclose_bg(self.row( - self.w(), - self.size.h().saturating_sub(12) as u16, - self.scene_header(), - self.scene_cells(self.is_editing()) + ":status" => self.view_editor().boxed(), + ":toolbar" => self.view_clock().boxed(), + ":tracks" => self.view_row(self.w(), 3, self.track_header(), self.track_cells()).boxed(), + ":inputs" => self.view_row(self.w(), 3, self.input_header(), self.input_cells()).boxed(), + ":outputs" => self.view_row(self.w(), 3, self.output_header(), self.output_cells()).boxed(), + ":scenes" => Outer(Style::default().fg(TuiTheme::g(0))).enclose_bg(self.view_row( + self.w(), self.size.h().saturating_sub(12) as u16, + self.scene_header(), self.scene_cells() )).boxed() }}); impl Tek { - pub fn new_arranger ( + fn new_arranger ( jack: &Arc>, bpm: Option, midi_froms: &[PortConnection], @@ -235,7 +218,7 @@ impl Tek { arranger.tracks_add(tracks, track_width, &[], &[]); Ok(arranger) } - pub fn new_groovebox ( + fn new_groovebox ( jack: &Arc>, bpm: Option, midi_froms: &[PortConnection], @@ -259,7 +242,7 @@ impl Tek { } Ok(app) } - pub fn new_sequencer ( + fn new_sequencer ( jack: &Arc>, bpm: Option, midi_froms: &[PortConnection], @@ -283,7 +266,7 @@ impl Tek { ..Self::new_clock(jack, bpm)? }) } - pub fn new_clock ( + fn new_clock ( jack: &Arc>, bpm: Option, ) -> Usually { @@ -317,13 +300,86 @@ impl Tek { fn editor (&self) -> impl Content + '_ { &self.editor } fn view_clock (&self) -> impl Content + use<'_> { Outer(Style::default().fg(TuiTheme::g(0))).enclose(row!( - OutputStats::new(self.compact, &self.clock), - " ", - PlayPause { compact: false, playing: self.clock.is_rolling() }, - " ", - BeatStats::new(self.compact, &self.clock), + self.view_engine_stats(), " ", + self.view_play_pause(), " ", + self.view_beat_stats(), )) } + fn view_beat_stats (&self) -> impl Content + use<'_> { + let Self { compact, ref clock, .. } = self; + let now = clock.started.read().unwrap().as_ref().map(|start|clock.global.usec.get() - start.usec.get()); + let beat = ||now.map(|now|clock.timebase.format_beats_1(clock.timebase.usecs_to_pulse(now))) + .unwrap_or("-.-.--".into()); + let time = ||now.map(|now|format!("{:.3}s", now/1000000.)) + .unwrap_or("-.---s".into()); + let bpm = ||format!("{:.3}", clock.timebase.bpm.get()); + Either::new(self.compact, + row!(FieldV(TuiTheme::g(128).into(), "BPM", bpm()), + FieldV(TuiTheme::g(128).into(), "Beat", beat()), + FieldV(TuiTheme::g(128).into(), "Time", time()),), + col!(Bsp::e(Tui::fg(TuiTheme::g(255), bpm()), " BPM"), + Bsp::e("Beat ", Tui::fg(TuiTheme::g(255), beat())), + Bsp::e("Time ", Tui::fg(TuiTheme::g(255), time())))) + } + fn view_engine_stats (&self) -> impl Content + use<'_> { + let Self { compact, ref clock, .. } = self; + let rate = clock.timebase.sr.get(); + let chunk = clock.chunk.load(Relaxed); + let sr = move||format!("{}", if *compact {format!("{:.1}kHz", rate / 1000.)} else {format!("{:.0}Hz", rate)}); + let buffer_size = move||format!("{chunk}"); + let latency = move||format!("{:.1}ms", chunk as f64 / rate * 1000.); + Either::new(self.compact, + row!(FieldV(TuiTheme::g(128).into(), "SR", sr()), + FieldV(TuiTheme::g(128).into(), "Buf", buffer_size()), + FieldV(TuiTheme::g(128).into(), "Lat", latency())), + col!(Bsp::e(Tui::fg(TuiTheme::g(255), format!("{}", sr())), " sample rate"), + Bsp::e(Tui::fg(TuiTheme::g(255), format!("{}", buffer_size())), " sample buffer"), + Bsp::e(Tui::fg(TuiTheme::g(255), format!("{:.3}ms", latency())), " latency"))) + } + fn view_meter <'a> (&'a self, label: &'a str, value: f32) -> impl Content + 'a { + col!( + Field(TuiTheme::g(128).into(), label, format!("{:>+9.3}", value)), + Fixed::xy(if value >= 0.0 { 13 } + else if value >= -1.0 { 12 } + else if value >= -2.0 { 11 } + else if value >= -3.0 { 10 } + else if value >= -4.0 { 9 } + else if value >= -6.0 { 8 } + else if value >= -9.0 { 7 } + else if value >= -12.0 { 6 } + else if value >= -15.0 { 5 } + else if value >= -20.0 { 4 } + else if value >= -25.0 { 3 } + else if value >= -30.0 { 2 } + else if value >= -40.0 { 1 } + else { 0 }, 1, Tui::bg(if value >= 0.0 { Color::Red } + else if value >= -3.0 { Color::Yellow } + else { Color::Green }, ()))) + } + fn view_meters (&self, values: &[f32;2]) -> impl Content + use<'_> { + col!( + format!("L/{:>+9.3}", values[0]), + format!("R/{:>+9.3}", values[1]), + ) + } + fn view_play_pause (&self) -> impl Content + use<'_> { + let playing = self.clock.is_rolling(); + Tui::bg( + if playing{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)}, + Either::new(self.compact, + Thunk::new(move||Fixed::x(9, Either::new(playing, + Tui::fg(Color::Rgb(0, 255, 0), " PLAYING "), + Tui::fg(Color::Rgb(255, 128, 0), " STOPPED ")))), + Thunk::new(move||Fixed::x(5, Either::new(playing, + Tui::fg(Color::Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), + Tui::fg(Color::Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))))) + } + fn view_editor (&self) -> impl Content + use<'_> { + self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())) + } + fn view_pool (&self) -> impl Content + use<'_> { + self.pool.as_ref().map(|pool|PoolView(self.compact(), pool)) + } fn pool (&self) -> impl Content + use<'_> { self.pool.as_ref() .map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))) @@ -340,7 +396,7 @@ impl Tek { let w = if self.is_editing() { 8 } else { w }; w } - fn row <'a> ( + fn view_row <'a> ( &'a self, w: u16, h: u16, a: impl Content + 'a, b: impl Content + 'a ) -> impl Content + 'a { Fixed::y(h, Bsp::e( @@ -438,9 +494,10 @@ impl Tek { } 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); + fn scene_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + let editing = self.is_editing(); + let tracks = move||self.tracks_sizes(editing, self.editor_w()); + let scenes = move||self.scenes_sizes(editing, 2, 15); let selected_track = self.selected().track(); let selected_scene = self.selected().scene(); (move||Align::c(Map::new(tracks, move|(_, track, x1, x2), t| { @@ -649,19 +706,19 @@ command!(|self: TekCommand, app: Tek|match self { } /// Focus identification methods impl Selection { - 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 track (&self) -> Option { + fn is_mix (&self) -> bool { matches!(self, Self::Mix) } + fn is_track (&self) -> bool { matches!(self, Self::Track(_)) } + fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) } + fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) } + fn track (&self) -> Option { use Selection::*; match self { Clip(t, _) => Some(*t), Track(t) => Some(*t), _ => None } } - pub fn scene (&self) -> Option { + fn scene (&self) -> Option { use Selection::*; match self { Clip(_, s) => Some(*s), Scene(s) => Some(*s), _ => None } } - pub fn description (&self, tracks: &[Track], scenes: &[Scene]) -> Arc { + fn description (&self, tracks: &[Track], scenes: &[Scene]) -> Arc { format!("Selected: {}", match self { Self::Mix => "Everything".to_string(), Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)) @@ -686,7 +743,7 @@ pub trait HasSelection { fn selected (&self) -> &Selection; fn selected_mut (&mut self) -> &mut Selection; } -#[derive(Debug, Default)] pub struct Track { +#[derive(Debug, Default)] struct Track { /// Name of track name: Arc, /// Preferred width of track column @@ -859,7 +916,7 @@ pub trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync trait Device: Send + Sync + std::fmt::Debug {} impl Device for Sampler {} impl Device for Plugin {} -#[derive(Debug, Default)] pub struct Scene { +#[derive(Debug, Default)] struct Scene { /// Name of scene name: Arc, /// Clips in scene, one per track @@ -869,14 +926,14 @@ impl Device for Plugin {} } impl Scene { /// Returns the pulse length of the longest clip in the scene - pub fn pulses (&self) -> usize { + 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: &[Track]) -> bool { + fn is_playing (&self, tracks: &[Track]) -> bool { self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() .all(|(track_index, clip)|match clip { Some(c) => tracks @@ -892,7 +949,7 @@ impl Scene { None => true }) } - pub fn clip (&self, index: usize) -> Option<&Arc>> { + fn clip (&self, index: usize) -> Option<&Arc>> { match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } } } @@ -1104,95 +1161,6 @@ fn button <'a> (key: &'a str, label: &'a str) -> impl Content + 'a { Margin::x(1, Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(96), label)), )) } -#[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]) -)); -pub struct ClockView<'a> { pub compact: bool, pub clock: &'a Clock } -content!(TuiOut: |self: ClockView<'a>| Outer(Style::default().fg(TuiTheme::g(0))).enclose(row!( - OutputStats::new(self.compact, self.clock), - " ", - PlayPause { compact: false, playing: self.clock.is_rolling() }, - " ", - BeatStats::new(self.compact, self.clock), -))); -impl<'a> ClockView<'a> { - pub fn new (compact: bool, clock: &'a Clock) -> Self { Self { compact, clock } } -} -pub struct PlayPause { pub compact: bool, pub playing: bool } -content!(TuiOut: |self: PlayPause| Tui::bg( - if self.playing{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)}, - Either::new(self.compact, - Thunk::new(||Fixed::x(9, Either::new(self.playing, - Tui::fg(Color::Rgb(0, 255, 0), " PLAYING "), - Tui::fg(Color::Rgb(255, 128, 0), " STOPPED ")))), - Thunk::new(||Fixed::x(5, Either::new(self.playing, - Tui::fg(Color::Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - Tui::fg(Color::Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))))))); -pub struct BeatStats { compact: bool, bpm: Arc, beat: Arc, time: Arc, } -content!(TuiOut: |self: BeatStats| Either::new(self.compact, - row!(FieldV(TuiTheme::g(128).into(), "BPM", &self.bpm), - FieldV(TuiTheme::g(128).into(), "Beat", &self.beat), - FieldV(TuiTheme::g(128).into(), "Time", &self.time),), - col!(Bsp::e(Tui::fg(TuiTheme::g(255), &self.bpm), " BPM"), - Bsp::e("Beat ", Tui::fg(TuiTheme::g(255), &self.beat)), - Bsp::e("Time ", Tui::fg(TuiTheme::g(255), &self.time))))); -impl BeatStats { - fn new (compact: bool, clock: &Clock) -> Self { - let bpm = format!("{:.3}", clock.timebase.bpm.get()).into(); - let (beat, time) = if let Some(started) = clock.started.read().unwrap().as_ref() { - let now = clock.global.usec.get() - started.usec.get(); - ( - clock.timebase.format_beats_1(clock.timebase.usecs_to_pulse(now)).into(), - format!("{:.3}s", now/1000000.).into() - ) - } else { - ("-.-.--".to_string().into(), "-.---s".to_string().into()) - }; - Self { compact, bpm, beat, time } - } -} -pub struct OutputStats { compact: bool, sample_rate: Arc, buffer_size: Arc, latency: Arc, } -content!(TuiOut: |self: OutputStats| Either::new(self.compact, - row!(FieldV(TuiTheme::g(128).into(), "SR", &self.sample_rate), - FieldV(TuiTheme::g(128).into(), "Buf", &self.buffer_size), - FieldV(TuiTheme::g(128).into(), "Lat", &self.latency)), - col!(Bsp::e(Tui::fg(TuiTheme::g(255), format!("{}", self.sample_rate)), " sample rate"), - Bsp::e(Tui::fg(TuiTheme::g(255), format!("{}", self.buffer_size)), " sample buffer"), - Bsp::e(Tui::fg(TuiTheme::g(255), format!("{:.3}ms", self.latency)), " latency")))); -impl OutputStats { - fn new (compact: bool, clock: &Clock) -> Self { - let rate = clock.timebase.sr.get(); - let chunk = clock.chunk.load(Relaxed); - let sr = if compact {format!("{:.1}kHz", rate / 1000.)} else {format!("{:.0}Hz", rate)}; - Self { - compact, - sample_rate: sr.into(), - buffer_size: format!("{chunk}").into(), - latency: format!("{:.1}ms", chunk as f64 / rate * 1000.).into(), - } - } -} #[cfg(test)] fn test_tek () { use clap::CommandFactory; TekCli::command().debug_assert();