#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 // solo necessita `stb_vorbis*` per punter — nunca per valor — así que el // forward decl n'hay prou i evita arrossegar el .c a tots los TU. // NOLINTNEXTLINE(readability-identifier-naming) — nom imposat per l'API de stb_vorbis struct stb_vorbis; // Deleter stateless para buffers reservats con `SDL_malloc` / `SDL_LoadWAV*`. // Compatible con `std::unique_ptr` — zero size overhead // gràcies a EBO, igual que un unique_ptr con 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 sin // singleton del joc; solo 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 pero nunca 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 // con efecte (eco/reverb). Si está al límit, las noves crides con efecte // cauen al camí sec — l'usuari sent el so igualment, sin la cua. inline constexpr int MAX_EFFECT_CHANNELS = 4; // --- Paràmetres d'efectes --- // Els camps los fixa el caller (Audio) llegint sounds.yaml; el motor solo // los passa a AudioEffects::applyEcho/applyReverb. Els defaults són // sensats pero los presets los sobreescriuen. struct EchoParams { float delay_ms{220.0F}; // Temps hasta 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}; // Tamaño 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 antes que l'Engine s'iniciï i // como 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 con SDL_free. std::unique_ptr buffer; }; // L'ordre (punters primer, ints despué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 este canal va arrencar con so processat per un efecte. // El motor compta canals actius con 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. Como que stb_vorbis guarda un punter persistent al // `.data()` d'este 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) // Duración total de la pista en mil·lisegons, mesurada via // `stb_vorbis_stream_length_in_samples / sample_rate` al // `loadMusic`. 0 si el cálculo no es possible (header malmès). // L'usen consumidors que necessiten un timeline pre-calculat — // p. ex. la FSM de sala — sin dependre de callbacks de fi. int duration_ms{0}; 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}; // Referència al Music original porque updateOutgoingFade puga // continuar descomprimint des de Vorbis sin al stream durante // tota la fosa. Sense això, solo tenim el pre-fill puntual i // SDL drena el stream més ràpid del previst cuando hay sounds // bound a la misma device (~2x), buidant-lo a meitat del // fade i sentint-se como un tall sec. Music* music{nullptr}; FadeState fade; }; // --- Engine --- // Encapsula tot l'estat que antes vivia como 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 antes 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 sin ha estat construït. L'usen // los deleters de recursos porque no los arriba sin 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; // Multiplicador de velocitat de reproducció de la música actual // via `SDL_SetAudioStreamFrequencyRatio`. 1.0 = normal, 2.0 = // doble velocitat. Cal saber que también puja el to (efecte // "chipmunk") — es el comportament arcade clàssic dels comptes // enrere. Cada `playMusic` crea un stream nuevo con ratio 1.0, // así que un canvi de track reseteja la velocitat // implícitament. No-op si no hay música activa. void setMusicSpeed(float ratio); // Registra un callback que es disparà cuando la música actual acabi de // drenar naturalment (times == 0 + stream buit). Es crida DESPRÉS de // stopMusic, así que el callback pot invocar playMusic sin córrer. // S'executa al mismo 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 es el current_music // s'atura antes que los 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; // Ajusta la velocitat de reproducció d'un canal actiu via // `SDL_SetAudioStreamFrequencyRatio`. 1.0 = normal. Igual que a // `setMusicSpeed`, puja/baixa el to junt con la velocitat // (efecte "chipmunk"); para SFX curts arcade es el que volem. // No-op si el canal no está actiu. Cridar-lo just después de // `playSound`/`playSoundOnChannel` porque el ratio cobreixi // tota la reproducció. void setChannelSpeed(int channel, float ratio); // Reproducció con so processat per un efecte. Retorna el canal // assignat o -1 si no queden slots d'efecte (MAX_EFFECT_CHANNELS). // El sound original solo s'usa per consultar el spec/buffer; el // canal manipula el buffer ya 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: los canals que el // referenciïn es paren antes 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 ya processat (S16) a un canal lliure y el deixa // sonar sin 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 con efecte. int effect_channels_active_{0}; // NOLINTNEXTLINE(readability-identifier-naming) — convenció projecte: private static con sufix _ static Engine* active_; }; // --- Factories y destructors (permanents) --- // No depenen de l'estat del motor: loadMusic/loadSound solo construeixen // objectes, deleteMusic/deleteSound consulten Engine::active() per parar // canals antes d'alliberar (si el motor aún 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