Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Quickstart

JACK Background

The jack crate provides Rust bindings to the JACK API. Typically, a JACK server is started and clients connect to it to gain access to audio and midi inputs and outputs, along with synchronization mechanisms and APIs.

Jack Server Architecture

The JACK server is responsible for:

  • Discovering and exposing audio and midi devices.
  • Synchronizing audio and midi data.
  • Managing the processing graph.

JACK clients are responsible for:

  • Registering themselves with JACK.
  • Registering callbacks to provide audio/midi data to the JACK server.

JACk Server

There are two Linux implementations tested with the jack crate.

  • JACK2 - The primary implementation of JACK. Can use realtime scheduling and alsa under the hood to provide the best latency. The JACK2 server may be started through the jackd CLI or qjackctl GUI.
  • Pipewire - The most commonly used audio & video stream server for Linux. May not provide the best latency, but is very convenient to use. Pipewire itself has its own API, but it also exposes a JACK server. pw-jack is often used to patch in Pipewire's JACK implementation. For example, you can run your Rust JACK app with: pw-jack cargo run.

JACK Clients

This is where the jack crate comes in. Once a JACK server is running on the system, you can run your client to produce audio. Here is a simple jack program that can take inputs and forward them to outputs.

fn main() {
    // 1. Create client
    let (client, _status) =
        jack::Client::new("rust_jack_simple", jack::ClientOptions::default()).unwrap();

    // 2. Register ports. They will be used in a callback that will be
    // called when new data is available.
    let in_a: jack::Port<jack::AudioIn> = client
        .register_port("rust_in_l", jack::AudioIn::default())
        .unwrap();
    let in_b: jack::Port<jack::AudioIn> = client
        .register_port("rust_in_r", jack::AudioIn::default())
        .unwrap();
    let mut out_a: jack::Port<jack::AudioOut> = client
        .register_port("rust_out_l", jack::AudioOut::default())
        .unwrap();
    let mut out_b: jack::Port<jack::AudioOut> = client
        .register_port("rust_out_r", jack::AudioOut::default())
        .unwrap();
    let process_callback = move |_: &jack::Client, ps: &jack::ProcessScope| -> jack::Control {
        let out_a_p = out_a.as_mut_slice(ps);
        let out_b_p = out_b.as_mut_slice(ps);
        let in_a_p = in_a.as_slice(ps);
        let in_b_p = in_b.as_slice(ps);
        out_a_p.clone_from_slice(in_a_p);
        out_b_p.clone_from_slice(in_b_p);
        jack::Control::Continue
    };
    let process = jack::contrib::ClosureProcessHandler::new(process_callback);

    // 3. Activate the client, which starts the processing.
    let active_client = client.activate_async((), process).unwrap();

    // 4. Wait for user input to quit
    println!("Press enter/return to quit...");
    let mut user_input = String::new();
    io::stdin().read_line(&mut user_input).ok();

    // 5. Not needed as the async client will cease processing on `drop`.
    if let Err(err) = active_client.deactivate() {
        eprintln!("JACK exited with error: {err}");
    }
}

Connecting Ports

  1. Run the JACK client using the Pipewire JACK server.
    pw-jack cargo run
    
  2. View the JACK processing graph. This can be done by using the qjackctl GUI and clicking Graphs.
    pw-jack qjackctl
    
  3. Connect the ports as you see fit! In the below, a webcam microphone is connected to the speakers. Warning, do not try this at home! Connecting a microphone input to a speaker output may produce a terrible echo. Connecting ports in QJackCtl.

Features

The Rust features for the jack crate are defined in https://github.com/RustAudio/rust-jack/blob/main/Cargo.toml. To see the documentation for Rust features in general, see the Rust Book.

Disabling Default Features

The jack crate ships with a reasonable set of default features. To enable just a subset of features, set default-features to false and select only the desired features.

jack = { version = "..", default-features = false, features = ["log"] }

log

Default: Yes

If the log crate should be used to handle JACK logging. Requires setting up a logging implementation to make messages available.

dynamic_loading

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.

Logging

JACK can communicate info and error messages. By default, the log crate is hooked up to output messages. However, other logging methods can be used with the set_logger function.

No Logging

Logging from jack can be disabled entirely by setting the logger to None.

#![allow(unused)]
fn main() {
jack::set_logger(jack::LoggerType::None);
}

Log Crate (default)

The log crate is the default logger if the log feature is enabled, which is enabled by default. The log crate provides a facade for logging; it provides macros to perform logging, but another mechanism or crate is required to actually perform the logging.

In the example below, we use the env_logger crate to display logging for info and error severity level messages.

#![allow(unused)]
fn main() {
env_logger::builder().filter(None, log::LevelFilter::Info).init();

// JACK may log things to `info!` or `error!`.
let (client, _status) =
      jack::Client::new("rust_jack_simple", jack::ClientOptions::default()).unwrap();
}

Stdio

If the log feature is not enabled, then jack will log info messages to stdout and error messages to stderr. These usually show up in the terminal.

#![allow(unused)]
fn main() {
jack::set_logger(jack::LoggerType::Stdio);
}

Custom

jack::LoggerType::Custom can be used to set a custom logger. Here is stdout/stderr implemented as a custom logger:

fn main() {
    jack::set_logger(jack::LoggerType::Custom{info: stdout_handler, error: stderr_handler});
    ...
}

unsafe extern "C" fn stdout_handler(msg: *const libc::c_char) {
    let res = std::panic::catch_unwind(|| match std::ffi::CStr::from_ptr(msg).to_str() {
        Ok(msg) => println!("{}", msg),
        Err(err) => println!("failed to log to JACK info: {:?}", err),
    });
    if let Err(err) = res {
        eprintln!("{err:?}");
        std::mem::forget(err); // Prevent from rethrowing panic.
    }
}

unsafe extern "C" fn stderr_handler(msg: *const libc::c_char) {
    let res = std::panic::catch_unwind(|| match std::ffi::CStr::from_ptr(msg).to_str() {
        Ok(msg) => eprintln!("{}", msg),
        Err(err) => eprintln!("failed to log to JACK error: {:?}", err),
    });
    if let Err(err) = res {
        eprintln!("{err:?}");
        std::mem::forget(err); // Prevent from rethrowing panic.
    }
}

Contrib

jack::contrib contains convenient but optional utilities.

Closure Callbacks

Closure callbacks allow you to define functionality inline.

Process Closure

Audio and midi processing can be defined through closures. This involves:

  1. Creating a closure that captures the appropriate state (including JACK ports) and
  2. Activating it within a client.
#![allow(unused)]
fn main() {
// 1. Create the client.
let (client, _status) =
    jack::Client::new("silence", jack::ClientOptions::default()).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.

#![allow(unused)]
fn main() {
// 1. Create the client.
let (client, _status) =
    jack::Client::new("silence", jack::ClientOptions::default()).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();
}