This diff adds an API to control knobs of audio interfaces. It aims to
address these points:

- allow the controls of modern audio hardware and sndiod software
  volume knobs in a uniform way. Hardware knobs are exposed by sndiod,
  which will be also important from security standpoint: by default
  programs using this API won't need to mess with /dev nodes.

- allow multiple programs to use the controls at the same time without
  the need to continuously scan the controls.

- the API is multi-channel and allows programs to "clump" controls and
  to represent many knobs as a single one. For instance the volume
  knob of a "modern" 10-channel azalia(4) could be represented as a
  single knob. This requires driver changes, not yet there.

- the API allows controls to be dynamically added or removed. This is
  useful to avoid restarting programs when devices are swapped,
  ex. when usb head-sets are used.

For now sndiod exposes only its own controls and the master output and
input volume knobs of the underlying hardware (if any). Other controls
don't seem necessary to expose to regular programs; we can keep things
simple and leave the other controls as configuration parameters set
only once, at system startup.

To ease discussions and review, the diff is split in 4 parts. This
diff is the first of 3 base diffs and 1 ports diff (see next
mails). To test the new code, apply all diffs, for instance:

cd /usr/src
patch -p0 <1.diff
patch -p0 <2.diff
patch -p0 <3.diff
cd /usr/src/include && doas make includes
cd /usr/src/lib/libsndio && make obj && make && doas make install
cd /usr/src/lib/libossaudio && make obj && make && doas make install
cd /usr/src/usr.bin/sndiod && make obj && make && doas make install
cd /usr/src/usr.bin/sndioctl && make obj && make && doas make install
doas rcctl restart sndiod
cd /usr/ports
patch -p0 <4.diff
cd /usr/ports/audio/gqmpeg && make && make install
cd /usr/ports/sysutils/tray-app && make && make install

I'm very interested in any regression. Feedback about the new features
is of course welcome as well.

Enjoy,

Index: include/sndio.h
===================================================================
RCS file: /cvs/src/include/sndio.h,v
retrieving revision 1.9
diff -u -p -u -p -r1.9 sndio.h
--- include/sndio.h     20 Dec 2015 11:29:29 -0000      1.9
+++ include/sndio.h     8 Feb 2020 14:49:37 -0000
@@ -24,12 +24,20 @@
  */
 #define SIO_DEVANY     "default"
 #define MIO_PORTANY    "default"
+#define SIOCTL_DEVANY  "default"
+
+/*
+ * limits
+ */
+#define SIOCTL_NAMEMAX         12      /* max name length */
+#define SIOCTL_VALMAX          127     /* max control value */
 
 /*
  * private ``handle'' structure
  */
 struct sio_hdl;
 struct mio_hdl;
+struct sioctl_hdl;
 
 /*
  * parameters of a full-duplex stream
@@ -85,12 +93,39 @@ struct sio_cap {
 #define SIO_XSTRINGS { "ignore", "sync", "error" }
 
 /*
+ * controlled component of the device
+ */
+struct sioctl_node {
+       char name[SIOCTL_NAMEMAX];      /* ex. "spkr" */
+       int unit;                       /* optional number or -1 */
+};
+
+/*
+ * description of a control (index, value) pair
+ */
+struct sioctl_desc {
+       unsigned int addr;              /* control address */
+#define SIOCTL_NONE            0       /* deleted */
+#define SIOCTL_NUM             2       /* integer in the 0..127 range */
+#define SIOCTL_SW              3       /* on/off switch (0 or 1) */
+#define SIOCTL_VEC             4       /* number, element of vector */
+#define SIOCTL_LIST            5       /* switch, element of a list */
+       unsigned int type;              /* one of above */
+       char func[SIOCTL_NAMEMAX];      /* function name, ex. "level" */
+       char group[SIOCTL_NAMEMAX];     /* group this control belongs to */
+       struct sioctl_node node0;       /* affected node */
+       struct sioctl_node node1;       /* dito for SIOCTL_{VEC,LIST} */
+};
+
+/*
  * mode bitmap
  */
 #define SIO_PLAY       1
 #define SIO_REC                2
 #define MIO_OUT                4
 #define MIO_IN         8
+#define SIOCTL_READ    0x100
+#define SIOCTL_WRITE   0x200
 
 /*
  * default bytes per sample for the given bits per sample
@@ -144,10 +179,24 @@ int mio_pollfd(struct mio_hdl *, struct 
 int mio_revents(struct mio_hdl *, struct pollfd *);
 int mio_eof(struct mio_hdl *);
 
+struct sioctl_hdl *sioctl_open(const char *, unsigned int, int);
+void sioctl_close(struct sioctl_hdl *);
+int sioctl_ondesc(struct sioctl_hdl *,
+    void (*)(void *, struct sioctl_desc *, int), void *);
+int sioctl_onval(struct sioctl_hdl *,
+    void (*)(void *, unsigned int, unsigned int), void *);
+int sioctl_setval(struct sioctl_hdl *, unsigned int, unsigned int);
+int sioctl_nfds(struct sioctl_hdl *);
+int sioctl_pollfd(struct sioctl_hdl *, struct pollfd *, int);
+int sioctl_revents(struct sioctl_hdl *, struct pollfd *);
+int sioctl_eof(struct sioctl_hdl *);
+
 int mio_rmidi_getfd(const char *, unsigned int, int);
 struct mio_hdl *mio_rmidi_fdopen(int, unsigned int, int);
 int sio_sun_getfd(const char *, unsigned int, int);
 struct sio_hdl *sio_sun_fdopen(int, unsigned int, int);
+int sioctl_sun_getfd(const char *, unsigned int, int);
+struct sioctl_hdl *sioctl_sun_fdopen(int, unsigned int, int);
 
 #ifdef __cplusplus
 }
Index: lib/libsndio/Makefile
===================================================================
RCS file: /cvs/src/lib/libsndio/Makefile,v
retrieving revision 1.13
diff -u -p -u -p -r1.13 Makefile
--- lib/libsndio/Makefile       26 Dec 2017 15:23:33 -0000      1.13
+++ lib/libsndio/Makefile       8 Feb 2020 14:49:37 -0000
@@ -1,9 +1,10 @@
 #      $OpenBSD: Makefile,v 1.13 2017/12/26 15:23:33 jca Exp $
 
 LIB=   sndio
-MAN=   sio_open.3 mio_open.3 sndio.7
+MAN=   sio_open.3 mio_open.3 sioctl_open.3 sndio.7
 SRCS=  debug.c aucat.c sio_aucat.c sio_sun.c sio.c \
-       mio_rmidi.c mio_aucat.c mio.c
+       mio_rmidi.c mio_aucat.c mio.c \
+       sioctl_aucat.c sioctl_sun.c sioctl.c
 CFLAGS+=-DDEBUG
 COPTS+=        -Wall -Wstrict-prototypes -Wmissing-prototypes -Wpointer-arith 
-Wundef
 
Index: lib/libsndio/Symbols.map
===================================================================
RCS file: /cvs/src/lib/libsndio/Symbols.map,v
retrieving revision 1.1
diff -u -p -u -p -r1.1 Symbols.map
--- lib/libsndio/Symbols.map    26 Dec 2017 19:12:22 -0000      1.1
+++ lib/libsndio/Symbols.map    8 Feb 2020 14:49:37 -0000
@@ -27,10 +27,22 @@
                mio_revents;
                mio_eof;
 
+               sioctl_open;
+               sioctl_close;
+               sioctl_ondesc;
+               sioctl_onval;
+               sioctl_setval;
+               sioctl_nfds;
+               sioctl_pollfd;
+               sioctl_revents;
+               sioctl_eof;
+
                mio_rmidi_getfd;
                mio_rmidi_fdopen;
                sio_sun_getfd;
                sio_sun_fdopen;
+               sioctl_sun_getfd;
+               sioctl_sun_fdopen;
        local:
                *;
 };
Index: lib/libsndio/amsg.h
===================================================================
RCS file: /cvs/src/lib/libsndio/amsg.h,v
retrieving revision 1.12
diff -u -p -u -p -r1.12 amsg.h
--- lib/libsndio/amsg.h 12 Jul 2019 06:30:55 -0000      1.12
+++ lib/libsndio/amsg.h 8 Feb 2020 14:49:37 -0000
@@ -43,6 +43,11 @@
 #define AUCAT_PORT             11025
 
 /*
+ * limits
+ */
+#define AMSG_CTL_NAMEMAX       16      /* max name length */
+
+/*
  * WARNING: since the protocol may be simultaneously used by static
  * binaries or by different versions of a shared library, we are not
  * allowed to change the packet binary representation in a backward
@@ -64,6 +69,9 @@ struct amsg {
 #define AMSG_HELLO     10      /* say hello, check versions and so ... */
 #define AMSG_BYE       11      /* ask server to drop connection */
 #define AMSG_AUTH      12      /* send authentication cookie */
+#define AMSG_CTLSUB    13      /* ondesc/onctl subscription */
+#define AMSG_CTLSET    14      /* set control value */
+#define AMSG_CTLSYNC   15      /* end of controls descriptions */
        uint32_t cmd;
        uint32_t __pad;
        union {
@@ -108,7 +116,37 @@ struct amsg {
 #define AMSG_COOKIELEN 16
                        uint8_t cookie[AMSG_COOKIELEN];
                } auth;
+               struct amsg_ctlsub {
+                       uint8_t desc, val;
+               } ctlsub;
+               struct amsg_ctlset {
+                       uint16_t addr, val;
+               } ctlset;
        } u;
