diff --git a/.old/tek.rs.old b/.old/tek.rs.old index 044e55da..b51b97ad 100644 --- a/.old/tek.rs.old +++ b/.old/tek.rs.old @@ -1821,3 +1821,29 @@ from_edn!("mixer/track" => |jack: &Arc>, 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"), +//}); diff --git a/Cargo.lock b/Cargo.lock index 328e45fe..38c0cff8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/cli/tek.rs b/cli/tek.rs index d2466dfe..0d2c7212 100644 --- a/cli/tek.rs +++ b/cli/tek.rs @@ -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, } - -#[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>, clip: Option<&Arc>>| - MidiPlayer::new(&jack, name, clip, &midi_froms, &midi_tos); - let default_sampler = |jack: &Arc>| - 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>|default_bpm(Clock::from(jack)); - // TODO: enable sync master/follow - //let sync_clock = |jack: &Arc>, 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::::new(jack, format!("M/{name}"), &midi_froms)?,], - //midi_outs: vec![JackPort::::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(); diff --git a/output/src/edn_view.rs b/output/src/edn_view.rs index 9667a5e0..58ab2629 100644 --- a/output/src/edn_view.rs +++ b/output/src/edn_view.rs @@ -12,10 +12,7 @@ use EdnItem::*; $( 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 - }) + Some(match edn.to_ref() { $(EdnItem::Sym($sym) => $value,)* _ => return None }) } } )* diff --git a/sampler/src/has_sampler.rs b/sampler/src/has_sampler.rs index 6ae84abe..a2e3bb8f 100644 --- a/sampler/src/has_sampler.rs +++ b/sampler/src/has_sampler.rs @@ -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 { &$e0 } + fn sampler_mut (&mut $self) -> &mut Option { &mut $e0 } + fn sample_index (&$self) -> usize { $e1 } + } + } +} diff --git a/tek/src/lib.rs b/tek/src/lib.rs index bf0032f6..313b90ee 100644 --- a/tek/src/lib.rs +++ b/tek/src/lib.rs @@ -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 = std::result::Result>; /// Standard optional result type. pub type Perhaps = std::result::Result, Box>; -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>, 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 { &self.sampler } - 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) } -} +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>, + bpm: Option, + ) -> Usually { + // TODO: enable sync master/follow + //let sync_clock = |jack: &Arc>, 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>, pool: MidiPool, editor: MidiEditor, - player: Option, midi_froms: &[PortConnection], midi_tos: &[PortConnection], - ) -> Self { - Self { + jack: &Arc>, + bpm: Option, + midi_froms: &[PortConnection], + midi_tos: &[PortConnection], + ) -> Usually { + 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>, 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 - ) + jack: &Arc>, + bpm: Option, + midi_froms: &[PortConnection], + midi_tos: &[PortConnection], + audio_froms: &[&[PortConnection];2], + audio_tos: &[&[PortConnection];2], + ) -> Usually { + 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>, 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>, + bpm: Option, + midi_froms: &[PortConnection], + midi_tos: &[PortConnection], + audio_froms: &[&[PortConnection];2], + audio_tos: &[&[PortConnection];2], + scenes: usize, + tracks: usize, + track_width: usize, + ) -> Usually { 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), } 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>] 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>] 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>, 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, 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 () { // TODO } diff --git a/tek/src/transport-keys.edn b/tek/src/transport-keys.edn new file mode 100644 index 00000000..e69de29b diff --git a/tek/src/transport-view.edn b/tek/src/transport-view.edn new file mode 100644 index 00000000..e69de29b diff --git a/time/Cargo.toml b/time/Cargo.toml index e0f204e7..83d7f3f5 100644 --- a/time/Cargo.toml +++ b/time/Cargo.toml @@ -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" } diff --git a/time/src/clock.rs b/time/src/clock.rs index e0fdb977..826d0b0e 100644 --- a/time/src/clock.rs +++ b/time/src/clock.rs @@ -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 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), @@ -77,25 +63,25 @@ pub struct Clock { /// Size of buffer in samples pub chunk: Arc, } - -from!(|jack: &Arc>| 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>> for Clock { + fn from (jack: &Arc>) -> 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>, bpm: Option) -> Self { + let clock = Self::from(jack); + if let Some(bpm) = bpm { + clock.timebase.bpm.set(bpm); + } + clock + } pub fn timebase (&self) -> &Arc { &self.timebase } diff --git a/time/src/clock_tui.rs b/time/src/clock_tui.rs deleted file mode 100644 index 0cab6fda..00000000 --- a/time/src/clock_tui.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::*; -use KeyCode::*; -use ClockCommand::{Play, Pause}; -/// Transport clock app. -pub struct ClockTui { pub jack: Arc>, 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, 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(), - } - } -} - -// 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"), -//}); diff --git a/time/src/lib.rs b/time/src/lib.rs index e0297cb6..405d9063 100644 --- a/time/src/lib.rs +++ b/time/src/lib.rs @@ -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 = Result>; +/// Standard optional result type. +pub(crate) type Perhaps = Result, Box>; + +pub trait Gettable { + /// Returns current value + fn get (&self) -> T; +} + +pub trait Mutable: Gettable { + /// Sets new value, returns old + fn set (&mut self, value: T) -> T; +} + +pub trait InteriorMutable: Gettable { + /// Sets new value, returns old + fn set (&self, value: T) -> T; +} + +impl Gettable for AtomicBool { + fn get (&self) -> bool { self.load(Relaxed) } +} + +impl InteriorMutable for AtomicBool { + fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) } +} + +impl Gettable for AtomicUsize { + fn get (&self) -> usize { self.load(Relaxed) } +} + +impl InteriorMutable for AtomicUsize { + fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) } +} #[cfg(test)] #[test] fn test_time () -> Usually<()> { // TODO! diff --git a/time/src/perf.rs b/time/src/perf.rs index b7e0adb5..00f4c799 100644 --- a/time/src/perf.rs +++ b/time/src/perf.rs @@ -34,6 +34,17 @@ impl PerfModel { None } } + pub fn get_t1 (&self, t0: Option) -> Option { + 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, scope: &ProcessScope) { if let Some(t0) = t0 { let t1 = self.clock.raw(); diff --git a/tui/Cargo.toml b/tui/Cargo.toml index 9bd4ba93..a06ce24e 100644 --- a/tui/Cargo.toml +++ b/tui/Cargo.toml @@ -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" } diff --git a/tui/src/lib.rs b/tui/src/lib.rs index 028fbd4d..917d32d0 100644 --- a/tui/src/lib.rs +++ b/tui/src/lib.rs @@ -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 { - /// Returns current value - fn get (&self) -> T; -} - -pub trait Mutable: Gettable { - /// Sets new value, returns old - fn set (&mut self, value: T) -> T; -} - -pub trait InteriorMutable: Gettable { - /// Sets new value, returns old - fn set (&self, value: T) -> T; -} - -impl Gettable for AtomicBool { - fn get (&self) -> bool { self.load(Relaxed) } -} - -impl InteriorMutable for AtomicBool { - fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) } -} - -impl Gettable for AtomicUsize { - fn get (&self) -> usize { self.load(Relaxed) } -} - -impl InteriorMutable 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}; diff --git a/tui/src/tui_engine.rs b/tui/src/tui_engine.rs index cd880ff4..79173631 100644 --- a/tui/src/tui_engine.rs +++ b/tui/src/tui_engine.rs @@ -5,6 +5,7 @@ pub struct Tui { pub backend: CrosstermBackend, 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 diff --git a/tui/src/tui_run.rs b/tui/src/tui_run.rs index 683ef72d..bda30725 100644 --- a/tui/src/tui_run.rs +++ b/tui/src/tui_run.rs @@ -62,6 +62,7 @@ impl + Handle + Sized + 'static> TuiRun for Arc + Handle + Sized + 'static> TuiRun for Arc03}.{:>03}ms", t1.as_millis(), t1.as_micros() % 1000), Style::default()); std::thread::sleep(sleep); }) }