Skip to content

Commit a8cf785

Browse files
committed
Add docs
1 parent 1dc0be6 commit a8cf785

File tree

2 files changed

+161
-0
lines changed

2 files changed

+161
-0
lines changed

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

0 commit comments

Comments
 (0)