+};
+
+/*
+ * network representation of sioctl_node structure
+ */
+struct amsg_ctl_node {
+       char name[AMSG_CTL_NAMEMAX];
+       int16_t unit;
+       uint8_t __pad[2];
+};
+
+/*
+ * network representation of sioctl_desc structure
+ */
+struct amsg_ctl_desc {
+       struct amsg_ctl_node node0;     /* affected channels */
+       struct amsg_ctl_node node1;     /* dito for AMSG_CTL_{SEL,VEC,LIST} */
+       char func[AMSG_CTL_NAMEMAX];    /* parameter function name */
+       char group[AMSG_CTL_NAMEMAX];   /* group of the control */
+       uint8_t type;                   /* see sioctl_desc structure */
+       uint8_t __pad1[1];
+       uint16_t addr;                  /* control address */
+       uint16_t __pad2[1];
+       uint16_t curval;
 };
 
 /*
Index: lib/libsndio/shlib_version
===================================================================
RCS file: /cvs/src/lib/libsndio/shlib_version,v
retrieving revision 1.11
diff -u -p -u -p -r1.11 shlib_version
--- lib/libsndio/shlib_version  26 Dec 2017 15:23:33 -0000      1.11
+++ lib/libsndio/shlib_version  8 Feb 2020 14:49:37 -0000
@@ -1,2 +1,2 @@
 major=7
-minor=0
+minor=1
Index: lib/libsndio/sioctl.c
===================================================================
RCS file: lib/libsndio/sioctl.c
diff -N lib/libsndio/sioctl.c
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ lib/libsndio/sioctl.c       8 Feb 2020 14:49:37 -0000
@@ -0,0 +1,177 @@
+/*     $OpenBSD$       */
+/*
+ * Copyright (c) 2008 Alexandre Ratchov <a...@caoua.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+#include <errno.h>
+#include <poll.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "debug.h"
+#include "sioctl_priv.h"
+
+struct sioctl_hdl *
+sioctl_open(const char *str, unsigned int mode, int nbio)
+{
+       static char devany[] = SIOCTL_DEVANY;
+       struct sioctl_hdl *hdl;
+
+#ifdef DEBUG
+       _sndio_debug_init();
+#endif
+       if (str == NULL) /* backward compat */
+               str = devany;
+       if (strcmp(str, devany) == 0 && !issetugid()) {
+               str = getenv("AUDIODEVICE");
+               if (str == NULL)
+                       str = devany;
+       }
+       if (strcmp(str, devany) == 0) {
+               hdl = _sioctl_aucat_open("snd/0", mode, nbio);
+               if (hdl != NULL)
+                       return hdl;
+               return _sioctl_sun_open("rsnd/0", mode, nbio);
+       }
+       if (_sndio_parsetype(str, "snd"))
+               return _sioctl_aucat_open(str, mode, nbio);
+       if (_sndio_parsetype(str, "rsnd"))
+               return _sioctl_sun_open(str, mode, nbio);
+       DPRINTF("sioctl_open: %s: unknown device type\n", str);
+       return NULL;
+}
+
+void
+_sioctl_create(struct sioctl_hdl *hdl, struct sioctl_ops *ops,
+    unsigned int mode, int nbio)
+{
+       hdl->ops = ops;
+       hdl->mode = mode;
+       hdl->nbio = nbio;
+       hdl->eof = 0;
+       hdl->ctl_cb = NULL;
+}
+
+int
+_sioctl_psleep(struct sioctl_hdl *hdl, int event)
+{
+       struct pollfd pfds[SIOCTL_MAXNFDS];
+       int revents, nfds;
+
+       for (;;) {
+               nfds = sioctl_pollfd(hdl, pfds, event);
+               if (nfds == 0)
+                       return 0;
+               while (poll(pfds, nfds, -1) < 0) {
+                       if (errno == EINTR)
+                               continue;
+                       DPERROR("sioctl_psleep: poll");
+                       hdl->eof = 1;
+                       return 0;
+               }
+               revents = sioctl_revents(hdl, pfds);
+               if (revents & POLLHUP) {
+                       DPRINTF("sioctl_psleep: hang-up\n");
+                       return 0;
+               }
+               if (event == 0 || (revents & event))
+                       break;
+       }
+       return 1;
+}
+
+void
+sioctl_close(struct sioctl_hdl *hdl)
+{
+       hdl->ops->close(hdl);
+}
+
+int
+sioctl_nfds(struct sioctl_hdl *hdl)
+{
+       return hdl->ops->nfds(hdl);
+}
+
+int
+sioctl_pollfd(struct sioctl_hdl *hdl, struct pollfd *pfd, int events)
+{
+       if (hdl->eof)
+               return 0;
+       return hdl->ops->pollfd(hdl, pfd, events);
+}
+
+int
+sioctl_revents(struct sioctl_hdl *hdl, struct pollfd *pfd)
+{
+       if (hdl->eof)
+               return POLLHUP;
+       return hdl->ops->revents(hdl, pfd);
+}
+
+int
+sioctl_eof(struct sioctl_hdl *hdl)
+{
+       return hdl->eof;
+}
+
+int
+sioctl_ondesc(struct sioctl_hdl *hdl,
+    void (*cb)(void *, struct sioctl_desc *, int), void *arg)
+{
+       hdl->desc_cb = cb;
+       hdl->desc_arg = arg;
+       return hdl->ops->ondesc(hdl);
+}
+
+int
+sioctl_onval(struct sioctl_hdl *hdl,
+    void (*cb)(void *, unsigned int, unsigned int), void *arg)
+{
+       hdl->ctl_cb = cb;
+       hdl->ctl_arg = arg;
+       return hdl->ops->onctl(hdl);
+}
+
+void
+_sioctl_ondesc_cb(struct sioctl_hdl *hdl,
+    struct sioctl_desc *desc, unsigned int val)
+{
+       if (desc) {
+               DPRINTF("_sioctl_ondesc_cb: %u -> %s[%d].%s=%s[%d]:%d\n",
+                   desc->addr,
+                   desc->node0.name, desc->node0.unit,
+                   desc->func,
+                   desc->node1.name, desc->node1.unit,
+                   val);
+       }
+       if (hdl->desc_cb)
+               hdl->desc_cb(hdl->desc_arg, desc, val);
+}
+
+void
+_sioctl_onval_cb(struct sioctl_hdl *hdl, unsigned int addr, unsigned int val)
+{
+       DPRINTF("_sioctl_onval_cb: %u -> %u\n", addr, val);
+       if (hdl->ctl_cb)
+               hdl->ctl_cb(hdl->ctl_arg, addr, val);
+}
+
+int
+sioctl_setval(struct sioctl_hdl *hdl, unsigned int addr, unsigned int val)
+{
+       if (!(hdl->mode & SIOCTL_WRITE))
+               return 0;
+       return hdl->ops->setctl(hdl, addr, val);
+}
Index: lib/libsndio/sioctl_aucat.c
===================================================================
RCS file: lib/libsndio/sioctl_aucat.c
diff -N lib/libsndio/sioctl_aucat.c
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ lib/libsndio/sioctl_aucat.c 8 Feb 2020 14:49:37 -0000
@@ -0,0 +1,272 @@
+/*
+ * Copyright (c) 2010-2011 Alexandre Ratchov <a...@caoua.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+#include <errno.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sndio.h>
+#include <unistd.h>
+#include <arpa/inet.h>
+#include "debug.h"
+#include "aucat.h"
+#include "sioctl_priv.h"
+
+struct sioctl_aucat_hdl {
+       struct sioctl_hdl sioctl;
+       struct aucat aucat;
+       struct sioctl_desc desc;
+       struct amsg_ctl_desc buf[16];
+       size_t buf_wpos;
+       int dump_wait;
+};
+
+static void sioctl_aucat_close(struct sioctl_hdl *);
+static int sioctl_aucat_nfds(struct sioctl_hdl *);
+static int sioctl_aucat_pollfd(struct sioctl_hdl *, struct pollfd *, int);
+static int sioctl_aucat_revents(struct sioctl_hdl *, struct pollfd *);
+static int sioctl_aucat_setctl(struct sioctl_hdl *, unsigned int, unsigned 
int);
+static int sioctl_aucat_onval(struct sioctl_hdl *);
+static int sioctl_aucat_ondesc(struct sioctl_hdl *);
+
+/*
+ * operations every device should support
+ */
+struct sioctl_ops sioctl_aucat_ops = {
+       sioctl_aucat_close,
+       sioctl_aucat_nfds,
+       sioctl_aucat_pollfd,
+       sioctl_aucat_revents,
+       sioctl_aucat_setctl,
+       sioctl_aucat_onval,
+       sioctl_aucat_ondesc
+};
+
+static int
+sioctl_aucat_rdata(struct sioctl_aucat_hdl *hdl)
+{
+       struct sioctl_desc desc;
+       struct amsg_ctl_desc *c;
+       size_t rpos;
+       int n;
+
+       while (hdl->aucat.rstate == RSTATE_DATA) {
+
+               /* read entries */
+               while (hdl->buf_wpos < sizeof(hdl->buf) &&
+                   hdl->aucat.rstate == RSTATE_DATA) {
+                       n = _aucat_rdata(&hdl->aucat,
+                           (unsigned char *)hdl->buf + hdl->buf_wpos,
+                           sizeof(hdl->buf) - hdl->buf_wpos,
+                           &hdl->sioctl.eof);
+                       if (n == 0 || hdl->sioctl.eof)
+                               return 0;
+                       hdl->buf_wpos += n;
+               }
+
+               /* parse entries */
+               c = hdl->buf;
+               rpos = 0;
+               while (rpos < hdl->buf_wpos) {
+                       strlcpy(desc.group, c->group, SIOCTL_NAMEMAX);
+                       strlcpy(desc.node0.name, c->node0.name, SIOCTL_NAMEMAX);
+                       desc.node0.unit = (int16_t)ntohs(c->node0.unit);
+                       strlcpy(desc.node1.name, c->node1.name, SIOCTL_NAMEMAX);
+                       desc.node1.unit = (int16_t)ntohs(c->node1.unit);
+                       strlcpy(desc.func, c->func, SIOCTL_NAMEMAX);
+                       desc.type = c->type;
+                       desc.addr = ntohs(c->addr);
+                       _sioctl_ondesc_cb(&hdl->sioctl,
+                           &desc, ntohs(c->curval));
+                       rpos += sizeof(struct amsg_ctl_desc);
+                       c++;
+               }
+               hdl->buf_wpos = 0;
+       }
+       return 1;
+}
+
+/*
+ * execute the next message, return 0 if blocked
+ */
+static int
+sioctl_aucat_runmsg(struct sioctl_aucat_hdl *hdl)
+{
+       if (!_aucat_rmsg(&hdl->aucat, &hdl->sioctl.eof))
+               return 0;
+       switch (ntohl(hdl->aucat.rmsg.cmd)) {
+       case AMSG_DATA:
+               hdl->buf_wpos = 0;
+               if (!sioctl_aucat_rdata(hdl))
+                       return 0;
+               break;
+       case AMSG_CTLSET:
+               DPRINTF("sioctl_aucat_runmsg: got CTLSET\n");
+               _sioctl_onval_cb(&hdl->sioctl,
+                   ntohs(hdl->aucat.rmsg.u.ctlset.addr),
+                   ntohs(hdl->aucat.rmsg.u.ctlset.val));
+               break;
+       case AMSG_CTLSYNC:
+               DPRINTF("sioctl_aucat_runmsg: got CTLSYNC\n");
+               hdl->dump_wait = 0;
+               _sioctl_ondesc_cb(&hdl->sioctl, NULL, 0);
+               break;
+       default:
+               DPRINTF("sio_aucat_runmsg: unhandled message %u\n",
+                   hdl->aucat.rmsg.cmd);
+               hdl->sioctl.eof = 1;
+               return 0;
+       }
+       hdl->aucat.rstate = RSTATE_MSG;
+       hdl->aucat.rtodo = sizeof(struct amsg);
+       return 1;
+}
+
+struct sioctl_hdl *
+_sioctl_aucat_open(const char *str, unsigned int mode, int nbio)
+{
+       struct sioctl_aucat_hdl *hdl;
+
+       hdl = malloc(sizeof(struct sioctl_aucat_hdl));
+       if (hdl == NULL)
+               return NULL;
+       if (!_aucat_open(&hdl->aucat, str, mode))
+               goto bad;
+       _sioctl_create(&hdl->sioctl, &sioctl_aucat_ops, mode, nbio);
+       if (!_aucat_setfl(&hdl->aucat, 1, &hdl->sioctl.eof))
+               goto bad;
+       hdl->dump_wait = 0;
+       return (struct sioctl_hdl *)hdl;
+bad:
+       free(hdl);
+       return NULL;
+}
+
+static void
+sioctl_aucat_close(struct sioctl_hdl *addr)
+{
+       struct sioctl_aucat_hdl *hdl = (struct sioctl_aucat_hdl *)addr;
+
+       if (!hdl->sioctl.eof)
+               _aucat_setfl(&hdl->aucat, 0, &hdl->sioctl.eof);
+       _aucat_close(&hdl->aucat, hdl->sioctl.eof);
+       free(hdl);
+}
+
+static int
+sioctl_aucat_ondesc(struct sioctl_hdl *addr)
+{
+       struct sioctl_aucat_hdl *hdl = (struct sioctl_aucat_hdl *)addr;
+
+       while (hdl->aucat.wstate != WSTATE_IDLE) {
+               if (!_sioctl_psleep(&hdl->sioctl, POLLOUT))
+                       return 0;
+       }
+       AMSG_INIT(&hdl->aucat.wmsg);
+       hdl->aucat.wmsg.cmd = htonl(AMSG_CTLSUB);
+       hdl->aucat.wmsg.u.ctlsub.desc = 1;
+       hdl->aucat.wmsg.u.ctlsub.val = 0;
+       hdl->aucat.wtodo = sizeof(struct amsg);
+       if (!_aucat_wmsg(&hdl->aucat, &hdl->sioctl.eof))
+               return 0;
+       hdl->dump_wait = 1;
+       while (hdl->dump_wait) {
+               DPRINTF("psleeping...\n");
+               if (!_sioctl_psleep(&hdl->sioctl, 0))
+                       return 0;
+               DPRINTF("psleeping done\n");
+       }
+       DPRINTF("done\n");
+       return 1;
+}
+
+static int
+sioctl_aucat_onval(struct sioctl_hdl *addr)
+{
+       struct sioctl_aucat_hdl *hdl = (struct sioctl_aucat_hdl *)addr;
+
+       while (hdl->aucat.wstate != WSTATE_IDLE) {
+               if (!_sioctl_psleep(&hdl->sioctl, POLLOUT))
+                       return 0;
+       }
+       AMSG_INIT(&hdl->aucat.wmsg);
+       hdl->aucat.wmsg.cmd = htonl(AMSG_CTLSUB);
+       hdl->aucat.wmsg.u.ctlsub.desc = 1;
+       hdl->aucat.wmsg.u.ctlsub.val = 1;
+       hdl->aucat.wtodo = sizeof(struct amsg);
+       if (!_aucat_wmsg(&hdl->aucat, &hdl->sioctl.eof))
+               return 0;
+       return 1;
+}
+
+static int
+sioctl_aucat_setctl(struct sioctl_hdl *addr, unsigned int a, unsigned int v)
+{
+       struct sioctl_aucat_hdl *hdl = (struct sioctl_aucat_hdl *)addr;
+
+       hdl->aucat.wstate = WSTATE_MSG;
+       hdl->aucat.wtodo = sizeof(struct amsg);
+       hdl->aucat.wmsg.cmd = htonl(AMSG_CTLSET);
+       hdl->aucat.wmsg.u.ctlset.addr = htons(a);
+       hdl->aucat.wmsg.u.ctlset.val = htons(v);
+       while (hdl->aucat.wstate != WSTATE_IDLE) {
+               if (_aucat_wmsg(&hdl->aucat, &hdl->sioctl.eof))
+                       break;
+               if (hdl->sioctl.nbio || !_sioctl_psleep(&hdl->sioctl, POLLOUT))
+                       return 0;
+       }
+       return 1;
+}
+
+static int
+sioctl_aucat_nfds(struct sioctl_hdl *addr)
+{
+       return 1;
+}
+
+static int
+sioctl_aucat_pollfd(struct sioctl_hdl *addr, struct pollfd *pfd, int events)
+{
+       struct sioctl_aucat_hdl *hdl = (struct sioctl_aucat_hdl *)addr;
+
+       return _aucat_pollfd(&hdl->aucat, pfd, events | POLLIN);
+}
+
+static int
+sioctl_aucat_revents(struct sioctl_hdl *addr, struct pollfd *pfd)
+{
+       struct sioctl_aucat_hdl *hdl = (struct sioctl_aucat_hdl *)addr;
+       int revents;
+
+       revents = _aucat_revents(&hdl->aucat, pfd);
+       if (revents & POLLIN) {
+               while (1) {
+                       if (hdl->aucat.rstate == RSTATE_MSG) {
+                               if (!sioctl_aucat_runmsg(hdl))
+                                       break;
+                       }
+                       if (hdl->aucat.rstate == RSTATE_DATA) {
+                               if (!sioctl_aucat_rdata(hdl))
+                                       break;
+                       }
+               }
+               revents &= ~POLLIN;
+       }
+       if (hdl->sioctl.eof)
+               return POLLHUP;
+       DPRINTFN(3, "sioctl_aucat_revents: revents = 0x%x\n", revents);
+       return revents;
+}
Index: lib/libsndio/sioctl_open.3
===================================================================
RCS file: lib/libsndio/sioctl_open.3
diff -N lib/libsndio/sioctl_open.3
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ lib/libsndio/sioctl_open.3  8 Feb 2020 14:49:37 -0000
@@ -0,0 +1,252 @@
+.\" $OpenBSD$
+.\"
+.\" Copyright (c) 2011 Alexandre Ratchov <a...@caoua.org>
+.\"
+.\" Permission to use, copy, modify, and distribute this software for any
+.\" purpose with or without fee is hereby granted, provided that the above
+.\" copyright notice and this permission notice appear in all copies.
+.\"
+.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+.\"
+.Dd $Mdocdate: September 29 2012 $
+.Dt SIO_OPEN 3
+.Os
+.Sh NAME
+.Nm sioctl_open ,
+.Nm sioctl_close ,
+.Nm sioctl_ondesc ,
+.Nm sioctl_onval ,
+.Nm sioctl_setval ,
+.Nm sioctl_nfds ,
+.Nm sioctl_pollfd ,
+.Nm sioctl_eof
+.Nd interface to audio parameters
+.Sh SYNOPSIS
+.Fd #include <sndio.h>
+.Ft "struct sioctl_hdl *"
+.Fn "sioctl_open" "const char *name" "unsigned int mode" "int nbio_flag"
+.Ft "void"
+.Fn "sioctl_close" "struct sioctl_hdl *hdl"
+.Ft "int"
+.Fn "sioctl_ondesc" "struct sioctl_hdl *hdl" "void (*cb)(void *arg, struct 
sioctl_desc *desc, int val)" "void *arg"
+.Ft "void"
+.Fn "sioctl_onval" "struct sioctl_hdl *hdl" "void (*cb)(void *arg, unsigned 
int addr, unsigned int val)" "void *arg"
+.Ft "int"
+.Fn "sioctl_setval" "struct sioctl_hdl *hdl" "unsigned int addr" "unsigned int 
val"
+.Ft "int"
+.Fn "sioctl_nfds" "struct sioctl_hdl *hdl"
+.Ft "int"
+.Fn "sioctl_pollfd" "struct sioctl_hdl *hdl" "struct pollfd *pfd" "int events"
+.Ft "int"
+.Fn "sioctl_revents" "struct sioctl_hdl *hdl" "struct pollfd *pfd"
+.Ft "int"
+.Fn "sioctl_eof" "struct sioctl_hdl *hdl"
+.Sh DESCRIPTION
+Audio devices may expose a number of controls, like the playback volume 
control.
+Each control has an integer
+.Em address
+and an integer
+.Em value .
+Depending on the control type, its integer value represents either a
+continuous quantity or a boolean.
+Any control may be changed by submitting
+a new value to its address.
+When values change, asynchronous notifications are sent.
+.Pp
+Controls descriptions are available, allowing them to be grouped and
+represented in a human usable form.
+.Sh Opening and closing the control device
+First the application must call the
+.Fn sioctl_open
+function to obtain a handle
+that will be passed as the
+.Ar hdl
+argument to other functions.
+.Pp
+The
+.Ar name
+parameter gives the device string discussed in
+.Xr sndio 7 .
+In most cases it should be set to SIOCTL_DEVANY to allow
+the user to select it using the
+.Ev AUDIODEVICE
+environment variable.
+The
+.Ar mode
+parameter is a bitmap of the SIOCTL_READ and SIOCTL_WRITE constants
+indicating whether control values can be read and
+modified respectively.
+.Pp
+If the
+.Ar nbio_flag
+argument is 1, then the
+.Fn sioctl_setval
+function (see below) may fail instead of blocking and
+the
+.Fn sioctl_ondesc
+function doesn't block.
+.Pp
+The
+.Fn sioctl_close
+function closes the control device and frees any allocated resources
+associated with the handle.
+.Sh Controls descriptions
+The
+.Fn sioctl_ondesc
+function can be used to obtain the description of all available controls
+and their initial values.
+It registers a call-back that is immediately invoked for all
+controls.
+It's called once with a NULL argument to indicate that the full
+description was sent and that the caller has a consistent
+representation of the controls set.
+.Pp
+Then, whenever a control description changes, the call-back is
+invoked with the updated information followed by a call with a NULL
+argument.
+.Pp
+Controls are described by the
+.Va sioctl_ondesc
+stucture as follows:
+.Bd -literal
+struct sioctl_node {
+       char name[SIOCTL_NAMEMAX];      /* ex. "spkr" */
+       int unit;                       /* optional number or -1 */
+};
+
+struct sioctl_desc {
+       unsigned int addr;              /* control address */
+#define SIOCTL_NONE            0       /* deleted */
+#define SIOCTL_NUM             2       /* integer in the 0..127 range */
+#define SIOCTL_SW              3       /* on/off switch (0 or 1) */
+#define SIOCTL_VEC             4       /* number, element of vector */
+#define SIOCTL_LIST            5       /* switch, element of a list */
+       unsigned int type;              /* one of above */
+       char func[SIOCTL_NAMEMAX];      /* function name, ex. "level" */
+       char group[SIOCTL_NAMEMAX];     /* group this control belongs to */
+       struct sioctl_node node0;       /* affected node */
+       struct sioctl_node node1;       /* dito for SIOCTL_{VEC,LIST} */
+};
+.Ed
+.Pp
+The
+.Va addr
+attribute is the control address, usable with
+.Fn sioctl_setval
+to set its value.
+.Pp
+The
+.Va type
+attribute indicates what the structure describes.
+Possible types are:
+.Bl -tag -width "SIOCTL_LIST"
+.It SIOCTL_NONE
+A previously valid control was deleted.
+.It SIOCTL_NUM
+A continuous control in the 0..SIOCTL_VALMAX range.
+For instance the volume of the speaker.
+.It SIOCTL_SW
+A on/off switch control.
+For instance the switch to mute the speaker.
+.It SIOCTL_VEC
+Element of an array of continuous controls.
+For instance the knob to control the amount of signal flowing
+from the line input to the speaker.
+.It SIOCTL_LIST
+An element of an array of on/off switches.
+For instance the line-in position of the
+speaker source selector.
+.El
+.Pp
+The
+.Va func
+attribute is the name of the parameter being controlled.
+There may be no parameters of different types with the same name.
+.Pp
+The
+.Va node0
+and
+.Va node1
+attributes indicate the names of the controlled nodes, typically
+channels of audio streams.
+.Va node1
+is meaningful for
+.Va SIOCTL_VEC
+and
+.Va SIOCTL_LIST
+only.
+.Pp
+Names in the
+.Va node0
+and
+.Va node1
+attributes and
+.Va func
+are strings usable as unique identifiers within the the given
+.Va group .
+.Sh Changing and reading control values
+Controls are changed with the
+.Fn sioctl_setval
+function, by giving the index of the control and the new value.
+The
+.Fn sioctl_onval
+function can be used to register a call-back which will be invoked whenever
+a control changes.
+Continuous values are in the 0..127 range.
+.Sh "Interface to" Xr poll 2
+The
+.Fn sioctl_pollfd
+function fills the array
+.Ar pfd
+of
+.Va pollfd
+structures, used by
+.Xr poll 2 ,
+with
+.Ar events ;
+the latter is a bit-mask of
+.Va POLLIN
+and
+.Va POLLOUT
+constants.
+.Fn sioctl_pollfd
+returns the number of
+.Va pollfd
+structures filled.
+The
+.Fn sioctl_revents
+function returns the bit-mask set by
+.Xr poll 2
+in the
+.Va pfd
+array of
+.Va pollfd
+structures.
+If
+.Va POLLOUT
+is set,
+.Fn sioctl_setval
+can be called without blocking.
+POLLHUP may be set if an error occurs, even if
+it is not selected with
+.Fn sioctl_pollfd .
+POLLIN is not used yet.
+.Pp
+The
+.Fn sioctl_nfds
+function returns the number of
+.Va pollfd
+structures the caller must preallocate in order to be sure
+that
+.Fn sioctl_pollfd
+will never overrun.
+.Sh SEE ALSO
+.Xr sndioctl 1 ,
+.Xr poll 2 ,
+.Xr sndio 7
Index: lib/libsndio/sioctl_priv.h
===================================================================
RCS file: lib/libsndio/sioctl_priv.h
diff -N lib/libsndio/sioctl_priv.h
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ lib/libsndio/sioctl_priv.h  8 Feb 2020 14:49:38 -0000
@@ -0,0 +1,62 @@
+/*     $OpenBSD$       */
+/*
+ * Copyright (c) 2008 Alexandre Ratchov <a...@caoua.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+#ifndef SIOCTL_PRIV_H
+#define SIOCTL_PRIV_H
+
+#include <sndio.h>
+
+#define SIOCTL_MAXNFDS 4
+
+/*
+ * private ``handle'' structure
+ */
+struct sioctl_hdl {
+       struct sioctl_ops *ops;
+       void (*desc_cb)(void *, struct sioctl_desc *, int);
+       void *desc_arg;
+       void (*ctl_cb)(void *, unsigned int, unsigned int);
+       void *ctl_arg;
+       unsigned int mode;              /* SIOCTL_READ | SIOCTL_WRITE */
+       int nbio;                       /* true if non-blocking io */
+       int eof;                        /* true if error occured */
+};
+
+/*
+ * operations every device should support
+ */
+struct sioctl_ops {
+       void (*close)(struct sioctl_hdl *);
+       int (*nfds)(struct sioctl_hdl *);
+       int (*pollfd)(struct sioctl_hdl *, struct pollfd *, int);
+       int (*revents)(struct sioctl_hdl *, struct pollfd *);
+       int (*setctl)(struct sioctl_hdl *, unsigned int, unsigned int);
+       int (*onctl)(struct sioctl_hdl *);
+       int (*ondesc)(struct sioctl_hdl *);
+};
+
+struct sioctl_hdl *_sioctl_aucat_open(const char *, unsigned int, int);
+struct sioctl_hdl *_sioctl_obsd_open(const char *, unsigned int, int);
+struct sioctl_hdl *_sioctl_fake_open(const char *, unsigned int, int);
+struct sioctl_hdl *_sioctl_sun_open(const char *, unsigned int, int);
+void _sioctl_create(struct sioctl_hdl *,
+    struct sioctl_ops *, unsigned int, int);
+void _sioctl_ondesc_cb(struct sioctl_hdl *,
+    struct sioctl_desc *, unsigned int);
+void _sioctl_onval_cb(struct sioctl_hdl *, unsigned int, unsigned int);
+int _sioctl_psleep(struct sioctl_hdl *, int);
+
+#endif /* !defined(SIOCTL_PRIV_H) */
Index: lib/libsndio/sioctl_sun.c
===================================================================
RCS file: lib/libsndio/sioctl_sun.c
diff -N lib/libsndio/sioctl_sun.c
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ lib/libsndio/sioctl_sun.c   8 Feb 2020 14:49:38 -0000
@@ -0,0 +1,486 @@
+/*
+ * Copyright (c) 2010-2011 Alexandre Ratchov <a...@caoua.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+/*
+ * the way the sun mixer is designed doesn't let us representing
+ * it easily with the sioctl api. For now expose only few
+ * white-listed controls the same way as we do in kernel
+ * for the wskbd volume keys.
+ */
+#include <sys/types.h>
+#include <sys/ioctl.h>
+#include <sys/audioio.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <poll.h>
+#include <sndio.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "debug.h"
+#include "sioctl_priv.h"
+
+#define DEVPATH_PREFIX "/dev/audioctl"
+#define DEVPATH_MAX    (1 +            \
+       sizeof(DEVPATH_PREFIX) - 1 +    \
+       sizeof(int) * 3)
+
+#define SUN_TO_SIOCTL(v) (((v) * 127 + 127) / 255)
+#define SIOCTL_TO_SUN(v) (((v) * 255 + 63) / 127)
+
+struct volume
+{
+       int nch;                        /* channels in the level control */
+       int level_idx;                  /* index of the level control */
+       int level_val[8];               /* current value */
+       int mute_idx;                   /* index of the mute control */
+       int mute_val;                   /* per channel state of mute control */
+       int base_addr;
+       char *name;
+};
+
+struct sioctl_sun_hdl {
+       struct sioctl_hdl sioctl;
+       struct volume output, input;
+       int fd, events;
+};
+
+static void sioctl_sun_close(struct sioctl_hdl *);
+static int sioctl_sun_nfds(struct sioctl_hdl *);
+static int sioctl_sun_pollfd(struct sioctl_hdl *, struct pollfd *, int);
+static int sioctl_sun_revents(struct sioctl_hdl *, struct pollfd *);
+static int sioctl_sun_setctl(struct sioctl_hdl *, unsigned int, unsigned int);
+static int sioctl_sun_onval(struct sioctl_hdl *);
+static int sioctl_sun_ondesc(struct sioctl_hdl *);
+
+/*
+ * operations every device should support
+ */
+struct sioctl_ops sioctl_sun_ops = {
+       sioctl_sun_close,
+       sioctl_sun_nfds,
+       sioctl_sun_pollfd,
+       sioctl_sun_revents,
+       sioctl_sun_setctl,
+       sioctl_sun_onval,
+       sioctl_sun_ondesc
+};
+
+static int
+initmute(struct sioctl_sun_hdl *hdl, struct mixer_devinfo *info)
+{
+       struct mixer_devinfo mi;
+
+       mi.index = info->next;
+       for (mi.index = info->next; mi.index != -1; mi.index = mi.next) {
+               if (ioctl(hdl->fd, AUDIO_MIXER_DEVINFO, &mi) < 0)
+                       break;
+               if (strcmp(mi.label.name, AudioNmute) == 0)
+                       return mi.index;
+       }
+       return -1;
+}
+
+static int
+initvol(struct sioctl_sun_hdl *hdl, struct volume *vol, char *cn, char *dn)
+{
+       struct mixer_devinfo dev, cls;
+
+       for (dev.index = 0; ; dev.index++) {
+               if (ioctl(hdl->fd, AUDIO_MIXER_DEVINFO, &dev) < 0)
+                       break;
+               if (dev.type != AUDIO_MIXER_VALUE)
+                       continue;
+               cls.index = dev.mixer_class;
+               if (ioctl(hdl->fd, AUDIO_MIXER_DEVINFO, &cls) < 0)
+                       break;
+               if (strcmp(cls.label.name, cn) == 0 &&
+                   strcmp(dev.label.name, dn) == 0) {
+                       vol->nch = dev.un.v.num_channels;
+                       vol->level_idx = dev.index;
+                       vol->mute_idx = initmute(hdl, &dev);
+                       DPRINTF("using %s.%s, %d channels, %s\n", cn, dn,
+                           vol->nch, vol->mute_idx >= 0 ? "mute" : "no mute");
+                       return 1;
+               }
+       }
+       vol->level_idx = vol->mute_idx = -1;
+       return 0;
+}
+
+static void
+init(struct sioctl_sun_hdl *hdl)
+{
+       static struct {
+               char *cn, *dn;
+       } output_names[] = {
+               {AudioCoutputs, AudioNmaster},
+               {AudioCinputs,  AudioNdac},
+               {AudioCoutputs, AudioNdac},
+               {AudioCoutputs, AudioNoutput}
+       }, input_names[] = {
+               {AudioCrecord, AudioNrecord},
+               {AudioCrecord, AudioNvolume},
+               {AudioCinputs, AudioNrecord},
+               {AudioCinputs, AudioNvolume},
+               {AudioCinputs, AudioNinput}
+       };
+       int i;
+
+       for (i = 0; i < sizeof(output_names) / sizeof(output_names[0]); i++) {
+               if (initvol(hdl, &hdl->output,
+                       output_names[i].cn, output_names[i].dn)) {
+                       hdl->output.name = "output";
+                       hdl->output.base_addr = 0;
+                       break;
+               }
+       }
+       for (i = 0; i < sizeof(input_names) / sizeof(input_names[0]); i++) {
+               if (initvol(hdl, &hdl->input,
+                       input_names[i].cn, input_names[i].dn)) {
+                       hdl->input.name = "input";
+                       hdl->input.base_addr = 64;
+                       break;
+               }
+       }
+}
+
+static int
+setvol(struct sioctl_sun_hdl *hdl, struct volume *vol, int addr, int val)
+{
+       struct mixer_ctrl ctrl;
+       int i;
+
+       addr -= vol->base_addr;
+       if (vol->level_idx >= 0 && addr >= 0 && addr < vol->nch) {
+               if (vol->level_val[addr] == val) {
+                       DPRINTF("level %d, no change\n", val);
+                       return 1;
+               }
+               vol->level_val[addr] = val;
+               ctrl.dev = vol->level_idx;
+               ctrl.type = AUDIO_MIXER_VALUE;
+               ctrl.un.value.num_channels = vol->nch;
+               for (i = 0; i < vol->nch; i++) {
+                       ctrl.un.value.level[i] =
+                           SIOCTL_TO_SUN(vol->level_val[i]);
+               }
+               DPRINTF("vol %d setting to %d\n", addr, vol->level_val[addr]);
+               if (ioctl(hdl->fd, AUDIO_MIXER_WRITE, &ctrl) < 0) {
+                       DPRINTF("level write failed\n");
+                       return 0;
+               }
+               _sioctl_onval_cb(&hdl->sioctl, vol->base_addr + addr, val);
+               return 1;
+       }
+
+       addr -= 32;
+       if (vol->mute_idx >= 0 && addr >= 0 && addr < vol->nch) {
+               val = val ? 1 : 0;
+               if (vol->mute_val == val) {
+                       DPRINTF("mute %d, no change\n", val);
+                       return 1;
+               }
+               vol->mute_val = val;
+               ctrl.dev = vol->mute_idx;
+               ctrl.type = AUDIO_MIXER_ENUM;
+               ctrl.un.ord = val;
+               DPRINTF("mute setting to %d\n", val);
+               if (ioctl(hdl->fd, AUDIO_MIXER_WRITE, &ctrl) < 0) {
+                       DPERROR("mute write\n");
+                       return 0;
+               }
+               for (i = 0; i < vol->nch; i++) {
+                       _sioctl_onval_cb(&hdl->sioctl,
+                           vol->base_addr + 32 + i, val);
+               }
+               return 1;
+       }
+       return 1;
+}
+
+static int
+scanvol(struct sioctl_sun_hdl *hdl, struct volume *vol)
+{
+       struct sioctl_desc desc;
+       struct mixer_ctrl ctrl;
+       int i, val;
+
+       memset(&desc, 0, sizeof(struct sioctl_desc));
+       if (vol->level_idx >= 0) {
+               ctrl.dev = vol->level_idx;
+               ctrl.type = AUDIO_MIXER_VALUE;
+               ctrl.un.value.num_channels = vol->nch;
+               if (ioctl(hdl->fd, AUDIO_MIXER_READ, &ctrl) < 0) {
+                       DPRINTF("level read failed\n");
+                       return 0;
+               }
+               desc.type = SIOCTL_NUM;
+               desc.node1.name[0] = 0;
+               desc.node1.unit = -1;
+               strlcpy(desc.func, "level", SIOCTL_NAMEMAX);
+               strlcpy(desc.node0.name, vol->name, SIOCTL_NAMEMAX);
+               for (i = 0; i < vol->nch; i++) {
+                       desc.node0.unit = i;
+                       desc.addr = vol->base_addr + i;
+                       val = SUN_TO_SIOCTL(ctrl.un.value.level[i]);
+                       vol->level_val[i] = val;
+                       _sioctl_ondesc_cb(&hdl->sioctl, &desc, val);
+               }
+       }
+       if (vol->mute_idx >= 0) {
+               ctrl.dev = vol->mute_idx;
+               ctrl.type = AUDIO_MIXER_ENUM;
+               if (ioctl(hdl->fd, AUDIO_MIXER_READ, &ctrl) < 0) {
+                       DPRINTF("mute read failed\n");
+                       return 0;
+               }
+               desc.type = SIOCTL_SW;
+               desc.node1.name[0] = 0;
+               desc.node1.unit = -1;
+               strlcpy(desc.func, "mute", SIOCTL_NAMEMAX);
+               strlcpy(desc.node0.name, vol->name, SIOCTL_NAMEMAX);
+               val = ctrl.un.ord ? 1 : 0;
+               vol->mute_val = val;
+               for (i = 0; i < vol->nch; i++) {
+                       desc.node0.unit = i;
+                       desc.addr = vol->base_addr + 32 + i;
+                       _sioctl_ondesc_cb(&hdl->sioctl, &desc, val);
+               }
+       }
+       return 1;
+}
+
+static int
+updatevol(struct sioctl_sun_hdl *hdl, struct volume *vol, int idx)
+{
+       struct mixer_ctrl ctrl;
+       int val, i;
+
+       if (idx == vol->mute_idx)
+               ctrl.type = AUDIO_MIXER_ENUM;
+       else {
+               ctrl.type = AUDIO_MIXER_VALUE;
+               ctrl.un.value.num_channels = vol->nch;
+       }
+       ctrl.dev = idx;
+       if (ioctl(hdl->fd, AUDIO_MIXER_READ, &ctrl) == -1) {
+               DPERROR("sioctl_sun_revents: ioctl\n");
+               hdl->sioctl.eof = 1;
+               return 0;
+       }
+       if (idx == vol->mute_idx) {
+               val = ctrl.un.ord ? 1 : 0;
+               if (vol->mute_val == val)
+                       return 1;
+               vol->mute_val = val;
+               for (i = 0; i < vol->nch; i++) {
+                       _sioctl_onval_cb(&hdl->sioctl,
+                           vol->base_addr + 32 + i, val);
+               }
+       } else {
+               for (i = 0; i < vol->nch; i++) {
+                       val = SUN_TO_SIOCTL(ctrl.un.value.level[i]);
+                       if (vol->level_val[i] == val)
+                               continue;
+                       vol->level_val[i] = val;
+                       _sioctl_onval_cb(&hdl->sioctl,
+                           vol->base_addr + i, val);
+               }
+       }
+       return 1;
+}
+
+int
+sioctl_sun_getfd(const char *str, unsigned int mode, int nbio)
+{
+       const char *p;
+       char path[DEVPATH_MAX];
+       unsigned int devnum;
+       int fd, flags;
+
+#ifdef DEBUG
+       _sndio_debug_init();
+#endif
+       p = _sndio_parsetype(str, "rsnd");
+       if (p == NULL) {
+               DPRINTF("sioctl_sun_getfd: %s: \"rsnd\" expected\n", str);
+               return -1;
+       }
+       switch (*p) {
+       case '/':
+               p++;
+               break;
+       default:
+               DPRINTF("sioctl_sun_getfd: %s: '/' expected\n", str);
+               return -1;
+       }
+       if (strcmp(p, "default") == 0) {
+               devnum = 0;
+       } else {
+               p = _sndio_parsenum(p, &devnum, 255);
+               if (p == NULL || *p != '\0') {
+                       DPRINTF("sioctl_sun_getfd: %s: number expected after 
'/'\n", str);
+                       return -1;
+               }
+       }
+       snprintf(path, sizeof(path), DEVPATH_PREFIX "%u", devnum);
+       if (mode == (SIOCTL_READ | SIOCTL_WRITE))
+               flags = O_RDWR;
+       else
+               flags = (mode & SIOCTL_WRITE) ? O_WRONLY : O_RDONLY;
+       while ((fd = open(path, flags | O_NONBLOCK | O_CLOEXEC)) < 0) {
+               if (errno == EINTR)
+                       continue;
+               DPERROR(path);
+               return -1;
+       }
+       return fd;
+}
+
+struct sioctl_hdl *
+sioctl_sun_fdopen(int fd, unsigned int mode, int nbio)
+{
+       struct sioctl_sun_hdl *hdl;
+
+#ifdef DEBUG
+       _sndio_debug_init();
+#endif
+       hdl = malloc(sizeof(struct sioctl_sun_hdl));
+       if (hdl == NULL)
+               return NULL;
+       _sioctl_create(&hdl->sioctl, &sioctl_sun_ops, mode, nbio);
+       hdl->fd = fd;
+       init(hdl);
+       return (struct sioctl_hdl *)hdl;
+}
+
+struct sioctl_hdl *
+_sioctl_sun_open(const char *str, unsigned int mode, int nbio)
+{
+       struct sioctl_hdl *hdl;
+       int fd;
+
+       fd = sioctl_sun_getfd(str, mode, nbio);
+       if (fd < 0)
+               return NULL;
+       hdl = sioctl_sun_fdopen(fd, mode, nbio);
+       if (hdl != NULL)
+               return hdl;
+       while (close(fd) < 0 && errno == EINTR)
+               ; /* retry */
+       return NULL;
+}
+
+static void
+sioctl_sun_close(struct sioctl_hdl *addr)
+{
+       struct sioctl_sun_hdl *hdl = (struct sioctl_sun_hdl *)addr;
+
+       close(hdl->fd);
+       free(hdl);
+}
+
+static int
+sioctl_sun_ondesc(struct sioctl_hdl *addr)
+{
+       struct sioctl_sun_hdl *hdl = (struct sioctl_sun_hdl *)addr;
+
+       if (!scanvol(hdl, &hdl->output) ||
+           !scanvol(hdl, &hdl->input)) {
+               hdl->sioctl.eof = 1;
+               return 0;
+       }
+       _sioctl_ondesc_cb(&hdl->sioctl, NULL, 0);
+       return 1;
+}
+
+static int
+sioctl_sun_onval(struct sioctl_hdl *addr)
+{
+       return 1;
+}
+
+static int
+sioctl_sun_setctl(struct sioctl_hdl *arg, unsigned int addr, unsigned int val)
+{
+       struct sioctl_sun_hdl *hdl = (struct sioctl_sun_hdl *)arg;
+
+       if (!setvol(hdl, &hdl->output, addr, val) ||
+           !setvol(hdl, &hdl->input, addr, val)) {
+               hdl->sioctl.eof = 1;
+               return 0;
+       }
+       return 1;
+}
+
+static int
+sioctl_sun_nfds(struct sioctl_hdl *addr)
+{
+       return 1;
+}
+
+static int
+sioctl_sun_pollfd(struct sioctl_hdl *addr, struct pollfd *pfd, int events)
+{
+       struct sioctl_sun_hdl *hdl = (struct sioctl_sun_hdl *)addr;
+
+       pfd->fd = hdl->fd;
+       pfd->events = POLLIN;
+       hdl->events = events;
+       return 1;
+}
+
+static int
+sioctl_sun_revents(struct sioctl_hdl *arg, struct pollfd *pfd)
+{
+       struct sioctl_sun_hdl *hdl = (struct sioctl_sun_hdl *)arg;
+       struct volume *vol;
+       int idx, n;
+
+       if (pfd->revents & POLLIN) {
+               while (1) {
+                       n = read(hdl->fd, &idx, sizeof(int));
+                       if (n == -1) {
+                               if (errno == EINTR || errno == EAGAIN)
+                                       break;
+                               DPERROR("read");
+                               hdl->sioctl.eof = 1;
+                               return POLLHUP;
+                       }
+                       if (n < sizeof(int)) {
+                               DPRINTF("sioctl_sun_revents: short read\n");
+                               hdl->sioctl.eof = 1;
+                               return POLLHUP;
+                       }
+
+                       if (idx == hdl->output.level_idx ||
+                           idx == hdl->output.mute_idx) {
+                               vol = &hdl->output;
+                       } else if (idx == hdl->input.level_idx ||
+                           idx == hdl->input.mute_idx) {
+                               vol = &hdl->input;
+                       } else
+                               continue;
+
+                       if (!updatevol(hdl, vol, idx))
+                               return POLLHUP;
+               }
+       }
+       return hdl->events & POLLOUT;
+}
Index: usr.bin/sndiod/Makefile
===================================================================
RCS file: /cvs/src/usr.bin/sndiod/Makefile,v
retrieving revision 1.5
diff -u -p -u -p -r1.5 Makefile
--- usr.bin/sndiod/Makefile     7 Jan 2016 07:41:01 -0000       1.5
+++ usr.bin/sndiod/Makefile     8 Feb 2020 14:49:38 -0000
@@ -1,8 +1,8 @@
 #      $OpenBSD: Makefile,v 1.5 2016/01/07 07:41:01 ratchov Exp $
 
 PROG=  sndiod
-SRCS=  abuf.c dev.c dsp.c fdpass.c file.c listen.c midi.c miofile.c \
-       opt.c siofile.c sndiod.c sock.c utils.c
+SRCS=  abuf.c dev.c dev_sioctl.c dsp.c fdpass.c file.c listen.c \
+       midi.c miofile.c opt.c siofile.c sndiod.c sock.c utils.c
 MAN=   sndiod.8
 CFLAGS+=-DDEBUG -I${.CURDIR}/../../lib/libsndio
 COPTS+=        -Wall -Wstrict-prototypes -Wmissing-prototypes -Wpointer-arith 
-Wundef
Index: usr.bin/sndiod/defs.h
===================================================================
RCS file: /cvs/src/usr.bin/sndiod/defs.h,v
retrieving revision 1.4
diff -u -p -u -p -r1.4 defs.h
--- usr.bin/sndiod/defs.h       28 Jul 2019 09:44:10 -0000      1.4
+++ usr.bin/sndiod/defs.h       8 Feb 2020 14:49:38 -0000
@@ -37,9 +37,12 @@
 #define MODE_MIDIOUT   0x04    /* allowed to read midi */
 #define MODE_MIDIIN    0x08    /* allowed to write midi */
 #define MODE_MON       0x10    /* allowed to monitor */
+#define MODE_CTLREAD   0x100   /* allowed to read controls */
+#define MODE_CTLWRITE  0x200   /* allowed to change controls */
 #define MODE_RECMASK   (MODE_REC | MODE_MON)
 #define MODE_AUDIOMASK (MODE_PLAY | MODE_REC | MODE_MON)
 #define MODE_MIDIMASK  (MODE_MIDIIN | MODE_MIDIOUT)
+#define MODE_CTLMASK   (MODE_CTLREAD | MODE_CTLWRITE)
 
 /*
  * underrun/overrun policies, must be the same as SIO_ constants
Index: usr.bin/sndiod/dev.c
===================================================================
RCS file: /cvs/src/usr.bin/sndiod/dev.c,v
retrieving revision 1.63
diff -u -p -u -p -r1.63 dev.c
--- usr.bin/sndiod/dev.c        10 Jan 2020 19:01:55 -0000      1.63
+++ usr.bin/sndiod/dev.c        8 Feb 2020 14:49:38 -0000
@@ -75,6 +75,7 @@ void dev_mmcstart(struct dev *);
 void dev_mmcstop(struct dev *);
 void dev_mmcloc(struct dev *, unsigned int);
 
+void slot_ctlname(struct slot *, char *, size_t);
 void slot_log(struct slot *);
 void slot_del(struct slot *);
 void slot_setvol(struct slot *, unsigned int);
@@ -91,6 +92,9 @@ void slot_write(struct slot *);
 void slot_read(struct slot *);
 int slot_skip(struct slot *);
 
+void ctl_node_log(struct ctl_node *);
+void ctl_log(struct ctl *);
+
 struct midiops dev_midiops = {
        dev_midi_imsg,
        dev_midi_omsg,
@@ -129,15 +133,22 @@ dev_log(struct dev *d)
 }
 
 void
+slot_ctlname(struct slot *s, char *name, size_t size)
+{
+       snprintf(name, size, "%s%u", s->name, s->unit);
+}
+
+void
 slot_log(struct slot *s)
 {
+       char name[CTL_NAMEMAX];
 #ifdef DEBUG
        static char *pstates[] = {
                "ini", "sta", "rdy", "run", "stp", "mid"
        };
 #endif
-       log_puts(s->name);
-       log_putu(s->unit);
+       slot_ctlname(s, name, CTL_NAMEMAX);
+       log_puts(name);
 #ifdef DEBUG
        if (log_level >= 3) {
                log_puts(" vol=");
@@ -365,10 +376,8 @@ dev_midi_slotdesc(struct dev *d, struct 
        x.dev = SYSEX_DEV_ANY;
        x.id0 = SYSEX_AUCAT;
        x.id1 = SYSEX_AUCAT_SLOTDESC;
-       if (*s->name != '\0') {
-               snprintf((char *)x.u.slotdesc.name, SYSEX_NAMELEN,
-                   "%s%u", s->name, s->unit);
-       }
+       if (*s->name != '\0')
+               slot_ctlname(s, (char *)x.u.slotdesc.name, SYSEX_NAMELEN);
        x.u.slotdesc.chan = s - d->slot;
        x.u.slotdesc.end = SYSEX_END;
        midi_send(d->midi, (unsigned char *)&x, SYSEX_SIZE(slotdesc));
@@ -419,6 +428,7 @@ dev_midi_omsg(void *arg, unsigned char *
                if (chan >= DEV_NSLOT)
                        return;
                slot_setvol(d->slot + chan, msg[2]);
+               dev_onval(d, CTLADDR_SLOT_LEVEL(chan), msg[2]);
                return;
        }
        x = (struct sysex *)msg;
@@ -429,8 +439,11 @@ dev_midi_omsg(void *arg, unsigned char *
        switch (x->type) {
        case SYSEX_TYPE_RT:
                if (x->id0 == SYSEX_CONTROL && x->id1 == SYSEX_MASTER) {
-                       if (len == SYSEX_SIZE(master))
+                       if (len == SYSEX_SIZE(master)) {
                                dev_master(d, x->u.master.coarse);
+                               dev_onval(d, CTLADDR_MASTER,
+                                   x->u.master.coarse);
+                       }
                        return;
                }
                if (x->id0 != SYSEX_MMC)
@@ -1001,10 +1014,17 @@ dev_new(char *path, struct aparams *par,
                d->slot[i].serial = d->serial++;
                strlcpy(d->slot[i].name, "prog", SLOT_NAMEMAX);
        }
+       for (i = 0; i < DEV_NCTLSLOT; i++) {
+               d->ctlslot[i].ops = NULL;
+               d->ctlslot[i].dev = d;
+               d->ctlslot[i].mask = 0;
+               d->ctlslot[i].mode = 0;
+       }
        d->slot_list = NULL;
        d->master = MIDI_MAXCTL;
        d->mtc.origin = 0;
        d->tstate = MMC_STOP;
+       d->ctl_list = NULL;
        d->next = dev_list;
        dev_list = d;
        return d;
@@ -1097,6 +1117,9 @@ dev_allocbufs(struct dev *d)
 int
 dev_open(struct dev *d)
 {
+       int i;
+       char name[CTL_NAMEMAX];
+
        d->mode = d->reqmode;
        d->round = d->reqround;
        d->bufsz = d->reqbufsz;
@@ -1117,6 +1140,17 @@ dev_open(struct dev *d)
        }
        if (!dev_allocbufs(d))
                return 0;
+
+       for (i = 0; i < DEV_NSLOT; i++) {
+               slot_ctlname(&d->slot[i], name, CTL_NAMEMAX);
+               dev_addctl(d, "app", CTL_NUM,
+                   CTLADDR_SLOT_LEVEL(i),
+                   name, -1, "level",
+                   NULL, -1, d->slot[i].vol);
+       }
+       dev_addctl(d, "", CTL_NUM,
+           CTLADDR_MASTER, "output", -1, "level", NULL, -1, d->master);
+
        d->pstate = DEV_INIT;
        return 1;
 }
@@ -1129,6 +1163,7 @@ dev_exitall(struct dev *d)
 {
        int i;
        struct slot *s;
+       struct ctlslot *c;
 
        for (s = d->slot, i = DEV_NSLOT; i > 0; i--, s++) {
                if (s->ops)
@@ -1136,6 +1171,12 @@ dev_exitall(struct dev *d)
                s->ops = NULL;
        }
        d->slot_list = NULL;
+
+       for (c = d->ctlslot, i = DEV_NCTLSLOT; i > 0; i--, c++) {
+               if (c->ops)
+                       c->ops->exit(c->arg);
+               c->ops = NULL;
+       }
 }
 
 /*
@@ -1169,10 +1210,18 @@ dev_freebufs(struct dev *d)
 void
 dev_close(struct dev *d)
 {
+       struct ctl *c;
+
        dev_exitall(d);
        d->pstate = DEV_CFG;
        dev_sio_close(d);
        dev_freebufs(d);
+
+       /* there are no clients, just free remaining local controls */
+       while ((c = d->ctl_list) != NULL) {
+               d->ctl_list = c->next;
+               xfree(c);
+       }
 }
 
 /*
@@ -1183,6 +1232,7 @@ int
 dev_reopen(struct dev *d)
 {
        struct slot *s;
+       struct ctl *c, **pc;
        long long pos;
        unsigned int pstate;
        int delta;
@@ -1236,6 +1286,25 @@ dev_reopen(struct dev *d)
                }
        }
 
+       /* remove controls of old device */
+       pc = &d->ctl_list;
+       while ((c = *pc) != NULL) {
+               if (c->addr >= CTLADDR_END) {
+                       c->refs_mask &= ~CTL_DEVMASK;
+                       if (c->refs_mask == 0) {
+                               *pc = c->next;
+                               xfree(c);
+                               continue;
+                       }
+                       c->type = CTL_NONE;
+                       c->desc_mask = ~0;
+               }
+               pc = &c->next;
+       }
+
+       /* add new device controls */
+       dev_sioctl_open(d);
+
        /* start the device if needed */
        if (pstate == DEV_RUN)
                dev_wakeup(d);
