From 1d124e967de6d95c2711ae77a849c74f7746a96a Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 13:21:01 +0200 Subject: [PATCH] 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