rill-io 0.6.0-M2

Audio I/O backends for Rill - CPAL, ALSA, PipeWire, JACK
docs.rs failed to build rill-io-0.6.0-M2
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: rill-io-0.5.0-beta.3

rill-io

Audio I/O backends — PortAudio, ALSA, PipeWire, JACK.

This crate provides I/O backends and the Output/Input graph nodes that own the reactive stream (process callback or similar).

I/O is split into three orthogonal traits:

  • IoDriverset_process_callback, run, stop (owns the timing loop)
  • IoCaptureread_input(channel, &mut [f32]), num_input_channels()
  • IoPlaybackwrite_output(channel, &[f32]), num_output_channels()

A single backend struct (e.g. PipewireBackend) implements IoDriver and optionally IoCapture / IoPlayback. IoBackend is a backward-compatible alias: pub trait IoBackend: IoDriver {}.

The process callback is registered via IoDriver::set_process_callback().

Nodes

  • OutputSink node. Holds Arc<dyn IoPlayback> and calls write_output() directly in consume(). The backend is injected via Sink::set_playback() by ProcessingState::wire_backends().

    let playback: Arc<dyn IoPlayback> = ...;
    let mut output = Output::<f32, 256>::with_channels(playback, 2);
    
  • InputSource node (push model). Holds Arc<dyn IoCapture>. Same pattern — Source::set_capture() injects the backend.

The process callback (registered by the orchestrator) does, per block_size chunk of the callback's buffer:

  1. ProcessingState::process_block(&tick) — adopts the tick's sample rate (re-initialising nodes if the hardware rate differs), drains the actor mailbox and applies SetParameter writes (sample-accurate ones at the block matching their sample_pos), then generates/processes/propagates
  2. ProcessingState::send_clock_tick(&tick) — forwards the tick to control actors (chunking backends dispatch one tick per block)

Backends

Backend Feature Thread model
PortAudioBackend portaudio (default) RT callback, large buffer chunked into block_size pieces
PipewireBackend pipewire RT callback (PW thread), buffer negotiated via SPA_PARAM_Buffers, chunked into block_size pieces
JackBackend jack RT callback (JACK thread); buffer size set by the JACK server
AlsaBackend alsa Callback-driven on the audio thread, event-driven via snd_pcm_wait (no thread::sleep); fires the capture then playback callbacks per period (exact period required)
NullBackend (always) No‑op, for testing

Buffer sizing (callback-driven backends)

A single block_size (256-frame) period is unstable through PipeWire (crackling via the ALSA plugin, xruns), so callback-driven backends request a larger DMA buffer and chunk it back into block_size pieces in the callback, emitting one ClockTick per rill block (the same model PipeWire uses internally):

  • PipeWire negotiates buffer_size × buffer_blocks frames via a SPA_PARAM_Buffers object on connect, instead of PipeWire's large default (~12288 frames).
  • PortAudio requests buffer_size × buffer_blocks as frames_per_buffer.

The multiplier is AudioConfig::buffer_blocks (default 16 → 4096 frames; "buffer_blocks" backend param). Because the whole buffer is one I/O callback, its duration is also the async-control look-ahead (ClockTick.io_quantum), so it trades control latency (~93 ms at 16 blocks) against stability (the stable minimum is hardware/config dependent). ALSA (period fixed to buffer_size) and JACK (buffer size set by the JACK server) ignore it.

Sample rate negotiation:

  • JACK: reads client.sample_rate() after activation and puts the actual hardware rate in the ClockTick; the graph re-initialises its nodes to it
  • ALSA: queries hw.get_rate() after set_rate(Nearest), checks hw.get_period_size() == BUF_SIZE
  • PipeWire: output uses requested rate, input reads negotiated rate atomically
  • PortAudio: opens stream with exact requested rate and buffer size
  • Null: uses config.sample_rate directly

Links