@@ -1760,6 +1829,7 @@ found:
        }
        if (!dev_ref(d))
                return NULL;
+       dev_label(d, s - d->slot);
        if ((mode & d->mode) != mode) {
                if (log_level >= 1) {
                        slot_log(s);
@@ -2096,4 +2166,278 @@ void
 slot_read(struct slot *s)
 {
        slot_skip_update(s);
+}
+
+/*
+ * allocate at control slot
+ */
+struct ctlslot *
+ctlslot_new(struct dev *d, struct ctlops *ops, void *arg)
+{
+       struct ctlslot *s;
+       struct ctl *c;
+       int i;
+
+       i = 0;
+       for (;;) {
+               if (i == DEV_NCTLSLOT)
+                       return NULL;
+               s = d->ctlslot + i;
+               if (s->ops == NULL)
+                       break;
+               i++;
+       }
+       s->dev = d;
+       s->mask = 1 << i;
+       if (!dev_ref(d))
+               return NULL;
+       s->ops = ops;
+       s->arg = arg;
+       for (c = d->ctl_list; c != NULL; c = c->next)
+               c->refs_mask |= s->mask;
+       return s;
+}
+
+/*
+ * free control slot
+ */
+void
+ctlslot_del(struct ctlslot *s)
+{
+       struct ctl *c, **pc;
+
+       pc = &s->dev->ctl_list;
+       while ((c = *pc) != NULL) {
+               c->refs_mask &= ~s->mask;
+               if (c->refs_mask == 0) {
+                       *pc = c->next;
+                       xfree(c);
+               } else
+                       pc = &c->next;
+       }
+       s->ops = NULL;
+       dev_unref(s->dev);
+}
+
+void
+ctl_node_log(struct ctl_node *c)
+{
+       log_puts(c->name);
+       if (c->unit >= 0)
+               log_putu(c->unit);
+}
+
+void
+ctl_log(struct ctl *c)
+{
+       if (c->group[0] != 0) {
+               log_puts(c->group);
+               log_puts("/");
+       }
+       ctl_node_log(&c->node0);
+       log_puts(".");
+       log_puts(c->func);
+       log_puts("=");
+       switch (c->type) {
+       case CTL_NUM:
+       case CTL_SW:
+               log_putu(c->curval);
+               break;
+       case CTL_VEC:
+       case CTL_LIST:
+               ctl_node_log(&c->node1);
+               log_puts(":");
+               log_putu(c->curval);
+       }
+       log_puts(" at ");
+       log_putu(c->addr);
+}
+
+/*
+ * add a ctl
+ */
+struct ctl *
+dev_addctl(struct dev *d, char *gstr, int type, int addr,
+    char *str0, int unit0, char *func, char *str1, int unit1, int val)
+{
+       struct ctl *c, **pc;
+       int i;
+
+       c = xmalloc(sizeof(struct ctl));
+       c->type = type;
+       strlcpy(c->func, func, CTL_NAMEMAX);
+       strlcpy(c->group, gstr, CTL_NAMEMAX);
+       strlcpy(c->node0.name, str0, CTL_NAMEMAX);
+       c->node0.unit = unit0;
+       if (c->type == CTL_VEC || c->type == CTL_LIST) {
+               strlcpy(c->node1.name, str1, CTL_NAMEMAX);
+               c->node1.unit = unit1;
+       } else
+               memset(&c->node1, 0, sizeof(struct ctl_node));
+       c->addr = addr;
+       c->val_mask = ~0;
+       c->desc_mask = ~0;
+       c->curval = val;
+       c->dirty = 0;
+       c->refs_mask = 0;
+       for (i = 0; i < DEV_NCTLSLOT; i++) {
+               c->refs_mask |= CTL_DEVMASK;
+               if (d->ctlslot[i].ops != NULL)
+                       c->refs_mask |= 1 << i;
+       }
+       for (pc = &d->ctl_list; *pc != NULL; pc = &(*pc)->next)
+               ; /* nothing */
+       c->next = NULL;
+       *pc = c;
+#ifdef DEBUG
+       if (log_level >= 3) {
+               dev_log(d);
+               log_puts(": adding ");
+               ctl_log(c);
+               log_puts("\n");
+       }
+#endif
+       return c;
+}
+
+void
+dev_rmctl(struct dev *d, int addr)
+{
+       struct ctl *c, **pc;
+
+       pc = &d->ctl_list;
+       for (;;) {
+               c = *pc;
+               if (c == NULL)
+                       return;
+               if (c->type != CTL_NONE && c->addr == addr)
+                       break;
+               pc = &c->next;
+       }
+       c->type = CTL_NONE;
+#ifdef DEBUG
+       if (log_level >= 3) {
+               dev_log(d);
+               log_puts(": removing ");
+               ctl_log(c);
+               log_puts(", refs_mask = 0x");
+               log_putx(c->refs_mask);
+               log_puts("\n");
+       }
+#endif
+       c->refs_mask &= ~CTL_DEVMASK;
+       if (c->refs_mask != 0)
+               return;
+       *pc = c->next;
+       xfree(c);
+}
+
+int
+dev_setctl(struct dev *d, int addr, int val)
+{
+       struct ctl *c;
+       int num;
+
+       if (val < 0 || val > MIDI_MAXCTL) {
+               if (log_level >= 3) {
+                       dev_log(d);
+                       log_puts(": ");
+                       log_putu(val);
+                       log_puts(": ctl val out of bounds\n");
+               }
+               return 0;
+       }
+       c = d->ctl_list;
+       for (;;) {
+               if (c == NULL) {
+                       if (log_level >= 3) {
+                               dev_log(d);
+                               log_puts(": ");
+                               log_putu(addr);
+                               log_puts(": no such ctl address\n");
+                       }
+                       return 0;
+               }
+               if (c->type != CTL_NONE && c->addr == addr)
+                       break;
+               c = c->next;
+       }
+       if (c->curval == val) {
+               if (log_level >= 3) {
+                       ctl_log(c);
+                       log_puts(": already set\n");
+               }
+               return 1;
+       }
+       if (addr >= CTLADDR_END) {
+               if (log_level >= 3) {
+                       ctl_log(c);
+                       log_puts(": marked as dirty\n");
+               }
+               c->dirty = 1;
+               dev_ref(d);
+       } else {
+               if (addr == CTLADDR_MASTER) {
+                       dev_master(d, val);
+                       dev_midi_master(d);
+               } else {
+                       num = addr - CTLADDR_SLOT_LEVEL(0);
+                       slot_setvol(d->slot + num, val);
+                       dev_midi_vol(d, d->slot + num);
+               }
+       }
+       c->curval = val;
+       c->val_mask = ~0U;
+       return 1;
+}
+
+int
+dev_onval(struct dev *d, int addr, int val)
+{
+       struct ctl *c;
+
+       c = d->ctl_list;
+       for (;;) {
+               if (c == NULL)
+                       return 0;
+               if (c->type != CTL_NONE && c->addr == addr)
+                       break;
+               c = c->next;
+       }
+       c->curval = val;
+       c->val_mask = ~0U;
+       return 1;
+}
+
+void
+dev_label(struct dev *d, int i)
+{
+       struct ctl *c;
+       char name[CTL_NAMEMAX];
+
+       c = d->ctl_list;
+       for (;;) {
+               if (c == NULL)
+                       return;
+               if (c->addr == CTLADDR_SLOT_LEVEL(i))
+                       break;
+               c = c->next;
+       }
+       slot_ctlname(&d->slot[i], name, CTL_NAMEMAX);
+       if (strcmp(c->node0.name, name) == 0)
+               return;
+       strlcpy(c->node0.name, name, CTL_NAMEMAX);
+       c->desc_mask = ~0;
+}
+
+int
+dev_nctl(struct dev *d)
+{
+       struct ctl *c;
+       int n;
+
+       n = 0;
+       for (c = d->ctl_list; c != NULL; c = c->next)
+               n++;
+       return n;
 }
Index: usr.bin/sndiod/dev.h
===================================================================
RCS file: /cvs/src/usr.bin/sndiod/dev.h,v
retrieving revision 1.22
diff -u -p -u -p -r1.22 dev.h
--- usr.bin/sndiod/dev.h        21 Sep 2019 04:42:46 -0000      1.22
+++ usr.bin/sndiod/dev.h        8 Feb 2020 14:49:38 -0000
@@ -20,6 +20,11 @@
 #include "abuf.h"
 #include "dsp.h"
 #include "siofile.h"
+#include "dev_sioctl.h"
+
+#define CTLADDR_SLOT_LEVEL(n)  (n)
+#define CTLADDR_MASTER         (DEV_NSLOT)
+#define CTLADDR_END            (DEV_NSLOT + 1)
 
 /*
  * audio stream state structure
@@ -28,13 +33,18 @@
 struct slotops
 {
        void (*onmove)(void *);                 /* clock tick */
-       void (*onvol)(void *);          /* tell client vol changed */
+       void (*onvol)(void *);                  /* tell client vol changed */
        void (*fill)(void *);                   /* request to fill a play block 
*/
        void (*flush)(void *);                  /* request to flush a rec block 
*/
        void (*eof)(void *);                    /* notify that play drained */
        void (*exit)(void *);                   /* delete client */
 };
 
+struct ctlops
+{
+       void (*exit)(void *);                   /* delete client */
+};
+
 struct slot {
        struct slotops *ops;                    /* client callbacks */
        struct slot *next;                      /* next on the play list */
@@ -105,6 +115,43 @@ struct opt {
 };
 
 /*
+ * subset of channels of a stream
+ */
+
+struct ctl {
+       struct ctl *next;
+#define CTL_NONE       0               /* deleted */
+#define CTL_NUM                2               /* number (aka integer value) */
+#define CTL_SW         3               /* on/off switch, only bit 7 counts */
+#define CTL_VEC                4               /* number, element of vector */
+#define CTL_LIST       5               /* switch, element of a list */
+       unsigned int type;              /* one of above */
+       unsigned int addr;              /* control address */
+#define CTL_NAMEMAX    16              /* max name lenght */
+       char func[CTL_NAMEMAX];         /* parameter function name */
+       char group[CTL_NAMEMAX];        /* group aka namespace */
+       struct ctl_node {
+               char name[CTL_NAMEMAX]; /* stream name */
+               int unit;
+       } node0, node1;                 /* affected channels */
+#define CTL_DEVMASK            (1 << 31)
+#define CTL_SLOTMASK(i)                (1 << (i))
+       unsigned int val_mask;
+       unsigned int desc_mask;
+       unsigned int refs_mask;
+       unsigned int curval;
+       int dirty;
+};
+
+struct ctlslot {
+       struct ctlops *ops;
+       void *arg;
+       struct dev *dev;
+       unsigned int mask;
+       unsigned int mode;
+};
+
+/*
  * audio device with plenty of slots
  */
 struct dev {
@@ -117,6 +164,7 @@ struct dev {
         * audio device (while opened)
         */
        struct dev_sio sio;
+       struct dev_sioctl sioctl;
        struct aparams par;                     /* encoding */
        int pchan, rchan;                       /* play & rec channels */
        adata_t *rbuf;                          /* rec buffer */
@@ -195,6 +243,14 @@ struct dev {
 #define MMC_RUN                3                       /* started */
        unsigned int tstate;                    /* one of above */
        unsigned int master;                    /* master volume controller */
+
+       /*
+        * control
+        */
+
+       struct ctl *ctl_list;
+#define DEV_NCTLSLOT 8
+       struct ctlslot ctlslot[DEV_NCTLSLOT];
 };
 
 extern struct dev *dev_list;
@@ -241,5 +297,20 @@ void slot_start(struct slot *);
 void slot_stop(struct slot *);
 void slot_read(struct slot *);
 void slot_write(struct slot *);
+
+/*
+ * control related functions
+ */
+void ctl_log(struct ctl *);
+struct ctlslot *ctlslot_new(struct dev *, struct ctlops *, void *);
+void ctlslot_del(struct ctlslot *);
+int dev_setctl(struct dev *, int, int);
+int dev_onval(struct dev *, int, int);
+int dev_nctl(struct dev *);
+void dev_label(struct dev *, int);
+struct ctl *dev_addctl(struct dev *, char *, int, int,
+    char *, int, char *, char *, int, int);
+void dev_rmctl(struct dev *, int);
+int dev_makeunit(struct dev *, char *);
 
 #endif /* !defined(DEV_H) */
Index: usr.bin/sndiod/dev_sioctl.c
===================================================================
RCS file: usr.bin/sndiod/dev_sioctl.c
diff -N usr.bin/sndiod/dev_sioctl.c
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ usr.bin/sndiod/dev_sioctl.c 8 Feb 2020 14:49:38 -0000
@@ -0,0 +1,204 @@
+/*     $OpenBSD$       */
+/*
+ * Copyright (c) 2014 Alexandre Ratchov <a...@caoua.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+#include <sys/time.h>
+#include <sys/types.h>
+
+#include <poll.h>
+#include <sndio.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "abuf.h"
+#include "defs.h"
+#include "dev.h"
+#include "dsp.h"
+#include "file.h"
+#include "dev_sioctl.h"
+#include "utils.h"
+
+void dev_sioctl_ondesc(void *, struct sioctl_desc *, int);
+void dev_sioctl_onval(void *, unsigned int, unsigned int);
+int dev_sioctl_pollfd(void *, struct pollfd *);
+int dev_sioctl_revents(void *, struct pollfd *);
+void dev_sioctl_in(void *);
+void dev_sioctl_out(void *);
+void dev_sioctl_hup(void *);
+
+struct fileops dev_sioctl_ops = {
+       "sioctl",
+       dev_sioctl_pollfd,
+       dev_sioctl_revents,
+       dev_sioctl_in,
+       dev_sioctl_out,
+       dev_sioctl_hup
+};
+
+void
+dev_sioctl_ondesc(void *arg, struct sioctl_desc *desc, int val)
+{
+#define GROUP_PREFIX           "hw"
+       char group_buf[CTL_NAMEMAX], *group;
+       struct dev *d = arg;
+       size_t len;
+       int addr;
+
+       if (desc == NULL)
+               return;
+       addr = CTLADDR_END + desc->addr;
+       dev_rmctl(d, addr);
+
+       /*
+        * prefix group names we use (top-level and "app") with "hw."
+        * to ensure that all controls have unique names when multiple
+        * sndiod's are chained
+        */
+       if (desc->group[0] == 0)
+               group = GROUP_PREFIX;
+       else if (strcmp(desc->group, GROUP_PREFIX) == 0 ||
+           strcmp(desc->group, "app") == 0) {
+               group = group_buf;
+               len = snprintf(group_buf, CTL_NAMEMAX,
+                   GROUP_PREFIX ".%s", desc->group);
+               if (len >= CTL_NAMEMAX)
+                       return;
+       } else
+               group = desc->group;
+
+       dev_addctl(d, group, desc->type, addr,
+           desc->node0.name, desc->node0.unit, desc->func,
+           desc->node1.name, desc->node1.unit, val);
+}
+
+void
+dev_sioctl_onval(void *arg, unsigned int addr, unsigned int val)
+{
+       struct dev *d = arg;
+       struct ctl *c;
+
+       addr += CTLADDR_END;
+
+       dev_log(d);
+       log_puts(": onctl: addr = ");
+       log_putu(addr);
+       log_puts(", val = ");
+       log_putu(val);
+       log_puts("\n");
+
+       for (c = d->ctl_list; c != NULL; c = c->next) {
+               if (c->addr != addr)
+                       continue;
+               ctl_log(c);
+               log_puts(": new value -> ");
+               log_putu(val);
+               log_puts("\n");
+               c->val_mask = ~0U;
+               c->curval = val;
+       }
+}
+
+/*
+ * open the control device.
+ */
+void
+dev_sioctl_open(struct dev *d)
+{
+       if (d->sioctl.hdl == NULL)
+               return;
+       sioctl_ondesc(d->sioctl.hdl, dev_sioctl_ondesc, d);
+       sioctl_onval(d->sioctl.hdl, dev_sioctl_onval, d);
+       d->sioctl.file = file_new(&dev_sioctl_ops, d, "mix",
+           sioctl_nfds(d->sioctl.hdl));
+}
+
+/*
+ * close the control device.
+ */
+void
+dev_sioctl_close(struct dev *d)
+{
+       if (d->sioctl.hdl == NULL)
+               return;
+       file_del(d->sioctl.file);
+}
+
+int
+dev_sioctl_pollfd(void *arg, struct pollfd *pfd)
+{
+       struct dev *d = arg;
+       struct ctl *c;
+       int events = 0;
+
+       for (c = d->ctl_list; c != NULL; c = c->next) {
+               if (c->dirty)
+                       events |= POLLOUT;
+       }
+       return sioctl_pollfd(d->sioctl.hdl, pfd, events);
+}
+
+int
+dev_sioctl_revents(void *arg, struct pollfd *pfd)
+{
+       struct dev *d = arg;
+
+       return sioctl_revents(d->sioctl.hdl, pfd);
+}
+
+void
+dev_sioctl_in(void *arg)
+{
+}
+
+void
+dev_sioctl_out(void *arg)
+{
+       struct dev *d = arg;
+       struct ctl *c;
+       int cnt;
+
+       /*
+        * for each dirty ctl, call sioctl_setval() and dev_unref(). As
+        * dev_unref() may destroy the ctl_list, we must call it after
+        * we've finished iterating on it.
+        */
+       cnt = 0;
+       for (c = d->ctl_list; c != NULL; c = c->next) {
+               if (!c->dirty)
+                       continue;
+               if (!sioctl_setval(d->sioctl.hdl,
+                       c->addr - CTLADDR_END, c->curval)) {
+                       ctl_log(c);
+                       log_puts(": set failed\n");
+                       break;
+               }
+               if (log_level >= 2) {
+                       ctl_log(c);
+                       log_puts(": changed\n");
+               }
+               c->dirty = 0;
+               cnt++;
+       }
+       while (cnt-- > 0)
+               dev_unref(d);
+}
+
+void
+dev_sioctl_hup(void *arg)
+{
+       struct dev *d = arg;
+
+       dev_sioctl_close(d);
+}
Index: usr.bin/sndiod/dev_sioctl.h
===================================================================
RCS file: usr.bin/sndiod/dev_sioctl.h
diff -N usr.bin/sndiod/dev_sioctl.h
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ usr.bin/sndiod/dev_sioctl.h 8 Feb 2020 14:49:38 -0000
@@ -0,0 +1,32 @@
+/*     $OpenBSD$       */
+/*
+ * Copyright (c) 2014 Alexandre Ratchov <a...@caoua.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+#ifndef DEV_SIOCTL_H
+#define DEV_SIOCTL_H
+
+#include "file.h"
+
+struct dev;
+
+struct dev_sioctl {
+       struct sioctl_hdl *hdl;
+       struct file *file;
+};
+
+void dev_sioctl_open(struct dev *);
+void dev_sioctl_close(struct dev *);
+
+#endif /* !defined(DEV_SIOCTL_H) */
Index: usr.bin/sndiod/fdpass.c
===================================================================
RCS file: /cvs/src/usr.bin/sndiod/fdpass.c,v
retrieving revision 1.8
diff -u -p -u -p -r1.8 fdpass.c
--- usr.bin/sndiod/fdpass.c     23 Jan 2020 05:40:09 -0000      1.8
+++ usr.bin/sndiod/fdpass.c     8 Feb 2020 14:49:38 -0000
@@ -32,6 +32,7 @@
 struct fdpass_msg {
 #define FDPASS_OPEN_SND                0       /* open an audio device */
 #define FDPASS_OPEN_MIDI       1       /* open a midi port */
+#define FDPASS_OPEN_CTL                2       /* open an audio control device 
*/
 #define FDPASS_RETURN          3       /* return after above commands */
        unsigned int cmd;               /* one of above */
        unsigned int num;               /* audio device or midi port number */
@@ -287,6 +288,22 @@ fdpass_mio_open(int num, int idx, unsign
        return mio_rmidi_fdopen(fd, mode, 1);
 }
 
+struct sioctl_hdl *
+fdpass_sioctl_open(int num, int idx, unsigned int mode)
+{
+       int fd;
+
+       if (fdpass_peer == NULL)
+               return NULL;
+       if (!fdpass_send(fdpass_peer, FDPASS_OPEN_CTL, num, idx, mode, -1))
+               return NULL;
+       if (!fdpass_waitret(fdpass_peer, &fd))
+               return NULL;
+       if (fd < 0)
+               return NULL;
+       return sioctl_sun_fdopen(fd, mode, 1);
+}
+
 void
 fdpass_in_worker(void *arg)
 {
@@ -345,6 +362,23 @@ fdpass_in_helper(void *arg)
                        return;
                }
                fd = mio_rmidi_getfd(path, mode, 1);
+               break;
+       case FDPASS_OPEN_CTL:
+               d = dev_bynum(num);
+               if (d == NULL || !(mode & (SIOCTL_READ | SIOCTL_WRITE))) {
+                       if (log_level >= 1) {
+                               fdpass_log(f);
+                               log_puts(": bad audio control device\n");
+                       }
+                       fdpass_close(f);
+                       return;
+               }
+               path = namelist_byindex(&d->path_list, idx);
+               if (path == NULL) {
+                       fdpass_close(f);
+                       return;
+               }
+               fd = sioctl_sun_getfd(path, mode, 1);
                break;
        default:
                fdpass_close(f);
Index: usr.bin/sndiod/fdpass.h
===================================================================
RCS file: /cvs/src/usr.bin/sndiod/fdpass.h,v
retrieving revision 1.2
diff -u -p -u -p -r1.2 fdpass.h
--- usr.bin/sndiod/fdpass.h     23 Jan 2020 05:40:09 -0000      1.2
+++ usr.bin/sndiod/fdpass.h     8 Feb 2020 14:49:38 -0000
@@ -27,5 +27,6 @@ extern struct fdpass *fdpass_peer;
 
 struct sio_hdl *fdpass_sio_open(int, int, unsigned int);
 struct mio_hdl *fdpass_mio_open(int, int, unsigned int);
+struct sioctl_hdl *fdpass_sioctl_open(int, int, unsigned int);
 
 #endif /* !defined(FDPASS_H) */
Index: usr.bin/sndiod/siofile.c
===================================================================
RCS file: /cvs/src/usr.bin/sndiod/siofile.c,v
retrieving revision 1.17
diff -u -p -u -p -r1.17 siofile.c
--- usr.bin/sndiod/siofile.c    23 Jan 2020 05:40:09 -0000      1.17
+++ usr.bin/sndiod/siofile.c    8 Feb 2020 14:49:39 -0000
@@ -26,6 +26,7 @@
 #include "abuf.h"
 #include "defs.h"
 #include "dev.h"
+#include "dev_sioctl.h"
 #include "dsp.h"
 #include "fdpass.h"
 #include "file.h"
@@ -88,10 +89,11 @@ dev_sio_timeout(void *arg)
  * open the device using one of the provided paths
  */
 static struct sio_hdl *
-dev_sio_openlist(struct dev *d, unsigned int mode)
+dev_sio_openlist(struct dev *d, unsigned int mode, struct sioctl_hdl **rctlhdl)
 {
        struct name *n;
        struct sio_hdl *hdl;
+       struct sioctl_hdl *ctlhdl;
        int idx;
 
        idx = 0;
@@ -107,6 +109,15 @@ dev_sio_openlist(struct dev *d, unsigned
                                log_puts(n->str);
                                log_puts("\n");
                        }
+                       ctlhdl = fdpass_sioctl_open(d->num, idx,
+                           SIOCTL_READ | SIOCTL_WRITE);
+                       if (ctlhdl == NULL) {
+                               if (log_level >= 1) {
+                                       dev_log(d);
+                                       log_puts(": no control device\n");
+                               }
+                       }
+                       *rctlhdl = ctlhdl;
                        return hdl;
                }
                n = n->next;
@@ -124,15 +135,16 @@ dev_sio_open(struct dev *d)
        struct sio_par par;
        unsigned int mode = d->mode & (MODE_PLAY | MODE_REC);
 
-       d->sio.hdl = dev_sio_openlist(d, mode);
+       d->sio.hdl = dev_sio_openlist(d, mode, &d->sioctl.hdl);
        if (d->sio.hdl == NULL) {
                if (mode != (SIO_PLAY | SIO_REC))
                        return 0;
-               d->sio.hdl = dev_sio_openlist(d, SIO_PLAY);
+               d->sio.hdl = dev_sio_openlist(d, SIO_PLAY, &d->sioctl.hdl);
                if (d->sio.hdl != NULL)
                        mode = SIO_PLAY;
                else {
-                       d->sio.hdl = dev_sio_openlist(d, SIO_REC);
+                       d->sio.hdl = dev_sio_openlist(d,
+                           SIO_REC, &d->sioctl.hdl);
                        if (d->sio.hdl != NULL)
                                mode = SIO_REC;
                        else
@@ -245,9 +257,14 @@ dev_sio_open(struct dev *d)
        sio_onmove(d->sio.hdl, dev_sio_onmove, d);
        d->sio.file = file_new(&dev_sio_ops, d, "dev", sio_nfds(d->sio.hdl));
        timo_set(&d->sio.watchdog, dev_sio_timeout, d);
+       dev_sioctl_open(d);
        return 1;
  bad_close:
        sio_close(d->sio.hdl);
+       if (d->sioctl.hdl) {
+               sioctl_close(d->sioctl.hdl);
+               d->sioctl.hdl = NULL;
+       }
        return 0;
 }
 
@@ -259,10 +276,11 @@ dev_sio_open(struct dev *d)
 int
 dev_sio_reopen(struct dev *d)
 {
+       struct sioctl_hdl *ctlhdl;
        struct sio_par par;
        struct sio_hdl *hdl;
 
-       hdl = dev_sio_openlist(d, d->mode & (MODE_PLAY | MODE_REC));
+       hdl = dev_sio_openlist(d, d->mode & (MODE_PLAY | MODE_REC), &ctlhdl);
        if (hdl == NULL) {
                if (log_level >= 1) {
                        dev_log(d);
@@ -303,6 +321,11 @@ dev_sio_reopen(struct dev *d)
        timo_del(&d->sio.watchdog);
        file_del(d->sio.file);
        sio_close(d->sio.hdl);
+       dev_sioctl_close(d);
+       if (d->sioctl.hdl) {
+               sioctl_close(d->sioctl.hdl);
+               d->sioctl.hdl = NULL;
+       }
 
        /* update parameters */
        d->par.bits = par.bits;
@@ -316,17 +339,21 @@ dev_sio_reopen(struct dev *d)
                d->rchan = par.rchan;
 
        d->sio.hdl = hdl;
+       d->sioctl.hdl = ctlhdl;
        d->sio.file = file_new(&dev_sio_ops, d, "dev", sio_nfds(hdl));
        sio_onmove(hdl, dev_sio_onmove, d);
        return 1;
 bad_close:
        sio_close(hdl);
+       if (ctlhdl)
+               sioctl_close(ctlhdl);
        return 0;
 }
 
 void
 dev_sio_close(struct dev *d)
 {
+       dev_sioctl_close(d);
 #ifdef DEBUG
        if (log_level >= 3) {
                dev_log(d);
@@ -336,6 +363,10 @@ dev_sio_close(struct dev *d)
        timo_del(&d->sio.watchdog);
        file_del(d->sio.file);
        sio_close(d->sio.hdl);
+       if (d->sioctl.hdl) {
+               sioctl_close(d->sioctl.hdl);
+               d->sioctl.hdl = NULL;
+       }
 }
 
 void
Index: usr.bin/sndiod/sndiod.c
===================================================================
RCS file: /cvs/src/usr.bin/sndiod/sndiod.c,v
retrieving revision 1.37
diff -u -p -u -p -r1.37 sndiod.c
--- usr.bin/sndiod/sndiod.c     21 Sep 2019 04:52:07 -0000      1.37
+++ usr.bin/sndiod/sndiod.c     8 Feb 2020 14:49:39 -0000
@@ -413,8 +413,10 @@ start_helper(int background)
                                err(1, "cannot drop privileges");
                }
                for (d = dev_list; d != NULL; d = d->next) {
-                       for (n = d->path_list; n != NULL; n = n->next)
+                       for (n = d->path_list; n != NULL; n = n->next) {
                                dounveil(n->str, "rsnd/", "/dev/audio");
+                               dounveil(n->str, "rsnd/", "/dev/audioctl");
+                       }
                }
                for (p = port_list; p != NULL; p = p->next) {
                        for (n = p->path_list; n != NULL; n = n->next)
Index: usr.bin/sndiod/sock.c
===================================================================
RCS file: /cvs/src/usr.bin/sndiod/sock.c,v
retrieving revision 1.31
diff -u -p -u -p -r1.31 sock.c
--- usr.bin/sndiod/sock.c       12 Jul 2019 06:30:55 -0000      1.31
+++ usr.bin/sndiod/sock.c       8 Feb 2020 14:49:39 -0000
@@ -32,6 +32,8 @@
 #include "sock.h"
 #include "utils.h"
 
+#define SOCK_CTLDESC_SIZE      16      /* number of entries in s->ctldesc */
+
 void sock_log(struct sock *);
 void sock_close(struct sock *);
 void sock_slot_fill(void *);
@@ -88,6 +90,10 @@ struct midiops sock_midiops = {
        sock_exit
 };
 
+struct ctlops sock_ctlops = {
+       sock_exit
+};
+
 struct sock *sock_list = NULL;
 unsigned int sock_sesrefs = 0;         /* connections to the session */
 uint8_t sock_sescookie[AMSG_COOKIELEN];        /* owner of the session */
@@ -103,7 +109,10 @@ sock_log(struct sock *f)
                slot_log(f->slot);
        else if (f->midi)
                midi_log(f->midi);
-       else
+       else if (f->ctlslot) {
+               log_puts("ctlslot");
+               log_putu(f->ctlslot - f->ctlslot->dev->ctlslot);
+       } else
                log_puts("sock");
 #ifdef DEBUG
        if (log_level >= 3) {
@@ -150,6 +159,11 @@ sock_close(struct sock *f)
                port_unref(f->port);
                f->port = NULL;
        }
+       if (f->ctlslot) {
+               ctlslot_del(f->ctlslot);
+               f->ctlslot = NULL;
+               xfree(f->ctldesc);
+       }
        file_del(f->file);
        close(f->fd);
        file_slowaccept = 0;
@@ -277,6 +291,7 @@ sock_new(int fd)
        f->slot = NULL;
        f->port = NULL;
        f->midi = NULL;
+       f->ctlslot = NULL;
        f->tickpending = 0;
        f->fillpending = 0;
        f->stoppending = 0;
@@ -286,6 +301,8 @@ sock_new(int fd)
        f->rtodo = sizeof(struct amsg);
        f->wmax = f->rmax = 0;
        f->lastvol = -1;
+       f->ctlops = 0;
+       f->ctlsyncpending = 0;
        f->file = file_new(&sock_fileops, f, "sock", 1);
        f->fd = fd;
        if (f->file == NULL) {
@@ -547,6 +564,11 @@ sock_wdata(struct sock *f)
                        data = abuf_rgetblk(&f->slot->sub.buf, &count);
                else if (f->midi)
                        data = abuf_rgetblk(&f->midi->obuf, &count);
+               else {
+                       data = (unsigned char *)f->ctldesc +
+                           (f->wsize - f->wtodo);
+                       count = f->wtodo;
+               }
                if (count > f->wtodo)
                        count = f->wtodo;
                n = sock_fdwrite(f, data, count);
@@ -800,6 +822,9 @@ sock_hello(struct sock *f)
        case MODE_REC:
        case MODE_PLAY:
        case MODE_PLAY | MODE_REC:
+       case MODE_CTLREAD:
+       case MODE_CTLWRITE:
+       case MODE_CTLREAD | MODE_CTLWRITE:
                break;
        default:
 #ifdef DEBUG
@@ -837,6 +862,31 @@ sock_hello(struct sock *f)
                        return 0;
                return 1;
        }
+       if (mode & MODE_CTLMASK) {
+               d = dev_bynum(p->devnum);
+               if (d == NULL) {
+                       if (log_level >= 2) {
+                               sock_log(f);
+                               log_puts(": ");
+                               log_putu(p->devnum);
+                               log_puts(": no such device\n");
+                       }
+                       return 0;
+               }
+               f->ctlslot = ctlslot_new(d, &sock_ctlops, f);
+               if (f->ctlslot == NULL) {
+                       if (log_level >= 2) {
+                               sock_log(f);
+                               log_puts(": couldn't get slot\n");
+                       }
+                       return 0;
+               }
+               f->ctldesc = xmalloc(SOCK_CTLDESC_SIZE *
+                   sizeof(struct amsg_ctl_desc));
+               f->ctlops = 0;
+               f->ctlsyncpending = 0;
+               return 1;
+       }
        d = dev_bynum(p->devnum);
        if (d == NULL)
                return 0;
@@ -856,6 +906,7 @@ sock_hello(struct sock *f)
 int
 sock_execmsg(struct sock *f)
 {
+       struct ctl *c;
        struct slot *s = f->slot;
        struct amsg *m = &f->rmsg;
        unsigned char *data;
@@ -1153,6 +1204,81 @@ sock_execmsg(struct sock *f)
                f->lastvol = ctl; /* dont trigger feedback message */
                slot_setvol(s, ctl);
                dev_midi_vol(s->dev, s);
+               dev_onval(s->dev,
+                   CTLADDR_SLOT_LEVEL(f->slot - s->dev->slot), ctl);
+               break;
+       case AMSG_CTLSUB:
+#ifdef DEBUG
+               if (log_level >= 3) {
+                       sock_log(f);
+                       log_puts(": CTLSUB message, desc = ");
+                       log_putx(m->u.ctlsub.desc);
+                       log_puts(", val = ");
+                       log_putx(m->u.ctlsub.val);
+                       log_puts("\n");
+               }
+#endif
+               if (f->pstate != SOCK_INIT || f->ctlslot == NULL) {
+#ifdef DEBUG
+                       if (log_level >= 1) {
+                               sock_log(f);
+                               log_puts(": CTLSUB, wrong state\n");
+                       }
+#endif
+                       sock_close(f);
+                       return 0;
+               }
+               if (m->u.ctlsub.desc) {
+                       if (!(f->ctlops & SOCK_CTLDESC)) {
+                               ctl = f->ctlslot->mask;
+                               c = f->ctlslot->dev->ctl_list;
+                               while (c != NULL) {
+                                       c->desc_mask |= ctl;
+                                       c = c->next;
+                               }
+                       }
+                       f->ctlops |= SOCK_CTLDESC;
+                       f->ctlsyncpending = 1;
+               } else
+                       f->ctlops &= ~SOCK_CTLDESC;
+               if (m->u.ctlsub.val) {
+                       f->ctlops |= SOCK_CTLVAL;
+               } else
+                       f->ctlops &= ~SOCK_CTLVAL;
+               f->rstate = SOCK_RMSG;
+               f->rtodo = sizeof(struct amsg);
+               break;
+       case AMSG_CTLSET:
+#ifdef DEBUG
+               if (log_level >= 3) {
+                       sock_log(f);
+                       log_puts(": CTLSET message\n");
+               }
+#endif
+               if (f->pstate < SOCK_INIT || f->ctlslot == NULL) {
+#ifdef DEBUG
+                       if (log_level >= 1) {
+                               sock_log(f);
+                               log_puts(": CTLSET, wrong state\n");
+                       }
+#endif
+                       sock_close(f);
+                       return 0;
+               }
+               if (!dev_setctl(f->ctlslot->dev,
+                       ntohs(m->u.ctlset.addr),
+                       ntohs(m->u.ctlset.val))) {
+#ifdef DEBUG
+                       if (log_level >= 1) {
+                               sock_log(f);
+                               log_puts(": CTLSET, wrong addr/val\n");
+                       }
+#endif
+                       sock_close(f);
+                       return 0;
+               }
+               f->rtodo = sizeof(struct amsg);
+               f->rstate = SOCK_RMSG;
                break;
        case AMSG_AUTH:
 #ifdef DEBUG
@@ -1241,7 +1367,9 @@ sock_execmsg(struct sock *f)
 int
 sock_buildmsg(struct sock *f)
 {
-       unsigned int size;
+       unsigned int size, mask;
+       struct amsg_ctl_desc *desc;
+       struct ctl *c, **pc;
 
        /*
         * If pos changed (or initial tick), build a MOVE message.
@@ -1378,6 +1506,105 @@ sock_buildmsg(struct sock *f)
                f->wmsg.cmd = htonl(AMSG_STOP);
                f->wtodo = sizeof(struct amsg);
                f->wstate = SOCK_WMSG;
+               return 1;
+       }
+
+       /*
+        * XXX: add a flag indicating if there are changes
+        * in controls not seen by this client, rather
+        * than walking through the full list of control
+        * searching for the {desc,val}_mask bits
+        */
+       if (f->ctlslot && (f->ctlops & SOCK_CTLDESC)) {
+               desc = f->ctldesc;
+               mask = f->ctlslot->mask;
+               size = 0;
+               pc = &f->ctlslot->dev->ctl_list;
+               while ((c = *pc) != NULL) {
+                       if ((c->desc_mask & mask) == 0 ||
+                           (c->refs_mask & mask) == 0) {
+                               pc = &c->next;
+                               continue;
+                       }
+                       if (size == SOCK_CTLDESC_SIZE *
+                               sizeof(struct amsg_ctl_desc))
+                               break;
+                       c->desc_mask &= ~mask;
+                       c->val_mask &= ~mask;
+                       strlcpy(desc->group, c->group,
+                           AMSG_CTL_NAMEMAX);
+                       strlcpy(desc->node0.name, c->node0.name,
+                           AMSG_CTL_NAMEMAX);
+                       desc->node0.unit = ntohs(c->node0.unit);
+                       strlcpy(desc->node1.name, c->node1.name,
+                           AMSG_CTL_NAMEMAX);
+                       desc->node1.unit = ntohs(c->node1.unit);
+                       desc->type = c->type;
+                       strlcpy(desc->func, c->func, AMSG_CTL_NAMEMAX);
+                       desc->addr = htons(c->addr);
+                       desc->curval = htons(c->curval);
+                       size += sizeof(struct amsg_ctl_desc);
+                       desc++;
+
+                       /* if this is a deleted entry unref it */
+                       if (c->type == CTL_NONE) {
+                               c->refs_mask &= ~mask;
+                               if (c->refs_mask == 0) {
+                                       *pc = c->next;
+                                       xfree(c);
+                                       continue;
+                               }
+                       }
+
+                       pc = &c->next;
+               }
+               if (size > 0) {
+                       AMSG_INIT(&f->wmsg);
+                       f->wmsg.cmd = htonl(AMSG_DATA);
+                       f->wmsg.u.data.size = htonl(size);
+                       f->wtodo = sizeof(struct amsg);
+                       f->wstate = SOCK_WMSG;
+#ifdef DEBUG
+                       if (log_level >= 3) {
+                               sock_log(f);
+                               log_puts(": building control DATA message\n");
+                       }
+#endif
+                       return 1;
+               }
+       }
+       if (f->ctlslot && (f->ctlops & SOCK_CTLVAL)) {
+               mask = f->ctlslot->mask;
+               for (c = f->ctlslot->dev->ctl_list; c != NULL; c = c->next) {
+                       if ((c->val_mask & mask) == 0)
+                               continue;
+                       c->val_mask &= ~mask;
+                       AMSG_INIT(&f->wmsg);
+                       f->wmsg.cmd = htonl(AMSG_CTLSET);
+                       f->wmsg.u.ctlset.addr = htons(c->addr);
+                       f->wmsg.u.ctlset.val = htons(c->curval);
+                       f->wtodo = sizeof(struct amsg);
+                       f->wstate = SOCK_WMSG;
+#ifdef DEBUG
+                       if (log_level >= 3) {
+                               sock_log(f);
+                               log_puts(": building CTLSET message\n");
+                       }
+#endif
+                       return 1;
+               }
+       }
+       if (f->ctlslot && f->ctlsyncpending) {
+               f->ctlsyncpending = 0;
+               f->wmsg.cmd = htonl(AMSG_CTLSYNC);
+               f->wtodo = sizeof(struct amsg);
+               f->wstate = SOCK_WMSG;
+#ifdef DEBUG
+               if (log_level >= 3) {
+                       sock_log(f);
+                       log_puts(": building CTLSYNC message\n");
+               }
+#endif
                return 1;
        }
 #ifdef DEBUG
Index: usr.bin/sndiod/sock.h
===================================================================
RCS file: /cvs/src/usr.bin/sndiod/sock.h,v
retrieving revision 1.5
diff -u -p -u -p -r1.5 sock.h
--- usr.bin/sndiod/sock.h       26 Jun 2018 07:13:54 -0000      1.5
+++ usr.bin/sndiod/sock.h       8 Feb 2020 14:49:39 -0000
@@ -58,6 +58,12 @@ struct sock {
        struct slot *slot;              /* audio device slot number */
        struct midi *midi;              /* midi endpoint */
        struct port *port;              /* midi port */
+       struct ctlslot *ctlslot;
+       struct amsg_ctl_desc *ctldesc;  /* temporary buffer */
+#define SOCK_CTLDESC   1               /* dump desc and send changes */
+#define SOCK_CTLVAL    2               /* send value changes */
+       unsigned int ctlops;            /* bitmap of above */
+       int ctlsyncpending;             /* CTLSYNC waiting to be transmitted */
 };
 
 struct sock *sock_new(int fd);

Reply via email to