#include "audio/jail_audio.hpp" #include #include #include #include #include #include // Només declaracions de stb_vorbis: STB_VORBIS_HEADER_ONLY omet el bloc // d'implementació. Les definicions les aporta source/external/stb_vorbis_impl.cpp // (TU aïllat perquè clang-analyzer no dispari fals positius al nostre codi). #define STB_VORBIS_HEADER_ONLY // clang-format off // NOLINTNEXTLINE(bugprone-suspicious-include) #include "stb_vorbis.c" // clang-format on namespace Ja { // --- Streaming internals (file-scope constants) --- namespace { // 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; } // namespace // --- Engine::active_ storage --- Engine* Engine::active_ = nullptr; auto Engine::active() noexcept -> Engine* { return active_; } // --- Ctor/Dtor --- Engine::Engine(const int freq, const SDL_AudioFormat format, const int num_channels) { assert(active_ == nullptr && "Ja::Engine: més d'una instància activa no està suportat"); active_ = this; audio_spec_ = {.format = format, .channels = num_channels, .freq = freq}; sdl_audio_device_ = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec_); if (sdl_audio_device_ == 0) { std::fprintf(stderr, "Ja::Engine: Failed to initialize SDL audio!\n"); } for (auto& channel : channels_) { channel.state = ChannelState::FREE; } } Engine::~Engine() { if (outgoing_music_.stream != nullptr) { SDL_DestroyAudioStream(outgoing_music_.stream); outgoing_music_.stream = nullptr; } if (sdl_audio_device_ != 0) { SDL_CloseAudioDevice(sdl_audio_device_); } sdl_audio_device_ = 0; if (active_ == this) { active_ = nullptr; } } // --- Helpers stateless (no toquen membres d'Engine) --- namespace { auto feedMusicChunk(Music* music) -> int { 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; } 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; } } } void preFillOutgoing(Music* music, const 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; } } } // 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); } } // namespace void Engine::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)); } } void Engine::updateIncomingFade() { if (!incoming_fade_.active) { return; } const float PROGRESS = fadeProgress(incoming_fade_); if (PROGRESS >= 1.0F) { incoming_fade_.active = false; SDL_SetAudioStreamGain(current_music_->stream, music_volume_); } else { SDL_SetAudioStreamGain(current_music_->stream, music_volume_ * PROGRESS); } } void Engine::updateCurrentMusic() { if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; } updateIncomingFade(); pumpMusic(current_music_); if (current_music_->times == 0 && SDL_GetAudioStreamAvailable(current_music_->stream) == 0) { // La pista ha acabat de drenar naturalment. L'aturem primer (deixa // l'engine en estat consistent) i llavors invoquem el callback; // així un eventual playMusic des del callback comença net. stopMusic(); if (on_music_ended_) { on_music_ended_(); } } } void Engine::updateSoundChannels() { 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); } } } void Engine::stealCurrentIntoOutgoing(const int duration_ms) { if (outgoing_music_.stream != nullptr) { SDL_DestroyAudioStream(outgoing_music_.stream); outgoing_music_.stream = nullptr; outgoing_music_.fade.active = false; } if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING || current_music_->stream == nullptr) { return; } preFillOutgoing(current_music_, duration_ms); outgoing_music_.stream = current_music_->stream; outgoing_music_.fade = { .active = true, .start_time = SDL_GetTicks(), .duration_ms = duration_ms, .initial_volume = music_volume_, }; current_music_->stream = nullptr; current_music_->state = MusicState::STOPPED; if (current_music_->vorbis != nullptr) { stb_vorbis_seek_start(current_music_->vorbis); } } template void Engine::forEachTargetChannel(const int channel, Fn&& fn) { if (channel == -1) { for (auto& ch : channels_) { fn(ch); } } else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) { fn(channels_[channel]); } } // --- Engine public API --- void Engine::update() { updateOutgoingFade(); updateCurrentMusic(); updateSoundChannels(); } void Engine::playMusic(Music* music, const int loop) { if (music == nullptr || music->vorbis == nullptr) { return; } stopMusic(); current_music_ = music; current_music_->state = MusicState::PLAYING; current_music_->times = loop; stb_vorbis_seek_start(current_music_->vorbis); current_music_->stream = SDL_CreateAudioStream(¤t_music_->spec, &audio_spec_); if (current_music_->stream == nullptr) { std::fprintf(stderr, "Ja::Engine::playMusic: Failed to create audio stream!\n"); current_music_->state = MusicState::STOPPED; return; } SDL_SetAudioStreamGain(current_music_->stream, music_volume_); pumpMusic(current_music_); if (!SDL_BindAudioStream(sdl_audio_device_, current_music_->stream)) { std::fprintf(stderr, "Ja::Engine::playMusic: SDL_BindAudioStream failed!\n"); } } void Engine::pauseMusic() { if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; } current_music_->state = MusicState::PAUSED; SDL_UnbindAudioStream(current_music_->stream); } void Engine::resumeMusic() { if (current_music_ == nullptr || current_music_->state != MusicState::PAUSED) { return; } current_music_->state = MusicState::PLAYING; SDL_BindAudioStream(sdl_audio_device_, current_music_->stream); } void Engine::stopMusic() { if (outgoing_music_.stream != nullptr) { SDL_DestroyAudioStream(outgoing_music_.stream); outgoing_music_.stream = nullptr; outgoing_music_.fade.active = false; } incoming_fade_.active = false; if (current_music_ == nullptr || current_music_->state == MusicState::INVALID || current_music_->state == MusicState::STOPPED) { return; } current_music_->state = MusicState::STOPPED; if (current_music_->stream != nullptr) { SDL_DestroyAudioStream(current_music_->stream); current_music_->stream = nullptr; } if (current_music_->vorbis != nullptr) { stb_vorbis_seek_start(current_music_->vorbis); } } void Engine::fadeOutMusic(const int milliseconds) { if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; } stealCurrentIntoOutgoing(milliseconds); incoming_fade_.active = false; } void Engine::crossfadeMusic(Music* music, const int crossfade_ms, const int loop) { if (music == nullptr || music->vorbis == nullptr) { return; } stealCurrentIntoOutgoing(crossfade_ms); current_music_ = music; current_music_->state = MusicState::PLAYING; current_music_->times = loop; stb_vorbis_seek_start(current_music_->vorbis); current_music_->stream = SDL_CreateAudioStream(¤t_music_->spec, &audio_spec_); if (current_music_->stream == nullptr) { std::fprintf(stderr, "Ja::Engine::crossfadeMusic: Failed to create audio stream!\n"); current_music_->state = MusicState::STOPPED; return; } SDL_SetAudioStreamGain(current_music_->stream, 0.0F); pumpMusic(current_music_); SDL_BindAudioStream(sdl_audio_device_, current_music_->stream); incoming_fade_ = { .active = true, .start_time = SDL_GetTicks(), .duration_ms = crossfade_ms, .initial_volume = 0.0F, }; } auto Engine::getMusicState() const -> MusicState { if (current_music_ == nullptr) { return MusicState::INVALID; } return current_music_->state; } auto Engine::setMusicVolume(float volume) -> float { music_volume_ = SDL_clamp(volume, 0.0F, 1.0F); if (current_music_ != nullptr && current_music_->stream != nullptr) { SDL_SetAudioStreamGain(current_music_->stream, music_volume_); } return music_volume_; } void Engine::setOnMusicEnded(std::function callback) { on_music_ended_ = std::move(callback); } void Engine::onMusicDeleted(const Music* music) { if (music == nullptr) { return; } if (current_music_ == music) { stopMusic(); current_music_ = nullptr; } } // --- Sound --- auto Engine::playSound(Sound* sound, const int loop, const int group) -> int { if (sound == nullptr) { return -1; } int channel = 0; while (channel < MAX_SIMULTANEOUS_CHANNELS && channels_[channel].state != ChannelState::FREE) { channel++; } if (channel == MAX_SIMULTANEOUS_CHANNELS) { // No hay canal libre, reemplazamos el primero channel = 0; } return playSoundOnChannel(sound, channel, loop, group); } auto Engine::playSoundOnChannel(Sound* sound, const int channel, const int loop, const int group) -> int { if (sound == nullptr) { return -1; } if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return -1; } stopChannel(channel); channels_[channel].sound = sound; channels_[channel].times = loop; channels_[channel].pos = 0; channels_[channel].group = group; channels_[channel].state = ChannelState::PLAYING; channels_[channel].stream = SDL_CreateAudioStream(&channels_[channel].sound->spec, &audio_spec_); if (channels_[channel].stream == nullptr) { std::fprintf(stderr, "Ja::Engine::playSoundOnChannel: Failed to create audio stream!\n"); channels_[channel].state = ChannelState::FREE; return -1; } SDL_PutAudioStreamData(channels_[channel].stream, channels_[channel].sound->buffer.get(), channels_[channel].sound->length); SDL_SetAudioStreamGain(channels_[channel].stream, sound_volume_[group]); SDL_BindAudioStream(sdl_audio_device_, channels_[channel].stream); return channel; } void Engine::pauseChannel(const int channel) { forEachTargetChannel(channel, [](Channel& ch) { if (ch.state == ChannelState::PLAYING) { ch.state = ChannelState::PAUSED; SDL_UnbindAudioStream(ch.stream); } }); } void Engine::resumeChannel(const int channel) { const SDL_AudioDeviceID DEVICE = sdl_audio_device_; forEachTargetChannel(channel, [DEVICE](Channel& ch) { if (ch.state == ChannelState::PAUSED) { ch.state = ChannelState::PLAYING; SDL_BindAudioStream(DEVICE, ch.stream); } }); } void Engine::stopChannel(const int channel) { forEachTargetChannel(channel, [this](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; if (ch.has_effect) { ch.has_effect = false; if (effect_channels_active_ > 0) { --effect_channels_active_; } } } }); } auto Engine::setSoundVolume(float volume, const int group) -> float { const float V = SDL_clamp(volume, 0.0F, 1.0F); if (group == -1) { std::ranges::fill(sound_volume_, V); } else if (group >= 0 && group < MAX_GROUPS) { sound_volume_[group] = V; } else { return V; } for (auto& ch : channels_) { if ((ch.state == ChannelState::PLAYING) || (ch.state == ChannelState::PAUSED)) { if (group == -1 || ch.group == group) { if (ch.stream != nullptr) { SDL_SetAudioStreamGain(ch.stream, sound_volume_[ch.group]); } } } } return V; } void Engine::onSoundDeleted(const Sound* sound) { if (sound == nullptr) { return; } for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) { if (channels_[i].sound == sound) { stopChannel(i); } } } auto Engine::playProcessedOnFreeChannel(const std::vector& bytes, const SDL_AudioSpec& spec, const int group) -> int { // El cap de canals amb efecte es valida abans de reservar slot — // així evitem crear i destruir un stream només per descartar el play. if (effect_channels_active_ >= MAX_EFFECT_CHANNELS) { return -1; } int channel = 0; while (channel < MAX_SIMULTANEOUS_CHANNELS && channels_[channel].state != ChannelState::FREE) { ++channel; } if (channel == MAX_SIMULTANEOUS_CHANNELS) { channel = 0; } stopChannel(channel); // El stream es crea contra l'spec del buffer processat (S16, ...) // perquè SDL faci el resampling cap a audio_spec_ del device. channels_[channel].stream = SDL_CreateAudioStream(&spec, &audio_spec_); if (channels_[channel].stream == nullptr) { std::fprintf(stderr, "Ja::Engine::playProcessedOnFreeChannel: Failed to create audio stream!\n"); return -1; } channels_[channel].sound = nullptr; // El buffer no és propietat de cap Ja::Sound. channels_[channel].times = 0; channels_[channel].pos = 0; const int CLAMPED_GROUP = (group >= 0 && group < MAX_GROUPS) ? group : 0; channels_[channel].group = CLAMPED_GROUP; channels_[channel].state = ChannelState::PLAYING; channels_[channel].has_effect = true; ++effect_channels_active_; SDL_PutAudioStreamData(channels_[channel].stream, bytes.data(), static_cast(bytes.size())); SDL_SetAudioStreamGain(channels_[channel].stream, sound_volume_[CLAMPED_GROUP]); SDL_BindAudioStream(sdl_audio_device_, channels_[channel].stream); return channel; } // shadertoy build: AudioEffects is not bundled. Both echo/reverb entry // points are stubbed; if an effect is requested, fall back to playing // the dry sound so callers don't lose audio. auto Engine::playSoundWithEcho(const Sound* sound, const EchoParams& /*params*/, const int group) -> int { if (sound == nullptr) { return -1; } return playSound(const_cast(sound), 0, group); } auto Engine::playSoundWithReverb(const Sound* sound, const ReverbParams& /*params*/, const int group) -> int { if (sound == nullptr) { return -1; } return playSound(const_cast(sound), 0, group); } // --- Factories i destructors (permanents) --- 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 deleteMusic(Music* music) { if (music == nullptr) { return; } // Notifiquem el motor actiu perquè pari la pista si és la current_music. // Si no hi ha motor (shutdown-order invertit), passem: els recursos // propis del Music es lliberen igualment a sota. if (Engine* eng = Engine::active()) { eng->onMusicDeleted(music); } if (music->stream != nullptr) { SDL_DestroyAudioStream(music->stream); } if (music->vorbis != nullptr) { stb_vorbis_close(music->vorbis); } delete music; } 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(); } void deleteSound(Sound* sound) { if (sound == nullptr) { return; } if (Engine* eng = Engine::active()) { eng->onSoundDeleted(sound); } // buffer es destrueix automàticament via RAII (SdlFreeDeleter). delete sound; } } // 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