shithub: purgatorio

ref: c0cbe87b357a4464a7d10ab8f38a65c77ea3b629
dir: /os/js/devcs4231.c/

View raw version
/*
 * preliminary Crystal CS4231 audio driver,
 * initially based on SB16 driver, and therefore needs work.
 * for instance, i suspect the linked-list buffering is excessive:
 * a rolling buffering scheme or double buffering should be fine,
 * and possibly simpler.
 *
 * To do:
 *	stop/start?
 *	is the linux mix_cvt ideal?
 *	ad1845 differences
 *	adpcm freezing
 */

#include	"u.h"
#include	"../port/lib.h"
#include	"mem.h"
#include	"dat.h"
#include	"fns.h"
#include	"../port/error.h"
#include	"devtab.h"
#include	"io.h"
#include	"audio.h"

#define	DPRINT	if(chatty)print

typedef struct	AChan	AChan;
typedef struct	AQueue	AQueue;
typedef struct	Buf	Buf;
typedef struct	Vol	Vol;

enum
{
	Qdir		= 0,
	Qaudio,
	Qaudioctl,

	Fmono		= 1,
	Fin		= 2,
	Fout		= 4,

	Vaudio		= 0,
	Vaux1,
	Vaux2,
	Vline,
	Vmic,
	Vmono,
	Vspeed,
	Vchans,
	Vbits,
	Nvol,

	Speed		= 22050,
	Ncmd		= 50,		/* max volume command words */
};

enum {
	Paddr=	0,
		TRD=	1<<5,
		MCE=	1<<6,
	Pdata=	1,
	Pstatus=	2,
	Pio=		3,

	LeftADC=	0,
		MGE=	1<<5,
		ISline=	0<<6,
		ISaux1=	1<<6,
		ISmic=	2<<6,
		ISloop=	3<<6,
		ISmask=	3<<6,
	RightADC= 1,
	LeftAux1= 2,
		Mute=	1<<7,
	RightAux1= 3,
	LeftAux2= 4,
	RightAux2= 5,
	LeftDAC= 6,
	RightDAC= 7,
	OutFormat=	8,
		Stereo=	1<<4,
		Linear8=	0<<5,
		uLaw=	1<<5,
		Linear16=	2<<5,
		aLaw=	3<<5,
		ADPCM=	5<<5,
		Fmask=	7<<5,
	Config=	9,
		PEN=	1<<0,
		CEN=	1<<1,
		Nocal=	0<<3,
		Convcal=	1<<3,
		DACcal=	2<<3,
		Fullcal=	3<<3,
	PinControl=	10,
		IEN=		1<<1,
		DEN=	1<<3,
		Xctl0=	1<<6,
		Xctl1=	1<<7,
	Status=	11,
		ACI=		1<<5,
	Mode=	12,
		Mode2=	1<<6,
	Loopback=	13,
		LBE=		1<<0,
	PlayCount1=	14,
	PlayCount0=	15,
	Feature1=	16,
		PMCE=	1<<4,
		CMCE=	1<<5,
	Feature2=	17,
	LeftLine=	18,
	RightLine=	19,
	Timer0=	20,
	Timer1=	21,
	Feature3=	23,
	FeatureStatus=	24,
		PI=	1<<4,	/* playback interrupt */
		CI=	1<<5,	/* capture interrupt */
		TI=	1<<6,	/* timer interrupt */
	ChipID= 25,
	MonoCtl=	26,
		MBY=	1<<5,	/* mono bypass */
		MOM=	1<<6,
	InFormat=	28,
	RecCount1=	30,
	RecCount0=	31,
};

#define	csdelay()	microdelay(1)

static Dirtab audiodir[] =
{
	"audio",	{Qaudio},		0,	0666,
	"audioctl",	{Qaudioctl},		0,	0666,
};
#define	NPORT		(sizeof audiodir/sizeof(Dirtab))

struct Buf
{
	uchar*	virt;
	int	count;
	Buf*	next;
};
struct AQueue
{
	Lock;
	Buf*	first;
	Buf*	last;
};
struct AChan
{
	QLock;
	Rendez	r;
	Buf	buf[Nbuf];	/* buffers and queues */
	AQueue	empty;
	AQueue	full;
	Buf*	current;
	Buf*	filling;
	int	flushing;
};
static struct
{
	QLock;
	int	opened;
	int	bufinit;	/* boolean if buffers allocated */
	int	rivol[Nvol];		/* right/left input/output volumes */
	int	livol[Nvol];
	int	rovol[Nvol];
	int	lovol[Nvol];
	int	loopback;

