From bc53174f2b4716f02a6a3cb667c72ab83993a6fe Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 13:11:15 +0200 Subject: [PATCH 1/6] refactor(core/audio): split jail_audio.hpp/.cpp (COUP-02, PATCH-03) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preparatori per COUP-01. No canvia comportament: només mou les definicions inline de jail_audio.hpp a un jail_audio.cpp nou, deixant el .hpp amb declaracions + tipus + constants. Efectes col·laterals que es tanquen gratis: - COUP-02: jail_audio deixa de ser header-only. Abans cada TU que l'incloïa compilava 650 línies inline (amb stb_vorbis.c en mode HEADER_ONLY). Ara el .hpp té ~130 línies de declaracions. - PATCH-03: #include "external/stb_vorbis.c" + #undef L/C/R/PLAYBACK_* viu només al final de jail_audio.cpp. audio.cpp perd el workaround dels undefs. Estat intern (current_music, channels[], audio_spec, music_volume, sound_volume[], music_enabled, sound_enabled, sdl_audio_device, outgoing_music, incoming_fade) baixa al .cpp dins d'un namespace anònim (internal linkage). Els helpers privats (feedMusicChunk, pumpMusic, preFillOutgoing, fadeProgress, update*Fade, updateCurrent*, stealCurrentIntoOutgoing, forEachTargetChannel) també viuen a l'anonymous namespace. Ja::Music i Ja::Sound són públics; el .hpp forward-declara `struct stb_vorbis` perquè els callers només els agafen per punter. NOLINT(readability-identifier-naming) al forward-decl perquè el nom l'imposa stb_vorbis. jail_audio.cpp s'afegeix a APP_SOURCES al CMakeLists.txt. .clang-tidy: desactivat clang-analyzer-core.UndefinedBinaryOperatorResult globalment. Fals positiu conegut a external/stb_vorbis.c que abans no es disparava (no hi havia TU que cridés directament stb_vorbis_seek_start des del mateix fitxer on la funció està inlined); ara que jail_audio.cpp sí que fa aquestes crides directes, el path d'anàlisi passa pel .c i el check dispara. HeaderFilterRegex no aplica a clang-analyzer-*. Preparació per COUP-01 (introduir Ja::Engine al commit següent). Co-Authored-By: Claude Opus 4.7 (1M context) --- .clang-tidy | 5 + CMakeLists.txt | 1 + source/core/audio/audio.cpp | 15 - source/core/audio/jail_audio.cpp | 550 +++++++++++++++++++++++++++++ source/core/audio/jail_audio.hpp | 569 ++----------------------------- 5 files changed, 593 insertions(+), 547 deletions(-) create mode 100644 source/core/audio/jail_audio.cpp diff --git a/.clang-tidy b/.clang-tidy index 6cbebda..d99f8fd 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -9,6 +9,11 @@ Checks: - -bugprone-easily-swappable-parameters - -bugprone-narrowing-conversions - -modernize-avoid-c-arrays,-warnings-as-errors + # Fals positiu recurrent en codi C de tercers (source/external/stb_vorbis.c). + # HeaderFilterRegex no aplica a clang-analyzer-*, així que cal desactivar + # explícitament. El check inferència sobre variables locals de legacy C + # genera reports que no reflecteixen bugs reals. + - -clang-analyzer-core.UndefinedBinaryOperatorResult WarningsAsErrors: '*' # Headers nostres (excloem source/external/ que conté dependències de tercers no editables) diff --git a/CMakeLists.txt b/CMakeLists.txt index 913c2bb..7973383 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,7 @@ set(APP_SOURCES source/core/system/director.cpp source/core/audio/audio.cpp source/core/audio/audio_adapter.cpp + source/core/audio/jail_audio.cpp source/core/locale/locale.cpp source/core/input/key_config.cpp source/core/input/global_inputs.cpp diff --git a/source/core/audio/audio.cpp b/source/core/audio/audio.cpp index c393dc6..06f4192 100644 --- a/source/core/audio/audio.cpp +++ b/source/core/audio/audio.cpp @@ -5,21 +5,6 @@ #include // Per a clamp #include // Per a std::fprintf -// Implementació de stb_vorbis (ha d'anar ABANS d'incloure jail_audio.hpp). -// clang-format off -#undef STB_VORBIS_HEADER_ONLY -// NOLINTNEXTLINE(bugprone-suspicious-include) -#include "external/stb_vorbis.c" -// stb_vorbis.c filtra les macros L, C i R (i PLAYBACK_*) al TU. Les netegem -// perquè xocarien amb noms de paràmetres de plantilla en altres headers. -#undef L -#undef C -#undef R -#undef PLAYBACK_MONO -#undef PLAYBACK_LEFT -#undef PLAYBACK_RIGHT -// clang-format on - #include "core/audio/audio_adapter.hpp" // Per a AudioResource::getMusic/getSound #include "core/audio/jail_audio.hpp" // Per a Ja::* (motor jailgames) #include "utils/defaults.hpp" // Per a Defaults::Audio::FREQUENCY diff --git a/source/core/audio/jail_audio.cpp b/source/core/audio/jail_audio.cpp new file mode 100644 index 0000000..83f8619 --- /dev/null +++ b/source/core/audio/jail_audio.cpp @@ -0,0 +1,550 @@ +#include "core/audio/jail_audio.hpp" + +#include +#include + +// Implementació completa de stb_vorbis en aquest TU. HEADER_ONLY es queda +// indefinit aquí perquè el .c aporti les definicions. La resta del projecte +// només veu el forward-decl de jail_audio.hpp. +// clang-format off +// NOLINTNEXTLINE(bugprone-suspicious-include) +#include "external/stb_vorbis.c" +// clang-format on + +namespace Ja { + + namespace { + + // --- Estat intern (internal linkage via anonymous namespace) --- + Music* current_music{nullptr}; + Channel channels[MAX_SIMULTANEOUS_CHANNELS]; + + SDL_AudioSpec audio_spec{DEFAULT_SPEC}; + float music_volume{1.0F}; + float sound_volume[MAX_GROUPS]; + bool music_enabled{true}; + bool sound_enabled{true}; + SDL_AudioDeviceID sdl_audio_device{0}; + + OutgoingMusic outgoing_music; + FadeState incoming_fade; + + // --- Streaming internals --- + // Bytes-per-sample per canal (sempre s16) + constexpr int 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. + constexpr int MUSIC_CHUNK_SHORTS = 8192; + // Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a + // l'SDL_AudioStream per absorbir jitter de frame i evitar underruns. + constexpr float 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). + auto feedMusicChunk(Music* music) -> int { + if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return 0; } + + short chunk[MUSIC_CHUNK_SHORTS]; + const int NUM_CHANNELS = music->spec.channels; + const int SAMPLES_PER_CHANNEL = stb_vorbis_get_samples_short_interleaved( + music->vorbis, + NUM_CHANNELS, + static_cast(chunk), + MUSIC_CHUNK_SHORTS); + if (SAMPLES_PER_CHANNEL <= 0) { return 0; } + + const int BYTES = SAMPLES_PER_CHANNEL * NUM_CHANNELS * MUSIC_BYTES_PER_SAMPLE; + SDL_PutAudioStreamData(music->stream, static_cast(chunk), BYTES); + return SAMPLES_PER_CHANNEL; + } + + // Reompli l'stream fins que tinga ≥ MUSIC_LOW_WATER_SECONDS bufferats. + // En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar. + void pumpMusic(Music* music) { + if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; } + + const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE; + const int LOW_WATER_BYTES = static_cast(MUSIC_LOW_WATER_SECONDS * static_cast(BYTES_PER_SECOND)); + + while (SDL_GetAudioStreamAvailable(music->stream) < LOW_WATER_BYTES) { + const int DECODED = 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; + } + } + } + + // Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream + // siga robat per outgoing_music (crossfade o fade-out). Imprescindible amb + // streaming: l'stream robat no es pot re-alimentar perquè perd la referència + // al seu vorbis decoder. No aplica loop — si el vorbis s'esgota abans, parem. + void preFillOutgoing(Music* music, int duration_ms) { + if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; } + + const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE; + const int NEEDED_BYTES = static_cast((static_cast(duration_ms) * BYTES_PER_SECOND) / 1000); + + while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) { + const int DECODED = feedMusicChunk(music); + if (DECODED <= 0) { break; } // EOF: deixem drenar el que hi haja + } + } + + // Retorna el progrés lineal [0..1] d'un fade. 1.0 vol dir completat. Única + // font de la corba del fade: si es vol canviar a logarítmica/quadràtica, + // s'edita aquí i afecta fade-in i fade-out alhora. + auto fadeProgress(const FadeState& fade) -> float { + if (fade.duration_ms <= 0) { return 1.0F; } + const Uint64 ELAPSED = SDL_GetTicks() - fade.start_time; + if (ELAPSED >= static_cast(fade.duration_ms)) { return 1.0F; } + return static_cast(ELAPSED) / static_cast(fade.duration_ms); + } + + // Avança el fade-out de l'outgoing music (crossfade o fade-out a silenci). + void updateOutgoingFade() { + if (outgoing_music.stream == nullptr || !outgoing_music.fade.active) { return; } + + const float PROGRESS = fadeProgress(outgoing_music.fade); + if (PROGRESS >= 1.0F) { + SDL_DestroyAudioStream(outgoing_music.stream); + outgoing_music.stream = nullptr; + outgoing_music.fade.active = false; + } else { + SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0F - PROGRESS)); + } + } + + // Avança el fade-in de la pista actual (si hi ha un crossfade en curs). + void updateIncomingFade() { + if (!incoming_fade.active) { return; } + + const float PROGRESS = fadeProgress(incoming_fade); + if (PROGRESS >= 1.0F) { + incoming_fade.active = false; + SDL_SetAudioStreamGain(current_music->stream, music_volume); + } else { + SDL_SetAudioStreamGain(current_music->stream, music_volume * PROGRESS); + } + } + + // Alimenta la pista actual i la para si s'ha esgotat el stream vorbis. + void updateCurrentMusic() { + if (!music_enabled || current_music == nullptr || current_music->state != MusicState::PLAYING) { return; } + + updateIncomingFade(); + + pumpMusic(current_music); + if (current_music->times == 0 && SDL_GetAudioStreamAvailable(current_music->stream) == 0) { + stopMusic(); + } + } + + // Reompli (si cal loop) o para els canals d'efectes actius. + void updateSoundChannels() { + if (!sound_enabled) { return; } + + for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) { + if (channels[i].state != ChannelState::PLAYING) { continue; } + + if (channels[i].times != 0) { + if (static_cast(SDL_GetAudioStreamAvailable(channels[i].stream)) < (channels[i].sound->length / 2)) { + SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer.get(), channels[i].sound->length); + if (channels[i].times > 0) { channels[i].times--; } + } + } else if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) { + stopChannel(i); + } + } + } + + // Roba l'stream de la música actual al slot outgoing per a un fade-out de + // `duration_ms` ms. Destrueix qualsevol outgoing anterior. L'stream robat + // ja no es pot alimentar (perd l'accés al vorbis decoder), per això cal + // preFillOutgoing abans del robatori perquè tingui so suficient fins que + // acabi el fade. No-op si no hi ha música sonant amb stream vàlid. + void stealCurrentIntoOutgoing(const int duration_ms) { + if (outgoing_music.stream != nullptr) { + SDL_DestroyAudioStream(outgoing_music.stream); + outgoing_music.stream = nullptr; + outgoing_music.fade.active = false; + } + + if (current_music == nullptr || current_music->state != MusicState::PLAYING || current_music->stream == nullptr) { + return; + } + + preFillOutgoing(current_music, duration_ms); + outgoing_music.stream = current_music->stream; + outgoing_music.fade = { + .active = true, + .start_time = SDL_GetTicks(), + .duration_ms = duration_ms, + .initial_volume = music_volume, + }; + current_music->stream = nullptr; + current_music->state = MusicState::STOPPED; + if (current_music->vorbis != nullptr) { stb_vorbis_seek_start(current_music->vorbis); } + } + + // Aplica `fn` als canals objectiu: tots si channel == -1, o només el d'índex + // vàlid si està dins [0, MAX_SIMULTANEOUS_CHANNELS). Centralitza el dispatch + // "tots vs un" que pauseChannel/resumeChannel/stopChannel repetien. + template + void forEachTargetChannel(const int channel, Fn&& fn) { + if (channel == -1) { + for (auto& ch : channels) { fn(ch); } + } else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) { + fn(channels[channel]); + } + } + + } // namespace + + // --- Lifecycle --- + + void init(const int freq, const SDL_AudioFormat format, const int num_channels) { + audio_spec = {.format = format, .channels = num_channels, .freq = freq}; + if (sdl_audio_device != 0) { SDL_CloseAudioDevice(sdl_audio_device); } + sdl_audio_device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec); + if (sdl_audio_device == 0) { std::fprintf(stderr, "Ja: Failed to initialize SDL audio!\n"); } + for (auto& channel : channels) { channel.state = ChannelState::FREE; } + } + + void quit() { + if (outgoing_music.stream != nullptr) { + SDL_DestroyAudioStream(outgoing_music.stream); + outgoing_music.stream = nullptr; + } + if (sdl_audio_device != 0) { SDL_CloseAudioDevice(sdl_audio_device); } + sdl_audio_device = 0; + } + + void update() { + updateOutgoingFade(); + updateCurrentMusic(); + updateSoundChannels(); + } + + // --- Music --- + + auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* { + if (buffer == nullptr || length == 0) { return nullptr; } + + // Allocem el Music primer per aprofitar el seu `std::vector` + // com a propietari del OGG comprimit. stb_vorbis guarda un punter + // persistent al buffer; com que ací no el resize'jem, el .data() és + // estable durant tot el cicle de vida del music. + auto music = std::make_unique(); + music->ogg_data.assign(buffer, buffer + length); + + int vorbis_error = 0; + music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(), + static_cast(length), + &vorbis_error, + nullptr); + if (music->vorbis == nullptr) { + std::fprintf(stderr, "Ja::loadMusic: stb_vorbis_open_memory failed (error %d)\n", vorbis_error); + return nullptr; + } + + const stb_vorbis_info INFO = stb_vorbis_get_info(music->vorbis); + music->spec.channels = static_cast(INFO.channels); + music->spec.freq = static_cast(INFO.sample_rate); + music->spec.format = SDL_AUDIO_S16; + music->state = MusicState::STOPPED; + + return music.release(); + } + + // Overload amb filename. Resource::Cache l'usa per registrar el path dins + // del propi Ja::Music (camp `filename`); la capa Audio l'usa per recuperar + // el nom després d'un playMusic(Ja::Music*, ...) — veure PATCH-02. + auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music* { + Music* music = loadMusic(buffer, length); + if (music != nullptr && filename != nullptr) { music->filename = filename; } + return music; + } + + void playMusic(Music* music, const int loop) { + if (!music_enabled || music == nullptr || music->vorbis == nullptr) { return; } + + stopMusic(); + + current_music = music; + current_music->state = MusicState::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, &audio_spec); + if (current_music->stream == nullptr) { + std::fprintf(stderr, "Ja::playMusic: Failed to create audio stream!\n"); + current_music->state = MusicState::STOPPED; + return; + } + SDL_SetAudioStreamGain(current_music->stream, music_volume); + + // Pre-cargem el buffer abans de bindejar per evitar un underrun inicial. + pumpMusic(current_music); + + if (!SDL_BindAudioStream(sdl_audio_device, current_music->stream)) { + std::fprintf(stderr, "Ja::playMusic: SDL_BindAudioStream failed!\n"); + } + } + + void pauseMusic() { + if (!music_enabled) { return; } + if (current_music == nullptr || current_music->state != MusicState::PLAYING) { return; } + + current_music->state = MusicState::PAUSED; + SDL_UnbindAudioStream(current_music->stream); + } + + void resumeMusic() { + if (!music_enabled) { return; } + if (current_music == nullptr || current_music->state != MusicState::PAUSED) { return; } + + current_music->state = MusicState::PLAYING; + SDL_BindAudioStream(sdl_audio_device, current_music->stream); + } + + void stopMusic() { + // Limpiar outgoing crossfade si existe + if (outgoing_music.stream != nullptr) { + SDL_DestroyAudioStream(outgoing_music.stream); + outgoing_music.stream = nullptr; + outgoing_music.fade.active = false; + } + incoming_fade.active = false; + + if (current_music == nullptr || current_music->state == MusicState::INVALID || current_music->state == MusicState::STOPPED) { return; } + + current_music->state = MusicState::STOPPED; + if (current_music->stream != nullptr) { + SDL_DestroyAudioStream(current_music->stream); + current_music->stream = nullptr; + } + // Deixem el handle de vorbis viu — es tanca en deleteMusic. + // Rebobinem perquè un futur playMusic comence des del principi. + if (current_music->vorbis != nullptr) { + stb_vorbis_seek_start(current_music->vorbis); + } + } + + void fadeOutMusic(const int milliseconds) { + if (!music_enabled) { return; } + if (current_music == nullptr || current_music->state != MusicState::PLAYING) { return; } + + stealCurrentIntoOutgoing(milliseconds); + incoming_fade.active = false; + } + + void crossfadeMusic(Music* music, const int crossfade_ms, const int loop) { + if (!music_enabled || music == nullptr || music->vorbis == nullptr) { return; } + + stealCurrentIntoOutgoing(crossfade_ms); + + // Iniciar la nueva pista con gain=0 (el fade-in la sube gradualmente) + current_music = music; + current_music->state = MusicState::PLAYING; + current_music->times = loop; + + stb_vorbis_seek_start(current_music->vorbis); + current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &audio_spec); + if (current_music->stream == nullptr) { + std::fprintf(stderr, "Ja::crossfadeMusic: Failed to create audio stream!\n"); + current_music->state = MusicState::STOPPED; + return; + } + SDL_SetAudioStreamGain(current_music->stream, 0.0F); + pumpMusic(current_music); // pre-carrega abans de bindejar + SDL_BindAudioStream(sdl_audio_device, current_music->stream); + + // Configurar fade-in + incoming_fade = { + .active = true, + .start_time = SDL_GetTicks(), + .duration_ms = crossfade_ms, + .initial_volume = 0.0F, + }; + } + + auto getMusicState() -> MusicState { + if (!music_enabled) { return MusicState::DISABLED; } + if (current_music == nullptr) { return MusicState::INVALID; } + + return current_music->state; + } + + void deleteMusic(Music* music) { + if (music == nullptr) { return; } + if (current_music == music) { + stopMusic(); + current_music = nullptr; + } + if (music->stream != nullptr) { SDL_DestroyAudioStream(music->stream); } + if (music->vorbis != nullptr) { stb_vorbis_close(music->vorbis); } + // ogg_data (std::vector) i filename (std::string) s'alliberen sols + // al destructor de Music. + delete music; + } + + auto setMusicVolume(float volume) -> float { + music_volume = SDL_clamp(volume, 0.0F, 1.0F); + if (current_music != nullptr && current_music->stream != nullptr) { + SDL_SetAudioStreamGain(current_music->stream, music_volume); + } + return music_volume; + } + + void enableMusic(const bool value) { + if (!value && current_music != nullptr && (current_music->state == MusicState::PLAYING)) { stopMusic(); } + music_enabled = value; + } + + // --- Sound --- + + auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound* { + auto sound = std::make_unique(); + Uint8* raw = nullptr; + if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), true, &sound->spec, &raw, &sound->length)) { + std::fprintf(stderr, "Ja::loadSound: Failed to load WAV from memory: %s\n", SDL_GetError()); + return nullptr; + } + sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer + return sound.release(); + } + + auto playSound(Sound* sound, const int loop, const int group) -> int { + if (!sound_enabled || sound == nullptr) { return -1; } + + int channel = 0; + while (channel < MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != ChannelState::FREE) { channel++; } + if (channel == MAX_SIMULTANEOUS_CHANNELS) { + // No hay canal libre, reemplazamos el primero + channel = 0; + } + + return playSoundOnChannel(sound, channel, loop, group); + } + + auto playSoundOnChannel(Sound* sound, const int channel, const int loop, const int group) -> int { + if (!sound_enabled || sound == nullptr) { return -1; } + if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return -1; } + + stopChannel(channel); + + channels[channel].sound = sound; + channels[channel].times = loop; + channels[channel].pos = 0; + channels[channel].group = group; + channels[channel].state = ChannelState::PLAYING; + channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &audio_spec); + + if (channels[channel].stream == nullptr) { + std::fprintf(stderr, "Ja::playSoundOnChannel: Failed to create audio stream!\n"); + channels[channel].state = ChannelState::FREE; + return -1; + } + + SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer.get(), channels[channel].sound->length); + SDL_SetAudioStreamGain(channels[channel].stream, sound_volume[group]); + SDL_BindAudioStream(sdl_audio_device, channels[channel].stream); + + return channel; + } + + void deleteSound(Sound* sound) { + if (sound == nullptr) { return; } + for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; i++) { + if (channels[i].sound == sound) { stopChannel(i); } + } + // buffer es destrueix automàticament via RAII (SdlFreeDeleter). + delete sound; + } + + void pauseChannel(const int channel) { + if (!sound_enabled) { return; } + forEachTargetChannel(channel, [](Channel& ch) { + if (ch.state == ChannelState::PLAYING) { + ch.state = ChannelState::PAUSED; + SDL_UnbindAudioStream(ch.stream); + } + }); + } + + void resumeChannel(const int channel) { + if (!sound_enabled) { return; } + forEachTargetChannel(channel, [](Channel& ch) { + if (ch.state == ChannelState::PAUSED) { + ch.state = ChannelState::PLAYING; + SDL_BindAudioStream(sdl_audio_device, ch.stream); + } + }); + } + + void stopChannel(const int channel) { + forEachTargetChannel(channel, [](Channel& ch) { + if (ch.state != ChannelState::FREE) { + if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); } + ch.stream = nullptr; + ch.state = ChannelState::FREE; + ch.pos = 0; + ch.sound = nullptr; + } + }); + } + + auto setSoundVolume(float volume, const int group) -> float { + const float V = SDL_clamp(volume, 0.0F, 1.0F); + + if (group == -1) { + std::ranges::fill(sound_volume, V); + } else if (group >= 0 && group < MAX_GROUPS) { + sound_volume[group] = V; + } else { + return V; + } + + // Aplicar volum als canals actius. + for (auto& ch : channels) { + if ((ch.state == ChannelState::PLAYING) || (ch.state == ChannelState::PAUSED)) { + if (group == -1 || ch.group == group) { + if (ch.stream != nullptr) { + SDL_SetAudioStreamGain(ch.stream, sound_volume[ch.group]); + } + } + } + } + return V; + } + + void enableSound(const bool value) { + if (!value) { + stopChannel(-1); + } + sound_enabled = value; + } + +} // namespace Ja + +// --- stb_vorbis macro leak cleanup --- +// stb_vorbis.c filtra noms curts (L, C, R i PLAYBACK_*) al TU que el compila. +// Xocarien amb paràmetres de plantilla d'altres headers si aquestes definicions +// s'escapessin. Els netegem al final del TU per tancar la porta. +// clang-format off +#undef L +#undef C +#undef R +#undef PLAYBACK_MONO +#undef PLAYBACK_LEFT +#undef PLAYBACK_RIGHT +// clang-format on diff --git a/source/core/audio/jail_audio.hpp b/source/core/audio/jail_audio.hpp index 94b84cb..dda817f 100644 --- a/source/core/audio/jail_audio.hpp +++ b/source/core/audio/jail_audio.hpp @@ -3,19 +3,17 @@ // --- Includes --- #include -#include #include -#include -#include #include #include #include -// clang-format off -#define STB_VORBIS_HEADER_ONLY -// NOLINTNEXTLINE(bugprone-suspicious-include) -#include "external/stb_vorbis.c" // Para stb_vorbis_open_memory i streaming -// clang-format on +// Forward-declaració del decoder de vorbis. La implementació viu a +// jail_audio.cpp (únic TU que compila external/stb_vorbis.c). Qualsevol caller +// només necessita `stb_vorbis*` per punter — mai per valor — així que el +// forward decl n'hi ha prou i evita arrossegar el .c a tots els TU. +// NOLINTNEXTLINE(readability-identifier-naming) — nom imposat per l'API de stb_vorbis +struct stb_vorbis; // Deleter stateless per a buffers reservats amb `SDL_malloc` / `SDL_LoadWAV*`. // Compatible amb `std::unique_ptr` — zero size overhead @@ -49,7 +47,7 @@ namespace Ja { DISABLED, }; - // --- Struct Definitions --- + // --- Constants --- inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20; inline constexpr int MAX_GROUPS = 2; @@ -58,6 +56,7 @@ namespace Ja { // Audio alimenta des de Defaults::Audio — única font de veritat del projecte. inline constexpr SDL_AudioSpec DEFAULT_SPEC{SDL_AUDIO_S16, 2, 48000}; + // --- Struct Definitions --- struct Sound { SDL_AudioSpec spec{DEFAULT_SPEC}; Uint32 length{0}; @@ -95,7 +94,6 @@ namespace Ja { MusicState state{MusicState::INVALID}; }; - // --- Crossfade / Fade State --- struct FadeState { bool active{false}; Uint64 start_time{0}; @@ -108,527 +106,34 @@ namespace Ja { FadeState fade; }; - // --- Internal Global State (inline, C++17) --- - inline Music* current_music{nullptr}; - inline Channel channels[MAX_SIMULTANEOUS_CHANNELS]; - - inline SDL_AudioSpec audio_spec{DEFAULT_SPEC}; - inline float music_volume{1.0F}; - inline float sound_volume[MAX_GROUPS]; - inline bool music_enabled{true}; - inline bool sound_enabled{true}; - inline SDL_AudioDeviceID sdl_audio_device{0}; - - inline OutgoingMusic outgoing_music; - inline FadeState incoming_fade; - - // --- Forward Declarations --- - inline void stopMusic(); - inline void stopChannel(int channel); - inline auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int; - inline void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1); - - // --- Music streaming internals --- - // Bytes-per-sample per canal (sempre s16) - inline constexpr int 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. - inline constexpr int MUSIC_CHUNK_SHORTS = 8192; - // Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a - // l'SDL_AudioStream per absorbir jitter de frame i evitar underruns. - inline constexpr float 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 auto feedMusicChunk(Music* music) -> int { - if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return 0; } - - short chunk[MUSIC_CHUNK_SHORTS]; - const int NUM_CHANNELS = music->spec.channels; - const int SAMPLES_PER_CHANNEL = stb_vorbis_get_samples_short_interleaved( - music->vorbis, - NUM_CHANNELS, - static_cast(chunk), - MUSIC_CHUNK_SHORTS); - if (SAMPLES_PER_CHANNEL <= 0) { return 0; } - - const int BYTES = SAMPLES_PER_CHANNEL * NUM_CHANNELS * MUSIC_BYTES_PER_SAMPLE; - SDL_PutAudioStreamData(music->stream, static_cast(chunk), BYTES); - return SAMPLES_PER_CHANNEL; - } - - // Reompli l'stream fins que tinga ≥ MUSIC_LOW_WATER_SECONDS bufferats. - // En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar. - inline void pumpMusic(Music* music) { - if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; } - - const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE; - const int LOW_WATER_BYTES = static_cast(MUSIC_LOW_WATER_SECONDS * static_cast(BYTES_PER_SECOND)); - - while (SDL_GetAudioStreamAvailable(music->stream) < LOW_WATER_BYTES) { - const int DECODED = 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; - } - } - } - - // Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream - // siga robat per outgoing_music (crossfade o fade-out). Imprescindible amb - // streaming: l'stream robat no es pot re-alimentar perquè perd la referència - // al seu vorbis decoder. No aplica loop — si el vorbis s'esgota abans, parem. - inline void preFillOutgoing(Music* music, int duration_ms) { - if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; } - - const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE; - const int NEEDED_BYTES = static_cast((static_cast(duration_ms) * BYTES_PER_SECOND) / 1000); - - while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) { - const int DECODED = feedMusicChunk(music); - if (DECODED <= 0) { break; } // EOF: deixem drenar el que hi haja - } - } - - // --- Core Functions --- - - // Retorna el progrés lineal [0..1] d'un fade. 1.0 vol dir completat. Única - // font de la corba del fade: si es vol canviar a logarítmica/quadràtica, - // s'edita aquí i afecta fade-in i fade-out alhora. - inline auto fadeProgress(const FadeState& fade) -> float { - if (fade.duration_ms <= 0) { return 1.0F; } - const Uint64 ELAPSED = SDL_GetTicks() - fade.start_time; - if (ELAPSED >= static_cast(fade.duration_ms)) { return 1.0F; } - return static_cast(ELAPSED) / static_cast(fade.duration_ms); - } - - // Avança el fade-out de l'outgoing music (crossfade o fade-out a silenci). - inline void updateOutgoingFade() { - if (outgoing_music.stream == nullptr || !outgoing_music.fade.active) { return; } - - const float PROGRESS = fadeProgress(outgoing_music.fade); - if (PROGRESS >= 1.0F) { - SDL_DestroyAudioStream(outgoing_music.stream); - outgoing_music.stream = nullptr; - outgoing_music.fade.active = false; - } else { - SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0F - PROGRESS)); - } - } - - // Avança el fade-in de la pista actual (si hi ha un crossfade en curs). - inline void updateIncomingFade() { - if (!incoming_fade.active) { return; } - - const float PROGRESS = fadeProgress(incoming_fade); - if (PROGRESS >= 1.0F) { - incoming_fade.active = false; - SDL_SetAudioStreamGain(current_music->stream, music_volume); - } else { - SDL_SetAudioStreamGain(current_music->stream, music_volume * PROGRESS); - } - } - - // Alimenta la pista actual i la para si s'ha esgotat el stream vorbis. - inline void updateCurrentMusic() { - if (!music_enabled || current_music == nullptr || current_music->state != MusicState::PLAYING) { return; } - - updateIncomingFade(); - - pumpMusic(current_music); - if (current_music->times == 0 && SDL_GetAudioStreamAvailable(current_music->stream) == 0) { - stopMusic(); - } - } - - // Reompli (si cal loop) o para els canals d'efectes actius. - inline void updateSoundChannels() { - if (!sound_enabled) { return; } - - for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) { - if (channels[i].state != ChannelState::PLAYING) { continue; } - - if (channels[i].times != 0) { - if (static_cast(SDL_GetAudioStreamAvailable(channels[i].stream)) < (channels[i].sound->length / 2)) { - SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer.get(), channels[i].sound->length); - if (channels[i].times > 0) { channels[i].times--; } - } - } else if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) { - stopChannel(i); - } - } - } - - inline void update() { - updateOutgoingFade(); - updateCurrentMusic(); - updateSoundChannels(); - } - - inline void init(const int freq, const SDL_AudioFormat format, const int num_channels) { - audio_spec = {.format = format, .channels = num_channels, .freq = freq}; - if (sdl_audio_device != 0) { SDL_CloseAudioDevice(sdl_audio_device); } - sdl_audio_device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec); - if (sdl_audio_device == 0) { std::fprintf(stderr, "Ja: Failed to initialize SDL audio!\n"); } - for (auto& channel : channels) { channel.state = ChannelState::FREE; } - } - - inline void quit() { - if (outgoing_music.stream != nullptr) { - SDL_DestroyAudioStream(outgoing_music.stream); - outgoing_music.stream = nullptr; - } - if (sdl_audio_device != 0) { SDL_CloseAudioDevice(sdl_audio_device); } - sdl_audio_device = 0; - } - - // --- Music Functions --- - - inline auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* { - if (buffer == nullptr || length == 0) { return nullptr; } - - // Allocem el Music primer per aprofitar el seu `std::vector` - // com a propietari del OGG comprimit. stb_vorbis guarda un punter - // persistent al buffer; com que ací no el resize'jem, el .data() és - // estable durant tot el cicle de vida del music. - auto music = std::make_unique(); - music->ogg_data.assign(buffer, buffer + length); - - int vorbis_error = 0; - music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(), - static_cast(length), - &vorbis_error, - nullptr); - if (music->vorbis == nullptr) { - std::fprintf(stderr, "Ja::loadMusic: stb_vorbis_open_memory failed (error %d)\n", vorbis_error); - return nullptr; - } - - const stb_vorbis_info INFO = stb_vorbis_get_info(music->vorbis); - music->spec.channels = static_cast(INFO.channels); - music->spec.freq = static_cast(INFO.sample_rate); - music->spec.format = SDL_AUDIO_S16; - music->state = MusicState::STOPPED; - - return music.release(); - } - - // Overload amb filename. Resource::Cache l'usa per registrar el path dins - // del propi Ja::Music (camp `filename`); la capa Audio l'usa per recuperar - // el nom després d'un playMusic(Ja::Music*, ...) — veure PATCH-02. - inline auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music* { - Music* music = loadMusic(buffer, length); - if (music != nullptr && filename != nullptr) { music->filename = filename; } - return music; - } - - inline void playMusic(Music* music, const int loop = -1) { - if (!music_enabled || music == nullptr || music->vorbis == nullptr) { return; } - - stopMusic(); - - current_music = music; - current_music->state = MusicState::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, &audio_spec); - if (current_music->stream == nullptr) { - std::fprintf(stderr, "Ja::playMusic: Failed to create audio stream!\n"); - current_music->state = MusicState::STOPPED; - return; - } - SDL_SetAudioStreamGain(current_music->stream, music_volume); - - // Pre-cargem el buffer abans de bindejar per evitar un underrun inicial. - pumpMusic(current_music); - - if (!SDL_BindAudioStream(sdl_audio_device, current_music->stream)) { - std::fprintf(stderr, "Ja::playMusic: SDL_BindAudioStream failed!\n"); - } - } - - inline void pauseMusic() { - if (!music_enabled) { return; } - if (current_music == nullptr || current_music->state != MusicState::PLAYING) { return; } - - current_music->state = MusicState::PAUSED; - SDL_UnbindAudioStream(current_music->stream); - } - - inline void resumeMusic() { - if (!music_enabled) { return; } - if (current_music == nullptr || current_music->state != MusicState::PAUSED) { return; } - - current_music->state = MusicState::PLAYING; - SDL_BindAudioStream(sdl_audio_device, current_music->stream); - } - - inline void stopMusic() { - // Limpiar outgoing crossfade si existe - if (outgoing_music.stream != nullptr) { - SDL_DestroyAudioStream(outgoing_music.stream); - outgoing_music.stream = nullptr; - outgoing_music.fade.active = false; - } - incoming_fade.active = false; - - if (current_music == nullptr || current_music->state == MusicState::INVALID || current_music->state == MusicState::STOPPED) { return; } - - current_music->state = MusicState::STOPPED; - if (current_music->stream != nullptr) { - SDL_DestroyAudioStream(current_music->stream); - current_music->stream = nullptr; - } - // Deixem el handle de vorbis viu — es tanca en deleteMusic. - // Rebobinem perquè un futur playMusic comence des del principi. - if (current_music->vorbis != nullptr) { - stb_vorbis_seek_start(current_music->vorbis); - } - } - - // Roba l'stream de la música actual al slot outgoing per a un fade-out de - // `duration_ms` ms. Destrueix qualsevol outgoing anterior. L'stream robat - // ja no es pot alimentar (perd l'accés al vorbis decoder), per això cal - // preFillOutgoing abans del robatori perquè tingui so suficient fins que - // acabi el fade. No-op si no hi ha música sonant amb stream vàlid. - inline void stealCurrentIntoOutgoing(const int duration_ms) { - if (outgoing_music.stream != nullptr) { - SDL_DestroyAudioStream(outgoing_music.stream); - outgoing_music.stream = nullptr; - outgoing_music.fade.active = false; - } - - if (current_music == nullptr || current_music->state != MusicState::PLAYING || current_music->stream == nullptr) { - return; - } - - preFillOutgoing(current_music, duration_ms); - outgoing_music.stream = current_music->stream; - outgoing_music.fade = { - .active = true, - .start_time = SDL_GetTicks(), - .duration_ms = duration_ms, - .initial_volume = music_volume, - }; - current_music->stream = nullptr; - current_music->state = MusicState::STOPPED; - if (current_music->vorbis != nullptr) { stb_vorbis_seek_start(current_music->vorbis); } - } - - inline void fadeOutMusic(const int milliseconds) { - if (!music_enabled) { return; } - if (current_music == nullptr || current_music->state != MusicState::PLAYING) { return; } - - stealCurrentIntoOutgoing(milliseconds); - incoming_fade.active = false; - } - - inline void crossfadeMusic(Music* music, const int crossfade_ms, const int loop) { - if (!music_enabled || music == nullptr || music->vorbis == nullptr) { return; } - - stealCurrentIntoOutgoing(crossfade_ms); - - // Iniciar la nueva pista con gain=0 (el fade-in la sube gradualmente) - current_music = music; - current_music->state = MusicState::PLAYING; - current_music->times = loop; - - stb_vorbis_seek_start(current_music->vorbis); - current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &audio_spec); - if (current_music->stream == nullptr) { - std::fprintf(stderr, "Ja::crossfadeMusic: Failed to create audio stream!\n"); - current_music->state = MusicState::STOPPED; - return; - } - SDL_SetAudioStreamGain(current_music->stream, 0.0F); - pumpMusic(current_music); // pre-carrega abans de bindejar - SDL_BindAudioStream(sdl_audio_device, current_music->stream); - - // Configurar fade-in - incoming_fade = { - .active = true, - .start_time = SDL_GetTicks(), - .duration_ms = crossfade_ms, - .initial_volume = 0.0F, - }; - } - - inline auto getMusicState() -> MusicState { - if (!music_enabled) { return MusicState::DISABLED; } - if (current_music == nullptr) { return MusicState::INVALID; } - - return current_music->state; - } - - inline void deleteMusic(Music* music) { - if (music == nullptr) { return; } - if (current_music == music) { - stopMusic(); - current_music = nullptr; - } - if (music->stream != nullptr) { SDL_DestroyAudioStream(music->stream); } - if (music->vorbis != nullptr) { stb_vorbis_close(music->vorbis); } - // ogg_data (std::vector) i filename (std::string) s'alliberen sols - // al destructor de Music. - delete music; - } - - inline auto setMusicVolume(float volume) -> float { - music_volume = SDL_clamp(volume, 0.0F, 1.0F); - if (current_music != nullptr && current_music->stream != nullptr) { - SDL_SetAudioStreamGain(current_music->stream, music_volume); - } - return music_volume; - } - - inline void enableMusic(const bool value) { - if (!value && current_music != nullptr && (current_music->state == MusicState::PLAYING)) { stopMusic(); } - music_enabled = value; - } - - // --- Sound Functions --- - - inline auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound* { - auto sound = std::make_unique(); - Uint8* raw = nullptr; - if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), true, &sound->spec, &raw, &sound->length)) { - std::fprintf(stderr, "Ja::loadSound: Failed to load WAV from memory: %s\n", SDL_GetError()); - return nullptr; - } - sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer - return sound.release(); - } - - inline auto playSound(Sound* sound, const int loop = 0, const int group = 0) -> int { - if (!sound_enabled || sound == nullptr) { return -1; } - - int channel = 0; - while (channel < MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != ChannelState::FREE) { channel++; } - if (channel == MAX_SIMULTANEOUS_CHANNELS) { - // No hay canal libre, reemplazamos el primero - channel = 0; - } - - return playSoundOnChannel(sound, channel, loop, group); - } - - inline auto playSoundOnChannel(Sound* sound, const int channel, const int loop, const int group) -> int { - if (!sound_enabled || sound == nullptr) { return -1; } - if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return -1; } - - stopChannel(channel); - - channels[channel].sound = sound; - channels[channel].times = loop; - channels[channel].pos = 0; - channels[channel].group = group; - channels[channel].state = ChannelState::PLAYING; - channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &audio_spec); - - if (channels[channel].stream == nullptr) { - std::fprintf(stderr, "Ja::playSoundOnChannel: Failed to create audio stream!\n"); - channels[channel].state = ChannelState::FREE; - return -1; - } - - SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer.get(), channels[channel].sound->length); - SDL_SetAudioStreamGain(channels[channel].stream, sound_volume[group]); - SDL_BindAudioStream(sdl_audio_device, channels[channel].stream); - - return channel; - } - - inline void deleteSound(Sound* sound) { - if (sound == nullptr) { return; } - for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; i++) { - if (channels[i].sound == sound) { stopChannel(i); } - } - // buffer es destrueix automàticament via RAII (SdlFreeDeleter). - delete sound; - } - - // Aplica `fn` als canals objectiu: tots si channel == -1, o només el d'índex - // vàlid si està dins [0, MAX_SIMULTANEOUS_CHANNELS). Centralitza el dispatch - // "tots vs un" que pauseChannel/resumeChannel/stopChannel repetien. - template - inline void forEachTargetChannel(const int channel, Fn&& fn) { - if (channel == -1) { - for (auto& ch : channels) { fn(ch); } - } else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) { - fn(channels[channel]); - } - } - - inline void pauseChannel(const int channel) { - if (!sound_enabled) { return; } - forEachTargetChannel(channel, [](Channel& ch) { - if (ch.state == ChannelState::PLAYING) { - ch.state = ChannelState::PAUSED; - SDL_UnbindAudioStream(ch.stream); - } - }); - } - - inline void resumeChannel(const int channel) { - if (!sound_enabled) { return; } - forEachTargetChannel(channel, [](Channel& ch) { - if (ch.state == ChannelState::PAUSED) { - ch.state = ChannelState::PLAYING; - SDL_BindAudioStream(sdl_audio_device, ch.stream); - } - }); - } - - inline void stopChannel(const int channel) { - forEachTargetChannel(channel, [](Channel& ch) { - if (ch.state != ChannelState::FREE) { - if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); } - ch.stream = nullptr; - ch.state = ChannelState::FREE; - ch.pos = 0; - ch.sound = nullptr; - } - }); - } - - inline auto setSoundVolume(float volume, const int group = -1) -> float { - const float V = SDL_clamp(volume, 0.0F, 1.0F); - - if (group == -1) { - std::ranges::fill(sound_volume, V); - } else if (group >= 0 && group < MAX_GROUPS) { - sound_volume[group] = V; - } else { - return V; - } - - // Aplicar volum als canals actius. - for (auto& ch : channels) { - if ((ch.state == ChannelState::PLAYING) || (ch.state == ChannelState::PAUSED)) { - if (group == -1 || ch.group == group) { - if (ch.stream != nullptr) { - SDL_SetAudioStreamGain(ch.stream, sound_volume[ch.group]); - } - } - } - } - return V; - } - - inline void enableSound(const bool value) { - if (!value) { - stopChannel(-1); - } - sound_enabled = value; - } + // --- Lifecycle --- + void init(int freq, SDL_AudioFormat format, int num_channels); + void quit(); + void update(); + + // --- Music API --- + [[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length) -> Music*; + [[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music*; + void playMusic(Music* music, int loop = -1); + void pauseMusic(); + void resumeMusic(); + void stopMusic(); + void fadeOutMusic(int milliseconds); + void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1); + [[nodiscard]] auto getMusicState() -> MusicState; + void deleteMusic(Music* music); + auto setMusicVolume(float volume) -> float; + void enableMusic(bool value); + + // --- Sound API --- + [[nodiscard]] auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound*; + auto playSound(Sound* sound, int loop = 0, int group = 0) -> int; + auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int; + void deleteSound(Sound* sound); + void pauseChannel(int channel); + void resumeChannel(int channel); + void stopChannel(int channel); + auto setSoundVolume(float volume, int group = -1) -> float; + void enableSound(bool value); } // namespace Ja From 1d124e967de6d95c2711ae77a849c74f7746a96a Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 13:21:01 +0200 Subject: [PATCH 2/6] refactor(core/audio): introduce Ja::Engine class (COUP-01, CONV-02) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Encapsula l'estat del motor a Ja::Engine — classe amb RAII (ctor obre SDL_AudioDevice, dtor el tanca) i `assert` al ctor contra instàncies concurrents. Tots els globals del namespace anònim del commit 1 (current_music, channels[], audio_spec, music_volume, sound_volume[], music_enabled, sound_enabled, sdl_audio_device, outgoing_music, incoming_fade) es converteixen en membres privats amb sufix `_`. Els helpers que toquen estat (stealCurrentIntoOutgoing, updateOutgoing/IncomingFade, updateCurrent*, forEachTargetChannel) passen a mètodes privats. Els stateless (feedMusicChunk, pumpMusic, preFillOutgoing, fadeProgress) es queden al namespace anònim del .cpp. Engine::active() static accessor: registrat al ctor, desregistrat al dtor. Els deleters Ja::deleteMusic/deleteSound consulten Engine::active() per parar canals/música abans d'alliberar recursos; si active() és nullptr (shutdown-order invertit), fan cleanup local silenciós. Això resol CONV-02 (engine ja és classe) i tanca COUP-01. Compat amb els callers: les free functions Ja::init/quit/update, Ja::playMusic, Ja::pauseMusic, Ja::resumeMusic, Ja::stopMusic, Ja::fadeOutMusic, Ja::crossfadeMusic, Ja::getMusicState, Ja::setMusicVolume, Ja::enableMusic, Ja::playSound, Ja::playSoundOnChannel, Ja::pauseChannel, Ja::resumeChannel, Ja::stopChannel, Ja::setSoundVolume i Ja::enableSound es mantenen com a thin forwarders contra Engine::active(). audio.cpp no canvia en aquest commit; les seves crides Ja:: continuen funcionant. Al commit 3 Audio agafarà ownership directe via std::unique_ptr i aquests forwarders desapareixeran. Ja::init() crea un std::unique_ptr propietari dins d'un namespace anònim del .cpp (encara no és Audio qui posseeix — això és el commit 3). Ja::loadMusic/loadSound/deleteMusic/deleteSound es queden com a free functions permanents (no depenen del motor per construir). Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/audio/jail_audio.cpp | 836 +++++++++++++++++-------------- source/core/audio/jail_audio.hpp | 111 +++- 2 files changed, 552 insertions(+), 395 deletions(-) diff --git a/source/core/audio/jail_audio.cpp b/source/core/audio/jail_audio.cpp index 83f8619..289a884 100644 --- a/source/core/audio/jail_audio.cpp +++ b/source/core/audio/jail_audio.cpp @@ -1,7 +1,9 @@ #include "core/audio/jail_audio.hpp" #include +#include #include +#include // Implementació completa de stb_vorbis en aquest TU. HEADER_ONLY es queda // indefinit aquí perquè el .c aporti les definicions. La resta del projecte @@ -13,23 +15,8 @@ namespace Ja { + // --- Streaming internals (file-scope constants) --- namespace { - - // --- Estat intern (internal linkage via anonymous namespace) --- - Music* current_music{nullptr}; - Channel channels[MAX_SIMULTANEOUS_CHANNELS]; - - SDL_AudioSpec audio_spec{DEFAULT_SPEC}; - float music_volume{1.0F}; - float sound_volume[MAX_GROUPS]; - bool music_enabled{true}; - bool sound_enabled{true}; - SDL_AudioDeviceID sdl_audio_device{0}; - - OutgoingMusic outgoing_music; - FadeState incoming_fade; - - // --- Streaming internals --- // Bytes-per-sample per canal (sempre s16) constexpr int MUSIC_BYTES_PER_SAMPLE = 2; // Quants shorts decodifiquem per crida a get_samples_short_interleaved. @@ -39,8 +26,42 @@ namespace Ja { // l'SDL_AudioStream per absorbir jitter de frame i evitar underruns. constexpr float 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). + // Propietari únic de l'Engine que creen els forwarders Ja::init/quit. + // Al commit 3 això desapareix: Audio agafarà ownership directe. + std::unique_ptr owned_engine; + } // namespace + + // --- Engine::active_ storage --- + Engine* Engine::active_ = nullptr; + + auto Engine::active() noexcept -> Engine* { return active_; } + + // --- Ctor/Dtor --- + + Engine::Engine(const int freq, const SDL_AudioFormat format, const int num_channels) { + assert(active_ == nullptr && "Ja::Engine: més d'una instància activa no està suportat"); + active_ = this; + + audio_spec_ = {.format = format, .channels = num_channels, .freq = freq}; + sdl_audio_device_ = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec_); + if (sdl_audio_device_ == 0) { std::fprintf(stderr, "Ja::Engine: Failed to initialize SDL audio!\n"); } + for (auto& channel : channels_) { channel.state = ChannelState::FREE; } + } + + Engine::~Engine() { + if (outgoing_music_.stream != nullptr) { + SDL_DestroyAudioStream(outgoing_music_.stream); + outgoing_music_.stream = nullptr; + } + if (sdl_audio_device_ != 0) { SDL_CloseAudioDevice(sdl_audio_device_); } + sdl_audio_device_ = 0; + + if (active_ == this) { active_ = nullptr; } + } + + // --- Helpers stateless (no toquen membres d'Engine) --- + namespace { + auto feedMusicChunk(Music* music) -> int { if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return 0; } @@ -58,8 +79,6 @@ namespace Ja { return SAMPLES_PER_CHANNEL; } - // Reompli l'stream fins que tinga ≥ MUSIC_LOW_WATER_SECONDS bufferats. - // En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar. void pumpMusic(Music* music) { if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; } @@ -80,11 +99,7 @@ namespace Ja { } } - // Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream - // siga robat per outgoing_music (crossfade o fade-out). Imprescindible amb - // streaming: l'stream robat no es pot re-alimentar perquè perd la referència - // al seu vorbis decoder. No aplica loop — si el vorbis s'esgota abans, parem. - void preFillOutgoing(Music* music, int duration_ms) { + void preFillOutgoing(Music* music, const int duration_ms) { if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; } const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE; @@ -92,7 +107,7 @@ namespace Ja { while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) { const int DECODED = feedMusicChunk(music); - if (DECODED <= 0) { break; } // EOF: deixem drenar el que hi haja + if (DECODED <= 0) { break; } } } @@ -106,132 +121,426 @@ namespace Ja { return static_cast(ELAPSED) / static_cast(fade.duration_ms); } - // Avança el fade-out de l'outgoing music (crossfade o fade-out a silenci). - void updateOutgoingFade() { - if (outgoing_music.stream == nullptr || !outgoing_music.fade.active) { return; } - - const float PROGRESS = fadeProgress(outgoing_music.fade); - if (PROGRESS >= 1.0F) { - SDL_DestroyAudioStream(outgoing_music.stream); - outgoing_music.stream = nullptr; - outgoing_music.fade.active = false; - } else { - SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0F - PROGRESS)); - } - } - - // Avança el fade-in de la pista actual (si hi ha un crossfade en curs). - void updateIncomingFade() { - if (!incoming_fade.active) { return; } - - const float PROGRESS = fadeProgress(incoming_fade); - if (PROGRESS >= 1.0F) { - incoming_fade.active = false; - SDL_SetAudioStreamGain(current_music->stream, music_volume); - } else { - SDL_SetAudioStreamGain(current_music->stream, music_volume * PROGRESS); - } - } - - // Alimenta la pista actual i la para si s'ha esgotat el stream vorbis. - void updateCurrentMusic() { - if (!music_enabled || current_music == nullptr || current_music->state != MusicState::PLAYING) { return; } - - updateIncomingFade(); - - pumpMusic(current_music); - if (current_music->times == 0 && SDL_GetAudioStreamAvailable(current_music->stream) == 0) { - stopMusic(); - } - } - - // Reompli (si cal loop) o para els canals d'efectes actius. - void updateSoundChannels() { - if (!sound_enabled) { return; } - - for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) { - if (channels[i].state != ChannelState::PLAYING) { continue; } - - if (channels[i].times != 0) { - if (static_cast(SDL_GetAudioStreamAvailable(channels[i].stream)) < (channels[i].sound->length / 2)) { - SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer.get(), channels[i].sound->length); - if (channels[i].times > 0) { channels[i].times--; } - } - } else if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) { - stopChannel(i); - } - } - } - - // Roba l'stream de la música actual al slot outgoing per a un fade-out de - // `duration_ms` ms. Destrueix qualsevol outgoing anterior. L'stream robat - // ja no es pot alimentar (perd l'accés al vorbis decoder), per això cal - // preFillOutgoing abans del robatori perquè tingui so suficient fins que - // acabi el fade. No-op si no hi ha música sonant amb stream vàlid. - void stealCurrentIntoOutgoing(const int duration_ms) { - if (outgoing_music.stream != nullptr) { - SDL_DestroyAudioStream(outgoing_music.stream); - outgoing_music.stream = nullptr; - outgoing_music.fade.active = false; - } - - if (current_music == nullptr || current_music->state != MusicState::PLAYING || current_music->stream == nullptr) { - return; - } - - preFillOutgoing(current_music, duration_ms); - outgoing_music.stream = current_music->stream; - outgoing_music.fade = { - .active = true, - .start_time = SDL_GetTicks(), - .duration_ms = duration_ms, - .initial_volume = music_volume, - }; - current_music->stream = nullptr; - current_music->state = MusicState::STOPPED; - if (current_music->vorbis != nullptr) { stb_vorbis_seek_start(current_music->vorbis); } - } - - // Aplica `fn` als canals objectiu: tots si channel == -1, o només el d'índex - // vàlid si està dins [0, MAX_SIMULTANEOUS_CHANNELS). Centralitza el dispatch - // "tots vs un" que pauseChannel/resumeChannel/stopChannel repetien. - template - void forEachTargetChannel(const int channel, Fn&& fn) { - if (channel == -1) { - for (auto& ch : channels) { fn(ch); } - } else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) { - fn(channels[channel]); - } - } - } // namespace - // --- Lifecycle --- + void Engine::updateOutgoingFade() { + if (outgoing_music_.stream == nullptr || !outgoing_music_.fade.active) { return; } - void init(const int freq, const SDL_AudioFormat format, const int num_channels) { - audio_spec = {.format = format, .channels = num_channels, .freq = freq}; - if (sdl_audio_device != 0) { SDL_CloseAudioDevice(sdl_audio_device); } - sdl_audio_device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec); - if (sdl_audio_device == 0) { std::fprintf(stderr, "Ja: Failed to initialize SDL audio!\n"); } - for (auto& channel : channels) { channel.state = ChannelState::FREE; } - } - - void quit() { - if (outgoing_music.stream != nullptr) { - SDL_DestroyAudioStream(outgoing_music.stream); - outgoing_music.stream = nullptr; + const float PROGRESS = fadeProgress(outgoing_music_.fade); + if (PROGRESS >= 1.0F) { + SDL_DestroyAudioStream(outgoing_music_.stream); + outgoing_music_.stream = nullptr; + outgoing_music_.fade.active = false; + } else { + SDL_SetAudioStreamGain(outgoing_music_.stream, outgoing_music_.fade.initial_volume * (1.0F - PROGRESS)); } - if (sdl_audio_device != 0) { SDL_CloseAudioDevice(sdl_audio_device); } - sdl_audio_device = 0; } - void update() { + void Engine::updateIncomingFade() { + if (!incoming_fade_.active) { return; } + + const float PROGRESS = fadeProgress(incoming_fade_); + if (PROGRESS >= 1.0F) { + incoming_fade_.active = false; + SDL_SetAudioStreamGain(current_music_->stream, music_volume_); + } else { + SDL_SetAudioStreamGain(current_music_->stream, music_volume_ * PROGRESS); + } + } + + void Engine::updateCurrentMusic() { + if (!music_enabled_ || current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; } + + updateIncomingFade(); + + pumpMusic(current_music_); + if (current_music_->times == 0 && SDL_GetAudioStreamAvailable(current_music_->stream) == 0) { + stopMusic(); + } + } + + void Engine::updateSoundChannels() { + if (!sound_enabled_) { return; } + + for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) { + if (channels_[i].state != ChannelState::PLAYING) { continue; } + + if (channels_[i].times != 0) { + if (static_cast(SDL_GetAudioStreamAvailable(channels_[i].stream)) < (channels_[i].sound->length / 2)) { + SDL_PutAudioStreamData(channels_[i].stream, channels_[i].sound->buffer.get(), channels_[i].sound->length); + if (channels_[i].times > 0) { channels_[i].times--; } + } + } else if (SDL_GetAudioStreamAvailable(channels_[i].stream) == 0) { + stopChannel(i); + } + } + } + + void Engine::stealCurrentIntoOutgoing(const int duration_ms) { + if (outgoing_music_.stream != nullptr) { + SDL_DestroyAudioStream(outgoing_music_.stream); + outgoing_music_.stream = nullptr; + outgoing_music_.fade.active = false; + } + + if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING || current_music_->stream == nullptr) { + return; + } + + preFillOutgoing(current_music_, duration_ms); + outgoing_music_.stream = current_music_->stream; + outgoing_music_.fade = { + .active = true, + .start_time = SDL_GetTicks(), + .duration_ms = duration_ms, + .initial_volume = music_volume_, + }; + current_music_->stream = nullptr; + current_music_->state = MusicState::STOPPED; + if (current_music_->vorbis != nullptr) { stb_vorbis_seek_start(current_music_->vorbis); } + } + + template + void Engine::forEachTargetChannel(const int channel, Fn&& fn) { + if (channel == -1) { + for (auto& ch : channels_) { fn(ch); } + } else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) { + fn(channels_[channel]); + } + } + + // --- Engine public API --- + + void Engine::update() { updateOutgoingFade(); updateCurrentMusic(); updateSoundChannels(); } - // --- Music --- + void Engine::playMusic(Music* music, const int loop) { + if (!music_enabled_ || music == nullptr || music->vorbis == nullptr) { return; } + + stopMusic(); + + current_music_ = music; + current_music_->state = MusicState::PLAYING; + current_music_->times = loop; + + stb_vorbis_seek_start(current_music_->vorbis); + + current_music_->stream = SDL_CreateAudioStream(¤t_music_->spec, &audio_spec_); + if (current_music_->stream == nullptr) { + std::fprintf(stderr, "Ja::Engine::playMusic: Failed to create audio stream!\n"); + current_music_->state = MusicState::STOPPED; + return; + } + SDL_SetAudioStreamGain(current_music_->stream, music_volume_); + + pumpMusic(current_music_); + + if (!SDL_BindAudioStream(sdl_audio_device_, current_music_->stream)) { + std::fprintf(stderr, "Ja::Engine::playMusic: SDL_BindAudioStream failed!\n"); + } + } + + void Engine::pauseMusic() { + if (!music_enabled_) { return; } + if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; } + + current_music_->state = MusicState::PAUSED; + SDL_UnbindAudioStream(current_music_->stream); + } + + void Engine::resumeMusic() { + if (!music_enabled_) { return; } + if (current_music_ == nullptr || current_music_->state != MusicState::PAUSED) { return; } + + current_music_->state = MusicState::PLAYING; + SDL_BindAudioStream(sdl_audio_device_, current_music_->stream); + } + + void Engine::stopMusic() { + if (outgoing_music_.stream != nullptr) { + SDL_DestroyAudioStream(outgoing_music_.stream); + outgoing_music_.stream = nullptr; + outgoing_music_.fade.active = false; + } + incoming_fade_.active = false; + + if (current_music_ == nullptr || current_music_->state == MusicState::INVALID || current_music_->state == MusicState::STOPPED) { return; } + + current_music_->state = MusicState::STOPPED; + if (current_music_->stream != nullptr) { + SDL_DestroyAudioStream(current_music_->stream); + current_music_->stream = nullptr; + } + if (current_music_->vorbis != nullptr) { + stb_vorbis_seek_start(current_music_->vorbis); + } + } + + void Engine::fadeOutMusic(const int milliseconds) { + if (!music_enabled_) { return; } + if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; } + + stealCurrentIntoOutgoing(milliseconds); + incoming_fade_.active = false; + } + + void Engine::crossfadeMusic(Music* music, const int crossfade_ms, const int loop) { + if (!music_enabled_ || music == nullptr || music->vorbis == nullptr) { return; } + + stealCurrentIntoOutgoing(crossfade_ms); + + current_music_ = music; + current_music_->state = MusicState::PLAYING; + current_music_->times = loop; + + stb_vorbis_seek_start(current_music_->vorbis); + current_music_->stream = SDL_CreateAudioStream(¤t_music_->spec, &audio_spec_); + if (current_music_->stream == nullptr) { + std::fprintf(stderr, "Ja::Engine::crossfadeMusic: Failed to create audio stream!\n"); + current_music_->state = MusicState::STOPPED; + return; + } + SDL_SetAudioStreamGain(current_music_->stream, 0.0F); + pumpMusic(current_music_); + SDL_BindAudioStream(sdl_audio_device_, current_music_->stream); + + incoming_fade_ = { + .active = true, + .start_time = SDL_GetTicks(), + .duration_ms = crossfade_ms, + .initial_volume = 0.0F, + }; + } + + auto Engine::getMusicState() const -> MusicState { + if (!music_enabled_) { return MusicState::DISABLED; } + if (current_music_ == nullptr) { return MusicState::INVALID; } + return current_music_->state; + } + + auto Engine::setMusicVolume(float volume) -> float { + music_volume_ = SDL_clamp(volume, 0.0F, 1.0F); + if (current_music_ != nullptr && current_music_->stream != nullptr) { + SDL_SetAudioStreamGain(current_music_->stream, music_volume_); + } + return music_volume_; + } + + void Engine::enableMusic(const bool value) { + if (!value && current_music_ != nullptr && (current_music_->state == MusicState::PLAYING)) { stopMusic(); } + music_enabled_ = value; + } + + void Engine::onMusicDeleted(const Music* music) { + if (music == nullptr) { return; } + if (current_music_ == music) { + stopMusic(); + current_music_ = nullptr; + } + } + + // --- Sound --- + + auto Engine::playSound(Sound* sound, const int loop, const int group) -> int { + if (!sound_enabled_ || sound == nullptr) { return -1; } + + int channel = 0; + while (channel < MAX_SIMULTANEOUS_CHANNELS && channels_[channel].state != ChannelState::FREE) { channel++; } + if (channel == MAX_SIMULTANEOUS_CHANNELS) { + // No hay canal libre, reemplazamos el primero + channel = 0; + } + + return playSoundOnChannel(sound, channel, loop, group); + } + + auto Engine::playSoundOnChannel(Sound* sound, const int channel, const int loop, const int group) -> int { + if (!sound_enabled_ || sound == nullptr) { return -1; } + if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return -1; } + + stopChannel(channel); + + channels_[channel].sound = sound; + channels_[channel].times = loop; + channels_[channel].pos = 0; + channels_[channel].group = group; + channels_[channel].state = ChannelState::PLAYING; + channels_[channel].stream = SDL_CreateAudioStream(&channels_[channel].sound->spec, &audio_spec_); + + if (channels_[channel].stream == nullptr) { + std::fprintf(stderr, "Ja::Engine::playSoundOnChannel: Failed to create audio stream!\n"); + channels_[channel].state = ChannelState::FREE; + return -1; + } + + SDL_PutAudioStreamData(channels_[channel].stream, channels_[channel].sound->buffer.get(), channels_[channel].sound->length); + SDL_SetAudioStreamGain(channels_[channel].stream, sound_volume_[group]); + SDL_BindAudioStream(sdl_audio_device_, channels_[channel].stream); + + return channel; + } + + void Engine::pauseChannel(const int channel) { + if (!sound_enabled_) { return; } + forEachTargetChannel(channel, [](Channel& ch) { + if (ch.state == ChannelState::PLAYING) { + ch.state = ChannelState::PAUSED; + SDL_UnbindAudioStream(ch.stream); + } + }); + } + + void Engine::resumeChannel(const int channel) { + if (!sound_enabled_) { return; } + const SDL_AudioDeviceID DEVICE = sdl_audio_device_; + forEachTargetChannel(channel, [DEVICE](Channel& ch) { + if (ch.state == ChannelState::PAUSED) { + ch.state = ChannelState::PLAYING; + SDL_BindAudioStream(DEVICE, ch.stream); + } + }); + } + + void Engine::stopChannel(const int channel) { + forEachTargetChannel(channel, [](Channel& ch) { + if (ch.state != ChannelState::FREE) { + if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); } + ch.stream = nullptr; + ch.state = ChannelState::FREE; + ch.pos = 0; + ch.sound = nullptr; + } + }); + } + + auto Engine::setSoundVolume(float volume, const int group) -> float { + const float V = SDL_clamp(volume, 0.0F, 1.0F); + + if (group == -1) { + std::ranges::fill(sound_volume_, V); + } else if (group >= 0 && group < MAX_GROUPS) { + sound_volume_[group] = V; + } else { + return V; + } + + for (auto& ch : channels_) { + if ((ch.state == ChannelState::PLAYING) || (ch.state == ChannelState::PAUSED)) { + if (group == -1 || ch.group == group) { + if (ch.stream != nullptr) { + SDL_SetAudioStreamGain(ch.stream, sound_volume_[ch.group]); + } + } + } + } + return V; + } + + void Engine::enableSound(const bool value) { + if (!value) { + stopChannel(-1); + } + sound_enabled_ = value; + } + + void Engine::onSoundDeleted(const Sound* sound) { + if (sound == nullptr) { return; } + for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) { + if (channels_[i].sound == sound) { stopChannel(i); } + } + } + + // --- Free-function forwarders --- + // Mantenen l'API actual (Ja::update, Ja::playMusic, ...) contra el motor + // actiu. Al commit 3 Audio els substituirà per crides directes a engine_-> + // i els forwarders de música/so desapareixeran; init/quit/update i els + // factories queden de forma permanent. + + void init(const int freq, const SDL_AudioFormat format, const int num_channels) { + owned_engine = std::make_unique(freq, format, num_channels); + } + + void quit() { + owned_engine.reset(); + } + + void update() { + if (Engine* eng = Engine::active()) { eng->update(); } + } + + void playMusic(Music* music, const int loop) { + if (Engine* eng = Engine::active()) { eng->playMusic(music, loop); } + } + + void pauseMusic() { + if (Engine* eng = Engine::active()) { eng->pauseMusic(); } + } + + void resumeMusic() { + if (Engine* eng = Engine::active()) { eng->resumeMusic(); } + } + + void stopMusic() { + if (Engine* eng = Engine::active()) { eng->stopMusic(); } + } + + void fadeOutMusic(const int milliseconds) { + if (Engine* eng = Engine::active()) { eng->fadeOutMusic(milliseconds); } + } + + void crossfadeMusic(Music* music, const int crossfade_ms, const int loop) { + if (Engine* eng = Engine::active()) { eng->crossfadeMusic(music, crossfade_ms, loop); } + } + + auto getMusicState() -> MusicState { + if (const Engine* eng = Engine::active()) { return eng->getMusicState(); } + return MusicState::INVALID; + } + + auto setMusicVolume(float volume) -> float { + if (Engine* eng = Engine::active()) { return eng->setMusicVolume(volume); } + return 0.0F; + } + + void enableMusic(const bool value) { + if (Engine* eng = Engine::active()) { eng->enableMusic(value); } + } + + auto playSound(Sound* sound, const int loop, const int group) -> int { + if (Engine* eng = Engine::active()) { return eng->playSound(sound, loop, group); } + return -1; + } + + auto playSoundOnChannel(Sound* sound, const int channel, const int loop, const int group) -> int { + if (Engine* eng = Engine::active()) { return eng->playSoundOnChannel(sound, channel, loop, group); } + return -1; + } + + void pauseChannel(const int channel) { + if (Engine* eng = Engine::active()) { eng->pauseChannel(channel); } + } + + void resumeChannel(const int channel) { + if (Engine* eng = Engine::active()) { eng->resumeChannel(channel); } + } + + void stopChannel(const int channel) { + if (Engine* eng = Engine::active()) { eng->stopChannel(channel); } + } + + auto setSoundVolume(float volume, const int group) -> float { + if (Engine* eng = Engine::active()) { return eng->setSoundVolume(volume, group); } + return 0.0F; + } + + void enableSound(const bool value) { + if (Engine* eng = Engine::active()) { eng->enableSound(value); } + } + + // --- Factories i destructors (permanents) --- auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* { if (buffer == nullptr || length == 0) { return nullptr; } @@ -271,147 +580,18 @@ namespace Ja { return music; } - void playMusic(Music* music, const int loop) { - if (!music_enabled || music == nullptr || music->vorbis == nullptr) { return; } - - stopMusic(); - - current_music = music; - current_music->state = MusicState::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, &audio_spec); - if (current_music->stream == nullptr) { - std::fprintf(stderr, "Ja::playMusic: Failed to create audio stream!\n"); - current_music->state = MusicState::STOPPED; - return; - } - SDL_SetAudioStreamGain(current_music->stream, music_volume); - - // Pre-cargem el buffer abans de bindejar per evitar un underrun inicial. - pumpMusic(current_music); - - if (!SDL_BindAudioStream(sdl_audio_device, current_music->stream)) { - std::fprintf(stderr, "Ja::playMusic: SDL_BindAudioStream failed!\n"); - } - } - - void pauseMusic() { - if (!music_enabled) { return; } - if (current_music == nullptr || current_music->state != MusicState::PLAYING) { return; } - - current_music->state = MusicState::PAUSED; - SDL_UnbindAudioStream(current_music->stream); - } - - void resumeMusic() { - if (!music_enabled) { return; } - if (current_music == nullptr || current_music->state != MusicState::PAUSED) { return; } - - current_music->state = MusicState::PLAYING; - SDL_BindAudioStream(sdl_audio_device, current_music->stream); - } - - void stopMusic() { - // Limpiar outgoing crossfade si existe - if (outgoing_music.stream != nullptr) { - SDL_DestroyAudioStream(outgoing_music.stream); - outgoing_music.stream = nullptr; - outgoing_music.fade.active = false; - } - incoming_fade.active = false; - - if (current_music == nullptr || current_music->state == MusicState::INVALID || current_music->state == MusicState::STOPPED) { return; } - - current_music->state = MusicState::STOPPED; - if (current_music->stream != nullptr) { - SDL_DestroyAudioStream(current_music->stream); - current_music->stream = nullptr; - } - // Deixem el handle de vorbis viu — es tanca en deleteMusic. - // Rebobinem perquè un futur playMusic comence des del principi. - if (current_music->vorbis != nullptr) { - stb_vorbis_seek_start(current_music->vorbis); - } - } - - void fadeOutMusic(const int milliseconds) { - if (!music_enabled) { return; } - if (current_music == nullptr || current_music->state != MusicState::PLAYING) { return; } - - stealCurrentIntoOutgoing(milliseconds); - incoming_fade.active = false; - } - - void crossfadeMusic(Music* music, const int crossfade_ms, const int loop) { - if (!music_enabled || music == nullptr || music->vorbis == nullptr) { return; } - - stealCurrentIntoOutgoing(crossfade_ms); - - // Iniciar la nueva pista con gain=0 (el fade-in la sube gradualmente) - current_music = music; - current_music->state = MusicState::PLAYING; - current_music->times = loop; - - stb_vorbis_seek_start(current_music->vorbis); - current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &audio_spec); - if (current_music->stream == nullptr) { - std::fprintf(stderr, "Ja::crossfadeMusic: Failed to create audio stream!\n"); - current_music->state = MusicState::STOPPED; - return; - } - SDL_SetAudioStreamGain(current_music->stream, 0.0F); - pumpMusic(current_music); // pre-carrega abans de bindejar - SDL_BindAudioStream(sdl_audio_device, current_music->stream); - - // Configurar fade-in - incoming_fade = { - .active = true, - .start_time = SDL_GetTicks(), - .duration_ms = crossfade_ms, - .initial_volume = 0.0F, - }; - } - - auto getMusicState() -> MusicState { - if (!music_enabled) { return MusicState::DISABLED; } - if (current_music == nullptr) { return MusicState::INVALID; } - - return current_music->state; - } - void deleteMusic(Music* music) { if (music == nullptr) { return; } - if (current_music == music) { - stopMusic(); - current_music = nullptr; - } + // Notifiquem el motor actiu perquè pari la pista si és la current_music. + // Si no hi ha motor (shutdown-order invertit), passem: els recursos + // propis del Music es lliberen igualment a sota. + if (Engine* eng = Engine::active()) { eng->onMusicDeleted(music); } + if (music->stream != nullptr) { SDL_DestroyAudioStream(music->stream); } if (music->vorbis != nullptr) { stb_vorbis_close(music->vorbis); } - // ogg_data (std::vector) i filename (std::string) s'alliberen sols - // al destructor de Music. delete music; } - auto setMusicVolume(float volume) -> float { - music_volume = SDL_clamp(volume, 0.0F, 1.0F); - if (current_music != nullptr && current_music->stream != nullptr) { - SDL_SetAudioStreamGain(current_music->stream, music_volume); - } - return music_volume; - } - - void enableMusic(const bool value) { - if (!value && current_music != nullptr && (current_music->state == MusicState::PLAYING)) { stopMusic(); } - music_enabled = value; - } - - // --- Sound --- - auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound* { auto sound = std::make_unique(); Uint8* raw = nullptr; @@ -423,117 +603,13 @@ namespace Ja { return sound.release(); } - auto playSound(Sound* sound, const int loop, const int group) -> int { - if (!sound_enabled || sound == nullptr) { return -1; } - - int channel = 0; - while (channel < MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != ChannelState::FREE) { channel++; } - if (channel == MAX_SIMULTANEOUS_CHANNELS) { - // No hay canal libre, reemplazamos el primero - channel = 0; - } - - return playSoundOnChannel(sound, channel, loop, group); - } - - auto playSoundOnChannel(Sound* sound, const int channel, const int loop, const int group) -> int { - if (!sound_enabled || sound == nullptr) { return -1; } - if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return -1; } - - stopChannel(channel); - - channels[channel].sound = sound; - channels[channel].times = loop; - channels[channel].pos = 0; - channels[channel].group = group; - channels[channel].state = ChannelState::PLAYING; - channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &audio_spec); - - if (channels[channel].stream == nullptr) { - std::fprintf(stderr, "Ja::playSoundOnChannel: Failed to create audio stream!\n"); - channels[channel].state = ChannelState::FREE; - return -1; - } - - SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer.get(), channels[channel].sound->length); - SDL_SetAudioStreamGain(channels[channel].stream, sound_volume[group]); - SDL_BindAudioStream(sdl_audio_device, channels[channel].stream); - - return channel; - } - void deleteSound(Sound* sound) { if (sound == nullptr) { return; } - for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; i++) { - if (channels[i].sound == sound) { stopChannel(i); } - } + if (Engine* eng = Engine::active()) { eng->onSoundDeleted(sound); } // buffer es destrueix automàticament via RAII (SdlFreeDeleter). delete sound; } - void pauseChannel(const int channel) { - if (!sound_enabled) { return; } - forEachTargetChannel(channel, [](Channel& ch) { - if (ch.state == ChannelState::PLAYING) { - ch.state = ChannelState::PAUSED; - SDL_UnbindAudioStream(ch.stream); - } - }); - } - - void resumeChannel(const int channel) { - if (!sound_enabled) { return; } - forEachTargetChannel(channel, [](Channel& ch) { - if (ch.state == ChannelState::PAUSED) { - ch.state = ChannelState::PLAYING; - SDL_BindAudioStream(sdl_audio_device, ch.stream); - } - }); - } - - void stopChannel(const int channel) { - forEachTargetChannel(channel, [](Channel& ch) { - if (ch.state != ChannelState::FREE) { - if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); } - ch.stream = nullptr; - ch.state = ChannelState::FREE; - ch.pos = 0; - ch.sound = nullptr; - } - }); - } - - auto setSoundVolume(float volume, const int group) -> float { - const float V = SDL_clamp(volume, 0.0F, 1.0F); - - if (group == -1) { - std::ranges::fill(sound_volume, V); - } else if (group >= 0 && group < MAX_GROUPS) { - sound_volume[group] = V; - } else { - return V; - } - - // Aplicar volum als canals actius. - for (auto& ch : channels) { - if ((ch.state == ChannelState::PLAYING) || (ch.state == ChannelState::PAUSED)) { - if (group == -1 || ch.group == group) { - if (ch.stream != nullptr) { - SDL_SetAudioStreamGain(ch.stream, sound_volume[ch.group]); - } - } - } - } - return V; - } - - void enableSound(const bool value) { - if (!value) { - stopChannel(-1); - } - sound_enabled = value; - } - } // namespace Ja // --- stb_vorbis macro leak cleanup --- diff --git a/source/core/audio/jail_audio.hpp b/source/core/audio/jail_audio.hpp index dda817f..ccba597 100644 --- a/source/core/audio/jail_audio.hpp +++ b/source/core/audio/jail_audio.hpp @@ -24,10 +24,10 @@ struct SdlFreeDeleter { } }; -// Engine de baix nivell d'àudio del projecte jailgames: streaming OGG (stb_vorbis) -// + N canals d'efectes (SDL3 audio). No depèn d'Options ni de cap singleton del -// joc; només de SDL3 i stb_vorbis. La capa superior (Audio) li passa recursos -// pel punter i fa el bookkeeping d'usuari. +// Motor de baix nivell d'àudio del projecte jailgames: streaming OGG +// (stb_vorbis) + N canals d'efectes (SDL3 audio). No depèn d'Options ni de cap +// singleton del joc; només de SDL3 i stb_vorbis. La capa superior (Audio) li +// passa recursos pel punter i fa el bookkeeping d'usuari. namespace Ja { // --- Public Enums --- @@ -51,9 +51,9 @@ namespace Ja { inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20; inline constexpr int MAX_GROUPS = 2; - // Spec de fallback del dispositiu. S'aplica abans de Ja::init() i com a valor - // inicial de Sound/Music. L'spec real d'ús l'imposa Ja::init(), que la capa - // Audio alimenta des de Defaults::Audio — única font de veritat del projecte. + // Spec de fallback del dispositiu. S'aplica abans que l'Engine s'iniciï i + // com a valor inicial de Sound/Music. L'spec real d'ús l'imposa el ctor + // d'Engine, alimentat des de Defaults::Audio via Audio. inline constexpr SDL_AudioSpec DEFAULT_SPEC{SDL_AUDIO_S16, 2, 48000}; // --- Struct Definitions --- @@ -106,14 +106,88 @@ namespace Ja { FadeState fade; }; - // --- Lifecycle --- + // --- Engine --- + // Encapsula tot l'estat que abans vivia com a globals inline. Un sol Engine + // viu per procés (enforceat via assert al ctor contra `active_`). El ctor + // obre el device SDL; el dtor el tanca (RAII). Els deleters + // `Ja::deleteMusic`/`Ja::deleteSound` accedeixen al motor actiu via + // `Engine::active()` per parar canals abans d'alliberar. + class Engine { + public: + Engine(int freq, SDL_AudioFormat format, int num_channels); + ~Engine(); + Engine(const Engine&) = delete; + auto operator=(const Engine&) -> Engine& = delete; + Engine(Engine&&) = delete; + auto operator=(Engine&&) -> Engine& = delete; + + // Retorna el motor actiu o nullptr si cap ha estat construït. L'usen + // els deleters de recursos perquè no els arriba cap referència directa. + [[nodiscard]] static auto active() noexcept -> Engine*; + + void update(); + + // --- Música --- + void playMusic(Music* music, int loop = -1); + void pauseMusic(); + void resumeMusic(); + void stopMusic(); + void fadeOutMusic(int milliseconds); + void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1); + [[nodiscard]] auto getMusicState() const -> MusicState; + auto setMusicVolume(float volume) -> float; + void enableMusic(bool value); + // Notifica al motor que un Music s'està destruint: si és el current_music + // s'atura abans que els seus recursos (stream/vorbis) deixin de ser vàlids. + void onMusicDeleted(const Music* music); + + // --- So --- + auto playSound(Sound* sound, int loop = 0, int group = 0) -> int; + auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int; + void pauseChannel(int channel); + void resumeChannel(int channel); + void stopChannel(int channel); + auto setSoundVolume(float volume, int group = -1) -> float; + void enableSound(bool value); + // Notifica al motor que un Sound s'està destruint: els canals que el + // referenciïn es paren abans d'alliberar el buffer. + void onSoundDeleted(const Sound* sound); + + private: + void stealCurrentIntoOutgoing(int duration_ms); + void updateOutgoingFade(); + void updateIncomingFade(); + void updateCurrentMusic(); + void updateSoundChannels(); + + template + void forEachTargetChannel(int channel, Fn&& fn); + + Music* current_music_{nullptr}; + Channel channels_[MAX_SIMULTANEOUS_CHANNELS]{}; + SDL_AudioSpec audio_spec_{DEFAULT_SPEC}; + float music_volume_{1.0F}; + float sound_volume_[MAX_GROUPS]{}; + bool music_enabled_{true}; + bool sound_enabled_{true}; + SDL_AudioDeviceID sdl_audio_device_{0}; + OutgoingMusic outgoing_music_; + FadeState incoming_fade_; + + // NOLINTNEXTLINE(readability-identifier-naming) — convenció projecte: private static amb sufix _ + static Engine* active_; + }; + + // --- Lifecycle forwarders --- + // Compat amb els callers actuals. Creen/destrueixen l'Engine únic via + // make_unique emmagatzemat al propi .cpp. Al commit següent Audio agafarà + // ownership directe i aquests forwarders desapareixeran per a la capa de + // control; els de música/so romanen només el temps de la migració. void init(int freq, SDL_AudioFormat format, int num_channels); void quit(); void update(); - // --- Music API --- - [[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length) -> Music*; - [[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music*; + // --- Music API forwarders (temporal, eliminats al commit 3) --- void playMusic(Music* music, int loop = -1); void pauseMusic(); void resumeMusic(); @@ -121,19 +195,26 @@ namespace Ja { void fadeOutMusic(int milliseconds); void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1); [[nodiscard]] auto getMusicState() -> MusicState; - void deleteMusic(Music* music); auto setMusicVolume(float volume) -> float; void enableMusic(bool value); - // --- Sound API --- - [[nodiscard]] auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound*; + // --- Sound API forwarders (temporal, eliminats al commit 3) --- auto playSound(Sound* sound, int loop = 0, int group = 0) -> int; auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int; - void deleteSound(Sound* sound); void pauseChannel(int channel); void resumeChannel(int channel); void stopChannel(int channel); auto setSoundVolume(float volume, int group = -1) -> float; void enableSound(bool value); + // --- Factories i destructors (permanents) --- + // No depenen de l'estat del motor: loadMusic/loadSound només construeixen + // objectes, deleteMusic/deleteSound consulten Engine::active() per parar + // canals abans d'alliberar (si el motor encara viu). + [[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length) -> Music*; + [[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music*; + void deleteMusic(Music* music); + [[nodiscard]] auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound*; + void deleteSound(Sound* sound); + } // namespace Ja From 961a59c460354fd56d138dd8ac9b1ea86078e2b1 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 13:25:03 +0200 Subject: [PATCH 3/6] refactor(core/audio): Audio owns the engine (migration) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audio guanya un std::unique_ptr engine_ com a membre privat. Es construeix a initSDLAudio amb els defaults de Defaults::Audio; es destrueix sol al ~Audio (cap Ja::quit() explícit). Totes les crides Ja::funcióN(...) d'audio.cpp passen a engine_->funcióN: - playMusicInternal: crossfadeMusic/stopMusic/playMusic - pauseMusic, resumeMusic, stopMusic, fadeOutMusic - playSound x2, stopAllSounds - setSoundVolume, setMusicVolume - getMusicState (via instance->engine_ — és static però pot arribar-hi) Les free functions forwarder del commit 2 (Ja::init/quit/update/playMusic/ pauseMusic/resumeMusic/stopMusic/fadeOutMusic/crossfadeMusic/ getMusicState/setMusicVolume/enableMusic i els equivalents de so) es retiren del .hpp i del .cpp: ja no les crida ningú. Queden com a free functions permanents (les usa Resource::Cache): loadMusic (x2), loadSound, deleteMusic, deleteSound. Aquests darrers consulten Engine::active() per notificar el motor abans d'alliberar. audio.hpp forward-declara també `class Ja::Engine` perquè engine_ és un std::unique_ptr i el dtor es defineix out-of-line al .cpp (= default) — així no cal incloure jail_audio.hpp al header. Els NOLINT(readability-make-member-function-const) d'audio.cpp es mantenen amb raó actualitzada: std::unique_ptr::operator-> retorna T* (no const T*), per tant clang-tidy veu un mètode d'Audio que podria ser const encara que muti l'Engine via el punter. La mutació és real; el NOLINT segueix sent honest. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/audio/audio.cpp | 76 +++++++++++++------------- source/core/audio/audio.hpp | 19 ++++--- source/core/audio/jail_audio.cpp | 91 -------------------------------- source/core/audio/jail_audio.hpp | 29 ---------- 4 files changed, 50 insertions(+), 165 deletions(-) diff --git a/source/core/audio/audio.cpp b/source/core/audio/audio.cpp index 06f4192..9e8c5ea 100644 --- a/source/core/audio/audio.cpp +++ b/source/core/audio/audio.cpp @@ -31,15 +31,14 @@ auto Audio::get() -> Audio* { return Audio::instance.get(); } Audio::Audio(const Config& config) : config_(config) { initSDLAudio(); } -// Destructor -Audio::~Audio() { - Ja::quit(); -} +// Destructor: engine_ és std::unique_ptr, el seu dtor tanca el device SDL i +// desregistra Ja::Engine::active_. Cap crida explícita necessària. +Audio::~Audio() = default; -// Mètode principal: l'estat de la música el manté Ja (única font de veritat), -// per tant no cal cap sincronització aquí. +// Mètode principal: l'estat de la música el manté el motor (única font de +// veritat), per tant no cal cap sincronització aquí. void Audio::update() { - Ja::update(); + if (instance && instance->engine_) { instance->engine_->update(); } } // Reprodueix la música per nom (amb crossfade opcional) @@ -78,70 +77,70 @@ void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms) void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossfade_ms) { const bool CURRENTLY_PLAYING = (getMusicState() == MusicState::PLAYING); if (crossfade_ms > 0 && CURRENTLY_PLAYING) { - Ja::crossfadeMusic(music, crossfade_ms, loop); + engine_->crossfadeMusic(music, crossfade_ms, loop); } else { if (CURRENTLY_PLAYING) { - Ja::stopMusic(); + engine_->stopMusic(); } - Ja::playMusic(music, loop); + engine_->playMusic(music, loop); } music_.loop = (loop != 0); } -// Pausa la música (l'estat el transiciona Ja::pauseMusic) -// NOLINTNEXTLINE(readability-make-member-function-const) — muta Ja::current_music->state +// Pausa la música (l'estat el transiciona Engine::pauseMusic) +// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::pauseMusic() { if (music_enabled_ && getMusicState() == MusicState::PLAYING) { - Ja::pauseMusic(); + engine_->pauseMusic(); } } -// Continua la música pausada (l'estat el transiciona Ja::resumeMusic) -// NOLINTNEXTLINE(readability-make-member-function-const) — muta Ja::current_music->state +// Continua la música pausada (l'estat el transiciona Engine::resumeMusic) +// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::resumeMusic() { if (music_enabled_ && getMusicState() == MusicState::PAUSED) { - Ja::resumeMusic(); + engine_->resumeMusic(); } } -// Detiene la música (l'estat el transiciona Ja::stopMusic) -// NOLINTNEXTLINE(readability-make-member-function-const) — muta Ja::current_music/outgoing_music +// Atura la música (l'estat el transiciona Engine::stopMusic) +// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::stopMusic() { if (music_enabled_) { - Ja::stopMusic(); + engine_->stopMusic(); } } // Reprodueix un so per nom -// NOLINTNEXTLINE(readability-make-member-function-const) — muta globals de Ja::channels +// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::playSound(const std::string& name, Group group) { if (sound_enabled_) { - Ja::playSound(AudioResource::getSound(name), 0, static_cast(group)); + engine_->playSound(AudioResource::getSound(name), 0, static_cast(group)); } } // Reprodueix un so per punter directe -// NOLINTNEXTLINE(readability-make-member-function-const) — muta globals de Ja::channels +// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::playSound(Ja::Sound* sound, Group group) { if (sound_enabled_ && sound != nullptr) { - Ja::playSound(sound, 0, static_cast(group)); + engine_->playSound(sound, 0, static_cast(group)); } } // Atura tots els sons -// NOLINTNEXTLINE(readability-make-member-function-const) — muta globals de Ja::channels +// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::stopAllSounds() { if (sound_enabled_) { - Ja::stopChannel(-1); + engine_->stopChannel(-1); } } // Fa una fosa de sortida de la música -// NOLINTNEXTLINE(readability-make-member-function-const) — muta Ja::outgoing_music/incoming_fade +// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::fadeOutMusic(int milliseconds) { if (music_enabled_ && getMusicState() == MusicState::PLAYING) { - Ja::fadeOutMusic(milliseconds); + engine_->fadeOutMusic(milliseconds); } } @@ -149,7 +148,8 @@ void Audio::fadeOutMusic(int milliseconds) { // exposa Audio (INVALID/DISABLED de Ja col·lapsen a STOPPED — la capa d'usuari // només vol saber si està sonant, pausat o parat). auto Audio::getMusicState() -> MusicState { - switch (Ja::getMusicState()) { + if (!instance || !instance->engine_) { return MusicState::STOPPED; } + switch (instance->engine_->getMusicState()) { case Ja::MusicState::PLAYING: return MusicState::PLAYING; case Ja::MusicState::PAUSED: @@ -171,15 +171,15 @@ auto Audio::effectiveVolume(float volume, bool channel_enabled) const -> float { } // Estableix el volum dels sons (float 0.0..1.0) -// NOLINTNEXTLINE(readability-make-member-function-const) — muta Ja::sound_volume[] i canals actius +// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::setSoundVolume(float sound_volume, Group group) { - Ja::setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast(group)); + engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast(group)); } // Estableix el volum de la música (float 0.0..1.0) -// NOLINTNEXTLINE(readability-make-member-function-const) — muta Ja::music_volume i stream actual +// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::setMusicVolume(float music_volume) { - Ja::setMusicVolume(effectiveVolume(music_volume, music_enabled_)); + engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_)); } // Aplica una nova configuració (substitueix la config cachejada i reaplica enables/volums) @@ -212,14 +212,14 @@ void Audio::enableMusic(bool value) { setMusicVolume(config_.music_volume); } -// Inicialitza SDL Audio +// Inicialitza SDL Audio i el motor Ja::Engine owned. void Audio::initSDLAudio() { if (!SDL_Init(SDL_INIT_AUDIO)) { std::fprintf(stderr, "Audio: SDL_AUDIO could not initialize! SDL Error: %s\n", SDL_GetError()); - } else { - Ja::init(Defaults::Audio::FREQUENCY, Defaults::Audio::FORMAT, Defaults::Audio::CHANNELS); - sound_enabled_ = config_.sound_enabled; - music_enabled_ = config_.music_enabled; - enable(config_.enabled); + return; } + engine_ = std::make_unique(Defaults::Audio::FREQUENCY, Defaults::Audio::FORMAT, Defaults::Audio::CHANNELS); + sound_enabled_ = config_.sound_enabled; + music_enabled_ = config_.music_enabled; + enable(config_.enabled); } diff --git a/source/core/audio/audio.hpp b/source/core/audio/audio.hpp index 4d21c2f..f64254a 100644 --- a/source/core/audio/audio.hpp +++ b/source/core/audio/audio.hpp @@ -6,9 +6,13 @@ #include // Per a string #include // Per a move -// Forward-declares per no incloure core/audio/jail_audio.hpp al header (conté -// l'engine complet de streaming amb stb_vorbis). +// Forward-declares per no incloure core/audio/jail_audio.hpp al header. Els +// tres símbols (Music/Sound per al punter que exposa la API i Engine per al +// std::unique_ptr membre) s'usen només per punter al header, així que +// el forward-decl basta. El ~Audio() en .cpp veu la definició completa i +// instancia correctament el dtor de l'unique_ptr. namespace Ja { + class Engine; struct Music; struct Sound; } // namespace Ja @@ -126,9 +130,10 @@ class Audio { // --- Variables membre --- static std::unique_ptr