From 4f844fe17e0843eb9f7b6d9b7d05b19ad9d70c67 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 4 May 2026 13:10:43 +0200 Subject: [PATCH] =?UTF-8?q?Migra=20a=20jail=5Faudio=20nou=20amb=20streamin?= =?UTF-8?q?g=20i=20prec=C3=A0rrega?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 3 +- Makefile | 3 +- src/audio/jail_audio.cpp | 578 +++++++++++++++++++++ src/audio/jail_audio.hpp | 229 ++++++++ src/main.cpp | 90 ++-- third_party/jail_audio.cpp | 477 ----------------- third_party/jail_audio.h | 43 -- third_party/{stb_vorbis.h => stb_vorbis.c} | 0 third_party/stb_vorbis_impl.cpp | 7 + 9 files changed, 865 insertions(+), 565 deletions(-) create mode 100644 src/audio/jail_audio.cpp create mode 100644 src/audio/jail_audio.hpp delete mode 100644 third_party/jail_audio.cpp delete mode 100644 third_party/jail_audio.h rename third_party/{stb_vorbis.h => stb_vorbis.c} (100%) create mode 100644 third_party/stb_vorbis_impl.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ec98df8..da57aed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,12 +20,13 @@ set(APP_SOURCES src/rendering/shader_backend.cpp src/rendering/opengl_shader_backend.cpp src/rendering/sdl3gpu/sdl3gpu_shader_backend.cpp + src/audio/jail_audio.cpp ) # Fuentes de librerías de terceros set(EXTERNAL_SOURCES third_party/glad/src/glad.c - third_party/jail_audio.cpp + third_party/stb_vorbis_impl.cpp ) # Configuración de SDL3 diff --git a/Makefile b/Makefile index 9a3afa6..29778d9 100644 --- a/Makefile +++ b/Makefile @@ -39,8 +39,9 @@ APP_SOURCES := \ src/rendering/shader_backend.cpp \ src/rendering/opengl_shader_backend.cpp \ src/rendering/sdl3gpu/sdl3gpu_shader_backend.cpp \ + src/audio/jail_audio.cpp \ third_party/glad/src/glad.c \ - third_party/jail_audio.cpp + third_party/stb_vorbis_impl.cpp # Includes INCLUDES := -Isrc -Ithird_party/glad/include -Ithird_party diff --git a/src/audio/jail_audio.cpp b/src/audio/jail_audio.cpp new file mode 100644 index 0000000..c70a98e --- /dev/null +++ b/src/audio/jail_audio.cpp @@ -0,0 +1,578 @@ +#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::fill(std::begin(sound_volume_), std::end(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 diff --git a/src/audio/jail_audio.hpp b/src/audio/jail_audio.hpp new file mode 100644 index 0000000..51d026d --- /dev/null +++ b/src/audio/jail_audio.hpp @@ -0,0 +1,229 @@ +#pragma once + +// --- Includes --- +#include + +#include +#include +#include +#include +#include + +// 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 +// gràcies a EBO, igual que un unique_ptr amb default_delete. +struct SdlFreeDeleter { + void operator()(Uint8* p) const noexcept { + if (p != nullptr) { SDL_free(p); } + } +}; + +// Motor de baix nivell d'àudio del projecte jailgames: streaming OGG +// (stb_vorbis) + N canals d'efectes (SDL3 audio). No depèn d'Options ni de cap +// singleton del joc; només de SDL3 i stb_vorbis. La capa superior (Audio) li +// passa recursos pel punter i fa el bookkeeping d'usuari. +namespace Ja { + + // --- Public Enums --- + enum class ChannelState : std::uint8_t { + FREE, + PLAYING, + PAUSED, + }; + + enum class MusicState : std::uint8_t { + INVALID, // Music carregat però mai play-ejat + PLAYING, + PAUSED, + STOPPED, + }; + + // --- Constants --- + inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20; + inline constexpr int MAX_GROUPS = 2; + // Cap superior de canals que poden estar simultàniament reproduint un so + // amb efecte (eco/reverb). Si està al límit, les noves crides amb efecte + // cauen al camí sec — l'usuari sent el so igualment, sense la cua. + inline constexpr int MAX_EFFECT_CHANNELS = 4; + + // --- Paràmetres d'efectes --- + // Els camps els fixa el caller (Audio) llegint sounds.yaml; el motor només + // els passa a AudioEffects::applyEcho/applyReverb. Els defaults són + // sensats però els presets els sobreescriuen. + struct EchoParams { + float delay_ms{220.0F}; // Temps fins al primer rebot. + float feedback{0.45F}; // Reinjecció (0..0.95). + float wet{0.35F}; // Mescla humida (0..1). + }; + + struct ReverbParams { + float room_size{0.7F}; // Mida percebuda (0..1). + float damping{0.5F}; // Atenuació d'aguts per rebot (0..1). + float wet{0.4F}; // Mescla humida (0..1). + }; + + // Spec de fallback del dispositiu. S'aplica abans que l'Engine s'iniciï i + // com a valor inicial de Sound/Music. L'spec real d'ús l'imposa el ctor + // d'Engine, alimentat des de Defaults::Audio via Audio. + inline constexpr SDL_AudioSpec DEFAULT_SPEC{SDL_AUDIO_S16, 2, 48000}; + + // --- Struct Definitions --- + struct Sound { + SDL_AudioSpec spec{DEFAULT_SPEC}; + Uint32 length{0}; + // Buffer descomprimit (PCM) propietat del sound. Reservat per SDL_LoadWAV + // via SDL_malloc; el deleter `SdlFreeDeleter` allibera amb SDL_free. + std::unique_ptr buffer; + }; + + // L'ordre (punters primer, ints després, enum de 8 bits al final) minimitza + // el padding a 64-bit (evita avisos de clang-analyzer-optin.performance.Padding). + struct Channel { + Sound* sound{nullptr}; + SDL_AudioStream* stream{nullptr}; + int pos{0}; + int times{0}; + int group{0}; + ChannelState state{ChannelState::FREE}; + // Marca si aquest canal va arrencar amb so processat per un efecte. + // El motor compta canals actius amb efecte per fer complir + // MAX_EFFECT_CHANNELS i alliberar el comptador en parar. + bool has_effect{false}; + }; + + struct Music { + SDL_AudioSpec spec{DEFAULT_SPEC}; + + // OGG comprimit en memòria. Propietat nostra; es copia des del buffer + // d'entrada una sola vegada en loadMusic i es descomprimix en chunks + // per streaming. Com que stb_vorbis guarda un punter persistent al + // `.data()` d'aquest vector, no el podem resize'jar un cop establert + // (una reallocation invalidaria el punter que el decoder conserva). + std::vector ogg_data; + stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del Music + + std::string filename; + + int times{0}; // loops restants (-1 = infinit, 0 = un sol play) + SDL_AudioStream* stream{nullptr}; + MusicState state{MusicState::INVALID}; + }; + + struct FadeState { + bool active{false}; + Uint64 start_time{0}; + int duration_ms{0}; + float initial_volume{0.0F}; + }; + + struct OutgoingMusic { + SDL_AudioStream* stream{nullptr}; + FadeState fade; + }; + + // --- Engine --- + // Encapsula tot l'estat que abans vivia com a globals inline. Un sol Engine + // viu per procés (enforceat via assert al ctor contra `active_`). El ctor + // obre el device SDL; el dtor el tanca (RAII). Els deleters + // `Ja::deleteMusic`/`Ja::deleteSound` accedeixen al motor actiu via + // `Engine::active()` per parar canals abans d'alliberar. + class Engine { + public: + Engine(int freq, SDL_AudioFormat format, int num_channels); + ~Engine(); + Engine(const Engine&) = delete; + auto operator=(const Engine&) -> Engine& = delete; + Engine(Engine&&) = delete; + auto operator=(Engine&&) -> Engine& = delete; + + // Retorna el motor actiu o nullptr si cap ha estat construït. L'usen + // els deleters de recursos perquè no els arriba cap referència directa. + [[nodiscard]] static auto active() noexcept -> Engine*; + + void update(); + + // --- Música --- + void playMusic(Music* music, int loop = -1); + void pauseMusic(); + void resumeMusic(); + void stopMusic(); + void fadeOutMusic(int milliseconds); + void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1); + [[nodiscard]] auto getMusicState() const -> MusicState; + auto setMusicVolume(float volume) -> float; + // Registra un callback que es disparà quan la música actual acabi de + // drenar naturalment (times == 0 + stream buit). Es crida DESPRÉS de + // stopMusic, així que el callback pot invocar playMusic sense córrer. + // S'executa al mateix thread que Engine::update (render loop); no fer + // operacions blocants. + void setOnMusicEnded(std::function callback); + // Notifica al motor que un Music s'està destruint: si és el current_music + // s'atura abans que els seus recursos (stream/vorbis) deixin de ser vàlids. + void onMusicDeleted(const Music* music); + + // --- So --- + auto playSound(Sound* sound, int loop = 0, int group = 0) -> int; + auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int; + // Reproducció amb so processat per un efecte. Retorna el canal + // assignat o -1 si no queden slots d'efecte (MAX_EFFECT_CHANNELS). + // El sound original només s'usa per consultar el spec/buffer; el + // canal manipula el buffer ja processat (no reapunta a `sound`). + auto playSoundWithEcho(const Sound* sound, const EchoParams& params, int group = 0) -> int; + auto playSoundWithReverb(const Sound* sound, const ReverbParams& params, int group = 0) -> int; + void pauseChannel(int channel); + void resumeChannel(int channel); + void stopChannel(int channel); + auto setSoundVolume(float volume, int group = -1) -> float; + // Notifica al motor que un Sound s'està destruint: els canals que el + // referenciïn es paren abans d'alliberar el buffer. + void onSoundDeleted(const Sound* sound); + + private: + void stealCurrentIntoOutgoing(int duration_ms); + void updateOutgoingFade(); + void updateIncomingFade(); + void updateCurrentMusic(); + void updateSoundChannels(); + // Empenta un buffer ja processat (S16) a un canal lliure i el deixa + // sonar sense bucle. Camí comú dels dos overloads playSoundWith*. + // Retorna el canal o -1 si no queden slots. + auto playProcessedOnFreeChannel(const std::vector& bytes, const SDL_AudioSpec& spec, int group) -> int; + + template + void forEachTargetChannel(int channel, Fn&& fn); + + Music* current_music_{nullptr}; + Channel channels_[MAX_SIMULTANEOUS_CHANNELS]{}; + SDL_AudioSpec audio_spec_{DEFAULT_SPEC}; + float music_volume_{1.0F}; + float sound_volume_[MAX_GROUPS]{}; + SDL_AudioDeviceID sdl_audio_device_{0}; + OutgoingMusic outgoing_music_; + FadeState incoming_fade_; + std::function on_music_ended_; + // Comptador derivat de Channel::has_effect — evita haver-lo de + // recalcular cada vegada que algú demana un play amb efecte. + int effect_channels_active_{0}; + + // NOLINTNEXTLINE(readability-identifier-naming) — convenció projecte: private static amb sufix _ + static Engine* active_; + }; + + // --- Factories i destructors (permanents) --- + // No depenen de l'estat del motor: loadMusic/loadSound només construeixen + // objectes, deleteMusic/deleteSound consulten Engine::active() per parar + // canals abans d'alliberar (si el motor encara viu). + [[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length) -> Music*; + [[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music*; + void deleteMusic(Music* music); + [[nodiscard]] auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound*; + void deleteSound(Sound* sound); + +} // namespace Ja diff --git a/src/main.cpp b/src/main.cpp index 659a659..9be6f51 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,8 +10,8 @@ #include +#include "audio/jail_audio.hpp" #include "defines.hpp" -#include "jail_audio.h" #include "rendering/shader_backend.hpp" struct Logger { @@ -51,9 +51,11 @@ static Uint32 fps_frame_count_ = 0; static Uint32 fps_last_update_ticks_ = 0; static float current_fps_ = 0.0f; -static std::vector music_list_; +static std::unique_ptr audio_engine_; +static std::vector music_list_; +static std::vector music_names_; static size_t current_music_index_ = 0; -static JA_Music_t* current_music_ = nullptr; +static bool music_muted_ = false; static std::vector scanShaderDirectory(const std::filesystem::path& directory) { std::vector shaders; @@ -89,48 +91,48 @@ static std::vector scanShaderDirectory(const std::filesystem::path& return shaders; } -static std::vector scanMusicDirectory(const std::filesystem::path& directory) { - std::vector music_files; - +static void preloadMusicDirectory(const std::filesystem::path& directory) { if (!std::filesystem::exists(directory) || !std::filesystem::is_directory(directory)) { Logger::info("Music directory does not exist: " + directory.string()); - return music_files; + return; } + std::vector ogg_paths; for (const auto& entry : std::filesystem::directory_iterator(directory)) { - if (entry.is_regular_file()) { - const auto ext = entry.path().extension().string(); - if (ext == ".ogg") { - music_files.push_back(entry.path()); - } + if (entry.is_regular_file() && entry.path().extension() == ".ogg") { + ogg_paths.push_back(entry.path()); } } + std::sort(ogg_paths.begin(), ogg_paths.end()); - std::sort(music_files.begin(), music_files.end()); + for (const auto& path : ogg_paths) { + std::size_t size = 0; + void* raw = SDL_LoadFile(path.string().c_str(), &size); + if (raw == nullptr || size == 0) { + Logger::error("Failed to read music file: " + path.string()); + if (raw != nullptr) { SDL_free(raw); } + continue; + } + Ja::Music* music = Ja::loadMusic(static_cast(raw), static_cast(size), + path.filename().string().c_str()); + SDL_free(raw); + if (music == nullptr) { + Logger::error("Failed to decode OGG: " + path.string()); + continue; + } + music_list_.push_back(music); + music_names_.push_back(path.filename().string()); + } - Logger::info("Found " + std::to_string(music_files.size()) + " music file(s) in " + directory.string()); - return music_files; + Logger::info("Preloaded " + std::to_string(music_list_.size()) + " music file(s) from " + directory.string()); } static void playRandomMusic() { - if (music_list_.empty()) { return; } - - if (current_music_ != nullptr) { - JA_DeleteMusic(current_music_); - current_music_ = nullptr; - } + if (music_list_.empty() || !audio_engine_) { return; } current_music_index_ = static_cast(rand()) % music_list_.size(); - - const auto& music_path = music_list_[current_music_index_]; - current_music_ = JA_LoadMusic(music_path.string().c_str()); - - if (current_music_ != nullptr) { - JA_PlayMusic(current_music_, 0); - Logger::info("Now playing: " + music_path.filename().string()); - } else { - Logger::error("Failed to load music: " + music_path.string()); - } + audio_engine_->playMusic(music_list_[current_music_index_], 0); + Logger::info("Now playing: " + music_names_[current_music_index_]); } static void updateWindowTitle() { @@ -288,6 +290,12 @@ void handleDebugEvents(const SDL_Event& event) { switch (event.key.key) { case SDLK_F3: { toggleFullscreen(); break; } case SDLK_F4: { toggleVSync(); break; } + case SDLK_M: { + music_muted_ = !music_muted_; + if (audio_engine_) { audio_engine_->setMusicVolume(music_muted_ ? 0.0f : 1.0f); } + Logger::info(music_muted_ ? "Music muted" : "Music unmuted"); + break; + } case SDLK_LEFT: { switchShader(-1); break; } case SDLK_RIGHT: { switchShader(+1); break; } default: break; @@ -368,14 +376,15 @@ int main(int argc, char** argv) { setFullscreenMode(); backend_->setVSync(Options_video.vsync); - JA_Init(48000, SDL_AUDIO_S16LE, 2); + audio_engine_ = std::make_unique(48000, SDL_AUDIO_S16, 2); + audio_engine_->setOnMusicEnded([]() { playRandomMusic(); }); const std::string resources_dir = getResourcesDirectory(); srand(static_cast(time(nullptr))); const std::filesystem::path music_directory = std::filesystem::path(resources_dir) / "data" / "music"; - music_list_ = scanMusicDirectory(music_directory); + preloadMusicDirectory(music_directory); if (!music_list_.empty()) { playRandomMusic(); @@ -456,11 +465,7 @@ int main(int argc, char** argv) { updateWindowTitle(); } - JA_Update(); - - if (!music_list_.empty() && JA_GetMusicState() == JA_MUSIC_STOPPED) { - playRandomMusic(); - } + if (audio_engine_) { audio_engine_->update(); } SDL_Event e; while (SDL_PollEvent(&e)) { @@ -493,11 +498,10 @@ int main(int argc, char** argv) { backend_->cleanup(); backend_.reset(); - if (current_music_ != nullptr) { - JA_DeleteMusic(current_music_); - current_music_ = nullptr; - } - JA_Quit(); + for (Ja::Music* m : music_list_) { Ja::deleteMusic(m); } + music_list_.clear(); + music_names_.clear(); + audio_engine_.reset(); SDL_DestroyWindow(window_); SDL_Quit(); diff --git a/third_party/jail_audio.cpp b/third_party/jail_audio.cpp deleted file mode 100644 index e8c2c3d..0000000 --- a/third_party/jail_audio.cpp +++ /dev/null @@ -1,477 +0,0 @@ -#ifndef JA_USESDLMIXER -#include "jail_audio.h" - -#include // Para SDL_AudioFormat, SDL_BindAudioStream, SDL_SetAudioStreamGain, SDL_PutAudioStreamData, SDL_DestroyAudioStream, SDL_GetAudioStreamAvailable, Uint8, SDL_CreateAudioStream, SDL_UnbindAudioStream, Uint32, SDL_CloseAudioDevice, SDL_GetTicks, SDL_Log, SDL_free, SDL_AudioSpec, SDL_AudioStream, SDL_IOFromMem, SDL_LoadWAV, SDL_LoadWAV_IO, SDL_OpenAudioDevice, SDL_clamp, SDL_malloc, SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, SDL_AudioDeviceID, SDL_memcpy -#include // Para uint32_t, uint8_t -#include // Para NULL, fseek, printf, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET -#include // Para free, malloc -#include // Para strcpy, strlen - -#include "stb_vorbis.h" // Para stb_vorbis_decode_memory - -#define JA_MAX_SIMULTANEOUS_CHANNELS 20 -#define JA_MAX_GROUPS 2 - -struct JA_Sound_t -{ - SDL_AudioSpec spec { SDL_AUDIO_S16, 2, 48000 }; - Uint32 length { 0 }; - Uint8 *buffer { NULL }; -}; - -struct JA_Channel_t -{ - JA_Sound_t *sound { nullptr }; - int pos { 0 }; - int times { 0 }; - int group { 0 }; - SDL_AudioStream *stream { nullptr }; - JA_Channel_state state { JA_CHANNEL_FREE }; -}; - -struct JA_Music_t -{ - SDL_AudioSpec spec { SDL_AUDIO_S16, 2, 48000 }; - Uint32 length { 0 }; - Uint8 *buffer { nullptr }; - char *filename { nullptr }; - - int pos { 0 }; - int times { 0 }; - SDL_AudioStream *stream { nullptr }; - JA_Music_state state { JA_MUSIC_INVALID }; -}; - -JA_Music_t *current_music { nullptr }; -JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS]; - -SDL_AudioSpec JA_audioSpec { SDL_AUDIO_S16, 2, 48000 }; -float JA_musicVolume { 1.0f }; -float JA_soundVolume[JA_MAX_GROUPS]; -bool JA_musicEnabled { true }; -bool JA_soundEnabled { true }; -SDL_AudioDeviceID sdlAudioDevice { 0 }; -//SDL_TimerID JA_timerID { 0 }; - -bool fading = false; -int fade_start_time; -int fade_duration; -int fade_initial_volume; - - -void JA_Update() -{ - if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) - { - if (fading) { - int time = SDL_GetTicks(); - if (time > (fade_start_time+fade_duration)) { - fading = false; - JA_StopMusic(); - return; - } else { - const int time_passed = time - fade_start_time; - const float percent = (float)time_passed / (float)fade_duration; - SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume*(1.0 - percent)); - } - } - - if (current_music->times != 0) - { - if ((Uint32)SDL_GetAudioStreamAvailable(current_music->stream) < (current_music->length/2)) { - SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length); - } - if (current_music->times>0) current_music->times--; - } - else - { - if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic(); - } - } - - if (JA_soundEnabled) - { - for (int i=0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) - if (channels[i].state == JA_CHANNEL_PLAYING) - { - if (channels[i].times != 0) - { - if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length/2)) { - SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length); - if (channels[i].times>0) channels[i].times--; - } - } - else - { - if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i); - } - } - - } - - return; -} - -void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) -{ - #ifdef DEBUG - SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG); - #endif - - JA_audioSpec = {format, num_channels, freq }; - if (!sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); - sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec); - if (sdlAudioDevice==0) SDL_Log("Failed to initialize SDL audio!"); - for (int i=0; ilength = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2; - - music->spec.channels = chan; - music->spec.freq = samplerate; - music->spec.format = SDL_AUDIO_S16; - music->buffer = (Uint8*)SDL_malloc(music->length); - SDL_memcpy(music->buffer, output, music->length); - free(output); - music->pos = 0; - music->state = JA_MUSIC_STOPPED; - - return music; -} - -JA_Music_t *JA_LoadMusic(const char* filename) -{ - // [RZC 28/08/22] Carreguem primer el arxiu en memòria i després el descomprimim. Es algo més rapid. - FILE *f = fopen(filename, "rb"); - fseek(f, 0, SEEK_END); - long fsize = ftell(f); - fseek(f, 0, SEEK_SET); - Uint8 *buffer = (Uint8*)malloc(fsize + 1); - if (fread(buffer, fsize, 1, f)!=1) return NULL; - fclose(f); - - JA_Music_t *music = JA_LoadMusic(buffer, fsize); - music->filename = (char*)malloc(strlen(filename)+1); - strcpy(music->filename, filename); - - free(buffer); - - return music; -} - -void JA_PlayMusic(JA_Music_t *music, const int loop) -{ - if (!JA_musicEnabled) return; - - JA_StopMusic(); - - current_music = music; - current_music->pos = 0; - current_music->state = JA_MUSIC_PLAYING; - current_music->times = loop; - - current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec); - if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n"); - SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume); - if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n"); - //SDL_ResumeAudioStreamDevice(current_music->stream); -} - -char *JA_GetMusicFilename(JA_Music_t *music) -{ - if (!music) music = current_music; - return music->filename; -} - -void JA_PauseMusic() -{ - if (!JA_musicEnabled) return; - if (!current_music || current_music->state == JA_MUSIC_INVALID) return; - - current_music->state = JA_MUSIC_PAUSED; - //SDL_PauseAudioStreamDevice(current_music->stream); - SDL_UnbindAudioStream(current_music->stream); -} - -void JA_ResumeMusic() -{ - if (!JA_musicEnabled) return; - if (!current_music || current_music->state == JA_MUSIC_INVALID) return; - - current_music->state = JA_MUSIC_PLAYING; - //SDL_ResumeAudioStreamDevice(current_music->stream); - SDL_BindAudioStream(sdlAudioDevice, current_music->stream); -} - -void JA_StopMusic() -{ - if (!JA_musicEnabled) return; - if (!current_music || current_music->state == JA_MUSIC_INVALID) return; - - current_music->pos = 0; - current_music->state = JA_MUSIC_STOPPED; - //SDL_PauseAudioStreamDevice(current_music->stream); - SDL_DestroyAudioStream(current_music->stream); - current_music->stream = nullptr; - free(current_music->filename); - current_music->filename = nullptr; -} - -void JA_FadeOutMusic(const int milliseconds) -{ - if (!JA_musicEnabled) return; - if (current_music == NULL || current_music->state == JA_MUSIC_INVALID) return; - - fading = true; - fade_start_time = SDL_GetTicks(); - fade_duration = milliseconds; - fade_initial_volume = JA_musicVolume; -} - -JA_Music_state JA_GetMusicState() -{ - if (!JA_musicEnabled) return JA_MUSIC_DISABLED; - if (!current_music) return JA_MUSIC_INVALID; - - return current_music->state; -} - -void JA_DeleteMusic(JA_Music_t *music) -{ - if (current_music == music) current_music = nullptr; - SDL_free(music->buffer); - if (music->stream) SDL_DestroyAudioStream(music->stream); - delete music; -} - -float JA_SetMusicVolume(float volume) -{ - JA_musicVolume = SDL_clamp( volume, 0.0f, 1.0f ); - if (current_music) SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume); - return JA_musicVolume; -} - -void JA_SetMusicPosition(float value) -{ - if (!current_music) return; - current_music->pos = value * current_music->spec.freq; -} - -float JA_GetMusicPosition() -{ - if (!current_music) return 0; - return float(current_music->pos)/float(current_music->spec.freq); -} - -void JA_EnableMusic(const bool value) -{ - if ( !value && current_music && (current_music->state==JA_MUSIC_PLAYING) ) JA_StopMusic(); - - JA_musicEnabled = value; -} - - - - - -JA_Sound_t *JA_NewSound(Uint8* buffer, Uint32 length) -{ - JA_Sound_t *sound = new JA_Sound_t(); - sound->buffer = buffer; - sound->length = length; - return sound; -} - -JA_Sound_t *JA_LoadSound(uint8_t* buffer, uint32_t size) -{ - JA_Sound_t *sound = new JA_Sound_t(); - SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size),1, &sound->spec, &sound->buffer, &sound->length); - - return sound; -} - -JA_Sound_t *JA_LoadSound(const char* filename) -{ - JA_Sound_t *sound = new JA_Sound_t(); - SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length); - - return sound; -} - -int JA_PlaySound(JA_Sound_t *sound, const int loop, const int group) -{ - if (!JA_soundEnabled) return -1; - - int channel = 0; - while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; } - if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) channel = 0; - JA_StopChannel(channel); - - channels[channel].sound = sound; - channels[channel].times = loop; - channels[channel].pos = 0; - channels[channel].state = JA_CHANNEL_PLAYING; - channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec); - SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length); - SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]); - SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream); - - return channel; -} - -int JA_PlaySoundOnChannel(JA_Sound_t *sound, const int channel, const int loop, const int group) -{ - if (!JA_soundEnabled) return -1; - - if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1; - JA_StopChannel(channel); - - channels[channel].sound = sound; - channels[channel].times = loop; - channels[channel].pos = 0; - channels[channel].state = JA_CHANNEL_PLAYING; - channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec); - SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length); - SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]); - SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream); - - return channel; -} - -void JA_DeleteSound(JA_Sound_t *sound) -{ - for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) { - if (channels[i].sound == sound) JA_StopChannel(i); - } - SDL_free(sound->buffer); - delete sound; -} - -void JA_PauseChannel(const int channel) -{ - if (!JA_soundEnabled) return; - - if (channel == -1) - { - for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) - if (channels[i].state == JA_CHANNEL_PLAYING) - { - channels[i].state = JA_CHANNEL_PAUSED; - //SDL_PauseAudioStreamDevice(channels[i].stream); - SDL_UnbindAudioStream(channels[i].stream); - } - } - else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) - { - if (channels[channel].state == JA_CHANNEL_PLAYING) - { - channels[channel].state = JA_CHANNEL_PAUSED; - //SDL_PauseAudioStreamDevice(channels[channel].stream); - SDL_UnbindAudioStream(channels[channel].stream); - } - } -} - -void JA_ResumeChannel(const int channel) -{ - if (!JA_soundEnabled) return; - - if (channel == -1) - { - for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) - if (channels[i].state == JA_CHANNEL_PAUSED) - { - channels[i].state = JA_CHANNEL_PLAYING; - //SDL_ResumeAudioStreamDevice(channels[i].stream); - SDL_BindAudioStream(sdlAudioDevice, channels[i].stream); - } - } - else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) - { - if (channels[channel].state == JA_CHANNEL_PAUSED) - { - channels[channel].state = JA_CHANNEL_PLAYING; - //SDL_ResumeAudioStreamDevice(channels[channel].stream); - SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream); - } - } -} - -void JA_StopChannel(const int channel) -{ - if (!JA_soundEnabled) return; - - if (channel == -1) - { - for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) { - if (channels[i].state != JA_CHANNEL_FREE) SDL_DestroyAudioStream(channels[i].stream); - channels[i].stream = nullptr; - channels[i].state = JA_CHANNEL_FREE; - channels[i].pos = 0; - channels[i].sound = NULL; - } - } - else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) - { - if (channels[channel].state != JA_CHANNEL_FREE) SDL_DestroyAudioStream(channels[channel].stream); - channels[channel].stream = nullptr; - channels[channel].state = JA_CHANNEL_FREE; - channels[channel].pos = 0; - channels[channel].sound = NULL; - } -} - -JA_Channel_state JA_GetChannelState(const int channel) -{ - if (!JA_soundEnabled) return JA_SOUND_DISABLED; - - if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID; - - return channels[channel].state; -} - -float JA_SetSoundVolume(float volume, const int group) -{ - const float v = SDL_clamp( volume, 0.0f, 1.0f ); - for (int i = 0; i < JA_MAX_GROUPS; ++i) { - if (group==-1 || group==i) JA_soundVolume[i]=v; - } - - for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) - if ( ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) && - ((group==-1) || (channels[i].group==group)) ) - SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume[i]); - - return v; -} - -void JA_EnableSound(const bool value) -{ - for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) - { - if (channels[i].state == JA_CHANNEL_PLAYING) JA_StopChannel(i); - } - JA_soundEnabled = value; -} - -float JA_SetVolume(float volume) -{ - JA_SetSoundVolume(JA_SetMusicVolume(volume) / 2.0f); - - return JA_musicVolume; -} - -#endif \ No newline at end of file diff --git a/third_party/jail_audio.h b/third_party/jail_audio.h deleted file mode 100644 index 716b7f9..0000000 --- a/third_party/jail_audio.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once -#include - -enum JA_Channel_state { JA_CHANNEL_INVALID, JA_CHANNEL_FREE, JA_CHANNEL_PLAYING, JA_CHANNEL_PAUSED, JA_SOUND_DISABLED }; -enum JA_Music_state { JA_MUSIC_INVALID, JA_MUSIC_PLAYING, JA_MUSIC_PAUSED, JA_MUSIC_STOPPED, JA_MUSIC_DISABLED }; - -struct JA_Sound_t; -struct JA_Music_t; - -void JA_Update(); - -void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels); -void JA_Quit(); - -JA_Music_t *JA_LoadMusic(const char* filename); -JA_Music_t *JA_LoadMusic(Uint8* buffer, Uint32 length); -void JA_PlayMusic(JA_Music_t *music, const int loop = -1); -char *JA_GetMusicFilename(JA_Music_t *music = nullptr); -void JA_PauseMusic(); -void JA_ResumeMusic(); -void JA_StopMusic(); -void JA_FadeOutMusic(const int milliseconds); -JA_Music_state JA_GetMusicState(); -void JA_DeleteMusic(JA_Music_t *music); -float JA_SetMusicVolume(float volume); -void JA_SetMusicPosition(float value); -float JA_GetMusicPosition(); -void JA_EnableMusic(const bool value); - -JA_Sound_t *JA_NewSound(Uint8* buffer, Uint32 length); -JA_Sound_t *JA_LoadSound(Uint8* buffer, Uint32 length); -JA_Sound_t *JA_LoadSound(const char* filename); -int JA_PlaySound(JA_Sound_t *sound, const int loop = 0, const int group=0); -int JA_PlaySoundOnChannel(JA_Sound_t *sound, const int channel, const int loop = 0, const int group=0); -void JA_PauseChannel(const int channel); -void JA_ResumeChannel(const int channel); -void JA_StopChannel(const int channel); -JA_Channel_state JA_GetChannelState(const int channel); -void JA_DeleteSound(JA_Sound_t *sound); -float JA_SetSoundVolume(float volume, const int group=0); -void JA_EnableSound(const bool value); - -float JA_SetVolume(float volume); diff --git a/third_party/stb_vorbis.h b/third_party/stb_vorbis.c similarity index 100% rename from third_party/stb_vorbis.h rename to third_party/stb_vorbis.c diff --git a/third_party/stb_vorbis_impl.cpp b/third_party/stb_vorbis_impl.cpp new file mode 100644 index 0000000..cc54d42 --- /dev/null +++ b/third_party/stb_vorbis_impl.cpp @@ -0,0 +1,7 @@ +// Isolated TU for the stb_vorbis implementation. +// jail_audio.cpp defines STB_VORBIS_HEADER_ONLY before including stb_vorbis.c +// (it only sees declarations there); this TU provides the definitions and +// the linker resolves them. + +// NOLINTNEXTLINE(bugprone-suspicious-include) +#include "stb_vorbis.c"