From bc53174f2b4716f02a6a3cb667c72ab83993a6fe Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 13:11:15 +0200 Subject: [PATCH] refactor(core/audio): split jail_audio.hpp/.cpp (COUP-02, PATCH-03) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .clang-tidy | 5 + CMakeLists.txt | 1 + source/core/audio/audio.cpp | 15 - source/core/audio/jail_audio.cpp | 550 +++++++++++++++++++++++++++++ source/core/audio/jail_audio.hpp | 569 ++----------------------------- 5 files changed, 593 insertions(+), 547 deletions(-) create mode 100644 source/core/audio/jail_audio.cpp diff --git a/.clang-tidy b/.clang-tidy index 6cbebda..d99f8fd 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -9,6 +9,11 @@ Checks: - -bugprone-easily-swappable-parameters - -bugprone-narrowing-conversions - -modernize-avoid-c-arrays,-warnings-as-errors + # Fals positiu recurrent en codi C de tercers (source/external/stb_vorbis.c). + # HeaderFilterRegex no aplica a clang-analyzer-*, així que cal desactivar + # explícitament. El check inferència sobre variables locals de legacy C + # genera reports que no reflecteixen bugs reals. + - -clang-analyzer-core.UndefinedBinaryOperatorResult WarningsAsErrors: '*' # Headers nostres (excloem source/external/ que conté dependències de tercers no editables) diff --git a/CMakeLists.txt b/CMakeLists.txt index 913c2bb..7973383 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,7 @@ set(APP_SOURCES source/core/system/director.cpp source/core/audio/audio.cpp source/core/audio/audio_adapter.cpp + source/core/audio/jail_audio.cpp source/core/locale/locale.cpp source/core/input/key_config.cpp source/core/input/global_inputs.cpp diff --git a/source/core/audio/audio.cpp b/source/core/audio/audio.cpp index c393dc6..06f4192 100644 --- a/source/core/audio/audio.cpp +++ b/source/core/audio/audio.cpp @@ -5,21 +5,6 @@ #include // Per a clamp #include // 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 diff --git a/source/core/audio/jail_audio.cpp b/source/core/audio/jail_audio.cpp new file mode 100644 index 0000000..83f8619 --- /dev/null +++ b/source/core/audio/jail_audio.cpp @@ -0,0 +1,550 @@ +#include "core/audio/jail_audio.hpp" + +#include +#include + +// 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(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(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(MUSIC_LOW_WATER_SECONDS * static_cast(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((static_cast(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(fade.duration_ms)) { return 1.0F; } + return static_cast(ELAPSED) / static_cast(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(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 + 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` + // 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->ogg_data.assign(buffer, buffer + length); + + int vorbis_error = 0; + music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(), + static_cast(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(INFO.channels); + music->spec.freq = static_cast(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(); + 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 diff --git a/source/core/audio/jail_audio.hpp b/source/core/audio/jail_audio.hpp index 94b84cb..dda817f 100644 --- a/source/core/audio/jail_audio.hpp +++ b/source/core/audio/jail_audio.hpp @@ -3,19 +3,17 @@ // --- Includes --- #include -#include #include -#include -#include #include #include #include -// 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` — 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(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(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(MUSIC_LOW_WATER_SECONDS * static_cast(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((static_cast(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(fade.duration_ms)) { return 1.0F; } - return static_cast(ELAPSED) / static_cast(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(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` - // 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->ogg_data.assign(buffer, buffer + length); - - int vorbis_error = 0; - music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(), - static_cast(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(INFO.channels); - music->spec.freq = static_cast(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(); - 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 - 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