Skip to content

Commit 4db3933

Browse files
authored
Add mechanism to control processing from a separate thread (#230)
1 parent 0ea7f5f commit 4db3933

10 files changed

Lines changed: 596 additions & 58 deletions

File tree

.github/workflows/site.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,6 @@ jobs:
2828
steps:
2929
- name: Checkout
3030
uses: actions/checkout@v4
31-
- name: Setup Ruby
32-
uses: ruby/setup-ruby@v1
33-
with:
34-
ruby-version: 3.0
35-
bundler-cache: true
36-
cache-version: 0 # Increment this number to re-download cached gems.
3731
- name: Setup Pages
3832
id: pages
3933
uses: actions/configure-pages@v5

.github/workflows/testing.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ jobs:
2626
run: cargo build --verbose
2727
- name: Build (No Features)
2828
run: cargo build --verbose --no-default-features
29+
- name: Build (All Features)
30+
run: cargo build --verbose --all-features
2931
- name: Build (examples)
30-
run: cargo build --verbose --examples
32+
run: cargo build --verbose --examples --all-features
3133
- name: Run Tests (Default Features)
3234
run: cargo nextest run
3335
- name: Run Doc Tests

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jack-sys = {version = "0.5", path = "./jack-sys"}
1616
lazy_static = "1.4"
1717
libc = "0.2"
1818
log = { version = "0.4", optional = true}
19+
rtrb = { version = "0.3.2", optional = true }
1920

2021
[dev-dependencies]
2122
approx = "0.5"
@@ -25,3 +26,8 @@ ctor = "0.2"
2526
[features]
2627
default = ["dynamic_loading", "log"]
2728
dynamic_loading = ["jack-sys/dynamic_loading"]
29+
controller = ["rtrb"]
30+
31+
[[example]]
32+
name = "controlled_sine"
33+
required-features = ["controller"]

README.md

Lines changed: 75 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,102 @@
11
# JACK (for Rust)
22

3-
Rust bindings for [JACK Audio Connection Kit](<https://jackaudio.org>).
3+
Rust bindings for [JACK Audio Connection Kit](https://jackaudio.org).
44

5-
| [![Crates.io](https://img.shields.io/crates/v/jack.svg)](https://crates.io/crates/jack) | [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) |
6-
|-----------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
7-
| [![Docs.rs](https://docs.rs/jack/badge.svg)](https://docs.rs/jack) | [![Test](https://github.com/RustAudio/rust-jack/actions/workflows/testing.yml/badge.svg)](https://github.com/RustAudio/rust-jack/actions/workflows/testing.yml) |
8-
| [📚 Documentation](https://rustaudio.github.io/rust-jack) | [:heart: Sponsor](<https://github.com/sponsors/wmedrano>) |
5+
[![Crates.io](https://img.shields.io/crates/v/jack.svg)](https://crates.io/crates/jack)
6+
[![Docs.rs](https://docs.rs/jack/badge.svg)](https://docs.rs/jack)
7+
[![Test](https://github.com/RustAudio/rust-jack/actions/workflows/testing.yml/badge.svg)](https://github.com/RustAudio/rust-jack/actions/workflows/testing.yml)
8+
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9+
[:heart: Sponsor](https://github.com/sponsors/wmedrano)
910

10-
## Using JACK
11+
## Overview
1112

13+
JACK is a low-latency audio server that allows multiple applications to share
14+
audio and MIDI devices and route signals between each other. This crate provides
15+
safe Rust bindings to create JACK clients that can process audio and MIDI in
16+
real-time.
1217

13-
The JACK server is usually started by the user or system. Clients can request
14-
that the JACK server is started on demand when they connect, but this can be
15-
disabled by creating a client with the `NO_START_SERVER` option or
16-
`ClientOptions::default()`.
18+
## Documentation
1719

18-
- Linux and BSD users may install JACK1, JACK2 (preferred for low latency), or
19-
Pipewire JACK (preferred for ease of use) from their system package manager.
20-
- Windows users may install JACK from the [official
21-
website](<http://jackaudio.org/downloads/>) or [Chocolatey](<https://community.chocolatey.org/packages/jack>).
22-
- MacOS users may install JACK from the [official
23-
website](<http://jackaudio.org/downloads/>) or [Homebrew](<https://formulae.brew.sh/formula/jack>).
20+
- [Guide](https://rustaudio.github.io/rust-jack) - Quickstart, features, and tutorials
21+
- [API Reference](https://docs.rs/jack/) - Complete API documentation
2422

25-
Refer to the [docs.rs documentation](<https://docs.rs/jack/>) for details about
26-
the API. For more general documentation, visit <https://rustaudio.github.io/rust-jack>.
23+
## Quick Example
2724

25+
```rust
26+
use std::io;
27+
28+
fn main() {
29+
// Create a JACK client
30+
let (client, _status) =
31+
jack::Client::new("rust_jack_simple", jack::ClientOptions::default()).unwrap();
32+
33+
// Register input and output ports
34+
let in_port = client
35+
.register_port("input", jack::AudioIn::default())
36+
.unwrap();
37+
let mut out_port = client
38+
.register_port("output", jack::AudioOut::default())
39+
.unwrap();
40+
41+
// Create a processing callback that copies input to output
42+
let process = jack::contrib::ClosureProcessHandler::new(
43+
move |_: &jack::Client, ps: &jack::ProcessScope| -> jack::Control {
44+
out_port.as_mut_slice(ps).clone_from_slice(in_port.as_slice(ps));
45+
jack::Control::Continue
46+
},
47+
);
48+
49+
// Activate the client
50+
let _active_client = client.activate_async((), process).unwrap();
51+
52+
// Wait for user to quit
53+
println!("Press enter to quit...");
54+
let mut input = String::new();
55+
io::stdin().read_line(&mut input).ok();
56+
}
57+
```
2858

29-
## FAQ
59+
See the [examples](examples/) directory for more.
3060

31-
### How do I return an `AsyncClient` with many generics?
61+
## Installation
3262

33-
This is especially useful when using `jack::contrib::ClosureProcessHandler`
34-
which may have an innaccessible type.
63+
Add to your `Cargo.toml`:
3564

36-
```rust
37-
// Shortest and allows access to the underlying client.
38-
fn make_client() -> impl AsRef<jack::Client> {
39-
todo!()
40-
}
65+
```toml
66+
[dependencies]
67+
jack = "0.13"
68+
```
4169

42-
// With extra bounds
43-
fn make_client() -> impl 'static + AsRef<jack::Client> {
44-
todo!();
45-
}
70+
### JACK Server Setup
4671

47-
// For the full async client
48-
fn async_client() -> impl jack::AsyncClient<impl Any, impl Any> {
49-
todo!();
50-
}
51-
```
72+
A JACK server must be running for clients to connect. Install one of:
5273

53-
# Testing
74+
- **Linux/BSD**: JACK2 (lowest latency), Pipewire JACK (easiest), or JACK1 via
75+
your package manager
76+
- **Windows**: [Official installer](http://jackaudio.org/downloads/) or
77+
[Chocolatey](https://community.chocolatey.org/packages/jack)
78+
- **macOS**: [Official installer](http://jackaudio.org/downloads/) or
79+
[Homebrew](https://formulae.brew.sh/formula/jack)
5480

55-
Testing requires setting up a dummy server and running the tests using a single
56-
thread. `rust-jack` automatically configures `cargo nextest` to use a single
57-
thread.
81+
By default, clients request the server to start on demand. Use
82+
`ClientOptions::default()` or the `NO_START_SERVER` flag to disable this.
83+
84+
## Testing
85+
86+
Tests require a dummy JACK server and must run single-threaded:
5887

5988
```sh
60-
# Set up a dummy server for tests. The script is included in this repository.
6189
./dummy_jack_server.sh &
62-
# Run tests
6390
cargo nextest run
6491
```
6592

66-
Note: If cargo nextest is not available, use `RUST_TEST_THREADS=1 cargo test` to
67-
run in single threaded mode.
68-
93+
If `cargo nextest` is unavailable: `RUST_TEST_THREADS=1 cargo test`
6994

70-
## Possible Issues
95+
### Troubleshooting
7196

72-
If the tests are failing, a possible gotcha may be timing issues.
97+
- Use `cargo nextest` instead of `cargo test` for better handling of timing-sensitive tests
98+
- Try libjack2 or pipewire-jack if tests fail with your current JACK implementation
7399

74-
1. If using `cargo test`, try `cargo nextest`. The `cargo nextest`
75-
configuration is set up to run single threaded and to retry flaky tests.
100+
## License
76101

77-
Another case is that libjack may be broken on your setup. Try using libjack2 or
78-
pipewire-jack.
102+
MIT - see [LICENSE](LICENSE) for details.

docs/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
- [Logging](./logging.md)
66
- [Contrib](./contrib/index.md)
77
- [Closure Callbacks](./contrib/closure_callbacks.md)
8+
- [Controller](./contrib/controller.md)
89

docs/src/contrib/controller.md

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Controller
2+
3+
**Note:** This module requires the `controller` feature, which is not enabled by default.
4+
Add `jack = { version = "...", features = ["controller"] }` to your `Cargo.toml`.
5+
6+
The controller module provides utilities for building controllable JACK processors
7+
with lock-free communication. This is useful when you need to send commands to or
8+
receive notifications from your audio processor without blocking the real-time thread.
9+
10+
## Overview
11+
12+
The controller pattern separates your audio processing into two parts:
13+
14+
1. **Processor** - Runs in the real-time audio thread and handles audio/midi processing
15+
2. **Controller** - Runs outside the real-time thread and can send commands or receive notifications
16+
17+
Communication between them uses lock-free ring buffers, making it safe for real-time audio.
18+
19+
## Basic Usage
20+
21+
Implement the `ControlledProcessorTrait` to create a controllable processor:
22+
23+
```rust
24+
use jack::contrib::controller::{
25+
ControlledProcessorTrait, ProcessorChannels, ProcessorHandle,
26+
};
27+
28+
// Define your command and notification types
29+
enum Command {
30+
SetVolume(f32),
31+
Mute,
32+
Unmute,
33+
}
34+
35+
enum Notification {
36+
ClippingDetected,
37+
VolumeChanged(f32),
38+
}
39+
40+
// Define your processor state
41+
struct VolumeProcessor {
42+
output: jack::Port<jack::AudioOut>,
43+
input: jack::Port<jack::AudioIn>,
44+
volume: f32,
45+
muted: bool,
46+
}
47+
48+
impl ControlledProcessorTrait for VolumeProcessor {
49+
type Command = Command;
50+
type Notification = Notification;
51+
52+
fn buffer_size(
53+
&mut self,
54+
_client: &jack::Client,
55+
_size: jack::Frames,
56+
_channels: &mut ProcessorChannels<Self::Command, Self::Notification>,
57+
) -> jack::Control {
58+
jack::Control::Continue
59+
}
60+
61+
fn process(
62+
&mut self,
63+
_client: &jack::Client,
64+
scope: &jack::ProcessScope,
65+
channels: &mut ProcessorChannels<Self::Command, Self::Notification>,
66+
) -> jack::Control {
67+
// Handle incoming commands
68+
while let Some(cmd) = channels.recv_command() {
69+
match cmd {
70+
Command::SetVolume(v) => {
71+
self.volume = v;
72+
let _ = channels.try_notify(Notification::VolumeChanged(v));
73+
}
74+
Command::Mute => self.muted = true,
75+
Command::Unmute => self.muted = false,
76+
}
77+
}
78+
79+
// Process audio
80+
let input = self.input.as_slice(scope);
81+
let output = self.output.as_mut_slice(scope);
82+
let gain = if self.muted { 0.0 } else { self.volume };
83+
84+
for (out, inp) in output.iter_mut().zip(input.iter()) {
85+
*out = inp * gain;
86+
}
87+
88+
jack::Control::Continue
89+
}
90+
}
91+
```
92+
93+
## Creating and Using the Processor
94+
95+
Use the `instance` method to create both the processor and its control handle:
96+
97+
```rust
98+
let (client, _status) =
99+
jack::Client::new("controlled", jack::ClientOptions::default()).unwrap();
100+
101+
let input = client.register_port("in", jack::AudioIn::default()).unwrap();
102+
let output = client.register_port("out", jack::AudioOut::default()).unwrap();
103+
104+
let processor = VolumeProcessor {
105+
input,
106+
output,
107+
volume: 1.0,
108+
muted: false,
109+
};
110+
111+
// Create the processor instance and control handle
112+
// Arguments: notification channel size, command channel size
113+
let (processor_instance, mut handle) = processor.instance(16, 16);
114+
115+
// Activate the client with the processor
116+
let active_client = client.activate_async((), processor_instance).unwrap();
117+
118+
// Now you can control the processor from any thread
119+
handle.send_command(Command::SetVolume(0.5)).unwrap();
120+
121+
// And receive notifications
122+
for notification in handle.drain_notifications() {
123+
match notification {
124+
Notification::ClippingDetected => println!("Clipping detected!"),
125+
Notification::VolumeChanged(v) => println!("Volume changed to {}", v),
126+
}
127+
}
128+
```
129+
130+
## Channel Capacities
131+
132+
When calling `instance`, you specify the capacity of both ring buffers:
133+
134+
- `notification_channel_size` - How many notifications can be queued from processor to controller
135+
- `command_channel_size` - How many commands can be queued from controller to processor
136+
137+
Choose sizes based on your expected message rates. If a channel is full, `push` will fail,
138+
so handle this appropriately in your code.
139+
140+
## Transport Sync
141+
142+
If your processor needs to respond to JACK transport changes, implement the `sync` method
143+
and optionally set `SLOW_SYNC`:
144+
145+
```rust
146+
impl ControlledProcessorTrait for MyProcessor {
147+
// ...
148+
149+
const SLOW_SYNC: bool = true; // Set if sync may take multiple cycles
150+
151+
fn sync(
152+
&mut self,
153+
_client: &jack::Client,
154+
state: jack::TransportState,
155+
pos: &jack::TransportPosition,
156+
channels: &mut ProcessorChannels<Self::Command, Self::Notification>,
157+
) -> bool {
158+
// Handle transport state changes
159+
// Return true when ready to play
160+
true
161+
}
162+
}
163+
```

docs/src/features.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,11 @@ Default: Yes
3030
Load `libjack` at runtime as opposed to the standard dynamic linking. This is
3131
preferred as it allows `pw-jack` to intercept the loading at runtime to provide
3232
the Pipewire JACK server implementation.
33+
34+
## `controller`
35+
36+
Default: No
37+
38+
Enables the `jack::contrib::controller` module which provides utilities for
39+
building controllable JACK processors with lock-free communication. See the
40+
[Controller documentation](contrib/controller.md) for usage details.

0 commit comments

Comments
 (0)