perf counter for render

This commit is contained in:
🪞👃🪞 2025-01-14 16:45:58 +01:00
parent c9677c87d8
commit 1b7f0e0b93
17 changed files with 331 additions and 358 deletions

View file

@ -1821,3 +1821,29 @@ from_edn!("mixer/track" => |jack: &Arc<RwLock<JackConnection>>, args| -> MixerTr
////})
////}
//}
// TODO:
//keymap!(TRANSPORT_BPM_KEYS = |state: Clock, input: Event| ClockCommand {
//key(Char(',')) => SetBpm(state.bpm().get() - 1.0),
//key(Char('.')) => SetBpm(state.bpm().get() + 1.0),
//key(Char('<')) => SetBpm(state.bpm().get() - 0.001),
//key(Char('>')) => SetBpm(state.bpm().get() + 0.001),
//});
//keymap!(TRANSPORT_QUANT_KEYS = |state: Clock, input: Event| ClockCommand {
//key(Char(',')) => SetQuant(state.quant.prev()),
//key(Char('.')) => SetQuant(state.quant.next()),
//key(Char('<')) => SetQuant(state.quant.prev()),
//key(Char('>')) => SetQuant(state.quant.next()),
//});
//keymap!(TRANSPORT_SYNC_KEYS = |sync: Clock, input: Event | ClockCommand {
//key(Char(',')) => SetSync(state.sync.prev()),
//key(Char('.')) => SetSync(state.sync.next()),
//key(Char('<')) => SetSync(state.sync.prev()),
//key(Char('>')) => SetSync(state.sync.next()),
//});
//keymap!(TRANSPORT_SEEK_KEYS = |state: Clock, input: Event| ClockCommand {
//key(Char(',')) => todo!("transport seek bar"),
//key(Char('.')) => todo!("transport seek bar"),
//key(Char('<')) => todo!("transport seek beat"),
//key(Char('>')) => todo!("transport seek beat"),
//});

4
Cargo.lock generated
View file

@ -1507,8 +1507,9 @@ version = "0.2.0"
dependencies = [
"atomic_float",
"quanta",
"tek_edn",
"tek_input",
"tek_jack",
"tek_tui",
]
[[package]]
@ -1523,6 +1524,7 @@ dependencies = [
"tek_edn",
"tek_input",
"tek_output",
"tek_time",
]
[[package]]

View file

@ -1,10 +1,7 @@
use std::sync::{Arc, RwLock};
use tek::*;
#[allow(unused_imports)] use clap::{self, Parser, Subcommand, ValueEnum};
#[derive(Debug, Parser)]
use clap::{self, Parser, Subcommand};
#[command(version, about, long_about = None)]
pub struct TekCli {
#[derive(Debug, Parser)] pub struct TekCli {
/// Which app to initialize
#[command(subcommand)] mode: TekMode,
/// Name of JACK client
@ -34,9 +31,7 @@ pub struct TekCli {
/// Audio ins to connect from right output
#[arg(short='R', long)] right_to: Vec<String>,
}
#[derive(Debug, Clone, Subcommand)]
pub enum TekMode {
#[derive(Debug, Clone, Subcommand)] pub enum TekMode {
/// A standalone transport clock.
Clock,
/// A MIDI sequencer.
@ -61,7 +56,6 @@ pub enum TekMode {
/// TODO: An audio plugin host
Plugin,
}
/// Application entrypoint.
pub fn main () -> Usually<()> {
let cli = TekCli::parse();
@ -78,114 +72,38 @@ pub fn main () -> Usually<()> {
let right_tos = PortConnection::collect(&cli.right_to, empty, empty);
let audio_froms = &[left_froms.as_slice(), right_froms.as_slice()];
let audio_tos = &[left_tos.as_slice(), right_tos.as_slice() ];
let default_clip = ||Arc::new(RwLock::new(MidiClip::new(
"Clip", true, 384usize, None, Some(ItemColor::random().into()))));
let default_player = |jack: &Arc<RwLock<JackConnection>>, clip: Option<&Arc<RwLock<MidiClip>>>|
MidiPlayer::new(&jack, name, clip, &midi_froms, &midi_tos);
let default_sampler = |jack: &Arc<RwLock<JackConnection>>|
Sampler::new(jack, &"sampler", &midi_froms,
&[&left_froms, &right_froms], &[&left_tos, &right_tos]);
let default_bpm = |clock: Clock|{
if let Some(bpm) = cli.bpm {
clock.timebase.bpm.set(bpm);
}
clock
};
let default_clock = |jack: &Arc<RwLock<JackConnection>>|default_bpm(Clock::from(jack));
// TODO: enable sync master/follow
//let sync_clock = |jack: &Arc<RwLock<JackConnection>>, app|{
//if cli.sync_lead {
//jack.read().unwrap().client().register_timebase_callback(false, |mut state|{
//app.clock().playhead.update_from_sample(state.position.frame() as f64);
//state.position.bbt = Some(app.clock().bbt());
//state.position
//})
//} else if cli.sync_follow {
//jack.read().unwrap().client().register_timebase_callback(false, |state|{
//app.clock().playhead.update_from_sample(state.position.frame() as f64);
//state.position
//})
//} else {
//Ok(())
//}
//};
Ok(match cli.mode {
TekMode::Clock => engine.run(&jack.activate_with(|jack|Ok(ClockTui {
jack: jack.clone(), clock: default_clock(jack),
}))?)?,
TekMode::Sampler => engine.run(&jack.activate_with(|jack|Ok(
SamplerTui {
cursor: (0, 0),
editing: None,
mode: None,
mode: None,
note_lo: 36.into(),
note_pt: 36.into(),
state: default_sampler(jack)?,
size: Measure::new(),
state: Sampler::new(jack, &"sampler", &midi_froms,
&[&left_froms, &right_froms],
&[&left_tos, &right_tos])?,
color,
size: Measure::new(),
}
))?)?,
TekMode::Sequencer => engine.run(&jack.activate_with(|jack|Ok({
let clip = default_clip();
let mut player = default_player(jack, Some(&clip))?;
player.clock = default_bpm(player.clock);
App::sequencer(
jack, (&clip).into(), (&clip).into(),
Some(player), &midi_froms, &midi_tos,
)
}))?)?,
TekMode::Groovebox => engine.run(&jack.activate_with(|jack|Ok({
let clip = default_clip();
let mut player = default_player(jack, Some(&clip))?;
player.clock = default_bpm(player.clock);
let sampler = default_sampler(jack)?;
jack.connect_ports(&player.midi_outs[0].port, &sampler.midi_in.as_ref().unwrap().port)?;
App::groovebox(
jack, (&clip).into(), (&clip).into(),
Some(player), &midi_froms, &midi_tos,
sampler, audio_froms, audio_tos,
)
}))?)?,
TekMode::Arranger { scenes, tracks, track_width, .. } => engine.run(&jack.activate_with(|jack|Ok({
App::arranger(
jack,
MidiPool::default(),
MidiEditor::default(), &midi_froms, &midi_tos,
default_sampler(jack)?, audio_froms, audio_tos,
scenes, tracks, track_width
)
}))?)?,
//let clock = default_clock(jack);
//let mut app = Arranger {
//jack: jack.clone(),
//midi_ins: vec![JackPort::<MidiIn>::new(jack, format!("M/{name}"), &midi_froms)?,],
//midi_outs: vec![JackPort::<MidiOut>::new(jack, format!("{name}/M"), &midi_tos)?, ],
//clock,
//pool: MidiPool::default(),//(&clip).into(),
//editor: MidiEditor::default(),//(&clip).into(),
//selected: ArrangerSelection::Clip(0, 0),
//scenes: vec![],
//tracks: vec![],
//splits: [12, 20],
//midi_buf: vec![vec![];65536],
//note_buf: vec![],
//compact: false,
//editing: true.into(),
//color,
//perf,
//size,
//};
//app.tracks_add(tracks, track_width, &midi_froms, &midi_tos)?;
//app.scenes_add(scenes)?;
//app
//}))?)?,
TekMode::Clock =>
engine.run(&jack.activate_with(|jack|App::clock(
jack, cli.bpm))?)?,
TekMode::Sequencer =>
engine.run(&jack.activate_with(|jack|App::sequencer(
jack, cli.bpm, &midi_froms, &midi_tos))?)?,
TekMode::Groovebox =>
engine.run(&jack.activate_with(|jack|App::groovebox(
jack, cli.bpm, &midi_froms, &midi_tos, &audio_froms, &audio_tos))?)?,
TekMode::Arranger { scenes, tracks, track_width, .. } =>
engine.run(&jack.activate_with(|jack|App::arranger(
jack, cli.bpm, &midi_froms, &midi_tos, &audio_froms, &audio_tos,
scenes, tracks, track_width,
))?)?,
_ => todo!()
})
}
#[test] fn verify_cli () {
use clap::CommandFactory;
TekCli::command().debug_assert();

View file

@ -12,10 +12,7 @@ use EdnItem::*;
$(
impl<'a> EdnProvide<'a, $type> for $App {
fn get <S: AsRef<str>> (&'a $self, edn: &'a EdnItem<S>) -> Option<$type> {
Some(match edn.to_ref() {
$(EdnItem::Sym($sym) => $value,)*
_ => return None
})
Some(match edn.to_ref() { $(EdnItem::Sym($sym) => $value,)* _ => return None })
}
}
)*

View file

@ -20,3 +20,13 @@ pub trait HasSampler {
))
}
}
#[macro_export] macro_rules! has_sampler {
(|$self:ident:$Struct:ty| { sampler = $e0:expr; index = $e1:expr; }) => {
impl HasSampler for $Struct {
fn sampler (&$self) -> &Option<Sampler> { &$e0 }
fn sampler_mut (&mut $self) -> &mut Option<Sampler> { &mut $e0 }
fn sample_index (&$self) -> usize { $e1 }
}
}
}

