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;
|
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,6 +1302,12 @@ 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 &&
|
||||||
|
atomic_load_explicit(
|
||||||
|
&audio.playback.backlogTrimTarget, memory_order_acquire) >= 0 &&
|
||||||
|
!atomic_flag_test_and_set_explicit(
|
||||||
|
&audio.playback.deviceStateGate, memory_order_acquire))
|
||||||
|
{
|
||||||
if (playbackGetState() == STREAM_STATE_RUN)
|
if (playbackGetState() == STREAM_STATE_RUN)
|
||||||
{
|
{
|
||||||
const int trimTarget = atomic_exchange_explicit(
|
const int trimTarget = atomic_exchange_explicit(
|
||||||
@@ -1283,6 +1328,9 @@ static int playbackPullFrames(uint8_t * dst, int frames)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
||||||
* position tracks source frames consumed from the ring for latency
|
* position tracks source frames consumed from the ring for latency
|
||||||
@@ -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;
|
||||||
|
const bool wasUnderrunning = data->underrunning;
|
||||||
|
data->underrunning = underrunning;
|
||||||
|
if (underrunning && !wasUnderrunning)
|
||||||
atomic_fetch_add_explicit(
|
atomic_fetch_add_explicit(
|
||||||
&audio.playback.underruns, 1, memory_order_relaxed);
|
&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 &&
|
|
||||||
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;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
state = expected;
|
const StreamState state = playbackGetState();
|
||||||
}
|
|
||||||
|
|
||||||
if (state != STREAM_STATE_STOP)
|
if (state != STREAM_STATE_STOP)
|
||||||
playbackStop();
|
playbackStop();
|
||||||
@@ -1416,6 +1483,7 @@ static bool playbackStart(const LG_AudioFormat * format,
|
|||||||
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))
|
||||||
@@ -2844,17 +2967,20 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount,
|
|||||||
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,
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -821,6 +821,7 @@ if(ENABLE_AUDIO)
|
|||||||
playback-retry
|
playback-retry
|
||||||
jitter
|
jitter
|
||||||
recovery
|
recovery
|
||||||
|
resume
|
||||||
consent
|
consent
|
||||||
quiesce
|
quiesce
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 },
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user