refactor(core/audio): introduce Ja::Engine class (COUP-01, CONV-02)

Encapsula l'estat del motor a Ja::Engine — classe amb RAII (ctor obre
SDL_AudioDevice, dtor el tanca) i `assert` al ctor contra instàncies
concurrents. Tots els globals del namespace anònim del commit 1
(current_music, channels[], audio_spec, music_volume, sound_volume[],
music_enabled, sound_enabled, sdl_audio_device, outgoing_music,
incoming_fade) es converteixen en membres privats amb sufix `_`. Els
helpers que toquen estat (stealCurrentIntoOutgoing,
updateOutgoing/IncomingFade, updateCurrent*, forEachTargetChannel)
passen a mètodes privats. Els stateless (feedMusicChunk, pumpMusic,
preFillOutgoing, fadeProgress) es queden al namespace anònim del .cpp.

Engine::active() static accessor: registrat al ctor, desregistrat al
dtor. Els deleters Ja::deleteMusic/deleteSound consulten Engine::active()
per parar canals/música abans d'alliberar recursos; si active() és
nullptr (shutdown-order invertit), fan cleanup local silenciós. Això
resol CONV-02 (engine ja és classe) i tanca COUP-01.

Compat amb els callers: les free functions Ja::init/quit/update,
Ja::playMusic, Ja::pauseMusic, Ja::resumeMusic, Ja::stopMusic,
Ja::fadeOutMusic, Ja::crossfadeMusic, Ja::getMusicState,
Ja::setMusicVolume, Ja::enableMusic, Ja::playSound, Ja::playSoundOnChannel,
Ja::pauseChannel, Ja::resumeChannel, Ja::stopChannel, Ja::setSoundVolume
i Ja::enableSound es mantenen com a thin forwarders contra Engine::active().
audio.cpp no canvia en aquest commit; les seves crides Ja:: continuen
funcionant. Al commit 3 Audio agafarà ownership directe via
std::unique_ptr<Engine> i aquests forwarders desapareixeran.

Ja::init() crea un std::unique_ptr<Engine> propietari dins d'un namespace
anònim del .cpp (encara no és Audio qui posseeix — això és el commit 3).
Ja::loadMusic/loadSound/deleteMusic/deleteSound es queden com a free
functions permanents (no depenen del motor per construir).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
JailDesignerandClaude Opus 4.7 committed 2026-04-20 13:21:01 +02:00
1 parent bc53174f2b
commit 1d124e967d
2 files changed
+552 -395

No files matched your search

