Refactor core/audio: Ja::Engine encapsula l'estat del motor (rama refactor/audio-engine-class)
Tanca els DEFERRED de l'auditoria: COUP-01 (engine sense globals), COUP-02 (header-only → .hpp/.cpp), PATCH-03 (stb_vorbis macros aïllades), CONV-02 (engine ja és classe), CONV-03 (callback music-ended), INCONS-05 complet (silenciar per gain, sense flags de motor). 5 commits atòmics amb build + xvfb entre cadascun. Detalls: plan a /home/sergio/.claude/plans/crea-un-plan-para-twinkling-bubble.md i docs/audit-core-audio.md actualitzat. Únic DEFERRED restant de l'auditoria: PATCH-05 (voice-stealing policy) — és feature, no deute.
This commit is contained in:
commit
bad71ec4a9
7 files changed
+705
-618
No files matched your search
@@ -9,6 +9,11 @@ Checks:
|
|||||||
- -bugprone-easily-swappable-parameters
|
- -bugprone-easily-swappable-parameters
|
||||||
- -bugprone-narrowing-conversions
|
- -bugprone-narrowing-conversions
|
||||||
- -modernize-avoid-c-arrays,-warnings-as-errors
|
- -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: '*'
|
WarningsAsErrors: '*'
|
||||||
# Headers nostres (excloem source/external/ que conté dependències de tercers no editables)
|
# Headers nostres (excloem source/external/ que conté dependències de tercers no editables)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ set(APP_SOURCES
|
|||||||
source/core/system/director.cpp
|
source/core/system/director.cpp
|
||||||
source/core/audio/audio.cpp
|
source/core/audio/audio.cpp
|
||||||
source/core/audio/audio_adapter.cpp
|
source/core/audio/audio_adapter.cpp
|
||||||
|
source/core/audio/jail_audio.cpp
|
||||||
source/core/locale/locale.cpp
|
source/core/locale/locale.cpp
|
||||||
source/core/input/key_config.cpp
|
source/core/input/key_config.cpp
|
||||||
source/core/input/global_inputs.cpp
|
source/core/input/global_inputs.cpp
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# Auditoría `core/audio/`
|
# Auditoría `core/audio/`
|
||||||
|
|
||||||
Rama: `audit/core-audio`
|
Rama inicial: `audit/core-audio` (ítems DONE/WONTFIX/DEFERRED inicials).
|
||||||
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 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:
|
Estados posibles:
|
||||||
- `PENDING` — identificado, sin tocar
|
- `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 |
|
| 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 |
|
| HC-02 | Formato/canales/freq hardcodeado en 3+ sitios | DONE |
|
||||||
| INCONS-02 | Doble `MusicState` + `getRealMusicState` como cache paralelo | DONE |
|
| INCONS-02 | Doble `MusicState` + `getRealMusicState` como cache paralelo | DONE |
|
||||||
| HC-01 | Parámetros de audio no configurables (viola convención del proyecto) | 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-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-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-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 |
|
| 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 |
|
| 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-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>`. `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 | DEFERRED | Depende de COUP-01 |
|
| 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 <iostream>` sólo para `std::cout` de errores | Importante | DONE | `<iostream>` → `<cstdio>` 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-03 | `audio.cpp:3`, `jail_audio.hpp:8` | `#include <iostream>` sólo para `std::cout` de errores | Importante | DONE | `<iostream>` → `<cstdio>` 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-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 |
|
| 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-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-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-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
|
## 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 |
|
| 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-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-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 | DEFERRED | Requiere COUP-01 |
|
| CONV-03 | `jail_audio.hpp` | No hay callback "music ended"; polling desde `update()` | Menor | DONE | `Ja::Engine::setOnMusicEnded(std::function<void()>)` disparat des de `updateCurrentMusic` després de `stopMusic()`. Exposat via `Audio::setOnMusicEnded`. Commit 5 del refactor |
|
||||||
+46
-54
@@ -5,21 +5,6 @@
|
|||||||
#include <algorithm> // Per a clamp
|
#include <algorithm> // Per a clamp
|
||||||
#include <cstdio> // Per a std::fprintf
|
#include <cstdio> // 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/audio_adapter.hpp" // Per a AudioResource::getMusic/getSound
|
||||||
#include "core/audio/jail_audio.hpp" // Per a Ja::* (motor jailgames)
|
#include "core/audio/jail_audio.hpp" // Per a Ja::* (motor jailgames)
|
||||||
#include "utils/defaults.hpp" // Per a Defaults::Audio::FREQUENCY
|
#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)
|
Audio::Audio(const Config& config)
|
||||||
: config_(config) { initSDLAudio(); }
|
: config_(config) { initSDLAudio(); }
|
||||||
|
|
||||||
// Destructor
|
// Destructor: engine_ és std::unique_ptr, el seu dtor tanca el device SDL i
|
||||||
Audio::~Audio() {
|
// desregistra Ja::Engine::active_. Cap crida explícita necessària.
|
||||||
Ja::quit();
|
Audio::~Audio() = default;
|
||||||
}
|
|
||||||
|
|
||||||
// Mètode principal: l'estat de la música el manté Ja (única font de veritat),
|
// Mètode principal: l'estat de la música el manté el motor (única font de
|
||||||
// per tant no cal cap sincronització aquí.
|
// veritat), per tant no cal cap sincronització aquí.
|
||||||
void Audio::update() {
|
void Audio::update() {
|
||||||
Ja::update();
|
if (instance && instance->engine_) { instance->engine_->update(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reprodueix la música per nom (amb crossfade opcional)
|
// 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) {
|
void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossfade_ms) {
|
||||||
const bool CURRENTLY_PLAYING = (getMusicState() == MusicState::PLAYING);
|
const bool CURRENTLY_PLAYING = (getMusicState() == MusicState::PLAYING);
|
||||||
if (crossfade_ms > 0 && CURRENTLY_PLAYING) {
|
if (crossfade_ms > 0 && CURRENTLY_PLAYING) {
|
||||||
Ja::crossfadeMusic(music, crossfade_ms, loop);
|
engine_->crossfadeMusic(music, crossfade_ms, loop);
|
||||||
} else {
|
} else {
|
||||||
if (CURRENTLY_PLAYING) {
|
if (CURRENTLY_PLAYING) {
|
||||||
Ja::stopMusic();
|
engine_->stopMusic();
|
||||||
}
|
}
|
||||||
Ja::playMusic(music, loop);
|
engine_->playMusic(music, loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
music_.loop = (loop != 0);
|
music_.loop = (loop != 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pausa la música (l'estat el transiciona Ja::pauseMusic)
|
// Pausa la música (l'estat el transiciona Engine::pauseMusic)
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta Ja::current_music->state
|
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
||||||
void Audio::pauseMusic() {
|
void Audio::pauseMusic() {
|
||||||
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
||||||
Ja::pauseMusic();
|
engine_->pauseMusic();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continua la música pausada (l'estat el transiciona Ja::resumeMusic)
|
// Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta Ja::current_music->state
|
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
||||||
void Audio::resumeMusic() {
|
void Audio::resumeMusic() {
|
||||||
if (music_enabled_ && getMusicState() == MusicState::PAUSED) {
|
if (music_enabled_ && getMusicState() == MusicState::PAUSED) {
|
||||||
Ja::resumeMusic();
|
engine_->resumeMusic();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detiene la música (l'estat el transiciona Ja::stopMusic)
|
// Atura la música (l'estat el transiciona Engine::stopMusic)
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta Ja::current_music/outgoing_music
|
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
||||||
void Audio::stopMusic() {
|
void Audio::stopMusic() {
|
||||||
if (music_enabled_) {
|
if (music_enabled_) {
|
||||||
Ja::stopMusic();
|
engine_->stopMusic();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reprodueix un so per nom
|
// 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) {
|
void Audio::playSound(const std::string& name, Group group) {
|
||||||
if (sound_enabled_) {
|
if (sound_enabled_) {
|
||||||
Ja::playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
|
engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reprodueix un so per punter directe
|
// 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) {
|
void Audio::playSound(Ja::Sound* sound, Group group) {
|
||||||
if (sound_enabled_ && sound != nullptr) {
|
if (sound_enabled_ && sound != nullptr) {
|
||||||
Ja::playSound(sound, 0, static_cast<int>(group));
|
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atura tots els sons
|
// 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() {
|
void Audio::stopAllSounds() {
|
||||||
if (sound_enabled_) {
|
if (sound_enabled_) {
|
||||||
Ja::stopChannel(-1);
|
engine_->stopChannel(-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fa una fosa de sortida de la música
|
// 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) {
|
void Audio::fadeOutMusic(int milliseconds) {
|
||||||
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
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<void()> callback) {
|
||||||
|
if (engine_) { engine_->setOnMusicEnded(std::move(callback)); }
|
||||||
|
}
|
||||||
|
|
||||||
// Consulta directament l'estat a Ja i el projecta al subconjunt d'estats que
|
// 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
|
// exposa Audio (INVALID/DISABLED de Ja col·lapsen a STOPPED — la capa d'usuari
|
||||||
// només vol saber si està sonant, pausat o parat).
|
// només vol saber si està sonant, pausat o parat).
|
||||||
auto Audio::getMusicState() -> MusicState {
|
auto Audio::getMusicState() -> MusicState {
|
||||||
switch (Ja::getMusicState()) {
|
if (!instance || !instance->engine_) { return MusicState::STOPPED; }
|
||||||
|
switch (instance->engine_->getMusicState()) {
|
||||||
case Ja::MusicState::PLAYING:
|
case Ja::MusicState::PLAYING:
|
||||||
return MusicState::PLAYING;
|
return MusicState::PLAYING;
|
||||||
case Ja::MusicState::PAUSED:
|
case Ja::MusicState::PAUSED:
|
||||||
return MusicState::PAUSED;
|
return MusicState::PAUSED;
|
||||||
case Ja::MusicState::STOPPED:
|
case Ja::MusicState::STOPPED:
|
||||||
case Ja::MusicState::INVALID:
|
case Ja::MusicState::INVALID:
|
||||||
case Ja::MusicState::DISABLED:
|
|
||||||
default:
|
default:
|
||||||
return MusicState::STOPPED;
|
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)
|
// 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) {
|
void Audio::setSoundVolume(float sound_volume, Group group) {
|
||||||
Ja::setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group));
|
engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estableix el volum de la música (float 0.0..1.0)
|
// 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) {
|
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)
|
// 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);
|
setMusicVolume(config_.music_volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inicialitza SDL Audio
|
// Inicialitza SDL Audio i el motor Ja::Engine owned.
|
||||||
void Audio::initSDLAudio() {
|
void Audio::initSDLAudio() {
|
||||||
if (!SDL_Init(SDL_INIT_AUDIO)) {
|
if (!SDL_Init(SDL_INIT_AUDIO)) {
|
||||||
std::fprintf(stderr, "Audio: SDL_AUDIO could not initialize! SDL Error: %s\n", SDL_GetError());
|
std::fprintf(stderr, "Audio: SDL_AUDIO could not initialize! SDL Error: %s\n", SDL_GetError());
|
||||||
} else {
|
return;
|
||||||
Ja::init(Defaults::Audio::FREQUENCY, Defaults::Audio::FORMAT, Defaults::Audio::CHANNELS);
|
|
||||||
sound_enabled_ = config_.sound_enabled;
|
|
||||||
music_enabled_ = config_.music_enabled;
|
|
||||||
enable(config_.enabled);
|
|
||||||
}
|
}
|
||||||
|
engine_ = std::make_unique<Ja::Engine>(Defaults::Audio::FREQUENCY, Defaults::Audio::FORMAT, Defaults::Audio::CHANNELS);
|
||||||
|
sound_enabled_ = config_.sound_enabled;
|
||||||
|
music_enabled_ = config_.music_enabled;
|
||||||
|
enable(config_.enabled);
|
||||||
}
|
}
|
||||||
+19
-12
@@ -1,14 +1,19 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cmath> // Per a std::lround
|
#include <cmath> // Per a std::lround
|
||||||
#include <cstdint> // Per a int8_t, uint8_t
|
#include <cstdint> // Per a int8_t, uint8_t
|
||||||
#include <memory> // Per a std::unique_ptr
|
#include <functional> // Per a std::function
|
||||||
#include <string> // Per a string
|
#include <memory> // Per a std::unique_ptr
|
||||||
#include <utility> // Per a move
|
#include <string> // Per a string
|
||||||
|
#include <utility> // Per a move
|
||||||
|
|
||||||
// Forward-declares per no incloure core/audio/jail_audio.hpp al header (conté
|
// Forward-declares per no incloure core/audio/jail_audio.hpp al header. Els
|
||||||
// l'engine complet de streaming amb stb_vorbis).
|
// tres símbols (Music/Sound per al punter que exposa la API i Engine per al
|
||||||
|
// std::unique_ptr<Engine> 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 {
|
namespace Ja {
|
||||||
|
class Engine;
|
||||||
struct Music;
|
struct Music;
|
||||||
struct Sound;
|
struct Sound;
|
||||||
} // namespace Ja
|
} // namespace Ja
|
||||||
@@ -70,6 +75,7 @@ class Audio {
|
|||||||
void resumeMusic(); // Continua la música pausada
|
void resumeMusic(); // Continua la música pausada
|
||||||
void stopMusic(); // Aturar completament la música
|
void stopMusic(); // Aturar completament la música
|
||||||
void fadeOutMusic(int milliseconds); // Fosa de sortida de la música (muta globals de Ja)
|
void fadeOutMusic(int milliseconds); // Fosa de sortida de la música (muta globals de Ja)
|
||||||
|
void setOnMusicEnded(std::function<void()> callback); // Callback disparat quan la pista actual acaba de drenar (CONV-03)
|
||||||
|
|
||||||
// --- Control de sons ---
|
// --- Control de sons ---
|
||||||
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
|
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 ---
|
// --- Variables membre ---
|
||||||
static std::unique_ptr<Audio> instance; // Instància única d'Audio
|
static std::unique_ptr<Audio> instance; // Instància única d'Audio
|
||||||
|
|
||||||
Config config_{}; // Configuració injectada (volums, enables)
|
std::unique_ptr<Ja::Engine> engine_; // Motor de baix nivell (owned); viu mentre Audio viu.
|
||||||
Music music_; // Estat de la música
|
Config config_{}; // Configuració injectada (volums, enables)
|
||||||
bool enabled_{true}; // Estat general de l'àudio
|
Music music_; // Estat de la música (nom + loop cachejats)
|
||||||
bool sound_enabled_{true}; // Estat dels efectes de so
|
bool enabled_{true}; // Estat general de l'àudio
|
||||||
bool music_enabled_{true}; // Estat de la música
|
bool sound_enabled_{true}; // Estat dels efectes de so
|
||||||
|
bool music_enabled_{true}; // Estat de la música
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,523 @@
|
|||||||
|
#include "core/audio/jail_audio.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
|
||||||
|
// --- Streaming internals (file-scope constants) ---
|
||||||
|
namespace {
|
||||||
|
// 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;
|
||||||
|
} // 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; }
|
||||||
|
|
||||||
|
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<short*>(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<const void*>(chunk), BYTES);
|
||||||
|
return SAMPLES_PER_CHANNEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
const int NEEDED_BYTES = static_cast<int>((static_cast<std::int64_t>(duration_ms) * BYTES_PER_SECOND) / 1000);
|
||||||
|
|
||||||
|
while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) {
|
||||||
|
const int DECODED = feedMusicChunk(music);
|
||||||
|
if (DECODED <= 0) { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<Uint64>(fade.duration_ms)) { return 1.0F; }
|
||||||
|
return static_cast<float>(ELAPSED) / static_cast<float>(fade.duration_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void Engine::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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
|
||||||
|
|
||||||
|
updateIncomingFade();
|
||||||
|
|
||||||
|
pumpMusic(current_music_);
|
||||||
|
if (current_music_->times == 0 && SDL_GetAudioStreamAvailable(current_music_->stream) == 0) {
|
||||||
|
// La pista ha acabat de drenar naturalment. L'aturem primer (deixa
|
||||||
|
// l'engine en estat consistent) i llavors invoquem el callback;
|
||||||
|
// així un eventual playMusic des del callback comença net.
|
||||||
|
stopMusic();
|
||||||
|
if (on_music_ended_) { on_music_ended_(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::updateSoundChannels() {
|
||||||
|
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
|
||||||
|
if (channels_[i].state != ChannelState::PLAYING) { continue; }
|
||||||
|
|
||||||
|
if (channels_[i].times != 0) {
|
||||||
|
if (static_cast<Uint32>(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 <typename Fn>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::playMusic(Music* music, const int loop) {
|
||||||
|
if (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 (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
|
||||||
|
|
||||||
|
current_music_->state = MusicState::PAUSED;
|
||||||
|
SDL_UnbindAudioStream(current_music_->stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::resumeMusic() {
|
||||||
|
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 (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 == 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 (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::setOnMusicEnded(std::function<void()> callback) {
|
||||||
|
on_music_ended_ = std::move(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 == 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 == 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) {
|
||||||
|
forEachTargetChannel(channel, [](Channel& ch) {
|
||||||
|
if (ch.state == ChannelState::PLAYING) {
|
||||||
|
ch.state = ChannelState::PAUSED;
|
||||||
|
SDL_UnbindAudioStream(ch.stream);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::resumeChannel(const int channel) {
|
||||||
|
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::onSoundDeleted(const Sound* sound) {
|
||||||
|
if (sound == nullptr) { return; }
|
||||||
|
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
|
||||||
|
if (channels_[i].sound == sound) { stopChannel(i); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Factories i destructors (permanents) ---
|
||||||
|
|
||||||
|
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<Uint8>`
|
||||||
|
// 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>();
|
||||||
|
music->ogg_data.assign(buffer, buffer + length);
|
||||||
|
|
||||||
|
int vorbis_error = 0;
|
||||||
|
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
|
||||||
|
static_cast<int>(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<int>(INFO.channels);
|
||||||
|
music->spec.freq = static_cast<int>(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 deleteMusic(Music* music) {
|
||||||
|
if (music == nullptr) { return; }
|
||||||
|
// 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); }
|
||||||
|
delete music;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound* {
|
||||||
|
auto sound = std::make_unique<Sound>();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteSound(Sound* sound) {
|
||||||
|
if (sound == nullptr) { return; }
|
||||||
|
if (Engine* eng = Engine::active()) { eng->onSoundDeleted(sound); }
|
||||||
|
// buffer es destrueix automàticament via RAII (SdlFreeDeleter).
|
||||||
|
delete sound;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // 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
|
||||||
+101
-543
@@ -3,19 +3,18 @@
|
|||||||
// --- Includes ---
|
// --- Includes ---
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <functional>
|
||||||
#include <iterator>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
// clang-format off
|
// Forward-declaració del decoder de vorbis. La implementació viu a
|
||||||
#define STB_VORBIS_HEADER_ONLY
|
// jail_audio.cpp (únic TU que compila external/stb_vorbis.c). Qualsevol caller
|
||||||
// NOLINTNEXTLINE(bugprone-suspicious-include)
|
// només necessita `stb_vorbis*` per punter — mai per valor — així que el
|
||||||
#include "external/stb_vorbis.c" // Para stb_vorbis_open_memory i streaming
|
// forward decl n'hi ha prou i evita arrossegar el .c a tots els TU.
|
||||||
// clang-format on
|
// 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*`.
|
// Deleter stateless per a buffers reservats amb `SDL_malloc` / `SDL_LoadWAV*`.
|
||||||
// Compatible amb `std::unique_ptr<Uint8[], SdlFreeDeleter>` — zero size overhead
|
// Compatible amb `std::unique_ptr<Uint8[], SdlFreeDeleter>` — zero size overhead
|
||||||
@@ -26,38 +25,36 @@ struct SdlFreeDeleter {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Engine de baix nivell d'àudio del projecte jailgames: streaming OGG (stb_vorbis)
|
// Motor de baix nivell d'àudio del projecte jailgames: streaming OGG
|
||||||
// + N canals d'efectes (SDL3 audio). No depèn d'Options ni de cap singleton del
|
// (stb_vorbis) + N canals d'efectes (SDL3 audio). No depèn d'Options ni de cap
|
||||||
// joc; només de SDL3 i stb_vorbis. La capa superior (Audio) li passa recursos
|
// singleton del joc; només de SDL3 i stb_vorbis. La capa superior (Audio) li
|
||||||
// pel punter i fa el bookkeeping d'usuari.
|
// passa recursos pel punter i fa el bookkeeping d'usuari.
|
||||||
namespace Ja {
|
namespace Ja {
|
||||||
|
|
||||||
// --- Public Enums ---
|
// --- Public Enums ---
|
||||||
enum class ChannelState : std::uint8_t {
|
enum class ChannelState : std::uint8_t {
|
||||||
INVALID,
|
|
||||||
FREE,
|
FREE,
|
||||||
PLAYING,
|
PLAYING,
|
||||||
PAUSED,
|
PAUSED,
|
||||||
SOUND_DISABLED,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class MusicState : std::uint8_t {
|
enum class MusicState : std::uint8_t {
|
||||||
INVALID,
|
INVALID, // Music carregat però mai play-ejat
|
||||||
PLAYING,
|
PLAYING,
|
||||||
PAUSED,
|
PAUSED,
|
||||||
STOPPED,
|
STOPPED,
|
||||||
DISABLED,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Struct Definitions ---
|
// --- Constants ---
|
||||||
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20;
|
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20;
|
||||||
inline constexpr int MAX_GROUPS = 2;
|
inline constexpr int MAX_GROUPS = 2;
|
||||||
|
|
||||||
// Spec de fallback del dispositiu. S'aplica abans de Ja::init() i com a valor
|
// Spec de fallback del dispositiu. S'aplica abans que l'Engine s'iniciï i
|
||||||
// inicial de Sound/Music. L'spec real d'ús l'imposa Ja::init(), que la capa
|
// com a valor inicial de Sound/Music. L'spec real d'ús l'imposa el ctor
|
||||||
// Audio alimenta des de Defaults::Audio — única font de veritat del projecte.
|
// d'Engine, alimentat des de Defaults::Audio via Audio.
|
||||||
inline constexpr SDL_AudioSpec DEFAULT_SPEC{SDL_AUDIO_S16, 2, 48000};
|
inline constexpr SDL_AudioSpec DEFAULT_SPEC{SDL_AUDIO_S16, 2, 48000};
|
||||||
|
|
||||||
|
// --- Struct Definitions ---
|
||||||
struct Sound {
|
struct Sound {
|
||||||
SDL_AudioSpec spec{DEFAULT_SPEC};
|
SDL_AudioSpec spec{DEFAULT_SPEC};
|
||||||
Uint32 length{0};
|
Uint32 length{0};
|
||||||
@@ -95,7 +92,6 @@ namespace Ja {
|
|||||||
MusicState state{MusicState::INVALID};
|
MusicState state{MusicState::INVALID};
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Crossfade / Fade State ---
|
|
||||||
struct FadeState {
|
struct FadeState {
|
||||||
bool active{false};
|
bool active{false};
|
||||||
Uint64 start_time{0};
|
Uint64 start_time{0};
|
||||||
@@ -108,527 +104,89 @@ namespace Ja {
|
|||||||
FadeState fade;
|
FadeState fade;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Internal Global State (inline, C++17) ---
|
// --- Engine ---
|
||||||
inline Music* current_music{nullptr};
|
// Encapsula tot l'estat que abans vivia com a globals inline. Un sol Engine
|
||||||
inline Channel channels[MAX_SIMULTANEOUS_CHANNELS];
|
// viu per procés (enforceat via assert al ctor contra `active_`). El ctor
|
||||||
|
// obre el device SDL; el dtor el tanca (RAII). Els deleters
|
||||||
inline SDL_AudioSpec audio_spec{DEFAULT_SPEC};
|
// `Ja::deleteMusic`/`Ja::deleteSound` accedeixen al motor actiu via
|
||||||
inline float music_volume{1.0F};
|
// `Engine::active()` per parar canals abans d'alliberar.
|
||||||
inline float sound_volume[MAX_GROUPS];
|
class Engine {
|
||||||
inline bool music_enabled{true};
|
public:
|
||||||
inline bool sound_enabled{true};
|
Engine(int freq, SDL_AudioFormat format, int num_channels);
|
||||||
inline SDL_AudioDeviceID sdl_audio_device{0};
|
~Engine();
|
||||||
|
Engine(const Engine&) = delete;
|
||||||
inline OutgoingMusic outgoing_music;
|
auto operator=(const Engine&) -> Engine& = delete;
|
||||||
inline FadeState incoming_fade;
|
Engine(Engine&&) = delete;
|
||||||
|
auto operator=(Engine&&) -> Engine& = delete;
|
||||||
// --- Forward Declarations ---
|
|
||||||
inline void stopMusic();
|
// Retorna el motor actiu o nullptr si cap ha estat construït. L'usen
|
||||||
inline void stopChannel(int channel);
|
// els deleters de recursos perquè no els arriba cap referència directa.
|
||||||
inline auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
|
[[nodiscard]] static auto active() noexcept -> Engine*;
|
||||||
inline void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
|
|
||||||
|
void update();
|
||||||
// --- Music streaming internals ---
|
|
||||||
// Bytes-per-sample per canal (sempre s16)
|
// --- Música ---
|
||||||
inline constexpr int MUSIC_BYTES_PER_SAMPLE = 2;
|
void playMusic(Music* music, int loop = -1);
|
||||||
// Quants shorts decodifiquem per crida a get_samples_short_interleaved.
|
void pauseMusic();
|
||||||
// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz.
|
void resumeMusic();
|
||||||
inline constexpr int MUSIC_CHUNK_SHORTS = 8192;
|
void stopMusic();
|
||||||
// Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
|
void fadeOutMusic(int milliseconds);
|
||||||
// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
|
void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
|
||||||
inline constexpr float MUSIC_LOW_WATER_SECONDS = 0.5F;
|
[[nodiscard]] auto getMusicState() const -> MusicState;
|
||||||
|
auto setMusicVolume(float volume) -> float;
|
||||||
// Decodifica un chunk del vorbis i el volca a l'stream. Retorna samples
|
// Registra un callback que es disparà quan la música actual acabi de
|
||||||
// decodificats per canal (0 = EOF de l'stream vorbis).
|
// drenar naturalment (times == 0 + stream buit). Es crida DESPRÉS de
|
||||||
inline auto feedMusicChunk(Music* music) -> int {
|
// stopMusic, així que el callback pot invocar playMusic sense córrer.
|
||||||
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return 0; }
|
// S'executa al mateix thread que Engine::update (render loop); no fer
|
||||||
|
// operacions blocants.
|
||||||
short chunk[MUSIC_CHUNK_SHORTS];
|
void setOnMusicEnded(std::function<void()> callback);
|
||||||
const int NUM_CHANNELS = music->spec.channels;
|
// Notifica al motor que un Music s'està destruint: si és el current_music
|
||||||
const int SAMPLES_PER_CHANNEL = stb_vorbis_get_samples_short_interleaved(
|
// s'atura abans que els seus recursos (stream/vorbis) deixin de ser vàlids.
|
||||||
music->vorbis,
|
void onMusicDeleted(const Music* music);
|
||||||
NUM_CHANNELS,
|
|
||||||
static_cast<short*>(chunk),
|
// --- So ---
|
||||||
MUSIC_CHUNK_SHORTS);
|
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
|
||||||
if (SAMPLES_PER_CHANNEL <= 0) { return 0; }
|
auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
|
||||||
|
void pauseChannel(int channel);
|
||||||
const int BYTES = SAMPLES_PER_CHANNEL * NUM_CHANNELS * MUSIC_BYTES_PER_SAMPLE;
|
void resumeChannel(int channel);
|
||||||
SDL_PutAudioStreamData(music->stream, static_cast<const void*>(chunk), BYTES);
|
void stopChannel(int channel);
|
||||||
return SAMPLES_PER_CHANNEL;
|
auto setSoundVolume(float volume, int group = -1) -> float;
|
||||||
}
|
// Notifica al motor que un Sound s'està destruint: els canals que el
|
||||||
|
// referenciïn es paren abans d'alliberar el buffer.
|
||||||
// Reompli l'stream fins que tinga ≥ MUSIC_LOW_WATER_SECONDS bufferats.
|
void onSoundDeleted(const Sound* sound);
|
||||||
// En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar.
|
|
||||||
inline void pumpMusic(Music* music) {
|
private:
|
||||||
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; }
|
void stealCurrentIntoOutgoing(int duration_ms);
|
||||||
|
void updateOutgoingFade();
|
||||||
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
void updateIncomingFade();
|
||||||
const int LOW_WATER_BYTES = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));
|
void updateCurrentMusic();
|
||||||
|
void updateSoundChannels();
|
||||||
while (SDL_GetAudioStreamAvailable(music->stream) < LOW_WATER_BYTES) {
|
|
||||||
const int DECODED = feedMusicChunk(music);
|
template <typename Fn>
|
||||||
if (DECODED > 0) { continue; }
|
void forEachTargetChannel(int channel, Fn&& fn);
|
||||||
|
|
||||||
// EOF: si queden loops, rebobinar; si no, tallar i deixar drenar.
|
Music* current_music_{nullptr};
|
||||||
if (music->times != 0) {
|
Channel channels_[MAX_SIMULTANEOUS_CHANNELS]{};
|
||||||
stb_vorbis_seek_start(music->vorbis);
|
SDL_AudioSpec audio_spec_{DEFAULT_SPEC};
|
||||||
if (music->times > 0) { music->times--; }
|
float music_volume_{1.0F};
|
||||||
} else {
|
float sound_volume_[MAX_GROUPS]{};
|
||||||
break;
|
SDL_AudioDeviceID sdl_audio_device_{0};
|
||||||
}
|
OutgoingMusic outgoing_music_;
|
||||||
}
|
FadeState incoming_fade_;
|
||||||
}
|
std::function<void()> on_music_ended_;
|
||||||
|
|
||||||
// Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream
|
// NOLINTNEXTLINE(readability-identifier-naming) — convenció projecte: private static amb sufix _
|
||||||
// siga robat per outgoing_music (crossfade o fade-out). Imprescindible amb
|
static Engine* active_;
|
||||||
// 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) {
|
// --- Factories i destructors (permanents) ---
|
||||||
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; }
|
// No depenen de l'estat del motor: loadMusic/loadSound només construeixen
|
||||||
|
// objectes, deleteMusic/deleteSound consulten Engine::active() per parar
|
||||||
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
// canals abans d'alliberar (si el motor encara viu).
|
||||||
const int NEEDED_BYTES = static_cast<int>((static_cast<std::int64_t>(duration_ms) * BYTES_PER_SECOND) / 1000);
|
[[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length) -> Music*;
|
||||||
|
[[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music*;
|
||||||
while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) {
|
void deleteMusic(Music* music);
|
||||||
const int DECODED = feedMusicChunk(music);
|
[[nodiscard]] auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound*;
|
||||||
if (DECODED <= 0) { break; } // EOF: deixem drenar el que hi haja
|
void deleteSound(Sound* sound);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 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<Uint64>(fade.duration_ms)) { return 1.0F; }
|
|
||||||
return static_cast<float>(ELAPSED) / static_cast<float>(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<Uint32>(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<Uint8>`
|
|
||||||
// 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>();
|
|
||||||
music->ogg_data.assign(buffer, buffer + length);
|
|
||||||
|
|
||||||
int vorbis_error = 0;
|
|
||||||
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
|
|
||||||
static_cast<int>(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<int>(INFO.channels);
|
|
||||||
music->spec.freq = static_cast<int>(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<Sound>();
|
|
||||||
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 <typename Fn>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Ja
|
} // namespace Ja
|
||||||
Reference in new issue
Block a user