shithub: battleship

Download patch

ref: b221c56a6bb90de4631f4b2eb999db8dad05a006
parent: 9c377e4f25344a908f18f5154ac9143ffb9577e6
author: rodri <rgl@antares-labs.eu>
date: Sat Sep 30 20:06:50 EDT 2023

add a mixer and some background sfx.

also postponed the connection establishment
until the subsystem initialization is
complete. there's no point in taking a seat
you're going to leave milliseconds after
a local resource failure.

binary files /dev/null b/assets/sfx/bg0.mp3 differ
binary files /dev/null b/assets/sfx/bg1.mp3 differ
binary files /dev/null b/assets/sfx/bg2.mp3 differ
binary files /dev/null b/assets/sfx/defeat.mp3 differ
--- a/bts.c
+++ b/bts.c
@@ -1,5 +1,6 @@
 #include <u.h>
 #include <libc.h>
+#include <bio.h>
 #include <thread.h>
 #include <draw.h>
 #include <mouse.h>
@@ -8,6 +9,7 @@
 #include <geometry.h>
 #include "dat.h"
 #include "fns.h"
+#include "mixer.h"
 
 enum {
 	PCBlack,
@@ -137,6 +139,7 @@
 Image *pal[NCOLORS];
 Image *screenb;
 Image *tiletab[NTILES];
+AudioSource *playlist[NSOUNDS];
 Board alienboard;
 Board localboard;
 Ship armada[NSHIPS];
@@ -538,6 +541,26 @@
 	}
 }
 
+void
+initsound(void)
+{
+	audio_init(44100);
+	audio_set_master_gain(0.5);
+
+	playlist[SBG0] = audio_new_source_from_mp3file("assets/sfx/bg0.mp3");
+	if(playlist[SBG0] == nil)
+		sysfatal("audio_new_source_from_mp3file: %r");
+	playlist[SBG1] = audio_new_source_from_mp3file("assets/sfx/bg1.mp3");
+	if(playlist[SBG1] == nil)
+		sysfatal("audio_new_source_from_mp3file: %r");
+	playlist[SBG2] = audio_new_source_from_mp3file("assets/sfx/bg2.mp3");
+	if(playlist[SBG2] == nil)
+		sysfatal("audio_new_source_from_mp3file: %r");
+
+	audio_play(playlist[SBG0]);
+	audio_set_loop(playlist[SBG0], 1);
+}
+
 int
 confirmdone(Mousectl *mc)
 {
@@ -917,6 +940,24 @@
 }
 
 void
+soundproc(void *)
+{
+	Biobuf *aout;
+	uchar adata[512];
+
+	threadsetname("soundproc");
+
+	aout = Bopen("/dev/audio", OWRITE);
+	if(aout == nil)
+		sysfatal("Bopen: %r");
+
+	for(;;){
+		audio_process((void*)adata, sizeof(adata)/2);
+		Bwrite(aout, adata, sizeof adata);
+	}
+}
+
+void
 netrecvthread(void *arg)
 {
 	Ioproc *io;
@@ -988,16 +1029,6 @@
 	if(argc != 1)
 		usage();
 
-	addr = netmkaddr(argv[0], "tcp", "3047");
-	if(debug)
-		fprint(2, "connecting to %s\n", addr);
-
-	fd = dial(addr, nil, nil, nil);
-	if(fd < 0)
-		sysfatal("dial: %r");
-	else if(debug)
-		fprint(2, "line established\n");
-
 	snprint(winspec, sizeof winspec, "-dx %d -dy %d", SCRW, SCRH);
 	if(newwindow(winspec) < 0)
 		sysfatal("newwindow: %r");
@@ -1033,6 +1064,18 @@
 	game.state = Waiting0;
 
 	/* TODO add sfx */
+	initsound();
+	proccreate(soundproc, nil, mainstacksize);
+
+	addr = netmkaddr(argv[0], "tcp", "3047");
+	if(debug)
+		fprint(2, "connecting to %s\n", addr);
+
+	fd = dial(addr, nil, nil, nil);
+	if(fd < 0)
+		sysfatal("dial: %r");
+	else if(debug)
+		fprint(2, "line established\n");
 
 	drawchan = chancreate(sizeof(void*), 1);
 	ingress = chancreate(sizeof(char*), 1);
--- a/dat.h
+++ b/dat.h
@@ -37,6 +37,11 @@
 		Borderwidth+MAPH*TH+Borderwidth+
 		Boardmargin,
 
+	SBG0 = 0,
+	SBG1,
+	SBG2,
+	NSOUNDS,
+
 	KB = 1024,
 	BY2MAP = (TBITS*MAPW*MAPH+7)/8,
 };
