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/docs/audit-core-audio.md b/docs/audit-core-audio.md index a5b3606..30fa289 100644 --- a/docs/audit-core-audio.md +++ b/docs/audit-core-audio.md @@ -1,7 +1,8 @@ # Auditoría `core/audio/` -Rama: `audit/core-audio` -Estado global: neteja aplicada. 17 ítems `DONE` (+1 parcial), 4 `WONTFIX`, 4 `DEFERRED`. Els `DEFERRED` apunten al refactor pendent d'encapsular `Ja::*` en classe (COUP-01), que arrossegarà COUP-02, PATCH-03, PATCH-05, CONV-02, CONV-03 i la meitat restant d'INCONS-05. +Rama inicial: `audit/core-audio` (ítems DONE/WONTFIX/DEFERRED inicials). +Rama posterior: `refactor/audio-engine-class` — tanca els `DEFERRED` principals (COUP-01/02, PATCH-03, CONV-02/03, INCONS-05 complet). +Estado global: auditoria + refactor aplicats. 22 ítems `DONE`, 4 `WONTFIX`, 1 `DEFERRED` (PATCH-05 — política voice-stealing, és feature no deute). Estados posibles: - `PENDING` — identificado, sin tocar @@ -18,7 +19,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | ID | Título | Estado | |----|--------|--------| -| COUP-01 | Encapsular globals `Ja::` en clase (engine sin estado global) | DEFERRED | +| COUP-01 | Encapsular globals `Ja::` en clase (engine sin estado global) | DONE | | HC-02 | Formato/canales/freq hardcodeado en 3+ sitios | DONE | | INCONS-02 | Doble `MusicState` + `getRealMusicState` como cache paralelo | DONE | | HC-01 | Parámetros de audio no configurables (viola convención del proyecto) | DONE | @@ -52,7 +53,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic |----|---------------|----------|-----------|--------|-------| | PATCH-01 | `audio.hpp:98,105,111` | `toggleEnabled/toggleSound/toggleMusic` sólo flipan el flag; no reaplican volúmenes | Importante | DONE | `toggle*()` i variants sense bool passen per `enable[Sound|Music](bool)`. Aquestes funcions ara reapliquen el volum cap a `Ja::` perquè els canals actius responguin al toggle | | PATCH-02 | `audio.cpp:98` | `music_.name.clear()` en overload por puntero → `getCurrentMusicName()` miente | Importante | DONE | `music_.name = music->filename`. Si `Ja::Music` va ser carregat amb filename, `getCurrentMusicName()` ara retorna el nom correcte; si no, queda buit (contracte explícit) | -| PATCH-03 | `audio.cpp:14-21` | `#undef L,C,R,PLAYBACK_*` workaround para macros filtradas por stb_vorbis | Importante | WONTFIX | Documentado; aislar stb_vorbis en TU propio sería más invasivo — se hará con COUP-01 | +| PATCH-03 | `audio.cpp:14-21` | `#undef L,C,R,PLAYBACK_*` workaround para macros filtradas por stb_vorbis | Importante | DONE | `stb_vorbis.c` + `#undef` migrats a `jail_audio.cpp` (TU únic). `audio.cpp` neta d'aquest workaround | | PATCH-04 | `jail_audio.hpp:501-507` | `setMusicPosition`/`getMusicPosition` stubs no-op | Importante | DONE | Eliminats (veure DEAD-01) | | PATCH-05 | `jail_audio.hpp:532-535` | "No hay canal libre → canal 0" robo de voz arbitrario | Importante | DEFERRED | Política de voice-stealing es una feature; se documenta | @@ -60,8 +61,8 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas | |----|---------------|----------|-----------|--------|-------| -| COUP-01 | `jail_audio.hpp:107-118` | `inline` globals en namespace: estado del engine sin encapsulación | Crítico | DEFERRED | Refactor grande; se hará en fase posterior | -| COUP-02 | `jail_audio.hpp` entero | Engine header-only: 678 líneas compilándose en cada TU | Importante | DEFERRED | Depende de COUP-01 | +| COUP-01 | `jail_audio.hpp:107-118` | `inline` globals en namespace: estado del engine sin encapsulación | Crítico | DONE | `Ja::Engine` classe amb RAII; `Audio` en posseeix `std::unique_ptr`. `Engine::active()` static accessor per als deleters de `Resource::Cache`. Branca `refactor/audio-engine-class` | +| COUP-02 | `jail_audio.hpp` entero | Engine header-only: 678 líneas compilándose en cada TU | Importante | DONE | `jail_audio.cpp` nou conté totes les definicions + `stb_vorbis.c`. El `.hpp` baixa a ~130 línies de declaracions | | COUP-03 | `audio.cpp:3`, `jail_audio.hpp:8` | `#include ` sólo para `std::cout` de errores | Importante | DONE | `` → `` als dos fitxers. `jail_audio.hpp` ja no arrossega iostream a cada TU que l'inclou. Missatges prefixats `Ja::` / `Audio:` (convenció `Module: message` del projecte) | | COUP-04 | `audio.cpp:43-45`, `208` | `Ja::init` en `initSDLAudio` vs `Ja::quit` en destructor (asimetría) | Menor | WONTFIX | Convive con singleton; se revisará con COUP-01 | | COUP-05 | `audio.hpp:71,79` | API pública expone `Ja::Music*`/`Ja::Sound*` | Menor | WONTFIX | Forward-declare ya mitiga; evita doble lookup por nombre | @@ -74,7 +75,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | INCONS-02 | `audio.hpp:44-48` + `jail_audio.hpp:44-50` | Dos `MusicState` paralelos + `getRealMusicState` como puente | Importante | DONE | Eliminat `music_.state` (cache) i `getRealMusicState`. `getMusicState()` consulta Ja::* directament i projecta INVALID/DISABLED → STOPPED. La font de veritat passa a ser Ja. `update()` ja no necessita sincronització | | INCONS-03 | `audio.cpp` varios | Comentarios mezclan catalán y español | Menor | DONE | Tots els comentaris en castellà passats a català (incloent-hi els dels enums, constants, `--- Secció ---`, includes `Per a ...`, variables membre). Resta els comentaris en català que ja hi eren | | INCONS-04 | `audio.hpp:70,78` | `playMusic` `loop=-1` vs `playSound` `loop=0` | Menor | WONTFIX | Convención razonable (música loop / sonido one-shot); documentar | -| INCONS-05 | `audio.hpp:102-105` | 4 formas de enable/disable sound (API redundante) + divergencia con `Ja::enableSound` | Importante | DONE (parcial) | 4 variants no-arg eliminades. La divergència amb `Ja::enableSound/enableMusic` es manté: Audio silencia per gain, Ja pararia els canals; la decisió es posposa a COUP-01 | +| INCONS-05 | `audio.hpp:102-105` | 4 formas de enable/disable sound (API redundante) + divergencia con `Ja::enableSound` | Importante | DONE | 4 variants no-arg eliminades. Divergència resolta: `Ja::Engine::enableMusic/enableSound` i els flags `music_enabled_`/`sound_enabled_` del motor eliminats (decisió de l'usuari: silenciar per gain des d'Audio via effectiveVolume). Tanca amb el commit 4 del refactor | ## 6. Código muerto @@ -91,5 +92,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas | |----|---------------|----------|-----------|--------|-------| | CONV-01 | varios | Parámetros de audio fuera de `Defaults`/`config.yaml` | Importante | DONE (parcial) | Els que encaixen (`VOLUME_STEP`, `FREQUENCY`, `CROSSFADE_MS`, `FORMAT`, `CHANNELS`) ja són a `Defaults::Audio` (HC-01, HC-02). La resta són tuning intern del motor i resten a `jail_audio.hpp` per preservar-ne la independència | -| CONV-02 | `jail_audio.hpp` | Engine no es clase (namespace + globals), viola OOP | Importante | DEFERRED | Cubierto por COUP-01 | -| CONV-03 | `jail_audio.hpp` | No hay callback "music ended"; polling desde `update()` | Menor | DEFERRED | Requiere COUP-01 | +| CONV-02 | `jail_audio.hpp` | Engine no es clase (namespace + globals), viola OOP | Importante | DONE | Resolt amb COUP-01: `Ja::Engine` és classe amb ctor/dtor RAII, membres privats i API explícita | +| CONV-03 | `jail_audio.hpp` | No hay callback "music ended"; polling desde `update()` | Menor | DONE | `Ja::Engine::setOnMusicEnded(std::function)` disparat des de `updateCurrentMusic` després de `stopMusic()`. Exposat via `Audio::setOnMusicEnded`. Commit 5 del refactor | diff --git a/source/core/audio/audio.cpp b/source/core/audio/audio.cpp index c393dc6..e9904ca 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 @@ -46,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) @@ -93,85 +77,93 @@ 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); } } +// Registra un callback que el motor dispararà quan la pista actual acabi de +// drenar (times == 0 + stream buit). S'executa al mateix thread que +// Audio::update (render loop); els consumidors no poden fer I/O blocant. +// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_ +void Audio::setOnMusicEnded(std::function callback) { + if (engine_) { engine_->setOnMusicEnded(std::move(callback)); } +} + // Consulta directament l'estat a Ja i el projecta al subconjunt d'estats que // 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: return MusicState::PAUSED; case Ja::MusicState::STOPPED: case Ja::MusicState::INVALID: - case Ja::MusicState::DISABLED: default: return MusicState::STOPPED; } @@ -186,15 +178,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) @@ -227,14 +219,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..09e8fed 100644 --- a/source/core/audio/audio.hpp +++ b/source/core/audio/audio.hpp @@ -1,14 +1,19 @@ #pragma once -#include // Per a std::lround -#include // Per a int8_t, uint8_t -#include // Per a std::unique_ptr -#include // Per a string -#include // Per a move +#include // Per a std::lround +#include // Per a int8_t, uint8_t +#include // Per a std::function +#include // Per a std::unique_ptr +#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 @@ -70,6 +75,7 @@ class Audio { void resumeMusic(); // Continua la música pausada void stopMusic(); // Aturar completament la música void fadeOutMusic(int milliseconds); // Fosa de sortida de la música (muta globals de Ja) + void setOnMusicEnded(std::function callback); // Callback disparat quan la pista actual acaba de drenar (CONV-03) // --- Control de sons --- void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja) @@ -126,9 +132,10 @@ class Audio { // --- Variables membre --- static std::unique_ptr