code: plan9front

Download patch

ref: 0c77ac17f41b22db134a50a3d9dcdeaf149c9482
parent: 0a7d581eec1319c643337d302176b3f9ba565ea5
author: Sigrid Solveig Haflínudóttir <sigrid@ftrv.se>
date: Thu Apr 4 20:50:47 EDT 2024

nusb/audio: great new features

- automatically choose configuration with the format closest to the
  s16c2r44100
- allow independent in/out formats to be configured (mono mic with
  lower rate will work)
- provide "fmtout" and "fmtin" via /dev/volumeU*; those can be changed
  by writing to /dev/volumeU*
- expose some of the useful controls (volume, mute etc), if available,
  via /dev/volumeU
- add "in on/off" and "out on/off" via /dev/audioctlU* for
  input/output toggling, which helps with headsets that force low
  quality on both
- expose supported intput/output formats via /dev/audiostatU*

Tested with:

- vid 0x1b3f did 0x2008 GeneralPlus 'USB Audio Device'
  (audio 1.0, in + out)
- vid 0x14ed did 0x3004 Shure 'Shure AONIC 50 USB Hi-Res'
  (audio 2.0, out only)
- vid 0x14ed did 0x3003 'Shure Inc' 'Shure AONIC 50 USB'
  (audio 1.0, headset, in + out)

Known issues:

- on MNT Reform, changing rate to 48kHz on GeneralPlus results in
  silence on the output if the device is connected to either of the
  closest (to the user) usb ports; it works well with the one closer to
  the back

--- a/sys/src/cmd/nusb/audio/audio.c
+++ b/sys/src/cmd/nusb/audio/audio.c
@@ -3,6 +3,8 @@
 #include <fcall.h>
 #include <thread.h>
 #include <9p.h>
+#include <pcm.h>
+#pragma varargck type "!" Pcmdesc
 #include "usb.h"
 
 enum {
@@ -10,15 +12,53 @@
 	Paudio2 = 0x20,
 
 	Csamfreq = 0x01,
+	Cclockvalid = 0x02,
 
 	/* audio 1 */
 	Rsetcur	= 0x01,
+	Rgetcur = 0x81,
+	Rgetmin,
+	Rgetmax,
+	Rgetres,
 
 	/* audio 2 */
 	Rcur = 0x01,
 	Rrange = 0x02,
+
+	/* feature unit control values */
+	Cmute = 1,
+	Cvolume,
+	Cbass,
+	Cmid,
+	Ctreble,
+	Cagc = 7,
+	Cbassboost = 9,
+	Cloudness,
+	Cnum,
+
+	Conlycur = 1<<Cmute | 1<<Cagc | 1<<Cbassboost | 1<<Cloudness,
+	Cszone = 1<<Cbass | 1<<Cmid | 1<<Ctreble,
+	Cmask1 = (1<<Cvolume | Conlycur | Cszone)>>1,
+	Cmask2 = (
+		3<<Cmute*2 | 3<<Cvolume*2 | 3<<Cbass*2 |
+		3<<Cmid*2 | 3<<Ctreble*2 | 3<<Cagc*2 |
+		3<<Cbassboost*2 | 3<<Cloudness*2
+	)>>2,
+
+	Silence = 0x8000,
 };
 
+char *ctrlname[Cnum] = {
+	[Cmute] = "mute",
+	[Cvolume] = "volume",
+	[Cbass] = "bass",
+	[Cmid] = "mid",
+	[Ctreble] = "treble",
+	[Cagc] = "agc",
+	[Cbassboost] = "bassboost",
+	[Cloudness] = "loudness",
+};
+
 typedef struct Range Range;
 struct Range
 {
@@ -26,17 +66,29 @@
 	uint	max;
 };
 
+typedef struct Ctrl Ctrl;
+struct Ctrl
+{
+	uchar	id;
+	uchar	cs;
+	uchar	cn;
+	short	cur;
+	short	min;
+	short	max;
+	short	res;
+};
+
 typedef struct Aconf Aconf;
 struct Aconf
 {
+	Pcmdesc;
+
 	Ep	*ep;
-	int	bits;
 	int	bps;	/* subslot size (bytes per sample) */
-	int	format;
-	int	channels;
 	int	terminal;
 	Range	*freq;
 	int	nfreq;
+	Iface	*zb;
 
 	/* audio 1 */
 	int	controls;
@@ -45,17 +97,20 @@
 	int	clock;
 };
 
-int audiodelay = 1764;	/* 40 ms */
-int audiofreq = 44100;
-int audiochan = 2;
-int audiores = 16;
+int audiodelay = 1764;	/* 40 ms for 44.1kHz */
 
 char user[] = "audio";
 
-Dev *audiodev = nil;
-Iface *audiocontrol = nil;
-Ep *audioepin = nil;
-Ep *audioepout = nil;
+Dev *adev;
+Iface *ac;
+Ep *epin;
+Ep *epout;
+int inoff, outoff;
+File *ctl;
+File *status;
+File *volume;
+Ctrl ctrl[32];
+int nctrl;
 
 Iface*
 findiface(Conf *conf, int class, int subclass, int id)
