Pro-audio routing. Tauri 2 + SvelteKit 5 (runes) frontend with a node graph, Rust engine (cpal + rtrb + rubato + hound), CoreAudio HAL and Swift ScreenCaptureKit bridge on macOS.
Inside cpal / SCK callbacks and DspWorker::run:
- Allocations (growing
Vec::push,String::from,Box::new) - Locks (
Mutex::lock;try_lockonly if a miss is acceptable) - Syscalls (I/O, logging, IPC)
- Non-deterministic latency ops
Shared RT↔UI state: Arc<AtomicU32> (f32-as-bits, Ordering::Relaxed).
See MeterHandle / EffectControl in audio/effects.rs.
Ring buffers: rtrb SPSC. Use bulk_pop / bulk_push, never per-sample loops.
Resampling uses two explicit policies:
- Fixed stream rates, such as pipeline → speaker, use
FftFixedIn. It is the high-throughput synchronous converter and can skip trailing physical channels that have never carried a route. - A rate that follows an independent or drifting clock uses
SincFixedInorSincFixedOut, including ratio adjustment where the receiver owns pacing.
Dev builds require:
[profile.dev.package.rubato] opt-level = 3
[profile.dev.package.realfft] opt-level = 3
[profile.dev.package.rustfft] opt-level = 3
Without these, one chunk takes ~16 ms and the worker stalls.
Clock— the single transport cadence for every worker (speaker, monitoring, wire sender, recording). Sleeps to a per-block deadline; a missed deadline produces silence, never a rate error. Recording must follow the wall clock, not the source — a file source decodes faster than real time and would otherwise over-run the encoder.- Stall: per-source
last_pop_at; >150 ms silence → zero-fill and proceed. - RT promotion happens after speaker startup prefill. Every overdue Linux tick performs a real blocking sleep before more DSP work; an unbounded catch-up loop is forbidden even when the ring can absorb it.
RuntimeEffectenum dispatch, noBox<dyn>— LLVM inlines per variant.- Params:
Arc<AtomicU32>cells shared withEffectControl. UI writes; RT reads next block. - Live updates:
update_effect(node_id, data)Tauri command →EffectControl::apply_update(&Value). - Only
LevelMeterpublishes telemetry back (peak/RMS atomics, tick thread).
src/lib/modules/
audio/ methods.ts, stores.svelte.ts, types.ts, ui/
flow/ ui/ (xyflow nodes; node.svelte wrapper, editor, sidebar)
form/ ui/ (combobox.svelte etc.)
pipeline/ methods.ts, stores.svelte.ts, types.ts
theme/ stores.ts
Each module's index.ts is a barrel. Module-internal files are
underscore-prefixed (_slider.svelte).
- Svelte 5 runes only. No
export let, no stores in component scope. - xyflow nodes wrap with
Wrapperfromflow/ui/node.svelte(accent,hasInput,hasOutput). - Interactive elements inside nodes:
nodrag nopan(+nowheelif scrollable). - Numeric readouts:
font-mono tabular-nums. - IDs:
@paralleldrive/cuid2. Not nanoid, not uuid. - Never serialise
-Infinity/NaNover Tauri —serde_jsonemitsnull, which failsisFinite(). Use a sentinel (e.g.-120dB floor) or send amplitude.
audio/macos_hal.rs— custom CoreAudio FFI.cpal'ssupported_*_configshides non-default routes;default_*_configerrors on inactive routes.audio/sck_capture.rs— FFI to a Swift static lib innative/, built via directswiftcinbuild.rs. Do not add thescreencapturekitcrate — itsswift buildfails on host CLT due to aPackageDescriptiondylib ABI mismatch.audio/permission.rs—CGPreflightScreenCaptureAccess(non-prompting).audio/recorder.rs—hound, f32 stereo PCM, periodic flush.
Three real backends: CoreAudio + ScreenCaptureKit (macOS), PipeWire (Linux),
WASAPI (Windows). device/, capture/, volume/, virtual_device/,
pipeline/input/, pipeline/output/ each carry one file per OS — a change
in one usually needs the other two.
A backend that cannot support the feature returns an error; it does not substitute a different rate, device, or format.
- PipeWire process callbacks and promoted DSP workers are RT code. They may touch only preallocated buffers, SPSC rings and relaxed atomics.
audio_thread_priorityobtains real-time scheduling through RTKit. Its frame argument is the maximum uninterrupted render quantum, not a latency target. Pass the actual known block size; pass0when PipeWire owns an unknown callback quantum.- Linux
RLIMIT_RTTIMEmeasures CPU time spent under real-time scheduling without a blocking syscall. Crossing the soft limit sendsSIGXCPU; crossing the hard limit sendsSIGKILL. Preemption andsched_yielddo not reset it. Startup prefill runs before promotion, and deadline catch-up must block between blocks. Never raise or disable the OS limit to hide an overload. - A
SIGXCPUfollowed bySIGKILLfrom the audio thread withsi_code=SI_KERNELis an RT-budget failure. A Rust panic hook and in-process crash modal cannot observeSIGKILL; preserve the previous-run unexpected-exit report.
- CoreAudio owns the device callback cadence. Callback code follows the common RT rules and must return within the negotiated buffer duration.
- Time-constraint scheduling and Audio Workgroups express period, computation
and deadline to Darwin. A deadline miss normally appears as an overload or
audio dropout; Linux
RLIMIT_RTTIMEsemantics do not apply. - Device sample-rate or channel-layout changes require rebuilding the stream. Do not retain callbacks, HAL objects or plugin UI objects past their documented owner lifetime.
- WASAPI owns the render callback cadence. Use the negotiated mix/device format exactly and rebuild after endpoint invalidation or format change.
- Time-critical audio work belongs to MMCSS/Pro Audio scheduling. Do not use generic process or thread priority boosts as a substitute, and never block, allocate or perform COM/UI work in the render callback.
- COM initialization and endpoint management remain on control threads. The callback exchanges audio and status only through preallocated buffers and lock-free state.
- The physical stream always receives its native channel width. Sample-rate conversion may stop at the highest routed channel; remaining channels are explicitly zero-filled before the device ring.
- Callback scratch storage is allocated before playback. Oversized callbacks
are processed in channel-aligned chunks and never grow a
Vecon the audio thread. - Fixed-rate conversion, drift correction and channel mapping are separate decisions. Do not choose a resampler merely because two nominal rates differ.