diff --git a/client/src/audio.c b/client/src/audio.c index 1c71e4b2..3eed0e83 100644 --- a/client/src/audio.c +++ b/client/src/audio.c @@ -2609,12 +2609,37 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount, sourceData->deviceClockStable) { devPosition = computeDevicePosition(curTime); - const double slew = devPosition + targetBufferFrames - curPosition; + const bool activeUnderrun = + bufferUnderrun && state == STREAM_STATE_RUN; + const double clockSlew = + devPosition + targetBufferFrames - curPosition; + double slew = clockSlew; + if (activeUnderrun) + { + const int occupancy = ringbuffer_getCount(audio.playback.buffer); + const double physicalSlew = + ceil(targetLowWaterFrames - occupancy); + if (physicalSlew > slew) + slew = physicalSlew; + } + const int slewFrames = clamp(llrint(slew), (int64_t)INT_MIN, (int64_t)INT_MAX); const int actualSlew = playbackSlewBuffer(sourceData, slewFrames); + + /* A negative unbounded ring must be restored in the physical domain even + * when the fitted clock asks for a smaller correction. Move the clock's + * phase translation by the excess so the forced refill neither appears as + * additional latency nor restarts the underrun/slew cycle. */ + const double correction = actualSlew - clockSlew; + if (activeUnderrun && correction > 0.0) + { + sourceData->devicePositionOffsetFrames += correction; + devPosition += correction; + } + sourceData->outputPosition += actualSlew; - curPosition += actualSlew; + curPosition += actualSlew; playbackResetRateControl(sourceData, providerRateControl); if (state == STREAM_STATE_KEEP_ALIVE || diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 29b46f89..9f2a6a21 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -821,6 +821,7 @@ if(ENABLE_AUDIO) playback-retry jitter recovery + stable-underrun resume consent quiesce diff --git a/client/tests/audio_test.c b/client/tests/audio_test.c index febb4ebf..7489fd6f 100644 --- a/client/tests/audio_test.c +++ b/client/tests/audio_test.c @@ -702,6 +702,78 @@ static void testPlaybackRecovery(void) stopAudio(); } +static void testPlaybackStableUnderrun(void) +{ + reset(); + startAudio(); + + const LG_AudioFormat format = makeFormat(LG_AUDIO_FMT_S16_LE); + CHECK(playbackStart(&format, NULL, false, false)); + CHECK(audio.playback.rateControl == PLAYBACK_RATE_BACKEND); + playbackSetState(STREAM_STATE_RUN); + + PlaybackSourceData * source = &audio.playback.sourceData; + const int64_t arrival = nanotime(); + playbackClockReset(&source->deviceClock, + arrival, 0.0, 1.0 / format.sampleRate); + source->deviceClockStable = true; + source->devicePositionOffsetFrames = 0.0; + source->outputPosition = 1000; + source->lastRatio = 0.999; + source->lastClockRatio = 1.0002; + source->sourcePacketDurationSec = 0.010; + playbackPublishDeviceTiming(16, arrival, 0, 0.0, 0); + + CHECK(source->outputPosition - computeDevicePosition(arrival) > 900.0); + CHECK(ringbuffer_append(audio.playback.buffer, NULL, 1000) == 1000); + CHECK(ringbuffer_consume(audio.playback.buffer, NULL, 1128) == 1128); + CHECK(ringbuffer_getCount(audio.playback.buffer) == -128); + uint8_t frames[64 * 2 * 2]; + uint8_t output[64 * 2 * 2]; + memset(frames, 0x5a, sizeof(frames)); + CHECK(playbackData(frames, 64, NULL, arrival) == + PLAYBACK_DATA_PROCESSED); + + const int recoveredOccupancy = + ringbuffer_getCount(audio.playback.buffer) - 64; + const double recoveredPosition = source->outputPosition - 64; + const double recoveredLatency = recoveredPosition - + computeDevicePosition(source->sourceClock.time); + const double targetLowWater = + audio.playback.deviceMaxPeriodFrames * 1.1 + + 0.001 * format.sampleRate; + const double sourceReserve = + max(source->sourcePacketDurationSec * 0.5, + source->sourcePhaseReserveSec) * format.sampleRate; + const double targetLatency = targetLowWater + sourceReserve; + CHECK(recoveredOccupancy >= (int)ceil(targetLowWater)); + CHECK(fabs(recoveredLatency - targetLatency) < 0.000001); + CHECK(!source->bufferOverrunPending); + CHECK(source->deviceClockStable); + CHECK(source->devicePositionOffsetFrames > 0.0); + CHECK(fabs(source->deviceClock.frameSec - + 1.0 / format.sampleRate) < 0.000000001); + CHECK(fabs(atomic_load(&audio.playback.backendResampleRatio) - + source->lastClockRatio) < 0.000001); + + CHECK(ringbuffer_consume(audio.playback.buffer, output, 64) == 64); + CHECK(ringbuffer_getCount(audio.playback.buffer) == recoveredOccupancy); + + const double recoveredOffset = + source->devicePositionOffsetFrames; + const int64_t secondPacketArrival = arrival + + llrint(64.0e9 / format.sampleRate); + CHECK(playbackData(frames, 64, NULL, + secondPacketArrival) == PLAYBACK_DATA_PROCESSED); + CHECK(source->devicePositionOffsetFrames == recoveredOffset); + CHECK(ringbuffer_getCount(audio.playback.buffer) == + recoveredOccupancy + 64); + CHECK(ringbuffer_consume(audio.playback.buffer, output, 64) == 64); + CHECK(ringbuffer_getCount(audio.playback.buffer) == recoveredOccupancy); + + stopAudio(); +} + static void testPlaybackResume(void) { reset(); @@ -942,15 +1014,16 @@ struct Test static const struct Test tests[] = { - { "format" , testFormat }, - { "convert" , testConvert }, - { "provider" , testProvider }, - { "playback-retry", testPlaybackRetry }, - { "jitter" , testPlaybackJitter }, - { "recovery" , testPlaybackRecovery }, - { "resume" , testPlaybackResume }, - { "consent" , testConsent }, - { "quiesce" , testQuiesce }, + { "format" , testFormat }, + { "convert" , testConvert }, + { "provider" , testProvider }, + { "playback-retry" , testPlaybackRetry }, + { "jitter" , testPlaybackJitter }, + { "recovery" , testPlaybackRecovery }, + { "stable-underrun", testPlaybackStableUnderrun }, + { "resume" , testPlaybackResume }, + { "consent" , testConsent }, + { "quiesce" , testQuiesce }, }; int main(int argc, char ** argv)