	AChan	in;
	AChan	out;
} audio;

static	char*	encname(int);

static	int	dacload(int, int);
static	int	auxload(int, int);
static	int	adcload(int, int);
static	int	monoload(int, int);

struct Vol
{
	char*	name;
	int	flag;
	int	ilval;		/* initial values */
	int	irval;
	int	reg;
	int	(*load)(int, int);
};

static	Vol	volumes[] = {
[Vaudio]	{"audio",	    Fout, 	50,	50,	LeftDAC, dacload},
[Vaux1]		{"aux1",		Fin,	0,	0,	LeftAux1, auxload},
[Vaux2]		{"aux2",		Fin,	0,	0,	LeftAux2, auxload},
[Vline]		{"line",		Fin,	0,	0,	LeftLine, auxload},
[Vmono]		{"mono",		Fin|Fout|Fmono,	0,	0,	MonoCtl, monoload},
[Vmic]		{"mic",		Fin,	0,	0,	LeftADC, adcload},

[Vspeed]	{"rate",	Fin|Fout|Fmono,	Speed,	Speed,},
[Vchans]	{"chans",	Fin|Fout|Fmono,	2,	2,},
[Vbits]	{"bits", Fin|Fout|Fmono, 8, 8,},
	{0},
};

static struct
{
	Lock;
	int	port;
	int	irq;
	uchar	sticky;
	uchar	regs[32];
} csdev;

static	void	contininput(void);
static	void	continoutput(void);

static	char	Evolume[]	= "illegal audioctl specifier";

static	int	chatty;

#include "cs4231.h"

static int
xin(int r)
{
	int i;

	for(i=100; --i >= 0 && IN(Paddr) & 0x80;)
		csdelay();
	OUT(Paddr, r|csdev.sticky);
	csdelay();
	return IN(Pdata);
}

static void
xout(int r, int v)
{
	int i;

	for(i=100; --i >= 0 && IN(Paddr) & 0x80;)
		csdelay();
	OUT(Paddr, r|csdev.sticky);
	csdelay();
	OUT(Pdata, v);
	//csdelay();
}

static void
speaker(int on)
{
	int s;

	s = xin(PinControl);
	if(on)
		s |= Xctl0;
	else
		s &= ~Xctl0;
	xout(PinControl, s);
}

static Buf*
getbuf(AQueue *q)
{
	Buf *b;

	ilock(q);
	b = q->first;
	if(b)
		q->first = b->next;
	iunlock(q);

	return b;
}

static void
putbuf(AQueue *q, Buf *b)
{
	ilock(q);
	b->next = 0;
	if(q->first)
		q->last->next = b;
	else
		q->first = b;
	q->last = b;
	iunlock(q);
}

static void
achanreset(AChan *ac)
{
	int i;

	ac->filling = 0;
	ac->flushing = 0;
	ac->current = 0;
	ac->empty.first = 0;
	ac->empty.last = 0;
	ac->full.first = 0;
	ac->full.last = 0;
	for(i=0; i<Nbuf; i++){
		ac->buf[i].count = 0;
		putbuf(&ac->empty, &ac->buf[i]);
	}
}

static void
startoutput(void)
{
	ilock(&csdev);
	if(audio.out.current == 0)
		continoutput();
	iunlock(&csdev);
}

static void
continoutput(void)
{
	Buf *b;
	int f;
	ulong n;

	b = getbuf(&audio.out.full);
	audio.out.current = b;
	//xout(Config, xin(Config)&~PEN);
	if(b){
		n = b->count;
		dmasetup(Wdma, b->virt, n, 0);
		f = xin(OutFormat);
		if((f & Fmask) == ADPCM)
			n >>= 2;
		else{
			if((f & Fmask) == Linear16)
				n >>= 1;
			if(f & Stereo)
				n >>= 1;
		}
		n--;
		xout(PlayCount0, n);
		xout(PlayCount1, n>>8);
		xout(Config, xin(Config)|PEN);
		DPRINT("cs: out %d\n", n);
	} else
		xout(Config, xin(Config)&~PEN);
}

