Fase 3: import del subsistema de audio desde AEEA
Reemplaza el audio antiguo de orni_attack (singleton con new/delete
raw, sin efectos, sin crossfade) por el subsistema moderno de AEEA
(unique_ptr, RAII, crossfade nativo, echo/reverb, pitch-shift,
callbacks de fin de pista, getMusicDurationMs para timelines
deterministas).
Eliminados:
- source/core/audio/audio_cache.{hpp,cpp} (1 cache por subsistema)
- source/core/audio/jail_audio.hpp viejo (motor inline globals)
- source/external/stb_vorbis.h (v1.20)
Añadidos (copiados de AEEA, traducidos comentarios al castellano):
- source/core/audio/audio.{hpp,cpp} — singleton con Audio::Config inyectada
- source/core/audio/audio_adapter.{hpp,cpp} — adapter para getMusic/getSound
- source/core/audio/audio_effects.{hpp,cpp} — Schroeder reverb + echo DSP
- source/core/audio/jail_audio.{hpp,cpp} — Ja::Engine class-based, streaming
- source/core/audio/sound_effects_config.{hpp,cpp} — presets YAML (opcional)
- source/external/stb_vorbis.c (v1.22) — OGG decoder, versión más reciente
- source/external/stb_vorbis_impl.cpp — TU aislada para evitar clang-tidy
Adaptaciones:
- audio_adapter.cpp implementado a medida para orni: usa
Resource::Helper::loadFile (no Resource::Cache de AEEA que orni no
tiene). Cache local con unique_ptr<Ja::Music> / unique_ptr<Ja::Sound>.
- Includes: utils/defaults.hpp -> core/defaults.hpp, utils/log.hpp
reemplazado por iostream con std::cerr/std::cout.
API breaking changes (callsites migrados):
- Audio::init() -> Audio::init(Config); el Director construye la Config
desde Defaults::Audio::* (ENABLED, VOLUME, MUSIC_*, SOUND_*).
- Audio::get()->getMusicState() -> Audio::getMusicState() (ahora static).
- AudioCache::getMusic/getSound -> AudioResource::getMusic/getSound.
Defaults::Audio consolidado: ahora aglutina las constantes que antes
estaban repartidas entre namespace Audio (VOLUME, ENABLED), namespace
Music (VOLUME, ENABLED), namespace Sound (VOLUME, ENABLED). Las pistas
y rutas de efectos siguen en Music::* / Sound::*. Añadidas FREQUENCY,
FORMAT, CHANNELS, CROSSFADE_MS, VOLUME_STEP para el motor.
Beneficios para fases siguientes:
- Crossfade en transiciones de escena (uso: playMusic(name, -1, 1500)).
- Pitch-shift para variaciones de SFX (Audio::playSound(name, group, 0.95)).
- Echo/reverb DSP via playSoundWithEcho/Reverb (sounds.yaml presets).
- Callbacks setOnMusicEnded para sincronizar eventos con el fin de pista.
Compila y enlaza. Pendiente: test runtime del usuario.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+218
-110
@@ -1,183 +1,291 @@
|
||||
#include "audio.hpp"
|
||||
#include "core/audio/audio.hpp"
|
||||
|
||||
#include <SDL3/SDL.h> // Para SDL_LogInfo, SDL_LogCategory, SDL_G...
|
||||
#include <SDL3/SDL.h> // Para SDL_GetError, SDL_Init
|
||||
|
||||
#include <algorithm> // Para clamp
|
||||
#include <iostream> // Para std::cout
|
||||
#include <cstdio> // Para std::fprintf
|
||||
|
||||
// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp)
|
||||
// clang-format off
|
||||
#undef STB_VORBIS_HEADER_ONLY
|
||||
#include "external/stb_vorbis.h"
|
||||
// clang-format on
|
||||
#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
|
||||
|
||||
#include "core/audio/audio_cache.hpp" // Para AudioCache
|
||||
#include "core/audio/jail_audio.hpp" // Para JA_FadeOutMusic, JA_Init, JA_PauseM...
|
||||
#include "game/options.hpp" // Para AudioOptions, audio, MusicOptions
|
||||
// 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
|
||||
Audio* Audio::instance = nullptr;
|
||||
std::unique_ptr<Audio> Audio::instance;
|
||||
|
||||
// Inicializa la instancia única del singleton
|
||||
void Audio::init() { Audio::instance = new Audio(); }
|
||||
// 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)); }
|
||||
|
||||
// Libera la instancia
|
||||
void Audio::destroy() { delete Audio::instance; }
|
||||
// Allibera la instància
|
||||
void Audio::destroy() { Audio::instance.reset(); }
|
||||
|
||||
// Obtiene la instancia
|
||||
auto Audio::get() -> Audio* { return Audio::instance; }
|
||||
// Obté la instància
|
||||
auto Audio::get() -> Audio* { return Audio::instance.get(); }
|
||||
|
||||
// Constructor
|
||||
Audio::Audio() { initSDLAudio(); }
|
||||
Audio::Audio(const Config& config)
|
||||
: config_(config) { initSDLAudio(); }
|
||||
|
||||
// Destructor
|
||||
Audio::~Audio() {
|
||||
JA_Quit();
|
||||
}
|
||||
// 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
|
||||
// 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() {
|
||||
JA_Update();
|
||||
if (instance && instance->engine_) { instance->engine_->update(); }
|
||||
}
|
||||
|
||||
// Reproduce la música
|
||||
void Audio::playMusic(const std::string& name, const int loop) {
|
||||
bool new_loop = (loop != 0);
|
||||
// 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 está sonando exactamente la misma pista y mismo modo loop, no hacemos nada
|
||||
if (music_.state == MusicState::PLAYING && music_.name == name && music_.loop == new_loop) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Intentar obtener recurso; si falla, no tocar estado
|
||||
auto* resource = AudioCache::getMusic(name);
|
||||
if (resource == nullptr) {
|
||||
// manejo de error opcional
|
||||
return;
|
||||
}
|
||||
if (!music_enabled_) { return; }
|
||||
|
||||
// Si hay algo reproduciéndose, detenerlo primero (si el backend lo requiere)
|
||||
if (music_.state == MusicState::PLAYING) {
|
||||
JA_StopMusic(); // sustituir por la función de stop real del API si tiene otro nombre
|
||||
}
|
||||
auto* resource = AudioResource::getMusic(name);
|
||||
if (resource == nullptr) { return; }
|
||||
|
||||
// Llamada al motor para reproducir la nueva pista
|
||||
JA_PlayMusic(resource, loop);
|
||||
|
||||
// Actualizar estado y metadatos después de start con éxito
|
||||
playMusicInternal(resource, loop, crossfade_ms);
|
||||
music_.name = name;
|
||||
music_.loop = new_loop;
|
||||
music_.state = MusicState::PLAYING;
|
||||
}
|
||||
|
||||
// Pausa la música
|
||||
// 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 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 gating
|
||||
// (music_enabled_, nullptr, same-track early return) y 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) {
|
||||
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 (music_enabled_ && music_.state == MusicState::PLAYING) {
|
||||
JA_PauseMusic();
|
||||
music_.state = MusicState::PAUSED;
|
||||
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
||||
engine_->pauseMusic();
|
||||
}
|
||||
}
|
||||
|
||||
// Continua la música pausada
|
||||
// Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
|
||||
void Audio::resumeMusic() {
|
||||
if (music_enabled_ && music_.state == MusicState::PAUSED) {
|
||||
JA_ResumeMusic();
|
||||
music_.state = MusicState::PLAYING;
|
||||
if (music_enabled_ && getMusicState() == MusicState::PAUSED) {
|
||||
engine_->resumeMusic();
|
||||
}
|
||||
}
|
||||
|
||||
// Detiene la música
|
||||
// Atura la música (l'estat el transiciona Engine::stopMusic)
|
||||
void Audio::stopMusic() {
|
||||
if (music_enabled_) {
|
||||
JA_StopMusic();
|
||||
music_.state = MusicState::STOPPED;
|
||||
engine_->stopMusic();
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduce un sonido por nombre
|
||||
void Audio::playSound(const std::string& name, Group group) const {
|
||||
void Audio::setMusicSpeed(float ratio) {
|
||||
if (music_enabled_) {
|
||||
engine_->setMusicSpeed(ratio);
|
||||
}
|
||||
}
|
||||
|
||||
// Reprodueix un so per nom
|
||||
void Audio::playSound(const std::string& name, Group group) {
|
||||
if (sound_enabled_) {
|
||||
JA_PlaySound(AudioCache::getSound(name), 0, static_cast<int>(group));
|
||||
engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduce un sonido por puntero directo
|
||||
void Audio::playSound(JA_Sound_t* sound, Group group) const {
|
||||
// Reprodueix un so per punter directe
|
||||
void Audio::playSound(Ja::Sound* sound, Group group) {
|
||||
if (sound_enabled_ && 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) {
|
||||
if (!sound_enabled_) { return; }
|
||||
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) {
|
||||
if (!sound_enabled_) { return; }
|
||||
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) {
|
||||
if (!sound_enabled_) { return; }
|
||||
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() {
|
||||
if (sound_enabled_) {
|
||||
JA_PlaySound(sound, 0, static_cast<int>(group));
|
||||
engine_->stopChannel(-1);
|
||||
}
|
||||
}
|
||||
|
||||
// Detiene todos los sonidos
|
||||
void Audio::stopAllSounds() const {
|
||||
if (sound_enabled_) {
|
||||
JA_StopChannel(-1);
|
||||
// Fa una fosa de sortida de la música
|
||||
void Audio::fadeOutMusic(int milliseconds) {
|
||||
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
||||
engine_->fadeOutMusic(milliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
// Realiza un fundido de salida de la música
|
||||
void Audio::fadeOutMusic(int milliseconds) const {
|
||||
if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) {
|
||||
JA_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)); }
|
||||
}
|
||||
|
||||
// Consulta directamente el estado real de la música en jailaudio
|
||||
auto Audio::getRealMusicState() -> MusicState {
|
||||
JA_Music_state ja_state = JA_GetMusicState();
|
||||
switch (ja_state) {
|
||||
case JA_MUSIC_PLAYING:
|
||||
// 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_MUSIC_PAUSED:
|
||||
case Ja::MusicState::PAUSED:
|
||||
return MusicState::PAUSED;
|
||||
case JA_MUSIC_STOPPED:
|
||||
case JA_MUSIC_INVALID:
|
||||
case JA_MUSIC_DISABLED:
|
||||
case Ja::MusicState::STOPPED:
|
||||
case Ja::MusicState::INVALID:
|
||||
default:
|
||||
return MusicState::STOPPED;
|
||||
}
|
||||
}
|
||||
|
||||
// Establece el volumen de los sonidos
|
||||
void Audio::setSoundVolume(float sound_volume, Group group) const {
|
||||
if (sound_enabled_) {
|
||||
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
|
||||
const float CONVERTED_VOLUME = sound_volume * Options::audio.volume;
|
||||
JA_SetSoundVolume(CONVERTED_VOLUME, static_cast<int>(group));
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Establece el volumen de la música
|
||||
void Audio::setMusicVolume(float music_volume) const {
|
||||
if (music_enabled_) {
|
||||
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
|
||||
const float CONVERTED_VOLUME = music_volume * Options::audio.volume;
|
||||
JA_SetMusicVolume(CONVERTED_VOLUME);
|
||||
}
|
||||
// Estableix el volum dels sons (float 0.0..1.0)
|
||||
void Audio::setSoundVolume(float sound_volume, Group group) {
|
||||
engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group));
|
||||
}
|
||||
|
||||
// Aplica la configuración
|
||||
void Audio::applySettings() {
|
||||
enable(Options::audio.enabled);
|
||||
// Estableix el volum de la música (float 0.0..1.0)
|
||||
void Audio::setMusicVolume(float music_volume) {
|
||||
engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_));
|
||||
}
|
||||
|
||||
// Establecer estado general
|
||||
// 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
|
||||
void Audio::enable(bool value) {
|
||||
enabled_ = value;
|
||||
|
||||
setSoundVolume(enabled_ ? Options::audio.sound.volume : MIN_VOLUME);
|
||||
setMusicVolume(enabled_ ? Options::audio.music.volume : MIN_VOLUME);
|
||||
setSoundVolume(enabled_ ? config_.sound_volume : MIN_VOLUME);
|
||||
setMusicVolume(enabled_ ? config_.music_volume : MIN_VOLUME);
|
||||
}
|
||||
|
||||
// Inicializa SDL Audio
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Inicialitza SDL Audio y el motor Ja::Engine owned.
|
||||
void Audio::initSDLAudio() {
|
||||
if (!SDL_Init(SDL_INIT_AUDIO)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_AUDIO could not initialize! SDL Error: %s", SDL_GetError());
|
||||
} else {
|
||||
JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2);
|
||||
enable(Options::audio.enabled);
|
||||
|
||||
std::cout << "\n** AUDIO SYSTEM **\n";
|
||||
std::cout << "Audio system initialized successfully\n";
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user