/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2024-2025 The FreeBSD Foundation
 *
 * This software was developed by Christos Margiolis <christos@FreeBSD.org>
 * under sponsorship from the FreeBSD Foundation.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/nv.h>
#include <sys/queue.h>
#include <sys/sbuf.h>
#include <sys/sndstat.h>
#include <sys/soundcard.h>
#include <sys/sysctl.h>

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <limits.h>
#include <mixer.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <libxo/xo.h>

#define SNDCTL_XO_VERSION	"1"

/* Taken from sys/dev/sound/pcm/ */
#define STATUS_LEN	64
#define FMTSTR_LEN	16

struct snd_chan {
	char name[NAME_MAX];
	char parentchan[NAME_MAX];
	int unit;
#define INPUT	0
#define OUTPUT	1
	int direction;
	char caps[BUFSIZ];
	int latency;
	int rate;
	char format[FMTSTR_LEN];
	int pid;
	char proc[NAME_MAX];
	int interrupts;
	int xruns;
	int feedcount;
	int volume;
	struct {
		char format[FMTSTR_LEN];
		int rate;
		int size_bytes;
		int size_frames;
		int blksz;
		int blkcnt;
		int free;
		int ready;
	} hwbuf, swbuf;
	char feederchain[BUFSIZ];
	struct snd_dev *dev;
	TAILQ_ENTRY(snd_chan) next;
};

struct snd_dev {
	char name[NAME_MAX];
	char desc[NAME_MAX];
	char status[BUFSIZ];
	char devnode[NAME_MAX];
	int from_user;
	int unit;
	char caps[BUFSIZ];
	int bitperfect;
	int realtime;
	int autoconv;
	int eq;
	char eq_preamp[BUFSIZ];
	struct {
		char format[FMTSTR_LEN];
		int rate;
		int pchans;
		int vchans;
		int min_rate;
		int max_rate;
		int min_chans;
		int max_chans;
		char formats[BUFSIZ];
	} play, rec;
	TAILQ_HEAD(, snd_chan) chans;
};

struct snd_ctl {
	const char *name;
	size_t off;
#define STR	0
#define NUM	1
#define VOL	2
#define GRP	3
	int type;
	int (*mod)(struct snd_dev *, void *);
};

struct map {
	int val;
	const char *str;
};

static int mod_bitperfect(struct snd_dev *, void *);
static int mod_autoconv(struct snd_dev *, void *);
static int mod_realtime(struct snd_dev *, void *);
static int mod_eq(struct snd_dev *, void *);
static int mod_eq_preamp(struct snd_dev *, void *);
static int mod_play_vchans(struct snd_dev *, void *);
static int mod_play_rate(struct snd_dev *, void *);
static int mod_play_format(struct snd_dev *, void *);
static int mod_rec_vchans(struct snd_dev *, void *);
static int mod_rec_rate(struct snd_dev *, void *);
static int mod_rec_format(struct snd_dev *, void *);

static struct snd_ctl dev_ctls[] = {
#define F(member)	offsetof(struct snd_dev, member)
	{ "name",		F(name),		STR,	NULL },
	{ "desc",		F(desc),		STR,	NULL },
	{ "status",		F(status),		STR,	NULL },
	{ "devnode",		F(devnode),		STR,	NULL },
	{ "from_user",		F(from_user),		NUM,	NULL },
	{ "unit",		F(unit),		NUM,	NULL },
	{ "caps",		F(caps),		STR,	NULL },
	{ "bitperfect",		F(bitperfect),		NUM,	mod_bitperfect },
	{ "autoconv",		F(autoconv),		NUM,	mod_autoconv },
	{ "realtime",		F(realtime),		NUM,	mod_realtime },
	{ "eq",			F(eq),			NUM,	mod_eq },
	{ "eq_preamp",		F(eq_preamp),		STR,	mod_eq_preamp },
	{ "play",		F(play),		GRP,	NULL },
	{ "play.format",	F(play.format),		STR,	mod_play_format },
	{ "play.rate",		F(play.rate),		NUM,	mod_play_rate },
	/*{ "play.pchans",	F(play.pchans),		NUM,	NULL },*/
	{ "play.vchans",	F(play.vchans),		NUM,	mod_play_vchans },
	{ "play.min_rate",	F(play.min_rate),	NUM,	NULL },
	{ "play.max_rate",	F(play.max_rate),	NUM,	NULL },
	{ "play.min_chans",	F(play.min_chans),	NUM,	NULL },
	{ "play.max_chans",	F(play.max_chans),	NUM,	NULL },
	{ "play.formats",	F(play.formats),	STR,	NULL },
	{ "rec",		F(rec),			GRP,	NULL },
	{ "rec.rate",		F(rec.rate),		NUM,	mod_rec_rate },
	{ "rec.format",		F(rec.format),		STR,	mod_rec_format },
	/*{ "rec.pchans",		F(rec.pchans),		NUM,	NULL },*/
	{ "rec.vchans",		F(rec.vchans),		NUM,	mod_rec_vchans },
	{ "rec.min_rate",	F(rec.min_rate),	NUM,	NULL },
	{ "rec.max_rate",	F(rec.max_rate),	NUM,	NULL },
	{ "rec.min_chans",	F(rec.min_chans),	NUM,	NULL },
	{ "rec.max_chans",	F(rec.max_chans),	NUM,	NULL },
	{ "rec.formats",	F(rec.formats),		STR,	NULL },
	{ NULL,			0,			0,	NULL }
#undef F
};

