[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; double appliedRatio;
int startupSilenceFrames; int startupSilenceFrames;
unsigned int discontinuity; unsigned int discontinuity;
bool underrunning;
} }
PlaybackDeviceData; PlaybackDeviceData;
@@ -238,6 +239,7 @@ PlaybackStartDiagnostics;
typedef struct typedef struct
{ {
unsigned int epoch;
double softwareLatencyMs; double softwareLatencyMs;
double targetLatencyMs; double targetLatencyMs;
double controlPpm; double controlPpm;
@@ -342,12 +344,14 @@ typedef struct
RingBuffer buffer; RingBuffer buffer;
PlaybackDeviceTiming deviceTiming; PlaybackDeviceTiming deviceTiming;
atomic_int backlogTrimTarget; atomic_int backlogTrimTarget;
atomic_flag deviceStateGate;
atomic_uint underruns; atomic_uint underruns;
atomic_uint_fast64_t backlogTrimmedFrames; atomic_uint_fast64_t backlogTrimmedFrames;
RingBuffer timings; RingBuffer timings;
GraphHandle graph; GraphHandle graph;
atomic_uint diagnosticsEpoch; atomic_uint diagnosticsEpoch;
atomic_uint syncDiagnosticsEpoch;
atomic_bool graphReady; atomic_bool graphReady;
bool graphRegistrationAttempted; bool graphRegistrationAttempted;
PlaybackDiagnostics diagnostics; PlaybackDiagnostics diagnostics;
@@ -622,6 +626,16 @@ static bool playbackClockUpdate(PlaybackClock * clock, int64_t time,
return true; 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, static bool playbackSourceClockUpdate(PlaybackClock * clock, int64_t time,
double position, double nominalFrameSec) double position, double nominalFrameSec)
{ {
@@ -1140,6 +1154,31 @@ static PlaybackDiagnostics * playbackDiagnosticsLocked(void)
return diagnostics; 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) static void playbackStop(void)
{ {
const bool alreadyStopped = playbackGetState() == STREAM_STATE_STOP; const bool alreadyStopped = playbackGetState() == STREAM_STATE_STOP;
@@ -1263,25 +1302,34 @@ 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) 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))
{ {
const int trimTarget = atomic_exchange_explicit( if (playbackGetState() == STREAM_STATE_RUN)
&audio.playback.backlogTrimTarget, -1, memory_order_acq_rel);
if (trimTarget >= 0)
{ {
const int queued = ringbuffer_getCount(audio.playback.buffer); const int trimTarget = atomic_exchange_explicit(
const int requested = max(queued - trimTarget, 0); &audio.playback.backlogTrimTarget, -1, memory_order_acq_rel);
const int dropped = ringbuffer_consume( if (trimTarget >= 0)
audio.playback.buffer, NULL, requested);
if (dropped > 0)
{ {
data->nextPosition += dropped; const int queued = ringbuffer_getCount(audio.playback.buffer);
++data->discontinuity; const int requested = max(queued - trimTarget, 0);
atomic_fetch_add_explicit( const int dropped = ringbuffer_consume(
&audio.playback.backlogTrimmedFrames, dropped, audio.playback.buffer, NULL, requested);
memory_order_relaxed); if (dropped > 0)
{
data->nextPosition += dropped;
++data->discontinuity;
atomic_fetch_add_explicit(
&audio.playback.backlogTrimmedFrames, dropped,
memory_order_relaxed);
}
} }
} }
atomic_flag_clear_explicit(
&audio.playback.deviceStateGate, memory_order_release);
} }
/* Timestamp the dequeue boundary before the current pull. The logical /* Timestamp the dequeue boundary before the current pull. The logical
@@ -1307,10 +1355,20 @@ static int playbackPullFrames(uint8_t * dst, int frames)
const int audioFrames = frames - silenceFrames; const int audioFrames = frames - silenceFrames;
if (g_params.audioDebug && 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 && playbackGetState() == STREAM_STATE_RUN &&
ringbuffer_getCount(audio.playback.buffer) < audioFrames) ringbuffer_getCount(audio.playback.buffer) < audioFrames;
atomic_fetch_add_explicit( const bool wasUnderrunning = data->underrunning;
&audio.playback.underruns, 1, memory_order_relaxed); 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, ringbuffer_consume(audio.playback.buffer,
dst + (size_t)silenceFrames * audio.playback.stride, audioFrames); dst + (size_t)silenceFrames * audio.playback.stride, audioFrames);
} }
@@ -1360,6 +1418,28 @@ static bool playbackSetupDevice(const LG_AudioFormat * format,
audio.playback.deviceStartFrames >= 0; 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, static bool playbackStart(const LG_AudioFormat * format,
const LG_AudioClock * sourceClock, bool providerRateControl, const LG_AudioClock * sourceClock, bool providerRateControl,
bool forceSoftwareResampler) bool forceSoftwareResampler)
@@ -1378,24 +1458,11 @@ static bool playbackStart(const LG_AudioFormat * format,
const int channels = format->channelCount; const int channels = format->channelCount;
const int sampleRate = format->sampleRate; const int sampleRate = format->sampleRate;
StreamState state = playbackGetState(); if (playbackResume(format, sourceClock, providerRateControl,
if (!forceSoftwareResampler && state == STREAM_STATE_KEEP_ALIVE && forceSoftwareResampler))
audio.playback.lastFormatValid && return true;
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);
return true;
}
state = expected; const StreamState state = playbackGetState();
}
if (state != STREAM_STATE_STOP) if (state != STREAM_STATE_STOP)
playbackStop(); playbackStop();
@@ -1415,7 +1482,8 @@ 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.deviceData.discontinuity = 0;
audio.playback.deviceData.underrunning = false;
audio.playback.sourceData.inputPosition = 0; audio.playback.sourceData.inputPosition = 0;
audio.playback.sourceData.outputPosition = 0; audio.playback.sourceData.outputPosition = 0;
@@ -1894,6 +1962,7 @@ static void playbackProcessDiagnostics(void)
LG_UNLOCK(audio.playback.sourceLock); LG_UNLOCK(audio.playback.sourceLock);
bool current = false; bool current = false;
bool syncCurrent = false;
double backendLatencyMs = 0.0; double backendLatencyMs = 0.0;
LG_LOCK(audio.playback.deviceLock); LG_LOCK(audio.playback.deviceLock);
const StreamState state = playbackGetState(); const StreamState state = playbackGetState();
@@ -1902,6 +1971,8 @@ static void playbackProcessDiagnostics(void)
state != STREAM_STATE_STOP && state != STREAM_STATE_STOP_PENDING; state != STREAM_STATE_STOP && state != STREAM_STATE_STOP_PENDING;
if (current) if (current)
{ {
syncCurrent = diagnostics.sync.epoch == atomic_load_explicit(
&audio.playback.syncDiagnosticsEpoch, memory_order_acquire);
const bool timingsCurrent = timings && const bool timingsCurrent = timings &&
timings == audio.playback.timings; timings == audio.playback.timings;
if ((diagnostics.pending & PLAYBACK_DIAGNOSTIC_REGISTER_GRAPH) && if ((diagnostics.pending & PLAYBACK_DIAGNOSTIC_REGISTER_GRAPH) &&
@@ -1920,6 +1991,7 @@ static void playbackProcessDiagnostics(void)
app_invalidateGraph(audio.playback.graph); app_invalidateGraph(audio.playback.graph);
if ((diagnostics.pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG) && if ((diagnostics.pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG) &&
syncCurrent &&
audio.audioDev && audio.audioDev->playback.latency) audio.audioDev && audio.audioDev->playback.latency)
backendLatencyMs = backendLatencyMs =
audio.audioDev->playback.latency() / 1000.0; audio.audioDev->playback.latency() / 1000.0;
@@ -1935,7 +2007,10 @@ static void playbackProcessDiagnostics(void)
diagnostics.start.targetLatencyMs, diagnostics.start.targetLatencyMs,
diagnostics.start.queuedLatencyMs); 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 = const char * controlName =
diagnostics.sync.rateControl == PLAYBACK_RATE_PROVIDER ? "feedback" : diagnostics.sync.rateControl == PLAYBACK_RATE_PROVIDER ? "feedback" :
@@ -1972,6 +2047,9 @@ static void playbackSourceStop(void)
memory_order_acq_rel, memory_order_acquire)) memory_order_acq_rel, memory_order_acquire))
break; break;
audio.playback.sourceData.backlogTrimArmed = true;
playbackResetSyncDiagnosticsLocked();
// Reset the software resampler so it is safe for the next playback // Reset the software resampler so it is safe for the next playback
if (audio.playback.sourceData.src) if (audio.playback.sourceData.src)
{ {
@@ -2135,11 +2213,13 @@ static int playbackSlewBuffer(
} }
static bool playbackUseLowWaterRecovery(bool providerRateControl, static bool playbackUseLowWaterRecovery(bool providerRateControl,
bool bufferUnderrun, const PlaybackSourceData * sourceData) bool bufferUnderrun, StreamState state,
const PlaybackSourceData * sourceData)
{ {
return providerRateControl || return providerRateControl ||
(bufferUnderrun && (!sourceData->deviceClock.valid || ((!sourceData->deviceClock.valid || !sourceData->deviceClockStable) &&
!sourceData->deviceClockStable)); (bufferUnderrun || state == STREAM_STATE_KEEP_ALIVE ||
state == STREAM_STATE_RESUMING));
} }
static void playbackResetRateControl(PlaybackSourceData * sourceData, static void playbackResetRateControl(PlaybackSourceData * sourceData,
@@ -2376,6 +2456,49 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
sourceData->lastRatio = 1.0; sourceData->lastRatio = 1.0;
sourceData->nextFeedbackTime = 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 else
{ {
const bool deviceClockUpdated = const bool deviceClockUpdated =
@@ -2459,7 +2582,7 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
double devPosition = DBL_MIN; double devPosition = DBL_MIN;
state = playbackGetState(); state = playbackGetState();
if (playbackUseLowWaterRecovery(providerRateControl, if (playbackUseLowWaterRecovery(providerRateControl,
bufferUnderrun, sourceData) && bufferUnderrun, state, sourceData) &&
(discontinuity || (discontinuity ||
state == STREAM_STATE_KEEP_ALIVE || state == STREAM_STATE_KEEP_ALIVE ||
state == STREAM_STATE_RESUMING)) state == STREAM_STATE_RESUMING))
@@ -2835,39 +2958,42 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
if (now >= sourceData->nextLogTime) if (now >= sourceData->nextLogTime)
{ {
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 ?
(playbackProviderRate(sourceData) / (playbackProviderRate(sourceData) /
audio.playback.sampleRate - 1.0) * 1.0e6 : audio.playback.sampleRate - 1.0) * 1.0e6 :
(ratio - 1.0) * 1.0e6; (ratio - 1.0) * 1.0e6;
const unsigned int underruns = atomic_exchange_explicit( const unsigned int underruns = atomic_exchange_explicit(
&audio.playback.underruns, 0, memory_order_relaxed); &audio.playback.underruns, 0, memory_order_relaxed);
PlaybackDiagnostics * diagnostics = playbackDiagnosticsLocked(); PlaybackDiagnostics * diagnostics = playbackDiagnosticsLocked();
const unsigned int pendingUnderruns = const unsigned int syncEpoch = atomic_load_explicit(
diagnostics->pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG ? &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; diagnostics->sync.underruns : 0;
const unsigned int pendingOverruns = const unsigned int pendingOverruns = pendingSync ?
diagnostics->pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG ?
diagnostics->sync.overruns : 0; diagnostics->sync.overruns : 0;
const double pendingBacklogTrimmedMs = const double pendingBacklogTrimmedMs = pendingSync ?
diagnostics->pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG ?
diagnostics->sync.backlogTrimmedMs : 0.0; diagnostics->sync.backlogTrimmedMs : 0.0;
diagnostics->sync = (PlaybackSyncDiagnostics) diagnostics->sync = (PlaybackSyncDiagnostics)
{ {
.epoch = syncEpoch,
.softwareLatencyMs = softwareLatencyMs, .softwareLatencyMs = softwareLatencyMs,
.targetLatencyMs = .targetLatencyMs =
targetLatencyFrames * 1000.0 / audio.playback.sampleRate, targetLatencyFrames * 1000.0 / audio.playback.sampleRate,
.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 + .backlogTrimmedMs = pendingBacklogTrimmedMs +
atomic_exchange_explicit( atomic_exchange_explicit(
&audio.playback.backlogTrimmedFrames, 0, &audio.playback.backlogTrimmedFrames, 0,
memory_order_relaxed) * 1000.0 / memory_order_relaxed) * 1000.0 /
audio.playback.sampleRate, audio.playback.sampleRate,
.rateControl = audio.playback.rateControl, .rateControl = audio.playback.rateControl,
}; };
diagnostics->pending |= PLAYBACK_DIAGNOSTIC_SYNC_LOG; diagnostics->pending |= PLAYBACK_DIAGNOSTIC_SYNC_LOG;
@@ -3664,6 +3790,7 @@ static void eventPlaybackStart(void * opaque, uint32_t generation,
const bool providerRateControl = const bool providerRateControl =
active.ops->clockFeedback && active.ops->clockFeedback &&
audio.feedback.wakeInitialized && audio.feedback.thread; audio.feedback.wakeInitialized && audio.feedback.thread;
bool wake = false;
LG_LOCK(audio.playback.sourceLock); LG_LOCK(audio.playback.sourceLock);
++audio.playback.requestSerial; ++audio.playback.requestSerial;
@@ -3681,7 +3808,32 @@ static void eventPlaybackStart(void * opaque, uint32_t generation,
if (audio.playback.requestedFormatValid) if (audio.playback.requestedFormatValid)
{ {
audio.playback.requestedFormat = *format; 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; audio.playback.startPending = true;
playbackWorkerWake(); playbackWorkerWake();
@@ -3702,6 +3854,8 @@ static void eventPlaybackStart(void * opaque, uint32_t generation,
audio.playback.requestedGeneration = 0; audio.playback.requestedGeneration = 0;
} }
LG_UNLOCK(audio.playback.sourceLock); LG_UNLOCK(audio.playback.sourceLock);
if (wake)
playbackWorkerWake();
eventEnd(); eventEnd();
} }
@@ -4285,7 +4439,9 @@ void lgAudio_init(void)
atomic_init(&audio.playback.activeAttemptSerial, 0); atomic_init(&audio.playback.activeAttemptSerial, 0);
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.syncDiagnosticsEpoch, 1);
atomic_init(&audio.playback.graphReady, false); atomic_init(&audio.playback.graphReady, false);
atomic_flag_clear(&audio.playback.deviceStateGate);
atomic_init(&audio.playback.backlogTrimTarget, -1); atomic_init(&audio.playback.backlogTrimTarget, -1);
atomic_init(&audio.playback.backlogTrimmedFrames, 0); atomic_init(&audio.playback.backlogTrimmedFrames, 0);
atomic_init(&audio.playback.deviceTiming.discontinuity, 0); atomic_init(&audio.playback.deviceTiming.discontinuity, 0);

View File

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

View File

@@ -702,6 +702,124 @@ static void testPlaybackRecovery(void)
stopAudio(); 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) static void testConsent(void)
{ {
reset(); reset();
@@ -830,6 +948,7 @@ static const struct Test tests[] =
{ "playback-retry", testPlaybackRetry }, { "playback-retry", testPlaybackRetry },
{ "jitter" , testPlaybackJitter }, { "jitter" , testPlaybackJitter },
{ "recovery" , testPlaybackRecovery }, { "recovery" , testPlaybackRecovery },
{ "resume" , testPlaybackResume },
{ "consent" , testConsent }, { "consent" , testConsent },
{ "quiesce" , testQuiesce }, { "quiesce" , testQuiesce },
}; };