shithub: musw

Download patch

ref: 29c02c33749f9ca21ffa1c22cc2cc37f58584467
parent: 5a725b7c6a1c22800656f218e46b5b16b0d21884
author: rodri <[email protected]>
date: Tue May 23 11:45:13 EDT 2023

integrated cmixer for sound and sfx.

fixed the vfx drawing procedure so that it treats p as the center when rendering the sprite.

diff: cannot open b/assets/sfx//null: file does not exist: 'b/assets/sfx//null'
binary files /dev/null b/assets/sfx/intro.wav differ
--- /dev/null
+++ b/cmixer.c
@@ -1,0 +1,775 @@
+/*
+** 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.
+**
+** Ported to plan 9 on 13mar2021
+**/
+
+#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
--- /dev/null
+++ b/cmixer.h
@@ -1,0 +1,106 @@
+/*
+** 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.
+**
+** Ported to plan 9 on 13mar2021
+**/
+
+#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 *);
--- a/mkfile
+++ b/mkfile
@@ -15,6 +15,7 @@
 	universe.$O\
 	sprite.$O\
 	vfx.$O\
+	cmixer.$O\
 	net.$O\
 	fmt.$O\
 
@@ -21,6 +22,7 @@
 HFILES=\
 	dat.h\
 	fns.h\
+	cmixer.h\
 
 </sys/src/cmd/mkmany
 
--- a/musw.c
+++ b/musw.c
@@ -11,6 +11,7 @@
 #include <geometry.h>
 #include "dat.h"
 #include "fns.h"
+#include "cmixer.h"
 
 enum {
 	GSIntro,
@@ -676,6 +677,24 @@
 }
 
 void
+soundproc(void *)
+{
+	Biobuf *aout;
+	uchar adata[512];
+
+	threadsetname("soundproc");
+
+	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 [-dg] server\n", argv0);
@@ -690,6 +709,7 @@
 	char *server;
 	int fd;
 	Vfx *vfx;
+	cm_Source *bgsound;
 	Mousectl *mc;
 	Ioproc *io;
 
@@ -717,6 +737,9 @@
 		sysfatal("initdraw: %r");
 	if((mc = initmouse(nil, screen)) == nil)
 		sysfatal("initmouse: %r");
+	cm_init(44100);
+	cm_set_master_gain(0.5);
+
 	display->locking = 1;
 	unlockdisplay(display);
 
@@ -750,6 +773,13 @@
 
 	vfxtab[VFX_BULLET_EXPLOSION] = readpngsprite("assets/vfx/bullet.explosion.png", ZP, Rect(0, 0, 32, 32), 12, 100);
 	initvfx(&vfxqueue);
+
+	bgsound = cm_new_source_from_file("assets/sfx/intro.wav");
+	if(bgsound == nil)
+		sysfatal("cm_new_source_from_file: %s", cm_get_error());
+	cm_play(bgsound);
+
+	proccreate(soundproc, nil, mainstacksize);
 
 	gamestates[GSIntro].δ = intro_δ;
 	gamestates[GSConnecting].δ = connecting_δ;
--- a/universe.c
+++ b/universe.c
@@ -99,7 +99,7 @@
 static void
 universe_collide(Universe *u)
 {
-	Ship *s;
+	Ship *s, *enemy;
 	Bullet *b;
 
 	for(s = u->ships; s < u->ships+nelem(u->ships); s++){
@@ -110,7 +110,15 @@
 					continue;
 				}
 				warp(b);
-				
+
+				enemy = &u->ships[(s - u->ships + 1) % nelem(u->ships)];
+				/*
+				 * TODO: fix it
+				 *
+				 * of course it fails, the server doesn't load VModels!
+				 */
+//				if(ptinpoly(b->p, enemy->mdl->pts, enemy->mdl->npts))
+//					fprint(2, "enemy hit\n");
 			}
 		warp(s);
 	}
--- a/vfx.c
+++ b/vfx.c
@@ -26,7 +26,7 @@
 static void
 vfx_draw(Vfx *v, Image *dst)
 {
-	v->a->draw(v->a, dst, v->p);
+	v->a->draw(v->a, dst, subpt(v->p, divpt(subpt(v->a->r.max, v->a->r.min), 2)));
 }
 
 Vfx *