230 lines
9.7 KiB
C++
230 lines
9.7 KiB
C++
#pragma once
|
|
|
|
// --- Includes ---
|
|
#include <SDL3/SDL.h>
|
|
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// 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<Uint8[], SdlFreeDeleter>` — 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<Uint8[], SdlFreeDeleter> 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<Uint8> 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<void()> 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<std::uint8_t>& bytes, const SDL_AudioSpec& spec, int group) -> int;
|
|
|
|
template <typename Fn>
|
|
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<void()> 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
|