Reestructura carpetes: src->source, third_party->source/external, shaders->data/shaders

This commit is contained in:
2026-05-04 13:21:34 +02:00
parent cec347a97c
commit e51ee84167
82 changed files with 36 additions and 39 deletions

578
source/audio/jail_audio.cpp Normal file
View File

@@ -0,0 +1,578 @@
#include "audio/jail_audio.hpp"
#include <algorithm>
#include <cassert>
#include <cstdio>
#include <memory>
#include <optional>
#include <vector>
// 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<short*>(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<const void*>(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<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(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<int>((static_cast<std::int64_t>(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<Uint64>(fade.duration_ms)) { return 1.0F; }
return static_cast<float>(ELAPSED) / static_cast<float>(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<Uint32>(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 <typename Fn>
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(&current_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(&current_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<void()> 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<std::uint8_t>& 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<int>(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*>(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*>(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<Uint8>`
// 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>();
music->ogg_data.assign(buffer, buffer + length);
int vorbis_error = 0;
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
static_cast<int>(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<int>(INFO.channels);
music->spec.freq = static_cast<int>(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<Sound>();
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

229
source/audio/jail_audio.hpp Normal file
View File

@@ -0,0 +1,229 @@
#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