Files
cmus/op/pulse.c
2026-03-29 14:01:52 +03:00

652 lines
14 KiB
C

/*
* Copyright (C) 2008-2013 Various Authors
* Copyright (C) 2009 Gregory Petrosyan
*
* 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, see <http://www.gnu.org/licenses/>.
*/
#include <string.h>
#include <pulse/pulseaudio.h>
#include "../op.h"
#include "../mixer.h"
#include "../debug.h"
#include "../utils.h"
#include "../xmalloc.h"
static pa_threaded_mainloop *mainloop;
static pa_context *context;
static pa_stream *stream;
static pa_channel_map channel_map;
static pa_cvolume volume;
static pa_sample_spec sample_spec;
static int mixer_notify_in;
static int mixer_notify_out;
static int mixer_notify_output_in;
static int mixer_notify_output_out;
static long last_output_idx;
/* configuration */
static int pa_restore_volume = 1;
#define RET_PA_ERROR(err) \
do { \
d_print("PulseAudio error: %s\n", pa_strerror(err)); \
return -OP_ERROR_INTERNAL; \
} while (0)
#define RET_PA_LAST_ERROR() RET_PA_ERROR(pa_context_errno(context))
static int pulse_wait_and_unlock(pa_operation *op)
{
pa_operation_state_t state;
if (!op) {
pa_threaded_mainloop_unlock(mainloop);
RET_PA_LAST_ERROR();
}
while ((state = pa_operation_get_state(op)) == PA_OPERATION_RUNNING)
pa_threaded_mainloop_wait(mainloop);
pa_operation_unref(op);
pa_threaded_mainloop_unlock(mainloop);
if (state == PA_OPERATION_DONE)
return OP_ERROR_SUCCESS;
else
RET_PA_LAST_ERROR();
}
static int pulse_nowait_and_unlock(pa_operation *op)
{
if (!op) {
pa_threaded_mainloop_unlock(mainloop);
RET_PA_LAST_ERROR();
}
pa_operation_unref(op);
pa_threaded_mainloop_unlock(mainloop);
return OP_ERROR_SUCCESS;
}
static int pulse_wait(pa_operation *op)
{
pa_operation_state_t state;
if (!op)
RET_PA_LAST_ERROR();
while ((state = pa_operation_get_state(op)) == PA_OPERATION_RUNNING)
pa_threaded_mainloop_wait(mainloop);
pa_operation_unref(op);
if (state == PA_OPERATION_DONE)
return OP_ERROR_SUCCESS;
else
RET_PA_LAST_ERROR();
}
static pa_sample_format_t convert_sample_format(sample_format_t sf)
{
const int _signed = sf_get_signed(sf);
const int big_endian = sf_get_bigendian(sf);
const int sample_size = sf_get_sample_size(sf) * 8;
if (!_signed && sample_size == 8)
return PA_SAMPLE_U8;
if (_signed) {
switch (sample_size) {
case 16:
return big_endian ? PA_SAMPLE_S16BE : PA_SAMPLE_S16LE;
case 24:
return big_endian ? PA_SAMPLE_S24BE : PA_SAMPLE_S24LE;
case 32:
return big_endian ? PA_SAMPLE_S32BE : PA_SAMPLE_S32LE;
}
}
return PA_SAMPLE_INVALID;
}
#define RET_IF(x) case CHANNEL_POSITION_ ## x: return PA_CHANNEL_POSITION_ ## x
static pa_channel_position_t convert_channel_position(channel_position_t p)
{
switch (p) {
RET_IF(MONO);
RET_IF(FRONT_LEFT); RET_IF(FRONT_RIGHT); RET_IF(FRONT_CENTER);
RET_IF(REAR_CENTER); RET_IF(REAR_LEFT); RET_IF(REAR_RIGHT);
RET_IF(LFE);
RET_IF(FRONT_LEFT_OF_CENTER); RET_IF(FRONT_RIGHT_OF_CENTER);
RET_IF(SIDE_LEFT); RET_IF(SIDE_RIGHT);
RET_IF(TOP_CENTER);
RET_IF(TOP_FRONT_LEFT); RET_IF(TOP_FRONT_RIGHT); RET_IF(TOP_FRONT_CENTER);
RET_IF(TOP_REAR_LEFT); RET_IF(TOP_REAR_RIGHT); RET_IF(TOP_REAR_CENTER);
default:
return PA_CHANNEL_POSITION_INVALID;
}
}
static pa_proplist *pulse_create_app_proplist(void)
{
pa_proplist *pl = pa_proplist_new();
BUG_ON(!pl);
int rc = pa_proplist_sets(pl, PA_PROP_APPLICATION_ID, "cmus");
BUG_ON(rc);
rc = pa_proplist_sets(pl, PA_PROP_APPLICATION_NAME, "C* Music Player");
BUG_ON(rc);
rc = pa_proplist_sets(pl, PA_PROP_APPLICATION_VERSION, VERSION);
BUG_ON(rc);
return pl;
}
static pa_proplist *pulse_create_stream_proplist(void)
{
pa_proplist *pl = pa_proplist_new();
BUG_ON(!pl);
int rc = pa_proplist_sets(pl, PA_PROP_MEDIA_ROLE, "music");
BUG_ON(rc);
rc = pa_proplist_sets(pl, PA_PROP_MEDIA_ICON_NAME, "audio-x-generic");
BUG_ON(rc);
return pl;
}
static const char *pa_context_state_str(pa_context_state_t s)
{
switch (s) {
case PA_CONTEXT_AUTHORIZING:
return "PA_CONTEXT_AUTHORIZING";
case PA_CONTEXT_CONNECTING:
return "PA_CONTEXT_CONNECTING";
case PA_CONTEXT_FAILED:
return "PA_CONTEXT_FAILED";
case PA_CONTEXT_READY:
return "PA_CONTEXT_READY";
case PA_CONTEXT_SETTING_NAME:
return "PA_CONTEXT_SETTING_NAME";
case PA_CONTEXT_TERMINATED:
return "PA_CONTEXT_TERMINATED";
case PA_CONTEXT_UNCONNECTED:
return "PA_CONTEXT_UNCONNECTED";
}
return "unknown";
}
static void pulse_context_state_cb(pa_context *c, void *data)
{
const pa_context_state_t cs = pa_context_get_state(c);
d_print("context state has changed to %s\n", pa_context_state_str(cs));
switch (cs) {
case PA_CONTEXT_READY:
case PA_CONTEXT_FAILED:
case PA_CONTEXT_TERMINATED:
pa_threaded_mainloop_signal(mainloop, 0);
default:
return;
}
}
static const char *pa_stream_state_str(pa_stream_state_t s)
{
switch (s) {
case PA_STREAM_CREATING:
return "PA_STREAM_CREATING";
case PA_STREAM_FAILED:
return "PA_STREAM_FAILED";
case PA_STREAM_READY:
return "PA_STREAM_READY";
case PA_STREAM_TERMINATED:
return "PA_STREAM_TERMINATED";
case PA_STREAM_UNCONNECTED:
return "PA_STREAM_UNCONNECTED";
}
return "unknown";
}
static void pulse_stream_state_cb(pa_stream *s, void *data)
{
const pa_stream_state_t ss = pa_stream_get_state(s);
d_print("stream state has changed to %s\n", pa_stream_state_str(ss));
switch (ss) {
case PA_STREAM_READY:
case PA_STREAM_FAILED:
case PA_STREAM_TERMINATED:
pa_threaded_mainloop_signal(mainloop, 0);
default:
return;
}
}
static void pulse_sink_input_info_cb(pa_context *c,
const pa_sink_input_info *i, int eol, void *data)
{
if (!i)
return;
memcpy(&volume, &i->volume, sizeof(volume));
notify_via_pipe(mixer_notify_in);
if (last_output_idx != i->sink) {
if (last_output_idx != -1)
notify_via_pipe(mixer_notify_output_in);
last_output_idx = i->sink;
}
}
static void pulse_context_subscription_cb(pa_context *ctx,
pa_subscription_event_type_t type, uint32_t idx, void *data)
{
type &= PA_SUBSCRIPTION_EVENT_TYPE_MASK;
if (type != PA_SUBSCRIPTION_EVENT_CHANGE)
return;
if (stream && idx == pa_stream_get_index(stream)) {
pa_context_get_sink_input_info(ctx, idx,
pulse_sink_input_info_cb, NULL);
}
}
static int pulse_create_context(void)
{
pa_mainloop_api *api = pa_threaded_mainloop_get_api(mainloop);
BUG_ON(!api);
pa_proplist *pl = pulse_create_app_proplist();
pa_threaded_mainloop_lock(mainloop);
context = pa_context_new_with_proplist(api, "C* Music Player", pl);
pa_proplist_free(pl);
BUG_ON(!context);
pa_context_set_state_callback(context, pulse_context_state_cb, NULL);
int rc = pa_context_connect(context, NULL, PA_CONTEXT_NOFLAGS, NULL);
if (rc)
goto err_free_ctx;
for (;;) {
pa_context_state_t s = pa_context_get_state(context);
if (s == PA_CONTEXT_READY)
break;
if (s == PA_CONTEXT_TERMINATED || s == PA_CONTEXT_FAILED)
goto err_disconnect_ctx;
pa_threaded_mainloop_wait(mainloop);
}
pa_context_set_subscribe_callback(context,
pulse_context_subscription_cb, NULL);
pa_operation *op = pa_context_subscribe(context,
PA_SUBSCRIPTION_MASK_SINK_INPUT, NULL, NULL);
if (!op)
goto err_disconnect_ctx;
pa_operation_unref(op);
pa_threaded_mainloop_unlock(mainloop);
return OP_ERROR_SUCCESS;
err_disconnect_ctx:
pa_context_disconnect(context);
err_free_ctx:
pa_context_unref(context);
context = NULL;
pa_threaded_mainloop_unlock(mainloop);
RET_PA_LAST_ERROR();
}
static void pulse_stream_success_cb(pa_stream *s, int success, void *data)
{
pa_threaded_mainloop_signal(mainloop, 0);
}
static int op_pulse_init(void)
{
mainloop = pa_threaded_mainloop_new();
BUG_ON(!mainloop);
int rc = pa_threaded_mainloop_start(mainloop);
if (rc) {
pa_threaded_mainloop_free(mainloop);
RET_PA_ERROR(rc);
}
return OP_ERROR_SUCCESS;
}
static int op_pulse_exit(void)
{
if (mainloop) {
pa_threaded_mainloop_stop(mainloop);
pa_threaded_mainloop_free(mainloop);
mainloop = NULL;
}
return OP_ERROR_SUCCESS;
}
static int op_pulse_open(sample_format_t sf, const channel_position_t *cmap)
{
pa_proplist *pl;
int rc, i;
const pa_sample_spec ss = {
.format = convert_sample_format(sf),
.rate = sf_get_rate(sf),
.channels = sf_get_channels(sf)
};
if (!pa_sample_spec_valid(&ss))
return -OP_ERROR_SAMPLE_FORMAT;
sample_spec = ss;
if (cmap && channel_map_valid(cmap)) {
pa_channel_map_init(&channel_map);
channel_map.channels = ss.channels;
for (i = 0; i < channel_map.channels; i++)
channel_map.map[i] = convert_channel_position(cmap[i]);
} else {
pa_channel_map_init_auto(&channel_map, ss.channels,
PA_CHANNEL_MAP_ALSA);
}
last_output_idx = -1;
rc = pulse_create_context();
if (rc)
return rc;
pl = pulse_create_stream_proplist();
pa_threaded_mainloop_lock(mainloop);
stream = pa_stream_new_with_proplist(context, "playback",
&ss, &channel_map, pl);
pa_proplist_free(pl);
if (!stream)
goto err;
pa_stream_set_state_callback(stream, pulse_stream_state_cb, NULL);
rc = pa_stream_connect_playback(stream, NULL, NULL, PA_STREAM_NOFLAGS,
pa_restore_volume ? NULL : &volume, NULL);
if (rc)
goto err_free_stream;
for (;;) {
pa_stream_state_t s = pa_stream_get_state(stream);
if (s == PA_STREAM_READY)
break;
if (s == PA_STREAM_FAILED || s == PA_STREAM_TERMINATED)
goto err_free_stream;
pa_threaded_mainloop_wait(mainloop);
}
pa_context_get_sink_input_info(context, pa_stream_get_index(stream),
pulse_sink_input_info_cb, NULL);
pa_threaded_mainloop_unlock(mainloop);
return OP_ERROR_SUCCESS;
err_free_stream:
pa_stream_unref(stream);
err:
pa_threaded_mainloop_unlock(mainloop);
RET_PA_LAST_ERROR();
}
static void pulse_drain_if_playing(void)
{
if (!pa_stream_is_corked(stream)
&& !pa_stream_is_suspended(stream)
&& pa_stream_get_state(stream) == PA_STREAM_READY)
{
pa_operation *op = pa_stream_drain(stream,
pulse_stream_success_cb, NULL);
pulse_wait(op);
}
}
static int op_pulse_close(void)
{
pa_threaded_mainloop_lock(mainloop);
if (stream) {
pulse_drain_if_playing();
pa_stream_disconnect(stream);
pa_stream_unref(stream);
stream = NULL;
}
if (context) {
pa_context_disconnect(context);
pa_context_unref(context);
context = NULL;
}
pa_threaded_mainloop_unlock(mainloop);
return OP_ERROR_SUCCESS;
}
static int op_pulse_drop(void)
{
pa_threaded_mainloop_lock(mainloop);
pa_operation *op = pa_stream_flush(stream, pulse_stream_success_cb, NULL);
return pulse_wait_and_unlock(op);
}
static int op_pulse_write(const char *buf, int count)
{
pa_threaded_mainloop_lock(mainloop);
int rc = pa_stream_write(stream, buf, count, NULL, 0, PA_SEEK_RELATIVE);
pa_threaded_mainloop_unlock(mainloop);
if (rc)
RET_PA_ERROR(rc);
else
return count;
}
static int op_pulse_buffer_space(void)
{
pa_threaded_mainloop_lock(mainloop);
size_t s = pa_stream_writable_size(stream);
pa_threaded_mainloop_unlock(mainloop);
if (s == (size_t)-1)
RET_PA_LAST_ERROR();
else
return s;
}
static int pulse_stream_cork(int pause)
{
pa_threaded_mainloop_lock(mainloop);
pa_operation *op = pa_stream_cork(stream, pause,
pulse_stream_success_cb, NULL);
return pulse_wait_and_unlock(op);
}
static int op_pulse_pause(void)
{
return pulse_stream_cork(1);
}
static int op_pulse_unpause(void)
{
return pulse_stream_cork(0);
}
static int op_pulse_mixer_init(void)
{
if (!pa_channel_map_init_stereo(&channel_map))
RET_PA_LAST_ERROR();
pa_cvolume_reset(&volume, 2);
init_pipes(&mixer_notify_out, &mixer_notify_in);
init_pipes(&mixer_notify_output_out, &mixer_notify_output_in);
return OP_ERROR_SUCCESS;
}
static int op_pulse_mixer_exit(void)
{
close(mixer_notify_out);
close(mixer_notify_in);
close(mixer_notify_output_out);
close(mixer_notify_output_in);
return OP_ERROR_SUCCESS;
}
static int op_pulse_mixer_open(int *volume_max)
{
*volume_max = PA_VOLUME_NORM;
return OP_ERROR_SUCCESS;
}
static int op_pulse_mixer_close(void)
{
return OP_ERROR_SUCCESS;
}
static int op_pulse_mixer_get_fds(int what, int *fds)
{
switch (what) {
case MIXER_FDS_VOLUME:
fds[0] = mixer_notify_out;
return 1;
case MIXER_FDS_OUTPUT:
fds[0] = mixer_notify_output_out;
return 1;
default:
return 0;
}
}
static int op_pulse_mixer_set_volume(int l, int r)
{
if (!stream && pa_restore_volume)
return -OP_ERROR_NOT_OPEN;
pa_cvolume_set(&volume, sample_spec.channels, (pa_volume_t)((l + r) / 2));
pa_cvolume_set_position(&volume, &channel_map,
PA_CHANNEL_POSITION_FRONT_LEFT, (pa_volume_t)l);
pa_cvolume_set_position(&volume, &channel_map,
PA_CHANNEL_POSITION_FRONT_RIGHT, (pa_volume_t)r);
if (!stream) {
return OP_ERROR_SUCCESS;
} else {
pa_threaded_mainloop_lock(mainloop);
pa_operation *op = pa_context_set_sink_input_volume(
context, pa_stream_get_index(stream),
&volume, NULL, NULL);
return pulse_nowait_and_unlock(op);
}
}
static int op_pulse_mixer_get_volume(int *l, int *r)
{
clear_pipe(mixer_notify_out, -1);
if (!stream && pa_restore_volume)
return -OP_ERROR_NOT_OPEN;
*l = pa_cvolume_get_position(&volume, &channel_map,
PA_CHANNEL_POSITION_FRONT_LEFT);
*r = pa_cvolume_get_position(&volume, &channel_map,
PA_CHANNEL_POSITION_FRONT_RIGHT);
return OP_ERROR_SUCCESS;
}
static int op_pulse_set_restore_volume(const char *val)
{
pa_restore_volume = is_freeform_true(val);
return 0;
}
static int op_pulse_get_restore_volume(char **val)
{
*val = xstrdup(pa_restore_volume ? "1" : "0");
return 0;
}
const struct output_plugin_ops op_pcm_ops = {
.init = op_pulse_init,
.exit = op_pulse_exit,
.open = op_pulse_open,
.close = op_pulse_close,
.drop = op_pulse_drop,
.write = op_pulse_write,
.buffer_space = op_pulse_buffer_space,
.pause = op_pulse_pause,
.unpause = op_pulse_unpause,
};
const struct mixer_plugin_ops op_mixer_ops = {
.init = op_pulse_mixer_init,
.exit = op_pulse_mixer_exit,
.open = op_pulse_mixer_open,
.close = op_pulse_mixer_close,
.get_fds.abi_2 = op_pulse_mixer_get_fds,
.set_volume = op_pulse_mixer_set_volume,
.get_volume = op_pulse_mixer_get_volume,
};
const struct output_plugin_opt op_pcm_options[] = {
{ NULL },
};
const struct mixer_plugin_opt op_mixer_options[] = {
OPT(op_pulse, restore_volume),
{ NULL },
};
const int op_priority = -2;
const unsigned op_abi_version = OP_ABI_VERSION;