Preparatori per COUP-01. No canvia comportament: només mou les
definicions inline de jail_audio.hpp a un jail_audio.cpp nou, deixant
el .hpp amb declaracions + tipus + constants.
Efectes col·laterals que es tanquen gratis:
- COUP-02: jail_audio deixa de ser header-only. Abans cada TU que
l'incloïa compilava 650 línies inline (amb stb_vorbis.c en mode
HEADER_ONLY). Ara el .hpp té ~130 línies de declaracions.
- PATCH-03: #include "external/stb_vorbis.c" + #undef L/C/R/PLAYBACK_*
viu només al final de jail_audio.cpp. audio.cpp perd el workaround
dels undefs.
Estat intern (current_music, channels[], audio_spec, music_volume,
sound_volume[], music_enabled, sound_enabled, sdl_audio_device,
outgoing_music, incoming_fade) baixa al .cpp dins d'un namespace
anònim (internal linkage). Els helpers privats (feedMusicChunk,
pumpMusic, preFillOutgoing, fadeProgress, update*Fade, updateCurrent*,
stealCurrentIntoOutgoing, forEachTargetChannel) també viuen a
l'anonymous namespace.
Ja::Music i Ja::Sound són públics; el .hpp forward-declara
`struct stb_vorbis` perquè els callers només els agafen per punter.
NOLINT(readability-identifier-naming) al forward-decl perquè el nom
l'imposa stb_vorbis.
jail_audio.cpp s'afegeix a APP_SOURCES al CMakeLists.txt.
.clang-tidy: desactivat clang-analyzer-core.UndefinedBinaryOperatorResult
globalment. Fals positiu conegut a external/stb_vorbis.c que abans no es
disparava (no hi havia TU que cridés directament stb_vorbis_seek_start
des del mateix fitxer on la funció està inlined); ara que jail_audio.cpp
sí que fa aquestes crides directes, el path d'anàlisi passa pel .c i el
check dispara. HeaderFilterRegex no aplica a clang-analyzer-*.
Preparació per COUP-01 (introduir Ja::Engine al commit següent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
226 lines
8.2 KiB
C++
226 lines
8.2 KiB
C++
#include "core/audio/audio.hpp"
|
|
|
|
#include <SDL3/SDL.h> // Per a SDL_GetError, SDL_Init
|
|
|
|
#include <algorithm> // Per a clamp
|
|
#include <cstdio> // Per a std::fprintf
|
|
|
|
#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
|
|
|
|
// Invariant compile-time: tots els valors d'Audio::Group han de cabre als slots
|
|
// de volum per grup que manté l'engine. Si s'afegeix una nova entrada a Group
|
|
// i no s'incrementa Ja::MAX_GROUPS, aquest assert falla abans de compilar.
|
|
static_assert(static_cast<int>(Audio::Group::INTERFACE) < Ja::MAX_GROUPS,
|
|
"Audio::Group té més entrades que slots té Ja::MAX_GROUPS");
|
|
|
|
// Singleton
|
|
std::unique_ptr<Audio> Audio::instance;
|
|
|
|
// Inicialitza la instància única del singleton amb la configuració rebuda
|
|
void Audio::init(const Config& config) { Audio::instance = std::unique_ptr<Audio>(new Audio(config)); }
|
|
|
|
// Allibera la instància
|
|
void Audio::destroy() { Audio::instance.reset(); }
|
|
|
|
// Obté la instància
|
|
auto Audio::get() -> Audio* { return Audio::instance.get(); }
|
|
|
|
// Constructor
|
|
Audio::Audio(const Config& config)
|
|
: config_(config) { initSDLAudio(); }
|
|
|
|
// Destructor
|
|
Audio::~Audio() {
|
|
Ja::quit();
|
|
}
|
|
|
|
// Mètode principal: l'estat de la música el manté Ja (única font de veritat),
|
|
// per tant no cal cap sincronització aquí.
|
|
void Audio::update() {
|
|
Ja::update();
|
|
}
|
|
|
|
// Reprodueix la música per nom (amb crossfade opcional)
|
|
void Audio::playMusic(const std::string& name, const int loop, const int crossfade_ms) {
|
|
const bool NEW_LOOP = (loop != 0);
|
|
|
|
// Si ja sona exactament la mateixa pista i mateix mode loop, no fem res
|
|
if (getMusicState() == MusicState::PLAYING && music_.name == name && music_.loop == NEW_LOOP) {
|
|
return;
|
|
}
|
|
|
|
if (!music_enabled_) { return; }
|
|
|
|
auto* resource = AudioResource::getMusic(name);
|
|
if (resource == nullptr) { return; }
|
|
|
|
playMusicInternal(resource, loop, crossfade_ms);
|
|
music_.name = name;
|
|
}
|
|
|
|
// Reprodueix la música per punter (amb crossfade opcional)
|
|
void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms) {
|
|
if (!music_enabled_ || music == nullptr) { return; }
|
|
|
|
playMusicInternal(music, loop, crossfade_ms);
|
|
// Si el Ja::Music es va crear amb filename (loadMusic amb 3 arguments), el
|
|
// recuperem perquè getCurrentMusicName() no menteixi. Si no, music_.name
|
|
// queda buit — el contracte d'aquest overload no garanteix el nom.
|
|
music_.name = music->filename;
|
|
}
|
|
|
|
// Camí comú dels dos overloads: fa el dispatch crossfade vs stop+play i
|
|
// actualitza el loop cachejat. Els callers s'encarreguen del gating
|
|
// (music_enabled_, nullptr, same-track early return) i del nom. L'estat el
|
|
// manté Ja (Ja::playMusic posa PLAYING al Ja::Music* corresponent).
|
|
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);
|
|
} else {
|
|
if (CURRENTLY_PLAYING) {
|
|
Ja::stopMusic();
|
|
}
|
|
Ja::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
|
|
void Audio::pauseMusic() {
|
|
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
|
Ja::pauseMusic();
|
|
}
|
|
}
|
|
|
|
// Continua la música pausada (l'estat el transiciona Ja::resumeMusic)
|
|
// NOLINTNEXTLINE(readability-make-member-function-const) — muta Ja::current_music->state
|
|
void Audio::resumeMusic() {
|
|
if (music_enabled_ && getMusicState() == MusicState::PAUSED) {
|
|
Ja::resumeMusic();
|
|
}
|
|
}
|
|
|
|
// Detiene la música (l'estat el transiciona Ja::stopMusic)
|
|
// NOLINTNEXTLINE(readability-make-member-function-const) — muta Ja::current_music/outgoing_music
|
|
void Audio::stopMusic() {
|
|
if (music_enabled_) {
|
|
Ja::stopMusic();
|
|
}
|
|
}
|
|
|
|
// Reprodueix un so per nom
|
|
// NOLINTNEXTLINE(readability-make-member-function-const) — muta globals de Ja::channels
|
|
void Audio::playSound(const std::string& name, Group group) {
|
|
if (sound_enabled_) {
|
|
Ja::playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
|
|
}
|
|
}
|
|
|
|
// Reprodueix un so per punter directe
|
|
// NOLINTNEXTLINE(readability-make-member-function-const) — muta globals de Ja::channels
|
|
void Audio::playSound(Ja::Sound* sound, Group group) {
|
|
if (sound_enabled_ && sound != nullptr) {
|
|
Ja::playSound(sound, 0, static_cast<int>(group));
|
|
}
|
|
}
|
|
|
|
// Atura tots els sons
|
|
// NOLINTNEXTLINE(readability-make-member-function-const) — muta globals de Ja::channels
|
|
void Audio::stopAllSounds() {
|
|
if (sound_enabled_) {
|
|
Ja::stopChannel(-1);
|
|
}
|
|
}
|
|
|
|
// Fa una fosa de sortida de la música
|
|
// NOLINTNEXTLINE(readability-make-member-function-const) — muta Ja::outgoing_music/incoming_fade
|
|
void Audio::fadeOutMusic(int milliseconds) {
|
|
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
|
Ja::fadeOutMusic(milliseconds);
|
|
}
|
|
}
|
|
|
|
// 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()) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Aplica el gate master (enabled_) + el gate del canal (sound/music_enabled_)
|
|
// i retorna el volum escalat pel master config_.volume. 0 si algun gate està
|
|
// tancat. Així els dos setters comparteixen la mateixa política.
|
|
auto Audio::effectiveVolume(float volume, bool channel_enabled) const -> float {
|
|
volume = std::clamp(volume, MIN_VOLUME, MAX_VOLUME);
|
|
return (enabled_ && channel_enabled) ? volume * config_.volume : 0.0F;
|
|
}
|
|
|
|
// Estableix el volum dels sons (float 0.0..1.0)
|
|
// NOLINTNEXTLINE(readability-make-member-function-const) — muta Ja::sound_volume[] i canals actius
|
|
void Audio::setSoundVolume(float sound_volume, Group group) {
|
|
Ja::setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(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
|
|
void Audio::setMusicVolume(float music_volume) {
|
|
Ja::setMusicVolume(effectiveVolume(music_volume, music_enabled_));
|
|
}
|
|
|
|
// Aplica una nova configuració (substitueix la config cachejada i reaplica enables/volums)
|
|
void Audio::applySettings(const Config& config) {
|
|
config_ = config;
|
|
sound_enabled_ = config_.sound_enabled;
|
|
music_enabled_ = config_.music_enabled;
|
|
enable(config_.enabled);
|
|
}
|
|
|
|
// Estableix l'estat general
|
|
void Audio::enable(bool value) {
|
|
enabled_ = value;
|
|
|
|
setSoundVolume(enabled_ ? config_.sound_volume : MIN_VOLUME);
|
|
setMusicVolume(enabled_ ? config_.music_volume : MIN_VOLUME);
|
|
}
|
|
|
|
// Estableix l'estat dels sons i reaplica el volum perquè els canals actius
|
|
// responguin a l'instant (evita que el toggle només surti efecte al pròxim
|
|
// setSoundVolume explícit).
|
|
void Audio::enableSound(bool value) {
|
|
sound_enabled_ = value;
|
|
setSoundVolume(config_.sound_volume);
|
|
}
|
|
|
|
// Estableix l'estat de la música i reaplica el volum per la mateixa raó.
|
|
void Audio::enableMusic(bool value) {
|
|
music_enabled_ = value;
|
|
setMusicVolume(config_.music_volume);
|
|
}
|
|
|
|
// Inicialitza SDL Audio
|
|
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);
|
|
}
|
|
}
|