static void
startinput(void)
{
	ilock(&csdev);
	if(audio.in.current == 0)
		contininput();
	iunlock(&csdev);
}

static void
contininput(void)
{
	Buf *b;
	int f;
	ulong n;

	xout(Config, xin(Config)&~CEN);
	if(!audio.opened || audio.in.flushing){
		return;
	}
	b = getbuf(&audio.in.empty);
	audio.in.current = b;
	if(b){
		n = Bufsize;
		dmasetup(Rdma, b->virt, Bufsize, 1);
		f = xin(InFormat);
		if((f & Fmask) == ADPCM)
			n >>= 2;
		else{
			if((f & Fmask) == Linear16)
				n >>= 1;
			if(f & Stereo)
				n >>= 1;
		}
		n--;
		xout(RecCount0, n);
		xout(RecCount1, n>>8);
		xout(Config, xin(Config)|CEN);
		DPRINT("cs: in %d\n", n);
	}
}

static void
cswait(void)
{
	int i;

	for(i=50; --i >= 0 && IN(Paddr) & 0x80;)
		microdelay(2000);
	if(i < 0)
		print("cswait1\n");
	for(i=1000; --i >= 0 && (xin(Status) & ACI) == 0;)
		csdelay();
	for(i=1000; --i >= 0 && xin(Status) & ACI;)
		microdelay(2000);
	/* could give error(Eio) if i < 0 */
	if(i < 0)
		print("cswait2\n");
}

static int
csspeed(int freq)
{
	int i;
	static int freqtab[] = {	/* p. 33 CFS2-CFS0 */
		/* xtal1 xtal2 */
		8000, 5510,
		16000, 11025,
		27420, 18900,
		32000, 22050,
		0, 37800,
		0, 44100,
		48000, 33075,
		9600, 6620,
	};
	for(i=0; i<16; i++)
		if(freqtab[i] == freq){
			xout(OutFormat, (xin(OutFormat)&~0xF) | i);
			return 1;
		}
	return 0;
}

static void
csformat(int r, int flag, int form, int *vec)
{
	int v;

	if(form == Linear8){
		if(vec[Vbits] == 16)
			form = Linear16;
		else if(vec[Vbits] == 4)
			form = ADPCM;
	}
	if(vec[Vchans] == 2)
		form |= Stereo;
	DPRINT("csformat(%x,%x,%x)\n", r, flag, form);
	if((xin(r)&0xF0) != form){
		v = xin(Feature1);
		xout(Feature1, v|flag);
		xout(r, (xin(r)&~0xF0)|form);
		xout(Feature1, v);
	}
	csdev.regs[r] = form;
}

static void
cs4231intr(Ureg*, void*)
{
	int ir, s;
	Buf *b;

	lock(&csdev);
	csdev.sticky |= TRD;
	ir = IN(Pstatus);
	s = xin(FeatureStatus);
	if(s & PI){
		b = audio.out.current;
		audio.out.current = 0;
		dmaend(Wdma);
		continoutput();
		if(b)
			putbuf(&audio.out.empty, b);
		wakeup(&audio.out.r);
	}
	if(s & CI){
		b = audio.in.current;
		audio.in.current = 0;
		dmaend(Rdma);
		contininput();
		if(b){
			b->count = Bufsize;
			putbuf(&audio.in.full, b);
		}
		wakeup(&audio.in.r);
	}
	OUT(Pstatus, 0);
	csdev.sticky &= ~TRD;
	unlock(&csdev);
	if(s & 0xF)
		DPRINT("audiointr: #%x\n", s);
}

static int
anybuf(void *p)
{
	return ((AChan*)p)->empty.first != 0;
}

static int
anyinput(void *p)
{
	return ((AChan*)p)->full.first != 0;
}

static int
outcomplete(void *p)
{
	return ((AChan*)p)->full.first == 0 && ((AChan*)p)->current==0;
}

static int
incomplete(void *p)
{
	return ((AChan*)p)->current == 0;
}

static void
acbufinit(AChan *ac)
{
	int i;
	void *p;

	for(i=0; i<Nbuf; i++) {
		//p = xspanalloc(Bufsize, CACHELINESZ, 64*1024);
		//dcflush(p, Bufsize);
		p = xalloc(Bufsize);
		ac->buf[i].virt = UNCACHED(uchar, p);
	}
}

