From b221c56a6bb90de4631f4b2eb999db8dad05a006 Mon Sep 17 00:00:00 2001 From: rodri Date: Sun, 1 Oct 2023 00:06:50 +0000 Subject: 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. --- assets/sfx/bg0.mp3 | Bin 0 -> 2763786 bytes assets/sfx/bg1.mp3 | Bin 0 -> 3887646 bytes assets/sfx/bg2.mp3 | Bin 0 -> 4393795 bytes assets/sfx/defeat.mp3 | Bin 0 -> 254612 bytes bts.c | 63 ++++- dat.h | 5 + mixer.c | 762 ++++++++++++++++++++++++++++++++++++++++++++++++++ mixer.h | 123 ++++++++ mkfile | 2 + 9 files changed, 945 insertions(+), 10 deletions(-) create mode 100644 assets/sfx/bg0.mp3 create mode 100644 assets/sfx/bg1.mp3 create mode 100644 assets/sfx/bg2.mp3 create mode 100644 assets/sfx/defeat.mp3 create mode 100644 mixer.c create mode 100644 mixer.h diff --git a/assets/sfx/bg0.mp3 b/assets/sfx/bg0.mp3 new file mode 100644 index 0000000..44f7603 Binary files /dev/null and b/assets/sfx/bg0.mp3 differ diff --git a/assets/sfx/bg1.mp3 b/assets/sfx/bg1.mp3 new file mode 100644 index 0000000..7909865 Binary files /dev/null and b/assets/sfx/bg1.mp3 differ diff --git a/assets/sfx/bg2.mp3 b/assets/sfx/bg2.mp3 new file mode 100644 index 0000000..90f25e1 Binary files /dev/null and b/assets/sfx/bg2.mp3 differ diff --git a/assets/sfx/defeat.mp3 b/assets/sfx/defeat.mp3 new file mode 100644 index 0000000..6bf9215 Binary files /dev/null and b/assets/sfx/defeat.mp3 differ diff --git a/bts.c b/bts.c index 15a7ebe..8ce8371 100644 --- a/bts.c +++ b/bts.c @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -8,6 +9,7 @@ #include #include "dat.h" #include "fns.h" +#include "mixer.h" enum { PCBlack, @@ -137,6 +139,7 @@ RFrame worldrf; Image *pal[NCOLORS]; Image *screenb; Image *tiletab[NTILES]; +AudioSource *playlist[NSOUNDS]; Board alienboard; Board localboard; Ship armada[NSHIPS]; @@ -538,6 +541,26 @@ initarmada(void) } } +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) { @@ -916,6 +939,24 @@ processcmd(char *cmd) nbsend(drawchan, nil); } +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) { @@ -988,16 +1029,6 @@ threadmain(int argc, char *argv[]) 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 @@ threadmain(int argc, char *argv[]) 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); diff --git a/dat.h b/dat.h index b7fa1f8..174f806 100644 --- a/dat.h +++ b/dat.h @@ -37,6 +37,11 @@ enum { Borderwidth+MAPH*TH+Borderwidth+ Boardmargin, + SBG0 = 0, + SBG1, + SBG2, + NSOUNDS, + KB = 1024, BY2MAP = (TBITS*MAPW*MAPH+7)/8, }; diff --git a/mixer.c b/mixer.c new file mode 100644 index 0000000..d2eb2a9 --- /dev/null +++ b/mixer.c @@ -0,0 +1,762 @@ +/* + * standalone sound mixer based on rxi's cmixer. + */ +#include +#include +#include +#include +#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; +} diff --git a/mixer.h b/mixer.h new file mode 100644 index 0000000..81e22ee --- /dev/null +++ b/mixer.h @@ -0,0 +1,123 @@ +/* + * standalone sound mixer based on rxi's cmixer. + */ +enum { + FIXED_BITS = 12, + FIXED_UNIT = 1<