static struct snd_ctl chan_ctls[] = {
#define F(member)	offsetof(struct snd_chan, member)
	{ "name",		F(name),		STR,	NULL },
	{ "parentchan",		F(parentchan),		STR,	NULL },
	{ "unit",		F(unit),		NUM,	NULL },
	{ "caps",		F(caps),		STR,	NULL },
	{ "latency",		F(latency),		NUM,	NULL },
	{ "rate",		F(rate),		NUM,	NULL },
	{ "format",		F(format),		STR,	NULL },
	{ "pid",		F(pid),			NUM,	NULL },
	{ "proc",		F(proc),		STR,	NULL },
	{ "interrupts",		F(interrupts),		NUM,	NULL },
	{ "xruns",		F(xruns),		NUM,	NULL },
	{ "feedcount",		F(feedcount),		NUM,	NULL },
	{ "volume",		F(volume),		VOL,	NULL },
	{ "hwbuf",		F(hwbuf),		GRP,	NULL },
	{ "hwbuf.format",	F(hwbuf.format),	STR,	NULL },
	{ "hwbuf.rate",		F(hwbuf.rate),		NUM,	NULL },
	{ "hwbuf.size_bytes",	F(hwbuf.size_bytes),	NUM,	NULL },
	{ "hwbuf.size_frames",	F(hwbuf.size_frames),	NUM,	NULL },
	{ "hwbuf.blksz",	F(hwbuf.blksz),		NUM,	NULL },
	{ "hwbuf.blkcnt",	F(hwbuf.blkcnt),	NUM,	NULL },
	{ "hwbuf.free",		F(hwbuf.free),		NUM,	NULL },
	{ "hwbuf.ready",	F(hwbuf.ready),		NUM,	NULL },
	{ "swbuf",		F(swbuf),		GRP,	NULL },
	{ "swbuf.format",	F(swbuf.format),	STR,	NULL },
	{ "swbuf.rate",		F(swbuf.rate),		NUM,	NULL },
	{ "swbuf.size_bytes",	F(swbuf.size_bytes),	NUM,	NULL },
	{ "swbuf.size_frames",	F(swbuf.size_frames),	NUM,	NULL },
	{ "swbuf.blksz",	F(swbuf.blksz),		NUM,	NULL },
	{ "swbuf.blkcnt",	F(swbuf.blkcnt),	NUM,	NULL },
	{ "swbuf.free",		F(swbuf.free),		NUM,	NULL },
	{ "swbuf.ready",	F(swbuf.ready),		NUM,	NULL },
	{ "feederchain",	F(feederchain),		STR,	NULL },
	{ NULL,			0,			0,	NULL }
#undef F
};

/*
 * Taken from the OSSv4 manual. Not all of them are supported on FreeBSD
 * however, and some of them are obsolete.
 */
static struct map capmap[] = {
	{ PCM_CAP_ANALOGIN,	"ANALOGIN" },
	{ PCM_CAP_ANALOGOUT,	"ANALOGOUT" },
	{ PCM_CAP_BATCH,	"BATCH" },
	{ PCM_CAP_BIND,		"BIND" },
	{ PCM_CAP_COPROC,	"COPROC" },
	{ PCM_CAP_DEFAULT,	"DEFAULT" },
	{ PCM_CAP_DIGITALIN,	"DIGITALIN" },
	{ PCM_CAP_DIGITALOUT,	"DIGITALOUT" },
	{ PCM_CAP_DUPLEX,	"DUPLEX" },
	{ PCM_CAP_FREERATE,	"FREERATE" },
	{ PCM_CAP_HIDDEN,	"HIDDEN" },
	{ PCM_CAP_INPUT,	"INPUT" },
	{ PCM_CAP_MMAP,		"MMAP" },
	{ PCM_CAP_MODEM,	"MODEM" },
	{ PCM_CAP_MULTI,	"MULTI" },
	{ PCM_CAP_OUTPUT,	"OUTPUT" },
	{ PCM_CAP_REALTIME,	"REALTIME" },
	{ PCM_CAP_REVISION,	"REVISION" },
	{ PCM_CAP_SHADOW,	"SHADOW" },
	{ PCM_CAP_SPECIAL,	"SPECIAL" },
	{ PCM_CAP_TRIGGER,	"TRIGGER" },
	{ PCM_CAP_VIRTUAL,	"VIRTUAL" },
	{ 0,			NULL }
};