View file

@ -5,29 +5,24 @@
#![feature(if_let_guard)]
#![feature(impl_trait_in_assoc_type)]
#![feature(type_alias_impl_trait)]
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::{self, *}}};
pub(crate) use std::error::Error;
/// Standard result type.
pub type Usually<T> = std::result::Result<T, Box<dyn Error>>;
/// Standard optional result type.
pub type Perhaps<T> = std::result::Result<Option<T>, Box<dyn Error>>;
pub use ::tek_tui::{
*, tek_edn::*, tek_input::*, tek_output::*,
crossterm, crossterm::event::{
Event, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers,
KeyCode::{self, *},
},
ratatui, ratatui::{
prelude::{Color, Style, Stylize, Buffer, Modifier},
buffer::Cell,
}
};
pub use ::tek_time::{self, *};
pub use ::tek_jack::{self, *, jack::{*, contrib::*}};
pub use ::tek_midi::{self, *, midly::{*, num::*, live::*}};
pub use ::tek_sampler::{self, *};
pub use ::tek_plugin::{self, *};
pub(crate) use std::error::Error;
pub(crate) use std::sync::atomic::{AtomicBool, Ordering::{self, *}};
pub(crate) use std::sync::{Arc, RwLock};
pub use ::tek_tui::{
*, tek_edn::*, tek_input::*, tek_output::*,
ratatui, ratatui::{prelude::{Color, Style, Stylize, Buffer, Modifier}, buffer::Cell},
crossterm, crossterm::event::{
Event, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, KeyCode::{self, *},
},
};
#[derive(Default, Debug)] pub struct App {
pub jack: Arc<RwLock<JackConnection>>,
pub edn: String,
@ -57,11 +52,10 @@ has_clock!(|self: App|&self.clock);
has_clips!(|self: App|self.pool.as_ref().expect("no clip pool").clips);
//has_editor!(|self: App|self.editor.as_ref().expect("no editor"));
has_jack!(|self: App|&self.jack);
impl HasSampler for App {
fn sampler (&self) -> &Option<Sampler> { &self.sampler }
fn sampler_mut (&mut self) -> &mut Option<Sampler> { &mut self.sampler }
fn sample_index (&self) -> usize { self.editor.as_ref().map(|e|e.note_point()).unwrap_or(0) }
}
has_sampler!(|self: App|{
sampler = self.sampler;
index = self.editor.as_ref().map(|e|e.note_point()).unwrap_or(0);
});
has_editor!(|self: App|{
editor = self.editor;
editor_w = {
@ -105,53 +99,101 @@ edn_view!(TuiOut: |self: App| self.size.of(EdnView::from_source(self, self.edn.a
":scenes" => self.row(self.w(), self.size.h().saturating_sub(9) as u16,
self.scene_header(), self.scene_cells(self.is_editing())).boxed() }});
impl App {
pub fn clock (
jack: &Arc<RwLock<JackConnection>>,
bpm: Option<f64>,
) -> Usually<Self> {
// TODO: enable sync master/follow
//let sync_clock = |jack: &Arc<RwLock<JackConnection>>, app|{
//if cli.sync_lead {
//jack.read().unwrap().client().register_timebase_callback(false, |mut state|{
//app.clock().playhead.update_from_sample(state.position.frame() as f64);
//state.position.bbt = Some(app.clock().bbt());
//state.position
//})
//} else if cli.sync_follow {
//jack.read().unwrap().client().register_timebase_callback(false, |state|{
//app.clock().playhead.update_from_sample(state.position.frame() as f64);
//state.position
//})
//} else {
//Ok(())
//}
//};
Ok(Self {
edn: include_str!("./transport-view.edn").to_string(),
jack: jack.clone(),
color: ItemPalette::random(),
clock: Clock::new(jack, bpm),
..Default::default()
})
}
pub fn sequencer (
jack: &Arc<RwLock<JackConnection>>, pool: MidiPool, editor: MidiEditor,
player: Option<MidiPlayer>, midi_froms: &[PortConnection], midi_tos: &[PortConnection],
) -> Self {
Self {
jack: &Arc<RwLock<JackConnection>>,
bpm: Option<f64>,
midi_froms: &[PortConnection],
midi_tos: &[PortConnection],
) -> Usually<Self> {
let clip = MidiClip::new("Clip", true, 384usize, None, Some(ItemColor::random().into()));
let clip = Arc::new(RwLock::new(clip));
Ok(Self {
edn: include_str!("./sequencer-view.edn").to_string(),
jack: jack.clone(),
pool: Some(pool),
editor: Some(editor),
player: player,
pool: Some((&clip).into()),
editor: Some((&clip).into()),
editing: false.into(),
midi_buf: vec![vec![];65536],
color: ItemPalette::random(),
..Default::default()
}
player: Some(MidiPlayer::new(
&jack,
"sequencer",
Some(&clip),
&midi_froms,
&midi_tos
)?),
..Self::clock(jack, bpm)?
})
}
pub fn groovebox (
jack: &Arc<RwLock<JackConnection>>, pool: MidiPool, editor: MidiEditor,
player: Option<MidiPlayer>, 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
)
jack: &Arc<RwLock<JackConnection>>,
bpm: Option<f64>,
midi_froms: &[PortConnection],
midi_tos: &[PortConnection],
audio_froms: &[&[PortConnection];2],
audio_tos: &[&[PortConnection];2],
) -> Usually<Self> {
let app = Self {
edn: include_str!("./groovebox-view.edn").to_string(),
sampler: Some(Sampler::new(
jack,
&"sampler",
midi_froms,
audio_froms,
audio_tos
)?),
..Self::sequencer(jack, bpm, midi_froms, midi_tos)?
};
if let Some(sampler) = app.sampler.as_ref().unwrap().midi_in.as_ref() {
jack.connect_ports(&app.player.as_ref().unwrap().midi_outs[0].port, &sampler.port)?;
}
Ok(app)
}
pub fn arranger (
jack: &Arc<RwLock<JackConnection>>, 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 {
jack: &Arc<RwLock<JackConnection>>,
bpm: Option<f64>,
midi_froms: &[PortConnection],
midi_tos: &[PortConnection],
audio_froms: &[&[PortConnection];2],
audio_tos: &[&[PortConnection];2],
scenes: usize,
tracks: usize,
track_width: usize,
) -> Usually<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
)
..Self::groovebox(jack, bpm, midi_froms, midi_tos, audio_froms, audio_tos)?
};
arranger.scenes_add(scenes);
arranger.tracks_add(tracks, track_width, &[], &[]);
arranger
Ok(arranger)
}
fn compact (&self) -> bool { false }
fn is_editing (&self) -> bool { self.editing.load(Relaxed) }
@ -351,20 +393,20 @@ handle!(TuiIn: |self: App, input| Ok(None));
Zoom(Option<usize>),
}
edn_command!(AppCommand: |state: App| {
("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<RwLock<MidiClip>>] 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)))
("clock" [a, ..b] Self::Clock(ClockCommand::from_edn(state, &a.to_ref(), b)))
("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default()))
("compact" [c: bool ] Self::Compact(c))
("editor" [a, ..b] Self::Editor(MidiEditCommand::from_edn(state.editor.as_ref().expect("no editor"), &a.to_ref(), b)))
("enqueue" [c: Arc<RwLock<MidiClip>>] Self::Enqueue(c))
("history" [d: isize] Self::History(d.unwrap_or(0)))
("pool" [a, ..b] Self::Pool(PoolCommand::from_edn(state.pool.as_ref().expect("no pool"), &a.to_ref(), b)))
("sampler" [a, ..b] Self::Sampler(SamplerCommand::from_edn(state.sampler.as_ref().expect("no sampler"), &a.to_ref(), b)))
("scene" [a, ..b] Self::Scene(SceneCommand::from_edn(state, &a.to_ref(), b)))
("select" [s: Selection] Self::Select(s.expect("no selection")))
("stop-all" [] Self::StopAll)
("track" [a, ..b] Self::Track(TrackCommand::from_edn(state, &a.to_ref(), b)))
("zoom" [z: usize] Self::Zoom(z))
});
command!(|self: AppCommand, state: App|match self {
Self::Zoom(_) => { todo!(); },
@ -815,9 +857,7 @@ audio!(|self: App, 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
}
self.clock().update_from_scope(scope).unwrap();
// Collect MIDI input (TODO preallocate)
let midi_in = self.midi_ins.iter()
.map(|port|port.port.iter(scope)
@ -957,6 +997,82 @@ content!(TuiOut: |self: Meters<'a>| col!(
format!("L/{:>+9.3}", self.0[0]),
format!("R/{:>+9.3}", self.0[1])
));
/// Transport clock app.
pub struct ClockTui { pub jack: Arc<RwLock<JackConnection>>, pub clock: Clock, }
handle!(TuiIn: |self: ClockTui, input|ClockCommand::execute_with_state(self, input.event()));
keymap!(TRANSPORT_KEYS = |state: ClockTui, input: Event| ClockCommand {
key(Char(' ')) =>
if state.clock().is_stopped() { ClockCommand::Play(None) } else { ClockCommand::Pause(None) },
shift(key(Char(' '))) =>
if state.clock().is_stopped() { ClockCommand::Play(Some(0)) } else { ClockCommand::Pause(Some(0)) }
});
has_clock!(|self: ClockTui|&self.clock);
content!(TuiOut:|self: ClockTui|ClockView { compact: false, clock: &self.clock });
pub struct ClockView<'a> { pub compact: bool, pub clock: &'a Clock }
content!(TuiOut: |self: ClockView<'a>| Outer(Style::default().fg(TuiTheme::g(255))).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 () {
// TODO
}

View file

View file

View file

@ -4,8 +4,9 @@ edition = "2021"
version = "0.2.0"
[dependencies]
tek_tui = { path = "../tui" }
tek_jack = { path = "../jack" }
tek_jack = { path = "../jack" }
tek_edn = { path = "../edn" }
tek_input = { path = "../input" }
atomic_float = "1.0.0"
quanta = "0.12.3"
#jack = { path = "../rust-jack" }

View file

@ -1,10 +1,7 @@
use crate::*;
use EdnItem::*;
pub trait HasClock: Send + Sync {
fn clock (&self) -> &Clock;
}
#[macro_export] macro_rules! has_clock {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasClock for $Struct $(<$($L),*$($T),*>)? {
@ -12,17 +9,6 @@ pub trait HasClock: Send + Sync {
}
}
}
/// Hosts the JACK callback for updating the temporal pointer and playback status.
pub struct ClockAudio<'a, T: HasClock>(pub &'a mut T);
impl<T: HasClock> Audio for ClockAudio<'_, T> {
#[inline] fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
self.0.clock().update_from_scope(scope).unwrap();
Control::Continue
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ClockCommand {
Play(Option<u32>),
@ -77,25 +63,25 @@ pub struct Clock {
/// Size of buffer in samples
pub chunk: Arc<AtomicUsize>,
}
from!(|jack: &Arc<RwLock<JackConnection>>| Clock = {
let jack = jack.read().unwrap();
let chunk = jack.client().buffer_size();
let transport = jack.client().transport();
let timebase = Arc::new(Timebase::default());
Self {
quant: Arc::new(24.into()),
sync: Arc::new(384.into()),
transport: Arc::new(Some(transport)),
chunk: Arc::new((chunk as usize).into()),
global: Arc::new(Moment::zero(&timebase)),
playhead: Arc::new(Moment::zero(&timebase)),
offset: Arc::new(Moment::zero(&timebase)),
started: RwLock::new(None).into(),
timebase,
impl From<&Arc<RwLock<JackConnection>>> for Clock {
fn from (jack: &Arc<RwLock<JackConnection>>) -> Self {
let jack = jack.read().unwrap();
let chunk = jack.client().buffer_size();
let transport = jack.client().transport();
let timebase = Arc::new(Timebase::default());
Self {
quant: Arc::new(24.into()),
sync: Arc::new(384.into()),
transport: Arc::new(Some(transport)),
chunk: Arc::new((chunk as usize).into()),
global: Arc::new(Moment::zero(&timebase)),
playhead: Arc::new(Moment::zero(&timebase)),
offset: Arc::new(Moment::zero(&timebase)),
started: RwLock::new(None).into(),
timebase,
}
}
});
}
impl std::fmt::Debug for Clock {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("Clock")
@ -109,8 +95,14 @@ impl std::fmt::Debug for Clock {
.finish()
}
}
impl Clock {
pub fn new (jack: &Arc<RwLock<JackConnection>>, bpm: Option<f64>) -> Self {
let clock = Self::from(jack);
if let Some(bpm) = bpm {
clock.timebase.bpm.set(bpm);
}
clock
}
pub fn timebase (&self) -> &Arc<Timebase> {
&self.timebase
}

View file

@ -1,106 +0,0 @@
use crate::*;
use KeyCode::*;
use ClockCommand::{Play, Pause};
/// Transport clock app.
pub struct ClockTui { pub jack: Arc<RwLock<JackConnection>>, pub clock: Clock, }
handle!(TuiIn: |self: ClockTui, input|ClockCommand::execute_with_state(self, input.event()));
keymap!(TRANSPORT_KEYS = |state: ClockTui, input: Event| ClockCommand {
key(Char(' ')) =>
if state.clock().is_stopped() { Play(None) } else { Pause(None) },
shift(key(Char(' '))) =>
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
});
has_clock!(|self: ClockTui|&self.clock);
audio!(|self: ClockTui, client, scope|ClockAudio(self).process(client, scope));
content!(TuiOut:|self: ClockTui|ClockView { compact: false, clock: &self.clock });
pub struct ClockView<'a> { pub compact: bool, pub clock: &'a Clock }
content!(TuiOut: |self: ClockView<'a>| Outer(Style::default().fg(TuiTheme::g(255))).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(),
}
}
}
// TODO:
//keymap!(TRANSPORT_BPM_KEYS = |state: Clock, input: Event| ClockCommand {
//key(Char(',')) => SetBpm(state.bpm().get() - 1.0),
//key(Char('.')) => SetBpm(state.bpm().get() + 1.0),
//key(Char('<')) => SetBpm(state.bpm().get() - 0.001),
//key(Char('>')) => SetBpm(state.bpm().get() + 0.001),
//});
//keymap!(TRANSPORT_QUANT_KEYS = |state: Clock, input: Event| ClockCommand {
//key(Char(',')) => SetQuant(state.quant.prev()),
//key(Char('.')) => SetQuant(state.quant.next()),
//key(Char('<')) => SetQuant(state.quant.prev()),
//key(Char('>')) => SetQuant(state.quant.next()),
//});
//keymap!(TRANSPORT_SYNC_KEYS = |sync: Clock, input: Event | ClockCommand {
//key(Char(',')) => SetSync(state.sync.prev()),
//key(Char('.')) => SetSync(state.sync.next()),
//key(Char('<')) => SetSync(state.sync.prev()),
//key(Char('>')) => SetSync(state.sync.next()),
//});
//keymap!(TRANSPORT_SEEK_KEYS = |state: Clock, input: Event| ClockCommand {
//key(Char(',')) => todo!("transport seek bar"),
//key(Char('.')) => todo!("transport seek bar"),
//key(Char('<')) => todo!("transport seek beat"),
//key(Char('>')) => todo!("transport seek beat"),
//});

