/*-*- linux-c -*-*/ /* * ALSA <-> PulseAudio mixer control plugin * * Copyright (c) 2006 by Pierre Ossman * * 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 #include #include #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);