Files
orni-attack/source/core/audio/audio.cpp
T

306 lines
12 KiB
C++

#include "core/audio/audio.hpp"
#include <SDL3/SDL.h> // Para SDL_GetError, SDL_Init
#include <algorithm> // Para clamp
#include <cstdio> // Para std::fprintf
#include "core/audio/audio_adapter.hpp" // Para AudioResource::getMusic/getSound
#include "core/audio/jail_audio.hpp" // Para Ja::* (motor jailgames)
#include "core/audio/sound_effects_config.hpp" // Para SoundEffectsConfig
#include "core/defaults.hpp" // Para Defaults::Audio::FREQUENCY
// Invariant compile-time: tots los valors d'Audio::Group han de cabre als slots
// de volum per grup que manté l'engine. Si s'afegeix una nueva entrada a Group
// y no s'incrementa Ja::MAX_GROUPS, este assert falla antes 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 con la configuración 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: engine_ es std::unique_ptr, el seu dtor tanca el device SDL i
// desregistra Ja::Engine::active_. Cap crida explícita necessària.
Audio::~Audio() = default;
// Método principal: l'estat de la música el manté el motor (única font de
// veritat), per tant no cal sin sincronització aquí.
void Audio::update() {
if (instance && instance->engine_) { instance->engine_->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 ya sona exactament la misma pista i mismo mode loop, no fem res
if (getMusicState() == MusicState::PLAYING && music_.name == name && music_.loop == NEW_LOOP) {
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 == nullptr) { return; }
playMusicInternal(music, loop, crossfade_ms);
// Si el Ja::Music es va crear con filename (loadMusic con 3 arguments), el
// recuperem porque getCurrentMusicName() no menteixi. Si no, music_.name
// queda buit — el contracte d'este 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 same-track early
// return i del nom. El gate de música deshabilitada NO atura la reproducció:
// effectiveVolume porta el volum efectiu a 0 i la pista continua sonant
// silenciada, per garantir que reactivar la música la torne a sentir sense
// haver de reiniciar la pista. 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) {
engine_->crossfadeMusic(music, crossfade_ms, loop);
} else {
if (CURRENTLY_PLAYING) {
engine_->stopMusic();
}
engine_->playMusic(music, loop);
}
music_.loop = (loop != 0);
}
// Pausa la música (l'estat el transiciona Engine::pauseMusic)
void Audio::pauseMusic() {
if (getMusicState() == MusicState::PLAYING) {
engine_->pauseMusic();
}
}
// Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
void Audio::resumeMusic() {
if (getMusicState() == MusicState::PAUSED) {
engine_->resumeMusic();
}
}
// Atura la música (l'estat el transiciona Engine::stopMusic)
void Audio::stopMusic() {
engine_->stopMusic();
}
void Audio::setMusicSpeed(float ratio) {
engine_->setMusicSpeed(ratio);
}
// Reprodueix un so per nom
void Audio::playSound(const std::string& name, Group group) {
engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
}
// Reprodueix un so per punter directe
void Audio::playSound(Ja::Sound* sound, Group group) {
if (sound != nullptr) {
engine_->playSound(sound, 0, static_cast<int>(group));
}
}
// Variant con velocitat (i to) escalats. Apliquem el ratio al canal
// just retornat per `playSound`: así el `SDL_AudioStream` recent creat
// processa tot el sample con el ratio des del primer pull del callback.
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
// la crida al ratio — sin efectes col·laterals.
void Audio::playSound(const std::string& name, Group group, float speed) {
auto* sound = AudioResource::getSound(name);
if (sound == nullptr) { return; }
const int CH = engine_->playSound(sound, 0, static_cast<int>(group));
if (CH >= 0 && speed != 1.0F) {
engine_->setChannelSpeed(CH, speed);
}
}
// Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no
// existeix o l'engine retorna -1 (sin de canals d'efecte plé), cau a playSound
// sec — l'usuari sent el so aún que la cua no s'apliqui.
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
auto* sound = AudioResource::getSound(name);
if (sound == nullptr) { return; }
const auto* params = SoundEffectsConfig::get().findEcho(preset_name);
if (params == nullptr) {
std::fprintf(stderr, "Audio: preset d'eco '%s' desconegut — so '%s' es reprodueix sec\n", preset_name.c_str(), name.c_str());
engine_->playSound(sound, 0, static_cast<int>(group));
return;
}
if (engine_->playSoundWithEcho(sound, *params, static_cast<int>(group)) < 0) {
engine_->playSound(sound, 0, static_cast<int>(group));
}
}
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
// fallback que playSoundWithEcho.
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
auto* sound = AudioResource::getSound(name);
if (sound == nullptr) { return; }
const auto* params = SoundEffectsConfig::get().findReverb(preset_name);
if (params == nullptr) {
std::fprintf(stderr, "Audio: preset de reverb '%s' desconegut — so '%s' es reprodueix sec\n", preset_name.c_str(), name.c_str());
engine_->playSound(sound, 0, static_cast<int>(group));
return;
}
if (engine_->playSoundWithReverb(sound, *params, static_cast<int>(group)) < 0) {
engine_->playSound(sound, 0, static_cast<int>(group));
}
}
// Atura tots los sons
void Audio::stopAllSounds() {
engine_->stopChannel(-1);
}
// Fa una fosa de sortida de la música
void Audio::fadeOutMusic(int milliseconds) {
if (getMusicState() == MusicState::PLAYING) {
engine_->fadeOutMusic(milliseconds);
}
}
// Registra un callback que el motor dispararà cuando la pista actual acabi de
// drenar (times == 0 + stream buit). S'executa al mismo thread que
// Audio::update (render loop); los consumidors no poden fer I/O blocant.
void Audio::setOnMusicEnded(std::function<void()> callback) {
if (engine_) { engine_->setOnMusicEnded(std::move(callback)); }
}
// Resol el nom contra el cache de recursos i retorna la duración pre-calculada
// al `loadMusic`. 0 si la pista no existeix — así el caller pot decidir
// fallback (p. ex. usar un timeout fix) sin haver de propagar errors.
auto Audio::getMusicDurationMs(const std::string& name) -> int {
auto* music = AudioResource::getMusic(name);
return (music != nullptr) ? music->duration_ms : 0;
}
// Consulta directament l'estat a Ja y el projecta al subconjunt d'estats que
// exposa Audio (INVALID/DISABLED de Ja col·lapsen a STOPPED — la capa d'usuari
// solo vol saber si está sonant, pausat o parat).
auto Audio::getMusicState() -> MusicState {
if (!instance || !instance->engine_) { return MusicState::STOPPED; }
switch (instance->engine_->getMusicState()) {
case Ja::MusicState::PLAYING:
return MusicState::PLAYING;
case Ja::MusicState::PAUSED:
return MusicState::PAUSED;
case Ja::MusicState::STOPPED:
case Ja::MusicState::INVALID:
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. Así los dos setters comparteixen la misma 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). Actualitza el valor cachejat
// a config_ perquè els getters i les re-aplicacions internes (enableSound,
// setMasterVolume) puguin tornar al volum que l'usuari va triar.
void Audio::setSoundVolume(float sound_volume, Group group) {
config_.sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
engine_->setSoundVolume(effectiveVolume(config_.sound_volume, sound_enabled_), static_cast<int>(group));
}
// Estableix el volum de la música (float 0.0..1.0). Cf. setSoundVolume.
void Audio::setMusicVolume(float music_volume) {
config_.music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
engine_->setMusicVolume(effectiveVolume(config_.music_volume, music_enabled_));
}
// Estableix el volum master (multiplicador aplicat a sound + music). Re-aplica
// els canals perquè el canvi tingui efecte immediat sense esperar al següent
// setSoundVolume/setMusicVolume explícit.
void Audio::setMasterVolume(float master_volume) {
config_.volume = std::clamp(master_volume, MIN_VOLUME, MAX_VOLUME);
setSoundVolume(config_.sound_volume);
setMusicVolume(config_.music_volume);
}
// Aplica una nueva configuración (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. Re-aplica els volums actuals; effectiveVolume
// retalla a 0 quan enabled_ és false, sense perdre els valors guardats.
void Audio::enable(bool value) {
enabled_ = value;
setSoundVolume(config_.sound_volume);
setMusicVolume(config_.music_volume);
}
// Estableix l'estat dels sons i reaplica el volum porque los canals actius
// responguin a l'instant (evita que el toggle solo 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 misma raó.
void Audio::enableMusic(bool value) {
music_enabled_ = value;
setMusicVolume(config_.music_volume);
}
// Silencia o restaura un grup de sons concret sense alterar config_ (el volum
// que l'usuari va triar) ni els altres grups. Silenciar posa la ganancia del
// grup a 0; restaurar-la torna al volum efectiu normal (que ja aplica els gates
// master/sound i el volum de l'usuari). A diferència de setSoundVolume, no
// xafa config_.sound_volume, así que el menu de servei segueix mostrant i
// operant el volum real durant la demo.
void Audio::silenceGroup(Group group, bool silenced) {
const float VOL = silenced ? 0.0F : effectiveVolume(config_.sound_volume, sound_enabled_);
engine_->setSoundVolume(VOL, static_cast<int>(group));
}
// Inicialitza SDL Audio y el motor Ja::Engine owned.
void Audio::initSDLAudio() {
if (!SDL_Init(SDL_INIT_AUDIO)) {
std::fprintf(stderr, "Audio: SDL_AUDIO could not initialize! SDL Error: %s\n", SDL_GetError());
return;
}
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);
}