/*-*- linux-c -*-*/
/*
* ALSA <-> PulseAudio mixer control plugin
*
* Copyright (c) 2006 by Pierre Ossman <ossman@cendio.se>
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <sys/poll.h>
#include <alsa/asoundlib.h>
#include <alsa/control_external.h>
#include "pulse.h"
typedef struct snd_ctl_pulse {
snd_ctl_ext_t ext;
snd_pulse_t *p;
char *source;
char *sink;
pa_cvolume sink_volume;
pa_cvolume source_volume;
int sink_muted;
int source_muted;
int subscribed;
int updated;
} snd_ctl_pulse_t;
#define SOURCE_VOL_NAME "Capture Volume"
#define SOURCE_MUTE_NAME "Capture Switch"
#define SINK_VOL_NAME "Master Playback Volume"
#define SINK_MUTE_NAME "Master Playback Switch"
#define UPDATE_SINK_VOL 0x01
#define UPDATE_SINK_MUTE 0x02
#define UPDATE_SOURCE_VOL 0x04
#define UPDATE_SOURCE_MUTE 0x08
static void sink_info_cb(pa_context * c, const pa_sink_info * i,
int is_last, void *userdata)
{
snd_ctl_pulse_t *ctl = (snd_ctl_pulse_t *) userdata;
int changed = 0;
assert(ctl);
if (is_last) {
pa_threaded_mainloop_signal(ctl->p->mainloop, 0);
return;
}
assert(i);
if (!!ctl->sink_muted != !!i->mute) {
ctl->sink_muted = i->mute;
ctl->updated |= UPDATE_SINK_MUTE;
changed = 1;
}
if (!pa_cvolume_equal(&ctl->sink_volume, &i->volume)) {
ctl->sink_volume = i->volume;
ctl->updated |= UPDATE_SINK_VOL;
changed = 1;
}
if (changed)
pulse_poll_activate(ctl->p);
}
static void source_info_cb(pa_context * c, const pa_source_info * i,
int is_last, void *userdata)
{
snd_ctl_pulse_t *ctl = (snd_ctl_pulse_t *) userdata;
int changed = 0;
assert(ctl);
if (is_last) {
pa_threaded_mainloop_signal(ctl->p->mainloop, 0);
return;
}
assert(i);
if (!!ctl->source_muted != !!i->mute) {
ctl->source_muted = i->mute;
ctl->updated |= UPDATE_SOURCE_MUTE;
changed = 1;
}
if (!pa_cvolume_equal(&ctl->source_volume, &i->volume)) {
ctl->source_volume = i->volume;
ctl->updated |= UPDATE_SOURCE_VOL;
changed = 1;
}
if (changed)
pulse_poll_activate(ctl->p);
}
static void event_cb(pa_context * c, pa_subscription_event_type_t t,
uint32_t index, void *userdata)
{
snd_ctl_pulse_t *ctl = (snd_ctl_pulse_t *) userdata;
pa_operation *o;
assert(ctl);
if (!ctl->p || !ctl->p->mainloop || !ctl->p->context)
return;
o = pa_context_get_sink_info_by_name(ctl->p->context, ctl->sink,
sink_info_cb, ctl);
if (o)
pa_operation_unref(o);
o = pa_context_get_source_info_by_name(ctl->p->context,
ctl->source, source_info_cb,
ctl);
if (o)
pa_operation_unref(o);
}
static int pulse_update_volume(snd_ctl_pulse_t * ctl)
{
int err;
pa_operation *o;
assert(ctl);
if (!ctl->p)
return -EBADFD;
err = pulse_check_connection(ctl->p);
if (err < 0)
return err;
o = pa_context_get_sink_info_by_name(ctl->p->context, ctl->sink,
sink_info_cb, ctl);
if (o) {
err = pulse_wait_operation(ctl->p, o);
pa_operation_unref(o);
} else
err = -EIO;
if (err < 0)
return err;
o = pa_context_get_source_info_by_name(ctl->p->context,
ctl->source, source_info_cb,
ctl);
if (o) {
err = pulse_wait_operation(ctl->p, o);
pa_operation_unref(o);
} else
err = -EIO;
if (err < 0)
return err;
return 0;
}
static int pulse_elem_count(snd_ctl_ext_t * ext)
{
snd_ctl_pulse_t *ctl = ext->private_data;
int count = 0, err;
assert(ctl);
if (!ctl->p || !ctl->p->mainloop)
return -EBADFD;
pa_threaded_mainloop_lock(ctl->p->mainloop);
err = pulse_check_connection(ctl->p);
if (err < 0) {
count = err;
goto finish;
}
if (ctl->source)
count += 2;
if (ctl->sink)
count += 2;
finish:
pa_threaded_mainloop_unlock(ctl->p->mainloop);
return count;
}
static int pulse_elem_list(snd_ctl_ext_t * ext, unsigned int offset,
snd_ctl_elem_id_t * id)
{
snd_ctl_pulse_t *ctl = ext->private_data;
int err;
assert(ctl);
if (!ctl->p || !ctl->p->mainloop)
return -EBADFD;
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER);
pa_threaded_mainloop_lock(ctl->p->mainloop);
err = pulse_check_connection(ctl->p);
if (err < 0)
goto finish;
if (ctl->source) {
if (offset == 0)
snd_ctl_elem_id_set_name(id, SOURCE_VOL_NAME);
else if (offset == 1)
snd_ctl_elem_id_set_name(id, SOURCE_MUTE_NAME);
} else
offset += 2;
err = 0;
finish:
pa_threaded_mainloop_unlock(ctl->p->mainloop);
if (err >= 0) {
if (offset == 2)
snd_ctl_elem_id_set_name(id, SINK_VOL_NAME);
else if (offset == 3)
snd_ctl_elem_id_set_name(id, SINK_MUTE_NAME);
}
return err;
}
static snd_ctl_ext_key_t pulse_find_elem(snd_ctl_ext_t * ext,
const snd_ctl_elem_id_t * id)
{
const char *name;
unsigned int numid;
numid = snd_ctl_elem_id_get_numid(id);
if (numid > 0 && numid <= 4)
return numid - 1;
name = snd_ctl_elem_id_get_name(id);
if (strcmp(name, SOURCE_VOL_NAME) == 0)
return 0;
if (strcmp(name, SOURCE_MUTE_NAME) == 0)
return 1;
if (strcmp(name, SINK_VOL_NAME) == 0)
return 2;
if (strcmp(name, SINK_MUTE_NAME) == 0)
return 3;
return SND_CTL_EXT_KEY_NOT_FOUND;
}
static int pulse_get_attribute(snd_ctl_ext_t * ext, snd_ctl_ext_key_t key,
int *type, unsigned int *acc,
unsigned int *count)
{
snd_ctl_pulse_t *ctl = ext->private_data;
int err = 0;
if (key > 3)
return -EINVAL;
assert(ctl);
if (!ctl->p || !ctl->p->mainloop)
return -EBADFD;
pa_threaded_mainloop_lock(ctl->p->mainloop);
err = pulse_check_connection(ctl->p);
if (err < 0)
goto finish;
err = pulse_update_volume(ctl);
if (err < 0)
goto finish;
if (key & 1)
*type = SND_CTL_ELEM_TYPE_BOOLEAN;
else
*type = SND_CTL_ELEM_TYPE_INTEGER;
*acc = SND_CTL_EXT_ACCESS_READWRITE;
if (key == 0)
*count = ctl->source_volume.channels;
else if (key == 2)
*count = ctl->sink_volume.channels;
else
*count = 1;
finish:
pa_threaded_mainloop_unlock(ctl->p->mainloop);
return err;
}
static int pulse_get_integer_info(snd_ctl_ext_t * ext,
snd_ctl_ext_key_t key, long *imin,
long *imax, long *istep)
{
*istep = 1;
*imin = 0;
*imax = PA_VOLUME_NORM;
return 0;
}
static int pulse_read_integer(snd_ctl_ext_t * ext, snd_ctl_ext_key_t key,
long *value)
{
snd_ctl_pulse_t *ctl = ext->private_data;
int err = 0, i;
pa_cvolume *vol = NULL;
assert(ctl);
if (!ctl->p || !ctl->p->mainloop)
return -EBADFD;
pa_threaded_mainloop_lock(ctl->p->mainloop);
err = pulse_check_connection(ctl->p);
if (err < 0)
goto finish;
err = pulse_update_volume(ctl);
if (err < 0)
goto finish;
switch (key) {
case 0:
vol = &ctl->source_volume;
break;
case 1:
*value = !ctl->source_muted;
break;
case 2:
vol = &ctl->sink_volume;
break;
case 3:
*value = !ctl->sink_muted;
break;
default:
err = -EINVAL;
goto finish;
}
if (vol) {
for (i = 0; i < vol->channels; i++)
value[i] = vol->values[i];
}
finish:
pa_threaded_mainloop_unlock(ctl->p->mainloop);
return err;
}
static int pulse_write_integer(snd_ctl_ext_t * ext, snd_ctl_ext_key_t key,
long *value)
{
snd_ctl_pulse_t *ctl = ext->private_data;
int err = 0, i;
pa_operation *o;
pa_cvolume *vol = NULL;
assert(ctl);
if (!ctl->p || !ctl->p->mainloop)
return -EBADFD;
pa_threaded_mainloop_lock(ctl->p->mainloop);
err = pulse_check_connection(ctl->p);
if (err < 0)
goto finish;
err = pulse_update_volume(ctl);
if (err < 0)
goto finish;
switch (key) {
case 0:
vol = &ctl->source_volume;
break;
case 1:
if (!!ctl->source_muted == !*value)
goto finish;
ctl->source_muted = !*value;
break;
case 2:
vol = &ctl->sink_volume;
break;
case 3:
if (!!ctl->sink_muted == !*value)
goto finish;
ctl->sink_muted = !*value;
break;
default:
err = -EINVAL;
goto finish;
}
if (vol) {
for (i = 0; i < vol->channels; i++)
if (value[i] != vol->values[i])
break;
if (i == vol->channels)
goto finish;
for (i = 0; i < vol->channels; i++)
vol->values[i] = value[i];
if (key == 0)
o = pa_context_set_source_volume_by_name(ctl->p->
context,
ctl->
source,
vol,
pulse_context_success_cb,
ctl->p);
else
o = pa_context_set_sink_volume_by_name(ctl->p->
context,
ctl->sink,
vol,
pulse_context_success_cb,
ctl->p);
} else {
if (key == 1)
o = pa_context_set_source_mute_by_name(ctl->p->
context,
ctl->source,
ctl->
source_muted,
pulse_context_success_cb,
ctl->p);
else
o = pa_context_set_sink_mute_by_name(ctl->p->
context,
ctl->sink,
ctl->
sink_muted,
pulse_context_success_cb,
ctl->p);
}
if (!o) {
err = -EIO;
goto finish;
}
err = pulse_wait_operation(ctl->p, o);
pa_operation_unref(o);
if (err < 0)
goto finish;
err = 1;
finish:
pa_threaded_mainloop_unlock(ctl->p->mainloop);
return err;
}
static void pulse_subscribe_events(snd_ctl_ext_t * ext, int subscribe)
{
snd_ctl_pulse_t *ctl = ext->private_data;
assert(ctl);
if (!ctl->p || !ctl->p->mainloop)
return;
pa_threaded_mainloop_lock(ctl->p->mainloop);
ctl->subscribed = !!(subscribe & SND_CTL_EVENT_MASK_VALUE);
pa_threaded_mainloop_unlock(ctl->p->mainloop);
}
static int pulse_read_event(snd_ctl_ext_t * ext, snd_ctl_elem_id_t * id,
unsigned int *event_mask)
{
snd_ctl_pulse_t *ctl = ext->private_data;
int offset;
int err;
assert(ctl);
if (!ctl->p || !ctl->p->mainloop)
return -EBADFD;
pa_threaded_mainloop_lock(ctl->p->mainloop);
err = pulse_check_connection(ctl->p);
if (err < 0)
goto finish;
if (!ctl->updated || !ctl->subscribed) {
err = -EAGAIN;
goto finish;
}
if (ctl->source)
offset = 2;
else
offset = 0;
if (ctl->updated & UPDATE_SOURCE_VOL) {
pulse_elem_list(ext, 0, id);
ctl->updated &= ~UPDATE_SOURCE_VOL;
} else if (ctl->updated & UPDATE_SOURCE_MUTE) {
pulse_elem_list(ext, 1, id);
ctl->updated &= ~UPDATE_SOURCE_MUTE;
} else if (ctl->updated & UPDATE_SINK_VOL) {
pulse_elem_list(ext, offset + 0, id);
ctl->updated &= ~UPDATE_SINK_VOL;
} else if (ctl->updated & UPDATE_SINK_MUTE) {
pulse_elem_list(ext, offset + 1, id);
ctl->updated &= ~UPDATE_SINK_MUTE;
}
*event_mask = SND_CTL_EVENT_MASK_VALUE;
if (!ctl->updated)
pulse_poll_deactivate(ctl->p);
err = 1;
finish:
pa_threaded_mainloop_unlock(ctl->p->mainloop);
return err;
}
static int pulse_ctl_poll_revents(snd_ctl_ext_t * ext, struct pollfd *pfd,
unsigned int nfds,
unsigned short *revents)
{
snd_ctl_pulse_t *ctl = ext->private_data;
int err;
assert(ctl);
if (!ctl->p || !ctl->p->mainloop)
return -EBADFD;
pa_threaded_mainloop_lock(ctl->p->mainloop);
err = pulse_check_connection(ctl->p);
if (err < 0)
goto finish;
if (ctl->updated)
*revents = POLLIN;
else
*revents = 0;
err = 0;
finish:
pa_threaded_mainloop_unlock(ctl->p->mainloop);
return err;
}
static void pulse_close(snd_ctl_ext_t * ext)
{
snd_ctl_pulse_t *ctl = ext->private_data;
assert(ctl);
if (ctl->p)
pulse_free(ctl->p);
free(ctl->source);
free(ctl->sink);
free(ctl);
}
static const snd_ctl_ext_callback_t pulse_ext_callback = {
.elem_count = pulse_elem_count,
.elem_list = pulse_elem_list,
.find_elem = pulse_find_elem,
.get_attribute = pulse_get_attribute,
.get_integer_info = pulse_get_integer_info,
.read_integer = pulse_read_integer,
.write_integer = pulse_write_integer,
.subscribe_events = pulse_subscribe_events,
.read_event = pulse_read_event,
.poll_revents = pulse_ctl_poll_revents,
.close = pulse_close,
};
static void server_info_cb(pa_context * c, const pa_server_info * i,
void *userdata)
{
snd_ctl_pulse_t *ctl = (snd_ctl_pulse_t *) userdata;
assert(ctl && i);
if (i->default_source_name && !ctl->source)
ctl->source = strdup(i->default_source_name);
if (i->default_sink_name && !ctl->sink)
ctl->sink = strdup(i->default_sink_name);
pa_threaded_mainloop_signal(ctl->p->mainloop, 0);
}
SND_CTL_PLUGIN_DEFINE_FUNC(pulse)
{
snd_config_iterator_t i, next;
const char *server = NULL;
const char *device = NULL;
const char *source = NULL;
const char *sink = NULL;
const char *fallback_name = NULL;
int err;
snd_ctl_pulse_t *ctl;
pa_operation *o;
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 (strcmp(id, "comment") == 0 || strcmp(id, "type") == 0
|| strcmp(id, "hint") == 0)
continue;
if (strcmp(id, "server") == 0) {
if (snd_config_get_string(n, &server) < 0) {
SNDERR("Invalid type for %s", id);
return -EINVAL;
}
continue;
}
if (strcmp(id, "device") == 0) {
if (snd_config_get_string(n, &device) < 0) {
SNDERR("Invalid type for %s", id);
return -EINVAL;
}
continue;
}
if (strcmp(id, "source") == 0) {
if (snd_config_get_string(n, &source) < 0) {
SNDERR("Invalid type for %s", id);
return -EINVAL;
}
continue;
}
if (strcmp(id, "sink") == 0) {
if (snd_config_get_string(n, &sink) < 0) {
SNDERR("Invalid type for %s", id);
return -EINVAL;
}
continue;
}
if (strcmp(id, "fallback") == 0) {
if (snd_config_get_string(n, &fallback_name) < 0) {
SNDERR("Invalid value for %s", id);
return -EINVAL;
}
continue;
}
SNDERR("Unknown field %s", id);
return -EINVAL;
}
if (fallback_name && name && !strcmp(name, fallback_name))
fallback_name = NULL; /* no fallback for the same name */
ctl = calloc(1, sizeof(*ctl));
if (!ctl)
return -ENOMEM;
ctl->p = pulse_new();
if (!ctl->p) {
err = -EIO;
goto error;
}
err = pulse_connect(ctl->p, server, fallback_name != NULL);
if (err < 0)
goto error;
if (source)
ctl->source = strdup(source);
else if (device)
ctl->source = strdup(device);
if ((source || device) && !ctl->source) {
err = -ENOMEM;
goto error;
}
if (sink)
ctl->sink = strdup(sink);
else if (device)
ctl->sink = strdup(device);
if ((sink || device) && !ctl->sink) {
err = -ENOMEM;
goto error;
}
if (!ctl->source || !ctl->sink) {
pa_threaded_mainloop_lock(ctl->p->mainloop);
o = pa_context_get_server_info(ctl->p->context,
server_info_cb, ctl);
if (o) {
err = pulse_wait_operation(ctl->p, o);
pa_operation_unref(o);
} else
err = -EIO;
pa_threaded_mainloop_unlock(ctl->p->mainloop);
if (err < 0)
goto error;
}
pa_threaded_mainloop_lock(ctl->p->mainloop);
pa_context_set_subscribe_callback(ctl->p->context, event_cb, ctl);
o = pa_context_subscribe(ctl->p->context,
PA_SUBSCRIPTION_MASK_SINK |
PA_SUBSCRIPTION_MASK_SOURCE,
pulse_context_success_cb, ctl->p);
if (o) {
err = pulse_wait_operation(ctl->p, o);
pa_operation_unref(o);
} else
err = -EIO;
pa_threaded_mainloop_unlock(ctl->p->mainloop);
if (err < 0)
goto error;
ctl->ext.version = SND_CTL_EXT_VERSION;
ctl->ext.card_idx = 0;
strncpy(ctl->ext.id, "pulse", sizeof(ctl->ext.id) - 1);
strncpy(ctl->ext.driver, "PulseAudio plugin",
sizeof(ctl->ext.driver) - 1);
strncpy(ctl->ext.name, "PulseAudio", sizeof(ctl->ext.name) - 1);
strncpy(ctl->ext.longname, "PulseAudio",
sizeof(ctl->ext.longname) - 1);
strncpy(ctl->ext.mixername, "PulseAudio",
sizeof(ctl->ext.mixername) - 1);
ctl->ext.poll_fd = ctl->p->main_fd;
ctl->ext.callback = &pulse_ext_callback;
ctl->ext.private_data = ctl;
err = snd_ctl_ext_create(&ctl->ext, name, mode);
if (err < 0)
goto error;
*handlep = ctl->ext.handle;
return 0;
error:
if (ctl->p)
pulse_free(ctl->p);
free(ctl->source);
free(ctl->sink);
free(ctl);
if (fallback_name)
return snd_ctl_open_fallback(handlep, root,
fallback_name, name, mode);
return err;
}
SND_CTL_PLUGIN_SYMBOL(pulse);