Audio I/O and latency
crates/engine/src/audio_io.rs, on cpal 0.18.
Hosts and devices
Section titled “Hosts and devices”- On Linux cpal is built with its PipeWire, PulseAudio and JACK hosts and
realtime-dbus. The preference order is PipeWire, PulseAudio, JACK, ALSA; JACK is skipped unless a server is running. Windows and macOS use cpal’s default host (WASAPI, Core Audio); ASIO is not enabled. list_deviceshides devices that only confuse: on ALSA onlyhw:devices and thepipewirePCM are listed; PipeWire and PulseAudio hide monitor sources. Device ids are cpal’shost:idstrings;find_deviceaccepts any id the host knows (headless tests usealsa:null) and falls back to the default device.AudioConfig(saved insettings.json›audio): host, input, output, sample rate (48000), buffer size (256),input_enabled,auto_monitor,recording_delay_ms.same_devicesdecides whether a change reopens the streams.choose_configneeds the requested rate to be supported (there is no fallback) and a sample format of f32, i32 or i16. The buffer size is fixed when the device allows it.
The duplex stream
Section titled “The duplex stream”audio_io::start spawns an audio-io thread that builds the streams, reports an
AudioStatus back and then owns them until the AudioIo handle is dropped.
- The output callback is the clock. It records the output latency (playback timestamp
minus callback timestamp), pulls the input that has arrived, calls
Engine::processundertry_lock(silence if a render holds the engine) and insidecatch_unwind(silence and anengine_errorscount if it panics), clamps to ±4 and converts to the device format. - Input arrives through an
rtrbring of one second. The input callback records the input latency (callback minus capture timestamp) and writes what fits. - Drift control: when the ring holds more than three output blocks, the excess is dropped; an underflow is padded with zeros. There is no resampling between the two devices.
ring_frames(how much input is waiting) is published for latency compensation.- Errors are only logged; after an interface is unplugged or a driver error, use Settings ›
Audio › Reset Audio Engine (
restart_audio), which reopens the devices and rebuilds the graph when the rate changed. - Real-time priority comes from cpal (
realtime-dbus, through rtkit) for its ALSA and PipeWire callback threads; JACK threads are real-time through the JACK server.
Latency compensation for recordings
Section titled “Latency compensation for recordings”Session::record_latency_frames = (input latency + output latency) + ring_frames +
Recording Delay + the PDC total. finish_recording moves each audio take earlier by that
amount (trimming anything before zero into the region offset). Recorded MIDI is moved by the
output latency plus the PDC total only, since MIDI travels one way. Cycle and Autopunch shaping
use the value in the RecPlan, computed when recording starts.
Live MIDI is delivered at offset 0 of the next callback, and recorded MIDI is stamped with the block’s start position, so both have up to one buffer of timing granularity.
Recording
Section titled “Recording”Recorder::begin (recorder.rs) creates the WAV file (24-bit integer or 32-bit float) and a
ring of four seconds, and returns the producer, which the session hands to the engine with
Cmd::RecordSink. The audio thread pushes the armed track’s input samples inside the capture
window; a full ring counts an overrun (EngineStatus::overruns). The recorder thread drains
every ring every 10 ms into the file, an in-memory copy (so the take plays straight away) and
peak previews for the UI (Tick::previews). finish_all finalizes the files.
Where a finished take goes over existing regions follows Settings › Recording › Overlapping
Recordings, read through RecordingPrefs::audio_mode / midi_mode: the control bar’s Replace
button (RecordingPrefs::replace) turns both into Replace. Audio is placed by place_take
(Replace trims the regions underneath). MIDI (place_recording) makes a new region, or merges
into the newest MIDI region it overlaps (merge_midi_take, notes and controllers), or with
Replace first clears the recorded range, MidiWindow::lo..hi (punch-in or record start to the
stop, or the cycle window), from every MIDI region on the track (replace_midi_take, with
midi::erase_range restoring changed controllers after the range) and then merges.
Step Input hears hardware MIDI through the ticks: Tick::midi_in carries the messages the
inputs’ activity log got since the last tick.
Without a device
Section titled “Without a device”When no output device is running, Session::send processes the command queue inline, so the
project stays consistent. The ALSA null device has no clock and runs as fast as the CPU
allows, which the headless examples rely on.