yambar/modules/alsa.c
Daniel Eklöf d1c7647b03
module/alsa: add support for capture devices
This mostly comes down to tracking whether each channel is a playback,
or capture channel, and using the appropriate APIs when dealing with
it.

Some cleanup related to this:

* Add a channel struct, for per-channel data. Previously, our channel
  list was just a list of ALSA channel IDs.
* We now store current volume per-channel (but volume min/max is
  per-device)
* Muted state is stored per-channel
* Track both the device’s playback and capture volume ranges, as well
  as whether the device *has* playback or capture volume.
* Get the playback/capture volume ranges once, during init, instead of
  at each update.
* Use struct pointers for the volume/muted channels. This way we don’t
  have to iterate all channels and to string comparisons on the name
  each time we update our state.
2021-08-26 11:03:12 +02:00

612 lines
17 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <sys/time.h>
#include <sys/inotify.h>
#include <alsa/asoundlib.h>
#include <tllist.h>
#define LOG_MODULE "alsa"
#define LOG_ENABLE_DBG 0
#include "../log.h"
#include "../bar/bar.h"
#include "../config-verify.h"
#include "../config.h"
#include "../plugin.h"
enum channel_type { CHANNEL_PLAYBACK, CHANNEL_CAPTURE };
struct channel {
snd_mixer_selem_channel_id_t id;
enum channel_type type;
char *name;
long vol_cur;
bool muted;
};
struct private {
char *card;
char *mixer;
char *volume_name;
char *muted_name;
struct particle *label;
tll(struct channel) channels;
bool online;
bool has_playback_volume;
long playback_vol_min;
long playback_vol_max;
bool has_capture_volume;
long capture_vol_min;
long capture_vol_max;
const struct channel *volume_chan;
const struct channel *muted_chan;
};
static void
channel_free(struct channel *chan)
{
free(chan->name);
}
static void
destroy(struct module *mod)
{
struct private *m = mod->private;
tll_foreach(m->channels, it) {
channel_free(&it->item);
tll_remove(m->channels, it);
}
m->label->destroy(m->label);
free(m->card);
free(m->mixer);
free(m->volume_name);
free(m->muted_name);
free(m);
module_default_destroy(mod);
}
static const char *
description(struct module *mod)
{
static char desc[32];
struct private *m = mod->private;
snprintf(desc, sizeof(desc), "alsa(%s)", m->card);
return desc;
}
static struct exposable *
content(struct module *mod)
{
struct private *m = mod->private;
mtx_lock(&mod->lock);
const struct channel *volume_chan = m->volume_chan;
const struct channel *muted_chan = m->muted_chan;
bool muted = muted_chan != NULL ? muted_chan->muted : false;
long vol_min = 0, vol_max = 0, vol_cur = 0;
if (volume_chan != NULL) {
if (volume_chan->type == CHANNEL_PLAYBACK) {
vol_min = m->playback_vol_min;
vol_max = m->playback_vol_max;
} else {
vol_min = m->capture_vol_min;
vol_max = m->capture_vol_max;
}
vol_cur = volume_chan->vol_cur;
}
int percent = vol_max - vol_min > 0
? round(100. * vol_cur / (vol_max - vol_min))
: 0;
struct tag_set tags = {
.tags = (struct tag *[]){
tag_new_bool(mod, "online", m->online),
tag_new_int_range(mod, "volume", vol_cur, vol_min, vol_max),
tag_new_int_range(mod, "percent", percent, 0, 100),
tag_new_bool(mod, "muted", muted),
},
.count = 4,
};
mtx_unlock(&mod->lock);
struct exposable *exposable = m->label->instantiate(m->label, &tags);
tag_set_destroy(&tags);
return exposable;
}
static void
update_state(struct module *mod, snd_mixer_elem_t *elem)
{
struct private *m = mod->private;
/* If volume level can be changed (i.e. this isn't just a switch;
* e.g. a digital channel), get current channel levels */
tll_foreach(m->channels, it) {
struct channel *chan = &it->item;
const bool has_volume = chan->type == CHANNEL_PLAYBACK
? m->has_playback_volume : m->has_capture_volume;
const long min = chan->type == CHANNEL_PLAYBACK
? m->playback_vol_min : m->capture_vol_min;
const long max = chan->type == CHANNEL_PLAYBACK
? m->playback_vol_max : m->capture_vol_max;
if (!has_volume)
continue;
assert(min <= max);
int r = chan->type == CHANNEL_PLAYBACK
? snd_mixer_selem_get_playback_volume(elem, chan->id, &chan->vol_cur)
: snd_mixer_selem_get_capture_volume(elem, chan->id, &chan->vol_cur);
if (r < 0) {
LOG_ERR("%s,%s: %s: failed to get current volume",
m->card, m->mixer, chan->name);
}
if (chan->vol_cur < min) {
LOG_WARN(
"%s,%s: %s: current volume is less than the indicated minimum: "
"%ld < %ld", m->card, m->mixer, chan->name, chan->vol_cur, min);
chan->vol_cur = min;
}
if (chan->vol_cur > max) {
LOG_WARN(
"%s,%s: %s: current volume is greater than the indicated maximum: "
"%ld > %ld", m->card, m->mixer, chan->name, chan->vol_cur, max);
chan->vol_cur = max;
}
assert(chan->vol_cur >= min);
assert(chan->vol_cur <= max );
LOG_DBG("%s,%s: %s: volume: %ld",
m->card, m->mixer, chan->name, chan->vol_cur);
}
/* Get channels muted state */
tll_foreach(m->channels, it) {
struct channel *chan = &it->item;
int unmuted;
int r = chan->type == CHANNEL_PLAYBACK
? snd_mixer_selem_get_playback_switch(elem, chan->id, &unmuted)
: snd_mixer_selem_get_capture_switch(elem, chan->id, &unmuted);
if (r < 0) {
LOG_WARN("%s,%s: %s: failed to get muted state",
m->card, m->mixer, chan->name);
unmuted = 1;
}
chan->muted = !unmuted;
LOG_DBG("%s,%s: %s: muted: %d", m->card, m->mixer, chan->name, !unmuted);
}
mtx_lock(&mod->lock);
m->online = true;
mtx_unlock(&mod->lock);
mod->bar->refresh(mod->bar);
}
enum run_state {
RUN_ERROR,
RUN_FAILED_CONNECT,
RUN_DISCONNECTED,
RUN_DONE,
};
static enum run_state
run_while_online(struct module *mod)
{
struct private *m = mod->private;
enum run_state ret = RUN_ERROR;
/* Make sure we arent still tracking channels from previous connects */
tll_free(m->channels);
snd_mixer_t *handle;
if (snd_mixer_open(&handle, 0) != 0) {
LOG_ERR("failed to open handle");
return ret;
}
if (snd_mixer_attach(handle, m->card) != 0 ||
snd_mixer_selem_register(handle, NULL, NULL) != 0 ||
snd_mixer_load(handle) != 0)
{
LOG_ERR("failed to attach to card");
ret = RUN_FAILED_CONNECT;
goto err;
}
snd_mixer_selem_id_t *sid;
snd_mixer_selem_id_alloca(&sid);
snd_mixer_selem_id_set_index(sid, 0);
snd_mixer_selem_id_set_name(sid, m->mixer);
snd_mixer_elem_t* elem = snd_mixer_find_selem(handle, sid);
if (elem == NULL) {
LOG_ERR("failed to find mixer");
goto err;
}
/* Get playback volume range */
m->has_playback_volume = snd_mixer_selem_has_playback_volume(elem) > 0;
if (m->has_playback_volume) {
if (snd_mixer_selem_get_playback_volume_range(
elem, &m->playback_vol_min, &m->playback_vol_max) < 0)
{
LOG_ERR("%s,%s: failed to get playback volume range",
m->card, m->mixer);
assert(m->playback_vol_min == 0);
assert(m->playback_vol_max == 0);
}
if (m->playback_vol_min > m->playback_vol_max) {
LOG_WARN(
"%s,%s: indicated minimum playback volume is greater than the "
"maximum: %ld > %ld",
m->card, m->mixer, m->playback_vol_min, m->playback_vol_max);
m->playback_vol_min = m->playback_vol_max;
}
}
/* Get capture volume range */
m->has_capture_volume = snd_mixer_selem_has_capture_volume(elem) > 0;
if (m->has_capture_volume) {
if (snd_mixer_selem_get_capture_volume_range(
elem, &m->capture_vol_min, &m->capture_vol_max) < 0)
{
LOG_ERR("%s,%s: failed to get capture volume range",
m->card, m->mixer);
assert(m->capture_vol_min == 0);
assert(m->capture_vol_max == 0);
}
if (m->capture_vol_min > m->capture_vol_max) {
LOG_WARN(
"%s,%s: indicated minimum capture volume is greater than the "
"maximum: %ld > %ld",
m->card, m->mixer, m->capture_vol_min, m->capture_vol_max);
m->capture_vol_min = m->capture_vol_max;
}
}
/* Get available channels */
for (size_t i = 0; i < SND_MIXER_SCHN_LAST; i++) {
bool is_playback = snd_mixer_selem_has_playback_channel(elem, i) == 1;
bool is_capture = snd_mixer_selem_has_capture_channel(elem, i) == 1;
if (is_playback || is_capture) {
struct channel chan = {
.id = i,
.type = is_playback ? CHANNEL_PLAYBACK : CHANNEL_CAPTURE,
.name = strdup(snd_mixer_selem_channel_name( i)),
};
tll_push_back(m->channels, chan);
}
}
if (tll_length(m->channels) == 0) {
LOG_ERR("%s,%s: no channels", m->card, m->mixer);
goto err;
}
char channels_str[1024];
int channels_idx = 0;
tll_foreach(m->channels, it) {
const struct channel *chan = &it->item;
channels_idx += snprintf(
&channels_str[channels_idx], sizeof(channels_str) - channels_idx,
channels_idx == 0 ? "%s (%s)" : ", %s (%s)",
chan->name, chan->type == CHANNEL_PLAYBACK ? "🔊" : "🎤");
assert(channels_idx <= sizeof(channels_str));
}
LOG_INFO("%s,%s: channels: %s", m->card, m->mixer, channels_str);
/* Verify volume/muted channel names are valid and exists */
bool volume_channel_is_valid = m->volume_name == NULL;
bool muted_channel_is_valid = m->muted_name == NULL;
tll_foreach(m->channels, it) {
const struct channel *chan = &it->item;
if (m->volume_name != NULL && strcmp(chan->name, m->volume_name) == 0) {
m->volume_chan = chan;
volume_channel_is_valid = true;
}
if (m->muted_name != NULL && strcmp(chan->name, m->muted_name) == 0) {
m->muted_chan = chan;
muted_channel_is_valid = true;
}
}
if (m->volume_name == NULL)
m->volume_chan = &tll_front(m->channels);
if (m->muted_name == NULL)
m->muted_chan = &tll_front(m->channels);
if (!volume_channel_is_valid) {
assert(m->volume_name != NULL);
LOG_ERR("volume: invalid channel name: %s", m->volume_name);
goto err;
}
if (!muted_channel_is_valid) {
assert(m->muted_name != NULL);
LOG_ERR("muted: invalid channel name: %s", m->muted_name);
goto err;
}
/* Initial state */
update_state(mod, elem);
LOG_INFO(
"%s,%s: volume range=%ld-%ld, current=%ld%s (sources: volume=%s, muted=%s)",
m->card, m->mixer,
m->volume_chan->type == CHANNEL_PLAYBACK
? m->playback_vol_min : m->capture_vol_min,
m->volume_chan->type == CHANNEL_PLAYBACK
? m->playback_vol_max : m->capture_vol_max,
m->volume_chan->vol_cur,
m->muted_chan->muted ? " (muted)" : "",
m->volume_chan->name, m->muted_chan->name);
mod->bar->refresh(mod->bar);
while (true) {
int fd_count = snd_mixer_poll_descriptors_count(handle);
assert(fd_count >= 1);
struct pollfd fds[1 + fd_count];
fds[0] = (struct pollfd){.fd = mod->abort_fd, .events = POLLIN};
snd_mixer_poll_descriptors(handle, &fds[1], fd_count);
int r = poll(fds, fd_count + 1, -1);
if (r < 0) {
if (errno == EINTR)
continue;
LOG_ERRNO("failed to poll");
break;
}
if (fds[0].revents & POLLIN) {
ret = RUN_DONE;
break;
}
for (size_t i = 0; i < fd_count; i++) {
if (fds[1 + i].revents & (POLLHUP | POLLERR | POLLNVAL)) {
LOG_ERR("disconnected from alsa");
mtx_lock(&mod->lock);
m->online = false;
mtx_unlock(&mod->lock);
mod->bar->refresh(mod->bar);
ret = RUN_DISCONNECTED;
goto err;
}
}
snd_mixer_handle_events(handle);
update_state(mod, elem);
}
err:
snd_mixer_close(handle);
snd_config_update_free_global();
return ret;
}
static int
run(struct module *mod)
{
int ret = 1;
int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
if (ifd < 0) {
LOG_ERRNO("failed to inotify");
return 1;
}
int wd = inotify_add_watch(ifd, "/dev/snd", IN_CREATE);
if (wd < 0) {
LOG_ERRNO("failed to create inotify watcher for /dev/snd");
close(ifd);
return 1;
}
while (true) {
enum run_state state = run_while_online(mod);
switch (state) {
case RUN_DONE:
ret = 0;
goto out;
case RUN_ERROR:
ret = 1;
goto out;
case RUN_FAILED_CONNECT:
break;
case RUN_DISCONNECTED:
/*
* Weve been connected - drain the watcher
*
* We dont want old, un-releated events (for other
* soundcards, for example) to trigger a storm of
* re-connect attempts.
*/
while (true) {
uint8_t buf[1024];
ssize_t amount = read(ifd, buf, sizeof(buf));
if (amount < 0) {
if (errno == EAGAIN)
break;
LOG_ERRNO("failed to drain inotify watcher");
ret = 1;
goto out;
}
if (amount == 0)
break;
}
break;
}
bool have_create_event = false;
while (!have_create_event) {
struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN},
{.fd = ifd, .events = POLLIN}};
int r = poll(fds, sizeof(fds) / sizeof(fds[0]), -1);
if (r < 0) {
if (errno == EINTR)
continue;
LOG_ERRNO("failed to poll");
ret = 1;
goto out;
}
if (fds[0].revents & (POLLIN | POLLHUP)) {
ret = 0;
goto out;
}
if (fds[1].revents & POLLHUP) {
LOG_ERR("inotify socket closed");
ret = 1;
goto out;
}
assert(fds[1].revents & POLLIN);
while (true) {
char buf[1024];
ssize_t len = read(ifd, buf, sizeof(buf));
if (len < 0) {
if (errno == EAGAIN)
break;
LOG_ERRNO("failed to read inotify events");
ret = 1;
goto out;
}
if (len == 0)
break;
/* Consume inotify data */
for (const char *ptr = buf; ptr < buf + len; ) {
const struct inotify_event *e = (const struct inotify_event *)ptr;
if (e->mask & IN_CREATE) {
LOG_DBG("inotify: CREATED: /dev/snd/%.*s", e->len, e->name);
have_create_event = true;
}
ptr += sizeof(*e) + e->len;
}
}
}
}
out:
if (wd >= 0)
inotify_rm_watch(ifd, wd);
if (ifd >= 0)
close (ifd);
return ret;
}
static struct module *
alsa_new(const char *card, const char *mixer,
const char *volume_channel_name, const char *muted_channel_name,
struct particle *label)
{
struct private *priv = calloc(1, sizeof(*priv));
priv->label = label;
priv->card = strdup(card);
priv->mixer = strdup(mixer);
priv->volume_name =
volume_channel_name != NULL ? strdup(volume_channel_name) : NULL;
priv->muted_name =
muted_channel_name != NULL ? strdup(muted_channel_name) : NULL;
struct module *mod = module_common_new();
mod->private = priv;
mod->run = &run;
mod->destroy = &destroy;
mod->content = &content;
mod->description = &description;
return mod;
}
static struct module *
from_conf(const struct yml_node *node, struct conf_inherit inherited)
{
const struct yml_node *card = yml_get_value(node, "card");
const struct yml_node *mixer = yml_get_value(node, "mixer");
const struct yml_node *volume = yml_get_value(node, "volume");
const struct yml_node *muted = yml_get_value(node, "muted");
const struct yml_node *content = yml_get_value(node, "content");
return alsa_new(
yml_value_as_string(card),
yml_value_as_string(mixer),
volume != NULL ? yml_value_as_string(volume) : NULL,
muted != NULL ? yml_value_as_string(muted) : NULL,
conf_to_particle(content, inherited));
}
static bool
verify_conf(keychain_t *chain, const struct yml_node *node)
{
static const struct attr_info attrs[] = {
{"card", true, &conf_verify_string},
{"mixer", true, &conf_verify_string},
{"volume", false, &conf_verify_string},
{"muted", false, &conf_verify_string},
MODULE_COMMON_ATTRS,
};
return conf_verify_dict(chain, node, attrs);
}
const struct module_iface module_alsa_iface = {
.verify_conf = &verify_conf,
.from_conf = &from_conf,
};
#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
extern const struct module_iface iface __attribute__((weak, alias("module_alsa_iface")));
#endif