From 0192d85a19b1705e41a26a280c5fe15c21c63f81 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 21 May 2025 01:45:23 +0300 Subject: [PATCH] wip: compiles again, after extensive jack rework --- crates/app/src/audio.rs | 6 +- crates/app/src/model.rs | 12 +- crates/app/src/view.rs | 127 +-------- crates/cli/tek.rs | 33 +-- crates/device/src/arranger/arranger_model.rs | 83 +++--- crates/device/src/arranger/arranger_tracks.rs | 20 +- crates/device/src/arranger/arranger_view.rs | 11 +- crates/device/src/clock/clock_model.rs | 3 + crates/device/src/clock/clock_view.rs | 84 ++++++ crates/device/src/port.rs | 210 ++++++++++++++- crates/device/src/port/port_audio_in.rs | 8 +- crates/device/src/port/port_audio_out.rs | 8 +- crates/device/src/port/port_connect.rs | 181 ------------- crates/device/src/port/port_midi_in.rs | 10 +- crates/device/src/port/port_midi_out.rs | 10 +- crates/engine/src/jack.rs | 241 +++++++++--------- crates/engine/src/lib.rs | 2 +- deps/tengri | 2 +- 18 files changed, 526 insertions(+), 525 deletions(-) delete mode 100644 crates/device/src/port/port_connect.rs diff --git a/crates/app/src/audio.rs b/crates/app/src/audio.rs index b8d7be61..099238bc 100644 --- a/crates/app/src/audio.rs +++ b/crates/app/src/audio.rs @@ -1,5 +1,9 @@ use crate::*; - +impl HasJack<'static> for App { + fn jack (&self) -> &Jack<'static> { + &self.jack + } +} audio!( |self: App, client, scope|{ let t0 = self.perf.get_t0(); diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index 80d7f80b..48285fc1 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -1,12 +1,9 @@ use crate::*; use std::path::PathBuf; - #[derive(Default, Debug)] pub struct App { /// Must not be dropped for the duration of the process pub jack: Jack<'static>, - /// Port handles - pub ports: std::collections::BTreeMap>, /// Display size pub size: Measure, /// Performance counter @@ -21,12 +18,9 @@ pub struct App { pub history: Vec<(AppCommand, Option)>, // Dialog overlay pub dialog: Option, - // Cache of formatted strings - pub view_cache: Arc>, /// Base color. pub color: ItemTheme, } - has!(Jack<'static>: |self: App|self.jack); has!(Pool: |self: App|self.pool); has!(Option: |self: App|self.dialog); @@ -69,7 +63,7 @@ from_dsl!(DialogCommand: |state: App, iter|Namespace::take_from(&state.dial impl App { pub fn update_clock (&self) { - ViewCache::update_clock(&self.view_cache, self.clock(), self.size.w() > 80) + ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) } pub fn toggle_dialog (&mut self, mut dialog: Option) -> Option { std::mem::swap(&mut self.dialog, &mut dialog); @@ -106,10 +100,10 @@ impl App { } fn device_add_sampler (&mut self) -> Usually<()> { let name = self.jack.with_client(|c|c.name().to_string()); - let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].name(); + let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); let track = self.track().expect("no active track"); let port = format!("{}/Sampler", &track.name); - let connect = Connect::exact(format!("{name}:{midi}")); + let connect = Connect::exact(format!("{name}:{midi}")); let sampler = if let Ok(sampler) = Sampler::new( &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] ) { diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index 6d1c2549..f590466a 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -60,7 +60,7 @@ impl App { ))); add(&" "); { - let cache = self.view_cache.read().unwrap(); + let cache = self.project.clock.view_cache.read().unwrap(); add(&Fixed::x(15, Align::w(Bsp::s( FieldH(theme, "Beat", cache.beat.view.clone()), FieldH(theme, "Time", cache.time.view.clone()), @@ -90,7 +90,7 @@ impl App { } pub fn view_status_v (&self) -> impl Content + use<'_> { self.update_clock(); - let cache = self.view_cache.read().unwrap(); + let cache = self.project.clock.view_cache.read().unwrap(); let theme = self.color; let playing = self.clock().is_rolling(); Tui::bg(theme.darker.rgb, Fixed::xy(20, 5, Outer(true, Style::default().fg(Tui::g(96))).enclose( @@ -120,13 +120,13 @@ impl App { } pub fn view_status (&self) -> impl Content + use<'_> { self.update_clock(); - let cache = self.view_cache.read().unwrap(); + let cache = self.project.clock.view_cache.read().unwrap(); view_status(Some(self.project.selection.describe(self.tracks(), self.scenes())), cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone()) } pub fn view_transport (&self) -> impl Content + use<'_> { self.update_clock(); - let cache = self.view_cache.read().unwrap(); + let cache = self.project.clock.view_cache.read().unwrap(); view_transport(self.project.clock.is_rolling(), cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone()) } @@ -225,122 +225,3 @@ impl ScenesView for App { (self.width() as u16).saturating_sub(self.w_side()) } } - -/// Clear a pre-allocated buffer, then write into it. -#[macro_export] macro_rules! rewrite { - ($buf:ident, $($rest:tt)*) => { |$buf,_,_|{ $buf.clear(); write!($buf, $($rest)*) } } -} - -#[derive(Debug, Default)] pub(crate) struct ViewMemo { - pub(crate) value: T, - pub(crate) view: Arc> -} - -impl ViewMemo { - fn new (value: T, view: U) -> Self { - Self { value, view: Arc::new(view.into()) } - } - pub(crate) fn update ( - &mut self, - newval: T, - render: impl Fn(&mut U, &T, &T)->R - ) -> Option { - if newval != self.value { - let result = render(&mut*self.view.write().unwrap(), &newval, &self.value); - self.value = newval; - return Some(result); - } - None - } -} - -#[derive(Debug)] pub struct ViewCache { - pub(crate) sr: ViewMemo, String>, - pub(crate) buf: ViewMemo, String>, - pub(crate) lat: ViewMemo, String>, - pub(crate) bpm: ViewMemo, String>, - pub(crate) beat: ViewMemo, String>, - pub(crate) time: ViewMemo, String>, - pub(crate) scns: ViewMemo, String>, - pub(crate) trks: ViewMemo, String>, - pub(crate) stop: Arc, - pub(crate) edit: Arc, -} - -impl Default for ViewCache { - fn default () -> Self { - let mut beat = String::with_capacity(16); - write!(beat, "{}", Self::BEAT_EMPTY); - let mut time = String::with_capacity(16); - write!(time, "{}", Self::TIME_EMPTY); - let mut bpm = String::with_capacity(16); - write!(bpm, "{}", Self::BPM_EMPTY); - Self { - beat: ViewMemo::new(None, beat), - time: ViewMemo::new(None, time), - bpm: ViewMemo::new(None, bpm), - sr: ViewMemo::new(None, String::with_capacity(16)), - buf: ViewMemo::new(None, String::with_capacity(16)), - lat: ViewMemo::new(None, String::with_capacity(16)), - scns: ViewMemo::new(None, String::with_capacity(16)), - trks: ViewMemo::new(None, String::with_capacity(16)), - stop: "⏹".into(), - edit: "edit".into(), - } - } -} - -impl ViewCache { - pub const BEAT_EMPTY: &'static str = "-.-.--"; - pub const TIME_EMPTY: &'static str = "-.---s"; - pub const BPM_EMPTY: &'static str = "---.---"; - - pub fn track_counter (cache: &Arc>, track: usize, tracks: usize) - -> Arc> - { - let data = (track, tracks); - cache.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1)); - cache.read().unwrap().trks.view.clone() - } - - pub fn scene_add (cache: &Arc>, scene: usize, scenes: usize, is_editing: bool) - -> impl Content - { - let data = (scene, scenes); - cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1)); - button_3("S", "add scene", cache.read().unwrap().scns.view.clone(), is_editing) - } - - pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { - let rate = clock.timebase.sr.get(); - let chunk = clock.chunk.load(Relaxed) as f64; - let lat = chunk / rate * 1000.; - let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); - let mut cache = cache.write().unwrap(); - cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); - cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); - cache.sr.update(Some((compact, rate)), |buf,_,_|{ - buf.clear(); - if compact { - write!(buf, "{:.1}kHz", rate / 1000.) - } else { - write!(buf, "{:.0}Hz", rate) - } - }); - if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { - let pulse = clock.timebase.usecs_to_pulse(now); - let time = now/1000000.; - let bpm = clock.timebase.bpm.get(); - cache.beat.update(Some(pulse), |buf, _, _|{ - buf.clear(); - clock.timebase.format_beats_1_to(buf, pulse) - }); - cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); - cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); - } else { - cache.beat.update(None, rewrite!(buf, "{}", ViewCache::BEAT_EMPTY)); - cache.time.update(None, rewrite!(buf, "{}", ViewCache::TIME_EMPTY)); - cache.bpm.update(None, rewrite!(buf, "{}", ViewCache::BPM_EMPTY)); - } - } -} diff --git a/crates/cli/tek.rs b/crates/cli/tek.rs index fd580d00..1e0ec536 100644 --- a/crates/cli/tek.rs +++ b/crates/cli/tek.rs @@ -78,25 +78,23 @@ impl Cli { let mut midi_outs = vec![]; let mut tracks = vec![]; let mut scenes = vec![]; - let midi_froms = PortConnect::collect(&self.midi_from, empty, &self.midi_from_re); - let midi_tos = PortConnect::collect(&self.midi_to, empty, &self.midi_to_re); - let left_froms = PortConnect::collect(&self.left_from, empty, empty); - let left_tos = PortConnect::collect(&self.left_to, empty, empty); - let right_froms = PortConnect::collect(&self.right_from, empty, empty); - let right_tos = PortConnect::collect(&self.right_to, empty, empty); + let midi_froms = Connect::collect(&self.midi_from, empty, &self.midi_from_re); + let midi_tos = Connect::collect(&self.midi_to, empty, &self.midi_to_re); + let left_froms = Connect::collect(&self.left_from, empty, empty); + let left_tos = Connect::collect(&self.left_to, empty, empty); + let right_froms = Connect::collect(&self.right_from, empty, empty); + let right_tos = Connect::collect(&self.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 clip = Arc::new(RwLock::new(MidiClip::new( "Clip", true, 384usize, None, Some(ItemColor::random().into())), )); - Tui::new()?.run(&Jack::new(name)?.run(|jack|{ + Tui::new()?.run(&Jack::new_run(&name, move|jack|{ for (index, connect) in midi_froms.iter().enumerate() { - let port = MidiInput::new(jack, &format!("M/{index}"), &[connect.clone()])?; - midi_ins.push(port); + midi_ins.push(jack.midi_in(&format!("M/{index}"), &[connect.clone()])?); } for (index, connect) in midi_tos.iter().enumerate() { - let port = MidiOutput::new(jack, &format!("{index}/M"), &[connect.clone()])?; - midi_outs.push(port); + midi_outs.push(jack.midi_out(&format!("{index}/M"), &[connect.clone()])?); }; let config = Configuration::new(&match self.mode { LaunchMode::Clock => "config/config_transport.edn", @@ -106,14 +104,14 @@ impl Cli { LaunchMode::Sampler => "config/config_sampler.edn", _ => todo!("{:?}", self.mode), }, false)?; - let clock = Clock::new(jack, self.bpm)?; + let clock = Clock::new(&jack, self.bpm)?; match self.mode { LaunchMode::Sequencer => tracks.push(Track::new( - &name, None, jack, Some(&clock), Some(&clip), + &name, None, &jack, Some(&clock), Some(&clip), midi_froms.as_slice(), midi_tos.as_slice() )?), LaunchMode::Groovebox | LaunchMode::Sampler => tracks.push(Track::new_with_sampler( - &name, None, jack, Some(&clock), Some(&clip), + &name, None, &jack, Some(&clock), Some(&clip), midi_froms.as_slice(), midi_tos.as_slice(), audio_froms, audio_tos, )?), _ => {} @@ -172,10 +170,5 @@ const HEADER: &'static str = r#" #[cfg(test)] #[test] fn test_cli () { use clap::CommandFactory; Cli::command().debug_assert(); - let jack = Jack::default(); - //TODO: - //let _ = App::new_clock(&jack, None, false, false, &[], &[]); - //let _ = App::new_sequencer(&jack, None, false, false, &[], &[]); - //let _ = App::new_groovebox(&jack, None, false, false, &[], &[], &[&[], &[]], &[&[], &[]]); - //let _ = App::new_arranger(&jack, None, false, false, &[], &[], &[&[], &[]], &[&[], &[]], 0, 0, 0); + //let jack = Jack::default(); } diff --git a/crates/device/src/arranger/arranger_model.rs b/crates/device/src/arranger/arranger_model.rs index 3e0a42c5..0f26c58e 100644 --- a/crates/device/src/arranger/arranger_model.rs +++ b/crates/device/src/arranger/arranger_model.rs @@ -1,4 +1,48 @@ use crate::*; +#[derive(Default, Debug)] pub struct Arrangement { + /// Project name. + pub name: Arc, + /// Base color. + pub color: ItemTheme, + /// Jack client handle + pub jack: Jack<'static>, + /// Source of time + pub clock: Clock, + /// Allows one MIDI clip to be edited + pub editor: Option, + /// List of global midi inputs + pub midi_ins: Vec, + /// List of global midi outputs + pub midi_outs: Vec, + /// List of global audio inputs + pub audio_ins: Vec, + /// List of global audio outputs + pub audio_outs: Vec, + /// Last track number (to avoid duplicate port names) + pub track_last: usize, + /// List of tracks + pub tracks: Vec, + /// Scroll offset of tracks + pub track_scroll: usize, + /// List of scenes + pub scenes: Vec, + /// Scroll offset of scenes + pub scene_scroll: usize, + /// Selected UI element + pub selection: Selection, + /// Contains a render of the project arrangement, redrawn on update. + /// TODO rename to "render_cache" or smth + pub arranger: Arc>, + /// Display size + pub size: Measure, + /// Display size of clips area + pub inner_size: Measure, +} +impl HasJack<'static> for Arrangement { + fn jack (&self) -> &Jack<'static> { + &self.jack + } +} has!(Jack<'static>: |self: Arrangement|self.jack); has!(Clock: |self: Arrangement|self.clock); has!(Selection: |self: Arrangement|self.selection); @@ -41,45 +85,6 @@ from_dsl!(ClipCommand: |state: Arrangement, iter|state.selected_clip().as_ref() Selection::Nothing } } -#[derive(Default, Debug)] pub struct Arrangement { - /// Project name. - pub name: Arc, - /// Base color. - pub color: ItemTheme, - /// Jack client handle - pub jack: Jack<'static>, - /// Source of time - pub clock: Clock, - /// Allows one MIDI clip to be edited - pub editor: Option, - /// List of global midi inputs - pub midi_ins: Vec, - /// List of global midi outputs - pub midi_outs: Vec, - /// List of global audio inputs - pub audio_ins: Vec, - /// List of global audio outputs - pub audio_outs: Vec, - /// Last track number (to avoid duplicate port names) - pub track_last: usize, - /// List of tracks - pub tracks: Vec, - /// Scroll offset of tracks - pub track_scroll: usize, - /// List of scenes - pub scenes: Vec, - /// Scroll offset of scenes - pub scene_scroll: usize, - /// Selected UI element - pub selection: Selection, - /// Contains a render of the project arrangement, redrawn on update. - /// TODO rename to "render_cache" or smth - pub arranger: Arc>, - /// Display size - pub size: Measure, - /// Display size of clips area - pub inner_size: Measure, -} impl Arrangement { /// Width of display pub fn w (&self) -> u16 { diff --git a/crates/device/src/arranger/arranger_tracks.rs b/crates/device/src/arranger/arranger_tracks.rs index 71a9e3ed..332a1aa8 100644 --- a/crates/device/src/arranger/arranger_tracks.rs +++ b/crates/device/src/arranger/arranger_tracks.rs @@ -222,15 +222,11 @@ impl Track { audio_to: &[&[Connect];2], ) -> Usually { let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; + let client_name = jack.with_client(|c|c.name().to_string()); + let port_name = track.sequencer.midi_outs[0].port_name(); + let connect = [Connect::exact(format!("{client_name}:{}", port_name))]; track.devices.push(Device::Sampler(Sampler::new( - jack, - &format!("{}/sampler", name.as_ref()), - &[Connect::exact(format!("{}:{}", - jack.with_client(|c|c.name().to_string()), - track.sequencer.midi_outs[0].name() - ))], - audio_from, - audio_to + jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to )?)); Ok(track) } @@ -273,7 +269,7 @@ pub trait HasTrack { Fixed::xy(20, 1 + ins, Outer(true, Style::default().fg(Tui::g(96))).enclose( Fixed::xy(20, 1 + ins, FieldV(theme, format!("MIDI ins: "), Map::south(1, ||track.sequencer.midi_ins.iter(), - |port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))}) + |port, index|Fill::x(Align::w(format!(" {index} {}", port.port_name()))))))))}) } fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Content { self.track().map(|track|{ @@ -281,7 +277,7 @@ pub trait HasTrack { Fixed::xy(20, 1 + outs, Outer(true, Style::default().fg(Tui::g(96))).enclose( Fixed::xy(20, 1 + outs, FieldV(theme, format!("MIDI outs: "), Map::south(1, ||track.sequencer.midi_outs.iter(), - |port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))}) + |port, index|Fill::x(Align::w(format!(" {index} {}", port.port_name()))))))))}) } fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Content { self.track().and_then(|track|track.devices.get(0)).map(|device|{ @@ -289,7 +285,7 @@ pub trait HasTrack { Fixed::xy(20, 1 + ins, Outer(true, Style::default().fg(Tui::g(96))).enclose( Fixed::xy(20, 1 + ins, FieldV(theme, format!("Audio ins: "), Map::south(1, ||device.audio_ins().iter(), - |port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))}) + |port, index|Fill::x(Align::w(format!(" {index} {}", port.port_name()))))))))}) } fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Content { self.track().and_then(|track|track.devices.last()).map(|device|{ @@ -297,7 +293,7 @@ pub trait HasTrack { Fixed::xy(20, 1 + outs, Outer(true, Style::default().fg(Tui::g(96))).enclose( Fixed::xy(20, 1 + outs, FieldV(theme, format!("Audio outs: "), Map::south(1, ||device.audio_outs().iter(), - |port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))}) + |port, index|Fill::x(Align::w(format!(" {index} {}", port.port_name()))))))))}) } } diff --git a/crates/device/src/arranger/arranger_view.rs b/crates/device/src/arranger/arranger_view.rs index 7a5cc937..89fef966 100644 --- a/crates/device/src/arranger/arranger_view.rs +++ b/crates/device/src/arranger/arranger_view.rs @@ -22,7 +22,8 @@ impl Arrangement { }))))); for (index, port) in self.midi_ins().iter().enumerate() { add(&Fixed::y(1, Bsp::e( - Fixed::x(20, Align::w(Bsp::e(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255),port.name()))))), + Fixed::x(20, Align::w(Bsp::e(" ● ", + Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), Bsp::w(Fixed::x(4, ()), Stack::east(move|add: &mut dyn FnMut(&dyn Render)|{ for (index, track, x1, x2) in self.tracks_with_sizes() { @@ -47,7 +48,7 @@ impl Arrangement { Fixed::y(h - 1, Fill::xy(Align::nw(Stack::south(|add: &mut dyn FnMut(&dyn Render)|{ for (index, port) in self.midi_outs().iter().enumerate() { add(&Fixed::y(1,Fill::x(Bsp::e( - Align::w(Bsp::e(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.name())))), + Align::w(Bsp::e(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), Fill::x(Align::e(format!("{}/{} ", port.port().get_connections().len(), port.connections.len()))))))); @@ -195,7 +196,7 @@ pub trait TracksView: Fill::x(Align::w(button_2("o", "utput", false))), Fill::xy(Stack::south(|add: &mut dyn FnMut(&dyn Render)|{ for port in self.midi_outs().iter() { - add(&Fill::x(Align::w(port.name()))); + add(&Fill::x(Align::w(port.port_name()))); } }))), button_2("O", "+", false), @@ -206,7 +207,7 @@ pub trait TracksView: Align::nw(Fill::y(Map::south(1, ||track.sequencer.midi_outs.iter(), |port, index|Tui::fg(Rgb(255, 255, 255), Fixed::y(1, Tui::bg(track.color.dark.rgb, Fill::x(Align::w( - format!("·o{index:02} {}", port.name()))))))))))); + format!("·o{index:02} {}", port.port_name()))))))))))); } }))))) } @@ -232,7 +233,7 @@ pub trait TracksView: )))), Map::south(1, ||track.sequencer.midi_ins.iter(), |port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb, - Fill::x(Align::w(format!("·i{index:02} {}", port.name()))))))))); + Fill::x(Align::w(format!("·i{index:02} {}", port.port_name()))))))))); }}))))) } fn track_width (&self, index: usize, track: &Track) -> u16 { diff --git a/crates/device/src/clock/clock_model.rs b/crates/device/src/clock/clock_model.rs index 2eca6b99..0a881025 100644 --- a/crates/device/src/clock/clock_model.rs +++ b/crates/device/src/clock/clock_model.rs @@ -26,6 +26,8 @@ pub struct Clock { pub midi_out: Arc>>, /// For emitting a metronome pub click_out: Arc>>, + // Cache of formatted strings + pub view_cache: Arc>, } impl std::fmt::Debug for Clock { @@ -59,6 +61,7 @@ impl Clock { midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))), midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))), click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))), + ..Default::default() }; if let Some(bpm) = bpm { clock.timebase.bpm.set(bpm); diff --git a/crates/device/src/clock/clock_view.rs b/crates/device/src/clock/clock_view.rs index 92e778eb..09b6561d 100644 --- a/crates/device/src/clock/clock_view.rs +++ b/crates/device/src/clock/clock_view.rs @@ -1,4 +1,5 @@ use crate::*; +use std::fmt::Write; pub fn view_transport ( play: bool, @@ -49,3 +50,86 @@ pub(crate) fn button_play_pause (playing: bool) -> impl Content { ) ) } + +#[derive(Debug)] pub struct ViewCache { + pub sr: Memo, String>, + pub buf: Memo, String>, + pub lat: Memo, String>, + pub bpm: Memo, String>, + pub beat: Memo, String>, + pub time: Memo, String>, +} + +impl Default for ViewCache { + fn default () -> Self { + let mut beat = String::with_capacity(16); + write!(beat, "{}", Self::BEAT_EMPTY); + let mut time = String::with_capacity(16); + write!(time, "{}", Self::TIME_EMPTY); + let mut bpm = String::with_capacity(16); + write!(bpm, "{}", Self::BPM_EMPTY); + Self { + beat: Memo::new(None, beat), + time: Memo::new(None, time), + bpm: Memo::new(None, bpm), + sr: Memo::new(None, String::with_capacity(16)), + buf: Memo::new(None, String::with_capacity(16)), + lat: Memo::new(None, String::with_capacity(16)), + } + } +} + +impl ViewCache { + pub const BEAT_EMPTY: &'static str = "-.-.--"; + pub const TIME_EMPTY: &'static str = "-.---s"; + pub const BPM_EMPTY: &'static str = "---.---"; + + //pub fn track_counter (cache: &Arc>, track: usize, tracks: usize) + //-> Arc> + //{ + //let data = (track, tracks); + //cache.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1)); + //cache.read().unwrap().trks.view.clone() + //} + + //pub fn scene_add (cache: &Arc>, scene: usize, scenes: usize, is_editing: bool) + //-> impl Content + //{ + //let data = (scene, scenes); + //cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1)); + //button_3("S", "add scene", cache.read().unwrap().scns.view.clone(), is_editing) + //} + + pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { + let rate = clock.timebase.sr.get(); + let chunk = clock.chunk.load(Relaxed) as f64; + let lat = chunk / rate * 1000.; + let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); + let mut cache = cache.write().unwrap(); + cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); + cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); + cache.sr.update(Some((compact, rate)), |buf,_,_|{ + buf.clear(); + if compact { + write!(buf, "{:.1}kHz", rate / 1000.) + } else { + write!(buf, "{:.0}Hz", rate) + } + }); + if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { + let pulse = clock.timebase.usecs_to_pulse(now); + let time = now/1000000.; + let bpm = clock.timebase.bpm.get(); + cache.beat.update(Some(pulse), |buf, _, _|{ + buf.clear(); + clock.timebase.format_beats_1_to(buf, pulse) + }); + cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); + cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); + } else { + cache.beat.update(None, rewrite!(buf, "{}", ViewCache::BEAT_EMPTY)); + cache.time.update(None, rewrite!(buf, "{}", ViewCache::TIME_EMPTY)); + cache.bpm.update(None, rewrite!(buf, "{}", ViewCache::BPM_EMPTY)); + } + } +} diff --git a/crates/device/src/port.rs b/crates/device/src/port.rs index d8735ae1..f75959ea 100644 --- a/crates/device/src/port.rs +++ b/crates/device/src/port.rs @@ -1,8 +1,216 @@ +use crate::*; + mod port_audio_out; pub use self::port_audio_out::*; mod port_audio_in; pub use self::port_audio_in::*; -mod port_connect; pub use self::port_connect::*; mod port_midi_out; pub use self::port_midi_out::*; mod port_midi_in; pub use self::port_midi_in::*; pub(crate) use ConnectName::*; pub(crate) use ConnectScope::*; pub(crate) use ConnectStatus::*; + +pub trait RegisterPorts: HasJack<'static> { + /// Register a MIDI input port. + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register a MIDI output port. + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio input port. + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio output port. + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; +} + +impl> RegisterPorts for J { + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiInput::new(self.jack(), name, connect) + } + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiOutput::new(self.jack(), name, connect) + } + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioInput::new(self.jack(), name, connect) + } + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioOutput::new(self.jack(), name, connect) + } +} + +pub trait JackPort: HasJack<'static> { + type Port: PortSpec + Default; + type Pair: PortSpec + Default; + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized; + fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { + jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) + .map_err(|e|e.into()) + } + fn port_name (&self) -> &Arc; + fn connections (&self) -> &[Connect]; + fn port (&self) -> &Port; + fn port_mut (&mut self) -> &mut Port; + fn into_port (self) -> Port where Self: Sized; + fn close (self) -> Usually<()> where Self: Sized { + let jack = self.jack().clone(); + Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) + } + fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { + self.with_client(|c|c.ports(re_name, re_type, flags)) + } + fn port_by_id (&self, id: u32) -> Option> { + self.with_client(|c|c.port_by_id(id)) + } + fn port_by_name (&self, name: impl AsRef) -> Option> { + self.with_client(|c|c.port_by_name(name.as_ref())) + } + fn connect_to_matching <'k> (&'k self) -> Usually<()> { + for connect in self.connections().iter() { + //panic!("{connect:?}"); + let status = match &connect.name { + Exact(name) => self.connect_exact(name), + RegExp(re) => self.connect_regexp(re, connect.scope), + }?; + *connect.status.write().unwrap() = status; + } + Ok(()) + } + fn connect_exact <'k> (&'k self, name: &str) -> + Usually, Arc, ConnectStatus)>> + { + self.with_client(move|c|{ + let mut status = vec![]; + for port in c.ports(None, None, PortFlags::empty()).iter() { + if port.as_str() == &*name { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected { + break + } + } + } + } + Ok(status) + }) + } + fn connect_regexp <'k> ( + &'k self, re: &str, scope: ConnectScope + ) -> Usually, Arc, ConnectStatus)>> { + self.with_client(move|c|{ + let mut status = vec![]; + let ports = c.ports(Some(&re), None, PortFlags::empty()); + for port in ports.iter() { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected && scope == One { + break + } + } + } + Ok(status) + }) + } + /** Connect to a matching port by name. */ + fn connect_to_name (&self, name: impl AsRef) -> Usually { + self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { + self.connect_to_unowned(port) + } else { + Ok(Missing) + }) + } + /** Connect to a matching port by reference. */ + fn connect_to_unowned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } + /** Connect to an owned matching port by reference. */ + fn connect_to_owned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum ConnectName { + /** Exact match */ + Exact(Arc), + /** Match regular expression */ + RegExp(Arc), +} + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { + One, + All +} + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { + Missing, + Disconnected, + Connected, + Mismatch, +} + +#[derive(Clone, Debug)] pub struct Connect { + pub name: ConnectName, + pub scope: ConnectScope, + pub status: Arc, Arc, ConnectStatus)>>>, + pub info: Arc, +} + +impl Connect { + pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) + -> Vec + { + let mut connections = vec![]; + for port in exact.iter() { connections.push(Self::exact(port)) } + for port in re.iter() { connections.push(Self::regexp(port)) } + for port in re_all.iter() { connections.push(Self::regexp_all(port)) } + connections + } + /// Connect to this exact port + pub fn exact (name: impl AsRef) -> Self { + let info = format!("=:{}", name.as_ref()).into(); + let name = Exact(name.as_ref().into()); + Self { name, scope: One, status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp (name: impl AsRef) -> Self { + let info = format!("~:{}", name.as_ref()).into(); + let name = RegExp(name.as_ref().into()); + Self { name, scope: One, status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp_all (name: impl AsRef) -> Self { + let info = format!("+:{}", name.as_ref()).into(); + let name = RegExp(name.as_ref().into()); + Self { name, scope: All, status: Arc::new(RwLock::new(vec![])), info } + } + pub fn info (&self) -> Arc { + let status = { + let status = self.status.read().unwrap(); + let mut ok = 0; + for (_, _, state) in status.iter() { + if *state == Connected { + ok += 1 + } + } + format!("{ok}/{}", status.len()) + }; + let scope = match self.scope { + One => " ", All => "*", + }; + let name = match &self.name { + Exact(name) => format!("= {name}"), RegExp(name) => format!("~ {name}"), + }; + format!(" ({}) {} {}", status, scope, name).into() + } +} diff --git a/crates/device/src/port/port_audio_in.rs b/crates/device/src/port/port_audio_in.rs index dcec321c..7edd0755 100644 --- a/crates/device/src/port/port_audio_in.rs +++ b/crates/device/src/port/port_audio_in.rs @@ -10,11 +10,15 @@ use crate::*; /// List of ports to connect to. pub connections: Vec, } -has!(Jack<'static>: |self: AudioInput|self.jack); +impl HasJack<'static> for AudioInput { + fn jack (&self) -> &Jack<'static> { + &self.jack + } +} impl JackPort for AudioInput { type Port = AudioIn; type Pair = AudioOut; - fn name (&self) -> &Arc { + fn port_name (&self) -> &Arc { &self.name } fn port (&self) -> &Port { diff --git a/crates/device/src/port/port_audio_out.rs b/crates/device/src/port/port_audio_out.rs index 9984cf13..fda9d606 100644 --- a/crates/device/src/port/port_audio_out.rs +++ b/crates/device/src/port/port_audio_out.rs @@ -10,11 +10,15 @@ use crate::*; /// List of ports to connect to. pub connections: Vec, } -has!(Jack<'static>: |self: AudioOutput|self.jack); +impl HasJack<'static> for AudioOutput { + fn jack (&self) -> &Jack<'static> { + &self.jack + } +} impl JackPort for AudioOutput { type Port = AudioOut; type Pair = AudioIn; - fn name (&self) -> &Arc { + fn port_name (&self) -> &Arc { &self.name } fn port (&self) -> &Port { diff --git a/crates/device/src/port/port_connect.rs b/crates/device/src/port/port_connect.rs deleted file mode 100644 index cd0a9192..00000000 --- a/crates/device/src/port/port_connect.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::*; - -pub trait JackPort: HasJack<'static> { - type Port: PortSpec + Default; - type Pair: PortSpec + Default; - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized; - fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { - Ok(jack.register_port::(name.as_ref())?) - } - fn name (&self) -> &Arc; - fn connections (&self) -> &[Connect]; - fn port (&self) -> &Port; - fn port_mut (&mut self) -> &mut Port; - fn into_port (self) -> Port where Self: Sized; - fn close (self) -> Usually<()> where Self: Sized { - let jack = self.jack().clone(); - Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) - } - fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { - self.with_client(|c|c.ports(re_name, re_type, flags)) - } - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - fn port_by_name (&self, name: impl AsRef) -> Option> { - self.with_client(|c|c.port_by_name(name.as_ref())) - } - fn connect_to_matching <'k> (&'k self) -> Usually<()> { - for connect in self.connections().iter() { - //panic!("{connect:?}"); - let status = match &connect.name { - Exact(name) => self.connect_exact(name), - RegExp(re) => self.connect_regexp(re, connect.scope), - }?; - *connect.status.write().unwrap() = status; - } - Ok(()) - } - fn connect_exact <'k> (&'k self, name: &str) -> - Usually, Arc, ConnectStatus)>> - { - self.with_client(move|c|{ - let mut status = vec![]; - for port in c.ports(None, None, PortFlags::empty()).iter() { - if port.as_str() == &*name { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected { - break - } - } - } - } - Ok(status) - }) - } - fn connect_regexp <'k> ( - &'k self, re: &str, scope: ConnectScope - ) -> Usually, Arc, ConnectStatus)>> { - self.with_client(move|c|{ - let mut status = vec![]; - let ports = c.ports(Some(&re), None, PortFlags::empty()); - for port in ports.iter() { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected && scope == One { - break - } - } - } - Ok(status) - }) - } - /** Connect to a matching port by name. */ - fn connect_to_name (&self, name: impl AsRef) -> Usually { - self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { - self.connect_to_unowned(port) - } else { - Ok(Missing) - }) - } - /** Connect to a matching port by reference. */ - fn connect_to_unowned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } - /** Connect to an owned matching port by reference. */ - fn connect_to_owned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum ConnectName { - /** Exact match */ - Exact(Arc), - /** Match regular expression */ - RegExp(Arc), -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { - One, - All -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { - Missing, - Disconnected, - Connected, - Mismatch, -} - -#[derive(Clone, Debug)] pub struct Connect { - pub name: ConnectName, - pub scope: ConnectScope, - pub status: Arc, Arc, ConnectStatus)>>>, - pub info: Arc, -} - -impl Connect { - pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) - -> Vec - { - let mut connections = vec![]; - for port in exact.iter() { connections.push(Self::exact(port)) } - for port in re.iter() { connections.push(Self::regexp(port)) } - for port in re_all.iter() { connections.push(Self::regexp_all(port)) } - connections - } - /// Connect to this exact port - pub fn exact (name: impl AsRef) -> Self { - let info = format!("=:{}", name.as_ref()).into(); - let name = Exact(name.as_ref().into()); - Self { name, scope: One, status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp (name: impl AsRef) -> Self { - let info = format!("~:{}", name.as_ref()).into(); - let name = RegExp(name.as_ref().into()); - Self { name, scope: One, status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp_all (name: impl AsRef) -> Self { - let info = format!("+:{}", name.as_ref()).into(); - let name = RegExp(name.as_ref().into()); - Self { name, scope: All, status: Arc::new(RwLock::new(vec![])), info } - } - pub fn info (&self) -> Arc { - let status = { - let status = self.status.read().unwrap(); - let mut ok = 0; - for (_, _, state) in status.iter() { - if *state == Connected { - ok += 1 - } - } - format!("{ok}/{}", status.len()) - }; - let scope = match self.scope { - One => " ", All => "*", - }; - let name = match &self.name { - Exact(name) => format!("= {name}"), RegExp(name) => format!("~ {name}"), - }; - format!(" ({}) {} {}", status, scope, name).into() - } -} diff --git a/crates/device/src/port/port_midi_in.rs b/crates/device/src/port/port_midi_in.rs index d3a3d91b..23c535c5 100644 --- a/crates/device/src/port/port_midi_in.rs +++ b/crates/device/src/port/port_midi_in.rs @@ -14,11 +14,15 @@ use crate::*; /// List of ports to connect to. pub connections: Vec, } -has!(Jack<'static>: |self: MidiInput|self.jack); +impl HasJack<'static> for MidiInput { + fn jack (&self) -> &Jack<'static> { + &self.jack + } +} impl JackPort for MidiInput { type Port = MidiIn; type Pair = MidiOut; - fn name (&self) -> &Arc { + fn port_name (&self) -> &Arc { &self.name } fn port (&self) -> &Port { @@ -85,7 +89,7 @@ pub trait HasMidiIns { let mut y = 0; self.midi_ins().iter().enumerate().map(move|(i, input)|{ let height = 1 + input.connections().len(); - let data = (i, input.name(), input.connections(), y, y + height); + let data = (i, input.port_name(), input.connections(), y, y + height); y += height; data }) diff --git a/crates/device/src/port/port_midi_out.rs b/crates/device/src/port/port_midi_out.rs index ecfc2e0b..b27978bb 100644 --- a/crates/device/src/port/port_midi_out.rs +++ b/crates/device/src/port/port_midi_out.rs @@ -16,11 +16,15 @@ use crate::*; /// Buffer output_buffer: Vec>>, } -has!(Jack<'static>: |self: MidiOutput|self.jack); +impl HasJack<'static> for MidiOutput { + fn jack (&self) -> &Jack<'static> { + &self.jack + } +} impl JackPort for MidiOutput { type Port = MidiOut; type Pair = MidiIn; - fn name (&self) -> &Arc { + fn port_name (&self) -> &Arc { &self.name } fn port (&self) -> &Port { @@ -120,7 +124,7 @@ pub trait HasMidiOuts { let mut y = 0; self.midi_outs().iter().enumerate().map(move|(i, output)|{ let height = 1 + output.connections().len(); - let data = (i, output.name(), output.connections(), y, y + height); + let data = (i, output.port_name(), output.connections(), y, y + height); y += height; data }) diff --git a/crates/engine/src/jack.rs b/crates/engine/src/jack.rs index 1691e13d..7e5214aa 100644 --- a/crates/engine/src/jack.rs +++ b/crates/engine/src/jack.rs @@ -1,12 +1,130 @@ use crate::*; pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; -pub(crate) use std::sync::{Arc, RwLock}; +/// Wraps [JackState] and through it [jack::Client] when connected. +#[derive(Clone, Debug, Default)] +pub struct Jack<'j>(Arc>>); +/// Implement [Jack] constructor and methods +impl<'j> Jack<'j> { + /// Register new [Client] and wrap it for shared use. + pub fn new_run + Audio + Send + Sync + 'static> ( + name: &impl AsRef, + init: impl FnOnce(Jack<'j>)->Usually + ) -> Usually>> { + Jack::new(name)?.run(init) + } + pub fn new (name: &impl AsRef) -> Usually { + let client = Client::new(name.as_ref(), ClientOptions::NO_START_SERVER)?.0; + Ok(Jack(Arc::new(RwLock::new(JackState::Inactive(client))))) + } + pub fn run + Audio + Send + Sync + 'static> + (self, init: impl FnOnce(Self)->Usually) -> Usually>> + { + let client_state = self.0.clone(); + let app: Arc> = Arc::new(RwLock::new(init(self)?)); + let mut state = Activating; + std::mem::swap(&mut*client_state.write().unwrap(), &mut state); + if let Inactive(client) = state { + let client = client.activate_async( + // This is the misc notifications handler. It's a struct that wraps a [Box] + // which performs type erasure on a callback that takes [JackEvent], which is + // one of the available misc notifications. + Notifications(Box::new({ + let app = app.clone(); + move|event|(&mut*app.write().unwrap()).handle(event) + }) as BoxedJackEventHandler), + // This is the main processing handler. It's a struct that wraps a [Box] + // which performs type erasure on a callback that takes [Client] and [ProcessScope] + // and passes them down to the `app`'s `process` callback, which in turn + // implements audio and MIDI input and output on a realtime basis. + ClosureProcessHandler::new(Box::new({ + let app = app.clone(); + move|c: &_, s: &_|if let Ok(mut app) = app.write() { + app.process(c, s) + } else { + Control::Quit + } + }) as BoxedAudioHandler), + )?; + *client_state.write().unwrap() = Active(client); + } else { + unreachable!(); + } + Ok(app) + } + /// Run something with the client. + pub fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { + match &*self.0.read().unwrap() { + Inert => panic!("jack client not activated"), + Inactive(ref client) => op(client), + Activating => panic!("jack client has not finished activation"), + Active(ref client) => op(client.as_client()), + } + } +} +impl<'j> HasJack<'j> for Jack<'j> { + fn jack (&self) -> &Jack<'j> { + self + } +} +impl<'j> HasJack<'j> for &Jack<'j> { + fn jack (&self) -> &Jack<'j> { + self + } +} +/// This is a connection which may be [Inactive], [Activating], or [Active]. +/// In the [Active] and [Inactive] states, [JackState::client] returns a +/// [jack::Client], which you can use to talk to the JACK API. +#[derive(Debug, Default)] +pub enum JackState<'j> { + /// Unused + #[default] Inert, + /// Before activation. + Inactive(Client), + /// During activation. + Activating, + /// After activation. Must not be dropped for JACK thread to persist. + Active(DynamicAsyncClient<'j>), +} +/// Things that can provide a [jack::Client] reference. +pub trait HasJack<'j>: Send + Sync { + /// Return the internal [jack::Client] handle + /// that lets you call the JACK API. + fn jack (&self) -> &Jack<'j>; + fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { + self.jack().with_client(op) + } + fn port_by_name (&self, name: &str) -> Option> { + self.with_client(|client|client.port_by_name(name)) + } + fn port_by_id (&self, id: u32) -> Option> { + self.with_client(|c|c.port_by_id(id)) + } + fn register_port (&self, name: impl AsRef) -> Usually> { + self.with_client(|client|Ok(client.register_port(name.as_ref(), PS::default())?)) + } + fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->Position) -> Usually<()> { + if enable { + self.with_client(|client|match client.register_timebase_callback(false, callback) { + Ok(_) => Ok(()), + Err(e) => Err(e) + })? + } + Ok(()) + } + fn sync_follow (&self, _enable: bool) -> Usually<()> { + // TODO: sync follow + Ok(()) + } +} /// Trait for thing that has a JACK process callback. pub trait Audio { + /// Handle a JACK event. fn handle (&mut self, _event: JackEvent) {} + /// Projecss a JACK chunk. fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { Control::Continue } + /// The JACK process callback function passed to the server. fn callback ( state: &Arc>, client: &Client, scope: &ProcessScope ) -> Control where Self: Sized { @@ -17,7 +135,6 @@ pub trait Audio { } } } - /// Implement [Audio]: provide JACK callbacks. #[macro_export] macro_rules! audio { (| @@ -30,7 +147,6 @@ pub trait Audio { } } } - /// Event enum for JACK events. #[derive(Debug, Clone, PartialEq)] pub enum JackEvent { ThreadInit, @@ -44,7 +160,6 @@ pub trait Audio { GraphReorder, XRun, } - /// Generic notification handler that emits [JackEvent] pub struct Notifications(pub T); @@ -105,121 +220,3 @@ pub type BoxedJackEventHandler<'j> = Box; use self::JackState::*; -impl<'j, T: Has>> HasJack<'j> for T { - fn jack (&self) -> &Jack<'j> { self.get() } -} -impl<'j> HasJack<'j> for Jack<'j> { - fn jack (&self) -> &Jack<'j> { self } -} -impl<'j> HasJack<'j> for &Jack<'j> { - fn jack (&self) -> &Jack<'j> { self } -} - -/// Things that can provide a [jack::Client] reference. -pub trait HasJack<'j> { - /// Return the internal [jack::Client] handle - /// that lets you call the JACK API. - fn jack (&self) -> &Jack<'j>; - /// Run the JACK thread. - fn run ( - &self, callback: impl FnOnce(&Jack)->Usually - ) -> Usually>> { - let jack = self.jack(); - let app = Arc::new(RwLock::new(callback(jack)?)); - let mut state = Activating; - std::mem::swap(&mut*jack.state.write().unwrap(), &mut state); - if let Inactive(client) = state { - let client = client.activate_async( - // This is the misc notifications handler. It's a struct that wraps a [Box] - // which performs type erasure on a callback that takes [JackEvent], which is - // one of the available misc notifications. - Notifications(Box::new({ - let app = app.clone(); - move|event|app.write().unwrap().handle(event) - }) as BoxedJackEventHandler), - // This is the main processing handler. It's a struct that wraps a [Box] - // which performs type erasure on a callback that takes [Client] and [ProcessScope] - // and passes them down to the `app`'s `process` callback, which in turn - // implements audio and MIDI input and output on a realtime basis. - ClosureProcessHandler::new(Box::new({ - let app = app.clone(); - move|c: &_, s: &_|if let Ok(mut app) = app.write() { - app.process(c, s) - } else { - Control::Quit - } - }) as BoxedAudioHandler), - )?; - *jack.state.write().unwrap() = Active(client); - } else { - unreachable!(); - } - Ok(app) - } - /// Run something with the client. - fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { - match &*self.jack().state.read().unwrap() { - Inert => panic!("jack client not activated"), - Inactive(ref client) => op(client), - Activating => panic!("jack client has not finished activation"), - Active(ref client) => op(client.as_client()), - } - } - fn port_by_name (&self, name: &str) -> Option> { - self.with_client(|client|client.port_by_name(name)) - } - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - fn register_port (&self, name: impl AsRef) -> Usually> { - self.with_client(|client|Ok(client.register_port(name.as_ref(), PS::default())?)) - } - fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->Position) -> Usually<()> { - if enable { - self.with_client(|client|match client.register_timebase_callback(false, callback) { - Ok(_) => Ok(()), - Err(e) => Err(e) - })? - } - Ok(()) - } - fn sync_follow (&self, _enable: bool) -> Usually<()> { - // TODO: sync follow - Ok(()) - } -} - -/// Wraps [JackState] and through it [jack::Client]. -#[derive(Clone, Debug, Default)] -pub struct Jack<'j> { - pub state: Arc>> -} - -impl<'j> Jack<'j> { - pub fn new (name: &str) -> Usually { - Ok(Self { - state: JackState::new(Client::new(name, ClientOptions::NO_START_SERVER)?.0) - }) - } -} - -/// This is a connection which may be [Inactive], [Activating], or [Active]. -/// In the [Active] and [Inactive] states, [JackState::client] returns a -/// [jack::Client], which you can use to talk to the JACK API. -#[derive(Debug, Default)] -pub enum JackState<'j> { - /// Unused - #[default] Inert, - /// Before activation. - Inactive(Client), - /// During activation. - Activating, - /// After activation. Must not be dropped for JACK thread to persist. - Active(DynamicAsyncClient<'j>), -} - -impl<'j> JackState<'j> { - fn new (client: Client) -> Arc> { - Arc::new(RwLock::new(Self::Inactive(client))) - } -} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 8ecfdb75..da077e32 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -89,7 +89,7 @@ mod note; pub use self::note::*; pub mod jack; pub use self::jack::*; pub mod midi; pub use self::midi::*; -pub(crate) use std::sync::{Arc, atomic::{AtomicUsize, AtomicBool, Ordering::Relaxed}}; +pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, AtomicBool, Ordering::Relaxed}}; pub(crate) use std::fmt::Debug; pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem}; diff --git a/deps/tengri b/deps/tengri index f714302f..2048dd22 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit f714302f21d64ef5f6dcbee09aa913e8e3b9fbc5 +Subproject commit 2048dd2263fc4389c8f510bfd12c851efe34026f