--- /dev/null
+++ b/mixer.c
@@ -1,0 +1,762 @@
+/*
+ * standalone sound mixer based on rxi's cmixer.
+ */
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include "mixer.h"
+
+static Mixer mixer;
+
+
+static int wav_init(AudioSourceInfo*, void*, int, int);
+static int mp3_init(AudioSourceInfo*, int);
+
+static int
+min(int a, int b)
+{
+	return a < b? a: b;
+}
+
+static int
+max(int a, int b)
+{
+	return a > b? a: b;
+}
+
+static int
+clamp(int n, int min, int max)
+{
+	return n < min? min: n > max? max: n;
+}
+
+static double
+fclamp(double n, double min, double max)
+{
+	return n < min? min: n > max? max: n;
+}
+
+static int
+float2fixed(double n)
+{
+	return n*FIXED_UNIT;
+}
+
+static int
+fixedlerp(int a, int b, int t)
+{
+	return a + ((b - a)*t >> FIXED_BITS);
+}
+
+//static void
+//fprintsource(int fd, AudioSource *src)
+//{
+//	fprint(fd, "src 0x%p:\n"
+//		"udata\t0x%p\n"
+//		"samplerate\t%d\n"
+//		"length\t%d\n"
+//		"end\t%d\n"
+//		"state\t%d\n"
+//		"position\t%lld\n"
+//		"gain l/r\t%d/%d\n"
+//		"rate\t%d\n"
+//		"nextfill\t%d\n"
+//		"loop\t%d\n"
+//		"rewind\t%d\n"
+//		"active\t%d\n"
+//		"gain\t%g\n"
+//		"pan\t%g\n",
+//		src, src->udata, src->samplerate, src->length, src->end, src->state, src->position,
+//		src->lgain, src->rgain, src->rate, src->nextfill, src->loop, src->rewind,
+//		src->active, src->gain, src->pan);
+//
+//}
+
+void
+audio_init(int samplerate)
+{
+	mixer.samplerate = samplerate;
+	mixer.sources = nil;
+	mixer.gain = FIXED_UNIT;
+}
+
+void
+audio_set_master_gain(double gain)
+{
+	mixer.gain = float2fixed(gain);
+}
+
+static void
+rewind_source(AudioSource *src)
+{
+	AudioEvent e;
+
+	e.type = AUDIO_EVENT_REWIND;
+	e.udata = src->udata;
+	src->handler(&e);
+	src->position = 0;
+	src->rewind = 0;
+	src->end = src->length;
+	src->nextfill = 0;
+}
+
+static void
+fill_source_buffer(AudioSource *src, int offset, int length)
+{
+	AudioEvent e;
+
+	e.type = AUDIO_EVENT_SAMPLES;
+	e.udata = src->udata;
+	e.buffer = src->buffer + offset;
+	e.length = length;
+	src->handler(&e);
+}
+
+static void
+process_source(AudioSource *src, int len)
+{
+	int i, n, a, b, p;
+	int frame, count;
+	s32int *dst;
+
+	dst = mixer.buffer;
+
+	/* Do rewind if flag is set */
+	if(src->rewind)
+		rewind_source(src);
+
+	/* Process audio */
+	while(len > 0){
+		/* Get current position frame */
+		frame = src->position >> FIXED_BITS;
+
+		/* Fill buffer if required */
+		if(frame + 3 >= src->nextfill){
+			fill_source_buffer(src, 2*src->nextfill & MIXBUFMASK, MIXBUFSIZE/2);
+			src->nextfill += MIXBUFSIZE/4;
+		}
+
+		/* Handle reaching the end of the playthrough */
+		if(frame >= src->end){
+			/*
+			 * As streams continously fill the raw buffer in a loop we simply
+			 * increment the end idx by one length and continue reading from it for
+			 * another play-through
+			 */
+			src->end = frame + src->length;
+			/* Set state and stop processing if we're not set to loop */
+			if(!src->loop){
+				src->state = AUDIO_STATE_STOPPED;
+				break;
+			}
+		}
+
+		/* Work out how many frames we should process in the loop */
+		n = min(src->nextfill - 2, src->end) - frame;
+		count = (n << FIXED_BITS) / src->rate;
+		count = max(count, 1);
+		count = min(count, len/2);
+		len -= count * 2;
+
+		/* Add audio to master buffer */
+		if(src->rate == FIXED_UNIT){
+			/* Add audio to buffer -- basic */
+			n = 2*frame;
+			for(i = 0; i < count; i++){
+				dst[0] += (src->buffer[n&MIXBUFMASK] * src->lgain) >> FIXED_BITS;
+				dst[1] += (src->buffer[(n+1)&MIXBUFMASK] * src->rgain) >> FIXED_BITS;
+				n += 2;
+				dst += 2;
+			}
+			src->position += count * FIXED_UNIT;
+		}else{
+			/* Add audio to buffer -- interpolated */
+			for(i = 0; i < count; i++){
+				n = (src->position >> FIXED_BITS) * 2;
+				p = src->position & FIXED_MASK;
+
+				a = src->buffer[n & MIXBUFMASK];
+				b = src->buffer[(n+2) & MIXBUFMASK];
+				dst[0] += fixedlerp(a, b, p)*src->lgain >> FIXED_BITS;
+				n++;
+				a = src->buffer[n & MIXBUFMASK];
+				b = src->buffer[(n+2) & MIXBUFMASK];
+				dst[1] += fixedlerp(a, b, p)*src->rgain >> FIXED_BITS;
+
+				src->position += src->rate;
+				dst += 2;
+			}
+		}
+	}
+}
+
+void
+audio_process(s16int *dst, int len)
+{
+	int i, x;
+	AudioSource **s;
+
+	/* Process in chunks of MIXBUFSIZE if `len` is larger than MIXBUFSIZE */
+	while(len > MIXBUFSIZE){
+		audio_process(dst, MIXBUFSIZE);
+		dst += MIXBUFSIZE;
+		len -= MIXBUFSIZE;
+	}
+
+	/* Zeroset internal buffer */
+	memset(mixer.buffer, 0, len * sizeof(mixer.buffer[0]));
+
+	/* Process active sources */
+	s = &mixer.sources;
+	while(*s){
+		process_source(*s, len);
+		/* Remove source from list if it is no longer playing */
+		if((*s)->state != AUDIO_STATE_PLAYING){
+			(*s)->active = 0;
+			*s = (*s)->next;
+		}else
+			s = &(*s)->next;
+	}
+
+	/* Copy internal buffer to destination and clip */
+	for(i = 0; i < len; i++){
+		x = (mixer.buffer[i] * mixer.gain) >> FIXED_BITS;
+		dst[i] = clamp(x, -32768, 32767);
+	}
+}
+
+AudioSource *
+audio_new_source(AudioSourceInfo *info)
+{
+	AudioSource *src;
+
+	src = malloc(sizeof *src);
+	if(src == nil){
+		werrstr("allocation failed");
+		return nil;
+	}
+
+	memset(src, 0, sizeof *src);
+	src->handler = info->handler;
+	src->length = info->length;
+	src->samplerate = info->samplerate;
+	src->udata = info->udata;
+	audio_set_gain(src, 1);
+	audio_set_pan(src, 0);
+	audio_set_pitch(src, 1);
+	audio_set_loop(src, 0);
+	audio_stop(src);
+	return src;
+}
+
+static int
+check_header(void *data, int size, char *str, int offset)
+{
+	int len;
+
+	len = strlen(str);
+	return size >= offset + len &&
+		memcmp((char*)data + offset, str, len) == 0;
+}
+
+static AudioSource *
+new_source_from_mem(void *data, int size, int ownsdata)
+{
+	AudioSourceInfo info;
+
+	if(check_header(data, size, "WAVE", 8)){
+		if(wav_init(&info, data, size, ownsdata) < 0)
+			return nil;
+		return audio_new_source(&info);
+	}
+
+	werrstr("unknown format or invalid data");
+	return nil;
+}
+
+static void *
+load_file(char *filename, int *size)
+{
+	Biobuf *fp;
+	void *data;
+	int n;
+
+	fp = Bopen(filename, OREAD);
+	if(fp == nil)
+		return nil;
+
+	Bseek(fp, 0, 2);
+	*size = Boffset(fp);
+	Bseek(fp, 0, 0);
+
+	data = malloc(*size);
+	if(data == nil){
+		Bterm(fp);
+		return nil;
+	}
+	n = Bread(fp, data, *size);
+	Bterm(fp);
+	if(n != *size){
+		free(data);
+		return nil;
+	}
+
+	return data;
+}
+
+AudioSource *
+audio_new_source_from_file(char *filename)
+{
+	int size;
+	AudioSource *src;
+	void *data;
+
+	/* Load file into memory */
+	data = load_file(filename, &size);
+	if(data == nil){
+		werrstr("could not load file");
+		return nil;
+	}
+
+	/* Try to load and return */
+	src = new_source_from_mem(data, size, 1);
+	if(src == nil){
+		free(data);
+		return nil;
+	}
+
+	return src;
+}
+
+AudioSource *
+audio_new_source_from_mp3file(char *path)
+{
+	AudioSourceInfo info;
+	uchar buf[3];
+	int fd;
+
+	fd = open(path, OREAD);
+	if(fd < 0)
+		return nil;
+
+	memset(buf, 0, sizeof buf);
+	readn(fd, buf, sizeof buf);
+	seek(fd, 0, 0);
+	if(memcmp(buf, "ID3", 3) != 0 && (buf[0] != 0xFF || buf[1] != 0xFB)){
+		werrstr("bad mp3 file");
+		close(fd);
+		return nil;
+	}
+
+	if(mp3_init(&info, fd) < 0){
+		close(fd);
+		return nil;
+	}
+	close(fd);
+
+	return audio_new_source(&info);
+}
+
+AudioSource *
+audio_new_source_from_mem(void *data, int size)
+{
+	return new_source_from_mem(data, size, 0);
+}
+
+void
+audio_destroy_source(AudioSource *src)
+{
+	AudioSource **s;
+	AudioEvent e;
+
+	if(src->active){
+		s = &mixer.sources;
+		while(*s) /* TODO potential spinlock. no bueno */
+			if(*s == src){
+				*s = src->next;
+				break;
+			}
+	}
+
+	e.type = AUDIO_EVENT_DESTROY;
+	e.udata = src->udata;
+	src->handler(&e);
+	free(src);
+}
+
+double
+audio_get_length(AudioSource *src)
+{
+	return src->length / (double)src->samplerate;
+}
+
+double
+audio_get_position(AudioSource *src)
+{
+	return ((src->position >> FIXED_BITS) % src->length) / (double)src->samplerate;
+}
+
+int
+audio_get_state(AudioSource *src)
+{
+	return src->state;
+}
+
+static void
+recalc_source_gains(AudioSource *src)
+{
+	double l, r;
+	double pan;
+
+	pan = src->pan;
+	l = src->gain * (pan <= 0 ? 1 : 1.0 - pan);
+	r = src->gain * (pan >= 0 ? 1 : 1.0 + pan);
+	src->lgain = float2fixed(l);
+	src->rgain = float2fixed(r);
+}
+
+void
+audio_set_gain(AudioSource *src, double gain)
+{
+	src->gain = gain;
+	recalc_source_gains(src);
+}
+
+void
+audio_set_pan(AudioSource *src, double pan)
+{
+	src->pan = fclamp(pan, -1.0, 1.0);
+	recalc_source_gains(src);
+}
+
+void
+audio_set_pitch(AudioSource *src, double pitch)
+{
+	double rate;
+
+	if(pitch > 0)
+		rate = (double)src->samplerate / mixer.samplerate * pitch;
+	else
+		rate = 0.001;
+	src->rate = float2fixed(rate);
+}
+
+void
+audio_set_loop(AudioSource *src, int loop)
+{
+	src->loop = loop;
+}
+
+void
+audio_play(AudioSource *src)
+{
+	src->state = AUDIO_STATE_PLAYING;
+	if(!src->active){
+		src->active = 1;
+		src->next = mixer.sources;
+		mixer.sources = src;
+	}
+}
+
+void
+audio_pause(AudioSource *src)
+{
+	src->state = AUDIO_STATE_PAUSED;
+}
+
+void
+audio_stop(AudioSource *src)
+{
+	src->state = AUDIO_STATE_STOPPED;
+	src->rewind = 1;
+}
+
+/*
+ * Wav stream processing
+ */
+
+static char *
+find_subchunk(char *data, int len, char *id, int *size)
+{
+	/* TODO : Error handling on malformed wav file */
+	int idlen;
+	char *p;
+
+	idlen = strlen(id);
+	p = data+12;
+next:
+	*size = *((u32int*)(p+4));
+	if(memcmp(p, id, idlen) != 0){
+		p += 8 + *size;
+		if(p > data + len)
+			return nil;
+		goto next;
+	}
+	return p+8;
+}
+
+static int
+read_wav(Wav *w, void *data, int len)
+{
+	uint format, channels, samplerate, bitdepth;
+	int sz;
+	char *p;
+
+	p = data;
+	memset(w, 0, sizeof(*w));
+
+	/* Check header */
+	if(memcmp(p, "RIFF", 4) != 0 || memcmp(p+8, "WAVE", 4) != 0){
+		werrstr("bad wav header");
+		return -1;
+	}
+
+	/* Find fmt subchunk */
+	p = find_subchunk(data, len, "fmt", &sz);
+	if(p == nil){
+		werrstr("no fmt subchunk");
+		return -1;
+	}
+
+	/* Load fmt info */
+	format		= *((u16int*)(p));
+	channels	= *((u16int*)(p+2));
+	samplerate	= *((u32int*)(p+4));
+	bitdepth	= *((u16int*)(p+14));
+
+	if(format != 1){
+		werrstr("unsupported format");
+		return -1;
+	}
+	if(channels == 0 || samplerate == 0 || bitdepth == 0){
+		werrstr("bad format");
+		return -1;
+	}
+	if(channels > 2 || (bitdepth != 16 && bitdepth != 8)){
+		werrstr("unsupported wav format");
+		return -1;
+	}
+
+	/* Find data subchunk */
+	p = find_subchunk(data, len, "data", &sz);
+	if(p == nil){
+		werrstr("no data subchunk");
+		return -1;
+	}
+
+	/* Init struct */
+	w->data = (void*)p;
+	w->samplerate = samplerate;
+	w->channels = channels;
+	w->length = (sz / (bitdepth/8)) / channels;
+	w->bitdepth = bitdepth;
+	return 0;
+}
+
+static void
+wav_handler(AudioEvent *e)
+{
+	int x, n;
+	s16int *dst;
+	WavStream *s;
+	int len;
+
+	s = e->udata;
+
+	switch(e->type){
+	case AUDIO_EVENT_DESTROY:
+		free(s->data);
+		free(s);
+		break;
+	case AUDIO_EVENT_SAMPLES:
+		dst = e->buffer;
+		len = e->length/2;
+fill:
+		n = min(len, s->wav.length - s->idx);
+		len -= n;
+		switch(s->wav.bitdepth){
+		case 16:
+			if(s->wav.channels == 1){
+				while(n--){
+					dst[0] = dst[1] = ((s16int*)s->wav.data)[s->idx];
+					dst += 2;
+					s->idx++;
+				}
+			}else if(s->wav.channels == 2){
+				while(n--){
+					x = s->idx * 2;
+					dst[0] = ((s16int*)s->wav.data)[x];
+					dst[1] = ((s16int*)s->wav.data)[x+1];
+					dst += 2;
+					s->idx++;
+				}
+			}
+			break;
+		case 8:
+			if(s->wav.channels == 1){
+				while(n--){
+					dst[0] = dst[1] = (((uchar*)s->wav.data)[s->idx] - 128) << 8;
+					dst += 2;
+					s->idx++;
+				}
+			}else if(s->wav.channels == 2){
+				while(n--){
+					x = s->idx * 2;
+					dst[0] = (((uchar*)s->wav.data)[x] - 128) << 8;
+					dst[1] = (((uchar*)s->wav.data)[x+1] - 128) << 8;
+					dst += 2;
+					s->idx++;
+				}
+			}
+			break;
+		}
+		/* Loop back and continue filling buffer if we didn't fill the buffer */
+		if(len > 0){
+			s->idx = 0;
+			goto fill;
+		}
+		break;
+	case AUDIO_EVENT_REWIND:
+		s->idx = 0;
+		break;
+	}
+}
+
+static int
+wav_init(AudioSourceInfo *info, void *data, int len, int ownsdata)
+{
+	WavStream *stream;
+
+	stream = malloc(sizeof *stream);
+	if(stream == nil){
+		werrstr("allocation failed");
+		return -1;
+	}
+	memset(stream, 0, sizeof *stream);
+
+	if(read_wav(&stream->wav, data, len) < 0)
+		return -1;
+
+	if(ownsdata)
+		stream->data = data;
+	stream->idx = 0;
+
+	info->udata = stream;
+	info->handler = wav_handler;
+	info->samplerate = stream->wav.samplerate;
+	info->length = stream->wav.length;
+
+	return 0;
+}
+
+static void
+pcm_handler(AudioEvent *e)
+{
+	Pcm *pcm;
+	s16int *dst;
+	int len, i, n;
+
+	pcm = e->udata;
+
+	switch(e->type){
+	case AUDIO_EVENT_DESTROY:
+		free(pcm->data);
+		free(pcm);
+		break;
+	case AUDIO_EVENT_SAMPLES:
+		dst = e->buffer;
+		len = e->length/2;
+Fillbuf:
+		n = min(len, pcm->len - pcm->off);
+		len -= n;
+		while(n--){
+			i = pcm->off * 2;
+			dst[0] = ((s16int*)pcm->data)[i];
+			dst[1] = ((s16int*)pcm->data)[i+1];
+			dst += 2;
+			pcm->off++;
+		}
+		if(len > 0){
+			pcm->off = 0;
+			goto Fillbuf;
+		}
+		break;
+	case AUDIO_EVENT_REWIND:
+		pcm->off = 0;
+		break;
+	}
+}
+
+static void
+mp3decproc(void *arg)
+{
+	int *pfd, fd;
+
+	pfd = arg;
+	fd = pfd[2];
+
+	close(pfd[0]);
+	dup(fd, 0);
+	close(fd);
+	dup(pfd[1], 1);
+	close(pfd[1]);
+
+	execl("/bin/audio/mp3dec", "mp3dec", nil);
+	threadexitsall("execl: %r");
+}
+
+static int
+mp3_init(AudioSourceInfo *info, int fd)
+{
+	Pcm *pcm;
+	void *data;
+	uchar buf[1024];
+	int pfd[3], n, len;
+
+	data = nil;
+	len = 0;
+
+	if(pipe(pfd) < 0){
+		werrstr("pipe: %r");
+		return -1;
+	}
+	pfd[2] = fd;
+
+	procrfork(mp3decproc, pfd, mainstacksize, RFFDG|RFNAMEG|RFNOTEG);
+	close(pfd[1]);
+	while((n = read(pfd[0], buf, sizeof buf)) > 0){
+		data = realloc(data, len+n);
+		if(data == nil){
+			werrstr("realloc: %r");
+			return -1;
+		}
+		memmove((uchar*)data+len, buf, n);
+		len += n;
+	}
+	close(pfd[0]);
+
+	pcm = malloc(sizeof *pcm);
+	if(pcm == nil){
+		free(data);
+		werrstr("malloc: %r");
+		return -1;
+	}
+	pcm->depth = 16;
+	pcm->chans = 2;
+	pcm->rate = 44100;
+	pcm->data = data;
+	pcm->len = len/(pcm->depth/8)/pcm->chans;
+
+	info->udata = pcm;
+	info->handler = pcm_handler;
+	info->samplerate = pcm->rate;
+	info->length = pcm->len;
+
+//	fprint(2, "pcm 0x%p:\ndata 0x%p\nlen %d\ndepth %d\nchans %d\nrate %d\n",
+//		pcm, pcm->data, pcm->len, pcm->depth, pcm->chans, pcm->rate);
+//	fprint(2, "info 0x%p:\nudata 0x%p\nhandler 0x%p\nsamplerate %d\nlength %d\n",
+//		info, info->udata, info->handler, info->samplerate, info->length);
+
+	return 0;
+}
--- /dev/null
+++ b/mixer.h
@@ -1,0 +1,123 @@
+/*
+ * standalone sound mixer based on rxi's cmixer.
+ */
+enum {
+	FIXED_BITS	= 12,
+	FIXED_UNIT	= 1<<FIXED_BITS,
+	FIXED_MASK	= FIXED_UNIT-1,
+
+	MIXBUFSIZE	= 512,
+	MIXBUFMASK	= MIXBUFSIZE-1,
+
+	AUDIO_STATE_STOPPED = 0,
+	AUDIO_STATE_PLAYING,
+	AUDIO_STATE_PAUSED,
+
+	AUDIO_EVENT_DESTROY = 0,
+	AUDIO_EVENT_SAMPLES,
+	AUDIO_EVENT_REWIND
+};
+
+typedef struct Wav Wav;
+typedef struct WavStream WavStream;
+typedef struct Pcm Pcm;
+typedef struct AudioEvent AudioEvent;
+typedef struct AudioSourceInfo AudioSourceInfo;
+typedef struct AudioSource AudioSource;
+typedef struct Mixer Mixer;
+
+struct Wav
+{
+	void *data;
+	int bitdepth;
+	int samplerate;
+	int channels;
+	int length;
+};
+
+struct WavStream
+{
+	Wav wav;
+	void *data;
+	int idx;
+};
+
+struct Pcm
+{
+	void *data;
+	int len;
+	int off;
+	int depth;
+	int chans;
+	int rate;
+};
+
+struct AudioEvent
+{
+	int type;
+	void *udata;
+	char *msg;
+	s16int *buffer;
+	int length;
+};
+
+struct AudioSourceInfo
+{
+	void *udata;
+	int samplerate;
+	int length;
+
+	void (*handler)(AudioEvent *e);	/* Event handler */
+};
+
+struct AudioSource
+{
+	AudioSource *next;		/* Next source in list */
+	s16int buffer[MIXBUFSIZE];	/* Internal buffer with raw stereo PCM */
+	void *udata;			/* Stream's udata (from AudioSourceInfo) */
+	int samplerate;			/* Stream's native samplerate */
+	int length;			/* Stream's length in samples */
+	int end;			/* End index for the current play-through */
+	int state;			/* Current state (playing|paused|stopped) */
+	s64int position;		/* Current playhead position (fixed point) */
+	int lgain, rgain;		/* Left and right gain (fixed point) */
+	int rate;			/* Playback rate (fixed point) */
+	int nextfill;			/* Next frame idx where the buffer needs to be filled */
+	int loop;			/* Whether the source will loop when `end` is reached */
+	int rewind;			/* Whether the source will rewind before playing */
+	int active;			/* Whether the source is part of `sources` list */
+	double gain;			/* Gain set by `audio_set_gain()` */
+	double pan;			/* Pan set by `audio_set_pan()` */
+
+	void (*handler)(AudioEvent *e);	/* Event handler */
+};
+
+struct Mixer
+{
+	char *lasterror;		/* Last error message */
+	AudioSource *sources;		/* Linked list of active (playing) sources */
+	s32int buffer[MIXBUFSIZE];	/* Internal master buffer */
+	int samplerate;			/* Master samplerate */
+	int gain;			/* Master gain (fixed point) */
+};
+
+
+void audio_init(int);
+void audio_set_master_gain(double);
+void audio_process(s16int*, int);
+
+AudioSource *audio_new_source(AudioSourceInfo*);
+AudioSource *audio_new_source_from_file(char*);
+AudioSource *audio_new_source_from_mp3file(char*);
+AudioSource *audio_new_source_from_mem(void*, int);
+void audio_destroy_source(AudioSource*);
+double audio_get_length(AudioSource*);
+double audio_get_position(AudioSource*);
+int audio_get_state(AudioSource*);
+void audio_set_gain(AudioSource*, double);
+void audio_set_pan(AudioSource*, double);
+void audio_set_pitch(AudioSource*, double);
+void audio_set_loop(AudioSource*, int);
+void audio_play(AudioSource*);
+void audio_pause(AudioSource*);
+void audio_stop(AudioSource*);
--- a/mkfile
+++ b/mkfile
@@ -11,10 +11,12 @@
 	parse.$O\
 	util.$O\
 	menulist.$O\
+	mixer.$O\
 
 HFILES=\
 	dat.h\
 	fns.h\
+	mixer.h\
 
 </sys/src/cmd/mkmany