static struct map fmtmap[] = {
	{ AFMT_A_LAW,		"alaw" },
	{ AFMT_MU_LAW,		"mulaw" },
	{ AFMT_S8,		"s8" },
	{ AFMT_U8,		"u8" },
	{ AFMT_AC3,		"ac3" },
	{ AFMT_S16_LE,		"s16le" },
	{ AFMT_S16_BE,		"s16be" },
	{ AFMT_U16_LE,		"u16le" },
	{ AFMT_U16_BE,		"u16be" },
	{ AFMT_S24_LE,		"s24le" },
	{ AFMT_S24_BE,		"s24be" },
	{ AFMT_U24_LE,		"u24le" },
	{ AFMT_U24_BE,		"u24be" },
	{ AFMT_S32_LE,		"s32le" },
	{ AFMT_S32_BE,		"s32be" },
	{ AFMT_U32_LE,		"u32le" },
	{ AFMT_U32_BE,		"u32be" },
	{ AFMT_F32_LE,		"f32le" },
	{ AFMT_F32_BE,		"f32be" },
	{ 0,			NULL }
};

static bool oflag = false;
static bool vflag = false;

static void
cap2str(char *buf, size_t size, int caps)
{
	struct map *p;

	for (p = capmap; p->str != NULL; p++) {
		if ((p->val & caps) == 0)
			continue;
		strlcat(buf, p->str, size);
		strlcat(buf, ",", size);
	}
	if (*buf == '\0')
		strlcpy(buf, "UNKNOWN", size);
	else
		buf[strlen(buf) - 1] = '\0';
}

static void
fmt2str(char *buf, size_t size, int fmt)
{
	struct map *p;
	int enc, ch, ext;

	enc = fmt & 0xf00fffff;
	ch = (fmt & 0x07f00000) >> 20;
	ext = (fmt & 0x08000000) >> 27;

	for (p = fmtmap; p->str != NULL; p++) {
		if ((p->val & enc) == 0)
			continue;
		strlcat(buf, p->str, size);
		if (ch) {
			snprintf(buf + strlen(buf), size,
			    ":%d.%d", ch - ext, ext);
		}
		strlcat(buf, ",", size);
	}
	if (*buf == '\0')
		strlcpy(buf, "UNKNOWN", size);
	else
		buf[strlen(buf) - 1] = '\0';
}

static int
bytes2frames(int bytes, int fmt)
{
	int enc, ch, samplesz;

	enc = fmt & 0xf00fffff;
	ch = (fmt & 0x07f00000) >> 20;
	/* Add the channel extension if present (e.g 2.1). */
	ch += (fmt & 0x08000000) >> 27;

	if (enc & (AFMT_S8 | AFMT_U8 | AFMT_MU_LAW | AFMT_A_LAW))
		samplesz = 1;
	else if (enc & (AFMT_S16_NE | AFMT_U16_NE))
		samplesz = 2;
	else if (enc & (AFMT_S24_NE | AFMT_U24_NE))
		samplesz = 3;
	else if (enc & (AFMT_S32_NE | AFMT_U32_NE | AFMT_F32_NE))
		samplesz = 4;
	else
		samplesz = 0;

	if (!samplesz || !ch)
		return (-1);

	return (bytes / (samplesz * ch));
}

static int
sysctl_int(const char *buf, const char *arg, int *var)
{
	size_t size;
	int n, prev;

	size = sizeof(int);
	/* Read current value. */
	if (sysctlbyname(buf, &prev, &size, NULL, 0) < 0) {
		xo_warn("sysctlbyname(%s)", buf);
		return (-1);
	}

	/* Read-only. */
	if (arg != NULL) {
		errno = 0;
		n = strtol(arg, NULL, 10);
		if (errno == EINVAL || errno == ERANGE) {
			xo_warn("strtol(%s)", arg);
			return (-1);
		}

		/* Apply new value. */
		if (sysctlbyname(buf, NULL, 0, &n, size) < 0) {
			xo_warn("sysctlbyname(%s, %d)", buf, n);
			return (-1);
		}
	}

	/* Read back applied value for good measure. */
	if (sysctlbyname(buf, &n, &size, NULL, 0) < 0) {
		xo_warn("sysctlbyname(%s)", buf);
		return (-1);
	}

	if (arg != NULL && xo_get_style(NULL) == XO_STYLE_TEXT)
		printf("%s: %d -> %d\n", buf, prev, n);
	if (var != NULL)
		*var = n;

	return (0);
}

