mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-23 15:41:31 +00:00
[client] audio: fix underrun clock recovery
Recover stable local playback in the device clock domain and restart rate control from the measured feed-forward clock ratio. Ignore catch-up packet sizing until the normal source cadence returns.
This commit is contained in:
@@ -173,7 +173,9 @@ typedef struct
|
|||||||
double sourcePhaseBaselineSec;
|
double sourcePhaseBaselineSec;
|
||||||
double sourcePhaseReserveSec;
|
double sourcePhaseReserveSec;
|
||||||
double sourcePacketDurationSec;
|
double sourcePacketDurationSec;
|
||||||
|
unsigned int sourcePacketPacedCount;
|
||||||
bool sourcePhaseBaselineValid;
|
bool sourcePhaseBaselineValid;
|
||||||
|
bool sourcePacketRecovering;
|
||||||
bool mediaClockValid;
|
bool mediaClockValid;
|
||||||
bool mediaPositionValid;
|
bool mediaPositionValid;
|
||||||
bool mediaClockFromSource;
|
bool mediaClockFromSource;
|
||||||
@@ -1438,7 +1440,9 @@ static bool playbackStart(const LG_AudioFormat * format,
|
|||||||
audio.playback.sourceData.sourcePhaseBaselineSec = 0.0;
|
audio.playback.sourceData.sourcePhaseBaselineSec = 0.0;
|
||||||
audio.playback.sourceData.sourcePhaseReserveSec = 0.0;
|
audio.playback.sourceData.sourcePhaseReserveSec = 0.0;
|
||||||
audio.playback.sourceData.sourcePacketDurationSec = 0.0;
|
audio.playback.sourceData.sourcePacketDurationSec = 0.0;
|
||||||
|
audio.playback.sourceData.sourcePacketPacedCount = 0;
|
||||||
audio.playback.sourceData.sourcePhaseBaselineValid = false;
|
audio.playback.sourceData.sourcePhaseBaselineValid = false;
|
||||||
|
audio.playback.sourceData.sourcePacketRecovering = false;
|
||||||
audio.playback.sourceData.deviceClock.valid = false;
|
audio.playback.sourceData.deviceClock.valid = false;
|
||||||
audio.playback.sourceData.outputClock.valid = false;
|
audio.playback.sourceData.outputClock.valid = false;
|
||||||
playbackPrepareMediaClock(&audio.playback.sourceData, sourceClock);
|
playbackPrepareMediaClock(&audio.playback.sourceData, sourceClock);
|
||||||
@@ -2130,6 +2134,26 @@ static int playbackSlewBuffer(
|
|||||||
return advanced;
|
return advanced;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool playbackUseLowWaterRecovery(bool providerRateControl,
|
||||||
|
bool bufferUnderrun, const PlaybackSourceData * sourceData)
|
||||||
|
{
|
||||||
|
return providerRateControl ||
|
||||||
|
(bufferUnderrun && (!sourceData->deviceClock.valid ||
|
||||||
|
!sourceData->deviceClockStable));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void playbackResetRateControl(PlaybackSourceData * sourceData,
|
||||||
|
bool providerRateControl)
|
||||||
|
{
|
||||||
|
sourceData->offsetError = 0.0;
|
||||||
|
sourceData->offsetErrorIntegral = 0.0;
|
||||||
|
sourceData->ratioIntegral = 0.0;
|
||||||
|
sourceData->lastRatio = providerRateControl ?
|
||||||
|
1.0 : sourceData->lastClockRatio;
|
||||||
|
if (providerRateControl)
|
||||||
|
sourceData->nextFeedbackTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
static PlaybackDataResult playbackData(const void * data, size_t frameCount,
|
static PlaybackDataResult playbackData(const void * data, size_t frameCount,
|
||||||
const LG_AudioClock * sourceClock, int64_t arrivalTime)
|
const LG_AudioClock * sourceClock, int64_t arrivalTime)
|
||||||
{
|
{
|
||||||
@@ -2201,6 +2225,7 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
|
|||||||
ringbuffer_getCount(audio.playback.buffer) < 0;
|
ringbuffer_getCount(audio.playback.buffer) < 0;
|
||||||
bool discontinuity =
|
bool discontinuity =
|
||||||
bufferUnderrun || (sourceClock && sourceClock->discontinuity);
|
bufferUnderrun || (sourceClock && sourceClock->discontinuity);
|
||||||
|
const int64_t previousArrivalTime = sourceData->lastArrivalTime;
|
||||||
const int64_t packetTime =
|
const int64_t packetTime =
|
||||||
playbackMapMediaTime(sourceData, sourceClock, frames,
|
playbackMapMediaTime(sourceData, sourceClock, frames,
|
||||||
audio.playback.sampleRate, arrivalTime, &discontinuity);
|
audio.playback.sampleRate, arrivalTime, &discontinuity);
|
||||||
@@ -2251,9 +2276,46 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
|
|||||||
sourceData->sourceClock.phaseResidualSec;
|
sourceData->sourceClock.phaseResidualSec;
|
||||||
const double packetSec =
|
const double packetSec =
|
||||||
frames * nominalFrameSec;
|
frames * nominalFrameSec;
|
||||||
|
const double decayedPacketDurationSec =
|
||||||
|
sourceData->sourcePacketDurationSec *
|
||||||
|
exp(-packetSec / PLAYBACK_PHASE_RESERVE_DECAY_SEC);
|
||||||
|
const bool cadenceReset = discontinuity ||
|
||||||
|
state == STREAM_STATE_KEEP_ALIVE ||
|
||||||
|
state == STREAM_STATE_RESUMING;
|
||||||
|
if (cadenceReset && sourceData->sourcePacketDurationSec > 0.0)
|
||||||
|
{
|
||||||
|
sourceData->sourcePacketRecovering = true;
|
||||||
|
sourceData->sourcePacketPacedCount = 0;
|
||||||
|
}
|
||||||
|
else if (sourceData->sourcePacketRecovering)
|
||||||
|
{
|
||||||
|
if (packetSec <= decayedPacketDurationSec + nominalFrameSec)
|
||||||
|
{
|
||||||
|
sourceData->sourcePacketRecovering = false;
|
||||||
|
sourceData->sourcePacketPacedCount = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const bool paced = previousArrivalTime != INT64_MIN &&
|
||||||
|
arrivalTime > previousArrivalTime &&
|
||||||
|
(arrivalTime - previousArrivalTime) * 2.0e-9 >= packetSec;
|
||||||
|
sourceData->sourcePacketPacedCount = paced ?
|
||||||
|
sourceData->sourcePacketPacedCount + 1 : 0;
|
||||||
|
if (sourceData->sourcePacketPacedCount >= 2)
|
||||||
|
{
|
||||||
|
sourceData->sourcePacketRecovering = false;
|
||||||
|
sourceData->sourcePacketPacedCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Catch-up packets describe audio accumulated during a gap, not the future
|
||||||
|
* delivery cadence. Resume when the prior cadence returns or a larger
|
||||||
|
* cadence is confirmed by consecutive plausibly paced packets. */
|
||||||
sourceData->sourcePacketDurationSec =
|
sourceData->sourcePacketDurationSec =
|
||||||
max(packetSec, sourceData->sourcePacketDurationSec *
|
!sourceData->sourcePacketRecovering ?
|
||||||
exp(-packetSec / PLAYBACK_PHASE_RESERVE_DECAY_SEC));
|
max(packetSec, decayedPacketDurationSec) :
|
||||||
|
decayedPacketDurationSec;
|
||||||
|
|
||||||
if (!sourceData->sourcePhaseBaselineValid ||
|
if (!sourceData->sourcePhaseBaselineValid ||
|
||||||
sourceData->sourceClock.updates == 1)
|
sourceData->sourceClock.updates == 1)
|
||||||
@@ -2396,7 +2458,8 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
|
|||||||
|
|
||||||
double devPosition = DBL_MIN;
|
double devPosition = DBL_MIN;
|
||||||
state = playbackGetState();
|
state = playbackGetState();
|
||||||
if ((providerRateControl || bufferUnderrun) &&
|
if (playbackUseLowWaterRecovery(providerRateControl,
|
||||||
|
bufferUnderrun, sourceData) &&
|
||||||
(discontinuity ||
|
(discontinuity ||
|
||||||
state == STREAM_STATE_KEEP_ALIVE ||
|
state == STREAM_STATE_KEEP_ALIVE ||
|
||||||
state == STREAM_STATE_RESUMING))
|
state == STREAM_STATE_RESUMING))
|
||||||
@@ -2409,14 +2472,7 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
|
|||||||
sourceData->outputPosition += actualSlew;
|
sourceData->outputPosition += actualSlew;
|
||||||
curPosition += actualSlew;
|
curPosition += actualSlew;
|
||||||
|
|
||||||
sourceData->offsetError = 0.0;
|
playbackResetRateControl(sourceData, providerRateControl);
|
||||||
sourceData->offsetErrorIntegral = 0.0;
|
|
||||||
sourceData->ratioIntegral = 0.0;
|
|
||||||
if (providerRateControl && bufferUnderrun)
|
|
||||||
{
|
|
||||||
sourceData->lastRatio = 1.0;
|
|
||||||
sourceData->nextFeedbackTime = 0;
|
|
||||||
}
|
|
||||||
if (state == STREAM_STATE_KEEP_ALIVE ||
|
if (state == STREAM_STATE_KEEP_ALIVE ||
|
||||||
state == STREAM_STATE_RESUMING)
|
state == STREAM_STATE_RESUMING)
|
||||||
atomic_compare_exchange_strong_explicit(
|
atomic_compare_exchange_strong_explicit(
|
||||||
@@ -2437,9 +2493,7 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
|
|||||||
sourceData->outputPosition += actualSlew;
|
sourceData->outputPosition += actualSlew;
|
||||||
curPosition += actualSlew;
|
curPosition += actualSlew;
|
||||||
|
|
||||||
sourceData->offsetError = 0.0;
|
playbackResetRateControl(sourceData, providerRateControl);
|
||||||
sourceData->offsetErrorIntegral = 0.0;
|
|
||||||
sourceData->ratioIntegral = 0.0;
|
|
||||||
if (state == STREAM_STATE_KEEP_ALIVE ||
|
if (state == STREAM_STATE_KEEP_ALIVE ||
|
||||||
state == STREAM_STATE_RESUMING)
|
state == STREAM_STATE_RESUMING)
|
||||||
atomic_compare_exchange_strong_explicit(
|
atomic_compare_exchange_strong_explicit(
|
||||||
|
|||||||
@@ -820,6 +820,7 @@ if(ENABLE_AUDIO)
|
|||||||
provider
|
provider
|
||||||
playback-retry
|
playback-retry
|
||||||
jitter
|
jitter
|
||||||
|
recovery
|
||||||
consent
|
consent
|
||||||
quiesce
|
quiesce
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -654,6 +654,54 @@ static void testPlaybackJitter(void)
|
|||||||
CHECK(source.arrivalJitterSec < 0.011);
|
CHECK(source.arrivalJitterSec < 0.011);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void testPlaybackRecovery(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->outputPosition = 0;
|
||||||
|
source->lastRatio = 0.999;
|
||||||
|
source->lastClockRatio = 1.0002;
|
||||||
|
source->sourcePacketDurationSec = 0.010;
|
||||||
|
playbackPublishDeviceTiming(16, arrival, 0, 0.0, 0);
|
||||||
|
|
||||||
|
CHECK(ringbuffer_consume(audio.playback.buffer, NULL, 16) == 16);
|
||||||
|
CHECK(ringbuffer_getCount(audio.playback.buffer) == -16);
|
||||||
|
uint8_t frames[1200 * 2 * 2] = { 0 };
|
||||||
|
CHECK(playbackData(frames, 1200, NULL, arrival) ==
|
||||||
|
PLAYBACK_DATA_PROCESSED);
|
||||||
|
|
||||||
|
CHECK(source->sourcePacketDurationSec < 0.010);
|
||||||
|
CHECK(source->sourcePacketDurationSec > 0.009);
|
||||||
|
CHECK(source->sourcePacketRecovering);
|
||||||
|
CHECK(source->outputPosition == 1770);
|
||||||
|
CHECK(ringbuffer_getCount(audio.playback.buffer) == 1754);
|
||||||
|
CHECK(fabs(atomic_load(&audio.playback.backendResampleRatio) -
|
||||||
|
source->lastClockRatio) < 0.000001);
|
||||||
|
|
||||||
|
CHECK(playbackData(frames, 1200, NULL, arrival + 1000) ==
|
||||||
|
PLAYBACK_DATA_PROCESSED);
|
||||||
|
CHECK(source->sourcePacketDurationSec < 0.010);
|
||||||
|
CHECK(source->sourcePacketRecovering);
|
||||||
|
|
||||||
|
CHECK(playbackData(frames, 480, NULL,
|
||||||
|
arrival + INT64_C(10000000)) == PLAYBACK_DATA_PROCESSED);
|
||||||
|
CHECK(!source->sourcePacketRecovering);
|
||||||
|
CHECK(fabs(source->sourcePacketDurationSec - 0.010) < 0.000001);
|
||||||
|
|
||||||
|
stopAudio();
|
||||||
|
}
|
||||||
|
|
||||||
static void testConsent(void)
|
static void testConsent(void)
|
||||||
{
|
{
|
||||||
reset();
|
reset();
|
||||||
@@ -781,6 +829,7 @@ static const struct Test tests[] =
|
|||||||
{ "provider" , testProvider },
|
{ "provider" , testProvider },
|
||||||
{ "playback-retry", testPlaybackRetry },
|
{ "playback-retry", testPlaybackRetry },
|
||||||
{ "jitter" , testPlaybackJitter },
|
{ "jitter" , testPlaybackJitter },
|
||||||
|
{ "recovery" , testPlaybackRecovery },
|
||||||
{ "consent" , testConsent },
|
{ "consent" , testConsent },
|
||||||
{ "quiesce" , testQuiesce },
|
{ "quiesce" , testQuiesce },
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user