diff --git a/usr.sbin/Makefile b/usr.sbin/Makefile --- a/usr.sbin/Makefile +++ b/usr.sbin/Makefile @@ -3,6 +3,7 @@ SUBDIR= adduser \ arp \ + audio \ binmiscctl \ boottrace \ bsdconfig \ diff --git a/usr.sbin/audio/Makefile b/usr.sbin/audio/Makefile new file mode 100644 --- /dev/null +++ b/usr.sbin/audio/Makefile @@ -0,0 +1,8 @@ +.include + +PROG= audio +SRCS= ${PROG}.c +MAN= ${PROG}.8 +LDFLAGS+= -lnv -lmixer + +.include diff --git a/usr.sbin/audio/audio.8 b/usr.sbin/audio/audio.8 new file mode 100644 --- /dev/null +++ b/usr.sbin/audio/audio.8 @@ -0,0 +1,188 @@ +.\"- +.\" SPDX-License-Identifier: BSD-2-Clause +.\" +.\" Copyright (c) 2024 The FreeBSD Foundation +.\" +.\" Portions of this software were developed by Christos Margiolis +.\" 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. +.\" +.Dd August 23, 2024 +.Dt AUDIO 8 +.Os +.Sh NAME +.Nm audio +.Nd list and modify soundcard properties +.Sh SYNOPSIS +.Nm +.Op Fl f Ar device +.Op Fl hov +.Op Ar control Ns Oo = Ns Ar value Oc Ar ... +.Sh DESCRIPTION +The +.Nm +utility is used to set and display sound card properties, using a +control-driven interface, in order to filter and/or set specific properties. +.Pp +The options are as follows: +.Bl -tag -width "-f device" +.It Fl f Ar device +Choose a specific audio device +.Pq see Sx FILES . +Userland devices (e.g those registered by +.Xr virtual_oss 8 +can also be selected. +.It Fl h +Print a help message. +.It Fl o +Print values in a format suitable for use inside scripts. +.It Fl v +Run in verbose mode. +This option will print all of the device's channel properties. +.El +.Pp +The device controls are as follows: +.Bl -column xxxxxxxxxxxxxxx xxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxx -offset indent +.It Sy Name Ta Sy Type Ta Sy Read/Write Ta Sy Action +.It name Ta String Ta Read Ta Device name +.It desc Ta String Ta Read Ta Device description +.It devnode Ta String Ta Read Ta Device node +.It status Ta String Ta Read Ta Device status +.It unit Ta Number Ta Read Ta Device unit +.It caps Ta String Ta Read Ta Device OSS capabitilies +.It from_user Ta Boolean Ta Read Ta Userland device +.It bitperfect Ta Boolean Ta Read/Write Ta Bit-perfect mode enabled +.It realtime Ta Boolean Ta Read/Write Ta Real-time mode enabled +.It play Ta Group Ta Read Ta Playback properties +.It play.rate Ta Number Ta Read/Write Ta Playback sample rate +.It play.format Ta String Ta Read/Write Ta Playback format +.It play.autoconv Ta Boolean Ta Read/Write Ta Playback auto-conversions enabled +.It play.hwchans Ta Number Ta Read Ta Number of hardware playback channels +.It play.vchans Ta Number Ta Read/Write Ta Number of playback VCHANs (virtual channels) +.It play.min_rate Ta Number Ta Read Ta Minimum playback sample rate +.It play.max_rate Ta Number Ta Read Ta Maximum playback sample rate +.It play.min_chans Ta Number Ta Read Ta Minimum playback sample channels +.It play.max_chans Ta Number Ta Read Ta Maximum playback sample channels +.It play.formats Ta String Ta Read Ta Playback formats +.It rec Ta Group Ta Read Ta Recording properties +.It rec.rate Ta Number Ta Read/Write Ta Recording sample rate +.It rec.format Ta String Ta Read/Write Ta Recording format +.It rec.autoconv Ta Boolean Ta Read/Write Ta Recording auto-conversions enabled +.It rec.hwchans Ta Number Ta Read Ta Number of hardware recording channels +.It rec.vchans Ta Number Ta Read/Write Ta Number of recording VCHANs (virtual channels) +.It rec.min_rate Ta Number Ta Read Ta Minimum recording sample rate +.It rec.max_rate Ta Number Ta Read Ta Maximum recording sample rate +.It rec.min_chans Ta Number Ta Read Ta Minimum recording sample channels +.It rec.max_chans Ta Number Ta Read Ta Maximum recording sample channels +.It rec.formats Ta String Ta Read Ta Recording formats +.El +.Pp +Note that the +.Pa play.rate , +.Pa play.format , +.Pa rec.rate and +.Pa rec.format +controls will be read-only if VCHANs are disabled. +.Pp +The device channel controls are as follows: +.Bl -column xxxxxxxxxxxxxxx xxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxx -offset indent +.It Sy Name Ta Sy Type Ta Sy Read/Write Ta Sy Action +.It name Ta String Ta Read Ta Channel name +.It parentchan Ta String Ta Read Ta Primary (parent) channel name +.It unit Ta Number Ta Read Ta Channel unit +.It caps Ta String Ta Read Ta Channel OSS capabilities +.It latency Ta Number Ta Read Ta Channel latency +.It rate Ta Number Ta Read Ta Channel sample rate +.It format Ta String Ta Read Ta Channel format +.It pid Ta Number Ta Read Ta PID of process consuming channel +.It proc Ta String Ta Read Ta Name of process consuming channel +.It interrupts Ta Number Ta Read Ta Number of interrupts since channel was opened +.It xruns Ta Number Ta Read Ta Number of playback underruns/recoring overruns +.It feedcount Ta Number Ta Read Ta Number of bytes fed to channel +.It volume Ta Volume Ta Read Ta Channel left-right volume in normalized form (0.00 to 1.00). +.It hwbuf Ta Group Ta Read Ta Hardware buffer properties +.It hwbuf.format Ta String Ta Read Ta Hardware buffer format +.It hwbuf.size_bytes Ta Number Ta Read Ta Hardware buffer size in bytes +.It hwbuf.size_frames Ta Number Ta Read Ta Hardware buffer size in frames +.It hwbuf.blksz Ta Number Ta Read Ta Hardware buffer block size +.It hwbuf.blkcnt Ta Number Ta Read Ta Hardware buffer block count +.It hwbuf.free Ta Number Ta Read Ta Hardware buffer free space in bytes +.It hwbuf.ready Ta Number Ta Read Ta Hardware buffer ready space in bytes +.It swbuf Ta Group Ta Read Ta Software buffer properties +.It swbuf.format Ta String Ta Read Ta Software buffer format +.It swbuf.size_bytes Ta Number Ta Read Ta Software buffer size in bytes +.It swbuf.size_frames Ta Number Ta Read Ta Software buffer size in frames +.It swbuf.blksz Ta Number Ta Read Ta Software buffer block size +.It swbuf.blkcnt Ta Number Ta Read Ta Software buffer block count +.It swbuf.free Ta Number Ta Read Ta Software buffer free space in bytes +.It swbuf.ready Ta Number Ta Read Ta Software buffer ready space in bytes +.It feederchain Ta String Ta Read Ta Channel feeder chain +.El +.Sh FILES +.Bl -tag -width /dev/dspX -compact +.It Pa /dev/dsp +The default audio device. +.It Pa /dev/dspX +The audio device file, where X is the unit of the device, for example +.Ar /dev/dsp0 . +.El +.Sh EXAMPLES +Disable auto-conversions and enable realtime mode to get as low latencies as +possible: +.Bd -literal -offset indent +$ audio play.autoconv=0 rec.autoconv=0 realtime=1 +.Ed +.Pp +Set the playback sample format to 2-channel signed 24-bit low endian, and sample +rate to 48000 Hz: +.Bd -literal -offset indent +$ audio play.format=s24le:2.0 play.rate=48000 +.Ed +.Pp +List the PIDs and process names of all channels for +.Pa /dev/dsp1 : +.Bd -literal -offset indent +$ audio -f /dev/dsp1 -v pid proc +.Ed +.Pp +Dump +.Pa /dev/dsp0 +information to a file and retrieve back later: +.Bd -literal -offset indent +$ audio -f /dev/dsp0 -o > info +\&... +$ audio -f /dev/dsp0 `cat info` +.Ed +.Sh SEE ALSO +.Xr sndstat 4 , +.Xr sound 4 , +.Xr mixer 8 , +.Xr sysctl 8 +.Sh AUTHORS +The +.Nm +utility was implemented by +.An Christos Margiolis Aq Mt christos@FreeBSD.org +under sponsorship from the +.Fx +Foundation. diff --git a/usr.sbin/audio/audio.c b/usr.sbin/audio/audio.c new file mode 100644 --- /dev/null +++ b/usr.sbin/audio/audio.c @@ -0,0 +1,993 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2024 The FreeBSD Foundation + * + * This software was developed by Christos Margiolis + * 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 +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Taken from sys/dev/sound/pcm/ */ +#define STATUS_LEN 64 +#define FMTSTR_LEN 16 + +struct audio_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 size_bytes; + int size_frames; + int blksz; + int blkcnt; + int free; + int ready; + } hwbuf, swbuf; + char feederchain[BUFSIZ]; + struct audio_dev *dev; + TAILQ_ENTRY(audio_chan) next; +}; + +struct audio_dev { + char name[NAME_MAX]; + char desc[NAME_MAX]; + char devnode[NAME_MAX]; + char status[BUFSIZ]; + int unit; + char caps[BUFSIZ]; + int from_user; + int bitperfect; + int realtime; + struct { + int rate; + char format[FMTSTR_LEN]; + int autoconv; + int hwchans; + int vchans; + int min_rate; + int max_rate; + int min_chans; + int max_chans; + char formats[BUFSIZ]; + } play, rec; + TAILQ_HEAD(, audio_chan) chans; +}; + +struct audio_ctl { + const char *name; + size_t off; +#define STR 0 +#define NUM 1 +#define VOL 2 +#define GRP 3 + int type; + int (*mod)(struct audio_dev *, void *); +}; + +struct map { + int val; + const char *str; +}; + +static int mod_bitperfect(struct audio_dev *, void *); +static int mod_realtime(struct audio_dev *, void *); +static int mod_play_rate(struct audio_dev *, void *); +static int mod_play_format(struct audio_dev *, void *); +static int mod_play_autoconv(struct audio_dev *, void *); +static int mod_play_vchans(struct audio_dev *, void *); +static int mod_rec_rate(struct audio_dev *, void *); +static int mod_rec_format(struct audio_dev *, void *); +static int mod_rec_autoconv(struct audio_dev *, void *); +static int mod_rec_vchans(struct audio_dev *, void *); + +static struct audio_ctl dev_ctls[] = { +#define F(member) offsetof(struct audio_dev, member) + { "name", F(name), STR, NULL }, + { "desc", F(desc), STR, NULL }, + { "devnode", F(devnode), STR, NULL }, + { "status", F(status), STR, NULL }, + { "unit", F(unit), NUM, NULL }, + { "caps", F(caps), STR, NULL }, + { "from_user", F(from_user), NUM, NULL }, + { "bitperfect", F(bitperfect), NUM, mod_bitperfect }, + { "realtime", F(realtime), NUM, mod_realtime }, + { "play", F(play), GRP, NULL }, + { "play.rate", F(play.rate), NUM, mod_play_rate }, + { "play.format", F(play.format), STR, mod_play_format }, + { "play.autoconv", F(play.autoconv), NUM, mod_play_autoconv }, + { "play.hwchans", F(play.hwchans), 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.autoconv", F(rec.autoconv), NUM, mod_rec_autoconv }, + { "rec.hwchans", F(rec.hwchans), 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 audio_ctl chan_ctls[] = { +#define F(member) offsetof(struct audio_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.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.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, "PCM_CAP_ANALOGIN" }, + { PCM_CAP_ANALOGOUT, "PCM_CAP_ANALOGOUT" }, + { PCM_CAP_BATCH, "PCM_CAP_BATCH" }, + { PCM_CAP_BIND, "PCM_CAP_BIND" }, + { PCM_CAP_COPROC, "PCM_CAP_COPROC" }, + { PCM_CAP_DEFAULT, "PCM_CAP_DEFAULT" }, + { PCM_CAP_DIGITALIN, "PCM_CAP_DIGITALIN" }, + { PCM_CAP_DIGITALOUT, "PCM_CAP_DIGITALOUT" }, + { PCM_CAP_DUPLEX, "PCM_CAP_DUPLEX" }, + { PCM_CAP_FREERATE, "PCM_CAP_FREERATE" }, + { PCM_CAP_HIDDEN, "PCM_CAP_HIDDEN" }, + { PCM_CAP_INPUT, "PCM_CAP_INPUT" }, + { PCM_CAP_MMAP, "PCM_CAP_MMAP" }, + { PCM_CAP_MODEM, "PCM_CAP_MODEM" }, + { PCM_CAP_MULTI, "PCM_CAP_MULTI" }, + { PCM_CAP_OUTPUT, "PCM_CAP_OUTPUT" }, + { PCM_CAP_REALTIME, "PCM_CAP_REALTIME" }, + { PCM_CAP_REVISION, "PCM_CAP_REVISION" }, + { PCM_CAP_SHADOW, "PCM_CAP_SHADOW" }, + { PCM_CAP_SPECIAL, "PCM_CAP_SPECIAL" }, + { PCM_CAP_TRIGGER, "PCM_CAP_TRIGGER" }, + { PCM_CAP_VIRTUAL, "PCM_CAP_VIRTUAL" }, + { 0, NULL } +}; + +static struct map fmtmap[] = { + { AFMT_A_LAW, "alaw" }, + { AFMT_MU_LAW, "mulaw" }, + { AFMT_S8, "s8" }, + { AFMT_U8, "u8" }, + { 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_AC3, "ac3" }, + { 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)) + samplesz = 4; + else + samplesz = 0; + + if (!samplesz || !ch) + return (-1); + + return (bytes / (samplesz * ch)); +} + +static struct audio_dev * +read_dev(char *path) +{ + nvlist_t *nvl; + const nvlist_t * const *di; + const nvlist_t * const *cdi; + struct sndstioc_nv_arg arg; + struct audio_dev *dp = NULL; + struct audio_chan *ch; + size_t nitems, nchans, i, j; + int fd, caps, unit; + + if ((fd = open("/dev/sndstat", O_RDONLY)) < 0) + err(1, "open(/dev/sndstat)"); + + if (ioctl(fd, SNDSTIOC_REFRESH_DEVS, NULL) < 0) + err(1, "ioctl(SNDSTIOC_REFRESH_DEVS)"); + + arg.nbytes = 0; + arg.buf = NULL; + if (ioctl(fd, SNDSTIOC_GET_DEVS, &arg) < 0) + err(1, "ioctl(SNDSTIOC_GET_DEVS#1)"); + + if ((arg.buf = malloc(arg.nbytes)) == NULL) + err(1, "malloc"); + + if (ioctl(fd, SNDSTIOC_GET_DEVS, &arg) < 0) + err(1, "ioctl(SNDSTIOC_GET_DEVS#2)"); + + if ((nvl = nvlist_unpack(arg.buf, arg.nbytes, 0)) == NULL) + err(1, "nvlist_unpack"); + + if (nvlist_empty(nvl) || !nvlist_exists(nvl, SNDST_DSPS)) + 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) + errx(1, "device not found"); + +#define NV(type, item) \ + nvlist_get_ ## type (di[i], SNDST_DSPS_ ## item) + if ((dp = calloc(1, sizeof(struct audio_dev))) == NULL) + 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.hwchans = NV(number, PCHAN); + dp->rec.hwchans = NV(number, RCHAN); +#undef NV + + if (dp->play.hwchans && !nvlist_exists(di[i], SNDST_DSPS_INFO_PLAY)) + errx(1, "playback channel list empty"); + if (dp->rec.hwchans && !nvlist_exists(di[i], SNDST_DSPS_INFO_REC)) + errx(1, "recording channel list empty"); + +#define NV(type, mode, item) \ + nvlist_get_ ## type (nvlist_get_nvlist(di[i], \ + SNDST_DSPS_INFO_ ## mode), SNDST_DSPS_INFO_ ## item) + if (dp->play.hwchans) { + 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.hwchans) { + 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)) + errx(1, "provider_info list empty"); + +#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(number, PVCHAN); + dp->play.rate = NV(number, PVCHANRATE); + fmt2str(dp->play.format, sizeof(dp->play.format), + NV(number, PVCHANFORMAT)); + dp->rec.vchans = NV(number, RVCHAN); + dp->rec.rate = NV(number, RVCHANRATE); + fmt2str(dp->rec.format, sizeof(dp->rec.format), + NV(number, RVCHANFORMAT)); +#undef NV + + dp->play.autoconv = dp->play.vchans ? 1 : 0; + dp->rec.autoconv = dp->rec.vchans ? 1 : 0; + + if (!nvlist_exists(nvlist_get_nvlist(di[i], + SNDST_DSPS_PROVIDER_INFO), SNDST_DSPS_SOUND4_CHAN_INFO)) + errx(1, "channel info list empty"); + + 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 audio_chan))) == NULL) + 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.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.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); +#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 audio_dev *dp) +{ + struct audio_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 audio_dev *dp, struct audio_ctl *ctl, bool simple, + bool showgrp) +{ + struct audio_ctl *cp; + size_t len; + + if (ctl->type != GRP) { + if (simple) + printf("%s=", ctl->name); + else + printf(" %-20s= ", ctl->name); + } + + switch (ctl->type) { + case STR: + printf("%s\n", (char *)dp + ctl->off); + break; + case NUM: + printf("%d\n", *(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 audio_chan *ch, struct audio_ctl *ctl, bool simple, + bool showgrp) +{ + struct audio_ctl *cp; + size_t len; + int v; + + if (ctl->type != GRP) { + if (simple) + printf("%s.%s=", ch->name, ctl->name); + else + printf(" %-20s= ", ctl->name); + } + + switch (ctl->type) { + case STR: + printf("%s\n", (char *)ch + ctl->off); + break; + case NUM: + printf("%d\n", *(int *)((intptr_t)ch + ctl->off)); + break; + case VOL: + v = *(int *)((intptr_t)ch + ctl->off); + printf("%.2f:%.2f\n", + 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 audio_dev *dp) +{ + struct audio_chan *ch; + struct audio_ctl *ctl; + + if (!oflag) { + printf("%s: <%s> %s", dp->name, dp->desc, dp->status); + + printf(" ("); + if (dp->play.hwchans) + printf("play"); + if (dp->play.hwchans && dp->rec.hwchans) + printf("/"); + if (dp->rec.hwchans) + printf("rec"); + printf(")\n"); + } + + for (ctl = dev_ctls; ctl->name != NULL; ctl++) + print_dev_ctl(dp, ctl, oflag, false); + + if (vflag) { + TAILQ_FOREACH(ch, &dp->chans, next) { + if (!oflag) + printf(" ---\n"); + for (ctl = chan_ctls; ctl->name != NULL; ctl++) + print_chan_ctl(ch, ctl, oflag, false); + } + } +} + +static int +sysctl_int(const char *buf, const char *arg, int *var) +{ + size_t size; + const char *val = arg; + int n, prev; + + n = strtol(val, NULL, 10); + if (errno == EINVAL || errno == ERANGE) { + warn("strtol(%s)", val); + return (-1); + } + + size = sizeof(int); + /* Read current value. */ + if (sysctlbyname(buf, &prev, &size, NULL, 0) < 0) { + warn("sysctlbyname(%s)", buf); + return (-1); + } + /* Apply new value. */ + if (sysctlbyname(buf, NULL, 0, &n, size) < 0) { + warn("sysctlbyname(%s, %d)", buf, n); + return (-1); + } + /* Read back applied value for good measure. */ + if (sysctlbyname(buf, &n, &size, NULL, 0) < 0) { + warn("sysctlbyname(%s)", buf); + return (-1); + } + + 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; + const char *val = arg; + char prev[BUFSIZ]; + char *tmp; + + /* Read current value. */ + if (sysctlbyname(buf, prev, &size, NULL, 0) < 0) { + warn("sysctlbyname(%s)", buf); + return (-1); + } + + size = strlen(val); + /* Apply new value. */ + if (sysctlbyname(buf, NULL, 0, val, size) < 0) { + warn("sysctlbyname(%s, %s)", buf, val); + return (-1); + } + /* Get size of new string. */ + if (sysctlbyname(buf, NULL, &size, NULL, 0) < 0) { + warn("sysctlbyname(%s)", buf); + return (-1); + } + if ((tmp = calloc(1, size)) == NULL) + err(1, "calloc"); + /* Read back applied value for good measure. */ + if (sysctlbyname(buf, tmp, &size, NULL, 0) < 0) { + warn("sysctlbyname(%s)", buf); + free(tmp); + return (-1); + } + + printf("%s: %s -> %s\n", buf, prev, tmp); + if (var != NULL) + strlcpy(var, tmp, varsz); + free(tmp); + + return (0); +} + +static int +mod_bitperfect(struct audio_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_realtime(struct audio_dev *dp, void *arg) +{ + const char *val = arg; + int rc = -1; + + if (dp->from_user) + return (-1); + + if (strcmp(val, "0") == 0) { + /* XXX */ + 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_play_rate(struct audio_dev *dp, void *arg) +{ + char buf[64]; + + if (dp->from_user) + return (-1); + + /* We cannot modify the primary channels' rate at the moment. */ + 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 audio_dev *dp, void *arg) +{ + char buf[64]; + + if (dp->from_user) + return (-1); + + /* We cannot modify the primary channels' format at the moment. */ + 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_play_autoconv(struct audio_dev *dp, void *arg) +{ + const char *val = arg; + /* FIXME ugly */ + char *zero = strdup("0"); + char *one = strdup("1"); + int rc = -1; + + if (dp->from_user) + return (-1); + + if (strcmp(val, zero) == 0) + rc = mod_play_vchans(dp, zero) || mod_bitperfect(dp, one); + else if (strcmp(val, one) == 0) + rc = mod_play_vchans(dp, one) || mod_bitperfect(dp, zero); + + free(zero); + free(one); + + return (rc); +} + +static int +mod_play_vchans(struct audio_dev *dp, void *arg) +{ + char buf[64]; + + if (dp->from_user) + return (-1); + + snprintf(buf, sizeof(buf), "dev.pcm.%d.play.vchans", dp->unit); + + return (sysctl_int(buf, arg, &dp->play.vchans)); +} + +static int +mod_rec_rate(struct audio_dev *dp, void *arg) +{ + char buf[64]; + + if (dp->from_user) + return (-1); + + /* We cannot modify the primary channels' rate at the moment. */ + 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 audio_dev *dp, void *arg) +{ + char buf[64]; + + if (dp->from_user) + return (-1); + + /* We cannot modify the primary channels' format at the moment. */ + 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 int +mod_rec_autoconv(struct audio_dev *dp, void *arg) +{ + const char *val = arg; + /* FIXME ugly */ + char *zero = strdup("0"); + char *one = strdup("1"); + int rc = -1; + + if (dp->from_user) + return (-1); + + if (strcmp(val, zero) == 0) + rc = mod_rec_vchans(dp, zero) || mod_bitperfect(dp, one); + else if (strcmp(val, one) == 0) + rc = mod_rec_vchans(dp, one) || mod_bitperfect(dp, zero); + + free(zero); + free(one); + + return (rc); +} + +static int +mod_rec_vchans(struct audio_dev *dp, void *arg) +{ + char buf[64]; + + if (dp->from_user) + return (-1); + + snprintf(buf, sizeof(buf), "dev.pcm.%d.rec.vchans", dp->unit); + + return (sysctl_int(buf, arg, &dp->rec.vchans)); +} + +static void __dead2 +usage(void) +{ + fprintf(stderr, "usage: %s [-f device] [-hov] [control[=value] ...]\n", + getprogname()); + exit(1); +} + +int +main(int argc, char *argv[]) +{ + struct audio_dev *dp; + struct audio_chan *ch; + struct audio_ctl *ctl; + char *path = NULL; + char *s, *propstr; + bool show = true, found; + int c; + + 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; + + dp = read_dev(path); + + while (argc > 0) { + if ((s = strdup(*argv)) == NULL) + 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) { + print_dev_ctl(dp, ctl, true, true); + show = false; + } else if (ctl->mod != NULL && ctl->mod(dp, s) < 0) + warnx("%s(%s) failed", ctl->name, s); + found = true; + break; + } + if (vflag) { + 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) + warnx("%s: no such property", propstr); +next: + free(s); + argc--; + argv++; + } + + /* XXX do we want this behavior? */ + if (show) + print_dev(dp); + free_dev(dp); + + return (0); +}