diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 404e5ef22..c78192bc4 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -28,12 +28,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: 3.0 - bundler-cache: true - cache-version: 0 # Increment this number to re-download cached gems. - name: Setup Pages id: pages uses: actions/configure-pages@v5 diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 15db47437..112273d08 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -26,8 +26,10 @@ jobs: run: cargo build --verbose - name: Build (No Features) run: cargo build --verbose --no-default-features + - name: Build (All Features) + run: cargo build --verbose --all-features - name: Build (examples) - run: cargo build --verbose --examples + run: cargo build --verbose --examples --all-features - name: Run Tests (Default Features) run: cargo nextest run - name: Run Doc Tests diff --git a/Cargo.toml b/Cargo.toml index 8a44aa766..00234e94f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ jack-sys = {version = "0.5", path = "./jack-sys"} lazy_static = "1.4" libc = "0.2" log = { version = "0.4", optional = true} +rtrb = { version = "0.3.2", optional = true } [dev-dependencies] approx = "0.5" @@ -25,3 +26,8 @@ ctor = "0.2" [features] default = ["dynamic_loading", "log"] dynamic_loading = ["jack-sys/dynamic_loading"] +controller = ["rtrb"] + +[[example]] +name = "controlled_sine" +required-features = ["controller"] diff --git a/README.md b/README.md index 5ff2749c3..acb725bab 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,102 @@ # JACK (for Rust) -Rust bindings for [JACK Audio Connection Kit](). +Rust bindings for [JACK Audio Connection Kit](https://jackaudio.org). -| [![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) | -|-----------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [![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) | -| [📚 Documentation](https://rustaudio.github.io/rust-jack) | [:heart: Sponsor]() | +[![Crates.io](https://img.shields.io/crates/v/jack.svg)](https://crates.io/crates/jack) +[![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) +[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[:heart: Sponsor](https://github.com/sponsors/wmedrano) -## Using JACK +## Overview +JACK is a low-latency audio server that allows multiple applications to share +audio and MIDI devices and route signals between each other. This crate provides +safe Rust bindings to create JACK clients that can process audio and MIDI in +real-time. -The JACK server is usually started by the user or system. Clients can request -that the JACK server is started on demand when they connect, but this can be -disabled by creating a client with the `NO_START_SERVER` option or -`ClientOptions::default()`. +## Documentation -- Linux and BSD users may install JACK1, JACK2 (preferred for low latency), or - Pipewire JACK (preferred for ease of use) from their system package manager. -- Windows users may install JACK from the [official - website]() or [Chocolatey](). -- MacOS users may install JACK from the [official - website]() or [Homebrew](). +- [Guide](https://rustaudio.github.io/rust-jack) - Quickstart, features, and tutorials +- [API Reference](https://docs.rs/jack/) - Complete API documentation -Refer to the [docs.rs documentation]() for details about -the API. For more general documentation, visit . +## Quick Example +```rust +use std::io; + +fn main() { + // Create a JACK client + let (client, _status) = + jack::Client::new("rust_jack_simple", jack::ClientOptions::default()).unwrap(); + + // Register input and output ports + let in_port = client + .register_port("input", jack::AudioIn::default()) + .unwrap(); + let mut out_port = client + .register_port("output", jack::AudioOut::default()) + .unwrap(); + + // Create a processing callback that copies input to output + let process = jack::contrib::ClosureProcessHandler::new( + move |_: &jack::Client, ps: &jack::ProcessScope| -> jack::Control { + out_port.as_mut_slice(ps).clone_from_slice(in_port.as_slice(ps)); + jack::Control::Continue + }, + ); + + // Activate the client + let _active_client = client.activate_async((), process).unwrap(); + + // Wait for user to quit + println!("Press enter to quit..."); + let mut input = String::new(); + io::stdin().read_line(&mut input).ok(); +} +``` -## FAQ +See the [examples](examples/) directory for more. -### How do I return an `AsyncClient` with many generics? +## Installation -This is especially useful when using `jack::contrib::ClosureProcessHandler` -which may have an innaccessible type. +Add to your `Cargo.toml`: -```rust -// Shortest and allows access to the underlying client. -fn make_client() -> impl AsRef { - todo!() -} +```toml +[dependencies] +jack = "0.13" +``` -// With extra bounds -fn make_client() -> impl 'static + AsRef { - todo!(); -} +### JACK Server Setup -// For the full async client -fn async_client() -> impl jack::AsyncClient { - todo!(); -} -``` +A JACK server must be running for clients to connect. Install one of: -# Testing +- **Linux/BSD**: JACK2 (lowest latency), Pipewire JACK (easiest), or JACK1 via + your package manager +- **Windows**: [Official installer](http://jackaudio.org/downloads/) or + [Chocolatey](https://community.chocolatey.org/packages/jack) +- **macOS**: [Official installer](http://jackaudio.org/downloads/) or + [Homebrew](https://formulae.brew.sh/formula/jack) -Testing requires setting up a dummy server and running the tests using a single -thread. `rust-jack` automatically configures `cargo nextest` to use a single -thread. +By default, clients request the server to start on demand. Use +`ClientOptions::default()` or the `NO_START_SERVER` flag to disable this. + +## Testing + +Tests require a dummy JACK server and must run single-threaded: ```sh -# Set up a dummy server for tests. The script is included in this repository. ./dummy_jack_server.sh & -# Run tests cargo nextest run ``` -Note: If cargo nextest is not available, use `RUST_TEST_THREADS=1 cargo test` to -run in single threaded mode. - +If `cargo nextest` is unavailable: `RUST_TEST_THREADS=1 cargo test` -## Possible Issues +### Troubleshooting -If the tests are failing, a possible gotcha may be timing issues. +- Use `cargo nextest` instead of `cargo test` for better handling of timing-sensitive tests +- Try libjack2 or pipewire-jack if tests fail with your current JACK implementation -1. If using `cargo test`, try `cargo nextest`. The `cargo nextest` - configuration is set up to run single threaded and to retry flaky tests. +## License -Another case is that libjack may be broken on your setup. Try using libjack2 or -pipewire-jack. +MIT - see [LICENSE](LICENSE) for details. diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 81b5edc41..cf49b7832 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -5,4 +5,5 @@ - [Logging](./logging.md) - [Contrib](./contrib/index.md) - [Closure Callbacks](./contrib/closure_callbacks.md) + - [Controller](./contrib/controller.md) diff --git a/docs/src/contrib/controller.md b/docs/src/contrib/controller.md new file mode 100644 index 000000000..09def4202 --- /dev/null +++ b/docs/src/contrib/controller.md @@ -0,0 +1,163 @@ +# Controller + +**Note:** This module requires the `controller` feature, which is not enabled by default. +Add `jack = { version = "...", features = ["controller"] }` to your `Cargo.toml`. + +The controller module provides utilities for building controllable JACK processors +with lock-free communication. This is useful when you need to send commands to or +receive notifications from your audio processor without blocking the real-time thread. + +## Overview + +The controller pattern separates your audio processing into two parts: + +1. **Processor** - Runs in the real-time audio thread and handles audio/midi processing +2. **Controller** - Runs outside the real-time thread and can send commands or receive notifications + +Communication between them uses lock-free ring buffers, making it safe for real-time audio. + +## Basic Usage + +Implement the `ControlledProcessorTrait` to create a controllable processor: + +```rust +use jack::contrib::controller::{ + ControlledProcessorTrait, ProcessorChannels, ProcessorHandle, +}; + +// Define your command and notification types +enum Command { + SetVolume(f32), + Mute, + Unmute, +} + +enum Notification { + ClippingDetected, + VolumeChanged(f32), +} + +// Define your processor state +struct VolumeProcessor { + output: jack::Port, + input: jack::Port, + volume: f32, + muted: bool, +} + +impl ControlledProcessorTrait for VolumeProcessor { + type Command = Command; + type Notification = Notification; + + fn buffer_size( + &mut self, + _client: &jack::Client, + _size: jack::Frames, + _channels: &mut ProcessorChannels, + ) -> jack::Control { + jack::Control::Continue + } + + fn process( + &mut self, + _client: &jack::Client, + scope: &jack::ProcessScope, + channels: &mut ProcessorChannels, + ) -> jack::Control { + // Handle incoming commands + while let Some(cmd) = channels.recv_command() { + match cmd { + Command::SetVolume(v) => { + self.volume = v; + let _ = channels.try_notify(Notification::VolumeChanged(v)); + } + Command::Mute => self.muted = true, + Command::Unmute => self.muted = false, + } + } + + // Process audio + let input = self.input.as_slice(scope); + let output = self.output.as_mut_slice(scope); + let gain = if self.muted { 0.0 } else { self.volume }; + + for (out, inp) in output.iter_mut().zip(input.iter()) { + *out = inp * gain; + } + + jack::Control::Continue + } +} +``` + +## Creating and Using the Processor + +Use the `instance` method to create both the processor and its control handle: + +```rust +let (client, _status) = + jack::Client::new("controlled", jack::ClientOptions::default()).unwrap(); + +let input = client.register_port("in", jack::AudioIn::default()).unwrap(); +let output = client.register_port("out", jack::AudioOut::default()).unwrap(); + +let processor = VolumeProcessor { + input, + output, + volume: 1.0, + muted: false, +}; + +// Create the processor instance and control handle +// Arguments: notification channel size, command channel size +let (processor_instance, mut handle) = processor.instance(16, 16); + +// Activate the client with the processor +let active_client = client.activate_async((), processor_instance).unwrap(); + +// Now you can control the processor from any thread +handle.send_command(Command::SetVolume(0.5)).unwrap(); + +// And receive notifications +for notification in handle.drain_notifications() { + match notification { + Notification::ClippingDetected => println!("Clipping detected!"), + Notification::VolumeChanged(v) => println!("Volume changed to {}", v), + } +} +``` + +## Channel Capacities + +When calling `instance`, you specify the capacity of both ring buffers: + +- `notification_channel_size` - How many notifications can be queued from processor to controller +- `command_channel_size` - How many commands can be queued from controller to processor + +Choose sizes based on your expected message rates. If a channel is full, `push` will fail, +so handle this appropriately in your code. + +## Transport Sync + +If your processor needs to respond to JACK transport changes, implement the `sync` method +and optionally set `SLOW_SYNC`: + +```rust +impl ControlledProcessorTrait for MyProcessor { + // ... + + const SLOW_SYNC: bool = true; // Set if sync may take multiple cycles + + fn sync( + &mut self, + _client: &jack::Client, + state: jack::TransportState, + pos: &jack::TransportPosition, + channels: &mut ProcessorChannels, + ) -> bool { + // Handle transport state changes + // Return true when ready to play + true + } +} +``` diff --git a/docs/src/features.md b/docs/src/features.md index 399bbdba8..71616a067 100644 --- a/docs/src/features.md +++ b/docs/src/features.md @@ -30,3 +30,11 @@ Default: Yes Load `libjack` at runtime as opposed to the standard dynamic linking. This is preferred as it allows `pw-jack` to intercept the loading at runtime to provide the Pipewire JACK server implementation. + +## `controller` + +Default: No + +Enables the `jack::contrib::controller` module which provides utilities for +building controllable JACK processors with lock-free communication. See the +[Controller documentation](contrib/controller.md) for usage details. diff --git a/examples/controlled_sine.rs b/examples/controlled_sine.rs new file mode 100644 index 000000000..6c1bafd4c --- /dev/null +++ b/examples/controlled_sine.rs @@ -0,0 +1,179 @@ +//! Sine wave generator using the controller pattern for lock-free communication. +//! +//! This example demonstrates how to use `ControlledProcessorTrait` to build +//! a controllable audio processor with commands and notifications. + +use jack::contrib::controller::{ControlledProcessorTrait, ProcessorChannels}; +use std::f64::consts::PI; +use std::io; +use std::str::FromStr; + +/// Commands that can be sent to the audio processor. +enum Command { + SetFrequency(f64), + SetVolume(f32), + Mute, + Unmute, +} + +/// Notifications sent from the audio processor. +enum Notification { + FrequencyChanged(f64), + VolumeChanged(f32), + MuteStateChanged(bool), +} + +/// The audio processor state. +struct SineProcessor { + out_port: jack::Port, + frequency: f64, + volume: f32, + muted: bool, + frame_t: f64, + time: f64, +} + +impl ControlledProcessorTrait for SineProcessor { + type Command = Command; + type Notification = Notification; + + fn buffer_size( + &mut self, + _client: &jack::Client, + _size: jack::Frames, + _channels: &mut ProcessorChannels, + ) -> jack::Control { + jack::Control::Continue + } + + fn process( + &mut self, + _client: &jack::Client, + scope: &jack::ProcessScope, + channels: &mut ProcessorChannels, + ) -> jack::Control { + // Handle incoming commands + while let Some(cmd) = channels.recv_command() { + match cmd { + Command::SetFrequency(f) => { + self.frequency = f; + self.time = 0.0; + let _ = channels.try_notify(Notification::FrequencyChanged(f)); + } + Command::SetVolume(v) => { + self.volume = v; + let _ = channels.try_notify(Notification::VolumeChanged(v)); + } + Command::Mute => { + self.muted = true; + let _ = channels.try_notify(Notification::MuteStateChanged(true)); + } + Command::Unmute => { + self.muted = false; + let _ = channels.try_notify(Notification::MuteStateChanged(false)); + } + } + } + + // Generate sine wave + let out = self.out_port.as_mut_slice(scope); + let gain = if self.muted { 0.0 } else { self.volume }; + + for sample in out.iter_mut() { + let x = self.frequency * self.time * 2.0 * PI; + *sample = (x.sin() as f32) * gain; + self.time += self.frame_t; + } + + jack::Control::Continue + } +} + +fn main() { + // 1. Open a client + let (client, _status) = + jack::Client::new("controlled_sine", jack::ClientOptions::default()).unwrap(); + + // 2. Register port + let out_port = client + .register_port("sine_out", jack::AudioOut::default()) + .unwrap(); + + // 3. Create the processor + let processor = SineProcessor { + out_port, + frequency: 220.0, + volume: 0.5, + muted: false, + frame_t: 1.0 / client.sample_rate() as f64, + time: 0.0, + }; + + // 4. Create the processor instance and control handle + let (processor_instance, mut handle) = processor.instance(16, 16); + + // 5. Activate the client + let active_client = client.activate_async((), processor_instance).unwrap(); + active_client + .as_client() + .connect_ports_by_name("controlled_sine:sine_out", "system:playback_1") + .unwrap(); + active_client + .as_client() + .connect_ports_by_name("controlled_sine:sine_out", "system:playback_2") + .unwrap(); + + // 6. Interactive control loop + println!("Controlled Sine Wave Generator"); + println!("Commands:"); + println!(" - Set frequency in Hz (e.g., 440)"); + println!(" v - Set volume 0-100 (e.g., v50)"); + println!(" m - Mute"); + println!(" u - Unmute"); + println!(" q - Quit"); + println!(); + + loop { + // Check for notifications + for notification in handle.drain_notifications() { + match notification { + Notification::FrequencyChanged(f) => println!("-> Frequency: {f} Hz"), + Notification::VolumeChanged(v) => println!("-> Volume: {:.0}%", v * 100.0), + Notification::MuteStateChanged(muted) => { + println!("-> {}", if muted { "Muted" } else { "Unmuted" }) + } + } + } + + // Read user input + let mut input = String::new(); + if io::stdin().read_line(&mut input).is_err() { + break; + } + let input = input.trim(); + + if input.eq_ignore_ascii_case("q") { + break; + } else if input.eq_ignore_ascii_case("m") { + let _ = handle.send_command(Command::Mute); + } else if input.eq_ignore_ascii_case("u") { + let _ = handle.send_command(Command::Unmute); + } else if let Some(vol_str) = input.strip_prefix('v').or_else(|| input.strip_prefix('V')) { + if let Ok(vol) = u8::from_str(vol_str) { + let volume = (vol.min(100) as f32) / 100.0; + let _ = handle.send_command(Command::SetVolume(volume)); + } else { + println!("Invalid volume. Use v0-v100"); + } + } else if let Ok(freq) = f64::from_str(input) { + let _ = handle.send_command(Command::SetFrequency(freq)); + } else if !input.is_empty() { + println!("Unknown command: {input}"); + } + } + + // 7. Deactivate + if let Err(err) = active_client.deactivate() { + eprintln!("JACK exited with error: {err}"); + } +} diff --git a/src/contrib/controller.rs b/src/contrib/controller.rs new file mode 100644 index 000000000..e4a5411a5 --- /dev/null +++ b/src/contrib/controller.rs @@ -0,0 +1,158 @@ +//! Utilities for building controllable JACK processors with lock-free communication. + +use rtrb::{Consumer, Producer, RingBuffer}; + +use crate::{ + Client, Control, Frames, ProcessHandler, ProcessScope, TransportPosition, TransportState, +}; + +/// Communication channels available to a processor in the real-time audio thread. +pub struct ProcessorChannels { + notifications: Producer, + commands: Consumer, +} + +impl ProcessorChannels { + /// Try to receive a command from the controller. + /// + /// Returns `Some(command)` if one was available, or `None` if the buffer was empty. + pub fn recv_command(&mut self) -> Option { + self.commands.pop().ok() + } + + /// Drain and return an iterator over all pending commands. + pub fn drain_commands(&mut self) -> impl Iterator + '_ { + std::iter::from_fn(move || self.commands.pop().ok()) + } + + /// Try to send a notification. + /// + /// Returns `Ok(())` if the notification was sent, or `Err(notification)` if the buffer was full. + pub fn try_notify(&mut self, notification: Notification) -> Result<(), Notification> { + self.notifications + .push(notification) + .map_err(|rtrb::PushError::Full(n)| n) + } +} + +/// Handle for controlling a processor from outside the real-time audio thread. +pub struct Controller { + notifications: Consumer, + commands: Producer, +} + +impl Controller { + /// Try to send a command to the processor. + /// + /// Returns `Ok(())` if the command was sent, or `Err(command)` if the buffer was full. + pub fn send_command(&mut self, command: Command) -> Result<(), Command> { + self.commands + .push(command) + .map_err(|rtrb::PushError::Full(cmd)| cmd) + } + + /// Try to receive a notification from the processor. + /// + /// Returns `Some(notification)` if one was available, or `None` if the buffer was empty. + pub fn recv_notification(&mut self) -> Option { + self.notifications.pop().ok() + } + + /// Drain and return an iterator over all pending notifications. + pub fn drain_notifications(&mut self) -> impl Iterator + '_ { + std::iter::from_fn(move || self.notifications.pop().ok()) + } +} + +/// A JACK processor that can be controlled via lock-free channels. +/// +/// Implement this trait to create a processor that communicates with external +/// code through commands and notifications while running in the real-time thread. +pub trait ControlledProcessorTrait: Send + Sized { + /// Commands sent from the controller to the processor. + type Command: Send; + /// Notifications sent from the processor to the controller. + type Notification: Send; + + /// See [`ProcessHandler::SLOW_SYNC`]. + const SLOW_SYNC: bool = false; + + /// Called when the transport state changes. See [`ProcessHandler::sync`]. + fn sync( + &mut self, + _client: &Client, + _state: TransportState, + _pos: &TransportPosition, + _channels: &mut ProcessorChannels, + ) -> bool { + true + } + + /// Called when the buffer size changes. See [`ProcessHandler::buffer_size`]. + fn buffer_size( + &mut self, + client: &Client, + size: Frames, + channels: &mut ProcessorChannels, + ) -> Control; + + /// Process audio. See [`ProcessHandler::process`]. + fn process( + &mut self, + client: &Client, + scope: &ProcessScope, + channels: &mut ProcessorChannels, + ) -> Control; + + /// Create a processor instance and its control handle with the given channel capacities. + #[must_use = "the processor instance must be used with Client::activate"] + fn instance( + self, + notification_channel_size: usize, + command_channel_size: usize, + ) -> ( + ControlledProcessorInstance, + Controller, + ) { + let (notifications, notifications_other) = + RingBuffer::::new(notification_channel_size); + let (commands_other, commands) = RingBuffer::::new(command_channel_size); + let controller = Controller { + notifications: notifications_other, + commands: commands_other, + }; + let processor = ControlledProcessorInstance { + inner: self, + channels: ProcessorChannels { + notifications, + commands, + }, + }; + (processor, controller) + } +} + +/// A [`ProcessHandler`] wrapper that provides channel-based communication. +/// +/// Created via [`ControlledProcessorTrait::instance`]. +pub struct ControlledProcessorInstance { + /// The inner processor implementation. + pub inner: T, + channels: ProcessorChannels, +} + +impl ProcessHandler for ControlledProcessorInstance { + fn process(&mut self, client: &Client, scope: &ProcessScope) -> Control { + self.inner.process(client, scope, &mut self.channels) + } + + const SLOW_SYNC: bool = T::SLOW_SYNC; + + fn buffer_size(&mut self, client: &Client, size: Frames) -> Control { + self.inner.buffer_size(client, size, &mut self.channels) + } + + fn sync(&mut self, client: &Client, state: TransportState, pos: &TransportPosition) -> bool { + self.inner.sync(client, state, pos, &mut self.channels) + } +} diff --git a/src/lib.rs b/src/lib.rs index 14f37ead2..dc815c892 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,6 +72,9 @@ pub mod contrib { mod closure; pub use closure::ClosureProcessHandler; + + #[cfg(feature = "controller")] + pub mod controller; } #[cfg(test)]