From 0252a6e136c2e22e744f2d87af2520ff243fe92a Mon Sep 17 00:00:00 2001 From: "casriel." <232106036+casrielasriel@users.noreply.github.com> Date: Fri, 9 Oct 2026 06:24:33 -0700 Subject: [PATCH 1/3] FMOD implementation --- src/audio/miniaudio/ma_audio_system.c | 52 +- src/audio/openal/al_audio_system.c | 120 ++- src/audio/ps2/ps2_audio_system.c | 2 + src/audio_system.h | 7 + src/fmod/VORBIS_SETUP_LICENSE.txt | 24 + src/fmod/fmod.c | 1063 +++++++++++++++++++++++++ src/fmod/fmod.h | 43 + src/fmod/fmod_bank.c | 438 ++++++++++ src/fmod/fmod_dsp.c | 96 +++ src/fmod/fmod_dsp.h | 15 + src/fmod/fmod_internal.h | 275 +++++++ src/fmod/fmod_reader.h | 114 +++ src/fmod/fsb.c | 242 ++++++ src/fmod/fsb.h | 21 + src/fmod/vorbis_setups.h | 80 ++ src/noop_audio_system.c | 7 + src/runner.c | 5 + src/vm_builtins.c | 247 ++++++ 18 files changed, 2840 insertions(+), 11 deletions(-) create mode 100644 src/fmod/VORBIS_SETUP_LICENSE.txt create mode 100644 src/fmod/fmod.c create mode 100644 src/fmod/fmod.h create mode 100644 src/fmod/fmod_bank.c create mode 100644 src/fmod/fmod_dsp.c create mode 100644 src/fmod/fmod_dsp.h create mode 100644 src/fmod/fmod_internal.h create mode 100644 src/fmod/fmod_reader.h create mode 100644 src/fmod/fsb.c create mode 100644 src/fmod/fsb.h create mode 100644 src/fmod/vorbis_setups.h diff --git a/src/audio/miniaudio/ma_audio_system.c b/src/audio/miniaudio/ma_audio_system.c index 295d2697f..ee36a31a9 100644 --- a/src/audio/miniaudio/ma_audio_system.c +++ b/src/audio/miniaudio/ma_audio_system.c @@ -33,6 +33,7 @@ #endif #include "ma_audio_system.h" +#include "fmod/fmod.h" #include "data_win.h" #include "utils.h" @@ -55,7 +56,7 @@ static SoundInstance* findFreeSlot(MaAudioSystem* ma) { SoundInstance* best = nullptr; repeat(MAX_SOUND_INSTANCES, i) { SoundInstance* inst = &ma->instances[i]; - if (!ma_sound_is_playing(&inst->maSound)) { + if (ma_sound_at_end(&inst->maSound)) { if (best == nullptr || best->priority > inst->priority) { best = inst; } @@ -147,6 +148,7 @@ static void maInit(AudioSystem* audio, DataWin* dataWin, FileSystem* fileSystem) } static void maDestroy(AudioSystem* audio) { + Fmod_audioDestroy(audio); MaAudioSystem* ma = (MaAudioSystem*) audio; free(audio->groupGains); @@ -344,6 +346,34 @@ static int32_t maPlaySound(AudioSystem* audio, int32_t soundIndex, int32_t prior return slot->instanceId; } +static int32_t maPlayEncoded(AudioSystem* audio, const uint8_t* data, size_t bytes, bool loop) { + MaAudioSystem* ma = (MaAudioSystem*) audio; + if (!data || !bytes || !ma_engine_get_sample_rate(&ma->engine)) return -1; + SoundInstance* slot = findFreeSlot(ma); + ma_decoder_config config = ma_decoder_config_init_default(); + if (!slot || ma_decoder_init_memory(data, bytes, &config, &slot->decoder) != MA_SUCCESS) return -1; + if (ma_sound_init_from_data_source(&ma->engine, &slot->decoder, 0, &ma->listenerGroups[0], &slot->maSound) != MA_SUCCESS) { + ma_decoder_uninit(&slot->decoder); + return -1; + } + slot->ownsDecoder = true; + slot->active = true; + slot->soundIndex = -1; + slot->instanceId = SOUND_INSTANCE_ID_BASE + ma->nextInstanceCounter++; + slot->currentGain = slot->targetGain = slot->startGain = 1; + slot->fadeTimeRemaining = slot->fadeTotalTime = 0; + slot->priority = 0; + ma_sound_set_spatialization_enabled(&slot->maSound, MA_FALSE); + ma_sound_set_looping(&slot->maSound, loop); + if (ma_sound_start(&slot->maSound) != MA_SUCCESS) { + ma_sound_uninit(&slot->maSound); + ma_decoder_uninit(&slot->decoder); + slot->active = false; + return -1; + } + return slot->instanceId; +} + static void maSetSoundSpatial(AudioSystem* audio, int32_t instanceId, float x, float y, float z, float ref, float max, float factor) { SoundInstance* inst = findInstanceById((MaAudioSystem*)audio, instanceId); if (inst == nullptr) return; @@ -394,7 +424,8 @@ static void maStopAll(AudioSystem* audio) { repeat(MAX_SOUND_INSTANCES, i) { SoundInstance* inst = &ma->instances[i]; - if (inst->active) { + // encoded voices have separate control ownership. + if (inst->active && inst->soundIndex >= 0) { ma_sound_stop(&inst->maSound); ma_sound_uninit(&inst->maSound); if (inst->ownsDecoder) { @@ -464,7 +495,7 @@ static void maPauseAll(AudioSystem* audio) { repeat(MAX_SOUND_INSTANCES, i) { SoundInstance* inst = &ma->instances[i]; - if (inst->active && ma_sound_is_playing(&inst->maSound)) { + if (inst->active && inst->soundIndex >= 0 && ma_sound_is_playing(&inst->maSound)) { ma_sound_stop(&inst->maSound); } } @@ -475,7 +506,7 @@ static void maResumeAll(AudioSystem* audio) { repeat(MAX_SOUND_INSTANCES, i) { SoundInstance* inst = &ma->instances[i]; - if (inst->active) { + if (inst->active && inst->soundIndex >= 0) { ma_sound_start(&inst->maSound); } } @@ -678,19 +709,27 @@ static float maGetTrackPosition(AudioSystem* audio, int32_t soundOrInstance) { return 0.0f; } +static void maSeekInstance(SoundInstance* inst, float positionSeconds) { + ma_uint32 sampleRate; + if (ma_sound_get_data_format(&inst->maSound, nullptr, nullptr, &sampleRate, nullptr, 0) != MA_SUCCESS || sampleRate == 0) return; + double frame = (double) positionSeconds * sampleRate; + if (frame < (double) UINT64_MAX) ma_sound_seek_to_pcm_frame(&inst->maSound, (ma_uint64) frame); +} + static void maSetTrackPosition(AudioSystem* audio, int32_t soundOrInstance, float positionSeconds) { MaAudioSystem* ma = (MaAudioSystem*) audio; + if (!isfinite(positionSeconds) || positionSeconds < 0) return; if (isValidSoundInstanceId(soundOrInstance)) { SoundInstance* inst = findInstanceById(ma, soundOrInstance); if (inst != nullptr) { - ma_sound_seek_to_pcm_frame(&inst->maSound, (ma_uint64) (positionSeconds * 44100.0f)); + maSeekInstance(inst, positionSeconds); } } else { repeat(MAX_SOUND_INSTANCES, i) { SoundInstance* inst = &ma->instances[i]; if (inst->active && inst->soundIndex == soundOrInstance) { - ma_sound_seek_to_pcm_frame(&inst->maSound, (ma_uint64) (positionSeconds * 44100.0f)); + maSeekInstance(inst, positionSeconds); } } } @@ -960,6 +999,7 @@ MaAudioSystem* MaAudioSystem_create(DataWin* dataWin) { maAudioSystemVtable.groupIsLoaded = maGroupIsLoaded; maAudioSystemVtable.createStream = maCreateStream; maAudioSystemVtable.destroyStream = maDestroyStream; + maAudioSystemVtable.playEncoded = maPlayEncoded; ma->base.vtable = &maAudioSystemVtable; return ma; } diff --git a/src/audio/openal/al_audio_system.c b/src/audio/openal/al_audio_system.c index 22783e985..495ad1a8d 100644 --- a/src/audio/openal/al_audio_system.c +++ b/src/audio/openal/al_audio_system.c @@ -6,6 +6,7 @@ #include "stb_vorbis.c" #include "al_audio_system.h" +#include "fmod/fmod.h" #include "binary_utils.h" #include "data_win.h" #include "utils.h" @@ -237,6 +238,7 @@ static void maInit(AudioSystem* audio, DataWin* dataWin, FileSystem* fileSystem) } static void maDestroy(AudioSystem* audio) { + Fmod_audioDestroy(audio); AlAudioSystem* ma = (AlAudioSystem*) audio; free(audio->groupGains); @@ -434,6 +436,64 @@ static FILE* openWavStream(const char* path, int32_t* outChannels, int32_t* outS return f; } +static int32_t alPlayEncoded(AudioSystem* audio, const uint8_t* data, size_t bytes, bool loop) { + AlAudioSystem* al = (AlAudioSystem*) audio; + if (!data || !bytes || bytes > INT32_MAX || !al->alContext) return -1; + SoundInstance* slot = findFreeSlot(al); + if (!slot) return -1; + int error = 0; + stb_vorbis* decoder = stb_vorbis_open_memory(data, (int) bytes, &error, nullptr); + if (!decoder) return -1; + stb_vorbis_info info = stb_vorbis_get_info(decoder); + if (info.channels < 1 || info.channels > 2 || info.sample_rate == 0) { + stb_vorbis_close(decoder); + return -1; + } + memset(slot, 0, sizeof(*slot)); + slot->active = true; + slot->streaming = true; + slot->loop = loop; + slot->vorbis = decoder; + slot->soundIndex = -1; + slot->instanceId = SOUND_INSTANCE_ID_BASE + al->nextInstanceCounter++; + slot->streamChannels = info.channels; + slot->streamSampleRate = (int) info.sample_rate; + slot->streamFormat = info.channels == 2 ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16; + slot->streamLengthSamples = stb_vorbis_stream_length_in_samples(decoder); + slot->streamLengthSeconds = (float) slot->streamLengthSamples / info.sample_rate; + slot->currentGain = slot->targetGain = slot->startGain = 1; + slot->decodeScratch = (int16_t*) malloc(AL_STREAM_BUFFER_SAMPLES * info.channels * sizeof(int16_t)); + if (!slot->decodeScratch) { + stb_vorbis_close(decoder); + slot->active = false; + return -1; + } + alGetError(); + alGenSources(1, &slot->alSource); + alGenBuffers(AL_STREAM_BUFFER_COUNT, slot->streamBuffers); + if (alGetError() != AL_NO_ERROR) { + releaseInstance(slot); + return -1; + } + alSourcei(slot->alSource, AL_SOURCE_RELATIVE, AL_TRUE); + alSourcef(slot->alSource, AL_GAIN, 1); + int queued = 0; + for (int i = 0; i < AL_STREAM_BUFFER_COUNT; i++) { + if (!streamFillBuffer(slot, slot->streamBuffers[i])) { + slot->streamEnded = true; + break; + } + alSourceQueueBuffers(slot->alSource, 1, &slot->streamBuffers[i]); + queued++; + } + if (!queued || alGetError() != AL_NO_ERROR) { + releaseInstance(slot); + return -1; + } + alSourcePlay(slot->alSource); + return slot->instanceId; +} + static int32_t maPlaySound(AudioSystem* audio, int32_t soundIndex, int32_t priority, bool loop) { AlAudioSystem* ma = (AlAudioSystem*) audio; @@ -747,7 +807,9 @@ static void maStopAll(AudioSystem* audio) { AlAudioSystem* ma = (AlAudioSystem*) audio; repeat(MAX_SOUND_INSTANCES, i) { - releaseInstance(&ma->instances[i]); + SoundInstance* inst = &ma->instances[i]; + // encoded voices have separate control ownership. + if (inst->soundIndex >= 0) releaseInstance(inst); } } @@ -817,7 +879,7 @@ static void maPauseAll(AudioSystem* audio) { repeat(MAX_SOUND_INSTANCES, i) { SoundInstance* inst = &ma->instances[i]; - if (inst->active && alSourceIsPlaying(inst->alSource)) { + if (inst->active && inst->soundIndex >= 0 && alSourceIsPlaying(inst->alSource)) { alSourcePause(inst->alSource); } } @@ -828,7 +890,7 @@ static void maResumeAll(AudioSystem* audio) { repeat(MAX_SOUND_INSTANCES, i) { SoundInstance* inst = &ma->instances[i]; - if (inst->active) { + if (inst->active && inst->soundIndex >= 0) { alSourcePlay(inst->alSource); } } @@ -1016,19 +1078,66 @@ static float maGetTrackPosition(AudioSystem* audio, int32_t soundOrInstance) { return 0.0f; } +static void alSeekInstance(SoundInstance* inst, float positionSeconds) { + if (!isfinite(positionSeconds) || positionSeconds < 0) return; + if (!inst->streaming) { + alSourcef(inst->alSource, AL_SEC_OFFSET, positionSeconds); + return; + } + if (inst->streamSampleRate <= 0) return; + double requestedFrame = (double) positionSeconds * inst->streamSampleRate; + uint64_t frame; + if (inst->streamLengthSamples && requestedFrame >= inst->streamLengthSamples) frame = inst->streamLengthSamples - 1; + else if (requestedFrame < (double) UINT64_MAX) frame = (uint64_t) requestedFrame; + else return; + ALint state, queued; + alGetSourcei(inst->alSource, AL_SOURCE_STATE, &state); + alSourceStop(inst->alSource); + alGetSourcei(inst->alSource, AL_BUFFERS_QUEUED, &queued); + for (int i = 0; i < queued; i++) { + ALuint buffer; + alSourceUnqueueBuffers(inst->alSource, 1, &buffer); + } + if (inst->vorbis) { + if (!stb_vorbis_seek((stb_vorbis*) inst->vorbis, (unsigned) frame)) { + stb_vorbis_seek_start((stb_vorbis*) inst->vorbis); + frame = 0; + } + } else if (inst->wavFile) { + uint64_t bytes = frame * inst->streamChannels * sizeof(int16_t); + fseek(inst->wavFile, (long) (inst->wavDataStart + bytes), SEEK_SET); + inst->wavSampleBytesRemaining = inst->wavDataBytes - (uint32_t) bytes; + } + inst->playedSamples = frame; + inst->streamEnded = false; + queued = 0; + for (int i = 0; i < AL_STREAM_BUFFER_COUNT; i++) { + if (!streamFillBuffer(inst, inst->streamBuffers[i])) { + inst->streamEnded = true; + break; + } + alSourceQueueBuffers(inst->alSource, 1, &inst->streamBuffers[i]); + queued++; + } + if (queued && (state == AL_PLAYING || state == AL_PAUSED)) { + alSourcePlay(inst->alSource); + if (state == AL_PAUSED) alSourcePause(inst->alSource); + } +} + static void maSetTrackPosition(AudioSystem* audio, int32_t soundOrInstance, float positionSeconds) { AlAudioSystem* ma = (AlAudioSystem*) audio; if (isValidSoundInstanceId(soundOrInstance)) { SoundInstance* inst = findInstanceById(ma, soundOrInstance); if (inst != nullptr) { - alSourcef(inst->alSource, AL_SEC_OFFSET, positionSeconds); + alSeekInstance(inst, positionSeconds); } } else { repeat(MAX_SOUND_INSTANCES, i) { SoundInstance* inst = &ma->instances[i]; if (inst->active && inst->soundIndex == soundOrInstance) { - alSourcef(inst->alSource, AL_SEC_OFFSET, positionSeconds); + alSeekInstance(inst, positionSeconds); } } } @@ -1284,6 +1393,7 @@ AlAudioSystem* AlAudioSystem_create(void) { AlAudioSystemVtable.groupIsLoaded = maGroupIsLoaded; AlAudioSystemVtable.createStream = maCreateStream; AlAudioSystemVtable.destroyStream = maDestroyStream; + AlAudioSystemVtable.playEncoded = alPlayEncoded; ma->base.vtable = &AlAudioSystemVtable; return ma; } diff --git a/src/audio/ps2/ps2_audio_system.c b/src/audio/ps2/ps2_audio_system.c index 2f3b1e7f7..19a55cb77 100644 --- a/src/audio/ps2/ps2_audio_system.c +++ b/src/audio/ps2/ps2_audio_system.c @@ -1,4 +1,5 @@ #include "ps2_audio_system.h" +#include "fmod/fmod.h" #include "ps2/ps2_utils.h" #include "utils.h" @@ -603,6 +604,7 @@ static void ps2Init(AudioSystem* audio, MAYBE_UNUSED DataWin* dataWin, MAYBE_UNU } static void ps2Destroy(AudioSystem* audio) { + Fmod_audioDestroy(audio); Ps2AudioSystem* ps2 = (Ps2AudioSystem*) audio; free(audio->groupGains); diff --git a/src/audio_system.h b/src/audio_system.h index 4ec4474bd..5f7c5e148 100644 --- a/src/audio_system.h +++ b/src/audio_system.h @@ -20,6 +20,7 @@ typedef struct { // ===[ AudioSystem Vtable ]=== typedef struct AudioSystem AudioSystem; +struct FmodSystem; typedef struct { void (*init)(AudioSystem* audio, DataWin* dataWin, FileSystem* fileSystem); @@ -54,6 +55,9 @@ typedef struct { bool (*groupIsLoaded)(AudioSystem* audio, int32_t groupIndex); int32_t (*createStream)(AudioSystem* audio, const char* filename); bool (*destroyStream)(AudioSystem* audio, int32_t streamIndex); + // encoded data stays alive until the voice ends or is stopped. + // returns a voice id, -1 on failure, or -2 for the headless sink. + int32_t (*playEncoded)(AudioSystem* audio, const uint8_t* data, size_t bytes, bool loop); } AudioSystemVtable; // ===[ AudioSystem Base Struct ]=== @@ -65,6 +69,8 @@ struct AudioSystem { float listenerX, listenerY, listenerZ; AudioGroupGain* groupGains; uint32_t groupGainCount; + struct FmodSystem* fmodSystem; + int32_t fmodNextHandle; }; static inline int32_t AudioSystem_soundGroup(const AudioSystem* audio, int32_t soundIndex) { @@ -80,6 +86,7 @@ static inline float AudioSystem_getGroupGain(const AudioSystem* audio, int32_t g } static inline float AudioSystem_soundGroupGain(const AudioSystem* audio, int32_t soundIndex) { + if (soundIndex < 0) return 1; // encoded voices don't belong to a gamemaker audio group. return AudioSystem_getGroupGain(audio, AudioSystem_soundGroup(audio, soundIndex)); } diff --git a/src/fmod/VORBIS_SETUP_LICENSE.txt b/src/fmod/VORBIS_SETUP_LICENSE.txt new file mode 100644 index 000000000..2fae7bcbc --- /dev/null +++ b/src/fmod/VORBIS_SETUP_LICENSE.txt @@ -0,0 +1,24 @@ +Vorbis setup lookup data from HearthSim/python-fsb5! :3 +https://github.com/HearthSim/python-fsb5/blob/master/fsb5/vorbis_headers.py + +The MIT License (MIT) + +Copyright (c) 2016 Simon Pinfold + +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. diff --git a/src/fmod/fmod.c b/src/fmod/fmod.c new file mode 100644 index 000000000..fe3ed9a99 --- /dev/null +++ b/src/fmod/fmod.c @@ -0,0 +1,1063 @@ +#include "fmod_internal.h" +#include "log.h" +#include "math_compat.h" +#include + +static unsigned nextRandom(FmodSystem* system); +static float exponentialWeight(double weight, float curvature) { + float x = (float) fmax(0, fmin(weight, 1)); + if (fabsf(curvature) < 0.00001f) return x; + float exponent = fmaxf(-1, fminf(curvature, 1)) * 6.95219755f; + return expm1f(exponent * x) / expm1f(exponent); +} + +static float interpolateCurve(float left, float right, double position, float curvature, uint32_t mode) { + float weight = (float) fmax(0, fmin(position, 1)); + if (mode == FM_CURVE_STEPPED) return left; + if (mode == FM_CURVE_SQUARED) { + // interpolate squared levels in log space without overflowing when + // a controller uses large values, such as filter frequencies. + if (weight <= 0) return left; + if (weight >= 1) return right; + double a = 2.0 * left, b = 2.0 * right, maximum = fmax(a, b); + return (float) ((maximum + log2((1 - weight) * exp2(a - maximum) + weight * exp2(b - maximum))) / 2); + } + if (mode == FM_CURVE_EXPONENTIAL) { + weight = exponentialWeight(weight, curvature); + } else if (mode == FM_CURVE_SYMMETRIC && curvature > 0) { + float power = 1 + 2 * curvature; + weight = weight <= 0.5f ? powf(2 * weight, power) / 2 : 1 - powf(2 * (1 - weight), power) / 2; + } else if (mode == FM_CURVE_SYMMETRIC && curvature < 0) { + float centered = 2 * weight - 1; + float cubic = (centered * centered * centered + 1) / 2; + weight += (weight - cubic) * curvature; + } else if (mode > FM_CURVE_SYMMETRIC && curvature != 0) { + weight = 0; + } + return left + (right - left) * weight; +} + +// ===[ events and parameters ]=== + +static EventInstance* findInstance(FmodSystem* system, int handle) { + if (!system || handle <= 0) return nullptr; + for (ptrdiff_t i = 0; i < arrlen(system->instances); i++) { + EventInstance* instance = system->instances[i]; + if (instance->handle == handle && !instance->released) return instance; + } + return nullptr; +} + +static Record* findEvent(FmodSystem* system, const char* path) { + if (!system || !path) return nullptr; + ptrdiff_t index = shgeti(system->index.names, path); + return index >= 0 ? lookup(system, "EVTB", system->index.names[index].value.b) : nullptr; +} + +bool Fmod_eventExists(FmodSystem* system, const char* path) { + return findEvent(system, path) != nullptr; +} + +static float readParameter(FmodSystem* system, EventInstance* instance, int index, bool target) { + if (index < 0 || index >= arrlen(system->index.parameters)) return 0; + if (!system->index.parameters[index].global && instance) { + for (ptrdiff_t i = 0; i < arrlen(instance->values); i++) { + if (instance->values[i].parameter == index) return target ? instance->values[i].value : instance->values[i].current; + } + } + Parameter* parameter = &system->index.parameters[index]; + return parameter->global ? (target ? parameter->value : parameter->current) : parameter->initial; +} + +static float parameterValue(FmodSystem* system, EventInstance* instance, int index) { + return readParameter(system, instance, index, false); +} + +static float advanceParameter(float current, float target, const Parameter* parameter, double seconds) { + float speed = target >= current ? parameter->seekUp : parameter->seekDown; + if (speed == 0) return target; + double distance = seconds * speed; + return target >= current ? (float) fmin(target, current + distance) : (float) fmax(target, current - distance); +} + +static int findParameter(FmodSystem* system, EventInstance* instance, const char* name) { + for (ptrdiff_t i = 0; i < arrlen(system->index.parameters); i++) { + Parameter* parameter = &system->index.parameters[i]; + if (strcmp(parameter->name, name)) continue; + if (parameter->global) return (int) i; + if (!instance || instance->event->size <= FM_EVENT_PARAMETERS) continue; + FmReader reader = {instance->event->data, instance->event->size, FM_EVENT_PARAMETERS, false}; + uint32_t count, stride; + const uint8_t* references = fmlist(&reader, &count, &stride); + if (reader.failed || stride != FM_GUID_SIZE) continue; + for (uint32_t j = 0; j < count; j++) { + Record* mapping = lookup(system, "PMLB", references + j * FM_GUID_SIZE); + if (mapping && equalGuid(mapping->data + FM_GUID_SIZE, parameter->id.b)) return (int) i; + } + } + return -1; +} + +bool Fmod_setParameter(FmodSystem* system, int handle, const char* name, float value, bool immediate) { + EventInstance* instance = handle ? findInstance(system, handle) : nullptr; + if (!system || !name || !isfinite(value) || (handle && !instance)) return false; + int index = findParameter(system, instance, name); + if (index < 0) return false; + Parameter* parameter = &system->index.parameters[index]; + if (value < parameter->min) value = parameter->min; + if (value > parameter->max) value = parameter->max; + if (parameter->global) { + parameter->value = value; + if (immediate) parameter->current = value; + else parameter->current = advanceParameter(parameter->current, value, parameter, 0); + } else { + ptrdiff_t i; + for (i = 0; i < arrlen(instance->values); i++) { + if (instance->values[i].parameter == index) break; + } + if (i == arrlen(instance->values)) { + LocalValue local = {index, value, immediate ? value : advanceParameter(parameter->initial, value, parameter, 0)}; + arrput(instance->values, local); + } else { + instance->values[i].value = value; + if (immediate) instance->values[i].current = value; + else instance->values[i].current = advanceParameter(instance->values[i].current, value, parameter, 0); + } + } + Fmod_step(system, 0); + return true; +} + +float Fmod_getParameter(FmodSystem* system, int handle, const char* name) { + EventInstance* instance = handle ? findInstance(system, handle) : nullptr; + if (!system || !name || (handle && !instance)) return 0; + return readParameter(system, instance, findParameter(system, instance, name), true); +} + +// ===[ bus volume ]=== + +static float evaluateCurve(FmodSystem* system, EventInstance* instance, Record* curve) { + if (!curve || curve->size < FM_CURVE_POINTS + 2) return 0; + double input = instance->position; + bool parameterInput = false; + for (ptrdiff_t i = 0; i < arrlen(system->index.inputs); i++) { + if (equalGuid(system->index.inputs[i].curve.b, curve->data)) { + input = parameterValue(system, instance, findParameterIndex(&system->index, system->index.inputs[i].parameter.b)); + parameterInput = true; + break; + } + } + FmReader reader = {curve->data, curve->size, FM_CURVE_POINTS, false}; + uint32_t count, stride; + const uint8_t* points = fmlist(&reader, &count, &stride); + if (reader.failed || !count || stride < 8) return 0; + double previousX = parameterInput ? fmfloat(points) : fm32(points) / FM_TICKS; + float previousY = fmfloat(points + 4); + if (input < previousX) return previousY; + for (uint32_t i = 1; i < count; i++) { + double x = parameterInput ? fmfloat(points + i * stride) : fm32(points + i * stride) / FM_TICKS; + float y = fmfloat(points + i * stride + 4); + if (input < x) { + const uint8_t* previous = points + (i - 1) * stride; + float curvature = stride >= 12 ? fmfloat(previous + 8) : 0; + uint32_t mode = stride >= 16 ? fm32(previous + 12) : 0; + return interpolateCurve(previousY, y, x > previousX ? (input - previousX) / (x - previousX) : 1, curvature, mode); + } + previousX = x; + previousY = y; + } + return previousY; +} + +// the volume property id stored in the bank's property map. +static const uint8_t volumeProperty[FM_GUID_SIZE] = {0x28, 0xa1, 0x5b, 0xd6, 0xf5, 0xa6, 0xd9, 0x41, 0xb6, 0xa1, 0xa7, 0xc4, 0x02, 0xf3, 0x29, 0xac}; +static const uint8_t pitchProperty[FM_GUID_SIZE] = {0x07, 0x21, 0x8c, 0xc1, 0xb9, 0x7b, 0xdc, 0x4c, 0x84, 0x53, 0x19, 0xb8, 0x81, 0xdb, 0x2e, 0x14}; + +static float mapProperty(FmodSystem* system, const uint8_t* id, float value, bool inverse) { + Record* map = lookup(system, "MAP ", id); + if (!map) return value; + FmReader reader = {map->data, map->size, FM_GUID_SIZE, false}; + uint32_t count, stride; + const uint8_t* points = fmlist(&reader, &count, &stride); + if (reader.failed || !count || stride != 8) return value; + unsigned input = inverse ? 4 : 0, output = inverse ? 0 : 4; + float previousX = fmfloat(points + input), previousY = fmfloat(points + output); + if (value <= previousX) return previousY; + for (uint32_t i = 1; i < count; i++) { + float x = fmfloat(points + i * stride + input); + float y = fmfloat(points + i * stride + output); + if (value <= x) { + float weight = x > previousX ? (value - previousX) / (x - previousX) : 1; + return previousY + (y - previousY) * weight; + } + previousX = x; + previousY = y; + } + return previousY; +} + +static float randomModulation(FmodSystem* system, Clip* clip, Record* modulator) { + if (!modulator || modulator->size < FM_RANDOM_MODULATOR_SIZE || fm32(modulator->data + FM_MODULATOR_TYPE) != 1 || fm32(modulator->data + FM_MODULATOR_MODE) != 0) return 0; + float amount = fmfloat(modulator->data + FM_MODULATOR_AMOUNT); + if (!isfinite(amount) || amount < 0) return 0; + for (ptrdiff_t i = 0; i < arrlen(clip->modulation); i++) { + if (clip->modulation[i].modulator == modulator) return clip->modulation[i].value; + } + ModulationValue value = {modulator, (float) ((nextRandom(system) / (double) UINT32_MAX * 2 - 1) * amount / 100)}; + arrput(clip->modulation, value); + return value.value; +} + +static float envelopeValue(FmodSystem* system, Record* envelope, double age) { + const uint8_t* data = envelope->data; + double attack = fmfloat(data + FM_ENVELOPE_ATTACK) / 1000.0; + double hold = fmfloat(data + FM_ENVELOPE_HOLD) / 1000.0; + double decay = fmfloat(data + FM_ENVELOPE_DECAY) / 1000.0; + float initial = mapProperty(system, volumeProperty, fmfloat(data + FM_ENVELOPE_INITIAL), true); + float peak = mapProperty(system, volumeProperty, fmfloat(data + FM_ENVELOPE_PEAK), true); + float sustain = mapProperty(system, volumeProperty, fmfloat(data + FM_ENVELOPE_SUSTAIN), true); + if (attack > 0 && age < attack) + return initial + (peak - initial) * exponentialWeight(age / attack, fmfloat(data + FM_ENVELOPE_ATTACK_CURVE)); + age -= fmax(attack, 0); + if (age < hold) return peak; + age -= fmax(hold, 0); + if (decay > 0 && age < decay) + return peak + (sustain - peak) * exponentialWeight(age / decay, fmfloat(data + FM_ENVELOPE_DECAY_CURVE)); + return sustain; +} + +static float evaluateEnvelope(FmodSystem* system, EventInstance* instance, Clip* clip, Record* owner, Record* envelope, double* releaseDuration) { + if (!envelope || envelope->size < FM_ENVELOPE_SIZE || fm32(envelope->data + FM_MODULATOR_TYPE) != 0) return 0; + for (size_t offset = FM_ENVELOPE_INITIAL; offset < FM_ENVELOPE_SIZE; offset += 4) { + if (!isfinite(fmfloat(envelope->data + offset))) return 0; + } + double release = fmax(0, fmfloat(envelope->data + FM_ENVELOPE_RELEASE) / 1000.0); + if (releaseDuration && release > *releaseDuration) *releaseDuration = release; + double activated = clip->activated; + if (!tagIs(owner, "INST")) { + ptrdiff_t i; + for (i = 0; i < arrlen(instance->clocks); i++) { + if (instance->clocks[i].owner == owner) break; + } + if (i == arrlen(instance->clocks)) { + BusClock clock = {owner, tagIs(owner, "MBSB") && equalGuid(owner->data, instance->event->data + FM_EVENT_MASTER_BUS) ? 0 : instance->elapsed}; + arrput(instance->clocks, clock); + } + activated = instance->clocks[i].activated; + } + double age = fmax(0, (instance->stopping ? instance->stopElapsed : instance->elapsed) - activated); + float value = envelopeValue(system, envelope, age); + if (instance->stopping) { + double time = instance->elapsed - instance->stopElapsed; + float final = mapProperty(system, volumeProperty, fmfloat(envelope->data + FM_ENVELOPE_FINAL), true); + value += (final - value) * exponentialWeight(release > 0 ? time / release : 1, fmfloat(envelope->data + FM_ENVELOPE_RELEASE_CURVE)); + } + return mapProperty(system, volumeProperty, value, false); +} + +static float ownerBase(Record* owner, bool pitch) { + if (tagIs(owner, "INST") && owner->size >= FM_INSTRUMENT_PITCH + 4) + return fmfloat(owner->data + (pitch ? FM_INSTRUMENT_PITCH : FM_INSTRUMENT_VOLUME)); + Record* bus = findSiblingRecord(owner, "BUS "); + if (!bus || bus->size < 8) return 0; + FmReader reader = {bus->data, bus->size, 8, false}; + for (unsigned i = 0; i < 2; i++) { + FmList list; + if (!FmList_begin(&list, &reader)) return 0; + while (list.remaining && !reader.failed) { + uint32_t bytes; + FmList_next(&list, &bytes); + } + } + uint32_t bytes = fmcount(&reader); + const uint8_t* values = fmread(&reader, bytes); + return values && bytes >= (pitch ? 8u : 4u) ? fmfloat(values + (pitch ? 4 : 0)) : 0; +} + +static float ownerValue(FmodSystem* system, EventInstance* instance, Clip* clip, Record* owner, bool pitch, double* releaseDuration) { + if (!owner) return 0; + const uint8_t* propertyId = pitch ? pitchProperty : volumeProperty; + float value = ownerBase(owner, pitch); + for (ptrdiff_t i = owner->parent + 1; i < arrlen(owner->bank->records); i++) { + Record* property = &owner->bank->records[i]; + if (property->parent < owner->parent) break; + if (property->parent < 0 || !tagIs(property, "PROP")) continue; + if (owner->bank->records[property->parent].parent != owner->parent) continue; + if (property->size < FM_PROPERTY_CONTROLLERS + 2) continue; + if (!equalGuid(property->data + FM_PROPERTY_ID, propertyId)) continue; + FmReader reader = {property->data, property->size, FM_PROPERTY_CONTROLLERS, false}; + for (unsigned group = 0; group < 2; group++) { + uint32_t count, stride; + const uint8_t* references = fmlist(&reader, &count, &stride); + if (reader.failed || (count && stride != FM_GUID_SIZE)) break; + for (uint32_t j = 0; j < count; j++) { + const uint8_t* id = references + j * stride; + if (group == 0) { + float contribution = evaluateCurve(system, instance, lookup(system, "CURV", id)); + if (!pitch && fm16(property->data + FM_PROPERTY_MAPPING) == 1) + contribution = mapProperty(system, volumeProperty, contribution, false); + value += contribution; + } else if (pitch) { + value += randomModulation(system, clip, lookup(system, "MODB", id)) * 24; + } else { + value += evaluateEnvelope(system, instance, clip, owner, lookup(system, "MODB", id), releaseDuration); + } + } + } + } + return value; +} + +static float ownerGain(FmodSystem* system, EventInstance* instance, Clip* clip, Record* owner, double* releaseDuration) { + float decibels = ownerValue(system, instance, clip, owner, false, releaseDuration); + if (!isfinite(decibels)) return 0; + return decibels <= -80 ? 0 : powf(10, fminf(decibels, 24) / 20); +} + +static float ownerPitch(FmodSystem* system, EventInstance* instance, Clip* clip, Record* owner) { + return ownerValue(system, instance, clip, owner, true, nullptr); +} + +static Record* findBus(FmodSystem* system, const uint8_t* id) { + Record* bus = lookup(system, "GBSB", id); + if (!bus) bus = lookup(system, "IBSB", id); + if (!bus) bus = lookup(system, "MBSB", id); + return bus; +} + +static Record* parentBus(FmodSystem* system, EventInstance** context, Record* bus, Record** instrument, double* activated) { + EventInstance* instance = *context; + *instrument = nullptr; + if (tagIs(bus, "MBSB") && equalGuid(bus->data, instance->event->data + FM_EVENT_MASTER_BUS)) + return lookup(system, "IBSB", instance->event->data + FM_EVENT_INPUT_BUS); + if (tagIs(bus, "IBSB") && instance->parent && equalGuid(bus->data, instance->event->data + FM_EVENT_INPUT_BUS)) { + *instrument = instance->parentInstrument; + *activated = instance->parentActivated; + *context = instance->parent; + return *instrument && (*instrument)->size >= FM_INSTRUMENT_BUS + FM_GUID_SIZE ? findBus(system, (*instrument)->data + FM_INSTRUMENT_BUS) : nullptr; + } + return findBus(system, bus->data + FM_BUS_PARENT); +} + +static float clipGain(FmodSystem* system, EventInstance* instance, Clip* clip, double* releaseDuration) { + float gain = ownerGain(system, instance, clip, clip->instrument, releaseDuration); + for (unsigned i = 0; i < clip->controllerCount; i++) gain *= ownerGain(system, instance, clip, clip->controllers[i], releaseDuration); + if (!clip->instrument || clip->instrument->size < FM_INSTRUMENT_BUS + FM_GUID_SIZE) return gain; + Record* bus = findBus(system, clip->instrument->data + FM_INSTRUMENT_BUS); + unsigned depth = 0; + while (bus && depth++ < 32) { + gain *= ownerGain(system, instance, clip, bus, releaseDuration); + Record* instrument; + double activated = 0; + bus = parentBus(system, &instance, bus, &instrument, &activated); + if (instrument) { + Clip routed = *clip; + routed.activated = activated; + gain *= ownerGain(system, instance, &routed, instrument, releaseDuration); + } + } + return isfinite(gain) ? gain : 0; +} + +static float clipPitch(FmodSystem* system, EventInstance* instance, Clip* clip) { + float semitones = ownerPitch(system, instance, clip, clip->instrument); + for (unsigned i = 0; i < clip->controllerCount; i++) semitones += ownerPitch(system, instance, clip, clip->controllers[i]); + Record* bus = clip->instrument && clip->instrument->size >= FM_INSTRUMENT_BUS + FM_GUID_SIZE ? findBus(system, clip->instrument->data + FM_INSTRUMENT_BUS) : nullptr; + unsigned depth = 0; + while (bus && depth++ < 32) { + semitones += ownerPitch(system, instance, clip, bus); + Record* instrument; + double activated = 0; + bus = parentBus(system, &instance, bus, &instrument, &activated); + if (instrument) semitones += ownerPitch(system, instance, clip, instrument); + } + return isfinite(semitones) ? powf(2, fmaxf(-96, fminf(semitones, 96)) / 12) : 1; +} + +// ===[ instrument scheduling ]=== + +static unsigned nextRandom(FmodSystem* system) { + system->random ^= system->random << 13; + system->random ^= system->random >> 17; + system->random ^= system->random << 5; + return system->random; +} + +static bool scheduleInstrument(FmodSystem* system, EventInstance* instance, const uint8_t* id, double start, double end, unsigned depth); +static bool timelineLength(FmodSystem* system, Record* event, double base, unsigned depth, double* length); + +static bool scheduleTimeline(FmodSystem* system, EventInstance* instance, Record* event, double base, unsigned depth) { + if (!event || depth > 16) return false; + Record* timeline = lookup(system, "TLNB", event->data + FM_EVENT_TIMELINE); + if (!timeline) return false; + FmReader reader = {timeline->data, timeline->size, FM_TIMELINE_LISTS, false}; + // nested events come first, then the regular instruments. + for (unsigned group = 0; group < 2; group++) { + uint32_t count, stride; + const uint8_t* list = fmlist(&reader, &count, &stride); + if (reader.failed || (count && stride != FM_TIMED_ITEM_SIZE)) return false; + for (uint32_t i = 0; i < count; i++) { + const uint8_t* item = list + i * stride; + ptrdiff_t first = arrlen(instance->clips); + double start = base + fm32(item + FM_TIMED_ITEM_START) / FM_TICKS; + double end = start + fm32(item + FM_TIMED_ITEM_DURATION) / FM_TICKS; + if (end > instance->length) instance->length = end; + if (!scheduleInstrument(system, instance, item, start, end, depth + 1)) return false; + for (ptrdiff_t j = first; j < arrlen(instance->clips); j++) { + Clip* clip = &instance->clips[j]; + if (clip->end > instance->length) instance->length = clip->end; + } + } + } + return true; +} + +static bool scheduleInstrument(FmodSystem* system, EventInstance* instance, const uint8_t* id, double start, double end, unsigned depth) { + if (depth > 16 || arrlen(instance->clips) >= FM_MAX_CLIPS) return false; + Record* instrument = lookup(system, "WAIB", id); + if (instrument) { + Record* wave = lookup(system, "WAV ", instrument->data + FM_GUID_SIZE); + if (!wave) return false; + uint32_t sample = fm32(wave->data + FM_WAVE_SAMPLE); + if (sample >= (uint32_t) arrlen(wave->bank->samples)) return false; + Clip clip = {0}; + clip.bank = wave->bank; + clip.sample = (int) sample; + clip.start = start; + clip.end = end; + clip.voice = -1; + clip.lastGain = -1; + clip.instrument = findSiblingRecord(instrument, "INST"); + if (end <= start) clip.end = start + (double) wave->bank->samples[sample].frames / wave->bank->samples[sample].rate; + arrput(instance->clips, clip); + return true; + } + instrument = lookup(system, "MUIB", id); + if (instrument) { + Record* playlist = findSiblingRecord(instrument, "PLST"); + if (!playlist || playlist->size < 10) return false; + FmReader reader = {playlist->data, playlist->size, 8, false}; + uint32_t count, stride; + const uint8_t* items = fmlist(&reader, &count, &stride); + if (reader.failed || !count || stride != FM_GUID_SIZE + 4) return false; + float total = 0; + for (uint32_t i = 0; i < count; i++) { + float weight = fmfloat(items + i * stride + FM_GUID_SIZE); + if (!isfinite(weight) || weight < 0) return false; + total += weight; + } + if (!isfinite(total) || total <= 0) return false; + float selection = (float) (nextRandom(system) / (double) UINT32_MAX) * total; + uint32_t index; + for (index = 0; index + 1 < count; index++) { + selection -= fmfloat(items + index * stride + FM_GUID_SIZE); + if (selection < 0) break; + } + ptrdiff_t first = arrlen(instance->clips); + if (!scheduleInstrument(system, instance, items + index * stride, start, end, depth + 1)) return false; + Record* controller = findSiblingRecord(instrument, "INST"); + if (!controller) controller = instrument; + for (ptrdiff_t i = first; i < arrlen(instance->clips); i++) { + Clip* clip = &instance->clips[i]; + if (clip->controllerCount >= 16) return false; + clip->controllers[clip->controllerCount++] = controller; + } + return true; + } + instrument = lookup(system, "EVIB", id); + if (instrument) { + Record* event = lookup(system, "EVTB", instrument->data + FM_GUID_SIZE); + if (!event || instance->depth >= 16 || arrlen(instance->children) >= FM_MAX_CLIPS) return false; + if (end <= start) { + double length = 0; + if (!timelineLength(system, event, 0, instance->depth + 1, &length)) return false; + end = start + length; + } + NestedEvent child = {event, findSiblingRecord(instrument, "INST"), start, end, nullptr, false}; + arrput(instance->children, child); + if (end > instance->length) instance->length = end; + return true; + } + if (lookup(system, "SLNB", id)) return true; + logWarn("FMOD: Event has an unresolved instrument\n"); + return false; +} + +// just the declared length. picking a playlist entry here would change what +// plays next, so keep this separate from scheduling. +static bool timelineLength(FmodSystem* system, Record* event, double base, unsigned depth, double* length) { + if (!event || depth > 16) return false; + Record* timeline = lookup(system, "TLNB", event->data + FM_EVENT_TIMELINE); + if (!timeline) return false; + FmReader reader = {timeline->data, timeline->size, FM_TIMELINE_LISTS, false}; + for (unsigned group = 0; group < 2; group++) { + uint32_t count, stride; + const uint8_t* items = fmlist(&reader, &count, &stride); + if (reader.failed || (count && stride != FM_TIMED_ITEM_SIZE)) return false; + for (uint32_t i = 0; i < count; i++) { + const uint8_t* item = items + i * stride; + double start = base + fm32(item + FM_TIMED_ITEM_START) / FM_TICKS; + double end = start + fm32(item + FM_TIMED_ITEM_DURATION) / FM_TICKS; + if (end > *length) *length = end; + Record* nested = lookup(system, "EVIB", item); + if (nested && end <= start && !timelineLength(system, lookup(system, "EVTB", nested->data + FM_GUID_SIZE), start, depth + 1, length)) return false; + } + } + return true; +} + +double Fmod_getLength(FmodSystem* system, const char* path) { + double length = 0; + return timelineLength(system, findEvent(system, path), 0, 0, &length) ? floor(length * 1000) : -4; +} + +// ===[ voice lifetime ]=== + +static void stopClip(FmodSystem* system, Clip* clip) { + if (clip->voice != -1) { + if (clip->voice >= 0) system->audio->vtable->stopSound(system->audio, clip->voice); + clip->bank->samples[clip->sample].references--; + system->activeVoices--; + } + clip->voice = -1; + clip->virtualized = false; + arrfree(clip->modulation); +} + +static void stopVoices(FmodSystem* system, EventInstance* instance) { + for (ptrdiff_t i = 0; i < arrlen(instance->clips); i++) stopClip(system, &instance->clips[i]); + for (ptrdiff_t i = 0; i < arrlen(instance->children); i++) { + if (instance->children[i].playback) stopVoices(system, instance->children[i].playback); + } +} + +static void freeInstance(FmodSystem* system, EventInstance* instance); + +static void clearPlayback(FmodSystem* system, EventInstance* instance) { + for (ptrdiff_t i = 0; i < arrlen(instance->clips); i++) stopClip(system, &instance->clips[i]); + for (ptrdiff_t i = 0; i < arrlen(instance->children); i++) { + if (instance->children[i].playback) freeInstance(system, instance->children[i].playback); + } + arrfree(instance->children); + arrfree(instance->clocks); + arrfree(instance->clips); +} + +static void freeInstance(FmodSystem* system, EventInstance* instance) { + clearPlayback(system, instance); + arrfree(instance->values); + free(instance); + system->instanceCount--; +} + +// ===[ system and instances ]=== + +FmodSystem* Fmod_create(AudioSystem* audio, FileSystem* fs, int channels) { + if (!audio || !audio->vtable || !fs || !fs->vtable || channels < 1) return nullptr; + AudioSystemVtable* backend = audio->vtable; + FileSystemVtable* files = fs->vtable; + if (!backend->playEncoded || !backend->stopSound || !backend->isPlaying || !backend->pauseSound || !backend->resumeSound || !backend->setSoundGain || !backend->setTrackPosition) return nullptr; + if (!files->binaryOpen || !files->binaryRead || !files->binarySeek || !files->binarySize || !files->binaryClose) return nullptr; + FmodSystem* system = (FmodSystem*) calloc(1, sizeof(*system)); + if (!system) return nullptr; + system->audio = audio; + system->fs = fs; + system->maxChannels = channels; + system->nextHandle = audio->fmodNextHandle > 0 ? audio->fmodNextHandle : 1; + system->random = 0x41c64e6d; + sh_new_strdup(system->index.names); + return system; +} + +void Fmod_destroy(FmodSystem* system) { + if (!system) return; + if (system->audio->fmodSystem == system) system->audio->fmodSystem = nullptr; + for (ptrdiff_t i = 0; i < arrlen(system->instances); i++) freeInstance(system, system->instances[i]); + for (ptrdiff_t i = 0; i < arrlen(system->banks); i++) FmodBank_destroy(system->banks[i]); + FmodBank_clearIndex(&system->index); + arrfree(system->instances); + arrfree(system->banks); + free(system); +} + +void Fmod_audioDestroy(AudioSystem* audio) { + if (!audio) return; + Fmod_destroy(audio->fmodSystem); + audio->fmodSystem = nullptr; +} + +int Fmod_createInstance(FmodSystem* system, const char* path) { + Record* event = findEvent(system, path); + if (!event || system->instanceCount >= FM_MAX_INSTANCES || system->nextHandle >= FM_HANDLE_LIMIT) return -4; + EventInstance* instance = (EventInstance*) calloc(1, sizeof(*instance)); + if (!instance) return -4; + system->instanceCount++; + instance->handle = system->nextHandle++; + system->audio->fmodNextHandle = system->nextHandle; + instance->event = event; + instance->timeline = lookup(system, "TLNB", event->data + FM_EVENT_TIMELINE); + arrput(system->instances, instance); + return instance->handle; +} + +static bool initializePlayback(FmodSystem* system, EventInstance* instance) { + clearPlayback(system, instance); + instance->playing = false; + instance->position = instance->length = 0; + instance->stopping = instance->held = false; + instance->elapsed = instance->stopElapsed = instance->releaseDuration = 0; + if (!scheduleTimeline(system, instance, instance->event, 0, 0)) { + clearPlayback(system, instance); + return false; + } + for (ptrdiff_t i = 0; i < arrlen(instance->clips); i++) { + Clip* clip = &instance->clips[i]; + if (clip->start <= 0 && !FmodBank_prepareSample(system, clip->bank, clip->sample)) return false; + } + instance->playing = true; + return true; +} + +bool Fmod_play(FmodSystem* system, int handle) { + EventInstance* instance = findInstance(system, handle); + if (!instance || !initializePlayback(system, instance)) return false; + bool silence = arrlen(instance->clips) == 0; + Fmod_step(system, 0); + instance = findInstance(system, handle); + if (silence) return true; + if (!instance) return false; + if (instance->held) return true; + for (ptrdiff_t i = 0; i < arrlen(instance->clips); i++) { + if (instance->clips[i].voice != -1 || instance->clips[i].virtualized || instance->clips[i].start > 0) return true; + } + instance->playing = false; + return false; +} + +static bool stopPlayback(FmodSystem* system, EventInstance* instance, bool immediate) { + if (!immediate && instance->stopping) return true; + instance->releaseDuration = 0; + for (ptrdiff_t i = 0; i < arrlen(instance->clips); i++) { + Clip* clip = &instance->clips[i]; + if (clip->started && !clip->finished) clipGain(system, instance, clip, &instance->releaseDuration); + } + for (ptrdiff_t i = 0; i < arrlen(instance->children); i++) { + EventInstance* child = instance->children[i].playback; + if (!child) continue; + stopPlayback(system, child, immediate); + if (child->playing && child->releaseDuration > instance->releaseDuration) instance->releaseDuration = child->releaseDuration; + } + if (immediate || !instance->playing || instance->releaseDuration <= 0) { + stopVoices(system, instance); + instance->playing = instance->stopping = instance->held = false; + } else { + instance->stopElapsed = instance->elapsed; + instance->stopping = true; + instance->held = false; + } + return true; +} + +bool Fmod_stop(FmodSystem* system, int handle, bool immediate) { + EventInstance* instance = findInstance(system, handle); + return instance && stopPlayback(system, instance, immediate); +} + +bool Fmod_release(FmodSystem* system, int handle) { + EventInstance* instance = findInstance(system, handle); + if (!instance) return false; + instance->released = true; + Fmod_step(system, 0); + return true; +} + +bool Fmod_isPlaying(FmodSystem* system, int handle) { + EventInstance* instance = findInstance(system, handle); + return instance && instance->playing && !instance->stopping && !instance->held; +} + +bool Fmod_getPaused(FmodSystem* system, int handle) { + EventInstance* instance = findInstance(system, handle); + return instance && instance->paused; +} + +static bool pausePlayback(FmodSystem* system, EventInstance* instance, bool paused) { + if (instance->paused == paused) return true; + instance->paused = paused; + for (ptrdiff_t i = 0; i < arrlen(instance->clips); i++) { + int voice = instance->clips[i].voice; + if (voice < 0) continue; + if (paused) system->audio->vtable->pauseSound(system->audio, voice); + else system->audio->vtable->resumeSound(system->audio, voice); + } + for (ptrdiff_t i = 0; i < arrlen(instance->children); i++) { + if (instance->children[i].playback) pausePlayback(system, instance->children[i].playback, paused); + } + return true; +} + +bool Fmod_pause(FmodSystem* system, int handle, bool paused) { + EventInstance* instance = findInstance(system, handle); + return instance && pausePlayback(system, instance, paused); +} + +void Fmod_pauseAll(FmodSystem* system, bool pause) { + if (!system) return; + for (ptrdiff_t i = 0; i < arrlen(system->instances); i++) { + EventInstance* instance = system->instances[i]; + if (!instance->released && !instance->oneshot) Fmod_pause(system, instance->handle, pause); + } +} + +bool Fmod_setSpatial(FmodSystem* system, int handle, float x, float y) { + EventInstance* instance = findInstance(system, handle); + if (!instance || !system->audio->vtable->setSoundSpatial || !isfinite(x) || !isfinite(y)) return false; + instance->spatial = true; + instance->x = x; + instance->y = y; + Fmod_step(system, 0); + return true; +} + +bool Fmod_setListeners(FmodSystem* system, int count) { + // the playback adapters only handle one spatial listener for now. + return system && count == 1; +} + +bool Fmod_setListener(FmodSystem* system, int listener, float x, float y) { + if (!system || listener != 0 || !system->audio->vtable->setListenerPosition || !isfinite(x) || !isfinite(y)) return false; + system->audio->vtable->setListenerPosition(system->audio, x, y, 0); + return true; +} + +// ===[ timeline lists and conditions ]=== + +static bool seekTimelineList(Record* timeline, unsigned index, FmReader* reader) { + if (!timeline) return false; + reader->data = timeline->data; + reader->size = timeline->size; + reader->position = FM_TIMELINE_LISTS; + reader->failed = false; + for (unsigned i = 0; i < index; i++) { + FmList list; + if (!FmList_begin(&list, reader)) return false; + while (list.remaining && !reader->failed) { + uint32_t bytes; + FmList_next(&list, &bytes); + } + if (reader->failed) return false; + } + return true; +} + +static bool markerPosition(Record* timeline, const uint8_t* id, double* position) { + FmReader reader; + if (!seekTimelineList(timeline, 3, &reader)) return false; + FmList list; + if (!FmList_begin(&list, &reader)) return false; + while (list.remaining && !reader.failed) { + uint32_t bytes; + const uint8_t* marker = FmList_next(&list, &bytes); + if (!marker || bytes < FM_GUID_SIZE + 4) return false; + if (equalGuid(marker, id)) { + *position = fm32(marker + FM_GUID_SIZE) / FM_TICKS; + return true; + } + } + return false; +} + +static bool parameterConditions(FmodSystem* system, EventInstance* instance, const uint8_t* data, size_t bytes) { + size_t position = 0; + while (position < bytes) { + if (bytes - position < FM_CONDITION_SIZE) return false; + uint32_t flags = fm32(data + position); + int index = findParameterIndex(&system->index, data + position + FM_CONDITION_PARAMETER); + if (index < 0) return false; + float value = parameterValue(system, instance, index); + float low = fmfloat(data + position + FM_CONDITION_LOW); + float high = fmfloat(data + position + FM_CONDITION_HIGH); + if (!isfinite(low) || !isfinite(high) || low > high || (flags & 0xff) != 0x11) return false; + if (value < low - 0.0001f || value > high + 0.0001f) return false; + position += FM_CONDITION_SIZE; + if (bytes - position >= 4 && fm32(data + position) <= 2) position += 4; + } + return true; +} + +static void updateSustain(FmodSystem* system, EventInstance* instance, double previous) { + FmReader reader; + instance->held = false; + if (!seekTimelineList(instance->timeline, 2, &reader)) return; + FmList list; + if (!FmList_begin(&list, &reader)) return; + while (list.remaining && !reader.failed) { + uint32_t entryBytes; + const uint8_t* point = FmList_next(&list, &entryBytes); + if (!point || entryBytes < 8) return; + double time = fm32(point) / FM_TICKS; + uint32_t bytes = fm32(point + 4); + if (bytes > entryBytes - 8) return; + if (previous <= time + 0.000001 && instance->position >= time && parameterConditions(system, instance, point + 8, bytes)) { + instance->position = time; + instance->held = true; + return; + } + } +} + +static void reposition(FmodSystem* system, EventInstance* instance, double position) { + instance->position = position; + instance->held = false; + for (ptrdiff_t i = 0; i < arrlen(instance->clips); i++) { + Clip* clip = &instance->clips[i]; + bool virtualized = clip->virtualized; + clip->finished = clip->started = false; + clip->virtualized = false; + if (position >= clip->start && position < clip->end && (clip->voice >= 0 || virtualized)) { + clip->sourcePosition = (position - clip->start) * clipPitch(system, instance, clip); + if (clip->voice >= 0) { + system->audio->vtable->setTrackPosition(system->audio, clip->voice, (float) clip->sourcePosition); + if (instance->playing && !instance->paused) system->audio->vtable->resumeSound(system->audio, clip->voice); + } + clip->started = true; + clip->virtualized = virtualized; + } else { + stopClip(system, clip); + } + } + for (ptrdiff_t i = 0; i < arrlen(instance->children); i++) { + NestedEvent* child = &instance->children[i]; + child->finished = false; + if (!child->playback) continue; + if (position >= child->start && position < child->end && child->playback->playing) { + reposition(system, child->playback, position - child->start); + } else { + freeInstance(system, child->playback); + child->playback = nullptr; + } + } +} + +bool Fmod_setPosition(FmodSystem* system, int handle, double milliseconds) { + EventInstance* instance = findInstance(system, handle); + if (!instance || !isfinite(milliseconds) || milliseconds < 0 || milliseconds > INT32_MAX) return false; + reposition(system, instance, milliseconds / 1000); + Fmod_step(system, 0); + return true; +} + +double Fmod_getPosition(FmodSystem* system, int handle) { + EventInstance* instance = findInstance(system, handle); + return instance ? floor(instance->position * 1000) : 0; +} + +// ===[ event update ]=== + +static void advanceTimeline(FmodSystem* system, EventInstance* instance, double seconds) { + if (!instance->playing || instance->paused) return; + bool starting = instance->elapsed == 0 && seconds > 0; + for (ptrdiff_t i = 0; i < arrlen(instance->values); i++) { + LocalValue* value = &instance->values[i]; + value->current = advanceParameter(value->current, value->value, &system->index.parameters[value->parameter], seconds); + } + double previous = instance->position; + instance->elapsed += seconds; + instance->position += seconds; + if (instance->stopping) { + if (instance->elapsed - instance->stopElapsed >= instance->releaseDuration) { + stopVoices(system, instance); + instance->playing = false; + } + return; + } + updateSustain(system, instance, previous); + if (instance->held || !instance->timeline) return; + FmBank* bank = instance->timeline->bank; + for (ptrdiff_t i = instance->timeline->parent + 1; i < arrlen(bank->records); i++) { + Record* transition = &bank->records[i]; + if (transition->parent < instance->timeline->parent) break; + if (!tagIs(transition, "TRNB") || transition->size < FM_TRANSITION_CONDITIONS + 16) continue; + double start = fm32(transition->data + FM_TRANSITION_START) / FM_TICKS; + double end = fm32(transition->data + FM_TRANSITION_END) / FM_TICKS; + if (instance->position < start) continue; + // a destination at the end of a transition region is already outside + // that region. repeated parameter/spatial polls must not seek it again. + if (end > start && instance->position >= end) continue; + if (start == end) { + // landing on a cue doesn't cross a loop point at that same time. + // only the initial origin can trigger without a prior position. + if (previous >= start && !(starting && start == 0 && previous == 0)) continue; + } + uint32_t bytes = fm32(transition->data + FM_TRANSITION_CONDITION_SIZE); + if (bytes > transition->size - FM_TRANSITION_CONDITIONS) continue; + if (!parameterConditions(system, instance, transition->data + FM_TRANSITION_CONDITIONS, bytes)) continue; + double target; + if (!markerPosition(instance->timeline, transition->data + FM_GUID_SIZE, &target)) continue; + if (start == end || bytes != 0) { + reposition(system, instance, target + (start == end ? fmax(0, instance->position - start) : 0)); + break; + } + } +} + +static bool startClip(FmodSystem* system, EventInstance* instance, Clip* clip) { + if (!clip->started) { + clip->activated = instance->elapsed; + clip->lastPitch = clipPitch(system, instance, clip); + clip->sourcePosition = fmax(0, instance->position - clip->start) * clip->lastPitch; + } + clip->started = true; + clip->virtualized = true; + if (system->activeVoices >= system->maxChannels) return true; + if (!FmodBank_prepareSample(system, clip->bank, clip->sample)) { + clip->virtualized = false; + return false; + } + FsbSample* sample = &clip->bank->samples[clip->sample]; + sample->references++; + clip->voice = system->audio->vtable->playEncoded(system->audio, sample->ogg, sample->oggSize, false); + if (clip->voice == -1) { + sample->references--; + return true; + } + system->activeVoices++; + clip->virtualized = false; + clip->lastGain = -1; + if (clip->voice >= 0 && system->audio->vtable->setSoundPitch) system->audio->vtable->setSoundPitch(system->audio, clip->voice, clip->lastPitch); + if (clip->voice >= 0 && clip->sourcePosition > 0) system->audio->vtable->setTrackPosition(system->audio, clip->voice, (float) clip->sourcePosition); + if (instance->paused && clip->voice >= 0) system->audio->vtable->pauseSound(system->audio, clip->voice); + return true; +} + +static bool updateClip(FmodSystem* system, EventInstance* instance, Clip* clip, double seconds) { + if (clip->started && !instance->paused) clip->sourcePosition += seconds * clip->lastPitch; + if (instance->stopping && !clip->started) { + clip->finished = true; + return false; + } + if (instance->held && !clip->started && clip->start >= instance->position - 0.000001) return true; + if (instance->position < clip->start) return true; + if (instance->position >= clip->end) { + stopClip(system, clip); + clip->finished = true; + return false; + } + if (!clip->finished && (!clip->started || (clip->virtualized && system->activeVoices < system->maxChannels && !instance->paused))) { + if (!startClip(system, instance, clip)) clip->finished = true; + } + float gain = clip->started && !clip->finished ? clipGain(system, instance, clip, nullptr) : 0; + float pitch = clip->started && !clip->finished ? clipPitch(system, instance, clip) : 1; + if (pitch != clip->lastPitch) { + clip->lastPitch = pitch; + if (clip->voice >= 0 && system->audio->vtable->setSoundPitch) system->audio->vtable->setSoundPitch(system->audio, clip->voice, pitch); + } + if (clip->voice >= 0) { + if (gain != clip->lastGain) { + system->audio->vtable->setSoundGain(system->audio, clip->voice, gain, 0); + clip->lastGain = gain; + } + if (instance->spatial) system->audio->vtable->setSoundSpatial(system->audio, clip->voice, instance->x, instance->y, 0, 100, 100000, 1); + if (!instance->paused && !system->audio->vtable->isPlaying(system->audio, clip->voice)) { + stopClip(system, clip); + clip->finished = true; + } + } + return !clip->finished; +} + +static void stepPlayback(FmodSystem* system, EventInstance* instance, double seconds) { + advanceTimeline(system, instance, seconds); + bool pending = false; + if (instance->playing) { + for (ptrdiff_t j = 0; j < arrlen(instance->clips); j++) { + if (updateClip(system, instance, &instance->clips[j], seconds)) pending = true; + } + for (ptrdiff_t j = 0; j < arrlen(instance->children); j++) { + NestedEvent* child = &instance->children[j]; + if (child->finished) continue; + if (instance->position >= child->end || (instance->stopping && !child->playback)) { + if (child->playback) stopPlayback(system, child->playback, true); + child->finished = true; + continue; + } + if (instance->position < child->start || (instance->held && !child->playback && child->start >= instance->position - 0.000001)) { + pending = true; + continue; + } + double delta = instance->paused ? 0 : seconds; + if (!child->playback) { + if (system->instanceCount >= FM_MAX_INSTANCES) { + child->finished = true; + continue; + } + child->playback = (EventInstance*) calloc(1, sizeof(*child->playback)); + if (!child->playback) { + child->finished = true; + continue; + } + system->instanceCount++; + EventInstance* playback = child->playback; + playback->event = child->event; + playback->timeline = lookup(system, "TLNB", child->event->data + FM_EVENT_TIMELINE); + playback->parent = instance; + playback->parentInstrument = child->instrument; + playback->depth = instance->depth + 1; + playback->paused = instance->paused; + if (!initializePlayback(system, playback)) { + child->finished = true; + continue; + } + double offset = fmax(0, instance->position - child->start); + delta = instance->paused ? 0 : fmin(seconds, offset); + playback->parentActivated = instance->elapsed - delta; + if (offset > delta) reposition(system, playback, offset - delta); + } + child->playback->spatial = instance->spatial; + child->playback->x = instance->x; + child->playback->y = instance->y; + stepPlayback(system, child->playback, delta); + if (child->playback->playing) pending = true; + else child->finished = true; + } + } + if (instance->playing && !instance->paused && !instance->held && !pending && instance->position >= instance->length) { + stopVoices(system, instance); + instance->playing = false; + } +} + +void Fmod_step(FmodSystem* system, double seconds) { + if (!system || !isfinite(seconds) || seconds < 0) return; + for (ptrdiff_t i = 0; i < arrlen(system->index.parameters); i++) { + Parameter* parameter = &system->index.parameters[i]; + if (parameter->global) parameter->current = advanceParameter(parameter->current, parameter->value, parameter, seconds); + } + for (ptrdiff_t i = 0; i < arrlen(system->instances);) { + EventInstance* instance = system->instances[i]; + stepPlayback(system, instance, seconds); + if (!instance->playing && (instance->released || instance->oneshot)) { + freeInstance(system, instance); + arrdel(system->instances, i); + } else { + i++; + } + } + FmodBank_evict(system, nullptr); +} + +bool Fmod_oneShot(FmodSystem* system, const char* path, bool spatial, float x, float y) { + if (spatial && (!isfinite(x) || !isfinite(y) || !system || !system->audio->vtable->setSoundSpatial)) return false; + int handle = Fmod_createInstance(system, path); + if (handle < 0) return false; + EventInstance* instance = findInstance(system, handle); + instance->oneshot = true; + instance->spatial = spatial; + instance->x = x; + instance->y = y; + if (!Fmod_play(system, handle)) { + Fmod_release(system, handle); + return false; + } + return true; +} diff --git a/src/fmod/fmod.h b/src/fmod/fmod.h new file mode 100644 index 000000000..c44b75829 --- /dev/null +++ b/src/fmod/fmod.h @@ -0,0 +1,43 @@ +#ifndef BS_FMOD_H +#define BS_FMOD_H + +#include "audio_system.h" + +typedef struct FmodSystem FmodSystem; + +#ifdef ENABLE_FMOD +// the system owns banks and samples; the audio backend owns the voices. +// stop the voices before freeing any sample memory they're using. +FmodSystem* Fmod_create(AudioSystem* audio, FileSystem* fs, int channels); +void Fmod_destroy(FmodSystem* system); +void Fmod_audioDestroy(AudioSystem* audio); +void Fmod_step(FmodSystem* system, double seconds); +bool Fmod_loadBank(FmodSystem* system, const char* path, bool nonblocking); +bool Fmod_loadSamples(FmodSystem* system, const char* path); +int Fmod_createInstance(FmodSystem* system, const char* path); +bool Fmod_play(FmodSystem* system, int handle); +bool Fmod_stop(FmodSystem* system, int handle, bool immediate); +bool Fmod_release(FmodSystem* system, int handle); +bool Fmod_isPlaying(FmodSystem* system, int handle); +bool Fmod_pause(FmodSystem* system, int handle, bool pause); +bool Fmod_getPaused(FmodSystem* system, int handle); +void Fmod_pauseAll(FmodSystem* system, bool pause); +bool Fmod_setPosition(FmodSystem* system, int handle, double milliseconds); +double Fmod_getPosition(FmodSystem* system, int handle); +double Fmod_getLength(FmodSystem* system, const char* path); +bool Fmod_eventExists(FmodSystem* system, const char* path); +bool Fmod_setParameter(FmodSystem* system, int handle, const char* name, float value, bool immediate); +float Fmod_getParameter(FmodSystem* system, int handle, const char* name); +bool Fmod_setSpatial(FmodSystem* system, int handle, float x, float y); +bool Fmod_setListener(FmodSystem* system, int listener, float x, float y); +bool Fmod_setListeners(FmodSystem* system, int count); +bool Fmod_oneShot(FmodSystem* system, const char* path, bool spatial, float x, float y); + +#else + +static inline void Fmod_audioDestroy(MAYBE_UNUSED AudioSystem* audio) {} +static inline void Fmod_step(MAYBE_UNUSED FmodSystem* system, MAYBE_UNUSED double seconds) {} + +#endif + +#endif diff --git a/src/fmod/fmod_bank.c b/src/fmod/fmod_bank.c new file mode 100644 index 000000000..ae888e11f --- /dev/null +++ b/src/fmod/fmod_bank.c @@ -0,0 +1,438 @@ +#include "fmod_internal.h" +#include "log.h" +#include "math_compat.h" +#include + +// ===[ files and records ]=== + +static char* copyString(const char* string) { + size_t bytes = strlen(string) + 1; + char* copy = (char*) malloc(bytes); + if (copy) memcpy(copy, string, bytes); + return copy; +} + +static char* normalizePath(const char* path) { + char* normalized = copyString(path); + if (normalized) { + for (char* p = normalized; *p; p++) { + if (*p == '\\') *p = '/'; + } + } + return normalized; +} + +static FmBank* findBank(FmodSystem* system, const char* path) { + for (ptrdiff_t i = 0; i < arrlen(system->banks); i++) { + if (strcmp(system->banks[i]->path, path) == 0) return system->banks[i]; + } + return nullptr; +} + +static bool readAt(FmodSystem* system, void* file, uint32_t offset, void* destination, size_t bytes) { + size_t done = 0; + if (offset > INT32_MAX || bytes > INT32_MAX || !system->fs->vtable->binarySeek(system->fs, file, (int32_t) offset)) return false; + while (done < bytes) { + int32_t read = system->fs->vtable->binaryRead(system->fs, file, (uint8_t*) destination + done, (int32_t) (bytes - done)); + if (read <= 0 || (size_t) read > bytes - done) return false; + done += (size_t) read; + } + return true; +} + +static bool parseRecords(FmBank* bank, size_t start, size_t end, int parent, unsigned depth) { + size_t position = start; + if (depth > 32) return false; + while (position < end) { + Record record = {0}; + if (arrlen(bank->records) >= FM_MAX_RECORDS) return false; + if (end - position < 8) return false; + size_t bytes = fm32(bank->metadata + position + 4); + if (bytes > end - position - 8) return false; + memcpy(record.tag, bank->metadata + position, 4); + record.data = bank->metadata + position + 8; + record.size = bytes; + record.parent = parent; + record.bank = bank; + int index = (int) arrlen(bank->records); + if (memcmp(record.tag, "LIST", 4) == 0) { + if (bytes < 4) return false; + memcpy(record.tag, record.data, 4); + record.data += 4; + record.size -= 4; + arrput(bank->records, record); + if (!parseRecords(bank, position + 12, position + 8 + bytes, index, depth + 1)) return false; + } else { + arrput(bank->records, record); + } + position += 8 + bytes + (bytes & 1); + if (position > end) return false; + } + return position == end; +} + +static bool parseEffects(FmBank* bank) { + for (ptrdiff_t i = 0; i < arrlen(bank->records); i++) { + Record* owner = &bank->records[i]; + if (!tagIs(owner, "BEFB")) continue; + if (owner->size < FM_EFFECT_SIZE) return false; + Record* payload = findSiblingRecord(owner, "PMEF"); + if (!payload || payload->size < 4) return false; + EffectDefinition effect = {owner, fm32(owner->data + FM_EFFECT_TYPE), owner->data[FM_EFFECT_BYPASS] != 0, nullptr}; + arrput(bank->effects, effect); + EffectDefinition* definition = &bank->effects[arrlen(bank->effects) - 1]; + FmReader reader = {payload->data, payload->size, 4, false}; + uint32_t count = fm32(payload->data); + if (count > FM_MAX_EFFECT_PARAMETERS) return false; + for (uint32_t parameter = 0; parameter < count; parameter++) { + const uint8_t* kind = fmread(&reader, 4); + if (!kind) return false; + EffectParameter value = {0}; + value.kind = fm32(kind); + if (value.kind == FM_EFFECT_FLOAT || value.kind == FM_EFFECT_INTEGER) { + const uint8_t* data = fmread(&reader, 4); + if (!data) return false; + if (value.kind == FM_EFFECT_FLOAT) { + value.value.real = fmfloat(data); + if (!isfinite(value.value.real)) return false; + } else { + value.value.integer = (int32_t) fm32(data); + } + } else if (value.kind == FM_EFFECT_BOOLEAN) { + const uint8_t* data = fmread(&reader, 1); + if (!data || *data > 1) return false; + value.value.boolean = *data != 0; + } else if (value.kind == FM_EFFECT_DATA) { + value.value.buffer.size = fmcount(&reader); + value.value.buffer.data = fmread(&reader, value.value.buffer.size); + if (reader.failed) return false; + } else { + return false; + } + arrput(definition->parameters, value); + } + } + return true; +} + +void FmodBank_destroy(FmBank* bank) { + if (!bank) return; + Fsb_free(bank->samples); + for (ptrdiff_t i = 0; i < arrlen(bank->effects); i++) arrfree(bank->effects[i].parameters); + arrfree(bank->effects); + arrfree(bank->records); + free(bank->metadata); + free(bank->path); + free(bank); +} + +static FmBank* parseBank(FmodSystem* system, const char* path) { + uint8_t riff[12], header[64]; + uint32_t position = 12, metadataEnd = 0, fsbOffset = 0, fsbBytes = 0; + FmBank* bank = nullptr; + uint8_t* fsb = nullptr; + void* file = system->fs->vtable->binaryOpen(system->fs, path, GML_FILE_BIN_READ); + if (!file) return nullptr; + int32_t fileSize = system->fs->vtable->binarySize(system->fs, file); + if (fileSize < 12 || !readAt(system, file, 0, riff, 12) || memcmp(riff, "RIFF", 4) || memcmp(riff + 8, "FEV ", 4) || fm32(riff + 4) != (uint32_t) fileSize - 8) goto fail; + + while (position < (uint32_t) fileSize) { + if ((uint32_t) fileSize - position < 8 || !readAt(system, file, position, header, 8)) goto fail; + uint32_t bytes = fm32(header + 4); + if (bytes > (uint32_t) fileSize - position - 8) goto fail; + if (memcmp(header, "SND ", 4) == 0) { + if (fsbOffset) goto fail; + metadataEnd = position; + uint32_t scan = bytes < sizeof(header) ? bytes : (uint32_t) sizeof(header); + if (!readAt(system, file, position + 8, header, scan)) goto fail; + uint32_t padding; + for (padding = 0; padding + 4 <= scan; padding++) { + if (memcmp(header + padding, "FSB5", 4) == 0) break; + } + if (padding + 4 > scan) goto fail; + fsbOffset = position + 8 + padding; + if (bytes - padding < 60 || !readAt(system, file, fsbOffset, header, 60)) goto fail; + if (fm32(header + 12) > FM_MAX_METADATA || fm32(header + 16) > FM_MAX_METADATA - fm32(header + 12)) goto fail; + fsbBytes = 60 + fm32(header + 12) + fm32(header + 16); + if (fsbBytes > bytes - padding || fm32(header + 20) > bytes - padding - fsbBytes) goto fail; + } else if (fsbOffset) { + goto fail; + } + position += 8 + bytes + (bytes & 1); + } + if (position != (uint32_t) fileSize) goto fail; + if (!metadataEnd) metadataEnd = (uint32_t) fileSize; + if (metadataEnd > FM_MAX_METADATA) goto fail; + bank = (FmBank*) calloc(1, sizeof(*bank)); + if (!bank) goto fail; + bank->path = copyString(path); + bank->metadata = (uint8_t*) malloc(metadataEnd); + if (!bank->path || !bank->metadata || !readAt(system, file, 0, bank->metadata, metadataEnd) || !parseRecords(bank, 12, metadataEnd, -1, 0)) goto fail; + + bool hasFormat = false; + for (ptrdiff_t i = 0; i < arrlen(bank->records); i++) { + Record* record = &bank->records[i]; + if (tagIs(record, "FMT ")) { + if (record->size != 8 || fm32(record->data) != 142 || fm32(record->data + 4) != 140) goto fail; + hasFormat = true; + } + } + if (!hasFormat || !parseEffects(bank)) goto fail; + if (fsbOffset) { + uint32_t dataOffset; + fsb = (uint8_t*) malloc(fsbBytes); + if (!fsb || !readAt(system, file, fsbOffset, fsb, fsbBytes) || !Fsb_parse(fsb, fsbBytes, &dataOffset, &bank->samples)) goto fail; + bank->dataOffset = fsbOffset + dataOffset; + } + free(fsb); + system->fs->vtable->binaryClose(system->fs, file); + return bank; + +fail: + free(fsb); + FmodBank_destroy(bank); + system->fs->vtable->binaryClose(system->fs, file); + return nullptr; +} + +// ===[ strings bank ]=== + +typedef struct { + const uint8_t* nodes; + uint32_t count, visits; + const uint8_t* guids; + uint32_t guidCount; + const uint8_t* strings; + uint32_t stringBytes; +} StringTable; + +static bool indexStringNode(FmodBankIndex* index, StringTable* table, uint32_t nodeIndex, char* path, size_t length, unsigned depth) { + if (nodeIndex >= table->count || depth > 128 || table->visits++ >= table->count) return false; + const uint8_t* node = table->nodes + nodeIndex * 8; + uint32_t stringOffset = fm24(node), firstChild = fm24(node + 4), childCount = node[7]; + if (stringOffset != 0xffffff) { + if (stringOffset >= table->stringBytes) return false; + const uint8_t* end = (const uint8_t*) memchr(table->strings + stringOffset, 0, table->stringBytes - stringOffset); + if (!end) return false; + size_t bytes = (size_t) (end - (table->strings + stringOffset)); + if (bytes >= 1024 - length) return false; + memcpy(path + length, table->strings + stringOffset, bytes); + length += bytes; + } + path[length] = 0; + if (childCount) { + if (firstChild > table->count || childCount > table->count - firstChild) return false; + for (uint32_t i = 0; i < childCount; i++) { + if (!indexStringNode(index, table, firstChild + i, path, length, depth + 1)) return false; + } + } else { + if (firstChild >= table->guidCount) return false; + Guid id; + memcpy(id.b, table->guids + firstChild * FM_GUID_SIZE, FM_GUID_SIZE); + shput(index->names, path, id); + } + return true; +} + +static bool indexStrings(FmodBankIndex* index, Record* record) { + FmReader reader = {record->data, record->size, 0, false}; + StringTable table = {0}; + uint32_t stride; + char path[1024]; + const uint8_t* version = fmread(&reader, 4); + if (!version || fm32(version) != 1) return false; + table.nodes = fmlist(&reader, &table.count, &stride); + if (reader.failed || stride != 8) return false; + table.guids = fmlist(&reader, &table.guidCount, &stride); + if (reader.failed || stride != FM_GUID_SIZE) return false; + table.stringBytes = fmcount(&reader); + table.strings = fmread(&reader, table.stringBytes); + if (reader.failed) return false; + return indexStringNode(index, &table, 0, path, 0, 0); +} + +// ===[ bank indexing ]=== + +static bool indexBank(FmodBankIndex* index, FmBank* bank) { + static const struct { + const char* tag; + size_t minimumBytes; + } indexedRecords[] = { + {"EVTB", FM_EVENT_PARAMETERS + 2}, {"TLNB", FM_TIMELINE_LISTS + 8}, + {"WAIB", 32}, {"MUIB", 16}, {"EVIB", 32}, {"SLNB", 20}, + {"WAV ", FM_WAVE_SAMPLE + 4}, {"INST", FM_INSTRUMENT_BUS + FM_GUID_SIZE}, + {"IBSB", FM_BUS_PARENT + FM_GUID_SIZE}, {"GBSB", FM_BUS_PARENT + FM_GUID_SIZE}, + {"MBSB", FM_BUS_PARENT + FM_GUID_SIZE}, {"PMLB", 32}, + {"PRMB", FM_PARAMETER_NAME}, {"CURV", FM_CURVE_POINTS + 2}, {"CTRL", 52}, + {"MODB", 46}, + {"MAP ", FM_GUID_SIZE + 2}, + {"BEFB", FM_EFFECT_SIZE}, + }; + + for (ptrdiff_t i = 0; i < arrlen(bank->records); i++) { + Record* record = &bank->records[i]; + if (tagIs(record, "STDT") && record->size && !indexStrings(index, record)) return false; + for (size_t j = 0; j < sizeof(indexedRecords) / sizeof(indexedRecords[0]); j++) { + if (!tagIs(record, indexedRecords[j].tag)) continue; + if (record->size < indexedRecords[j].minimumBytes) return false; + Key key; + memcpy(key.b, record->data + (tagIs(record, "MODB") ? FM_MODULATOR_GUID : 0), FM_GUID_SIZE); + memcpy(key.b + FM_GUID_SIZE, record->tag, 4); + hmput(index->records, key, record); + break; + } + if (tagIs(record, "PRMB")) { + uint32_t nameBytes = fm16(record->data + FM_PARAMETER_NAME_LENGTH); + size_t limitsOffset = FM_PARAMETER_NAME + nameBytes; + if (nameBytes > 255 || limitsOffset + 12 > record->size) return false; + float minimum = fmfloat(record->data + limitsOffset); + float maximum = fmfloat(record->data + limitsOffset + 4); + float initial = fmfloat(record->data + limitsOffset + 8); + if (!isfinite(minimum) || !isfinite(maximum) || !isfinite(initial) || minimum > maximum || initial < minimum || initial > maximum) return false; + if (findParameterIndex(index, record->data) >= 0) continue; + Parameter parameter = {0}; + memcpy(parameter.id.b, record->data, FM_GUID_SIZE); + parameter.name = (char*) malloc(nameBytes + 1); + if (!parameter.name) return false; + memcpy(parameter.name, record->data + FM_PARAMETER_NAME, nameBytes); + parameter.name[nameBytes] = 0; + parameter.global = fm32(record->data + FM_PARAMETER_GLOBAL) != 0; + parameter.min = minimum; + parameter.max = maximum; + parameter.initial = parameter.value = initial; + parameter.current = initial; + if (record->size >= limitsOffset + 24) { + parameter.seekUp = fmfloat(record->data + limitsOffset + 16); + parameter.seekDown = fmfloat(record->data + limitsOffset + 20); + if (!isfinite(parameter.seekUp) || !isfinite(parameter.seekDown) || parameter.seekUp < 0 || parameter.seekDown < 0) { + free(parameter.name); + return false; + } + } + arrput(index->parameters, parameter); + } + } + for (ptrdiff_t i = 0; i < arrlen(bank->records); i++) { + Record* record = &bank->records[i]; + if (!tagIs(record, "PMLB")) continue; + Record* controllers = findSiblingRecord(record, "CTRO"); + if (!controllers) continue; + FmReader reader = {controllers->data, controllers->size, 0, false}; + uint32_t count, stride; + const uint8_t* list = fmlist(&reader, &count, &stride); + if (reader.failed || (count && stride != FM_GUID_SIZE)) return false; + for (uint32_t j = 0; j < count; j++) { + CurveInput input; + memcpy(input.curve.b, list + j * FM_GUID_SIZE, FM_GUID_SIZE); + memcpy(input.parameter.b, record->data + FM_GUID_SIZE, FM_GUID_SIZE); + arrput(index->inputs, input); + } + } + return true; +} + +void FmodBank_clearIndex(FmodBankIndex* index) { + for (ptrdiff_t i = 0; i < arrlen(index->parameters); i++) free(index->parameters[i].name); + arrfree(index->parameters); + arrfree(index->inputs); + hmfree(index->records); + shfree(index->names); +} + +static bool publishBank(FmodSystem* system, FmBank* bank) { + // don't swap anything into the live system until this bank checks out. + FmodBankIndex staged = {0}; + sh_new_strdup(staged.names); + for (ptrdiff_t i = 0; i < arrlen(system->banks); i++) { + if (!indexBank(&staged, system->banks[i])) goto fail; + } + if (!indexBank(&staged, bank)) goto fail; + + // instances still use the old parameter indices. keep those in place, + // along with any values the game has changed since loading the banks. + if (arrlen(staged.parameters) < arrlen(system->index.parameters)) goto fail; + for (ptrdiff_t i = 0; i < arrlen(system->index.parameters); i++) { + if (!equalGuid(staged.parameters[i].id.b, system->index.parameters[i].id.b)) goto fail; + staged.parameters[i].value = system->index.parameters[i].value; + staged.parameters[i].current = system->index.parameters[i].current; + } + FmodBank_clearIndex(&system->index); + system->index = staged; + arrput(system->banks, bank); + return true; + +fail: + FmodBank_clearIndex(&staged); + return false; +} + +bool Fmod_loadBank(FmodSystem* system, const char* path, bool nonblocking) { + (void) nonblocking; // this loader finishes on the game thread, even if async was requested. + if (!system || !path || !*path) return false; + char* normalized = normalizePath(path); + if (!normalized) return false; + if (findBank(system, normalized)) { + free(normalized); + return true; + } + FmBank* bank = parseBank(system, normalized); + free(normalized); + if (!bank || !publishBank(system, bank)) { + FmodBank_destroy(bank); + logWarn("FMOD: Cannot load bank '%s' (missing, malformed, or unsupported format)\n", path); + return false; + } + logInfo("FMOD: Loaded %s (%td samples)\n", path, arrlen(bank->samples)); + return true; +} + +// ===[ encoded sample cache ]=== + +void FmodBank_evict(FmodSystem* system, FsbSample* protectedSample) { + while (system->cacheBytes > FM_CACHE_LIMIT) { + FsbSample* oldest = nullptr; + for (ptrdiff_t i = 0; i < arrlen(system->banks); i++) { + for (ptrdiff_t j = 0; j < arrlen(system->banks[i]->samples); j++) { + FsbSample* sample = &system->banks[i]->samples[j]; + if (sample != protectedSample && sample->ogg && !sample->references && (!oldest || sample->lastUse < oldest->lastUse)) oldest = sample; + } + } + if (!oldest) break; + system->cacheBytes -= oldest->oggSize; + arrfree(oldest->ogg); + oldest->oggSize = 0; + } +} + +bool FmodBank_prepareSample(FmodSystem* system, FmBank* bank, int index) { + if (index < 0 || index >= arrlen(bank->samples)) return false; + FsbSample* sample = &bank->samples[index]; + sample->lastUse = ++system->useCounter; + if (sample->ogg) return true; + uint8_t* packets = (uint8_t*) malloc(sample->bytes); + if (!packets) return false; + void* file = system->fs->vtable->binaryOpen(system->fs, bank->path, GML_FILE_BIN_READ); + bool loaded = file && readAt(system, file, bank->dataOffset + sample->offset, packets, sample->bytes) && Fsb_rebuild(sample, packets, sample->bytes); + if (file) system->fs->vtable->binaryClose(system->fs, file); + free(packets); + if (loaded) { + system->cacheBytes += sample->oggSize; + FmodBank_evict(system, sample); + } else { + logWarn("FMOD: Could not decode %s sample %d\n", bank->path, index); + } + return loaded; +} + +bool Fmod_loadSamples(FmodSystem* system, const char* path) { + if (!Fmod_loadBank(system, path, false)) return false; + char* normalized = normalizePath(path); + if (!normalized) return false; + FmBank* bank = findBank(system, normalized); + free(normalized); + if (!bank) return false; + for (ptrdiff_t i = 0; i < arrlen(bank->samples); i++) { + if (!FmodBank_prepareSample(system, bank, (int) i)) return false; + } + return true; +} diff --git a/src/fmod/fmod_dsp.c b/src/fmod/fmod_dsp.c new file mode 100644 index 000000000..00a0c9e9d --- /dev/null +++ b/src/fmod/fmod_dsp.c @@ -0,0 +1,96 @@ +#include "fmod_dsp.h" +#include "math_compat.h" +#include + +enum { ECHO = 6 }; + +struct FmodDsp { + unsigned type, rate, channels; + bool bypass; + struct { + float* samples; + unsigned capacity, position; + double delay; + float feedback, dry, wet; + } echo; +}; + +static float decibelGain(float value) { + return value <= -80 ? 0 : powf(10, value / 20); +} + +bool FmodDsp_setParameter(FmodDsp* effect, unsigned index, const EffectParameter* parameter) { + if (!effect || !parameter || parameter->kind != FM_EFFECT_FLOAT || !isfinite(parameter->value.real)) return false; + float value = parameter->value.real; + if (effect->type != ECHO) return false; + switch (index) { + case 0: + effect->echo.delay = fmax(1, fmin(value, 5000)) * effect->rate / 1000; + break; + case 1: + effect->echo.feedback = fmaxf(0, fminf(value, 100)) / 100; + break; + case 2: + effect->echo.dry = decibelGain(fmaxf(-80, fminf(value, 10))); + break; + case 3: + effect->echo.wet = decibelGain(fmaxf(-80, fminf(value, 10))); + break; + default: return false; + } + return true; +} + +FmodDsp* FmodDsp_create(const EffectDefinition* definition, unsigned rate, unsigned channels) { + if (!definition || definition->type != ECHO || rate == 0 || rate > 192000 || channels == 0 || channels > 8) return nullptr; + if (arrlen(definition->parameters) > 4) return nullptr; + FmodDsp* effect = (FmodDsp*) calloc(1, sizeof(*effect)); + if (!effect) return nullptr; + effect->type = definition->type; + effect->rate = rate; + effect->channels = channels; + effect->bypass = definition->bypass; + effect->echo.capacity = rate * 5 + 1; + effect->echo.samples = (float*) calloc((size_t) effect->echo.capacity * channels, sizeof(float)); + if (!effect->echo.samples) { + free(effect); + return nullptr; + } + effect->echo.delay = rate / 2.0; + effect->echo.feedback = 0.5f; + effect->echo.dry = effect->echo.wet = 1; + for (ptrdiff_t i = 0; i < arrlen(definition->parameters); i++) { + if (!FmodDsp_setParameter(effect, (unsigned) i, &definition->parameters[i])) { + FmodDsp_destroy(effect); + return nullptr; + } + } + return effect; +} + +void FmodDsp_destroy(FmodDsp* effect) { + if (!effect) return; + free(effect->echo.samples); + free(effect); +} + +void FmodDsp_process(FmodDsp* effect, float* pcm, unsigned frames) { + if (!effect || !pcm || effect->bypass) return; + float* buffer = effect->echo.samples; + for (unsigned frame = 0; frame < frames; frame++) { + double position = effect->echo.position - effect->echo.delay; + if (position < 0) position += effect->echo.capacity; + unsigned first = (unsigned) position; + unsigned second = (first + 1) % effect->echo.capacity; + float weight = (float) (position - first); + for (unsigned channel = 0; channel < effect->channels; channel++) { + float left = buffer[(size_t) first * effect->channels + channel]; + float right = buffer[(size_t) second * effect->channels + channel]; + float delayed = left + (right - left) * weight; + float input = pcm[(size_t) frame * effect->channels + channel]; + buffer[(size_t) effect->echo.position * effect->channels + channel] = input + delayed * effect->echo.feedback; + pcm[(size_t) frame * effect->channels + channel] = input * effect->echo.dry + delayed * effect->echo.wet; + } + effect->echo.position = (effect->echo.position + 1) % effect->echo.capacity; + } +} diff --git a/src/fmod/fmod_dsp.h b/src/fmod/fmod_dsp.h new file mode 100644 index 000000000..5931cc5ec --- /dev/null +++ b/src/fmod/fmod_dsp.h @@ -0,0 +1,15 @@ +#ifndef BS_FMOD_DSP_H +#define BS_FMOD_DSP_H + +#include "fmod_internal.h" + +typedef struct FmodDsp FmodDsp; + +// effect state belongs to one bus, not to each voice routed through it. +// unsupported effect types return null until their processors are implemented. +FmodDsp* FmodDsp_create(const EffectDefinition* definition, unsigned rate, unsigned channels); +void FmodDsp_destroy(FmodDsp* effect); +bool FmodDsp_setParameter(FmodDsp* effect, unsigned index, const EffectParameter* parameter); +void FmodDsp_process(FmodDsp* effect, float* pcm, unsigned frames); + +#endif diff --git a/src/fmod/fmod_internal.h b/src/fmod/fmod_internal.h new file mode 100644 index 000000000..81f0a67aa --- /dev/null +++ b/src/fmod/fmod_internal.h @@ -0,0 +1,275 @@ +#ifndef BS_FMOD_INTERNAL_H +#define BS_FMOD_INTERNAL_H + +#include "fmod.h" +#include "fsb.h" +#include "fmod_reader.h" +#include "stb_ds.h" +#include + +#define FM_TICKS 48000.0 +#define FM_CACHE_LIMIT (64u * 1024u * 1024u) +#define FM_MAX_METADATA (16u * 1024u * 1024u) +#define FM_MAX_RECORDS 262144 +#define FM_MAX_CLIPS 2048 +#define FM_MAX_INSTANCES 4096 +#define FM_MAX_EFFECT_PARAMETERS 256 +#define FM_HANDLE_LIMIT 16777216 + +// offsets for bank format 142/140. references are 16-byte ids on disk; +// list headers need decoding before the entries can be read. +enum { + FM_GUID_SIZE = 16, + FM_EVENT_TIMELINE = 32, + FM_EVENT_INPUT_BUS = 48, + FM_EVENT_MASTER_BUS = 64, + FM_EVENT_PARAMETERS = 93, + FM_TIMELINE_LISTS = 16, + FM_TIMED_ITEM_START = 16, + FM_TIMED_ITEM_DURATION = 20, + FM_TIMED_ITEM_SIZE = 24, + FM_INSTRUMENT_BUS = 86, + FM_INSTRUMENT_VOLUME = 16, + FM_INSTRUMENT_PITCH = 20, + FM_BUS_PARENT = 18, + FM_WAVE_SAMPLE = 22, + FM_PARAMETER_GLOBAL = 16, + FM_PARAMETER_NAME_LENGTH = 24, + FM_PARAMETER_NAME = 26, + FM_CURVE_POINTS = 32, + FM_PROPERTY_ID = 8, + FM_PROPERTY_MAPPING = 6, + FM_PROPERTY_CONTROLLERS = 24, + FM_MODULATOR_GUID = 2, + FM_MODULATOR_TYPE = 38, + FM_MODULATOR_MODE = 42, + FM_MODULATOR_AMOUNT = 46, + FM_RANDOM_MODULATOR_SIZE = 50, + FM_EFFECT_TYPE = 16, + FM_EFFECT_BYPASS = 20, + FM_EFFECT_SIZE = 21, + FM_ENVELOPE_INITIAL = 46, + FM_ENVELOPE_PEAK = 50, + FM_ENVELOPE_SUSTAIN = 54, + FM_ENVELOPE_ATTACK = 58, + FM_ENVELOPE_HOLD = 62, + FM_ENVELOPE_DECAY = 66, + FM_ENVELOPE_RELEASE = 70, + FM_ENVELOPE_ATTACK_CURVE = 74, + FM_ENVELOPE_DECAY_CURVE = 78, + FM_ENVELOPE_RELEASE_CURVE = 82, + FM_ENVELOPE_FINAL = 86, + FM_ENVELOPE_SIZE = 90, + FM_TRANSITION_START = 32, + FM_TRANSITION_END = 36, + FM_TRANSITION_CONDITION_SIZE = 40, + FM_TRANSITION_CONDITIONS = 44, + FM_CONDITION_PARAMETER = 4, + FM_CONDITION_LOW = 24, + FM_CONDITION_HIGH = 28, + FM_CONDITION_SIZE = 32, +}; + +enum { + FM_EFFECT_FLOAT = 0, + FM_EFFECT_INTEGER = 1, + FM_EFFECT_BOOLEAN = 2, + FM_EFFECT_DATA = 3, +}; + +enum { + FM_CURVE_EXPONENTIAL = 0, + FM_CURVE_SYMMETRIC = 1, + FM_CURVE_SQUARED = 2, + FM_CURVE_STEPPED = 3, +}; + +typedef struct { + uint8_t b[FM_GUID_SIZE]; +} Guid; + +typedef struct { + uint8_t b[FM_GUID_SIZE + 4]; +} Key; +typedef struct FmBank FmBank; + +// record data lives in the bank's metadata buffer. once indexed, the records +// stay put until teardown, so an instance can keep pointers to them. +typedef struct { + char tag[4]; + const uint8_t* data; + size_t size; + int parent; + FmBank* bank; +} Record; + +typedef struct { + Key key; + Record* value; +} RecordMap; + +typedef struct { + char* key; + Guid value; +} NameMap; + +typedef struct { + uint32_t kind; + union { + float real; + int32_t integer; + bool boolean; + struct { + const uint8_t* data; + uint32_t size; + } buffer; + } value; +} EffectParameter; + +typedef struct { + Record* owner; + uint32_t type; + bool bypass; + EffectParameter* parameters; +} EffectDefinition; + +typedef struct { + Guid id; + char* name; + bool global; + float min, max, initial, value; + float current, seekUp, seekDown; +} Parameter; + +typedef struct { + Guid curve, parameter; +} CurveInput; + +typedef struct { + RecordMap* records; + NameMap* names; + Parameter* parameters; + CurveInput* inputs; +} FmodBankIndex; + +struct FmBank { + char* path; + uint8_t* metadata; + uint32_t dataOffset; + FsbSample* samples; + Record* records; + EffectDefinition* effects; +}; + +typedef struct { + int parameter; + float value; + float current; +} LocalValue; + +typedef struct { + Record* modulator; + float value; +} ModulationValue; + +typedef struct { + FmBank* bank; + int sample; + Record* instrument; + Record* controllers[16]; + unsigned controllerCount; + ModulationValue* modulation; + double start, end; + double activated; + double sourcePosition; + int voice; + bool started, finished, virtualized; + float lastGain; + float lastPitch; +} Clip; + +typedef struct EventInstance EventInstance; + +typedef struct { + Record* event; + Record* instrument; + double start, end; + EventInstance* playback; + bool finished; +} NestedEvent; + +typedef struct { + Record* owner; + double activated; +} BusClock; + +struct EventInstance { + int handle; + Record* event; + Record* timeline; + Clip* clips; + LocalValue* values; + NestedEvent* children; + BusClock* clocks; + EventInstance* parent; + Record* parentInstrument; + double parentActivated; + unsigned depth; + bool playing, paused, released, oneshot, spatial, stopping, held; + float x, y; + double position, length; + double elapsed, stopElapsed, releaseDuration; +}; + +struct FmodSystem { + AudioSystem* audio; + FileSystem* fs; + FmBank** banks; + FmodBankIndex index; + EventInstance** instances; + unsigned random; + int nextHandle, maxChannels, activeVoices; + int instanceCount; + size_t cacheBytes; + uint64_t useCounter; +}; + +static inline bool tagIs(const Record* record, const char* tag) { + return record && memcmp(record->tag, tag, 4) == 0; +} + +static inline bool equalGuid(const uint8_t* a, const uint8_t* b) { + return memcmp(a, b, FM_GUID_SIZE) == 0; +} + +static inline Record* lookup(FmodSystem* system, const char* tag, const uint8_t* id) { + Key key; + if (!id) return nullptr; + memcpy(key.b, id, FM_GUID_SIZE); + memcpy(key.b + FM_GUID_SIZE, tag, 4); + return hmget(system->index.records, key); +} + +static inline Record* findSiblingRecord(Record* owner, const char* tag) { + if (!owner) return nullptr; + for (ptrdiff_t i = owner->parent + 1; i < arrlen(owner->bank->records); i++) { + Record* record = &owner->bank->records[i]; + if (record->parent < owner->parent) break; + if (record->parent == owner->parent && tagIs(record, tag)) return record; + } + return nullptr; +} + +static inline int findParameterIndex(FmodBankIndex* index, const uint8_t* id) { + for (ptrdiff_t i = 0; i < arrlen(index->parameters); i++) { + if (equalGuid(index->parameters[i].id.b, id)) return (int) i; + } + return -1; +} + +void FmodBank_destroy(FmBank* bank); +void FmodBank_clearIndex(FmodBankIndex* index); +void FmodBank_evict(FmodSystem* system, FsbSample* protectedSample); +bool FmodBank_prepareSample(FmodSystem* system, FmBank* bank, int index); + +#endif diff --git a/src/fmod/fmod_reader.h b/src/fmod/fmod_reader.h new file mode 100644 index 000000000..508109d49 --- /dev/null +++ b/src/fmod/fmod_reader.h @@ -0,0 +1,114 @@ +#ifndef BS_FMOD_READER_H +#define BS_FMOD_READER_H +#include "common.h" +#include +#include + +static inline uint16_t fm16(const uint8_t* p) { + return (uint16_t) (p[0] | ((uint16_t) p[1] << 8)); +} + +static inline uint32_t fm24(const uint8_t* p) { + return p[0] | ((uint32_t) p[1] << 8) | ((uint32_t) p[2] << 16); +} + +static inline uint32_t fm32(const uint8_t* p) { + return fm16(p) | ((uint32_t) fm16(p + 2) << 16); +} + +static inline uint64_t fm64(const uint8_t* p) { + return fm32(p) | ((uint64_t) fm32(p + 4) << 32); +} + +static inline float fmfloat(const uint8_t* p) { + uint32_t n = fm32(p); + float value; + memcpy(&value, &n, 4); + return value; +} + +static inline void fmput32(uint8_t* p, uint32_t n) { + p[0] = (uint8_t) n; + p[1] = (uint8_t) (n >> 8); + p[2] = (uint8_t) (n >> 16); + p[3] = (uint8_t) (n >> 24); +} + +typedef struct { + const uint8_t* data; + size_t size, position; + bool failed; +} FmReader; + +static inline const uint8_t* fmread(FmReader* reader, size_t bytes) { + if (reader->failed || reader->position > reader->size || bytes > reader->size - reader->position) { + reader->failed = true; + return nullptr; + } + const uint8_t* data = reader->data + reader->position; + reader->position += bytes; + return data; +} + +// bit 15 means another word follows; the first word contributes 15 bits. +static inline uint32_t fmcount(FmReader* reader) { + const uint8_t* data = fmread(reader, 2); + if (!data) return 0; + uint32_t value = fm16(data); + if (value & 0x8000) { + data = fmread(reader, 2); + if (!data) return 0; + value = (value & 0x7fff) | ((uint32_t) fm16(data) << 15); + } + return value; +} + +static inline const uint8_t* fmlist(FmReader* reader, uint32_t* count, uint32_t* stride) { + uint32_t encoded = fmcount(reader); + *count = encoded >> 1; + // this reads a contiguous list. markers and sustain points can have + // different-sized entries, so those need to be walked one at a time. + if (*count > 1 && !(encoded & 1)) { + reader->failed = true; + return nullptr; + } + *stride = *count ? fmcount(reader) : 0; + if (*stride == 0 && *count) { + reader->failed = true; + return nullptr; + } + if (*count > reader->size / (*stride ? *stride : 1)) { + reader->failed = true; + return nullptr; + } + return fmread(reader, (size_t) *count * *stride); +} + +typedef struct { + FmReader* reader; + uint32_t remaining; + uint32_t stride; + bool fixedStride; +} FmList; + +static inline bool FmList_begin(FmList* list, FmReader* reader) { + uint32_t encoded = fmcount(reader); + list->reader = reader; + list->remaining = encoded >> 1; + list->fixedStride = (encoded & 1) != 0; + list->stride = 0; + if (list->fixedStride && list->remaining) { + list->stride = fmcount(reader); + if (!list->stride) reader->failed = true; + } + return !reader->failed; +} + +static inline const uint8_t* FmList_next(FmList* list, uint32_t* bytes) { + if (!list->remaining || list->reader->failed) return nullptr; + *bytes = list->fixedStride ? list->stride : fmcount(list->reader); + list->remaining--; + return fmread(list->reader, *bytes); +} + +#endif diff --git a/src/fmod/fsb.c b/src/fmod/fsb.c new file mode 100644 index 000000000..25ee8febd --- /dev/null +++ b/src/fmod/fsb.c @@ -0,0 +1,242 @@ +#include "fsb.h" +#include "fmod_reader.h" +#include "vorbis_setups.h" +#include "miniz.h" +#include "stb_ds.h" +#include "log.h" +#include +#include +#include + +// ===[ sample headers ]=== + +bool Fsb_parse(const uint8_t* h, size_t size, uint32_t* dataOffset, FsbSample** out) { + static const uint32_t rates[] = {4000, 8000, 11000, 11025, 16000, 22050, 24000, 32000, 44100, 48000, 96000}; + uint32_t count, headers, names, bytes, mode, i; + size_t pos = 60, nameBase, end; + FsbSample* samples = nullptr; + *out = nullptr; + if (size < 60 || memcmp(h, "FSB5", 4) || fm32(h + 4) != 1) return false; + count = fm32(h + 8); + headers = fm32(h + 12); + names = fm32(h + 16); + bytes = fm32(h + 20); + mode = fm32(h + 24); + if (count > 8192 || headers > size - 60 || names > size - 60 - headers || mode != 15) return false; + nameBase = 60 + headers; + end = nameBase + names; + if (end > UINT32_MAX || (names && count > names / 4)) return false; + for (i = 0; i < count; i++) { + uint64_t bits; + bool extra; + FsbSample s; + memset(&s, 0, sizeof(s)); + s.peak = 1; + if (pos + 8 > nameBase) goto bad; + bits = fm64(h + pos); + pos += 8; + extra = (bits & 1) != 0; + s.channels = (uint32_t) ((bits >> 5) & 1) + 1; + s.offset = (uint32_t) ((bits >> 6) & 0xfffffff) * 16; + s.frames = (uint32_t) (bits >> 34); + { + unsigned r = (unsigned) ((bits >> 1) & 15); + if (r >= sizeof(rates) / sizeof(rates[0])) goto bad; + s.rate = rates[r]; + } + while (extra) { + uint32_t info, n, type; + if (pos + 4 > nameBase) goto bad; + info = fm32(h + pos); + pos += 4; + extra = (info & 1) != 0; + n = (info >> 1) & 0xffffff; + type = info >> 25; + if (n > nameBase - pos) goto bad; + if (type == 1 && n == 1) s.channels = h[pos]; + if (type == 2 && n == 4) s.rate = fm32(h + pos); + if (type == 3 && n >= 8) { + s.loopStart = fm32(h + pos); + s.loopEnd = fm32(h + pos + 4) + 1; + } + if (type == 11 && n >= 4) s.setup = fm32(h + pos); + if (type == 13 && n == 4) s.peak = fmfloat(h + pos); + pos += n; + } + if (!s.setup || !s.channels || s.channels > 2 || !s.rate || s.offset > bytes) goto bad; + if (names) { + uint32_t off = fm32(h + nameBase + i * 4); + const uint8_t* nul; + if (off >= names || off < count * 4) goto bad; + nul = (const uint8_t*) memchr(h + nameBase + off, 0, names - off); + if (!nul) goto bad; + s.name = (char*) malloc((size_t) (nul - (h + nameBase + off)) + 1); + if (!s.name) goto bad; + memcpy(s.name, h + nameBase + off, (size_t) (nul - (h + nameBase + off)) + 1); + } + if (i && s.offset < samples[i - 1].offset) { + free(s.name); + goto bad; + } + arrput(samples, s); + } + if (pos != nameBase) goto bad; + for (i = 0; i < count; i++) { + samples[i].bytes = (i + 1 < count ? samples[i + 1].offset : bytes) - samples[i].offset; + } + *dataOffset = (uint32_t) end; + *out = samples; + return true; +bad: + Fsb_free(samples); + return false; +} + +// ===[ ogg pages ]=== + +// one complete packet per page. the decoder needs the granule positions for seeking. +static bool appendOggPage(uint8_t** out, const uint8_t* packet, size_t packetSize, uint64_t granule, uint32_t sequence, uint8_t flags) { + static uint32_t table[256]; + static bool ready = false; + size_t start = (size_t) arrlen(*out), segments = packetSize / 255 + 1, i, total = 27 + segments + packetSize; + uint8_t* p; + uint32_t crc = 0; + if (segments > 255 || total > INT_MAX || start > INT_MAX - total) return false; + arrsetlen(*out, start + total); + p = *out + start; + memset(p, 0, 27); + memcpy(p, "OggS", 4); + p[5] = flags; + fmput32(p + 6, (uint32_t) granule); + fmput32(p + 10, (uint32_t) (granule >> 32)); + fmput32(p + 14, 1); + fmput32(p + 18, sequence); + p[26] = (uint8_t) segments; + for (i = 0; i < segments; i++) { + p[27 + i] = (uint8_t) (i + 1 < segments ? 255 : packetSize % 255); + } + memcpy(p + 27 + segments, packet, packetSize); + if (!ready) { + unsigned j; + for (j = 0; j < 256; j++) { + uint32_t v = (uint32_t) j << 24; + int bit; + for (bit = 0; bit < 8; bit++) { + v = (v << 1) ^ ((v & 0x80000000) ? 0x04c11db7 : 0); + } + table[j] = v; + } + ready = true; + } + for (i = 0; i < total; i++) { + crc = (crc << 8) ^ table[((crc >> 24) ^ p[i]) & 255]; + } + fmput32(p + 22, crc); + return true; +} + +// ===[ vorbis setup packets ]=== + +static bool rebuildVorbisSetup(uint32_t key, uint8_t* out, size_t* bytes) { + // the stored fragments stop before the mapping and modes. these tails + // finish the packets; they match the complete setups in python-fsb5. + static const uint8_t stereoTail[] = { + 0x0c, 0, 0xf0, 0, 0, 0x90, 0x5c, 0, 0x11, 0x11, 0xd1, 0xcc, 0x61, 0x64, 0x68, 0x6c, + 0x70, 0x74, 0x78, 0x7c, 0x80, 0x84, 0x88, 0x8c, 0x90, 8, 0, 0, 0, 0, 0, 0x17, + 0, 0x7c, 0, 0, 0x24, 0x25, 0x40, 0x44, 0x44, 0x34, 0x73, 0x18, 0x19, 0x1a, 0x1b, 0x1c, + 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 1, 0, 0x80, 0, 2, 0, 0, 0, + 0, 0x20, 0x80, 0, 4, 4, 4, 0, 0, 0, 0, 0, 2, 0, 0, 0, 4, 4 + }; + static const uint8_t monoTail[] = { + 0x60, 0, 0, 0x0f, 0, 0, 0xc7, 5, 0x10, 0x11, 0xd1, 0x1c, 0x46, 0x86, 0xc6, 6, + 0x47, 0x87, 0xc7, 7, 0x48, 0x48, 0, 0, 0, 0, 0, 0xb8, 0, 0xc0, 7, 0, + 0xc0, 0x21, 2, 0x44, 0x44, 0x34, 0x87, 0x91, 0xa1, 0xb1, 0xc1, 0xd1, 0xe1, 0xf1, 1, 0x12, + 0x12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0, 2, 0, 0, 0, 4, 4 + }; + const char* text; + const char* alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + uint8_t compressed[4096]; + size_t n = 0; + unsigned accum = 0, bits = 0; + mz_ulong len = (mz_ulong) *bytes; + if (key == 0xc4c30a29) { + text = fmSetupStereo; + } else if (key == 0x355295ca) { + text = fmSetupMono; + } else { + logWarn("FMOD: Unsupported Vorbis setup CRC %08x\n", key); + return false; + } + for (; *text && *text != '='; text++) { + const char* a = strchr(alphabet, *text); + if (!a) return false; + accum = (accum << 6) | (unsigned) (a - alphabet); + bits += 6; + if (bits >= 8) { + bits -= 8; + if (n == sizeof(compressed)) return false; + compressed[n++] = (uint8_t) (accum >> bits); + } + } + if (mz_uncompress(out, &len, compressed, (mz_ulong) n) != MZ_OK) return false; + { + const uint8_t* tail = key == 0xc4c30a29 ? stereoTail : monoTail; + size_t tailSize = key == 0xc4c30a29 ? sizeof(stereoTail) : sizeof(monoTail); + if ((size_t) len > *bytes || tailSize > *bytes - (size_t) len) return false; + memcpy(out + len, tail, tailSize); + len += (mz_ulong) tailSize; + } + *bytes = (size_t) len; + return true; +} + +// ===[ sample reconstruction ]=== + +bool Fsb_rebuild(FsbSample* sample, const uint8_t* data, size_t bytes) { + uint8_t identification[30] = {1, 'v', 'o', 'r', 'b', 'i', 's'}, comment[16] = {3, 'v', 'o', 'r', 'b', 'i', 's'}; + uint8_t setup[8192], *ogg = nullptr; + size_t setupBytes = sizeof(setup), position = 0; + uint32_t sequence = 0, previousBlock = 0; + uint64_t granule = 0; + if (sample->ogg) return true; + if (!rebuildVorbisSetup(sample->setup, setup, &setupBytes)) return false; + identification[11] = (uint8_t) sample->channels; + fmput32(identification + 12, sample->rate); + identification[28] = 0xb8; + identification[29] = 1; + comment[15] = 1; + if (!appendOggPage(&ogg, identification, sizeof(identification), 0, sequence++, 2) || + !appendOggPage(&ogg, comment, sizeof(comment), 0, sequence++, 0) || + !appendOggPage(&ogg, setup, setupBytes, 0, sequence++, 0)) goto bad; + while (position + 2 <= bytes) { + uint32_t packetSize = fm16(data + position), blockFrames; + bool last; + position += 2; + if (!packetSize) break; + if (packetSize > bytes - position || (data[position] & 1)) goto bad; + // these two setups use modes 0/1 for blocks of 256/2048 samples. + blockFrames = (data[position] & 2) ? 2048 : 256; + if (previousBlock) granule += (previousBlock + blockFrames) / 4; + previousBlock = blockFrames; + last = position + packetSize + 2 > bytes || fm16(data + position + packetSize) == 0; + if (!appendOggPage(&ogg, data + position, packetSize, last ? sample->frames : granule, sequence++, last ? 4 : 0)) goto bad; + position += packetSize; + if (last) break; + } + if (sequence <= 3) goto bad; + sample->ogg = ogg; + sample->oggSize = (size_t) arrlen(ogg); + return true; +bad: + arrfree(ogg); + return false; +} + +void Fsb_free(FsbSample* samples) { + ptrdiff_t i; + for (i = 0; i < arrlen(samples); i++) { + free(samples[i].name); + arrfree(samples[i].ogg); + } + arrfree(samples); +} diff --git a/src/fmod/fsb.h b/src/fmod/fsb.h new file mode 100644 index 000000000..74ef5e5b9 --- /dev/null +++ b/src/fmod/fsb.h @@ -0,0 +1,21 @@ +#ifndef BS_FSB_H +#define BS_FSB_H +#include "common.h" +#include + +typedef struct { + uint32_t offset, bytes, frames, rate, channels, setup; + uint32_t loopStart, loopEnd; + float peak; + char* name; + uint8_t* ogg; + size_t oggSize; + unsigned references; + uint64_t lastUse; +} FsbSample; + +bool Fsb_parse(const uint8_t* header, size_t size, uint32_t* dataOffset, FsbSample** samples); +bool Fsb_rebuild(FsbSample* sample, const uint8_t* packets, size_t size); +void Fsb_free(FsbSample* samples); + +#endif diff --git a/src/fmod/vorbis_setups.h b/src/fmod/vorbis_setups.h new file mode 100644 index 000000000..45c340f3f --- /dev/null +++ b/src/fmod/vorbis_setups.h @@ -0,0 +1,80 @@ +/* generated by generate_setups.py from python-fsb5's lookup. + * the accompanying setup license contains the upstream copyright notice. + * these fragments are zlib-compressed, then base64-encoded. + * fsb.c adds the mapping and mode tails to finish each packet. */ +#ifndef BS_FMOD_VORBIS_SETUPS_H +#define BS_FMOD_VORBIS_SETUPS_H + +static const char fmSetupStereo[] = + "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" + "t3yY+0Lb+bXPTOFyl5h9aP4LVeM9Hw=="; + +static const char fmSetupMono[] = + "eJyFVn1QU1cWvyAfD6TsEwIGGlvSJpKH2CE0UR5qlwQj8CBKXniQj81QwbRoGlcJz+rssIPkhdKIDI0hYho7YHkWLNjGGizu6FiX" + "4AeUqpggRRZbK8HaVdvOGme6f+y8pLjdP3Z658yZ837vnHvu+d1z597oPX+ur9lueklaWBkBCgDgmdhZm2hTKgWnS8vT7nKkE4Yf" + "0qRly5o5uWXaMb6pg9vbLW3VCvZf6yAAE/COZC+LskogLsXfoLKZMzdAXKoV3cuitjMYLF1UMrTOZs6UQCuouzl5CeZ7eU85lAxV" + "weZMyUIG9Qfp507zAApxKZ64uNwcgy5wqIvZAmvzSM7nqVSUWNDW/F3O5ZvmpdJWp7lE/DSdelOyNzWcgycp7jDHSC6zqU0Slc0c" + "g37OosYkfelUFgq9SGWItqWADgC6jwmsxwTxcPqpeHAZALhAwbq0m3PlPc4VN2cp9Jw6GoAIAAHQXWztLW7/rtjGU9v4IZl/90T2" + "2KmQ5IbCiwEoVyTIdrNiQ3HbAIC686y0ykrXWQOt7RDVMhAJmgCIAgAWFSrXF9k3flVBTA0ZZ34jWoc+LRReAAB8Y/sb/TGtOUvS" + "YkL7AAHQtMyaM9K0SHITG0s8FjeIzrnudqsTnZgbXeEq9uyzvW8dRgMu08SVeZdwAg24jrnUCc64QTTgGulWx4etTPe+a84sxto9" + "cf9117JRNOBaObivw3nQgwZc0YPb252lbjTgKh7eh79fymCmiUX1vfc+x5XMWH3nhh3vX/OiAdeW0fv/cMHPXEIqRDKIAgVwy8Gt" + "txHoRqAuY9OymvTc+5LgQk5cutkl/f76Ql2vPR/id+7Gscww4ZGgAOJaWHjxoqpp5baw668iH3U3Y1n9fcfacseyPxsPqfwfBj5L" + "XJcgHIELVw2O4Eqj2K8yPEQDTvPpSo1h0INjCG3H40P8ZQAQJY1J29ahCE+KfOSqj8/qC9OZwWwNAFt+4lxftuK/8iPnq2V8RRJf" + "kbR6Mlc8qZQRpH7KQarPNGpvWXTBxtlg4+xTcqvKoVf/j4RLjwQgg4Kl4UKQUbZpHF3g2eHFMiEAwC5WUajfSk7xSm/wvryRveVn" + "0ZbkDQqWbFJEXE/RKdYaFWv0k2Kdco2OqDRWOYzaIXLmsH4m2Dj3G9HFhkpMAiDf9jJiewmx3c1r55UU1F3OAgUxTLNYvxW0382z" + "8Ups/O22N/c5+o447x/5IOtEyAXAjAszrMf3Wvv3Wvv2ti+02uqOOPr6nauuHv/h7MkHvydLQw3OA6CBV1TPK8J5MpxXVB9itymB" + "YZfzdyFPVp89dkMUrvF68oZJpayqYf9vNwCAhCufssauc+RJfB++mlCu9uEyn5L4vSGfFMmToefUccyBvaYxjQ8ebpcsDBy+LOjt" + "ntqMzvdOjQuaXQFYWNt1CRcWJow6JJQ9WlpYGQUUADQJLwtHOPUTJd/zZBNo1gp6FC3q//CmAnMf16zD6KEKw13XhzcVVwW0pvTj" + "FXSF4YQbgCYQC5rA0p5rHSkRBSwA9ifg8YjFXjOBBDrwq+g8p8aKfuSqnUAFR+1X0cyjh5IQ2lU7gQRc9omSedehGyUCADIuRICM" + "C1KBaJsMwJngzi6O/JBIzuLLk2VbWLqpZJmvU/SlSKyoIOQsua+CUKfKCQdf/qpcziKUyTIihVA65IRH/5XIOF1BzKYSviG99klj" + "lYOcfVU+zXzKCQ9Z5SB1Q8bZw8+mIqYqiJk1hG9IrGjQKQi9uoJQ79Gpp4gqf6OcRfiU8ukUgnAQ0z494SCrG4jZSt30FKH2N874" + "LdX+xot9J87uvHry5MenBn459covZ//5wF4yWcYsyLdRQcirPEYladQmy6ZSCJ9DpiT1hKOxmtRPT228TsqITj2TslOve1tfdUZG" + "+IzEnsa5Bp3uMKlTvaENWiZJUtepn6vSq4N63SU/ORcsmxyST/nISYKc85P3jhhnnsoIH0k4LNWd+rkpyzf7ds4u/0BBkLN2XXWV" + "Xr18Z/Xar8/vXP+JsXbmtVOfvJxQdP7TbN3MmrPn5V3k7MLzhd+sbRFNnjZOEuRsg676MDlb9+BseQWhq9JPB4mqJ43aoOWbJ41z" + "bGIqSKhTW3yOo1uDjfTDpZ3EkGXOTwa6jDrVX+8u/Pt8bgOprXtw1j1+/uT6v+1uaHktEoAYDgCRZYIllpYBCHwbBdiUq7BM0soy" + "w9JihOrAR9jUOIqkmXAp0m/B8QNscwe6km/ajAl66HKstIfySxG3Bde8Nehzag6IqRsaAe0pxw700+lag3hTm+Sy65ANzaTtoR/j" + "aNZRsz0fQmi7JhvxqtBMvrkdi+XSNqyMT7MLs9yd9ppsxHtL89awt8skoL027C0+rdKWoUNsUzuTPDOUnPbguKHThOdDiD4Ry0K8" + "KgwTmjYzUyVKMT7NVmJuC16zCvGpsB1ub5epVOgvxw/waXYpJhxim3a67/kldat7k2oF/NHU2pODnvRCJJzcl1o7eI5Jzh+1YQeZ" + "5AeHR9mmze57Ts3AsPeWwTj8L6fpWr9HoywbZlY1HK02PTxnvqYR0N5yrG3Qo9G2oe/8mtzq8XaZDO4n5Vgbn1aVYuIhtqFd/MSp" + "+djj7TLscM87TY+E87eUBvEZtmGz+57G9PDcvL82AoAliQBErLTvlxZWRjLnPoqLCWN6JK3CQK8IEs677FZhIKMXF85zKDw3kGG3" + "4YyFx9O9NtzQY8fxHYMANEUy5/55ET81ViDaxgcd8eD1XZyxn9YrRMT0nuwrP2+8LiJ8QytCiJzwGMfCCEnOhnwIUqcLblSICR+p" + "Vwc3hBH1k8bJihAyZKkWEVWkTu3Qz6l0IYTUsit9DGLUrm3RTpGzQctWNP/MzgdnH/1y8cd1lwB4HBULuiOfryzKQ9KXZodWFQEK" + "2OYOSTHXDOPFXBrGsTgA2LsAgHJ6Eljpp5JAayQA0Hs4hhy0M/ckY9H2pGdvs/+josHTLXng6R9vdSPuvIIe7qqIpl0AXGDTXfUJ" + "+XGIuA2N6bHb8R2DW9uEEN17U1PaaYKxkn7PrZoBl7kcxwa96bWxXNqvGXD7nesghE41DKC3ONj21aMq08S5YAduEPo1pkfnDvtr" + "BsSB22b44mlW7cnhJ3f+8ggZ0hi+3PA0gyp3+zSmx8xD74AweNv8eAPE/VMbGniBevxF7FYA3jYvARubD746wDua0h6znKkWigMZ" + "cI1VGHCZYSn0Ht6GBjgULBXw8RCWKIX42xYx2t4W9ouj7bZnGJ4b6A5jOBbCIMa6/wxDGH7CFv+ZRVMwxsQy3MJS5jUSZnkRsz3D" + "cHwRw7FfY+12vCQaAE4eAFFsSWYakbr1w7jlLzSF+rcAANZIpfLdo5Jx4Zv9XEjY1y/FkBZh4dWM44gynmvhmqzcyB5vZakXAABH" + "AQiKAzkF5TlLQEIi/FqsJLNg1+kIAEm3tRSLsqPeie0Ey4QpgLdS8F1kgcUr+9ob3ZB2p/mVh/mwFG6GR74F8cUXvohbu/7Hlszb" + "d6KOHsNjI5gL+z9XWORe"; + +#endif diff --git a/src/noop_audio_system.c b/src/noop_audio_system.c index 302d28528..5e6a53550 100644 --- a/src/noop_audio_system.c +++ b/src/noop_audio_system.c @@ -1,4 +1,5 @@ #include "noop_audio_system.h" +#include "fmod/fmod.h" #include "data_win.h" #include "stb_ds.h" @@ -10,6 +11,7 @@ static void noopInit(AudioSystem* audio, DataWin* dataWin, MAYBE_UNUSED FileSyst } static void noopDestroy(AudioSystem* audio) { + Fmod_audioDestroy(audio); free(audio->groupGains); arrfree(audio->audioGroups); free(audio); @@ -23,6 +25,10 @@ static int32_t noopPlaySound(MAYBE_UNUSED AudioSystem* audio, MAYBE_UNUSED int32 return -1; } +static int32_t noopPlayEncoded(MAYBE_UNUSED AudioSystem* audio, MAYBE_UNUSED const uint8_t* data, MAYBE_UNUSED size_t bytes, MAYBE_UNUSED bool loop) { + return -2; +} + static void noopSetSoundSpatial(MAYBE_UNUSED AudioSystem* audio, MAYBE_UNUSED int32_t instanceId, MAYBE_UNUSED float x, MAYBE_UNUSED float y, MAYBE_UNUSED float z, MAYBE_UNUSED float ref, MAYBE_UNUSED float max, MAYBE_UNUSED float factor) {} static void noopSetListenerPosition(MAYBE_UNUSED AudioSystem* audio, MAYBE_UNUSED float x, MAYBE_UNUSED float y, MAYBE_UNUSED float z) {} @@ -124,6 +130,7 @@ NoopAudioSystem* NoopAudioSystem_create(void) { noopVtable.groupIsLoaded = noopGroupIsLoaded, noopVtable.createStream = noopCreateStream, noopVtable.destroyStream = noopDestroyStream, + noopVtable.playEncoded = noopPlayEncoded, audio->base.vtable = &noopVtable; return audio; } diff --git a/src/runner.c b/src/runner.c index c2c2ee0a4..b8e0d065a 100644 --- a/src/runner.c +++ b/src/runner.c @@ -8,6 +8,7 @@ #include "json_writer.h" #include "collision.h" #include "video.h" +#include "fmod/fmod.h" #include #include "stdio_compat.h" @@ -2262,6 +2263,7 @@ static void cleanupState(Runner* runner) { // ===[ Public API ]=== void Runner_reset(Runner* runner) { + Fmod_audioDestroy(runner->audioSystem); // This actually sets the default runner values, used for initialization and restarting cleanupState(runner); @@ -4201,6 +4203,9 @@ static void dispatchSpriteMessages(Runner* runner, PendingSpriteMessage* pending } void Runner_step(Runner* runner) { + if (runner->audioSystem) { + Fmod_step(runner->audioSystem->fmodSystem, fmax(0.0, (double) runner->deltaTime / 1000000.0)); + } runner->fpsRealFrameStartNanos = nowNanos(); // The snapshot arena is stack-like and every push must be matched with a pop within the same frame. Assert that invariant at the top of each step: a non-zero length here means some site below pushed without popping, and we want a loud failure with the offending length so we can find it instead of silently leaking until the next frame. diff --git a/src/vm_builtins.c b/src/vm_builtins.c index 2a26c0c0d..fad85cf42 100644 --- a/src/vm_builtins.c +++ b/src/vm_builtins.c @@ -32,6 +32,7 @@ #include "collision.h" #include "ini.h" #include "audio_system.h" +#include "fmod/fmod.h" #include "file_system.h" #include "md5.h" #include "sha1.h" @@ -7573,6 +7574,249 @@ STUB_RETURN_UNDEFINED(steam_file_write) STUB_RETURN_UNDEFINED(steam_file_read) STUB_RETURN_ZERO(steam_get_persona_name) +// ===[ fmod extension functions ]=== + +#ifdef ENABLE_FMOD + +static FmodSystem* fmodGetSystem(VMContext* ctx) { + AudioSystem* audio = ctx->runner->audioSystem; + return audio != nullptr ? audio->fmodSystem : nullptr; +} + +static int32_t fmodHandleArgument(RValue value, bool roundHandle) { + double handle = RValue_toReal(value); + if (roundHandle) handle = round(handle); + return isfinite(handle) && handle > 0 && handle < 16777216 ? (int32_t) handle : 0; +} + +static const char* fmodStringArgument(RValue value) { + return value.type == RVALUE_STRING ? value.string : nullptr; +} + +static RValue builtin_fmod_init(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_init", 1, RValue_makeReal(0)); + AudioSystem* audio = ctx->runner->audioSystem; + double channels = RValue_toReal(args[0]); + if (audio == nullptr || !isfinite(channels) || channels < 1 || channels > 4096) return RValue_makeReal(0); + Fmod_audioDestroy(audio); + audio->fmodSystem = Fmod_create(audio, ctx->runner->fileSystem, (int32_t) channels); + if (audio->fmodSystem != nullptr) logInfo("FMOD initiated! :3\n"); + return RValue_makeReal(audio->fmodSystem != nullptr); +} + +static RValue builtin_fmod_destroy(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + AudioSystem* audio = ctx->runner->audioSystem; + if (audio == nullptr) return RValue_makeReal(0); + Fmod_audioDestroy(audio); + return RValue_makeReal(1); +} + +static RValue builtin_fmod_update(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + FmodSystem* system = fmodGetSystem(ctx); + if (system == nullptr) return RValue_makeReal(0); + // the runner already advances time each step. don't count it twice here. + Fmod_step(system, 0); + return RValue_makeReal(1); +} + +static RValue builtin_fmod_bank_load(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_bank_load", 2, RValue_makeReal(0)); + const char* path = fmodStringArgument(args[0]); + return RValue_makeReal(Fmod_loadBank(fmodGetSystem(ctx), path, RValue_toReal(args[1]) != 0)); +} + +static RValue builtin_fmod_bank_load_sample_data(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_bank_load_sample_data", 1, RValue_makeReal(0)); + return RValue_makeReal(Fmod_loadSamples(fmodGetSystem(ctx), fmodStringArgument(args[0]))); +} + +static RValue builtin_fmod_event_load(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_load", 1, RValue_makeReal(0)); + const char* path = fmodStringArgument(args[0]); + if (path == nullptr) return RValue_makeReal(0); + return RValue_makeReal(Fmod_eventExists(fmodGetSystem(ctx), path) ? 1 : -4); +} + +static RValue builtin_fmod_event_create_instance(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_create_instance", 1, RValue_makeReal(-4)); + return RValue_makeReal(Fmod_createInstance(fmodGetSystem(ctx), fmodStringArgument(args[0]))); +} + +static RValue builtin_fmod_event_instance_play(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_instance_play", 1, RValue_makeReal(0)); + return RValue_makeReal(Fmod_play(fmodGetSystem(ctx), fmodHandleArgument(args[0], false))); +} + +static RValue builtin_fmod_event_instance_stop(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_instance_stop", 2, RValue_makeReal(0)); + int32_t handle = fmodHandleArgument(args[0], false); + Fmod_stop(fmodGetSystem(ctx), handle, RValue_toReal(args[1]) != 0); + // the original wrapper returns zero here, including for an unknown handle. + return RValue_makeReal(0); +} + +static RValue builtin_fmod_event_instance_release(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_instance_release", 1, RValue_makeReal(0)); + // release rounds the handle. the other instance calls truncate it. + return RValue_makeReal(Fmod_release(fmodGetSystem(ctx), fmodHandleArgument(args[0], true))); +} + +static RValue builtin_fmod_event_instance_is_playing(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_instance_is_playing", 1, RValue_makeReal(0)); + return RValue_makeReal(Fmod_isPlaying(fmodGetSystem(ctx), fmodHandleArgument(args[0], false))); +} + +static RValue builtin_fmod_event_instance_set_paused(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_instance_set_paused", 2, RValue_makeReal(0)); + int32_t handle = fmodHandleArgument(args[0], false); + return RValue_makeReal(Fmod_pause(fmodGetSystem(ctx), handle, RValue_toReal(args[1]) != 0)); +} + +static RValue builtin_fmod_event_instance_get_paused(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_instance_get_paused", 1, RValue_makeReal(0)); + return RValue_makeReal(Fmod_getPaused(fmodGetSystem(ctx), fmodHandleArgument(args[0], false))); +} + +static RValue builtin_fmod_event_instance_set_paused_all(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_instance_set_paused_all", 1, RValue_makeReal(0)); + FmodSystem* system = fmodGetSystem(ctx); + if (system == nullptr) return RValue_makeReal(0); + Fmod_pauseAll(system, RValue_toReal(args[0]) != 0); + return RValue_makeReal(1); +} + +static RValue builtin_fmod_event_instance_set_timeline_pos(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_instance_set_timeline_pos", 2, RValue_makeReal(0)); + int32_t handle = fmodHandleArgument(args[0], false); + return RValue_makeReal(Fmod_setPosition(fmodGetSystem(ctx), handle, RValue_toReal(args[1]))); +} + +static RValue builtin_fmod_event_instance_get_timeline_pos(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_instance_get_timeline_pos", 1, RValue_makeReal(0)); + return RValue_makeReal((GMLReal) Fmod_getPosition(fmodGetSystem(ctx), fmodHandleArgument(args[0], false))); +} + +static RValue builtin_fmod_event_get_length(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_get_length", 1, RValue_makeReal(-4)); + return RValue_makeReal((GMLReal) Fmod_getLength(fmodGetSystem(ctx), fmodStringArgument(args[0]))); +} + +static RValue builtin_fmod_event_instance_set_parameter(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_instance_set_parameter", 4, RValue_makeReal(0)); + int32_t handle = fmodHandleArgument(args[0], false); + if (handle == 0) return RValue_makeReal(0); + const char* name = fmodStringArgument(args[1]); + return RValue_makeReal(Fmod_setParameter(fmodGetSystem(ctx), handle, name, (float) RValue_toReal(args[2]), RValue_toReal(args[3]) != 0)); +} + +static RValue builtin_fmod_event_instance_get_parameter(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_instance_get_parameter", 2, RValue_makeReal(0)); + int32_t handle = fmodHandleArgument(args[0], false); + if (handle == 0) return RValue_makeReal(0); + return RValue_makeReal(Fmod_getParameter(fmodGetSystem(ctx), handle, fmodStringArgument(args[1]))); +} + +static RValue builtin_fmod_set_parameter(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_set_parameter", 3, RValue_makeReal(0)); + const char* name = fmodStringArgument(args[0]); + return RValue_makeReal(Fmod_setParameter(fmodGetSystem(ctx), 0, name, (float) RValue_toReal(args[1]), RValue_toReal(args[2]) != 0)); +} + +static RValue builtin_fmod_get_parameter(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_get_parameter", 1, RValue_makeReal(0)); + return RValue_makeReal(Fmod_getParameter(fmodGetSystem(ctx), 0, fmodStringArgument(args[0]))); +} + +static RValue builtin_fmod_event_instance_set_3d_attributes(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_instance_set_3d_attributes", 3, RValue_makeReal(0)); + int32_t handle = fmodHandleArgument(args[0], false); + return RValue_makeReal(Fmod_setSpatial(fmodGetSystem(ctx), handle, (float) RValue_toReal(args[1]), (float) RValue_toReal(args[2]))); +} + +static RValue builtin_fmod_set_listener_attributes(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_set_listener_attributes", 3, RValue_makeReal(0)); + double listener = RValue_toReal(args[0]); + if (!isfinite(listener) || listener < 0 || listener >= 8) return RValue_makeReal(0); + return RValue_makeReal(Fmod_setListener(fmodGetSystem(ctx), (int32_t) listener, (float) RValue_toReal(args[1]), (float) RValue_toReal(args[2]))); +} + +static RValue builtin_fmod_set_num_listeners(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_set_num_listeners", 1, RValue_makeReal(0)); + double count = RValue_toReal(args[0]); + if (!isfinite(count) || count > 8) return RValue_makeReal(0); + return RValue_makeReal(Fmod_setListeners(fmodGetSystem(ctx), count < 1 ? 1 : (int32_t) count)); +} + +static RValue builtin_fmod_event_one_shot(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_one_shot", 1, RValue_makeReal(0)); + return RValue_makeReal(Fmod_oneShot(fmodGetSystem(ctx), fmodStringArgument(args[0]), false, 0, 0)); +} + +static RValue builtin_fmod_event_one_shot_3d(VMContext* ctx, RValue* args, int32_t argCount) { + REQUIRE_ARGC_AT_LEAST("fmod_event_one_shot_3d", 3, RValue_makeReal(0)); + const char* path = fmodStringArgument(args[0]); + return RValue_makeReal(Fmod_oneShot(fmodGetSystem(ctx), path, true, (float) RValue_toReal(args[1]), (float) RValue_toReal(args[2]))); +} + +static void registerFmodBuiltins(VMContext* ctx) { + // extensions can rename the gml function while keeping the same dll symbol. + static const struct { + const char* name; + const char* symbol; + BuiltinFunc function; + } bindings[] = { + {"fmod_init", "FMOD_Init", builtin_fmod_init}, + {"fmod_destroy", "FMOD_Destroy", builtin_fmod_destroy}, + {"fmod_bank_load", "FMOD_Bank_Load", builtin_fmod_bank_load}, + {"fmod_event_load", "FMOD_Event_Load", builtin_fmod_event_load}, + {"fmod_update", "FMOD_Update", builtin_fmod_update}, + {"fmod_event_create_instance", "FMOD_Event_CreateInstance", builtin_fmod_event_create_instance}, + {"fmod_event_instance_play", "FMOD_EventInstance_Play", builtin_fmod_event_instance_play}, + {"fmod_event_instance_stop", "FMOD_EventInstance_Stop", builtin_fmod_event_instance_stop}, + {"fmod_event_instance_release", "FMOD_EventInstance_Release", builtin_fmod_event_instance_release}, + {"fmod_event_instance_set_3d_attributes", "FMOD_EventInstance_Set3DAttributes", builtin_fmod_event_instance_set_3d_attributes}, + {"fmod_set_listener_attributes", "FMOD_SetListenerAttributes", builtin_fmod_set_listener_attributes}, + {"fmod_set_num_listeners", "FMOD_SetNumListeners", builtin_fmod_set_num_listeners}, + {"fmod_event_instance_set_parameter", "FMOD_EventInstance_SetParameter", builtin_fmod_event_instance_set_parameter}, + {"fmod_event_instance_get_parameter", "FMOD_EventInstance_GetParameter", builtin_fmod_event_instance_get_parameter}, + {"fmod_set_parameter", "FMOD_SetParameter", builtin_fmod_set_parameter}, + {"fmod_get_parameter", "FMOD_GetParameter", builtin_fmod_get_parameter}, + {"fmod_event_instance_set_paused", "FMOD_EventInstance_SetPaused", builtin_fmod_event_instance_set_paused}, + {"fmod_event_instance_get_paused", "FMOD_EventInstance_GetPaused", builtin_fmod_event_instance_get_paused}, + {"fmod_event_instance_set_paused_all", "FMOD_EventInstance_SetPaused_All", builtin_fmod_event_instance_set_paused_all}, + {"fmod_event_one_shot", "FMOD_Event_OneShot", builtin_fmod_event_one_shot}, + {"fmod_event_one_shot_3d", "FMOD_Event_OneShot_3D", builtin_fmod_event_one_shot_3d}, + {"fmod_event_instance_is_playing", "FMOD_EventInstance_IsPlaying", builtin_fmod_event_instance_is_playing}, + {"fmod_event_instance_get_timeline_pos", "FMOD_EventInstance_GetTimelinePosition", builtin_fmod_event_instance_get_timeline_pos}, + {"fmod_event_instance_set_timeline_pos", "FMOD_EventInstance_SetTimelinePosition", builtin_fmod_event_instance_set_timeline_pos}, + {"fmod_bank_load_sample_data", "FMOD_Bank_LoadSampleData", builtin_fmod_bank_load_sample_data}, + {"fmod_event_get_length", "FMOD_Event_GetLength", builtin_fmod_event_get_length}, + }; + + for (size_t i = 0; i < sizeof(bindings) / sizeof(bindings[0]); i++) { + VM_registerBuiltin(ctx, bindings[i].name, bindings[i].function); + VM_registerBuiltin(ctx, bindings[i].symbol, bindings[i].function); + } + repeat(ctx->dataWin->extn.count, e) { + Extension* extension = &ctx->dataWin->extn.extensions[e]; + repeat(extension->fileCount, f) { + ExtensionFile* file = &extension->files[f]; + repeat(file->functionCount, g) { + ExtensionFunction* function = &file->functions[g]; + if (function->name == nullptr || function->extName == nullptr || VM_findBuiltin(ctx, function->name)) continue; + for (size_t i = 0; i < sizeof(bindings) / sizeof(bindings[0]); i++) { + if (strcmp(function->extName, bindings[i].symbol) == 0) { + VM_registerBuiltin(ctx, function->name, bindings[i].function); + break; + } + } + } + } + } +} + +#endif + // ===[ Audio Built-in Functions ]=== static GMLReal audioPositiveMinimum(void) { @@ -23042,6 +23286,9 @@ static RValue builtin_physics_raycast(VMContext* ctx, RValue* args, int32_t coun void VMBuiltins_registerAll(VMContext* ctx) { requireMessage(!ctx->registeredBuiltinFunctions, "Attempting to register all VMBuiltins, but it was already registered!"); ctx->registeredBuiltinFunctions = true; +#ifdef ENABLE_FMOD + registerFmodBuiltins(ctx); +#endif // Physics #define PHYSICS_FUNCTION(name, scope, minimum, returnsReal) VM_registerBuiltin(ctx, #name, builtin_##name); From b87887534204975e604f09f2cc3102f762ef3bab Mon Sep 17 00:00:00 2001 From: "casriel." <232106036+casrielasriel@users.noreply.github.com> Date: Fri, 9 Oct 2026 06:25:09 -0700 Subject: [PATCH 2/3] cmakelists --- CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index b44b96de7..8c6b83d5b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,6 +121,7 @@ endif() option(ENABLE_MODERN_GL "Enable the modern OpenGL renderer" ON) option(ENABLE_NOOP_RENDERER "Enable the no-op renderer (headless stub, no drawing)" OFF) option(ENABLE_PHYSICS "Enable Box2D physics support" ON) +option(ENABLE_FMOD "Enable standalone FMOD bank/event compatibility" ON) # Noop renderer is exclusive to the noop desktop backend (no window, no GL). # Auto-enable for BACKEND=noop and reject otherwise. if(BACKEND STREQUAL "noop") @@ -155,8 +156,14 @@ endif() if(ENABLE_PHYSICS) add_compile_definitions(ENABLE_PHYSICS) endif() +if(ENABLE_FMOD) + add_compile_definitions(ENABLE_FMOD) +endif() file(GLOB SOURCES src/*.c) +if(ENABLE_FMOD) + list(APPEND SOURCES src/fmod/fmod.c src/fmod/fmod_bank.c src/fmod/fsb.c src/fmod/fmod_dsp.c) +endif() if(ENABLE_PHYSICS) file(GLOB PHYSICS_SOURCES CONFIGURE_DEPENDS src/physics/*.c) else() From 241ea9fdbb9ee7fc8804f27fbdae1c9c78e1ff48 Mon Sep 17 00:00:00 2001 From: "casriel." <232106036+casrielasriel@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:19:49 -0700 Subject: [PATCH 3/3] fix building --- src/fmod/fmod.c | 35 ++++++++++++++++++----------------- src/fmod/fmod_internal.h | 3 ++- src/runner.c | 7 +++++-- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/fmod/fmod.c b/src/fmod/fmod.c index fe3ed9a99..0287a7a80 100644 --- a/src/fmod/fmod.c +++ b/src/fmod/fmod.c @@ -19,8 +19,9 @@ static float interpolateCurve(float left, float right, double position, float cu // a controller uses large values, such as filter frequencies. if (weight <= 0) return left; if (weight >= 1) return right; - double a = 2.0 * left, b = 2.0 * right, maximum = fmax(a, b); - return (float) ((maximum + log2((1 - weight) * exp2(a - maximum) + weight * exp2(b - maximum))) / 2); + double a = 2 * (double) left, b = 2 * (double) right, maximum = fmax(a, b); + double blend = (double) weight; + return (float) ((maximum + log2((1 - blend) * exp2(a - maximum) + blend * exp2(b - maximum))) / 2); } if (mode == FM_CURVE_EXPONENTIAL) { weight = exponentialWeight(weight, curvature); @@ -76,8 +77,8 @@ static float parameterValue(FmodSystem* system, EventInstance* instance, int ind static float advanceParameter(float current, float target, const Parameter* parameter, double seconds) { float speed = target >= current ? parameter->seekUp : parameter->seekDown; if (speed == 0) return target; - double distance = seconds * speed; - return target >= current ? (float) fmin(target, current + distance) : (float) fmax(target, current - distance); + double distance = seconds * (double) speed; + return target >= current ? (float) fmin((double) target, (double) current + distance) : (float) fmax((double) target, (double) current - distance); } static int findParameter(FmodSystem* system, EventInstance* instance, const char* name) { @@ -151,11 +152,11 @@ static float evaluateCurve(FmodSystem* system, EventInstance* instance, Record* uint32_t count, stride; const uint8_t* points = fmlist(&reader, &count, &stride); if (reader.failed || !count || stride < 8) return 0; - double previousX = parameterInput ? fmfloat(points) : fm32(points) / FM_TICKS; + double previousX = parameterInput ? (double) fmfloat(points) : fm32(points) / FM_TICKS; float previousY = fmfloat(points + 4); if (input < previousX) return previousY; for (uint32_t i = 1; i < count; i++) { - double x = parameterInput ? fmfloat(points + i * stride) : fm32(points + i * stride) / FM_TICKS; + double x = parameterInput ? (double) fmfloat(points + i * stride) : fm32(points + i * stride) / FM_TICKS; float y = fmfloat(points + i * stride + 4); if (input < x) { const uint8_t* previous = points + (i - 1) * stride; @@ -203,16 +204,16 @@ static float randomModulation(FmodSystem* system, Clip* clip, Record* modulator) for (ptrdiff_t i = 0; i < arrlen(clip->modulation); i++) { if (clip->modulation[i].modulator == modulator) return clip->modulation[i].value; } - ModulationValue value = {modulator, (float) ((nextRandom(system) / (double) UINT32_MAX * 2 - 1) * amount / 100)}; + ModulationValue value = {modulator, (float) ((nextRandom(system) / (double) UINT32_MAX * 2 - 1) * (double) amount / 100)}; arrput(clip->modulation, value); return value.value; } static float envelopeValue(FmodSystem* system, Record* envelope, double age) { const uint8_t* data = envelope->data; - double attack = fmfloat(data + FM_ENVELOPE_ATTACK) / 1000.0; - double hold = fmfloat(data + FM_ENVELOPE_HOLD) / 1000.0; - double decay = fmfloat(data + FM_ENVELOPE_DECAY) / 1000.0; + double attack = (double) fmfloat(data + FM_ENVELOPE_ATTACK) / 1000; + double hold = (double) fmfloat(data + FM_ENVELOPE_HOLD) / 1000; + double decay = (double) fmfloat(data + FM_ENVELOPE_DECAY) / 1000; float initial = mapProperty(system, volumeProperty, fmfloat(data + FM_ENVELOPE_INITIAL), true); float peak = mapProperty(system, volumeProperty, fmfloat(data + FM_ENVELOPE_PEAK), true); float sustain = mapProperty(system, volumeProperty, fmfloat(data + FM_ENVELOPE_SUSTAIN), true); @@ -231,7 +232,7 @@ static float evaluateEnvelope(FmodSystem* system, EventInstance* instance, Clip* for (size_t offset = FM_ENVELOPE_INITIAL; offset < FM_ENVELOPE_SIZE; offset += 4) { if (!isfinite(fmfloat(envelope->data + offset))) return 0; } - double release = fmax(0, fmfloat(envelope->data + FM_ENVELOPE_RELEASE) / 1000.0); + double release = fmax(0, (double) fmfloat(envelope->data + FM_ENVELOPE_RELEASE) / 1000); if (releaseDuration && release > *releaseDuration) *releaseDuration = release; double activated = clip->activated; if (!tagIs(owner, "INST")) { @@ -798,7 +799,7 @@ static void updateSustain(FmodSystem* system, EventInstance* instance, double pr double time = fm32(point) / FM_TICKS; uint32_t bytes = fm32(point + 4); if (bytes > entryBytes - 8) return; - if (previous <= time + 0.000001 && instance->position >= time && parameterConditions(system, instance, point + 8, bytes)) { + if (previous <= time + FM_TIME_EPSILON && instance->position >= time && parameterConditions(system, instance, point + 8, bytes)) { instance->position = time; instance->held = true; return; @@ -815,7 +816,7 @@ static void reposition(FmodSystem* system, EventInstance* instance, double posit clip->finished = clip->started = false; clip->virtualized = false; if (position >= clip->start && position < clip->end && (clip->voice >= 0 || virtualized)) { - clip->sourcePosition = (position - clip->start) * clipPitch(system, instance, clip); + clip->sourcePosition = (position - clip->start) * (double) clipPitch(system, instance, clip); if (clip->voice >= 0) { system->audio->vtable->setTrackPosition(system->audio, clip->voice, (float) clip->sourcePosition); if (instance->playing && !instance->paused) system->audio->vtable->resumeSound(system->audio, clip->voice); @@ -905,7 +906,7 @@ static bool startClip(FmodSystem* system, EventInstance* instance, Clip* clip) { if (!clip->started) { clip->activated = instance->elapsed; clip->lastPitch = clipPitch(system, instance, clip); - clip->sourcePosition = fmax(0, instance->position - clip->start) * clip->lastPitch; + clip->sourcePosition = fmax(0, instance->position - clip->start) * (double) clip->lastPitch; } clip->started = true; clip->virtualized = true; @@ -931,12 +932,12 @@ static bool startClip(FmodSystem* system, EventInstance* instance, Clip* clip) { } static bool updateClip(FmodSystem* system, EventInstance* instance, Clip* clip, double seconds) { - if (clip->started && !instance->paused) clip->sourcePosition += seconds * clip->lastPitch; + if (clip->started && !instance->paused) clip->sourcePosition += seconds * (double) clip->lastPitch; if (instance->stopping && !clip->started) { clip->finished = true; return false; } - if (instance->held && !clip->started && clip->start >= instance->position - 0.000001) return true; + if (instance->held && !clip->started && clip->start >= instance->position - FM_TIME_EPSILON) return true; if (instance->position < clip->start) return true; if (instance->position >= clip->end) { stopClip(system, clip); @@ -981,7 +982,7 @@ static void stepPlayback(FmodSystem* system, EventInstance* instance, double sec child->finished = true; continue; } - if (instance->position < child->start || (instance->held && !child->playback && child->start >= instance->position - 0.000001)) { + if (instance->position < child->start || (instance->held && !child->playback && child->start >= instance->position - FM_TIME_EPSILON)) { pending = true; continue; } diff --git a/src/fmod/fmod_internal.h b/src/fmod/fmod_internal.h index 81f0a67aa..072e98a32 100644 --- a/src/fmod/fmod_internal.h +++ b/src/fmod/fmod_internal.h @@ -7,7 +7,8 @@ #include "stb_ds.h" #include -#define FM_TICKS 48000.0 +#define FM_TICKS ((double) 48000) +#define FM_TIME_EPSILON ((double) 0.000001L) #define FM_CACHE_LIMIT (64u * 1024u * 1024u) #define FM_MAX_METADATA (16u * 1024u * 1024u) #define FM_MAX_RECORDS 262144 diff --git a/src/runner.c b/src/runner.c index b8e0d065a..934ea0678 100644 --- a/src/runner.c +++ b/src/runner.c @@ -4203,9 +4203,12 @@ static void dispatchSpriteMessages(Runner* runner, PendingSpriteMessage* pending } void Runner_step(Runner* runner) { - if (runner->audioSystem) { - Fmod_step(runner->audioSystem->fmodSystem, fmax(0.0, (double) runner->deltaTime / 1000000.0)); +#ifdef ENABLE_FMOD + if (runner->audioSystem && runner->audioSystem->fmodSystem) { + double seconds = (double) runner->deltaTime / 1000000; + Fmod_step(runner->audioSystem->fmodSystem, seconds > 0 ? seconds : 0); } +#endif runner->fpsRealFrameStartNanos = nowNanos(); // The snapshot arena is stack-like and every push must be matched with a pop within the same frame. Assert that invariant at the top of each step: a non-zero length here means some site below pushed without popping, and we want a loud failure with the offending length so we can find it instead of silently leaking until the next frame.