[client] audio: reset playback cleanly on resume

Count one underrun per starvation episode and end the sync diagnostics
epoch when a logical source stops.

Resume a compatible backend synchronously so data after a same-wire
restart is accepted immediately. Rebase the first post-idle device tick
without passing a long gap through the short-step clock filter.

Cancel in-flight backlog trims before fresh audio can be queued.
This commit is contained in:
Geoffrey McRae
2026-08-22 14:25:33 +10:00
parent d8e7966087
commit ebf764ae25
3 changed files with 333 additions and 57 deletions

View File

@@ -131,6 +131,7 @@ typedef struct
double appliedRatio;
int startupSilenceFrames;
unsigned int discontinuity;
bool underrunning;
}
PlaybackDeviceData;
@@ -238,6 +239,7 @@ PlaybackStartDiagnostics;
typedef struct
{
unsigned int epoch;
double softwareLatencyMs;
double targetLatencyMs;
double controlPpm;
@@ -342,12 +344,14 @@ typedef struct
RingBuffer buffer;
PlaybackDeviceTiming deviceTiming;
atomic_int backlogTrimTarget;
atomic_flag deviceStateGate;
atomic_uint underruns;
atomic_uint_fast64_t backlogTrimmedFrames;
RingBuffer timings;
GraphHandle graph;
atomic_uint diagnosticsEpoch;
atomic_uint syncDiagnosticsEpoch;
atomic_bool graphReady;
bool graphRegistrationAttempted;
PlaybackDiagnostics diagnostics;
@@ -622,6 +626,16 @@ static bool playbackClockUpdate(PlaybackClock * clock, int64_t time,
return true;
}
static void playbackClockRebase(
PlaybackClock * clock, int64_t time, double position)
{
DEBUG_ASSERT(clock->valid);
clock->time = time;
clock->position = position;
clock->phaseResidualSec = 0.0;
++clock->updates;
}
static bool playbackSourceClockUpdate(PlaybackClock * clock, int64_t time,
double position, double nominalFrameSec)
{
@@ -1140,6 +1154,31 @@ static PlaybackDiagnostics * playbackDiagnosticsLocked(void)
return diagnostics;
}
/* sourceLock must be held. End the old source's diagnostic interval without
* invalidating graph work, which belongs to the lifetime of the backend. */
static void playbackResetSyncDiagnosticsLocked(void)
{
atomic_fetch_add_explicit(
&audio.playback.syncDiagnosticsEpoch, 1, memory_order_release);
while (atomic_flag_test_and_set_explicit(
&audio.playback.deviceStateGate, memory_order_acquire))
;
atomic_store_explicit(
&audio.playback.backlogTrimTarget, -1, memory_order_relaxed);
atomic_store_explicit(
&audio.playback.underruns, 0, memory_order_relaxed);
atomic_store_explicit(
&audio.playback.backlogTrimmedFrames, 0, memory_order_relaxed);
audio.playback.deviceData.underrunning = false;
atomic_flag_clear_explicit(
&audio.playback.deviceStateGate, memory_order_release);
audio.playback.sourceData.bufferOverruns = 0;
PlaybackDiagnostics * diagnostics = playbackDiagnosticsLocked();
diagnostics->pending &= ~PLAYBACK_DIAGNOSTIC_SYNC_LOG;
diagnostics->sync = (PlaybackSyncDiagnostics) { 0 };
}
static void playbackStop(void)
{
const bool alreadyStopped = playbackGetState() == STREAM_STATE_STOP;
@@ -1263,6 +1302,12 @@ static int playbackPullFrames(uint8_t * dst, int frames)
memory_order_acq_rel, memory_order_acquire);
}
if (playbackGetState() == STREAM_STATE_RUN &&
atomic_load_explicit(
&audio.playback.backlogTrimTarget, memory_order_acquire) >= 0 &&
!atomic_flag_test_and_set_explicit(
&audio.playback.deviceStateGate, memory_order_acquire))
{
if (playbackGetState() == STREAM_STATE_RUN)
{
const int trimTarget = atomic_exchange_explicit(
@@ -1283,6 +1328,9 @@ static int playbackPullFrames(uint8_t * dst, int frames)
}
}
}
atomic_flag_clear_explicit(
&audio.playback.deviceStateGate, memory_order_release);
}
/* Timestamp the dequeue boundary before the current pull. The logical
* position tracks source frames consumed from the ring for latency
@@ -1307,10 +1355,20 @@ static int playbackPullFrames(uint8_t * dst, int frames)
const int audioFrames = frames - silenceFrames;
if (g_params.audioDebug &&
!atomic_flag_test_and_set_explicit(
&audio.playback.deviceStateGate, memory_order_acquire))
{
const bool underrunning = audioFrames > 0 &&
playbackGetState() == STREAM_STATE_RUN &&
ringbuffer_getCount(audio.playback.buffer) < audioFrames)
ringbuffer_getCount(audio.playback.buffer) < audioFrames;
const bool wasUnderrunning = data->underrunning;
data->underrunning = underrunning;
if (underrunning && !wasUnderrunning)
atomic_fetch_add_explicit(
&audio.playback.underruns, 1, memory_order_relaxed);
atomic_flag_clear_explicit(
&audio.playback.deviceStateGate, memory_order_release);
}
ringbuffer_consume(audio.playback.buffer,
dst + (size_t)silenceFrames * audio.playback.stride, audioFrames);
}
@@ -1360,6 +1418,28 @@ static bool playbackSetupDevice(const LG_AudioFormat * format,
audio.playback.deviceStartFrames >= 0;
}
static bool playbackResume(const LG_AudioFormat * format,
const LG_AudioClock * sourceClock, bool providerRateControl,
bool forceSoftwareResampler)
{
if (forceSoftwareResampler ||
!audio.playback.lastFormatValid ||
audio.playback.lastProviderRateControl != providerRateControl ||
!audioFormatEqual(format, &audio.playback.lastFormat))
return false;
StreamState expected = STREAM_STATE_KEEP_ALIVE;
if (!atomic_compare_exchange_strong_explicit(
&audio.playback.state, &expected, STREAM_STATE_RESUMING,
memory_order_acq_rel, memory_order_acquire))
return false;
playbackPrepareMediaClock(&audio.playback.sourceData, sourceClock);
audio.playback.sourceData.nextLogTime =
nanotime() + INT64_C(5000000000);
return true;
}
static bool playbackStart(const LG_AudioFormat * format,
const LG_AudioClock * sourceClock, bool providerRateControl,
bool forceSoftwareResampler)
@@ -1378,24 +1458,11 @@ static bool playbackStart(const LG_AudioFormat * format,
const int channels = format->channelCount;
const int sampleRate = format->sampleRate;
StreamState state = playbackGetState();
if (!forceSoftwareResampler && state == STREAM_STATE_KEEP_ALIVE &&
audio.playback.lastFormatValid &&
audio.playback.lastProviderRateControl == providerRateControl &&
audioFormatEqual(format, &audio.playback.lastFormat))
{
StreamState expected = STREAM_STATE_KEEP_ALIVE;
if (atomic_compare_exchange_strong_explicit(
&audio.playback.state, &expected, STREAM_STATE_RESUMING,
memory_order_acq_rel, memory_order_acquire))
{
playbackPrepareMediaClock(
&audio.playback.sourceData, sourceClock);
if (playbackResume(format, sourceClock, providerRateControl,
forceSoftwareResampler))
return true;
}
state = expected;
}
const StreamState state = playbackGetState();
if (state != STREAM_STATE_STOP)
playbackStop();
@@ -1416,6 +1483,7 @@ static bool playbackStart(const LG_AudioFormat * format,
audio.playback.deviceData.appliedRatio = 1.0;
audio.playback.deviceData.startupSilenceFrames = 0;
audio.playback.deviceData.discontinuity = 0;
audio.playback.deviceData.underrunning = false;
audio.playback.sourceData.inputPosition = 0;
audio.playback.sourceData.outputPosition = 0;
@@ -1894,6 +1962,7 @@ static void playbackProcessDiagnostics(void)
LG_UNLOCK(audio.playback.sourceLock);
bool current = false;
bool syncCurrent = false;
double backendLatencyMs = 0.0;
LG_LOCK(audio.playback.deviceLock);
const StreamState state = playbackGetState();
@@ -1902,6 +1971,8 @@ static void playbackProcessDiagnostics(void)
state != STREAM_STATE_STOP && state != STREAM_STATE_STOP_PENDING;
if (current)
{
syncCurrent = diagnostics.sync.epoch == atomic_load_explicit(
&audio.playback.syncDiagnosticsEpoch, memory_order_acquire);
const bool timingsCurrent = timings &&
timings == audio.playback.timings;
if ((diagnostics.pending & PLAYBACK_DIAGNOSTIC_REGISTER_GRAPH) &&
@@ -1920,6 +1991,7 @@ static void playbackProcessDiagnostics(void)
app_invalidateGraph(audio.playback.graph);
if ((diagnostics.pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG) &&
syncCurrent &&
audio.audioDev && audio.audioDev->playback.latency)
backendLatencyMs =
audio.audioDev->playback.latency() / 1000.0;
@@ -1935,7 +2007,10 @@ static void playbackProcessDiagnostics(void)
diagnostics.start.targetLatencyMs,
diagnostics.start.queuedLatencyMs);
if (diagnostics.pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG)
if ((diagnostics.pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG) &&
syncCurrent &&
diagnostics.sync.epoch == atomic_load_explicit(
&audio.playback.syncDiagnosticsEpoch, memory_order_acquire))
{
const char * controlName =
diagnostics.sync.rateControl == PLAYBACK_RATE_PROVIDER ? "feedback" :
@@ -1972,6 +2047,9 @@ static void playbackSourceStop(void)
memory_order_acq_rel, memory_order_acquire))
break;
audio.playback.sourceData.backlogTrimArmed = true;
playbackResetSyncDiagnosticsLocked();
// Reset the software resampler so it is safe for the next playback
if (audio.playback.sourceData.src)
{
@@ -2135,11 +2213,13 @@ static int playbackSlewBuffer(
}
static bool playbackUseLowWaterRecovery(bool providerRateControl,
bool bufferUnderrun, const PlaybackSourceData * sourceData)
bool bufferUnderrun, StreamState state,
const PlaybackSourceData * sourceData)
{
return providerRateControl ||
(bufferUnderrun && (!sourceData->deviceClock.valid ||
!sourceData->deviceClockStable));
((!sourceData->deviceClock.valid || !sourceData->deviceClockStable) &&
(bufferUnderrun || state == STREAM_STATE_KEEP_ALIVE ||
state == STREAM_STATE_RESUMING));
}
static void playbackResetRateControl(PlaybackSourceData * sourceData,
@@ -2376,6 +2456,49 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
sourceData->lastRatio = 1.0;
sourceData->nextFeedbackTime = 0;
}
else if (state == STREAM_STATE_RESUMING &&
sourceData->deviceClock.valid)
{
/* The callback clock continued throughout KEEP_ALIVE. Rebase its phase
* to the newest published tick while preserving the disciplined rate;
* feeding the entire idle gap through the short-step filter produces
* unstable gains. An incomplete acquisition starts over from this tick. */
const bool monotonic =
deviceTick.nextTime >= sourceData->deviceClock.time &&
deviceTick.nextPosition >= sourceData->deviceClock.position &&
(audio.playback.rateControl != PLAYBACK_RATE_BACKEND ||
!sourceData->outputClock.valid ||
(deviceTick.nextTime >= sourceData->outputClock.time &&
deviceTick.outputPosition >= sourceData->outputClock.position));
if (!monotonic)
{
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);
discontinuity = true;
}
else
{
playbackClockRebase(&sourceData->deviceClock,
deviceTick.nextTime, deviceTick.nextPosition);
if (audio.playback.rateControl == PLAYBACK_RATE_BACKEND)
{
if (sourceData->outputClock.valid)
playbackClockRebase(&sourceData->outputClock,
deviceTick.nextTime, deviceTick.outputPosition);
else
playbackClockReset(&sourceData->outputClock,
deviceTick.nextTime, deviceTick.outputPosition,
nominalFrameSec);
}
if (!sourceData->deviceClockStable)
playbackDeviceClockAcquireReset(sourceData);
}
}
else
{
const bool deviceClockUpdated =
@@ -2459,7 +2582,7 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
double devPosition = DBL_MIN;
state = playbackGetState();
if (playbackUseLowWaterRecovery(providerRateControl,
bufferUnderrun, sourceData) &&
bufferUnderrun, state, sourceData) &&
(discontinuity ||
state == STREAM_STATE_KEEP_ALIVE ||
state == STREAM_STATE_RESUMING))
@@ -2844,17 +2967,20 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
const unsigned int underruns = atomic_exchange_explicit(
&audio.playback.underruns, 0, memory_order_relaxed);
PlaybackDiagnostics * diagnostics = playbackDiagnosticsLocked();
const unsigned int pendingUnderruns =
diagnostics->pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG ?
const unsigned int syncEpoch = atomic_load_explicit(
&audio.playback.syncDiagnosticsEpoch, memory_order_acquire);
const bool pendingSync =
(diagnostics->pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG) &&
diagnostics->sync.epoch == syncEpoch;
const unsigned int pendingUnderruns = pendingSync ?
diagnostics->sync.underruns : 0;
const unsigned int pendingOverruns =
diagnostics->pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG ?
const unsigned int pendingOverruns = pendingSync ?
diagnostics->sync.overruns : 0;
const double pendingBacklogTrimmedMs =
diagnostics->pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG ?
const double pendingBacklogTrimmedMs = pendingSync ?
diagnostics->sync.backlogTrimmedMs : 0.0;
diagnostics->sync = (PlaybackSyncDiagnostics)
{
.epoch = syncEpoch,
.softwareLatencyMs = softwareLatencyMs,
.targetLatencyMs =
targetLatencyFrames * 1000.0 / audio.playback.sampleRate,
@@ -3664,6 +3790,7 @@ static void eventPlaybackStart(void * opaque, uint32_t generation,
const bool providerRateControl =
active.ops->clockFeedback &&
audio.feedback.wakeInitialized && audio.feedback.thread;
bool wake = false;
LG_LOCK(audio.playback.sourceLock);
++audio.playback.requestSerial;
@@ -3681,7 +3808,32 @@ static void eventPlaybackStart(void * opaque, uint32_t generation,
if (audio.playback.requestedFormatValid)
{
audio.playback.requestedFormat = *format;
if (audio.playback.startInProgress)
bool resumed = false;
if (!audio.playback.startInProgress &&
playbackGetState() == STREAM_STATE_KEEP_ALIVE &&
audio.playback.backendStartTime != 0)
{
LG_LOCK(audio.playback.deviceLock);
resumed = playbackResume(format, NULL, providerRateControl,
false);
if (resumed)
{
if (atomic_load_explicit(&audio.playback.activeAttemptSerial,
memory_order_acquire))
audio.playback.backendRequestSerial =
audio.playback.requestSerial;
atomic_store_explicit(&audio.playback.streamGeneration,
generation, memory_order_release);
audio.playback.controlsPending = true;
audio.playback.nextStartRetry = 0;
wake = true;
}
LG_UNLOCK(audio.playback.deviceLock);
}
if (resumed)
audio.playback.startPending = false;
else if (audio.playback.startInProgress)
{
audio.playback.startPending = true;
playbackWorkerWake();
@@ -3702,6 +3854,8 @@ static void eventPlaybackStart(void * opaque, uint32_t generation,
audio.playback.requestedGeneration = 0;
}
LG_UNLOCK(audio.playback.sourceLock);
if (wake)
playbackWorkerWake();
eventEnd();
}
@@ -4285,7 +4439,9 @@ void lgAudio_init(void)
atomic_init(&audio.playback.activeAttemptSerial, 0);
atomic_init(&audio.playback.failedAttemptSerial, 0);
atomic_init(&audio.playback.diagnosticsEpoch, 1);
atomic_init(&audio.playback.syncDiagnosticsEpoch, 1);
atomic_init(&audio.playback.graphReady, false);
atomic_flag_clear(&audio.playback.deviceStateGate);
atomic_init(&audio.playback.backlogTrimTarget, -1);
atomic_init(&audio.playback.backlogTrimmedFrames, 0);
atomic_init(&audio.playback.deviceTiming.discontinuity, 0);

View File

@@ -821,6 +821,7 @@ if(ENABLE_AUDIO)
playback-retry
jitter
recovery
resume
consent
quiesce
)

View File

@@ -702,6 +702,124 @@ static void testPlaybackRecovery(void)
stopAudio();
}
static void testPlaybackResume(void)
{
reset();
g_params.audioDebug = true;
startAudio();
struct Provider p;
initProvider(&p, "provider", true);
lgAudio_setFallback(&ops, &p);
CHECK(p.events);
const LG_AudioFormat format = makeFormat(LG_AUDIO_FMT_S16_LE);
p.events->playbackStart(p.eventOpaque, 11, &format, NULL);
for (unsigned int i = 0; i < WAIT_MS; ++i)
{
if (atomic_load_explicit(&audio.playback.streamGeneration,
memory_order_acquire) == 11)
break;
usleep(1000);
}
CHECK(atomic_load(&audio.playback.streamGeneration) == 11);
playbackSetState(STREAM_STATE_RUN);
audio.playback.backendStartTime = nanotime();
uint8_t output[16 * 2 * 2];
CHECK(b.pull(output, 16) == 16);
CHECK(b.pull(output, 16) == 16);
CHECK(atomic_load(&audio.playback.underruns) == 1);
const int refill = -ringbuffer_getCount(audio.playback.buffer) + 16;
CHECK(ringbuffer_append(audio.playback.buffer, NULL, refill) == refill);
CHECK(b.pull(output, 16) == 16);
CHECK(!audio.playback.deviceData.underrunning);
CHECK(b.pull(output, 16) == 16);
CHECK(b.pull(output, 16) == 16);
CHECK(atomic_load(&audio.playback.underruns) == 2);
const unsigned int oldSyncEpoch = atomic_load_explicit(
&audio.playback.syncDiagnosticsEpoch, memory_order_acquire);
LG_LOCK(audio.playback.sourceLock);
PlaybackDiagnostics * diagnostics = playbackDiagnosticsLocked();
diagnostics->sync = (PlaybackSyncDiagnostics)
{
.epoch = oldSyncEpoch,
.underruns = 99,
};
diagnostics->pending |= PLAYBACK_DIAGNOSTIC_SYNC_LOG;
LG_UNLOCK(audio.playback.sourceLock);
atomic_store(&audio.playback.backlogTrimTarget, 0);
atomic_store(&audio.playback.backlogTrimmedFrames, 123);
audio.playback.sourceData.bufferOverruns = 7;
audio.playback.sourceData.backlogTrimArmed = false;
p.events->playbackStop(p.eventOpaque, 11);
CHECK(playbackGetState() == STREAM_STATE_KEEP_ALIVE);
CHECK(atomic_load(&audio.playback.underruns) == 0);
CHECK(!audio.playback.deviceData.underrunning);
CHECK(atomic_load(&audio.playback.backlogTrimTarget) == -1);
CHECK(atomic_load(&audio.playback.backlogTrimmedFrames) == 0);
CHECK(audio.playback.sourceData.bufferOverruns == 0);
CHECK(audio.playback.sourceData.backlogTrimArmed);
CHECK(atomic_load(&audio.playback.syncDiagnosticsEpoch) != oldSyncEpoch);
CHECK(!(audio.playback.diagnostics.pending &
PLAYBACK_DIAGNOSTIC_SYNC_LOG));
CHECK(b.pull(output, 16) == 16);
CHECK(b.pull(output, 16) == 16);
CHECK(atomic_load(&audio.playback.underruns) == 0);
const unsigned int setupN = atomic_load(&b.setupN);
p.events->playbackStart(p.eventOpaque, 12, &format, NULL);
CHECK(atomic_load(&audio.playback.streamGeneration) == 12);
CHECK(playbackGetState() == STREAM_STATE_RESUMING);
CHECK(atomic_load(&b.setupN) == setupN);
uint8_t input[480 * 2 * 2] = { 0 };
p.events->playbackData(p.eventOpaque, 12, input, 480, NULL);
CHECK(atomic_load(&audio.playback.streamGeneration) == 12);
CHECK(playbackGetState() == STREAM_STATE_RUN);
CHECK(atomic_load(&audio.playback.underruns) == 0);
p.events->playbackStop(p.eventOpaque, 12);
CHECK(playbackGetState() == STREAM_STATE_KEEP_ALIVE);
PlaybackSourceData * source = &audio.playback.sourceData;
const int64_t arrival = nanotime();
const int64_t idleFrames = INT64_C(30) * format.sampleRate;
const double disciplinedFrameSec =
1.0002 / format.sampleRate;
playbackClockReset(&source->deviceClock,
arrival - INT64_C(30000000000), 0.0, disciplinedFrameSec);
playbackClockReset(&source->outputClock,
arrival - INT64_C(30000000000), 0.0, disciplinedFrameSec);
source->deviceClockStable = true;
source->devicePositionOffsetFrames = 37.5;
source->deviceTimingSequence = atomic_load_explicit(
&audio.playback.deviceTiming.sequence, memory_order_acquire);
const int idleDebt = clamp(
(int64_t)ringbuffer_getCount(audio.playback.buffer) + idleFrames,
INT64_C(0), (int64_t)INT_MAX);
CHECK(ringbuffer_consume(
audio.playback.buffer, NULL, idleDebt) == idleDebt);
playbackPublishDeviceTiming(16, arrival, idleFrames,
(double)idleFrames, source->deviceDiscontinuity);
p.events->playbackStart(p.eventOpaque, 13, &format, NULL);
CHECK(atomic_load(&audio.playback.streamGeneration) == 13);
p.events->playbackData(p.eventOpaque, 13, input, 480, NULL);
CHECK(playbackGetState() == STREAM_STATE_RUN);
CHECK(source->deviceClock.time == arrival);
CHECK(source->deviceClock.position == idleFrames);
CHECK(fabs(source->deviceClock.frameSec - disciplinedFrameSec) < 1.0e-15);
CHECK(source->devicePositionOffsetFrames == 37.5);
p.events->playbackStop(p.eventOpaque, 13);
stopAudio();
}
static void testConsent(void)
{
reset();
@@ -830,6 +948,7 @@ static const struct Test tests[] =
{ "playback-retry", testPlaybackRetry },
{ "jitter" , testPlaybackJitter },
{ "recovery" , testPlaybackRecovery },
{ "resume" , testPlaybackResume },
{ "consent" , testConsent },
{ "quiesce" , testQuiesce },
};