Skip to content

Commit

Permalink
Add PipeWire audio backend
Browse files Browse the repository at this point in the history
based on this original pull request
mpv-player#7902 by andreaskem
  • Loading branch information
Oschowa committed Feb 18, 2021
1 parent c766e47 commit fddb143
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 0 deletions.
4 changes: 4 additions & 0 deletions audio/out/ao.c
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ extern const struct ao_driver audio_out_audiounit;
extern const struct ao_driver audio_out_coreaudio;
extern const struct ao_driver audio_out_coreaudio_exclusive;
extern const struct ao_driver audio_out_rsound;
extern const struct ao_driver audio_out_pipewire;
extern const struct ao_driver audio_out_pulse;
extern const struct ao_driver audio_out_jack;
extern const struct ao_driver audio_out_openal;
Expand All @@ -62,6 +63,9 @@ static const struct ao_driver * const audio_out_drivers[] = {
#if HAVE_COREAUDIO
&audio_out_coreaudio,
#endif
#if HAVE_PIPEWIRE
&audio_out_pipewire,
#endif
#if HAVE_PULSE
&audio_out_pulse,
#endif
Expand Down
284 changes: 284 additions & 0 deletions audio/out/ao_pipewire.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@

#include <pipewire/pipewire.h>
#include <spa/param/audio/format-utils.h>

#include "common/msg.h"
#include "options/m_config.h"
#include "options/m_option.h"
#include "ao.h"
#include "audio/format.h"
#include "config.h"
#include "internal.h"
#include "osdep/timer.h"

struct ao_pipewire_opts {
int buffer_samples;
};

#define OPT_BASE_STRUCT struct ao_pipewire_opts
static const struct m_sub_options ao_pipewire_conf = {
.opts = (const struct m_option[]) {
{"pipewire-buffer-samples", OPT_INT(buffer_samples)},
{0}
},
.defaults = &(const struct ao_pipewire_opts) {
.buffer_samples = 2048,
},
.size = sizeof(struct ao_pipewire_opts),
};

struct priv {
struct pw_thread_loop *loop;
struct pw_stream *stream;

struct ao_pipewire_opts *opts;
};

static enum spa_audio_format af_fmt_to_pw(enum af_format format)
{
switch (format) {
case AF_FORMAT_U8: return SPA_AUDIO_FORMAT_U8;
case AF_FORMAT_S16: return SPA_AUDIO_FORMAT_S16;
case AF_FORMAT_S32: return SPA_AUDIO_FORMAT_S32;
case AF_FORMAT_FLOAT: return SPA_AUDIO_FORMAT_F32;
case AF_FORMAT_DOUBLE: return SPA_AUDIO_FORMAT_F64;
case AF_FORMAT_U8P: return SPA_AUDIO_FORMAT_U8P;
case AF_FORMAT_S16P: return SPA_AUDIO_FORMAT_S16P;
case AF_FORMAT_S32P: return SPA_AUDIO_FORMAT_S32P;
case AF_FORMAT_FLOATP: return SPA_AUDIO_FORMAT_F32P;
case AF_FORMAT_DOUBLEP: return SPA_AUDIO_FORMAT_F64P;
default: return SPA_AUDIO_FORMAT_UNKNOWN;
}
}

static const enum spa_audio_channel mp_speaker_id_to_spa[] = {
[MP_SPEAKER_ID_FL] = SPA_AUDIO_CHANNEL_FL,
[MP_SPEAKER_ID_FR] = SPA_AUDIO_CHANNEL_FR,
[MP_SPEAKER_ID_FC] = SPA_AUDIO_CHANNEL_FC,
[MP_SPEAKER_ID_LFE] = SPA_AUDIO_CHANNEL_LFE,
[MP_SPEAKER_ID_BL] = SPA_AUDIO_CHANNEL_RL,
[MP_SPEAKER_ID_BR] = SPA_AUDIO_CHANNEL_RR,
[MP_SPEAKER_ID_FLC] = SPA_AUDIO_CHANNEL_FLC,
[MP_SPEAKER_ID_FRC] = SPA_AUDIO_CHANNEL_FRC,
[MP_SPEAKER_ID_BC] = SPA_AUDIO_CHANNEL_RC,
[MP_SPEAKER_ID_SL] = SPA_AUDIO_CHANNEL_SL,
[MP_SPEAKER_ID_SR] = SPA_AUDIO_CHANNEL_SR,
[MP_SPEAKER_ID_TC] = SPA_AUDIO_CHANNEL_TC,
[MP_SPEAKER_ID_TFL] = SPA_AUDIO_CHANNEL_TFL,
[MP_SPEAKER_ID_TFC] = SPA_AUDIO_CHANNEL_TFC,
[MP_SPEAKER_ID_TFR] = SPA_AUDIO_CHANNEL_TFR,
[MP_SPEAKER_ID_TBL] = SPA_AUDIO_CHANNEL_TRL,
[MP_SPEAKER_ID_TBC] = SPA_AUDIO_CHANNEL_TRC,
[MP_SPEAKER_ID_TBR] = SPA_AUDIO_CHANNEL_TRR,
[MP_SPEAKER_ID_DL] = SPA_AUDIO_CHANNEL_FL,
[MP_SPEAKER_ID_DR] = SPA_AUDIO_CHANNEL_FR,
[MP_SPEAKER_ID_WL] = SPA_AUDIO_CHANNEL_FL,
[MP_SPEAKER_ID_WR] = SPA_AUDIO_CHANNEL_FR,
[MP_SPEAKER_ID_SDL] = SPA_AUDIO_CHANNEL_SL,
[MP_SPEAKER_ID_SDR] = SPA_AUDIO_CHANNEL_SL,
[MP_SPEAKER_ID_LFE2] = SPA_AUDIO_CHANNEL_LFE2,
[MP_SPEAKER_ID_NA] = SPA_AUDIO_CHANNEL_NA,
};

static void on_process(void *userdata)
{
struct ao *ao = userdata;
struct priv *p = ao->priv;
struct pw_time time;
struct pw_buffer *b;
void *data[MP_NUM_CHANNELS];

if ((b = pw_stream_dequeue_buffer(p->stream)) == NULL) {
pw_log_warn("out of buffers: %m");
return;
}

struct spa_buffer *buf = b->buffer;

int bytes_per_channel = buf->datas[0].maxsize / ao->channels.num;
int nframes = bytes_per_channel / ao->sstride;

for (int i = 0; i < buf->n_datas; i++) {
data[i] = buf->datas[i].data;
buf->datas[i].chunk->size = bytes_per_channel;
buf->datas[i].chunk->offset = 0;
}

pw_stream_get_time(p->stream, &time);
if (time.rate.denom == 0)
time.rate.denom = ao->samplerate;

int64_t end_time = mp_time_us();
/* time.queued is always going to be 0, so we don't need to care */
end_time += (nframes + time.delay) * SPA_USEC_PER_SEC / time.rate.denom;

ao_read_data(ao, data, nframes, end_time);

pw_stream_queue_buffer(p->stream, b);
}

static void on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param)
{
struct ao *ao = userdata;
struct priv *p = ao->priv;
const struct spa_pod *params[1];
uint8_t buffer[1024];
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));

