From 801a8ad1bd7b1a4a7710b1a114b89aee0c51c0f0 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 15 Apr 2026 18:23:34 +0200 Subject: [PATCH] fase 3: port de jail_audio header-only amb streaming i sense SDL_AddTimer --- CLAUDE.md | 2 +- source/core/jail/jail_audio.cpp | 456 +------------------------ source/core/jail/jail_audio.hpp | 578 ++++++++++++++++++++++++++++++-- source/core/system/director.cpp | 5 + 4 files changed, 557 insertions(+), 484 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4a252a6..515471a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -216,7 +216,7 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text 3. **No sound effects in game**: Game code never calls `JA_PlaySound*`/`JA_LoadSound` — only music via `JA_PlayMusic`/`JA_FadeOutMusic`. The SONS and VOL SONS menu items control volume of an empty channel pool. Infrastructure ready for future SFX. 4. **Raw `malloc`/`free` in gameplay structs**: `Sprite`/`Entitat` use `malloc` for `Frame[]` and `Animacio[]`; `jfile.cpp` uses a global `scratch[255]` buffer (UB under concurrent calls); `jail_audio.cpp` mixes `new`/`malloc`/`SDL_malloc`. Scheduled for Phase 1 (RAII pass). 5. **Blocking loops in cinematics and fades**: `ModuleSequence::doIntro()` has 15+ `while(!JG_ShouldUpdate())` spin-waits; `JD8_FadeOut`/`JD8_FadeToPal` run 32 internal iterations calling `JD8_Flip`. Incompatible with `SDL_AppIterate`. Scheduled for Phase 2 (state-machine refactor). -6. **`SDL_AddTimer` in audio**: `JA_Init` registers a 30ms timer callback for mixing/fade update. Incompatible with emscripten single-threaded model. Scheduled for Phase 3 (manual `JA_Update(delta_ms)` driven from Director). +6. ~~**`SDL_AddTimer` in audio**~~: Fixed in Phase 3. `jail_audio` is now a header-only `inline` module (single `.cpp` stub only hosts the `stb_vorbis` implementation to avoid multiple definitions). Music uses true streaming via `stb_vorbis_open_memory` + `JA_PumpMusic` with a 0.5s low-water-mark instead of decoding the whole OGG into RAM. Mixing/fade update runs manually via `JA_Update()` called once per frame from `Director::run()`. Ported from the `jaildoctors_dilemma` codebase. 7. **Game thread + `publishFrame` mutex/cv**: the emulator-style architecture is only tenable while native. Incompatible with `SDL_AppIterate`. Scheduled for Phase 5 (single-threaded state machine). ### Pending / Ideas for Later diff --git a/source/core/jail/jail_audio.cpp b/source/core/jail/jail_audio.cpp index 1804058..87176fc 100644 --- a/source/core/jail/jail_audio.cpp +++ b/source/core/jail/jail_audio.cpp @@ -1,450 +1,12 @@ -#ifndef JA_USESDLMIXER -#include "core/jail/jail_audio.hpp" - -#include -#include +// Aquest fitxer existeix per a albergar la implementació completa de +// stb_vorbis en una única unitat de compilació. Totes les funcions JA_* +// viuen `inline` a jail_audio.hpp (header-only, inspirat en el motor de +// jaildoctors_dilemma). Sense aquest stub tindríem múltiples definicions +// de les funcions de stb_vorbis si més d'un .cpp inclou el header. +// clang-format off +#undef STB_VORBIS_HEADER_ONLY #include "external/stb_vorbis.h" +// clang-format on -#define JA_MAX_SIMULTANEOUS_CHANNELS 5 - -struct JA_Sound_t { - SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000}; - Uint32 length{0}; - Uint8* buffer{NULL}; -}; - -struct JA_Channel_t { - JA_Sound_t* sound{nullptr}; - int pos{0}; - int times{0}; - SDL_AudioStream* stream{nullptr}; - JA_Channel_state state{JA_CHANNEL_FREE}; -}; - -struct JA_Music_t { - SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000}; - Uint32 length{0}; - Uint8* buffer{nullptr}; - char* filename{nullptr}; - - int pos{0}; - int times{0}; - SDL_AudioStream* stream{nullptr}; - JA_Music_state state{JA_MUSIC_INVALID}; -}; - -JA_Music_t* current_music{nullptr}; -JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS]; - -SDL_AudioSpec JA_audioSpec{SDL_AUDIO_S16, 2, 48000}; -float JA_musicVolume{1.0f}; -float JA_soundVolume{0.5f}; -bool JA_musicEnabled{true}; -bool JA_soundEnabled{true}; -SDL_AudioDeviceID sdlAudioDevice{0}; -SDL_TimerID JA_timerID{0}; - -bool fading = false; -int fade_start_time; -int fade_duration; -int fade_initial_volume; - -/* -void audioCallback(void * userdata, uint8_t * stream, int len) { - SDL_memset(stream, 0, len); - if (current_music != NULL && current_music->state == JA_MUSIC_PLAYING) { - const int size = SDL_min(len, current_music->samples*2-current_music->pos); - SDL_MixAudioFormat(stream, (Uint8*)(current_music->output+current_music->pos), AUDIO_S16, size, JA_musicVolume); - current_music->pos += size/2; - if (size < len) { - if (current_music->times != 0) { - SDL_MixAudioFormat(stream+size, (Uint8*)current_music->output, AUDIO_S16, len-size, JA_musicVolume); - current_music->pos = (len-size)/2; - if (current_music->times > 0) current_music->times--; - } else { - current_music->pos = 0; - current_music->state = JA_MUSIC_STOPPED; - } - } - } - // Mixar els channels mi amol - for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) { - if (channels[i].state == JA_CHANNEL_PLAYING) { - const int size = SDL_min(len, channels[i].sound->length - channels[i].pos); - SDL_MixAudioFormat(stream, channels[i].sound->buffer + channels[i].pos, AUDIO_S16, size, JA_soundVolume); - channels[i].pos += size; - if (size < len) { - if (channels[i].times != 0) { - SDL_MixAudioFormat(stream + size, channels[i].sound->buffer, AUDIO_S16, len-size, JA_soundVolume); - channels[i].pos = len-size; - if (channels[i].times > 0) channels[i].times--; - } else { - JA_StopChannel(i); - } - } - } - } -} -*/ - -Uint32 JA_UpdateCallback(void* userdata, SDL_TimerID timerID, Uint32 interval) { - if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) { - if (fading) { - int time = SDL_GetTicks(); - if (time > (fade_start_time + fade_duration)) { - fading = false; - JA_StopMusic(); - return 30; - } else { - const int time_passed = time - fade_start_time; - const float percent = (float)time_passed / (float)fade_duration; - SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0 - percent)); - } - } - - if (current_music->times != 0) { - if (SDL_GetAudioStreamAvailable(current_music->stream) < int(current_music->length / 2)) { - SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length); - } - if (current_music->times > 0) current_music->times--; - } else { - if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic(); - } - } - - if (JA_soundEnabled) { - for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) - if (channels[i].state == JA_CHANNEL_PLAYING) { - if (channels[i].times != 0) { - if (SDL_GetAudioStreamAvailable(channels[i].stream) < int(channels[i].sound->length / 2)) { - SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length); - if (channels[i].times > 0) channels[i].times--; - } - } else { - if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i); - } - } - } - - return 30; -} - -void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) { -#ifdef DEBUG - SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG); -#endif - - SDL_Log("Iniciant JailAudio..."); - JA_audioSpec = {format, num_channels, freq}; - if (!sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); - sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec); - SDL_Log((sdlAudioDevice == 0) ? "Failed to initialize SDL audio!\n" : "OK!\n"); - for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE; - // SDL_PauseAudioDevice(sdlAudioDevice); - JA_timerID = SDL_AddTimer(30, JA_UpdateCallback, nullptr); -} - -void JA_Quit() { - if (JA_timerID) SDL_RemoveTimer(JA_timerID); - - if (!sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); - sdlAudioDevice = 0; -} - -JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename) { - JA_Music_t* music = new JA_Music_t(); - - int chan, samplerate; - short* output; - music->length = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2; - - music->spec.channels = chan; - music->spec.freq = samplerate; - music->spec.format = SDL_AUDIO_S16; - music->buffer = (Uint8*)SDL_malloc(music->length); - SDL_memcpy(music->buffer, output, music->length); - free(output); - music->pos = 0; - music->state = JA_MUSIC_STOPPED; - if (filename) { - music->filename = (char*)malloc(strlen(filename) + 1); - strcpy(music->filename, filename); - } - - return music; -} - -JA_Music_t* JA_LoadMusic(const char* filename) { - // [RZC 28/08/22] Carreguem primer el arxiu en memòria i després el descomprimim. Es algo més rapid. - FILE* f = fopen(filename, "rb"); - fseek(f, 0, SEEK_END); - long fsize = ftell(f); - fseek(f, 0, SEEK_SET); - Uint8* buffer = (Uint8*)malloc(fsize + 1); - if (fread(buffer, fsize, 1, f) != 1) return NULL; - fclose(f); - - JA_Music_t* music = JA_LoadMusic(buffer, fsize, filename); - - free(buffer); - - return music; -} - -void JA_PlayMusic(JA_Music_t* music, const int loop) { - if (!JA_musicEnabled) return; - - JA_StopMusic(); - - current_music = music; - current_music->pos = 0; - current_music->state = JA_MUSIC_PLAYING; - current_music->times = loop; - - current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec); - if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n"); - SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume); - if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n"); - // SDL_ResumeAudioStreamDevice(current_music->stream); -} - -char* JA_GetMusicFilename(JA_Music_t* music) { - if (!music) music = current_music; - return music->filename; -} - -void JA_PauseMusic() { - if (!JA_musicEnabled) return; - if (!current_music || current_music->state == JA_MUSIC_INVALID) return; - - current_music->state = JA_MUSIC_PAUSED; - // SDL_PauseAudioStreamDevice(current_music->stream); - SDL_UnbindAudioStream(current_music->stream); -} - -void JA_ResumeMusic() { - if (!JA_musicEnabled) return; - if (!current_music || current_music->state == JA_MUSIC_INVALID) return; - - current_music->state = JA_MUSIC_PLAYING; - // SDL_ResumeAudioStreamDevice(current_music->stream); - SDL_BindAudioStream(sdlAudioDevice, current_music->stream); -} - -void JA_StopMusic() { - if (!JA_musicEnabled) return; - if (!current_music || current_music->state == JA_MUSIC_INVALID) return; - - current_music->pos = 0; - current_music->state = JA_MUSIC_STOPPED; - // SDL_PauseAudioStreamDevice(current_music->stream); - SDL_DestroyAudioStream(current_music->stream); - current_music->stream = nullptr; - free(current_music->filename); - current_music->filename = nullptr; -} - -void JA_FadeOutMusic(const int milliseconds) { - if (!JA_musicEnabled) return; - if (current_music == NULL || current_music->state == JA_MUSIC_INVALID) return; - - fading = true; - fade_start_time = SDL_GetTicks(); - fade_duration = milliseconds; - fade_initial_volume = JA_musicVolume; -} - -JA_Music_state JA_GetMusicState() { - if (!JA_musicEnabled) return JA_MUSIC_DISABLED; - if (!current_music) return JA_MUSIC_INVALID; - - return current_music->state; -} - -void JA_DeleteMusic(JA_Music_t* music) { - if (current_music == music) current_music = nullptr; - SDL_free(music->buffer); - if (music->stream) SDL_DestroyAudioStream(music->stream); - delete music; -} - -float JA_SetMusicVolume(float volume) { - JA_musicVolume = SDL_clamp(volume, 0.0f, 1.0f); - if (current_music) SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume); - return JA_musicVolume; -} - -void JA_SetMusicPosition(float value) { - if (!current_music) return; - current_music->pos = value * current_music->spec.freq; -} - -float JA_GetMusicPosition() { - if (!current_music) return 0; - return float(current_music->pos) / float(current_music->spec.freq); -} - -void JA_EnableMusic(const bool value) { - if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic(); - - JA_musicEnabled = value; -} - -JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) { - JA_Sound_t* sound = new JA_Sound_t(); - sound->buffer = buffer; - sound->length = length; - return sound; -} - -JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) { - JA_Sound_t* sound = new JA_Sound_t(); - SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length); - - return sound; -} - -JA_Sound_t* JA_LoadSound(const char* filename) { - JA_Sound_t* sound = new JA_Sound_t(); - SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length); - - return sound; -} - -int JA_PlaySound(JA_Sound_t* sound, const int loop) { - if (!JA_soundEnabled) return -1; - - int channel = 0; - while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; } - if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) channel = 0; - JA_StopChannel(channel); - - channels[channel].sound = sound; - channels[channel].times = loop; - channels[channel].pos = 0; - channels[channel].state = JA_CHANNEL_PLAYING; - channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec); - SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length); - SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume); - SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream); - - return channel; -} - -int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop) { - if (!JA_soundEnabled) return -1; - - if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1; - JA_StopChannel(channel); - - channels[channel].sound = sound; - channels[channel].times = loop; - channels[channel].pos = 0; - channels[channel].state = JA_CHANNEL_PLAYING; - channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec); - SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length); - SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume); - SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream); - - return channel; -} - -void JA_DeleteSound(JA_Sound_t* sound) { - for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) { - if (channels[i].sound == sound) JA_StopChannel(i); - } - SDL_free(sound->buffer); - delete sound; -} - -void JA_PauseChannel(const int channel) { - if (!JA_soundEnabled) return; - - if (channel == -1) { - for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) - if (channels[i].state == JA_CHANNEL_PLAYING) { - channels[i].state = JA_CHANNEL_PAUSED; - // SDL_PauseAudioStreamDevice(channels[i].stream); - SDL_UnbindAudioStream(channels[i].stream); - } - } else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) { - if (channels[channel].state == JA_CHANNEL_PLAYING) { - channels[channel].state = JA_CHANNEL_PAUSED; - // SDL_PauseAudioStreamDevice(channels[channel].stream); - SDL_UnbindAudioStream(channels[channel].stream); - } - } -} - -void JA_ResumeChannel(const int channel) { - if (!JA_soundEnabled) return; - - if (channel == -1) { - for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) - if (channels[i].state == JA_CHANNEL_PAUSED) { - channels[i].state = JA_CHANNEL_PLAYING; - // SDL_ResumeAudioStreamDevice(channels[i].stream); - SDL_BindAudioStream(sdlAudioDevice, channels[i].stream); - } - } else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) { - if (channels[channel].state == JA_CHANNEL_PAUSED) { - channels[channel].state = JA_CHANNEL_PLAYING; - // SDL_ResumeAudioStreamDevice(channels[channel].stream); - SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream); - } - } -} - -void JA_StopChannel(const int channel) { - if (!JA_soundEnabled) return; - - if (channel == -1) { - for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) { - if (channels[i].state != JA_CHANNEL_FREE) SDL_DestroyAudioStream(channels[i].stream); - channels[i].stream = nullptr; - channels[i].state = JA_CHANNEL_FREE; - channels[i].pos = 0; - channels[i].sound = NULL; - } - } else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) { - if (channels[channel].state != JA_CHANNEL_FREE) SDL_DestroyAudioStream(channels[channel].stream); - channels[channel].stream = nullptr; - channels[channel].state = JA_CHANNEL_FREE; - channels[channel].pos = 0; - channels[channel].sound = NULL; - } -} - -JA_Channel_state JA_GetChannelState(const int channel) { - if (!JA_soundEnabled) return JA_SOUND_DISABLED; - - if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID; - - return channels[channel].state; -} - -float JA_SetSoundVolume(float volume) { - JA_soundVolume = SDL_clamp(volume, 0.0f, 1.0f); - - for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) - if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) - SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume); - - return JA_soundVolume; -} - -void JA_EnableSound(const bool value) { - for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) { - if (channels[i].state == JA_CHANNEL_PLAYING) JA_StopChannel(i); - } - JA_soundEnabled = value; -} - -float JA_SetVolume(float volume) { - JA_SetSoundVolume(JA_SetMusicVolume(volume) / 2.0f); - - return JA_musicVolume; -} - -#endif \ No newline at end of file +#include "core/jail/jail_audio.hpp" diff --git a/source/core/jail/jail_audio.hpp b/source/core/jail/jail_audio.hpp index 040293f..6cbbf85 100644 --- a/source/core/jail/jail_audio.hpp +++ b/source/core/jail/jail_audio.hpp @@ -1,49 +1,555 @@ #pragma once -#include -enum JA_Channel_state { JA_CHANNEL_INVALID, +// --- Includes --- +#include +#include +#include +#include +#include + +#define STB_VORBIS_HEADER_ONLY +#include "external/stb_vorbis.h" + +// --- Public Enums --- +enum JA_Channel_state { + JA_CHANNEL_INVALID, JA_CHANNEL_FREE, JA_CHANNEL_PLAYING, JA_CHANNEL_PAUSED, - JA_SOUND_DISABLED }; -enum JA_Music_state { JA_MUSIC_INVALID, + JA_SOUND_DISABLED, +}; +enum JA_Music_state { + JA_MUSIC_INVALID, JA_MUSIC_PLAYING, JA_MUSIC_PAUSED, JA_MUSIC_STOPPED, - JA_MUSIC_DISABLED }; + JA_MUSIC_DISABLED, +}; -struct JA_Sound_t; -struct JA_Music_t; +// --- Struct Definitions --- +#define JA_MAX_SIMULTANEOUS_CHANNELS 20 +#define JA_MAX_GROUPS 2 -void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels); -void JA_Quit(); +struct JA_Sound_t { + SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000}; + Uint32 length{0}; + Uint8* buffer{nullptr}; +}; -JA_Music_t* JA_LoadMusic(const char* filename); -JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename = nullptr); -void JA_PlayMusic(JA_Music_t* music, const int loop = -1); -char* JA_GetMusicFilename(JA_Music_t* music = nullptr); -void JA_PauseMusic(); -void JA_ResumeMusic(); -void JA_StopMusic(); -void JA_FadeOutMusic(const int milliseconds); -JA_Music_state JA_GetMusicState(); -void JA_DeleteMusic(JA_Music_t* music); -float JA_SetMusicVolume(float volume); -void JA_SetMusicPosition(float value); -float JA_GetMusicPosition(); -void JA_EnableMusic(const bool value); +struct JA_Channel_t { + JA_Sound_t* sound{nullptr}; + int pos{0}; + int times{0}; + int group{0}; + SDL_AudioStream* stream{nullptr}; + JA_Channel_state state{JA_CHANNEL_FREE}; +}; -JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length); -JA_Sound_t* JA_LoadSound(Uint8* buffer, Uint32 length); -JA_Sound_t* JA_LoadSound(const char* filename); -int JA_PlaySound(JA_Sound_t* sound, const int loop = 0); -int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0); -void JA_PauseChannel(const int channel); -void JA_ResumeChannel(const int channel); -void JA_StopChannel(const int channel); -JA_Channel_state JA_GetChannelState(const int channel); -void JA_DeleteSound(JA_Sound_t* sound); -float JA_SetSoundVolume(float volume); -void JA_EnableSound(const bool value); +struct JA_Music_t { + SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000}; -float JA_SetVolume(float volume); + // OGG comprimit en memòria. Propietat nostra; es copia des del fitxer una + // sola vegada en JA_LoadMusic i es descomprimix en chunks per streaming. + Uint8* ogg_data{nullptr}; + Uint32 ogg_length{0}; + stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del JA_Music_t + + char* filename{nullptr}; + + int times{0}; // loops restants (-1 = infinit, 0 = un sol play) + SDL_AudioStream* stream{nullptr}; + JA_Music_state state{JA_MUSIC_INVALID}; +}; + +// --- Internal Global State (inline, C++17) --- + +inline JA_Music_t* current_music{nullptr}; +inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS]; + +inline SDL_AudioSpec JA_audioSpec{SDL_AUDIO_S16, 2, 48000}; +inline float JA_musicVolume{1.0f}; +inline float JA_soundVolume[JA_MAX_GROUPS]; +inline bool JA_musicEnabled{true}; +inline bool JA_soundEnabled{true}; +inline SDL_AudioDeviceID sdlAudioDevice{0}; + +inline bool fading{false}; +inline int fade_start_time{0}; +inline int fade_duration{0}; +inline float fade_initial_volume{0.0f}; + +// --- Forward Declarations --- +inline void JA_StopMusic(); +inline void JA_StopChannel(const int channel); +inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0, const int group = 0); + +// --- Music streaming internals --- +// Bytes-per-sample per canal (sempre s16) +static constexpr int JA_MUSIC_BYTES_PER_SAMPLE = 2; +// Quants shorts decodifiquem per crida a get_samples_short_interleaved. +// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz. +static constexpr int JA_MUSIC_CHUNK_SHORTS = 8192; +// Umbral d'àudio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a +// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns. +static constexpr float JA_MUSIC_LOW_WATER_SECONDS = 0.5f; + +// Decodifica un chunk del vorbis i el volca a l'stream. Retorna samples +// decodificats per canal (0 = EOF de l'stream vorbis). +inline int JA_FeedMusicChunk(JA_Music_t* music) { + if (!music || !music->vorbis || !music->stream) return 0; + + short chunk[JA_MUSIC_CHUNK_SHORTS]; + const int channels = music->spec.channels; + const int samples_per_channel = stb_vorbis_get_samples_short_interleaved( + music->vorbis, + channels, + chunk, + JA_MUSIC_CHUNK_SHORTS); + if (samples_per_channel <= 0) return 0; + + const int bytes = samples_per_channel * channels * JA_MUSIC_BYTES_PER_SAMPLE; + SDL_PutAudioStreamData(music->stream, chunk, bytes); + return samples_per_channel; +} + +// Reompli l'stream fins que tinga ≥ JA_MUSIC_LOW_WATER_SECONDS bufferats. +// En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar. +inline void JA_PumpMusic(JA_Music_t* music) { + if (!music || !music->vorbis || !music->stream) return; + + const int bytes_per_second = music->spec.freq * music->spec.channels * JA_MUSIC_BYTES_PER_SAMPLE; + const int low_water_bytes = static_cast(JA_MUSIC_LOW_WATER_SECONDS * static_cast(bytes_per_second)); + + while (SDL_GetAudioStreamAvailable(music->stream) < low_water_bytes) { + const int decoded = JA_FeedMusicChunk(music); + if (decoded > 0) continue; + + // EOF: si queden loops, rebobinar; si no, tallar i deixar drenar. + if (music->times != 0) { + stb_vorbis_seek_start(music->vorbis); + if (music->times > 0) music->times--; + } else { + break; + } + } +} + +// --- Core Functions --- + +// Crida-la una vegada per frame des del main loop (Director). Substituïx +// el callback asíncron SDL_AddTimer de la versió antiga — imprescindible +// per al port a emscripten/SDL_AppIterate. +inline void JA_Update() { + if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) { + if (fading) { + int time = SDL_GetTicks(); + if (time > (fade_start_time + fade_duration)) { + fading = false; + JA_StopMusic(); + return; + } else { + const int time_passed = time - fade_start_time; + const float percent = (float)time_passed / (float)fade_duration; + SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0f - percent)); + } + } + + // Streaming: rellenem l'stream fins al low-water-mark i parem si el + // vorbis s'ha esgotat i no queden loops. + JA_PumpMusic(current_music); + if (current_music->times == 0 && SDL_GetAudioStreamAvailable(current_music->stream) == 0) { + JA_StopMusic(); + } + } + + if (JA_soundEnabled) { + for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) + if (channels[i].state == JA_CHANNEL_PLAYING) { + if (channels[i].times != 0) { + if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length / 2)) { + SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length); + if (channels[i].times > 0) channels[i].times--; + } + } else { + if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i); + } + } + } +} + +inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) { +#ifdef _DEBUG + SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG); +#endif + + JA_audioSpec = {format, num_channels, freq}; + if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); + sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec); + if (sdlAudioDevice == 0) SDL_Log("Failed to initialize SDL audio!"); + for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE; + for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5f; +} + +inline void JA_Quit() { + if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); + sdlAudioDevice = 0; +} + +// --- Music Functions --- + +inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) { + if (!buffer || length == 0) return nullptr; + + // Còpia del OGG comprimit: stb_vorbis llig de forma persistent aquesta + // memòria mentre el handle estiga viu, així que hem de posseir-la nosaltres. + Uint8* ogg_copy = static_cast(SDL_malloc(length)); + if (!ogg_copy) return nullptr; + SDL_memcpy(ogg_copy, buffer, length); + + int error = 0; + stb_vorbis* vorbis = stb_vorbis_open_memory(ogg_copy, static_cast(length), &error, nullptr); + if (!vorbis) { + SDL_free(ogg_copy); + SDL_Log("JA_LoadMusic: stb_vorbis_open_memory failed (error %d)", error); + return nullptr; + } + + auto* music = new JA_Music_t(); + music->ogg_data = ogg_copy; + music->ogg_length = length; + music->vorbis = vorbis; + + const stb_vorbis_info info = stb_vorbis_get_info(vorbis); + music->spec.channels = info.channels; + music->spec.freq = static_cast(info.sample_rate); + music->spec.format = SDL_AUDIO_S16; + music->state = JA_MUSIC_STOPPED; + + return music; +} + +// Overload de compatibilitat: accepta filename per a no trencar els call +// sites existents que passaven el nom del fitxer junt amb el buffer. +inline JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename) { + JA_Music_t* music = JA_LoadMusic(static_cast(buffer), length); + if (music && filename) { + music->filename = static_cast(malloc(strlen(filename) + 1)); + if (music->filename) strcpy(music->filename, filename); + } + return music; +} + +inline JA_Music_t* JA_LoadMusic(const char* filename) { + FILE* f = fopen(filename, "rb"); + if (!f) return nullptr; + fseek(f, 0, SEEK_END); + long fsize = ftell(f); + fseek(f, 0, SEEK_SET); + auto* buffer = static_cast(malloc(fsize + 1)); + if (!buffer) { + fclose(f); + return nullptr; + } + if (fread(buffer, fsize, 1, f) != 1) { + fclose(f); + free(buffer); + return nullptr; + } + fclose(f); + + JA_Music_t* music = JA_LoadMusic(buffer, static_cast(fsize), filename); + free(buffer); + + return music; +} + +inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) { + if (!JA_musicEnabled || !music || !music->vorbis) return; + + JA_StopMusic(); + + current_music = music; + current_music->state = JA_MUSIC_PLAYING; + current_music->times = loop; + + // Rebobinem l'stream de vorbis al principi. Cobreix tant play-per-primera- + // vegada com replays/canvis de track que tornen a la mateixa pista. + stb_vorbis_seek_start(current_music->vorbis); + + current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec); + if (!current_music->stream) { + SDL_Log("Failed to create audio stream!"); + current_music->state = JA_MUSIC_STOPPED; + return; + } + SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume); + + // Pre-cargem el buffer abans de bindejar per evitar un underrun inicial. + JA_PumpMusic(current_music); + + if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n"); +} + +inline char* JA_GetMusicFilename(JA_Music_t* music = nullptr) { + if (!music) music = current_music; + if (!music) return nullptr; + return music->filename; +} + +inline void JA_PauseMusic() { + if (!JA_musicEnabled) return; + if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; + + current_music->state = JA_MUSIC_PAUSED; + SDL_UnbindAudioStream(current_music->stream); +} + +inline void JA_ResumeMusic() { + if (!JA_musicEnabled) return; + if (!current_music || current_music->state != JA_MUSIC_PAUSED) return; + + current_music->state = JA_MUSIC_PLAYING; + SDL_BindAudioStream(sdlAudioDevice, current_music->stream); +} + +inline void JA_StopMusic() { + if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return; + + current_music->state = JA_MUSIC_STOPPED; + if (current_music->stream) { + SDL_DestroyAudioStream(current_music->stream); + current_music->stream = nullptr; + } + // Deixem el handle de vorbis viu — es tanca en JA_DeleteMusic. + // Rebobinem perquè un futur JA_PlayMusic comence des del principi. + if (current_music->vorbis) { + stb_vorbis_seek_start(current_music->vorbis); + } +} + +inline void JA_FadeOutMusic(const int milliseconds) { + if (!JA_musicEnabled) return; + if (!current_music || current_music->state == JA_MUSIC_INVALID) return; + + fading = true; + fade_start_time = SDL_GetTicks(); + fade_duration = milliseconds; + fade_initial_volume = JA_musicVolume; +} + +inline JA_Music_state JA_GetMusicState() { + if (!JA_musicEnabled) return JA_MUSIC_DISABLED; + if (!current_music) return JA_MUSIC_INVALID; + + return current_music->state; +} + +inline void JA_DeleteMusic(JA_Music_t* music) { + if (!music) return; + if (current_music == music) { + JA_StopMusic(); + current_music = nullptr; + } + if (music->stream) SDL_DestroyAudioStream(music->stream); + if (music->vorbis) stb_vorbis_close(music->vorbis); + SDL_free(music->ogg_data); + free(music->filename); + delete music; +} + +inline float JA_SetMusicVolume(float volume) { + JA_musicVolume = SDL_clamp(volume, 0.0f, 1.0f); + if (current_music && current_music->stream) { + SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume); + } + return JA_musicVolume; +} + +inline void JA_SetMusicPosition(float /*value*/) { + // No implementat amb el backend de streaming. +} + +inline float JA_GetMusicPosition() { + return 0.0f; +} + +inline void JA_EnableMusic(const bool value) { + if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic(); + JA_musicEnabled = value; +} + +// --- Sound Functions --- + +inline JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) { + JA_Sound_t* sound = new JA_Sound_t(); + sound->buffer = buffer; + sound->length = length; + return sound; +} + +inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) { + JA_Sound_t* sound = new JA_Sound_t(); + if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length)) { + SDL_Log("Failed to load WAV from memory: %s", SDL_GetError()); + delete sound; + return nullptr; + } + return sound; +} + +inline JA_Sound_t* JA_LoadSound(const char* filename) { + JA_Sound_t* sound = new JA_Sound_t(); + if (!SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length)) { + SDL_Log("Failed to load WAV file: %s", SDL_GetError()); + delete sound; + return nullptr; + } + return sound; +} + +inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) { + if (!JA_soundEnabled || !sound) return -1; + + int channel = 0; + while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; } + if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) channel = 0; + + return JA_PlaySoundOnChannel(sound, channel, loop, group); +} + +inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop, const int group) { + if (!JA_soundEnabled || !sound) return -1; + if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1; + + JA_StopChannel(channel); + + channels[channel].sound = sound; + channels[channel].times = loop; + channels[channel].pos = 0; + channels[channel].group = group; + channels[channel].state = JA_CHANNEL_PLAYING; + channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec); + + if (!channels[channel].stream) { + SDL_Log("Failed to create audio stream for sound!"); + channels[channel].state = JA_CHANNEL_FREE; + return -1; + } + + SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length); + SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]); + SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream); + + return channel; +} + +inline void JA_DeleteSound(JA_Sound_t* sound) { + if (!sound) return; + for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) { + if (channels[i].sound == sound) JA_StopChannel(i); + } + SDL_free(sound->buffer); + delete sound; +} + +inline void JA_PauseChannel(const int channel) { + if (!JA_soundEnabled) return; + + if (channel == -1) { + for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) + if (channels[i].state == JA_CHANNEL_PLAYING) { + channels[i].state = JA_CHANNEL_PAUSED; + SDL_UnbindAudioStream(channels[i].stream); + } + } else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) { + if (channels[channel].state == JA_CHANNEL_PLAYING) { + channels[channel].state = JA_CHANNEL_PAUSED; + SDL_UnbindAudioStream(channels[channel].stream); + } + } +} + +inline void JA_ResumeChannel(const int channel) { + if (!JA_soundEnabled) return; + + if (channel == -1) { + for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) + if (channels[i].state == JA_CHANNEL_PAUSED) { + channels[i].state = JA_CHANNEL_PLAYING; + SDL_BindAudioStream(sdlAudioDevice, channels[i].stream); + } + } else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) { + if (channels[channel].state == JA_CHANNEL_PAUSED) { + channels[channel].state = JA_CHANNEL_PLAYING; + SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream); + } + } +} + +inline void JA_StopChannel(const int channel) { + if (channel == -1) { + for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) { + if (channels[i].state != JA_CHANNEL_FREE) { + if (channels[i].stream) SDL_DestroyAudioStream(channels[i].stream); + channels[i].stream = nullptr; + channels[i].state = JA_CHANNEL_FREE; + channels[i].pos = 0; + channels[i].sound = nullptr; + } + } + } else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) { + if (channels[channel].state != JA_CHANNEL_FREE) { + if (channels[channel].stream) SDL_DestroyAudioStream(channels[channel].stream); + channels[channel].stream = nullptr; + channels[channel].state = JA_CHANNEL_FREE; + channels[channel].pos = 0; + channels[channel].sound = nullptr; + } + } +} + +inline JA_Channel_state JA_GetChannelState(const int channel) { + if (!JA_soundEnabled) return JA_SOUND_DISABLED; + if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID; + + return channels[channel].state; +} + +inline float JA_SetSoundVolume(float volume, const int group = -1) { + const float v = SDL_clamp(volume, 0.0f, 1.0f); + + if (group == -1) { + for (int i = 0; i < JA_MAX_GROUPS; ++i) { + JA_soundVolume[i] = v; + } + } else if (group >= 0 && group < JA_MAX_GROUPS) { + JA_soundVolume[group] = v; + } else { + return v; + } + + // Aplicar volum als canals actius. + for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) { + if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) { + if (group == -1 || channels[i].group == group) { + if (channels[i].stream) { + SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume[channels[i].group]); + } + } + } + } + return v; +} + +inline void JA_EnableSound(const bool value) { + if (!value) { + JA_StopChannel(-1); + } + JA_soundEnabled = value; +} + +inline float JA_SetVolume(float volume) { + float v = JA_SetMusicVolume(volume); + JA_SetSoundVolume(v, -1); + return v; +} diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index e4372ba..347d6fc 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -71,6 +71,11 @@ void Director::run() { GlobalInputs::handle(); Mouse::updateCursorVisibility(); + // Bombeig de l'àudio: reomple l'stream de música i para els canals + // drenats. Substituïx el callback de SDL_AddTimer de la versió + // antiga — imprescindible per al futur port a emscripten. + JA_Update(); + // Dispara els crèdits cinematogràfics la primera vegada que el joc // arriba al menú del títol (info::ctx.num_piramide == 0). Lectura no // atòmica d'un int global: race benigna, tard d'1 frame en el pitjor cas.