static void
setempty(void)
{
	ilock(&csdev);
	achanreset(&audio.in);
	achanreset(&audio.out);
	iunlock(&csdev);
}

void
cs4231reset(void)
{
}

static char mix_cvt[101] = {
	0, 0,3,7,10,13,16,19,21,23,26,28,30,32,34,35,37,39,40,42,
	43,45,46,47,49,50,51,52,53,55,56,57,58,59,60,61,62,63,64,65,
	65,66,67,68,69,70,70,71,72,73,73,74,75,75,76,77,77,78,79,79,
	80,81,81,82,82,83,84,84,85,85,86,86,87,87,88,88,89,89,90,90,
	91,91,92,92,93,93,94,94,95,95,96,96,96,97,97,98,98,98,99,99,
	100
};

static int
dacload(int r, int v)
{
	USED(r);
	DPRINT("dacload(%x,%d)\n", r, v);
	if(v == 0)
		return Mute;
	return 63-((v*63)/100);
}

static int
monoload(int r, int v)
{
	DPRINT("monoload(%x,%d)\n", r, v);
	if(v == 0)
		return r|Mute;
	return (r&~(Mute|MBY))|(15-((v*15)/100));
}

static int
auxload(int r, int v)
{
	DPRINT("auxload(%x,%d)\n", r, v);
	USED(r);
	if(v == 0)
		return Mute;
	return 31-(v*31)/100;
}

static int
adcload(int r, int v)
{
	DPRINT("adcload(%x,%d)\n", r, v);
	return (r&~0xF)|((v*15)/100)|MGE;
}

static void
mxvolume(void)
{
	Vol *v;
	int i, l, r;

	ilock(&csdev);
	speaker(0);
	for(i =0; volumes[i].name; i++){
		v = &volumes[i];
		if(v->load == 0)
			continue;
		if(v->flag & Fin){
			l = audio.livol[i];
			r = audio.rivol[i];
		} else {
			l = audio.lovol[i];
			r = audio.rovol[i];
		}
		if(l < 0)
			l = 0;
		if(r < 0)
			r = 0;
		if(l > 100)
			l = 100;
		if(r > 100)
			r = 100;
		l = mix_cvt[l];
		r = mix_cvt[r];
		if((v->flag & Fmono) == 0){
			xout(v->reg, (*v->load)(xin(v->reg), l));
			xout(v->reg+1, (*v->load)(xin(v->reg+1), r));
		} else
			xout(v->reg, (*v->load)(xin(v->reg), l));
	}
	xout(LeftADC, (xin(LeftADC)&~ISmask)|csdev.regs[LeftADC]);
	xout(RightADC, (xin(RightADC)&~ISmask)|csdev.regs[RightADC]);
	if(audio.loopback)
		xout(Loopback, xin(Loopback)|LBE);
	else
		xout(Loopback, xin(Loopback)&~LBE);
	csformat(InFormat, CMCE, csdev.regs[InFormat], audio.livol);
	csformat(OutFormat, PMCE, csdev.regs[OutFormat], audio.lovol);
	if(audio.lovol[Vaudio] || audio.rovol[Vaudio])
		speaker(1);
	iunlock(&csdev);
}

static void
flushinput(void)
{
	Buf *b;

	ilock(&csdev);
	audio.in.flushing = 1;
	iunlock(&csdev);
	qlock(&audio.in);
	if(waserror()){
		qunlock(&audio.in);
		nexterror();
	}
	sleep(&audio.in.r, incomplete, &audio.in);
	qunlock(&audio.in);
	poperror();
	ilock(&csdev);
	audio.in.flushing = 0;
	iunlock(&csdev);
	if((b = audio.in.filling) != 0){
		audio.in.filling = 0;
		putbuf(&audio.in.empty, b);
	}
	while((b = getbuf(&audio.in.full)) != 0)
		putbuf(&audio.in.empty, b);
}

static void
waitoutput(void)
{
	qlock(&audio.out);
	if(waserror()){
		qunlock(&audio.out);
		nexterror();
	}
	startoutput();
	while(!outcomplete(&audio.out))
		sleep(&audio.out.r, outcomplete, &audio.out);
	qunlock(&audio.out);
	poperror();
}

static	void
resetlevel(void)
{
	int i;

	for(i=0; volumes[i].name; i++) {
		audio.lovol[i] = volumes[i].ilval;
		audio.rovol[i] = volumes[i].irval;
		audio.livol[i] = volumes[i].ilval;
		audio.rivol[i] = volumes[i].irval;
	}
}

