#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