From 2923d0fed8e7489a537193277f78da48563fca8d Mon Sep 17 00:00:00 2001 From: Will Date: Sat, 14 Sep 2024 08:14:38 -0700 Subject: [PATCH] Move closure handler to contrib. (#211) * Move closure handler to contrib. * Add documentation for closure API. --- .github/workflows/testing.yml | 4 ++ docs/contrib/closure_callbacks.md | 79 ++++++++++++++++++++ docs/contrib/index.md | 10 +++ docs/quickstart.md | 2 +- examples/playback_capture.rs | 2 +- examples/show_midi.rs | 2 +- examples/sine.rs | 37 ++++++---- src/client/async_client.rs | 2 +- src/client/callbacks.rs | 2 +- src/client/handler_impls.rs | 23 +----- src/client/mod.rs | 1 + src/client/test.rs | 3 +- src/contrib/closure.rs | 116 ++++++++++++++++++++++++++++++ src/lib.rs | 13 +++- src/port/audio.rs | 2 +- src/port/midi.rs | 2 +- 16 files changed, 256 insertions(+), 44 deletions(-) create mode 100644 docs/contrib/closure_callbacks.md create mode 100644 docs/contrib/index.md create mode 100644 src/contrib/closure.rs diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 1108e5164..4caa1de69 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -26,5 +26,9 @@ jobs: run: cargo build --verbose --no-default-features - name: Build (metadata) run: cargo build --verbose --no-default-features --features metadata + - name: Build (examples) + run: cargo build --verbose --examples - name: Run Tests run: cargo nextest run --all-features + - name: Run Doc Tests + run: cargo doc && cargo test --doc diff --git a/docs/contrib/closure_callbacks.md b/docs/contrib/closure_callbacks.md new file mode 100644 index 000000000..dd4268273 --- /dev/null +++ b/docs/contrib/closure_callbacks.md @@ -0,0 +1,79 @@ +--- +layout: page +title: Closure Callbacks +parent: Contrib +permalink: /closure-callbacks +nav_order: 1 +--- + +# Closure Callbacks + +Closure callbacks allow you to define functionality inline. + +## Process Closure + +The typical use case for a process closure involves creating a closure that +contains captures the required state and then activating it. + +```rust +// 1. Create the client. +let (client, _status) = + jack::Client::new("silence", jack::ClientOptions::NO_START_SERVER).unwrap(); + +// 2. Define the state. +let mut output = client.register_port("out", jack::AudioOut::default()); +let silence_value = 0.0; + +// 3. Define the closure. Use `move` to capture the required state. +let process_callback = move |_: &jack::Client, ps: &jack::ProcessScope| -> jack::Control { + output.as_mut_slice(ps).fill(silence_value); + jack::Control::Continue +}; + +// 4. Start processing. +let process = jack::contrib::ClosureProcessHandler::new(process_callback); +let active_client = client.activate_async((), process).unwrap(); +``` + +## State + Process Closure + Buffer Closure + +`jack::contrib::ClosureProcessHandler` also allows defining a buffer size +callback that can share state with the process callback. The buffer size +callback is useful as it allows the handler to adapt to any changes in the +buffer size. + +```rust +// 1. Create the client. +let (client, _status) = + jack::Client::new("silence", jack::ClientOptions::NO_START_SERVER).unwrap(); + +// 2. Define the state. +struct State { + silence: Vec, + output: jack::Port, +} +let state = State { + silence: Vec::new(), + output: client + .register_port("out", jack::AudioOut::default()) + .unwrap(), +}; + +// 3. Define the state and closure. +let process_callback = |state: &mut State, _: &jack::Client, ps: &jack::ProcessScope| { + state + .output + .as_mut_slice(ps) + .copy_from_slice(state.silence.as_slice()); + jack::Control::Continue +}; +let buffer_callback = |state: &mut State, _: &jack::Client, len: jack::Frames| { + state.silence = vec![0f32; len as usize]; + jack::Control::Continue +}; + +// 4. Start processing. +let process = + jack::contrib::ClosureProcessHandler::with_state(state, process_callback, buffer_callback); +let active_client = client.activate_async((), process).unwrap(); +``` diff --git a/docs/contrib/index.md b/docs/contrib/index.md new file mode 100644 index 000000000..713d19a1f --- /dev/null +++ b/docs/contrib/index.md @@ -0,0 +1,10 @@ +--- +layout: page +title: Contrib +permalink: /contrib +nav_order: 3 +--- + +# Contrib + +`jack::contrib` contains convenient but optional utilities. diff --git a/docs/quickstart.md b/docs/quickstart.md index 517039710..a9c795794 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -80,7 +80,7 @@ fn main() { out_b_p.clone_from_slice(in_b_p); jack::Control::Continue }; - let process = jack::ClosureProcessHandler::new(process_callback); + let process = jack::contrib::ClosureProcessHandler::new(process_callback); // 3. Activate the client, which starts the processing. let active_client = client.activate_async((), process).unwrap(); diff --git a/examples/playback_capture.rs b/examples/playback_capture.rs index c69784027..38ccc28f5 100644 --- a/examples/playback_capture.rs +++ b/examples/playback_capture.rs @@ -31,7 +31,7 @@ fn main() { out_b_p.clone_from_slice(in_b_p); jack::Control::Continue }; - let process = jack::ClosureProcessHandler::new(process_callback); + let process = jack::contrib::ClosureProcessHandler::new(process_callback); // Activate the client, which starts the processing. let active_client = client.activate_async(Notifications, process).unwrap(); diff --git a/examples/show_midi.rs b/examples/show_midi.rs index fbffd25e3..01a6170c1 100644 --- a/examples/show_midi.rs +++ b/examples/show_midi.rs @@ -89,7 +89,7 @@ fn main() { // Activate let active_client = client - .activate_async((), jack::ClosureProcessHandler::new(cback)) + .activate_async((), jack::contrib::ClosureProcessHandler::new(cback)) .unwrap(); // Spawn a non-real-time thread that prints out the midi messages we get. diff --git a/examples/sine.rs b/examples/sine.rs index 8db9ed533..10d78cc61 100644 --- a/examples/sine.rs +++ b/examples/sine.rs @@ -11,38 +11,49 @@ fn main() { jack::Client::new("rust_jack_sine", jack::ClientOptions::NO_START_SERVER).unwrap(); // 2. register port - let mut out_port = client + let out_port = client .register_port("sine_out", jack::AudioOut::default()) .unwrap(); // 3. define process callback handler - let mut frequency = 220.0; - let sample_rate = client.sample_rate(); - let frame_t = 1.0 / sample_rate as f64; - let mut time = 0.0; let (tx, rx) = bounded(1_000_000); - let process = jack::ClosureProcessHandler::new( - move |_: &jack::Client, ps: &jack::ProcessScope| -> jack::Control { + struct State { + out_port: jack::Port, + rx: crossbeam_channel::Receiver, + frequency: f64, + frame_t: f64, + time: f64, + } + let process = jack::contrib::ClosureProcessHandler::with_state( + State { + out_port, + rx, + frequency: 220.0, + frame_t: 1.0 / client.sample_rate() as f64, + time: 0.0, + }, + |state, _, ps| -> jack::Control { // Get output buffer - let out = out_port.as_mut_slice(ps); + let out = state.out_port.as_mut_slice(ps); // Check frequency requests - while let Ok(f) = rx.try_recv() { - time = 0.0; - frequency = f; + while let Ok(f) = state.rx.try_recv() { + state.time = 0.0; + state.frequency = f; } // Write output for v in out.iter_mut() { - let x = frequency * time * 2.0 * std::f64::consts::PI; + let x = state.frequency * state.time * 2.0 * std::f64::consts::PI; let y = x.sin(); *v = y as f32; - time += frame_t; + state.time += state.frame_t; } // Continue as normal jack::Control::Continue }, + move |_, _, _| jack::Control::Continue, ); // 4. Activate the client. Also connect the ports to the system audio. diff --git a/src/client/async_client.rs b/src/client/async_client.rs index d22c388f4..e09ba22bd 100644 --- a/src/client/async_client.rs +++ b/src/client/async_client.rs @@ -21,7 +21,7 @@ use crate::Error; /// // Create a client and a handler /// let (client, _status) = /// jack::Client::new("my_client", jack::ClientOptions::NO_START_SERVER).unwrap(); -/// let process_handler = jack::ClosureProcessHandler::new( +/// let process_handler = jack::contrib::ClosureProcessHandler::new( /// move |_: &jack::Client, _: &jack::ProcessScope| jack::Control::Continue, /// ); /// diff --git a/src/client/callbacks.rs b/src/client/callbacks.rs index a273a3e0c..eb34d2534 100644 --- a/src/client/callbacks.rs +++ b/src/client/callbacks.rs @@ -22,7 +22,7 @@ pub trait NotificationHandler: Send { /// pipe so that the rest of the application knows that the JACK client thread has shut down. /// /// # Safety - /// See https://man7.org/linux/man-pages/man7/signal-safety.7.html for details about + /// See for details about /// what is legal in an async-signal-safe callback. unsafe fn shutdown(&mut self, _status: ClientStatus, _reason: &str) {} diff --git a/src/client/handler_impls.rs b/src/client/handler_impls.rs index 7c619076f..6548d610c 100644 --- a/src/client/handler_impls.rs +++ b/src/client/handler_impls.rs @@ -13,24 +13,5 @@ impl ProcessHandler for () { /// Wrap a closure that can handle the `process` callback. This is called every time data from ports /// is available from JACK. -pub struct ClosureProcessHandler Control> { - pub process_fn: F, -} - -impl ClosureProcessHandler -where - F: 'static + Send + FnMut(&Client, &ProcessScope) -> Control, -{ - pub fn new(f: F) -> ClosureProcessHandler { - ClosureProcessHandler { process_fn: f } - } -} - -impl ProcessHandler for ClosureProcessHandler -where - F: 'static + Send + FnMut(&Client, &ProcessScope) -> Control, -{ - fn process(&mut self, c: &Client, ps: &ProcessScope) -> Control { - (self.process_fn)(c, ps) - } -} +#[deprecated = "Prefer using jack::contrib::ClosureProcessHandler directly."] +pub type ClosureProcessHandler = crate::contrib::ClosureProcessHandler<(), F>; diff --git a/src/client/mod.rs b/src/client/mod.rs index 971513bcf..e78d2705d 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -17,6 +17,7 @@ pub use self::client_options::ClientOptions; pub use self::client_status::ClientStatus; pub use self::common::CLIENT_NAME_SIZE; +#[allow(deprecated)] pub use self::handler_impls::ClosureProcessHandler; // client.rs excluding functionality that involves ports or callbacks diff --git a/src/client/test.rs b/src/client/test.rs index 3eb10f753..137bae154 100644 --- a/src/client/test.rs +++ b/src/client/test.rs @@ -1,6 +1,7 @@ use crate::client::*; +use crate::contrib::ClosureProcessHandler; use crate::jack_enums::Error; -use crate::{ClosureProcessHandler, Control, RingBuffer}; +use crate::{Control, RingBuffer}; fn open_test_client(name: &str) -> (Client, ClientStatus) { Client::new(name, ClientOptions::NO_START_SERVER).unwrap() diff --git a/src/contrib/closure.rs b/src/contrib/closure.rs new file mode 100644 index 000000000..ee407ad70 --- /dev/null +++ b/src/contrib/closure.rs @@ -0,0 +1,116 @@ +use crate::{Client, Control, Frames, ProcessHandler, ProcessScope}; + +/// Wrap a closure that can handle the `process` callback. This is called every time data from ports +/// is available from JACK. +pub struct ClosureProcessHandler { + pub state: T, + pub callbacks: F, +} + +impl ClosureProcessHandler<(), ProcessCallback> +where + ProcessCallback: 'static + Send + FnMut(&Client, &ProcessScope) -> Control, +{ + /// Create a new `jack::ProcessHandler` with the given process callback. + /// + /// ```rust + /// // Run one cycle of processing + /// let mut has_run = false; + /// let handler = jack::contrib::ClosureProcessHandler::new(move |_client, _process_scope| { + /// if has_run { + /// jack::Control::Quit + /// } else { + /// has_run = true; + /// jack::Control::Continue + /// } + /// }); + /// ``` + pub fn new(process_callback: ProcessCallback) -> Self { + ClosureProcessHandler { + state: (), + callbacks: process_callback, + } + } +} + +impl ProcessHandler for ClosureProcessHandler<(), ProcessCallback> +where + ProcessCallback: 'static + Send + FnMut(&Client, &ProcessScope) -> Control, +{ + fn process(&mut self, c: &Client, ps: &ProcessScope) -> Control { + (self.callbacks)(c, ps) + } +} + +pub struct ProcessCallbacks { + process: ProcessCallback, + buffer: BufferCallback, +} + +impl + ClosureProcessHandler> +where + T: Send, + ProcessCallback: 'static + Send + FnMut(&mut T, &Client, &ProcessScope) -> Control, + BufferCallback: 'static + Send + FnMut(&mut T, &Client, Frames) -> Control, +{ + /// Create a new `jack::ProcessHandler` with some state. + /// + /// ```rust + /// // 1. Create the client. + /// let (client, _status) = jack::Client::new("silence", jack::ClientOptions::NO_START_SERVER).unwrap(); + /// + /// // 2. Define the state. + /// struct State{ + /// silence: Vec, + /// output: jack::Port, + /// } + /// let state = State{ + /// silence: Vec::new(), + /// output: client.register_port("out", jack::AudioOut::default()).unwrap(), + /// }; + /// + /// // 3. Define the state and closure. + /// let process_callback = |state: &mut State, _: &jack::Client, ps: &jack::ProcessScope| -> jack::Control { + /// state.output.as_mut_slice(ps).copy_from_slice(state.silence.as_slice()); + /// jack::Control::Continue + /// }; + /// let buffer_callback = |state: &mut State, _: &jack::Client, len: jack::Frames| -> jack::Control { + /// state.silence = vec![0f32; len as usize]; + /// jack::Control::Continue + /// }; + /// + /// // 4. Start processing. + /// let process = jack::contrib::ClosureProcessHandler::with_state(state, process_callback, buffer_callback); + /// let active_client = client.activate_async((), process).unwrap(); + /// ``` + pub fn with_state( + state: T, + process_callback: ProcessCallback, + buffer_callback: BufferCallback, + ) -> Self { + ClosureProcessHandler { + state, + callbacks: ProcessCallbacks { + process: process_callback, + buffer: buffer_callback, + }, + } + } +} + +impl ProcessHandler + for ClosureProcessHandler> +where + T: Send, + ProcessCallback: 'static + Send + FnMut(&mut T, &Client, &ProcessScope) -> Control, + BufferCallback: 'static + Send + FnMut(&mut T, &Client, Frames) -> Control, +{ + fn process(&mut self, c: &Client, ps: &ProcessScope) -> Control { + (self.callbacks.process)(&mut self.state, c, ps) + } + + fn buffer_size(&mut self, c: &Client, size: Frames) -> Control { + (self.callbacks.buffer)(&mut self.state, c, size) + } +} diff --git a/src/lib.rs b/src/lib.rs index 45bb393c9..de767d4ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,9 +33,11 @@ //! callback. For example, `Port::as_mut_slice` returns a audio buffer that can be written //! to. +#[allow(deprecated)] +pub use crate::client::ClosureProcessHandler; pub use crate::client::{ - AsyncClient, Client, ClientOptions, ClientStatus, ClosureProcessHandler, CycleTimes, - InternalClientID, NotificationHandler, ProcessHandler, ProcessScope, CLIENT_NAME_SIZE, + AsyncClient, Client, ClientOptions, ClientStatus, CycleTimes, InternalClientID, + NotificationHandler, ProcessHandler, ProcessScope, CLIENT_NAME_SIZE, }; pub use crate::jack_enums::{Control, Error, LatencyType}; pub use crate::logging::{set_logger, LoggerType}; @@ -68,6 +70,13 @@ mod properties; mod ringbuffer; mod transport; +/// A collection of useful but optional functionality. +pub mod contrib { + mod closure; + + pub use closure::ClosureProcessHandler; +} + static TIME_CLIENT: std::sync::LazyLock = std::sync::LazyLock::new(|| { Client::new("deprecated_get_time", ClientOptions::NO_START_SERVER) .unwrap() diff --git a/src/port/audio.rs b/src/port/audio.rs index 61bceeb97..3a328670c 100644 --- a/src/port/audio.rs +++ b/src/port/audio.rs @@ -100,7 +100,7 @@ impl Port { #[cfg(test)] mod test { use super::*; - use crate::{Client, ClientOptions, ClosureProcessHandler, Control}; + use crate::{contrib::ClosureProcessHandler, Client, ClientOptions, Control}; fn open_test_client(name: &str) -> Client { Client::new(name, ClientOptions::NO_START_SERVER).unwrap().0 diff --git a/src/port/midi.rs b/src/port/midi.rs index 774e1a555..841e43181 100644 --- a/src/port/midi.rs +++ b/src/port/midi.rs @@ -213,8 +213,8 @@ impl<'a> MidiWriter<'a> { mod test { use super::*; use crate::client::Client; - use crate::client::ClosureProcessHandler; use crate::client::ProcessHandler; + use crate::contrib::ClosureProcessHandler; use crate::jack_enums::Control; use crate::primitive_types::Frames; use crate::ClientOptions;