Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions .github/workflows/site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"]
126 changes: 75 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,78 +1,102 @@
# JACK (for Rust)

Rust bindings for [JACK Audio Connection Kit](<https://jackaudio.org>).
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](<https://github.com/sponsors/wmedrano>) |
[![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](<http://jackaudio.org/downloads/>) or [Chocolatey](<https://community.chocolatey.org/packages/jack>).
- MacOS users may install JACK from the [official
website](<http://jackaudio.org/downloads/>) or [Homebrew](<https://formulae.brew.sh/formula/jack>).
- [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](<https://docs.rs/jack/>) for details about
the API. For more general documentation, visit <https://rustaudio.github.io/rust-jack>.
## 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<jack::Client> {
todo!()
}
```toml
[dependencies]
jack = "0.13"
```

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

// For the full async client
fn async_client() -> impl jack::AsyncClient<impl Any, impl Any> {
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.
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
- [Logging](./logging.md)
- [Contrib](./contrib/index.md)
- [Closure Callbacks](./contrib/closure_callbacks.md)
- [Controller](./contrib/controller.md)

163 changes: 163 additions & 0 deletions docs/src/contrib/controller.md
Original file line number Diff line number Diff line change
@@ -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<jack::AudioOut>,
input: jack::Port<jack::AudioIn>,
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<Self::Command, Self::Notification>,
) -> jack::Control {
jack::Control::Continue
}

fn process(
&mut self,
_client: &jack::Client,
scope: &jack::ProcessScope,
channels: &mut ProcessorChannels<Self::Command, Self::Notification>,
) -> 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<Self::Command, Self::Notification>,
) -> bool {
// Handle transport state changes
// Return true when ready to play
true
}
}
```
8 changes: 8 additions & 0 deletions docs/src/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading