NativeRingAudioSink

A sink that owns its device callback in C, and therefore owns the ring that callback reads.

Why this exists

Every sink used to be handed an AudioRenderCallback and called it from whatever thread its device used. On macOS that made the device's real-time thread a Kotlin mutator the garbage collector has to stop at a safepoint, and the measured worst stop-the-world pause on the development machine was 63 to 256 microseconds against a 10.67 millisecond period. The fix is a callback that never leaves C, and a callback that never leaves C cannot be handed a Kotlin lambda. So a sink that has one says so by implementing this interface, and gets a ring instead.

Why it is a capability and not a fork

The choice belongs to the sink, not to the platform, because the sink is what owns the device callback. A native sink with no C callback simply does not implement this interface and keeps working exactly as before, through AudioSink.open and a Kotlin ring. Nothing in the engine branches on the operating system, and there is one code path for policy: everything about backpressure, clock anchoring, flush ordering and buffer sizing stays in commonMain.

Ownership, which is the part that bites

The ring belongs to the sink for its whole life, and AudioSink.close releases it. That is not a convention that could have gone the other way. Clearing or freeing a ring requires the device callback to be provably out of its render path, and the only code that can promise that is the code that stopped the device. So the engine writes into the ring and never frees it.

What a caller must not do

AudioSink.open is not the entry point for a sink that implements this. Calling it must fail loudly rather than open a device whose callback ignores the lambda it was given, because a device that plays silence while the caller believes its callback is being called is the worst of the three possible behaviours.

Properties

Link copied to clipboard

Whether stopping or pausing this device cuts the sound at whatever sample it reached (#486).

Link copied to clipboard
abstract val deviceBufferFrames: Int

The device's own buffer size in sample frames. Sizes the engine's ring.

Link copied to clipboard
abstract val events: Flow<AudioSinkEvent>

Device loss, underrun, format change. The sink reports; the engine decides what to do.

Link copied to clipboard

How far latencyNanos can be trusted.

Link copied to clipboard

A platform handle for effects that attach to this device's stream, or null when the platform has no such concept.

Functions

Link copied to clipboard
abstract fun close()
Link copied to clipboard
abstract suspend fun drain()

Plays out what is queued, then stops. This is the end-of-media path.

Link copied to clipboard
abstract fun latencyNanos(): Long

Nanoseconds of audio handed over but not yet audible, including everything inside the OS and the hardware.

Link copied to clipboard
abstract suspend fun open(request: AudioFormat, render: AudioRenderCallback): AudioFormat

Opens the device.

Link copied to clipboard
abstract suspend fun openWithRing(request: AudioFormat, capacityFrames: (AudioFormat) -> Int): NativeRingHandoff

Opens the device and the ring behind it together, and hands back both.

Link copied to clipboard
open fun setContent(content: AudioContent)

What the sound the next open plays is, for the platform's own sound processing (#446). The engine calls this before every open with the item's AudioContent, never AudioContent.Automatic, which it has already resolved. It takes effect at that open: a device already open keeps what it was opened with. A platform with no such setting ignores it, which is the default.

Link copied to clipboard
abstract suspend fun setPaused(paused: Boolean): Boolean

Pauses without discarding.

Link copied to clipboard
abstract suspend fun start()

Starts the device pulling samples through the render callback.

Link copied to clipboard
abstract suspend fun stop()

Stops and discards everything unplayed. This is the seek path.