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.
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:
IoDriver—set_process_callback,run,stop(owns the timing loop)IoCapture—read_input(channel, &mut [f32]),num_input_channels()IoPlayback—write_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
-
Output—Sinknode. HoldsArc<dyn IoPlayback>and callswrite_output()directly inconsume(). The backend is injected viaSink::set_playback()byProcessingState::wire_backends().let playback: = ...; let mut output = with_channels; -
Input—Sourcenode (push model). HoldsArc<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:
ProcessingState::process_block(&tick)— adopts the tick's sample rate (re-initialising nodes if the hardware rate differs), drains the actor mailbox and appliesSetParameterwrites (sample-accurate ones at the block matching theirsample_pos), then generates/processes/propagatesProcessingState::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_blocksframes via aSPA_PARAM_Buffersobject on connect, instead of PipeWire's large default (~12288 frames). - PortAudio requests
buffer_size × buffer_blocksasframes_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 theClockTick; the graph re-initialises its nodes to it - ALSA: queries
hw.get_rate()afterset_rate(Nearest), checkshw.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_ratedirectly
Links
- Repository: https://github.com/DigitalRats/rill
- Documentation: https://docs.rs/rill-io