+456 -380
View File
@@ -1,7 +1,9 @@
#include "core/audio/jail_audio.hpp" #include "core/audio/jail_audio.hpp"
#include <algorithm> #include <algorithm>
#include <cassert>
#include <cstdio> #include <cstdio>
#include <memory>
// Implementació completa de stb_vorbis en aquest TU. HEADER_ONLY es queda // Implementació completa de stb_vorbis en aquest TU. HEADER_ONLY es queda
// indefinit aquí perquè el .c aporti les definicions. La resta del projecte // indefinit aquí perquè el .c aporti les definicions. La resta del projecte
@@ -13,23 +15,8 @@
namespace Ja { namespace Ja {
// --- Streaming internals (file-scope constants) ---
namespace { namespace {
// --- Estat intern (internal linkage via anonymous namespace) ---
Music* current_music{nullptr};
Channel channels[MAX_SIMULTANEOUS_CHANNELS];
SDL_AudioSpec audio_spec{DEFAULT_SPEC};
float music_volume{1.0F};
float sound_volume[MAX_GROUPS];
bool music_enabled{true};
bool sound_enabled{true};
SDL_AudioDeviceID sdl_audio_device{0};
OutgoingMusic outgoing_music;
FadeState incoming_fade;
// --- Streaming internals ---
// Bytes-per-sample per canal (sempre s16) // Bytes-per-sample per canal (sempre s16)
constexpr int MUSIC_BYTES_PER_SAMPLE = 2; constexpr int MUSIC_BYTES_PER_SAMPLE = 2;
// Quants shorts decodifiquem per crida a get_samples_short_interleaved. // Quants shorts decodifiquem per crida a get_samples_short_interleaved.
@@ -39,8 +26,42 @@ namespace Ja {
// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns. // l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
constexpr float MUSIC_LOW_WATER_SECONDS = 0.5F; constexpr float MUSIC_LOW_WATER_SECONDS = 0.5F;
// Decodifica un chunk del vorbis i el volca a l'stream. Retorna samples // Propietari únic de l'Engine que creen els forwarders Ja::init/quit.
// decodificats per canal (0 = EOF de l'stream vorbis). // Al commit 3 això desapareix: Audio agafarà ownership directe.
std::unique_ptr<Engine> owned_engine;
} // 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 { auto feedMusicChunk(Music* music) -> int {
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return 0; } if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return 0; }
@@ -58,8 +79,6 @@ namespace Ja {
return SAMPLES_PER_CHANNEL; return SAMPLES_PER_CHANNEL;
} }
// Reompli l'stream fins que tinga ≥ MUSIC_LOW_WATER_SECONDS bufferats.
// En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar.
void pumpMusic(Music* music) { void pumpMusic(Music* music) {
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; } if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; }
@@ -80,11 +99,7 @@ namespace Ja {
} }
} }
// Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream void preFillOutgoing(Music* music, const int duration_ms) {
// siga robat per outgoing_music (crossfade o fade-out). Imprescindible amb
// 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.
void preFillOutgoing(Music* music, int duration_ms) {
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; } 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 BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
@@ -92,7 +107,7 @@ namespace Ja {
while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) { while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) {
const int DECODED = feedMusicChunk(music); const int DECODED = feedMusicChunk(music);
if (DECODED <= 0) { break; } // EOF: deixem drenar el que hi haja if (DECODED <= 0) { break; }
} }
} }
@@ -106,132 +121,426 @@ namespace Ja {
return static_cast<float>(ELAPSED) / static_cast<float>(fade.duration_ms); 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).
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).
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.
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.
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);
}
}
}
// 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.
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); }
}
// 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>
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]);
}
}
} // namespace } // namespace
// --- Lifecycle --- void Engine::updateOutgoingFade() {
if (outgoing_music_.stream == nullptr || !outgoing_music_.fade.active) { return; }
void init(const int freq, const SDL_AudioFormat format, const int num_channels) { const float PROGRESS = fadeProgress(outgoing_music_.fade);
audio_spec = {.format = format, .channels = num_channels, .freq = freq}; if (PROGRESS >= 1.0F) {
if (sdl_audio_device != 0) { SDL_CloseAudioDevice(sdl_audio_device); } SDL_DestroyAudioStream(outgoing_music_.stream);
sdl_audio_device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec); outgoing_music_.stream = nullptr;
if (sdl_audio_device == 0) { std::fprintf(stderr, "Ja: Failed to initialize SDL audio!\n"); } outgoing_music_.fade.active = false;
for (auto& channel : channels) { channel.state = ChannelState::FREE; } } else {
} SDL_SetAudioStreamGain(outgoing_music_.stream, outgoing_music_.fade.initial_volume * (1.0F - PROGRESS));
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;
} }
void update() { 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 (!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();
}
}
void Engine::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);
}
}
}
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(); updateOutgoingFade();
updateCurrentMusic(); updateCurrentMusic();
updateSoundChannels(); updateSoundChannels();
} }
// --- Music --- void Engine::playMusic(Music* music, const int loop) {
if (!music_enabled_ || 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(&current_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 (!music_enabled_) { return; }
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
current_music_->state = MusicState::PAUSED;
SDL_UnbindAudioStream(current_music_->stream);
}
void Engine::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);
}
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 (!music_enabled_) { return; }
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_enabled_ || 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(&current_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 (!music_enabled_) { return MusicState::DISABLED; }
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::enableMusic(const bool value) {
if (!value && current_music_ != nullptr && (current_music_->state == MusicState::PLAYING)) { stopMusic(); }
music_enabled_ = value;
}
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_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);
}
auto Engine::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::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) {
if (!sound_enabled_) { return; }
forEachTargetChannel(channel, [](Channel& ch) {
if (ch.state == ChannelState::PLAYING) {
ch.state = ChannelState::PAUSED;
SDL_UnbindAudioStream(ch.stream);
}
});
}
void Engine::resumeChannel(const int channel) {
if (!sound_enabled_) { return; }
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::enableSound(const bool value) {
if (!value) {
stopChannel(-1);
}
sound_enabled_ = value;
}
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); }
}
}
// --- Free-function forwarders ---
// Mantenen l'API actual (Ja::update, Ja::playMusic, ...) contra el motor
// actiu. Al commit 3 Audio els substituirà per crides directes a engine_->
// i els forwarders de música/so desapareixeran; init/quit/update i els
// factories queden de forma permanent.
void init(const int freq, const SDL_AudioFormat format, const int num_channels) {
owned_engine = std::make_unique<Engine>(freq, format, num_channels);
}
void quit() {
owned_engine.reset();
}
void update() {
if (Engine* eng = Engine::active()) { eng->update(); }
}
void playMusic(Music* music, const int loop) {
if (Engine* eng = Engine::active()) { eng->playMusic(music, loop); }
}
void pauseMusic() {
if (Engine* eng = Engine::active()) { eng->pauseMusic(); }
}
void resumeMusic() {
if (Engine* eng = Engine::active()) { eng->resumeMusic(); }
}
void stopMusic() {
if (Engine* eng = Engine::active()) { eng->stopMusic(); }
}
void fadeOutMusic(const int milliseconds) {
if (Engine* eng = Engine::active()) { eng->fadeOutMusic(milliseconds); }
}
void crossfadeMusic(Music* music, const int crossfade_ms, const int loop) {
if (Engine* eng = Engine::active()) { eng->crossfadeMusic(music, crossfade_ms, loop); }
}
auto getMusicState() -> MusicState {
if (const Engine* eng = Engine::active()) { return eng->getMusicState(); }
return MusicState::INVALID;
}
auto setMusicVolume(float volume) -> float {
if (Engine* eng = Engine::active()) { return eng->setMusicVolume(volume); }
return 0.0F;
}
void enableMusic(const bool value) {
if (Engine* eng = Engine::active()) { eng->enableMusic(value); }
}
auto playSound(Sound* sound, const int loop, const int group) -> int {
if (Engine* eng = Engine::active()) { return eng->playSound(sound, loop, group); }
return -1;
}
auto playSoundOnChannel(Sound* sound, const int channel, const int loop, const int group) -> int {
if (Engine* eng = Engine::active()) { return eng->playSoundOnChannel(sound, channel, loop, group); }
return -1;
}
void pauseChannel(const int channel) {
if (Engine* eng = Engine::active()) { eng->pauseChannel(channel); }
}
void resumeChannel(const int channel) {
if (Engine* eng = Engine::active()) { eng->resumeChannel(channel); }
}
void stopChannel(const int channel) {
if (Engine* eng = Engine::active()) { eng->stopChannel(channel); }
}
auto setSoundVolume(float volume, const int group) -> float {
if (Engine* eng = Engine::active()) { return eng->setSoundVolume(volume, group); }
return 0.0F;
}
void enableSound(const bool value) {
if (Engine* eng = Engine::active()) { eng->enableSound(value); }
}
// --- Factories i destructors (permanents) ---
auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* { auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
if (buffer == nullptr || length == 0) { return nullptr; } if (buffer == nullptr || length == 0) { return nullptr; }
@@ -271,147 +580,18 @@ namespace Ja {
return music; return music;
} }
void playMusic(Music* music, const int loop) {
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(&current_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");
}
}
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);
}
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);
}
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);
}
}
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;
}
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(&current_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,
};
}
auto getMusicState() -> MusicState {
if (!music_enabled) { return MusicState::DISABLED; }
if (current_music == nullptr) { return MusicState::INVALID; }
return current_music->state;
}
void deleteMusic(Music* music) { void deleteMusic(Music* music) {
if (music == nullptr) { return; } if (music == nullptr) { return; }
if (current_music == music) { // Notifiquem el motor actiu perquè pari la pista si és la current_music.
stopMusic(); // Si no hi ha motor (shutdown-order invertit), passem: els recursos
current_music = nullptr; // 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->stream != nullptr) { SDL_DestroyAudioStream(music->stream); }
if (music->vorbis != nullptr) { stb_vorbis_close(music->vorbis); } 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; delete music;
} }
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;
}
void enableMusic(const bool value) {
if (!value && current_music != nullptr && (current_music->state == MusicState::PLAYING)) { stopMusic(); }
music_enabled = value;
}
// --- Sound ---
auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound* { auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound* {
auto sound = std::make_unique<Sound>(); auto sound = std::make_unique<Sound>();
Uint8* raw = nullptr; Uint8* raw = nullptr;
@@ -423,117 +603,13 @@ namespace Ja {
return sound.release(); return sound.release();
} }
auto playSound(Sound* sound, const int loop, const int group) -> 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);
}
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;
}
void deleteSound(Sound* sound) { void deleteSound(Sound* sound) {
if (sound == nullptr) { return; } if (sound == nullptr) { return; }
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; i++) { if (Engine* eng = Engine::active()) { eng->onSoundDeleted(sound); }
if (channels[i].sound == sound) { stopChannel(i); }
}
// buffer es destrueix automàticament via RAII (SdlFreeDeleter). // buffer es destrueix automàticament via RAII (SdlFreeDeleter).
delete sound; delete sound;
} }
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);
}
});
}
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);
}
});
}
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;
}
});
}
auto 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;
}
// 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;
}
void enableSound(const bool value) {
if (!value) {
stopChannel(-1);
}
sound_enabled = value;
}
} // namespace Ja } // namespace Ja
// --- stb_vorbis macro leak cleanup --- // --- stb_vorbis macro leak cleanup ---
+96 -15
View File
@@ -24,10 +24,10 @@ 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 ---
@@ -51,9 +51,9 @@ namespace Ja {
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 Definitions ---
@@ -106,14 +106,88 @@ namespace Ja {
FadeState fade; FadeState fade;
}; };
// --- Lifecycle --- // --- Engine ---
// Encapsula tot l'estat que abans vivia com a globals inline. Un sol Engine
// viu per procés (enforceat via assert al ctor contra `active_`). El ctor
// obre el device SDL; el dtor el tanca (RAII). Els deleters
// `Ja::deleteMusic`/`Ja::deleteSound` accedeixen al motor actiu via
// `Engine::active()` per parar canals abans d'alliberar.
class Engine {
public:
Engine(int freq, SDL_AudioFormat format, int num_channels);
~Engine();
Engine(const Engine&) = delete;
auto operator=(const Engine&) -> Engine& = delete;
Engine(Engine&&) = delete;
auto operator=(Engine&&) -> Engine& = delete;
// Retorna el motor actiu o nullptr si cap ha estat construït. L'usen
// els deleters de recursos perquè no els arriba cap referència directa.
[[nodiscard]] static auto active() noexcept -> Engine*;
void update();
// --- Música ---
void playMusic(Music* music, int loop = -1);
void pauseMusic();
void resumeMusic();
void stopMusic();
void fadeOutMusic(int milliseconds);
void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
[[nodiscard]] auto getMusicState() const -> MusicState;
auto setMusicVolume(float volume) -> float;
void enableMusic(bool value);
// Notifica al motor que un Music s'està destruint: si és el current_music
// s'atura abans que els seus recursos (stream/vorbis) deixin de ser vàlids.
void onMusicDeleted(const Music* music);
// --- So ---
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
void pauseChannel(int channel);
void resumeChannel(int channel);
void stopChannel(int channel);
auto setSoundVolume(float volume, int group = -1) -> float;
void enableSound(bool value);
// Notifica al motor que un Sound s'està destruint: els canals que el
// referenciïn es paren abans d'alliberar el buffer.
void onSoundDeleted(const Sound* sound);
private:
void stealCurrentIntoOutgoing(int duration_ms);
void updateOutgoingFade();
void updateIncomingFade();
void updateCurrentMusic();
void updateSoundChannels();
template <typename Fn>
void forEachTargetChannel(int channel, Fn&& fn);
Music* current_music_{nullptr};
Channel channels_[MAX_SIMULTANEOUS_CHANNELS]{};
SDL_AudioSpec audio_spec_{DEFAULT_SPEC};
float music_volume_{1.0F};
float sound_volume_[MAX_GROUPS]{};
bool music_enabled_{true};
bool sound_enabled_{true};
SDL_AudioDeviceID sdl_audio_device_{0};
OutgoingMusic outgoing_music_;
FadeState incoming_fade_;
// NOLINTNEXTLINE(readability-identifier-naming) — convenció projecte: private static amb sufix _
static Engine* active_;
};
// --- Lifecycle forwarders ---
// Compat amb els callers actuals. Creen/destrueixen l'Engine únic via
// make_unique emmagatzemat al propi .cpp. Al commit següent Audio agafarà
// ownership directe i aquests forwarders desapareixeran per a la capa de
// control; els de música/so romanen només el temps de la migració.
void init(int freq, SDL_AudioFormat format, int num_channels); void init(int freq, SDL_AudioFormat format, int num_channels);
void quit(); void quit();
void update(); void update();
// --- Music API --- // --- Music API forwarders (temporal, eliminats al commit 3) ---
[[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length) -> Music*;
[[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music*;
void playMusic(Music* music, int loop = -1); void playMusic(Music* music, int loop = -1);
void pauseMusic(); void pauseMusic();
void resumeMusic(); void resumeMusic();
@@ -121,19 +195,26 @@ namespace Ja {
void fadeOutMusic(int milliseconds); void fadeOutMusic(int milliseconds);
void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1); void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
[[nodiscard]] auto getMusicState() -> MusicState; [[nodiscard]] auto getMusicState() -> MusicState;
void deleteMusic(Music* music);
auto setMusicVolume(float volume) -> float; auto setMusicVolume(float volume) -> float;
void enableMusic(bool value); void enableMusic(bool value);
// --- Sound API --- // --- Sound API forwarders (temporal, eliminats al commit 3) ---
[[nodiscard]] auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound*;
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int; auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int; auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
void deleteSound(Sound* sound);
void pauseChannel(int channel); void pauseChannel(int channel);
void resumeChannel(int channel); void resumeChannel(int channel);
void stopChannel(int channel); void stopChannel(int channel);
auto setSoundVolume(float volume, int group = -1) -> float; auto setSoundVolume(float volume, int group = -1) -> float;
void enableSound(bool value); void enableSound(bool value);
// --- Factories i destructors (permanents) ---
// No depenen de l'estat del motor: loadMusic/loadSound només construeixen
// objectes, deleteMusic/deleteSound consulten Engine::active() per parar
// canals abans d'alliberar (si el motor encara viu).
[[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length) -> Music*;
[[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music*;
void deleteMusic(Music* music);
[[nodiscard]] auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound*;
void deleteSound(Sound* sound);
} // namespace Ja } // namespace Ja