/* SPDX-License-Identifier: GPL-2.0 */ /** \file alsa_mix.c The ALSA mixer plugin. */ #include #include "para.h" #include "error.h" #include "mix.h" #include "string.h" struct mixer_handle { /* The ALSA mixer handle */ snd_mixer_t *mixer; /* Copy of the alsa device name, e.g. "hw:0". */ char *card; /* ALSA's representation of the given mixer channel. */ snd_mixer_elem_t *elem; /* Set in ->set_channel(), used for ->get() and ->set(). */ long pmin, pmax; }; static void alsa_mix_close(struct mixer_handle *h) { if (!h) return; PARA_INFO_LOG("closing mixer handle\n"); if (h->mixer) snd_mixer_close(h->mixer); free(h->card); free(h); /* * The global ALSA configuration is cached for next usage, * which causes valgrind to report many memory leaks. Calling * snd_config_update_free_global() frees the cache. */ snd_config_update_free_global(); } static int alsa_mix_open(const char *dev, struct mixer_handle **handle) { int ret; char *msg; struct mixer_handle *h; PARA_INFO_LOG("snd_mixer_{open,attach,register,load}\n"); *handle = NULL; h = zalloc(sizeof(*h)); h->card = para_strdup(dev? dev : "hw:0"); ret = snd_mixer_open(&h->mixer, 0); if (ret < 0) { msg = make_message("snd_mixer_open() failed: %s", snd_strerror(ret)); goto fail; } ret = snd_mixer_attach(h->mixer, h->card); if (ret < 0) { msg = make_message("mixer attach error (%s): %s", h->card, snd_strerror(ret)); goto fail; } ret = snd_mixer_selem_register(h->mixer, NULL, NULL); if (ret < 0) { msg = make_message("mixer register error (%s): %s", h->card, snd_strerror(ret)); goto fail; } ret = snd_mixer_load(h->mixer); if (ret < 0) { msg = make_message("mixer load error (%s): %s", h->card, snd_strerror(ret)); goto fail; } /* success */ *handle = h; return 1; fail: PARA_NOTICE_LOG("%s\n", msg); free(msg); alsa_mix_close(h); return -E_ALSA_MIX_OPEN; } static bool channel_has_playback(snd_mixer_selem_channel_id_t chn, snd_mixer_elem_t *elem) { return snd_mixer_selem_has_playback_channel(elem, chn); } static char *alsa_mix_get_channels(struct mixer_handle *handle) { int ret; char *list = NULL; snd_mixer_selem_id_t *sid; snd_mixer_elem_t *elem; ret = snd_mixer_selem_id_malloc(&sid); assert(ret >= 0); elem = snd_mixer_first_elem(handle->mixer); for (; elem; elem = snd_mixer_elem_next(elem)) { char *tmp = list; const char *name; if (!channel_has_playback(0, elem)) continue; if (!snd_mixer_selem_has_playback_volume(elem)) continue; snd_mixer_selem_get_id(elem, sid); name = snd_mixer_selem_id_get_name(sid); list = make_message("%s%s%s", list? list : "", list? ", " : "", name); free(tmp); } snd_mixer_selem_id_free(sid); return list; } static int alsa_mix_set_channel(struct mixer_handle *h, const char *mixer_channel) { int ret, selem_id = 0; snd_mixer_selem_id_t *sid; if (!mixer_channel) mixer_channel = "Master"; ret = snd_mixer_selem_id_malloc(&sid); assert(ret >= 0); snd_mixer_selem_id_set_index(sid, selem_id); snd_mixer_selem_id_set_name(sid, mixer_channel); h->elem = snd_mixer_find_selem(h->mixer, sid); if (!h->elem) { PARA_NOTICE_LOG("unable to find simple control '%s',%u\n", snd_mixer_selem_id_get_name(sid), snd_mixer_selem_id_get_index(sid)); ret = -E_BAD_CHANNEL; goto out; } ret = snd_mixer_selem_get_playback_volume_range(h->elem, &h->pmin, &h->pmax); if (ret < 0) { PARA_NOTICE_LOG("unable to get %s range (%s): %s\n", mixer_channel, h->card, snd_strerror(ret)); ret = -E_ALSA_MIX_RANGE; goto out; } PARA_INFO_LOG("%s: %s (%ld-%ld)\n", h->card, mixer_channel, h->pmin, h->pmax); if (h->pmin >= h->pmax) { ret = -E_ALSA_MIX_RANGE; goto out; } ret = 1; out: snd_mixer_selem_id_free(sid); return ret; } static int alsa_mix_get(const struct mixer_handle *h) { snd_mixer_selem_channel_id_t chn; int n = 0; float sum = 0; for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++) { long val; int ret; if (!channel_has_playback(chn, h->elem)) continue; ret = snd_mixer_selem_get_playback_volume(h->elem, chn, &val); if (ret < 0) { PARA_NOTICE_LOG("unable to get value for channel %d (%s): %s\n", (int)chn, h->card, snd_strerror(ret)); return -E_ALSA_MIX_GET_VAL; } sum += val - h->pmin; n++; } if (n == 0 || h->pmax == h->pmin) return 0; /* compute average over all channels and scale to 0..100 */ return 100.0 * sum / ((h->pmax - h->pmin) * n) + 0.5; } static int alsa_mix_set(struct mixer_handle *h, int val) { int ret; snd_mixer_selem_channel_id_t chn; long scaled = val / 100.0 * (h->pmax - h->pmin) + h->pmin + 0.5; PARA_DEBUG_LOG("new value: %d\n", val); for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++) { if (!channel_has_playback(chn, h->elem)) continue; ret = snd_mixer_selem_set_playback_volume(h->elem, chn, scaled); if (ret < 0) { PARA_NOTICE_LOG("unable to set value for channel %d (%s): %s\n", (int)chn, h->card, snd_strerror(ret)); return -E_ALSA_MIX_SET_VAL; } } return 1; } /** The mixer operations for the ALSA mixer. */ const struct mixer alsa_mixer = { .name = "alsa", .open = alsa_mix_open, .get_channels = alsa_mix_get_channels, .set_channel = alsa_mix_set_channel, .close = alsa_mix_close, .get = alsa_mix_get, .set = alsa_mix_set };