aboutsummaryrefslogtreecommitdiff
path: root/lib/libmixer/mixer.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/libmixer/mixer.c')
-rw-r--r--lib/libmixer/mixer.c520
1 files changed, 520 insertions, 0 deletions
diff --git a/lib/libmixer/mixer.c b/lib/libmixer/mixer.c
new file mode 100644
index 000000000000..2be3a48eb755
--- /dev/null
+++ b/lib/libmixer/mixer.c
@@ -0,0 +1,520 @@
+/*-
+ * Copyright (c) 2021 Christos Margiolis <christos@FreeBSD.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/ioctl.h>
+#include <sys/sysctl.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "mixer.h"
+
+#define BASEPATH "/dev/mixer"
+
+static int _mixer_readvol(struct mix_dev *);
+
+/*
+ * Fetch volume from the device.
+ */
+static int
+_mixer_readvol(struct mix_dev *dev)
+{
+ int v;
+
+ if (ioctl(dev->parent_mixer->fd, MIXER_READ(dev->devno), &v) < 0)
+ return (-1);
+ dev->vol.left = MIX_VOLNORM(v & 0x00ff);
+ dev->vol.right = MIX_VOLNORM((v >> 8) & 0x00ff);
+
+ return (0);
+}
+
+/*
+ * Open a mixer device in `/dev/mixerN`, where N is the number of the mixer.
+ * Each device maps to an actual pcm audio card, so `/dev/mixer0` is the
+ * mixer for pcm0, and so on.
+ *
+ * @param name path to mixer device. NULL or "/dev/mixer" for the
+ * the default mixer (i.e `hw.snd.default_unit`).
+ */
+struct mixer *
+mixer_open(const char *name)
+{
+ struct mixer *m = NULL;
+ struct mix_dev *dp;
+ const char *names[SOUND_MIXER_NRDEVICES] = SOUND_DEVICE_NAMES;
+ int i;
+
+ if ((m = calloc(1, sizeof(struct mixer))) == NULL)
+ goto fail;
+
+ if (name != NULL) {
+ /* `name` does not start with "/dev/mixer". */
+ if (strncmp(name, BASEPATH, strlen(BASEPATH)) != 0) {
+ m->unit = -1;
+ } else {
+ /* `name` is "/dev/mixer" so, we'll use the default unit. */
+ if (strncmp(name, BASEPATH, strlen(name)) == 0)
+ goto dunit;
+ m->unit = strtol(name + strlen(BASEPATH), NULL, 10);
+ }
+ (void)strlcpy(m->name, name, sizeof(m->name));
+ } else {
+dunit:
+ if ((m->unit = mixer_get_dunit()) < 0)
+ goto fail;
+ (void)snprintf(m->name, sizeof(m->name), BASEPATH "%d", m->unit);
+ }
+
+ if ((m->fd = open(m->name, O_RDWR)) < 0)
+ goto fail;
+
+ m->devmask = m->recmask = m->recsrc = 0;
+ m->f_default = m->unit == mixer_get_dunit();
+ m->mode = mixer_get_mode(m->unit);
+ /* The unit number _must_ be set before the ioctl. */
+ m->mi.dev = m->unit;
+ m->ci.card = m->unit;
+ if (ioctl(m->fd, SNDCTL_MIXERINFO, &m->mi) < 0) {
+ memset(&m->mi, 0, sizeof(m->mi));
+ strlcpy(m->mi.name, m->name, sizeof(m->mi.name));
+ }
+ if (ioctl(m->fd, SNDCTL_CARDINFO, &m->ci) < 0)
+ memset(&m->ci, 0, sizeof(m->ci));
+ if (ioctl(m->fd, SOUND_MIXER_READ_DEVMASK, &m->devmask) < 0 ||
+ ioctl(m->fd, SOUND_MIXER_READ_MUTE, &m->mutemask) < 0 ||
+ ioctl(m->fd, SOUND_MIXER_READ_RECMASK, &m->recmask) < 0 ||
+ ioctl(m->fd, SOUND_MIXER_READ_RECSRC, &m->recsrc) < 0)
+ goto fail;
+
+ TAILQ_INIT(&m->devs);
+ for (i = 0; i < SOUND_MIXER_NRDEVICES; i++) {
+ if (!MIX_ISDEV(m, i) && !MIX_ISREC(m, i))
+ continue;
+ if ((dp = calloc(1, sizeof(struct mix_dev))) == NULL)
+ goto fail;
+ dp->parent_mixer = m;
+ dp->devno = i;
+ dp->nctl = 0;
+ if (MIX_ISDEV(m, i) && _mixer_readvol(dp) < 0)
+ goto fail;
+ (void)strlcpy(dp->name, names[i], sizeof(dp->name));
+ TAILQ_INIT(&dp->ctls);
+ TAILQ_INSERT_TAIL(&m->devs, dp, devs);
+ m->ndev++;
+ }
+
+ /* The default device is always "vol". */
+ m->dev = TAILQ_FIRST(&m->devs);
+
+ return (m);
+fail:
+ if (m != NULL)
+ (void)mixer_close(m);
+
+ return (NULL);
+}
+
+/*
+ * Free resources and close the mixer.
+ */
+int
+mixer_close(struct mixer *m)
+{
+ struct mix_dev *dp;
+ int r;
+
+ r = close(m->fd);
+ while (!TAILQ_EMPTY(&m->devs)) {
+ dp = TAILQ_FIRST(&m->devs);
+ TAILQ_REMOVE(&m->devs, dp, devs);
+ while (!TAILQ_EMPTY(&dp->ctls))
+ (void)mixer_remove_ctl(TAILQ_FIRST(&dp->ctls));
+ free(dp);
+ }
+ free(m);
+
+ return (r);
+}
+
+/*
+ * Select a mixer device. The mixer structure keeps a list of all the devices
+ * the mixer has, but only one can be manipulated at a time -- this is what
+ * the `dev` in the mixer structure field is for. Each time a device is to be
+ * manipulated, `dev` has to point to it first.
+ *
+ * The caller must manually assign the return value to `m->dev`.
+ */
+struct mix_dev *
+mixer_get_dev(struct mixer *m, int dev)
+{
+ struct mix_dev *dp;
+
+ if (dev < 0 || dev >= m->ndev) {
+ errno = ERANGE;
+ return (NULL);
+ }
+ TAILQ_FOREACH(dp, &m->devs, devs) {
+ if (dp->devno == dev)
+ return (dp);
+ }
+ errno = EINVAL;
+
+ return (NULL);
+}
+
+/*
+ * Select a device by name.
+ *
+ * @param name device name (e.g vol, pcm, ...)
+ */
+struct mix_dev *
+mixer_get_dev_byname(struct mixer *m, const char *name)
+{
+ struct mix_dev *dp;
+
+ TAILQ_FOREACH(dp, &m->devs, devs) {
+ if (!strncmp(dp->name, name, sizeof(dp->name)))
+ return (dp);
+ }
+ errno = EINVAL;
+
+ return (NULL);
+}
+
+/*
+ * Add a mixer control to a device.
+ */
+int
+mixer_add_ctl(struct mix_dev *parent_dev, int id, const char *name,
+ int (*mod)(struct mix_dev *, void *),
+ int (*print)(struct mix_dev *, void *))
+{
+ struct mix_dev *dp;
+ mix_ctl_t *ctl, *cp;
+
+ /* XXX: should we accept NULL name? */
+ if (parent_dev == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+ if ((ctl = calloc(1, sizeof(mix_ctl_t))) == NULL)
+ return (-1);
+ ctl->parent_dev = parent_dev;
+ ctl->id = id;
+ if (name != NULL)
+ (void)strlcpy(ctl->name, name, sizeof(ctl->name));
+ ctl->mod = mod;
+ ctl->print = print;
+ dp = ctl->parent_dev;
+ /* Make sure the same ID or name doesn't exist already. */
+ TAILQ_FOREACH(cp, &dp->ctls, ctls) {
+ if (!strncmp(cp->name, name, sizeof(cp->name)) || cp->id == id) {
+ errno = EINVAL;
+ return (-1);
+ }
+ }
+ TAILQ_INSERT_TAIL(&dp->ctls, ctl, ctls);
+ dp->nctl++;
+
+ return (0);
+}
+
+/*
+ * Same as `mixer_add_ctl`.
+ */
+int
+mixer_add_ctl_s(mix_ctl_t *ctl)
+{
+ if (ctl == NULL)
+ return (-1);
+
+ return (mixer_add_ctl(ctl->parent_dev, ctl->id, ctl->name,
+ ctl->mod, ctl->print));
+}
+
+/*
+ * Remove a mixer control from a device.
+ */
+int
+mixer_remove_ctl(mix_ctl_t *ctl)
+{
+ struct mix_dev *p;
+
+ if (ctl == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+ p = ctl->parent_dev;
+ if (!TAILQ_EMPTY(&p->ctls)) {
+ TAILQ_REMOVE(&p->ctls, ctl, ctls);
+ free(ctl);
+ }
+
+ return (0);
+}
+
+/*
+ * Get a mixer control by id.
+ */
+mix_ctl_t *
+mixer_get_ctl(struct mix_dev *d, int id)
+{
+ mix_ctl_t *cp;
+
+ TAILQ_FOREACH(cp, &d->ctls, ctls) {
+ if (cp->id == id)
+ return (cp);
+ }
+ errno = EINVAL;
+
+ return (NULL);
+}
+
+/*
+ * Get a mixer control by name.
+ */
+mix_ctl_t *
+mixer_get_ctl_byname(struct mix_dev *d, const char *name)
+{
+ mix_ctl_t *cp;
+
+ TAILQ_FOREACH(cp, &d->ctls, ctls) {
+ if (!strncmp(cp->name, name, sizeof(cp->name)))
+ return (cp);
+ }
+ errno = EINVAL;
+
+ return (NULL);
+}
+
+/*
+ * Change the mixer's left and right volume. The allowed volume values are
+ * between MIX_VOLMIN and MIX_VOLMAX. The `ioctl` for volume change requires
+ * an integer value between 0 and 100 stored as `lvol | rvol << 8` -- for
+ * that reason, we de-normalize the 32-bit float volume value, before
+ * we pass it to the `ioctl`.
+ *
+ * Volume clumping should be done by the caller.
+ */
+int
+mixer_set_vol(struct mixer *m, mix_volume_t vol)
+{
+ int v;
+
+ if (vol.left < MIX_VOLMIN || vol.left > MIX_VOLMAX ||
+ vol.right < MIX_VOLMIN || vol.right > MIX_VOLMAX) {
+ errno = ERANGE;
+ return (-1);
+ }
+ v = MIX_VOLDENORM(vol.left) | MIX_VOLDENORM(vol.right) << 8;
+ if (ioctl(m->fd, MIXER_WRITE(m->dev->devno), &v) < 0)
+ return (-1);
+ if (_mixer_readvol(m->dev) < 0)
+ return (-1);
+
+ return (0);
+}
+
+/*
+ * Manipulate a device's mute.
+ *
+ * @param opt MIX_MUTE mute device
+ * MIX_UNMUTE unmute device
+ * MIX_TOGGLEMUTE toggle device's mute
+ */
+int
+mixer_set_mute(struct mixer *m, int opt)
+{
+ switch (opt) {
+ case MIX_MUTE:
+ m->mutemask |= (1 << m->dev->devno);
+ break;
+ case MIX_UNMUTE:
+ m->mutemask &= ~(1 << m->dev->devno);
+ break;
+ case MIX_TOGGLEMUTE:
+ m->mutemask ^= (1 << m->dev->devno);
+ break;
+ default:
+ errno = EINVAL;
+ return (-1);
+ }
+ if (ioctl(m->fd, SOUND_MIXER_WRITE_MUTE, &m->mutemask) < 0)
+ return (-1);
+ if (ioctl(m->fd, SOUND_MIXER_READ_MUTE, &m->mutemask) < 0)
+ return (-1);
+
+ return 0;
+}
+
+/*
+ * Modify a recording device. The selected device has to be a recording device,
+ * otherwise the function will fail.
+ *
+ * @param opt MIX_ADDRECSRC add device to recording sources
+ * MIX_REMOVERECSRC remove device from recording sources
+ * MIX_SETRECSRC set device as the only recording source
+ * MIX_TOGGLERECSRC toggle device from recording sources
+ */
+int
+mixer_mod_recsrc(struct mixer *m, int opt)
+{
+ if (!m->recmask || !MIX_ISREC(m, m->dev->devno)) {
+ errno = ENODEV;
+ return (-1);
+ }
+ switch (opt) {
+ case MIX_ADDRECSRC:
+ m->recsrc |= (1 << m->dev->devno);
+ break;
+ case MIX_REMOVERECSRC:
+ m->recsrc &= ~(1 << m->dev->devno);
+ break;
+ case MIX_SETRECSRC:
+ m->recsrc = (1 << m->dev->devno);
+ break;
+ case MIX_TOGGLERECSRC:
+ m->recsrc ^= (1 << m->dev->devno);
+ break;
+ default:
+ errno = EINVAL;
+ return (-1);
+ }
+ if (ioctl(m->fd, SOUND_MIXER_WRITE_RECSRC, &m->recsrc) < 0)
+ return (-1);
+ if (ioctl(m->fd, SOUND_MIXER_READ_RECSRC, &m->recsrc) < 0)
+ return (-1);
+
+ return (0);
+}
+
+/*
+ * Get default audio card's number. This is used to open the default mixer
+ * and set the mixer structure's `f_default` flag.
+ */
+int
+mixer_get_dunit(void)
+{
+ size_t size;
+ int unit;
+
+ size = sizeof(int);
+ if (sysctlbyname("hw.snd.default_unit", &unit, &size, NULL, 0) < 0)
+ return (-1);
+
+ return (unit);
+}
+
+/*
+ * Change the default audio card. This is normally _not_ a mixer feature, but
+ * it's useful to have, so the caller can avoid having to manually use
+ * the sysctl API.
+ *
+ * @param unit the audio card number (e.g pcm0, pcm1, ...).
+ */
+int
+mixer_set_dunit(struct mixer *m, int unit)
+{
+ size_t size;
+
+ size = sizeof(int);
+ if (sysctlbyname("hw.snd.default_unit", NULL, 0, &unit, size) < 0)
+ return (-1);
+ /* XXX: how will other mixers get updated? */
+ m->f_default = m->unit == unit;
+
+ return (0);
+}
+
+/*
+ * Get sound device mode (none, play, rec, play+rec). Userland programs can
+ * use the MIX_MODE_* flags to determine the mode of the device.
+ */
+int
+mixer_get_mode(int unit)
+{
+ char buf[64];
+ size_t size;
+ unsigned int mode;
+
+ (void)snprintf(buf, sizeof(buf), "dev.pcm.%d.mode", unit);
+ size = sizeof(unsigned int);
+ if (sysctlbyname(buf, &mode, &size, NULL, 0) < 0)
+ return (0);
+
+ return (mode);
+}
+
+/*
+ * Get the total number of mixers in the system.
+ */
+int
+mixer_get_nmixers(void)
+{
+ struct mixer *m;
+ oss_sysinfo si;
+
+ /*
+ * Open a dummy mixer because we need the `fd` field for the
+ * `ioctl` to work.
+ */
+ if ((m = mixer_open(NULL)) == NULL)
+ return (-1);
+ if (ioctl(m->fd, OSS_SYSINFO, &si) < 0) {
+ (void)mixer_close(m);
+ return (-1);
+ }
+ (void)mixer_close(m);
+
+ return (si.nummixers);
+}
+
+/*
+ * Get the full path to a mixer device.
+ */
+int
+mixer_get_path(char *buf, size_t size, int unit)
+{
+ size_t n;
+
+ if (!(unit == -1 || (unit >= 0 && unit < mixer_get_nmixers()))) {
+ errno = EINVAL;
+ return (-1);
+ }
+ if (unit == -1)
+ n = strlcpy(buf, BASEPATH, size);
+ else
+ n = snprintf(buf, size, BASEPATH "%d", unit);
+
+ if (n >= size) {
+ errno = ENOMEM;
+ return (-1);
+ }
+
+ return (0);
+}