void
cs4231init(void)
{
	cs4231install();

	csdev.regs[LeftADC] = ISmic;
	csdev.regs[RightADC] = ISmic;
	dmasize(Wdma, 8);
	dmasize(Rdma, 8);
	csdev.sticky = 0;
	OUT(Paddr, Mode);
	csdelay();
	if((IN(Pdata) & 0x8F) != 0x8a){
		DPRINT("port %x not cs4231a: %x\n", IN(Pdata));
		return;
	}
	print("audio0: cs4231a: port %x irq %d wdma %d rdma %d\n", csdev.port, csdev.irq, Wdma, Rdma);

	resetlevel();

	cswait();
	OUT(Paddr, Mode);
	csdelay();
	OUT(Pdata, Mode2|IN(Pdata));	/* mode2 for all the trimmings */
	csdelay();
	cswait();

	csdev.sticky = MCE;
	xout(Config, Fullcal);
	csspeed(volumes[Vspeed].ilval);
	csformat(InFormat, CMCE, Linear8, audio.livol);
	csformat(OutFormat, PMCE, Linear8, audio.lovol);
	csdev.sticky &= ~MCE;
	OUT(Paddr, csdev.sticky);
	microdelay(10000);
	cswait();	/* recalibration takes ages */

	xout(FeatureStatus, 0);
	OUT(Pstatus, 0);
	setvec(csdev.irq, cs4231intr, 0);
	xout(PinControl, xin(PinControl)|IEN);
}

Chan*
cs4231attach(char *param)
{
	return devattach('A', param);
}

Chan*
cs4231clone(Chan *c, Chan *nc)
{
	return devclone(c, nc);
}

int
cs4231walk(Chan *c, char *name)
{
	return devwalk(c, name, audiodir, NPORT, devgen);
}

void
cs4231stat(Chan *c, char *db)
{
	devstat(c, db, audiodir, NPORT, devgen);
}

Chan*
cs4231open(Chan *c, int omode)
{
	switch(c->qid.path & ~CHDIR) {
	default:
		error(Eperm);
		break;

	case Qaudioctl:
	case Qdir:
		break;

	case Qaudio:
		qlock(&audio);
		if(audio.opened){
			qunlock(&audio);
			error(Einuse);
		}
		if(audio.bufinit == 0) {
			audio.bufinit = 1;
			acbufinit(&audio.in);
			acbufinit(&audio.out);
		}
		audio.opened = 1;
		setempty();
		qunlock(&audio);
		mxvolume();
		break;
	}
	c = devopen(c, omode, audiodir, NPORT, devgen);
	c->mode = openmode(omode);
	c->flag |= COPEN;
	c->offset = 0;

	return c;
}

void
cs4231create(Chan *c, char *name, int omode, ulong perm)
{
	USED(c, name, omode, perm);
	error(Eperm);
}

void
cs4231close(Chan *c)
{
	Buf *b;

	switch(c->qid.path & ~CHDIR) {
	default:
		error(Eperm);
		break;

	case Qdir:
	case Qaudioctl:
		break;

	case Qaudio:
		if(c->flag & COPEN) {
			qlock(&audio);
			audio.opened = 0;
			if(waserror()){
				qunlock(&audio);
				nexterror();
			}
			b = audio.out.filling;
			if(b){
				audio.out.filling = 0;
				putbuf(&audio.out.full, b);
			}
			waitoutput();
			flushinput();
			//tsleep(&up->sleep, return0, 0, 500);
			//speaker(0);
			qunlock(&audio);
			poperror();
		}
		break;
	}
}

long
cs4231read(Chan *c, char *a, long n, vlong offset)
{
	int liv, riv, lov, rov, ifmt, ofmt;
	long m, n0;
	char buf[350];
	Buf *b;
	int j;

	n0 = n;
	switch(c->qid.path & ~CHDIR) {
	default:
		error(Eperm);
		break;

	case Qdir:
		return devdirread(c, a, n, audiodir, NPORT, devgen);

	case Qaudio:
		qlock(&audio.in);
		if(waserror()){
			qunlock(&audio.in);
			nexterror();
		}
		while(n > 0) {
			b = audio.in.filling;
			if(b == 0) {
				b = getbuf(&audio.in.full);
				if(b == 0) {
					startinput();
					sleep(&audio.in.r, anyinput, &audio.in);
					continue;
				}
				audio.in.filling = b;
				b->count = 0;
			}
			m = Bufsize-b->count;
			if(m > n)
				m = n;
			memmove(a, b->virt+b->count, m);

			b->count += m;
			n -= m;
			a += m;
			if(b->count >= Bufsize) {
				audio.in.filling = 0;
				putbuf(&audio.in.empty, b);
			}
		}
		qunlock(&audio.in);
		poperror();
		break;

	case Qaudioctl:
		j = 0;
		buf[0] = 0;
		for(m=0; volumes[m].name; m++){
			liv = audio.livol[m];
			riv = audio.rivol[m];
			lov = audio.lovol[m];
			rov = audio.rovol[m];
			j += snprint(buf+j, sizeof(buf)-j, "%s", volumes[m].name);
			if((volumes[m].flag & Fmono) || liv==riv && lov==rov){
				if((volumes[m].flag&(Fin|Fout))==(Fin|Fout) && liv==lov)
					j += snprint(buf+j, sizeof(buf)-j, " %d", liv);
				else{
					if(volumes[m].flag & Fin)
						j += snprint(buf+j, sizeof(buf)-j, " in %d", liv);
					if(volumes[m].flag & Fout)
						j += snprint(buf+j, sizeof(buf)-j, " out %d", lov);
				}
			}else{
				if((volumes[m].flag&(Fin|Fout))==(Fin|Fout) && liv==lov && riv==rov)
					j += snprint(buf+j, sizeof(buf)-j, " left %d right %d",
						liv, riv);
				else{
					if(volumes[m].flag & Fin)
						j += snprint(buf+j, sizeof(buf)-j, " in left %d right %d",
							liv, riv);
					if(volumes[m].flag & Fout)
						j += snprint(buf+j, sizeof(buf)-j, " out left %d right %d",
							lov, rov);
				}
			}
			j += snprint(buf+j, sizeof(buf)-j, "\n");
		}
		ifmt = xin(InFormat);
		ofmt = xin(OutFormat);
		if(ifmt != ofmt){
			j += snprint(buf+j, sizeof(buf)-j, "in enc %s\n", encname(ifmt));
			j += snprint(buf+j, sizeof(buf)-j, "out enc %s\n", encname(ofmt));
		} else
			j += snprint(buf+j, sizeof(buf)-j, "enc %s\n", encname(ifmt));
		j += snprint(buf+j, sizeof(buf)-j, "loop %d\n", audio.loopback);
		{int i; for(i=0; i<32; i++){j += snprint(buf+j, sizeof(buf)-j, " %d:%x", i, xin(i)); }j += snprint(buf+j,sizeof(buf)-j,"\n");}
		USED(j);

		return readstr(offset, a, n, buf);
	}
	return n0-n;
}

Block*
cs4231bread(Chan *c, long n, ulong offset)
{
	return devbread(c, n, offset);
}

