#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