diff options
-rw-r--r-- | assets/sounds/steamboat.wav | bin | 0 -> 411342 bytes | |||
-rw-r--r-- | cmixer.c | 773 | ||||
-rw-r--r-- | cmixer.h | 104 | ||||
-rw-r--r-- | main.c | 46 | ||||
-rw-r--r-- | mkfile | 2 |
5 files changed, 923 insertions, 2 deletions
diff --git a/assets/sounds/steamboat.wav b/assets/sounds/steamboat.wav Binary files differnew file mode 100644 index 0000000..36a1823 --- /dev/null +++ b/assets/sounds/steamboat.wav diff --git a/cmixer.c b/cmixer.c new file mode 100644 index 0000000..f63e23d --- /dev/null +++ b/cmixer.c @@ -0,0 +1,773 @@ +/* +** Copyright (c) 2017 rxi +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to +** deal in the Software without restriction, including without limitation the +** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +** sell copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +** IN THE SOFTWARE. +**/ + +#include <u.h> +#include <libc.h> +#include <bio.h> +#include "cmixer.h" + + +static struct { + char *lasterror; /* Last error message */ + cm_EventHandler lock; /* Event handler for lock/unlock events */ + cm_Source *sources; /* Linked list of active (playing) sources */ + cm_Int32 buffer[BUFFER_SIZE]; /* Internal master buffer */ + int samplerate; /* Master samplerate */ + int gain; /* Master gain (fixed point) */ +} cmixer; + + +static void +dummy_handler(cm_Event *e) +{ + USED(e); +} + + +static void +cm_lock(void) +{ + cm_Event e; + e.type = CM_EVENT_LOCK; + cmixer.lock(&e); +} + + +static void +cm_unlock(void) +{ + cm_Event e; + e.type = CM_EVENT_UNLOCK; + cmixer.lock(&e); +} + + +char* +cm_get_error(void) +{ + char *res = cmixer.lasterror; + cmixer.lasterror = nil; + return res; +} + + +static char* +error(char *msg) +{ + cmixer.lasterror = msg; + return msg; +} + + +void +cm_init(int samplerate) +{ + cmixer.samplerate = samplerate; + cmixer.lock = dummy_handler; + cmixer.sources = nil; + cmixer.gain = FX_UNIT; +} + + +void +cm_set_lock(cm_EventHandler lk) +{ + cmixer.lock = lk; +} + + +void +cm_set_master_gain(double gain) +{ + cmixer.gain = FX_FROM_FLOAT(gain); +} + + +static void +rewind_source(cm_Source *src) +{ + cm_Event e; + e.type = CM_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(cm_Source *src, int offset, int length) +{ + cm_Event e; + e.type = CM_EVENT_SAMPLES; + e.udata = src->udata; + e.buffer = src->buffer + offset; + e.length = length; + src->handler(&e); +} + + +static void +process_source(cm_Source *src, int len) +{ + int i, n, a, b, p; + int frame, count; + cm_Int32 *dst = cmixer.buffer; + + /* Do rewind if flag is set */ + if (src->rewind) { + rewind_source(src); + } + + /* Don't process if not playing */ + if (src->state != CM_STATE_PLAYING) { + return; + } + + /* Process audio */ + while (len > 0) { + /* Get current position frame */ + frame = src->position >> FX_BITS; + + /* Fill buffer if required */ + if (frame + 3 >= src->nextfill) { + fill_source_buffer(src, (src->nextfill*2) & BUFFER_MASK, BUFFER_SIZE/2); + src->nextfill += BUFFER_SIZE / 4; + } + + /* Handle reaching the end of the playthrough */ + if (frame >= src->end) { + /* As streams continiously 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 = CM_STATE_STOPPED; + break; + } + } + + /* Work out how many frames we should process in the loop */ + n = MIN(src->nextfill - 2, src->end) - frame; + count = (n << FX_BITS) / src->rate; + count = MAX(count, 1); + count = MIN(count, len / 2); + len -= count * 2; + + /* Add audio to master buffer */ + if (src->rate == FX_UNIT) { + /* Add audio to buffer -- basic */ + n = frame * 2; + for (i = 0; i < count; i++) { + dst[0] += (src->buffer[(n ) & BUFFER_MASK] * src->lgain) >> FX_BITS; + dst[1] += (src->buffer[(n + 1) & BUFFER_MASK] * src->rgain) >> FX_BITS; + n += 2; + dst += 2; + } + src->position += count * FX_UNIT; + + } else { + /* Add audio to buffer -- interpolated */ + for (i = 0; i < count; i++) { + n = (src->position >> FX_BITS) * 2; + p = src->position & FX_MASK; + a = src->buffer[(n ) & BUFFER_MASK]; + b = src->buffer[(n + 2) & BUFFER_MASK]; + dst[0] += (FX_LERP(a, b, p) * src->lgain) >> FX_BITS; + n++; + a = src->buffer[(n ) & BUFFER_MASK]; + b = src->buffer[(n + 2) & BUFFER_MASK]; + dst[1] += (FX_LERP(a, b, p) * src->rgain) >> FX_BITS; + src->position += src->rate; + dst += 2; + } + } + + } +} + + +void +cm_process(cm_Int16 *dst, int len) +{ + int i; + cm_Source **s; + + /* Process in chunks of BUFFER_SIZE if `len` is larger than BUFFER_SIZE */ + while (len > BUFFER_SIZE) { + cm_process(dst, BUFFER_SIZE); + dst += BUFFER_SIZE; + len -= BUFFER_SIZE; + } + + /* Zeroset internal buffer */ + memset(cmixer.buffer, 0, len * sizeof(cmixer.buffer[0])); + + /* Process active sources */ + cm_lock(); + s = &cmixer.sources; + while (*s) { + process_source(*s, len); + /* Remove source from list if it is no longer playing */ + if ((*s)->state != CM_STATE_PLAYING) { + (*s)->active = 0; + *s = (*s)->next; + } else { + s = &(*s)->next; + } + } + cm_unlock(); + + /* Copy internal buffer to destination and clip */ + for (i = 0; i < len; i++) { + int x = (cmixer.buffer[i] * cmixer.gain) >> FX_BITS; + dst[i] = CLAMP(x, -32768, 32767); + } +} + + +cm_Source* +cm_new_source(cm_SourceInfo *info) +{ + cm_Source *src = calloc(1, sizeof(*src)); + if (!src) { + error("allocation failed"); + return nil; + } + src->handler = info->handler; + src->length = info->length; + src->samplerate = info->samplerate; + src->udata = info->udata; + cm_set_gain(src, 1); + cm_set_pan(src, 0); + cm_set_pitch(src, 1); + cm_set_loop(src, 0); + cm_stop(src); + return src; +} + + +static char* wav_init(cm_SourceInfo *info, void *data, int len, int ownsdata); + +#ifdef CM_USE_STB_VORBIS +static char* ogg_init(cm_SourceInfo *info, void *data, int len, int ownsdata); +#endif + + +static int +check_header(void *data, int size, char *str, int offset) +{ + int len = strlen(str); + return (size >= offset + len) && !memcmp((char*) data + offset, str, len); +} + + +static cm_Source* +new_source_from_mem(void *data, int size, int ownsdata) +{ + char *err; + cm_SourceInfo info; + + if (check_header(data, size, "WAVE", 8)) { + err = wav_init(&info, data, size, ownsdata); + if (err) { + return nil; + } + return cm_new_source(&info); + } + +#ifdef CM_USE_STB_VORBIS + if (check_header(data, size, "OggS", 0)) { + err = ogg_init(&info, data, size, ownsdata); + if (err) { + return nil; + } + return cm_new_source(&info); + } +#endif + + error("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) { + return nil; + } + + /* Get size */ + Bseek(fp, 0, 2); + *size = Boffset(fp); + Bseek(fp, 0, 0); + + /* Malloc, read and return data */ + data = malloc(*size); + if (!data) { + Bterm(fp); + return nil; + } + n = Bread(fp, data, *size); + Bterm(fp); + if (n != *size) { + free(data); + return nil; + } + + return data; +} + + +cm_Source* +cm_new_source_from_file(char *filename) +{ + int size; + cm_Source *src; + void *data; + + /* Load file into memory */ + data = load_file(filename, &size); + if (!data) { + error("could not load file"); + return nil; + } + + /* Try to load and return */ + src = new_source_from_mem(data, size, 1); + if (!src) { + free(data); + return nil; + } + + return src; +} + + +cm_Source* +cm_new_source_from_mem(void *data, int size) +{ + return new_source_from_mem(data, size, 0); +} + + +void +cm_destroy_source(cm_Source *src) +{ + cm_Event e; + cm_lock(); + if (src->active) { + cm_Source **s = &cmixer.sources; + while (*s) { + if (*s == src) { + *s = src->next; + break; + } + } + } + cm_unlock(); + e.type = CM_EVENT_DESTROY; + e.udata = src->udata; + src->handler(&e); + free(src); +} + + +double +cm_get_length(cm_Source *src) +{ + return src->length / (double) src->samplerate; +} + + +double +cm_get_position(cm_Source *src) +{ + return ((src->position >> FX_BITS) % src->length) / (double) src->samplerate; +} + + +int +cm_get_state(cm_Source *src) +{ + return src->state; +} + + +static void +recalc_source_gains(cm_Source *src) +{ + double l, r; + double pan = src->pan; + l = src->gain * (pan <= 0. ? 1. : 1. - pan); + r = src->gain * (pan >= 0. ? 1. : 1. + pan); + src->lgain = FX_FROM_FLOAT(l); + src->rgain = FX_FROM_FLOAT(r); +} + + +void +cm_set_gain(cm_Source *src, double gain) +{ + src->gain = gain; + recalc_source_gains(src); +} + + +void +cm_set_pan(cm_Source *src, double pan) +{ + src->pan = CLAMP(pan, -1.0, 1.0); + recalc_source_gains(src); +} + + +void +cm_set_pitch(cm_Source *src, double pitch) +{ + double rate; + if (pitch > 0.) { + rate = src->samplerate / (double) cmixer.samplerate * pitch; + } else { + rate = 0.001; + } + src->rate = FX_FROM_FLOAT(rate); +} + + +void +cm_set_loop(cm_Source *src, int loop) +{ + src->loop = loop; +} + + +void +cm_play(cm_Source *src) +{ + cm_lock(); + src->state = CM_STATE_PLAYING; + if (!src->active) { + src->active = 1; + src->next = cmixer.sources; + cmixer.sources = src; + } + cm_unlock(); +} + + +void +cm_pause(cm_Source *src) +{ + src->state = CM_STATE_PAUSED; +} + + +void +cm_stop(cm_Source *src) +{ + src->state = CM_STATE_STOPPED; + src->rewind = 1; +} + + +/*============================================================================ +** Wav stream +**============================================================================*/ + +typedef struct { + void *data; + int bitdepth; + int samplerate; + int channels; + int length; +} Wav; + +typedef struct { + Wav wav; + void *data; + int idx; +} WavStream; + + +static char* +find_subchunk(char *data, int len, char *id, int *size) +{ + /* TODO : Error handling on malformed wav file */ + int idlen = strlen(id); + char *p = data + 12; +next: + *size = *((cm_UInt32*) (p + 4)); + if (memcmp(p, id, idlen)) { + p += 8 + *size; + if (p > data + len) return nil; + goto next; + } + return p + 8; +} + + +static char* +read_wav(Wav *w, void *data, int len) +{ + int bitdepth, channels, samplerate, format; + int sz; + char *p = data; + memset(w, 0, sizeof(*w)); + + /* Check header */ + if (memcmp(p, "RIFF", 4) || memcmp(p + 8, "WAVE", 4)) { + return error("bad wav header"); + } + /* Find fmt subchunk */ + p = find_subchunk(data, len, "fmt", &sz); + if (!p) { + return error("no fmt subchunk"); + } + + /* Load fmt info */ + format = *((cm_UInt16*) (p)); + channels = *((cm_UInt16*) (p + 2)); + samplerate = *((cm_UInt32*) (p + 4)); + bitdepth = *((cm_UInt16*) (p + 14)); + if (format != 1) { + return error("unsupported format"); + } + if (channels == 0 || samplerate == 0 || bitdepth == 0) { + return error("bad format"); + } + + /* Find data subchunk */ + p = find_subchunk(data, len, "data", &sz); + if (!p) { + return error("no data subchunk"); + } + + /* Init struct */ + w->data = (void*) p; + w->samplerate = samplerate; + w->channels = channels; + w->length = (sz / (bitdepth / 8)) / channels; + w->bitdepth = bitdepth; + /* Done */ + return nil; +} + + +#define WAV_PROCESS_LOOP(X) \ + while (n--) { \ + X \ + dst += 2; \ + s->idx++; \ + } + +static void +wav_handler(cm_Event *e) +{ + int x, n; + cm_Int16 *dst; + WavStream *s = e->udata; + int len; + + switch (e->type) { + + case CM_EVENT_DESTROY: + free(s->data); + free(s); + break; + + case CM_EVENT_SAMPLES: + dst = e->buffer; + len = e->length / 2; +fill: + n = MIN(len, s->wav.length - s->idx); + len -= n; + if (s->wav.bitdepth == 16 && s->wav.channels == 1) { + WAV_PROCESS_LOOP({ + dst[0] = dst[1] = ((cm_Int16*) s->wav.data)[s->idx]; + }); + } else if (s->wav.bitdepth == 16 && s->wav.channels == 2) { + WAV_PROCESS_LOOP({ + x = s->idx * 2; + dst[0] = ((cm_Int16*) s->wav.data)[x ]; + dst[1] = ((cm_Int16*) s->wav.data)[x + 1]; + }); + } else if (s->wav.bitdepth == 8 && s->wav.channels == 1) { + WAV_PROCESS_LOOP({ + dst[0] = dst[1] = (((cm_UInt8*) s->wav.data)[s->idx] - 128) << 8; + }); + } else if (s->wav.bitdepth == 8 && s->wav.channels == 2) { + WAV_PROCESS_LOOP({ + x = s->idx * 2; + dst[0] = (((cm_UInt8*) s->wav.data)[x ] - 128) << 8; + dst[1] = (((cm_UInt8*) s->wav.data)[x + 1] - 128) << 8; + }); + } + /* Loop back and continue filling buffer if we didn't fill the buffer */ + if (len > 0) { + s->idx = 0; + goto fill; + } + break; + + case CM_EVENT_REWIND: + s->idx = 0; + break; + } +} + + +static char* +wav_init(cm_SourceInfo *info, void *data, int len, int ownsdata) +{ + WavStream *stream; + Wav wav; + + char *err = read_wav(&wav, data, len); + if (err != nil) { + return err; + } + + if (wav.channels > 2 || (wav.bitdepth != 16 && wav.bitdepth != 8)) { + return error("unsupported wav format"); + } + + stream = calloc(1, sizeof(*stream)); + if (!stream) { + return error("allocation failed"); + } + stream->wav = wav; + + if (ownsdata) { + stream->data = data; + } + stream->idx = 0; + + info->udata = stream; + info->handler = wav_handler; + info->samplerate = wav.samplerate; + info->length = wav.length; + + /* Return nil (no error) for success */ + return nil; +} + + +/*============================================================================ +** Ogg stream +**============================================================================*/ + +#ifdef CM_USE_STB_VORBIS + +#define STB_VORBIS_HEADER_ONLY +#include "stb_vorbis.c" + +typedef struct { + stb_vorbis *ogg; + void *data; +} OggStream; + + +static void +ogg_handler(cm_Event *e) +{ + int n, len; + OggStream *s = e->udata; + cm_Int16 *buf; + + switch (e->type) { + + case CM_EVENT_DESTROY: + stb_vorbis_close(s->ogg); + free(s->data); + free(s); + break; + + case CM_EVENT_SAMPLES: + len = e->length; + buf = e->buffer; +fill: + n = stb_vorbis_get_samples_short_interleaved(s->ogg, 2, buf, len); + n *= 2; + /* rewind and fill remaining buffer if we reached the end of the ogg + ** before filling it */ + if (len != n) { + stb_vorbis_seek_start(s->ogg); + buf += n; + len -= n; + goto fill; + } + break; + + case CM_EVENT_REWIND: + stb_vorbis_seek_start(s->ogg); + break; + } +} + + +static char* +ogg_init(cm_SourceInfo *info, void *data, int len, int ownsdata) +{ + OggStream *stream; + stb_vorbis *ogg; + stb_vorbis_info ogginfo; + int err; + + ogg = stb_vorbis_open_memory(data, len, &err, nil); + if (!ogg) { + return error("invalid ogg data"); + } + + stream = calloc(1, sizeof(*stream)); + if (!stream) { + stb_vorbis_close(ogg); + return error("allocation failed"); + } + + stream->ogg = ogg; + if (ownsdata) { + stream->data = data; + } + + ogginfo = stb_vorbis_get_info(ogg); + + info->udata = stream; + info->handler = ogg_handler; + info->samplerate = ogginfo.sample_rate; + info->length = stb_vorbis_stream_length_in_samples(ogg); + + /* Return nil (no error) for success */ + return nil; +} + + +#endif diff --git a/cmixer.h b/cmixer.h new file mode 100644 index 0000000..c03300a --- /dev/null +++ b/cmixer.h @@ -0,0 +1,104 @@ +/* +** Copyright (c) 2017 rxi +** +** This library is free software; you can redistribute it and/or modify it +** under the terms of the MIT license. See `cmixer.c` for details. +**/ + +#define CM_VERSION "0.1.1" + +#define CLAMP(x, a, b) ((x) < (a) ? (a) : (x) > (b) ? (b) : (x)) +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#define MAX(a, b) ((a) > (b) ? (a) : (b)) + +#define FX_BITS (12) +#define FX_UNIT (1 << FX_BITS) +#define FX_MASK (FX_UNIT - 1) +#define FX_FROM_FLOAT(f) ((f) * FX_UNIT) +#define FX_LERP(a, b, p) ((a) + ((((b) - (a)) * (p)) >> FX_BITS)) + +#define BUFFER_SIZE (512) +#define BUFFER_MASK (BUFFER_SIZE - 1) + +typedef short cm_Int16; +typedef int cm_Int32; +typedef vlong cm_Int64; +typedef uchar cm_UInt8; +typedef ushort cm_UInt16; +typedef ulong cm_UInt32; + + +typedef struct { + int type; + void *udata; + char *msg; + cm_Int16 *buffer; + int length; +} cm_Event; + +typedef void (*cm_EventHandler)(cm_Event *e); + +typedef struct { + cm_EventHandler handler; + void *udata; + int samplerate; + int length; +} cm_SourceInfo; + + +enum { + CM_STATE_STOPPED, + CM_STATE_PLAYING, + CM_STATE_PAUSED +}; + +enum { + CM_EVENT_LOCK, + CM_EVENT_UNLOCK, + CM_EVENT_DESTROY, + CM_EVENT_SAMPLES, + CM_EVENT_REWIND +}; + +typedef struct cm_Source cm_Source; +struct cm_Source { + cm_Source *next; /* Next source in list */ + cm_Int16 buffer[BUFFER_SIZE]; /* Internal buffer with raw stereo PCM */ + cm_EventHandler handler; /* Event handler */ + void *udata; /* Stream's udata (from cm_SourceInfo) */ + int samplerate; /* Stream's native samplerate */ + int length; /* Stream's length in frames */ + int end; /* End index for the current play-through */ + int state; /* Current state (playing|paused|stopped) */ + cm_Int64 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 `cm_set_gain()` */ + double pan; /* Pan set by `cm_set_pan()` */ +}; + + +char* cm_get_error(void); +void cm_init(int); +void cm_set_lock(cm_EventHandler); +void cm_set_master_gain(double); +void cm_process(cm_Int16*, int); + +cm_Source* cm_new_source(cm_SourceInfo*); +cm_Source* cm_new_source_from_file(char *); +cm_Source* cm_new_source_from_mem(void *, int); +void cm_destroy_source(cm_Source *); +double cm_get_length(cm_Source *); +double cm_get_position(cm_Source *); +int cm_get_state(cm_Source *); +void cm_set_gain(cm_Source *, double); +void cm_set_pan(cm_Source *, double); +void cm_set_pitch(cm_Source *, double); +void cm_set_loop(cm_Source *, int); +void cm_play(cm_Source *); +void cm_pause(cm_Source *); +void cm_stop(cm_Source *); @@ -1,5 +1,6 @@ #include <u.h> #include <libc.h> +#include <bio.h> #include <thread.h> #include <draw.h> #include <mouse.h> @@ -7,6 +8,7 @@ #include <geometry.h> #include "dat.h" #include "fns.h" +#include "cmixer.h" RFrame screenrf; GameState state; @@ -106,8 +108,9 @@ resetsim(void) } void -rmb(Mousectl *mc) +rmbproc(void *arg) { + threadsetname("rmbproc"); enum { RESET, }; @@ -116,19 +119,24 @@ rmb(Mousectl *mc) nil }; static Menu menu = { .item = items }; + Mousectl *mc; + + mc = (Mousectl*)arg; switch(menuhit(3, mc, &menu, _screen)){ case RESET: resetsim(); break; } + + threadexits(nil); } void mouse(Mousectl *mc, Keyboardctl *) { if((mc->buttons & 4) != 0) - rmb(mc); + proccreate(rmbproc, mc, 2048); } void @@ -142,6 +150,23 @@ key(Rune r) } void +soundproc(void *) +{ + threadsetname("soundproc"); + Biobuf *aout; + uchar adata[512]; + + aout = Bopen("/dev/audio", OWRITE); + if(aout == nil) + sysfatal("Bopen: %r"); + + for(;;){ + cm_process((void *)adata, sizeof(adata)/2); + Bwrite(aout, adata, sizeof adata); + } +} + +void usage(void) { fprint(2, "usage: %s\n", argv0); @@ -156,6 +181,7 @@ threadmain(int argc, char *argv[]) Rune r; uvlong then, now; double frametime, timeacc; + cm_Source *bgsound, *boatsound; ARGBEGIN{ default: usage(); @@ -169,6 +195,8 @@ threadmain(int argc, char *argv[]) sysfatal("initmouse: %r"); if((kc = initkeyboard(nil)) == nil) sysfatal("initkeyboard: %r"); + cm_init(44100); + cm_set_master_gain(0.5); display->locking = 1; unlockdisplay(display); @@ -180,6 +208,20 @@ threadmain(int argc, char *argv[]) ghoul = readsprite("assets/sheets/NpcCemet.pic", Pt(48,0), Rect(0,0,16,16), 5, 150); ghoulbig = newsprite(ghoul->sheet, Pt(144,64), Rect(0,0,24,24), 5, 120); + bgsound = cm_new_source_from_file("assets/sounds/birds.wav"); + if(bgsound == nil) + sysfatal("cm_new_source_from_file: %s", cm_get_error()); + boatsound = cm_new_source_from_file("assets/sounds/steamboat.wav"); + if(boatsound == nil) + sysfatal("cm_new_source_from_file: %s", cm_get_error()); + cm_set_loop(bgsound, 1); + cm_set_loop(boatsound, 1); + cm_set_gain(boatsound, 0.2); + cm_play(bgsound); + cm_play(boatsound); + + proccreate(soundproc, nil, 2048); + Δt = 0.01; then = nanosec(); timeacc = 0; @@ -8,12 +8,14 @@ OFILES=\ physics.$O\ sprite.$O\ nanosec.$O\ + cmixer.$O\ main.$O\ HFILES=\ dat.h\ fns.h\ libgeometry/geometry.h\ + cmixer.h\ LIB=\ libgeometry/libgeometry.a$O\ |