long
cs4231write(Chan *c, char *a, long n, vlong offset)
{
	long m, n0;
	int i, nf, v, left, right, in, out, fmt, doload;
	char buf[255], *field[Ncmd];
	Buf *b;

	USED(offset);

	n0 = n;
	switch(c->qid.path & ~CHDIR) {
	default:
		error(Eperm);
		break;

	case Qaudioctl:
		waitoutput();
		flushinput();
		qlock(&audio);
		if(waserror()){
			qunlock(&audio);
			nexterror();
		}
		v = Vaudio;
		doload = 0;
		left = 1;
		right = 1;
		in = 1;
		out = 1;
		if(n > sizeof(buf)-1)
			n = sizeof(buf)-1;
		memmove(buf, a, n);
		buf[n] = '\0';

		nf = getfields(buf, field, Ncmd, 1, " \t\n,");
		for(i = 0; i < nf; i++){
			/*
			 * a number is volume
			 */
			if(field[i][0] >= '0' && field[i][0] <= '9') {
				m = strtoul(field[i], 0, 10);
				if(left && out)
					audio.lovol[v] = m;
				if(left && in)
					audio.livol[v] = m;
				if(right && out)
					audio.rovol[v] = m;
				if(right && in)
					audio.rivol[v] = m;
				if(v == Vspeed){
					ilock(&csdev);
					csdev.sticky = MCE;
					csspeed(m);
					csdev.sticky &= ~MCE;
					OUT(Paddr, csdev.sticky);
					microdelay(10000);
					cswait();
					iunlock(&csdev);
				} else
					doload = 1;
				continue;
			}

			for(m=0; volumes[m].name; m++) {
				if(strcmp(field[i], volumes[m].name) == 0) {
					v = m;
					in = 1;
					out = 1;
					left = 1;
					right = 1;
					break;
				}
			}
			if(volumes[m].name)
				continue;

			if(strcmp(field[i], "chat") == 0){
				chatty = !chatty;
				continue;
			}

			if(strcmp(field[i], "reset") == 0) {
				resetlevel();
				doload = 1;
				continue;
			}
			if(strcmp(field[i], "loop") == 0) {
				if(++i >= nf)
					error(Evolume);
				audio.loopback = strtoul(field[i], 0, 10);
				doload = 1;
				continue;
			}
			if(strcmp(field[i], "enc") == 0) {
				if(++i >= nf)
					error(Evolume);
				fmt = -1;
				if(strcmp(field[i], "ulaw") == 0)
					fmt = uLaw;
				else if(strcmp(field[i], "alaw") == 0)
					fmt = aLaw;
				else if(strcmp(field[i], "pcm") == 0)
					fmt = Linear8;
				else if(strcmp(field[i], "adpcm") == 0)
					fmt = ADPCM;
				else
					error(Evolume);
				if(in)
					csdev.regs[InFormat] = fmt;
				if(out)
					csdev.regs[OutFormat] = fmt;
				doload = 1;
				continue;
			}
			if(strcmp(field[i], "dev") == 0) {
				if(++i >= nf)
					error(Evolume);
				if(in){
					fmt = -1;
					if(strcmp(field[i], "mic") == 0)
						fmt = ISmic;
					else if(strcmp(field[i], "line") == 0)
						fmt = ISline;
					else if(strcmp(field[i], "aux1") == 0)
						fmt = ISaux1;
					else if(strcmp(field[i], "loop") == 0)
						fmt = ISloop;
					else
						error(Evolume);
					if(left)
						csdev.regs[LeftADC] = fmt;
					if(right)
						csdev.regs[RightADC] = fmt;
					doload = 1;
				}
				continue;
			}
			if(strcmp(field[i], "in") == 0) {
				in = 1;
				out = 0;
				continue;
			}
			if(strcmp(field[i], "out") == 0) {
				in = 0;
				out = 1;
				continue;
			}
			if(strcmp(field[i], "left") == 0) {
				left = 1;
				right = 0;
				continue;
			}
			if(strcmp(field[i], "right") == 0) {
				left = 0;
				right = 1;
				continue;
			}
			error(Evolume);
		}
		if(doload)
			mxvolume();
		qunlock(&audio);
		poperror();
		n=0;
		break;

	case Qaudio:
		qlock(&audio.out);
		if(waserror()){
			qunlock(&audio.out);
			nexterror();
		}
		while(n > 0) {
			b = audio.out.filling;
			if(b == 0) {
				b = getbuf(&audio.out.empty);
				if(b == 0) {
					startoutput();
					sleep(&audio.out.r, anybuf, &audio.out);
					continue;
				}
				b->count = 0;
				audio.out.filling = b;
			}

			m = Bufsize-b->count;
			if(m > n)
				m = n;
			memmove(b->virt+b->count, a, m);

			b->count += m;
			n -= m;
			a += m;
			if(b->count >= Bufsize) {
				audio.out.filling = 0;
				putbuf(&audio.out.full, b);
			}
		}
		qunlock(&audio.out);
		poperror();
		break;
	}
	return n0 - n;
}

long
cs4231bwrite(Chan *c, Block *bp, ulong offset)
{
	return devbwrite(c, bp, offset);
}

void
cs4231remove(Chan *c)
{
	USED(c);
	error(Eperm);
}

void
cs4231wstat(Chan *c, char *dp)
{
	USED(c, dp);
	error(Eperm);
}

static char *
encname(int v)
{
	switch(v & ~(0xF|Stereo)){
	case uLaw:	return "ulaw";
	case aLaw:	return "alaw";
	case Linear8:	return "pcm";
	case Linear16:	return "pcm16";
	case ADPCM:	return "adpcm";
	default:	return "?";
	}
}