static int
sysctl_str(const char *buf, const char *arg, char *var, size_t varsz)
{
	size_t size;
	char prev[BUFSIZ];
	char *tmp;

	/* Read current value. */
	size = sizeof(prev);
	if (sysctlbyname(buf, prev, &size, NULL, 0) < 0) {
		xo_warn("sysctlbyname(%s)", buf);
		return (-1);
	}

	/* Read-only. */
	if (arg != NULL) {
		size = strlen(arg);
		/* Apply new value. */
		if (sysctlbyname(buf, NULL, 0, arg, size) < 0) {
			xo_warn("sysctlbyname(%s, %s)", buf, arg);
			return (-1);
		}
		/* Get size of new string. */
		if (sysctlbyname(buf, NULL, &size, NULL, 0) < 0) {
			xo_warn("sysctlbyname(%s)", buf);
			return (-1);
		}
	}

	if ((tmp = calloc(1, size)) == NULL)
		xo_err(1, "calloc");
	/* Read back applied value for good measure. */
	if (sysctlbyname(buf, tmp, &size, NULL, 0) < 0) {
		xo_warn("sysctlbyname(%s)", buf);
		free(tmp);
		return (-1);
	}

	if (arg != NULL && xo_get_style(NULL) == XO_STYLE_TEXT)
		printf("%s: %s -> %s\n", buf, prev, tmp);
	if (var != NULL)
		strlcpy(var, tmp, varsz);
	free(tmp);

	return (0);
}

static struct snd_dev *
read_dev(char *path)
{
	nvlist_t *nvl;
	const nvlist_t * const *di;
	const nvlist_t * const *cdi;
	struct sndstioc_nv_arg arg;
	struct snd_dev *dp = NULL;
	struct snd_chan *ch;
	char buf[64];
	size_t nitems, nchans, i, j;
	int fd, caps, unit, t1, t2, t3;

	if ((fd = open("/dev/sndstat", O_RDONLY)) < 0)
		xo_err(1, "open(/dev/sndstat)");

	if (ioctl(fd, SNDSTIOC_REFRESH_DEVS, NULL) < 0)
		xo_err(1, "ioctl(SNDSTIOC_REFRESH_DEVS)");

	arg.nbytes = 0;
	arg.buf = NULL;
	if (ioctl(fd, SNDSTIOC_GET_DEVS, &arg) < 0)
		xo_err(1, "ioctl(SNDSTIOC_GET_DEVS#1)");

	if ((arg.buf = malloc(arg.nbytes)) == NULL)
		xo_err(1, "malloc");

	if (ioctl(fd, SNDSTIOC_GET_DEVS, &arg) < 0)
		xo_err(1, "ioctl(SNDSTIOC_GET_DEVS#2)");

	if ((nvl = nvlist_unpack(arg.buf, arg.nbytes, 0)) == NULL)
		xo_err(1, "nvlist_unpack");

	if (nvlist_empty(nvl) || !nvlist_exists(nvl, SNDST_DSPS))
		xo_errx(1, "no soundcards attached");

	if (path == NULL || (path != NULL && strcmp(basename(path), "dsp") == 0))
		unit = mixer_get_dunit();
	else
		unit = -1;

	/* Find whether the requested device exists */
	di = nvlist_get_nvlist_array(nvl, SNDST_DSPS, &nitems);
	for (i = 0; i < nitems; i++) {
		if (unit == -1 && strcmp(basename(path),
		    nvlist_get_string(di[i], SNDST_DSPS_DEVNODE)) == 0)
			break;
		else if (nvlist_exists(di[i], SNDST_DSPS_PROVIDER_INFO) &&
		    (int)nvlist_get_number(nvlist_get_nvlist(di[i],
		    SNDST_DSPS_PROVIDER_INFO), SNDST_DSPS_SOUND4_UNIT) == unit)
			break;;
	}
	if (i == nitems)
		xo_errx(1, "device not found");

#define NV(type, item)	\
	nvlist_get_ ## type (di[i], SNDST_DSPS_ ## item)
	if ((dp = calloc(1, sizeof(struct snd_dev))) == NULL)
		xo_err(1, "calloc");

	dp->unit = -1;
	strlcpy(dp->name, NV(string, NAMEUNIT), sizeof(dp->name));
	strlcpy(dp->desc, NV(string, DESC), sizeof(dp->desc));
	strlcpy(dp->devnode, NV(string, DEVNODE), sizeof(dp->devnode));
	dp->from_user = NV(bool, FROM_USER);
	dp->play.pchans = NV(number, PCHAN);
	dp->rec.pchans = NV(number, RCHAN);
#undef NV

	if (dp->play.pchans && !nvlist_exists(di[i], SNDST_DSPS_INFO_PLAY))
		xo_errx(1, "%s: playback channel list empty", dp->name);
	if (dp->rec.pchans && !nvlist_exists(di[i], SNDST_DSPS_INFO_REC))
		xo_errx(1, "%s: recording channel list empty", dp->name);

#define NV(type, mode, item)						\
	nvlist_get_ ## type (nvlist_get_nvlist(di[i],			\
	    SNDST_DSPS_INFO_ ## mode), SNDST_DSPS_INFO_ ## item)
	if (dp->play.pchans) {
		dp->play.min_rate = NV(number, PLAY, MIN_RATE);
		dp->play.max_rate = NV(number, PLAY, MAX_RATE);
		dp->play.min_chans = NV(number, PLAY, MIN_CHN);
		dp->play.max_chans = NV(number, PLAY, MAX_CHN);
		fmt2str(dp->play.formats, sizeof(dp->play.formats),
		    NV(number, PLAY, FORMATS));
	}
	if (dp->rec.pchans) {
		dp->rec.min_rate = NV(number, REC, MIN_RATE);
		dp->rec.max_rate = NV(number, REC, MAX_RATE);
		dp->rec.min_chans = NV(number, REC, MIN_CHN);
		dp->rec.max_chans = NV(number, REC, MAX_CHN);
		fmt2str(dp->rec.formats, sizeof(dp->rec.formats),
		    NV(number, REC, FORMATS));
	}
#undef NV

	/*
	 * Skip further parsing if the provider is not sound(4), as the
	 * following code is sound(4)-specific.
	 */
	if (strcmp(nvlist_get_string(di[i], SNDST_DSPS_PROVIDER),
	    SNDST_DSPS_SOUND4_PROVIDER) != 0)
		goto done;

	if (!nvlist_exists(di[i], SNDST_DSPS_PROVIDER_INFO))
		xo_errx(1, "%s: provider_info list empty", dp->name);

#define NV(type, item)							\
	nvlist_get_ ## type (nvlist_get_nvlist(di[i],			\
	    SNDST_DSPS_PROVIDER_INFO), SNDST_DSPS_SOUND4_ ## item)
	strlcpy(dp->status, NV(string, STATUS), sizeof(dp->status));
	dp->unit = NV(number, UNIT);
	dp->bitperfect = NV(bool, BITPERFECT);
	dp->play.vchans = NV(bool, PVCHAN);
	dp->play.rate = NV(number, PVCHANRATE);
	fmt2str(dp->play.format, sizeof(dp->play.format),
	    NV(number, PVCHANFORMAT));
	dp->rec.vchans = NV(bool, RVCHAN);
	dp->rec.rate = NV(number, RVCHANRATE);
	fmt2str(dp->rec.format, sizeof(dp->rec.format),
	    NV(number, RVCHANFORMAT));
