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