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:
1 parent
bc53174f2b
commit
1d124e967d
2 files changed
+512
-355
No files matched your search
+416
-340
@@ -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).
|
} // namespace
|
||||||
void updateOutgoingFade() {
|
|
||||||
if (outgoing_music.stream == nullptr || !outgoing_music.fade.active) { return; }
|
|
||||||
|
|
||||||
const float PROGRESS = fadeProgress(outgoing_music.fade);
|
void Engine::updateOutgoingFade() {
|
||||||
|
if (outgoing_music_.stream == nullptr || !outgoing_music_.fade.active) { return; }
|
||||||
|
|
||||||
|
const float PROGRESS = fadeProgress(outgoing_music_.fade);
|
||||||
if (PROGRESS >= 1.0F) {
|
if (PROGRESS >= 1.0F) {
|
||||||
SDL_DestroyAudioStream(outgoing_music.stream);
|
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||||
outgoing_music.stream = nullptr;
|
outgoing_music_.stream = nullptr;
|
||||||
outgoing_music.fade.active = false;
|
outgoing_music_.fade.active = false;
|
||||||
} else {
|
} else {
|
||||||
SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0F - PROGRESS));
|
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 Engine::updateIncomingFade() {
|
||||||
void updateIncomingFade() {
|
if (!incoming_fade_.active) { return; }
|
||||||
if (!incoming_fade.active) { return; }
|
|
||||||
|
|
||||||
const float PROGRESS = fadeProgress(incoming_fade);
|
const float PROGRESS = fadeProgress(incoming_fade_);
|
||||||
if (PROGRESS >= 1.0F) {
|
if (PROGRESS >= 1.0F) {
|
||||||
incoming_fade.active = false;
|
incoming_fade_.active = false;
|
||||||
SDL_SetAudioStreamGain(current_music->stream, music_volume);
|
SDL_SetAudioStreamGain(current_music_->stream, music_volume_);
|
||||||
} else {
|
} else {
|
||||||
SDL_SetAudioStreamGain(current_music->stream, music_volume * PROGRESS);
|
SDL_SetAudioStreamGain(current_music_->stream, music_volume_ * PROGRESS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alimenta la pista actual i la para si s'ha esgotat el stream vorbis.
|
void Engine::updateCurrentMusic() {
|
||||||
void updateCurrentMusic() {
|
if (!music_enabled_ || current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
|
||||||
if (!music_enabled || current_music == nullptr || current_music->state != MusicState::PLAYING) { return; }
|
|
||||||
|
|
||||||
updateIncomingFade();
|
updateIncomingFade();
|
||||||
|
|
||||||
pumpMusic(current_music);
|
pumpMusic(current_music_);
|
||||||
if (current_music->times == 0 && SDL_GetAudioStreamAvailable(current_music->stream) == 0) {
|
if (current_music_->times == 0 && SDL_GetAudioStreamAvailable(current_music_->stream) == 0) {
|
||||||
stopMusic();
|
stopMusic();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reompli (si cal loop) o para els canals d'efectes actius.
|
void Engine::updateSoundChannels() {
|
||||||
void updateSoundChannels() {
|
if (!sound_enabled_) { return; }
|
||||||
if (!sound_enabled) { return; }
|
|
||||||
|
|
||||||
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
|
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
|
||||||
if (channels[i].state != ChannelState::PLAYING) { continue; }
|
if (channels_[i].state != ChannelState::PLAYING) { continue; }
|
||||||
|
|
||||||
if (channels[i].times != 0) {
|
if (channels_[i].times != 0) {
|
||||||
if (static_cast<Uint32>(SDL_GetAudioStreamAvailable(channels[i].stream)) < (channels[i].sound->length / 2)) {
|
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);
|
SDL_PutAudioStreamData(channels_[i].stream, channels_[i].sound->buffer.get(), channels_[i].sound->length);
|
||||||
if (channels[i].times > 0) { channels[i].times--; }
|
if (channels_[i].times > 0) { channels_[i].times--; }
|
||||||
}
|
}
|
||||||
} else if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) {
|
} else if (SDL_GetAudioStreamAvailable(channels_[i].stream) == 0) {
|
||||||
stopChannel(i);
|
stopChannel(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Roba l'stream de la música actual al slot outgoing per a un fade-out de
|
void Engine::stealCurrentIntoOutgoing(const int duration_ms) {
|
||||||
// `duration_ms` ms. Destrueix qualsevol outgoing anterior. L'stream robat
|
if (outgoing_music_.stream != nullptr) {
|
||||||
// ja no es pot alimentar (perd l'accés al vorbis decoder), per això cal
|
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||||
// preFillOutgoing abans del robatori perquè tingui so suficient fins que
|
outgoing_music_.stream = nullptr;
|
||||||
// acabi el fade. No-op si no hi ha música sonant amb stream vàlid.
|
outgoing_music_.fade.active = false;
|
||||||
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) {
|
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING || current_music_->stream == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
preFillOutgoing(current_music, duration_ms);
|
preFillOutgoing(current_music_, duration_ms);
|
||||||
outgoing_music.stream = current_music->stream;
|
outgoing_music_.stream = current_music_->stream;
|
||||||
outgoing_music.fade = {
|
outgoing_music_.fade = {
|
||||||
.active = true,
|
.active = true,
|
||||||
.start_time = SDL_GetTicks(),
|
.start_time = SDL_GetTicks(),
|
||||||
.duration_ms = duration_ms,
|
.duration_ms = duration_ms,
|
||||||
.initial_volume = music_volume,
|
.initial_volume = music_volume_,
|
||||||
};
|
};
|
||||||
current_music->stream = nullptr;
|
current_music_->stream = nullptr;
|
||||||
current_music->state = MusicState::STOPPED;
|
current_music_->state = MusicState::STOPPED;
|
||||||
if (current_music->vorbis != nullptr) { stb_vorbis_seek_start(current_music->vorbis); }
|
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>
|
template <typename Fn>
|
||||||
void forEachTargetChannel(const int channel, Fn&& fn) {
|
void Engine::forEachTargetChannel(const int channel, Fn&& fn) {
|
||||||
if (channel == -1) {
|
if (channel == -1) {
|
||||||
for (auto& ch : channels) { fn(ch); }
|
for (auto& ch : channels_) { fn(ch); }
|
||||||
} else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) {
|
} else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) {
|
||||||
fn(channels[channel]);
|
fn(channels_[channel]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
// --- Engine public API ---
|
||||||
|
|
||||||
// --- Lifecycle ---
|
void Engine::update() {
|
||||||
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
|
||||||
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(¤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 (!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(¤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 (!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(¤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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(¤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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ---
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in new issue
Block a user