mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-23 15:41:31 +00:00
[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:
@@ -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);
|
||||
|
||||
@@ -821,6 +821,7 @@ if(ENABLE_AUDIO)
|
||||
playback-retry
|
||||
jitter
|
||||
recovery
|
||||
resume
|
||||
consent
|
||||
quiesce
|
||||
)
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user