ref: 6159713b2a0f55e2cd41797ed7aa3a5a4c0a06a3
parent: b09f9d2dd1242bbe3298d6b2d5b2e7749c21dd32
author: Iliyas Jorio <iliyas@jor.io>
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);