View file

@ -1,5 +1,4 @@
mod clock; pub use self::clock::*;
mod clock_tui; pub use self::clock_tui::*;
mod microsecond; pub use self::microsecond::*;
mod moment; pub use self::moment::*;
mod note_duration; pub use self::note_duration::*;
@ -11,17 +10,47 @@ mod timebase; pub use self::timebase::*;
mod unit; pub use self::unit::*;
pub(crate) use ::tek_jack::{*, jack::{*, contrib::*}};
pub(crate) use std::sync::{Arc, Mutex, RwLock, atomic::{AtomicUsize, Ordering::*}};
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}};
pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem};
pub use ::atomic_float; pub(crate) use atomic_float::*;
pub(crate) use ::tek_tui::{
*,
tek_output::*,
tek_input::*,
tek_edn::*,
ratatui::prelude::*,
crossterm::event::*,
};
pub(crate) use ::tek_edn::*;
pub(crate) use ::tek_input::*;
/// Standard result type.
pub(crate) type Usually<T> = Result<T, Box<dyn std::error::Error>>;
/// Standard optional result type.
pub(crate) type Perhaps<T> = Result<Option<T>, Box<dyn std::error::Error>>;
pub trait Gettable<T> {
/// Returns current value
fn get (&self) -> T;
}
pub trait Mutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&mut self, value: T) -> T;
}
pub trait InteriorMutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&self, value: T) -> T;
}
impl Gettable<bool> for AtomicBool {
fn get (&self) -> bool { self.load(Relaxed) }
}
impl InteriorMutable<bool> for AtomicBool {
fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) }
}
impl Gettable<usize> for AtomicUsize {
fn get (&self) -> usize { self.load(Relaxed) }
}
impl InteriorMutable<usize> for AtomicUsize {
fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) }
}
#[cfg(test)] #[test] fn test_time () -> Usually<()> {
// TODO!