#undef NV

	dp->autoconv = (dp->play.vchans || dp->rec.vchans) && !dp->bitperfect;

	if (sysctl_int("hw.snd.latency", NULL, &t1) ||
	    sysctl_int("hw.snd.latency_profile", NULL, &t2) ||
	    sysctl_int("kern.timecounter.alloweddeviation", NULL, &t3))
		xo_err(1, "%s: sysctl", dp->name);
	if (t1 == 0 && t2 == 0 && t3 == 0)
		dp->realtime = 1;

	snprintf(buf, sizeof(buf), "dev.pcm.%d.eq", dp->unit);
	if (sysctl_int(buf, NULL, &dp->eq))
		xo_err(1, "%s: sysctl", dp->name);

	snprintf(buf, sizeof(buf), "dev.pcm.%d.eq_preamp", dp->unit);
	if (sysctl_str(buf, NULL, dp->eq_preamp, sizeof(dp->eq_preamp)))
		xo_err(1, "%s: sysctl", dp->name);

	if (!nvlist_exists(nvlist_get_nvlist(di[i],
	    SNDST_DSPS_PROVIDER_INFO), SNDST_DSPS_SOUND4_CHAN_INFO))
		xo_errx(1, "%s: channel info list empty", dp->name);

	cdi = nvlist_get_nvlist_array(
	    nvlist_get_nvlist(di[i], SNDST_DSPS_PROVIDER_INFO),
	    SNDST_DSPS_SOUND4_CHAN_INFO, &nchans);

	TAILQ_INIT(&dp->chans);
	caps = 0;
	for (j = 0; j < nchans; j++) {
#define NV(type, item)	\
	nvlist_get_ ## type (cdi[j], SNDST_DSPS_SOUND4_CHAN_ ## item)
		if ((ch = calloc(1, sizeof(struct snd_chan))) == NULL)
			xo_err(1, "calloc");

		strlcpy(ch->name, NV(string, NAME), sizeof(ch->name));
		strlcpy(ch->parentchan, NV(string, PARENTCHAN),
		    sizeof(ch->parentchan));
		ch->unit = NV(number, UNIT);
		ch->direction = (NV(number, CAPS) & PCM_CAP_INPUT) ?
		    INPUT : OUTPUT;
		cap2str(ch->caps, sizeof(ch->caps), NV(number, CAPS));
		ch->latency = NV(number, LATENCY);
		ch->rate = NV(number, RATE);
		fmt2str(ch->format, sizeof(ch->format), NV(number, FORMAT));
		ch->pid = NV(number, PID);
		strlcpy(ch->proc, NV(string, COMM), sizeof(ch->proc));
		ch->interrupts = NV(number, INTR);
		ch->xruns = NV(number, XRUNS);
		ch->feedcount = NV(number, FEEDCNT);
		ch->volume = NV(number, LEFTVOL) |
		    NV(number, RIGHTVOL) << 8;
		fmt2str(ch->hwbuf.format, sizeof(ch->hwbuf.format),
		    NV(number, HWBUF_FORMAT));
		ch->hwbuf.rate = NV(number, HWBUF_RATE);
		ch->hwbuf.size_bytes = NV(number, HWBUF_SIZE);
		ch->hwbuf.size_frames =
		    bytes2frames(ch->hwbuf.size_bytes, NV(number, HWBUF_FORMAT));
		ch->hwbuf.blksz = NV(number, HWBUF_BLKSZ);
		ch->hwbuf.blkcnt = NV(number, HWBUF_BLKCNT);
		ch->hwbuf.free = NV(number, HWBUF_FREE);
		ch->hwbuf.ready = NV(number, HWBUF_READY);
		fmt2str(ch->swbuf.format, sizeof(ch->swbuf.format),
		    NV(number, SWBUF_FORMAT));
		ch->swbuf.rate = NV(number, SWBUF_RATE);
		ch->swbuf.size_bytes = NV(number, SWBUF_SIZE);
		ch->swbuf.size_frames =
		    bytes2frames(ch->swbuf.size_bytes, NV(number, SWBUF_FORMAT));
		ch->swbuf.blksz = NV(number, SWBUF_BLKSZ);
		ch->swbuf.blkcnt = NV(number, SWBUF_BLKCNT);
		ch->swbuf.free = NV(number, SWBUF_FREE);
		ch->swbuf.ready = NV(number, SWBUF_READY);
		strlcpy(ch->feederchain, NV(string, FEEDERCHAIN),
		    sizeof(ch->feederchain));
		ch->dev = dp;

		caps |= NV(number, CAPS);
		TAILQ_INSERT_TAIL(&dp->chans, ch, next);

		if (!dp->rec.vchans && ch->direction == INPUT) {
			strlcpy(dp->rec.format, ch->hwbuf.format,
			    sizeof(dp->rec.format));
			dp->rec.rate = ch->hwbuf.rate;
		} else if (!dp->play.vchans && ch->direction == OUTPUT) {
			strlcpy(dp->play.format, ch->hwbuf.format,
			    sizeof(dp->play.format));
			dp->play.rate = ch->hwbuf.rate;
		}
#undef NV
	}
	cap2str(dp->caps, sizeof(dp->caps), caps);

