Files
LookingGlass/client/src/audio.c
2026-08-12 13:10:11 +10:00

4372 lines
135 KiB
C

/**
* 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 <errno.h>
#include <float.h>
#include <limits.h>
#include <math.h>
#include <samplerate.h>
#include <semaphore.h>
#include <stdalign.h>
#include <stdatomic.h>
#include <string.h>
#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