mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-23 15:41:31 +00:00
Do not treat the capacity of an allocated PipeWire buffer as the requested playback period when PipeWire supplies no per-buffer hint. Use the current stream quantum when available and fall back to the configured period.
1960 lines
57 KiB
C
1960 lines
57 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
|
|
*/
|
|
|
|
#include "interface/audiodev.h"
|
|
|
|
#include <spa/param/audio/format-utils.h>
|
|
#include <spa/param/buffers.h>
|
|
#include <spa/param/props.h>
|
|
#include <spa/utils/result.h>
|
|
#include <pipewire/pipewire.h>
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
#include <math.h>
|
|
#include <semaphore.h>
|
|
#include <stddef.h>
|
|
#include <stdatomic.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "common/debug.h"
|
|
#include "common/stringutils.h"
|
|
#include "common/util.h"
|
|
#include "common/option.h"
|
|
#include "common/ringbuffer.h"
|
|
#include "common/thread.h"
|
|
#include "common/time.h"
|
|
|
|
typedef enum
|
|
{
|
|
STREAM_STATE_INACTIVE,
|
|
STREAM_STATE_ACTIVE
|
|
}
|
|
StreamState;
|
|
|
|
typedef enum
|
|
{
|
|
RECORD_LATEST_FREE,
|
|
RECORD_LATEST_WRITING,
|
|
RECORD_LATEST_READY,
|
|
RECORD_LATEST_READING
|
|
}
|
|
RecordLatestState;
|
|
|
|
typedef struct
|
|
{
|
|
int frames;
|
|
bool clockValid;
|
|
LG_AudioClock clock;
|
|
}
|
|
RecordBlock;
|
|
|
|
#define RECORD_LATEST_SLOTS 2
|
|
#define RECORD_CLOCK_TOLERANCE_FRAMES 2.0
|
|
#define RECORD_ERROR_REPORT_INTERVAL_NS INT64_C(5000000000)
|
|
#define PIPEWIRE_CONNECT_TIMEOUT_NS INT64_C(5000000000)
|
|
|
|
struct PipeWire
|
|
{
|
|
struct pw_loop * loop;
|
|
struct pw_context * context;
|
|
struct pw_core * core;
|
|
struct pw_thread_loop * thread;
|
|
|
|
struct
|
|
{
|
|
struct pw_stream * stream;
|
|
_Atomic(struct spa_io_rate_match *) rateMatch;
|
|
_Atomic(int64_t) latencyNs;
|
|
atomic_bool latencyUpdateRequested;
|
|
atomic_uint bufferErrors;
|
|
atomic_uint pullErrors;
|
|
atomic_uint timingErrors;
|
|
atomic_int streamError;
|
|
#if PW_CHECK_VERSION(1, 4, 0)
|
|
double appliedResampleRatio;
|
|
atomic_int resampleError;
|
|
#endif
|
|
enum pw_stream_state connectionState;
|
|
bool resamplerEnabled;
|
|
|
|
LG_AudioFormat format;
|
|
int stride;
|
|
LG_AudioPullFn pullFn;
|
|
LG_AudioFailureFn failureFn;
|
|
uint32_t failureCookie;
|
|
int maxPeriodFrames;
|
|
int startFrames;
|
|
|
|
atomic_uint state;
|
|
}
|
|
playback;
|
|
|
|
struct
|
|
{
|
|
struct pw_stream * stream;
|
|
enum pw_stream_state connectionState;
|
|
|
|
LG_AudioFormat format;
|
|
int stride;
|
|
LG_AudioPushFn pushFn;
|
|
LG_AudioFailureFn failureFn;
|
|
uint32_t failureCookie;
|
|
|
|
RingBuffer sendQueue;
|
|
RingBuffer sendBlocks;
|
|
uint8_t * sendBuffer;
|
|
int sendBufferFrames;
|
|
int maxQueuedFrames;
|
|
sem_t sendWake;
|
|
bool sendWakeInitialized;
|
|
LGThread * sendThread;
|
|
atomic_bool sendStop;
|
|
atomic_bool sendWakePending;
|
|
atomic_bool latestMode;
|
|
atomic_uint_fast64_t latestSerial;
|
|
uint8_t * latestBuffer;
|
|
struct
|
|
{
|
|
atomic_uint state;
|
|
atomic_uint_fast64_t serial;
|
|
int frames;
|
|
bool clockValid;
|
|
LG_AudioClock clock;
|
|
}
|
|
latest[RECORD_LATEST_SLOTS];
|
|
atomic_uint bufferErrors;
|
|
atomic_uint droppedFrames;
|
|
atomic_uint rejectedBatches;
|
|
atomic_uint rejectedFrames;
|
|
atomic_uint signalErrors;
|
|
atomic_int recycleError;
|
|
|
|
uint64_t clockPosition;
|
|
uint64_t clockLastPosition;
|
|
uint64_t clockLastTicks;
|
|
int64_t clockLastTime;
|
|
struct spa_fraction clockLastRate;
|
|
bool clockLastValid;
|
|
bool clockLastTimingValid;
|
|
bool clockDiscontinuityPending;
|
|
bool active;
|
|
}
|
|
record;
|
|
};
|
|
|
|
static struct PipeWire pw = {0};
|
|
|
|
static inline void pipewire_storeError(atomic_int * target, int result)
|
|
{
|
|
if (result >= 0)
|
|
return;
|
|
|
|
int expected = 0;
|
|
atomic_compare_exchange_strong_explicit(
|
|
target, &expected, result,
|
|
memory_order_relaxed, memory_order_relaxed);
|
|
}
|
|
|
|
static bool pipewire_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,
|
|
a->channelCount * sizeof(*a->channels)) == 0;
|
|
}
|
|
|
|
static enum spa_audio_channel pipewire_channel(
|
|
LG_AudioChannel channel, uint8_t index)
|
|
{
|
|
switch (channel)
|
|
{
|
|
case LG_AUDIO_CH_UNKNOWN:
|
|
return (enum spa_audio_channel)(SPA_AUDIO_CHANNEL_AUX0 + index);
|
|
case LG_AUDIO_CH_MONO : return SPA_AUDIO_CHANNEL_MONO;
|
|
case LG_AUDIO_CH_FRONT_LEFT : return SPA_AUDIO_CHANNEL_FL;
|
|
case LG_AUDIO_CH_FRONT_RIGHT : return SPA_AUDIO_CHANNEL_FR;
|
|
case LG_AUDIO_CH_FRONT_CENTER : return SPA_AUDIO_CHANNEL_FC;
|
|
case LG_AUDIO_CH_LFE : return SPA_AUDIO_CHANNEL_LFE;
|
|
case LG_AUDIO_CH_REAR_LEFT : return SPA_AUDIO_CHANNEL_RL;
|
|
case LG_AUDIO_CH_REAR_RIGHT : return SPA_AUDIO_CHANNEL_RR;
|
|
case LG_AUDIO_CH_FRONT_LEFT_CENTER : return SPA_AUDIO_CHANNEL_FLC;
|
|
case LG_AUDIO_CH_FRONT_RIGHT_CENTER : return SPA_AUDIO_CHANNEL_FRC;
|
|
case LG_AUDIO_CH_REAR_CENTER : return SPA_AUDIO_CHANNEL_RC;
|
|
case LG_AUDIO_CH_SIDE_LEFT : return SPA_AUDIO_CHANNEL_SL;
|
|
case LG_AUDIO_CH_SIDE_RIGHT : return SPA_AUDIO_CHANNEL_SR;
|
|
case LG_AUDIO_CH_TOP_CENTER : return SPA_AUDIO_CHANNEL_TC;
|
|
case LG_AUDIO_CH_TOP_FRONT_LEFT : return SPA_AUDIO_CHANNEL_TFL;
|
|
case LG_AUDIO_CH_TOP_FRONT_CENTER : return SPA_AUDIO_CHANNEL_TFC;
|
|
case LG_AUDIO_CH_TOP_FRONT_RIGHT : return SPA_AUDIO_CHANNEL_TFR;
|
|
case LG_AUDIO_CH_TOP_REAR_LEFT : return SPA_AUDIO_CHANNEL_TRL;
|
|
case LG_AUDIO_CH_TOP_REAR_CENTER : return SPA_AUDIO_CHANNEL_TRC;
|
|
case LG_AUDIO_CH_TOP_REAR_RIGHT : return SPA_AUDIO_CHANNEL_TRR;
|
|
}
|
|
|
|
return (enum spa_audio_channel)(SPA_AUDIO_CHANNEL_AUX0 + index);
|
|
}
|
|
|
|
static enum spa_audio_format pipewire_sampleFormat(
|
|
LG_AudioSampleFormat format)
|
|
{
|
|
switch (format)
|
|
{
|
|
case LG_AUDIO_FMT_U8 : return SPA_AUDIO_FORMAT_U8;
|
|
case LG_AUDIO_FMT_S16_LE : return SPA_AUDIO_FORMAT_S16_LE;
|
|
case LG_AUDIO_FMT_S24_LE : return SPA_AUDIO_FORMAT_S24_LE;
|
|
case LG_AUDIO_FMT_S32_LE : return SPA_AUDIO_FORMAT_S32_LE;
|
|
case LG_AUDIO_FMT_F32_LE : return SPA_AUDIO_FORMAT_F32_LE;
|
|
case LG_AUDIO_FMT_F32_NE : return SPA_AUDIO_FORMAT_F32;
|
|
case LG_AUDIO_FMT_F64_LE : return SPA_AUDIO_FORMAT_F64_LE;
|
|
}
|
|
|
|
return SPA_AUDIO_FORMAT_UNKNOWN;
|
|
}
|
|
|
|
static int pipewire_sampleSize(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 struct spa_audio_info_raw pipewire_audioInfo(
|
|
const LG_AudioFormat * format, enum spa_audio_format sampleFormat)
|
|
{
|
|
struct spa_audio_info_raw info = SPA_AUDIO_INFO_RAW_INIT(
|
|
.format = sampleFormat,
|
|
.channels = format->channelCount,
|
|
.rate = format->sampleRate
|
|
);
|
|
|
|
for (uint8_t i = 0; i < format->channelCount; ++i)
|
|
info.position[i] = pipewire_channel(format->channels[i], i);
|
|
|
|
return info;
|
|
}
|
|
|
|
static void pipewire_reportPlaybackErrors(void)
|
|
{
|
|
const unsigned int bufferErrors = atomic_exchange_explicit(
|
|
&pw.playback.bufferErrors, 0, memory_order_relaxed);
|
|
const unsigned int pullErrors = atomic_exchange_explicit(
|
|
&pw.playback.pullErrors, 0, memory_order_relaxed);
|
|
const unsigned int timingErrors = atomic_exchange_explicit(
|
|
&pw.playback.timingErrors, 0, memory_order_relaxed);
|
|
const int streamError = atomic_exchange_explicit(
|
|
&pw.playback.streamError, 0, memory_order_relaxed);
|
|
#if PW_CHECK_VERSION(1, 4, 0)
|
|
const int resampleError = atomic_exchange_explicit(
|
|
&pw.playback.resampleError, 0, memory_order_relaxed);
|
|
#endif
|
|
|
|
if (bufferErrors)
|
|
DEBUG_WARN("PipeWire playback ran out of buffers %u time(s)",
|
|
bufferErrors);
|
|
if (pullErrors)
|
|
DEBUG_WARN("PipeWire playback returned an invalid frame count %u time(s)",
|
|
pullErrors);
|
|
if (timingErrors)
|
|
DEBUG_WARN("PipeWire playback timing query failed %u time(s)",
|
|
timingErrors);
|
|
if (streamError)
|
|
DEBUG_WARN("PipeWire playback stream operation failed: %s",
|
|
spa_strerror(streamError));
|
|
#if PW_CHECK_VERSION(1, 4, 0)
|
|
if (resampleError)
|
|
DEBUG_WARN("PipeWire resampler rate update failed: %s",
|
|
spa_strerror(resampleError));
|
|
#endif
|
|
}
|
|
|
|
static void pipewire_reportRecordErrors(void)
|
|
{
|
|
const unsigned int bufferErrors = atomic_exchange_explicit(
|
|
&pw.record.bufferErrors, 0, memory_order_relaxed);
|
|
const unsigned int droppedFrames = atomic_exchange_explicit(
|
|
&pw.record.droppedFrames, 0, memory_order_relaxed);
|
|
const unsigned int rejectedBatches = atomic_exchange_explicit(
|
|
&pw.record.rejectedBatches, 0, memory_order_relaxed);
|
|
const unsigned int rejectedFrames = atomic_exchange_explicit(
|
|
&pw.record.rejectedFrames, 0, memory_order_relaxed);
|
|
const unsigned int signalErrors = atomic_exchange_explicit(
|
|
&pw.record.signalErrors, 0, memory_order_relaxed);
|
|
const int recycleError = atomic_exchange_explicit(
|
|
&pw.record.recycleError, 0, memory_order_relaxed);
|
|
|
|
if (bufferErrors)
|
|
DEBUG_WARN("PipeWire recording encountered %u buffer error(s)",
|
|
bufferErrors);
|
|
if (droppedFrames)
|
|
DEBUG_WARN("PipeWire recording dropped %u frame(s)", droppedFrames);
|
|
if (rejectedFrames)
|
|
DEBUG_WARN("PipeWire recording provider rejected %u frame(s) "
|
|
"in %u batch(es)", rejectedFrames, rejectedBatches);
|
|
if (signalErrors)
|
|
DEBUG_WARN("PipeWire recording worker notification failed %u time(s)",
|
|
signalErrors);
|
|
if (recycleError)
|
|
DEBUG_WARN("PipeWire recording buffer recycle failed: %s",
|
|
spa_strerror(recycleError));
|
|
}
|
|
|
|
static void pipewire_reportRecordErrorsDue(uint64_t * nextReport)
|
|
{
|
|
const uint64_t now = nanotime();
|
|
if (now < *nextReport)
|
|
return;
|
|
|
|
pipewire_reportRecordErrors();
|
|
*nextReport = now + RECORD_ERROR_REPORT_INTERVAL_NS;
|
|
}
|
|
|
|
#if !PW_CHECK_VERSION(1, 1, 0)
|
|
static int64_t pipewire_monotonicTime(void)
|
|
{
|
|
struct timespec time;
|
|
clock_gettime(CLOCK_MONOTONIC, &time);
|
|
return SPA_TIMESPEC_TO_NSEC(&time);
|
|
}
|
|
#endif
|
|
|
|
static inline void pipewire_updatePlaybackLatency(void)
|
|
{
|
|
if (!atomic_exchange_explicit(
|
|
&pw.playback.latencyUpdateRequested, false,
|
|
memory_order_acquire))
|
|
return;
|
|
|
|
struct pw_time time;
|
|
#if PW_CHECK_VERSION(0, 3, 50)
|
|
if (pw_stream_get_time_n(pw.playback.stream, &time, sizeof(time)) < 0)
|
|
#else
|
|
if (pw_stream_get_time(pw.playback.stream, &time) < 0)
|
|
#endif
|
|
{
|
|
atomic_fetch_add_explicit(
|
|
&pw.playback.timingErrors, 1, memory_order_relaxed);
|
|
return;
|
|
}
|
|
|
|
if (time.rate.num == 0 || time.rate.denom == 0)
|
|
return;
|
|
|
|
#if PW_CHECK_VERSION(1, 1, 0)
|
|
const int64_t now = (int64_t)pw_stream_get_nsec(pw.playback.stream);
|
|
#else
|
|
const int64_t now = pipewire_monotonicTime();
|
|
#endif
|
|
const int64_t elapsedNs = max(INT64_C(0), now - time.now);
|
|
const double graphLatencyNs =
|
|
(double)time.delay * time.rate.num *
|
|
SPA_NSEC_PER_SEC / time.rate.denom - elapsedNs;
|
|
#if PW_CHECK_VERSION(0, 3, 50)
|
|
const double streamFrames = time.queued + time.buffered;
|
|
#else
|
|
const double streamFrames =
|
|
(double)time.queued / pw.playback.stride;
|
|
#endif
|
|
const double streamLatencyNs = streamFrames * SPA_NSEC_PER_SEC /
|
|
pw.playback.format.sampleRate;
|
|
const int64_t latencyNs =
|
|
llrint(max(0.0, graphLatencyNs + streamLatencyNs));
|
|
atomic_store_explicit(
|
|
&pw.playback.latencyNs, latencyNs, memory_order_release);
|
|
}
|
|
|
|
#if PW_CHECK_VERSION(1, 4, 0)
|
|
static bool pipewire_playbackSetRate(double * ratio)
|
|
{
|
|
if (!ratio || *ratio <= 0.0 ||
|
|
!pw.playback.resamplerEnabled)
|
|
return false;
|
|
if (*ratio == pw.playback.appliedResampleRatio)
|
|
return true;
|
|
|
|
const int result = pw_stream_set_rate(pw.playback.stream, *ratio);
|
|
if (result < 0)
|
|
{
|
|
int expected = 0;
|
|
atomic_compare_exchange_strong_explicit(
|
|
&pw.playback.resampleError, &expected, result,
|
|
memory_order_relaxed, memory_order_relaxed);
|
|
return false;
|
|
}
|
|
|
|
pw.playback.appliedResampleRatio = *ratio;
|
|
return true;
|
|
}
|
|
|
|
static bool pipewire_configurePlaybackResampler(
|
|
bool enable, bool * reusable)
|
|
{
|
|
const bool wasEnabled = pw.playback.resamplerEnabled;
|
|
const int result = pw_stream_set_rate(
|
|
pw.playback.stream, enable ? 1.0 : 0.0);
|
|
if (reusable)
|
|
*reusable = result >= 0 || !wasEnabled;
|
|
if (result < 0)
|
|
return false;
|
|
|
|
pw.playback.resamplerEnabled = enable;
|
|
pw.playback.appliedResampleRatio = enable ? 1.0 : 0.0;
|
|
return enable;
|
|
}
|
|
#endif
|
|
|
|
static void pipewire_onPlaybackIoChanged(void * userdata, uint32_t id,
|
|
void * data, uint32_t size)
|
|
{
|
|
switch (id)
|
|
{
|
|
case SPA_IO_RateMatch:
|
|
atomic_store_explicit(&pw.playback.rateMatch,
|
|
data && size >= offsetof(struct spa_io_rate_match, size) +
|
|
sizeof(((struct spa_io_rate_match *)0)->size) ? data : NULL,
|
|
memory_order_release);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void pipewire_onPlaybackStateChanged(void * userdata,
|
|
enum pw_stream_state old, enum pw_stream_state state,
|
|
const char * error)
|
|
{
|
|
(void)error;
|
|
pw.playback.connectionState = state;
|
|
if ((state == PW_STREAM_STATE_ERROR ||
|
|
state == PW_STREAM_STATE_UNCONNECTED) &&
|
|
pw.playback.failureFn)
|
|
{
|
|
LG_AudioFailureFn failureFn = pw.playback.failureFn;
|
|
const uint32_t failureCookie = pw.playback.failureCookie;
|
|
pw.playback.failureFn = NULL;
|
|
pw.playback.failureCookie = 0;
|
|
failureFn(failureCookie);
|
|
}
|
|
pw_thread_loop_signal(pw.thread, false);
|
|
}
|
|
|
|
static void pipewire_onPlaybackProcess(void * userdata)
|
|
{
|
|
struct pw_buffer * pbuf;
|
|
|
|
if (!(pbuf = pw_stream_dequeue_buffer(pw.playback.stream)))
|
|
{
|
|
atomic_fetch_add_explicit(
|
|
&pw.playback.bufferErrors, 1, memory_order_relaxed);
|
|
return;
|
|
}
|
|
|
|
struct spa_buffer * sbuf = pbuf->buffer;
|
|
uint8_t * dst;
|
|
|
|
if (!sbuf || sbuf->n_datas == 0 || !sbuf->datas ||
|
|
!sbuf->datas[0].chunk ||
|
|
!(dst = sbuf->datas[0].data))
|
|
{
|
|
#if PW_CHECK_VERSION(1, 4, 0)
|
|
const int recycleResult =
|
|
pw_stream_return_buffer(pw.playback.stream, pbuf);
|
|
#else
|
|
const int recycleResult =
|
|
pw_stream_queue_buffer(pw.playback.stream, pbuf);
|
|
#endif
|
|
pipewire_storeError(&pw.playback.streamError, recycleResult);
|
|
return;
|
|
}
|
|
|
|
int frames =
|
|
sbuf->datas[0].maxsize / pw.playback.stride;
|
|
struct spa_io_rate_match * rateMatch = atomic_load_explicit(
|
|
&pw.playback.rateMatch, memory_order_acquire);
|
|
if (rateMatch && rateMatch->size > 0)
|
|
frames = min(frames, rateMatch->size);
|
|
#if PW_CHECK_VERSION(0, 3, 50)
|
|
else if (pbuf->requested > 0)
|
|
frames = min(frames, pbuf->requested);
|
|
#endif
|
|
else
|
|
{
|
|
#if PW_CHECK_VERSION(1, 1, 0)
|
|
struct pw_time time = {0};
|
|
if (pw_stream_get_time_n(
|
|
pw.playback.stream, &time, sizeof(time)) >= 0 && time.size > 0)
|
|
frames = (int)min((uint64_t)frames, time.size);
|
|
else
|
|
#endif
|
|
frames = min(frames, pw.playback.maxPeriodFrames);
|
|
}
|
|
|
|
const int requestedFrames = frames;
|
|
frames = pw.playback.pullFn(dst, requestedFrames);
|
|
if (frames < 0 || frames > requestedFrames)
|
|
{
|
|
atomic_fetch_add_explicit(
|
|
&pw.playback.pullErrors, 1, memory_order_relaxed);
|
|
frames = clamp(frames, 0, requestedFrames);
|
|
}
|
|
pipewire_updatePlaybackLatency();
|
|
if (!frames)
|
|
{
|
|
pbuf->size = 0;
|
|
sbuf->datas[0].chunk->offset = 0;
|
|
sbuf->datas[0].chunk->stride = pw.playback.stride;
|
|
sbuf->datas[0].chunk->size = 0;
|
|
pipewire_storeError(&pw.playback.streamError,
|
|
pw_stream_queue_buffer(pw.playback.stream, pbuf));
|
|
return;
|
|
}
|
|
|
|
pbuf->size = frames;
|
|
sbuf->datas[0].chunk->offset = 0;
|
|
sbuf->datas[0].chunk->stride = pw.playback.stride;
|
|
sbuf->datas[0].chunk->size = frames * pw.playback.stride;
|
|
|
|
pipewire_storeError(&pw.playback.streamError,
|
|
pw_stream_queue_buffer(pw.playback.stream, pbuf));
|
|
}
|
|
|
|
static struct Option pipewire_options[] =
|
|
{
|
|
{
|
|
.module = "pipewire",
|
|
.name = "outDevice",
|
|
.description = "The default playback device to use",
|
|
.type = OPTION_TYPE_STRING
|
|
},
|
|
{
|
|
.module = "pipewire",
|
|
.name = "recDevice",
|
|
.description = "The default record device to use",
|
|
.type = OPTION_TYPE_STRING
|
|
},
|
|
|
|
{0}
|
|
};
|
|
|
|
static void pipewire_earlyInit(void)
|
|
{
|
|
option_register(pipewire_options);
|
|
}
|
|
|
|
static bool pipewire_waitForStream(enum pw_stream_state * state)
|
|
{
|
|
int result = 0;
|
|
#if PW_CHECK_VERSION(0, 3, 7)
|
|
struct timespec deadline;
|
|
result = pw_thread_loop_get_time(
|
|
pw.thread, &deadline, PIPEWIRE_CONNECT_TIMEOUT_NS);
|
|
if (result < 0)
|
|
return false;
|
|
|
|
while (*state == PW_STREAM_STATE_CONNECTING && result >= 0)
|
|
result = pw_thread_loop_timed_wait_full(pw.thread, &deadline);
|
|
#else
|
|
while (*state == PW_STREAM_STATE_CONNECTING && result >= 0)
|
|
result = pw_thread_loop_timed_wait(pw.thread, 5);
|
|
#endif
|
|
return result >= 0;
|
|
}
|
|
|
|
static bool pipewire_init(void)
|
|
{
|
|
pw_init(NULL, NULL);
|
|
|
|
pw.loop = pw_loop_new(NULL);
|
|
if (!pw.loop)
|
|
{
|
|
DEBUG_ERROR("Failed to create a PipeWire loop");
|
|
goto err;
|
|
}
|
|
#if PW_CHECK_VERSION(1, 3, 81)
|
|
pw.context = pw_context_new(
|
|
pw.loop,
|
|
NULL,
|
|
0);
|
|
#else
|
|
pw.context = pw_context_new(
|
|
pw.loop,
|
|
pw_properties_new(
|
|
// Request real-time priority on the PipeWire threads
|
|
PW_KEY_CONFIG_NAME, "client-rt.conf",
|
|
NULL
|
|
),
|
|
0);
|
|
#endif
|
|
if (!pw.context)
|
|
{
|
|
DEBUG_ERROR("Failed to create a context");
|
|
goto err;
|
|
}
|
|
|
|
/* this is just to test for PipeWire availabillity */
|
|
pw.core = pw_context_connect(pw.context, NULL, 0);
|
|
if (!pw.core)
|
|
goto err_context;
|
|
|
|
/* PipeWire is available so create the loop thread and start it */
|
|
pw.thread = pw_thread_loop_new_full(pw.loop, "PipeWire", NULL);
|
|
if (!pw.thread)
|
|
{
|
|
DEBUG_ERROR("Failed to create the thread loop");
|
|
goto err_core;
|
|
}
|
|
|
|
const int result = pw_thread_loop_start(pw.thread);
|
|
if (result < 0)
|
|
{
|
|
DEBUG_ERROR("Failed to start the PipeWire thread loop: %s",
|
|
spa_strerror(result));
|
|
pw_thread_loop_destroy(pw.thread);
|
|
pw.thread = NULL;
|
|
goto err_core;
|
|
}
|
|
return true;
|
|
|
|
err_core:
|
|
pw_core_disconnect(pw.core);
|
|
pw.core = NULL;
|
|
|
|
err_context:
|
|
pw_context_destroy(pw.context);
|
|
pw.context = NULL;
|
|
|
|
err:
|
|
if (pw.loop)
|
|
pw_loop_destroy(pw.loop);
|
|
pw.loop = NULL;
|
|
pw_deinit();
|
|
return false;
|
|
}
|
|
|
|
static void pipewire_playbackStopStream(void)
|
|
{
|
|
if (!pw.playback.stream)
|
|
{
|
|
pipewire_reportPlaybackErrors();
|
|
return;
|
|
}
|
|
|
|
pw_thread_loop_lock(pw.thread);
|
|
atomic_store_explicit(
|
|
&pw.playback.rateMatch, NULL, memory_order_release);
|
|
pw.playback.failureFn = NULL;
|
|
pw.playback.failureCookie = 0;
|
|
pw_stream_destroy(pw.playback.stream);
|
|
pw.playback.stream = NULL;
|
|
pw.playback.resamplerEnabled = false;
|
|
atomic_store_explicit(&pw.playback.state,
|
|
STREAM_STATE_INACTIVE, memory_order_release);
|
|
atomic_store_explicit(
|
|
&pw.playback.latencyNs, 0, memory_order_release);
|
|
atomic_store_explicit(
|
|
&pw.playback.latencyUpdateRequested, false, memory_order_relaxed);
|
|
pw_thread_loop_unlock(pw.thread);
|
|
pipewire_reportPlaybackErrors();
|
|
}
|
|
|
|
static bool pipewire_playbackSetup(const LG_AudioFormat * format,
|
|
int requestedPeriodFrames, bool requestResampler,
|
|
bool * resamplerEnabled, int * maxPeriodFrames, int * startFrames,
|
|
LG_AudioPullFn pullFn)
|
|
{
|
|
*resamplerEnabled = false;
|
|
|
|
const int channels = format->channelCount;
|
|
const int sampleRate = format->sampleRate;
|
|
const int sampleSize =
|
|
pipewire_sampleSize(format->sampleFormat);
|
|
const enum spa_audio_format sampleFormat =
|
|
pipewire_sampleFormat(format->sampleFormat);
|
|
if (!sampleSize || sampleFormat == SPA_AUDIO_FORMAT_UNKNOWN)
|
|
return false;
|
|
|
|
const struct spa_pod * params[1];
|
|
uint8_t buffer[1024];
|
|
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
|
static const struct pw_stream_events events =
|
|
{
|
|
.version = PW_VERSION_STREAM_EVENTS,
|
|
.state_changed = pipewire_onPlaybackStateChanged,
|
|
.io_changed = pipewire_onPlaybackIoChanged,
|
|
.process = pipewire_onPlaybackProcess
|
|
};
|
|
|
|
bool reuse = false;
|
|
pw_thread_loop_lock(pw.thread);
|
|
if (pw.playback.stream &&
|
|
pw.playback.connectionState == PW_STREAM_STATE_PAUSED &&
|
|
atomic_load_explicit(
|
|
&pw.playback.state, memory_order_acquire) == STREAM_STATE_INACTIVE &&
|
|
pipewire_audioFormatEqual(&pw.playback.format, format))
|
|
{
|
|
pw.playback.pullFn = pullFn;
|
|
bool resamplerReusable = true;
|
|
#if PW_CHECK_VERSION(1, 4, 0)
|
|
atomic_store_explicit(
|
|
&pw.playback.resampleError, 0, memory_order_relaxed);
|
|
*resamplerEnabled =
|
|
pipewire_configurePlaybackResampler(
|
|
requestResampler, &resamplerReusable);
|
|
#endif
|
|
if (resamplerReusable)
|
|
{
|
|
*maxPeriodFrames = pw.playback.maxPeriodFrames;
|
|
*startFrames = pw.playback.startFrames;
|
|
reuse = true;
|
|
}
|
|
}
|
|
pw_thread_loop_unlock(pw.thread);
|
|
if (reuse)
|
|
return true;
|
|
|
|
pipewire_playbackStopStream();
|
|
|
|
char requestedNodeLatency[32];
|
|
snprintf(requestedNodeLatency, sizeof(requestedNodeLatency), "%d/%d",
|
|
requestedPeriodFrames, sampleRate);
|
|
|
|
pw.playback.format = *format;
|
|
pw.playback.stride = sampleSize * channels;
|
|
pw.playback.pullFn = pullFn;
|
|
|
|
pw_thread_loop_lock(pw.thread);
|
|
atomic_store_explicit(
|
|
&pw.playback.rateMatch, NULL, memory_order_release);
|
|
|
|
struct pw_properties * props =
|
|
pw_properties_new(
|
|
PW_KEY_APP_NAME , "Looking Glass",
|
|
PW_KEY_NODE_NAME , "Looking Glass",
|
|
PW_KEY_MEDIA_TYPE , "Audio",
|
|
PW_KEY_MEDIA_CATEGORY, "Playback",
|
|
PW_KEY_MEDIA_ROLE , "Music",
|
|
PW_KEY_NODE_LATENCY , requestedNodeLatency,
|
|
NULL
|
|
);
|
|
if (!props)
|
|
{
|
|
pw_thread_loop_unlock(pw.thread);
|
|
DEBUG_ERROR("Failed to create playback stream properties");
|
|
return false;
|
|
}
|
|
|
|
const char * device = option_get_string("pipewire", "outDevice");
|
|
if (device)
|
|
{
|
|
#ifdef PW_KEY_TARGET_OBJECT
|
|
pw_properties_set(props, PW_KEY_TARGET_OBJECT, device);
|
|
#else
|
|
pw_properties_set(props, PW_KEY_NODE_TARGET, device);
|
|
#endif
|
|
}
|
|
|
|
pw.playback.stream = pw_stream_new_simple(
|
|
pw.loop,
|
|
"Looking Glass",
|
|
props,
|
|
&events,
|
|
NULL
|
|
);
|
|
|
|
if (!pw.playback.stream)
|
|
{
|
|
pw_thread_loop_unlock(pw.thread);
|
|
DEBUG_ERROR("Failed to create the stream");
|
|
return false;
|
|
}
|
|
|
|
// The user can override the default node latency with the PIPEWIRE_LATENCY
|
|
// environment variable, so get the actual node latency value from the stream.
|
|
// The actual quantum size may be lower than this value depending on what else
|
|
// is using the audio device, but we can treat this value as a maximum
|
|
const struct pw_properties * properties =
|
|
pw_stream_get_properties(pw.playback.stream);
|
|
const char * actualNodeLatency = properties ?
|
|
pw_properties_get(properties, PW_KEY_NODE_LATENCY) : NULL;
|
|
|
|
unsigned num, denom;
|
|
uint64_t actualPeriodFrames = 0;
|
|
if (!actualNodeLatency ||
|
|
sscanf(actualNodeLatency, "%u/%u", &num, &denom) != 2 ||
|
|
num == 0 || denom == 0 ||
|
|
(actualPeriodFrames =
|
|
((uint64_t)num * sampleRate + denom - 1) / denom) > INT_MAX)
|
|
{
|
|
DEBUG_WARN(
|
|
"PIPEWIRE_LATENCY value '%s' is invalid; using %d/%d",
|
|
actualNodeLatency ? actualNodeLatency : "(unset)", requestedPeriodFrames,
|
|
sampleRate);
|
|
|
|
struct spa_dict_item items[] = {
|
|
{ PW_KEY_NODE_LATENCY, requestedNodeLatency }
|
|
};
|
|
pw_stream_update_properties(pw.playback.stream,
|
|
&SPA_DICT_INIT_ARRAY(items));
|
|
|
|
pw.playback.maxPeriodFrames = requestedPeriodFrames;
|
|
}
|
|
else
|
|
pw.playback.maxPeriodFrames = (int)actualPeriodFrames;
|
|
|
|
// If the previous quantum size was very small, PipeWire can request two full
|
|
// periods almost immediately at the start of playback
|
|
pw.playback.startFrames = (int)min(
|
|
(int64_t)pw.playback.maxPeriodFrames * 2, (int64_t)INT_MAX);
|
|
|
|
*maxPeriodFrames = pw.playback.maxPeriodFrames;
|
|
*startFrames = pw.playback.startFrames;
|
|
|
|
struct spa_audio_info_raw info =
|
|
pipewire_audioInfo(format, sampleFormat);
|
|
params[0] = spa_format_audio_raw_build(
|
|
&b, SPA_PARAM_EnumFormat, &info);
|
|
|
|
pw.playback.connectionState = PW_STREAM_STATE_CONNECTING;
|
|
const int result = pw_stream_connect(
|
|
pw.playback.stream,
|
|
PW_DIRECTION_OUTPUT,
|
|
PW_ID_ANY,
|
|
PW_STREAM_FLAG_AUTOCONNECT |
|
|
PW_STREAM_FLAG_MAP_BUFFERS |
|
|
PW_STREAM_FLAG_RT_PROCESS |
|
|
PW_STREAM_FLAG_INACTIVE,
|
|
params, 1);
|
|
|
|
if (result < 0)
|
|
{
|
|
DEBUG_ERROR("Failed to connect playback stream: %s", spa_strerror(result));
|
|
atomic_store_explicit(
|
|
&pw.playback.rateMatch, NULL, memory_order_release);
|
|
pw_stream_destroy(pw.playback.stream);
|
|
pw.playback.stream = NULL;
|
|
pw_thread_loop_unlock(pw.thread);
|
|
return false;
|
|
}
|
|
|
|
if (!pipewire_waitForStream(&pw.playback.connectionState) ||
|
|
pw.playback.connectionState != PW_STREAM_STATE_PAUSED)
|
|
{
|
|
DEBUG_ERROR("PipeWire playback stream did not become ready");
|
|
atomic_store_explicit(
|
|
&pw.playback.rateMatch, NULL, memory_order_release);
|
|
pw_stream_destroy(pw.playback.stream);
|
|
pw.playback.stream = NULL;
|
|
pw_thread_loop_unlock(pw.thread);
|
|
return false;
|
|
}
|
|
|
|
atomic_store_explicit(&pw.playback.state,
|
|
STREAM_STATE_INACTIVE, memory_order_release);
|
|
atomic_store_explicit(
|
|
&pw.playback.latencyNs, 0, memory_order_release);
|
|
atomic_store_explicit(
|
|
&pw.playback.latencyUpdateRequested, false, memory_order_relaxed);
|
|
atomic_store_explicit(
|
|
&pw.playback.bufferErrors, 0, memory_order_relaxed);
|
|
atomic_store_explicit(
|
|
&pw.playback.pullErrors, 0, memory_order_relaxed);
|
|
atomic_store_explicit(
|
|
&pw.playback.timingErrors, 0, memory_order_relaxed);
|
|
atomic_store_explicit(
|
|
&pw.playback.streamError, 0, memory_order_relaxed);
|
|
#if PW_CHECK_VERSION(1, 4, 0)
|
|
atomic_store_explicit(
|
|
&pw.playback.resampleError, 0, memory_order_relaxed);
|
|
*resamplerEnabled =
|
|
pipewire_configurePlaybackResampler(
|
|
requestResampler, NULL);
|
|
#else
|
|
(void)requestResampler;
|
|
#endif
|
|
pw_thread_loop_unlock(pw.thread);
|
|
return true;
|
|
}
|
|
|
|
static int pipewire_playbackControl(bool active,
|
|
LG_AudioFailureFn failureFn, uint32_t failureCookie)
|
|
{
|
|
int error = 0;
|
|
pw_thread_loop_lock(pw.thread);
|
|
if (!active)
|
|
{
|
|
pw.playback.failureFn = NULL;
|
|
pw.playback.failureCookie = 0;
|
|
}
|
|
|
|
if (!pw.playback.stream)
|
|
error = active ? -ENODEV : 0;
|
|
else if (active)
|
|
{
|
|
if (pw.playback.connectionState == PW_STREAM_STATE_ERROR ||
|
|
pw.playback.connectionState == PW_STREAM_STATE_UNCONNECTED)
|
|
error = -EPIPE;
|
|
else
|
|
{
|
|
pw.playback.failureFn = failureFn;
|
|
pw.playback.failureCookie = failureCookie;
|
|
if (atomic_load_explicit(
|
|
&pw.playback.state, memory_order_acquire) != STREAM_STATE_ACTIVE)
|
|
{
|
|
/* A stopped stream must not replay data queued before reactivation. */
|
|
error = pw_stream_flush(pw.playback.stream, false);
|
|
if (error >= 0)
|
|
error = pw_stream_set_active(pw.playback.stream, true);
|
|
}
|
|
if (error >= 0)
|
|
{
|
|
atomic_store_explicit(&pw.playback.state,
|
|
STREAM_STATE_ACTIVE, memory_order_release);
|
|
}
|
|
}
|
|
|
|
if (error < 0 ||
|
|
pw.playback.connectionState == PW_STREAM_STATE_ERROR ||
|
|
pw.playback.connectionState == PW_STREAM_STATE_UNCONNECTED)
|
|
{
|
|
pw.playback.failureFn = NULL;
|
|
pw.playback.failureCookie = 0;
|
|
if (error >= 0)
|
|
error = -EPIPE;
|
|
}
|
|
}
|
|
else if (atomic_load_explicit(
|
|
&pw.playback.state, memory_order_acquire) == STREAM_STATE_ACTIVE)
|
|
{
|
|
const int activeError =
|
|
pw_stream_set_active(pw.playback.stream, false);
|
|
const int flushError =
|
|
pw_stream_flush(pw.playback.stream, false);
|
|
|
|
error = activeError < 0 ? activeError : flushError;
|
|
if (activeError >= 0)
|
|
atomic_store_explicit(&pw.playback.state,
|
|
STREAM_STATE_INACTIVE, memory_order_release);
|
|
}
|
|
|
|
pw_thread_loop_unlock(pw.thread);
|
|
if (error < 0)
|
|
{
|
|
int expected = 0;
|
|
atomic_compare_exchange_strong_explicit(
|
|
&pw.playback.streamError, &expected, error,
|
|
memory_order_relaxed, memory_order_relaxed);
|
|
}
|
|
return error;
|
|
}
|
|
|
|
static bool pipewire_playbackStart(
|
|
LG_AudioFailureFn failureFn, uint32_t failureCookie)
|
|
{
|
|
return pipewire_playbackControl(
|
|
true, failureFn, failureCookie) >= 0;
|
|
}
|
|
|
|
static void pipewire_playbackStop(void)
|
|
{
|
|
if (pipewire_playbackControl(false, NULL, 0) < 0)
|
|
{
|
|
pipewire_playbackStopStream();
|
|
return;
|
|
}
|
|
atomic_store_explicit(
|
|
&pw.playback.latencyNs, 0, memory_order_release);
|
|
atomic_store_explicit(
|
|
&pw.playback.latencyUpdateRequested, false, memory_order_relaxed);
|
|
}
|
|
|
|
static void pipewire_playbackVolume(int channels, const uint16_t volume[])
|
|
{
|
|
if (channels <= 0 || channels > LG_AUDIO_MAX_CHANNELS)
|
|
return;
|
|
|
|
float param[channels];
|
|
for(int i = 0; i < channels; ++i)
|
|
param[i] = max(0.0,
|
|
9.3234e-7 * pow(1.000211902, volume[i]) - 0.000172787);
|
|
|
|
int result = 0;
|
|
pw_thread_loop_lock(pw.thread);
|
|
if (pw.playback.stream &&
|
|
channels == pw.playback.format.channelCount)
|
|
result = pw_stream_set_control(pw.playback.stream,
|
|
SPA_PROP_channelVolumes, channels, param, 0);
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
if (result < 0)
|
|
DEBUG_WARN("Failed to set PipeWire playback volume: %s",
|
|
spa_strerror(result));
|
|
}
|
|
|
|
static void pipewire_playbackMute(bool mute)
|
|
{
|
|
int result = 0;
|
|
pw_thread_loop_lock(pw.thread);
|
|
if (pw.playback.stream)
|
|
{
|
|
float val = mute ? 1.0f : 0.0f;
|
|
result = pw_stream_set_control(
|
|
pw.playback.stream, SPA_PROP_mute, 1, &val, 0);
|
|
}
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
if (result < 0)
|
|
DEBUG_WARN("Failed to set PipeWire playback mute: %s",
|
|
spa_strerror(result));
|
|
}
|
|
|
|
static uint64_t pipewire_playbackLatency(void)
|
|
{
|
|
atomic_store_explicit(
|
|
&pw.playback.latencyUpdateRequested, true, memory_order_release);
|
|
|
|
const int64_t latencyNs = atomic_load_explicit(
|
|
&pw.playback.latencyNs, memory_order_acquire);
|
|
if (latencyNs <= 0)
|
|
return 0;
|
|
|
|
return latencyNs / 1000;
|
|
}
|
|
|
|
static void pipewire_recordSignalSender(void)
|
|
{
|
|
if (atomic_exchange_explicit(
|
|
&pw.record.sendWakePending, true, memory_order_acq_rel))
|
|
return;
|
|
|
|
if (sem_post(&pw.record.sendWake) < 0)
|
|
{
|
|
atomic_fetch_add_explicit(
|
|
&pw.record.signalErrors, 1, memory_order_relaxed);
|
|
if (errno != EOVERFLOW)
|
|
atomic_store_explicit(
|
|
&pw.record.sendWakePending, false, memory_order_release);
|
|
}
|
|
}
|
|
|
|
static void pipewire_recordRejected(int frames)
|
|
{
|
|
atomic_fetch_add_explicit(
|
|
&pw.record.rejectedBatches, 1, memory_order_relaxed);
|
|
atomic_fetch_add_explicit(
|
|
&pw.record.rejectedFrames, frames, memory_order_relaxed);
|
|
}
|
|
|
|
static void pipewire_recordAdvanceClock(
|
|
LG_AudioClock * clock, int frames)
|
|
{
|
|
const double rate = clock->rate > 0.0 ?
|
|
clock->rate : pw.record.format.sampleRate;
|
|
clock->position += frames;
|
|
if (rate > 0.0)
|
|
clock->time += (int64_t)llrint(
|
|
(double)frames * SPA_NSEC_PER_SEC / rate);
|
|
clock->discontinuity = false;
|
|
}
|
|
|
|
static bool pipewire_recordClockJump(
|
|
double elapsedNs, double expectedNs)
|
|
{
|
|
const double tolerance =
|
|
RECORD_CLOCK_TOLERANCE_FRAMES * SPA_NSEC_PER_SEC /
|
|
pw.record.format.sampleRate;
|
|
return elapsedNs <= 0 ||
|
|
fabs(elapsedNs - expectedNs) > tolerance;
|
|
}
|
|
|
|
static RecordBlock pipewire_recordMakeBlock(
|
|
struct pw_buffer * pbuf, const struct spa_buffer * sbuf, int frames)
|
|
{
|
|
RecordBlock block =
|
|
{
|
|
.frames = frames
|
|
};
|
|
struct pw_time timing = {0};
|
|
#if PW_CHECK_VERSION(0, 3, 50)
|
|
const int timingResult = pw_stream_get_time_n(
|
|
pw.record.stream, &timing, sizeof(timing));
|
|
#else
|
|
const int timingResult = pw_stream_get_time(
|
|
pw.record.stream, &timing);
|
|
#endif
|
|
const bool timingValid = timingResult >= 0 &&
|
|
timing.rate.num != 0 && timing.rate.denom != 0;
|
|
|
|
int64_t clockTime = 0;
|
|
#if PW_CHECK_VERSION(1, 0, 5)
|
|
if (pbuf->time)
|
|
clockTime = (int64_t)pbuf->time;
|
|
#endif
|
|
if (!clockTime && timingResult >= 0)
|
|
clockTime = timing.now;
|
|
|
|
const uint64_t position = pw.record.clockPosition;
|
|
pw.record.clockPosition += frames;
|
|
|
|
bool discontinuity = pw.record.clockDiscontinuityPending;
|
|
const struct spa_meta_header * header = spa_buffer_find_meta_data(
|
|
sbuf, SPA_META_Header, sizeof(*header));
|
|
if (header &&
|
|
(header->flags & (SPA_META_HEADER_FLAG_DISCONT |
|
|
SPA_META_HEADER_FLAG_CORRUPTED)))
|
|
discontinuity = true;
|
|
if (sbuf->datas[0].chunk->flags & SPA_CHUNK_FLAG_CORRUPTED)
|
|
discontinuity = true;
|
|
|
|
block.clockValid = clockTime > 0;
|
|
if (block.clockValid)
|
|
{
|
|
const bool clockStable = pw.record.clockLastValid;
|
|
if (pw.record.clockLastValid)
|
|
{
|
|
const uint64_t elapsedFrames =
|
|
position - pw.record.clockLastPosition;
|
|
const double expectedNs =
|
|
(double)elapsedFrames * SPA_NSEC_PER_SEC /
|
|
pw.record.format.sampleRate;
|
|
if (pipewire_recordClockJump(
|
|
clockTime - pw.record.clockLastTime, expectedNs))
|
|
discontinuity = true;
|
|
}
|
|
|
|
if (timingValid && pw.record.clockLastTimingValid)
|
|
{
|
|
if (timing.rate.num != pw.record.clockLastRate.num ||
|
|
timing.rate.denom != pw.record.clockLastRate.denom ||
|
|
timing.ticks <= pw.record.clockLastTicks)
|
|
discontinuity = true;
|
|
else
|
|
{
|
|
const double elapsedTickNs =
|
|
(double)(timing.ticks - pw.record.clockLastTicks) *
|
|
timing.rate.num * SPA_NSEC_PER_SEC / timing.rate.denom;
|
|
const double expectedNs =
|
|
(double)(position - pw.record.clockLastPosition) *
|
|
SPA_NSEC_PER_SEC / pw.record.format.sampleRate;
|
|
if (pipewire_recordClockJump(elapsedTickNs, expectedNs))
|
|
discontinuity = true;
|
|
}
|
|
}
|
|
|
|
block.clock = (LG_AudioClock)
|
|
{
|
|
.position = position,
|
|
.time = clockTime,
|
|
.rate = 0.0,
|
|
.stable = clockStable && !discontinuity,
|
|
.discontinuity = discontinuity
|
|
};
|
|
pw.record.clockDiscontinuityPending = false;
|
|
pw.record.clockLastPosition = position;
|
|
pw.record.clockLastTime = clockTime;
|
|
pw.record.clockLastValid = true;
|
|
}
|
|
else
|
|
{
|
|
pw.record.clockDiscontinuityPending |= discontinuity;
|
|
pw.record.clockLastValid = false;
|
|
}
|
|
|
|
if (timingValid)
|
|
{
|
|
pw.record.clockLastTicks = timing.ticks;
|
|
pw.record.clockLastRate = timing.rate;
|
|
pw.record.clockLastTimingValid = block.clockValid;
|
|
}
|
|
else
|
|
pw.record.clockLastTimingValid = false;
|
|
|
|
return block;
|
|
}
|
|
|
|
static bool pipewire_recordLatestPending(void)
|
|
{
|
|
for (int i = 0; i < RECORD_LATEST_SLOTS; ++i)
|
|
if (atomic_load_explicit(
|
|
&pw.record.latest[i].state, memory_order_acquire) !=
|
|
RECORD_LATEST_FREE)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool pipewire_recordSendLatest(void)
|
|
{
|
|
int chosen = -1;
|
|
uint64_t chosenSerial = 0;
|
|
for (int i = 0; i < RECORD_LATEST_SLOTS; ++i)
|
|
{
|
|
if (atomic_load_explicit(
|
|
&pw.record.latest[i].state, memory_order_acquire) !=
|
|
RECORD_LATEST_READY)
|
|
continue;
|
|
|
|
const uint64_t serial = atomic_load_explicit(
|
|
&pw.record.latest[i].serial, memory_order_relaxed);
|
|
if (chosen < 0 || serial > chosenSerial)
|
|
{
|
|
chosen = i;
|
|
chosenSerial = serial;
|
|
}
|
|
}
|
|
|
|
if (chosen < 0)
|
|
return false;
|
|
|
|
unsigned int expected = RECORD_LATEST_READY;
|
|
if (!atomic_compare_exchange_strong_explicit(
|
|
&pw.record.latest[chosen].state, &expected,
|
|
RECORD_LATEST_READING, memory_order_acq_rel, memory_order_acquire))
|
|
return true;
|
|
|
|
/* Only the newest overload block is useful. Drop older blocks before
|
|
* delivering it so recovery cannot build another backlog. */
|
|
for (int i = 0; i < RECORD_LATEST_SLOTS; ++i)
|
|
{
|
|
if (i == chosen ||
|
|
atomic_load_explicit(
|
|
&pw.record.latest[i].serial, memory_order_relaxed) > chosenSerial)
|
|
continue;
|
|
|
|
expected = RECORD_LATEST_READY;
|
|
if (atomic_compare_exchange_strong_explicit(
|
|
&pw.record.latest[i].state, &expected,
|
|
RECORD_LATEST_READING,
|
|
memory_order_acq_rel, memory_order_acquire))
|
|
{
|
|
const int frames = pw.record.latest[i].frames;
|
|
atomic_store_explicit(&pw.record.latest[i].state,
|
|
RECORD_LATEST_FREE, memory_order_release);
|
|
atomic_fetch_add_explicit(&pw.record.droppedFrames,
|
|
frames, memory_order_relaxed);
|
|
}
|
|
}
|
|
|
|
const int frames = pw.record.latest[chosen].frames;
|
|
const LG_AudioClock * clock =
|
|
pw.record.latest[chosen].clockValid ?
|
|
&pw.record.latest[chosen].clock : NULL;
|
|
if (!pw.record.pushFn(
|
|
pw.record.latestBuffer +
|
|
(size_t)chosen * pw.record.sendBufferFrames * pw.record.stride,
|
|
frames, clock))
|
|
pipewire_recordRejected(frames);
|
|
atomic_store_explicit(&pw.record.latest[chosen].state,
|
|
RECORD_LATEST_FREE, memory_order_release);
|
|
return true;
|
|
}
|
|
|
|
static int pipewire_recordSendThread(void * opaque)
|
|
{
|
|
uint64_t nextErrorReport = nanotime() +
|
|
RECORD_ERROR_REPORT_INTERVAL_NS;
|
|
|
|
for (;;)
|
|
{
|
|
int result;
|
|
do
|
|
result = sem_wait(&pw.record.sendWake);
|
|
while (result < 0 && errno == EINTR);
|
|
|
|
if (result < 0)
|
|
{
|
|
DEBUG_ERROR("Failed to wait for the PipeWire recording worker");
|
|
break;
|
|
}
|
|
|
|
/* Clear before draining. A producer racing before this point is covered by
|
|
* this wake, while a producer racing after it publishes the sole next
|
|
* wake. */
|
|
atomic_store_explicit(
|
|
&pw.record.sendWakePending, false, memory_order_release);
|
|
if (atomic_load_explicit(&pw.record.sendStop, memory_order_acquire))
|
|
break;
|
|
|
|
for (;;)
|
|
{
|
|
pipewire_reportRecordErrorsDue(&nextErrorReport);
|
|
|
|
if (atomic_load_explicit(
|
|
&pw.record.sendStop, memory_order_acquire))
|
|
break;
|
|
|
|
if (atomic_load_explicit(
|
|
&pw.record.latestMode, memory_order_acquire))
|
|
{
|
|
const int queued = ringbuffer_getCount(pw.record.sendQueue);
|
|
if (queued > 0)
|
|
{
|
|
ringbuffer_consume(pw.record.sendQueue, NULL, queued);
|
|
atomic_fetch_add_explicit(
|
|
&pw.record.droppedFrames, queued, memory_order_relaxed);
|
|
}
|
|
const int queuedBlocks = ringbuffer_getCount(pw.record.sendBlocks);
|
|
if (queuedBlocks > 0)
|
|
ringbuffer_consume(
|
|
pw.record.sendBlocks, NULL, queuedBlocks);
|
|
|
|
while (!atomic_load_explicit(
|
|
&pw.record.sendStop, memory_order_acquire) &&
|
|
pipewire_recordSendLatest())
|
|
;
|
|
|
|
atomic_store_explicit(
|
|
&pw.record.latestMode, false, memory_order_release);
|
|
if (pipewire_recordLatestPending())
|
|
{
|
|
atomic_store_explicit(
|
|
&pw.record.latestMode, true, memory_order_release);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
RecordBlock block;
|
|
if (ringbuffer_consume(
|
|
pw.record.sendBlocks, &block, 1) != 1)
|
|
break;
|
|
|
|
LG_AudioClock clock = block.clock;
|
|
int remaining = block.frames;
|
|
while (remaining > 0 && !atomic_load_explicit(
|
|
&pw.record.sendStop, memory_order_acquire))
|
|
{
|
|
const int frames = min(
|
|
remaining, pw.record.sendBufferFrames);
|
|
const int consumed = ringbuffer_consume(
|
|
pw.record.sendQueue, pw.record.sendBuffer, frames);
|
|
DEBUG_ASSERT(consumed == frames);
|
|
if (consumed != frames)
|
|
{
|
|
atomic_fetch_add_explicit(
|
|
&pw.record.bufferErrors, 1, memory_order_relaxed);
|
|
break;
|
|
}
|
|
|
|
if (!pw.record.pushFn(pw.record.sendBuffer, frames,
|
|
block.clockValid ? &clock : NULL))
|
|
pipewire_recordRejected(frames);
|
|
remaining -= frames;
|
|
if (block.clockValid)
|
|
pipewire_recordAdvanceClock(&clock, frames);
|
|
}
|
|
}
|
|
|
|
if (atomic_load_explicit(
|
|
&pw.record.sendStop, memory_order_acquire))
|
|
break;
|
|
}
|
|
|
|
pipewire_reportRecordErrors();
|
|
return 0;
|
|
}
|
|
|
|
static void pipewire_recordStopSender(void)
|
|
{
|
|
const bool hadThread = pw.record.sendThread != NULL;
|
|
if (pw.record.sendThread)
|
|
{
|
|
atomic_store_explicit(
|
|
&pw.record.sendStop, true, memory_order_release);
|
|
pipewire_recordSignalSender();
|
|
lgJoinThread(pw.record.sendThread, NULL);
|
|
pw.record.sendThread = NULL;
|
|
}
|
|
|
|
if (pw.record.sendWakeInitialized)
|
|
{
|
|
sem_destroy(&pw.record.sendWake);
|
|
pw.record.sendWakeInitialized = false;
|
|
}
|
|
|
|
ringbuffer_free(&pw.record.sendQueue);
|
|
ringbuffer_free(&pw.record.sendBlocks);
|
|
free(pw.record.sendBuffer);
|
|
free(pw.record.latestBuffer);
|
|
pw.record.sendBuffer = NULL;
|
|
pw.record.latestBuffer = NULL;
|
|
pw.record.sendBufferFrames = 0;
|
|
pw.record.maxQueuedFrames = 0;
|
|
if (!hadThread)
|
|
pipewire_reportRecordErrors();
|
|
}
|
|
|
|
static bool pipewire_recordStartSender(int sampleRate)
|
|
{
|
|
/* This is overload capacity, not a prebuffer. The sender drains whatever is
|
|
* available as soon as the RT callback transitions the queue from empty. */
|
|
const int queueFrames = max(sampleRate / 10, 1);
|
|
const int sendFrames = max(sampleRate / 100, 1);
|
|
|
|
pw.record.sendQueue = ringbuffer_new(
|
|
queueFrames, pw.record.stride);
|
|
pw.record.sendBlocks = ringbuffer_new(
|
|
queueFrames, sizeof(RecordBlock));
|
|
pw.record.sendBuffer = malloc(
|
|
(size_t)sendFrames * pw.record.stride);
|
|
pw.record.latestBuffer = malloc(
|
|
(size_t)RECORD_LATEST_SLOTS * sendFrames * pw.record.stride);
|
|
pw.record.sendBufferFrames = sendFrames;
|
|
pw.record.maxQueuedFrames = max(sampleRate / 50, 1);
|
|
if (!pw.record.sendQueue || !pw.record.sendBlocks ||
|
|
!pw.record.sendBuffer || !pw.record.latestBuffer)
|
|
{
|
|
DEBUG_ERROR("Failed to allocate the PipeWire recording queue");
|
|
pipewire_recordStopSender();
|
|
return false;
|
|
}
|
|
|
|
if (sem_init(&pw.record.sendWake, 0, 0) < 0)
|
|
{
|
|
DEBUG_ERROR("Failed to create the PipeWire recording worker semaphore");
|
|
pipewire_recordStopSender();
|
|
return false;
|
|
}
|
|
pw.record.sendWakeInitialized = true;
|
|
|
|
atomic_store_explicit(
|
|
&pw.record.sendStop, false, memory_order_relaxed);
|
|
atomic_store_explicit(
|
|
&pw.record.sendWakePending, false, memory_order_relaxed);
|
|
atomic_store_explicit(
|
|
&pw.record.latestMode, false, memory_order_relaxed);
|
|
atomic_store_explicit(
|
|
&pw.record.latestSerial, 0, memory_order_relaxed);
|
|
for (int i = 0; i < RECORD_LATEST_SLOTS; ++i)
|
|
{
|
|
atomic_store_explicit(&pw.record.latest[i].state,
|
|
RECORD_LATEST_FREE, memory_order_relaxed);
|
|
atomic_store_explicit(
|
|
&pw.record.latest[i].serial, 0, memory_order_relaxed);
|
|
pw.record.latest[i].frames = 0;
|
|
pw.record.latest[i].clockValid = false;
|
|
pw.record.latest[i].clock = (LG_AudioClock) {0};
|
|
}
|
|
pw.record.clockPosition = 0;
|
|
pw.record.clockLastPosition = 0;
|
|
pw.record.clockLastTicks = 0;
|
|
pw.record.clockLastTime = 0;
|
|
pw.record.clockLastRate = (struct spa_fraction) {0};
|
|
pw.record.clockLastValid = false;
|
|
pw.record.clockLastTimingValid = false;
|
|
pw.record.clockDiscontinuityPending = false;
|
|
atomic_store_explicit(
|
|
&pw.record.bufferErrors, 0, memory_order_relaxed);
|
|
atomic_store_explicit(
|
|
&pw.record.droppedFrames, 0, memory_order_relaxed);
|
|
atomic_store_explicit(
|
|
&pw.record.rejectedBatches, 0, memory_order_relaxed);
|
|
atomic_store_explicit(
|
|
&pw.record.rejectedFrames, 0, memory_order_relaxed);
|
|
atomic_store_explicit(
|
|
&pw.record.signalErrors, 0, memory_order_relaxed);
|
|
atomic_store_explicit(
|
|
&pw.record.recycleError, 0, memory_order_relaxed);
|
|
if (!lgCreateThread("pwRecordSend", pipewire_recordSendThread,
|
|
NULL, &pw.record.sendThread))
|
|
{
|
|
pipewire_recordStopSender();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void pipewire_recordStopStream(void)
|
|
{
|
|
pw_thread_loop_lock(pw.thread);
|
|
pw.record.failureFn = NULL;
|
|
pw.record.failureCookie = 0;
|
|
if (pw.record.stream)
|
|
{
|
|
pw_stream_destroy(pw.record.stream);
|
|
pw.record.stream = NULL;
|
|
}
|
|
pw.record.active = false;
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
pipewire_recordStopSender();
|
|
pw.record.pushFn = NULL;
|
|
}
|
|
|
|
static void pipewire_recordQueueFrames(
|
|
const void * data, const RecordBlock * block)
|
|
{
|
|
const int frames = block->frames;
|
|
int chosen = -1;
|
|
if (atomic_load_explicit(
|
|
&pw.record.latestMode, memory_order_acquire))
|
|
goto latest;
|
|
|
|
const int occupancy = ringbuffer_getCount(pw.record.sendQueue);
|
|
const int available =
|
|
max(0, ringbuffer_getLength(pw.record.sendQueue) - occupancy);
|
|
const bool exceedsBacklog = occupancy > 0 &&
|
|
(occupancy >= pw.record.maxQueuedFrames ||
|
|
frames > pw.record.maxQueuedFrames - occupancy);
|
|
if (frames > available || exceedsBacklog)
|
|
goto latest;
|
|
|
|
const int advanced =
|
|
ringbuffer_append(pw.record.sendQueue, data, frames);
|
|
DEBUG_ASSERT(advanced == frames);
|
|
if (advanced == frames)
|
|
{
|
|
const int blocks = ringbuffer_append(
|
|
pw.record.sendBlocks, block, 1);
|
|
DEBUG_ASSERT(blocks == 1);
|
|
if (blocks == 1)
|
|
{
|
|
pipewire_recordSignalSender();
|
|
return;
|
|
}
|
|
}
|
|
|
|
latest:
|
|
for (int i = 0; i < RECORD_LATEST_SLOTS; ++i)
|
|
{
|
|
unsigned int expected = RECORD_LATEST_FREE;
|
|
if (atomic_compare_exchange_strong_explicit(
|
|
&pw.record.latest[i].state, &expected,
|
|
RECORD_LATEST_WRITING, memory_order_acq_rel, memory_order_acquire))
|
|
{
|
|
chosen = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (chosen < 0)
|
|
{
|
|
int oldest = -1;
|
|
uint64_t oldestSerial = UINT64_MAX;
|
|
for (int i = 0; i < RECORD_LATEST_SLOTS; ++i)
|
|
{
|
|
if (atomic_load_explicit(
|
|
&pw.record.latest[i].state, memory_order_acquire) !=
|
|
RECORD_LATEST_READY)
|
|
continue;
|
|
|
|
const uint64_t serial = atomic_load_explicit(
|
|
&pw.record.latest[i].serial, memory_order_relaxed);
|
|
if (serial < oldestSerial)
|
|
{
|
|
oldest = i;
|
|
oldestSerial = serial;
|
|
}
|
|
}
|
|
|
|
if (oldest >= 0)
|
|
{
|
|
unsigned int expected = RECORD_LATEST_READY;
|
|
if (atomic_compare_exchange_strong_explicit(
|
|
&pw.record.latest[oldest].state, &expected,
|
|
RECORD_LATEST_WRITING,
|
|
memory_order_acq_rel, memory_order_acquire))
|
|
{
|
|
chosen = oldest;
|
|
const int dropped = pw.record.latest[oldest].frames;
|
|
atomic_fetch_add_explicit(&pw.record.droppedFrames,
|
|
dropped, memory_order_relaxed);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (chosen < 0)
|
|
{
|
|
atomic_fetch_add_explicit(
|
|
&pw.record.droppedFrames, frames, memory_order_relaxed);
|
|
pw.record.clockDiscontinuityPending = true;
|
|
atomic_store_explicit(
|
|
&pw.record.latestMode, true, memory_order_release);
|
|
pipewire_recordSignalSender();
|
|
return;
|
|
}
|
|
|
|
const int keep = min(frames, pw.record.sendBufferFrames);
|
|
memcpy(
|
|
pw.record.latestBuffer +
|
|
(size_t)chosen * pw.record.sendBufferFrames * pw.record.stride,
|
|
(const uint8_t *)data + (size_t)(frames - keep) * pw.record.stride,
|
|
(size_t)keep * pw.record.stride);
|
|
pw.record.latest[chosen].frames = keep;
|
|
pw.record.latest[chosen].clockValid = block->clockValid;
|
|
if (block->clockValid)
|
|
{
|
|
pw.record.latest[chosen].clock = block->clock;
|
|
pipewire_recordAdvanceClock(
|
|
&pw.record.latest[chosen].clock, frames - keep);
|
|
pw.record.latest[chosen].clock.discontinuity = true;
|
|
pw.record.latest[chosen].clock.stable = false;
|
|
}
|
|
atomic_store_explicit(&pw.record.latest[chosen].serial,
|
|
atomic_fetch_add_explicit(
|
|
&pw.record.latestSerial, 1, memory_order_relaxed) + 1,
|
|
memory_order_relaxed);
|
|
atomic_store_explicit(&pw.record.latest[chosen].state,
|
|
RECORD_LATEST_READY, memory_order_release);
|
|
atomic_store_explicit(
|
|
&pw.record.latestMode, true, memory_order_release);
|
|
|
|
if (keep != frames)
|
|
atomic_fetch_add_explicit(
|
|
&pw.record.droppedFrames, frames - keep, memory_order_relaxed);
|
|
pipewire_recordSignalSender();
|
|
}
|
|
|
|
static void pipewire_onRecordProcess(void * userdata)
|
|
{
|
|
struct pw_buffer * pbuf;
|
|
|
|
if (!(pbuf = pw_stream_dequeue_buffer(pw.record.stream)))
|
|
{
|
|
atomic_fetch_add_explicit(
|
|
&pw.record.bufferErrors, 1, memory_order_relaxed);
|
|
pw.record.clockDiscontinuityPending = true;
|
|
return;
|
|
}
|
|
|
|
struct spa_buffer * sbuf = pbuf->buffer;
|
|
if (!sbuf || sbuf->n_datas == 0 || !sbuf->datas[0].data ||
|
|
!sbuf->datas[0].chunk ||
|
|
sbuf->datas[0].chunk->offset > sbuf->datas[0].maxsize)
|
|
{
|
|
atomic_fetch_add_explicit(
|
|
&pw.record.bufferErrors, 1, memory_order_relaxed);
|
|
pw.record.clockDiscontinuityPending = true;
|
|
#if PW_CHECK_VERSION(1, 4, 0)
|
|
const int recycleResult =
|
|
pw_stream_return_buffer(pw.record.stream, pbuf);
|
|
#else
|
|
const int recycleResult =
|
|
pw_stream_queue_buffer(pw.record.stream, pbuf);
|
|
#endif
|
|
pipewire_storeError(&pw.record.recycleError, recycleResult);
|
|
return;
|
|
}
|
|
|
|
const uint32_t offset = sbuf->datas[0].chunk->offset;
|
|
const uint32_t flags = sbuf->datas[0].chunk->flags;
|
|
const bool empty = flags & SPA_CHUNK_FLAG_EMPTY;
|
|
if (flags & (SPA_CHUNK_FLAG_EMPTY | SPA_CHUNK_FLAG_CORRUPTED))
|
|
pw.record.clockDiscontinuityPending = true;
|
|
|
|
const uint32_t bytes = empty ? 0 : min(
|
|
sbuf->datas[0].chunk->size,
|
|
sbuf->datas[0].maxsize - offset);
|
|
const int frames = bytes / pw.record.stride;
|
|
if (bytes % pw.record.stride)
|
|
pw.record.clockDiscontinuityPending = true;
|
|
if (frames > 0)
|
|
{
|
|
const RecordBlock block =
|
|
pipewire_recordMakeBlock(pbuf, sbuf, frames);
|
|
pipewire_recordQueueFrames(
|
|
(uint8_t *)sbuf->datas[0].data + offset, &block);
|
|
}
|
|
|
|
pipewire_storeError(&pw.record.recycleError,
|
|
pw_stream_queue_buffer(pw.record.stream, pbuf));
|
|
}
|
|
|
|
static void pipewire_onRecordStateChanged(void * userdata,
|
|
enum pw_stream_state old, enum pw_stream_state state,
|
|
const char * error)
|
|
{
|
|
(void)error;
|
|
pw.record.connectionState = state;
|
|
if (state == PW_STREAM_STATE_ERROR ||
|
|
state == PW_STREAM_STATE_UNCONNECTED)
|
|
{
|
|
LG_AudioFailureFn failureFn = pw.record.failureFn;
|
|
const uint32_t failureCookie = pw.record.failureCookie;
|
|
pw.record.failureFn = NULL;
|
|
pw.record.failureCookie = 0;
|
|
|
|
if (failureFn && failureCookie)
|
|
failureFn(failureCookie);
|
|
}
|
|
pw_thread_loop_signal(pw.thread, false);
|
|
}
|
|
|
|
static int pipewire_recordControl(bool active)
|
|
{
|
|
int error = 0;
|
|
pw_thread_loop_lock(pw.thread);
|
|
if (!pw.record.stream)
|
|
error = active ? -ENODEV : 0;
|
|
else if (pw.record.active != active)
|
|
{
|
|
error = pw_stream_set_active(pw.record.stream, active);
|
|
if (error >= 0)
|
|
pw.record.active = active;
|
|
}
|
|
pw_thread_loop_unlock(pw.thread);
|
|
return error;
|
|
}
|
|
|
|
static bool pipewire_recordStart(const LG_AudioFormat * format,
|
|
LG_AudioPushFn pushFn, LG_AudioFailureFn failureFn,
|
|
uint32_t failureCookie)
|
|
{
|
|
pipewire_recordStopStream();
|
|
|
|
const int channels = format->channelCount;
|
|
const int sampleRate = format->sampleRate;
|
|
const int sampleSize = pipewire_sampleSize(format->sampleFormat);
|
|
const enum spa_audio_format sampleFormat =
|
|
pipewire_sampleFormat(format->sampleFormat);
|
|
if (!sampleSize || sampleFormat == SPA_AUDIO_FORMAT_UNKNOWN)
|
|
return false;
|
|
|
|
const int periodFrames = max(sampleRate / 1000, 1);
|
|
char requestedNodeLatency[32];
|
|
snprintf(requestedNodeLatency, sizeof(requestedNodeLatency), "%d/%d",
|
|
periodFrames, sampleRate);
|
|
const struct spa_pod * params[2];
|
|
uint8_t buffer[1024];
|
|
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
|
static const struct pw_stream_events events =
|
|
{
|
|
.version = PW_VERSION_STREAM_EVENTS,
|
|
.state_changed = pipewire_onRecordStateChanged,
|
|
.process = pipewire_onRecordProcess
|
|
};
|
|
|
|
pw.record.format = *format;
|
|
pw.record.stride = sampleSize * channels;
|
|
pw.record.pushFn = pushFn;
|
|
if (!pipewire_recordStartSender(sampleRate))
|
|
return false;
|
|
|
|
struct pw_properties * props =
|
|
pw_properties_new(
|
|
PW_KEY_NODE_NAME , "Looking Glass",
|
|
PW_KEY_MEDIA_TYPE , "Audio",
|
|
PW_KEY_MEDIA_CATEGORY, "Capture",
|
|
PW_KEY_MEDIA_ROLE , "Music",
|
|
PW_KEY_NODE_LATENCY , requestedNodeLatency,
|
|
NULL
|
|
);
|
|
if (!props)
|
|
{
|
|
DEBUG_ERROR("Failed to create recording stream properties");
|
|
pipewire_recordStopSender();
|
|
return false;
|
|
}
|
|
|
|
const char * device = option_get_string("pipewire", "recDevice");
|
|
if (device)
|
|
{
|
|
#ifdef PW_KEY_TARGET_OBJECT
|
|
pw_properties_set(props, PW_KEY_TARGET_OBJECT, device);
|
|
#else
|
|
pw_properties_set(props, PW_KEY_NODE_TARGET, device);
|
|
#endif
|
|
}
|
|
|
|
pw_thread_loop_lock(pw.thread);
|
|
pw.record.failureFn = failureFn;
|
|
pw.record.failureCookie = failureCookie;
|
|
pw.record.stream = pw_stream_new_simple(
|
|
pw.loop,
|
|
"Looking Glass",
|
|
props,
|
|
&events,
|
|
NULL
|
|
);
|
|
|
|
if (!pw.record.stream)
|
|
{
|
|
pw.record.failureFn = NULL;
|
|
pw.record.failureCookie = 0;
|
|
pw_thread_loop_unlock(pw.thread);
|
|
DEBUG_ERROR("Failed to create the stream");
|
|
pipewire_recordStopSender();
|
|
return false;
|
|
}
|
|
|
|
struct spa_audio_info_raw info =
|
|
pipewire_audioInfo(format, sampleFormat);
|
|
params[0] = spa_format_audio_raw_build(
|
|
&b, SPA_PARAM_EnumFormat, &info);
|
|
params[1] = spa_pod_builder_add_object(&b,
|
|
SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
|
|
SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
|
|
SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
|
|
|
|
pw.record.connectionState = PW_STREAM_STATE_CONNECTING;
|
|
int result = pw_stream_connect(
|
|
pw.record.stream,
|
|
PW_DIRECTION_INPUT,
|
|
PW_ID_ANY,
|
|
PW_STREAM_FLAG_AUTOCONNECT |
|
|
PW_STREAM_FLAG_MAP_BUFFERS |
|
|
PW_STREAM_FLAG_RT_PROCESS |
|
|
PW_STREAM_FLAG_INACTIVE,
|
|
params, 2);
|
|
|
|
if (result < 0)
|
|
{
|
|
DEBUG_ERROR("Failed to connect recording stream: %s",
|
|
spa_strerror(result));
|
|
pw.record.failureFn = NULL;
|
|
pw.record.failureCookie = 0;
|
|
pw_stream_destroy(pw.record.stream);
|
|
pw.record.stream = NULL;
|
|
pw_thread_loop_unlock(pw.thread);
|
|
pipewire_recordStopSender();
|
|
return false;
|
|
}
|
|
|
|
if (!pipewire_waitForStream(&pw.record.connectionState) ||
|
|
pw.record.connectionState != PW_STREAM_STATE_PAUSED)
|
|
{
|
|
DEBUG_ERROR("PipeWire recording stream did not become ready");
|
|
pw.record.failureFn = NULL;
|
|
pw.record.failureCookie = 0;
|
|
pw_stream_destroy(pw.record.stream);
|
|
pw.record.stream = NULL;
|
|
pw_thread_loop_unlock(pw.thread);
|
|
pipewire_recordStopSender();
|
|
return false;
|
|
}
|
|
|
|
pw_thread_loop_unlock(pw.thread);
|
|
result = pipewire_recordControl(true);
|
|
if (result < 0)
|
|
{
|
|
DEBUG_ERROR("Failed to activate recording stream: %s",
|
|
spa_strerror(result));
|
|
pipewire_recordStopStream();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void pipewire_recordStop(void)
|
|
{
|
|
pipewire_recordStopStream();
|
|
}
|
|
|
|
static void pipewire_recordVolume(int channels, const uint16_t volume[])
|
|
{
|
|
if (channels <= 0 || channels > LG_AUDIO_MAX_CHANNELS)
|
|
return;
|
|
|
|
float param[channels];
|
|
for(int i = 0; i < channels; ++i)
|
|
param[i] = max(0.0,
|
|
9.3234e-7 * pow(1.000211902, volume[i]) - 0.000172787);
|
|
|
|
int result = 0;
|
|
pw_thread_loop_lock(pw.thread);
|
|
if (pw.record.stream &&
|
|
channels == pw.record.format.channelCount)
|
|
result = pw_stream_set_control(pw.record.stream,
|
|
SPA_PROP_channelVolumes, channels, param, 0);
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
if (result < 0)
|
|
DEBUG_WARN("Failed to set PipeWire recording volume: %s",
|
|
spa_strerror(result));
|
|
}
|
|
|
|
static void pipewire_recordMute(bool mute)
|
|
{
|
|
int result = 0;
|
|
pw_thread_loop_lock(pw.thread);
|
|
if (pw.record.stream)
|
|
{
|
|
float val = mute ? 1.0f : 0.0f;
|
|
result = pw_stream_set_control(
|
|
pw.record.stream, SPA_PROP_mute, 1, &val, 0);
|
|
}
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
if (result < 0)
|
|
DEBUG_WARN("Failed to set PipeWire recording mute: %s",
|
|
spa_strerror(result));
|
|
}
|
|
|
|
static void pipewire_free(void)
|
|
{
|
|
pipewire_playbackStopStream();
|
|
pipewire_recordStopStream();
|
|
|
|
pw_thread_loop_lock(pw.thread);
|
|
if (pw.core)
|
|
{
|
|
pw_core_disconnect(pw.core);
|
|
pw.core = NULL;
|
|
}
|
|
pw_thread_loop_unlock(pw.thread);
|
|
pw_thread_loop_stop(pw.thread);
|
|
pw_thread_loop_destroy(pw.thread);
|
|
pw_context_destroy(pw.context);
|
|
pw_loop_destroy(pw.loop);
|
|
|
|
pw.loop = NULL;
|
|
pw.context = NULL;
|
|
pw.thread = NULL;
|
|
|
|
pw_deinit();
|
|
}
|
|
|
|
struct LG_AudioDevOps LGAD_PipeWire =
|
|
{
|
|
.name = "PipeWire",
|
|
.earlyInit = pipewire_earlyInit,
|
|
.init = pipewire_init,
|
|
.free = pipewire_free,
|
|
.playback =
|
|
{
|
|
.setup = pipewire_playbackSetup,
|
|
.start = pipewire_playbackStart,
|
|
.stop = pipewire_playbackStop,
|
|
.volume = pipewire_playbackVolume,
|
|
.mute = pipewire_playbackMute,
|
|
#if PW_CHECK_VERSION(1, 4, 0)
|
|
.setRate = pipewire_playbackSetRate,
|
|
#endif
|
|
.latency = pipewire_playbackLatency
|
|
},
|
|
.record =
|
|
{
|
|
.start = pipewire_recordStart,
|
|
.stop = pipewire_recordStop,
|
|
.volume = pipewire_recordVolume,
|
|
.mute = pipewire_recordMute
|
|
}
|
|
};
|