diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6a192c3..53d06d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,9 +69,7 @@ jobs: 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 diff --git a/src/music.cpp b/src/music.c similarity index 53% rename from src/music.cpp rename to src/music.c index e79f512..6428ef6 100644 --- a/src/music.cpp +++ b/src/music.c @@ -1,42 +1,30 @@ // music.c #include -#include -#include - -extern "C" -{ - #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 -#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; +static struct CMVoice* s_musicChannel = NULL; void EnableMusic( MBoolean on ) { if (s_musicChannel) { - s_musicChannel->SetGain(on? 1.0: 0.0); + CMVoice_SetGain(s_musicChannel, on ? 1 : 0); } } @@ -44,7 +32,7 @@ void FastMusic( void ) { if (s_musicChannel && !s_musicFast) { - s_musicChannel->SetPlaybackSpeed(1.3); + CMVoice_SetMODPlaybackSpeed(s_musicChannel, 1.3); s_musicFast = true; } } @@ -53,7 +41,7 @@ void SlowMusic( void ) { if (s_musicChannel && s_musicFast) { - s_musicChannel->SetPlaybackSpeed(1.0); + CMVoice_SetMODPlaybackSpeed(s_musicChannel, 1.0); s_musicFast = false; } } @@ -62,7 +50,7 @@ void PauseMusic( void ) { if (s_musicChannel) { - s_musicChannel->Pause(); + CMVoice_Pause(s_musicChannel); s_musicPaused++; } } @@ -71,45 +59,34 @@ void ResumeMusic( void ) { if (s_musicChannel) { - s_musicChannel->Play(); + CMVoice_Play(s_musicChannel); s_musicPaused--; } } -static std::vector LoadFile(char const* filename) -{ - std::ifstream ifs(filename, std::ios::binary | std::ios::ate); - auto pos = ifs.tellg(); - std::vector 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)) { + const char* qrn = QuickResourceName("mod", which+128, ".mod"); + if (!FileExists(qrn)) + { qrn = QuickResourceName("mod", which+128, ".s3m"); } - if (!FileExists(qrn)) { + if (!FileExists(qrn)) + { return; } - auto rawFileData = LoadFile(qrn); - s_musicChannel = new cmixer::ModStream(LoadFile(qrn)); + s_musicChannel = CMVoice_LoadMOD(qrn); EnableMusic(musicOn); - s_musicChannel->Play(); + CMVoice_Play(s_musicChannel); musicSelection = which; s_musicPaused = 0; @@ -120,8 +97,7 @@ void ShutdownMusic() { if (s_musicChannel) { - s_musicChannel->RemoveFromMixer(); - delete s_musicChannel; + CMVoice_Free(s_musicChannel); s_musicChannel = NULL; } } diff --git a/src/soundfx.c b/src/soundfx.c new file mode 100644 index 0000000..acc0c40 --- /dev/null +++ b/src/soundfx.c @@ -0,0 +1,81 @@ +// soundfx.c + +#include "support/cmixer.h" +#include + +#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() +{ +} diff --git a/src/soundfx.cpp b/src/soundfx.cpp deleted file mode 100644 index 880ffd8..0000000 --- a/src/soundfx.cpp +++ /dev/null @@ -1,82 +0,0 @@ -// soundfx.c - -#include "support/cmixer.h" -#include - -extern "C" -{ - #include "main.h" - #include "soundfx.h" - #include "music.h" - - MBoolean soundOn = true; -} - -static std::vector 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 -#include -#include "ModStream.h" - -using namespace cmixer; - -ModStream::ModStream(std::vector &&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; - } -} diff --git a/src/support/ModStream.h b/src/support/ModStream.h deleted file mode 100644 index b1f08fc..0000000 --- a/src/support/ModStream.h +++ /dev/null @@ -1,29 +0,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 moduleFile; - std::vector 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&& rawModule); - void SetPlaybackSpeed(double f); -}; -} \ No newline at end of file diff --git a/src/support/cmixer.c b/src/support/cmixer.c new file mode 100644 index 0000000..3cc564a --- /dev/null +++ b/src/support/cmixer.c @@ -0,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 +#include +#include +#include +#include + +#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; + } +} diff --git a/src/support/cmixer.cpp b/src/support/cmixer.cpp deleted file mode 100644 index a015224..0000000 --- a/src/support/cmixer.cpp +++ /dev/null @@ -1,711 +0,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 - -#include -#include -#include - -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 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 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 WavStream::GetBuffer(int nBytesOut) -{ - userBuffer.clear(); - userBuffer.reserve(nBytesOut); - return std::span(userBuffer.data(), nBytesOut); -} - -std::span WavStream::SetBuffer(std::vector&& 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 LoadFile(char const* filename) -{ - std::ifstream ifs(filename, std::ios::binary | std::ios::ate); - auto pos = ifs.tellg(); - std::vector 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(p, p + sz))); - return wavStream; -} diff --git a/src/support/cmixer.h b/src/support/cmixer.h index 69b97cb..db6610e 100644 --- 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 -#include -#include -#include - -#define BUFFER_SIZE (512) - -namespace cmixer +enum { - - enum - { - CM_STATE_STOPPED, - CM_STATE_PLAYING, - CM_STATE_PAUSED - }; - - 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 onComplete; // Callback - - void ClearPrivate(); - - protected: - Source(); - - 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 span; - std::vector userBuffer; - - void ClearImplementation() override; - - void RewindImplementation() override; - - void FillBuffer(int16_t* buffer, int length) override; - - inline uint8_t* data8() const - { return reinterpret_cast(span.data()); } - - inline int16_t* data16() const - { return reinterpret_cast(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 data - ); - - std::span GetBuffer(int nBytesOut); - - std::span SetBuffer(std::vector&& data); - }; - - - void InitWithSDL(); - - void ShutdownWithSDL(); - - double GetMasterGain(); - - void SetMasterGain(double); - - WavStream LoadWAVFromFile(const char* path); - -} + CM_STATE_STOPPED, + CM_STATE_PLAYING, + CM_STATE_PAUSED +}; + +typedef struct CMVoice* CMVoicePtr; +typedef const struct CMVoice* CMVoiceConstPtr; + +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); + +CMVoicePtr CMVoice_LoadWAV(const char* path); + +CMVoicePtr CMVoice_LoadMOD(const char* path); +void CMVoice_SetMODPlaybackSpeed(CMVoicePtr voice, double speed); + +void cmixer_InitWithSDL(void); +void cmixer_ShutdownWithSDL(void); +double cmixer_GetMasterGain(void); +void cmixer_SetMasterGain(double newGain);