mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2025-10-29 05:40:27 -04:00
The eventfd is read/written from/to the data thread and the main thread concurrently with the update_active() function. Use an atomic compare and swap to make this update atomic and avoid an inconsistency between the active boolean and the eventfd. This could result in the eventfd being unsignaled while the active flag was true and the application receiving a timeout and XRun in its poll loop. Fixes #3711
1424 lines
39 KiB
C
1424 lines
39 KiB
C
/* PCM - PipeWire plugin */
|
|
/* SPDX-FileCopyrightText: Copyright © 2017 Wim Taymans */
|
|
/* SPDX-License-Identifier: MIT */
|
|
|
|
#define __USE_GNU
|
|
|
|
#include <limits.h>
|
|
#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
|
|
#include <byteswap.h>
|
|
#endif
|
|
#include <sys/shm.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/mman.h>
|
|
|
|
#include <alsa/asoundlib.h>
|
|
#include <alsa/pcm_external.h>
|
|
|
|
#include <spa/param/audio/format-utils.h>
|
|
#include <spa/debug/types.h>
|
|
#include <spa/param/props.h>
|
|
#include <spa/utils/atomic.h>
|
|
#include <spa/utils/result.h>
|
|
#include <spa/utils/string.h>
|
|
#include <spa/utils/json.h>
|
|
|
|
#include <pipewire/pipewire.h>
|
|
|
|
PW_LOG_TOPIC_STATIC(alsa_log_topic, "alsa.pcm");
|
|
#define PW_LOG_TOPIC_DEFAULT alsa_log_topic
|
|
|
|
#define MIN_BUFFERS 2u
|
|
#define MAX_BUFFERS 64u
|
|
|
|
#define MAX_CHANNELS 64
|
|
#define MAX_RATE (48000*8)
|
|
|
|
#define MIN_PERIOD 64
|
|
|
|
#define MIN_PERIOD_BYTES (128)
|
|
#define MAX_PERIOD_BYTES (2*1024*1024)
|
|
|
|
#define MIN_BUFFER_BYTES (2*MIN_PERIOD_BYTES)
|
|
#define MAX_BUFFER_BYTES (2*MAX_PERIOD_BYTES)
|
|
|
|
typedef struct {
|
|
snd_pcm_ioplug_t io;
|
|
|
|
snd_output_t *output;
|
|
FILE *log_file;
|
|
|
|
int fd;
|
|
int error;
|
|
unsigned int activated:1; /* PipeWire is activated? */
|
|
unsigned int drained:1;
|
|
unsigned int draining:1;
|
|
unsigned int xrun_detected:1;
|
|
unsigned int hw_params_changed:1;
|
|
unsigned int negotiated:1;
|
|
|
|
bool active;
|
|
|
|
snd_pcm_uframes_t hw_ptr;
|
|
snd_pcm_uframes_t boundary;
|
|
snd_pcm_uframes_t min_avail;
|
|
unsigned int sample_bits;
|
|
uint32_t blocks;
|
|
uint32_t stride;
|
|
|
|
struct spa_system *system;
|
|
struct pw_thread_loop *main_loop;
|
|
|
|
struct pw_properties *props;
|
|
struct pw_context *context;
|
|
|
|
struct pw_core *core;
|
|
struct spa_hook core_listener;
|
|
|
|
struct pw_stream *stream;
|
|
struct spa_hook stream_listener;
|
|
|
|
int64_t delay;
|
|
uint64_t transfered;
|
|
uint64_t buffered;
|
|
int64_t now;
|
|
uintptr_t seq;
|
|
|
|
struct spa_audio_info_raw format;
|
|
} snd_pcm_pipewire_t;
|
|
|
|
static int snd_pcm_pipewire_stop(snd_pcm_ioplug_t *io);
|
|
|
|
static int update_active(snd_pcm_ioplug_t *io)
|
|
{
|
|
snd_pcm_pipewire_t *pw = io->private_data;
|
|
snd_pcm_sframes_t avail;
|
|
bool active, old;
|
|
|
|
retry:
|
|
avail = snd_pcm_ioplug_avail(io, pw->hw_ptr, io->appl_ptr);
|
|
|
|
if (pw->error > 0) {
|
|
active = true;
|
|
}
|
|
else if (io->state == SND_PCM_STATE_DRAINING) {
|
|
active = pw->drained;
|
|
}
|
|
else if (avail >= 0 && avail < (snd_pcm_sframes_t)pw->min_avail) {
|
|
active = false;
|
|
}
|
|
else if (avail >= (snd_pcm_sframes_t)pw->min_avail) {
|
|
active = true;
|
|
}
|
|
else {
|
|
active = false;
|
|
}
|
|
old = SPA_ATOMIC_LOAD(pw->active);
|
|
if (old != active) {
|
|
uint64_t val;
|
|
|
|
pw_log_trace("%p: avail:%lu min-avail:%lu state:%s hw:%lu appl:%lu active:%d->%d state:%s",
|
|
pw, avail, pw->min_avail, snd_pcm_state_name(io->state),
|
|
pw->hw_ptr, io->appl_ptr, pw->active, active,
|
|
snd_pcm_state_name(io->state));
|
|
|
|
if (active)
|
|
spa_system_eventfd_write(pw->system, io->poll_fd, 1);
|
|
else
|
|
spa_system_eventfd_read(pw->system, io->poll_fd, &val);
|
|
|
|
if (!SPA_ATOMIC_CAS(pw->active, old, active))
|
|
goto retry;
|
|
}
|
|
return active;
|
|
}
|
|
|
|
static void snd_pcm_pipewire_free(snd_pcm_pipewire_t *pw)
|
|
{
|
|
if (pw == NULL)
|
|
return;
|
|
|
|
pw_log_debug("%p: free", pw);
|
|
if (pw->main_loop)
|
|
pw_thread_loop_stop(pw->main_loop);
|
|
if (pw->stream)
|
|
pw_stream_destroy(pw->stream);
|
|
if (pw->context)
|
|
pw_context_destroy(pw->context);
|
|
if (pw->fd >= 0)
|
|
spa_system_close(pw->system, pw->fd);
|
|
if (pw->main_loop)
|
|
pw_thread_loop_destroy(pw->main_loop);
|
|
pw_properties_free(pw->props);
|
|
snd_output_close(pw->output);
|
|
fclose(pw->log_file);
|
|
free(pw);
|
|
}
|
|
|
|
static int snd_pcm_pipewire_close(snd_pcm_ioplug_t *io)
|
|
{
|
|
snd_pcm_pipewire_t *pw = io->private_data;
|
|
pw_log_debug("%p: close", pw);
|
|
snd_pcm_pipewire_free(pw);
|
|
return 0;
|
|
}
|
|
|
|
static int snd_pcm_pipewire_poll_revents(snd_pcm_ioplug_t *io,
|
|
struct pollfd *pfds, unsigned int nfds,
|
|
unsigned short *revents)
|
|
{
|
|
snd_pcm_pipewire_t *pw = io->private_data;
|
|
|
|
assert(pfds && nfds == 1 && revents);
|
|
|
|
if (pw->error < 0)
|
|
return pw->error;
|
|
|
|
*revents = pfds[0].revents & ~(POLLIN | POLLOUT);
|
|
if (pfds[0].revents & POLLIN && update_active(io))
|
|
*revents |= (io->stream == SND_PCM_STREAM_PLAYBACK) ? POLLOUT : POLLIN;
|
|
|
|
pw_log_trace_fp("poll %d", *revents);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static snd_pcm_sframes_t snd_pcm_pipewire_pointer(snd_pcm_ioplug_t *io)
|
|
{
|
|
snd_pcm_pipewire_t *pw = io->private_data;
|
|
if (pw->xrun_detected)
|
|
return -EPIPE;
|
|
if (pw->error < 0)
|
|
return pw->error;
|
|
if (io->buffer_size == 0)
|
|
return 0;
|
|
#ifdef SND_PCM_IOPLUG_FLAG_BOUNDARY_WA
|
|
return pw->hw_ptr;
|
|
#else
|
|
return pw->hw_ptr % io->buffer_size;
|
|
#endif
|
|
}
|
|
|
|
static int snd_pcm_pipewire_delay(snd_pcm_ioplug_t *io, snd_pcm_sframes_t *delayp)
|
|
{
|
|
snd_pcm_pipewire_t *pw = io->private_data;
|
|
uintptr_t seq1, seq2;
|
|
int64_t elapsed = 0, delay, now, avail;
|
|
int64_t diff;
|
|
|
|
do {
|
|
seq1 = SPA_SEQ_READ(pw->seq);
|
|
|
|
delay = pw->delay + pw->transfered;
|
|
now = pw->now;
|
|
if (io->stream == SND_PCM_STREAM_PLAYBACK)
|
|
avail = snd_pcm_ioplug_hw_avail(io, pw->hw_ptr, io->appl_ptr);
|
|
else
|
|
avail = snd_pcm_ioplug_avail(io, pw->hw_ptr, io->appl_ptr);
|
|
|
|
seq2 = SPA_SEQ_READ(pw->seq);
|
|
} while (!SPA_SEQ_READ_SUCCESS(seq1, seq2));
|
|
|
|
if (now != 0 && (io->state == SND_PCM_STATE_RUNNING ||
|
|
io->state == SND_PCM_STATE_DRAINING)) {
|
|
diff = pw_stream_get_nsec(pw->stream) - now;
|
|
elapsed = (io->rate * diff) / SPA_NSEC_PER_SEC;
|
|
|
|
if (io->stream == SND_PCM_STREAM_PLAYBACK)
|
|
delay -= SPA_MIN(elapsed, delay);
|
|
else
|
|
delay += SPA_MIN(elapsed, (int64_t)io->buffer_size);
|
|
}
|
|
|
|
*delayp = delay + avail;
|
|
|
|
pw_log_trace("avail:%"PRIi64" filled %"PRIi64" elapsed:%"PRIi64" delay:%ld hw:%lu appl:%lu",
|
|
avail, delay, elapsed, *delayp, pw->hw_ptr, io->appl_ptr);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static snd_pcm_uframes_t
|
|
snd_pcm_pipewire_process(snd_pcm_pipewire_t *pw, struct pw_buffer *b,
|
|
snd_pcm_uframes_t *hw_avail,snd_pcm_uframes_t want)
|
|
{
|
|
snd_pcm_ioplug_t *io = &pw->io;
|
|
snd_pcm_channel_area_t *pwareas;
|
|
snd_pcm_uframes_t xfer = 0;
|
|
snd_pcm_uframes_t nframes;
|
|
unsigned int channel;
|
|
struct spa_data *d;
|
|
void *ptr;
|
|
uint32_t bl, offset, size;
|
|
|
|
d = b->buffer->datas;
|
|
pwareas = alloca(io->channels * sizeof(snd_pcm_channel_area_t));
|
|
|
|
for (bl = 0; bl < pw->blocks; bl++) {
|
|
if (io->stream == SND_PCM_STREAM_PLAYBACK) {
|
|
size = SPA_MIN(d[bl].maxsize, pw->min_avail * pw->stride);
|
|
} else {
|
|
offset = SPA_MIN(d[bl].chunk->offset, d[bl].maxsize);
|
|
size = SPA_MIN(d[bl].chunk->size, d[bl].maxsize - offset);
|
|
}
|
|
want = SPA_MIN(want, size / pw->stride);
|
|
}
|
|
nframes = SPA_MIN(want, *hw_avail);
|
|
|
|
if (pw->blocks == 1) {
|
|
if (io->stream == SND_PCM_STREAM_PLAYBACK) {
|
|
d[0].chunk->size = want * pw->stride;
|
|
d[0].chunk->offset = offset = 0;
|
|
} else {
|
|
offset = SPA_MIN(d[0].chunk->offset, d[0].maxsize);
|
|
}
|
|
ptr = SPA_PTROFF(d[0].data, offset, void);
|
|
for (channel = 0; channel < io->channels; channel++) {
|
|
pwareas[channel].addr = ptr;
|
|
pwareas[channel].first = channel * pw->sample_bits;
|
|
pwareas[channel].step = io->channels * pw->sample_bits;
|
|
}
|
|
} else {
|
|
for (channel = 0; channel < io->channels; channel++) {
|
|
if (io->stream == SND_PCM_STREAM_PLAYBACK) {
|
|
d[channel].chunk->size = want * pw->stride;
|
|
d[channel].chunk->offset = offset = 0;
|
|
} else {
|
|
offset = SPA_MIN(d[channel].chunk->offset, d[channel].maxsize);
|
|
}
|
|
ptr = SPA_PTROFF(d[channel].data, offset, void);
|
|
pwareas[channel].addr = ptr;
|
|
pwareas[channel].first = 0;
|
|
pwareas[channel].step = pw->sample_bits;
|
|
}
|
|
}
|
|
|
|
if (io->state == SND_PCM_STATE_RUNNING ||
|
|
io->state == SND_PCM_STATE_DRAINING) {
|
|
snd_pcm_uframes_t hw_ptr = pw->hw_ptr;
|
|
xfer = nframes;
|
|
if (xfer > 0) {
|
|
const snd_pcm_channel_area_t *areas = snd_pcm_ioplug_mmap_areas(io);
|
|
if (areas != NULL) {
|
|
const snd_pcm_uframes_t offset = hw_ptr % io->buffer_size;
|
|
if (io->stream == SND_PCM_STREAM_PLAYBACK)
|
|
snd_pcm_areas_copy_wrap(pwareas, 0, nframes,
|
|
areas, offset,
|
|
io->buffer_size,
|
|
io->channels, xfer,
|
|
io->format);
|
|
else
|
|
snd_pcm_areas_copy_wrap(areas, offset,
|
|
io->buffer_size,
|
|
pwareas, 0, nframes,
|
|
io->channels, xfer,
|
|
io->format);
|
|
}
|
|
hw_ptr += xfer;
|
|
if (hw_ptr >= pw->boundary)
|
|
hw_ptr -= pw->boundary;
|
|
pw->hw_ptr = hw_ptr;
|
|
*hw_avail -= xfer;
|
|
}
|
|
}
|
|
/* check if requested frames were copied */
|
|
if (xfer < want) {
|
|
/* always fill the not yet written PipeWire buffer with silence */
|
|
if (io->stream == SND_PCM_STREAM_PLAYBACK) {
|
|
const snd_pcm_uframes_t frames = want - xfer;
|
|
|
|
snd_pcm_areas_silence(pwareas, xfer, io->channels,
|
|
frames, io->format);
|
|
xfer += frames;
|
|
}
|
|
if (io->state == SND_PCM_STATE_RUNNING ||
|
|
io->state == SND_PCM_STATE_DRAINING) {
|
|
/* report Xrun to user application */
|
|
pw->xrun_detected = true;
|
|
}
|
|
}
|
|
return xfer;
|
|
}
|
|
|
|
static void on_stream_param_changed(void *data, uint32_t id, const struct spa_pod *param)
|
|
{
|
|
snd_pcm_pipewire_t *pw = data;
|
|
snd_pcm_ioplug_t *io = &pw->io;
|
|
const struct spa_pod *params[4];
|
|
uint32_t n_params = 0;
|
|
uint8_t buffer[4096];
|
|
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
|
uint32_t buffers, size;
|
|
|
|
if (param == NULL || id != SPA_PARAM_Format)
|
|
return;
|
|
|
|
io->period_size = pw->min_avail;
|
|
|
|
buffers = SPA_CLAMP(io->buffer_size / io->period_size, MIN_BUFFERS, MAX_BUFFERS);
|
|
size = io->period_size * pw->stride;
|
|
|
|
pw_log_info("%p: buffer_size:%lu period_size:%lu buffers:%u size:%u min_avail:%lu",
|
|
pw, io->buffer_size, io->period_size, buffers, size, pw->min_avail);
|
|
|
|
params[n_params++] = spa_pod_builder_add_object(&b,
|
|
SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
|
|
SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(buffers, MIN_BUFFERS, MAX_BUFFERS),
|
|
SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(pw->blocks),
|
|
SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(size, size, INT_MAX),
|
|
SPA_PARAM_BUFFERS_stride, SPA_POD_Int(pw->stride));
|
|
|
|
pw_stream_update_params(pw->stream, params, n_params);
|
|
|
|
pw->negotiated = true;
|
|
pw_thread_loop_signal(pw->main_loop, false);
|
|
}
|
|
|
|
static void on_stream_state_changed(void *data, enum pw_stream_state old, enum pw_stream_state state, const char *error)
|
|
{
|
|
snd_pcm_pipewire_t *pw = data;
|
|
|
|
if (state == PW_STREAM_STATE_ERROR) {
|
|
pw_log_warn("%s", error);
|
|
pw->error = -EIO;
|
|
update_active(&pw->io);
|
|
}
|
|
}
|
|
|
|
static void on_stream_drained(void *data)
|
|
{
|
|
snd_pcm_pipewire_t *pw = data;
|
|
pw->drained = true;
|
|
pw->draining = false;
|
|
pw_log_debug("%p: drained", pw);
|
|
pw_thread_loop_signal(pw->main_loop, false);
|
|
}
|
|
|
|
static void on_stream_process(void *data)
|
|
{
|
|
snd_pcm_pipewire_t *pw = data;
|
|
snd_pcm_ioplug_t *io = &pw->io;
|
|
struct pw_buffer *b;
|
|
snd_pcm_uframes_t hw_avail, before, want, xfer;
|
|
struct pw_time pwt;
|
|
int64_t delay;
|
|
|
|
pw_stream_get_time_n(pw->stream, &pwt, sizeof(pwt));
|
|
|
|
delay = pwt.delay;
|
|
if (pwt.rate.num != 0)
|
|
delay = delay * io->rate * pwt.rate.num / pwt.rate.denom;
|
|
|
|
before = hw_avail = snd_pcm_ioplug_hw_avail(io, pw->hw_ptr, io->appl_ptr);
|
|
|
|
if (pw->drained)
|
|
goto done;
|
|
|
|
b = pw_stream_dequeue_buffer(pw->stream);
|
|
if (b == NULL)
|
|
return;
|
|
|
|
want = b->requested ? b->requested : hw_avail;
|
|
|
|
SPA_SEQ_WRITE(pw->seq);
|
|
|
|
if (pw->now != pwt.now) {
|
|
pw->transfered = pw->buffered;
|
|
pw->buffered = 0;
|
|
}
|
|
|
|
xfer = snd_pcm_pipewire_process(pw, b, &hw_avail, want);
|
|
|
|
pw->delay = delay;
|
|
/* the buffer is now queued in the stream and consumed */
|
|
if (io->stream == SND_PCM_STREAM_PLAYBACK)
|
|
pw->transfered += xfer;
|
|
|
|
/* more then requested data transfered, use them in next iteration */
|
|
pw->buffered = (want == 0 || pw->transfered < want) ? 0 : (pw->transfered % want);
|
|
|
|
pw->now = pwt.now;
|
|
SPA_SEQ_WRITE(pw->seq);
|
|
|
|
pw_log_trace("%p: avail-before:%lu avail:%lu want:%lu xfer:%lu hw:%lu appl:%lu",
|
|
pw, before, hw_avail, want, xfer, pw->hw_ptr, io->appl_ptr);
|
|
|
|
pw_stream_queue_buffer(pw->stream, b);
|
|
|
|
if (io->state == SND_PCM_STATE_DRAINING && !pw->draining && hw_avail == 0) {
|
|
if (io->stream == SND_PCM_STREAM_CAPTURE) {
|
|
on_stream_drained (pw); /* since pw_stream does not call drained() for capture */
|
|
} else {
|
|
pw_stream_flush(pw->stream, true);
|
|
pw->draining = true;
|
|
pw->drained = false;
|
|
}
|
|
}
|
|
done:
|
|
update_active(io);
|
|
}
|
|
|
|
static const struct pw_stream_events stream_events = {
|
|
PW_VERSION_STREAM_EVENTS,
|
|
.param_changed = on_stream_param_changed,
|
|
.state_changed = on_stream_state_changed,
|
|
.process = on_stream_process,
|
|
.drained = on_stream_drained,
|
|
};
|
|
|
|
static int pipewire_start(snd_pcm_pipewire_t *pw)
|
|
{
|
|
if (!pw->activated && pw->stream != NULL) {
|
|
pw_stream_set_active(pw->stream, true);
|
|
pw->activated = true;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int snd_pcm_pipewire_drain(snd_pcm_ioplug_t *io)
|
|
{
|
|
int res;
|
|
snd_pcm_pipewire_t *pw = io->private_data;
|
|
|
|
pw_thread_loop_lock(pw->main_loop);
|
|
pw_log_debug("%p: drain", pw);
|
|
pw->drained = false;
|
|
pw->draining = false;
|
|
pipewire_start(pw);
|
|
while (!pw->drained && pw->error >= 0 && pw->activated) {
|
|
pw_thread_loop_wait(pw->main_loop);
|
|
}
|
|
res = pw->error;
|
|
pw_thread_loop_unlock(pw->main_loop);
|
|
return res;
|
|
}
|
|
|
|
static int snd_pcm_pipewire_prepare(snd_pcm_ioplug_t *io)
|
|
{
|
|
snd_pcm_pipewire_t *pw = io->private_data;
|
|
snd_pcm_sw_params_t *swparams;
|
|
const struct spa_pod *params[1];
|
|
uint8_t buffer[1024];
|
|
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
|
uint32_t min_period;
|
|
|
|
pw_thread_loop_lock(pw->main_loop);
|
|
|
|
snd_pcm_sw_params_alloca(&swparams);
|
|
if (snd_pcm_sw_params_current(io->pcm, swparams) == 0) {
|
|
int event;
|
|
snd_pcm_sw_params_get_period_event(swparams, &event);
|
|
if (event)
|
|
pw->min_avail = io->period_size;
|
|
else
|
|
snd_pcm_sw_params_get_avail_min(swparams, &pw->min_avail);
|
|
snd_pcm_sw_params_get_boundary(swparams, &pw->boundary);
|
|
snd_pcm_sw_params_dump(swparams, pw->output);
|
|
fflush(pw->log_file);
|
|
} else {
|
|
pw->min_avail = io->period_size;
|
|
pw->boundary = io->buffer_size;
|
|
}
|
|
|
|
min_period = (MIN_PERIOD * io->rate / 48000);
|
|
pw->min_avail = SPA_MAX(pw->min_avail, min_period);
|
|
|
|
pw_log_debug("%p: prepare error:%d stream:%p buffer-size:%lu "
|
|
"period-size:%lu min-avail:%ld", pw, pw->error,
|
|
pw->stream, io->buffer_size, io->period_size, pw->min_avail);
|
|
|
|
if (pw->error >= 0 && pw->stream != NULL && !pw->hw_params_changed)
|
|
goto done;
|
|
pw->hw_params_changed = false;
|
|
|
|
pw_properties_setf(pw->props, PW_KEY_NODE_LATENCY, "%lu/%u", pw->min_avail, io->rate);
|
|
pw_properties_setf(pw->props, PW_KEY_NODE_RATE, "1/%u", io->rate);
|
|
|
|
params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &pw->format);
|
|
|
|
if (pw->stream != NULL) {
|
|
pw_stream_set_active(pw->stream, false);
|
|
pw_stream_update_properties(pw->stream, &pw->props->dict);
|
|
pw_stream_update_params(pw->stream, params, 1);
|
|
pw_stream_set_active(pw->stream, true);
|
|
goto done;
|
|
}
|
|
|
|
pw->stream = pw_stream_new(pw->core, NULL, pw_properties_copy(pw->props));
|
|
if (pw->stream == NULL)
|
|
goto error;
|
|
|
|
pw_stream_add_listener(pw->stream, &pw->stream_listener, &stream_events, pw);
|
|
|
|
pw->error = 0;
|
|
|
|
pw->negotiated = false;
|
|
pw_stream_connect(pw->stream,
|
|
io->stream == SND_PCM_STREAM_PLAYBACK ?
|
|
PW_DIRECTION_OUTPUT :
|
|
PW_DIRECTION_INPUT,
|
|
PW_ID_ANY,
|
|
PW_STREAM_FLAG_AUTOCONNECT |
|
|
PW_STREAM_FLAG_MAP_BUFFERS |
|
|
PW_STREAM_FLAG_RT_PROCESS,
|
|
params, 1);
|
|
|
|
done:
|
|
pw->hw_ptr = 0;
|
|
pw->now = 0;
|
|
pw->xrun_detected = false;
|
|
pw->drained = false;
|
|
pw->draining = false;
|
|
|
|
while (!pw->negotiated && pw->error >= 0)
|
|
pw_thread_loop_wait(pw->main_loop);
|
|
if (pw->error < 0)
|
|
goto error;
|
|
|
|
pw_thread_loop_unlock(pw->main_loop);
|
|
|
|
return 0;
|
|
|
|
error:
|
|
pw_thread_loop_unlock(pw->main_loop);
|
|
return pw->error < 0 ? pw->error : -ENOMEM;
|
|
}
|
|
|
|
static int snd_pcm_pipewire_start(snd_pcm_ioplug_t *io)
|
|
{
|
|
snd_pcm_pipewire_t *pw = io->private_data;
|
|
|
|
pw_thread_loop_lock(pw->main_loop);
|
|
pw_log_debug("%p: start", pw);
|
|
pipewire_start(pw);
|
|
pw_thread_loop_unlock(pw->main_loop);
|
|
return 0;
|
|
}
|
|
|
|
static int snd_pcm_pipewire_stop(snd_pcm_ioplug_t *io)
|
|
{
|
|
snd_pcm_pipewire_t *pw = io->private_data;
|
|
|
|
pw_log_debug("%p: stop", pw);
|
|
update_active(io);
|
|
|
|
pw_thread_loop_lock(pw->main_loop);
|
|
if (pw->activated && pw->stream != NULL) {
|
|
pw_stream_set_active(pw->stream, false);
|
|
pw->activated = false;
|
|
}
|
|
pw_thread_loop_unlock(pw->main_loop);
|
|
return 0;
|
|
}
|
|
|
|
static int snd_pcm_pipewire_pause(snd_pcm_ioplug_t * io, int enable)
|
|
{
|
|
pw_log_debug("%p: pause", io);
|
|
|
|
if (enable)
|
|
snd_pcm_pipewire_stop(io);
|
|
else
|
|
snd_pcm_pipewire_start(io);
|
|
|
|
return 0;
|
|
}
|
|
|
|
#if __BYTE_ORDER == __BIG_ENDIAN
|
|
#define _FORMAT_LE(p, fmt) p ? SPA_AUDIO_FORMAT_UNKNOWN : SPA_AUDIO_FORMAT_ ## fmt ## _OE
|
|
#define _FORMAT_BE(p, fmt) p ? SPA_AUDIO_FORMAT_ ## fmt ## P : SPA_AUDIO_FORMAT_ ## fmt
|
|
#elif __BYTE_ORDER == __LITTLE_ENDIAN
|
|
#define _FORMAT_LE(p, fmt) p ? SPA_AUDIO_FORMAT_ ## fmt ## P : SPA_AUDIO_FORMAT_ ## fmt
|
|
#define _FORMAT_BE(p, fmt) p ? SPA_AUDIO_FORMAT_UNKNOWN : SPA_AUDIO_FORMAT_ ## fmt ## _OE
|
|
#endif
|
|
|
|
static int set_default_channels(struct spa_audio_info_raw *info)
|
|
{
|
|
switch (info->channels) {
|
|
case 8:
|
|
info->position[6] = SPA_AUDIO_CHANNEL_SL;
|
|
info->position[7] = SPA_AUDIO_CHANNEL_SR;
|
|
SPA_FALLTHROUGH
|
|
case 6:
|
|
info->position[5] = SPA_AUDIO_CHANNEL_LFE;
|
|
SPA_FALLTHROUGH
|
|
case 5:
|
|
info->position[4] = SPA_AUDIO_CHANNEL_FC;
|
|
SPA_FALLTHROUGH
|
|
case 4:
|
|
info->position[2] = SPA_AUDIO_CHANNEL_RL;
|
|
info->position[3] = SPA_AUDIO_CHANNEL_RR;
|
|
SPA_FALLTHROUGH
|
|
case 2:
|
|
info->position[0] = SPA_AUDIO_CHANNEL_FL;
|
|
info->position[1] = SPA_AUDIO_CHANNEL_FR;
|
|
return 1;
|
|
case 1:
|
|
info->position[0] = SPA_AUDIO_CHANNEL_MONO;
|
|
return 1;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static int snd_pcm_pipewire_hw_params(snd_pcm_ioplug_t * io,
|
|
snd_pcm_hw_params_t * params)
|
|
{
|
|
snd_pcm_pipewire_t *pw = io->private_data;
|
|
bool planar;
|
|
|
|
snd_pcm_hw_params_dump(params, pw->output);
|
|
fflush(pw->log_file);
|
|
|
|
pw_log_debug("%p: hw_params buffer_size:%lu period_size:%lu", pw, io->buffer_size, io->period_size);
|
|
|
|
switch(io->access) {
|
|
case SND_PCM_ACCESS_MMAP_INTERLEAVED:
|
|
case SND_PCM_ACCESS_RW_INTERLEAVED:
|
|
planar = false;
|
|
break;
|
|
case SND_PCM_ACCESS_MMAP_NONINTERLEAVED:
|
|
case SND_PCM_ACCESS_RW_NONINTERLEAVED:
|
|
planar = true;
|
|
break;
|
|
default:
|
|
SNDERR("PipeWire: invalid access: %d\n", io->access);
|
|
return -EINVAL;
|
|
}
|
|
|
|
switch(io->format) {
|
|
case SND_PCM_FORMAT_U8:
|
|
pw->format.format = planar ? SPA_AUDIO_FORMAT_U8P : SPA_AUDIO_FORMAT_U8;
|
|
break;
|
|
case SND_PCM_FORMAT_S16_LE:
|
|
pw->format.format = _FORMAT_LE(planar, S16);
|
|
break;
|
|
case SND_PCM_FORMAT_S16_BE:
|
|
pw->format.format = _FORMAT_BE(planar, S16);
|
|
break;
|
|
case SND_PCM_FORMAT_S24_LE:
|
|
pw->format.format = _FORMAT_LE(planar, S24_32);
|
|
break;
|
|
case SND_PCM_FORMAT_S24_BE:
|
|
pw->format.format = _FORMAT_BE(planar, S24_32);
|
|
break;
|
|
case SND_PCM_FORMAT_S32_LE:
|
|
pw->format.format = _FORMAT_LE(planar, S32);
|
|
break;
|
|
case SND_PCM_FORMAT_S32_BE:
|
|
pw->format.format = _FORMAT_BE(planar, S32);
|
|
break;
|
|
case SND_PCM_FORMAT_S24_3LE:
|
|
pw->format.format = _FORMAT_LE(planar, S24);
|
|
break;
|
|
case SND_PCM_FORMAT_S24_3BE:
|
|
pw->format.format = _FORMAT_BE(planar, S24);
|
|
break;
|
|
case SND_PCM_FORMAT_FLOAT_LE:
|
|
pw->format.format = _FORMAT_LE(planar, F32);
|
|
break;
|
|
case SND_PCM_FORMAT_FLOAT_BE:
|
|
pw->format.format = _FORMAT_BE(planar, F32);
|
|
break;
|
|
default:
|
|
SNDERR("PipeWire: invalid format: %d\n", io->format);
|
|
return -EINVAL;
|
|
}
|
|
pw->format.channels = io->channels;
|
|
pw->format.rate = io->rate;
|
|
|
|
set_default_channels(&pw->format);
|
|
|
|
pw->sample_bits = snd_pcm_format_physical_width(io->format);
|
|
if (planar) {
|
|
pw->blocks = io->channels;
|
|
pw->stride = pw->sample_bits / 8;
|
|
} else {
|
|
pw->blocks = 1;
|
|
pw->stride = (io->channels * pw->sample_bits) / 8;
|
|
}
|
|
pw->hw_params_changed = true;
|
|
pw_log_info("%p: format:%s channels:%d rate:%d stride:%d blocks:%d", pw,
|
|
spa_debug_type_find_name(spa_type_audio_format, pw->format.format),
|
|
io->channels, io->rate, pw->stride, pw->blocks);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int snd_pcm_pipewire_sw_params(snd_pcm_ioplug_t * io,
|
|
snd_pcm_sw_params_t * sw_params)
|
|
{
|
|
snd_pcm_pipewire_t *pw = io->private_data;
|
|
|
|
pw_thread_loop_lock(pw->main_loop);
|
|
if (pw->stream) {
|
|
snd_pcm_uframes_t min_avail;
|
|
snd_pcm_sw_params_get_avail_min( sw_params, &min_avail);
|
|
snd_pcm_sw_params_get_boundary(sw_params, &pw->boundary);
|
|
if (min_avail != pw->min_avail) {
|
|
char latency[64];
|
|
struct spa_dict_item item[1];
|
|
uint32_t min_period = (MIN_PERIOD * io->rate / 48000);
|
|
|
|
pw->min_avail = SPA_MAX(min_avail, min_period);
|
|
|
|
spa_scnprintf(latency, sizeof(latency), "%lu/%u", pw->min_avail, io->rate);
|
|
item[0] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_LATENCY, latency);
|
|
|
|
pw_log_debug("%p: sw_params update props %p %ld", pw, pw->stream, pw->min_avail);
|
|
pw_stream_update_properties(pw->stream, &SPA_DICT_INIT(item, 1));
|
|
}
|
|
} else {
|
|
pw_log_debug("%p: sw_params pre-prepare noop", pw);
|
|
}
|
|
pw_thread_loop_unlock(pw->main_loop);
|
|
|
|
return 0;
|
|
}
|
|
|
|
struct chmap_info {
|
|
enum snd_pcm_chmap_position pos;
|
|
enum spa_audio_channel channel;
|
|
};
|
|
|
|
static const struct chmap_info chmap_info[] = {
|
|
[SND_CHMAP_UNKNOWN] = { SND_CHMAP_UNKNOWN, SPA_AUDIO_CHANNEL_UNKNOWN },
|
|
[SND_CHMAP_NA] = { SND_CHMAP_NA, SPA_AUDIO_CHANNEL_NA },
|
|
[SND_CHMAP_MONO] = { SND_CHMAP_MONO, SPA_AUDIO_CHANNEL_MONO },
|
|
[SND_CHMAP_FL] = { SND_CHMAP_FL, SPA_AUDIO_CHANNEL_FL },
|
|
[SND_CHMAP_FR] = { SND_CHMAP_FR, SPA_AUDIO_CHANNEL_FR },
|
|
[SND_CHMAP_RL] = { SND_CHMAP_RL, SPA_AUDIO_CHANNEL_RL },
|
|
[SND_CHMAP_RR] = { SND_CHMAP_RR, SPA_AUDIO_CHANNEL_RR },
|
|
[SND_CHMAP_FC] = { SND_CHMAP_FC, SPA_AUDIO_CHANNEL_FC },
|
|
[SND_CHMAP_LFE] = { SND_CHMAP_LFE, SPA_AUDIO_CHANNEL_LFE },
|
|
[SND_CHMAP_SL] = { SND_CHMAP_SL, SPA_AUDIO_CHANNEL_SL },
|
|
[SND_CHMAP_SR] = { SND_CHMAP_SR, SPA_AUDIO_CHANNEL_SR },
|
|
[SND_CHMAP_RC] = { SND_CHMAP_RC, SPA_AUDIO_CHANNEL_RC },
|
|
[SND_CHMAP_FLC] = { SND_CHMAP_FLC, SPA_AUDIO_CHANNEL_FLC },
|
|
[SND_CHMAP_FRC] = { SND_CHMAP_FRC, SPA_AUDIO_CHANNEL_FRC },
|
|
[SND_CHMAP_RLC] = { SND_CHMAP_RLC, SPA_AUDIO_CHANNEL_RLC },
|
|
[SND_CHMAP_RRC] = { SND_CHMAP_RRC, SPA_AUDIO_CHANNEL_RRC },
|
|
[SND_CHMAP_FLW] = { SND_CHMAP_FLW, SPA_AUDIO_CHANNEL_FLW },
|
|
[SND_CHMAP_FRW] = { SND_CHMAP_FRW, SPA_AUDIO_CHANNEL_FRW },
|
|
[SND_CHMAP_FLH] = { SND_CHMAP_FLH, SPA_AUDIO_CHANNEL_FLH },
|
|
[SND_CHMAP_FCH] = { SND_CHMAP_FCH, SPA_AUDIO_CHANNEL_FCH },
|
|
[SND_CHMAP_FRH] = { SND_CHMAP_FRH, SPA_AUDIO_CHANNEL_FRH },
|
|
[SND_CHMAP_TC] = { SND_CHMAP_TC, SPA_AUDIO_CHANNEL_TC },
|
|
[SND_CHMAP_TFL] = { SND_CHMAP_TFL, SPA_AUDIO_CHANNEL_TFL },
|
|
[SND_CHMAP_TFR] = { SND_CHMAP_TFR, SPA_AUDIO_CHANNEL_TFR },
|
|
[SND_CHMAP_TFC] = { SND_CHMAP_TFC, SPA_AUDIO_CHANNEL_TFC },
|
|
[SND_CHMAP_TRL] = { SND_CHMAP_TRL, SPA_AUDIO_CHANNEL_TRL },
|
|
[SND_CHMAP_TRR] = { SND_CHMAP_TRR, SPA_AUDIO_CHANNEL_TRR },
|
|
[SND_CHMAP_TRC] = { SND_CHMAP_TRC, SPA_AUDIO_CHANNEL_TRC },
|
|
[SND_CHMAP_TFLC] = { SND_CHMAP_TFLC, SPA_AUDIO_CHANNEL_TFLC },
|
|
[SND_CHMAP_TFRC] = { SND_CHMAP_TFRC, SPA_AUDIO_CHANNEL_TFRC },
|
|
[SND_CHMAP_TSL] = { SND_CHMAP_TSL, SPA_AUDIO_CHANNEL_TSL },
|
|
[SND_CHMAP_TSR] = { SND_CHMAP_TSR, SPA_AUDIO_CHANNEL_TSR },
|
|
[SND_CHMAP_LLFE] = { SND_CHMAP_LLFE, SPA_AUDIO_CHANNEL_LLFE },
|
|
[SND_CHMAP_RLFE] = { SND_CHMAP_RLFE, SPA_AUDIO_CHANNEL_RLFE },
|
|
[SND_CHMAP_BC] = { SND_CHMAP_BC, SPA_AUDIO_CHANNEL_BC },
|
|
[SND_CHMAP_BLC] = { SND_CHMAP_BLC, SPA_AUDIO_CHANNEL_BLC },
|
|
[SND_CHMAP_BRC] = { SND_CHMAP_BRC, SPA_AUDIO_CHANNEL_BRC },
|
|
};
|
|
|
|
static enum snd_pcm_chmap_position channel_to_chmap(enum spa_audio_channel channel)
|
|
{
|
|
SPA_FOR_EACH_ELEMENT_VAR(chmap_info, info)
|
|
if (info->channel == channel)
|
|
return info->pos;
|
|
return SND_CHMAP_UNKNOWN;
|
|
}
|
|
|
|
static enum spa_audio_channel chmap_to_channel(enum snd_pcm_chmap_position pos)
|
|
{
|
|
if (pos >= SPA_N_ELEMENTS(chmap_info))
|
|
return SPA_AUDIO_CHANNEL_UNKNOWN;
|
|
return chmap_info[pos].channel;
|
|
}
|
|
|
|
static int snd_pcm_pipewire_set_chmap(snd_pcm_ioplug_t * io,
|
|
const snd_pcm_chmap_t * map)
|
|
{
|
|
snd_pcm_pipewire_t *pw = io->private_data;
|
|
unsigned int i;
|
|
|
|
pw->format.channels = map->channels;
|
|
for (i = 0; i < map->channels; i++) {
|
|
pw->format.position[i] = chmap_to_channel(map->pos[i]);
|
|
pw_log_debug("map %d: %s / %s", i,
|
|
snd_pcm_chmap_name(map->pos[i]),
|
|
spa_debug_type_find_short_name(spa_type_audio_channel,
|
|
pw->format.position[i]));
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static snd_pcm_chmap_t * snd_pcm_pipewire_get_chmap(snd_pcm_ioplug_t * io)
|
|
{
|
|
snd_pcm_pipewire_t *pw = io->private_data;
|
|
snd_pcm_chmap_t *map;
|
|
uint32_t i;
|
|
|
|
map = calloc(1, sizeof(snd_pcm_chmap_t) +
|
|
pw->format.channels * sizeof(unsigned int));
|
|
map->channels = pw->format.channels;
|
|
for (i = 0; i < pw->format.channels; i++)
|
|
map->pos[i] = channel_to_chmap(pw->format.position[i]);
|
|
|
|
return map;
|
|
}
|
|
|
|
static void make_map(snd_pcm_chmap_query_t **maps, int index, int channels, ...)
|
|
{
|
|
va_list args;
|
|
int i;
|
|
|
|
maps[index] = malloc(sizeof(snd_pcm_chmap_query_t) + (channels * sizeof(unsigned int)));
|
|
maps[index]->type = SND_CHMAP_TYPE_FIXED;
|
|
maps[index]->map.channels = channels;
|
|
va_start(args, channels);
|
|
for (i = 0; i < channels; i++)
|
|
maps[index]->map.pos[i] = va_arg(args, int);
|
|
va_end(args);
|
|
}
|
|
|
|
static snd_pcm_chmap_query_t **snd_pcm_pipewire_query_chmaps(snd_pcm_ioplug_t *io)
|
|
{
|
|
snd_pcm_chmap_query_t **maps;
|
|
|
|
maps = calloc(7, sizeof(*maps));
|
|
make_map(maps, 0, 1, SND_CHMAP_MONO);
|
|
make_map(maps, 1, 2, SND_CHMAP_FL, SND_CHMAP_FR);
|
|
make_map(maps, 2, 4, SND_CHMAP_FL, SND_CHMAP_FR, SND_CHMAP_RL, SND_CHMAP_RR);
|
|
make_map(maps, 3, 5, SND_CHMAP_FL, SND_CHMAP_FR, SND_CHMAP_RL, SND_CHMAP_RR,
|
|
SND_CHMAP_FC);
|
|
make_map(maps, 4, 6, SND_CHMAP_FL, SND_CHMAP_FR, SND_CHMAP_RL, SND_CHMAP_RR,
|
|
SND_CHMAP_FC, SND_CHMAP_LFE);
|
|
make_map(maps, 5, 8, SND_CHMAP_FL, SND_CHMAP_FR, SND_CHMAP_RL, SND_CHMAP_RR,
|
|
SND_CHMAP_FC, SND_CHMAP_LFE, SND_CHMAP_SL, SND_CHMAP_SR);
|
|
|
|
return maps;
|
|
}
|
|
|
|
static snd_pcm_ioplug_callback_t pipewire_pcm_callback = {
|
|
.close = snd_pcm_pipewire_close,
|
|
.start = snd_pcm_pipewire_start,
|
|
.stop = snd_pcm_pipewire_stop,
|
|
.pause = snd_pcm_pipewire_pause,
|
|
.pointer = snd_pcm_pipewire_pointer,
|
|
.delay = snd_pcm_pipewire_delay,
|
|
.drain = snd_pcm_pipewire_drain,
|
|
.prepare = snd_pcm_pipewire_prepare,
|
|
.poll_revents = snd_pcm_pipewire_poll_revents,
|
|
.hw_params = snd_pcm_pipewire_hw_params,
|
|
.sw_params = snd_pcm_pipewire_sw_params,
|
|
.set_chmap = snd_pcm_pipewire_set_chmap,
|
|
.get_chmap = snd_pcm_pipewire_get_chmap,
|
|
.query_chmaps = snd_pcm_pipewire_query_chmaps,
|
|
};
|
|
|
|
#define MAX_VALS 64
|
|
struct param_info {
|
|
const char *prop;
|
|
int key;
|
|
#define TYPE_LIST 0
|
|
#define TYPE_MIN_MAX 1
|
|
int type;
|
|
unsigned int vals[MAX_VALS];
|
|
unsigned int n_vals;
|
|
int (*collect) (const char *str, int len, unsigned int *val);
|
|
|
|
};
|
|
|
|
static int collect_access(const char *str, int len, unsigned int *val)
|
|
{
|
|
char key[64];
|
|
|
|
if (spa_json_parse_stringn(str, len, key, sizeof(key)) <= 0)
|
|
return -EINVAL;
|
|
|
|
if (strcasecmp(key, "MMAP_INTERLEAVED") == 0)
|
|
*val = SND_PCM_ACCESS_MMAP_INTERLEAVED;
|
|
else if (strcasecmp(key, "MMAP_NONINTERLEAVED") == 0)
|
|
*val = SND_PCM_ACCESS_MMAP_NONINTERLEAVED;
|
|
else if (strcasecmp(key, "RW_INTERLEAVED") == 0)
|
|
*val = SND_PCM_ACCESS_RW_INTERLEAVED;
|
|
else if (strcasecmp(key, "RW_NONINTERLEAVED") == 0)
|
|
*val = SND_PCM_ACCESS_RW_NONINTERLEAVED;
|
|
else
|
|
return -EINVAL;
|
|
return 0;
|
|
}
|
|
|
|
static int collect_format(const char *str, int len, unsigned int *val)
|
|
{
|
|
char key[64];
|
|
snd_pcm_format_t fmt;
|
|
|
|
if (spa_json_parse_stringn(str, len, key, sizeof(key)) < 0)
|
|
return -EINVAL;
|
|
|
|
fmt = snd_pcm_format_value(key);
|
|
if (fmt != SND_PCM_FORMAT_UNKNOWN)
|
|
*val = fmt;
|
|
else
|
|
return -EINVAL;
|
|
return 0;
|
|
}
|
|
|
|
static int collect_int(const char *str, int len, unsigned int *val)
|
|
{
|
|
int v;
|
|
if (spa_json_parse_int(str, len, &v) > 0)
|
|
*val = v;
|
|
else
|
|
return -EINVAL;
|
|
return 0;
|
|
}
|
|
|
|
struct param_info infos[] = {
|
|
{ "alsa.access", SND_PCM_IOPLUG_HW_ACCESS, TYPE_LIST,
|
|
{ SND_PCM_ACCESS_MMAP_INTERLEAVED,
|
|
SND_PCM_ACCESS_MMAP_NONINTERLEAVED,
|
|
SND_PCM_ACCESS_RW_INTERLEAVED,
|
|
SND_PCM_ACCESS_RW_NONINTERLEAVED }, 4, collect_access },
|
|
{ "alsa.format", SND_PCM_IOPLUG_HW_FORMAT, TYPE_LIST,
|
|
{
|
|
#if __BYTE_ORDER == __LITTLE_ENDIAN
|
|
SND_PCM_FORMAT_FLOAT_LE,
|
|
SND_PCM_FORMAT_S32_LE,
|
|
SND_PCM_FORMAT_S24_LE,
|
|
SND_PCM_FORMAT_S24_3LE,
|
|
SND_PCM_FORMAT_S24_3BE,
|
|
SND_PCM_FORMAT_S16_LE,
|
|
#elif __BYTE_ORDER == __BIG_ENDIAN
|
|
SND_PCM_FORMAT_FLOAT_BE,
|
|
SND_PCM_FORMAT_S32_BE,
|
|
SND_PCM_FORMAT_S24_BE,
|
|
SND_PCM_FORMAT_S24_3LE,
|
|
SND_PCM_FORMAT_S24_3BE,
|
|
SND_PCM_FORMAT_S16_BE,
|
|
#endif
|
|
SND_PCM_FORMAT_U8 }, 7, collect_format },
|
|
{ "alsa.rate", SND_PCM_IOPLUG_HW_RATE, TYPE_MIN_MAX,
|
|
{ 1, MAX_RATE }, 2, collect_int },
|
|
{ "alsa.channels", SND_PCM_IOPLUG_HW_CHANNELS, TYPE_MIN_MAX,
|
|
{ 1, MAX_CHANNELS }, 2, collect_int },
|
|
{ "alsa.buffer-bytes", SND_PCM_IOPLUG_HW_BUFFER_BYTES, TYPE_MIN_MAX,
|
|
{ MIN_BUFFER_BYTES, MAX_BUFFER_BYTES }, 2, collect_int },
|
|
{ "alsa.period-bytes", SND_PCM_IOPLUG_HW_PERIOD_BYTES, TYPE_MIN_MAX,
|
|
{ MIN_PERIOD_BYTES, MAX_PERIOD_BYTES }, 2, collect_int },
|
|
{ "alsa.periods", SND_PCM_IOPLUG_HW_PERIODS, TYPE_MIN_MAX,
|
|
{ MIN_BUFFERS, 1024 }, 2, collect_int },
|
|
};
|
|
|
|
static struct param_info *param_info_by_key(int key)
|
|
{
|
|
SPA_FOR_EACH_ELEMENT_VAR(infos, p) {
|
|
if (p->key == key)
|
|
return p;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int parse_value(const char *str, struct param_info *info)
|
|
{
|
|
struct spa_json it[2];
|
|
unsigned int v;
|
|
const char *val;
|
|
int len;
|
|
|
|
spa_json_init(&it[0], str, strlen(str));
|
|
if ((len = spa_json_next(&it[0], &val)) <= 0)
|
|
return -EINVAL;
|
|
|
|
if (spa_json_is_array(val, len)) {
|
|
info->type = TYPE_LIST;
|
|
info->n_vals = 0;
|
|
spa_json_enter(&it[0], &it[1]);
|
|
while ((len = spa_json_next(&it[1], &val)) > 0 && info->n_vals < MAX_VALS) {
|
|
if (info->collect(val, len, &v) < 0)
|
|
continue;
|
|
info->vals[info->n_vals++] = v;
|
|
}
|
|
}
|
|
else if (spa_json_is_object(val, len)) {
|
|
char key[64];
|
|
info->type = TYPE_MIN_MAX;
|
|
info->n_vals = 2;
|
|
spa_json_enter(&it[0], &it[1]);
|
|
while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
|
|
if ((len = spa_json_next(&it[1], &val)) <= 0)
|
|
break;
|
|
if (info->collect(val, len, &v) < 0)
|
|
continue;
|
|
if (spa_streq(key, "min"))
|
|
info->vals[0] = v;
|
|
else if (spa_streq(key, "max"))
|
|
info->vals[1] = v;
|
|
}
|
|
}
|
|
else if (info->collect(val, len, &v) >= 0) {
|
|
info->type = TYPE_LIST;
|
|
info->vals[0] = v;
|
|
info->n_vals = 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int set_constraint(snd_pcm_pipewire_t *pw, int key)
|
|
{
|
|
struct param_info *p = param_info_by_key(key), info;
|
|
const char *str;
|
|
int err;
|
|
|
|
if (p == NULL)
|
|
return -EINVAL;
|
|
|
|
info = *p;
|
|
|
|
str = pw_properties_get(pw->props, p->prop);
|
|
if (str != NULL)
|
|
parse_value(str, &info);
|
|
|
|
switch (info.type) {
|
|
case TYPE_LIST:
|
|
pw_log_info("%s: list %d", p->prop, info.n_vals);
|
|
err = snd_pcm_ioplug_set_param_list(&pw->io, key, info.n_vals, info.vals);
|
|
break;
|
|
case TYPE_MIN_MAX:
|
|
pw_log_info("%s: min:%u max:%u", p->prop, info.vals[0], info.vals[1]);
|
|
err = snd_pcm_ioplug_set_param_minmax(&pw->io, key, info.vals[0], info.vals[1]);
|
|
break;
|
|
default:
|
|
return -EIO;
|
|
}
|
|
if (err < 0)
|
|
pw_log_warn("Can't set param %s: %s", info.prop, snd_strerror(err));
|
|
|
|
return err;
|
|
|
|
}
|
|
static int pipewire_set_hw_constraint(snd_pcm_pipewire_t *pw)
|
|
{
|
|
int err;
|
|
if ((err = set_constraint(pw, SND_PCM_IOPLUG_HW_ACCESS)) < 0 ||
|
|
(err = set_constraint(pw, SND_PCM_IOPLUG_HW_FORMAT)) < 0 ||
|
|
(err = set_constraint(pw, SND_PCM_IOPLUG_HW_RATE)) < 0 ||
|
|
(err = set_constraint(pw, SND_PCM_IOPLUG_HW_CHANNELS)) < 0 ||
|
|
(err = set_constraint(pw, SND_PCM_IOPLUG_HW_PERIOD_BYTES)) < 0 ||
|
|
(err = set_constraint(pw, SND_PCM_IOPLUG_HW_BUFFER_BYTES)) < 0 ||
|
|
(err = set_constraint(pw, SND_PCM_IOPLUG_HW_PERIODS)) < 0)
|
|
return err;
|
|
return 0;
|
|
}
|
|
|
|
static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
|
|
{
|
|
snd_pcm_pipewire_t *pw = data;
|
|
|
|
pw_log_warn("%p: error id:%u seq:%d res:%d (%s): %s", pw,
|
|
id, seq, res, spa_strerror(res), message);
|
|
|
|
if (id == PW_ID_CORE) {
|
|
pw->error = res;
|
|
if (pw->fd != -1)
|
|
update_active(&pw->io);
|
|
}
|
|
pw_thread_loop_signal(pw->main_loop, false);
|
|
}
|
|
|
|
static const struct pw_core_events core_events = {
|
|
PW_VERSION_CORE_EVENTS,
|
|
.error = on_core_error,
|
|
};
|
|
|
|
|
|
static ssize_t log_write(void *cookie, const char *buf, size_t size)
|
|
{
|
|
int len;
|
|
|
|
while (size > 0) {
|
|
len = strcspn(buf, "\n");
|
|
if (len > 0)
|
|
pw_log_debug("%.*s", (int)len, buf);
|
|
buf += len + 1;
|
|
size -= len + 1;
|
|
}
|
|
return size;
|
|
}
|
|
|
|
static cookie_io_functions_t io_funcs = {
|
|
.write = log_write,
|
|
};
|
|
|
|
static int execute_match(void *data, const char *location, const char *action,
|
|
const char *val, size_t len)
|
|
{
|
|
snd_pcm_pipewire_t *pw = data;
|
|
if (spa_streq(action, "update-props"))
|
|
pw_properties_update_string(pw->props, val, len);
|
|
return 1;
|
|
}
|
|
|
|
static int snd_pcm_pipewire_open(snd_pcm_t **pcmp,
|
|
struct pw_properties *props, snd_pcm_stream_t stream, int mode)
|
|
{
|
|
snd_pcm_pipewire_t *pw;
|
|
int err;
|
|
const char *str, *node_name = NULL;
|
|
struct pw_loop *loop;
|
|
|
|
assert(pcmp);
|
|
pw = calloc(1, sizeof(*pw));
|
|
if (!pw)
|
|
return -ENOMEM;
|
|
|
|
pw->props = props;
|
|
pw->fd = -1;
|
|
pw->io.poll_fd = -1;
|
|
pw->log_file = fopencookie(pw, "w", io_funcs);
|
|
if (pw->log_file == NULL) {
|
|
pw_log_error("can't create log file: %m");
|
|
err = -errno;
|
|
goto error;
|
|
}
|
|
if ((err = snd_output_stdio_attach(&pw->output, pw->log_file, 0)) < 0) {
|
|
pw_log_error("can't attach log file: %s", snd_strerror(err));
|
|
goto error;
|
|
}
|
|
|
|
pw->main_loop = pw_thread_loop_new("alsa-pipewire", NULL);
|
|
if (pw->main_loop == NULL) {
|
|
err = -errno;
|
|
goto error;
|
|
}
|
|
loop = pw_thread_loop_get_loop(pw->main_loop);
|
|
pw->system = loop->system;
|
|
if ((pw->context = pw_context_new(loop,
|
|
pw_properties_new(
|
|
PW_KEY_CONFIG_NAME, "client-rt.conf",
|
|
PW_KEY_CLIENT_API, "alsa",
|
|
NULL),
|
|
0)) == NULL) {
|
|
err = -errno;
|
|
goto error;
|
|
}
|
|
|
|
pw_context_conf_update_props(pw->context, "alsa.properties", pw->props);
|
|
|
|
pw_context_conf_section_match_rules(pw->context, "alsa.rules",
|
|
&pw_context_get_properties(pw->context)->dict, execute_match, pw);
|
|
|
|
if (pw_properties_get(pw->props, PW_KEY_APP_NAME) == NULL)
|
|
pw_properties_setf(pw->props, PW_KEY_APP_NAME, "PipeWire ALSA [%s]",
|
|
pw_get_prgname());
|
|
if (pw_properties_get(pw->props, PW_KEY_NODE_NAME) == NULL)
|
|
pw_properties_setf(pw->props, PW_KEY_NODE_NAME, "alsa_%s.%s",
|
|
stream == SND_PCM_STREAM_PLAYBACK ? "playback" : "capture",
|
|
pw_get_prgname());
|
|
if (pw_properties_get(pw->props, PW_KEY_NODE_DESCRIPTION) == NULL)
|
|
pw_properties_setf(pw->props, PW_KEY_NODE_DESCRIPTION, "ALSA %s [%s]",
|
|
stream == SND_PCM_STREAM_PLAYBACK ? "Playback" : "Capture",
|
|
pw_get_prgname());
|
|
if (pw_properties_get(pw->props, PW_KEY_MEDIA_NAME) == NULL)
|
|
pw_properties_setf(pw->props, PW_KEY_MEDIA_NAME, "ALSA %s",
|
|
stream == SND_PCM_STREAM_PLAYBACK ? "Playback" : "Capture");
|
|
if (pw_properties_get(pw->props, PW_KEY_MEDIA_TYPE) == NULL)
|
|
pw_properties_set(pw->props, PW_KEY_MEDIA_TYPE, "Audio");
|
|
if (pw_properties_get(pw->props, PW_KEY_MEDIA_CATEGORY) == NULL)
|
|
pw_properties_set(pw->props, PW_KEY_MEDIA_CATEGORY,
|
|
stream == SND_PCM_STREAM_PLAYBACK ?
|
|
"Playback" : "Capture");
|
|
|
|
str = getenv("PIPEWIRE_ALSA");
|
|
if (str != NULL)
|
|
pw_properties_update_string(pw->props, str, strlen(str));
|
|
|
|
if ((str = pw_properties_get(pw->props, "alsa.deny")) != NULL &&
|
|
spa_atob(str)) {
|
|
err = -EACCES;
|
|
goto error;
|
|
}
|
|
|
|
str = getenv("PIPEWIRE_NODE");
|
|
if (str != NULL && str[0])
|
|
pw_properties_set(pw->props, PW_KEY_TARGET_OBJECT, str);
|
|
|
|
node_name = pw_properties_get(pw->props, PW_KEY_NODE_NAME);
|
|
if (pw_properties_get(pw->props, PW_KEY_MEDIA_NAME) == NULL)
|
|
pw_properties_set(pw->props, PW_KEY_MEDIA_NAME, node_name);
|
|
|
|
if ((err = pw_thread_loop_start(pw->main_loop)) < 0)
|
|
goto error;
|
|
|
|
pw_thread_loop_lock(pw->main_loop);
|
|
pw->core = pw_context_connect(pw->context, pw_properties_copy(pw->props), 0);
|
|
if (pw->core == NULL) {
|
|
err = -errno;
|
|
pw_thread_loop_unlock(pw->main_loop);
|
|
goto error;
|
|
}
|
|
pw_core_add_listener(pw->core, &pw->core_listener, &core_events, pw);
|
|
pw_thread_loop_unlock(pw->main_loop);
|
|
|
|
pw->fd = spa_system_eventfd_create(pw->system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
|
|
if (pw->fd < 0) {
|
|
err = pw->fd;
|
|
goto error;
|
|
}
|
|
|
|
pw->io.version = SND_PCM_IOPLUG_VERSION;
|
|
pw->io.name = "ALSA <-> PipeWire PCM I/O Plugin";
|
|
pw->io.callback = &pipewire_pcm_callback;
|
|
pw->io.private_data = pw;
|
|
pw->io.poll_fd = pw->fd;
|
|
pw->io.poll_events = POLLIN;
|
|
pw->io.mmap_rw = 1;
|
|
#ifdef SND_PCM_IOPLUG_FLAG_BOUNDARY_WA
|
|
pw->io.flags = SND_PCM_IOPLUG_FLAG_BOUNDARY_WA;
|
|
#else
|
|
#warning hw_ptr updates of buffer_size will not be recognized by the ALSA library. Consider to update your ALSA library.
|
|
#endif
|
|
pw->io.flags |= SND_PCM_IOPLUG_FLAG_MONOTONIC;
|
|
|
|
if ((err = snd_pcm_ioplug_create(&pw->io, node_name, stream, mode)) < 0)
|
|
goto error;
|
|
|
|
if ((err = pipewire_set_hw_constraint(pw)) < 0)
|
|
goto error;
|
|
|
|
pw_log_debug("%p: opened name:%s stream:%s mode:%d", pw, node_name,
|
|
snd_pcm_stream_name(pw->io.stream), mode);
|
|
|
|
*pcmp = pw->io.pcm;
|
|
|
|
return 0;
|
|
|
|
error:
|
|
pw_log_debug("%p: failed to open %s :%s", pw, node_name, spa_strerror(err));
|
|
snd_pcm_pipewire_free(pw);
|
|
return err;
|
|
}
|
|
|
|
|
|
SPA_EXPORT
|
|
SND_PCM_PLUGIN_DEFINE_FUNC(pipewire)
|
|
{
|
|
snd_config_iterator_t i, next;
|
|
struct pw_properties *props;
|
|
const char *str;
|
|
long val;
|
|
int err;
|
|
|
|
pw_init(NULL, NULL);
|
|
if (spa_strstartswith(pw_get_library_version(), "0.2"))
|
|
return -ENOTSUP;
|
|
|
|
props = pw_properties_new(NULL, NULL);
|
|
if (props == NULL)
|
|
return -errno;
|
|
|
|
PW_LOG_TOPIC_INIT(alsa_log_topic);
|
|
|
|
snd_config_for_each(i, next, conf) {
|
|
snd_config_t *n = snd_config_iterator_entry(i);
|
|
const char *id;
|
|
if (snd_config_get_id(n, &id) < 0)
|
|
continue;
|
|
if (spa_streq(id, "comment") || spa_streq(id, "type") || spa_streq(id, "hint"))
|
|
continue;
|
|
if (spa_streq(id, "name")) {
|
|
if (snd_config_get_string(n, &str) == 0)
|
|
pw_properties_set(props, PW_KEY_NODE_NAME, str);
|
|
continue;
|
|
}
|
|
if (spa_streq(id, "server")) {
|
|
if (snd_config_get_string(n, &str) == 0)
|
|
pw_properties_set(props, PW_KEY_REMOTE_NAME, str);
|
|
continue;
|
|
}
|
|
if (spa_streq(id, "playback_node")) {
|
|
if (stream == SND_PCM_STREAM_PLAYBACK &&
|
|
snd_config_get_string(n, &str) == 0)
|
|
if (str != NULL && !spa_streq(str, "-1"))
|
|
pw_properties_set(props, PW_KEY_TARGET_OBJECT, str);
|
|
continue;
|
|
}
|
|
if (spa_streq(id, "capture_node")) {
|
|
if (stream == SND_PCM_STREAM_CAPTURE &&
|
|
snd_config_get_string(n, &str) == 0)
|
|
if (str != NULL && !spa_streq(str, "-1"))
|
|
pw_properties_set(props, PW_KEY_TARGET_OBJECT, str);
|
|
continue;
|
|
}
|
|
if (spa_streq(id, "role")) {
|
|
if (snd_config_get_string(n, &str) == 0)
|
|
if (str != NULL && *str)
|
|
pw_properties_set(props, PW_KEY_MEDIA_ROLE, str);
|
|
continue;
|
|
}
|
|
if (spa_streq(id, "exclusive")) {
|
|
if (snd_config_get_bool(n))
|
|
pw_properties_set(props, PW_KEY_NODE_EXCLUSIVE, "true");
|
|
continue;
|
|
}
|
|
if (spa_streq(id, "rate")) {
|
|
if (snd_config_get_integer(n, &val) == 0) {
|
|
if (val != 0)
|
|
pw_properties_setf(props, "alsa.rate", "%ld", val);
|
|
} else {
|
|
SNDERR("%s: invalid type", id);
|
|
}
|
|
continue;
|
|
}
|
|
if (spa_streq(id, "format")) {
|
|
if (snd_config_get_string(n, &str) == 0) {
|
|
if (str != NULL && *str)
|
|
pw_properties_set(props, "alsa.format", str);
|
|
} else {
|
|
SNDERR("%s: invalid type", id);
|
|
}
|
|
continue;
|
|
}
|
|
if (spa_streq(id, "channels")) {
|
|
if (snd_config_get_integer(n, &val) == 0) {
|
|
if (val != 0)
|
|
pw_properties_setf(props, "alsa.channels", "%ld", val);
|
|
} else {
|
|
SNDERR("%s: invalid type", id);
|
|
}
|
|
continue;
|
|
}
|
|
if (spa_streq(id, "period_bytes")) {
|
|
if (snd_config_get_integer(n, &val) == 0) {
|
|
if (val != 0)
|
|
pw_properties_setf(props, "alsa.period-bytes", "%ld", val);
|
|
} else {
|
|
SNDERR("%s: invalid type", id);
|
|
}
|
|
continue;
|
|
}
|
|
if (spa_streq(id, "buffer_bytes")) {
|
|
long val;
|
|
|
|
if (snd_config_get_integer(n, &val) == 0) {
|
|
if (val != 0)
|
|
pw_properties_setf(props, "alsa.buffer-bytes", "%ld", val);
|
|
} else {
|
|
SNDERR("%s: invalid type", id);
|
|
}
|
|
continue;
|
|
}
|
|
SNDERR("Unknown field %s", id);
|
|
pw_properties_free(props);
|
|
return -EINVAL;
|
|
}
|
|
|
|
err = snd_pcm_pipewire_open(pcmp, props, stream, mode);
|
|
|
|
return err;
|
|
}
|
|
|
|
SPA_EXPORT
|
|
SND_PCM_PLUGIN_SYMBOL(pipewire);
|