shithub: candycrisis

Download patch

ref: 6159713b2a0f55e2cd41797ed7aa3a5a4c0a06a3
parent: b09f9d2dd1242bbe3298d6b2d5b2e7749c21dd32
author: Iliyas Jorio <[email protected]>
date: Thu Feb 9 16:38:11 EST 2023

Rewrite mixer in pure C

--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -69,9 +69,7 @@
         run: |
           cmake --install build --prefix build/install
           copy build/ReadMe.txt build/Release
-          copy build/install/bin/msvcp140.dll build/Release
           copy build/install/bin/vcruntime140.dll build/Release
-          copy build/install/bin/vcruntime140_1.dll build/Release
 
       - name: Upload
         uses: actions/upload-artifact@v3
--- /dev/null
+++ b/src/music.c
@@ -1,0 +1,103 @@
+// music.c
+
+#include <string.h>
+#include "main.h"
+#include "music.h"
+#include "gworld.h"
+#include "gameticks.h"
+#include "soundfx.h"
+#include "graphics.h"
+#include "support/cmixer.h"
+
+#define k_noMusic (-1)
+#define k_songs 14
+
+MBoolean                musicOn = true;
+int                     musicSelection = k_noMusic;
+
+static MBoolean         s_musicFast = false;
+int                     s_musicPaused = 0;
+
+static struct CMVoice* s_musicChannel = NULL;
+
+void EnableMusic( MBoolean on )
+{
+    if (s_musicChannel)
+    {
+        CMVoice_SetGain(s_musicChannel, on ? 1 : 0);
+    }
+}
+
+void FastMusic( void )
+{
+    if (s_musicChannel && !s_musicFast)
+    {
+        CMVoice_SetMODPlaybackSpeed(s_musicChannel, 1.3);
+        s_musicFast = true;
+    }
+}
+
+void SlowMusic( void )
+{
+    if (s_musicChannel && s_musicFast)
+    {
+        CMVoice_SetMODPlaybackSpeed(s_musicChannel, 1.0);
+        s_musicFast = false;
+    }
+}
+
+void PauseMusic( void )
+{
+    if (s_musicChannel)
+    {
+        CMVoice_Pause(s_musicChannel);
+        s_musicPaused++;
+    }
+}
+
+void ResumeMusic( void )
+{
+    if (s_musicChannel)
+    {
+        CMVoice_Play(s_musicChannel);
+        s_musicPaused--;
+    }
+}
+
+void ChooseMusic( short which )
+{
+    // Kill existing song first, if any
+    ShutdownMusic();
+
+    musicSelection = -1;
+
+    if (which >= 0 && which <= k_songs)
+    {
+        const char* qrn = QuickResourceName("mod", which+128, ".mod");
+        if (!FileExists(qrn))
+        {
+            qrn = QuickResourceName("mod", which+128, ".s3m");
+        }
+        if (!FileExists(qrn))
+        {
+            return;
+        }
+
+        s_musicChannel = CMVoice_LoadMOD(qrn);
+
+        EnableMusic(musicOn);
+        CMVoice_Play(s_musicChannel);
+    
+        musicSelection = which;
+        s_musicPaused  = 0;
+    }
+}
+
+void ShutdownMusic()
+{
+    if (s_musicChannel)
+    {
+        CMVoice_Free(s_musicChannel);
+        s_musicChannel = NULL;
+    }
+}
--- a/src/music.cpp
+++ /dev/null
@@ -1,127 +1,0 @@
-// music.c
-
-#include <string.h>
-#include <vector>
-#include <fstream>
-
-extern "C"
-{
-
-#include "main.h"
-#include "music.h"
-#include "gworld.h"
-#include "gameticks.h"
-#include "soundfx.h"
-#include "graphics.h"
-
-}
-
-#include "support/ModStream.h"
-
-const int               k_noMusic = -1;
-const int               k_songs = 14;
-
-extern "C"
-{
-MBoolean                musicOn = true;
-int                     musicSelection = k_noMusic;
-
-static MBoolean         s_musicFast = false;
-int                     s_musicPaused = 0;
-}
-
-static cmixer::ModStream* s_musicChannel = NULL;
-
-void EnableMusic( MBoolean on )
-{
-    if (s_musicChannel)
-    {
-        s_musicChannel->SetGain(on? 1.0: 0.0);
-    }
-}
-
-void FastMusic( void )
-{
-    if (s_musicChannel && !s_musicFast)
-    {
-        s_musicChannel->SetPlaybackSpeed(1.3);
-        s_musicFast = true;
-    }
-}
-
-void SlowMusic( void )
-{
-    if (s_musicChannel && s_musicFast)
-    {
-        s_musicChannel->SetPlaybackSpeed(1.0);
-        s_musicFast = false;
-    }
-}
-
-void PauseMusic( void )
-{
-    if (s_musicChannel)
-    {
-        s_musicChannel->Pause();
-        s_musicPaused++;
-    }
-}
-
-void ResumeMusic( void )
-{
-    if (s_musicChannel)
-    {
-        s_musicChannel->Play();
-        s_musicPaused--;
-    }
-}
-
-static std::vector<char> LoadFile(char const* filename)
-{
-    std::ifstream ifs(filename, std::ios::binary | std::ios::ate);
-    auto pos = ifs.tellg();
-    std::vector<char> bytes(pos);
-    ifs.seekg(0, std::ios::beg);
-    ifs.read(&bytes[0], pos);
-    return bytes;
-}
-
-void ChooseMusic( short which )
-{
-    // Kill existing song first, if any
-    ShutdownMusic();
-
-    musicSelection = -1;
-    
-    if (which >= 0 && which <= k_songs)
-    {
-        //printf("Music: %d\n" , which + 128);
-        
-        auto qrn = QuickResourceName("mod", which+128, ".mod");
-        if (!FileExists(qrn)) {
-            qrn = QuickResourceName("mod", which+128, ".s3m");
-        }
-        if (!FileExists(qrn)) {
-            return;
-        }
-        auto rawFileData = LoadFile(qrn);
-
-        s_musicChannel = new cmixer::ModStream(LoadFile(qrn));
-
-        EnableMusic(musicOn);
-        s_musicChannel->Play();
-    
-        musicSelection = which;
-        s_musicPaused  = 0;
-    }
-}
-
-void ShutdownMusic()
-{
-    if (s_musicChannel)
-    {
-        s_musicChannel->RemoveFromMixer();
-        delete s_musicChannel;
-        s_musicChannel = NULL;
-    }
-}
--- /dev/null
+++ b/src/soundfx.c
@@ -1,0 +1,81 @@
+// soundfx.c
+
+#include "support/cmixer.h"
+#include <stdio.h>
+
+#include "main.h"
+#include "soundfx.h"
+#include "music.h"
+
+MBoolean soundOn = true;
+
+#define k_playerStereoSeparation (0.5f)
+#define k_soundEffectGain (0.7f)
+static CMVoicePtr s_soundBank[kNumSounds];
+
+void InitSound()
+{
+    cmixer_InitWithSDL();
+
+    for (int i = 0; i < kNumSounds; i++)
+    {
+        const char* path = QuickResourceName("snd", i+128, ".wav");
+        if (!FileExists(path))
+        {
+            Error(path);
+        }
+
+        s_soundBank[i] = CMVoice_LoadWAV(path);
+        CMVoice_SetInterpolation(s_soundBank[i], true);
+    }
+}
+
+void ShutdownSound()
+{
+    for (int i = 0; i < kNumSounds; i++)
+    {
+        CMVoice_Free(s_soundBank[i]);
+        s_soundBank[i] = NULL;
+    }
+
+    cmixer_ShutdownWithSDL();
+}
+
+void PlayMono( short which )
+{
+    PlayStereoFrequency(2, which, 0);
+}
+
+void PlayStereo( short player, short which )
+{
+    PlayStereoFrequency(player, which, 0);
+}
+
+void PlayStereoFrequency( short player, short which, short freq )
+{
+    if (soundOn)
+    {
+        CMVoicePtr effect = s_soundBank[which];
+
+        double pan;
+        switch (player)
+        {
+            case 0: pan = -k_playerStereoSeparation; break;
+            case 1: pan = +k_playerStereoSeparation; break;
+            default: pan = 0.0; break;
+        }
+
+        //CMVoice_Stop(effect);
+        CMVoice_Rewind(effect);
+        CMVoice_SetGain(effect, k_soundEffectGain);
+        CMVoice_SetPan(effect, pan);
+        CMVoice_SetPitch(effect, 1.0 + freq/16.0);
+        CMVoice_Play(effect);
+
+        UpdateSound();
+    }
+}
+
+void UpdateSound()
+{
+}
--- a/src/soundfx.cpp
+++ /dev/null
@@ -1,82 +1,0 @@
-// soundfx.c
-
-#include "support/cmixer.h"
-#include <stdio.h>
-
-extern "C"
-{
-    #include "main.h"
-    #include "soundfx.h"
-    #include "music.h"
-
-    MBoolean soundOn = true;
-}
-
-static std::vector<cmixer::WavStream> s_soundBank;
-static constexpr float k_playerStereoSeparation = 0.5f;
-static constexpr float k_soundEffectGain = 0.7f;
-
-void InitSound()
-{
-    cmixer::InitWithSDL();
-    
-    for (int index=0; index<kNumSounds; index++)
-    {
-        const char* path = QuickResourceName("snd", index+128, ".wav");
-        if (!FileExists(path))
-        {
-            Error(path);
-        }
-
-        s_soundBank.emplace_back(cmixer::LoadWAVFromFile(path));
-        s_soundBank.back().SetInterpolation(true);
-    }
-}
-
-void ShutdownSound()
-{
-    for (auto& wavStream : s_soundBank)
-    {
-        wavStream.RemoveFromMixer();
-    }
-    s_soundBank.clear();
-    cmixer::ShutdownWithSDL();
-}
-
-void PlayMono( short which )
-{
-    PlayStereoFrequency(2, which, 0);
-}
-
-void PlayStereo( short player, short which )
-{
-    PlayStereoFrequency(player, which, 0);
-}
-
-void PlayStereoFrequency( short player, short which, short freq )
-{
-    if (soundOn)
-    {
-        auto& effect = s_soundBank.at(which);
-
-        double pan;
-        switch (player)
-        {
-            case 0: pan = -k_playerStereoSeparation; break;
-            case 1: pan = +k_playerStereoSeparation; break;
-            default: pan = 0.0; break;
-        }
-
-        effect.Stop();
-        effect.SetGain(k_soundEffectGain);
-        effect.SetPan(pan);
-        effect.SetPitch(1.0 + freq/16.0);
-        effect.Play();
-
-        UpdateSound();
-    }
-}
-
-void UpdateSound()
-{
-}
--- a/src/support/ModStream.cpp
+++ /dev/null
@@ -1,61 +1,0 @@
-#include <climits>
-#include <cstdio>
-#include "ModStream.h"
-
-using namespace cmixer;
-
-ModStream::ModStream(std::vector<char> &&rawModuleData)
-        : Source()
-        , moduleFile(rawModuleData)
-        , replayBuffer(2048*8)
-        , rbOffset(0)
-        , rbLength(0)
-        , playbackSpeedMult(1.0)
-{
-    Init(44100, INT_MAX);
-    ibxm::data d = {};
-    d.buffer = moduleFile.data();
-    d.length = moduleFile.size();
-    char errors[256];
-    errors[0] = '\0';
-    this->module = ibxm::module_load(&d, errors);
-    this->replay = ibxm::new_replay(this->module, 44100, 0);
-    //printf("%p IBXM Error: %s\n", this->module, errors);
-}
-
-void ModStream::SetPlaybackSpeed(double f)
-{
-    playbackSpeedMult = f;
-}
-
-void ModStream::FillBuffer(int16_t *output, int length)
-{
-    length /= 2;
-
-    while (length > 0) {
-        // refill replay buffer if exhausted
-        if (rbLength == 0) {
-            rbOffset = 0;
-            rbLength = ibxm::replay_get_audio(replay, replayBuffer.data(), 0, (int)(playbackSpeedMult * 100.0));
-        }
-
-        // number of stereo samples to copy from replay buffer to output buffer
-        int nToCopy = std::min(rbLength, length);
-
-        int *input = &replayBuffer[rbOffset * 2];
-
-        // Copy samples
-        for (int i = 0; i < nToCopy * 2; i++) {
-            int sample = *(input++);
-
-            if (sample < -32768) sample = -32768;
-            if (sample >  32767) sample =  32767;
-
-            *(output++) = sample;
-        }
-
-        rbOffset += nToCopy;
-        rbLength -= nToCopy;
-        length -= nToCopy;
-    }
-}
--- a/src/support/ModStream.h
+++ /dev/null
@@ -1,29 +1,0 @@
-#pragma once
-
-#include "cmixer.h"
-
-namespace ibxm {
-extern "C" {
-#include "ibxm.h"
-}
-}
-
-namespace cmixer {
-class ModStream : public Source {
-    ibxm::module* module;
-    ibxm::replay* replay;
-    std::vector<char> moduleFile;
-    std::vector<int> replayBuffer;
-    int rbOffset;
-    int rbLength;
-    double playbackSpeedMult;
-
-    void ClearImplementation() override {};
-    void RewindImplementation() override {};
-    void FillBuffer(int16_t* buffer, int length) override;
-
-public:
-    ModStream(std::vector<char>&& rawModule);
-    void SetPlaybackSpeed(double f);
-};
-}
\ No newline at end of file
--- /dev/null
+++ b/src/support/cmixer.c
@@ -1,0 +1,1002 @@
+/*
+
+Derivative work of cmixer by rxi (https://github.com/rxi/cmixer)
+
+Copyright (c) 2017 rxi
+Copyright (c) 2023 Iliyas Jorio
+
+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 "cmixer.h"
+#include "ibxm.h"
+
+#include <SDL.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <limits.h>
+#include <stdio.h>
+
+#define MAX_CONCURRENT_VOICES 8
+#define BUFFER_SIZE 512
+
+#define CM_DIE(message) \
+do { \
+	char buf[256]; \
+	snprintf(buf, sizeof(buf), "%s:%d: %s", __func__, __LINE__, (message)); \
+	SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "cmixer", buf, NULL); \
+	abort(); \
+} while(0)
+
+#define CM_ASSERT(assertion, message) do { if (!(assertion)) CM_DIE(message); } while(0)
+
+#define FX_BITS (12L)
+#define FX_UNIT (1L << FX_BITS)
+#define FX_MASK (FX_UNIT - 1)
+
+#define BUFFER_MASK (BUFFER_SIZE - 1)
+
+enum
+{
+	PCMFORMAT_NULL		= 0x00,
+	PCMFORMAT_1CH_8		= 0x11,
+	PCMFORMAT_2CH_8		= 0x21,
+	PCMFORMAT_1CH_LE16	= 0x12,
+	PCMFORMAT_2CH_LE16	= 0x22,
+	PCMFORMAT_1CH_BE16	= PCMFORMAT_1CH_LE16 | 0x80,
+	PCMFORMAT_2CH_BE16	= PCMFORMAT_2CH_LE16 | 0x80,
+};
+
+struct CMWavStream
+{
+	uint8_t pcmformat;
+	int idx;
+
+	char* data;
+	size_t dataLength;
+	bool ownData;
+
+	uint32_t cookie;
+};
+
+struct CMModStream
+{
+	struct module* module;
+	struct replay* replay;
+
+	char* moduleFileMemory;
+	int* replayBuffer;
+
+	int replayBufferOffset;
+	int replayBufferSamples;
+	double playbackSpeedMult;
+
+	uint32_t cookie;
+};
+
+struct CMVoice
+{
+	int16_t pcmbuf[BUFFER_SIZE];	// Internal buffer with raw stereo PCM
+	int sampleRate;					// Stream's native samplerate
+	int sampleCount;				// Stream's length in frames
+	int sustainOffset;				// Offset of the sustain loop in frames
+	int end;						// End index for the current play-through
+	int state;						// Current state (playing|paused|stopped)
+	int64_t 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
+	bool loop;						// Whether the voice will loop when `end` is reached
+	bool rewind;					// Whether the voice will rewind before playing
+	bool active;					// Whether the voice is part of `voices` list
+	bool interpolate;				// Interpolated resampling when played back at a non-native rate
+	double gain;					// Gain set by `cm_set_gain()`
+	double pan;						// Pan set by `cm_set_pan()`
+
+	struct
+	{
+		void (*fillBuffer)(struct CMVoice* voice, int16_t* into, int len);
+		void (*completed)(struct CMVoice* voice);
+		void (*rewind)(struct CMVoice* voice);
+		void (*free)(struct CMVoice* voice);
+	} callbacks;
+
+	uint32_t cookie;
+
+	union
+	{
+		struct CMWavStream wav;
+		struct CMModStream mod;
+	};
+};
+
+typedef struct CMVoice CMVoice;
+typedef struct CMWavStream CMWavStream;
+typedef struct CMModStream CMModStream;
+
+static void CMVoice_RemoveFromMixer(CMVoice* voice);
+static void CMVoice_AddToMix(CMVoice* voice, int len, int32_t* dst);
+
+static inline CMWavStream* CMWavStream_Check(CMVoice* voice);
+static void StreamWav(CMVoice* voice, int16_t* output, int length);
+static void RewindWav(CMVoice* voice);
+static void FreeWav(CMVoice* voice);
+
+static inline CMModStream* CMModStream_Check(CMVoice* voice);
+static void StreamMod(CMVoice* voice, int16_t* output, int length);
+static void FreeMod(CMVoice* voice);
+
+//-----------------------------------------------------------------------------
+// Utilities
+
+static inline int DoubleToFixed(double f)
+{
+	return (int) (f * FX_UNIT);
+}
+
+static inline double FixedToDouble(int f)
+{
+	return (double) f / FX_UNIT;
+}
+
+static inline int FixedLerp(int a, int b, int p)
+{
+	return a + (((b - a) * p) >> FX_BITS);
+}
+
+static inline int16_t UnpackI16BE(const void* data)
+{
+#if __BIG_ENDIAN__
+	// no-op on big-endian systems
+	return *(const uint16_t*) data;
+#else
+	const uint8_t* p = (uint8_t*) data;
+	return	( p[0] << 8 )
+		|	( p[1]      );
+#endif
+}
+
+static inline int16_t UnpackI16LE(const void* data)
+{
+#if __BIG_ENDIAN__
+	const uint8_t* p = (uint8_t*) data;
+	return	( p[0]      )
+		|	( p[1] << 8 );
+#else
+	// no-op on little-endian systems
+	return *(const uint16_t*) data;
+#endif
+}
+
+static inline int MinInt(int a, int b) { return a < b ? a : b; }
+static inline int MaxInt(int a, int b) { return a < b ? b : a; }
+
+static inline int ClampInt(int x, int a, int b) { return x < a ? a : x > b ? b : x; }
+static inline double ClampDouble(double x, double a, double b) { return x < a ? a : x > b ? b : x; }
+
+static char* LoadFile(const char* filename, size_t* outSize)
+{
+	FILE* ifs = fopen(filename, "rb");
+	if (!ifs)
+		return NULL;
+
+	fseek(ifs, 0, SEEK_END);
+	long filesize = ftell(ifs);
+	fseek(ifs, 0, SEEK_SET);
+
+	void* bytes = SDL_malloc(filesize);
+	fread(bytes, 1, filesize, ifs);
+	fclose(ifs);
+
+	if (outSize)
+		*outSize = filesize;
+
+	return (char*)bytes;
+}
+
+static uint8_t BuildPCMFormat(int bitdepth, int channels, bool bigEndian)
+{
+	return ((!!bigEndian) << 7)
+		| (channels << 4)
+		| (bitdepth / 8);
+}
+
+//-----------------------------------------------------------------------------
+// Global mixer
+
+static struct Mixer
+{
+	SDL_mutex* sdlAudioMutex;
+	CMVoice* voices[MAX_CONCURRENT_VOICES];	// List of active (playing) voices
+	int32_t pcmmixbuf[BUFFER_SIZE];			// Internal master buffer
+	int samplerate;							// Master samplerate
+	int gain;								// Master gain (fixed point)
+} gMixer;
+
+static void Mixer_Init(struct Mixer* mixer, int samplerate);
+static void Mixer_Process(struct Mixer* mixer, int16_t* dst, int len);
+static void Mixer_Lock(struct Mixer* mixer);
+static void Mixer_Unlock(struct Mixer* mixer);
+static void Mixer_SetMasterGain(struct Mixer* mixer, double newGain);
+
+//-----------------------------------------------------------------------------
+// Global init/shutdown
+
+static bool sdlAudioSubSystemInited = false;
+static SDL_AudioDeviceID sdlDeviceID = 0;
+
+static void MixerCallback(void* udata, Uint8* stream, int size)
+{
+	struct Mixer* mixer = (struct Mixer*) udata;
+	Mixer_Process(mixer, (int16_t*) stream, size / 2);
+}
+
+void cmixer_InitWithSDL(void)
+{
+	CM_ASSERT(!sdlAudioSubSystemInited, "SDL audio subsystem already inited");
+
+	if (0 != SDL_InitSubSystem(SDL_INIT_AUDIO))
+		CM_DIE(SDL_GetError());
+
+	sdlAudioSubSystemInited = true;
+
+	// Init SDL audio
+	SDL_AudioSpec fmt =
+	{
+		.freq = 44100,
+		.format = AUDIO_S16SYS,
+		.channels = 2,
+		.samples = 1024,
+		.callback = MixerCallback,
+		.userdata = &gMixer,
+	};
+
+	SDL_AudioSpec got;
+	sdlDeviceID = SDL_OpenAudioDevice(NULL, 0, &fmt, &got, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE);
+
+	CM_ASSERT(sdlDeviceID, SDL_GetError());
+
+	// Init library
+	Mixer_Init(&gMixer, got.freq);
+	Mixer_SetMasterGain(&gMixer, 0.5);
+
+	// Start audio
+	SDL_PauseAudioDevice(sdlDeviceID, 0);
+}
+
+void cmixer_ShutdownWithSDL()
+{
+	if (sdlDeviceID)
+	{
+		SDL_CloseAudioDevice(sdlDeviceID);
+		sdlDeviceID = 0;
+	}
+	if (gMixer.sdlAudioMutex)
+	{
+		SDL_DestroyMutex(gMixer.sdlAudioMutex);
+		gMixer.sdlAudioMutex = NULL;
+	}
+	if (sdlAudioSubSystemInited)
+	{
+		SDL_QuitSubSystem(SDL_INIT_AUDIO);
+		sdlAudioSubSystemInited = false;
+	}
+}
+
+double cmixer_GetMasterGain()
+{
+	return FixedToDouble(gMixer.gain);
+}
+
+void cmixer_SetMasterGain(double newGain)
+{
+	Mixer_SetMasterGain(&gMixer, newGain);
+}
+
+//-----------------------------------------------------------------------------
+// Global mixer impl
+
+static void Mixer_Init(struct Mixer* mixer, int newSamplerate)
+{
+	SDL_memset(mixer, 0, sizeof(mixer));
+
+	mixer->sdlAudioMutex = SDL_CreateMutex();
+
+	mixer->samplerate = newSamplerate;
+	mixer->gain = FX_UNIT;
+}
+
+static void Mixer_Lock(struct Mixer* mixer)
+{
+	SDL_LockMutex(mixer->sdlAudioMutex);
+}
+
+static void Mixer_Unlock(struct Mixer* mixer)
+{
+	SDL_UnlockMutex(mixer->sdlAudioMutex);
+}
+
+static void Mixer_SetMasterGain(struct Mixer* mixer, double newGain)
+{
+	if (newGain < 0)
+		newGain = 0;
+	mixer->gain = DoubleToFixed(newGain);
+}
+
+static int Mixer_AddVoice(struct Mixer* mixer, CMVoice* voice)
+{
+	CM_ASSERT(voice->callbacks.fillBuffer, "fill buffer callback not set");
+
+	// Look for a free slot
+	for (int i = 0; i < MAX_CONCURRENT_VOICES; i++)
+	{
+		CM_ASSERT(mixer->voices[i] != voice, "voice added twice to mixer");
+
+		if (!mixer->voices[i])
+		{
+			mixer->voices[i] = voice;
+			return i;
+		}
+	}
+
+	return -1;
+}
+
+static void Mixer_RemoveVoice(struct Mixer* mixer, CMVoice* voice)
+{
+	for (int i = 0; i < MAX_CONCURRENT_VOICES; i++)
+	{
+		if (mixer->voices[i] == voice)
+		{
+			mixer->voices[i] = NULL;
+			break;
+		}
+	}
+}
+
+static void Mixer_Process(struct Mixer* mixer, int16_t* dst, int len)
+{
+	// Process in chunks of BUFFER_SIZE if `len` is larger than BUFFER_SIZE
+	while (len > BUFFER_SIZE)
+	{
+		Mixer_Process(mixer, dst, BUFFER_SIZE);
+		dst += BUFFER_SIZE;
+		len -= BUFFER_SIZE;
+	}
+
+	// Zeroset internal buffer
+	SDL_memset(mixer->pcmmixbuf, 0, len * sizeof(mixer->pcmmixbuf[0]));
+
+	// Process active voices
+	Mixer_Lock(mixer);
+	for (int i = 0; i < MAX_CONCURRENT_VOICES; i++)
+	{
+		CMVoice* voice = mixer->voices[i];
+
+		if (!voice)
+			continue;
+
+		CMVoice_AddToMix(voice, len, mixer->pcmmixbuf);
+
+		// Remove voice from list if it is no longer playing
+		if (voice->state != CM_STATE_PLAYING)
+		{
+			voice->active = false;
+			mixer->voices[i] = NULL;
+		}
+	}
+	Mixer_Unlock(mixer);
+
+	// Copy internal buffer to destination and clip
+	for (int i = 0; i < len; i++)
+	{
+		int x = (mixer->pcmmixbuf[i] * mixer->gain) >> FX_BITS;
+		dst[i] = ClampInt(x, -32768, 32767);
+	}
+}
+
+//-----------------------------------------------------------------------------
+// Voice implementation
+
+static inline CMVoice* CMVoice_Check(void* ptr)
+{
+	CMVoice* voice = (CMVoice*) ptr;
+	CM_ASSERT(voice->cookie == 'VOIX', "VOIX cookie not found");
+	return voice;
+}
+
+static CMVoice* CMVoice_New(int sampleRate, int sampleCount)
+{
+	CMVoice* voice = SDL_calloc(1, sizeof(CMVoice));
+	
+	voice->cookie = 'VOIX';
+	voice->sampleRate = 0;
+	voice->sampleCount = 0;
+	voice->end = 0;
+	voice->state = CM_STATE_STOPPED;
+	voice->position = 0;
+	voice->lgain = 0;
+	voice->rgain = 0;
+	voice->rate = 0;
+	voice->nextfill = 0;
+	voice->loop = false;
+	voice->rewind = true;
+	voice->interpolate = false;
+	voice->gain = 0;
+	voice->pan = 0;
+
+	voice->active = false;
+
+	voice->sampleRate = sampleRate;
+	voice->sampleCount = sampleCount;
+	voice->sustainOffset = 0;
+	CMVoice_SetGain(voice, 1);
+	CMVoice_SetPan(voice, 0);
+	CMVoice_SetPitch(voice, 1);
+	CMVoice_SetLoop(voice, false);
+	CMVoice_Stop(voice);
+
+	return voice;
+}
+
+void CMVoice_Free(CMVoice* voice)
+{
+	CMVoice_Check(voice);
+	CMVoice_RemoveFromMixer(voice);
+
+	if (voice->callbacks.free)
+		voice->callbacks.free(voice);
+
+	voice->cookie = 'DEAD';
+	SDL_free(voice);
+}
+
+static void CMVoice_RemoveFromMixer(CMVoice* voice)
+{
+	CMVoice_Check(voice);
+
+	Mixer_Lock(&gMixer);
+	if (voice->active)
+	{
+		Mixer_RemoveVoice(&gMixer, voice);
+		voice->active = false;
+	}
+	Mixer_Unlock(&gMixer);
+}
+
+void CMVoice_Rewind(CMVoice* voice)
+{
+	if (voice->callbacks.rewind)
+		voice->callbacks.rewind(voice);
+
+	voice->position = 0;
+	voice->rewind = false;
+	voice->end = voice->sampleCount;
+	voice->nextfill = 0;
+}
+
+static void CMVoice_AddToMix(CMVoice* voice, int len, int32_t* dst)
+{
+	CMVoice_Check(voice);		// check pointer validity
+
+	// Do rewind if flag is set
+	if (voice->rewind)
+	{
+		CMVoice_Rewind(voice);
+	}
+
+	// Don't process if not playing
+	if (voice->state != CM_STATE_PLAYING)
+	{
+		return;
+	}
+
+	// Process audio
+	while (len > 0)
+	{
+		// Get current position frame
+		int frame = (int) (voice->position >> FX_BITS);
+
+		// Fill buffer if required
+		if (frame + 3 >= voice->nextfill)
+		{
+			int fillOffset = (voice->nextfill * 2) & BUFFER_MASK;
+			int fillLength = BUFFER_SIZE / 2;
+
+			voice->callbacks.fillBuffer(voice, voice->pcmbuf + fillOffset, fillLength);
+			voice->nextfill += BUFFER_SIZE / 4;
+		}
+
+		// Handle reaching the end of the playthrough
+		if (frame >= voice->end)
+		{
+			// As streams continuously fill the raw buffer in a loop,
+			// increment the end idx by one length
+			// and continue reading from it another playthrough
+			voice->end = frame + voice->sampleCount;
+
+			// Set state and stop processing if we're not set to loop
+			if (!voice->loop)
+			{
+				voice->state = CM_STATE_STOPPED;
+
+				if (voice->callbacks.completed)
+					voice->callbacks.completed(voice);
+
+				break;
+			}
+		}
+
+		// Work out how many frames we should process in the loop
+		int n = MinInt(voice->nextfill - 2, voice->end) - frame;
+		int count = (n << FX_BITS) / voice->rate;
+		count = MaxInt(count, 1);
+		count = MinInt(count, len / 2);
+		len -= count * 2;
+
+		// Add audio to master buffer
+		if (voice->rate == FX_UNIT)
+		{
+			// Add audio to buffer -- basic
+			n = frame * 2;
+			for (int i = 0; i < count; i++)
+			{
+				dst[0] += (voice->pcmbuf[(n    ) & BUFFER_MASK] * voice->lgain) >> FX_BITS;
+				dst[1] += (voice->pcmbuf[(n + 1) & BUFFER_MASK] * voice->rgain) >> FX_BITS;
+				n += 2;
+				dst += 2;
+			}
+			voice->position += count * FX_UNIT;
+		}
+		else if (voice->interpolate)
+		{
+			// Resample audio (with linear interpolation) and add to buffer
+			for (int i = 0; i < count; i++)
+			{
+				n = (int) (voice->position >> FX_BITS) * 2;
+				int p = voice->position & FX_MASK;
+				int a = voice->pcmbuf[(n    ) & BUFFER_MASK];
+				int b = voice->pcmbuf[(n + 2) & BUFFER_MASK];
+				dst[0] += (FixedLerp(a, b, p) * voice->lgain) >> FX_BITS;
+				n++;
+				a = voice->pcmbuf[(n    ) & BUFFER_MASK];
+				b = voice->pcmbuf[(n + 2) & BUFFER_MASK];
+				dst[1] += (FixedLerp(a, b, p) * voice->rgain) >> FX_BITS;
+				voice->position += voice->rate;
+				dst += 2;
+			}
+		}
+		else
+		{
+			// Resample audio (without interpolation) and add to buffer
+			for (int i = 0; i < count; i++)
+			{
+				n = (int) (voice->position >> FX_BITS) * 2;
+				dst[0] += (voice->pcmbuf[(n    ) & BUFFER_MASK] * voice->lgain) >> FX_BITS;
+				dst[1] += (voice->pcmbuf[(n + 1) & BUFFER_MASK] * voice->rgain) >> FX_BITS;
+				voice->position += voice->rate;
+				dst += 2;
+			}
+		}
+	}
+}
+
+double CMVoice_GetLength(const CMVoice* voice)
+{
+	return voice->sampleCount / (double) voice->sampleRate;
+}
+
+double CMVoice_GetPosition(const CMVoice* voice)
+{
+	return ((voice->position >> FX_BITS) % voice->sampleCount) / (double) voice->sampleRate;
+}
+
+int CMVoice_GetState(const CMVoice* voice)
+{
+	return voice->state;
+}
+
+static void CMVoice_RecalcGains(CMVoice* voice)
+{
+	double l = voice->gain * (voice->pan <= 0. ? 1. : 1. - voice->pan);
+	double r = voice->gain * (voice->pan >= 0. ? 1. : 1. + voice->pan);
+	voice->lgain = DoubleToFixed(l);
+	voice->rgain = DoubleToFixed(r);
+}
+
+void CMVoice_SetGain(CMVoice* voice, double newGain)
+{
+	voice->gain = newGain;
+	CMVoice_RecalcGains(voice);
+}
+
+void CMVoice_SetPan(CMVoice* voice, double newPan)
+{
+	voice->pan = ClampDouble(newPan, -1.0, 1.0);
+	CMVoice_RecalcGains(voice);
+}
+
+void CMVoice_SetPitch(CMVoice* voice, double newPitch)
+{
+	double newRate;
+	if (newPitch > 0.)
+	{
+		newRate = (double)voice->sampleRate / (double) gMixer.samplerate * newPitch;
+	}
+	else
+	{
+		newRate = 0.001;
+	}
+	voice->rate = DoubleToFixed(newRate);
+}
+
+void CMVoice_SetLoop(CMVoice* voice, int newLoop)
+{
+	voice->loop = newLoop;
+}
+
+void CMVoice_SetInterpolation(CMVoice* voice, int newInterpolation)
+{
+	voice->interpolate = newInterpolation;
+}
+
+void CMVoice_Play(CMVoice* voice)
+{
+	CMVoice_Check(voice);	// check pointer validity
+
+	if (voice->sampleCount == 0)
+	{
+		// Don't attempt to play an empty voice as this would result
+		// in instant starvation when filling mixer buffer
+		return;
+	}
+
+	Mixer_Lock(&gMixer);
+	if (!voice->active)
+	{
+		int rc = Mixer_AddVoice(&gMixer, voice);
+		if (rc < 0)
+		{
+			// couldn't add voice
+		}
+		else
+		{
+			voice->state = CM_STATE_PLAYING;
+			voice->active = true;
+		}
+	}
+	Mixer_Unlock(&gMixer);
+}
+
+void CMVoice_Pause(CMVoice* voice)
+{
+	voice->state = CM_STATE_PAUSED;
+}
+
+void CMVoice_TogglePause(CMVoice* voice)
+{
+	if (voice->state == CM_STATE_PAUSED)
+		CMVoice_Play(voice);
+	else if (voice->state == CM_STATE_PLAYING)
+		CMVoice_Pause(voice);
+}
+
+void CMVoice_Stop(CMVoice* voice)
+{
+	voice->state = CM_STATE_STOPPED;
+	voice->rewind = true;
+}
+
+//-----------------------------------------------------------------------------
+// WavStream implementation
+
+static inline CMWavStream* CMWavStream_Check(CMVoice* voice)
+{
+	CM_ASSERT(voice->cookie == 'VOIX', "VOIX cookie not found");
+	CM_ASSERT(voice->wav.cookie == 'WAVS', "WAVS cookie not found");
+	return &voice->wav;
+}
+
+static CMWavStream* InstallWavStream(CMVoice* voice)
+{
+	CMWavStream* wav = &voice->wav;
+
+	wav->cookie = 'WAVS';
+	wav->pcmformat = PCMFORMAT_NULL;
+	wav->idx = 0;
+
+	voice->callbacks.fillBuffer = StreamWav;
+	voice->callbacks.rewind = RewindWav;
+	voice->callbacks.free = FreeWav;
+
+	return wav;
+}
+
+static void FreeWav(CMVoice* voice)
+{
+	CMWavStream* wav = CMWavStream_Check(voice);
+
+	if (!wav->data)
+	{
+		return;
+	}
+
+	if (wav->ownData)
+	{
+		SDL_free(wav->data);
+	}
+
+	wav->data = NULL;
+	wav->dataLength = 0;
+	wav->ownData = false;
+	wav->cookie = 'DEAD';
+}
+
+static void RewindWav(CMVoice* voice)
+{
+	CMWavStream* wav = CMWavStream_Check(voice);
+	wav->idx = 0;
+}
+
+static void StreamWav(CMVoice* voice, int16_t* dst, int fillLength)
+{
+	CMWavStream* wav = CMWavStream_Check(voice);
+
+	int x, n;
+
+	fillLength /= 2;
+
+	const int16_t* data16 = (const int16_t*) wav->data;
+	const uint8_t* data8 = (const uint8_t*) wav->data;
+
+#define WAV_PROCESS_LOOP(X) \
+	while (n--)             \
+	{                       \
+		X                   \
+		dst += 2;           \
+		wav->idx++;         \
+	}
+
+	while (fillLength > 0)
+	{
+		n = MinInt(fillLength, voice->sampleCount - wav->idx);
+
+		fillLength -= n;
+
+		switch (wav->pcmformat)
+		{
+			case PCMFORMAT_1CH_BE16:
+				WAV_PROCESS_LOOP({
+					dst[0] = dst[1] = UnpackI16BE(&data16[wav->idx]);
+				});
+				break;
+
+			case PCMFORMAT_2CH_BE16:
+				WAV_PROCESS_LOOP({
+					x = wav->idx * 2;
+					dst[0] = UnpackI16BE(&data16[x]);
+					dst[1] = UnpackI16BE(&data16[x + 1]);
+				});
+				break;
+
+			case PCMFORMAT_1CH_LE16:
+				WAV_PROCESS_LOOP({
+					dst[0] = dst[1] = UnpackI16LE(&data16[wav->idx]);
+				});
+				break;
+		
+			case PCMFORMAT_2CH_LE16:
+				WAV_PROCESS_LOOP({
+					x = wav->idx * 2;
+					dst[0] = UnpackI16LE(&data16[x]);
+					dst[1] = UnpackI16LE(&data16[x + 1]);
+				});
+				break;
+
+			case PCMFORMAT_1CH_8:
+			case PCMFORMAT_1CH_8 | 0x80:		// with big-endian flag
+				WAV_PROCESS_LOOP({
+					dst[0] = dst[1] = (data8[wav->idx] - 128) << 8;
+				});
+				break;
+
+			case PCMFORMAT_2CH_8:
+			case PCMFORMAT_2CH_8 | 0x80:		// with big-endian flag
+				WAV_PROCESS_LOOP({
+					x = wav->idx * 2;
+					dst[0] = (data8[x] - 128) << 8;
+					dst[1] = (data8[x + 1] - 128) << 8;
+				});
+				break;
+
+			default:
+				CM_DIE("unknown pcmformat");
+				break;
+		}
+
+		// Loop back and continue filling buffer if we didn't fill the buffer
+		if (fillLength > 0)
+		{
+			wav->idx = voice->sustainOffset;
+		}
+	}
+
+#undef WAV_PROCESS_LOOP
+}
+
+//-----------------------------------------------------------------------------
+// LoadWAVFromFile
+
+static const char* FindRIFFChunk(const char* data, size_t len, const char* id, int* size)
+{
+	// TODO : Error handling on malformed wav file
+	size_t idlen = SDL_strlen(id);
+	const char* p = data + 12;
+next:
+	*size = *((uint32_t*)(p + 4));
+	if (SDL_memcmp(p, id, idlen))
+	{
+		p += 8 + *size;
+		if (p > data + len)
+			return NULL;
+		goto next;
+	}
+	return p + 8;
+}
+
+CMVoice* CMVoice_LoadWAV(const char* path)
+{
+	int sz;
+
+	size_t len = 0;
+	char *const data = LoadFile(path, &len);
+
+	const char* p = (char*)data;
+
+	// Check header
+	if (SDL_memcmp(p, "RIFF", 4) || SDL_memcmp(p + 8, "WAVE", 4))
+		CM_DIE("not a WAVE file");
+
+	// Find fmt subchunk
+	p = FindRIFFChunk(data, len, "fmt ", &sz);
+	CM_ASSERT(p, "no fmt chunk in WAVE");
+
+	// Load fmt info
+	int format		= *((uint16_t*)(p));
+	int channels	= *((uint16_t*)(p + 2));
+	int samplerate	= *((uint32_t*)(p + 4));
+	int bitdepth	= *((uint16_t*)(p + 14));
+	CM_ASSERT(format == 1, "unsupported WAVE format");
+	CM_ASSERT(channels == 1 || channels == 2, "unsupported channel count");
+	CM_ASSERT(bitdepth == 8 || bitdepth == 16, "unsupported bitdepth");
+	CM_ASSERT(samplerate != 0, "weird samplerate");
+
+	// Find data subchunk
+	p = FindRIFFChunk(data, len, "data", &sz);
+	CM_ASSERT(p, "no data chunk in WAVE");
+
+	const char* sampleData = p;
+	int sampleDataLength = sz;
+	int samplecount = (sampleDataLength / (bitdepth / 8)) / channels;
+
+	CMVoice* voice = CMVoice_New(samplerate, samplecount);
+	CMWavStream* wav = InstallWavStream(voice);
+
+	wav->pcmformat = BuildPCMFormat(bitdepth, channels, 0);
+	wav->data = SDL_malloc(sampleDataLength);
+	wav->dataLength = sampleDataLength;
+	wav->ownData = true;
+	SDL_memcpy(wav->data, sampleData, sampleDataLength);
+
+	SDL_free(data);
+
+	CM_ASSERT(wav->pcmformat != 0, "weird pcmformat");
+
+	return voice;
+}
+
+//-----------------------------------------------------------------------------
+// ModStream
+
+static inline CMModStream* CMModStream_Check(CMVoice* voice)
+{
+	CM_ASSERT(voice->cookie == 'VOIX', "VOIX cookie not found");
+	CM_ASSERT(voice->mod.cookie == 'MODS', "MODS cookie not found");
+	return &voice->mod;
+}
+
+CMVoice* CMVoice_LoadMOD(const char* path)
+{
+	char errors[64];
+	errors[0] = '\0';
+
+	size_t moduleFileSize = 0;
+	char* moduleFile = LoadFile(path, &moduleFileSize);
+	struct data d = { .buffer = moduleFile, .length = (int)moduleFileSize };
+
+	CMVoice* voice = CMVoice_New(gMixer.samplerate, INT_MAX);
+	voice->callbacks.fillBuffer = StreamMod;
+	voice->callbacks.free = FreeMod;
+
+	voice->mod.cookie = 'MODS';
+	voice->mod.replayBuffer = SDL_calloc(1, 2048 * 8 * sizeof(voice->mod.replayBuffer[0]));
+	voice->mod.replayBufferOffset = 0;
+	voice->mod.replayBufferSamples = 0;
+	voice->mod.playbackSpeedMult = 1.0;
+	voice->mod.moduleFileMemory = moduleFile;
+	voice->mod.module = module_load(&d, errors);
+	voice->mod.replay = new_replay(voice->mod.module, gMixer.samplerate, 0);
+
+	CM_ASSERT(!errors[0], errors);
+
+	return voice;
+}
+
+static void FreeMod(CMVoice* voice)
+{
+	CMModStream* mod = CMModStream_Check(voice);
+
+	dispose_module(mod->module);
+	SDL_free(mod->moduleFileMemory);
+	SDL_free(mod->replayBuffer);
+
+	mod->cookie = 'DEAD';
+}
+
+void CMVoice_SetMODPlaybackSpeed(CMVoice* voice, double speed)
+{
+	CMModStream* mod = CMModStream_Check(voice);
+
+	mod->playbackSpeedMult = speed;
+}
+
+static void StreamMod(CMVoice* voice, int16_t* output, int length)
+{
+	CMModStream* mod = CMModStream_Check(voice);
+
+	length /= 2;
+
+	while (length > 0)
+	{
+		// refill replay buffer if exhausted
+		if (mod->replayBufferSamples == 0)
+		{
+			mod->replayBufferOffset = 0;
+			mod->replayBufferSamples = replay_get_audio(mod->replay, mod->replayBuffer, 0, (int)(mod->playbackSpeedMult * 100.0));
+		}
+
+		// number of stereo samples to copy from replay buffer to output buffer
+		int nToCopy = MinInt(mod->replayBufferSamples, length);
+
+		int* input = &mod->replayBuffer[mod->replayBufferOffset * 2];
+
+		// Copy samples
+		for (int i = 0; i < nToCopy * 2; i++)
+		{
+			int sample = *(input++);
+			sample = ClampInt(sample, -32768, 32767);
+			*(output++) = sample;
+		}
+
+		mod->replayBufferOffset += nToCopy;
+		mod->replayBufferSamples -= nToCopy;
+		length -= nToCopy;
+	}
+}
--- a/src/support/cmixer.cpp
+++ /dev/null
@@ -1,711 +1,0 @@
-// Adapted from cmixer by rxi (https://github.com/rxi/cmixer)
-
-/*
-** 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 "cmixer.h"
-#include <SDL.h>
-
-#include <vector>
-#include <fstream>
-#include <list>
-
-using namespace cmixer;
-
-#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)  ((long)((f) * FX_UNIT))
-#define DOUBLE_FROM_FX(f)  ((double)f / FX_UNIT)
-#define FX_LERP(a, b, p)  ((a) + ((((b) - (a)) * (p)) >> FX_BITS))
-
-#define BUFFER_MASK (BUFFER_SIZE - 1)
-
-static inline int16_t UnpackI16BE(const void* data)
-{
-#if __BIG_ENDIAN__
-	// no-op on big-endian systems
-	return *(const uint16_t*) data;
-#else
-	const uint8_t* p = (uint8_t*) data;
-	return	( p[0] << 8 )
-		|	( p[1]      );
-#endif
-}
-
-static inline int16_t UnpackI16LE(const void* data)
-{
-#if __BIG_ENDIAN__
-	const uint8_t* p = (uint8_t*) data;
-	return	( p[0]      )
-		|	( p[1] << 8 );
-#else
-	// no-op on little-endian systems
-	return *(const uint16_t*) data;
-#endif
-}
-
-//-----------------------------------------------------------------------------
-// Global mixer
-
-static struct Mixer
-{
-	SDL_mutex* sdlAudioMutex;
-
-	std::list<Source*> sources;   // Linked list of active (playing) sources
-	int32_t pcmmixbuf[BUFFER_SIZE]; // Internal master buffer
-	int samplerate;               // Master samplerate
-	int gain;                     // Master gain (fixed point)
-
-	void Init(int samplerate);
-
-	void Process(int16_t* dst, int len);
-
-	void Lock();
-
-	void Unlock();
-
-	void SetMasterGain(double newGain);
-} gMixer = {};
-
-//-----------------------------------------------------------------------------
-// Global init/shutdown
-
-static bool sdlAudioSubSystemInited = false;
-static SDL_AudioDeviceID sdlDeviceID = 0;
-
-void cmixer::InitWithSDL()
-{
-	if (sdlAudioSubSystemInited)
-		throw std::runtime_error("SDL audio subsystem already inited");
-
-	if (0 != SDL_InitSubSystem(SDL_INIT_AUDIO))
-		throw std::runtime_error("couldn't init SDL audio subsystem");
-
-	sdlAudioSubSystemInited = true;
-
-	// Init SDL audio
-	SDL_AudioSpec fmt = {};
-	fmt.freq = 44100;
-	fmt.format = AUDIO_S16SYS;
-	fmt.channels = 2;
-	fmt.samples = 1024;
-	fmt.callback = [](void* udata, Uint8* stream, int size)
-	{
-		(void) udata;
-		gMixer.Process((int16_t*) stream, size / 2);
-	};
-
-	SDL_AudioSpec got;
-	sdlDeviceID = SDL_OpenAudioDevice(NULL, 0, &fmt, &got, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE);
-	if (!sdlDeviceID)
-		throw std::runtime_error(SDL_GetError());
-
-	// Init library
-	gMixer.Init(got.freq);
-	gMixer.SetMasterGain(0.5);
-
-	// Start audio
-	SDL_PauseAudioDevice(sdlDeviceID, 0);
-}
-
-void cmixer::ShutdownWithSDL()
-{
-	if (sdlDeviceID)
-	{
-		SDL_CloseAudioDevice(sdlDeviceID);
-		sdlDeviceID = 0;
-	}
-	if (gMixer.sdlAudioMutex)
-	{
-		SDL_DestroyMutex(gMixer.sdlAudioMutex);
-		gMixer.sdlAudioMutex = nullptr;
-	}
-	if (sdlAudioSubSystemInited)
-	{
-		SDL_QuitSubSystem(SDL_INIT_AUDIO);
-		sdlAudioSubSystemInited = false;
-	}
-}
-
-double cmixer::GetMasterGain()
-{
-	return DOUBLE_FROM_FX(gMixer.gain);
-}
-
-void cmixer::SetMasterGain(double newGain)
-{
-	gMixer.SetMasterGain(newGain);
-}
-
-//-----------------------------------------------------------------------------
-// Global mixer impl
-
-void Mixer::Lock()
-{
-	SDL_LockMutex(sdlAudioMutex);
-}
-
-void Mixer::Unlock()
-{
-	SDL_UnlockMutex(sdlAudioMutex);
-}
-
-void Mixer::Init(int newSamplerate)
-{
-	sdlAudioMutex = SDL_CreateMutex();
-
-	samplerate = newSamplerate;
-	gain = FX_UNIT;
-}
-
-void Mixer::SetMasterGain(double newGain)
-{
-	if (newGain < 0)
-		newGain = 0;
-	gain = (int) FX_FROM_FLOAT(newGain);
-}
-
-void Mixer::Process(int16_t* dst, int len)
-{
-	// Process in chunks of BUFFER_SIZE if `len` is larger than BUFFER_SIZE
-	while (len > BUFFER_SIZE)
-	{
-		Process(dst, BUFFER_SIZE);
-		dst += BUFFER_SIZE;
-		len -= BUFFER_SIZE;
-	}
-
-	// Zeroset internal buffer
-	memset(pcmmixbuf, 0, len * sizeof(pcmmixbuf[0]));
-
-	// Process active sources
-	Lock();
-	for (auto si = sources.begin(); si != sources.end();)
-	{
-		auto& s = **si;
-		s.Process(len);
-		// Remove source from list if it is no longer playing
-		if (s.state != CM_STATE_PLAYING)
-		{
-			s.active = false;
-			si = sources.erase(si);
-		}
-		else
-		{
-			++si;
-		}
-	}
-	Unlock();
-
-	// Copy internal buffer to destination and clip
-	for (int i = 0; i < len; i++)
-	{
-		int x = (pcmmixbuf[i] * gain) >> FX_BITS;
-		dst[i] = CLAMP(x, -32768, 32767);
-	}
-}
-
-//-----------------------------------------------------------------------------
-// Source implementation
-
-Source::Source()
-{
-	ClearPrivate();
-	active = false;
-}
-
-void Source::ClearPrivate()
-{
-	samplerate	= 0;
-	length		= 0;
-	end			= 0;
-	state		= CM_STATE_STOPPED;
-	position	= 0;
-	lgain		= 0;
-	rgain		= 0;
-	rate		= 0;
-	nextfill	= 0;
-	loop		= false;
-	rewind		= true;
-	interpolate = false;
-	// DON'T touch active. The source may still be in gMixer!
-	gain		= 0;
-	pan			= 0;
-	onComplete	= nullptr;
-}
-
-void Source::Clear()
-{
-	gMixer.Lock();
-	ClearPrivate();
-	ClearImplementation();
-	gMixer.Unlock();
-}
-
-void Source::Init(int theSampleRate, int theLength)
-{
-	this->samplerate = theSampleRate;
-	this->length = theLength;
-	this->sustainOffset = 0;
-	SetGain(1);
-	SetPan(0);
-	SetPitch(1);
-	SetLoop(false);
-	Stop();
-}
-
-void Source::RemoveFromMixer()
-{
-	gMixer.Lock();
-	if (active)
-	{
-		gMixer.sources.remove(this);
-		active = false;
-	}
-	gMixer.Unlock();
-}
-
-Source::~Source()
-{
-	if (active)
-	{
-		// You MUST call RemoveFromMixer before destroying a source. If you get here, your program is incorrect.
-		fprintf(stderr, "Source wasn't removed from mixer prior to destruction!\n");
-#if _DEBUG
-		std::terminate();
-#endif
-	}
-}
-
-void Source::Rewind()
-{
-	RewindImplementation();
-	position = 0;
-	rewind = false;
-	end = length;
-	nextfill = 0;
-}
-
-void Source::FillBuffer(int offset, int fillLength)
-{
-	FillBuffer(pcmbuf + offset, fillLength);
-}
-
-void Source::Process(int len)
-{
-	int32_t* dst = gMixer.pcmmixbuf;
-
-	// Do rewind if flag is set
-	if (rewind)
-	{
-		Rewind();
-	}
-
-	// Don't process if not playing
-	if (state != CM_STATE_PLAYING)
-	{
-		return;
-	}
-
-	// Process audio
-	while (len > 0)
-	{
-		// Get current position frame
-		int frame = int(position >> FX_BITS);
-
-		// Fill buffer if required
-		if (frame + 3 >= nextfill)
-		{
-			FillBuffer((nextfill * 2) & BUFFER_MASK, BUFFER_SIZE / 2);
-			nextfill += BUFFER_SIZE / 4;
-		}
-
-		// Handle reaching the end of the playthrough
-		if (frame >= 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
-			end = frame + this->length;
-			// Set state and stop processing if we're not set to loop
-			if (!loop)
-			{
-				state = CM_STATE_STOPPED;
-				if (onComplete != nullptr)
-					onComplete();
-				break;
-			}
-		}
-
-		// Work out how many frames we should process in the loop
-		int n = MIN(nextfill - 2, end) - frame;
-		int count = (n << FX_BITS) / rate;
-		count = MAX(count, 1);
-		count = MIN(count, len / 2);
-		len -= count * 2;
-
-		// Add audio to master buffer
-		if (rate == FX_UNIT)
-		{
-			// Add audio to buffer -- basic
-			n = frame * 2;
-			for (int i = 0; i < count; i++)
-			{
-				dst[0] += (pcmbuf[(n    ) & BUFFER_MASK] * lgain) >> FX_BITS;
-				dst[1] += (pcmbuf[(n + 1) & BUFFER_MASK] * rgain) >> FX_BITS;
-				n += 2;
-				dst += 2;
-			}
-			this->position += count * FX_UNIT;
-		}
-		else if (interpolate)
-		{
-			// Resample audio (with linear interpolation) and add to buffer
-			for (int i = 0; i < count; i++)
-			{
-				n = int(position >> FX_BITS) * 2;
-				int p = position & FX_MASK;
-				int a = pcmbuf[(n    ) & BUFFER_MASK];
-				int b = pcmbuf[(n + 2) & BUFFER_MASK];
-				dst[0] += (FX_LERP(a, b, p) * lgain) >> FX_BITS;
-				n++;
-				a = pcmbuf[(n    ) & BUFFER_MASK];
-				b = pcmbuf[(n + 2) & BUFFER_MASK];
-				dst[1] += (FX_LERP(a, b, p) * rgain) >> FX_BITS;
-				position += rate;
-				dst += 2;
-			}
-		}
-		else
-		{
-			// Resample audio (without interpolation) and add to buffer
-			for (int i = 0; i < count; i++)
-			{
-				n = int(position >> FX_BITS) * 2;
-				dst[0] += (pcmbuf[(n    ) & BUFFER_MASK] * lgain) >> FX_BITS;
-				dst[1] += (pcmbuf[(n + 1) & BUFFER_MASK] * rgain) >> FX_BITS;
-				position += rate;
-				dst += 2;
-			}
-		}
-	}
-}
-
-double Source::GetLength() const
-{
-	return length / (double) samplerate;
-}
-
-double Source::GetPosition() const
-{
-	return ((position >> FX_BITS) % length) / (double) samplerate;
-}
-
-int Source::GetState() const
-{
-	return state;
-}
-
-void Source::RecalcGains()
-{
-	double l = this->gain * (pan <= 0. ? 1. : 1. - pan);
-	double r = this->gain * (pan >= 0. ? 1. : 1. + pan);
-	this->lgain = (int) FX_FROM_FLOAT(l);
-	this->rgain = (int) FX_FROM_FLOAT(r);
-}
-
-void Source::SetGain(double newGain)
-{
-	gain = newGain;
-	RecalcGains();
-}
-
-void Source::SetPan(double newPan)
-{
-	pan = CLAMP(newPan, -1.0, 1.0);
-	RecalcGains();
-}
-
-void Source::SetPitch(double newPitch)
-{
-	double newRate;
-	if (newPitch > 0.)
-	{
-		newRate = samplerate / (double) gMixer.samplerate * newPitch;
-	}
-	else
-	{
-		newRate = 0.001;
-	}
-	rate = (int) FX_FROM_FLOAT(newRate);
-}
-
-void Source::SetLoop(bool newLoop)
-{
-	loop = newLoop;
-}
-
-void Source::SetInterpolation(bool newInterpolation)
-{
-	interpolate = newInterpolation;
-}
-
-void Source::Play()
-{
-	if (length == 0)
-	{
-		// Don't attempt to play an empty source as this would result
-		// in instant starvation when filling mixer buffer
-		return;
-	}
-
-	gMixer.Lock();
-	state = CM_STATE_PLAYING;
-	if (!active)
-	{
-		active = true;
-		gMixer.sources.push_front(this);
-	}
-	gMixer.Unlock();
-}
-
-void Source::Pause()
-{
-	state = CM_STATE_PAUSED;
-}
-
-void Source::TogglePause()
-{
-	if (state == CM_STATE_PAUSED)
-		Play();
-	else if (state == CM_STATE_PLAYING)
-		Pause();
-}
-
-void Source::Stop()
-{
-	state = CM_STATE_STOPPED;
-	rewind = true;
-}
-
-//-----------------------------------------------------------------------------
-// WavStream implementation
-
-#define WAV_PROCESS_LOOP(X) \
-	while (n--)             \
-	{                       \
-		X                   \
-		dst += 2;           \
-		idx++;              \
-	}
-
-WavStream::WavStream()
-	: Source()
-{
-	ClearImplementation();
-}
-
-void WavStream::ClearImplementation()
-{
-	bitdepth = 0;
-	channels = 0;
-	idx = 0;
-
-#if __BIG_ENDIAN__		// default to native endianness
-	bigEndian = true;
-#else
-	bigEndian = false;
-#endif
-
-	userBuffer.clear();
-}
-
-void WavStream::Init(
-	int theSampleRate,
-	int theBitDepth,
-	int theNChannels,
-	bool theBigEndian,
-	std::span<char> theSpan)
-{
-	Clear();
-	Source::Init(theSampleRate, int((theSpan.size() / (theBitDepth / 8)) / theNChannels));
-	this->bitdepth = theBitDepth;
-	this->channels = theNChannels;
-	this->idx = 0;
-	this->span = theSpan;
-	this->bigEndian = theBigEndian;
-}
-
-std::span<char> WavStream::GetBuffer(int nBytesOut)
-{
-	userBuffer.clear();
-	userBuffer.reserve(nBytesOut);
-	return std::span(userBuffer.data(), nBytesOut);
-}
-
-std::span<char> WavStream::SetBuffer(std::vector<char>&& data)
-{
-	userBuffer = std::move(data);
-	return std::span(userBuffer.data(), userBuffer.size());
-}
-
-void WavStream::RewindImplementation()
-{
-	idx = 0;
-}
-
-void WavStream::FillBuffer(int16_t* dst, int fillLength)
-{
-	int x, n;
-
-	fillLength /= 2;
-
-	while (fillLength > 0)
-	{
-		n = MIN(fillLength, length - idx);
-
-		fillLength -= n;
-
-		if (bigEndian && bitdepth == 16 && channels == 1)
-		{
-			WAV_PROCESS_LOOP({
-				dst[0] = dst[1] = UnpackI16BE(&data16()[idx]);
-			});
-		}
-		else if (bigEndian && bitdepth == 16 && channels == 2)
-		{
-			WAV_PROCESS_LOOP({
-				x = idx * 2;
-				dst[0] = UnpackI16BE(&data16()[x]);
-				dst[1] = UnpackI16BE(&data16()[x + 1]);
-			});
-		}
-		else if (bitdepth == 16 && channels == 1)
-		{
-			WAV_PROCESS_LOOP({
-				dst[0] = dst[1] = UnpackI16LE(&data16()[idx]);
-			});
-		}
-		else if (bitdepth == 16 && channels == 2)
-		{
-			WAV_PROCESS_LOOP({
-				x = idx * 2;
-				dst[0] = UnpackI16LE(&data16()[x]);
-				dst[1] = UnpackI16LE(&data16()[x + 1]);
-			});
-		}
-		else if (bitdepth == 8 && channels == 1)
-		{
-			WAV_PROCESS_LOOP({
-				dst[0] = dst[1] = (data8()[idx] - 128) << 8;
-			});
-		}
-		else if (bitdepth == 8 && channels == 2)
-		{
-			WAV_PROCESS_LOOP({
-				x = idx * 2;
-				dst[0] = (data8()[x] - 128) << 8;
-				dst[1] = (data8()[x + 1] - 128) << 8;
-			});
-		}
-		// Loop back and continue filling buffer if we didn't fill the buffer
-		if (fillLength > 0)
-		{
-			idx = sustainOffset;
-		}
-	}
-}
-
-//-----------------------------------------------------------------------------
-// LoadWAVFromFile
-
-static std::vector<char> LoadFile(char const* filename)
-{
-	std::ifstream ifs(filename, std::ios::binary | std::ios::ate);
-	auto pos = ifs.tellg();
-	std::vector<char> bytes(pos);
-	ifs.seekg(0, std::ios::beg);
-	ifs.read(&bytes[0], pos);
-	return bytes;
-}
-
-static const char* FindChunk(const char* data, int len, const char* id, int* size)
-{
-	// TODO : Error handling on malformed wav file
-	int idlen = strlen(id);
-	const char* p = data + 12;
-next:
-	*size = *((uint32_t*)(p + 4));
-	if (memcmp(p, id, idlen)) {
-		p += 8 + *size;
-		if (p > data + len) return NULL;
-		goto next;
-	}
-	return p + 8;
-}
-
-WavStream cmixer::LoadWAVFromFile(const char* path)
-{
-	int sz;
-	auto filebuf = LoadFile(path);
-	auto len = filebuf.size();
-	const char* data = filebuf.data();
-	const char* p = (char*)data;
-
-	// Check header
-	if (memcmp(p, "RIFF", 4) || memcmp(p + 8, "WAVE", 4))
-		throw std::invalid_argument("bad wav header");
-
-	// Find fmt subchunk
-	p = FindChunk(data, len, "fmt ", &sz);
-	if (!p)
-		throw std::invalid_argument("no fmt subchunk");
-
-	// Load fmt info
-	int format = *((uint16_t*)(p));
-	int channels = *((uint16_t*)(p + 2));
-	int samplerate = *((uint32_t*)(p + 4));
-	int bitdepth = *((uint16_t*)(p + 14));
-	if (format != 1)
-		throw std::invalid_argument("unsupported format");
-	if (channels == 0 || samplerate == 0 || bitdepth == 0)
-		throw std::invalid_argument("bad format");
-
-	// Find data subchunk
-	p = FindChunk(data, len, "data", &sz);
-	if (!p)
-		throw std::invalid_argument("no data subchunk");
-
-	WavStream wavStream;
-	wavStream.Init(
-		samplerate,
-		bitdepth,
-		channels,
-		false,
-		wavStream.SetBuffer(std::vector<char>(p, p + sz)));
-	return wavStream;
-}
--- a/src/support/cmixer.h
+++ b/src/support/cmixer.h
@@ -1,167 +1,36 @@
-// Adapted from cmixer by rxi (https://github.com/rxi/cmixer)
-
-/*
-** 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.
-**/
-
 #pragma once
 