done:
	free(arg.buf);
	nvlist_destroy(nvl);
	close(fd);

	return (dp);
}

static void
free_dev(struct snd_dev *dp)
{
	struct snd_chan *ch;

	while (!TAILQ_EMPTY(&dp->chans)) {
		ch = TAILQ_FIRST(&dp->chans);
		TAILQ_REMOVE(&dp->chans, ch, next);
		free(ch);
	}
	free(dp);
}

static void
print_dev_ctl(struct snd_dev *dp, struct snd_ctl *ctl, bool simple,
    bool showgrp)
{
	struct snd_ctl *cp;
	size_t len;

	if (ctl->type != GRP && xo_get_style(NULL) == XO_STYLE_TEXT) {
		if (simple)
			printf("%s=", ctl->name);
		else
			printf("    %-20s= ", ctl->name);
	}

	switch (ctl->type) {
	case STR:
		xo_emit("{a:%s/%s}\n", ctl->name, (char *)dp + ctl->off);
		break;
	case NUM:
		xo_emit("{a:%s/%d}\n", ctl->name, *(int *)((intptr_t)dp + ctl->off));
		break;
	case VOL:
		break;
	case GRP:
		if (!simple || !showgrp)
			break;
		for (cp = dev_ctls; cp->name != NULL; cp++) {
			len = strlen(ctl->name);
			if (strncmp(ctl->name, cp->name, len) == 0 &&
			    cp->name[len] == '.' && cp->type != GRP)
				print_dev_ctl(dp, cp, simple, showgrp);
		}
		break;
	}
}

static void
print_chan_ctl(struct snd_chan *ch, struct snd_ctl *ctl, bool simple,
    bool showgrp)
{
	struct snd_ctl *cp;
	size_t len;
	int v;

	if (ctl->type != GRP && xo_get_style(NULL) == XO_STYLE_TEXT) {
		if (simple)
			printf("%s.%s=", ch->name, ctl->name);
		else
			printf("        %-20s= ", ctl->name);
	}

	switch (ctl->type) {
	case STR:
		xo_emit("{a:%s/%s}\n", ctl->name, (char *)ch + ctl->off);
		break;
	case NUM:
		xo_emit("{a:%s/%d}\n", ctl->name, *(int *)((intptr_t)ch + ctl->off));
		break;
	case VOL:
		v = *(int *)((intptr_t)ch + ctl->off);
		xo_emit("{a:%s/%.2f:%.2f}\n", ctl->name,
		    MIX_VOLNORM(v & 0x00ff), MIX_VOLNORM((v >> 8) & 0x00ff));
		break;
	case GRP:
		if (!simple || !showgrp)
			break;
		for (cp = chan_ctls; cp->name != NULL; cp++) {
			len = strlen(ctl->name);
			if (strncmp(ctl->name, cp->name, len) == 0 &&
			    cp->name[len] == '.' && cp->type != GRP)
				print_chan_ctl(ch, cp, simple, showgrp);
		}
		break;
	}
}