View file

@ -34,6 +34,17 @@ impl PerfModel {
None
}
}
pub fn get_t1 (&self, t0: Option<u64>) -> Option<std::time::Duration> {
if let Some(t0) = t0 {
if self.enabled {
Some(self.clock.delta(t0, self.clock.raw()))
} else {
None
}
} else {
None
}
}
pub fn update (&self, t0: Option<u64>, scope: &ProcessScope) {
if let Some(t0) = t0 {
let t1 = self.clock.raw();

View file

@ -13,3 +13,4 @@ better-panic = "0.3.0"
tek_edn = { path = "../edn" }
tek_input = { path = "../input" }
tek_output = { path = "../output" }
tek_time = { path = "../time" }

View file

@ -7,6 +7,9 @@ pub(crate) use tek_output::*;
pub use ::tek_edn;
pub(crate) use ::tek_edn::*;
pub use ::tek_time;
pub(crate) use ::tek_time::*;
mod tui_engine; pub use self::tui_engine::*;
mod tui_content; pub use self::tui_content::*;
mod tui_input; pub use self::tui_input::*;
@ -39,37 +42,6 @@ pub(crate) use std::ffi::OsString;
};
}
pub trait Gettable<T> {
/// Returns current value
fn get (&self) -> T;
}
pub trait Mutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&mut self, value: T) -> T;
}
pub trait InteriorMutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&self, value: T) -> T;
}
impl Gettable<bool> for AtomicBool {
fn get (&self) -> bool { self.load(Relaxed) }
}
impl InteriorMutable<bool> for AtomicBool {
fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) }
}
impl Gettable<usize> for AtomicUsize {
fn get (&self) -> usize { self.load(Relaxed) }
}
impl InteriorMutable<usize> for AtomicUsize {
fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) }
}
pub use ::better_panic; pub(crate) use better_panic::{Settings, Verbosity};
pub use ::palette; pub(crate) use ::palette::{*, convert::*, okhsl::*};
@ -89,7 +61,6 @@ pub(crate) use ratatui::{
buffer::Cell
};
#[cfg(test)] #[test] fn test_tui_engine () -> Usually<()> {
use crate::*;
use std::sync::{Arc, RwLock};

