AudioRenderCallback
Called by the audio device on its own real-time thread.
Every clause of this contract exists because breaking it produces an audible glitch:
Not a suspending function. There is no coroutine on this thread and no dispatcher to resume on.
Must not allocate, must not take a contended lock, must not log, must not throw.
Reads from a preallocated single-producer single-consumer ring, and when that ring is dry writes silence and returns rather than waiting for the feeder.
The
deadlineNanosargument of onRender is when the last frame of this buffer becomes audible, on the engine's monotonic clock. It is what the audio clock is anchored to, and it is the reason this callback takes a time at all.
ffplay does the opposite of all of this: it resamples, allocates and computes A/V correction inside the device callback. The busy-wait workaround in its Windows path is the visible scar.
What this interface can and cannot promise, corrected
An earlier version of this note said the ring is read "with a try-lock, and on contention writes silence". That was never true of any ring in this library: KotlinAudioRing takes no lock at all. What it does instead is worse in one specific place: while publishing the clock anchor, the real-time thread reads a sequence counter the feeder writes, and it retries with no bound if it catches the feeder mid-update. That is a priority inversion on a real-time thread, and it is why the shipped macOS path no longer goes through this interface at all: CoreAudioSink owns a C callback over a C ring in which the real-time thread is the writer and never waits.
This interface remains the contract for every other sink, and KotlinAudioRing remains its implementation, permanently: js and wasmJs can never contain C, and the Kotlin ring is the only oracle the C one can be checked against. What it must not be presented as is coverage of the macOS device path.
Return
frames written, counted from the start of the buffer. Fewer than the frames asked for means the remainder is silence, and writing that silence is the sink's own obligation. That is stated rather than implied because the engine's silence fill moved into kprt_ring_render, which is the C path and is not this one: on this path nothing above the sink zeroes the tail, and an unwritten device buffer plays whatever was left in it. AudioSinkBuffer.writeSilence is what a sink writes it with.
Functions
Writes up to frames sample frames into destination, whose last frame becomes audible at deadlineNanos.