static void
print_dev(struct snd_dev *dp)
{
	struct snd_chan *ch;
	struct snd_ctl *ctl;
	struct sbuf sb;
	char buf[16];

	xo_open_instance("devices");

	if (!oflag || xo_get_style(NULL) != XO_STYLE_TEXT) {
		sbuf_new(&sb, buf, sizeof(buf), SBUF_FIXEDLEN);

		sbuf_printf(&sb, "(");
		if (dp->play.pchans)
			sbuf_printf(&sb, "play");
		if (dp->play.pchans && dp->rec.pchans)
			sbuf_printf(&sb, "/");
		if (dp->rec.pchans)
			sbuf_printf(&sb, "rec");
		sbuf_printf(&sb, ")");

		xo_emit("{:header/%s: <%s> %s %s}\n",
		    dp->name, dp->desc, dp->status, sbuf_data(&sb));

		sbuf_delete(&sb);
	}

	for (ctl = dev_ctls; ctl->name != NULL; ctl++)
		print_dev_ctl(dp, ctl, oflag, false);

	if (vflag) {
		xo_open_list("channels");
		TAILQ_FOREACH(ch, &dp->chans, next) {
			xo_open_instance("channels");
			if (!oflag && xo_get_style(NULL) == XO_STYLE_TEXT)
				printf("    %s\n", ch->name);
			for (ctl = chan_ctls; ctl->name != NULL; ctl++)
				print_chan_ctl(ch, ctl, oflag, false);
			xo_close_instance("channels");
		}
		xo_close_list("channels");
	}

	xo_close_instance("devices");
}

static int
mod_bitperfect(struct snd_dev *dp, void *arg)
{
	char buf[64];

	if (dp->from_user)
		return (-1);

	snprintf(buf, sizeof(buf), "dev.pcm.%d.bitperfect", dp->unit);

	return (sysctl_int(buf, arg, &dp->bitperfect));
}

static int
mod_autoconv(struct snd_dev *dp, void *arg)
{
	const char *val = arg;
	const char *zero = "0";
	const char *one = "1";
	int rc = -1;

	if (dp->from_user)
		return (rc);

	if (strcmp(val, zero) == 0) {
		rc = mod_play_vchans(dp, __DECONST(char *, zero)) ||
		    mod_rec_vchans(dp, __DECONST(char *, zero)) ||
		    mod_bitperfect(dp, __DECONST(char *, one));
		if (rc == 0)
			dp->autoconv = 0;
	} else if (strcmp(val, one) == 0) {
		rc = mod_play_vchans(dp, __DECONST(char *, one)) ||
		    mod_rec_vchans(dp, __DECONST(char *, one)) ||
		    mod_bitperfect(dp, __DECONST(char *, zero));
		if (rc == 0)
			dp->autoconv = 1;
	}

	return (rc);
}

static int
mod_realtime(struct snd_dev *dp, void *arg)
{
	const char *val = arg;
	int rc = -1;

	if (dp->from_user)
		return (-1);

	if (strcmp(val, "0") == 0) {
		/* TODO */
		rc = sysctl_int("hw.snd.latency", "2", NULL) ||
		    sysctl_int("hw.snd.latency_profile", "1", NULL) ||
		    sysctl_int("kern.timecounter.alloweddeviation", "5", NULL);
		if (rc == 0)
			dp->realtime = 0;
	} else if (strcmp(val, "1") == 0) {
		rc = sysctl_int("hw.snd.latency", "0", NULL) ||
		    sysctl_int("hw.snd.latency_profile", "0", NULL) ||
		    sysctl_int("kern.timecounter.alloweddeviation", "0", NULL);
		if (rc == 0)
			dp->realtime = 1;
	}

	return (rc);
}

static int
mod_eq(struct snd_dev *dp, void *arg)
{
	char buf[64];

	if (dp->from_user)
		return (-1);

	snprintf(buf, sizeof(buf), "dev.pcm.%d.eq", dp->unit);

	return (sysctl_int(buf, arg, &dp->eq));
}

static int
mod_eq_preamp(struct snd_dev *dp, void *arg)
{
	char buf[64];

	if (dp->from_user)
		return (-1);

	snprintf(buf, sizeof(buf), "dev.pcm.%d.eq_preamp", dp->unit);

	return (sysctl_str(buf, arg, dp->eq_preamp, sizeof(dp->eq_preamp)));
}

static int
mod_play_vchans(struct snd_dev *dp, void *arg)
{
	char buf[64];

	if (dp->from_user)
		return (-1);
	if (!dp->play.pchans)
		return (0);

	snprintf(buf, sizeof(buf), "dev.pcm.%d.play.vchans", dp->unit);

	return (sysctl_int(buf, arg, &dp->play.vchans));
}

