AudioPlayback

class AudioPlayback(sink: AudioSink, clock: MonotonicClock = MonotonicClock.System, bufferDuration: Duration = 200.milliseconds, onWarning: (PlaybackWarning) -> Unit = {}, downmix: DownmixConfig = DownmixConfig(), resampler: AudioResamplerFactory? = null, upmix: UpmixMode = UpmixMode.Off) : AutoCloseable

The engine's audio half: a device, a ring, and the clock derived from them.

The full player composes this with the video path. On its own it is a complete audio player, which is why it is public: a music application needs exactly this and nothing more.

What it is responsible for

Accepting decoded PCM, holding it so the device's period size is invisible to the decoder, and maintaining the master clock. That last part is the reason this class exists rather than the caller writing to a sink directly. The clock is not counted from samples submitted; it is anchored to the instant the device says a specific frame becomes audible. Every player that instead estimates the device latency ships a fixed audio delay that nothing corrects.

Threading

submitDecoded belongs to one coroutine, the audio feeder, because it is the ring's single producer, and the conversion stage behind it belongs to the same coroutine. The device's real-time callback is the single consumer and touches nothing else in this class, so neither side takes a lock.

anchorClock, position, buffered, underruns, the speed setter and every gain change are guarded by one internal lock. The first two write the media clock, which has one writer by design, and a player reports progress from a thread that is not the one driving playback: two callers re-anchoring the same clock at once is what the lock is for. buffered and underruns take it for a second reason that came with the C callback: they read the ring, and the ring can now be memory close frees. A gain change writes into the ring, so it takes the lock for the same reason. Reading speed is a plain read of one value and needs nothing.

open, play, pause, flush, drain, endOfStream and close are thread confined to the session owner instead. submitDecoded runs on the feed worker and reads the ring FIELD under the lock, so the rule is one sentence again: any member that may run beside another thread touches that field only under the lock. A lock cannot be held across a suspension point, so the suspending ones could not be guarded even in principle, and their contract is confinement. The core's session actor is that owner. The seek path already depends on this: the ring's own flush requires both of its sides to be quiescent first.

close is the one member that is confined AND takes the lock, for one statement. Confinement says no other owner call runs beside it; it says nothing about the four members above, which are documented safe from any thread. Clearing the ring reference inside the lock is what makes those four safe against a teardown that frees a C ring underneath them.

Constructors

Link copied to clipboard
constructor(sink: AudioSink, clock: MonotonicClock = MonotonicClock.System, bufferDuration: Duration = 200.milliseconds, onWarning: (PlaybackWarning) -> Unit = {}, downmix: DownmixConfig = DownmixConfig(), resampler: AudioResamplerFactory? = null, upmix: UpmixMode = UpmixMode.Off)

Properties

Link copied to clipboard

Stereo balance: -1 is hard left, 0 is centre, 1 is hard right.

Link copied to clipboard

How much submitted audio has not yet been handed to the device.

Link copied to clipboard

How far, in decibels, the centre channel is raised or lowered where the downmix folds it into other speakers (#442). Applied as audio is written, so it is heard after the ring's depth, as the balance is.

Link copied to clipboard

The ten-band equaliser. Flat by default, and free while it is.

Link copied to clipboard

The sink's own event feed, surfaced so the engine can warn on device loss.

Link copied to clipboard

What the device says it is holding: handed over, not yet audible.

Link copied to clipboard
Link copied to clipboard

Rendered frames the ring's peak limiter turned down because they would have passed full scale. Under the lock, as buffered is.

Link copied to clipboard

Silence without losing the volume setting. Ramped like volume, and safe from any thread.

Link copied to clipboard

The format the device accepted. Null before open.

Link copied to clipboard

Whether the night mode narrows the distance between the quiet and the loud parts of the sound (#442). Applied as audio is written, after the downmix and the stereo mode, so it is heard after the ring's depth, as the balance is.

Link copied to clipboard

How far, in semitones, the pitch is moved without changing how fast the sound plays (#465). Applied to the next buffer the feeder converts with no seam, as a speed change is, and dated in the timeline as nothing at all, because it changes no timing.

Link copied to clipboard

The sink's platform handle for audio effects, or null. See AudioSink.platformSessionId.

Link copied to clipboard

Whether speed keeps pitch. True stretches the sound in time; false plays it faster or slower like a turntable, so pitch moves with the rate, mpv's audio-pitch-correction=no. A change applies to the next buffer with no seam, the same way a speed change does.

Link copied to clipboard

The ReplayGain to apply to the material, as a linear multiplier. 1 applies nothing.

Link copied to clipboard

Whether every pause longer than a fifth of a second is cut down to a fifth of a second (#429). Applied as audio is written, so it is heard after the ring's depth, as the balance is. Each cut is a line in the playout timeline, where the output reaches it, so position follows it with no jump of its own. The engine turns it off for an item with a picture and for a live stream.

Link copied to clipboard

The playback rate as a multiplier of real time, within TempoStage.MIN_SPEED to TempoStage.MAX_SPEED. Real: the tempo stage in the pipeline makes the sound take 1/speed as long, at its own pitch unless preservePitch is false, and the clock runs to match.

Link copied to clipboard

What the two front speakers play: see StereoMode. Applied as audio is written, after the downmix and before the balance, so it is heard after the ring's depth, as the balance is.

Link copied to clipboard

Callbacks handed silence because the ring had run dry. Under the lock, as buffered is.

Link copied to clipboard

Playback volume, from silence at 0 through unity at 1 to amplification at 2.

Functions

Link copied to clipboard

Anchors the clock from what the device last reported.

Link copied to clipboard
open override fun close()

Quiescence precondition, stated in the same words flush's is: the feeder must not be between a submit call's start and its return when this runs. Confinement alone does not give that, because submit runs on the feed worker rather than the session owner; what gives it is the engine joining the feeder's job before teardown reaches this call. A submit that races a close anyway reads the cleared field under the lock and fails loudly instead of touching freed memory.

Link copied to clipboard
suspend fun drain()

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

Link copied to clipboard

Tells the audio path that no more audio is coming.

Link copied to clipboard
suspend fun finishDecoded(abort: () -> Boolean = { false }): Int

Pushes the last of the decoded audio out of the DSP stages and into the ring.

Link copied to clipboard
suspend fun flush(newGeneration: Generation)

Discards everything unplayed and invalidates the clock. This is the seek path.

Link copied to clipboard
suspend fun open(request: AudioFormat): AudioFormat

Opens the device and sizes the ring for it.

Link copied to clipboard
suspend fun pause()

Freezes the clock and holds the device without discarding. Belongs to the session owner.

Link copied to clipboard
suspend fun play()

Starts the device and lets the clock run. Belongs to the session owner.

Link copied to clipboard
fun position(): Pts?

What media timestamp is audible now, or null when nothing has played since the last flush.

Link copied to clipboard
suspend fun submitDecoded(pts: Pts?, interleaved: FloatArray, frames: Int, sourceFormat: AudioFormat, abort: () -> Boolean = { false })

Hands decoded audio over, suspending until all of it has been accepted.