View file

@ -5,6 +5,7 @@ pub struct Tui {
pub backend: CrosstermBackend<Stdout>,
pub buffer: Buffer,
pub area: [u16;4],
pub perf: PerfModel,
}
impl Tui {
@ -17,6 +18,7 @@ impl Tui {
buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }),
area: [0, 0, width, height],
backend,
perf: Default::default(),
})))
}
/// True if done

View file

@ -62,6 +62,7 @@ impl<T: Render<TuiOut> + Handle<TuiIn> + Sized + 'static> TuiRun<T> for Arc<RwLo
if exited.fetch_and(true, Relaxed) {
break
}
let t0 = engine.read().unwrap().perf.get_t0();
let Size { width, height } = engine.read().unwrap().backend.size()
.expect("get size failed");
if let Ok(state) = state.try_read() {
@ -76,6 +77,8 @@ impl<T: Render<TuiOut> + Handle<TuiIn> + Sized + 'static> TuiRun<T> for Arc<RwLo
state.render(&mut output);
buffer = engine.write().unwrap().flip(output.buffer, size);
}
let t1 = engine.read().unwrap().perf.get_t1(t0).unwrap();
buffer.set_string(0, 0, &format!("{:>03}.{:>03}ms", t1.as_millis(), t1.as_micros() % 1000), Style::default());
std::thread::sleep(sleep);
})
}