@@ -74,18 +129,18 @@
 }
 
 Desc*
-findiad(Usbdev *u, int id, int csp)
+findiad(int csp)
 {
 	int i;
 	Desc *dd;
 	uchar *b;
 
-	for(i = 0; i < nelem(u->ddesc); i++){
-		dd = u->ddesc[i];
+	for(i = 0; i < nelem(adev->usb->ddesc); i++){
+		dd = adev->usb->ddesc[i];
 		if(dd == nil || dd->data.bDescriptorType != 11 || dd->data.bLength != 8)
 			continue;
 		b = dd->data.bbytes;
-		if(b[0] == id && b[0]+b[1] <= Niface && csp == CSP(b[2], b[3], b[4]))
+		if(b[0] == ac->id && b[0]+b[1] <= Niface && csp == CSP(b[2], b[3], b[4]))
 			return dd;
 	}
 	return nil;
@@ -92,14 +147,14 @@
 }
 
 Desc*
-findacheader(Usbdev *u, Iface *ac)
+findacheader(void)
 {
 	Desc *dd;
 	uchar *b;
 	int i;
 
-	for(i = 0; i < nelem(u->ddesc); i++){
-		dd = u->ddesc[i];
+	for(i = 0; i < nelem(adev->usb->ddesc); i++){
+		dd = adev->usb->ddesc[i];
 		if(dd == nil || dd->iface != ac || dd->data.bDescriptorType != 0x24)
 			continue;
 		if(dd->data.bLength < 8 || dd->data.bbytes[0] != 1)
@@ -120,14 +175,14 @@
 }
 
 Desc*
-findterminal(Usbdev *u, Iface *ac, int id)
+findterminal(int id)
 {
 	Desc *dd;
 	uchar *b;
 	int i;
 
-	for(i = 0; i < nelem(u->ddesc); i++){
-		dd = u->ddesc[i];
+	for(i = 0; i < nelem(adev->usb->ddesc); i++){
+		dd = adev->usb->ddesc[i];
 		if(dd == nil || dd->iface != ac)
 			continue;
 		if(dd->data.bDescriptorType != 0x24 || dd->data.bLength < 4)
@@ -148,14 +203,14 @@
 }
 
 Desc*
-findclocksource(Usbdev *u, Iface *ac, int id)
+findclocksource(int id)
 {
 	Desc *dd;
 	uchar *b;
 	int i;
 
-	for(i = 0; i < nelem(u->ddesc); i++){
-		dd = u->ddesc[i];
+	for(i = 0; i < nelem(adev->usb->ddesc); i++){
+		dd = adev->usb->ddesc[i];
 		if(dd == nil || dd->iface != ac)
 			continue;
 		if(dd->data.bDescriptorType != 0x24 || dd->data.bLength != 8)
@@ -167,7 +222,20 @@
 	return nil;
 }
 
-void
+Rune
+tofmt(int v)
+{
+	switch(v){
+	case 1: return L's';
+	case 2: return L'u';
+	case 3: return L'f';
+	case 4: return L'a';
+	case 5: return L'µ';
+	}
+	return 0;
+}
+
+int
 parseasdesc1(Desc *dd, Aconf *c)
 {
 	uchar *b;
@@ -177,20 +245,22 @@
 	switch(dd->data.bDescriptorType<<8 | b[0]){
 	case 0x2501:	/* CS_ENDPOINT, EP_GENERAL */
 		if(dd->data.bLength != 7)
-			return;
+			return -1;
 		c->controls = b[1];
 		break;
 
 	case 0x2401:	/* CS_INTERFACE, AS_GENERAL */
 		if(dd->data.bLength != 7)
-			return;
+			return -1;
 		c->terminal = b[1];
-		c->format = GET2(&b[3]);
+		c->fmt = tofmt(GET2(&b[3]));
+		if(!c->fmt)
+			return -1;
 		break;
 
 	case 0x2402:	/* CS_INTERFACE, FORMAT_TYPE */
 		if(dd->data.bLength < 8 || b[1] != 1)
-			return;
+			return -1;
 		c->channels = b[2];
 		c->bps = b[3];
 		c->bits = b[4];
@@ -199,7 +269,7 @@
 			c->freq = emallocz(sizeof(*f), 1);
 			c->freq->min = b[6] | b[7]<<8 | b[8]<<16;
 			c->freq->max = b[9] | b[10]<<8 | b[11]<<16;
-		}else{		/* discrete sampling frequencies */
+		} else {		/* discrete sampling frequencies */
 			c->nfreq = b[5];
 			c->freq = emallocz(c->nfreq * sizeof(*f), 1);
 			b += 6;
@@ -210,9 +280,10 @@
 		}
 		break;
 	}
+	return 0;
 }
 
-void
+int
 parseasdesc2(Desc *dd, Aconf *c)
 {
 	uchar *b;
@@ -221,23 +292,26 @@
 	switch(dd->data.bDescriptorType<<8 | b[0]){
 	case 0x2401:	/* CS_INTERFACE, AS_GENERAL */
 		if(dd->data.bLength != 16 || b[3] != 1)
-			return;
+			return -1;
 		c->terminal = b[1];
-		c->format = GET4(&b[4]);
 		c->channels = b[8];
+		c->fmt = tofmt(GET4(&b[4]));
+		if(!c->fmt)
+			return -1;
 		break;
 
 	case 0x2402:	/* CS_INTERFACE, FORMAT_TYPE */
 		if(dd->data.bLength != 6 || b[1] != 1)
-			return;
+			return -1;
 		c->bps = b[2];
 		c->bits = b[3];
 		break;
 	}
+	return 0;
 }
 
 int
-setclock(Dev *d, Iface *ac, Aconf *c, int speed)
+setclock(Aconf *c, int speed)
 {
 	uchar b[4];
 	int index;
@@ -252,22 +326,30 @@
 		index = c->ep->id & Epmax;
 		if(c->ep->dir == Ein)
 			index |= 0x80;
-		return usbcmd(d, Rh2d|Rclass|Rep, Rsetcur, Csamfreq<<8, index, b, 3);
+		if(usbcmd(adev, Rh2d|Rclass|Rep, Rsetcur, Csamfreq<<8, index, b, 3) < 0)
+			break;
+		if(usbcmd(adev, Rd2h|Rclass|Rep, Rgetcur, Csamfreq<<8, index, b, 3) != 3)
+			break;
+		return b[0] | b[1]<<8 | b[2]<<16;
 	case Paudio2:
 		PUT4(b, speed);
 		index = c->clock<<8 | ac->id;
-		return usbcmd(d, Rh2d|Rclass|Riface, Rcur, Csamfreq<<8, index, b, 4);
+		if(usbcmd(adev, Rh2d|Rclass|Riface, Rcur, Csamfreq<<8, index, b, 4) < 0)
+			break;
+		if(usbcmd(adev, Rd2h|Rclass|Riface, Rcur, Csamfreq<<8, index, b, 4) != 4)
+			break;
+		return GET4(b);
 	}
-	return 0;
+	return -1;
 }
 
 int
-getclockrange(Dev *d, Iface *ac, Aconf *c)
+getclockrange(Aconf *c)
 {
 	uchar b[2 + 32*12];
 	int i, n, rc;
 
-	rc = usbcmd(d, Rd2h|Rclass|Riface, Rrange, Csamfreq<<8, c->clock<<8 | ac->id, b, sizeof(b));
+	rc = usbcmd(adev, Rd2h|Rclass|Riface, Rrange, Csamfreq<<8, c->clock<<8 | ac->id, b, sizeof(b));
 	if(rc < 0)
 		return -1;
 	if(rc < 2 || rc < 2 + (n = GET2(b))*12){
@@ -281,11 +363,204 @@
 	return 0;
 }
 
+int
+setvalue(uchar id, uchar hi, uchar lo, short value, int sz)
+{
+	uchar b[2];
+
+	if(sz == 1)
+		b[0] = value;
+	else
+		PUT2(b, value);
+
+	if(Proto(ac->csp) == Paudio1)
+		return usbcmd(adev, Rh2d|Rclass|Riface, Rsetcur, hi<<8 | lo, id<<8 | ac->id, b, sz);
+
+	return usbcmd(adev, Rh2d|Rclass|Riface, Rcur, hi<<8 | lo, id<<8 | ac->id, b, sz);
+}
+
+int
+getvalue(int r, uchar id, uchar hi, uchar lo, short *value)
+{
+	uchar b[2 + 32*3*4];
+	int rc, i, n;
+
+	*value = 0;
+	i = 0;
+	if(Proto(ac->csp) == Paudio1)
+		rc = usbcmd(adev, Rd2h|Rclass|Riface, r, hi<<8 | lo, id<<8 | ac->id, b, sizeof(b));
+	else {
+		i = r - Rgetmin;
+		r = r == Rgetcur ? Rcur : Rrange;
+		rc = usbcmd(adev, Rd2h|Rclass|Riface, r, hi<<8 | lo, id<<8 | ac->id, b, sizeof(b));
+	}
+	if(rc < 0)
+		return -1;
+	if(rc < 1)
+		goto Invalid;
+	if(r == Rrange){
+		rc -= 2;
+		if(rc < 3 || (n = GET2(b)) < 1)
+			goto Invalid;
+		else if(rc == n*3*2)
+			*value = GET2(&b[2 + i*2]);
+		else if(rc == n*3*1)
+			*value = b[2 + i];
+		else
+			goto Invalid;
+	} else
+		*value = rc > 1 ? GET2(b) : b[0];
+	return 0;
+Invalid:
+	werrstr("invalid response");
+	return -1;
+}
+
+int
+getvalues(Ctrl *c)
+{
+	char *s;
+	int onlycur;
+
+	onlycur = Conlycur & (1<<c->cs);
+	if(((s = "cur") && getvalue(Rgetcur, c->id, c->cs, c->cn, &c->cur) < 0) ||
+		(!onlycur && (
+			((s = "min") && getvalue(Rgetmin, c->id, c->cs, c->cn, &c->min) < 0) ||
+			((s = "max") && getvalue(Rgetmax, c->id, c->cs, c->cn, &c->max) < 0) ||
+			((s = "res") && getvalue(Rgetres, c->id, c->cs, c->cn, &c->res) < 0)))){
+		fprint(2, "getvalue: %s: %s: %r\n", ctrlname[c->cs], s);
+		return -1;
+	}
+	if(onlycur){
+		c->min = 0;
+		c->max = 1;
+		c->res = 1;
+	} else if(c->res < 1){
+		fprint(2, "getvalue: %s: invalid res: %d\n", ctrlname[c->cs], c->res);
+		return -1;
+	}
+	return 0;
+}
+
+int
+cmpctrl(void *a_, void *b_)
+{
+	Ctrl *a, *b;
+	a = a_;
+	b = b_;
+	if(a->id != b->id)
+		return a->id - b->id;
+	if(a->cs != b->cs)
+		return a->cs - b->cs;
+	return a->cn - b->cn;
+}
+
 void
-parsestream(Dev *d, Iface *ac, int id)
+findcontrols1(void)
 {
-	Iface *as;
+	int i, k, n, x;
+	uchar cs, cn;
+	uchar *b;
 	Desc *dd;
+	Ctrl *c;
+
+	for(i = 0; i < nelem(adev->usb->ddesc); i++){
+		dd = adev->usb->ddesc[i];
+		if(dd == nil)
+			continue;
+		b = dd->data.bbytes;
+		switch(dd->data.bDescriptorType<<8 | b[0]){
+		case 0x2406:	/* CS_INTERFACE, FEATURE_UNIT */
+			if(dd->data.bLength < 8 || nctrl >= nelem(ctrl) || (n = b[3]) < 1)
+				continue;
+			for(k = 0; k < nctrl; k++)
+				if(ctrl[k].id == b[1])
+					break;
+			if(k < nctrl)
+				break;
+			for(k = 4, cn = 0; k <= dd->data.bLength-2-n-1; k += n, cn++){
+				x = (n > 1 ? GET2(b+k) : b[k]) & Cmask1;
+				if(x == 0)
+					continue;
+				for(cs = 1; cs < Cnum && nctrl < nelem(ctrl); cs++, x >>= 1){
+					if((x & 1) != 1)
+						continue;
+					c = &ctrl[nctrl++];
+					c->cs = cs;
+					c->cn = cn;
+					c->id = b[1];
+					getvalues(c);
+				}
+			}
+			break;
+		}
+	}
+}
+
+void
+findcontrols2(void)
+{
+	int i, k, n, x;
+	uchar cs, cn;
+	uchar *b;
+	Desc *dd;
+	Ctrl *c;
+
+	for(i = 0; i < nelem(adev->usb->ddesc); i++){
+		dd = adev->usb->ddesc[i];
+		if(dd == nil)
+			continue;
+		b = dd->data.bbytes;
+		switch(dd->data.bDescriptorType<<8 | b[0]){
+		case 0x2406:	/* CS_INTERFACE, FEATURE_UNIT */
+			if(dd->data.bLength < 9 || nctrl >= nelem(ctrl))
+				continue;
+			for(k = 0; k < nctrl; k++)
+				if(ctrl[k].id == b[1])
+					break;
+			if(k < nctrl)
+				break;
+			n = 4;
+			for(k = 3, cn = 0; k <= dd->data.bLength-2-n-1; k += n, cn++){
+				x = GET4(b+k) & Cmask2;
+				if(x == 0)
+					continue;
+				for(cs = 1; cs < Cnum && nctrl < nelem(ctrl); cs++, x >>= 2){
+					if((x & 3) != 3)
+						continue;
+					c = &ctrl[nctrl++];
+					c->cs = cs;
+					c->cn = cn;
+					c->id = b[1];
+					getvalues(c);
+				}
+			}
+			break;
+		}
+	}
+}
+
+void
+parseterminal2(Desc *dd, Aconf *c)
+{
+	uchar *b;
+
+	b = dd->data.bbytes;
+	switch(b[0]){
+	case 0x02:	/* INPUT_TERMINAL */
+		c->clock = b[5];
+		break;
+	case 0x03:	/* OUTPUT_TERMINAL */
+		c->clock = b[6];
+		break;
+	}
+}
+
+void
+parsestream(int id)
+{
+	Iface *as, *zb;
+	Desc *dd;
 	Ep *e;
 	Aconf *c;
 	uchar *b;
@@ -292,8 +567,11 @@
 	int i;
 
 	/* find AS interface */
-	as = findiface(d->usb->conf[0], Claudio, 2, id);
+	as = findiface(adev->usb->conf[0], Claudio, 2, id);
 
+	/* find zero-bandwidth setting, if any */
+	for(zb = as; zb != nil && zb->alt != 0; zb = zb->next);
+
 	/* enumerate through alt. settings */
 	for(; as != nil; as = as->next){
 		c = emallocz(sizeof(*c), 1);
@@ -313,18 +591,21 @@
 			as->aux = nil;
 			continue;
 		}
+		c->zb = zb;
 
 		/* parse AS descriptors */
-		for(i = 0; i < nelem(d->usb->ddesc); i++){
-			dd = d->usb->ddesc[i];
+		for(i = 0; i < nelem(adev->usb->ddesc); i++){
+			dd = adev->usb->ddesc[i];
 			if(dd == nil || dd->iface != as)
 				continue;
 			switch(Proto(ac->csp)){
 			case Paudio1:
-				parseasdesc1(dd, c);
+				if(parseasdesc1(dd, c) != 0)
+					goto Skip;
 				break;
 			case Paudio2:
-				parseasdesc2(dd, c);
+				if(parseasdesc2(dd, c) != 0)
+					goto Skip;
 				break;
 			}
 		}
@@ -332,20 +613,12 @@
 		if(Proto(ac->csp) == Paudio1)
 			continue;
 
-		dd = findterminal(d->usb, ac, c->terminal);
+		dd = findterminal(c->terminal);
 		if(dd == nil)
 			goto Skip;
-		b = dd->data.bbytes;
-		switch(b[0]){
-		case 0x02:	/* INPUT_TERMINAL */
-			c->clock = b[5];
-			break;
-		case 0x03:	/* OUTPUT_TERMINAL */
-			c->clock = b[6];
-			break;
-		}
+		parseterminal2(dd, c);
 
-		dd = findclocksource(d->usb, ac, c->clock);
+		dd = findclocksource(c->clock);
 		if(dd == nil)
 			goto Skip;
 		b = dd->data.bbytes;
@@ -352,123 +625,367 @@
 		/* check that clock has rw frequency control */
 		if((b[3] & 3) != 3)
 			goto Skip;
-		if(getclockrange(d, ac, c) != 0){
-			fprint(2, "getclockrange %d: %r", c->clock);
+		if(getclockrange(c) != 0){
+			fprint(2, "getclockrange %d: %r\n", c->clock);
 			goto Skip;
 		}
 	}
 }
 
+int
+fmtcmp(Pcmdesc *a, Pcmdesc *b)
+{
+	if(a->rate != b->rate)
+		return a->rate - b->rate;
+	if(a->channels != b->channels)
+		return a->channels - b->channels;
+	if(a->bits != b->bits)
+		return a->bits - b->bits;
+	if(a->fmt != b->fmt){
+		if(a->fmt == L's' || a->fmt == L'f')
+			return 1;
+		if(b->fmt == L's' || b->fmt == L'f')
+			return -1;
+	}
+	return a->fmt - b->fmt;
+}
+
 Dev*
-setupep(Dev *d, Iface *ac, Ep *e, int *speed, int force)
+setupep(Ep *e, Pcmdesc *fmt, int exact)
 {
-	int dir = e->dir;
 	Aconf *c, *bestc;
+	Ep *beste, *ep;
+	int n, r, dir;
+	Pcmdesc p;
 	Range *f;
-	Ep *beste;
-	int closest, sp;
+	Dev *d;
 
+	if(e == epout && outoff){
+		werrstr("output disabled");
+		return nil;
+	}
+	if(e == epin && inoff){
+		werrstr("input disabled");
+		return nil;
+	}
+
+	dir = e->dir;
+	ep = e;
 	bestc = nil;
 	beste = nil;
-	closest = 1<<30;
-	sp = *speed;
+	r = -1;
 
 	for(;e != nil; e = e->next){
 		c = e->iface->aux;
-		if(c == nil || e != c->ep || e->dir != dir)
+		if(c == nil || e != c->ep || e->dir != dir || c->bits != 8*c->bps)
 			continue;
-		if(c->format != 1 || c->bits != audiores || 8*c->bps != audiores || c->channels != audiochan)
-			continue;
 		for(f = c->freq; f != c->freq+c->nfreq; f++){
-			if(sp >= f->min && sp <= f->max)
-				goto Foundaltc;
-			if(force)
-				continue;
-			if(f->min >= sp && closest-sp >= f->min-sp){
-				closest = f->min;
+			p = *c;
+			if(f->min >= fmt->rate)
+				p.rate = f->min;
+			else if(f->max <= fmt->rate)
+				p.rate = f->max;
+			else
+				p.rate = fmt->rate;
+			if((n = fmtcmp(&p, fmt)) == 0 || bestc == nil){
+Better:
+				c->rate = p.rate;
 				bestc = c;
 				beste = e;
-			}else if(bestc == nil || (f->max < sp && closest < sp && sp-closest > sp-f->max)){
-				closest = f->max;
-				bestc = c;
-				beste = e;
+				if((r = n) == 0)
+					goto Done;
+				continue;
 			}
+			/* both better, but the new is closer */
+			if(n > 0 && (r > 0 && fmtcmp(&p, bestc) < 0))
+				goto Better;
+			/* both worse, but the new one is better so far */
+			if(n < 0 && (r < 0 && fmtcmp(&p, bestc) > 0))
+				goto Better;
 		}
 	}
-	if(bestc == nil){
+
+Done:
+	if(bestc == nil || (exact && r != 0)){
 		werrstr("no altc found");
 		return nil;
 	}
 	e = beste;
 	c = bestc;
-	sp = closest;
 
-Foundaltc:
-	if(setalt(d, e->iface) < 0){
-		fprint(2, "setalt: %r\n");
+	/* jump to alt 0 before trying to do anything */
+	if(c->zb != nil)
+		setalt(adev, c->zb);
+
+	if(setalt(adev, e->iface) < 0){
+		werrstr("setalt: %r\n");
 		return nil;
 	}
-	if(setclock(d, ac, c, sp) < 0){
-		werrstr("setclock: %r");
-		return nil;
-	}
-	if((d = openep(d, e)) == nil){
+
+	/* ignore errors as updated clock isn't always required */
+	r = setclock(c, c->rate);
+
+	if((d = openep(adev, e)) == nil){
 		werrstr("openep: %r");
 		return nil;
 	}
-	devctl(d, "samplesz %d", audiochan*audiores/8);
+
+	/* update the rate only if the value makes sense */
+	if(r >= c->rate*9/10 && r <= c->rate*10/9)
+		c->rate = r;
+
+	ep->aux = c;
+	devctl(d, "samplesz %d", c->channels*c->bits/8);
 	devctl(d, "sampledelay %d", audiodelay);
-	devctl(d, "hz %d", sp);
-	devctl(d, "name audio%sU%s", e->dir==Ein ? "in" : "", audiodev->hname);
-	*speed = sp;
+	devctl(d, "hz %d", c->rate);
+	devctl(d, "name audio%sU%s", e->dir==Ein ? "in" : "", adev->hname);
 	return d;
 }
 
+char *
+ctrlvalue(Ctrl *c)
+{
+	static char v[64];
+	int x, n;
+
+	if((Conlycur & (1<<c->cs)) == 0){
+		if((ushort)c->cur == Silence)
+			x = 0;
+		else {
+			n = (c->max - c->min);
+			x = (c->cur - c->min) * 100;
+			x /= n;
+		}
+	}else{
+		x = !!c->cur;
+	}
+	snprint(v, sizeof(v), "%d", x);
+	return v;
+}
+
+char *
+seprintaconf(char *s, char *e, Ep *ep)
+{
+	Pcmdesc d;
+	Range *f;
+	Aconf *c;
+	int dir;
+
+	dir = ep->dir;
+	for(; ep != nil; ep = ep->next){
+		c = ep->iface->aux;
+		if(c == nil || ep != c->ep || ep->dir != dir || c->bits != 8*c->bps)
+			continue;
+		for(f = c->freq; f != c->freq+c->nfreq; f++){
+			d = c->Pcmdesc;
+			d.rate = f->min;
+			s = seprint(s, e, " %!", d);
+			if(f->min < f->max){
+				d.rate = f->max;
+				s = seprint(s, e, "-%!", d);
+			}
+		}
+	}
+	return seprint(s, e, "\n");
+}
+
 void
 fsread(Req *r)
 {
-	char *msg;
+	static char msg[2048];
+	Ctrl *cur, *prev;
+	char *s, *e;
+	Aconf *c;
+	int i;
 
-	msg = smprint("master 100 100\nspeed %d\ndelay %d\n", audiofreq, audiodelay);
+	s = msg;
+	e = msg+sizeof(msg);
+	*s = 0;
+
+	if(r->fid->file == ctl){
+		if(epout != nil)
+			s = seprint(s, e, "out %s\n", outoff ? "off" : "on");
+		if(epin != nil)
+			seprint(s, e, "in %s\n", inoff ? "off" : "on");
+	} else if(r->fid->file == status){
+		s = seprint(s, e, "bufsize %6d buffered %6d\n", 0, 0);
+		if(epout != nil)
+			s = seprintaconf(seprint(s, e, "fmtout"), e, epout);
+		if(epin != nil)
+			seprintaconf(seprint(s, e, "fmtin"), e, epin);
+	} else if(r->fid->file == volume){
+		if(epout != nil){
+			c = epout->aux;
+			s = seprint(s, e, "delay %d\nfmtout %!\nspeed %d\n",
+				audiodelay, c->Pcmdesc, c->rate);
+		}
+		if(epin != nil){
+			c = epin->aux;
+			s = seprint(s, e, "fmtin %!\n", c->Pcmdesc);
+		}
+
+		prev = nil;
+		for(i = 0; i < nctrl; i++, prev = cur){
+			cur = ctrl+i;
+			if(prev == nil || prev->id != cur->id || prev->cs != cur->cs)
+				s = seprint(s, e, "%s%s%d", i?"\n":"", ctrlname[cur->cs], cur->id);
+			s = seprint(s, e, " %s", ctrlvalue(cur));
+		}
+		if(i > 0)
+			seprint(s, e, "\n");
+	} else {
+		respond(r, "protocol botch");
+		return;
+	}
+
 	readstr(r, msg);
 	respond(r, nil);
-	free(msg);
 }
 
+int
+setctrl(char *f[8], int nf)
+{
+	int i, j, id, n, x, sz;
+	char *s, *e;
+	uchar cs;
+	Ctrl *c;
+
+	id = -1;
+	s = nil;
+	for(e = f[0]; *e; e++){
+		if(*e >= '0' && *e <= '9'){
+			s = e;
+			id = strtol(e, &e, 10);
+			break;
+		}
+	}
+	if(id < 0 || *e != 0){
+Invalid:
+		werrstr("invalid name");
+		goto Error;
+	}
+	*s = 0;
+	for(i = 0, c = ctrl; i < nctrl; i++, c++){
+		if(ctrl[i].id == id && strcmp(ctrlname[c->cs], f[0]) == 0)
+			break;
+	}
+	if(i == nctrl)
+		goto Invalid;
+
+	cs = c->cs;
+	for(j = 1; j < nf; j++){
+		x = atoi(f[j]);
+		if((Conlycur & (1<<c->cs)) == 0){
+			sz = (Cszone & (1<<c->cs)) ? 1 : 2;
+			if(x <= 0 && sz == 2)
+				x = Silence;
+			else{
+				n = (c->max - c->min) / c->res;
+				x = (x * n * c->res)/100 + c->min;
+				if(x < c->min)
+					x = c->min;
+				else if(x > c->max)
+					x = c->max;
+			}
+		} else {
+			x = !!x;
+			sz = 1;
+		}
+Groupset:
+		if(setvalue(c->id, c->cs, c->cn, x, sz) < 0)
+			goto Error;
+		if(getvalue(Rgetcur, c->id, c->cs, c->cn, &c->cur) < 0)
+			goto Error;
+
+		c++;
+		if(c >= ctrl+nctrl || c->id != id || c->cs != cs)
+			break; /* just ignore anything extra */
+		if(nf == 2)
+			goto Groupset;
+	}
+
+	return 0;
+Error:
+	werrstr("%s: %r", f[0]);
+	return -1;
+}
+
 void
 fswrite(Req *r)
 {
-	char msg[256], *f[4];
-	int nf, speed;
+	char msg[256], *f[8];
+	int nf, off;
+	Pcmdesc pd;
+	Aconf *c;
+	Dev *d;
+	Ep *e;
 
 	snprint(msg, sizeof(msg), "%.*s",
 		utfnlen((char*)r->ifcall.data, r->ifcall.count), (char*)r->ifcall.data);
 	nf = tokenize(msg, f, nelem(f));
 	if(nf < 2){
+Invalid:
 		respond(r, "invalid ctl message");
 		return;
 	}
-	if(strcmp(f[0], "speed") == 0){
-		Dev *d;
+	c = epout->aux;
 
-		speed = atoi(f[1]);
-Setup:
-		if((d = setupep(audiodev, audiocontrol, audioepout, &speed, 1)) == nil){
-			responderror(r);
+	if(r->fid->file == ctl){
+		if(strcmp(f[0], "out") == 0)
+			e = epout;
+		else if(strcmp(f[0], "in") == 0)
+			e = epin;
+		else
+			goto Invalid;
+		if(strcmp(f[1], "on") == 0)
+			off = 0;
+		else if(strcmp(f[1], "off") == 0)
+			off = 1;
+		else
+			goto Invalid;
+		c = e->aux;
+		if(c->zb == nil){
+			respond(r, "no zero-bandwidth config");
 			return;
 		}
-		audiofreq = speed;
+		if(setalt(adev, c->zb) < 0)
+			goto Error;
+		if(e == epout)
+			outoff = off;
+		else
+			inoff = off;
+	} else if(r->fid->file != volume){
+		werrstr("protocol botch");
+		goto Error;
+	} else if((strcmp(f[0], "speed") == 0 || strcmp(f[0], "fmtout") == 0) && epout != nil){
+		if(f[0][0] == 's'){
+			pd = c->Pcmdesc;
+			pd.rate = atoi(f[1]);
+		}else if(mkpcmdesc(f[1], &pd) != 0)
+			goto Error;
+Setup:
+		if((d = setupep(epout, &pd, 1)) == nil)
+			goto Error;
 		closedev(d);
-		if(audioepin != nil)
-			if(d = setupep(audiodev, audiocontrol, audioepin, &speed, 1))
-				closedev(d);
-	} else if(strcmp(f[0], "delay") == 0){
+	} else if(strcmp(f[0], "fmtin") == 0 && epin != nil){
+		if(mkpcmdesc(f[1], &pd) != 0)
+			goto Error;
+		if((d = setupep(epin, &pd, 1)) == nil)
+			goto Error;
+		closedev(d);
+	} else if(strcmp(f[0], "delay") == 0 && epout != nil){
+		pd = c->Pcmdesc;
 		audiodelay = atoi(f[1]);
-		speed = audiofreq;
 		goto Setup;
+	} else if(setctrl(f, nf) < 0){
+		goto Error;
 	}
+
 	r->ofcall.count = r->ifcall.count;
 	respond(r, nil);
+	return;
+Error:
+	responderror(r);
 }
 
 Srv fs = {
@@ -487,10 +1004,9 @@
 main(int argc, char *argv[])
 {
 	char buf[32];
-	Dev *d, *ed;
+	Dev *ed;
 	Desc *dd;
 	Conf *conf;
-	Iface *ac;
 	Aconf *c;
 	Ep *e;
 	uchar *b;
@@ -508,37 +1024,36 @@
 	if(argc == 0)
 		usage();
 
-	if((d = getdev(*argv)) == nil)
+	fmtinstall('!', pcmdescfmt);
+	if((adev = getdev(*argv)) == nil)
 		sysfatal("getdev: %r");
-	audiodev = d;
 
-	conf = d->usb->conf[0];
+	conf = adev->usb->conf[0];
 	ac = findiface(conf, Claudio, 1, -1);
 	if(ac == nil)
 		sysfatal("no audio control interface");
-	audiocontrol = ac;
 
 	switch(Proto(ac->csp)){
 	case Paudio1:
-		dd = findacheader(d->usb, ac);
+		dd = findacheader();
 		if(dd == nil)
 			sysfatal("no audio control header");
 		b = dd->data.bbytes;
 		for(i = 6; i < dd->data.bLength-2; i++)
-			parsestream(d, ac, b[i]);
+			parsestream(b[i]);
 		break;
 	case Paudio2:
-		dd = findiad(d->usb, ac->id, CSP(Claudio, 0, Paudio2));
+		dd = findiad(CSP(Claudio, 0, Paudio2));
 		if(dd == nil)
 			sysfatal("no audio function");
 		b = dd->data.bbytes;
 		for(i = b[0]+1; i < b[0]+b[1]; i++)
-			parsestream(d, ac, i);
+			parsestream(i);
 		break;
 	}
 
-	for(i = 0; i < nelem(d->usb->ep); i++){
-		for(e = d->usb->ep[i]; e != nil; e = e->next){
+	for(i = 0; i < nelem(adev->usb->ep); i++){
+		for(e = adev->usb->ep[i]; e != nil; e = e->next){
 			c = e->iface->aux;
 			if(c != nil && c->ep == e)
 				break;
@@ -547,36 +1062,49 @@
 			continue;
 		switch(e->dir){
 		case Ein:
-			if(audioepin != nil)
+			if(epin != nil)
 				continue;
-			audioepin = e;
+			epin = e;
 			break;
 		case Eout:
-			if(audioepout != nil)
+			if(epout != nil)
 				continue;
-			audioepout = e;
+			epout = e;
 			break;
 		}
-		if((ed = setupep(d, ac, e, &audiofreq, 0)) == nil){
-			fprint(2, "setupep: %r\n");
-
-			if(e == audioepin)
-				audioepin = nil;
-			if(e == audioepout)
-				audioepout = nil;
+		if((ed = setupep(e, &pcmdescdef, 0)) == nil){
+			fprint(2, "setupep: %s: %r\n", epout == e ? "out" : "in");
+			if(e == epin)
+				epin = nil;
+			if(e == epout)
+				epout = nil;
 			continue;
 		}
 		closedev(ed);
 	}
-	if(audioepout == nil)
-		sysfatal("no output stream found");
+	if(epout == nil && epin == nil)
+		sysfatal("no streams found");
 
+	switch(Proto(ac->csp)){
+	case Paudio1:
+		findcontrols1();
+		break;
+	case Paudio2:
+		findcontrols2();
+		break;
+	}
+	qsort(ctrl, nctrl, sizeof(Ctrl), cmpctrl);
+
 	fs.tree = alloctree(user, "usb", DMDIR|0555, nil);
-	snprint(buf, sizeof buf, "volumeU%s", audiodev->hname);
-	createfile(fs.tree->root, buf, user, 0666, nil);
+	snprint(buf, sizeof buf, "audioctlU%s", adev->hname);
+	ctl = createfile(fs.tree->root, buf, user, 0666, nil);
+	snprint(buf, sizeof buf, "audiostatU%s", adev->hname);
+	status = createfile(fs.tree->root, buf, user, 0444, nil);
+	snprint(buf, sizeof buf, "volumeU%s", adev->hname);
+	volume = createfile(fs.tree->root, buf, user, 0666, nil);
 
-	snprint(buf, sizeof buf, "%d.audio", audiodev->id);
+	snprint(buf, sizeof buf, "%d.audio", adev->id);
 	postsharesrv(&fs, nil, "usb", buf);
 
-	exits(0);
+	exits(nil);
 }