/** * Looking Glass * Copyright © 2017-2026 The Looking Glass Authors * https://looking-glass.io * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., 59 * Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #if ENABLE_AUDIO #include "audio.h" #include "main.h" #include "common/array.h" #include "common/debug.h" #include "common/event.h" #include "common/locking.h" #include "common/thread.h" #include "common/util.h" #include "common/ringbuffer.h" #include "dynamic/audiodev.h" #include #include #include #include #include #include #include #include #include #define PLAYBACK_CLOCK_BANDWIDTH_HZ 0.05 #define PLAYBACK_ACQUIRE_PHASE_BANDWIDTH_HZ 0.05 #define PLAYBACK_PHASE_BANDWIDTH_HZ 0.005 #define PLAYBACK_OFFSET_FILTER_BANDWIDTH_HZ \ (20.0 * PLAYBACK_PHASE_BANDWIDTH_HZ) #define PLAYBACK_PHASE_DEADBAND_SEC 0.0005 #define PLAYBACK_MAX_RATE_CORRECTION 0.005 #define PLAYBACK_MAX_RATE_SLEW_PER_SEC 0.005 #define PLAYBACK_MAX_JITTER_SEC 0.1 #define PLAYBACK_JITTER_DECAY_SEC 10.0 #define AUDIO_START_RETRY_MIN_NS INT64_C(5000000) #define AUDIO_START_RETRY_MAX_NS INT64_C(250000000) #define AUDIO_RETRY_RESET_NS INT64_C(1000000000) #define PLAYBACK_PHASE_BASELINE_TIME_SEC 5.0 #define PLAYBACK_PHASE_RESERVE_DECAY_SEC 60.0 /* libsamplerate does not expose its buffered-frame delay. SRC_SINC_FASTEST * retains 20 frames at unity; the bounded ratio range changes this by less * than one frame. Keep it in the latency model, but not in the safety buffer. */ #define PLAYBACK_RESAMPLER_DELAY_FRAMES 20 #define PLAYBACK_TIMESTAMP_DISCONTINUITY_NS INT64_C(2000000000) #define PLAYBACK_RATE_WINDOW_MS 60000 #define PLAYBACK_RATE_MIN_SPAN_MS 45000 #define PLAYBACK_RATE_SAMPLE_INTERVAL_MS 100 #define PLAYBACK_RATE_FILTER_TIME_SEC 60.0 #define PLAYBACK_RATE_MAX_SAMPLES 1024 #define PLAYBACK_DEVICE_RATE_CHECK_SEC 1.0 #define PLAYBACK_DEVICE_RATE_STABLE_SEC 2.0 #define PLAYBACK_DEVICE_RATE_STABLE_DELTA_PPM 50.0 #define PLAYBACK_DEVICE_RATE_MAX_ACQUIRE_SEC 20.0 #define PLAYBACK_MAX_SOURCE_PACKET_MS 100 #define PLAYBACK_FEEDBACK_INTERVAL_NS INT64_C(1000000) #define PLAYBACK_FEEDBACK_FAILURE_NS INT64_C(100000000) #define PLAYBACK_GRAPH_INTERVAL_NS INT64_C(25000000) typedef enum { STREAM_STATE_STOP, STREAM_STATE_SETUP_SOURCE, STREAM_STATE_SETUP_DEVICE, STREAM_STATE_RUN, STREAM_STATE_KEEP_ALIVE, STREAM_STATE_RESUMING, STREAM_STATE_STOP_PENDING } StreamState; typedef enum { PLAYBACK_DATA_DROP, PLAYBACK_DATA_PROCESSED, PLAYBACK_DATA_RETRY, PLAYBACK_DATA_RETRY_NOW } PlaybackDataResult; typedef enum { PLAYBACK_RATE_SOFTWARE, PLAYBACK_RATE_BACKEND, PLAYBACK_RATE_PROVIDER, } PlaybackRateControl; typedef enum { PLAYBACK_DIAGNOSTIC_REGISTER_GRAPH = 1U << 0, PLAYBACK_DIAGNOSTIC_INVALIDATE = 1U << 1, PLAYBACK_DIAGNOSTIC_START_LOG = 1U << 2, PLAYBACK_DIAGNOSTIC_SYNC_LOG = 1U << 3, } PlaybackDiagnosticFlags; #define STREAM_ACTIVE(state) \ (state == STREAM_STATE_RUN || \ state == STREAM_STATE_KEEP_ALIVE || \ state == STREAM_STATE_RESUMING) #define PLAYBACK_CALLBACK_DISABLED (UINT32_C(1) << 31) #define PLAYBACK_CALLBACK_COUNT_MASK (PLAYBACK_CALLBACK_DISABLED - 1) #define ACTIVE_CALLBACK_WAITING (UINT32_C(1) << 31) #define ACTIVE_CALLBACK_COUNT_MASK (ACTIVE_CALLBACK_WAITING - 1) typedef struct { int64_t nextPosition; double outputPosition; double appliedRatio; int startupSilenceFrames; } PlaybackDeviceData; typedef struct { bool valid; unsigned int updates; int64_t time; double position; double frameSec; double phaseResidualSec; } PlaybackClock; typedef struct { int64_t timeMs; int64_t position; } PlaybackRateSample; typedef struct { float * framesIn; float * framesOut; int framesInSize; int framesOutSize; int maxSourcePacketFrames; int64_t inputPosition; int64_t outputPosition; int64_t mediaTime; int64_t mediaElapsed; int64_t mediaTimeMs; int64_t mediaLocalOrigin; uint64_t mediaPosition; int64_t lastArrivalTime; double arrivalJitterSec; double sourcePhaseBaselineSec; double sourcePhaseReserveSec; double sourcePacketDurationSec; bool sourcePhaseBaselineValid; bool mediaClockValid; bool mediaPositionValid; bool mediaClockFromSource; PlaybackRateSample rateSamples[PLAYBACK_RATE_MAX_SAMPLES]; unsigned int rateSampleStart; unsigned int rateSampleCount; int64_t rateLastSampleTimeMs; int64_t rateFilterTimeMs; double sourceRateFrameSec; bool sourceRateValid; bool bufferOverrunPending; int devPeriodFrames; int64_t devReadPosition; unsigned int deviceTimingSequence; int64_t deviceClockAcquireStart; int64_t deviceClockCheckTime; double deviceClockCheckFrameSec; double deviceClockStableSec; double devicePositionOffsetFrames; bool deviceClockStable; double offsetError; double offsetErrorIntegral; double ratioIntegral; double lastRatio; double lastClockRatio; int64_t nextFeedbackTime; int64_t nextGraphTime; int64_t nextLogTime; unsigned int bufferOverruns; PlaybackClock sourceClock; PlaybackClock deviceClock; PlaybackClock outputClock; SRC_STATE * src; } PlaybackSourceData; typedef struct { atomic_uint sequence; atomic_int periodFrames; _Atomic(int64_t) time; _Atomic(int64_t) position; _Atomic(double) outputPosition; } PlaybackDeviceTiming; typedef struct { double targetLatencyMs; double queuedLatencyMs; } PlaybackStartDiagnostics; typedef struct { double softwareLatencyMs; double targetLatencyMs; double sourcePpm; double devicePpm; double controlPpm; double jitterMs; unsigned int underruns; unsigned int overruns; PlaybackRateControl rateControl; } PlaybackSyncDiagnostics; typedef struct { unsigned int epoch; unsigned int pending; float graphMax; PlaybackStartDiagnostics start; PlaybackSyncDiagnostics sync; } PlaybackDiagnostics; typedef struct { const LG_AudioOps * ops; void * opaque; bool available; uint32_t epoch; uint32_t generation; } AudioBinding; typedef struct { struct LG_AudioDevOps * audioDev; atomic_bool ready; LG_Lock bindingLock; LG_Lock providerLock; LG_RWLock activeLock; uint32_t nextBindingEpoch; atomic_uint activeCallbacks; LGEvent * activeIdle; AudioBinding fallback; AudioBinding transport; AudioBinding active; struct { LG_Lock sourceLock; LG_Lock deviceLock; struct { sem_t wake; bool wakeInitialized; atomic_bool wakePending; LGThread * thread; atomic_bool stop; } worker; _Atomic(StreamState) state; atomic_uint callbackState; sem_t callbackIdle; bool callbackIdleInitialized; atomic_uint streamGeneration; uint64_t requestSerial; uint32_t requestedGeneration; LG_AudioFormat requestedFormat; bool requestedFormatValid; bool requestedProviderRateControl; bool forceSoftwareResampler; bool startPending; bool startInProgress; unsigned int startFailures; int64_t nextStartRetry; atomic_uint activeAttemptSerial; atomic_uint failedAttemptSerial; uint32_t nextAttemptSerial; uint64_t backendRequestSerial; int64_t backendStartTime; uint64_t controlSerial; bool controlsPending; int volumeChannels; uint16_t volume[LG_AUDIO_MAX_CHANNELS]; bool mute; LG_AudioFormat format; LG_AudioFormat lastFormat; bool lastFormatValid; int channels; int sampleRate; int stride; bool convertToFloat; int deviceMaxPeriodFrames; int deviceStartFrames; int targetStartFrames; int startupLowWaterFrames; int64_t startupPacketDeadline; int64_t startupPacketPeriod; PlaybackRateControl rateControl; bool lastProviderRateControl; _Atomic(double) backendResampleRatio; atomic_bool backendResamplerFailed; RingBuffer buffer; PlaybackDeviceTiming deviceTiming; atomic_uint underruns; RingBuffer timings; GraphHandle graph; atomic_uint diagnosticsEpoch; atomic_bool graphReady; bool graphRegistrationAttempted; PlaybackDiagnostics diagnostics; /* These two structs contain data specifically for use in the device and * source data threads respectively. Keep them on separate cache lines to * avoid false sharing. */ alignas(64) PlaybackDeviceData deviceData; alignas(64) PlaybackSourceData sourceData; } playback; struct { LG_RWLock lock; LGEvent * wake; LGThread * thread; atomic_bool workerAlive; atomic_uint deliverySerial; atomic_uint failedAttemptSerial; uint32_t nextAttemptSerial; AudioBinding deliveryBinding; uint32_t deliveryGeneration; bool shuttingDown; bool requested; bool enabled; unsigned int startFailures; int64_t nextStartRetry; uint64_t requestSerial; AudioBinding requestedBinding; uint32_t requestedGeneration; LG_AudioFormat requestedFormat; int volumeChannels; uint16_t volume[LG_AUDIO_MAX_CHANNELS]; bool mute; uint64_t controlSerial; MsgBoxHandle confirmHandle; uint64_t confirmGeneration; bool confirmPending; } record; struct { LG_Lock lock; sem_t wake; bool wakeInitialized; atomic_bool wakePending; LGThread * thread; atomic_bool stop; bool pending; const LG_AudioOps * ops; void * opaque; uint32_t bindingEpoch; uint32_t bindingGeneration; uint32_t generation; LG_AudioClock clock; double targetRate; } feedback; } AudioState; static AudioState audio = { 0 }; static size_t audioSampleSize(LG_AudioSampleFormat format) { switch (format) { case LG_AUDIO_FMT_U8: return 1; case LG_AUDIO_FMT_S16_LE: return 2; case LG_AUDIO_FMT_S24_LE: return 3; case LG_AUDIO_FMT_S32_LE: case LG_AUDIO_FMT_F32_LE: case LG_AUDIO_FMT_F32_NE: return 4; case LG_AUDIO_FMT_F64_LE: return 8; } return 0; } static bool audioFormatValid(const LG_AudioFormat * format) { if (!format || format->channelCount < 1 || format->channelCount > LG_AUDIO_MAX_CHANNELS || format->sampleRate < 8000 || format->sampleRate > 384000 || audioSampleSize(format->sampleFormat) == 0) return false; for (unsigned int i = 0; i < format->channelCount; ++i) if (format->channels[i] > LG_AUDIO_CH_TOP_REAR_RIGHT) return false; return true; } static bool audioFormatEqual(const LG_AudioFormat * a, const LG_AudioFormat * b) { return a->sampleFormat == b->sampleFormat && a->sampleRate == b->sampleRate && a->channelCount == b->channelCount && memcmp(a->channels, b->channels, sizeof(*a->channels) * a->channelCount) == 0; } static bool audioConvertToFloat(float * dst, const void * src, size_t samples, LG_AudioSampleFormat format) { if (!dst || !src) return false; switch (format) { case LG_AUDIO_FMT_U8: { const uint8_t * in = src; for (size_t i = 0; i < samples; ++i) dst[i] = ((int)in[i] - 128) / 128.0f; return true; } case LG_AUDIO_FMT_S16_LE: { const uint8_t * in = src; for (size_t i = 0; i < samples; ++i, in += 2) { const int16_t value = (int16_t)( (uint16_t)in[0] | (uint16_t)in[1] << 8); dst[i] = value / 32768.0f; } return true; } case LG_AUDIO_FMT_S24_LE: { const uint8_t * in = src; for (size_t i = 0; i < samples; ++i, in += 3) { int32_t value = (int32_t)((uint32_t)in[0] | (uint32_t)in[1] << 8 | (uint32_t)in[2] << 16); if (value & 0x800000) value |= (int32_t)0xff000000; dst[i] = value / 8388608.0f; } return true; } case LG_AUDIO_FMT_S32_LE: { const uint8_t * in = src; for (size_t i = 0; i < samples; ++i, in += 4) { const int32_t value = (int32_t)( (uint32_t)in[0] | (uint32_t)in[1] << 8 | (uint32_t)in[2] << 16 | (uint32_t)in[3] << 24); dst[i] = value / 2147483648.0f; } return true; } case LG_AUDIO_FMT_F32_LE: { #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ memcpy(dst, src, samples * sizeof(*dst)); #else const uint8_t * in = src; for (size_t i = 0; i < samples; ++i, in += 4) { const uint32_t bits = (uint32_t)in[0] | (uint32_t)in[1] << 8 | (uint32_t)in[2] << 16 | (uint32_t)in[3] << 24; memcpy(&dst[i], &bits, sizeof(bits)); } #endif return true; } case LG_AUDIO_FMT_F32_NE: memcpy(dst, src, samples * sizeof(*dst)); return true; case LG_AUDIO_FMT_F64_LE: { const uint8_t * in = src; for (size_t i = 0; i < samples; ++i, in += 8) { const uint64_t bits = (uint64_t)in[0] | (uint64_t)in[1] << 8 | (uint64_t)in[2] << 16 | (uint64_t)in[3] << 24 | (uint64_t)in[4] << 32 | (uint64_t)in[5] << 40 | (uint64_t)in[6] << 48 | (uint64_t)in[7] << 56; double value; memcpy(&value, &bits, sizeof(value)); dst[i] = value; } return true; } } return false; } typedef struct { int periodFrames; int64_t nextTime; int64_t nextPosition; double outputPosition; } PlaybackDeviceTick; static void playbackClockReset(PlaybackClock * clock, int64_t time, double position, double frameSec) { clock->valid = true; clock->updates = 1; clock->time = time; clock->position = position; clock->frameSec = frameSec; clock->phaseResidualSec = 0.0; } static bool playbackClockUpdate(PlaybackClock * clock, int64_t time, double position, double nominalFrameSec) { if (!clock->valid) { playbackClockReset(clock, time, position, nominalFrameSec); return true; } const double frames = position - clock->position; if (frames <= 0) return frames == 0; const double predicted = clock->time + frames * clock->frameSec * 1.0e9; const double error = (time - predicted) * 1.0e-9; if (fabs(error) >= 0.2) { playbackClockReset(clock, time, position, nominalFrameSec); return false; } clock->phaseResidualSec = error; const double periodSec = frames * clock->frameSec; const double omega = 2.0 * M_PI * PLAYBACK_CLOCK_BANDWIDTH_HZ * periodSec; const double b = M_SQRT2 * omega; const double c = omega * omega; clock->time = llrint(predicted + b * error * 1.0e9); clock->position = position; clock->frameSec += c * error / frames; clock->frameSec = clamp(clock->frameSec, nominalFrameSec * (1.0 - PLAYBACK_MAX_RATE_CORRECTION), nominalFrameSec * (1.0 + PLAYBACK_MAX_RATE_CORRECTION)); ++clock->updates; return true; } static bool playbackSourceClockUpdate(PlaybackClock * clock, int64_t time, double position, double nominalFrameSec) { if (!clock->valid) { playbackClockReset(clock, time, position, nominalFrameSec); return true; } const double frames = position - clock->position; if (frames <= 0) return frames == 0; const double predicted = clock->time + frames * clock->frameSec * 1.0e9; const double residual = (time - predicted) * 1.0e-9; if (fabs(residual) >= 0.2) { playbackClockReset(clock, time, position, nominalFrameSec); return false; } /* source media time periodically changes phase by several * milliseconds. Preserve it as a diagnostic and discontinuity signal, but * advance the source clock solely from decoded sample position. Short-term * timestamp corrections must not move the playback buffer. */ clock->time = llrint(predicted); clock->position = position; clock->phaseResidualSec = residual; ++clock->updates; return true; } static void playbackDeviceClockAcquireReset(PlaybackSourceData * sourceData) { sourceData->deviceClockAcquireStart = INT64_MIN; sourceData->deviceClockCheckTime = INT64_MIN; sourceData->deviceClockStableSec = 0.0; sourceData->devicePositionOffsetFrames = 0.0; sourceData->deviceClockStable = false; sourceData->ratioIntegral = 0.0; sourceData->lastClockRatio = 1.0; } static bool playbackDeviceClockAcquire( PlaybackSourceData * sourceData, int64_t time) { if (sourceData->deviceClockStable) return false; if (sourceData->deviceClockAcquireStart == INT64_MIN) { sourceData->deviceClockAcquireStart = time; sourceData->deviceClockCheckTime = time; sourceData->deviceClockCheckFrameSec = sourceData->deviceClock.frameSec; return false; } const double checkSec = (time - sourceData->deviceClockCheckTime) * 1.0e-9; if (checkSec < PLAYBACK_DEVICE_RATE_CHECK_SEC) return false; const double rateDeltaPpm = fabs( sourceData->deviceClock.frameSec / sourceData->deviceClockCheckFrameSec - 1.0) * 1.0e6; if (rateDeltaPpm <= PLAYBACK_DEVICE_RATE_STABLE_DELTA_PPM) sourceData->deviceClockStableSec += checkSec; else sourceData->deviceClockStableSec = 0.0; sourceData->deviceClockCheckTime = time; sourceData->deviceClockCheckFrameSec = sourceData->deviceClock.frameSec; const double acquireSec = (time - sourceData->deviceClockAcquireStart) * 1.0e-9; if (sourceData->deviceClockStableSec < PLAYBACK_DEVICE_RATE_STABLE_SEC && acquireSec < PLAYBACK_DEVICE_RATE_MAX_ACQUIRE_SEC) return false; sourceData->deviceClockStable = true; return true; } static double playbackClockPosition(const PlaybackClock * clock, int64_t time) { return clock->position + (time - clock->time) * 1.0e-9 / clock->frameSec; } static void playbackSourceRateReset(PlaybackSourceData * sourceData) { sourceData->rateSampleStart = 0; sourceData->rateSampleCount = 0; sourceData->rateLastSampleTimeMs = INT64_MIN; sourceData->rateFilterTimeMs = INT64_MIN; sourceData->sourceRateValid = false; } static void playbackSourceRateAdd(PlaybackSourceData * sourceData, int64_t timeMs, double nominalFrameSec) { if (sourceData->rateLastSampleTimeMs != INT64_MIN && timeMs - sourceData->rateLastSampleTimeMs < PLAYBACK_RATE_SAMPLE_INTERVAL_MS) return; sourceData->rateLastSampleTimeMs = timeMs; while (sourceData->rateSampleCount > 0) { const PlaybackRateSample * oldest = &sourceData->rateSamples[sourceData->rateSampleStart]; if (timeMs - oldest->timeMs <= PLAYBACK_RATE_WINDOW_MS) break; sourceData->rateSampleStart = (sourceData->rateSampleStart + 1) % PLAYBACK_RATE_MAX_SAMPLES; --sourceData->rateSampleCount; } if (sourceData->rateSampleCount == PLAYBACK_RATE_MAX_SAMPLES) { sourceData->rateSampleStart = (sourceData->rateSampleStart + 1) % PLAYBACK_RATE_MAX_SAMPLES; --sourceData->rateSampleCount; } const unsigned int index = (sourceData->rateSampleStart + sourceData->rateSampleCount) % PLAYBACK_RATE_MAX_SAMPLES; sourceData->rateSamples[index] = (PlaybackRateSample) { .timeMs = timeMs, .position = sourceData->inputPosition }; ++sourceData->rateSampleCount; const PlaybackRateSample * first = &sourceData->rateSamples[sourceData->rateSampleStart]; if (sourceData->rateSampleCount < 2 || timeMs - first->timeMs < PLAYBACK_RATE_MIN_SPAN_MS) return; double sumPosition = 0.0; double sumTime = 0.0; double sumPosition2 = 0.0; double sumPositionTime = 0.0; for (unsigned int i = 0; i < sourceData->rateSampleCount; ++i) { const PlaybackRateSample * sample = &sourceData->rateSamples[ (sourceData->rateSampleStart + i) % PLAYBACK_RATE_MAX_SAMPLES]; const double position = sample->position - first->position; const double timeSec = (sample->timeMs - first->timeMs) / 1000.0; sumPosition += position; sumTime += timeSec; sumPosition2 += position * position; sumPositionTime += position * timeSec; } const double count = sourceData->rateSampleCount; const double denominator = sumPosition2 - sumPosition * sumPosition / count; if (denominator <= 0.0) return; const double frameSec = (sumPositionTime - sumPosition * sumTime / count) / denominator; if (frameSec < nominalFrameSec * (1.0 - PLAYBACK_MAX_RATE_CORRECTION) || frameSec > nominalFrameSec * (1.0 + PLAYBACK_MAX_RATE_CORRECTION)) return; if (!sourceData->sourceRateValid) { sourceData->sourceRateFrameSec = frameSec; sourceData->sourceRateValid = true; } else { const double elapsedSec = (timeMs - sourceData->rateFilterTimeMs) / 1000.0; const double alpha = -expm1(-elapsedSec / PLAYBACK_RATE_FILTER_TIME_SEC); sourceData->sourceRateFrameSec += alpha * (frameSec - sourceData->sourceRateFrameSec); } sourceData->rateFilterTimeMs = timeMs; } static void playbackPublishDeviceTiming( int periodFrames, int64_t time, int64_t position, double outputPosition) { PlaybackDeviceTiming * timing = &audio.playback.deviceTiming; /* Publish the odd writer marker before any snapshot field. */ atomic_fetch_add_explicit(&timing->sequence, 1, memory_order_seq_cst); atomic_store_explicit( &timing->periodFrames, periodFrames, memory_order_relaxed); atomic_store_explicit(&timing->time, time, memory_order_relaxed); atomic_store_explicit(&timing->position, position, memory_order_relaxed); atomic_store_explicit( &timing->outputPosition, outputPosition, memory_order_relaxed); atomic_fetch_add_explicit(&timing->sequence, 1, memory_order_release); } static bool playbackReadDeviceTiming( unsigned int previousSequence, PlaybackDeviceTick * tick, unsigned int * sequence) { PlaybackDeviceTiming * timing = &audio.playback.deviceTiming; unsigned int before; unsigned int after; do { before = atomic_load_explicit(&timing->sequence, memory_order_acquire); if ((before & 1) || before == previousSequence) return false; tick->periodFrames = atomic_load_explicit(&timing->periodFrames, memory_order_relaxed); tick->nextTime = atomic_load_explicit(&timing->time, memory_order_relaxed); tick->nextPosition = atomic_load_explicit(&timing->position, memory_order_relaxed); tick->outputPosition = atomic_load_explicit( &timing->outputPosition, memory_order_relaxed); atomic_thread_fence(memory_order_acquire); after = atomic_load_explicit(&timing->sequence, memory_order_relaxed); } while (before != after); *sequence = after; return true; } static void playbackResetMediaClock(PlaybackSourceData * sourceData, int64_t time, uint64_t position, bool fromSource, int64_t now) { sourceData->mediaTime = time; sourceData->mediaElapsed = 0; sourceData->mediaTimeMs = 0; sourceData->mediaLocalOrigin = now; sourceData->mediaPosition = position; sourceData->lastArrivalTime = INT64_MIN; sourceData->mediaClockValid = true; sourceData->mediaPositionValid = true; sourceData->mediaClockFromSource = fromSource; sourceData->sourceClock.valid = false; playbackSourceRateReset(sourceData); } static void playbackPrepareMediaClock( PlaybackSourceData * sourceData, const LG_AudioClock * sourceClock) { sourceData->mediaTime = 0; sourceData->mediaClockValid = false; sourceData->mediaPositionValid = sourceClock != NULL; sourceData->mediaClockFromSource = sourceClock != NULL; sourceData->mediaPosition = sourceClock ? sourceClock->position : 0; sourceData->inputPosition = 0; sourceData->sourceClock.valid = false; playbackSourceRateReset(sourceData); } static int64_t playbackMapMediaTime(PlaybackSourceData * sourceData, const LG_AudioClock * clock, int frames, int sampleRate, int64_t now, bool * discontinuity) { const bool fromSource = clock != NULL; const uint64_t position = clock ? clock->position : (uint64_t)sourceData->inputPosition; const int64_t time = clock ? clock->time : llrint(sourceData->inputPosition * (1.0e9 / sampleRate)); if (clock && clock->discontinuity) *discontinuity = true; if (!sourceData->mediaClockValid) { if (sourceData->mediaPositionValid && (sourceData->mediaClockFromSource != fromSource || sourceData->mediaPosition != position)) *discontinuity = true; playbackResetMediaClock( sourceData, time, position, fromSource, now); sourceData->mediaPosition = position + frames; return now; } const int64_t delta = time - sourceData->mediaTime; if ((!clock || !clock->discontinuity) && sourceData->mediaClockFromSource == fromSource && sourceData->mediaPositionValid && sourceData->mediaPosition == position && delta >= 0 && delta <= PLAYBACK_TIMESTAMP_DISCONTINUITY_NS && sourceData->lastArrivalTime != INT64_MIN && now >= sourceData->lastArrivalTime) { const double mediaDelta = delta * 1.0e-9; const double arrivalDelta = (now - sourceData->lastArrivalTime) * 1.0e-9; const double lateness = max(arrivalDelta - mediaDelta, 0.0); /* Learn harmful late delivery before a buffer underrun rebases the media * clock. Early catch-up packets do not require additional reserve. */ sourceData->arrivalJitterSec = min(PLAYBACK_MAX_JITTER_SEC, max(lateness, sourceData->arrivalJitterSec * exp(-arrivalDelta / PLAYBACK_JITTER_DECAY_SEC))); } if (*discontinuity || sourceData->mediaClockFromSource != fromSource || !sourceData->mediaPositionValid || sourceData->mediaPosition != position || delta < 0 || delta > PLAYBACK_TIMESTAMP_DISCONTINUITY_NS) { playbackResetMediaClock( sourceData, time, position, fromSource, now); sourceData->mediaPosition = position + frames; *discontinuity = true; return now; } sourceData->mediaTime = time; sourceData->mediaPosition = position + frames; sourceData->mediaElapsed += delta; sourceData->mediaTimeMs = sourceData->mediaElapsed / INT64_C(1000000); return sourceData->mediaLocalOrigin + sourceData->mediaElapsed; } static void playbackStop(void); static bool playbackEnsureConversionBuffers( PlaybackSourceData * sourceData, int frames); static MsgBoxHandle recordCancelConfirmLocked(void); static void recordStop( const AudioBinding * binding, uint32_t generation); static bool eventBegin( const AudioBinding * binding, AudioBinding * active); static void eventEnd(void); static StreamState playbackGetState(void) { return atomic_load_explicit( &audio.playback.state, memory_order_acquire); } static void playbackSetState(StreamState state) { atomic_store_explicit( &audio.playback.state, state, memory_order_release); } static bool playbackCallbackEnter(void) { unsigned int state = atomic_load_explicit( &audio.playback.callbackState, memory_order_relaxed); for (;;) { if (state & PLAYBACK_CALLBACK_DISABLED) return false; DEBUG_ASSERT((state & PLAYBACK_CALLBACK_COUNT_MASK) != PLAYBACK_CALLBACK_COUNT_MASK); if (atomic_compare_exchange_weak_explicit( &audio.playback.callbackState, &state, state + 1, memory_order_acquire, memory_order_relaxed)) return true; } } static void playbackCallbackExit(void) { const unsigned int previous = atomic_fetch_sub_explicit( &audio.playback.callbackState, 1, memory_order_release); if ((previous & PLAYBACK_CALLBACK_DISABLED) && (previous & PLAYBACK_CALLBACK_COUNT_MASK) == 1 && audio.playback.callbackIdleInitialized) sem_post(&audio.playback.callbackIdle); } static void playbackDisableCallbacks(void) { atomic_fetch_or_explicit( &audio.playback.callbackState, PLAYBACK_CALLBACK_DISABLED, memory_order_acq_rel); } static void playbackWaitForCallbacks(void) { while((atomic_load_explicit( &audio.playback.callbackState, memory_order_acquire) & PLAYBACK_CALLBACK_COUNT_MASK) != 0) { int result; do result = sem_wait(&audio.playback.callbackIdle); while (result < 0 && errno == EINTR); if (result < 0) break; } } static void playbackWorkerWake(void) { if (!audio.playback.worker.wakeInitialized || !audio.playback.worker.thread || atomic_exchange_explicit( &audio.playback.worker.wakePending, true, memory_order_acq_rel)) return; if (sem_post(&audio.playback.worker.wake) < 0 && errno != EOVERFLOW) atomic_store_explicit( &audio.playback.worker.wakePending, false, memory_order_release); } static void feedbackWorkerWake(void) { if (!audio.feedback.wakeInitialized || !audio.feedback.thread || atomic_exchange_explicit( &audio.feedback.wakePending, true, memory_order_acq_rel)) return; if (sem_post(&audio.feedback.wake) < 0 && errno != EOVERFLOW) atomic_store_explicit( &audio.feedback.wakePending, false, memory_order_release); } static void playbackQueueStop(void) { atomic_store_explicit( &audio.playback.streamGeneration, 0, memory_order_release); playbackDisableCallbacks(); playbackSetState(STREAM_STATE_STOP_PENDING); playbackWorkerWake(); } static void playbackQueueSourceStop(void) { atomic_store_explicit( &audio.playback.activeAttemptSerial, 0, memory_order_release); playbackQueueStop(); } static void playbackBackendFailed(uint32_t attemptSerial) { unsigned int expected = attemptSerial; if (!attemptSerial || !atomic_compare_exchange_strong_explicit( &audio.playback.activeAttemptSerial, &expected, 0, memory_order_acq_rel, memory_order_acquire)) return; atomic_store_explicit(&audio.playback.failedAttemptSerial, attemptSerial, memory_order_release); playbackQueueStop(); } /* sourceLock must be held. */ static uint32_t playbackArmBackend(void) { if (!++audio.playback.nextAttemptSerial) ++audio.playback.nextAttemptSerial; const uint32_t attemptSerial = audio.playback.nextAttemptSerial; atomic_store_explicit( &audio.playback.failedAttemptSerial, 0, memory_order_release); audio.playback.backendRequestSerial = audio.playback.requestSerial; audio.playback.backendStartTime = 0; atomic_store_explicit(&audio.playback.activeAttemptSerial, attemptSerial, memory_order_release); return attemptSerial; } bool lgAudio_supportsPlayback(void) { return atomic_load_explicit(&audio.ready, memory_order_acquire) && audio.audioDev && audio.audioDev->playback.start; } static const char * audioGraphFormatFn(const char * name, float min, float max, float avg, float freq, float last) { static char title[64]; snprintf(title, sizeof(title), "%s: min:%4.2f max:%4.2f avg:%4.2f now:%4.2f", name, min, max, avg, last); return title; } /* sourceLock must be held. */ static PlaybackDiagnostics * playbackDiagnosticsLocked(void) { PlaybackDiagnostics * diagnostics = &audio.playback.diagnostics; const unsigned int epoch = atomic_load_explicit( &audio.playback.diagnosticsEpoch, memory_order_acquire); if (diagnostics->epoch != epoch) *diagnostics = (PlaybackDiagnostics) { .epoch = epoch }; return diagnostics; } static void playbackStop(void) { const bool alreadyStopped = playbackGetState() == STREAM_STATE_STOP; playbackDisableCallbacks(); atomic_store_explicit( &audio.playback.activeAttemptSerial, 0, memory_order_release); if (!alreadyStopped) audio.audioDev->playback.stop(); playbackWaitForCallbacks(); atomic_store_explicit( &audio.playback.failedAttemptSerial, 0, memory_order_release); audio.playback.backendRequestSerial = 0; audio.playback.backendStartTime = 0; if (alreadyStopped) return; atomic_store_explicit( &audio.playback.graphReady, false, memory_order_release); atomic_fetch_add_explicit( &audio.playback.diagnosticsEpoch, 1, memory_order_release); playbackSetState(STREAM_STATE_STOP); ringbuffer_free(&audio.playback.buffer); audio.playback.sourceData.src = src_delete(audio.playback.sourceData.src); if (audio.playback.sourceData.framesIn) { free(audio.playback.sourceData.framesIn); free(audio.playback.sourceData.framesOut); audio.playback.sourceData.framesIn = NULL; audio.playback.sourceData.framesOut = NULL; audio.playback.sourceData.framesInSize = 0; audio.playback.sourceData.framesOutSize = 0; } audio.playback.sourceData.maxSourcePacketFrames = 0; if (audio.playback.timings) { if (audio.playback.graph) app_unregisterGraph(audio.playback.graph); ringbuffer_free(&audio.playback.timings); } audio.playback.graph = NULL; audio.playback.graphRegistrationAttempted = false; } static int playbackPullFrames(uint8_t * dst, int frames) { DEBUG_ASSERT(frames >= 0); if (frames == 0) return frames; if (!playbackCallbackEnter()) return 0; PlaybackDeviceData * data = &audio.playback.deviceData; double nextRatio = 1.0; if (audio.playback.rateControl == PLAYBACK_RATE_BACKEND) { nextRatio = atomic_load_explicit( &audio.playback.backendResampleRatio, memory_order_acquire); if (!audio.audioDev->playback.setRate(&nextRatio)) { atomic_store_explicit( &audio.playback.backendResamplerFailed, true, memory_order_release); nextRatio = data->appliedRatio; } } const int64_t now = nanotime(); if (audio.playback.buffer) { if (playbackGetState() == STREAM_STATE_SETUP_DEVICE) { /* The backend may begin pulling either immediately or long after it was * activated. Only retain enough of the source packet to reach its next * expected delivery time; retaining the complete packet after part of * its interval has already elapsed turns backend startup delay into * persistent playback latency. Still cover the backend's immediate * startup pull if it is larger than the remaining packet interval. */ const int64_t packetPeriod = max(audio.playback.startupPacketPeriod, INT64_C(1)); const int64_t remainingNs = audio.playback.startupPacketDeadline > now ? audio.playback.startupPacketDeadline - now : packetPeriod - (now - audio.playback.startupPacketDeadline) % packetPeriod; const int remainingFrames = clamp( (remainingNs * audio.playback.sampleRate + INT64_C(999999999)) / INT64_C(1000000000), INT64_C(0), (int64_t)INT_MAX); const int targetFrames = min( (int64_t)audio.playback.startupLowWaterFrames + max(audio.playback.deviceStartFrames, remainingFrames), (int64_t)ringbuffer_getLength(audio.playback.buffer)); /* Align in both directions: insert silence if the backend started before * the target was available, or discard the oldest queued audio if it * started late. */ const int offset = ringbuffer_getCount(audio.playback.buffer) - targetFrames; if (offset > 0) { data->nextPosition += offset; ringbuffer_consume(audio.playback.buffer, NULL, offset); } else if (offset < 0) { /* Seeking the reader backwards exposes storage from a previous ring * wrap. Preserve the logical position but generate the missing startup * reserve explicitly as silence. */ data->nextPosition += offset; data->startupSilenceFrames = -offset; } StreamState expected = STREAM_STATE_SETUP_DEVICE; atomic_compare_exchange_strong_explicit( &audio.playback.state, &expected, STREAM_STATE_RUN, memory_order_acq_rel, memory_order_acquire); } /* Timestamp the dequeue boundary before the current pull. The logical * position tracks source frames consumed from the ring for latency * measurement. With backend resampling, outputPosition separately tracks * the equivalent number of device-rate frames. PipeWire computes the * current request using the rate set by the previous callback, so apply * that same ratio to this period before adopting nextRatio. */ playbackPublishDeviceTiming( frames, now, data->nextPosition, data->outputPosition); data->nextPosition += frames; data->outputPosition += frames * data->appliedRatio; data->appliedRatio = nextRatio; const int silenceFrames = min(frames, data->startupSilenceFrames); if (silenceFrames > 0) { memset(dst, 0, (size_t)silenceFrames * audio.playback.stride); data->startupSilenceFrames -= silenceFrames; } const int audioFrames = frames - silenceFrames; if (g_params.audioDebug && playbackGetState() == STREAM_STATE_RUN && ringbuffer_getCount(audio.playback.buffer) < audioFrames) atomic_fetch_add_explicit( &audio.playback.underruns, 1, memory_order_relaxed); ringbuffer_consume(audio.playback.buffer, dst + (size_t)silenceFrames * audio.playback.stride, audioFrames); } else frames = 0; // Close the stream if nothing has played for a while if (audio.playback.buffer && audio.playback.worker.wakeInitialized && audio.playback.worker.thread && playbackGetState() == STREAM_STATE_KEEP_ALIVE) { int stopTimeSec = 30; int stopTimeFrames = stopTimeSec * audio.playback.sampleRate; if (ringbuffer_getCount(audio.playback.buffer) <= -stopTimeFrames) { StreamState expected = STREAM_STATE_KEEP_ALIVE; if (atomic_compare_exchange_strong_explicit( &audio.playback.state, &expected, STREAM_STATE_STOP_PENDING, memory_order_acq_rel, memory_order_acquire)) { playbackDisableCallbacks(); playbackWorkerWake(); frames = 0; } } } playbackCallbackExit(); return frames; } static bool playbackSetupDevice(const LG_AudioFormat * format, int requestedPeriodFrames, bool requestResampler, bool * backendResampler) { audio.playback.deviceMaxPeriodFrames = 0; audio.playback.deviceStartFrames = 0; *backendResampler = false; return audio.audioDev->playback.setup(format, requestedPeriodFrames, requestResampler, backendResampler, &audio.playback.deviceMaxPeriodFrames, &audio.playback.deviceStartFrames, playbackPullFrames) && audio.playback.deviceMaxPeriodFrames > 0 && audio.playback.deviceStartFrames >= 0; } static bool playbackStart(const LG_AudioFormat * format, const LG_AudioClock * sourceClock, bool providerRateControl, bool forceSoftwareResampler) { if (!audio.audioDev) return false; if (!audioFormatValid(format)) { DEBUG_ERROR("Invalid playback format"); if (playbackGetState() != STREAM_STATE_STOP) playbackStop(); return false; } 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; } state = expected; } if (state != STREAM_STATE_STOP) playbackStop(); audio.playback.format = *format; audio.playback.lastFormat = *format; audio.playback.lastFormatValid = true; audio.playback.lastProviderRateControl = providerRateControl; audio.playback.channels = channels; audio.playback.sampleRate = sampleRate; audio.playback.sourceData.maxSourcePacketFrames = max( (sampleRate * PLAYBACK_MAX_SOURCE_PACKET_MS + 999) / 1000, 1); playbackSetState(STREAM_STATE_SETUP_SOURCE); audio.playback.deviceData.nextPosition = 0; audio.playback.deviceData.outputPosition = 0.0; audio.playback.deviceData.appliedRatio = 1.0; audio.playback.deviceData.startupSilenceFrames = 0; audio.playback.sourceData.inputPosition = 0; audio.playback.sourceData.outputPosition = 0; audio.playback.sourceData.devPeriodFrames = 0; audio.playback.sourceData.devReadPosition = 0; audio.playback.sourceData.deviceTimingSequence = 0; playbackDeviceClockAcquireReset(&audio.playback.sourceData); audio.playback.sourceData.offsetError = 0.0; audio.playback.sourceData.offsetErrorIntegral = 0.0; audio.playback.sourceData.ratioIntegral = 0.0; audio.playback.sourceData.lastRatio = 1.0; audio.playback.sourceData.lastClockRatio = 1.0; audio.playback.sourceData.nextFeedbackTime = 0; audio.playback.sourceData.nextGraphTime = 0; audio.playback.sourceData.bufferOverrunPending = false; audio.playback.sourceData.bufferOverruns = 0; audio.playback.sourceData.nextLogTime = nanotime() + INT64_C(5000000000); audio.playback.sourceData.arrivalJitterSec = 0.0; audio.playback.sourceData.sourcePhaseBaselineSec = 0.0; audio.playback.sourceData.sourcePhaseReserveSec = 0.0; audio.playback.sourceData.sourcePacketDurationSec = 0.0; audio.playback.sourceData.sourcePhaseBaselineValid = false; audio.playback.sourceData.deviceClock.valid = false; audio.playback.sourceData.outputClock.valid = false; playbackPrepareMediaClock(&audio.playback.sourceData, sourceClock); atomic_store_explicit( &audio.playback.deviceTiming.sequence, 0, memory_order_relaxed); atomic_store_explicit( &audio.playback.deviceTiming.periodFrames, 0, memory_order_relaxed); atomic_store_explicit( &audio.playback.deviceTiming.time, 0, memory_order_relaxed); atomic_store_explicit( &audio.playback.deviceTiming.position, 0, memory_order_relaxed); atomic_store_explicit( &audio.playback.deviceTiming.outputPosition, 0.0, memory_order_relaxed); atomic_store_explicit( &audio.playback.underruns, 0, memory_order_relaxed); atomic_store_explicit( &audio.playback.backendResampleRatio, 1.0, memory_order_relaxed); atomic_store_explicit( &audio.playback.backendResamplerFailed, false, memory_order_relaxed); const int requestedPeriodFrames = g_params.audioPeriodSize > 0 ? clamp(g_params.audioPeriodSize, 1, sampleRate) : max(sampleRate / 100, 1); audio.playback.targetStartFrames = 0; audio.playback.startupLowWaterFrames = 0; audio.playback.startupPacketDeadline = 0; audio.playback.startupPacketPeriod = 0; const bool requestBackendResampler = !providerRateControl && !forceSoftwareResampler && g_params.audioResampler != AUDIO_RESAMPLER_LIBSAMPLERATE; LG_AudioFormat deviceFormat = *format; /* The ring generates zero-filled silence. Keep unsigned PCM on the float * path because its silence level is biased rather than zero. */ if ((!requestBackendResampler && !providerRateControl) || deviceFormat.sampleFormat == LG_AUDIO_FMT_U8) deviceFormat.sampleFormat = LG_AUDIO_FMT_F32_NE; bool backendResampler; bool deviceConfigured = playbackSetupDevice( &deviceFormat, requestedPeriodFrames, requestBackendResampler, &backendResampler); /* Native samples require either provider feedback or backend rate control. * Otherwise reconnect using float samples for the libsamplerate path. This * also provides a float fallback for formats unsupported by the backend. */ if ((!deviceConfigured || (!providerRateControl && !backendResampler)) && deviceFormat.sampleFormat != LG_AUDIO_FMT_F32_NE) { deviceFormat.sampleFormat = LG_AUDIO_FMT_F32_NE; deviceConfigured = playbackSetupDevice( &deviceFormat, requestedPeriodFrames, requestBackendResampler, &backendResampler); } if (!deviceConfigured) { DEBUG_ERROR("Failed to configure audio playback device"); playbackStop(); return false; } audio.playback.stride = channels * audioSampleSize(deviceFormat.sampleFormat); audio.playback.convertToFloat = (!providerRateControl && !backendResampler) || deviceFormat.sampleFormat != format->sampleFormat; audio.playback.rateControl = providerRateControl ? PLAYBACK_RATE_PROVIDER : backendResampler ? PLAYBACK_RATE_BACKEND : PLAYBACK_RATE_SOFTWARE; const int conversionBufferFrames = max(requestedPeriodFrames, audio.playback.sourceData.maxSourcePacketFrames); if (!playbackEnsureConversionBuffers( &audio.playback.sourceData, conversionBufferFrames)) { playbackStop(); return false; } audio.playback.buffer = ringbuffer_newUnbounded( sampleRate, audio.playback.stride); if (!audio.playback.buffer) { playbackStop(); return false; } if (g_params.audioResampler == AUDIO_RESAMPLER_BACKEND && !providerRateControl && !backendResampler) DEBUG_WARN("%s could not activate backend resampling; " "using libsamplerate", audio.audioDev->name); if (audio.playback.rateControl == PLAYBACK_RATE_SOFTWARE) { int srcError; audio.playback.sourceData.src = src_new(SRC_SINC_FASTEST, channels, &srcError); if (!audio.playback.sourceData.src) { DEBUG_ERROR("Failed to create resampler: %s", src_strerror(srcError)); playbackStop(); return false; } } else audio.playback.sourceData.src = NULL; switch (audio.playback.rateControl) { case PLAYBACK_RATE_PROVIDER: DEBUG_INFO("Using audio rate control: provider feedback"); break; case PLAYBACK_RATE_BACKEND: DEBUG_INFO("Using audio resampler: %s", audio.audioDev->name); break; case PLAYBACK_RATE_SOFTWARE: DEBUG_INFO("Using audio resampler: libsamplerate"); break; } // Set up synchronization instrumentation only when explicitly requested. if (g_params.audioDebug) audio.playback.timings = ringbuffer_new(1200, sizeof(float)); atomic_store_explicit( &audio.playback.callbackState, 0, memory_order_release); return true; } static int64_t audioStartRetryDelay(unsigned int failures) { const unsigned int shift = min(failures, 6U); return min(AUDIO_START_RETRY_MIN_NS << shift, AUDIO_START_RETRY_MAX_NS); } /* sourceLock must be held. */ static bool playbackDelayStart(void) { const unsigned int failures = audio.playback.startFailures; audio.playback.nextStartRetry = nanotime() + audioStartRetryDelay(failures); if (audio.playback.startFailures < 7U) ++audio.playback.startFailures; return failures == 0U || failures == 6U; } /* sourceLock must be held. */ static void playbackScheduleStart(void) { if (!audio.playback.requestedGeneration || !audio.playback.requestedFormatValid || audio.playback.startPending || audio.playback.startInProgress || (atomic_load_explicit(&audio.playback.streamGeneration, memory_order_acquire) == audio.playback.requestedGeneration && playbackGetState() != STREAM_STATE_STOP) || nanotime() < audio.playback.nextStartRetry) return; audio.playback.startPending = true; playbackWorkerWake(); } static void playbackProcessControls(void) { uint64_t controlSerial; uint32_t generation; int volumeChannels; uint16_t volume[LG_AUDIO_MAX_CHANNELS]; bool mute; LG_LOCK(audio.playback.sourceLock); generation = atomic_load_explicit( &audio.playback.streamGeneration, memory_order_acquire); if (!audio.playback.controlsPending || !generation || generation != audio.playback.requestedGeneration) { LG_UNLOCK(audio.playback.sourceLock); return; } controlSerial = audio.playback.controlSerial; volumeChannels = audio.playback.volumeChannels; if (volumeChannels) memcpy(volume, audio.playback.volume, sizeof(*volume) * volumeChannels); mute = audio.playback.mute; LG_UNLOCK(audio.playback.sourceLock); bool applied = false; LG_LOCK(audio.playback.deviceLock); const StreamState state = playbackGetState(); if (audio.audioDev && state != STREAM_STATE_STOP && state != STREAM_STATE_STOP_PENDING && atomic_load_explicit(&audio.playback.streamGeneration, memory_order_acquire) == generation) { if (volumeChannels && audio.audioDev->playback.volume) audio.audioDev->playback.volume(volumeChannels, volume); if (audio.audioDev->playback.mute) audio.audioDev->playback.mute(mute); applied = true; } LG_UNLOCK(audio.playback.deviceLock); if (!applied) return; LG_LOCK(audio.playback.sourceLock); if (audio.playback.requestedGeneration == generation && atomic_load_explicit(&audio.playback.streamGeneration, memory_order_acquire) == generation && audio.playback.controlSerial == controlSerial) audio.playback.controlsPending = false; LG_UNLOCK(audio.playback.sourceLock); } static void playbackProcessStopPending(void) { bool pending = false; bool reportFailure = false; uint32_t failedAttempt = 0; uint64_t backendRequestSerial = 0; int64_t backendStartTime = 0; LG_LOCK(audio.playback.sourceLock); if (audio.audioDev && playbackGetState() == STREAM_STATE_STOP_PENDING) { failedAttempt = atomic_load_explicit( &audio.playback.failedAttemptSerial, memory_order_acquire); backendRequestSerial = audio.playback.backendRequestSerial; backendStartTime = audio.playback.backendStartTime; atomic_store_explicit( &audio.playback.streamGeneration, 0, memory_order_release); pending = true; } LG_UNLOCK(audio.playback.sourceLock); if (!pending) return; LG_LOCK(audio.playback.deviceLock); playbackStop(); LG_UNLOCK(audio.playback.deviceLock); LG_LOCK(audio.playback.sourceLock); if (failedAttempt && audio.playback.requestedGeneration && audio.playback.requestedFormatValid && backendRequestSerial == audio.playback.requestSerial) { if (backendStartTime && nanotime() - backendStartTime >= AUDIO_RETRY_RESET_NS) { audio.playback.startFailures = 0; audio.playback.nextStartRetry = 0; } audio.playback.startPending = false; reportFailure = playbackDelayStart(); } LG_UNLOCK(audio.playback.sourceLock); if (reportFailure) DEBUG_ERROR("Audio playback device failed; retrying"); } static void playbackProcessStart(void) { LG_AudioFormat format; uint64_t requestSerial; uint32_t generation; bool providerRateControl; bool forceSoftwareResampler; LG_LOCK(audio.playback.sourceLock); if (!audio.playback.startPending || audio.playback.startInProgress || !audio.playback.requestedGeneration || !audio.playback.requestedFormatValid || nanotime() < audio.playback.nextStartRetry) { LG_UNLOCK(audio.playback.sourceLock); return; } audio.playback.startPending = false; audio.playback.startInProgress = true; format = audio.playback.requestedFormat; requestSerial = audio.playback.requestSerial; generation = audio.playback.requestedGeneration; providerRateControl = audio.playback.requestedProviderRateControl; forceSoftwareResampler = audio.playback.forceSoftwareResampler; LG_UNLOCK(audio.playback.sourceLock); LG_LOCK(audio.playback.deviceLock); const bool started = playbackStart(&format, NULL, providerRateControl, forceSoftwareResampler); LG_UNLOCK(audio.playback.deviceLock); bool stopStale = false; bool wake = false; LG_LOCK(audio.playback.sourceLock); const bool current = requestSerial == audio.playback.requestSerial && generation == audio.playback.requestedGeneration && audio.playback.requestedFormatValid && audioFormatEqual(&format, &audio.playback.requestedFormat) && providerRateControl == audio.playback.requestedProviderRateControl && forceSoftwareResampler == audio.playback.forceSoftwareResampler; audio.playback.startInProgress = false; if (started && current) { audio.playback.startPending = false; LG_LOCK(audio.playback.deviceLock); if (playbackGetState() != STREAM_STATE_STOP_PENDING) { if (atomic_load_explicit(&audio.playback.activeAttemptSerial, memory_order_acquire)) audio.playback.backendRequestSerial = requestSerial; atomic_store_explicit(&audio.playback.streamGeneration, generation, memory_order_release); audio.playback.controlsPending = true; audio.playback.nextStartRetry = 0; } LG_UNLOCK(audio.playback.deviceLock); } else { atomic_store_explicit( &audio.playback.streamGeneration, 0, memory_order_release); if (started) stopStale = true; else if (current) playbackDelayStart(); } wake = audio.playback.startPending; LG_UNLOCK(audio.playback.sourceLock); if (stopStale) { LG_LOCK(audio.playback.deviceLock); playbackStop(); LG_UNLOCK(audio.playback.deviceLock); } if (wake) playbackWorkerWake(); } static void playbackProcessDeviceStart(void) { uint64_t requestSerial = 0; uint32_t generation = 0; uint32_t attemptSerial = 0; LG_LOCK(audio.playback.sourceLock); const StreamState state = playbackGetState(); attemptSerial = atomic_load_explicit( &audio.playback.activeAttemptSerial, memory_order_acquire); generation = atomic_load_explicit( &audio.playback.streamGeneration, memory_order_acquire); if (audio.audioDev && state == STREAM_STATE_SETUP_DEVICE && attemptSerial && !audio.playback.backendStartTime && generation && generation == audio.playback.requestedGeneration) requestSerial = audio.playback.requestSerial; else attemptSerial = 0; LG_UNLOCK(audio.playback.sourceLock); if (!attemptSerial) return; bool started = false; LG_LOCK(audio.playback.deviceLock); if (audio.audioDev && playbackGetState() == STREAM_STATE_SETUP_DEVICE && atomic_load_explicit(&audio.playback.activeAttemptSerial, memory_order_acquire) == attemptSerial && atomic_load_explicit(&audio.playback.streamGeneration, memory_order_acquire) == generation) started = audio.audioDev->playback.start( playbackBackendFailed, attemptSerial); LG_UNLOCK(audio.playback.deviceLock); LG_LOCK(audio.playback.sourceLock); const StreamState completedState = playbackGetState(); const bool attemptCurrent = attemptSerial == atomic_load_explicit( &audio.playback.activeAttemptSerial, memory_order_acquire); const bool current = started && attemptCurrent && requestSerial == audio.playback.requestSerial && requestSerial == audio.playback.backendRequestSerial && generation == audio.playback.requestedGeneration && generation == atomic_load_explicit( &audio.playback.streamGeneration, memory_order_acquire) && (completedState == STREAM_STATE_SETUP_DEVICE || completedState == STREAM_STATE_RUN); const bool keepAlive = started && attemptCurrent && completedState == STREAM_STATE_KEEP_ALIVE; if (current || keepAlive) audio.playback.backendStartTime = nanotime(); LG_UNLOCK(audio.playback.sourceLock); if (!current && !keepAlive) playbackBackendFailed(attemptSerial); } static void playbackProcessDiagnostics(void) { PlaybackDiagnostics diagnostics; RingBuffer timings; LG_LOCK(audio.playback.sourceLock); if (!audio.playback.diagnostics.pending) { LG_UNLOCK(audio.playback.sourceLock); return; } diagnostics = audio.playback.diagnostics; timings = audio.playback.timings; audio.playback.diagnostics.pending = 0; LG_UNLOCK(audio.playback.sourceLock); bool current = false; double backendLatencyMs = 0.0; LG_LOCK(audio.playback.deviceLock); const StreamState state = playbackGetState(); current = diagnostics.epoch == atomic_load_explicit( &audio.playback.diagnosticsEpoch, memory_order_acquire) && state != STREAM_STATE_STOP && state != STREAM_STATE_STOP_PENDING; if (current) { const bool timingsCurrent = timings && timings == audio.playback.timings; if ((diagnostics.pending & PLAYBACK_DIAGNOSTIC_REGISTER_GRAPH) && timingsCurrent && !audio.playback.graph && !audio.playback.graphRegistrationAttempted) { audio.playback.graphRegistrationAttempted = true; audio.playback.graph = app_registerGraph("PLAYBACK RING", timings, 0.0f, diagnostics.graphMax, audioGraphFormatFn); atomic_store_explicit(&audio.playback.graphReady, audio.playback.graph != NULL, memory_order_release); } if ((diagnostics.pending & PLAYBACK_DIAGNOSTIC_INVALIDATE) && timingsCurrent && audio.playback.graph) app_invalidateGraph(audio.playback.graph); if ((diagnostics.pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG) && audio.audioDev && audio.audioDev->playback.latency) backendLatencyMs = audio.audioDev->playback.latency() / 1000.0; } LG_UNLOCK(audio.playback.deviceLock); if (!current) return; if (diagnostics.pending & PLAYBACK_DIAGNOSTIC_START_LOG) DEBUG_INFO( "Audio start: %.2f/%.2f ms target/queued", diagnostics.start.targetLatencyMs, diagnostics.start.queuedLatencyMs); if (diagnostics.pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG) { const bool providerControl = diagnostics.sync.rateControl == PLAYBACK_RATE_PROVIDER; const char * controlName = providerControl ? "feedback" : diagnostics.sync.rateControl == PLAYBACK_RATE_BACKEND ? "backend" : "software"; const char * sourceRateName = providerControl ? "arrival" : "source"; DEBUG_INFO( "Audio sync: ring %.2f/%.2f ms, backend %.2f ms, " "%s %+.1f ppm, rates %s/device %+.1f/%+.1f ppm, " "jitter %.2f ms, xruns %u/%u", diagnostics.sync.softwareLatencyMs, diagnostics.sync.targetLatencyMs, backendLatencyMs, controlName, diagnostics.sync.controlPpm, sourceRateName, diagnostics.sync.sourcePpm, diagnostics.sync.devicePpm, diagnostics.sync.jitterMs, diagnostics.sync.underruns, diagnostics.sync.overruns); } } static void playbackSourceStop(void) { if (!audio.audioDev) return; StreamState state = playbackGetState(); switch (state) { case STREAM_STATE_RUN: case STREAM_STATE_RESUMING: { // Keep the audio device open for a while to reduce startup latency if // playback starts again if (!atomic_compare_exchange_strong_explicit( &audio.playback.state, &state, STREAM_STATE_KEEP_ALIVE, memory_order_acq_rel, memory_order_acquire)) break; // Reset the software resampler so it is safe for the next playback if (audio.playback.sourceData.src) { int error = src_reset(audio.playback.sourceData.src); if (error) { DEBUG_ERROR("Failed to reset resampler: %s", src_strerror(error)); playbackStop(); } } break; } case STREAM_STATE_SETUP_SOURCE: case STREAM_STATE_SETUP_DEVICE: case STREAM_STATE_STOP_PENDING: // Playback hasn't actually started yet so just clean up playbackStop(); break; case STREAM_STATE_KEEP_ALIVE: case STREAM_STATE_STOP: // Nothing to do break; } } static int playbackStoreVolume(int channels, const uint16_t volume[]) { if (!audio.audioDev || !audio.audioDev->playback.volume || !g_params.audioSyncVolume) return 0; // store the values so we can restore the state if the stream is restarted channels = min(ARRAY_LENGTH(audio.playback.volume), channels); memcpy(audio.playback.volume, volume, sizeof(uint16_t) * channels); audio.playback.volumeChannels = channels; return channels; } static bool playbackStoreMute(bool mute) { if (!audio.audioDev || !audio.audioDev->playback.mute) return false; // store the value so we can restore it if the stream is restarted audio.playback.mute = mute; return true; } static double computeDevicePosition(int64_t curTime) { const PlaybackSourceData * sourceData = &audio.playback.sourceData; return playbackClockPosition( &sourceData->deviceClock, curTime) + sourceData->devicePositionOffsetFrames; } static double playbackProviderRate(const PlaybackSourceData * sourceData) { const double nominalRate = audio.playback.sampleRate; if (!sourceData->deviceClock.valid || sourceData->deviceClock.frameSec <= 0.0) return nominalRate; return clamp( sourceData->lastRatio / sourceData->deviceClock.frameSec, nominalRate * (1.0 - PLAYBACK_MAX_RATE_CORRECTION), nominalRate * (1.0 + PLAYBACK_MAX_RATE_CORRECTION)); } static bool playbackEnsureConversionBuffers( PlaybackSourceData * sourceData, int frames) { if (audio.playback.convertToFloat && frames > sourceData->framesInSize) { const int capacity = max(frames, max(sourceData->framesInSize * 2, 64)); float * framesIn = realloc(sourceData->framesIn, (size_t)capacity * audio.playback.channels * sizeof(float)); if (!framesIn) { DEBUG_ERROR("Failed to grow playback input buffer"); return false; } sourceData->framesIn = framesIn; sourceData->framesInSize = capacity; } if (audio.playback.rateControl == PLAYBACK_RATE_SOFTWARE) { const int framesOut = (int)ceil(frames * (1.0 + PLAYBACK_MAX_RATE_CORRECTION)) + 64; if (framesOut > sourceData->framesOutSize) { const int capacity = max(framesOut, max(sourceData->framesOutSize * 2, 64)); float * output = realloc(sourceData->framesOut, (size_t)capacity * audio.playback.channels * sizeof(float)); if (!output) { DEBUG_ERROR("Failed to grow playback output buffer"); return false; } sourceData->framesOut = output; sourceData->framesOutSize = capacity; } } return true; } static int playbackAppendFrames( PlaybackSourceData * sourceData, const void * frames, int count) { const int occupancy = ringbuffer_getCount(audio.playback.buffer); const int length = ringbuffer_getLength(audio.playback.buffer); const int64_t available = (int64_t)length - occupancy; const int append = clamp( (int64_t)count, INT64_C(0), max(INT64_C(0), available)); const int advanced = ringbuffer_append(audio.playback.buffer, frames, append); DEBUG_ASSERT(advanced == append); if (append != count) { /* Never allow the logical writer to get beyond the physical storage. * Doing so makes a positive buffer count refer to overwritten samples and * sounds like corrupted PCM rather than an underrun. Resynchronize on the * next packet after dropping the excess output. */ sourceData->bufferOverrunPending = true; ++sourceData->bufferOverruns; } return advanced; } static int playbackSlewBuffer( PlaybackSourceData * sourceData, int requested) { const int occupancy = ringbuffer_getCount(audio.playback.buffer); const int length = ringbuffer_getLength(audio.playback.buffer); const int64_t minimum = -max(occupancy, 0); const int64_t maximum = (int64_t)length - occupancy; const int slew = clamp((int64_t)requested, minimum, maximum); const int advanced = ringbuffer_append(audio.playback.buffer, NULL, slew); DEBUG_ASSERT(advanced == slew); if (slew != requested) sourceData->bufferOverrunPending = true; return advanced; } static PlaybackDataResult playbackData(const void * data, size_t frameCount, const LG_AudioClock * sourceClock, int64_t arrivalTime) { StreamState state = playbackGetState(); if (state == STREAM_STATE_STOP_PENDING) return PLAYBACK_DATA_DROP; if (frameCount == 0) return PLAYBACK_DATA_DROP; if (state == STREAM_STATE_STOP || !audio.audioDev) return PLAYBACK_DATA_RETRY_NOW; PlaybackSourceData * sourceData = &audio.playback.sourceData; if (!data || frameCount > (size_t)sourceData->maxSourcePacketFrames) { DEBUG_ERROR("Invalid playback packet length: %zu frames", frameCount); return PLAYBACK_DATA_DROP; } const int frames = frameCount; if (audio.playback.rateControl == PLAYBACK_RATE_BACKEND && atomic_exchange_explicit( &audio.playback.backendResamplerFailed, false, memory_order_acq_rel)) { DEBUG_WARN("Audio backend resampler failed; using libsamplerate"); audio.playback.forceSoftwareResampler = true; playbackQueueSourceStop(); return PLAYBACK_DATA_RETRY_NOW; } /* Backend resampling changes how many source frames PipeWire requests per * device period. Use the command-normalized output clock for rate matching, * while deviceClock remains in the ring's source-frame domain for latency. */ const PlaybackClock * rateClock = audio.playback.rateControl == PLAYBACK_RATE_BACKEND ? &sourceData->outputClock : &sourceData->deviceClock; const int64_t now = nanotime(); if (audio.playback.startFailures && audio.playback.backendStartTime && now - audio.playback.backendStartTime >= AUDIO_RETRY_RESET_NS) { audio.playback.startFailures = 0; audio.playback.nextStartRetry = 0; } const double nominalFrameSec = 1.0 / audio.playback.sampleRate; const void * inputFrames = data; if (audio.playback.convertToFloat) { if (!audioConvertToFloat(sourceData->framesIn, data, (size_t)frames * audio.playback.channels, audio.playback.format.sampleFormat)) { DEBUG_ERROR("Failed to convert playback samples"); playbackQueueSourceStop(); return PLAYBACK_DATA_RETRY; } inputFrames = sourceData->framesIn; } const bool providerRateControl = audio.playback.rateControl == PLAYBACK_RATE_PROVIDER; /* An unbounded ring represents an underrun by advancing the reader beyond * the writer. Do not carry that logical debt into resumed playback: bounded * rate correction would otherwise discard fresh audio for many seconds. */ const bool bufferUnderrun = STREAM_ACTIVE(playbackGetState()) && ringbuffer_getCount(audio.playback.buffer) < 0; bool discontinuity = bufferUnderrun || (sourceClock && sourceClock->discontinuity); const int64_t packetTime = playbackMapMediaTime(sourceData, sourceClock, frames, audio.playback.sampleRate, arrivalTime, &discontinuity); if (sourceData->bufferOverrunPending) { discontinuity = true; sourceData->bufferOverrunPending = false; } sourceData->lastArrivalTime = arrivalTime; const bool sourceRateWasValid = sourceData->sourceRateValid; const int64_t sourceRateTimeMs = providerRateControl ? (arrivalTime - sourceData->mediaLocalOrigin) / INT64_C(1000000) : sourceData->mediaTimeMs; playbackSourceRateAdd( sourceData, sourceRateTimeMs, nominalFrameSec); if (sourceClock && sourceClock->stable && sourceClock->rate > 0.0) { const double frameSec = 1.0 / sourceClock->rate; if (frameSec >= nominalFrameSec * (1.0 - PLAYBACK_MAX_RATE_CORRECTION) && frameSec <= nominalFrameSec * (1.0 + PLAYBACK_MAX_RATE_CORRECTION)) { if (!sourceData->sourceRateValid || sourceData->rateFilterTimeMs == INT64_MIN) sourceData->rateFilterTimeMs = sourceRateTimeMs; sourceData->sourceRateFrameSec = frameSec; sourceData->sourceRateValid = true; } } const bool sourceRateBecameValid = !sourceRateWasValid && sourceData->sourceRateValid; if (!playbackSourceClockUpdate(&sourceData->sourceClock, packetTime, sourceData->inputPosition, nominalFrameSec)) discontinuity = true; if (sourceData->sourceRateValid && !providerRateControl) sourceData->sourceClock.frameSec = sourceData->sourceRateFrameSec; /* Track phase variation around its local baseline, not its absolute value. * The absolute phase depends on the arbitrary local origin assigned to the * source media clock and must not become buffer reserve. Positive * deviation means the latency model temporarily overstates how much audio * remains in the ring. */ const double sourcePhaseSec = sourceData->sourceClock.phaseResidualSec; const double packetSec = frames * nominalFrameSec; sourceData->sourcePacketDurationSec = max(packetSec, sourceData->sourcePacketDurationSec * exp(-packetSec / PLAYBACK_PHASE_RESERVE_DECAY_SEC)); if (!sourceData->sourcePhaseBaselineValid || sourceData->sourceClock.updates == 1) { sourceData->sourcePhaseBaselineSec = sourcePhaseSec; sourceData->sourcePhaseBaselineValid = true; } else { const double alpha = -expm1(-packetSec / PLAYBACK_PHASE_BASELINE_TIME_SEC); sourceData->sourcePhaseBaselineSec += alpha * (sourcePhaseSec - sourceData->sourcePhaseBaselineSec); } const double sourcePhaseDeviationSec = max(0.0, sourcePhaseSec - sourceData->sourcePhaseBaselineSec); sourceData->sourcePhaseReserveSec = min(PLAYBACK_MAX_JITTER_SEC, max(sourcePhaseDeviationSec, sourceData->sourcePhaseReserveSec * exp(-packetSec / PLAYBACK_PHASE_RESERVE_DECAY_SEC))); int64_t curTime = sourceData->sourceClock.time; int64_t curPosition = sourceData->outputPosition; const double sourceReserveFrames = max(sourceData->sourcePacketDurationSec * 0.5, sourceData->sourcePhaseReserveSec) * audio.playback.sampleRate; // Receive the newest timing information from the audio device thread. PlaybackDeviceTick deviceTick; unsigned int deviceSequence; bool deviceClockBecameStable = false; if (playbackReadDeviceTiming(sourceData->deviceTimingSequence, &deviceTick, &deviceSequence)) { sourceData->deviceTimingSequence = deviceSequence; sourceData->devPeriodFrames = deviceTick.periodFrames; sourceData->devReadPosition = deviceTick.nextPosition + deviceTick.periodFrames; const bool deviceClockUpdated = playbackClockUpdate(&sourceData->deviceClock, deviceTick.nextTime, deviceTick.nextPosition, nominalFrameSec); const bool outputClockUpdated = audio.playback.rateControl != PLAYBACK_RATE_BACKEND || playbackClockUpdate(&sourceData->outputClock, deviceTick.nextTime, deviceTick.outputPosition, nominalFrameSec); if (!deviceClockUpdated || !outputClockUpdated) { playbackDeviceClockAcquireReset(sourceData); discontinuity = true; } else deviceClockBecameStable = playbackDeviceClockAcquire(sourceData, deviceTick.nextTime); } if (deviceClockBecameStable) { /* Give the fitted device timeline the same latency reported by the * acquisition model. Their position origins are otherwise unrelated, so * switching models would create a false phase step and drive the resampler * despite an already-correct ring level. Keep the source clock untouched: * changing it would also disturb source phase and jitter tracking. */ const int64_t referenceTime = providerRateControl ? now : curTime; const double rawDevicePosition = playbackClockPosition(&sourceData->deviceClock, referenceTime); sourceData->devicePositionOffsetFrames = sourceData->devReadPosition - rawDevicePosition - (providerRateControl ? 0.0 : sourceReserveFrames); } if (discontinuity && sourceData->src) { const int error = src_reset(sourceData->src); if (error) { DEBUG_ERROR("Failed to reset resampler: %s", src_strerror(error)); playbackQueueSourceStop(); return PLAYBACK_DATA_RETRY; } } const int maxPeriodFrames = max(audio.playback.deviceMaxPeriodFrames, sourceData->devPeriodFrames); /* The device period, delivery jitter, packet phase, and resampler delay * define the minimum viable latency. Provider feedback directly controls * the source rate, so latencyOffset only applies to local rate control. */ const double latencyOffsetFrames = providerRateControl ? 0.0 : max(g_params.audioLatencyOffset, 0) * audio.playback.sampleRate / 1000.0; const double arrivalReserveFrames = (sourceData->arrivalJitterSec + 0.001) * audio.playback.sampleRate; const double minimumLowWaterReserveFrames = maxPeriodFrames * 0.1 + arrivalReserveFrames; const double minimumLowWaterFrames = maxPeriodFrames + minimumLowWaterReserveFrames; const double targetLowWaterFrames = minimumLowWaterFrames + latencyOffsetFrames; const double minimumBufferFrames = minimumLowWaterFrames + sourceReserveFrames; const double targetBufferFrames = minimumBufferFrames + latencyOffsetFrames; const double resamplerDelayFrames = audio.playback.rateControl == PLAYBACK_RATE_SOFTWARE ? PLAYBACK_RESAMPLER_DELAY_FRAMES : 0.0; const double minimumLatencyFrames = minimumBufferFrames + resamplerDelayFrames; const double targetLatencyFrames = minimumLatencyFrames + latencyOffsetFrames; double devPosition = DBL_MIN; state = playbackGetState(); if ((providerRateControl || bufferUnderrun) && (discontinuity || state == STREAM_STATE_KEEP_ALIVE || state == STREAM_STATE_RESUMING)) { const int occupancy = ringbuffer_getCount(audio.playback.buffer); const int slewFrames = clamp( llrint(targetLowWaterFrames - occupancy), (int64_t)INT_MIN, (int64_t)INT_MAX); const int actualSlew = playbackSlewBuffer(sourceData, slewFrames); sourceData->outputPosition += actualSlew; curPosition += actualSlew; sourceData->offsetError = 0.0; sourceData->offsetErrorIntegral = 0.0; sourceData->ratioIntegral = 0.0; if (providerRateControl && bufferUnderrun) { sourceData->lastRatio = 1.0; sourceData->nextFeedbackTime = 0; } if (state == STREAM_STATE_KEEP_ALIVE || state == STREAM_STATE_RESUMING) atomic_compare_exchange_strong_explicit( &audio.playback.state, &state, STREAM_STATE_RUN, memory_order_acq_rel, memory_order_acquire); } else if ((discontinuity || state == STREAM_STATE_KEEP_ALIVE || state == STREAM_STATE_RESUMING) && sourceData->deviceClock.valid && sourceData->deviceClockStable) { devPosition = computeDevicePosition(curTime); const double slew = devPosition + targetBufferFrames - curPosition; const int slewFrames = clamp(llrint(slew), (int64_t)INT_MIN, (int64_t)INT_MAX); const int actualSlew = playbackSlewBuffer(sourceData, slewFrames); sourceData->outputPosition += actualSlew; curPosition += actualSlew; sourceData->offsetError = 0.0; sourceData->offsetErrorIntegral = 0.0; sourceData->ratioIntegral = 0.0; if (state == STREAM_STATE_KEEP_ALIVE || state == STREAM_STATE_RESUMING) atomic_compare_exchange_strong_explicit( &audio.playback.state, &state, STREAM_STATE_RUN, memory_order_acq_rel, memory_order_acquire); } double actualLatencyFrames = 0.0; double actualOffsetError = 0.0; if (providerRateControl) { const int occupancy = ringbuffer_getCount(audio.playback.buffer); actualLatencyFrames = occupancy + sourceReserveFrames; actualOffsetError = targetLowWaterFrames - occupancy; const double error = actualOffsetError - sourceData->offsetError; const double periodSec = frames * nominalFrameSec; const double omega = 2.0 * M_PI * PLAYBACK_OFFSET_FILTER_BANDWIDTH_HZ * periodSec; const double b = M_SQRT2 * omega; const double c = omega * omega; sourceData->offsetError += b * error + sourceData->offsetErrorIntegral; sourceData->offsetErrorIntegral += c * error; } else if (sourceData->deviceClock.valid) { if (sourceData->deviceClockStable) { if (devPosition == DBL_MIN) devPosition = computeDevicePosition(curTime); actualLatencyFrames = curPosition - devPosition + resamplerDelayFrames; actualOffsetError = targetLatencyFrames - actualLatencyFrames; } else { actualLatencyFrames = curPosition - sourceData->devReadPosition + sourceReserveFrames + resamplerDelayFrames; actualOffsetError = targetLatencyFrames - actualLatencyFrames; } const double error = actualOffsetError - sourceData->offsetError; const double periodSec = frames * nominalFrameSec; const double omega = 2.0 * M_PI * PLAYBACK_OFFSET_FILTER_BANDWIDTH_HZ * periodSec; const double b = M_SQRT2 * omega; const double c = omega * omega; sourceData->offsetError += b * error + sourceData->offsetErrorIntegral; sourceData->offsetErrorIntegral += c * error; } /* Feed forward the measured source/device rate ratio, then use a slow, * bounded phase controller to keep the ring at its target. While the device * clock is acquiring, its rate estimate is not trustworthy, but the logical * producer/consumer latency above is. Use that with a faster, one-sided * controller which can restore missing reserve without draining an initial * surplus. The stable controller is critically damped so latency approaches * the target without a designed-in overshoot. * * Before the long-term source estimate is available, the phase integral * necessarily contains the clock-rate error. Discard that provisional * integral when measured feed-forward first takes over, then allow later * filtered clock updates to change the requested ratio directly. Hiding * those updates in the integral preserves a stale correction and steadily * moves an already-correct buffer away from its target. */ const double naturalFrequency = 2.0 * M_PI * PLAYBACK_PHASE_BANDWIDTH_HZ; const double kp = 2.0 * naturalFrequency / audio.playback.sampleRate; const double ki = naturalFrequency * naturalFrequency / audio.playback.sampleRate; if (sourceRateBecameValid && !providerRateControl) sourceData->ratioIntegral = 0.0; if (!providerRateControl && sourceData->deviceClockStable && sourceData->sourceRateValid && rateClock->updates >= 2) { const double clockRatio = clamp( sourceData->sourceRateFrameSec / rateClock->frameSec, 1.0 - PLAYBACK_MAX_RATE_CORRECTION, 1.0 + PLAYBACK_MAX_RATE_CORRECTION); sourceData->lastClockRatio = clockRatio; } const double periodSec = frames * nominalFrameSec; /* source timestamps have millisecond resolution. Do not resample in * response to phase error that cannot be distinguished from quantization; * subtracting the deadband outside it keeps the response continuous. */ const double phaseDeadbandFrames = PLAYBACK_PHASE_DEADBAND_SEC * audio.playback.sampleRate; const double rawPhaseError = sourceData->offsetError; double phaseError = rawPhaseError; if (fabs(phaseError) <= phaseDeadbandFrames) phaseError = 0.0; else phaseError -= copysign(phaseDeadbandFrames, phaseError); const bool acquiringDeviceClock = sourceData->deviceClock.valid && !sourceData->deviceClockStable; double controllerKp = kp; double controllerKi = ki; double controllerError = phaseError; double controllerBase = providerRateControl ? 1.0 : sourceData->lastClockRatio; if (acquiringDeviceClock) { const double acquireFrequency = 2.0 * M_PI * PLAYBACK_ACQUIRE_PHASE_BANDWIDTH_HZ; controllerKp = 2.0 * acquireFrequency / audio.playback.sampleRate; controllerBase = 1.0; sourceData->ratioIntegral = 0.0; if (actualOffsetError <= 0.0) controllerError = 0.0; else controllerError = max(phaseError, 0.0); } else if (deviceClockBecameStable) { /* Acquisition correction is transient, not a clock-rate estimate. Start * the stable integral clean; the output-rate slew keeps the applied ratio * continuous across this transition. */ sourceData->ratioIntegral = 0.0; } /* Use the unfiltered latency error here so filter lag cannot retain a phase * correction after the target has already been crossed. */ else if (sourceData->ratioIntegral * actualOffsetError <= 0.0) sourceData->ratioIntegral = 0.0; const double candidateIntegral = acquiringDeviceClock ? 0.0 : sourceData->ratioIntegral + (deviceClockBecameStable ? 0.0 : controllerError * periodSec); const double phaseCorrection = controllerKp * controllerError + (acquiringDeviceClock ? 0.0 : controllerKi * candidateIntegral); const double desiredRatio = controllerBase + phaseCorrection; const double boundedRatio = clamp(desiredRatio, acquiringDeviceClock ? 1.0 : 1.0 - PLAYBACK_MAX_RATE_CORRECTION, 1.0 + PLAYBACK_MAX_RATE_CORRECTION); if (!acquiringDeviceClock && sourceData->deviceClockStable && (desiredRatio == boundedRatio || (desiredRatio > boundedRatio && controllerError < 0.0) || (desiredRatio < boundedRatio && controllerError > 0.0))) sourceData->ratioIntegral = candidateIntegral; const double maxRatioStep = PLAYBACK_MAX_RATE_SLEW_PER_SEC * periodSec; const double ratio = clamp(boundedRatio, sourceData->lastRatio - maxRatioStep, sourceData->lastRatio + maxRatioStep); sourceData->lastRatio = ratio; if (audio.playback.rateControl == PLAYBACK_RATE_BACKEND) { atomic_store_explicit( &audio.playback.backendResampleRatio, ratio, memory_order_release); const int outputFrames = playbackAppendFrames(sourceData, inputFrames, frames); sourceData->outputPosition += outputFrames; } else if (audio.playback.rateControl == PLAYBACK_RATE_PROVIDER) { const int outputFrames = playbackAppendFrames(sourceData, inputFrames, frames); sourceData->outputPosition += outputFrames; } else { int consumed = 0; while (consumed < frames) { SRC_DATA srcData = { .data_in = sourceData->framesIn + consumed * audio.playback.channels, .data_out = sourceData->framesOut, .input_frames = frames - consumed, .output_frames = sourceData->framesOutSize, .input_frames_used = 0, .output_frames_gen = 0, .end_of_input = 0, .src_ratio = ratio }; int error = src_process(sourceData->src, &srcData); if (error) { DEBUG_ERROR("Resampling failed: %s", src_strerror(error)); playbackQueueSourceStop(); return PLAYBACK_DATA_RETRY; } if (srcData.input_frames_used == 0 && srcData.output_frames_gen == 0) { DEBUG_ERROR("Resampler made no progress"); playbackQueueSourceStop(); return PLAYBACK_DATA_RETRY; } const int outputFrames = playbackAppendFrames( sourceData, sourceData->framesOut, srcData.output_frames_gen); consumed += srcData.input_frames_used; sourceData->outputPosition += outputFrames; } } sourceData->inputPosition += frames; if (playbackGetState() == STREAM_STATE_SETUP_SOURCE) { /* At a packet boundary, targetLowWaterFrames is the physical ring target; * sourceReserveFrames accounts for the packet's average delivery phase * and must not be prefetched a second time. Cover whichever is larger: * the backend's immediate startup pull or the interval until the next * source packet. This starts at the requested average latency without * risking an underrun before that packet arrives. */ const int bufferLength = ringbuffer_getLength(audio.playback.buffer); const int startupLowWaterFrames = clamp( llrint(ceil(targetLowWaterFrames)), INT64_C(0), (int64_t)bufferLength); audio.playback.targetStartFrames = min( (int64_t)startupLowWaterFrames + max(audio.playback.deviceStartFrames, frames), (int64_t)bufferLength); if (ringbuffer_getCount(audio.playback.buffer) >= audio.playback.targetStartFrames) { if (g_params.audioDebug) { PlaybackDiagnostics * diagnostics = playbackDiagnosticsLocked(); if (audio.playback.timings) { diagnostics->graphMax = targetLatencyFrames * 1000.0 / audio.playback.sampleRate * 2; diagnostics->pending |= PLAYBACK_DIAGNOSTIC_REGISTER_GRAPH; } diagnostics->start.targetLatencyMs = targetLatencyFrames * 1000.0 / audio.playback.sampleRate; diagnostics->start.queuedLatencyMs = audio.playback.targetStartFrames * 1000.0 / audio.playback.sampleRate; diagnostics->pending |= PLAYBACK_DIAGNOSTIC_START_LOG; } audio.playback.startupLowWaterFrames = startupLowWaterFrames; audio.playback.startupPacketPeriod = max(llrint(packetSec * 1.0e9), INT64_C(1)); audio.playback.startupPacketDeadline = arrivalTime + audio.playback.startupPacketPeriod; playbackSetState(STREAM_STATE_SETUP_DEVICE); playbackArmBackend(); playbackWorkerWake(); } } if (!g_params.audioDebug) return PLAYBACK_DATA_PROCESSED; const double softwareLatencyMs = actualLatencyFrames * 1000.0 / audio.playback.sampleRate; bool wakeDiagnostics = false; if (audio.playback.timings && atomic_load_explicit( &audio.playback.graphReady, memory_order_acquire) && now >= sourceData->nextGraphTime) { sourceData->nextGraphTime = now + PLAYBACK_GRAPH_INTERVAL_NS; const float latency = softwareLatencyMs; ringbuffer_push(audio.playback.timings, &latency); playbackDiagnosticsLocked()->pending |= PLAYBACK_DIAGNOSTIC_INVALIDATE; wakeDiagnostics = true; } if (now >= sourceData->nextLogTime) { const double sourcePpm = sourceData->sourceRateValid ? (nominalFrameSec / sourceData->sourceRateFrameSec - 1.0) * 1.0e6 : 0.0; const double devicePpm = rateClock->valid ? (nominalFrameSec / rateClock->frameSec - 1.0) * 1.0e6 : 0.0; const bool providerControl = audio.playback.rateControl == PLAYBACK_RATE_PROVIDER; 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( &audio.playback.underruns, 0, memory_order_relaxed); PlaybackDiagnostics * diagnostics = playbackDiagnosticsLocked(); const unsigned int pendingUnderruns = diagnostics->pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG ? diagnostics->sync.underruns : 0; const unsigned int pendingOverruns = diagnostics->pending & PLAYBACK_DIAGNOSTIC_SYNC_LOG ? diagnostics->sync.overruns : 0; diagnostics->sync = (PlaybackSyncDiagnostics) { .softwareLatencyMs = softwareLatencyMs, .targetLatencyMs = targetLatencyFrames * 1000.0 / audio.playback.sampleRate, .sourcePpm = sourcePpm, .devicePpm = devicePpm, .controlPpm = controlPpm, .jitterMs = sourceData->arrivalJitterSec * 1000.0, .underruns = pendingUnderruns + underruns, .overruns = pendingOverruns + sourceData->bufferOverruns, .rateControl = audio.playback.rateControl, }; diagnostics->pending |= PLAYBACK_DIAGNOSTIC_SYNC_LOG; sourceData->bufferOverruns = 0; sourceData->nextLogTime = now + INT64_C(5000000000); wakeDiagnostics = true; } if (wakeDiagnostics) playbackWorkerWake(); return PLAYBACK_DATA_PROCESSED; } static bool playbackGetFeedback( LG_AudioClock * clock, double * targetRate) { PlaybackSourceData * sourceData = &audio.playback.sourceData; if (!clock || !targetRate || audio.playback.rateControl != PLAYBACK_RATE_PROVIDER || !sourceData->deviceClock.valid || sourceData->deviceClock.frameSec <= 0.0) return false; const int64_t now = nanotime(); if (now < sourceData->nextFeedbackTime) return false; sourceData->nextFeedbackTime = now + PLAYBACK_FEEDBACK_INTERVAL_NS; const double position = playbackClockPosition(&sourceData->deviceClock, now); if (position < 0.0) return false; const uint64_t latency = audio.audioDev->playback.latency ? audio.audioDev->playback.latency() : 0; *clock = (LG_AudioClock) { .position = llrint(position), .time = now + latency * 1000, .rate = 1.0 / sourceData->deviceClock.frameSec, .stable = sourceData->deviceClockStable, }; *targetRate = playbackProviderRate(sourceData); return true; } static bool audioBindingEqual( const AudioBinding * a, const AudioBinding * b) { return a->ops == b->ops && a->opaque == b->opaque && a->epoch == b->epoch && a->generation == b->generation; } static void recordWorkerWake(void) { LG_LOCK_SHARED(audio.record.lock); if (audio.record.wake && audio.record.thread) lgSignalEvent(audio.record.wake); LG_UNLOCK_SHARED(audio.record.lock); } static void recordDisarmLocked(void) { atomic_store_explicit( &audio.record.deliverySerial, 0, memory_order_release); } static void recordBackendFailed(uint32_t attemptSerial) { unsigned int expected = attemptSerial; if (!attemptSerial || !atomic_compare_exchange_strong_explicit( &audio.record.deliverySerial, &expected, 0, memory_order_acq_rel, memory_order_acquire)) return; atomic_store_explicit(&audio.record.failedAttemptSerial, attemptSerial, memory_order_release); lgSignalEvent(audio.record.wake); } static void recordAdvanceRequestLocked(void) { if (!++audio.record.requestSerial) ++audio.record.requestSerial; } static void recordResetStartRetryLocked(void) { audio.record.startFailures = 0; audio.record.nextStartRetry = 0; } /* record.lock must be held exclusively. */ static bool recordDelayStartLocked(void) { const unsigned int failures = audio.record.startFailures; audio.record.nextStartRetry = nanotime() + audioStartRetryDelay(failures); if (audio.record.startFailures < 7U) ++audio.record.startFailures; return failures == 0U || failures == 6U; } /* record.lock must be held exclusively. */ static bool recordDelayRequestLocked( uint64_t requestSerial, bool resetRetry) { if (audio.record.shuttingDown || !audio.record.requested || !audio.record.enabled || !audio.record.requestedGeneration || requestSerial != audio.record.requestSerial) return false; if (resetRetry) recordResetStartRetryLocked(); return recordDelayStartLocked(); } static unsigned int recordWorkerWaitTimeout(void) { unsigned int timeout = TIMEOUT_INFINITE; LG_LOCK_SHARED(audio.record.lock); if (!audio.record.shuttingDown && audio.record.requested && audio.record.enabled && audio.record.requestedGeneration && audio.record.nextStartRetry) { const int64_t remaining = audio.record.nextStartRetry - nanotime(); timeout = remaining <= 0 ? 0 : (unsigned int)((remaining + INT64_C(999999)) / INT64_C(1000000)); } LG_UNLOCK_SHARED(audio.record.lock); return timeout; } bool lgAudio_supportsRecord(void) { return atomic_load_explicit(&audio.ready, memory_order_acquire) && audio.audioDev && audio.audioDev->record.start && audio.audioDev->record.stop && audio.record.wake && audio.record.thread && atomic_load_explicit( &audio.record.workerAlive, memory_order_acquire); } static bool recordPushFrames(uint8_t * data, int frames, const LG_AudioClock * sourceClock) { if (frames <= 0) return true; const uint32_t serial = atomic_load_explicit( &audio.record.deliverySerial, memory_order_acquire); if (!serial) return true; AudioBinding binding; uint32_t generation; LG_LOCK_SHARED(audio.record.lock); if (serial != atomic_load_explicit( &audio.record.deliverySerial, memory_order_acquire)) { LG_UNLOCK_SHARED(audio.record.lock); return true; } binding = audio.record.deliveryBinding; generation = audio.record.deliveryGeneration; LG_UNLOCK_SHARED(audio.record.lock); AudioBinding active; if (!eventBegin(&binding, &active)) return true; bool accepted = true; if (serial == atomic_load_explicit( &audio.record.deliverySerial, memory_order_acquire) && active.ops->recordData) accepted = active.ops->recordData(active.opaque, generation, data, frames, sourceClock); eventEnd(); return accepted; } static MsgBoxHandle recordCancelConfirmLocked(void) { MsgBoxHandle handle = audio.record.confirmHandle; audio.record.confirmHandle = NULL; audio.record.confirmPending = false; ++audio.record.confirmGeneration; return handle; } static void recordConfirm(bool yes, void * opaque) { const uint64_t generation = (uint64_t)(uintptr_t)opaque; bool wake = false; LG_LOCK_EXCLUSIVE(audio.record.lock); if (!audio.record.confirmPending || generation != audio.record.confirmGeneration) { LG_UNLOCK_EXCLUSIVE(audio.record.lock); return; } audio.record.confirmPending = false; audio.record.confirmHandle = NULL; if (yes && audio.record.requested && audio.record.requestedGeneration && !audio.record.shuttingDown && audio.audioDev && audio.record.thread && atomic_load_explicit( &audio.record.workerAlive, memory_order_acquire)) { DEBUG_INFO("Microphone access granted"); audio.record.enabled = true; recordResetStartRetryLocked(); recordAdvanceRequestLocked(); recordDisarmLocked(); wake = true; } else if (yes) DEBUG_INFO("Ignoring stale microphone access confirmation"); else DEBUG_INFO("Microphone access denied"); LG_UNLOCK_EXCLUSIVE(audio.record.lock); if (wake) recordWorkerWake(); } static void recordCreateConfirm( MsgBoxHandle oldConfirm, uint64_t generation) { app_msgBoxClose(oldConfirm); MsgBoxHandle handle = app_confirmMsgBox( "Microphone", recordConfirm, (void *)(uintptr_t)generation, "An application just opened the microphone!\n" "Do you want it to access your microphone?"); MsgBoxHandle stale = NULL; LG_LOCK_EXCLUSIVE(audio.record.lock); if (handle && audio.record.confirmPending && generation == audio.record.confirmGeneration) audio.record.confirmHandle = handle; else { stale = handle; if (!handle && audio.record.confirmPending && generation == audio.record.confirmGeneration) { audio.record.confirmPending = false; ++audio.record.confirmGeneration; } } LG_UNLOCK_EXCLUSIVE(audio.record.lock); app_msgBoxClose(stale); } static void recordStart(const AudioBinding * binding, uint32_t generation, const LG_AudioFormat * format) { MsgBoxHandle oldConfirm = NULL; uint64_t confirmGeneration = 0; bool wake = false; LG_LOCK_EXCLUSIVE(audio.record.lock); if (!audio.audioDev || !audio.audioDev->record.start || !audio.audioDev->record.stop || !audio.record.thread || audio.record.shuttingDown || !atomic_load_explicit( &audio.record.workerAlive, memory_order_acquire) || !binding || !generation) { LG_UNLOCK_EXCLUSIVE(audio.record.lock); return; } if (!audioFormatValid(format)) { oldConfirm = recordCancelConfirmLocked(); audio.record.requested = false; audio.record.enabled = false; audio.record.requestedBinding = (AudioBinding) { 0 }; audio.record.requestedGeneration = 0; recordResetStartRetryLocked(); recordAdvanceRequestLocked(); recordDisarmLocked(); LG_UNLOCK_EXCLUSIVE(audio.record.lock); app_msgBoxClose(oldConfirm); recordWorkerWake(); DEBUG_ERROR("Invalid recording format"); return; } const bool sameRequest = audio.record.requested && generation == audio.record.requestedGeneration && audioBindingEqual(binding, &audio.record.requestedBinding); if (sameRequest) { const bool formatChanged = !audioFormatEqual( format, &audio.record.requestedFormat); if (formatChanged) { audio.record.requestedFormat = *format; recordResetStartRetryLocked(); recordAdvanceRequestLocked(); recordDisarmLocked(); wake = true; } LG_UNLOCK_EXCLUSIVE(audio.record.lock); if (wake) recordWorkerWake(); return; } const bool keepEnabled = audio.record.requested && audio.record.enabled && audioBindingEqual(binding, &audio.record.requestedBinding); wake = true; oldConfirm = recordCancelConfirmLocked(); audio.record.requested = true; audio.record.enabled = false; audio.record.requestedBinding = *binding; audio.record.requestedGeneration = generation; audio.record.requestedFormat = *format; recordResetStartRetryLocked(); recordAdvanceRequestLocked(); recordDisarmLocked(); if (keepEnabled) { audio.record.enabled = true; wake = true; } else if (g_state.micDefaultState == MIC_DEFAULT_DENY) DEBUG_INFO("Microphone access denied by default"); else if (g_state.micDefaultState == MIC_DEFAULT_ALLOW) { DEBUG_INFO("Microphone access granted by default"); audio.record.enabled = true; wake = true; } else { audio.record.confirmPending = true; confirmGeneration = ++audio.record.confirmGeneration; } LG_UNLOCK_EXCLUSIVE(audio.record.lock); if (wake) recordWorkerWake(); if (confirmGeneration) recordCreateConfirm(oldConfirm, confirmGeneration); else app_msgBoxClose(oldConfirm); } static void recordStop( const AudioBinding * binding, uint32_t generation) { LG_LOCK_EXCLUSIVE(audio.record.lock); if (binding && (!generation || !audio.record.requested || generation != audio.record.requestedGeneration || !audioBindingEqual(binding, &audio.record.requestedBinding))) { LG_UNLOCK_EXCLUSIVE(audio.record.lock); return; } const bool wasRequested = audio.record.requested; audio.record.requested = false; audio.record.enabled = false; audio.record.requestedBinding = (AudioBinding) { 0 }; audio.record.requestedGeneration = 0; recordResetStartRetryLocked(); recordAdvanceRequestLocked(); recordDisarmLocked(); MsgBoxHandle confirm = recordCancelConfirmLocked(); LG_UNLOCK_EXCLUSIVE(audio.record.lock); if (wasRequested) DEBUG_INFO("Microphone recording stopped"); app_msgBoxClose(confirm); recordWorkerWake(); } void lgAudio_recordToggleKeybind(int sc, void * opaque) { if (!atomic_load_explicit(&audio.ready, memory_order_acquire)) return; LG_LOCK_EXCLUSIVE(audio.record.lock); if (!audio.audioDev || !audio.record.thread || !atomic_load_explicit( &audio.record.workerAlive, memory_order_acquire) || audio.record.shuttingDown) { LG_UNLOCK_EXCLUSIVE(audio.record.lock); return; } if (!audio.record.requested) { LG_UNLOCK_EXCLUSIVE(audio.record.lock); app_alert(LG_ALERT_WARNING, "No application is requesting microphone access."); return; } MsgBoxHandle confirm = recordCancelConfirmLocked(); audio.record.enabled = !audio.record.enabled; const bool enabled = audio.record.enabled; recordResetStartRetryLocked(); recordAdvanceRequestLocked(); recordDisarmLocked(); LG_UNLOCK_EXCLUSIVE(audio.record.lock); DEBUG_INFO("Microphone recording %s by user", enabled ? "started" : "stopped"); app_msgBoxClose(confirm); recordWorkerWake(); app_alert(LG_ALERT_INFO, enabled ? "Microphone enabled" : "Microphone disabled"); } static void recordVolume(const AudioBinding * binding, uint32_t generation, int channels, const uint16_t volume[]) { LG_LOCK_EXCLUSIVE(audio.record.lock); if (!audio.audioDev || !audio.audioDev->record.volume || !g_params.audioSyncVolume || audio.record.shuttingDown || !audio.record.requested || generation != audio.record.requestedGeneration || !audioBindingEqual(binding, &audio.record.requestedBinding)) { LG_UNLOCK_EXCLUSIVE(audio.record.lock); return; } channels = min(ARRAY_LENGTH(audio.record.volume), channels); memcpy(audio.record.volume, volume, sizeof(uint16_t) * channels); audio.record.volumeChannels = channels; if (!++audio.record.controlSerial) ++audio.record.controlSerial; LG_UNLOCK_EXCLUSIVE(audio.record.lock); recordWorkerWake(); } static void recordMute( const AudioBinding * binding, uint32_t generation, bool mute) { LG_LOCK_EXCLUSIVE(audio.record.lock); if (!audio.audioDev || !audio.audioDev->record.mute || audio.record.shuttingDown || !audio.record.requested || generation != audio.record.requestedGeneration || !audioBindingEqual(binding, &audio.record.requestedBinding)) { LG_UNLOCK_EXCLUSIVE(audio.record.lock); return; } audio.record.mute = mute; if (!++audio.record.controlSerial) ++audio.record.controlSerial; LG_UNLOCK_EXCLUSIVE(audio.record.lock); recordWorkerWake(); } static bool recordRequestCurrentLocked(uint64_t requestSerial, const AudioBinding * binding, uint32_t generation, const LG_AudioFormat * format) { return !audio.record.shuttingDown && audio.record.requested && audio.record.enabled && requestSerial == audio.record.requestSerial && generation == audio.record.requestedGeneration && audioBindingEqual(binding, &audio.record.requestedBinding) && audioFormatEqual(format, &audio.record.requestedFormat); } static uint32_t recordArmLocked( const AudioBinding * binding, uint32_t generation) { if (!++audio.record.nextAttemptSerial) ++audio.record.nextAttemptSerial; atomic_store_explicit( &audio.record.failedAttemptSerial, 0, memory_order_release); audio.record.deliveryBinding = *binding; audio.record.deliveryGeneration = generation; atomic_store_explicit(&audio.record.deliverySerial, audio.record.nextAttemptSerial, memory_order_release); return audio.record.nextAttemptSerial; } static int recordThread(void * opaque) { bool backendStarted = false; bool controlsApplied = false; uint64_t backendRequestSerial = 0; uint32_t backendAttemptSerial = 0; int64_t backendStartTime = 0; uint64_t appliedControlSerial = 0; for (;;) { const unsigned int timeout = recordWorkerWaitTimeout(); if (!lgWaitEvent(audio.record.wake, timeout) && timeout == TIMEOUT_INFINITE) { DEBUG_ERROR("Failed to wait for audio recording work"); break; } for (;;) { enum { RECORD_ACTION_WAIT, RECORD_ACTION_START, RECORD_ACTION_STOP, RECORD_ACTION_CONTROLS, RECORD_ACTION_EXIT, } action = RECORD_ACTION_WAIT; uint64_t requestSerial = 0; uint32_t attemptSerial = 0; uint64_t controlSerial = 0; AudioBinding binding = { 0 }; uint32_t generation = 0; LG_AudioFormat format = { 0 }; int volumeChannels = 0; uint16_t volume[LG_AUDIO_MAX_CHANNELS]; bool mute = false; bool reportStartFailure = false; bool runtimeFailure = false; bool resetStartRetry = false; LG_LOCK_EXCLUSIVE(audio.record.lock); const bool desired = !audio.record.shuttingDown && audio.record.requested && audio.record.enabled && audio.record.requestedGeneration && audio.audioDev && audio.audioDev->record.start && audio.audioDev->record.stop; const bool backendFailed = backendStarted && backendAttemptSerial == atomic_load_explicit( &audio.record.failedAttemptSerial, memory_order_acquire); if (backendStarted && (backendFailed || !desired || backendRequestSerial != audio.record.requestSerial)) { recordDisarmLocked(); if (backendFailed && desired && backendRequestSerial == audio.record.requestSerial) { requestSerial = backendRequestSerial; runtimeFailure = true; resetStartRetry = nanotime() - backendStartTime >= AUDIO_RETRY_RESET_NS; } action = RECORD_ACTION_STOP; } else if (!backendStarted && desired && nanotime() >= audio.record.nextStartRetry) { requestSerial = audio.record.requestSerial; binding = audio.record.requestedBinding; generation = audio.record.requestedGeneration; format = audio.record.requestedFormat; attemptSerial = recordArmLocked(&binding, generation); action = RECORD_ACTION_START; } else if (backendStarted && (!controlsApplied || appliedControlSerial != audio.record.controlSerial)) { requestSerial = backendRequestSerial; controlSerial = audio.record.controlSerial; volumeChannels = audio.record.volumeChannels; if (volumeChannels) memcpy(volume, audio.record.volume, sizeof(*volume) * volumeChannels); mute = audio.record.mute; action = RECORD_ACTION_CONTROLS; } else if (audio.record.shuttingDown) action = RECORD_ACTION_EXIT; LG_UNLOCK_EXCLUSIVE(audio.record.lock); if (action == RECORD_ACTION_WAIT) break; if (action == RECORD_ACTION_EXIT) goto exit; if (action == RECORD_ACTION_STOP) { if (audio.audioDev && audio.audioDev->record.stop) audio.audioDev->record.stop(); if (runtimeFailure) { LG_LOCK_EXCLUSIVE(audio.record.lock); reportStartFailure = recordDelayRequestLocked( requestSerial, resetStartRetry); LG_UNLOCK_EXCLUSIVE(audio.record.lock); } backendStarted = false; controlsApplied = false; backendRequestSerial = 0; backendAttemptSerial = 0; backendStartTime = 0; if (g_params.micShowIndicator) app_showRecord(false); if (runtimeFailure && reportStartFailure) DEBUG_ERROR("Audio recording device failed; retrying"); continue; } if (action == RECORD_ACTION_START) { const bool started = audio.audioDev->record.start( &format, recordPushFrames, recordBackendFailed, attemptSerial); LG_LOCK_EXCLUSIVE(audio.record.lock); const bool current = recordRequestCurrentLocked( requestSerial, &binding, generation, &format); const bool failed = attemptSerial == atomic_load_explicit( &audio.record.failedAttemptSerial, memory_order_acquire); const bool delayAfterStop = started && current && failed; if (started && current && !failed) { backendStarted = true; backendRequestSerial = requestSerial; backendAttemptSerial = attemptSerial; backendStartTime = nanotime(); controlsApplied = false; audio.record.nextStartRetry = 0; } else { if (attemptSerial == atomic_load_explicit( &audio.record.deliverySerial, memory_order_relaxed)) recordDisarmLocked(); if (current && !started) reportStartFailure = recordDelayStartLocked(); } LG_UNLOCK_EXCLUSIVE(audio.record.lock); if (started && (!current || failed)) audio.audioDev->record.stop(); if (delayAfterStop) { LG_LOCK_EXCLUSIVE(audio.record.lock); if (recordRequestCurrentLocked( requestSerial, &binding, generation, &format)) reportStartFailure = recordDelayStartLocked(); LG_UNLOCK_EXCLUSIVE(audio.record.lock); } else if (started && current && g_params.micShowIndicator) app_showRecord(true); if (current && (!started || failed) && reportStartFailure) DEBUG_ERROR("Failed to start audio recording device; retrying"); continue; } if (volumeChannels && audio.audioDev->record.volume) audio.audioDev->record.volume(volumeChannels, volume); if (audio.audioDev->record.mute) audio.audioDev->record.mute(mute); LG_LOCK_EXCLUSIVE(audio.record.lock); if (backendStarted && requestSerial == backendRequestSerial && requestSerial == audio.record.requestSerial) { controlsApplied = true; appliedControlSerial = controlSerial; } LG_UNLOCK_EXCLUSIVE(audio.record.lock); } } exit: if (backendStarted && audio.audioDev && audio.audioDev->record.stop) audio.audioDev->record.stop(); LG_LOCK_EXCLUSIVE(audio.record.lock); recordDisarmLocked(); LG_UNLOCK_EXCLUSIVE(audio.record.lock); if (backendStarted && g_params.micShowIndicator) app_showRecord(false); atomic_store_explicit( &audio.record.workerAlive, false, memory_order_release); return 0; } static bool bindingActiveNL(const AudioBinding * binding) { return binding->ops && audioBindingEqual(&audio.active, binding); } static bool eventBegin( const AudioBinding * binding, AudioBinding * active) { LG_LOCK_SHARED(audio.activeLock); const bool current = bindingActiveNL(binding); if (current) { *active = audio.active; atomic_fetch_add_explicit( &audio.activeCallbacks, 1, memory_order_acq_rel); } LG_UNLOCK_SHARED(audio.activeLock); return current; } static void eventEnd(void) { const unsigned int previous = atomic_fetch_sub_explicit( &audio.activeCallbacks, 1, memory_order_release); if ((previous & ACTIVE_CALLBACK_WAITING) && (previous & ACTIVE_CALLBACK_COUNT_MASK) == 1) lgSignalEvent(audio.activeIdle); } static void waitForActiveCallbacks(void) { atomic_fetch_or_explicit( &audio.activeCallbacks, ACTIVE_CALLBACK_WAITING, memory_order_acq_rel); while ((atomic_load_explicit( &audio.activeCallbacks, memory_order_acquire) & ACTIVE_CALLBACK_COUNT_MASK) != 0) lgWaitEvent(audio.activeIdle, TIMEOUT_INFINITE); atomic_fetch_and_explicit( &audio.activeCallbacks, ~ACTIVE_CALLBACK_WAITING, memory_order_release); } static void queueFeedback(const LG_AudioOps * ops, void * opaque, uint32_t bindingEpoch, uint32_t bindingGeneration, uint32_t generation, const LG_AudioClock * clock, double targetRate) { if (!audio.feedback.wakeInitialized || !audio.feedback.thread || !clock) return; LG_LOCK(audio.feedback.lock); audio.feedback.ops = ops; audio.feedback.opaque = opaque; audio.feedback.bindingEpoch = bindingEpoch; audio.feedback.bindingGeneration = bindingGeneration; audio.feedback.generation = generation; audio.feedback.clock = *clock; audio.feedback.targetRate = targetRate; audio.feedback.pending = true; LG_UNLOCK(audio.feedback.lock); feedbackWorkerWake(); } static void eventPlaybackStart(void * opaque, uint32_t generation, const LG_AudioFormat * format, const LG_AudioClock * sourceClock) { AudioBinding * binding = opaque; AudioBinding active; (void)sourceClock; if (!eventBegin(binding, &active)) return; const bool providerRateControl = active.ops->clockFeedback && audio.feedback.wakeInitialized && audio.feedback.thread; LG_LOCK(audio.playback.sourceLock); ++audio.playback.requestSerial; audio.playback.requestedGeneration = generation; audio.playback.requestedFormatValid = generation && audioFormatValid(format); audio.playback.requestedProviderRateControl = providerRateControl; audio.playback.forceSoftwareResampler = false; audio.playback.startPending = false; audio.playback.startFailures = 0; audio.playback.nextStartRetry = 0; atomic_store_explicit( &audio.playback.streamGeneration, 0, memory_order_release); if (audio.playback.requestedFormatValid) { audio.playback.requestedFormat = *format; if (audio.playback.startInProgress) { audio.playback.startPending = true; playbackWorkerWake(); } else playbackScheduleStart(); } else { if (generation) DEBUG_ERROR("Invalid playback format"); if (playbackGetState() != STREAM_STATE_STOP) { LG_LOCK(audio.playback.deviceLock); playbackStop(); LG_UNLOCK(audio.playback.deviceLock); } audio.playback.requestedGeneration = 0; } LG_UNLOCK(audio.playback.sourceLock); eventEnd(); } static void eventPlaybackStop(void * opaque, uint32_t generation) { AudioBinding * binding = opaque; AudioBinding active; if (!eventBegin(binding, &active)) return; LG_LOCK(audio.playback.sourceLock); if (audio.playback.requestedGeneration == generation) { ++audio.playback.requestSerial; if (atomic_load_explicit(&audio.playback.streamGeneration, memory_order_acquire) == generation) { LG_LOCK(audio.playback.deviceLock); playbackSourceStop(); LG_UNLOCK(audio.playback.deviceLock); } audio.playback.requestedGeneration = 0; audio.playback.requestedFormatValid = false; audio.playback.requestedProviderRateControl = false; audio.playback.forceSoftwareResampler = false; audio.playback.startPending = false; audio.playback.startFailures = 0; audio.playback.nextStartRetry = 0; atomic_store_explicit( &audio.playback.streamGeneration, 0, memory_order_release); } LG_UNLOCK(audio.playback.sourceLock); eventEnd(); } static void eventPlaybackVolume(void * opaque, uint32_t generation, uint8_t channels, const uint16_t volume[]) { AudioBinding * binding = opaque; AudioBinding active; if (!eventBegin(binding, &active)) return; bool wake = false; LG_LOCK(audio.playback.sourceLock); if (volume && audio.playback.requestedGeneration == generation) { const int storedChannels = playbackStoreVolume(channels, volume); if (storedChannels) { if (!++audio.playback.controlSerial) ++audio.playback.controlSerial; audio.playback.controlsPending = true; wake = atomic_load_explicit(&audio.playback.streamGeneration, memory_order_acquire) == generation; } } LG_UNLOCK(audio.playback.sourceLock); if (wake) playbackWorkerWake(); eventEnd(); } static void eventPlaybackMute(void * opaque, uint32_t generation, bool mute) { AudioBinding * binding = opaque; AudioBinding active; if (!eventBegin(binding, &active)) return; bool wake = false; LG_LOCK(audio.playback.sourceLock); if (audio.playback.requestedGeneration == generation) { const bool stored = playbackStoreMute(mute); if (stored) { if (!++audio.playback.controlSerial) ++audio.playback.controlSerial; audio.playback.controlsPending = true; wake = atomic_load_explicit(&audio.playback.streamGeneration, memory_order_acquire) == generation; } } LG_UNLOCK(audio.playback.sourceLock); if (wake) playbackWorkerWake(); eventEnd(); } static void eventPlaybackData(void * opaque, uint32_t generation, const void * data, size_t frames, const LG_AudioClock * sourceClock) { const int64_t arrivalTime = nanotime(); AudioBinding * binding = opaque; AudioBinding active; if (!eventBegin(binding, &active)) return; LG_LOCK(audio.playback.sourceLock); if (audio.playback.requestedGeneration == generation) { const uint32_t liveGeneration = atomic_load_explicit( &audio.playback.streamGeneration, memory_order_acquire); PlaybackDataResult result = PLAYBACK_DATA_DROP; if (liveGeneration == generation) { result = playbackData(data, frames, sourceClock, arrivalTime); if (result == PLAYBACK_DATA_PROCESSED && atomic_load_explicit(&audio.playback.streamGeneration, memory_order_acquire) != generation) result = PLAYBACK_DATA_DROP; if (result == PLAYBACK_DATA_RETRY || result == PLAYBACK_DATA_RETRY_NOW) { atomic_store_explicit( &audio.playback.streamGeneration, 0, memory_order_release); if (result == PLAYBACK_DATA_RETRY_NOW) audio.playback.nextStartRetry = 0; else playbackDelayStart(); } } if (result != PLAYBACK_DATA_PROCESSED) playbackScheduleStart(); LG_AudioClock feedback; double targetRate; if (result == PLAYBACK_DATA_PROCESSED && active.ops->clockFeedback && playbackGetFeedback(&feedback, &targetRate)) queueFeedback(active.ops, active.opaque, active.epoch, active.generation, generation, &feedback, targetRate); } LG_UNLOCK(audio.playback.sourceLock); eventEnd(); } static void eventRecordStart(void * opaque, uint32_t generation, const LG_AudioFormat * format) { AudioBinding * binding = opaque; AudioBinding active; if (!eventBegin(binding, &active)) return; if (active.ops->recordData) recordStart(&active, generation, format); eventEnd(); } static void eventRecordStop(void * opaque, uint32_t generation) { AudioBinding * binding = opaque; AudioBinding active; if (!eventBegin(binding, &active)) return; recordStop(&active, generation); eventEnd(); } static void eventRecordVolume(void * opaque, uint32_t generation, uint8_t channels, const uint16_t volume[]) { AudioBinding * binding = opaque; AudioBinding active; if (!eventBegin(binding, &active)) return; if (volume) recordVolume(&active, generation, channels, volume); eventEnd(); } static void eventRecordMute(void * opaque, uint32_t generation, bool mute) { AudioBinding * binding = opaque; AudioBinding active; if (!eventBegin(binding, &active)) return; recordMute(&active, generation, mute); eventEnd(); } static const LG_AudioEventOps eventOps = { .playbackStart = eventPlaybackStart, .playbackStop = eventPlaybackStop, .playbackVolume = eventPlaybackVolume, .playbackMute = eventPlaybackMute, .playbackData = eventPlaybackData, .recordStart = eventRecordStart, .recordStop = eventRecordStop, .recordVolume = eventRecordVolume, .recordMute = eventRecordMute, }; static bool validOps(const LG_AudioOps * ops) { return ops && ops->name && ops->attach && ops->detach; } static AudioBinding makeBinding(const LG_AudioOps * ops, void * opaque) { uint32_t epoch = 0; if (ops) { epoch = ++audio.nextBindingEpoch; if (!epoch) epoch = ++audio.nextBindingEpoch; } return (AudioBinding) { .ops = ops, .opaque = opaque, .available = ops && !ops->setStatusListener, .epoch = epoch, .generation = 0, }; } static AudioBinding * nextBindingSlotNL(void) { if (audio.transport.available) return &audio.transport; if (audio.fallback.available) return &audio.fallback; return NULL; } static void stopStreams(void) { LG_LOCK(audio.playback.sourceLock); if (audio.audioDev) { LG_LOCK(audio.playback.deviceLock); playbackStop(); LG_UNLOCK(audio.playback.deviceLock); } ++audio.playback.requestSerial; audio.playback.requestedGeneration = 0; audio.playback.requestedFormatValid = false; audio.playback.requestedProviderRateControl = false; audio.playback.forceSoftwareResampler = false; audio.playback.startPending = false; audio.playback.startFailures = 0; audio.playback.nextStartRetry = 0; atomic_store_explicit( &audio.playback.streamGeneration, 0, memory_order_release); LG_UNLOCK(audio.playback.sourceLock); recordStop(NULL, 0); } /* providerLock must be held. dropActive suppresses calls into an endpoint * which has already disappeared. */ static void updateActive(bool dropActive) { for (;;) { LG_LOCK_EXCLUSIVE(audio.activeLock); AudioBinding * slot = nextBindingSlotNL(); AudioBinding next = slot ? *slot : (AudioBinding) { 0 }; const AudioBinding old = audio.active; if (old.ops == next.ops && old.opaque == next.opaque && old.epoch == next.epoch && old.generation == next.generation) { audio.active = next; LG_UNLOCK_EXCLUSIVE(audio.activeLock); return; } audio.active = (AudioBinding) { 0 }; LG_UNLOCK_EXCLUSIVE(audio.activeLock); waitForActiveCallbacks(); if (old.ops && !dropActive) old.ops->detach(old.opaque); dropActive = false; stopStreams(); LG_LOCK_EXCLUSIVE(audio.activeLock); slot = nextBindingSlotNL(); if (!slot) { LG_UNLOCK_EXCLUSIVE(audio.activeLock); DEBUG_INFO("Audio is unavailable"); return; } next = *slot; audio.active = next; LG_UNLOCK_EXCLUSIVE(audio.activeLock); if (next.ops->attach(next.opaque, &eventOps, slot)) { DEBUG_INFO("Using Audio: %s", next.ops->name); return; } LG_LOCK_EXCLUSIVE(audio.activeLock); if (audio.active.ops == next.ops && audio.active.opaque == next.opaque && audio.active.epoch == next.epoch && audio.active.generation == next.generation) audio.active = (AudioBinding) { 0 }; if (slot->ops == next.ops && slot->opaque == next.opaque) slot->available = false; LG_UNLOCK_EXCLUSIVE(audio.activeLock); waitForActiveCallbacks(); next.ops->detach(next.opaque); stopStreams(); DEBUG_WARN("Failed to attach Audio provider: %s", next.ops->name); } } static void fallbackStatusChanged(void * opaque, const LG_AudioStatus * status) { if (!status) return; LG_LOCK(audio.providerLock); LG_LOCK_EXCLUSIVE(audio.activeLock); const bool current = audio.fallback.ops && audio.fallback.opaque == opaque; if (current) { audio.fallback.available = status->available; audio.fallback.generation = status->generation; } LG_UNLOCK_EXCLUSIVE(audio.activeLock); if (current) updateActive(false); LG_UNLOCK(audio.providerLock); } static void transportStatusChanged(void * opaque, const LG_AudioStatus * status) { if (!status) return; LG_LOCK(audio.providerLock); LG_LOCK_EXCLUSIVE(audio.activeLock); const bool current = audio.transport.ops && audio.transport.opaque == opaque; if (current) { audio.transport.available = status->available; audio.transport.generation = status->generation; } LG_UNLOCK_EXCLUSIVE(audio.activeLock); if (current) updateActive(false); LG_UNLOCK(audio.providerLock); } /* bindingLock must be held. */ static void setBindingLocked(AudioBinding * target, const LG_AudioOps * ops, void * opaque, LG_AudioStatusFn statusFn) { LG_LOCK(audio.providerLock); const AudioBinding old = *target; LG_UNLOCK(audio.providerLock); if (old.ops && old.ops->setStatusListener) old.ops->setStatusListener(old.opaque, NULL, NULL); const AudioBinding next = makeBinding(ops, opaque); LG_LOCK(audio.providerLock); LG_LOCK_EXCLUSIVE(audio.activeLock); *target = next; LG_UNLOCK_EXCLUSIVE(audio.activeLock); updateActive(false); LG_UNLOCK(audio.providerLock); if (next.ops && next.ops->setStatusListener) next.ops->setStatusListener(next.opaque, statusFn, next.opaque); } static void setBinding(AudioBinding * target, const LG_AudioOps * ops, void * opaque, LG_AudioStatusFn statusFn) { if (ops && !validOps(ops)) { DEBUG_ERROR("Invalid audio operations"); ops = NULL; opaque = NULL; } LG_LOCK(audio.bindingLock); if (atomic_load_explicit(&audio.ready, memory_order_acquire)) setBindingLocked(target, ops, opaque, statusFn); LG_UNLOCK(audio.bindingLock); } static int playbackThread(void * opaque) { for (;;) { int result; do result = sem_wait(&audio.playback.worker.wake); while (result < 0 && errno == EINTR); if (result < 0) break; atomic_store_explicit( &audio.playback.worker.wakePending, false, memory_order_release); if (atomic_load_explicit( &audio.playback.worker.stop, memory_order_acquire)) break; playbackProcessStopPending(); playbackProcessStart(); playbackProcessControls(); playbackProcessDeviceStart(); playbackProcessDiagnostics(); } return 0; } static void playbackFeedbackFailed(uint32_t generation) { bool restart = false; LG_LOCK(audio.playback.sourceLock); if (generation && audio.playback.requestedGeneration == generation && audio.playback.requestedProviderRateControl && audio.playback.rateControl == PLAYBACK_RATE_PROVIDER && atomic_load_explicit(&audio.playback.streamGeneration, memory_order_acquire) == generation) { ++audio.playback.requestSerial; audio.playback.requestedProviderRateControl = false; audio.playback.forceSoftwareResampler = false; audio.playback.startPending = true; audio.playback.startFailures = 0; audio.playback.nextStartRetry = 0; playbackQueueSourceStop(); restart = true; } LG_UNLOCK(audio.playback.sourceLock); if (restart) DEBUG_WARN("Audio feedback stopped; using local rate control"); } static int feedbackThread(void * opaque) { AudioBinding rejectedBinding = { 0 }; uint32_t rejectedGeneration = 0; int64_t rejectedSince = 0; for (;;) { int result; do result = sem_wait(&audio.feedback.wake); while (result < 0 && errno == EINTR); if (result < 0) break; atomic_store_explicit( &audio.feedback.wakePending, false, memory_order_release); if (atomic_load_explicit( &audio.feedback.stop, memory_order_acquire)) break; const LG_AudioOps * ops; void * providerOpaque; uint32_t bindingEpoch; uint32_t bindingGeneration; uint32_t generation; LG_AudioClock clock; double targetRate; LG_LOCK(audio.feedback.lock); const bool pending = audio.feedback.pending; ops = audio.feedback.ops; providerOpaque = audio.feedback.opaque; bindingEpoch = audio.feedback.bindingEpoch; bindingGeneration = audio.feedback.bindingGeneration; generation = audio.feedback.generation; clock = audio.feedback.clock; targetRate = audio.feedback.targetRate; audio.feedback.pending = false; LG_UNLOCK(audio.feedback.lock); if (!pending) continue; const AudioBinding binding = { .ops = ops, .opaque = providerOpaque, .epoch = bindingEpoch, .generation = bindingGeneration, }; AudioBinding active; if (!eventBegin(&binding, &active)) { rejectedSince = 0; continue; } bool attempted = false; bool accepted = false; if (active.ops->clockFeedback && atomic_load_explicit(&audio.playback.streamGeneration, memory_order_acquire) == generation) { attempted = true; accepted = active.ops->clockFeedback( active.opaque, generation, &clock, targetRate); } if (!attempted || accepted) { rejectedSince = 0; eventEnd(); continue; } const int64_t now = nanotime(); if (!rejectedSince || !audioBindingEqual(&rejectedBinding, &binding) || rejectedGeneration != generation) { rejectedBinding = binding; rejectedGeneration = generation; rejectedSince = now; eventEnd(); continue; } if (now - rejectedSince >= PLAYBACK_FEEDBACK_FAILURE_NS) { rejectedSince = 0; playbackFeedbackFailed(generation); } eventEnd(); } return 0; } void lgAudio_init(void) { atomic_init(&audio.ready, false); LG_LOCK_INIT(audio.bindingLock); LG_LOCK_INIT(audio.providerLock); LG_RWLOCK_INIT(audio.activeLock); atomic_init(&audio.activeCallbacks, 0); LG_LOCK_INIT(audio.playback.sourceLock); LG_LOCK_INIT(audio.playback.deviceLock); LG_RWLOCK_INIT(audio.record.lock); LG_LOCK_INIT(audio.feedback.lock); audio.record.shuttingDown = false; atomic_init(&audio.playback.streamGeneration, 0); atomic_init(&audio.playback.activeAttemptSerial, 0); atomic_init(&audio.playback.failedAttemptSerial, 0); atomic_init(&audio.playback.diagnosticsEpoch, 1); atomic_init(&audio.playback.graphReady, false); atomic_init(&audio.playback.worker.stop, false); atomic_init(&audio.playback.worker.wakePending, false); atomic_init(&audio.record.deliverySerial, 0); atomic_init(&audio.record.failedAttemptSerial, 0); atomic_init(&audio.record.workerAlive, false); atomic_init(&audio.feedback.stop, false); atomic_init(&audio.feedback.wakePending, false); atomic_store_explicit( &audio.playback.callbackState, PLAYBACK_CALLBACK_DISABLED, memory_order_release); if (sem_init(&audio.playback.callbackIdle, 0, 0) != 0) { DEBUG_ERROR("Failed to create the audio callback semaphore"); return; } audio.playback.callbackIdleInitialized = true; audio.activeIdle = lgCreateEvent(true, 0); if (!audio.activeIdle) { DEBUG_ERROR("Failed to create the audio provider event"); goto err_callback; } if (sem_init(&audio.playback.worker.wake, 0, 0) != 0) { DEBUG_ERROR("Failed to create the audio playback semaphore"); goto err_active; } audio.playback.worker.wakeInitialized = true; if (!lgCreateThread("audioPlayback", playbackThread, NULL, &audio.playback.worker.thread)) { DEBUG_ERROR("Failed to create the audio playback thread"); goto err_playbackWake; } if (sem_init(&audio.feedback.wake, 0, 0) != 0) { DEBUG_ERROR("Failed to create the audio feedback semaphore"); goto err_playbackThread; } audio.feedback.wakeInitialized = true; if (!lgCreateThread("audioFeedback", feedbackThread, NULL, &audio.feedback.thread)) { DEBUG_ERROR("Failed to create the audio feedback thread"); goto err_feedbackWake; } audio.record.wake = lgCreateEvent(true, 0); if (!audio.record.wake) DEBUG_ERROR("Failed to create the audio recording event"); else { atomic_store_explicit( &audio.record.workerAlive, true, memory_order_release); if (!lgCreateThread("audioRecord", recordThread, NULL, &audio.record.thread)) { atomic_store_explicit( &audio.record.workerAlive, false, memory_order_release); DEBUG_ERROR("Failed to create the audio recording thread"); lgFreeEvent(audio.record.wake); audio.record.wake = NULL; } } for (int i = 0; i < LG_AUDIODEV_COUNT; ++i) if (LG_AudioDevs[i]->init()) { audio.audioDev = LG_AudioDevs[i]; atomic_store_explicit(&audio.ready, true, memory_order_release); DEBUG_INFO("Using AudioDev: %s", audio.audioDev->name); return; } DEBUG_WARN("Failed to initialize an audio backend"); return; err_feedbackWake: sem_destroy(&audio.feedback.wake); audio.feedback.wakeInitialized = false; err_playbackThread: atomic_store_explicit( &audio.playback.worker.stop, true, memory_order_release); playbackWorkerWake(); lgJoinThread(audio.playback.worker.thread, NULL); audio.playback.worker.thread = NULL; err_playbackWake: sem_destroy(&audio.playback.worker.wake); audio.playback.worker.wakeInitialized = false; err_active: lgFreeEvent(audio.activeIdle); audio.activeIdle = NULL; err_callback: sem_destroy(&audio.playback.callbackIdle); audio.playback.callbackIdleInitialized = false; } void lgAudio_free(void) { LG_LOCK(audio.bindingLock); const bool ready = atomic_exchange_explicit( &audio.ready, false, memory_order_acq_rel); if (ready) { setBindingLocked( &audio.fallback, NULL, NULL, fallbackStatusChanged); setBindingLocked( &audio.transport, NULL, NULL, transportStatusChanged); } LG_UNLOCK(audio.bindingLock); stopStreams(); if (audio.playback.worker.thread) { atomic_store_explicit( &audio.playback.worker.stop, true, memory_order_release); playbackWorkerWake(); lgJoinThread(audio.playback.worker.thread, NULL); audio.playback.worker.thread = NULL; } if (audio.playback.worker.wakeInitialized) { sem_destroy(&audio.playback.worker.wake); audio.playback.worker.wakeInitialized = false; } if (audio.feedback.thread) { atomic_store_explicit( &audio.feedback.stop, true, memory_order_release); feedbackWorkerWake(); lgJoinThread(audio.feedback.thread, NULL); audio.feedback.thread = NULL; } if (audio.feedback.wakeInitialized) { sem_destroy(&audio.feedback.wake); audio.feedback.wakeInitialized = false; } LG_LOCK_EXCLUSIVE(audio.record.lock); audio.record.shuttingDown = true; audio.record.requested = false; audio.record.enabled = false; recordAdvanceRequestLocked(); recordDisarmLocked(); MsgBoxHandle confirm = recordCancelConfirmLocked(); LG_UNLOCK_EXCLUSIVE(audio.record.lock); app_msgBoxClose(confirm); if (audio.record.thread) { recordWorkerWake(); lgJoinThread(audio.record.thread, NULL); } LG_LOCK_EXCLUSIVE(audio.record.lock); LGEvent * recordWake = audio.record.wake; audio.record.thread = NULL; audio.record.wake = NULL; LG_UNLOCK_EXCLUSIVE(audio.record.lock); if (recordWake) lgFreeEvent(recordWake); if (audio.playback.callbackIdleInitialized) { sem_destroy(&audio.playback.callbackIdle); audio.playback.callbackIdleInitialized = false; } if (audio.activeIdle) { lgFreeEvent(audio.activeIdle); audio.activeIdle = NULL; } struct LG_AudioDevOps * audioDev = audio.audioDev; audio.audioDev = NULL; if (audioDev) audioDev->free(); LG_RWLOCK_FREE(audio.activeLock); LG_LOCK_FREE(audio.playback.sourceLock); LG_LOCK_FREE(audio.playback.deviceLock); LG_LOCK_FREE(audio.bindingLock); LG_LOCK_FREE(audio.providerLock); LG_RWLOCK_FREE(audio.record.lock); LG_LOCK_FREE(audio.feedback.lock); } void lgAudio_setFallback(const LG_AudioOps * ops, void * opaque) { setBinding(&audio.fallback, ops, opaque, fallbackStatusChanged); } static void dropBinding(AudioBinding * target) { LG_LOCK(audio.bindingLock); if (!atomic_load_explicit(&audio.ready, memory_order_acquire)) { LG_UNLOCK(audio.bindingLock); return; } LG_LOCK(audio.providerLock); LG_LOCK_EXCLUSIVE(audio.activeLock); const bool wasActive = bindingActiveNL(target); *target = (AudioBinding) { 0 }; LG_UNLOCK_EXCLUSIVE(audio.activeLock); updateActive(wasActive); LG_UNLOCK(audio.providerLock); LG_UNLOCK(audio.bindingLock); } void lgAudio_dropFallback(void) { dropBinding(&audio.fallback); } void lgAudio_setTransport(const LG_AudioOps * ops, void * opaque) { setBinding(&audio.transport, ops, opaque, transportStatusChanged); } void lgAudio_dropTransport(void) { dropBinding(&audio.transport); } #endif