static int
mod_play_rate(struct snd_dev *dp, void *arg)
{
	char buf[64];

	if (dp->from_user)
		return (-1);
	if (!dp->play.vchans)
		return (0);

	snprintf(buf, sizeof(buf), "dev.pcm.%d.play.vchanrate", dp->unit);

	return (sysctl_int(buf, arg, &dp->play.rate));
}

static int
mod_play_format(struct snd_dev *dp, void *arg)
{
	char buf[64];

	if (dp->from_user)
		return (-1);
	if (!dp->play.vchans)
		return (0);

	snprintf(buf, sizeof(buf), "dev.pcm.%d.play.vchanformat", dp->unit);

	return (sysctl_str(buf, arg, dp->play.format, sizeof(dp->play.format)));
}

static int
mod_rec_vchans(struct snd_dev *dp, void *arg)
{
	char buf[64];

	if (dp->from_user)
		return (-1);
	if (!dp->rec.pchans)
		return (0);

	snprintf(buf, sizeof(buf), "dev.pcm.%d.rec.vchans", dp->unit);

	return (sysctl_int(buf, arg, &dp->rec.vchans));
}

static int
mod_rec_rate(struct snd_dev *dp, void *arg)
{
	char buf[64];

	if (dp->from_user)
		return (-1);
	if (!dp->rec.vchans)
		return (0);

	snprintf(buf, sizeof(buf), "dev.pcm.%d.rec.vchanrate", dp->unit);

	return (sysctl_int(buf, arg, &dp->rec.rate));
}

static int
mod_rec_format(struct snd_dev *dp, void *arg)
{
	char buf[64];

	if (dp->from_user)
		return (-1);
	if (!dp->rec.vchans)
		return (0);

	snprintf(buf, sizeof(buf), "dev.pcm.%d.rec.vchanformat", dp->unit);

	return (sysctl_str(buf, arg, dp->rec.format, sizeof(dp->rec.format)));
}

static void __dead2
usage(void)
{
	xo_error("usage: %s [--libxo] [-f device] [-hov] [control[=value] ...]\n",
	    getprogname());
	xo_finish();
	exit(1);
}

int
main(int argc, char *argv[])
{
	struct snd_dev *dp;
	struct snd_chan *ch;
	struct snd_ctl *ctl;
	char *path = NULL;
	char *s, *propstr;
	bool show = true, found;
	int c;

	argc = xo_parse_args(argc, argv);
	if (argc < 0)
		exit(1);

	while ((c = getopt(argc, argv, "f:hov")) != -1) {
		switch (c) {
		case 'f':
			path = optarg;
			break;
		case 'o':
			oflag = true;
			break;
		case 'v':
			vflag = true;
			break;
		case 'h':	/* FALLTHROUGH */
		case '?':
		default:
			usage();
		}
	}
	argc -= optind;
	argv += optind;

	xo_set_version(SNDCTL_XO_VERSION);
	xo_open_container("sndctl");

	dp = read_dev(path);

	xo_open_container("executed_controls");
	while (argc > 0) {
		if ((s = strdup(*argv)) == NULL) {
			xo_close_container("executed_controls");
			xo_close_container("sndctl");
			if (xo_finish() < 0)
				xo_err(1, "xo_finish");
			xo_err(1, "strdup(%s)", *argv);
		}

		propstr = strsep(&s, "=");
		if (propstr == NULL)
			goto next;

		found = false;
		for (ctl = dev_ctls; ctl->name != NULL; ctl++) {
			if (strcmp(ctl->name, propstr) != 0)
				continue;
			if (s == NULL)
				show = false;
			else if (ctl->mod != NULL && ctl->mod(dp, s) < 0)
				xo_warnx("%s(%s) failed", ctl->name, s);
			if (s == NULL || xo_get_style(NULL) != XO_STYLE_TEXT) {
				/*
				 * Print the control in libxo mode in all
				 * cases, otherwise we'll not be printing any
				 * controls that were modified or whose
				 * ctl->mod() failed.
				 */
				print_dev_ctl(dp, ctl, true, true);
			}
			found = true;
			break;
		}
		TAILQ_FOREACH(ch, &dp->chans, next) {
			for (ctl = chan_ctls; ctl->name != NULL; ctl++) {
				if (strcmp(ctl->name, propstr) != 0)
					continue;
				print_chan_ctl(ch, ctl, true, true);
				show = false;
				found = true;
				break;
			}
		}
		if (!found)
			xo_warnx("%s: no such property", propstr);
next:
		free(s);
		argc--;
		argv++;
	}
	xo_close_container("executed_controls");

	if (show || xo_get_style(NULL) != XO_STYLE_TEXT) {
		xo_open_list("devices");
		print_dev(dp);
		xo_close_list("devices");
	}
	free_dev(dp);


	xo_close_container("sndctl");
	if (xo_finish() < 0)
		xo_err(1, "xo_finish");

	return (0);
}