if (param == NULL || id != SPA_PARAM_Format)
return;

int buffer_size = ao->device_buffer * af_fmt_to_bytes(ao->format) * ao->channels.num;

params[0] = spa_pod_builder_add_object(&b,
SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(ao->num_planes),
SPA_PARAM_BUFFERS_size, SPA_POD_Int(buffer_size),
SPA_PARAM_BUFFERS_stride, SPA_POD_Int(ao->sstride));

pw_stream_update_params(p->stream, params, 1);
}

static void on_state_changed(void *userdata, enum pw_stream_state old, enum pw_stream_state state, const char *error)
{
struct ao *ao = userdata;
MP_DBG(ao, "Stream state changed: old_state=%d state=%d error=%s\n", old, state, error);

if (state == PW_STREAM_STATE_ERROR) {
MP_WARN(ao, "Stream in error state, trying to reload...\n");
ao_request_reload(ao);
}
}

static const struct pw_stream_events stream_events = {
.version = PW_VERSION_STREAM_EVENTS,
.param_changed = on_param_changed,
.process = on_process,
.state_changed = on_state_changed,
};

static void uninit(struct ao *ao)
{
struct priv *p = ao->priv;
if (p->loop)
pw_thread_loop_stop(p->loop);
if (p->stream)
pw_stream_destroy(p->stream);
p->stream = NULL;
if (p->loop)
pw_thread_loop_destroy(p->loop);
p->loop = NULL;
pw_deinit();
}