-#include <vector>
-#include <functional>
-#include <cstdint>
-#include <span>
-
-#define BUFFER_SIZE (512)
-
-namespace cmixer
+enum
 {
+	CM_STATE_STOPPED,
+	CM_STATE_PLAYING,
+	CM_STATE_PAUSED
+};
 
-	enum
-	{
-		CM_STATE_STOPPED,
-		CM_STATE_PLAYING,
-		CM_STATE_PAUSED
-	};
+typedef struct CMVoice* CMVoicePtr;
+typedef const struct CMVoice* CMVoiceConstPtr;
 
-	struct Source
-	{
-		int16_t pcmbuf[BUFFER_SIZE];    // Internal buffer with raw stereo PCM
-		int samplerate;                 // Stream's native samplerate
-		int length;                     // Stream's length in frames
-		int sustainOffset;              // Offset of the sustain loop in frames
-		int end;                        // End index for the current play-through
-		int state;                      // Current state (playing|paused|stopped)
-		int64_t 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
-		bool loop;                      // Whether the source will loop when `end` is reached
-		bool rewind;                    // Whether the source will rewind before playing
-		bool active;                    // Whether the source is part of `sources` list
-		bool interpolate;               // Interpolated resampling when played back at a non-native rate
-		double gain;                    // Gain set by `cm_set_gain()`
-		double pan;                     // Pan set by `cm_set_pan()`
-		std::function<void()> onComplete;        // Callback
+void					CMVoice_Free(CMVoicePtr voice);
+void					CMVoice_Rewind(CMVoicePtr voice);
+double					CMVoice_GetLength(CMVoiceConstPtr voice);
+double					CMVoice_GetPosition(CMVoiceConstPtr voice);
+int						CMVoice_GetState(CMVoiceConstPtr voice);
+void					CMVoice_SetGain(CMVoicePtr voice, double gain);
+void					CMVoice_SetPan(CMVoicePtr voice, double pan);
+void					CMVoice_SetPitch(CMVoicePtr voice, double pitch);
+void					CMVoice_SetLoop(CMVoicePtr voice, int loop);
+void					CMVoice_SetInterpolation(CMVoicePtr voice, int interpolation);
+void					CMVoice_Play(CMVoicePtr voice);
+void					CMVoice_Pause(CMVoicePtr voice);
+void					CMVoice_TogglePause(CMVoicePtr voice);
+void					CMVoice_Stop(CMVoicePtr voice);
 
-		void ClearPrivate();
+CMVoicePtr				CMVoice_LoadWAV(const char* path);
 
-	protected:
-		Source();
+CMVoicePtr				CMVoice_LoadMOD(const char* path);
+void					CMVoice_SetMODPlaybackSpeed(CMVoicePtr voice, double speed);
 
-		void Init(int samplerate, int length);
-
-		virtual void RewindImplementation() = 0;
-
-		virtual void ClearImplementation() = 0;
-
-		virtual void FillBuffer(int16_t* buffer, int length) = 0;
-
-	public:
-		virtual ~Source();
-
-		void RemoveFromMixer();
-
-		void Clear();
-
-		void Rewind();
-
-		void RecalcGains();
-
-		void FillBuffer(int offset, int length);
-
-		void Process(int len);
-
-		double GetLength() const;
-
-		double GetPosition() const;
-
-		int GetState() const;
-
-		void SetGain(double gain);
-
-		void SetPan(double pan);
-
-		void SetPitch(double pitch);
-
-		void SetLoop(bool loop);
-
-		void SetInterpolation(bool interpolation);
-
-		void Play();
-
-		void Pause();
-
-		void TogglePause();
-
-		void Stop();
-	};
-
-	class WavStream : public Source
-	{
-		int bitdepth;
-		int channels;
-		bool bigEndian;
-		int idx;
-		std::span<char> span;
-		std::vector<char> userBuffer;
-
-		void ClearImplementation() override;
-
-		void RewindImplementation() override;
-
-		void FillBuffer(int16_t* buffer, int length) override;
-
-		inline uint8_t* data8() const
-		{ return reinterpret_cast<uint8_t*>(span.data()); }
-
-		inline int16_t* data16() const
-		{ return reinterpret_cast<int16_t*>(span.data()); }
-
-	public:
-		WavStream();
-
-		WavStream(WavStream&&) = default;	// move constructor ensures span stays in sync with userBuffer!
-
-		void Init(
-			int theSampleRate,
-			int theBitDepth,
-			int nChannels,
-			bool bigEndian,
-			std::span<char> data
-		);
-
-		std::span<char> GetBuffer(int nBytesOut);
-
-		std::span<char> SetBuffer(std::vector<char>&& data);
-	};
-
-
-	void InitWithSDL();
-
-	void ShutdownWithSDL();
-
-	double GetMasterGain();
-
-	void SetMasterGain(double);
-
-	WavStream LoadWAVFromFile(const char* path);
-
-}
+void					cmixer_InitWithSDL(void);
+void					cmixer_ShutdownWithSDL(void);
+double					cmixer_GetMasterGain(void);
+void					cmixer_SetMasterGain(double newGain);