mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-10 09:11:31 +00:00
[client] audio: harden low-latency streaming
Rework audio provider and backend lifecycles so playback and capture callbacks quiesce without blocking real-time threads. Move activation, teardown, controls, retries, and diagnostics onto bounded workers. Harden USB audio cadence, feedback, and capture recovery. Preserve source clocks through recording and pace packets from the device clock. Bound queues, waits, conversion buffers, and packet sizes. Make PipeWire and PulseAudio stream control thread-safe and recoverable. Correct latency clock domains, coalesce rate updates, preserve recent capture under overload, and keep logging outside real-time callbacks.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -28,18 +28,23 @@
|
||||
#include "common/debug.h"
|
||||
#include "common/time.h"
|
||||
|
||||
#define PULSEAUDIO_ERROR_REPORT_INTERVAL_NS INT64_C(1000000000)
|
||||
#define PULSEAUDIO_OPERATION_TIMEOUT_US UINT64_C(250000)
|
||||
#define PULSEAUDIO_READY_TIMEOUT_US UINT64_C(5000000)
|
||||
#define PULSEAUDIO_RATE_UPDATE_INTERVAL_NS INT64_C(50000000)
|
||||
#define PULSEAUDIO_RATE_UPDATE_MAX_HOLD_NS INT64_C(250000000)
|
||||
#define PULSEAUDIO_RATE_UPDATE_DEADBAND_HZ UINT32_C(1)
|
||||
|
||||
struct PulseAudio
|
||||
{
|
||||
pa_threaded_mainloop * loop;
|
||||
pa_mainloop_api * api;
|
||||
pa_context * context;
|
||||
pa_operation * contextSub;
|
||||
bool loopStarted;
|
||||
|
||||
pa_stream * sink;
|
||||
int sinkIndex;
|
||||
uint32_t sinkIndex;
|
||||
bool sinkCorked;
|
||||
bool sinkMuted;
|
||||
bool sinkStarting;
|
||||
bool sinkResamplerEnabled;
|
||||
bool sinkRateFailed;
|
||||
int sinkMaxPeriodFrames;
|
||||
@@ -50,9 +55,26 @@ struct PulseAudio
|
||||
uint32_t sinkAppliedRate;
|
||||
uint32_t sinkPendingRate;
|
||||
uint32_t sinkRequestedRate;
|
||||
uint32_t sinkDeferredRate;
|
||||
int64_t sinkNextRateUpdate;
|
||||
int64_t sinkRateDeferredSince;
|
||||
bool sinkRateUpdateArmed;
|
||||
pa_operation * sinkRateOperation;
|
||||
pa_operation * sinkStartOperation;
|
||||
pa_time_event * sinkStartTimer;
|
||||
uint32_t sinkStartSerial;
|
||||
LG_AudioPullFn sinkPullFn;
|
||||
_Atomic(int64_t) sinkPresentationDeadline;
|
||||
LG_AudioFailureFn sinkFailureFn;
|
||||
uint32_t sinkFailureCookie;
|
||||
_Atomic(int64_t) sinkLatencyNs;
|
||||
atomic_bool sinkLatencyUpdateRequested;
|
||||
atomic_bool sinkErrorsPending;
|
||||
_Atomic(int64_t) sinkNextErrorReport;
|
||||
atomic_uint sinkUnderflows;
|
||||
atomic_uint sinkOverflows;
|
||||
atomic_uint sinkWriteErrors;
|
||||
atomic_uint sinkRateErrors;
|
||||
atomic_uint sinkControlErrors;
|
||||
};
|
||||
|
||||
static struct PulseAudio pa = {0};
|
||||
@@ -136,13 +158,83 @@ static pa_channel_position_t pulseaudio_channel(
|
||||
return (pa_channel_position_t)(PA_CHANNEL_POSITION_AUX0 + index);
|
||||
}
|
||||
|
||||
static void pulseaudio_unrefOperation(pa_operation * operation)
|
||||
static void pulseaudio_noteError(atomic_uint * counter)
|
||||
{
|
||||
if (operation)
|
||||
atomic_fetch_add_explicit(counter, 1, memory_order_relaxed);
|
||||
atomic_store_explicit(
|
||||
&pa.sinkErrorsPending, true, memory_order_release);
|
||||
}
|
||||
|
||||
static void pulseaudio_trackControlOperation(pa_operation * operation)
|
||||
{
|
||||
if (!operation)
|
||||
pulseaudio_noteError(&pa.sinkControlErrors);
|
||||
else
|
||||
pa_operation_unref(operation);
|
||||
}
|
||||
|
||||
static bool pulseaudio_submitRateUpdate(void);
|
||||
static void pulseaudio_streamControl_cb(
|
||||
pa_stream * stream, int success, void * userdata)
|
||||
{
|
||||
if (!success)
|
||||
pulseaudio_noteError(&pa.sinkControlErrors);
|
||||
}
|
||||
|
||||
static void pulseaudio_contextControl_cb(
|
||||
pa_context * context, int success, void * userdata)
|
||||
{
|
||||
if (!success)
|
||||
pulseaudio_noteError(&pa.sinkControlErrors);
|
||||
}
|
||||
|
||||
static void pulseaudio_reportErrors(bool force)
|
||||
{
|
||||
if (!atomic_load_explicit(
|
||||
&pa.sinkErrorsPending, memory_order_acquire))
|
||||
return;
|
||||
|
||||
const int64_t now = (int64_t)nanotime();
|
||||
if (!force)
|
||||
{
|
||||
int64_t next = atomic_load_explicit(
|
||||
&pa.sinkNextErrorReport, memory_order_relaxed);
|
||||
if (now < next || !atomic_compare_exchange_strong_explicit(
|
||||
&pa.sinkNextErrorReport, &next,
|
||||
now + PULSEAUDIO_ERROR_REPORT_INTERVAL_NS,
|
||||
memory_order_relaxed, memory_order_relaxed))
|
||||
return;
|
||||
}
|
||||
|
||||
if (!atomic_exchange_explicit(
|
||||
&pa.sinkErrorsPending, false, memory_order_acq_rel))
|
||||
return;
|
||||
|
||||
const unsigned int underflows = atomic_exchange_explicit(
|
||||
&pa.sinkUnderflows, 0, memory_order_relaxed);
|
||||
const unsigned int overflows = atomic_exchange_explicit(
|
||||
&pa.sinkOverflows, 0, memory_order_relaxed);
|
||||
const unsigned int writeErrors = atomic_exchange_explicit(
|
||||
&pa.sinkWriteErrors, 0, memory_order_relaxed);
|
||||
const unsigned int rateErrors = atomic_exchange_explicit(
|
||||
&pa.sinkRateErrors, 0, memory_order_relaxed);
|
||||
const unsigned int controlErrors = atomic_exchange_explicit(
|
||||
&pa.sinkControlErrors, 0, memory_order_relaxed);
|
||||
|
||||
if (underflows)
|
||||
DEBUG_WARN("PulseAudio playback underflowed %u time(s)", underflows);
|
||||
if (overflows)
|
||||
DEBUG_WARN("PulseAudio playback overflowed %u time(s)", overflows);
|
||||
if (writeErrors)
|
||||
DEBUG_WARN("PulseAudio playback write failed %u time(s)", writeErrors);
|
||||
if (rateErrors)
|
||||
DEBUG_WARN("PulseAudio sample rate update failed %u time(s)",
|
||||
rateErrors);
|
||||
if (controlErrors)
|
||||
DEBUG_WARN("PulseAudio control operation failed %u time(s)",
|
||||
controlErrors);
|
||||
}
|
||||
|
||||
static bool pulseaudio_submitRateUpdate(bool force);
|
||||
|
||||
static void pulseaudio_rateUpdate_cb(pa_stream * stream, int success,
|
||||
void * userdata)
|
||||
@@ -156,33 +248,46 @@ static void pulseaudio_rateUpdate_cb(pa_stream * stream, int success,
|
||||
|
||||
if (!success)
|
||||
{
|
||||
DEBUG_ERROR("Failed to update PulseAudio sample rate: %s",
|
||||
pa_strerror(pa_context_errno(pa.context)));
|
||||
pulseaudio_noteError(&pa.sinkRateErrors);
|
||||
pa.sinkRateFailed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
pa.sinkAppliedRate = pa.sinkPendingRate;
|
||||
if (pa.sinkCorked)
|
||||
pulseaudio_submitRateUpdate();
|
||||
pulseaudio_submitRateUpdate(true);
|
||||
}
|
||||
|
||||
static bool pulseaudio_submitRateUpdate(void)
|
||||
static bool pulseaudio_submitRateUpdate(bool force)
|
||||
{
|
||||
if (pa.sinkRateFailed)
|
||||
return false;
|
||||
|
||||
if (pa.sinkRateOperation ||
|
||||
pa.sinkRequestedRate == pa.sinkAppliedRate)
|
||||
if (pa.sinkRateOperation)
|
||||
return true;
|
||||
|
||||
pa.sinkPendingRate = pa.sinkRequestedRate;
|
||||
if (pa.sinkRequestedRate == pa.sinkAppliedRate)
|
||||
{
|
||||
pa.sinkDeferredRate = pa.sinkRequestedRate;
|
||||
pa.sinkRateUpdateArmed = false;
|
||||
pa.sinkRateDeferredSince = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!force && !pa.sinkRateUpdateArmed)
|
||||
return true;
|
||||
|
||||
pa.sinkPendingRate = pa.sinkRequestedRate;
|
||||
pa.sinkDeferredRate = pa.sinkPendingRate;
|
||||
pa.sinkRateUpdateArmed = false;
|
||||
pa.sinkRateDeferredSince = 0;
|
||||
pa.sinkNextRateUpdate =
|
||||
(int64_t)nanotime() + PULSEAUDIO_RATE_UPDATE_INTERVAL_NS;
|
||||
pa.sinkRateOperation = pa_stream_update_sample_rate(pa.sink,
|
||||
pa.sinkPendingRate, pulseaudio_rateUpdate_cb, NULL);
|
||||
if (!pa.sinkRateOperation)
|
||||
{
|
||||
DEBUG_ERROR("Failed to request a PulseAudio sample rate update: %s",
|
||||
pa_strerror(pa_context_errno(pa.context)));
|
||||
pulseaudio_noteError(&pa.sinkRateErrors);
|
||||
pa.sinkRateFailed = true;
|
||||
return false;
|
||||
}
|
||||
@@ -190,31 +295,43 @@ static bool pulseaudio_submitRateUpdate(void)
|
||||
return true;
|
||||
}
|
||||
|
||||
static void pulseaudio_sink_input_cb(pa_context *c, const pa_sink_input_info *i,
|
||||
int eol, void *userdata)
|
||||
static void pulseaudio_cancelStartTimer_nl(void)
|
||||
{
|
||||
if (eol < 0 || eol == 1)
|
||||
if (!pa.sinkStartTimer)
|
||||
return;
|
||||
|
||||
pa.sinkIndex = i->index;
|
||||
pa_time_event * timer = pa.sinkStartTimer;
|
||||
pa.sinkStartTimer = NULL;
|
||||
pa.api->time_free(timer);
|
||||
}
|
||||
|
||||
static void pulseaudio_subscribe_cb(pa_context *c,
|
||||
pa_subscription_event_type_t t, uint32_t index, void *userdata)
|
||||
static void pulseaudio_cancelStartOperation_nl(void)
|
||||
{
|
||||
switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK)
|
||||
{
|
||||
case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
|
||||
if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE)
|
||||
pa.sinkIndex = 0;
|
||||
else
|
||||
{
|
||||
pa_operation *o = pa_context_get_sink_input_info(c, index,
|
||||
pulseaudio_sink_input_cb, NULL);
|
||||
pulseaudio_unrefOperation(o);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!pa.sinkStartOperation)
|
||||
return;
|
||||
|
||||
pa_operation * operation = pa.sinkStartOperation;
|
||||
pa.sinkStartOperation = NULL;
|
||||
pa_operation_cancel(operation);
|
||||
pa_operation_unref(operation);
|
||||
}
|
||||
|
||||
static void pulseaudio_sinkDisarm_nl(void)
|
||||
{
|
||||
++pa.sinkStartSerial;
|
||||
pulseaudio_cancelStartTimer_nl();
|
||||
pulseaudio_cancelStartOperation_nl();
|
||||
pa.sinkFailureFn = NULL;
|
||||
pa.sinkFailureCookie = 0;
|
||||
}
|
||||
|
||||
static void pulseaudio_sinkFailed_nl(void)
|
||||
{
|
||||
LG_AudioFailureFn failureFn = pa.sinkFailureFn;
|
||||
const uint32_t failureCookie = pa.sinkFailureCookie;
|
||||
pulseaudio_sinkDisarm_nl();
|
||||
if (failureFn)
|
||||
failureFn(failureCookie);
|
||||
}
|
||||
|
||||
static void pulseaudio_ctx_state_change_cb(pa_context * c, void * userdata)
|
||||
@@ -228,28 +345,120 @@ static void pulseaudio_ctx_state_change_cb(pa_context * c, void * userdata)
|
||||
|
||||
case PA_CONTEXT_READY:
|
||||
DEBUG_INFO("Connected to PulseAudio server");
|
||||
pa_context_set_subscribe_callback(c, pulseaudio_subscribe_cb, NULL);
|
||||
pa_context_subscribe(c, PA_SUBSCRIPTION_MASK_SINK_INPUT, NULL, NULL);
|
||||
pa_threaded_mainloop_signal(pa.loop, 0);
|
||||
break;
|
||||
|
||||
case PA_CONTEXT_TERMINATED:
|
||||
if (pa.contextSub)
|
||||
{
|
||||
pa_operation_unref(pa.contextSub);
|
||||
pa.contextSub = NULL;
|
||||
}
|
||||
if (c == pa.context)
|
||||
pulseaudio_sinkFailed_nl();
|
||||
pa_threaded_mainloop_signal(pa.loop, 0);
|
||||
break;
|
||||
|
||||
case PA_CONTEXT_FAILED:
|
||||
default:
|
||||
if (c == pa.context)
|
||||
pulseaudio_sinkFailed_nl();
|
||||
DEBUG_ERROR("context error: %s", pa_strerror(pa_context_errno(c)));
|
||||
pa_threaded_mainloop_signal(pa.loop, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void pulseaudio_context_close_nl(void)
|
||||
{
|
||||
if (!pa.context)
|
||||
return;
|
||||
|
||||
pa_context_set_state_callback(pa.context, NULL, NULL);
|
||||
pa_context_disconnect(pa.context);
|
||||
pa_context_unref(pa.context);
|
||||
pa.context = NULL;
|
||||
}
|
||||
|
||||
struct PulseWait
|
||||
{
|
||||
bool timedOut;
|
||||
};
|
||||
|
||||
static void pulseaudio_timeout_cb(pa_mainloop_api * api,
|
||||
pa_time_event * event, const struct timeval * tv, void * userdata)
|
||||
{
|
||||
(void)api;
|
||||
(void)event;
|
||||
(void)tv;
|
||||
struct PulseWait * wait = userdata;
|
||||
wait->timedOut = true;
|
||||
pa_threaded_mainloop_signal(pa.loop, 0);
|
||||
}
|
||||
|
||||
/* The PulseAudio threaded mainloop must be locked. */
|
||||
static bool pulseaudio_contextConnect_nl(void)
|
||||
{
|
||||
pa_proplist * propList = pa_proplist_new();
|
||||
if (!propList)
|
||||
{
|
||||
DEBUG_ERROR("Failed to create the PulseAudio property list");
|
||||
return false;
|
||||
}
|
||||
pa_proplist_sets(propList, PA_PROP_MEDIA_ROLE, "video");
|
||||
|
||||
pa.context = pa_context_new_with_proplist(
|
||||
pa.api, "Looking Glass", propList);
|
||||
pa_proplist_free(propList);
|
||||
if (!pa.context)
|
||||
{
|
||||
DEBUG_ERROR("Failed to create the PulseAudio context");
|
||||
return false;
|
||||
}
|
||||
|
||||
pa_context_set_state_callback(pa.context,
|
||||
pulseaudio_ctx_state_change_cb, NULL);
|
||||
if (pa_context_connect(
|
||||
pa.context, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL) < 0)
|
||||
{
|
||||
DEBUG_ERROR("Failed to connect to the PulseAudio server: %s",
|
||||
pa_strerror(pa_context_errno(pa.context)));
|
||||
pulseaudio_context_close_nl();
|
||||
return false;
|
||||
}
|
||||
|
||||
struct PulseWait wait = {0};
|
||||
pa_time_event * timer = pa_context_rttime_new(pa.context,
|
||||
pa_rtclock_now() + PULSEAUDIO_READY_TIMEOUT_US,
|
||||
pulseaudio_timeout_cb, &wait);
|
||||
if (!timer)
|
||||
{
|
||||
DEBUG_ERROR("Failed to create the PulseAudio connection timer");
|
||||
pulseaudio_context_close_nl();
|
||||
return false;
|
||||
}
|
||||
|
||||
pa_context_state_t state;
|
||||
while (!wait.timedOut)
|
||||
{
|
||||
state = pa_context_get_state(pa.context);
|
||||
if (state == PA_CONTEXT_READY || !PA_CONTEXT_IS_GOOD(state))
|
||||
break;
|
||||
pa_threaded_mainloop_wait(pa.loop);
|
||||
}
|
||||
state = pa_context_get_state(pa.context);
|
||||
pa.api->time_free(timer);
|
||||
|
||||
if (state == PA_CONTEXT_READY)
|
||||
return true;
|
||||
|
||||
if (wait.timedOut)
|
||||
DEBUG_ERROR("Timed out connecting to the PulseAudio server");
|
||||
else
|
||||
DEBUG_ERROR("PulseAudio context did not become ready: %s",
|
||||
pa_strerror(pa_context_errno(pa.context)));
|
||||
pulseaudio_context_close_nl();
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool pulseaudio_init(void)
|
||||
{
|
||||
pa.sinkIndex = PA_INVALID_INDEX;
|
||||
pa.loop = pa_threaded_mainloop_new();
|
||||
if (!pa.loop)
|
||||
{
|
||||
@@ -258,74 +467,33 @@ static bool pulseaudio_init(void)
|
||||
}
|
||||
|
||||
pa.api = pa_threaded_mainloop_get_api(pa.loop);
|
||||
if (pa_signal_init(pa.api) != 0)
|
||||
{
|
||||
DEBUG_ERROR("Failed to init signals");
|
||||
goto err_loop;
|
||||
}
|
||||
|
||||
if (pa_threaded_mainloop_start(pa.loop) < 0)
|
||||
{
|
||||
DEBUG_ERROR("Failed to start the main loop");
|
||||
goto err_loop;
|
||||
}
|
||||
|
||||
pa_proplist * propList = pa_proplist_new();
|
||||
if (!propList)
|
||||
{
|
||||
DEBUG_ERROR("Failed to create the proplist");
|
||||
goto err_thread;
|
||||
}
|
||||
pa_proplist_sets(propList, PA_PROP_MEDIA_ROLE, "video");
|
||||
pa.loopStarted = true;
|
||||
|
||||
pa_threaded_mainloop_lock(pa.loop);
|
||||
pa.context = pa_context_new_with_proplist(
|
||||
pa.api,
|
||||
"Looking Glass",
|
||||
propList);
|
||||
if (!pa.context)
|
||||
if (!pulseaudio_contextConnect_nl())
|
||||
{
|
||||
DEBUG_ERROR("Failed to create the context");
|
||||
goto err_context;
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
goto err_thread;
|
||||
}
|
||||
|
||||
pa_context_set_state_callback(pa.context,
|
||||
pulseaudio_ctx_state_change_cb, NULL);
|
||||
|
||||
if (pa_context_connect(pa.context, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL) < 0)
|
||||
{
|
||||
DEBUG_ERROR("Failed to connect to the context server");
|
||||
goto err_context;
|
||||
}
|
||||
|
||||
for(;;)
|
||||
{
|
||||
pa_context_state_t state = pa_context_get_state(pa.context);
|
||||
if(!PA_CONTEXT_IS_GOOD(state))
|
||||
{
|
||||
DEBUG_ERROR("Context is bad");
|
||||
goto err_context;
|
||||
}
|
||||
|
||||
if (state == PA_CONTEXT_READY)
|
||||
break;
|
||||
|
||||
pa_threaded_mainloop_wait(pa.loop);
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
pa_proplist_free(propList);
|
||||
return true;
|
||||
|
||||
err_context:
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
pa_proplist_free(propList);
|
||||
|
||||
err_thread:
|
||||
pa_threaded_mainloop_stop(pa.loop);
|
||||
if (pa.loopStarted)
|
||||
{
|
||||
pa_threaded_mainloop_stop(pa.loop);
|
||||
pa.loopStarted = false;
|
||||
}
|
||||
|
||||
err_loop:
|
||||
pa_threaded_mainloop_free(pa.loop);
|
||||
pa.loop = NULL;
|
||||
pa.api = NULL;
|
||||
|
||||
err:
|
||||
return false;
|
||||
@@ -333,6 +501,7 @@ err:
|
||||
|
||||
static void pulseaudio_sink_close_nl(void)
|
||||
{
|
||||
pulseaudio_sinkDisarm_nl();
|
||||
if (!pa.sink)
|
||||
return;
|
||||
|
||||
@@ -347,51 +516,92 @@ static void pulseaudio_sink_close_nl(void)
|
||||
pa_operation_cancel(operation);
|
||||
pa_operation_unref(operation);
|
||||
}
|
||||
pulseaudio_unrefOperation(pa_stream_flush(pa.sink, NULL, NULL));
|
||||
pa_stream_disconnect(pa.sink);
|
||||
pa_stream_unref(pa.sink);
|
||||
pa.sink = NULL;
|
||||
pa.sinkResamplerEnabled = false;
|
||||
pa.sinkRateFailed = false;
|
||||
pa.sinkNominalRate = 0;
|
||||
pa.sinkAppliedRate = 0;
|
||||
pa.sinkPendingRate = 0;
|
||||
pa.sinkRequestedRate = 0;
|
||||
pa.sink = NULL;
|
||||
pa.sinkIndex = PA_INVALID_INDEX;
|
||||
pa.sinkResamplerEnabled = false;
|
||||
pa.sinkRateFailed = false;
|
||||
pa.sinkNominalRate = 0;
|
||||
pa.sinkAppliedRate = 0;
|
||||
pa.sinkPendingRate = 0;
|
||||
pa.sinkRequestedRate = 0;
|
||||
pa.sinkDeferredRate = 0;
|
||||
pa.sinkNextRateUpdate = 0;
|
||||
pa.sinkRateDeferredSince = 0;
|
||||
pa.sinkRateUpdateArmed = false;
|
||||
atomic_store_explicit(
|
||||
&pa.sinkPresentationDeadline, 0, memory_order_release);
|
||||
&pa.sinkLatencyNs, 0, memory_order_release);
|
||||
atomic_store_explicit(
|
||||
&pa.sinkLatencyUpdateRequested, false, memory_order_relaxed);
|
||||
}
|
||||
|
||||
static void pulseaudio_free(void)
|
||||
{
|
||||
if (!pa.loop)
|
||||
return;
|
||||
|
||||
pa_threaded_mainloop_lock(pa.loop);
|
||||
|
||||
pulseaudio_sink_close_nl();
|
||||
|
||||
pa_context_set_state_callback(pa.context, NULL, NULL);
|
||||
pa_context_set_subscribe_callback(pa.context, NULL, NULL);
|
||||
pa_context_disconnect(pa.context);
|
||||
pa_context_unref(pa.context);
|
||||
|
||||
if (pa.contextSub)
|
||||
{
|
||||
pa_operation_unref(pa.contextSub);
|
||||
pa.contextSub = NULL;
|
||||
}
|
||||
|
||||
pulseaudio_context_close_nl();
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
pulseaudio_reportErrors(true);
|
||||
|
||||
if (pa.loopStarted)
|
||||
{
|
||||
pa_threaded_mainloop_stop(pa.loop);
|
||||
pa.loopStarted = false;
|
||||
}
|
||||
pa_threaded_mainloop_free(pa.loop);
|
||||
pa.loop = NULL;
|
||||
pa.api = NULL;
|
||||
}
|
||||
|
||||
static void pulseaudio_state_cb(pa_stream * p, void * userdata)
|
||||
{
|
||||
if (pa.sinkStarting && pa_stream_get_state(pa.sink) == PA_STREAM_READY)
|
||||
{
|
||||
pulseaudio_unrefOperation(pa_stream_cork(pa.sink, 0, NULL, NULL));
|
||||
pa.sinkCorked = false;
|
||||
pa.sinkStarting = false;
|
||||
}
|
||||
|
||||
const pa_stream_state_t state = pa_stream_get_state(p);
|
||||
if (p == pa.sink &&
|
||||
(state == PA_STREAM_FAILED || state == PA_STREAM_TERMINATED))
|
||||
pulseaudio_sinkFailed_nl();
|
||||
pa_threaded_mainloop_signal(pa.loop, 0);
|
||||
}
|
||||
|
||||
static void pulseaudio_start_cb(
|
||||
pa_stream * stream, int success, void * userdata)
|
||||
{
|
||||
const uint32_t serial = (uint32_t)(uintptr_t)userdata;
|
||||
if (stream != pa.sink || serial != pa.sinkStartSerial)
|
||||
return;
|
||||
|
||||
pa_operation * operation = pa.sinkStartOperation;
|
||||
pa.sinkStartOperation = NULL;
|
||||
if (operation)
|
||||
pa_operation_unref(operation);
|
||||
pulseaudio_cancelStartTimer_nl();
|
||||
if (!success || pa_stream_get_state(stream) != PA_STREAM_READY ||
|
||||
!pa.context ||
|
||||
pa_context_get_state(pa.context) != PA_CONTEXT_READY)
|
||||
{
|
||||
pa.sinkCorked = true;
|
||||
pulseaudio_sinkFailed_nl();
|
||||
}
|
||||
}
|
||||
|
||||
static void pulseaudio_startTimeout_cb(pa_mainloop_api * api,
|
||||
pa_time_event * event, const struct timeval * tv, void * userdata)
|
||||
{
|
||||
(void)tv;
|
||||
const uint32_t serial = (uint32_t)(uintptr_t)userdata;
|
||||
if (event != pa.sinkStartTimer || serial != pa.sinkStartSerial)
|
||||
return;
|
||||
|
||||
pa.sinkStartTimer = NULL;
|
||||
api->time_free(event);
|
||||
pa.sinkCorked = true;
|
||||
pulseaudio_sinkFailed_nl();
|
||||
}
|
||||
|
||||
static void pulseaudio_write_cb(pa_stream * p, size_t nbytes, void * userdata)
|
||||
{
|
||||
// PulseAudio tries to pull data from the stream as soon as it is created for
|
||||
@@ -403,11 +613,17 @@ static void pulseaudio_write_cb(pa_stream * p, size_t nbytes, void * userdata)
|
||||
|
||||
if (pa_stream_begin_write(p, (void **)&dst, &nbytes) < 0)
|
||||
{
|
||||
DEBUG_ERROR("pa_stream_begin_write failed: %s",
|
||||
pa_strerror(pa_context_errno(pa.context)));
|
||||
pulseaudio_noteError(&pa.sinkWriteErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
pa_usec_t latency = 0;
|
||||
int negative = 0;
|
||||
bool latencyValid = false;
|
||||
if (atomic_exchange_explicit(
|
||||
&pa.sinkLatencyUpdateRequested, false, memory_order_acquire))
|
||||
latencyValid = pa_stream_get_latency(p, &latency, &negative) == 0;
|
||||
|
||||
int frames = nbytes / pa.sinkStride;
|
||||
frames = pa.sinkPullFn(dst, frames);
|
||||
if (frames <= 0)
|
||||
@@ -419,8 +635,7 @@ static void pulseaudio_write_cb(pa_stream * p, size_t nbytes, void * userdata)
|
||||
if (pa_stream_write(
|
||||
p, dst, frames * pa.sinkStride, NULL, 0, PA_SEEK_RELATIVE) < 0)
|
||||
{
|
||||
DEBUG_ERROR("pa_stream_write failed: %s",
|
||||
pa_strerror(pa_context_errno(pa.context)));
|
||||
pulseaudio_noteError(&pa.sinkWriteErrors);
|
||||
pa_stream_cancel_write(p);
|
||||
return;
|
||||
}
|
||||
@@ -428,26 +643,24 @@ static void pulseaudio_write_cb(pa_stream * p, size_t nbytes, void * userdata)
|
||||
/* Queue rate changes after the current audio block. This keeps the rate
|
||||
* reported by the preceding pull aligned with the block PulseAudio has
|
||||
* already received. */
|
||||
pulseaudio_submitRateUpdate();
|
||||
pulseaudio_submitRateUpdate(false);
|
||||
|
||||
pa_usec_t latency;
|
||||
int negative;
|
||||
if (pa_stream_get_latency(p, &latency, &negative) == 0)
|
||||
if (latencyValid)
|
||||
{
|
||||
const int64_t latencyNs = negative ? 0 : (int64_t)latency * 1000;
|
||||
atomic_store_explicit(&pa.sinkPresentationDeadline,
|
||||
(int64_t)nanotime() + latencyNs, memory_order_release);
|
||||
atomic_store_explicit(
|
||||
&pa.sinkLatencyNs, latencyNs, memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
static void pulseaudio_underflow_cb(pa_stream * p, void * userdata)
|
||||
{
|
||||
DEBUG_WARN("Underflow");
|
||||
pulseaudio_noteError(&pa.sinkUnderflows);
|
||||
}
|
||||
|
||||
static void pulseaudio_overflow_cb(pa_stream * p, void * userdata)
|
||||
{
|
||||
DEBUG_WARN("Overflow");
|
||||
pulseaudio_noteError(&pa.sinkOverflows);
|
||||
}
|
||||
|
||||
static bool pulseaudio_setup(const LG_AudioFormat * format,
|
||||
@@ -456,6 +669,7 @@ static bool pulseaudio_setup(const LG_AudioFormat * format,
|
||||
LG_AudioPullFn pullFn)
|
||||
{
|
||||
*resamplerEnabled = false;
|
||||
pulseaudio_reportErrors(false);
|
||||
|
||||
const int channels = format->channelCount;
|
||||
const int sampleRate = format->sampleRate;
|
||||
@@ -488,10 +702,25 @@ static bool pulseaudio_setup(const LG_AudioFormat * format,
|
||||
};
|
||||
|
||||
pa_threaded_mainloop_lock(pa.loop);
|
||||
if (!pa.context ||
|
||||
pa_context_get_state(pa.context) != PA_CONTEXT_READY)
|
||||
{
|
||||
pulseaudio_sink_close_nl();
|
||||
pulseaudio_context_close_nl();
|
||||
if (!pulseaudio_contextConnect_nl())
|
||||
{
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* pa_stream_update_sample_rate requires protocol version 12. */
|
||||
const bool enableResampler = requestResampler &&
|
||||
pa_context_get_server_protocol_version(pa.context) >= 12;
|
||||
if (pa.sink && !pa.sinkRateFailed &&
|
||||
if (pa.sink && pa.context &&
|
||||
pa_stream_get_state(pa.sink) == PA_STREAM_READY &&
|
||||
pa_context_get_state(pa.context) == PA_CONTEXT_READY &&
|
||||
!pa.sinkRateFailed &&
|
||||
pa.sinkResamplerEnabled == enableResampler &&
|
||||
!pa.sinkRateOperation &&
|
||||
pa.sinkAppliedRate == pa.sinkNominalRate &&
|
||||
@@ -511,13 +740,16 @@ static bool pulseaudio_setup(const LG_AudioFormat * format,
|
||||
pa.sinkStride = stride;
|
||||
pa.sinkPullFn = pullFn;
|
||||
pa.sinkCorked = true;
|
||||
pa.sinkStarting = false;
|
||||
pa.sinkResamplerEnabled = enableResampler;
|
||||
pa.sinkRateFailed = false;
|
||||
pa.sinkNominalRate = sampleRate;
|
||||
pa.sinkAppliedRate = sampleRate;
|
||||
pa.sinkPendingRate = sampleRate;
|
||||
pa.sinkRequestedRate = sampleRate;
|
||||
pa.sinkDeferredRate = sampleRate;
|
||||
pa.sinkNextRateUpdate = 0;
|
||||
pa.sinkRateDeferredSince = 0;
|
||||
pa.sinkRateUpdateArmed = false;
|
||||
|
||||
pa.sink = pa_stream_new(
|
||||
pa.context, "Looking Glass", &spec, &channelMap);
|
||||
@@ -550,18 +782,44 @@ static bool pulseaudio_setup(const LG_AudioFormat * format,
|
||||
return false;
|
||||
}
|
||||
|
||||
while (pa_stream_get_state(pa.sink) == PA_STREAM_CREATING)
|
||||
pa_threaded_mainloop_wait(pa.loop);
|
||||
|
||||
if (pa_stream_get_state(pa.sink) != PA_STREAM_READY)
|
||||
pa_stream * sink = pa.sink;
|
||||
pa_context * context = pa.context;
|
||||
struct PulseWait wait = {0};
|
||||
pa_time_event * timer = pa_context_rttime_new(context,
|
||||
pa_rtclock_now() + PULSEAUDIO_READY_TIMEOUT_US,
|
||||
pulseaudio_timeout_cb, &wait);
|
||||
if (!timer)
|
||||
{
|
||||
DEBUG_ERROR("PulseAudio stream did not become ready: %s",
|
||||
pa_strerror(pa_context_errno(pa.context)));
|
||||
DEBUG_ERROR("Failed to create the PulseAudio stream setup timer");
|
||||
pulseaudio_sink_close_nl();
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
return false;
|
||||
}
|
||||
|
||||
while (!wait.timedOut && pa.sink == sink &&
|
||||
pa_stream_get_state(sink) == PA_STREAM_CREATING &&
|
||||
pa.context == context &&
|
||||
PA_CONTEXT_IS_GOOD(pa_context_get_state(context)))
|
||||
pa_threaded_mainloop_wait(pa.loop);
|
||||
|
||||
const bool ready = pa.sink == sink && pa.context == context &&
|
||||
pa_stream_get_state(sink) == PA_STREAM_READY &&
|
||||
pa_context_get_state(context) == PA_CONTEXT_READY;
|
||||
pa.api->time_free(timer);
|
||||
|
||||
if (!ready)
|
||||
{
|
||||
if (wait.timedOut)
|
||||
DEBUG_ERROR("Timed out setting up the PulseAudio stream");
|
||||
else
|
||||
DEBUG_ERROR("PulseAudio stream did not become ready: %s",
|
||||
pa_strerror(pa_context_errno(context)));
|
||||
pulseaudio_sink_close_nl();
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
return false;
|
||||
}
|
||||
pa.sinkIndex = pa_stream_get_index(pa.sink);
|
||||
|
||||
const pa_buffer_attr * actual = pa_stream_get_buffer_attr(pa.sink);
|
||||
const uint64_t minRequestFrames =
|
||||
actual && actual->minreq != UINT32_MAX ?
|
||||
@@ -584,79 +842,123 @@ static bool pulseaudio_setup(const LG_AudioFormat * format,
|
||||
*resamplerEnabled = pa.sinkResamplerEnabled;
|
||||
|
||||
atomic_store_explicit(
|
||||
&pa.sinkPresentationDeadline, 0, memory_order_release);
|
||||
&pa.sinkLatencyNs, 0, memory_order_release);
|
||||
atomic_store_explicit(
|
||||
&pa.sinkLatencyUpdateRequested, false, memory_order_relaxed);
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void pulseaudio_start(void)
|
||||
static bool pulseaudio_start(
|
||||
LG_AudioFailureFn failureFn, uint32_t failureCookie)
|
||||
{
|
||||
if (!pa.sink)
|
||||
return;
|
||||
pulseaudio_reportErrors(false);
|
||||
if (!pa.loop || pa_threaded_mainloop_in_thread(pa.loop))
|
||||
return false;
|
||||
|
||||
pa_threaded_mainloop_lock(pa.loop);
|
||||
|
||||
pa_stream_state_t state = pa_stream_get_state(pa.sink);
|
||||
if (state == PA_STREAM_CREATING)
|
||||
pa.sinkStarting = true;
|
||||
else
|
||||
pa_stream * sink = pa.sink;
|
||||
pa_context * context = pa.context;
|
||||
if (!sink || !context ||
|
||||
pa_stream_get_state(sink) != PA_STREAM_READY ||
|
||||
pa_context_get_state(context) != PA_CONTEXT_READY)
|
||||
{
|
||||
pulseaudio_unrefOperation(pa_stream_cork(pa.sink, 0, NULL, NULL));
|
||||
pa.sinkCorked = false;
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
return false;
|
||||
}
|
||||
|
||||
pulseaudio_sinkDisarm_nl();
|
||||
pa.sinkFailureFn = failureFn;
|
||||
pa.sinkFailureCookie = failureCookie;
|
||||
const uint32_t serial = pa.sinkStartSerial;
|
||||
pa.sinkStartTimer = pa_context_rttime_new(context,
|
||||
pa_rtclock_now() + PULSEAUDIO_OPERATION_TIMEOUT_US,
|
||||
pulseaudio_startTimeout_cb, (void *)(uintptr_t)serial);
|
||||
if (!pa.sinkStartTimer)
|
||||
{
|
||||
pulseaudio_sinkDisarm_nl();
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
return false;
|
||||
}
|
||||
|
||||
pa.sinkStartOperation = pa_stream_cork(sink, 0,
|
||||
pulseaudio_start_cb, (void *)(uintptr_t)serial);
|
||||
if (!pa.sinkStartOperation)
|
||||
{
|
||||
pulseaudio_sinkDisarm_nl();
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
return false;
|
||||
}
|
||||
pa.sinkCorked = false;
|
||||
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void pulseaudio_stop(void)
|
||||
{
|
||||
if (!pa.sink)
|
||||
if (!pa.loop)
|
||||
return;
|
||||
|
||||
bool needLock = !pa_threaded_mainloop_in_thread(pa.loop);
|
||||
if (needLock)
|
||||
pa_threaded_mainloop_lock(pa.loop);
|
||||
|
||||
pulseaudio_unrefOperation(pa_stream_cork(pa.sink, 1, NULL, NULL));
|
||||
pa.sinkCorked = true;
|
||||
pa.sinkStarting = false;
|
||||
pulseaudio_sinkDisarm_nl();
|
||||
if (!pa.sink)
|
||||
{
|
||||
if (needLock)
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
return;
|
||||
}
|
||||
|
||||
pa.sinkCorked = true;
|
||||
if (pa_stream_get_state(pa.sink) == PA_STREAM_READY)
|
||||
{
|
||||
pulseaudio_trackControlOperation(
|
||||
pa_stream_cork(pa.sink, 1, pulseaudio_streamControl_cb, NULL));
|
||||
pulseaudio_trackControlOperation(
|
||||
pa_stream_flush(pa.sink, pulseaudio_streamControl_cb, NULL));
|
||||
}
|
||||
if (pa.sinkResamplerEnabled)
|
||||
{
|
||||
pa.sinkRequestedRate = pa.sinkNominalRate;
|
||||
pulseaudio_submitRateUpdate();
|
||||
pulseaudio_submitRateUpdate(true);
|
||||
}
|
||||
atomic_store_explicit(
|
||||
&pa.sinkPresentationDeadline, 0, memory_order_release);
|
||||
&pa.sinkLatencyNs, 0, memory_order_release);
|
||||
atomic_store_explicit(
|
||||
&pa.sinkLatencyUpdateRequested, false, memory_order_relaxed);
|
||||
|
||||
if (needLock)
|
||||
{
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
pulseaudio_reportErrors(false);
|
||||
}
|
||||
}
|
||||
|
||||
static void pulseaudio_volume(int channels, const uint16_t volume[])
|
||||
{
|
||||
if (!pa.sink || !pa.sinkIndex)
|
||||
return;
|
||||
|
||||
struct pa_cvolume v = { .channels = channels };
|
||||
for(int i = 0; i < channels; ++i)
|
||||
v.values[i] = pa_sw_volume_from_linear(
|
||||
9.3234e-7 * pow(1.000211902, volume[i]) - 0.000172787);
|
||||
max(0.0,
|
||||
9.3234e-7 * pow(1.000211902, volume[i]) - 0.000172787));
|
||||
|
||||
pa_threaded_mainloop_lock(pa.loop);
|
||||
pulseaudio_unrefOperation(pa_context_set_sink_input_volume(
|
||||
pa.context, pa.sinkIndex, &v, NULL, NULL));
|
||||
if (pa.sink && pa.sinkIndex != PA_INVALID_INDEX)
|
||||
pulseaudio_trackControlOperation(pa_context_set_sink_input_volume(
|
||||
pa.context, pa.sinkIndex, &v, pulseaudio_contextControl_cb, NULL));
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
}
|
||||
|
||||
static void pulseaudio_mute(bool mute)
|
||||
{
|
||||
if (!pa.sink || !pa.sinkIndex || pa.sinkMuted == mute)
|
||||
return;
|
||||
|
||||
pa.sinkMuted = mute;
|
||||
pa_threaded_mainloop_lock(pa.loop);
|
||||
pulseaudio_unrefOperation(pa_context_set_sink_input_mute(
|
||||
pa.context, pa.sinkIndex, mute, NULL, NULL));
|
||||
if (pa.sink && pa.sinkIndex != PA_INVALID_INDEX)
|
||||
pulseaudio_trackControlOperation(pa_context_set_sink_input_mute(
|
||||
pa.context, pa.sinkIndex, mute, pulseaudio_contextControl_cb, NULL));
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
}
|
||||
|
||||
@@ -671,8 +973,53 @@ static bool pulseaudio_setRate(double * ratio)
|
||||
return false;
|
||||
|
||||
pa.sinkRequestedRate = (uint32_t)llround(requestedRate);
|
||||
const uint32_t scheduledRate = pa.sinkRateOperation ?
|
||||
pa.sinkPendingRate : pa.sinkRequestedRate;
|
||||
|
||||
uint32_t scheduledRate;
|
||||
if (pa.sinkRateOperation)
|
||||
{
|
||||
scheduledRate = pa.sinkPendingRate;
|
||||
if (pa.sinkRequestedRate == scheduledRate)
|
||||
pa.sinkRateDeferredSince = 0;
|
||||
else if (!pa.sinkRateDeferredSince ||
|
||||
pa.sinkDeferredRate != pa.sinkRequestedRate)
|
||||
{
|
||||
pa.sinkDeferredRate = pa.sinkRequestedRate;
|
||||
pa.sinkRateDeferredSince = (int64_t)nanotime();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
scheduledRate = pa.sinkAppliedRate;
|
||||
pa.sinkRateUpdateArmed = false;
|
||||
if (pa.sinkRequestedRate == scheduledRate)
|
||||
pa.sinkRateDeferredSince = 0;
|
||||
else
|
||||
{
|
||||
const int64_t now = (int64_t)nanotime();
|
||||
if (!pa.sinkRateDeferredSince ||
|
||||
pa.sinkDeferredRate != pa.sinkRequestedRate)
|
||||
{
|
||||
pa.sinkDeferredRate = pa.sinkRequestedRate;
|
||||
pa.sinkRateDeferredSince = now;
|
||||
}
|
||||
|
||||
const uint32_t difference = pa.sinkRequestedRate > scheduledRate ?
|
||||
pa.sinkRequestedRate - scheduledRate :
|
||||
scheduledRate - pa.sinkRequestedRate;
|
||||
/* Coalesce controller noise around an integer-Hz boundary while still
|
||||
* applying a persistent one-Hz correction. Larger corrections only
|
||||
* wait for the operation-rate limit. */
|
||||
if (now >= pa.sinkNextRateUpdate &&
|
||||
(difference > PULSEAUDIO_RATE_UPDATE_DEADBAND_HZ ||
|
||||
now - pa.sinkRateDeferredSince >=
|
||||
PULSEAUDIO_RATE_UPDATE_MAX_HOLD_NS))
|
||||
{
|
||||
pa.sinkRateUpdateArmed = true;
|
||||
scheduledRate = pa.sinkRequestedRate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!scheduledRate)
|
||||
return false;
|
||||
|
||||
@@ -682,12 +1029,15 @@ static bool pulseaudio_setRate(double * ratio)
|
||||
|
||||
static uint64_t pulseaudio_latency(void)
|
||||
{
|
||||
const int64_t deadline = atomic_load_explicit(
|
||||
&pa.sinkPresentationDeadline, memory_order_acquire);
|
||||
if (deadline <= 0)
|
||||
atomic_store_explicit(
|
||||
&pa.sinkLatencyUpdateRequested, true, memory_order_release);
|
||||
|
||||
const int64_t latencyNs = atomic_load_explicit(
|
||||
&pa.sinkLatencyNs, memory_order_acquire);
|
||||
if (latencyNs <= 0)
|
||||
return 0;
|
||||
|
||||
return max(INT64_C(0), deadline - (int64_t)nanotime()) / 1000;
|
||||
return latencyNs / 1000;
|
||||
}
|
||||
|
||||
struct LG_AudioDevOps LGAD_PulseAudio =
|
||||
|
||||
@@ -106,7 +106,8 @@ typedef struct LG_AudioEventOps
|
||||
{
|
||||
/* Format and clock pointers are borrowed for the duration of each call.
|
||||
* A NULL source clock indicates that the provider has no usable clock.
|
||||
* Providers must serialize event delivery for an attachment. Stream
|
||||
* Providers must serialize event delivery within each stream direction.
|
||||
* Playback and recording events may be delivered concurrently. Stream
|
||||
* generations are nonzero and uniquely identify each stream instance. */
|
||||
void (*playbackStart)(void * opaque, uint32_t generation,
|
||||
const LG_AudioFormat * format, const LG_AudioClock * sourceClock);
|
||||
@@ -163,7 +164,9 @@ typedef struct LG_AudioOps
|
||||
* audio callback with the measured playback device clock. Its position uses
|
||||
* the device's independent output-frame timeline and its time includes the
|
||||
* backend's estimated presentation latency. targetRate is the source frame
|
||||
* rate requested by the client's buffer controller. */
|
||||
* rate requested by the client's buffer controller. Return false while
|
||||
* active rate feedback is unavailable; sustained failure makes the client
|
||||
* restart playback with local rate control. */
|
||||
bool (*clockFeedback)(void * opaque, uint32_t generation,
|
||||
const LG_AudioClock * playbackClock, double targetRate);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
#include "interface/audio.h"
|
||||
|
||||
typedef int (*LG_AudioPullFn)(uint8_t * dst, int frames);
|
||||
typedef void (*LG_AudioPushFn)(uint8_t * src, int frames);
|
||||
typedef bool (*LG_AudioPushFn)(uint8_t * src, int frames,
|
||||
const LG_AudioClock * sourceClock);
|
||||
typedef void (*LG_AudioFailureFn)(uint32_t cookie);
|
||||
|
||||
struct LG_AudioDevOps
|
||||
{
|
||||
@@ -55,10 +57,13 @@ struct LG_AudioDevOps
|
||||
bool requestResampler, bool * resamplerEnabled,
|
||||
int * maxPeriodFrames, int * startFrames, LG_AudioPullFn pullFn);
|
||||
|
||||
/* called when there is data available to start playback */
|
||||
void (*start)(void);
|
||||
/* Called when there is data available to start playback. failureFn may
|
||||
* report an asynchronous failure using the supplied cookie. Returning
|
||||
* false means the failure callback is already quiescent. */
|
||||
bool (*start)(LG_AudioFailureFn failureFn, uint32_t cookie);
|
||||
|
||||
/* called when the source reports the audio stream has stopped */
|
||||
/* Called when the source reports the audio stream has stopped. This must
|
||||
* synchronously quiesce the failure callback before returning. */
|
||||
void (*stop)(void);
|
||||
|
||||
/* [optional] called to set the volume of the channels */
|
||||
@@ -80,10 +85,17 @@ struct LG_AudioDevOps
|
||||
|
||||
struct
|
||||
{
|
||||
/* start the record stream using the requested interleaved format */
|
||||
void (*start)(const LG_AudioFormat * format, LG_AudioPushFn pushFn);
|
||||
/* Start the record stream using the requested interleaved format.
|
||||
* sourceClock is borrowed for the duration of pushFn and describes the
|
||||
* first frame. It is NULL when the backend has no source clock. pushFn
|
||||
* returns false when the active provider rejects the frames.
|
||||
* failureFn may report an asynchronous failure using the supplied cookie.
|
||||
* Returning false means both callbacks are already quiescent. */
|
||||
bool (*start)(const LG_AudioFormat * format, LG_AudioPushFn pushFn,
|
||||
LG_AudioFailureFn failureFn, uint32_t cookie);
|
||||
|
||||
/* called when the source reports the audio stream has stopped */
|
||||
/* Called when the source reports the audio stream has stopped. This must
|
||||
* synchronously quiesce both callbacks before returning. */
|
||||
void (*stop)(void);
|
||||
|
||||
/* [optional] called to set the volume of the channels */
|
||||
|
||||
2428
client/src/audio.c
2428
client/src/audio.c
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@
|
||||
#include "audio_spice.h"
|
||||
|
||||
#include "common/debug.h"
|
||||
#include "common/event.h"
|
||||
#include "common/locking.h"
|
||||
|
||||
#include <stdatomic.h>
|
||||
@@ -50,6 +51,22 @@ typedef struct SpiceAudioEventTarget
|
||||
}
|
||||
SpiceAudioEventTarget;
|
||||
|
||||
typedef struct SpiceAudioCallbackWaiter
|
||||
{
|
||||
struct SpiceAudioCallbackWaiter * next;
|
||||
LGEvent * event;
|
||||
unsigned int depth;
|
||||
}
|
||||
SpiceAudioCallbackWaiter;
|
||||
|
||||
typedef struct SpiceAudioCallbackWaitQueue
|
||||
{
|
||||
LG_Lock lock;
|
||||
atomic_uint count;
|
||||
SpiceAudioCallbackWaiter * waiters;
|
||||
}
|
||||
SpiceAudioCallbackWaitQueue;
|
||||
|
||||
static struct
|
||||
{
|
||||
LG_RWLock lock;
|
||||
@@ -59,11 +76,13 @@ static struct
|
||||
LG_AudioStatusFn statusCallback;
|
||||
void * statusOpaque;
|
||||
atomic_uint statusInFlight;
|
||||
SpiceAudioCallbackWaitQueue statusWait;
|
||||
|
||||
const LG_AudioEventOps * events;
|
||||
void * eventOpaque;
|
||||
uint32_t eventGeneration;
|
||||
atomic_uint inFlight;
|
||||
SpiceAudioCallbackWaitQueue eventWait;
|
||||
|
||||
SpiceAudioStream playback;
|
||||
SpiceAudioStream record;
|
||||
@@ -83,7 +102,17 @@ l_spice =
|
||||
.writer = ATOMIC_FLAG_INIT,
|
||||
},
|
||||
.statusInFlight = ATOMIC_VAR_INIT(0),
|
||||
.statusWait =
|
||||
{
|
||||
.lock = ATOMIC_FLAG_INIT,
|
||||
.count = ATOMIC_VAR_INIT(0),
|
||||
},
|
||||
.inFlight = ATOMIC_VAR_INIT(0),
|
||||
.eventWait =
|
||||
{
|
||||
.lock = ATOMIC_FLAG_INIT,
|
||||
.count = ATOMIC_VAR_INIT(0),
|
||||
},
|
||||
};
|
||||
|
||||
static _Thread_local unsigned int l_eventDepth;
|
||||
@@ -96,6 +125,96 @@ static uint32_t nextGeneration(uint32_t generation)
|
||||
return generation;
|
||||
}
|
||||
|
||||
static LGEvent * createWaitEvent(void)
|
||||
{
|
||||
LGEvent * event = lgCreateEvent(true, 0);
|
||||
if (!event)
|
||||
DEBUG_FATAL("Failed to create SPICE audio wait event");
|
||||
return event;
|
||||
}
|
||||
|
||||
static void waitEvent(LGEvent * event)
|
||||
{
|
||||
if (!lgWaitEvent(event, TIMEOUT_INFINITE))
|
||||
DEBUG_FATAL("Failed to wait for SPICE audio event");
|
||||
}
|
||||
|
||||
static void signalWaitEvent(LGEvent * event)
|
||||
{
|
||||
if (!lgSignalEvent(event))
|
||||
DEBUG_FATAL("Failed to signal SPICE audio event");
|
||||
}
|
||||
|
||||
static void signalCallbackWaiters(
|
||||
SpiceAudioCallbackWaitQueue * queue, unsigned int remaining)
|
||||
{
|
||||
if (!atomic_load_explicit(&queue->count, memory_order_acquire))
|
||||
return;
|
||||
|
||||
LG_LOCK(queue->lock);
|
||||
SpiceAudioCallbackWaiter ** link = &queue->waiters;
|
||||
while (*link)
|
||||
{
|
||||
SpiceAudioCallbackWaiter * waiter = *link;
|
||||
if (remaining > waiter->depth)
|
||||
{
|
||||
link = &waiter->next;
|
||||
continue;
|
||||
}
|
||||
|
||||
*link = waiter->next;
|
||||
atomic_fetch_sub_explicit(
|
||||
&queue->count, 1, memory_order_release);
|
||||
signalWaitEvent(waiter->event);
|
||||
}
|
||||
LG_UNLOCK(queue->lock);
|
||||
}
|
||||
|
||||
static void endCallback(atomic_uint * inFlight,
|
||||
SpiceAudioCallbackWaitQueue * waitQueue)
|
||||
{
|
||||
const unsigned int previous = atomic_fetch_sub_explicit(
|
||||
inFlight, 1, memory_order_release);
|
||||
DEBUG_ASSERT(previous > 0);
|
||||
signalCallbackWaiters(waitQueue, previous - 1);
|
||||
}
|
||||
|
||||
static void waitCallbacks(atomic_uint * inFlight,
|
||||
SpiceAudioCallbackWaitQueue * waitQueue, unsigned int depth)
|
||||
{
|
||||
if (atomic_load_explicit(inFlight, memory_order_acquire) <= depth)
|
||||
return;
|
||||
|
||||
SpiceAudioCallbackWaiter waiter =
|
||||
{
|
||||
.event = createWaitEvent(),
|
||||
.depth = depth,
|
||||
};
|
||||
|
||||
bool queued = false;
|
||||
LG_LOCK(waitQueue->lock);
|
||||
if (atomic_load_explicit(inFlight, memory_order_acquire) > depth)
|
||||
{
|
||||
waiter.next = waitQueue->waiters;
|
||||
waitQueue->waiters = &waiter;
|
||||
atomic_fetch_add_explicit(
|
||||
&waitQueue->count, 1, memory_order_release);
|
||||
queued = true;
|
||||
}
|
||||
LG_UNLOCK(waitQueue->lock);
|
||||
|
||||
if (queued)
|
||||
{
|
||||
waitEvent(waiter.event);
|
||||
|
||||
/* The signaler owns the waiter until it releases the queue lock. */
|
||||
LG_LOCK(waitQueue->lock);
|
||||
LG_UNLOCK(waitQueue->lock);
|
||||
}
|
||||
|
||||
lgFreeEvent(waiter.event);
|
||||
}
|
||||
|
||||
/* l_spice.lock must be held exclusively while admitting a callback so detach
|
||||
* cannot invalidate the target between the snapshot and in-flight increment. */
|
||||
static bool beginEventNL(SpiceAudioEventTarget * target)
|
||||
@@ -142,7 +261,7 @@ static bool recordEventCurrent(const SpiceAudioEventTarget * target,
|
||||
static void endEvent(void)
|
||||
{
|
||||
--l_eventDepth;
|
||||
atomic_fetch_sub_explicit(&l_spice.inFlight, 1, memory_order_release);
|
||||
endCallback(&l_spice.inFlight, &l_spice.eventWait);
|
||||
}
|
||||
|
||||
static bool sampleFormat(PSAudioFormat source,
|
||||
@@ -318,13 +437,11 @@ static void spiceSetStatusListener(void * opaque,
|
||||
{
|
||||
callback(callbackOpaque, &status);
|
||||
--l_statusDepth;
|
||||
atomic_fetch_sub_explicit(
|
||||
&l_spice.statusInFlight, 1, memory_order_release);
|
||||
endCallback(&l_spice.statusInFlight, &l_spice.statusWait);
|
||||
}
|
||||
else
|
||||
while (atomic_load_explicit(
|
||||
&l_spice.statusInFlight, memory_order_acquire) > l_statusDepth)
|
||||
;
|
||||
waitCallbacks(&l_spice.statusInFlight,
|
||||
&l_spice.statusWait, l_statusDepth);
|
||||
}
|
||||
|
||||
static bool spiceAttach(void * opaque, const LG_AudioEventOps * events,
|
||||
@@ -413,9 +530,7 @@ static void spiceDetach(void * opaque)
|
||||
nextGeneration(l_spice.eventGeneration);
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
|
||||
while (atomic_load_explicit(
|
||||
&l_spice.inFlight, memory_order_acquire) > l_eventDepth)
|
||||
;
|
||||
waitCallbacks(&l_spice.inFlight, &l_spice.eventWait, l_eventDepth);
|
||||
}
|
||||
|
||||
static bool spiceRecordData(void * opaque, uint32_t generation,
|
||||
@@ -501,8 +616,7 @@ void lgaSpice_setAvailable(bool available)
|
||||
{
|
||||
callback(callbackOpaque, &status);
|
||||
--l_statusDepth;
|
||||
atomic_fetch_sub_explicit(
|
||||
&l_spice.statusInFlight, 1, memory_order_release);
|
||||
endCallback(&l_spice.statusInFlight, &l_spice.statusWait);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,11 +22,15 @@
|
||||
|
||||
#include "usb_audio.h"
|
||||
|
||||
#include "common/debug.h"
|
||||
#include "common/event.h"
|
||||
#include "common/locking.h"
|
||||
#include "common/time.h"
|
||||
|
||||
#include <stdalign.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define USB_AUDIO_NS_PER_SECOND INT64_C(1000000000)
|
||||
|
||||
@@ -37,49 +41,94 @@ typedef struct USBAudioCallbackFrame
|
||||
}
|
||||
USBAudioCallbackFrame;
|
||||
|
||||
typedef struct USBAudioCallbackWaiter
|
||||
{
|
||||
struct USBAudioCallbackWaiter * next;
|
||||
LGEvent * event;
|
||||
unsigned int depth;
|
||||
}
|
||||
USBAudioCallbackWaiter;
|
||||
|
||||
typedef struct USBAudioCallbackWaitQueue
|
||||
{
|
||||
LG_Lock lock;
|
||||
atomic_uint count;
|
||||
USBAudioCallbackWaiter * waiters;
|
||||
}
|
||||
USBAudioCallbackWaitQueue;
|
||||
|
||||
typedef struct USBAudioCallbackGate
|
||||
{
|
||||
atomic_uint inFlight;
|
||||
USBAudioCallbackWaitQueue wait;
|
||||
}
|
||||
USBAudioCallbackGate;
|
||||
|
||||
typedef struct USBAudioEventTarget
|
||||
{
|
||||
const LG_AudioEventOps * events;
|
||||
void * opaque;
|
||||
uint32_t attachmentGeneration;
|
||||
USBAudioCallbackFrame frame;
|
||||
const LG_AudioEventOps * events;
|
||||
void * opaque;
|
||||
uint32_t attachmentGeneration;
|
||||
USBAudioCallbackFrame frame;
|
||||
USBAudioCallbackFrame ** frames;
|
||||
USBAudioCallbackGate * gate;
|
||||
}
|
||||
USBAudioEventTarget;
|
||||
|
||||
typedef struct USBAudioOperationWaiter
|
||||
{
|
||||
struct USBAudioOperationWaiter * next;
|
||||
LGEvent * event;
|
||||
}
|
||||
USBAudioOperationWaiter;
|
||||
|
||||
typedef struct USBAudioOperationQueue
|
||||
{
|
||||
LG_Lock lock;
|
||||
bool active;
|
||||
USBAudioOperationWaiter * head;
|
||||
USBAudioOperationWaiter * tail;
|
||||
}
|
||||
USBAudioOperationQueue;
|
||||
|
||||
struct LGA_USBState
|
||||
{
|
||||
LG_USBAudio * device;
|
||||
LG_USBRedir * redir;
|
||||
|
||||
LG_Lock statusLock;
|
||||
atomic_bool available;
|
||||
uint32_t statusGeneration;
|
||||
LG_AudioStatusFn statusCallback;
|
||||
void * statusOpaque;
|
||||
atomic_uint statusInFlight;
|
||||
atomic_uint_fast64_t statusNextTicket;
|
||||
atomic_uint_fast64_t statusServingTicket;
|
||||
LG_Lock statusLock;
|
||||
atomic_bool available;
|
||||
uint32_t statusGeneration;
|
||||
LG_AudioStatusFn statusCallback;
|
||||
void * statusOpaque;
|
||||
atomic_uint statusInFlight;
|
||||
USBAudioCallbackWaitQueue statusWait;
|
||||
USBAudioOperationQueue statusOperation;
|
||||
|
||||
LG_Lock stateLock;
|
||||
bool attached;
|
||||
bool detaching;
|
||||
uint32_t attachmentGeneration;
|
||||
const LG_AudioEventOps * events;
|
||||
void * eventOpaque;
|
||||
LG_Lock stateLock;
|
||||
LGEvent * detachEvent;
|
||||
bool attached;
|
||||
bool detaching;
|
||||
uint32_t attachmentGeneration;
|
||||
const LG_AudioEventOps * events;
|
||||
void * eventOpaque;
|
||||
|
||||
LG_AudioFormat playbackFormat;
|
||||
uint32_t playbackGeneration;
|
||||
LG_AudioFormat recordFormat;
|
||||
uint32_t recordGeneration;
|
||||
uint32_t generationSerial;
|
||||
int64_t playbackClockOrigin;
|
||||
atomic_uint_fast64_t playbackPosition;
|
||||
atomic_uint playbackDeliveryGeneration;
|
||||
atomic_uint recordDeliveryGeneration;
|
||||
atomic_uint inFlight;
|
||||
LG_AudioFormat playbackFormat;
|
||||
uint32_t playbackGeneration;
|
||||
LG_AudioFormat recordFormat;
|
||||
uint32_t recordGeneration;
|
||||
uint32_t generationSerial;
|
||||
int64_t playbackClockOrigin;
|
||||
atomic_uint_fast64_t playbackPosition;
|
||||
atomic_uint playbackDeliveryGeneration;
|
||||
atomic_uint recordDeliveryGeneration;
|
||||
alignas(64) USBAudioCallbackGate playbackGate;
|
||||
alignas(64) USBAudioCallbackGate recordGate;
|
||||
USBAudioOperationQueue recordOperation;
|
||||
};
|
||||
|
||||
static _Thread_local USBAudioCallbackFrame * l_eventFrames;
|
||||
static _Thread_local USBAudioCallbackFrame * l_playbackFrames;
|
||||
static _Thread_local USBAudioCallbackFrame * l_recordFrames;
|
||||
static _Thread_local USBAudioCallbackFrame * l_statusFrames;
|
||||
|
||||
static const LG_AudioFormat l_formatTemplate =
|
||||
@@ -139,6 +188,51 @@ static unsigned int callbackDepth(
|
||||
return depth;
|
||||
}
|
||||
|
||||
static LGEvent * createWaitEvent(void)
|
||||
{
|
||||
LGEvent * event = lgCreateEvent(true, 0);
|
||||
if (!event)
|
||||
DEBUG_FATAL("Failed to create USB audio wait event");
|
||||
return event;
|
||||
}
|
||||
|
||||
static void waitEvent(LGEvent * event)
|
||||
{
|
||||
if (!lgWaitEvent(event, TIMEOUT_INFINITE))
|
||||
DEBUG_FATAL("Failed to wait for USB audio event");
|
||||
}
|
||||
|
||||
static void signalWaitEvent(LGEvent * event)
|
||||
{
|
||||
if (!lgSignalEvent(event))
|
||||
DEBUG_FATAL("Failed to signal USB audio event");
|
||||
}
|
||||
|
||||
static void signalCallbackWaiters(
|
||||
USBAudioCallbackWaitQueue * queue, unsigned int remaining)
|
||||
{
|
||||
if (!atomic_load_explicit(&queue->count, memory_order_seq_cst))
|
||||
return;
|
||||
|
||||
LG_LOCK(queue->lock);
|
||||
USBAudioCallbackWaiter ** link = &queue->waiters;
|
||||
while (*link)
|
||||
{
|
||||
USBAudioCallbackWaiter * waiter = *link;
|
||||
if (remaining > waiter->depth)
|
||||
{
|
||||
link = &waiter->next;
|
||||
continue;
|
||||
}
|
||||
|
||||
*link = waiter->next;
|
||||
atomic_fetch_sub_explicit(
|
||||
&queue->count, 1, memory_order_release);
|
||||
signalWaitEvent(waiter->event);
|
||||
}
|
||||
LG_UNLOCK(queue->lock);
|
||||
}
|
||||
|
||||
static void beginCallback(LGA_USBState * state,
|
||||
USBAudioCallbackFrame * frame, USBAudioCallbackFrame ** frames,
|
||||
atomic_uint * inFlight)
|
||||
@@ -150,10 +244,59 @@ static void beginCallback(LGA_USBState * state,
|
||||
}
|
||||
|
||||
static void endCallback(USBAudioCallbackFrame * frame,
|
||||
USBAudioCallbackFrame ** frames, atomic_uint * inFlight)
|
||||
USBAudioCallbackFrame ** frames, atomic_uint * inFlight,
|
||||
USBAudioCallbackWaitQueue * waitQueue)
|
||||
{
|
||||
*frames = frame->previous;
|
||||
atomic_fetch_sub_explicit(inFlight, 1, memory_order_seq_cst);
|
||||
const unsigned int previous = atomic_fetch_sub_explicit(
|
||||
inFlight, 1, memory_order_seq_cst);
|
||||
DEBUG_ASSERT(previous > 0);
|
||||
signalCallbackWaiters(waitQueue, previous - 1);
|
||||
}
|
||||
|
||||
static void waitCallbacks(LGA_USBState * state,
|
||||
USBAudioCallbackFrame * frames, atomic_uint * inFlight,
|
||||
USBAudioCallbackWaitQueue * waitQueue)
|
||||
{
|
||||
const unsigned int depth = callbackDepth(frames, state);
|
||||
if (atomic_load_explicit(inFlight, memory_order_seq_cst) <= depth)
|
||||
return;
|
||||
|
||||
USBAudioCallbackWaiter waiter =
|
||||
{
|
||||
.event = createWaitEvent(),
|
||||
.depth = depth,
|
||||
};
|
||||
|
||||
bool queued = false;
|
||||
LG_LOCK(waitQueue->lock);
|
||||
|
||||
/* Publish the waiter before rechecking inFlight. This prevents the final
|
||||
* callback from taking the lock-free path while a waiter is being queued. */
|
||||
atomic_fetch_add_explicit(
|
||||
&waitQueue->count, 1, memory_order_seq_cst);
|
||||
if (atomic_load_explicit(inFlight, memory_order_seq_cst) > depth)
|
||||
{
|
||||
waiter.next = waitQueue->waiters;
|
||||
waitQueue->waiters = &waiter;
|
||||
queued = true;
|
||||
}
|
||||
else
|
||||
atomic_fetch_sub_explicit(
|
||||
&waitQueue->count, 1, memory_order_seq_cst);
|
||||
|
||||
LG_UNLOCK(waitQueue->lock);
|
||||
|
||||
if (queued)
|
||||
{
|
||||
waitEvent(waiter.event);
|
||||
|
||||
/* The signaler owns the waiter until it releases the queue lock. */
|
||||
LG_LOCK(waitQueue->lock);
|
||||
LG_UNLOCK(waitQueue->lock);
|
||||
}
|
||||
|
||||
lgFreeEvent(waiter.event);
|
||||
}
|
||||
|
||||
static LG_AudioClock makePlaybackClock(
|
||||
@@ -178,7 +321,8 @@ static LG_AudioClock makePlaybackClock(
|
||||
/* stateLock must be held while admitting a control event. The data event
|
||||
* path performs the equivalent admission using deliveryGeneration. */
|
||||
static bool beginEventNL(
|
||||
LGA_USBState * state, USBAudioEventTarget * target)
|
||||
LGA_USBState * state, USBAudioEventTarget * target,
|
||||
USBAudioCallbackFrame ** frames, USBAudioCallbackGate * gate)
|
||||
{
|
||||
if (!state->attached || !state->events)
|
||||
return false;
|
||||
@@ -186,11 +330,27 @@ static bool beginEventNL(
|
||||
target->events = state->events;
|
||||
target->opaque = state->eventOpaque;
|
||||
target->attachmentGeneration = state->attachmentGeneration;
|
||||
target->frames = frames;
|
||||
target->gate = gate;
|
||||
beginCallback(
|
||||
state, &target->frame, &l_eventFrames, &state->inFlight);
|
||||
state, &target->frame, frames, &gate->inFlight);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool beginPlaybackEventNL(
|
||||
LGA_USBState * state, USBAudioEventTarget * target)
|
||||
{
|
||||
return beginEventNL(
|
||||
state, target, &l_playbackFrames, &state->playbackGate);
|
||||
}
|
||||
|
||||
static bool beginRecordEventNL(
|
||||
LGA_USBState * state, USBAudioEventTarget * target)
|
||||
{
|
||||
return beginEventNL(
|
||||
state, target, &l_recordFrames, &state->recordGate);
|
||||
}
|
||||
|
||||
static bool attachmentCurrentNL(const LGA_USBState * state,
|
||||
const USBAudioEventTarget * target)
|
||||
{
|
||||
@@ -214,37 +374,84 @@ static bool recordEventCurrentNL(const LGA_USBState * state,
|
||||
state->recordGeneration == streamGeneration;
|
||||
}
|
||||
|
||||
static void endEvent(LGA_USBState * state, USBAudioEventTarget * target)
|
||||
static void endEvent(USBAudioEventTarget * target)
|
||||
{
|
||||
endCallback(&target->frame, &l_eventFrames, &state->inFlight);
|
||||
endCallback(&target->frame, target->frames,
|
||||
&target->gate->inFlight, &target->gate->wait);
|
||||
}
|
||||
|
||||
static void waitEvents(const LGA_USBState * state)
|
||||
static void waitPlaybackEvents(LGA_USBState * state)
|
||||
{
|
||||
const unsigned int depth = callbackDepth(l_eventFrames, state);
|
||||
while (atomic_load_explicit(
|
||||
&state->inFlight, memory_order_seq_cst) > depth)
|
||||
;
|
||||
waitCallbacks(state, l_playbackFrames,
|
||||
&state->playbackGate.inFlight, &state->playbackGate.wait);
|
||||
}
|
||||
|
||||
static bool beginStatusOperation(LGA_USBState * state)
|
||||
static void waitRecordEvents(LGA_USBState * state)
|
||||
{
|
||||
if (callbackDepth(l_statusFrames, state))
|
||||
waitCallbacks(state, l_recordFrames,
|
||||
&state->recordGate.inFlight, &state->recordGate.wait);
|
||||
}
|
||||
|
||||
static bool beginOperation(LGA_USBState * state,
|
||||
USBAudioOperationQueue * operation,
|
||||
const USBAudioCallbackFrame * callbackFrames)
|
||||
{
|
||||
/* Reentrant control must run inline: the current owner may be waiting for
|
||||
* this callback to finish before it can release the operation. */
|
||||
if (callbackDepth(callbackFrames, state))
|
||||
return false;
|
||||
|
||||
const uint_fast64_t ticket = atomic_fetch_add_explicit(
|
||||
&state->statusNextTicket, 1, memory_order_relaxed);
|
||||
while (atomic_load_explicit(
|
||||
&state->statusServingTicket, memory_order_acquire) != ticket)
|
||||
;
|
||||
USBAudioOperationWaiter waiter =
|
||||
{
|
||||
.event = createWaitEvent(),
|
||||
};
|
||||
|
||||
bool queued = false;
|
||||
LG_LOCK(operation->lock);
|
||||
if (operation->active)
|
||||
{
|
||||
if (operation->tail)
|
||||
operation->tail->next = &waiter;
|
||||
else
|
||||
operation->head = &waiter;
|
||||
operation->tail = &waiter;
|
||||
queued = true;
|
||||
}
|
||||
else
|
||||
operation->active = true;
|
||||
LG_UNLOCK(operation->lock);
|
||||
|
||||
if (queued)
|
||||
{
|
||||
waitEvent(waiter.event);
|
||||
|
||||
/* The previous owner owns the waiter until it releases the queue lock. */
|
||||
LG_LOCK(operation->lock);
|
||||
LG_UNLOCK(operation->lock);
|
||||
}
|
||||
|
||||
lgFreeEvent(waiter.event);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void endStatusOperation(LGA_USBState * state, bool owner)
|
||||
static void endOperation(
|
||||
USBAudioOperationQueue * operation, bool owner)
|
||||
{
|
||||
if (owner)
|
||||
atomic_fetch_add_explicit(
|
||||
&state->statusServingTicket, 1, memory_order_release);
|
||||
if (!owner)
|
||||
return;
|
||||
|
||||
LG_LOCK(operation->lock);
|
||||
USBAudioOperationWaiter * waiter = operation->head;
|
||||
if (waiter)
|
||||
{
|
||||
operation->head = waiter->next;
|
||||
if (!operation->head)
|
||||
operation->tail = NULL;
|
||||
signalWaitEvent(waiter->event);
|
||||
}
|
||||
else
|
||||
operation->active = false;
|
||||
LG_UNLOCK(operation->lock);
|
||||
}
|
||||
|
||||
static void beginStatusCallback(
|
||||
@@ -257,15 +464,32 @@ static void beginStatusCallback(
|
||||
static void endStatusCallback(
|
||||
LGA_USBState * state, USBAudioCallbackFrame * frame)
|
||||
{
|
||||
endCallback(frame, &l_statusFrames, &state->statusInFlight);
|
||||
endCallback(frame, &l_statusFrames,
|
||||
&state->statusInFlight, &state->statusWait);
|
||||
}
|
||||
|
||||
static void waitStatusCallbacks(const LGA_USBState * state)
|
||||
static void waitStatusCallbacks(LGA_USBState * state)
|
||||
{
|
||||
const unsigned int depth = callbackDepth(l_statusFrames, state);
|
||||
while (atomic_load_explicit(
|
||||
&state->statusInFlight, memory_order_acquire) > depth)
|
||||
;
|
||||
waitCallbacks(state, l_statusFrames,
|
||||
&state->statusInFlight, &state->statusWait);
|
||||
}
|
||||
|
||||
/* Returns with stateLock held. A callback must not wait for a detach which
|
||||
* may itself be waiting for that callback to return. */
|
||||
static bool lockStateAfterDetach(LGA_USBState * state)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
LG_LOCK(state->stateLock);
|
||||
if (!state->detaching)
|
||||
return true;
|
||||
LG_UNLOCK(state->stateLock);
|
||||
|
||||
if (callbackDepth(l_playbackFrames, state) ||
|
||||
callbackDepth(l_recordFrames, state))
|
||||
return false;
|
||||
waitEvent(state->detachEvent);
|
||||
}
|
||||
}
|
||||
|
||||
static void usbPlaybackStart(
|
||||
@@ -294,10 +518,10 @@ static void usbPlaybackStart(
|
||||
atomic_store_explicit(
|
||||
&state->playbackDeliveryGeneration, 0, memory_order_seq_cst);
|
||||
clock = makePlaybackClock(state, 0);
|
||||
const bool admitted = beginEventNL(state, &target);
|
||||
const bool admitted = beginPlaybackEventNL(state, &target);
|
||||
dispatch = admitted && target.events->playbackStart;
|
||||
if (admitted && !dispatch)
|
||||
endEvent(state, &target);
|
||||
endEvent(&target);
|
||||
LG_UNLOCK(state->stateLock);
|
||||
|
||||
if (!dispatch)
|
||||
@@ -311,7 +535,7 @@ static void usbPlaybackStart(
|
||||
atomic_store_explicit(&state->playbackDeliveryGeneration,
|
||||
generation, memory_order_seq_cst);
|
||||
LG_UNLOCK(state->stateLock);
|
||||
endEvent(state, &target);
|
||||
endEvent(&target);
|
||||
}
|
||||
|
||||
static void usbPlaybackStop(void * opaque)
|
||||
@@ -330,10 +554,10 @@ static void usbPlaybackStop(void * opaque)
|
||||
state->playbackGeneration = 0;
|
||||
atomic_store_explicit(
|
||||
&state->playbackDeliveryGeneration, 0, memory_order_seq_cst);
|
||||
const bool admitted = beginEventNL(state, &target);
|
||||
const bool admitted = beginPlaybackEventNL(state, &target);
|
||||
LG_UNLOCK(state->stateLock);
|
||||
|
||||
waitEvents(state);
|
||||
waitPlaybackEvents(state);
|
||||
|
||||
if (!admitted)
|
||||
return;
|
||||
@@ -345,7 +569,7 @@ static void usbPlaybackStop(void * opaque)
|
||||
|
||||
if (dispatch)
|
||||
target.events->playbackStop(target.opaque, generation);
|
||||
endEvent(state, &target);
|
||||
endEvent(&target);
|
||||
}
|
||||
|
||||
static void usbPlaybackData(
|
||||
@@ -353,7 +577,8 @@ static void usbPlaybackData(
|
||||
{
|
||||
LGA_USBState * state = opaque;
|
||||
USBAudioCallbackFrame frame;
|
||||
beginCallback(state, &frame, &l_eventFrames, &state->inFlight);
|
||||
beginCallback(state, &frame, &l_playbackFrames,
|
||||
&state->playbackGate.inFlight);
|
||||
|
||||
const uint64_t position = atomic_fetch_add_explicit(
|
||||
&state->playbackPosition, frames, memory_order_relaxed);
|
||||
@@ -370,7 +595,8 @@ static void usbPlaybackData(
|
||||
target, generation, data, frames, &clock);
|
||||
}
|
||||
}
|
||||
endCallback(&frame, &l_eventFrames, &state->inFlight);
|
||||
endCallback(&frame, &l_playbackFrames,
|
||||
&state->playbackGate.inFlight, &state->playbackGate.wait);
|
||||
}
|
||||
|
||||
static void usbRecordStart(
|
||||
@@ -378,11 +604,15 @@ static void usbRecordStart(
|
||||
{
|
||||
LGA_USBState * state = opaque;
|
||||
USBAudioEventTarget target;
|
||||
LG_AudioFormat format;
|
||||
bool dispatch;
|
||||
uint32_t generation;
|
||||
const bool recordOwner = beginOperation(
|
||||
state, &state->recordOperation, l_recordFrames);
|
||||
|
||||
LG_LOCK(state->stateLock);
|
||||
setStreamFormat(&state->recordFormat, sampleRate, channelMask);
|
||||
format = state->recordFormat;
|
||||
generation = state->recordGeneration;
|
||||
if (!generation)
|
||||
{
|
||||
@@ -392,49 +622,58 @@ static void usbRecordStart(
|
||||
}
|
||||
atomic_store_explicit(
|
||||
&state->recordDeliveryGeneration, 0, memory_order_seq_cst);
|
||||
const bool admitted = beginEventNL(state, &target);
|
||||
const bool admitted = beginRecordEventNL(state, &target);
|
||||
dispatch = admitted && target.events->recordStart;
|
||||
if (dispatch)
|
||||
atomic_store_explicit(&state->recordDeliveryGeneration,
|
||||
generation, memory_order_seq_cst);
|
||||
if (admitted && !dispatch)
|
||||
endEvent(state, &target);
|
||||
endEvent(&target);
|
||||
LG_UNLOCK(state->stateLock);
|
||||
|
||||
if (!dispatch)
|
||||
{
|
||||
endOperation(&state->recordOperation, recordOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
target.events->recordStart(
|
||||
target.opaque, generation, &state->recordFormat);
|
||||
|
||||
LG_LOCK(state->stateLock);
|
||||
if (recordEventCurrentNL(state, &target, generation))
|
||||
atomic_store_explicit(&state->recordDeliveryGeneration,
|
||||
generation, memory_order_seq_cst);
|
||||
LG_UNLOCK(state->stateLock);
|
||||
endEvent(state, &target);
|
||||
target.opaque, generation, &format);
|
||||
endEvent(&target);
|
||||
endOperation(&state->recordOperation, recordOwner);
|
||||
}
|
||||
|
||||
static void usbRecordStop(void * opaque)
|
||||
{
|
||||
LGA_USBState * state = opaque;
|
||||
USBAudioEventTarget target;
|
||||
const bool recordOwner = beginOperation(
|
||||
state, &state->recordOperation, l_recordFrames);
|
||||
|
||||
LG_LOCK(state->stateLock);
|
||||
const uint32_t generation = state->recordGeneration;
|
||||
if (!generation)
|
||||
{
|
||||
LG_UNLOCK(state->stateLock);
|
||||
endOperation(&state->recordOperation, recordOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
state->recordGeneration = 0;
|
||||
atomic_store_explicit(
|
||||
&state->recordDeliveryGeneration, 0, memory_order_seq_cst);
|
||||
const bool admitted = beginEventNL(state, &target);
|
||||
const bool admitted = beginRecordEventNL(state, &target);
|
||||
LG_UNLOCK(state->stateLock);
|
||||
|
||||
waitEvents(state);
|
||||
/* A reentrant stop cannot quiesce callbacks for the same reason that it
|
||||
* cannot wait for ownership above. */
|
||||
waitRecordEvents(state);
|
||||
|
||||
if (!admitted)
|
||||
{
|
||||
endOperation(&state->recordOperation, recordOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
LG_LOCK(state->stateLock);
|
||||
const bool dispatch = attachmentCurrentNL(state, &target) &&
|
||||
@@ -443,7 +682,8 @@ static void usbRecordStop(void * opaque)
|
||||
|
||||
if (dispatch)
|
||||
target.events->recordStop(target.opaque, generation);
|
||||
endEvent(state, &target);
|
||||
endEvent(&target);
|
||||
endOperation(&state->recordOperation, recordOwner);
|
||||
}
|
||||
|
||||
static const LG_USBAudioEventOps l_usbAudioEvents =
|
||||
@@ -458,7 +698,8 @@ static const LG_USBAudioEventOps l_usbAudioEvents =
|
||||
static void usbSetAvailable(void * opaque, bool available)
|
||||
{
|
||||
LGA_USBState * state = opaque;
|
||||
const bool statusOwner = beginStatusOperation(state);
|
||||
const bool statusOwner = beginOperation(
|
||||
state, &state->statusOperation, l_statusFrames);
|
||||
|
||||
LG_LOCK(state->statusLock);
|
||||
const bool changed = atomic_load_explicit(
|
||||
@@ -488,14 +729,15 @@ static void usbSetAvailable(void * opaque, bool available)
|
||||
callback(callbackOpaque, &status);
|
||||
endStatusCallback(state, &frame);
|
||||
}
|
||||
endStatusOperation(state, statusOwner);
|
||||
endOperation(&state->statusOperation, statusOwner);
|
||||
}
|
||||
|
||||
static void usbSetStatusListener(void * opaque,
|
||||
LG_AudioStatusFn callback, void * callbackOpaque)
|
||||
{
|
||||
LGA_USBState * state = opaque;
|
||||
const bool statusOwner = beginStatusOperation(state);
|
||||
const bool statusOwner = beginOperation(
|
||||
state, &state->statusOperation, l_statusFrames);
|
||||
|
||||
LG_LOCK(state->statusLock);
|
||||
state->statusCallback = callback;
|
||||
@@ -518,7 +760,7 @@ static void usbSetStatusListener(void * opaque,
|
||||
}
|
||||
else
|
||||
waitStatusCallbacks(state);
|
||||
endStatusOperation(state, statusOwner);
|
||||
endOperation(&state->statusOperation, statusOwner);
|
||||
}
|
||||
|
||||
static bool usbAttach(void * opaque, const LG_AudioEventOps * events,
|
||||
@@ -529,25 +771,21 @@ static bool usbAttach(void * opaque, const LG_AudioEventOps * events,
|
||||
&state->available, memory_order_acquire))
|
||||
return false;
|
||||
|
||||
USBAudioEventTarget target;
|
||||
USBAudioEventTarget playbackTarget;
|
||||
USBAudioEventTarget recordTarget;
|
||||
LG_AudioFormat playbackFormat;
|
||||
LG_AudioFormat recordFormat;
|
||||
LG_AudioClock playbackClock;
|
||||
uint32_t attachmentGeneration;
|
||||
uint32_t playbackGeneration;
|
||||
uint32_t recordGeneration;
|
||||
bool playbackDispatch;
|
||||
bool recordDispatch;
|
||||
bool admitted;
|
||||
bool playbackAdmitted;
|
||||
bool recordAdmitted;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
LG_LOCK(state->stateLock);
|
||||
if (!state->detaching)
|
||||
break;
|
||||
LG_UNLOCK(state->stateLock);
|
||||
if (callbackDepth(l_eventFrames, state))
|
||||
return false;
|
||||
}
|
||||
if (!lockStateAfterDetach(state))
|
||||
return false;
|
||||
|
||||
if (state->attached || !atomic_load_explicit(
|
||||
&state->available, memory_order_acquire))
|
||||
@@ -561,61 +799,72 @@ static bool usbAttach(void * opaque, const LG_AudioEventOps * events,
|
||||
nextGeneration(state->attachmentGeneration);
|
||||
state->events = events;
|
||||
state->eventOpaque = eventOpaque;
|
||||
attachmentGeneration = state->attachmentGeneration;
|
||||
playbackGeneration = state->playbackGeneration;
|
||||
recordGeneration = state->recordGeneration;
|
||||
playbackDispatch = playbackGeneration && events->playbackStart;
|
||||
recordDispatch = recordGeneration && events->recordStart;
|
||||
admitted = (playbackDispatch || recordDispatch) &&
|
||||
beginEventNL(state, &target);
|
||||
playbackDispatch = playbackDispatch && admitted;
|
||||
recordDispatch = recordDispatch && admitted;
|
||||
playbackAdmitted = playbackDispatch &&
|
||||
beginPlaybackEventNL(state, &playbackTarget);
|
||||
playbackDispatch = playbackDispatch && playbackAdmitted;
|
||||
recordAdmitted = false;
|
||||
if (playbackDispatch)
|
||||
{
|
||||
playbackFormat = state->playbackFormat;
|
||||
playbackClock = makePlaybackClock(state, atomic_load_explicit(
|
||||
&state->playbackPosition, memory_order_relaxed));
|
||||
}
|
||||
if (recordDispatch)
|
||||
recordFormat = state->recordFormat;
|
||||
lgUsbRedir_setPlugged(state->redir, true);
|
||||
LG_UNLOCK(state->stateLock);
|
||||
|
||||
if (playbackDispatch)
|
||||
{
|
||||
target.events->playbackStart(
|
||||
target.opaque, playbackGeneration,
|
||||
playbackTarget.events->playbackStart(
|
||||
playbackTarget.opaque, playbackGeneration,
|
||||
&playbackFormat, &playbackClock);
|
||||
|
||||
LG_LOCK(state->stateLock);
|
||||
if (playbackEventCurrentNL(
|
||||
state, &target, playbackGeneration))
|
||||
state, &playbackTarget, playbackGeneration))
|
||||
atomic_store_explicit(&state->playbackDeliveryGeneration,
|
||||
playbackGeneration, memory_order_seq_cst);
|
||||
LG_UNLOCK(state->stateLock);
|
||||
}
|
||||
if (playbackAdmitted)
|
||||
endEvent(&playbackTarget);
|
||||
|
||||
if (recordDispatch)
|
||||
{
|
||||
const bool recordOwner = beginOperation(
|
||||
state, &state->recordOperation, l_recordFrames);
|
||||
|
||||
LG_LOCK(state->stateLock);
|
||||
recordDispatch = recordEventCurrentNL(
|
||||
state, &target, recordGeneration);
|
||||
recordDispatch = state->attached &&
|
||||
state->attachmentGeneration == attachmentGeneration &&
|
||||
state->events == events &&
|
||||
state->eventOpaque == eventOpaque &&
|
||||
state->recordGeneration == recordGeneration;
|
||||
recordAdmitted = recordDispatch &&
|
||||
beginRecordEventNL(state, &recordTarget);
|
||||
recordDispatch = recordAdmitted &&
|
||||
recordEventCurrentNL(state, &recordTarget, recordGeneration);
|
||||
if (recordDispatch)
|
||||
{
|
||||
recordFormat = state->recordFormat;
|
||||
atomic_store_explicit(&state->recordDeliveryGeneration,
|
||||
recordGeneration, memory_order_seq_cst);
|
||||
}
|
||||
LG_UNLOCK(state->stateLock);
|
||||
|
||||
if (recordDispatch)
|
||||
target.events->recordStart(
|
||||
target.opaque, recordGeneration, &recordFormat);
|
||||
recordTarget.events->recordStart(
|
||||
recordTarget.opaque, recordGeneration, &recordFormat);
|
||||
|
||||
LG_LOCK(state->stateLock);
|
||||
if (recordDispatch &&
|
||||
recordEventCurrentNL(state, &target, recordGeneration))
|
||||
atomic_store_explicit(&state->recordDeliveryGeneration,
|
||||
recordGeneration, memory_order_seq_cst);
|
||||
LG_UNLOCK(state->stateLock);
|
||||
if (recordAdmitted)
|
||||
endEvent(&recordTarget);
|
||||
endOperation(&state->recordOperation, recordOwner);
|
||||
}
|
||||
|
||||
if (admitted)
|
||||
endEvent(state, &target);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -623,17 +872,11 @@ static void usbDetach(void * opaque)
|
||||
{
|
||||
LGA_USBState * state = opaque;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
LG_LOCK(state->stateLock);
|
||||
if (!state->detaching)
|
||||
break;
|
||||
LG_UNLOCK(state->stateLock);
|
||||
if (callbackDepth(l_eventFrames, state))
|
||||
return;
|
||||
}
|
||||
if (!lockStateAfterDetach(state))
|
||||
return;
|
||||
|
||||
state->detaching = true;
|
||||
lgResetEvent(state->detachEvent);
|
||||
state->attached = false;
|
||||
state->attachmentGeneration =
|
||||
nextGeneration(state->attachmentGeneration);
|
||||
@@ -646,7 +889,8 @@ static void usbDetach(void * opaque)
|
||||
lgUsbRedir_setPlugged(state->redir, false);
|
||||
LG_UNLOCK(state->stateLock);
|
||||
|
||||
waitEvents(state);
|
||||
waitPlaybackEvents(state);
|
||||
waitRecordEvents(state);
|
||||
|
||||
LG_LOCK(state->stateLock);
|
||||
if (!state->attached &&
|
||||
@@ -655,6 +899,7 @@ static void usbDetach(void * opaque)
|
||||
state->events = NULL;
|
||||
state->eventOpaque = NULL;
|
||||
state->detaching = false;
|
||||
signalWaitEvent(state->detachEvent);
|
||||
}
|
||||
LG_UNLOCK(state->stateLock);
|
||||
}
|
||||
@@ -666,7 +911,8 @@ static bool usbClockFeedback(void * opaque, uint32_t generation,
|
||||
|
||||
LG_LOCK(state->stateLock);
|
||||
if (!state->attached ||
|
||||
state->playbackGeneration != generation)
|
||||
state->playbackGeneration != generation ||
|
||||
!lgUsbAudio_feedbackActive(state->device))
|
||||
{
|
||||
LG_UNLOCK(state->stateLock);
|
||||
return false;
|
||||
@@ -686,17 +932,19 @@ static bool usbRecordData(void * opaque, uint32_t generation,
|
||||
const void * data, size_t frames,
|
||||
const LG_AudioClock * sourceClock)
|
||||
{
|
||||
(void)sourceClock;
|
||||
LGA_USBState * state = opaque;
|
||||
USBAudioCallbackFrame frame;
|
||||
beginCallback(state, &frame, &l_eventFrames, &state->inFlight);
|
||||
beginCallback(state, &frame, &l_recordFrames,
|
||||
&state->recordGate.inFlight);
|
||||
|
||||
const bool valid = generation && generation == atomic_load_explicit(
|
||||
&state->recordDeliveryGeneration, memory_order_seq_cst);
|
||||
const bool result = valid &&
|
||||
lgUsbAudio_recordData(state->device, data, frames);
|
||||
lgUsbAudio_recordData(
|
||||
state->device, data, frames, sourceClock);
|
||||
|
||||
endCallback(&frame, &l_eventFrames, &state->inFlight);
|
||||
endCallback(&frame, &l_recordFrames,
|
||||
&state->recordGate.inFlight, &state->recordGate.wait);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -710,28 +958,47 @@ const LG_AudioOps LGA_USB =
|
||||
.clockFeedback = usbClockFeedback,
|
||||
};
|
||||
|
||||
LGA_USBState * lgaUsb_create(void)
|
||||
LGA_USBState * lgaUsb_create(bool debug)
|
||||
{
|
||||
LGA_USBState * state = calloc(1, sizeof(*state));
|
||||
LGA_USBState * state = aligned_alloc(
|
||||
alignof(LGA_USBState), sizeof(*state));
|
||||
if (!state)
|
||||
return NULL;
|
||||
memset(state, 0, sizeof(*state));
|
||||
|
||||
LG_LOCK_INIT(state->statusLock);
|
||||
LG_LOCK_INIT(state->statusWait.lock);
|
||||
LG_LOCK_INIT(state->statusOperation.lock);
|
||||
LG_LOCK_INIT(state->stateLock);
|
||||
LG_LOCK_INIT(state->playbackGate.wait.lock);
|
||||
LG_LOCK_INIT(state->recordGate.wait.lock);
|
||||
LG_LOCK_INIT(state->recordOperation.lock);
|
||||
atomic_init(&state->available, false);
|
||||
atomic_init(&state->playbackPosition, 0);
|
||||
atomic_init(&state->playbackDeliveryGeneration, 0);
|
||||
atomic_init(&state->recordDeliveryGeneration, 0);
|
||||
atomic_init(&state->inFlight, 0);
|
||||
atomic_init(&state->playbackGate.inFlight, 0);
|
||||
atomic_init(&state->recordGate.inFlight, 0);
|
||||
atomic_init(&state->statusInFlight, 0);
|
||||
atomic_init(&state->statusNextTicket, 0);
|
||||
atomic_init(&state->statusServingTicket, 0);
|
||||
atomic_init(&state->playbackGate.wait.count, 0);
|
||||
atomic_init(&state->recordGate.wait.count, 0);
|
||||
atomic_init(&state->statusWait.count, 0);
|
||||
state->playbackFormat = l_formatTemplate;
|
||||
state->recordFormat = l_formatTemplate;
|
||||
|
||||
state->device = lgUsbAudio_create(&l_usbAudioEvents, state);
|
||||
state->detachEvent = lgCreateEvent(false, 0);
|
||||
if (!state->detachEvent || !lgSignalEvent(state->detachEvent))
|
||||
{
|
||||
if (state->detachEvent)
|
||||
lgFreeEvent(state->detachEvent);
|
||||
free(state);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
state->device = lgUsbAudio_create(&l_usbAudioEvents, state, debug);
|
||||
if (!state->device)
|
||||
{
|
||||
lgFreeEvent(state->detachEvent);
|
||||
free(state);
|
||||
return NULL;
|
||||
}
|
||||
@@ -741,6 +1008,7 @@ LGA_USBState * lgaUsb_create(void)
|
||||
if (!state->redir)
|
||||
{
|
||||
lgUsbAudio_destroy(state->device);
|
||||
lgFreeEvent(state->detachEvent);
|
||||
free(state);
|
||||
return NULL;
|
||||
}
|
||||
@@ -757,7 +1025,13 @@ void lgaUsb_destroy(LGA_USBState * state)
|
||||
usbDetach(state);
|
||||
lgUsbRedir_destroy(state->redir);
|
||||
lgUsbAudio_destroy(state->device);
|
||||
lgFreeEvent(state->detachEvent);
|
||||
LG_LOCK_FREE(state->recordOperation.lock);
|
||||
LG_LOCK_FREE(state->recordGate.wait.lock);
|
||||
LG_LOCK_FREE(state->playbackGate.wait.lock);
|
||||
LG_LOCK_FREE(state->stateLock);
|
||||
LG_LOCK_FREE(state->statusOperation.lock);
|
||||
LG_LOCK_FREE(state->statusWait.lock);
|
||||
LG_LOCK_FREE(state->statusLock);
|
||||
free(state);
|
||||
}
|
||||
@@ -767,7 +1041,7 @@ LG_USBRedir * lgaUsb_redir(LGA_USBState * state)
|
||||
return state ? state->redir : NULL;
|
||||
}
|
||||
|
||||
bool lgaUsb_recording(const LGA_USBState * state)
|
||||
uint64_t lgaUsb_processDelayNs(const LGA_USBState * state)
|
||||
{
|
||||
return state && lgUsbAudio_recording(state->device);
|
||||
return state ? lgUsbAudio_processDelayNs(state->device) : UINT64_MAX;
|
||||
}
|
||||
|
||||
@@ -24,14 +24,16 @@
|
||||
#include "interface/audio.h"
|
||||
#include "usbredir.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct LGA_USBState LGA_USBState;
|
||||
|
||||
LGA_USBState * lgaUsb_create(void);
|
||||
LGA_USBState * lgaUsb_create(bool debug);
|
||||
/* Detach this provider and stop PureSpice before destroying its state. */
|
||||
void lgaUsb_destroy(LGA_USBState * state);
|
||||
|
||||
LG_USBRedir * lgaUsb_redir(LGA_USBState * state);
|
||||
bool lgaUsb_recording(const LGA_USBState * state);
|
||||
uint64_t lgaUsb_processDelayNs(const LGA_USBState * state);
|
||||
|
||||
extern const LG_AudioOps LGA_USB;
|
||||
|
||||
|
||||
@@ -1728,7 +1728,7 @@ int spiceThread(void * arg)
|
||||
DEBUG_WARN("USB audio requires a playback backend, using SPICE audio");
|
||||
g_params.useSpiceUSBAudio = false;
|
||||
}
|
||||
else if (!(usbAudio = lgaUsb_create()))
|
||||
else if (!(usbAudio = lgaUsb_create(g_params.audioDebug)))
|
||||
{
|
||||
DEBUG_WARN("Failed to initialize USB audio, using SPICE audio");
|
||||
g_params.useSpiceUSBAudio = false;
|
||||
@@ -1839,7 +1839,17 @@ int spiceThread(void * arg)
|
||||
if (usbRedir && !lgUsbRedir_process(usbRedir))
|
||||
DEBUG_WARN("Failed to process USB audio redirection");
|
||||
if (usbAudio)
|
||||
processTimeout = lgaUsb_recording(usbAudio) ? 1 : 10;
|
||||
{
|
||||
const uint64_t delay = lgaUsb_processDelayNs(usbAudio);
|
||||
if (delay == UINT64_MAX)
|
||||
processTimeout = 10;
|
||||
else
|
||||
{
|
||||
const uint64_t timeout = delay / UINT64_C(1000000) +
|
||||
(delay % UINT64_C(1000000) != 0);
|
||||
processTimeout = (int)min(timeout, UINT64_C(10));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if ((status = purespice_process(processTimeout)) != PS_STATUS_RUN)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "interface/overlay.h"
|
||||
#include "math.h"
|
||||
#include "cimgui.h"
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include "../overlays.h"
|
||||
#include "../main.h"
|
||||
@@ -32,7 +33,7 @@
|
||||
//TODO: Make this user configurable?
|
||||
#define ICON_SIZE 32
|
||||
|
||||
static bool l_state[LG_USER_STATUS_MAX] = { 0 };
|
||||
static atomic_bool l_state[LG_USER_STATUS_MAX] = { 0 };
|
||||
static OverlayImage l_image[LG_USER_STATUS_MAX] = { 0 };
|
||||
static bool l_recordToggle;
|
||||
static double l_scale = 1.0;
|
||||
@@ -92,7 +93,8 @@ static int status_render(void * udata, bool interactive, struct Rect * windowRec
|
||||
for(int i = 0; i < LG_USER_STATUS_MAX; ++i)
|
||||
{
|
||||
OverlayImage * img = &l_image[i];
|
||||
if (!l_state[i] || !img->tex)
|
||||
if (!atomic_load_explicit(&l_state[i], memory_order_relaxed) ||
|
||||
!img->tex)
|
||||
continue;
|
||||
|
||||
// if the recording indicator is off, don't draw but reserve space
|
||||
@@ -147,9 +149,9 @@ struct LG_OverlayOps LGOverlayStatus =
|
||||
|
||||
void overlayStatus_set(LGUserStatus status, bool value)
|
||||
{
|
||||
if (l_state[status] == value)
|
||||
if (atomic_exchange_explicit(
|
||||
&l_state[status], value, memory_order_relaxed) == value)
|
||||
return;
|
||||
|
||||
l_state[status] = value;
|
||||
app_invalidateOverlay(true);
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@
|
||||
#ifndef _H_LG_CLIENT_USB_AUDIO_
|
||||
#define _H_LG_CLIENT_USB_AUDIO_
|
||||
|
||||
#include "interface/audio.h"
|
||||
#include "usbredir.h"
|
||||
|
||||
#include <stddef.h>
|
||||
@@ -49,21 +50,25 @@ typedef struct LG_USBAudioEventOps
|
||||
LG_USBAudioEventOps;
|
||||
|
||||
LG_USBAudio * lgUsbAudio_create(
|
||||
const LG_USBAudioEventOps * events, void * eventOpaque);
|
||||
const LG_USBAudioEventOps * events, void * eventOpaque, bool debug);
|
||||
/* Destroy the LG_USBRedir using this device before destroying the device. */
|
||||
void lgUsbAudio_destroy(LG_USBAudio * audio);
|
||||
|
||||
/* Publish the requested source rate without touching usbredir from the audio
|
||||
* feedback thread. */
|
||||
void lgUsbAudio_setFeedbackRate(LG_USBAudio * audio, double sampleRate);
|
||||
bool lgUsbAudio_feedbackActive(const LG_USBAudio * audio);
|
||||
|
||||
/* Queue interleaved packed signed 24-bit microphone frames. This may be
|
||||
* called from the audio recording thread. */
|
||||
/* Queue interleaved packed signed 24-bit microphone frames. sourceClock is
|
||||
* borrowed for the call and identifies the first frame when present. This may
|
||||
* be called from the audio recording thread. */
|
||||
bool lgUsbAudio_recordData(
|
||||
LG_USBAudio * audio, const void * data, size_t frames);
|
||||
LG_USBAudio * audio, const void * data, size_t frames,
|
||||
const LG_AudioClock * sourceClock);
|
||||
|
||||
/* This must be queried on the PureSpice processing thread. */
|
||||
bool lgUsbAudio_recording(const LG_USBAudio * audio);
|
||||
/* Return the time until ISO-IN processing is needed. This must be queried on
|
||||
* the PureSpice processing thread. */
|
||||
uint64_t lgUsbAudio_processDelayNs(const LG_USBAudio * audio);
|
||||
|
||||
const LG_USBRedirDeviceOps * lgUsbAudio_deviceOps(void);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "usbredir.h"
|
||||
|
||||
#include "common/debug.h"
|
||||
#include "common/time.h"
|
||||
|
||||
#include <usbredirparser.h>
|
||||
|
||||
@@ -29,6 +30,9 @@
|
||||
#include <string.h>
|
||||
|
||||
#define USB_REDIR_CHANNEL_COUNT 256
|
||||
#define USB_REDIR_DISCONNECT_TIMEOUT_NS INT64_C(500000000)
|
||||
#define USB_REDIR_RECONNECT_DELAY_NS INT64_C(250000000)
|
||||
#define USB_REDIR_MAX_OUTPUT_BYTES UINT64_C(1048576)
|
||||
|
||||
struct LG_USBRedir
|
||||
{
|
||||
@@ -49,6 +53,8 @@ struct LG_USBRedir
|
||||
atomic_bool available;
|
||||
bool plugged;
|
||||
bool disconnectPending;
|
||||
int64_t disconnectDeadline;
|
||||
int64_t reconnectDeadline;
|
||||
};
|
||||
|
||||
static void setAvailable(LG_USBRedir * usbredir, bool available)
|
||||
@@ -123,6 +129,7 @@ static void deviceDisconnectAck(void * opaque)
|
||||
{
|
||||
LG_USBRedir * usbredir = opaque;
|
||||
usbredir->disconnectPending = false;
|
||||
usbredir->disconnectDeadline = 0;
|
||||
}
|
||||
|
||||
static void unplugDevice(LG_USBRedir * usbredir)
|
||||
@@ -139,6 +146,7 @@ static void destroyParser(LG_USBRedir * usbredir)
|
||||
setAvailable(usbredir, false);
|
||||
unplugDevice(usbredir);
|
||||
usbredir->disconnectPending = false;
|
||||
usbredir->disconnectDeadline = 0;
|
||||
|
||||
if (!usbredir->parser)
|
||||
return;
|
||||
@@ -149,8 +157,20 @@ static void destroyParser(LG_USBRedir * usbredir)
|
||||
|
||||
static bool flushUSBRedir(LG_USBRedir * usbredir)
|
||||
{
|
||||
return !usbredir->parser ||
|
||||
usbredirparser_do_write(usbredir->parser) == 0;
|
||||
if (!usbredir->parser)
|
||||
return true;
|
||||
|
||||
if (usbredirparser_do_write(usbredir->parser) != 0)
|
||||
return false;
|
||||
|
||||
const uint64_t buffered =
|
||||
usbredirparser_get_bufferered_output_size(usbredir->parser);
|
||||
if (buffered <= USB_REDIR_MAX_OUTPUT_BYTES)
|
||||
return true;
|
||||
|
||||
DEBUG_ERROR("USB redirection output queue exceeded %u bytes",
|
||||
(unsigned int)USB_REDIR_MAX_OUTPUT_BYTES);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool connectChannel(LG_USBRedir * usbredir,
|
||||
@@ -166,10 +186,10 @@ static bool connectChannel(LG_USBRedir * usbredir,
|
||||
return false;
|
||||
}
|
||||
|
||||
static void selectChannel(LG_USBRedir * usbredir)
|
||||
static bool selectChannel(LG_USBRedir * usbredir)
|
||||
{
|
||||
if (usbredir->channel)
|
||||
return;
|
||||
return true;
|
||||
|
||||
for (unsigned int i = 0; i < USB_REDIR_CHANNEL_COUNT; ++i)
|
||||
{
|
||||
@@ -178,8 +198,28 @@ static void selectChannel(LG_USBRedir * usbredir)
|
||||
continue;
|
||||
|
||||
if (connectChannel(usbredir, channel))
|
||||
return;
|
||||
{
|
||||
usbredir->reconnectDeadline = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
usbredir->reconnectDeadline =
|
||||
(int64_t)nanotime() + USB_REDIR_RECONNECT_DELAY_NS;
|
||||
return false;
|
||||
}
|
||||
|
||||
static void resetChannel(LG_USBRedir * usbredir)
|
||||
{
|
||||
PSUSBRedirChannel * channel = usbredir->channel;
|
||||
usbredir->channel = NULL;
|
||||
destroyParser(usbredir);
|
||||
|
||||
if (channel && purespice_usbRedirConnected(channel))
|
||||
purespice_usbRedirDisconnect(channel);
|
||||
|
||||
usbredir->reconnectDeadline =
|
||||
(int64_t)nanotime() + USB_REDIR_RECONNECT_DELAY_NS;
|
||||
}
|
||||
|
||||
static bool createParser(LG_USBRedir * usbredir)
|
||||
@@ -268,14 +308,37 @@ bool lgUsbRedir_disconnectPending(const LG_USBRedir * usbredir)
|
||||
return usbredir->disconnectPending;
|
||||
}
|
||||
|
||||
bool lgUsbRedir_process(LG_USBRedir * usbredir)
|
||||
static bool processUSBRedir(LG_USBRedir * usbredir, bool recover)
|
||||
{
|
||||
const int64_t now = (int64_t)nanotime();
|
||||
if (!usbredir->channel &&
|
||||
now >= usbredir->reconnectDeadline)
|
||||
selectChannel(usbredir);
|
||||
|
||||
if (!usbredir->parser ||
|
||||
!atomic_load_explicit(&usbredir->available, memory_order_acquire))
|
||||
return flushUSBRedir(usbredir);
|
||||
{
|
||||
const bool result = flushUSBRedir(usbredir);
|
||||
if (!result && recover)
|
||||
resetChannel(usbredir);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (usbredir->disconnectPending)
|
||||
return flushUSBRedir(usbredir);
|
||||
{
|
||||
if (usbredir->disconnectDeadline &&
|
||||
now >= usbredir->disconnectDeadline)
|
||||
{
|
||||
DEBUG_WARN("USB redirection disconnect acknowledgement timed out");
|
||||
resetChannel(usbredir);
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool result = flushUSBRedir(usbredir);
|
||||
if (!result && recover)
|
||||
resetChannel(usbredir);
|
||||
return result;
|
||||
}
|
||||
|
||||
const bool desired = atomic_load_explicit(&usbredir->desiredPlugged,
|
||||
memory_order_acquire);
|
||||
@@ -292,6 +355,8 @@ bool lgUsbRedir_process(LG_USBRedir * usbredir)
|
||||
unplugDevice(usbredir);
|
||||
usbredir->disconnectPending = usbredirparser_peer_has_cap(
|
||||
usbredir->parser, usb_redir_cap_device_disconnect_ack);
|
||||
usbredir->disconnectDeadline = usbredir->disconnectPending ?
|
||||
now + USB_REDIR_DISCONNECT_TIMEOUT_NS : 0;
|
||||
usbredirparser_send_device_disconnect(usbredir->parser);
|
||||
}
|
||||
}
|
||||
@@ -299,7 +364,15 @@ bool lgUsbRedir_process(LG_USBRedir * usbredir)
|
||||
if (usbredir->plugged && usbredir->deviceOps->process)
|
||||
usbredir->deviceOps->process(usbredir->deviceOpaque);
|
||||
|
||||
return flushUSBRedir(usbredir);
|
||||
const bool result = flushUSBRedir(usbredir);
|
||||
if (!result && recover)
|
||||
resetChannel(usbredir);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool lgUsbRedir_process(LG_USBRedir * usbredir)
|
||||
{
|
||||
return processUSBRedir(usbredir, true);
|
||||
}
|
||||
|
||||
void lgUsbRedir_state(PSUSBRedirChannel * channel,
|
||||
@@ -328,7 +401,13 @@ void lgUsbRedir_state(PSUSBRedirChannel * channel,
|
||||
|
||||
case PS_USB_REDIR_DISCONNECTED:
|
||||
if (channel == usbredir->channel)
|
||||
{
|
||||
destroyParser(usbredir);
|
||||
usbredir->channel = NULL;
|
||||
if (purespice_usbRedirAvailable(channel))
|
||||
usbredir->reconnectDeadline =
|
||||
(int64_t)nanotime() + USB_REDIR_RECONNECT_DELAY_NS;
|
||||
}
|
||||
break;
|
||||
|
||||
case PS_USB_REDIR_UNAVAILABLE:
|
||||
@@ -375,7 +454,9 @@ bool lgUsbRedir_data(PSUSBRedirChannel * channel,
|
||||
return false;
|
||||
}
|
||||
|
||||
return lgUsbRedir_process(usbredir);
|
||||
/* Returning false lets PureSpice disconnect after this callback unwinds;
|
||||
* resetting it here would destroy the channel during its own dispatch. */
|
||||
return processUSBRedir(usbredir, false);
|
||||
}
|
||||
|
||||
void * lgUsbRedir_device(void * parserOpaque)
|
||||
|
||||
Reference in New Issue
Block a user