static int init(struct ao *ao)
{
struct priv *p = ao->priv;
uint8_t buffer[1024];
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
const struct spa_pod *params[1];
char latency_str[64];

p->opts = mp_get_config_group(ao, ao->global, &ao_pipewire_conf);

enum spa_audio_format spa_format = af_fmt_to_pw(ao->format);
if (spa_format == SPA_AUDIO_FORMAT_UNKNOWN) {
ao->format = AF_FORMAT_FLOATP;
spa_format = SPA_AUDIO_FORMAT_F32P;
}

struct spa_audio_info_raw audio_info = {
.format = spa_format,
.rate = ao->samplerate,
.channels = ao->channels.num,
};

for (int i = 0; i < ao->channels.num; i++)
audio_info.position[i] = mp_speaker_id_to_spa[ao->channels.speaker[i]];

params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &audio_info);

if (af_fmt_is_planar(ao->format)) {
ao->num_planes = ao->channels.num;
ao->sstride = af_fmt_to_bytes(ao->format);
} else {
ao->num_planes = 1;
ao->sstride = ao->channels.num * af_fmt_to_bytes(ao->format);
}

ao->device_buffer = p->opts->buffer_samples;
snprintf(latency_str, sizeof(latency_str), "%d/%d", ao->device_buffer, ao->samplerate);

pw_init(NULL, NULL);

p->loop = pw_thread_loop_new("ao-pipewire", NULL);
if (p->loop == NULL)
goto error;

p->stream = pw_stream_new_simple(
pw_thread_loop_get_loop(p->loop),
"audio-src",
pw_properties_new(
PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, "Playback",
PW_KEY_MEDIA_ROLE, "Music",
PW_KEY_NODE_NAME, "mpv",
PW_KEY_NODE_LATENCY, latency_str,
NULL),
&stream_events,
ao);
if (p->stream == NULL)
goto error;

if (pw_stream_connect(p->stream,
PW_DIRECTION_OUTPUT,
PW_ID_ANY,
PW_STREAM_FLAG_AUTOCONNECT |
PW_STREAM_FLAG_MAP_BUFFERS |
PW_STREAM_FLAG_RT_PROCESS,
params, 1) < 0)
goto error;

if (pw_thread_loop_start(p->loop) < 0)
goto error;

return 0;

error:
uninit(ao);
return -1;
}

static void reset(struct ao *ao)
{
struct priv *p = ao->priv;
pw_thread_loop_lock(p->loop);
pw_stream_set_active(p->stream, false);
pw_thread_loop_unlock(p->loop);
}

static void start(struct ao *ao)
{
struct priv *p = ao->priv;
pw_thread_loop_lock(p->loop);
pw_stream_set_active(p->stream, true);
pw_thread_loop_unlock(p->loop);
}

const struct ao_driver audio_out_pipewire = {
.description = "PipeWire audio output",
.name = "pipewire",

.init = init,
.uninit = uninit,
.reset = reset,
.start = start,

.priv_size = sizeof(struct priv),
.priv_defaults = &(const struct priv)
{
.loop = NULL,
.stream = NULL,
},
.global_opts = &ao_pipewire_conf,
};
4 changes: 4 additions & 0 deletions wscript
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,10 @@ audio_output_features = [
'desc': 'SDL2 audio output',
'deps': 'sdl2',
'func': check_true,
}, {
'name': '--pipewire',
'desc': 'PipeWire audio output',
'func': check_pkg_config('libpipewire-0.3', '>= 0.3.0')
}, {
'name': '--pulse',
'desc': 'PulseAudio audio output',
Expand Down
1 change: 1 addition & 0 deletions wscript_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ def swift(task):
( "audio/out/ao_openal.c", "openal" ),
( "audio/out/ao_opensles.c", "opensles" ),
( "audio/out/ao_pcm.c" ),
( "audio/out/ao_pipewire.c", "pipewire" ),
( "audio/out/ao_pulse.c", "pulse" ),
( "audio/out/ao_sdl.c", "sdl2-audio" ),
( "audio/out/ao_wasapi.c", "wasapi" ),
Expand Down

0 comments on commit fddb143

Please sign in to comment.