wip: merge components

This commit is contained in:
🪞👃🪞 2025-01-16 11:59:48 +01:00
parent 8e2aed58af
commit ed90196a60

View file

@ -110,25 +110,11 @@ impl TekCli {
jack, self.bpm, &midi_froms, &midi_tos, &audio_froms, &audio_tos, jack, self.bpm, &midi_froms, &midi_tos, &audio_froms, &audio_tos,
scenes, tracks, track_width, 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!() _ => todo!()
}) })
} }
} }
#[derive(Default, Debug)] pub struct Tek { #[derive(Default, Debug)] struct Tek {
pub jack: Arc<RwLock<JackConnection>>, pub jack: Arc<RwLock<JackConnection>>,
pub edn: String, pub edn: String,
pub clock: Clock, pub clock: Clock,
@ -156,7 +142,6 @@ impl TekCli {
has_size!(<TuiOut>|self: Tek|&self.size); has_size!(<TuiOut>|self: Tek|&self.size);
has_clock!(|self: Tek|self.clock); has_clock!(|self: Tek|self.clock);
has_clips!(|self: Tek|self.pool.as_ref().expect("no clip pool").clips); 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_jack!(|self: Tek|&self.jack);
has_sampler!(|self: Tek|{ has_sampler!(|self: Tek|{
sampler = self.sampler; sampler = self.sampler;
@ -201,22 +186,20 @@ edn_view!(TuiOut: |self: Tek| self.size.of(EdnView::from_source(self, self.edn.a
// Provide components: // Provide components:
Box<dyn Render<TuiOut> + 'a> { Box<dyn Render<TuiOut> + 'a> {
":editor" => (&self.editor).boxed(), ":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(), ":sample" => self.view_sample(self.is_editing()).boxed(),
":sampler" => self.view_sampler(self.is_editing(), &self.editor).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(), ":status" => self.view_editor().boxed(),
":toolbar" => self.view_clock().boxed(),//ClockView::new(true, &self.clock).boxed(), ":toolbar" => self.view_clock().boxed(),
":tracks" => self.row(self.w(), 3, self.track_header(), self.track_cells()).boxed(), ":tracks" => self.view_row(self.w(), 3, self.track_header(), self.track_cells()).boxed(),
":inputs" => self.row(self.w(), 3, self.input_header(), self.input_cells()).boxed(), ":inputs" => self.view_row(self.w(), 3, self.input_header(), self.input_cells()).boxed(),
":outputs" => self.row(self.w(), 3, self.output_header(), self.output_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.row( ":scenes" => Outer(Style::default().fg(TuiTheme::g(0))).enclose_bg(self.view_row(
self.w(), self.w(), self.size.h().saturating_sub(12) as u16,
self.size.h().saturating_sub(12) as u16, self.scene_header(), self.scene_cells()
self.scene_header(),
self.scene_cells(self.is_editing())
)).boxed() }}); )).boxed() }});
impl Tek { impl Tek {
pub fn new_arranger ( fn new_arranger (
jack: &Arc<RwLock<JackConnection>>, jack: &Arc<RwLock<JackConnection>>,
bpm: Option<f64>, bpm: Option<f64>,
midi_froms: &[PortConnection], midi_froms: &[PortConnection],
@ -235,7 +218,7 @@ impl Tek {
arranger.tracks_add(tracks, track_width, &[], &[]); arranger.tracks_add(tracks, track_width, &[], &[]);
Ok(arranger) Ok(arranger)
} }
pub fn new_groovebox ( fn new_groovebox (
jack: &Arc<RwLock<JackConnection>>, jack: &Arc<RwLock<JackConnection>>,
bpm: Option<f64>, bpm: Option<f64>,
midi_froms: &[PortConnection], midi_froms: &[PortConnection],
@ -259,7 +242,7 @@ impl Tek {
} }
Ok(app) Ok(app)
} }
pub fn new_sequencer ( fn new_sequencer (
jack: &Arc<RwLock<JackConnection>>, jack: &Arc<RwLock<JackConnection>>,
bpm: Option<f64>, bpm: Option<f64>,
midi_froms: &[PortConnection], midi_froms: &[PortConnection],
@ -283,7 +266,7 @@ impl Tek {
..Self::new_clock(jack, bpm)? ..Self::new_clock(jack, bpm)?
}) })
} }
pub fn new_clock ( fn new_clock (
jack: &Arc<RwLock<JackConnection>>, jack: &Arc<RwLock<JackConnection>>,
bpm: Option<f64>, bpm: Option<f64>,
) -> Usually<Self> { ) -> Usually<Self> {
@ -317,13 +300,86 @@ impl Tek {
fn editor (&self) -> impl Content<TuiOut> + '_ { &self.editor } fn editor (&self) -> impl Content<TuiOut> + '_ { &self.editor }
fn view_clock (&self) -> impl Content<TuiOut> + use<'_> { fn view_clock (&self) -> impl Content<TuiOut> + use<'_> {
Outer(Style::default().fg(TuiTheme::g(0))).enclose(row!( Outer(Style::default().fg(TuiTheme::g(0))).enclose(row!(
OutputStats::new(self.compact, &self.clock), self.view_engine_stats(), " ",
" ", self.view_play_pause(), " ",
PlayPause { compact: false, playing: self.clock.is_rolling() }, self.view_beat_stats(),
" ",
BeatStats::new(self.compact, &self.clock),
)) ))
} }
fn view_beat_stats (&self) -> impl Content<TuiOut> + 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<TuiOut> + 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<TuiOut> + '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<TuiOut> + use<'_> {
col!(
format!("L/{:>+9.3}", values[0]),
format!("R/{:>+9.3}", values[1]),
)
}
fn view_play_pause (&self) -> impl Content<TuiOut> + 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<TuiOut> + use<'_> {
self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status()))
}
fn view_pool (&self) -> impl Content<TuiOut> + use<'_> {
self.pool.as_ref().map(|pool|PoolView(self.compact(), pool))
}
fn pool (&self) -> impl Content<TuiOut> + use<'_> { fn pool (&self) -> impl Content<TuiOut> + use<'_> {
self.pool.as_ref() self.pool.as_ref()
.map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))) .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 }; let w = if self.is_editing() { 8 } else { w };
w w
} }
fn row <'a> ( fn view_row <'a> (
&'a self, w: u16, h: u16, a: impl Content<TuiOut> + 'a, b: impl Content<TuiOut> + 'a &'a self, w: u16, h: u16, a: impl Content<TuiOut> + 'a, b: impl Content<TuiOut> + 'a
) -> impl Content<TuiOut> + 'a { ) -> impl Content<TuiOut> + 'a {
Fixed::y(h, Bsp::e( Fixed::y(h, Bsp::e(
@ -438,9 +494,10 @@ impl Tek {
} }
Ok(()) Ok(())
} }
fn scene_cells <'a> (&'a self, editing: bool) -> BoxThunk<'a, TuiOut> { fn scene_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
let tracks = move||self.tracks_sizes(self.is_editing(), self.editor_w()); let editing = self.is_editing();
let scenes = ||self.scenes_sizes(self.is_editing(), 2, 15); 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_track = self.selected().track();
let selected_scene = self.selected().scene(); let selected_scene = self.selected().scene();
(move||Align::c(Map::new(tracks, move|(_, track, x1, x2), t| { (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 /// Focus identification methods
impl Selection { impl Selection {
pub fn is_mix (&self) -> bool { matches!(self, Self::Mix) } fn is_mix (&self) -> bool { matches!(self, Self::Mix) }
pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) } fn is_track (&self) -> bool { matches!(self, Self::Track(_)) }
pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) } fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) }
pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) } fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) }
pub fn track (&self) -> Option<usize> { fn track (&self) -> Option<usize> {
use Selection::*; use Selection::*;
match self { Clip(t, _) => Some(*t), Track(t) => Some(*t), _ => None } match self { Clip(t, _) => Some(*t), Track(t) => Some(*t), _ => None }
} }
pub fn scene (&self) -> Option<usize> { fn scene (&self) -> Option<usize> {
use Selection::*; use Selection::*;
match self { Clip(_, s) => Some(*s), Scene(s) => Some(*s), _ => None } match self { Clip(_, s) => Some(*s), Scene(s) => Some(*s), _ => None }
} }
pub fn description (&self, tracks: &[Track], scenes: &[Scene]) -> Arc<str> { fn description (&self, tracks: &[Track], scenes: &[Scene]) -> Arc<str> {
format!("Selected: {}", match self { format!("Selected: {}", match self {
Self::Mix => "Everything".to_string(), Self::Mix => "Everything".to_string(),
Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)) 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 (&self) -> &Selection;
fn selected_mut (&mut self) -> &mut Selection; fn selected_mut (&mut self) -> &mut Selection;
} }
#[derive(Debug, Default)] pub struct Track { #[derive(Debug, Default)] struct Track {
/// Name of track /// Name of track
name: Arc<str>, name: Arc<str>,
/// Preferred width of track column /// 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 {} trait Device: Send + Sync + std::fmt::Debug {}
impl Device for Sampler {} impl Device for Sampler {}
impl Device for Plugin {} impl Device for Plugin {}
#[derive(Debug, Default)] pub struct Scene { #[derive(Debug, Default)] struct Scene {
/// Name of scene /// Name of scene
name: Arc<str>, name: Arc<str>,
/// Clips in scene, one per track /// Clips in scene, one per track
@ -869,14 +926,14 @@ impl Device for Plugin {}
} }
impl Scene { impl Scene {
/// Returns the pulse length of the longest clip in the 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|{ self.clips.iter().fold(0, |a, p|{
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
}) })
} }
/// Returns true if all clips in the scene are /// Returns true if all clips in the scene are
/// currently playing on the given collection of tracks. /// 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() self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
.all(|(track_index, clip)|match clip { .all(|(track_index, clip)|match clip {
Some(c) => tracks Some(c) => tracks
@ -892,7 +949,7 @@ impl Scene {
None => true None => true
}) })
} }
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> { fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } 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<TuiOut> + 'a {
Margin::x(1, Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(96), label)), 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<str>, beat: Arc<str>, time: Arc<str>, }
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<str>, buffer_size: Arc<str>, latency: Arc<str>, }
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 () { #[cfg(test)] fn test_tek () {
use clap::CommandFactory; use clap::CommandFactory;
TekCli::command().debug_assert(); TekCli::command().debug_assert();