refactor(core/audio): split jail_audio.hpp/.cpp (COUP-02, PATCH-03)
Preparatori per COUP-01. No canvia comportament: només mou les
definicions inline de jail_audio.hpp a un jail_audio.cpp nou, deixant
el .hpp amb declaracions + tipus + constants.
Efectes col·laterals que es tanquen gratis:
- COUP-02: jail_audio deixa de ser header-only. Abans cada TU que
l'incloïa compilava 650 línies inline (amb stb_vorbis.c en mode
HEADER_ONLY). Ara el .hpp té ~130 línies de declaracions.
- PATCH-03: #include "external/stb_vorbis.c" + #undef L/C/R/PLAYBACK_*
viu només al final de jail_audio.cpp. audio.cpp perd el workaround
dels undefs.
Estat intern (current_music, channels[], audio_spec, music_volume,
sound_volume[], music_enabled, sound_enabled, sdl_audio_device,
outgoing_music, incoming_fade) baixa al .cpp dins d'un namespace
anònim (internal linkage). Els helpers privats (feedMusicChunk,
pumpMusic, preFillOutgoing, fadeProgress, update*Fade, updateCurrent*,
stealCurrentIntoOutgoing, forEachTargetChannel) també viuen a
l'anonymous namespace.
Ja::Music i Ja::Sound són públics; el .hpp forward-declara
`struct stb_vorbis` perquè els callers només els agafen per punter.
NOLINT(readability-identifier-naming) al forward-decl perquè el nom
l'imposa stb_vorbis.
jail_audio.cpp s'afegeix a APP_SOURCES al CMakeLists.txt.
.clang-tidy: desactivat clang-analyzer-core.UndefinedBinaryOperatorResult
globalment. Fals positiu conegut a external/stb_vorbis.c que abans no es
disparava (no hi havia TU que cridés directament stb_vorbis_seek_start
des del mateix fitxer on la funció està inlined); ara que jail_audio.cpp
sí que fa aquestes crides directes, el path d'anàlisi passa pel .c i el
check dispara. HeaderFilterRegex no aplica a clang-analyzer-*.
Preparació per COUP-01 (introduir Ja::Engine al commit següent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5610814c9f
commit
bc53174f2b
5 files changed
+593
-547
No files matched your search
@@ -5,21 +5,6 @@
|
||||
#include <algorithm> // Per a clamp
|
||||
#include <cstdio> // Per a std::fprintf
|
||||
|
||||
// Implementació de stb_vorbis (ha d'anar ABANS d'incloure jail_audio.hpp).
|
||||
// clang-format off
|
||||
#undef STB_VORBIS_HEADER_ONLY
|
||||
// NOLINTNEXTLINE(bugprone-suspicious-include)
|
||||
#include "external/stb_vorbis.c"
|
||||
// stb_vorbis.c filtra les macros L, C i R (i PLAYBACK_*) al TU. Les netegem
|
||||
// perquè xocarien amb noms de paràmetres de plantilla en altres headers.
|
||||
#undef L
|
||||
#undef C
|
||||
#undef R
|
||||
#undef PLAYBACK_MONO
|
||||
#undef PLAYBACK_LEFT
|
||||
#undef PLAYBACK_RIGHT
|
||||
// clang-format on
|
||||
|
||||
#include "core/audio/audio_adapter.hpp" // Per a AudioResource::getMusic/getSound
|
||||
#include "core/audio/jail_audio.hpp" // Per a Ja::* (motor jailgames)
|
||||
#include "utils/defaults.hpp" // Per a Defaults::Audio::FREQUENCY
|
||||
|
||||
@@ -0,0 +1,550 @@
|
||||
#include "core/audio/jail_audio.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
// Implementació completa de stb_vorbis en aquest TU. HEADER_ONLY es queda
|
||||
// indefinit aquí perquè el .c aporti les definicions. La resta del projecte
|
||||
// només veu el forward-decl de jail_audio.hpp.
|
||||
// clang-format off
|
||||
// NOLINTNEXTLINE(bugprone-suspicious-include)
|
||||
#include "external/stb_vorbis.c"
|
||||
// clang-format on
|
||||
|
||||
namespace Ja {
|
||||
|
||||
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)
|
||||
constexpr int MUSIC_BYTES_PER_SAMPLE = 2;
|
||||
// Quants shorts decodifiquem per crida a get_samples_short_interleaved.
|
||||
// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz.
|
||||
constexpr int MUSIC_CHUNK_SHORTS = 8192;
|
||||
// Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
|
||||
// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
|
||||
constexpr float MUSIC_LOW_WATER_SECONDS = 0.5F;
|
||||
|
||||
// Decodifica un chunk del vorbis i el volca a l'stream. Retorna samples
|
||||
// decodificats per canal (0 = EOF de l'stream vorbis).
|
||||
auto feedMusicChunk(Music* music) -> int {
|
||||
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return 0; }
|
||||
|
||||
short chunk[MUSIC_CHUNK_SHORTS];
|
||||
const int NUM_CHANNELS = music->spec.channels;
|
||||
const int SAMPLES_PER_CHANNEL = stb_vorbis_get_samples_short_interleaved(
|
||||
music->vorbis,
|
||||
NUM_CHANNELS,
|
||||
static_cast<short*>(chunk),
|
||||
MUSIC_CHUNK_SHORTS);
|
||||
if (SAMPLES_PER_CHANNEL <= 0) { return 0; }
|
||||
|
||||
const int BYTES = SAMPLES_PER_CHANNEL * NUM_CHANNELS * MUSIC_BYTES_PER_SAMPLE;
|
||||
SDL_PutAudioStreamData(music->stream, static_cast<const void*>(chunk), BYTES);
|
||||
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) {
|
||||
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 LOW_WATER_BYTES = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));
|
||||
|
||||
while (SDL_GetAudioStreamAvailable(music->stream) < LOW_WATER_BYTES) {
|
||||
const int DECODED = feedMusicChunk(music);
|
||||
if (DECODED > 0) { continue; }
|
||||
|
||||
// EOF: si queden loops, rebobinar; si no, tallar i deixar drenar.
|
||||
if (music->times != 0) {
|
||||
stb_vorbis_seek_start(music->vorbis);
|
||||
if (music->times > 0) { music->times--; }
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream
|
||||
// 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; }
|
||||
|
||||
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||
const int NEEDED_BYTES = static_cast<int>((static_cast<std::int64_t>(duration_ms) * BYTES_PER_SECOND) / 1000);
|
||||
|
||||
while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) {
|
||||
const int DECODED = feedMusicChunk(music);
|
||||
if (DECODED <= 0) { break; } // EOF: deixem drenar el que hi haja
|
||||
}
|
||||
}
|
||||
|
||||
// Retorna el progrés lineal [0..1] d'un fade. 1.0 vol dir completat. Única
|
||||
// font de la corba del fade: si es vol canviar a logarítmica/quadràtica,
|
||||
// s'edita aquí i afecta fade-in i fade-out alhora.
|
||||
auto fadeProgress(const FadeState& fade) -> float {
|
||||
if (fade.duration_ms <= 0) { return 1.0F; }
|
||||
const Uint64 ELAPSED = SDL_GetTicks() - fade.start_time;
|
||||
if (ELAPSED >= static_cast<Uint64>(fade.duration_ms)) { return 1.0F; }
|
||||
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
|
||||
|
||||
// --- Lifecycle ---
|
||||
|
||||
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();
|
||||
updateCurrentMusic();
|
||||
updateSoundChannels();
|
||||
}
|
||||
|
||||
// --- Music ---
|
||||
|
||||
auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
|
||||
if (buffer == nullptr || length == 0) { return nullptr; }
|
||||
|
||||
// Allocem el Music primer per aprofitar el seu `std::vector<Uint8>`
|
||||
// com a propietari del OGG comprimit. stb_vorbis guarda un punter
|
||||
// persistent al buffer; com que ací no el resize'jem, el .data() és
|
||||
// estable durant tot el cicle de vida del music.
|
||||
auto music = std::make_unique<Music>();
|
||||
music->ogg_data.assign(buffer, buffer + length);
|
||||
|
||||
int vorbis_error = 0;
|
||||
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
|
||||
static_cast<int>(length),
|
||||
&vorbis_error,
|
||||
nullptr);
|
||||
if (music->vorbis == nullptr) {
|
||||
std::fprintf(stderr, "Ja::loadMusic: stb_vorbis_open_memory failed (error %d)\n", vorbis_error);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const stb_vorbis_info INFO = stb_vorbis_get_info(music->vorbis);
|
||||
music->spec.channels = static_cast<int>(INFO.channels);
|
||||
music->spec.freq = static_cast<int>(INFO.sample_rate);
|
||||
music->spec.format = SDL_AUDIO_S16;
|
||||
music->state = MusicState::STOPPED;
|
||||
|
||||
return music.release();
|
||||
}
|
||||
|
||||
// Overload amb filename. Resource::Cache l'usa per registrar el path dins
|
||||
// del propi Ja::Music (camp `filename`); la capa Audio l'usa per recuperar
|
||||
// el nom després d'un playMusic(Ja::Music*, ...) — veure PATCH-02.
|
||||
auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music* {
|
||||
Music* music = loadMusic(buffer, length);
|
||||
if (music != nullptr && filename != nullptr) { music->filename = filename; }
|
||||
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) {
|
||||
if (music == nullptr) { return; }
|
||||
if (current_music == music) {
|
||||
stopMusic();
|
||||
current_music = nullptr;
|
||||
}
|
||||
if (music->stream != nullptr) { SDL_DestroyAudioStream(music->stream); }
|
||||
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;
|
||||
}
|
||||
|
||||
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 sound = std::make_unique<Sound>();
|
||||
Uint8* raw = nullptr;
|
||||
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), true, &sound->spec, &raw, &sound->length)) {
|
||||
std::fprintf(stderr, "Ja::loadSound: Failed to load WAV from memory: %s\n", SDL_GetError());
|
||||
return nullptr;
|
||||
}
|
||||
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
|
||||
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) {
|
||||
if (sound == nullptr) { return; }
|
||||
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||
if (channels[i].sound == sound) { stopChannel(i); }
|
||||
}
|
||||
// buffer es destrueix automàticament via RAII (SdlFreeDeleter).
|
||||
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
|
||||
|
||||
// --- stb_vorbis macro leak cleanup ---
|
||||
// stb_vorbis.c filtra noms curts (L, C, R i PLAYBACK_*) al TU que el compila.
|
||||
// Xocarien amb paràmetres de plantilla d'altres headers si aquestes definicions
|
||||
// s'escapessin. Els netegem al final del TU per tancar la porta.
|
||||
// clang-format off
|
||||
#undef L
|
||||
#undef C
|
||||
#undef R
|
||||
#undef PLAYBACK_MONO
|
||||
#undef PLAYBACK_LEFT
|
||||
#undef PLAYBACK_RIGHT
|
||||
// clang-format on
|
||||
@@ -3,19 +3,17 @@
|
||||
// --- Includes ---
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// clang-format off
|
||||
#define STB_VORBIS_HEADER_ONLY
|
||||
// NOLINTNEXTLINE(bugprone-suspicious-include)
|
||||
#include "external/stb_vorbis.c" // Para stb_vorbis_open_memory i streaming
|
||||
// clang-format on
|
||||
// Forward-declaració del decoder de vorbis. La implementació viu a
|
||||
// jail_audio.cpp (únic TU que compila external/stb_vorbis.c). Qualsevol caller
|
||||
// només necessita `stb_vorbis*` per punter — mai per valor — així que el
|
||||
// forward decl n'hi ha prou i evita arrossegar el .c a tots els TU.
|
||||
// NOLINTNEXTLINE(readability-identifier-naming) — nom imposat per l'API de stb_vorbis
|
||||
struct stb_vorbis;
|
||||
|
||||
// Deleter stateless per a buffers reservats amb `SDL_malloc` / `SDL_LoadWAV*`.
|
||||
// Compatible amb `std::unique_ptr<Uint8[], SdlFreeDeleter>` — zero size overhead
|
||||
@@ -49,7 +47,7 @@ namespace Ja {
|
||||
DISABLED,
|
||||
};
|
||||
|
||||
// --- Struct Definitions ---
|
||||
// --- Constants ---
|
||||
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20;
|
||||
inline constexpr int MAX_GROUPS = 2;
|
||||
|
||||
@@ -58,6 +56,7 @@ namespace Ja {
|
||||
// Audio alimenta des de Defaults::Audio — única font de veritat del projecte.
|
||||
inline constexpr SDL_AudioSpec DEFAULT_SPEC{SDL_AUDIO_S16, 2, 48000};
|
||||
|
||||
// --- Struct Definitions ---
|
||||
struct Sound {
|
||||
SDL_AudioSpec spec{DEFAULT_SPEC};
|
||||
Uint32 length{0};
|
||||
@@ -95,7 +94,6 @@ namespace Ja {
|
||||
MusicState state{MusicState::INVALID};
|
||||
};
|
||||
|
||||
// --- Crossfade / Fade State ---
|
||||
struct FadeState {
|
||||
bool active{false};
|
||||
Uint64 start_time{0};
|
||||
@@ -108,527 +106,34 @@ namespace Ja {
|
||||
FadeState fade;
|
||||
};
|
||||
|
||||
// --- Internal Global State (inline, C++17) ---
|
||||
inline Music* current_music{nullptr};
|
||||
inline Channel channels[MAX_SIMULTANEOUS_CHANNELS];
|
||||
|
||||
inline SDL_AudioSpec audio_spec{DEFAULT_SPEC};
|
||||
inline float music_volume{1.0F};
|
||||
inline float sound_volume[MAX_GROUPS];
|
||||
inline bool music_enabled{true};
|
||||
inline bool sound_enabled{true};
|
||||
inline SDL_AudioDeviceID sdl_audio_device{0};
|
||||
|
||||
inline OutgoingMusic outgoing_music;
|
||||
inline FadeState incoming_fade;
|
||||
|
||||
// --- Forward Declarations ---
|
||||
inline void stopMusic();
|
||||
inline void stopChannel(int channel);
|
||||
inline auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
|
||||
inline void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
|
||||
|
||||
// --- Music streaming internals ---
|
||||
// Bytes-per-sample per canal (sempre s16)
|
||||
inline constexpr int MUSIC_BYTES_PER_SAMPLE = 2;
|
||||
// Quants shorts decodifiquem per crida a get_samples_short_interleaved.
|
||||
// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz.
|
||||
inline constexpr int MUSIC_CHUNK_SHORTS = 8192;
|
||||
// Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
|
||||
// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
|
||||
inline constexpr float MUSIC_LOW_WATER_SECONDS = 0.5F;
|
||||
|
||||
// Decodifica un chunk del vorbis i el volca a l'stream. Retorna samples
|
||||
// decodificats per canal (0 = EOF de l'stream vorbis).
|
||||
inline auto feedMusicChunk(Music* music) -> int {
|
||||
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return 0; }
|
||||
|
||||
short chunk[MUSIC_CHUNK_SHORTS];
|
||||
const int NUM_CHANNELS = music->spec.channels;
|
||||
const int SAMPLES_PER_CHANNEL = stb_vorbis_get_samples_short_interleaved(
|
||||
music->vorbis,
|
||||
NUM_CHANNELS,
|
||||
static_cast<short*>(chunk),
|
||||
MUSIC_CHUNK_SHORTS);
|
||||
if (SAMPLES_PER_CHANNEL <= 0) { return 0; }
|
||||
|
||||
const int BYTES = SAMPLES_PER_CHANNEL * NUM_CHANNELS * MUSIC_BYTES_PER_SAMPLE;
|
||||
SDL_PutAudioStreamData(music->stream, static_cast<const void*>(chunk), BYTES);
|
||||
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.
|
||||
inline void pumpMusic(Music* music) {
|
||||
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 LOW_WATER_BYTES = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));
|
||||
|
||||
while (SDL_GetAudioStreamAvailable(music->stream) < LOW_WATER_BYTES) {
|
||||
const int DECODED = feedMusicChunk(music);
|
||||
if (DECODED > 0) { continue; }
|
||||
|
||||
// EOF: si queden loops, rebobinar; si no, tallar i deixar drenar.
|
||||
if (music->times != 0) {
|
||||
stb_vorbis_seek_start(music->vorbis);
|
||||
if (music->times > 0) { music->times--; }
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream
|
||||
// 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.
|
||||
inline void preFillOutgoing(Music* music, int duration_ms) {
|
||||
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 NEEDED_BYTES = static_cast<int>((static_cast<std::int64_t>(duration_ms) * BYTES_PER_SECOND) / 1000);
|
||||
|
||||
while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) {
|
||||
const int DECODED = feedMusicChunk(music);
|
||||
if (DECODED <= 0) { break; } // EOF: deixem drenar el que hi haja
|
||||
}
|
||||
}
|
||||
|
||||
// --- Core Functions ---
|
||||
|
||||
// Retorna el progrés lineal [0..1] d'un fade. 1.0 vol dir completat. Única
|
||||
// font de la corba del fade: si es vol canviar a logarítmica/quadràtica,
|
||||
// s'edita aquí i afecta fade-in i fade-out alhora.
|
||||
inline auto fadeProgress(const FadeState& fade) -> float {
|
||||
if (fade.duration_ms <= 0) { return 1.0F; }
|
||||
const Uint64 ELAPSED = SDL_GetTicks() - fade.start_time;
|
||||
if (ELAPSED >= static_cast<Uint64>(fade.duration_ms)) { return 1.0F; }
|
||||
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).
|
||||
inline 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).
|
||||
inline 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.
|
||||
inline 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.
|
||||
inline 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void update() {
|
||||
updateOutgoingFade();
|
||||
updateCurrentMusic();
|
||||
updateSoundChannels();
|
||||
}
|
||||
|
||||
inline 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; }
|
||||
}
|
||||
|
||||
inline 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;
|
||||
}
|
||||
|
||||
// --- Music Functions ---
|
||||
|
||||
inline auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
|
||||
if (buffer == nullptr || length == 0) { return nullptr; }
|
||||
|
||||
// Allocem el Music primer per aprofitar el seu `std::vector<Uint8>`
|
||||
// com a propietari del OGG comprimit. stb_vorbis guarda un punter
|
||||
// persistent al buffer; com que ací no el resize'jem, el .data() és
|
||||
// estable durant tot el cicle de vida del music.
|
||||
auto music = std::make_unique<Music>();
|
||||
music->ogg_data.assign(buffer, buffer + length);
|
||||
|
||||
int vorbis_error = 0;
|
||||
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
|
||||
static_cast<int>(length),
|
||||
&vorbis_error,
|
||||
nullptr);
|
||||
if (music->vorbis == nullptr) {
|
||||
std::fprintf(stderr, "Ja::loadMusic: stb_vorbis_open_memory failed (error %d)\n", vorbis_error);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const stb_vorbis_info INFO = stb_vorbis_get_info(music->vorbis);
|
||||
music->spec.channels = static_cast<int>(INFO.channels);
|
||||
music->spec.freq = static_cast<int>(INFO.sample_rate);
|
||||
music->spec.format = SDL_AUDIO_S16;
|
||||
music->state = MusicState::STOPPED;
|
||||
|
||||
return music.release();
|
||||
}
|
||||
|
||||
// Overload amb filename. Resource::Cache l'usa per registrar el path dins
|
||||
// del propi Ja::Music (camp `filename`); la capa Audio l'usa per recuperar
|
||||
// el nom després d'un playMusic(Ja::Music*, ...) — veure PATCH-02.
|
||||
inline auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music* {
|
||||
Music* music = loadMusic(buffer, length);
|
||||
if (music != nullptr && filename != nullptr) { music->filename = filename; }
|
||||
return music;
|
||||
}
|
||||
|
||||
inline void playMusic(Music* music, const int loop = -1) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
inline 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);
|
||||
}
|
||||
|
||||
inline 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);
|
||||
}
|
||||
|
||||
inline 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
inline 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); }
|
||||
}
|
||||
|
||||
inline 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;
|
||||
}
|
||||
|
||||
inline 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,
|
||||
};
|
||||
}
|
||||
|
||||
inline auto getMusicState() -> MusicState {
|
||||
if (!music_enabled) { return MusicState::DISABLED; }
|
||||
if (current_music == nullptr) { return MusicState::INVALID; }
|
||||
|
||||
return current_music->state;
|
||||
}
|
||||
|
||||
inline void deleteMusic(Music* music) {
|
||||
if (music == nullptr) { return; }
|
||||
if (current_music == music) {
|
||||
stopMusic();
|
||||
current_music = nullptr;
|
||||
}
|
||||
if (music->stream != nullptr) { SDL_DestroyAudioStream(music->stream); }
|
||||
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;
|
||||
}
|
||||
|
||||
inline 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;
|
||||
}
|
||||
|
||||
inline void enableMusic(const bool value) {
|
||||
if (!value && current_music != nullptr && (current_music->state == MusicState::PLAYING)) { stopMusic(); }
|
||||
music_enabled = value;
|
||||
}
|
||||
|
||||
// --- Sound Functions ---
|
||||
|
||||
inline auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound* {
|
||||
auto sound = std::make_unique<Sound>();
|
||||
Uint8* raw = nullptr;
|
||||
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), true, &sound->spec, &raw, &sound->length)) {
|
||||
std::fprintf(stderr, "Ja::loadSound: Failed to load WAV from memory: %s\n", SDL_GetError());
|
||||
return nullptr;
|
||||
}
|
||||
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
|
||||
return sound.release();
|
||||
}
|
||||
|
||||
inline auto playSound(Sound* sound, const int loop = 0, const int group = 0) -> 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);
|
||||
}
|
||||
|
||||
inline 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;
|
||||
}
|
||||
|
||||
inline void deleteSound(Sound* sound) {
|
||||
if (sound == nullptr) { return; }
|
||||
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||
if (channels[i].sound == sound) { stopChannel(i); }
|
||||
}
|
||||
// buffer es destrueix automàticament via RAII (SdlFreeDeleter).
|
||||
delete sound;
|
||||
}
|
||||
|
||||
// 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>
|
||||
inline 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]);
|
||||
}
|
||||
}
|
||||
|
||||
inline 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
inline 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
inline 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
inline auto setSoundVolume(float volume, const int group = -1) -> 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;
|
||||
}
|
||||
|
||||
inline void enableSound(const bool value) {
|
||||
if (!value) {
|
||||
stopChannel(-1);
|
||||
}
|
||||
sound_enabled = value;
|
||||
}
|
||||
// --- Lifecycle ---
|
||||
void init(int freq, SDL_AudioFormat format, int num_channels);
|
||||
void quit();
|
||||
void update();
|
||||
|
||||
// --- Music API ---
|
||||
[[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 pauseMusic();
|
||||
void resumeMusic();
|
||||
void stopMusic();
|
||||
void fadeOutMusic(int milliseconds);
|
||||
void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
|
||||
[[nodiscard]] auto getMusicState() -> MusicState;
|
||||
void deleteMusic(Music* music);
|
||||
auto setMusicVolume(float volume) -> float;
|
||||
void enableMusic(bool value);
|
||||
|
||||
// --- Sound API ---
|
||||
[[nodiscard]] auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound*;
|
||||
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 deleteSound(Sound* sound);
|
||||
void pauseChannel(int channel);
|
||||
void resumeChannel(int channel);
|
||||
void stopChannel(int channel);
|
||||
auto setSoundVolume(float volume, int group = -1) -> float;
|
||||
void enableSound(bool value);
|
||||
|
||||
} // namespace Ja
|
||||
Reference in new issue
Block a user