Skip to content

Commit

Permalink
Move closure handler to contrib. (#211)
Browse files Browse the repository at this point in the history
* Move closure handler to contrib.
* Add documentation for closure API.
  • Loading branch information
wmedrano authored Sep 14, 2024
1 parent cb708b2 commit 2923d0f
Show file tree
Hide file tree
Showing 16 changed files with 256 additions and 44 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
79 changes: 79 additions & 0 deletions docs/contrib/closure_callbacks.md
Original file line number Diff line number Diff line change
@@ -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<f32>,
output: jack::Port<jack::AudioOut>,
}
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();
```
10 changes: 10 additions & 0 deletions docs/contrib/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
layout: page
title: Contrib
permalink: /contrib
nav_order: 3
---

# Contrib

`jack::contrib` contains convenient but optional utilities.
2 changes: 1 addition & 1 deletion docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion examples/playback_capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion examples/show_midi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
37 changes: 24 additions & 13 deletions examples/sine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<jack::AudioOut>,
rx: crossbeam_channel::Receiver<f64>,
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.
Expand Down
2 changes: 1 addition & 1 deletion src/client/async_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
/// );
///
Expand Down
2 changes: 1 addition & 1 deletion src/client/callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://man7.org/linux/man-pages/man7/signal-safety.7.html> for details about
/// what is legal in an async-signal-safe callback.
unsafe fn shutdown(&mut self, _status: ClientStatus, _reason: &str) {}

Expand Down
23 changes: 2 additions & 21 deletions src/client/handler_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<F: 'static + Send + FnMut(&Client, &ProcessScope) -> Control> {
pub process_fn: F,
}

impl<F> ClosureProcessHandler<F>
where
F: 'static + Send + FnMut(&Client, &ProcessScope) -> Control,
{
pub fn new(f: F) -> ClosureProcessHandler<F> {
ClosureProcessHandler { process_fn: f }
}
}

impl<F> ProcessHandler for ClosureProcessHandler<F>
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<F> = crate::contrib::ClosureProcessHandler<(), F>;
1 change: 1 addition & 0 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/client/test.rs
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
116 changes: 116 additions & 0 deletions src/contrib/closure.rs
Original file line number Diff line number Diff line change
@@ -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<T, F> {
pub state: T,
pub callbacks: F,
}

impl<ProcessCallback> 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<ProcessCallback> 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<ProcessCallback, BufferCallback> {
process: ProcessCallback,
buffer: BufferCallback,
}

impl<T, ProcessCallback, BufferCallback>
ClosureProcessHandler<T, ProcessCallbacks<ProcessCallback, BufferCallback>>
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<f32>,
/// output: jack::Port<jack::AudioOut>,
/// }
/// 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<T, ProcessCallback, BufferCallback> ProcessHandler
for ClosureProcessHandler<T, ProcessCallbacks<ProcessCallback, BufferCallback>>
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)
}
}
13 changes: 11 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@
//! callback. For example, `Port<AudioIn>::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};
Expand Down Expand Up @@ -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<Client> = std::sync::LazyLock::new(|| {
Client::new("deprecated_get_time", ClientOptions::NO_START_SERVER)
.unwrap()
Expand Down
Loading

0 comments on commit 2923d0f

Please sign in to comment.