diff --git a/client/src/audio.c b/client/src/audio.c index fe6d4735..1c71e4b2 100644 --- a/client/src/audio.c +++ b/client/src/audio.c @@ -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,25 +1302,34 @@ static int playbackPullFrames(uint8_t * dst, int frames) 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( - &audio.playback.backlogTrimTarget, -1, memory_order_acq_rel); - if (trimTarget >= 0) + if (playbackGetState() == STREAM_STATE_RUN) { - 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) + const int trimTarget = atomic_exchange_explicit( + &audio.playback.backlogTrimTarget, -1, memory_order_acq_rel); + if (trimTarget >= 0) { - data->nextPosition += dropped; - ++data->discontinuity; - atomic_fetch_add_explicit( - &audio.playback.backlogTrimmedFrames, dropped, - memory_order_relaxed); + 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); + } } } + atomic_flag_clear_explicit( + &audio.playback.deviceStateGate, memory_order_release); } /* 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; 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) - atomic_fetch_add_explicit( - &audio.playback.underruns, 1, memory_order_relaxed); + 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); - return true; - } + if (playbackResume(format, sourceClock, providerRateControl, + forceSoftwareResampler)) + return true; - state = expected; - } + const StreamState state = playbackGetState(); if (state != STREAM_STATE_STOP) playbackStop(); @@ -1415,7 +1482,8 @@ 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.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)) @@ -2835,39 +2958,42 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount, if (now >= sourceData->nextLogTime) { - const bool providerControl = + const bool providerControl = audio.playback.rateControl == PLAYBACK_RATE_PROVIDER; - const double controlPpm = providerControl ? + const double controlPpm = providerControl ? (playbackProviderRate(sourceData) / audio.playback.sampleRate - 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); - PlaybackDiagnostics * diagnostics = playbackDiagnosticsLocked(); - const unsigned int pendingUnderruns = - diagnostics->pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG ? + PlaybackDiagnostics * diagnostics = playbackDiagnosticsLocked(); + 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, - .controlPpm = controlPpm, - .jitterMs = sourceData->arrivalJitterSec * 1000.0, - .underruns = pendingUnderruns + underruns, - .overruns = pendingOverruns + sourceData->bufferOverruns, - .backlogTrimmedMs = pendingBacklogTrimmedMs + + .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, + .rateControl = audio.playback.rateControl, }; diagnostics->pending |= PLAYBACK_DIAGNOSTIC_SYNC_LOG; @@ -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); diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index f77a3613..29b46f89 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -821,6 +821,7 @@ if(ENABLE_AUDIO) playback-retry jitter recovery + resume consent quiesce ) diff --git a/client/tests/audio_test.c b/client/tests/audio_test.c index 1622315e..febb4ebf 100644 --- a/client/tests/audio_test.c +++ b/client/tests/audio_test.c @@ -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 }, };