259 lines
12 KiB
C++
259 lines
12 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
|
|
// 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<Uint8[], SdlFreeDeleter>` — 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 = 50;
|
|
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<Uint8[], SdlFreeDeleter> 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<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)
|
|
// 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<void()> 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<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 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
|