[client] audio: discard stale playback backlog

This commit is contained in:
Geoffrey McRae
2026-08-12 13:43:07 +10:00
parent 88b9bc32d1
commit 573204d44e

View File

@@ -130,6 +130,7 @@ typedef struct
double outputPosition;
double appliedRatio;
int startupSilenceFrames;
unsigned int discontinuity;
}
PlaybackDeviceData;
@@ -185,10 +186,12 @@ typedef struct
double sourceRateFrameSec;
bool sourceRateValid;
bool bufferOverrunPending;
bool backlogTrimArmed;
int devPeriodFrames;
int64_t devReadPosition;
unsigned int deviceTimingSequence;
unsigned int deviceDiscontinuity;
int64_t deviceClockAcquireStart;
int64_t deviceClockCheckTime;
double deviceClockCheckFrameSec;
@@ -216,6 +219,7 @@ PlaybackSourceData;
typedef struct
{
atomic_uint sequence;
atomic_uint discontinuity;
atomic_int periodFrames;
_Atomic(int64_t) time;
_Atomic(int64_t) position;
@@ -234,10 +238,9 @@ typedef struct
{
double softwareLatencyMs;
double targetLatencyMs;
double sourcePpm;
double devicePpm;
double controlPpm;
double jitterMs;
double backlogTrimmedMs;
unsigned int underruns;
unsigned int overruns;
PlaybackRateControl rateControl;
@@ -336,7 +339,9 @@ typedef struct
atomic_bool backendResamplerFailed;
RingBuffer buffer;
PlaybackDeviceTiming deviceTiming;
atomic_int backlogTrimTarget;
atomic_uint underruns;
atomic_uint_fast64_t backlogTrimmedFrames;
RingBuffer timings;
GraphHandle graph;
@@ -561,6 +566,7 @@ typedef struct
int64_t nextTime;
int64_t nextPosition;
double outputPosition;
unsigned int discontinuity;
}
PlaybackDeviceTick;
@@ -810,15 +816,17 @@ static void playbackSourceRateAdd(PlaybackSourceData * sourceData,
sourceData->rateFilterTimeMs = timeMs;
}
static void playbackPublishDeviceTiming(
int periodFrames, int64_t time, int64_t position,
double outputPosition)
static void playbackPublishDeviceTiming(int periodFrames, int64_t time,
int64_t position, double outputPosition,
unsigned int discontinuity)
{
PlaybackDeviceTiming * timing = &audio.playback.deviceTiming;
/* Publish the odd writer marker before any snapshot field. */
atomic_fetch_add_explicit(&timing->sequence, 1, memory_order_seq_cst);
atomic_store_explicit(
&timing->periodFrames, periodFrames, memory_order_relaxed);
atomic_store_explicit(
&timing->discontinuity, discontinuity, memory_order_relaxed);
atomic_store_explicit(&timing->time, time, memory_order_relaxed);
atomic_store_explicit(&timing->position, position, memory_order_relaxed);
atomic_store_explicit(
@@ -842,6 +850,8 @@ static bool playbackReadDeviceTiming(
tick->periodFrames =
atomic_load_explicit(&timing->periodFrames, memory_order_relaxed);
tick->discontinuity = atomic_load_explicit(
&timing->discontinuity, memory_order_relaxed);
tick->nextTime =
atomic_load_explicit(&timing->time, memory_order_relaxed);
tick->nextPosition =
@@ -1250,6 +1260,27 @@ static int playbackPullFrames(uint8_t * dst, int frames)
memory_order_acq_rel, memory_order_acquire);
}
if (playbackGetState() == STREAM_STATE_RUN)
{
const int trimTarget = atomic_exchange_explicit(
&audio.playback.backlogTrimTarget, -1, memory_order_acq_rel);
if (trimTarget >= 0)
{
const int queued = ringbuffer_getCount(audio.playback.buffer);
const int requested = max(queued - trimTarget, 0);
const int dropped = ringbuffer_consume(
audio.playback.buffer, NULL, requested);
if (dropped > 0)
{
data->nextPosition += dropped;
++data->discontinuity;
atomic_fetch_add_explicit(
&audio.playback.backlogTrimmedFrames, dropped,
memory_order_relaxed);
}
}
}
/* Timestamp the dequeue boundary before the current pull. The logical
* position tracks source frames consumed from the ring for latency
* measurement. With backend resampling, outputPosition separately tracks
@@ -1257,7 +1288,8 @@ static int playbackPullFrames(uint8_t * dst, int frames)
* current request using the rate set by the previous callback, so apply
* that same ratio to this period before adopting nextRatio. */
playbackPublishDeviceTiming(
frames, now, data->nextPosition, data->outputPosition);
frames, now, data->nextPosition, data->outputPosition,
data->discontinuity);
data->nextPosition += frames;
data->outputPosition += frames * data->appliedRatio;
data->appliedRatio = nextRatio;
@@ -1380,12 +1412,14 @@ static bool playbackStart(const LG_AudioFormat * format,
audio.playback.deviceData.outputPosition = 0.0;
audio.playback.deviceData.appliedRatio = 1.0;
audio.playback.deviceData.startupSilenceFrames = 0;
audio.playback.deviceData.discontinuity = 0;
audio.playback.sourceData.inputPosition = 0;
audio.playback.sourceData.outputPosition = 0;
audio.playback.sourceData.devPeriodFrames = 0;
audio.playback.sourceData.devReadPosition = 0;
audio.playback.sourceData.deviceTimingSequence = 0;
audio.playback.sourceData.deviceDiscontinuity = 0;
playbackDeviceClockAcquireReset(&audio.playback.sourceData);
audio.playback.sourceData.offsetError = 0.0;
audio.playback.sourceData.offsetErrorIntegral = 0.0;
@@ -1395,6 +1429,7 @@ static bool playbackStart(const LG_AudioFormat * format,
audio.playback.sourceData.nextFeedbackTime = 0;
audio.playback.sourceData.nextGraphTime = 0;
audio.playback.sourceData.bufferOverrunPending = false;
audio.playback.sourceData.backlogTrimArmed = true;
audio.playback.sourceData.bufferOverruns = 0;
audio.playback.sourceData.nextLogTime =
nanotime() + INT64_C(5000000000);
@@ -1407,8 +1442,16 @@ static bool playbackStart(const LG_AudioFormat * format,
audio.playback.sourceData.outputClock.valid = false;
playbackPrepareMediaClock(&audio.playback.sourceData, sourceClock);
atomic_store_explicit(
&audio.playback.backlogTrimTarget, -1, memory_order_relaxed);
atomic_store_explicit(
&audio.playback.backlogTrimmedFrames, 0, memory_order_relaxed);
atomic_store_explicit(
&audio.playback.deviceTiming.sequence, 0, memory_order_relaxed);
atomic_store_explicit(
&audio.playback.deviceTiming.discontinuity, 0,
memory_order_relaxed);
atomic_store_explicit(
&audio.playback.deviceTiming.periodFrames, 0, memory_order_relaxed);
atomic_store_explicit(
@@ -1889,23 +1932,20 @@ static void playbackProcessDiagnostics(void)
if (diagnostics.pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG)
{
const bool providerControl =
diagnostics.sync.rateControl == PLAYBACK_RATE_PROVIDER;
const char * controlName = providerControl ? "feedback" :
const char * controlName =
diagnostics.sync.rateControl == PLAYBACK_RATE_PROVIDER ? "feedback" :
diagnostics.sync.rateControl == PLAYBACK_RATE_BACKEND ?
"backend" : "software";
const char * sourceRateName = providerControl ? "arrival" : "source";
DEBUG_INFO(
"Audio sync: ring %.2f/%.2f ms, backend %.2f ms, "
"%s %+.1f ppm, rates %s/device %+.1f/%+.1f ppm, "
"jitter %.2f ms, xruns %u/%u",
"%s %+.1f ppm, jitter %.2f ms, xruns %u/%u, drop %.2f ms",
diagnostics.sync.softwareLatencyMs,
diagnostics.sync.targetLatencyMs,
backendLatencyMs, controlName, diagnostics.sync.controlPpm,
sourceRateName, diagnostics.sync.sourcePpm,
diagnostics.sync.devicePpm, diagnostics.sync.jitterMs,
diagnostics.sync.underruns, diagnostics.sync.overruns);
diagnostics.sync.jitterMs,
diagnostics.sync.underruns, diagnostics.sync.overruns,
diagnostics.sync.backlogTrimmedMs);
}
}
@@ -2257,6 +2297,24 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
sourceData->devPeriodFrames = deviceTick.periodFrames;
sourceData->devReadPosition =
deviceTick.nextPosition + deviceTick.periodFrames;
if (deviceTick.discontinuity != sourceData->deviceDiscontinuity)
{
sourceData->deviceDiscontinuity = deviceTick.discontinuity;
playbackClockReset(&sourceData->deviceClock,
deviceTick.nextTime, deviceTick.nextPosition, nominalFrameSec);
if (audio.playback.rateControl == PLAYBACK_RATE_BACKEND)
playbackClockReset(&sourceData->outputClock,
deviceTick.nextTime, deviceTick.outputPosition,
nominalFrameSec);
playbackDeviceClockAcquireReset(sourceData);
sourceData->offsetError = 0.0;
sourceData->offsetErrorIntegral = 0.0;
sourceData->ratioIntegral = 0.0;
sourceData->lastRatio = 1.0;
sourceData->nextFeedbackTime = 0;
}
else
{
const bool deviceClockUpdated =
playbackClockUpdate(&sourceData->deviceClock,
deviceTick.nextTime, deviceTick.nextPosition, nominalFrameSec);
@@ -2274,6 +2332,7 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
deviceClockBecameStable =
playbackDeviceClockAcquire(sourceData, deviceTick.nextTime);
}
}
if (deviceClockBecameStable)
{
@@ -2303,15 +2362,23 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
const int maxPeriodFrames =
max(audio.playback.deviceMaxPeriodFrames, sourceData->devPeriodFrames);
const double backlogGuardFrames =
max((double)maxPeriodFrames,
sourceData->sourcePacketDurationSec * audio.playback.sampleRate);
/* The device period, delivery jitter, packet phase, and resampler delay
* define the minimum viable latency. Provider feedback directly controls
* the source rate, so latencyOffset only applies to local rate control. */
* the source rate, so latencyOffset only applies to local rate control.
* Provider delivery reserve is bounded because late USB packets must be
* skipped rather than allowed to become persistent playback latency. */
const double latencyOffsetFrames = providerRateControl ? 0.0 :
max(g_params.audioLatencyOffset, 0) *
audio.playback.sampleRate / 1000.0;
const double arrivalJitterFrames =
sourceData->arrivalJitterSec * audio.playback.sampleRate;
const double arrivalReserveFrames =
(sourceData->arrivalJitterSec + 0.001) *
audio.playback.sampleRate;
(providerRateControl ?
min(arrivalJitterFrames, backlogGuardFrames) :
arrivalJitterFrames) + 0.001 * audio.playback.sampleRate;
const double minimumLowWaterReserveFrames =
maxPeriodFrames * 0.1 + arrivalReserveFrames;
const double minimumLowWaterFrames =
@@ -2387,10 +2454,19 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
double actualOffsetError = 0.0;
if (providerRateControl)
{
const bool trimPending = !sourceData->backlogTrimArmed;
const int occupancy = ringbuffer_getCount(audio.playback.buffer);
actualLatencyFrames = occupancy + sourceReserveFrames;
actualOffsetError = targetLowWaterFrames - occupancy;
if (trimPending)
{
actualOffsetError = 0.0;
sourceData->offsetError = 0.0;
sourceData->offsetErrorIntegral = 0.0;
}
else
{
const double error =
actualOffsetError - sourceData->offsetError;
const double periodSec = frames * nominalFrameSec;
@@ -2403,6 +2479,7 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
sourceData->offsetErrorIntegral;
sourceData->offsetErrorIntegral += c * error;
}
}
else if (sourceData->deviceClock.valid)
{
if (sourceData->deviceClockStable)
@@ -2562,6 +2639,32 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
const int outputFrames =
playbackAppendFrames(sourceData, inputFrames, frames);
sourceData->outputPosition += outputFrames;
const int occupancy = ringbuffer_getCount(audio.playback.buffer);
if (!sourceData->backlogTrimArmed &&
occupancy <= targetLowWaterFrames + backlogGuardFrames)
sourceData->backlogTrimArmed = true;
/* Feedback corrects clock drift, but cannot remove audio which is
* already queued. Ask the device thread to discard the oldest part of a
* delayed burst instead of replaying stale audio for several seconds. */
if (playbackGetState() == STREAM_STATE_RUN && !discontinuity &&
sourceData->backlogTrimArmed &&
occupancy > targetLowWaterFrames + 2.0 * backlogGuardFrames)
{
const int trimTarget = clamp(
llrint(ceil(targetLowWaterFrames)),
INT64_C(0), (int64_t)INT_MAX);
atomic_store_explicit(
&audio.playback.backlogTrimTarget, trimTarget,
memory_order_release);
sourceData->backlogTrimArmed = false;
sourceData->offsetError = 0.0;
sourceData->offsetErrorIntegral = 0.0;
sourceData->ratioIntegral = 0.0;
sourceData->lastRatio = 1.0;
sourceData->nextFeedbackTime = 0;
}
}
else
{
@@ -2681,12 +2784,6 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
if (now >= sourceData->nextLogTime)
{
const double sourcePpm = sourceData->sourceRateValid ?
(nominalFrameSec / sourceData->sourceRateFrameSec - 1.0) * 1.0e6 :
0.0;
const double devicePpm = rateClock->valid ?
(nominalFrameSec / rateClock->frameSec - 1.0) * 1.0e6 :
0.0;
const bool providerControl =
audio.playback.rateControl == PLAYBACK_RATE_PROVIDER;
const double controlPpm = providerControl ?
@@ -2702,17 +2799,23 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
const unsigned int pendingOverruns =
diagnostics->pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG ?
diagnostics->sync.overruns : 0;
const double pendingBacklogTrimmedMs =
diagnostics->pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG ?
diagnostics->sync.backlogTrimmedMs : 0.0;
diagnostics->sync = (PlaybackSyncDiagnostics)
{
.softwareLatencyMs = softwareLatencyMs,
.targetLatencyMs =
targetLatencyFrames * 1000.0 / audio.playback.sampleRate,
.sourcePpm = sourcePpm,
.devicePpm = devicePpm,
.controlPpm = controlPpm,
.jitterMs = sourceData->arrivalJitterSec * 1000.0,
.underruns = pendingUnderruns + underruns,
.overruns = pendingOverruns + sourceData->bufferOverruns,
.backlogTrimmedMs = pendingBacklogTrimmedMs +
atomic_exchange_explicit(
&audio.playback.backlogTrimmedFrames, 0,
memory_order_relaxed) * 1000.0 /
audio.playback.sampleRate,
.rateControl = audio.playback.rateControl,
};
diagnostics->pending |= PLAYBACK_DIAGNOSTIC_SYNC_LOG;
@@ -4132,6 +4235,9 @@ void lgAudio_init(void)
atomic_init(&audio.playback.failedAttemptSerial, 0);
atomic_init(&audio.playback.diagnosticsEpoch, 1);
atomic_init(&audio.playback.graphReady, false);
atomic_init(&audio.playback.backlogTrimTarget, -1);
atomic_init(&audio.playback.backlogTrimmedFrames, 0);
atomic_init(&audio.playback.deviceTiming.discontinuity, 0);
atomic_init(&audio.playback.worker.stop, false);
atomic_init(&audio.playback.worker.wakePending, false);
atomic_init(&audio.record.deliverySerial, 0);