diff --git a/Cargo.lock b/Cargo.lock index bd5f287..d1b3d2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -402,6 +402,25 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -1018,6 +1037,15 @@ dependencies = [ "libc", ] +[[package]] +name = "midly" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207d755f4cb882d20c4da58d707ca9130a0c9bc5061f657a4f299b8e36362b7a" +dependencies = [ + "rayon", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1728,6 +1756,26 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2072,6 +2120,7 @@ dependencies = [ "crossterm 0.29.0", "dizzle", "jack", + "midly", "palette", "proptest", "proptest-derive", diff --git a/Cargo.toml b/Cargo.toml index 8657a68..588f2a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,11 +5,12 @@ version = "0.15.0" description = "UI metaframework." [features] -default = ["lang", "sing", "draw", "play", "term", "text", "time", "rand", "okhsl"] +default = ["lang", "sing", "midi", "draw", "play", "term", "text", "time", "rand", "okhsl"] bumpalo = ["dep:bumpalo"] draw = [] gui = ["draw", "dep:winit"] lang = ["dep:dizzle"] +midi = ["dep:midly"] okhsl = ["dep:palette"] play = [] rand = ["dep:rand"] @@ -27,6 +28,7 @@ bumpalo = { optional = true, version = "3.19.0" } crossterm = { optional = true, version = "0.29.0" } dizzle = { optional = true, path = "./dizzle" } jack = { optional = true, path = "./rust-jack" } +midly = { optional = true, version = "0.5" } palette = { optional = true, version = "0.7.6", features = [ "random" ] } quanta = { optional = true, version = "0.12.3" } rand = { optional = true, version = "0.8.5" } diff --git a/dizzle b/dizzle index e4e01c0..4424ef7 160000 --- a/dizzle +++ b/dizzle @@ -1 +1 @@ -Subproject commit e4e01c025befd4fd7ed217fa5cc492373f5d7e3b +Subproject commit 4424ef7fc3fd8bfbea1fb55b4922f7a8cfd2a8ef diff --git a/examples/mode_01.rs b/examples/mode_01.rs index 6f1173f..18a9dae 100644 --- a/examples/mode_01.rs +++ b/examples/mode_01.rs @@ -30,23 +30,37 @@ tui_view!(self: State { let widget = thunk(move|to: &mut Tui|self.interpret(to, &src)); self.size.of(south(title, north(code, widget))) }); -tui_ns!(self: State, to, src { - match src.src()? { - Some(":foo") => "foo".draw(to), - Some(":bar") => "bar".draw(to), - Some(":foobar") => "FOOBAR".draw(to), - _ => todo!() +impl Interpret for State { + fn interpret_expr (&self, to: &mut Tui, expr: &impl Language) -> Usually { + let expr = expr.expr()?; + match expr.head()? { + Some("g") if let Some(tail) = expr.tail()? => { + Color::new_g(tail.head()?, try_to_u8) + }, + Some("rgb") if let Some(tail) = expr.tail()? => { + Color::new_rgb(tail.head()?) + }, + _ => Err(format!("not a color").into()) + } } -}); -#[derive(Debug)] -enum Action { - /** Increment cursor */ Next, - /** Decrement cursor */ Prev, } -impl Action { - fn eval (&self, state: &mut State) -> Perhaps { - use Action::*; - match self { Next => state.next(), Prev => state.prev(), } +impl Interpret>> for State { + fn interpret_word (&self, to: &mut Tui, sym: &impl Language) -> Perhaps> { + match sym.src()? { + Some(":foo") => "foo".draw(to), + Some(":bar") => "bar".draw(to), + Some(":foobar") => "FOOBAR".draw(to), + _ => todo!() + } + } + fn interpret_expr (&self, to: &mut Tui, src: &impl Expression) -> Perhaps> { + Ok(Some(if let Some(area) = eval_view(self, to, src)? { + area + } else if let Some(area) = eval_view_tui(self, to, src)? { + area + } else { + return Err(format!("App::interpret_expr: unexpected: {src:?}").into()) + })) } } impl State { @@ -59,6 +73,17 @@ impl State { Ok(Some(Action::Next)) } } +#[derive(Debug)] +enum Action { + /** Increment cursor */ Next, + /** Decrement cursor */ Prev, +} +impl Action { + fn eval (&self, state: &mut State) -> Perhaps { + use Action::*; + match self { Next => state.next(), Prev => state.prev(), } + } +} const VIEWS: &'static [&'static str] = &[ stringify! { :foobar }, stringify! { (bg (g 8) :foobar) }, diff --git a/src/draw.rs b/src/draw.rs index 38eaa78..b92b229 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -22,7 +22,7 @@ use crate::*; /// area: impl Into>>, /// draw: impl FnOnce(&mut Self)->T /// ) -> T -/// { draw(self) } +/// { draw(self } /// } /// /// impl_draw!(|self: String, to: TestOut|{ diff --git a/src/draw/layout.rs b/src/draw/layout.rs index 366eadf..6c0eb2a 100644 --- a/src/draw/layout.rs +++ b/src/draw/layout.rs @@ -55,7 +55,7 @@ pub trait Layout: Draw + Sized { Pull::X(self, x.into()) } fn pull_y >> (self, y: N) -> impl Draw { - Pull::X(self, y.into()) + Pull::Y(self, y.into()) } fn pull_xy >> (self, x: N, y: N) -> impl Draw { Pull::XY(self, x.into(), y.into()) @@ -65,7 +65,7 @@ pub trait Layout: Draw + Sized { Push::X(self, x.into()) } fn push_y >> (self, y: N) -> impl Draw { - Push::X(self, y.into()) + Push::Y(self, y.into()) } fn push_xy >> (self, x: N, y: N) -> impl Draw { Push::XY(self, x.into(), y.into()) @@ -299,19 +299,17 @@ pub enum Exact, X: Into>> { } impl_draw!(, X: Into>,>|self: Exact, to: T|{ - match self { - Self::__(_) => unreachable!(), - Self::W(item, w1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - to.clip(XYWH(x, y, w1.into().unwrap_or(w), h), |to|item.draw(to)) - }, - Self::H(item, h1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - to.clip(XYWH(x, y, w, h1.into().unwrap_or(h)), |to|item.draw(to)) - }, - Self::WH(item, w1, h1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - to.clip(XYWH(x, y, w1.into().unwrap_or(w), h1.into().unwrap_or(h)), |to|item.draw(to)) - }, - _ => Ok(None) - } + let area: XYWH = to.area(); + let (item, area) = match self { + Self::W(item, w1) => + (item, XYWH(area.0, area.1, w1.into().unwrap_or(area.2), area.3)), + Self::H(item, h1) => + (item, XYWH(area.0, area.1, area.2, h1.into().unwrap_or(area.3))), + Self::WH(item, w1, h1) => + (item, XYWH(area.0, area.1, w1.into().unwrap_or(area.2), h1.into().unwrap_or(area.3))), + _ => return Ok(None) + }; + to.clip(area, |to|item.draw(to)) }); /// Define inner drawing area. diff --git a/src/draw/split.rs b/src/draw/split.rs index 2ee67b4..11e38a1 100644 --- a/src/draw/split.rs +++ b/src/draw/split.rs @@ -37,6 +37,23 @@ pub const fn below , B: Draw> (a: A, b: B) -> impl Draw impl Split { + /// ``` + /// use tengri::*; + /// let _ = Split::Above.stack("", ""); + /// let _ = Split::Below.stack("", ""); + /// let _ = Split::North.stack("", ""); + /// let _ = Split::South.stack("", ""); + /// let _ = Split::East.stack("", ""); + /// let _ = Split::West.stack("", ""); + /// ``` + pub const fn stack , B: Draw> (&self, a: A, b: B) -> impl Draw { + thunk(move|to: &mut S|{ + let (area_a, area_b) = stack_areas(self, to.area(), &a, &b)?; + let (drawn_a, drawn_b) = draw_stacks(self, to, a, area_a, None, b, area_b, None)?; + Ok(stack_drawn(self, drawn_a, drawn_b)) + }) + } + /// ``` /// use tengri::*; /// let _ = Split::Above.half("", ""); @@ -48,38 +65,10 @@ impl Split { /// ``` pub const fn half , B: Draw> (&self, a: A, b: B) -> impl Draw { thunk(move|to: &mut S|{ - let (area_a, area_b) = to.xywh().split_half(self); + let (area_a, area_b) = to.xywh().split_half(self); let (origin_a, origin_b) = self.origins(); - let (a, b) = match self { - Self::Below => ( - to.show(area(area_b, b.align(origin_b)))?, - to.show(area(area_b, a.align(origin_a)))?, - ), - _ => ( - to.show(area(area_a, a.align(origin_a)))?, - to.show(area(area_b, b.align(origin_b)))?, - ) - }; - Ok(if let (Some(XYWH(xa, ya, wa, ha)), Some(XYWH(xb, yb, wb, hb))) = (a, b) { - match self { - Self::South => - Some(XYWH(xa.min(xb), ya, wa.max(wb), ha + hb)), - Self::East => - Some(XYWH(xa, ya.min(yb), wa + wb, ha.max(hb))), - Self::North => - Some(XYWH(xa.min(xb), yb, wa.max(wb), ha + hb)), - Self::West => - Some(XYWH(xb, ya.min(yb), wa + wb, ha.max(hb))), - Self::Above | Self::Below => - Some(XYWH(xa.min(xb), ya.min(yb), wa.max(wb), ha.max(hb))), - } - } else if let Some(a) = a { - Some(a) - } else if let Some(b) = b { - Some(b) - } else { - None - }) + let (drawn_a, drawn_b) = draw_stacks(self, to, a, area_a, origin_a, b, area_b, origin_b)?; + Ok(stack_drawn(self, drawn_a, drawn_b)) }) } @@ -128,6 +117,121 @@ impl Split { } +fn draw_stacks ( + split: &Split, + to: &mut S, + a: impl Draw, + area_a: impl Into>>, + origin_a: impl Into>, + b: impl Draw, + area_b: impl Into>>, + origin_b: impl Into>, +) -> Usually<(Option>, Option>)> { + Ok(match split { + Split::Below => { + let drawn_b = to.clip(area_b.into(), |to|b.align(origin_b.into()).draw(to))?; + let drawn_a = to.clip(area_a.into(), |to|a.align(origin_a.into()).draw(to))?; + (drawn_a, drawn_b) + }, + _ => { + let drawn_a = to.clip(area_a.into(), |to|a.align(origin_a.into()).draw(to))?; + let drawn_b = to.clip(area_b.into(), |to|b.align(origin_b.into()).draw(to))?; + (drawn_a, drawn_b) + } + }) +} + + +fn stack_areas ( + split: &Split, + area: XYWH, + a: &impl Draw, + b: &impl Draw, +) -> Usually<(Option>, Option>)> { + let area_a = a.layout(area)?; + Ok(match split { + Split::South => ( + area_a, + if let Some(used) = area_a { + b.layout(XYWH( + area.x(), area.y() + used.h(), area.w(), area.h().minus(used.h()) + ))? + } else { + None + } + ), + Split::East => ( + area_a, + if let Some(used) = area_a { + b.layout(XYWH( + area.x() + used.w(), area.y(), area.w().minus(used.w()), area.h() + ))? + } else { + None + } + ), + Split::North => ( + if let Some(used) = area_a { + Some(XYWH(used.x(), area.y() + used.h(), used.w(), used.h())) + } else { + None + }, + if let Some(used) = area_a { + b.layout(XYWH( + area.x(), area.y(), area.w(), area.h().minus(used.h()) + ))? + } else { + b.layout(area)?.map(|area_b|XYWH( + area_b.x(), area_b.y() + area_b.h(), area_b.w(), area_b.h() + )) + } + ), + Split::West => ( + if let Some(used) = area_a { + Some(XYWH(area.x() + used.w(), used.y(), used.w(), used.h())) + } else { + None + }, + if let Some(used) = area_a { + b.layout(XYWH( + area.x(), area.y(), area.w().minus(used.w()), area.h() + ))? + } else { + b.layout(area)?.map(|area_b|XYWH( + area_b.x() + area_b.w(), area_b.y(), area_b.w(), area_b.h() + )) + } + ), + Split::Above | Split::Below => ( + area_a, + b.layout(area)?, + ), + }) +} + +fn stack_drawn ( + split: &Split, + drawn_a: Option>, + drawn_b: Option>, +) -> Option> { + if let (Some(XYWH(xa, ya, wa, ha)), Some(XYWH(xb, yb, wb, hb))) = (drawn_a, drawn_b) { + match split { + Split::South => Some(XYWH(xa.min(xb), ya, wa.max(wb), ha + hb)), + Split::East => Some(XYWH(xa, ya.min(yb), wa + wb, ha.max(hb))), + Split::North => Some(XYWH(xa.min(xb), yb, wa.max(wb), ha + hb)), + Split::West => Some(XYWH(xb, ya.min(yb), wa + wb, ha.max(hb))), + Split::Above | Split::Below => + Some(XYWH(xa.min(xb), ya.min(yb), wa.max(wb), ha.max(hb))), + } + } else if let Some(a) = drawn_a { + Some(a) + } else if let Some(b) = drawn_b { + Some(b) + } else { + None + } +} + #[macro_export] macro_rules! north { ($head:expr $(,)?) => { $head }; ($head:expr, $($tail:expr),* $(,)?) => { north($head, north!($($tail,)*)) }; diff --git a/src/eval.rs b/src/eval.rs index 8cab4b0..2debaa1 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -226,7 +226,6 @@ pub fn eval_view_tui <'a, S> ( let tail0 = args.tail(); let arg1 = tail0.head(); match frags.next() { - Some("text") => { if let Some(src) = args?.src()? { output.show(src) diff --git a/src/exit.rs b/src/exit.rs index 787e3e5..8d96419 100644 --- a/src/exit.rs +++ b/src/exit.rs @@ -1,5 +1,5 @@ +use crate::*; use std::sync::{Arc, atomic::AtomicBool}; -use crate::Usually; #[derive(Clone)] pub struct Exit(Arc); @@ -7,6 +7,14 @@ impl Exit { pub fn run (run: impl FnOnce(Self)->Usually) -> Usually { run(Self(Arc::new(AtomicBool::new(false)))) } + pub fn is (event: &Event) -> bool { + matches!(event, Event::Key(KeyEvent { + modifiers: KeyModifiers::CONTROL, + code: KeyCode::Char('c'), + kind: KeyEventKind::Press, + state: KeyEventState::NONE + })) + } } impl AsRef> for Exit { diff --git a/src/lib.rs b/src/lib.rs index e3eec5c..155a53f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ pub extern crate palette; pub extern crate better_panic; pub extern crate unicode_width; #[cfg(feature = "sing")] pub extern crate jack; +#[cfg(feature = "midi")] pub extern crate midly; #[cfg(feature = "term")] pub extern crate ratatui; #[cfg(feature = "term")] pub extern crate crossterm; #[cfg(feature = "lang")] pub extern crate dizzle as lang; @@ -33,7 +34,7 @@ macro_rules! features { ($($feature:literal: [ $($module:ident),* ]),*) => { $( $( - #[cfg(feature = $feature)] mod $module; + #[cfg(feature = $feature)] pub mod $module; #[cfg(feature = $feature)] pub use $module::*; )* )* diff --git a/src/sing.rs b/src/sing.rs index ce776a6..c06a1e5 100644 --- a/src/sing.rs +++ b/src/sing.rs @@ -1,9 +1,205 @@ -pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; use crate::{*, time::PerfModel}; +pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; +pub use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; +use ConnectName::*; +use ConnectScope::*; +use ConnectStatus::*; +use JackState::*; -mod jack; pub use self::jack::*; -mod jack_event; pub use self::jack_event::*; -mod jack_perf; pub use self::jack_perf::*; +/// Wraps [JackState], and through it [jack::Client] when connected. +/// +/// ``` +/// let jack = tengri::Jack::default(); +/// ``` +#[derive(Clone, Debug, Default)] pub struct Jack<'j> ( + pub(crate) Arc>> +); + +/// 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. +/// +/// ``` +/// let state = tengri::JackState::default(); +/// ``` +#[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>), +} + +/// 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))))) + } + /// 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(client) => op(client), + Activating => panic!("jack client has not finished activation"), + Active(client) => op(client.as_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 { + // 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. + let notify = JackNotify(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. + let process = ::jack::contrib::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); + // Launch a client with the two handlers. + *client_state.write().unwrap() = Active( + client.activate_async(notify, process)? + ); + } else { + unreachable!(); + } + Ok(app) + } +} + +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 + } +} + +impl<'j, T: HasJack<'j>> HasJack<'j> for Arc { + fn jack (&self) -> &Jack<'j> { + (&**self).jack() + } +} + +/// Event enum for JACK events. +/// +/// ``` +/// let event = tengri::JackEvent::XRun; // kerpop +/// ``` +#[derive(Debug, Clone, PartialEq)] pub enum JackEvent { + ThreadInit, + Shutdown(ClientStatus, Arc), + Freewheel(bool), + SampleRate(Frames), + ClientRegistration(Arc, bool), + PortRegistration(PortId, bool), + PortRename(PortId, Arc, Arc), + PortsConnected(PortId, PortId, bool), + GraphReorder, + XRun, +} + +/// Generic notification handler that emits [JackEvent] +/// +/// ``` +/// let notify = tengri::JackNotify(|_|{}); +/// ``` +pub struct JackNotify(pub T); + +/// Notification handler wrapper for [BoxedJackEventHandler]. +pub type DynamicNotifications<'j> = + JackNotify>; + +/// Boxed [JackEvent] callback. +pub type BoxedJackEventHandler<'j> = + Box; + +impl NotificationHandler for JackNotify { + fn thread_init(&self, _: &Client) { + self.0(JackEvent::ThreadInit); + } + unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { + self.0(JackEvent::Shutdown(status, reason.into())); + } + fn freewheel(&mut self, _: &Client, enabled: bool) { + self.0(JackEvent::Freewheel(enabled)); + } + fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { + self.0(JackEvent::SampleRate(frames)); + Control::Quit + } + fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { + self.0(JackEvent::ClientRegistration(name.into(), reg)); + } + fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { + self.0(JackEvent::PortRegistration(id, reg)); + } + fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + self.0(JackEvent::PortRename(id, old.into(), new.into())); + Control::Continue + } + fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { + self.0(JackEvent::PortsConnected(a, b, are)); + } + fn graph_reorder(&mut self, _: &Client) -> Control { + self.0(JackEvent::GraphReorder); + Control::Continue + } + fn xrun(&mut self, _: &Client) -> Control { + self.0(JackEvent::XRun); + Control::Continue + } +} + +pub trait JackPerfModel { + fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope); +} + +impl JackPerfModel for PerfModel { + fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope) { + if let Some(t0) = t0 { + let t1 = self.clock.raw(); + self.used.store( + self.clock.delta_as_nanos(t0, t1) as f64, + Relaxed, + ); + self.window.store( + scope.cycle_times().unwrap().period_usecs as f64, + Relaxed, + ); + } + } +} /// Trait for thing that has a JACK process callback. pub trait Audio { @@ -91,7 +287,6 @@ pub trait HasJack<'j>: Send + Sync { /// Implement [Audio]: provide JACK callbacks. #[macro_export] macro_rules! impl_audio { - (| $self1:ident: $Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident @@ -120,5 +315,611 @@ pub trait HasJack<'j>: Send + Sync { } } }; - +} + +pub trait JackPorts: 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> JackPorts 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> { + const KIND: &'static str = "Port"; + 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 close (self) -> Usually<()> where Self: Sized { + let jack = self.jack().clone(); + Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) + } + + fn into_port (self) -> Port where Self: Sized; + fn port_name (&self) -> &Arc; + fn port (&self) -> &Port; + fn port_mut (&mut self) -> &mut 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 connections (&self) -> &[Connect]; + fn connect_to_matching <'k> (&'k self) -> Usually<()> { + for connect in self.connections().iter() { + match &connect.name { + Some(Exact(name)) => { + *connect.status.write().unwrap() = self.connect_exact(name)?; + }, + Some(RegExp(re)) => { + *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; + }, + _ => {}, + }; + } + 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: Option + ) -> 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 == Some(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 + })) + } +} + +/// Audio input port. +#[derive(Debug)] pub struct AudioInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +/// Audio output port. +#[derive(Debug)] pub struct AudioOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +/// MIDI input port. +#[derive(Debug)] pub struct MidiInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of currently held notes. + pub held: Arc>, + /// List of ports to connect to. + pub connections: Vec, +} + +/// MIDI output port. +#[derive(Debug)] pub struct MidiOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of currently held notes. + pub held: Arc>, + /// List of ports to connect to. + pub connections: Vec, + /// Buffer + pub note_buffer: Vec, + /// Buffer + pub output_buffer: Vec>>, +} + +macro_rules! jack_port { + ($($Struct:ty = ($Port:ty => $Pair:ty) $({ $($tt:tt)* })?),*) => { + $( + impl HasJack<'static> for $Struct { + fn jack (&self) -> &Jack<'static> { &self.jack } + } + impl JackPort for $Struct { + type Port = $Port; + type Pair = $Pair; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + $($($tt)*)? + } + )* + }; +} + +jack_port!( + AudioInput = (AudioIn => AudioOut) { + const KIND: &'static str = "Audio In"; + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec(), + }; + port.connect_to_matching()?; + Ok(port) + } + }, + + AudioOutput = (AudioOut => AudioIn) { + const KIND: &'static str = "Audio Out"; + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec(), + }; + port.connect_to_matching()?; + Ok(port) + } + }, + + MidiInput = (MidiIn => MidiOut) { + const KIND: &'static str = "MIDI In"; + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec(), + held: Arc::new(RwLock::new([false;128])) + }; + port.connect_to_matching()?; + Ok(port) + } + }, + + MidiOutput = (MidiOut => MidiIn) { + const KIND: &'static str = "MIDI Out"; + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self::register(jack, name)?; + let jack = jack.clone(); + let name = name.as_ref().into(); + let connections = connect.to_vec(); + let port = Self { + jack, + port, + name, + connections, + held: Arc::new([false;128].into()), + note_buffer: vec![0;8], + output_buffer: vec![vec![];65536], + }; + port.connect_to_matching()?; + Ok(port) + } + } +); + +pub type CollectedMidiInput<'a> = Vec, MidiError>)>>; + +/// Trait for thing that may receive MIDI. +pub trait HasMidiIns { + fn midi_ins (&self) -> &Vec; + fn midi_ins_mut (&mut self) -> &mut Vec; + /// Collect MIDI input from app ports (TODO preallocate large buffers) + fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { + self.midi_ins().iter() + .map(|port|port.port().iter(scope) + .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) + .collect::>()) + .collect::>() + } + fn midi_ins_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_ins().iter().enumerate().map(move|(i, input)|{ + let height = 1 + input.connections().len(); + let data = (i, input.port_name(), input.connections(), y, y + height); + y += height; + data + }) + } +} +/// Trait for thing that may output MIDI. +pub trait HasMidiOuts { + fn midi_outs (&self) -> &Vec; + fn midi_outs_mut (&mut self) -> &mut Vec; + fn midi_outs_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_outs().iter().enumerate().map(move|(i, output)|{ + let height = 1 + output.connections().len(); + let data = (i, output.port_name(), output.connections(), y, y + height); + y += height; + data + }) + } + fn midi_outs_emit (&mut self, scope: &ProcessScope) { + for port in self.midi_outs_mut().iter_mut() { + port.buffer_emit(scope) + } + } +} + +impl MidiOutput { + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); + for frame in &mut self.output_buffer[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(&mut self.output_buffer); + } + } + /// Write a note to the output buffer + pub fn buffer_write <'a> ( + &'a mut self, + sample: usize, + event: LiveEvent, + ) { + self.note_buffer.fill(0); + event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); + self.output_buffer[sample].push(self.note_buffer.clone()); + // Update the list of currently held notes. + if let LiveEvent::Midi { ref message, .. } = event { + update_keys(&mut*self.held.write().unwrap(), message); + } + } + /// Write a chunk of MIDI data from the output buffer to the output port. + pub fn buffer_emit (&mut self, scope: &ProcessScope) { + let samples = scope.n_frames() as usize; + let mut writer = self.port.writer(scope); + for (time, events) in self.output_buffer.iter().enumerate().take(samples) { + for bytes in events.iter() { + writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ + panic!("Failed to write MIDI data: {bytes:?}"); + }); + } + } + } +} + +impl MidiInput { + pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { + parse_midi_input(self.port().iter(scope)) + } +} + +/// Return boxed iterator of MIDI events +pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>) + -> Box, &'a [u8])> + 'a> +{ + Box::new(input.map(|::jack::RawMidi { time, bytes }|( + time as usize, + LiveEvent::parse(bytes).unwrap(), + bytes + ))) +} + +/// Add "all notes off" to the start of a buffer. +pub fn all_notes_off (output: &mut [Vec>]) { + let mut buf = vec![]; + let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; + let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; + evt.write(&mut buf).unwrap(); + output[0].push(buf); +} + +/// Update notes_in array +pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { + match message { + MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } + MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, + _ => {} + } +} + +impl> + AsMut>> HasMidiIns for T { + fn midi_ins (&self) -> &Vec { self.as_ref() } + fn midi_ins_mut (&mut self) -> &mut Vec { self.as_mut() } +} + +impl> + AsMut>> HasMidiOuts for T { + fn midi_outs (&self) -> &Vec { self.as_ref() } + fn midi_outs_mut (&mut self) -> &mut Vec { self.as_mut() } +} + +impl> AddMidiIn for T { + fn midi_in_add (&mut self) -> Usually<()> { + let index = self.midi_ins().len(); + let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; + self.midi_ins_mut().push(port); + Ok(()) + } +} + +/// Trail for thing that may gain new MIDI ports. +impl> AddMidiOut for T { + fn midi_out_add (&mut self) -> Usually<()> { + let index = self.midi_outs().len(); + let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; + self.midi_outs_mut().push(port); + Ok(()) + } +} + +/// May create new MIDI input ports. +pub trait AddMidiIn { + fn midi_in_add (&mut self) -> Usually<()>; +} + +/// May create new MIDI output ports. +pub trait AddMidiOut { + fn midi_out_add (&mut self) -> Usually<()>; +} + +#[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, +} + +/// Port connection manager. +/// +/// ``` +/// let connect = tek::Connect::default(); +/// ``` +#[derive(Clone, Debug, Default)] +pub struct Connect { + pub name: Option, + pub scope: Option, + pub status: Arc, Arc, ConnectStatus)>>>, + pub info: Arc, +} + +impl Connect { + + pub fn new > ( + exact: Option>, + re: Option>, + re_all: Option>, + ) -> Vec { + let mut connections = vec![]; + if let Some(exact ) = exact { for port in exact { connections.push(Self::exact(port)) } } + if let Some(regexp) = re { for port in regexp { connections.push(Self::regexp(port)) } } + if let Some(re_all) = re_all { for port in re_all { connections.push(Self::regexp_all(port)) } } + connections + } + + pub fn midi_ins > ( + jack: &Jack<'static>, + name: &T, + midi_from: &[T], + midi_from_re: Option<&[T]>, + ) -> Usually> { + Ok(Connect::new( + Some(midi_from.into_iter()), + Some([].into_iter()), + midi_from_re.map(|x|x.into_iter())).iter().enumerate() + .map(|(index, connect)|jack.midi_in(&format!("{}/{index}", name.as_ref()), &[connect.clone()])) + .collect::>()?) + } + + pub fn midi_outs > ( + jack: &Jack<'static>, + name: &T, + midi_to: &[T], + midi_to_re: Option<&[T]>, + ) -> Usually> { + Ok(Connect::new( + Some(midi_to.into_iter()), + Some([].into_iter()), + midi_to_re.map(|x|x.into_iter())).iter().enumerate() + .map(|(index, connect)|jack.midi_out(&format!("{index}/{}", name.as_ref()), &[connect.clone()])) + .collect::>()?) + } + + pub fn audio_ins > ( + jack: &Jack<'static>, + name: &T, + audio_from: &[T], + audio_from_re: Option<&[T]>, + ) -> Usually> { + Ok(Connect::new( + Some(audio_from.into_iter()), + Some([].into_iter()), + audio_from_re.map(|x|x.into_iter())).iter().enumerate() + .map(|(index, connect)|jack.audio_in(&format!("{}/{index}", name.as_ref()), &[connect.clone()])) + .collect::>()?) + } + + pub fn audio_outs > ( + jack: &Jack<'static>, + name: &T, + audio_to: &[T], + audio_to_re: Option<&[T]>, + ) -> Usually> { + Ok(Connect::new( + Some(audio_to.into_iter()), + Some([].into_iter()), + audio_to_re.map(|x|x.into_iter())).iter().enumerate() + .map(|(index, connect)|jack.audio_out(&format!("{index}/{}", name.as_ref()), &[connect.clone()])) + .collect::>()?) + } + + /// Connect to this exact port + pub fn exact (name: impl AsRef) -> Self { + let info = format!("=:{}", name.as_ref()).into(); + let name = Some(Exact(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + + pub fn regexp (name: impl AsRef) -> Self { + let info = format!("~:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(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 = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } + } + + pub fn info (&self) -> Arc { + format!(" ({}) {} {}", { + let status = self.status.read().unwrap(); + let mut ok = 0; + for (_, _, state) in status.iter() { + if *state == Connected { + ok += 1 + } + } + format!("{ok}/{}", status.len()) + }, match self.scope { + None => "x", + Some(One) => " ", + Some(All) => "*", + }, match &self.name { + None => format!("x"), + Some(Exact(name)) => format!("= {name}"), + Some(RegExp(name)) => format!("~ {name}"), + }).into() + } } diff --git a/src/sing/jack.rs b/src/sing/jack.rs index 0100fed..e69de29 100644 --- a/src/sing/jack.rs +++ b/src/sing/jack.rs @@ -1,112 +0,0 @@ -use crate::{*, PerfModel}; -pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; - -use JackState::*; - -/// Wraps [JackState], and through it [jack::Client] when connected. -/// -/// ``` -/// let jack = tengri::Jack::default(); -/// ``` -#[derive(Clone, Debug, Default)] pub struct Jack<'j> ( - pub(crate) Arc>> -); - -/// 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. -/// -/// ``` -/// let state = tengri::JackState::default(); -/// ``` -#[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>), -} - -/// 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 { - // 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. - let notify = JackNotify(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. - let process = ::jack::contrib::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); - // Launch a client with the two handlers. - *client_state.write().unwrap() = Active( - client.activate_async(notify, process)? - ); - } 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(client) => op(client), - Activating => panic!("jack client has not finished activation"), - Active(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 - } -} - -impl<'j, T: HasJack<'j>> HasJack<'j> for Arc { - fn jack (&self) -> &Jack<'j> { - (&**self).jack() - } -} diff --git a/src/sing/jack_event.rs b/src/sing/jack_event.rs index cfeae6c..e69de29 100644 --- a/src/sing/jack_event.rs +++ b/src/sing/jack_event.rs @@ -1,72 +0,0 @@ -use crate::{*, time::PerfModel}; -pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; - -/// Event enum for JACK events. -/// -/// ``` -/// let event = tengri::JackEvent::XRun; // kerpop -/// ``` -#[derive(Debug, Clone, PartialEq)] pub enum JackEvent { - ThreadInit, - Shutdown(ClientStatus, Arc), - Freewheel(bool), - SampleRate(Frames), - ClientRegistration(Arc, bool), - PortRegistration(PortId, bool), - PortRename(PortId, Arc, Arc), - PortsConnected(PortId, PortId, bool), - GraphReorder, - XRun, -} - -/// Generic notification handler that emits [JackEvent] -/// -/// ``` -/// let notify = tengri::JackNotify(|_|{}); -/// ``` -pub struct JackNotify(pub T); - -/// Notification handler wrapper for [BoxedJackEventHandler]. -pub type DynamicNotifications<'j> = - JackNotify>; - -/// Boxed [JackEvent] callback. -pub type BoxedJackEventHandler<'j> = - Box; - -impl NotificationHandler for JackNotify { - fn thread_init(&self, _: &Client) { - self.0(JackEvent::ThreadInit); - } - unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { - self.0(JackEvent::Shutdown(status, reason.into())); - } - fn freewheel(&mut self, _: &Client, enabled: bool) { - self.0(JackEvent::Freewheel(enabled)); - } - fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { - self.0(JackEvent::SampleRate(frames)); - Control::Quit - } - fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { - self.0(JackEvent::ClientRegistration(name.into(), reg)); - } - fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { - self.0(JackEvent::PortRegistration(id, reg)); - } - fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { - self.0(JackEvent::PortRename(id, old.into(), new.into())); - Control::Continue - } - fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { - self.0(JackEvent::PortsConnected(a, b, are)); - } - fn graph_reorder(&mut self, _: &Client) -> Control { - self.0(JackEvent::GraphReorder); - Control::Continue - } - fn xrun(&mut self, _: &Client) -> Control { - self.0(JackEvent::XRun); - Control::Continue - } -} diff --git a/src/sing/jack_perf.rs b/src/sing/jack_perf.rs index ee0d7d6..e69de29 100644 --- a/src/sing/jack_perf.rs +++ b/src/sing/jack_perf.rs @@ -1,22 +0,0 @@ -use crate::{*, time::PerfModel}; -pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; - -pub trait JackPerfModel { - fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope); -} - -impl JackPerfModel for PerfModel { - fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope) { - if let Some(t0) = t0 { - let t1 = self.clock.raw(); - self.used.store( - self.clock.delta_as_nanos(t0, t1) as f64, - Relaxed, - ); - self.window.store( - scope.cycle_times().unwrap().period_usecs as f64, - Relaxed, - ); - } - } -} diff --git a/src/term.rs b/src/term.rs index 420d0eb..7feaa97 100644 --- a/src/term.rs +++ b/src/term.rs @@ -29,13 +29,13 @@ } } -#[macro_export] macro_rules! tui_ns { - ($self:ident: $State:ident, $to:pat, $pat:ident $body:expr) => { - impl Interpret>> for $State { - fn interpret_word <'a> (&'a $self, $to: &mut Tui, $pat: &'a impl Symbol) -> Drawn { - $body +#[macro_export] macro_rules! tui_interpret { + ($self:ident: $State:ident, $to:pat, $pat:ident -> $Result:ty { $($body:tt)+ }) => { + impl Interpret for $State { + fn interpret_word <'a> (&'a $self, $to: &mut Tui, $pat: &'a impl Symbol) -> Usually<$Result> { + $($body)+ } - fn interpret_expr <'a> (&'a self, to: &mut Tui, src: &'a impl Expression) -> Drawn { + fn interpret_expr <'a> (&'a self, to: &mut Tui, src: &'a impl Expression) -> Usually<$Result> { Ok(Some(if let Some(area) = eval_view(self, to, src)? { area } else if let Some(area) = eval_view_tui(self, to, src)? { @@ -50,9 +50,9 @@ /// Enable TUI keyboard input for main state struct. #[macro_export] macro_rules! tui_keys { - ($self:ident:$State:ty,$input:ident $body:block) => { + ($self:ident:$State:ty,$input:ident $($body:tt)+) => { impl Apply> for $State { - fn apply (&mut $self, $input: &TuiEvent) -> Usually<()> $body + fn apply (&mut $self, $input: &TuiEvent) -> Usually<()> $($body)+ } }; } @@ -154,23 +154,10 @@ impl Tui { let state = state.clone(); Task::new_poll(exited.clone(), poll, move |_| { let event = read().unwrap(); - match event { - - // Hardcoded exit. - Event::Key(KeyEvent { - modifiers: KeyModifiers::CONTROL, - code: KeyCode::Char('c'), - kind: KeyEventKind::Press, - state: KeyEventState::NONE - }) => { exited.store(true, Relaxed); }, - - // Handle all other events by the state: - event => { - if let Err(e) = state.write().unwrap().apply(&TuiEvent(event)) { - panic!("{e}") - } - }, - + if Exit::is(&event) { + exited.store(true, Relaxed); + } else if let Err(e) = state.write().unwrap().apply(&TuiEvent(event)) { + panic!("{e}") } }) } diff --git a/src/term/colors.rs b/src/term/colors.rs index 5298746..f224b69 100644 --- a/src/term/colors.rs +++ b/src/term/colors.rs @@ -2,21 +2,24 @@ use crate::*; use ratatui::prelude::Color; use dizzle::{Ostensibly, Expression, LanguageError::*}; -pub trait ColorDsl: Sized { - fn new_g (expr: T, try_to_u8: impl Fn(Ostensibly<&str>)->Ostensibly) -> Ostensibly; - fn new_rgb (expr: T, try_to_u8: impl Fn(Ostensibly<&str>)->Ostensibly) -> Ostensibly; +pub trait ColorDsl: Sized { + fn new_g (expr: T, try_to_u8: impl Fn(Perhaps<&str>)->Perhaps) -> Usually; + fn new_rgb (expr: T, try_to_u8: impl Fn(Perhaps<&str>)->Perhaps) -> Usually; } -impl ColorDsl for Color { - fn new_g (expr: T, try_to_u8: impl Fn(Ostensibly<&str>)->Ostensibly) -> Ostensibly { +impl ColorDsl for Color { + fn new_g (expr: T, try_to_u8: impl Fn(Perhaps<&str>)->Perhaps) -> Usually { let n = try_to_u8(expr.tail().map_err(Into::into))?.ok_or(Domain("not gray"))?; - Ok(Some(Self::Rgb(n, n, n))) + Ok(Self::Rgb(n, n, n)) } - fn new_rgb (expr: T, try_to_u8: impl Fn(Ostensibly<&str>)->Ostensibly) -> Ostensibly { - let r = try_to_u8(expr.tail().map_err(Into::into))?.ok_or(Domain("not red"))?; - let g = try_to_u8(expr.tail().map_err(Into::into).tail().head())?.ok_or(Domain("not green"))?; - let b = try_to_u8(expr.tail().map_err(Into::into).tail().tail().head())?.ok_or(Domain("not blue"))?; - Ok(Some(Color::Rgb(r, g, b))) + fn new_rgb (expr: T, try_to_u8: impl Fn(Perhaps<&str>)->Perhaps) -> Usually { + let r = try_to_u8(expr.tail().map_err(Into::into))? + .ok_or(Domain("not red"))?; + let g = try_to_u8(expr.tail().tail().head().map_err(Into::into))? + .ok_or(Domain("not green"))?; + let b = try_to_u8(expr.tail().tail().tail().head().map_err(Into::into))? + .ok_or(Domain("not blue"))?; + Ok(Color::Rgb(r, g, b)) } }