Merge branch 'feat/sound-effects' — efectes d'àudio (eco/reverb) i reverb d'INTRO
This commit is contained in:
commit
7d725f99f4
18 files changed
+635
-2
No files matched your search
@@ -55,7 +55,9 @@ set(APP_SOURCES
|
|||||||
source/core/system/director.cpp
|
source/core/system/director.cpp
|
||||||
source/core/audio/audio.cpp
|
source/core/audio/audio.cpp
|
||||||
source/core/audio/audio_adapter.cpp
|
source/core/audio/audio_adapter.cpp
|
||||||
|
source/core/audio/audio_effects.cpp
|
||||||
source/core/audio/jail_audio.cpp
|
source/core/audio/jail_audio.cpp
|
||||||
|
source/core/audio/sound_effects_config.cpp
|
||||||
source/core/locale/locale.cpp
|
source/core/locale/locale.cpp
|
||||||
source/core/ui/ui_config.cpp
|
source/core/ui/ui_config.cpp
|
||||||
source/core/input/key_config.cpp
|
source/core/input/key_config.cpp
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ room_intro:
|
|||||||
line_gap_px: -4 # px extra entre les dues línies (piràmide / sala)
|
line_gap_px: -4 # px extra entre les dues línies (piràmide / sala)
|
||||||
player_spawn_delay_ms: 2400 # ms des de l'inici d'INTRO al spawn aeri.
|
player_spawn_delay_ms: 2400 # ms des de l'inici d'INTRO al spawn aeri.
|
||||||
player2_spawn_delay_ms: 2700 # només mode BOTH. Trenca la simultaneïtat.
|
player2_spawn_delay_ms: 2700 # només mode BOTH. Trenca la simultaneïtat.
|
||||||
|
# Preset de reverb (catàleg a `data/sounds.yaml`) aplicat al primer
|
||||||
|
# LAND de cada jugador durant la cinemàtica d'entrada. La sala dona
|
||||||
|
# "més detall" sonor a l'INTRO; els salts del joc i els respawns
|
||||||
|
# post-mort cauen al so sec. Buit o absent = sense reverb.
|
||||||
|
land_reverb: stone_hall
|
||||||
|
|
||||||
room_outro:
|
room_outro:
|
||||||
text_enter_ms: 400
|
text_enter_ms: 400
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Presets d'efectes per a sons puntuals. Les crides
|
||||||
|
# Audio::playSoundWithEcho(name, preset, group) i
|
||||||
|
# Audio::playSoundWithReverb(name, preset, group) referencien aquests
|
||||||
|
# presets pel seu nom (camp `preset`). Si el nom no existeix, el so es
|
||||||
|
# reprodueix sec amb un avís a stderr.
|
||||||
|
#
|
||||||
|
# Camps:
|
||||||
|
# echo:
|
||||||
|
# delay_ms → temps fins al primer rebot, en mil·lisegons.
|
||||||
|
# feedback → reinjecció (0..0.95). >0.7 sona "metàl·lic".
|
||||||
|
# wet → mescla del senyal humit (0..1). 1 = només eco.
|
||||||
|
# reverb:
|
||||||
|
# room_size → mida percebuda de la sala (0..1).
|
||||||
|
# damping → atenuació de freqüències altes en cada rebot (0..1).
|
||||||
|
# wet → mescla humida (0..1).
|
||||||
|
#
|
||||||
|
# Els processadors limiten la cua a 800 ms (echo) i 1500 ms (reverb).
|
||||||
|
|
||||||
|
echo:
|
||||||
|
cave:
|
||||||
|
delay_ms: 220
|
||||||
|
feedback: 0.45
|
||||||
|
wet: 0.35
|
||||||
|
short:
|
||||||
|
delay_ms: 80
|
||||||
|
feedback: 0.30
|
||||||
|
wet: 0.25
|
||||||
|
|
||||||
|
reverb:
|
||||||
|
stone_hall:
|
||||||
|
room_size: 0.78
|
||||||
|
damping: 0.50
|
||||||
|
wet: 0.40
|
||||||
|
small_room:
|
||||||
|
room_size: 0.40
|
||||||
|
damping: 0.70
|
||||||
|
wet: 0.30
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "core/audio/audio_adapter.hpp" // Per a AudioResource::getMusic/getSound
|
#include "core/audio/audio_adapter.hpp" // Per a AudioResource::getMusic/getSound
|
||||||
#include "core/audio/jail_audio.hpp" // Per a Ja::* (motor jailgames)
|
#include "core/audio/jail_audio.hpp" // Per a Ja::* (motor jailgames)
|
||||||
|
#include "core/audio/sound_effects_config.hpp" // Per a SoundEffectsConfig
|
||||||
#include "utils/defaults.hpp" // Per a Defaults::Audio::FREQUENCY
|
#include "utils/defaults.hpp" // Per a Defaults::Audio::FREQUENCY
|
||||||
|
|
||||||
// Invariant compile-time: tots els valors d'Audio::Group han de cabre als slots
|
// Invariant compile-time: tots els valors d'Audio::Group han de cabre als slots
|
||||||
@@ -128,6 +129,47 @@ void Audio::playSound(Ja::Sound* sound, Group group) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no
|
||||||
|
// existeix o l'engine retorna -1 (cap de canals d'efecte plé), cau a playSound
|
||||||
|
// sec — l'usuari sent el so encara que la cua no s'apliqui.
|
||||||
|
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
|
||||||
|
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
|
||||||
|
if (!sound_enabled_) { return; }
|
||||||
|
auto* sound = AudioResource::getSound(name);
|
||||||
|
if (sound == nullptr) { return; }
|
||||||
|
|
||||||
|
const auto* params = SoundEffectsConfig::get().findEcho(preset_name);
|
||||||
|
if (params == nullptr) {
|
||||||
|
std::fprintf(stderr, "Audio: preset d'eco '%s' desconegut — so '%s' es reprodueix sec\n", preset_name.c_str(), name.c_str());
|
||||||
|
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (engine_->playSoundWithEcho(sound, *params, static_cast<int>(group)) < 0) {
|
||||||
|
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
|
||||||
|
// fallback que playSoundWithEcho.
|
||||||
|
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
|
||||||
|
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
|
||||||
|
if (!sound_enabled_) { return; }
|
||||||
|
auto* sound = AudioResource::getSound(name);
|
||||||
|
if (sound == nullptr) { return; }
|
||||||
|
|
||||||
|
const auto* params = SoundEffectsConfig::get().findReverb(preset_name);
|
||||||
|
if (params == nullptr) {
|
||||||
|
std::fprintf(stderr, "Audio: preset de reverb '%s' desconegut — so '%s' es reprodueix sec\n", preset_name.c_str(), name.c_str());
|
||||||
|
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (engine_->playSoundWithReverb(sound, *params, static_cast<int>(group)) < 0) {
|
||||||
|
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Atura tots els sons
|
// Atura tots els sons
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
||||||
void Audio::stopAllSounds() {
|
void Audio::stopAllSounds() {
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ class Audio {
|
|||||||
// --- Control de sons ---
|
// --- Control de sons ---
|
||||||
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
|
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
|
||||||
void playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (muta globals de Ja)
|
void playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (muta globals de Ja)
|
||||||
|
// Reprodueix un so processat per un efecte definit a data/sounds.yaml
|
||||||
|
// (preset_name busca a SoundEffectsConfig). Si el preset no existeix
|
||||||
|
// o el motor està al cap de canals amb efecte, fa fallback a playSound
|
||||||
|
// sec — l'usuari sent el so igualment, sense la cua.
|
||||||
|
void playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group = Group::GAME);
|
||||||
|
void playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group = Group::GAME);
|
||||||
void stopAllSounds(); // Aturar tots els sons (muta globals de Ja)
|
void stopAllSounds(); // Aturar tots els sons (muta globals de Ja)
|
||||||
|
|
||||||
// --- Control de volum (API interna: float 0.0..1.0) ---
|
// --- Control de volum (API interna: float 0.0..1.0) ---
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
#include "core/audio/audio_effects.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/audio/jail_audio.hpp"
|
||||||
|
|
||||||
|
namespace AudioEffects {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// --- Caps de cua ---
|
||||||
|
constexpr float ECHO_TAIL_MS = 800.0F;
|
||||||
|
constexpr float REVERB_TAIL_MS = 1500.0F;
|
||||||
|
|
||||||
|
// --- Constants Freeverb ---
|
||||||
|
// Delays de comb i allpass tunats per a 44.1 kHz; els reescalem per
|
||||||
|
// freqüència real de la font.
|
||||||
|
constexpr int COMB_REFERENCE_RATE = 44100;
|
||||||
|
constexpr std::array<int, 8> COMB_DELAYS_L = {1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617};
|
||||||
|
constexpr std::array<int, 4> ALLPASS_DELAYS_L = {556, 441, 341, 225};
|
||||||
|
constexpr int STEREO_SPREAD = 23;
|
||||||
|
|
||||||
|
// Mapeig de Schroeder/Dattorro/Freeverb estàndard.
|
||||||
|
constexpr float FIXED_GAIN = 0.015F;
|
||||||
|
constexpr float SCALE_ROOM = 0.28F;
|
||||||
|
constexpr float OFFSET_ROOM = 0.7F;
|
||||||
|
constexpr float SCALE_DAMP = 0.4F;
|
||||||
|
|
||||||
|
// --- Decodificació a float -1..1 ---
|
||||||
|
// Suporta U8/S16, mono/estèreo. Mono es duplica a L i R (la cadena
|
||||||
|
// d'efectes treballa sempre amb dos canals per simplicitat).
|
||||||
|
auto decodeToStereoFloat(const Ja::Sound& src, std::vector<float>& left, std::vector<float>& right) -> bool {
|
||||||
|
const auto& spec = src.spec;
|
||||||
|
const Uint8* buf = src.buffer.get();
|
||||||
|
if (buf == nullptr || src.length == 0) { return false; }
|
||||||
|
|
||||||
|
int bytes_per_sample = 0;
|
||||||
|
if (spec.format == SDL_AUDIO_S16) {
|
||||||
|
bytes_per_sample = 2;
|
||||||
|
} else if (spec.format == SDL_AUDIO_U8) {
|
||||||
|
bytes_per_sample = 1;
|
||||||
|
} else {
|
||||||
|
std::cerr << "AudioEffects: format de so no suportat (només U8 o S16)\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (spec.channels < 1 || spec.channels > 2) {
|
||||||
|
std::cerr << "AudioEffects: el so ha de ser mono o estèreo\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t TOTAL_FRAMES = src.length / static_cast<std::size_t>(bytes_per_sample * spec.channels);
|
||||||
|
left.resize(TOTAL_FRAMES);
|
||||||
|
right.resize(TOTAL_FRAMES);
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < TOTAL_FRAMES; ++i) {
|
||||||
|
float sample_l = 0.0F;
|
||||||
|
float sample_r = 0.0F;
|
||||||
|
if (spec.format == SDL_AUDIO_S16) {
|
||||||
|
const auto* p = reinterpret_cast<const std::int16_t*>(buf + (i * spec.channels * 2));
|
||||||
|
sample_l = static_cast<float>(p[0]) / 32768.0F;
|
||||||
|
sample_r = (spec.channels == 2) ? static_cast<float>(p[1]) / 32768.0F : sample_l;
|
||||||
|
} else { // U8
|
||||||
|
const Uint8* p = buf + (i * spec.channels);
|
||||||
|
sample_l = (static_cast<float>(p[0]) - 128.0F) / 128.0F;
|
||||||
|
sample_r = (spec.channels == 2) ? (static_cast<float>(p[1]) - 128.0F) / 128.0F : sample_l;
|
||||||
|
}
|
||||||
|
left[i] = sample_l;
|
||||||
|
right[i] = sample_r;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empaqueta dos canals float (-1..1) a S16 entrellaçat.
|
||||||
|
void encodeStereoS16(const std::vector<float>& left, const std::vector<float>& right, std::vector<std::uint8_t>& out) {
|
||||||
|
const std::size_t LEN = left.size();
|
||||||
|
out.resize(LEN * 2 * sizeof(std::int16_t));
|
||||||
|
auto* dst = reinterpret_cast<std::int16_t*>(out.data());
|
||||||
|
for (std::size_t i = 0; i < LEN; ++i) {
|
||||||
|
const float L = std::clamp(left[i], -1.0F, 1.0F);
|
||||||
|
const float R = std::clamp(right[i], -1.0F, 1.0F);
|
||||||
|
dst[(i * 2) + 0] = static_cast<std::int16_t>(std::lround(L * 32767.0F));
|
||||||
|
dst[(i * 2) + 1] = static_cast<std::int16_t>(std::lround(R * 32767.0F));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reescala un delay de la taula de Freeverb per a la freqüència real.
|
||||||
|
auto scaledDelay(int reference_delay, int rate) -> int {
|
||||||
|
const long SCALED = std::lround(static_cast<double>(reference_delay) * static_cast<double>(rate) / static_cast<double>(COMB_REFERENCE_RATE));
|
||||||
|
return std::max(1, static_cast<int>(SCALED));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Filtres bàsics ---
|
||||||
|
struct Comb {
|
||||||
|
std::vector<float> buf;
|
||||||
|
std::size_t idx{0};
|
||||||
|
float feedback{0.0F};
|
||||||
|
float damp1{0.0F};
|
||||||
|
float damp2{1.0F};
|
||||||
|
float store{0.0F};
|
||||||
|
|
||||||
|
void init(int delay, float fb, float damping) {
|
||||||
|
buf.assign(static_cast<std::size_t>(delay), 0.0F);
|
||||||
|
idx = 0;
|
||||||
|
feedback = fb;
|
||||||
|
damp1 = damping;
|
||||||
|
damp2 = 1.0F - damping;
|
||||||
|
store = 0.0F;
|
||||||
|
}
|
||||||
|
auto tick(float in) -> float {
|
||||||
|
const float OUT = buf[idx];
|
||||||
|
store = (OUT * damp2) + (store * damp1);
|
||||||
|
buf[idx] = in + (store * feedback);
|
||||||
|
idx = (idx + 1) % buf.size();
|
||||||
|
return OUT;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Allpass {
|
||||||
|
std::vector<float> buf;
|
||||||
|
std::size_t idx{0};
|
||||||
|
|
||||||
|
void init(int delay) {
|
||||||
|
buf.assign(static_cast<std::size_t>(delay), 0.0F);
|
||||||
|
idx = 0;
|
||||||
|
}
|
||||||
|
auto tick(float in) -> float {
|
||||||
|
const float BUFOUT = buf[idx];
|
||||||
|
const float OUT = -in + BUFOUT;
|
||||||
|
buf[idx] = in + (BUFOUT * 0.5F);
|
||||||
|
idx = (idx + 1) % buf.size();
|
||||||
|
return OUT;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto applyEcho(const Ja::Sound& src, const Ja::EchoParams& params) -> std::optional<ProcessedSound> {
|
||||||
|
std::vector<float> left;
|
||||||
|
std::vector<float> right;
|
||||||
|
if (!decodeToStereoFloat(src, left, right)) { return std::nullopt; }
|
||||||
|
|
||||||
|
const int RATE = src.spec.freq;
|
||||||
|
const int DELAY_SAMPLES = std::max(1, static_cast<int>(std::lround(params.delay_ms * 0.001F * static_cast<float>(RATE))));
|
||||||
|
const auto TAIL_SAMPLES = static_cast<std::size_t>(std::lround(ECHO_TAIL_MS * 0.001F * static_cast<float>(RATE)));
|
||||||
|
|
||||||
|
const float FEEDBACK = std::clamp(params.feedback, 0.0F, 0.95F);
|
||||||
|
const float WET = std::clamp(params.wet, 0.0F, 1.0F);
|
||||||
|
const float DRY = 1.0F - WET;
|
||||||
|
|
||||||
|
const std::size_t INPUT_LEN = left.size();
|
||||||
|
const std::size_t TOTAL_LEN = INPUT_LEN + TAIL_SAMPLES;
|
||||||
|
|
||||||
|
std::vector<float> ring_l(static_cast<std::size_t>(DELAY_SAMPLES), 0.0F);
|
||||||
|
std::vector<float> ring_r(static_cast<std::size_t>(DELAY_SAMPLES), 0.0F);
|
||||||
|
std::size_t cursor = 0;
|
||||||
|
|
||||||
|
std::vector<float> out_l(TOTAL_LEN);
|
||||||
|
std::vector<float> out_r(TOTAL_LEN);
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < TOTAL_LEN; ++i) {
|
||||||
|
const float IN_L = (i < INPUT_LEN) ? left[i] : 0.0F;
|
||||||
|
const float IN_R = (i < INPUT_LEN) ? right[i] : 0.0F;
|
||||||
|
|
||||||
|
const float DELAYED_L = ring_l[cursor];
|
||||||
|
const float DELAYED_R = ring_r[cursor];
|
||||||
|
|
||||||
|
out_l[i] = (DRY * IN_L) + (WET * DELAYED_L);
|
||||||
|
out_r[i] = (DRY * IN_R) + (WET * DELAYED_R);
|
||||||
|
|
||||||
|
ring_l[cursor] = IN_L + (DELAYED_L * FEEDBACK);
|
||||||
|
ring_r[cursor] = IN_R + (DELAYED_R * FEEDBACK);
|
||||||
|
cursor = (cursor + 1) % static_cast<std::size_t>(DELAY_SAMPLES);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessedSound result;
|
||||||
|
result.spec = SDL_AudioSpec{.format = SDL_AUDIO_S16, .channels = 2, .freq = RATE};
|
||||||
|
encodeStereoS16(out_l, out_r, result.bytes);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto applyReverb(const Ja::Sound& src, const Ja::ReverbParams& params) -> std::optional<ProcessedSound> {
|
||||||
|
std::vector<float> left;
|
||||||
|
std::vector<float> right;
|
||||||
|
if (!decodeToStereoFloat(src, left, right)) { return std::nullopt; }
|
||||||
|
|
||||||
|
const int RATE = src.spec.freq;
|
||||||
|
const auto TAIL_SAMPLES = static_cast<std::size_t>(std::lround(REVERB_TAIL_MS * 0.001F * static_cast<float>(RATE)));
|
||||||
|
|
||||||
|
const float ROOM_SIZE = std::clamp(params.room_size, 0.0F, 1.0F);
|
||||||
|
const float DAMPING = std::clamp(params.damping, 0.0F, 1.0F);
|
||||||
|
const float WET = std::clamp(params.wet, 0.0F, 1.0F);
|
||||||
|
const float DRY = 1.0F - WET;
|
||||||
|
|
||||||
|
const float FEEDBACK = (ROOM_SIZE * SCALE_ROOM) + OFFSET_ROOM; // 0.7..0.98
|
||||||
|
const float DAMP1 = DAMPING * SCALE_DAMP; // 0..0.4
|
||||||
|
|
||||||
|
// Inicialitza els 8 comb filters per cada canal i els 4 allpass.
|
||||||
|
std::array<Comb, 8> comb_l;
|
||||||
|
std::array<Comb, 8> comb_r;
|
||||||
|
for (std::size_t i = 0; i < COMB_DELAYS_L.size(); ++i) {
|
||||||
|
comb_l[i].init(scaledDelay(COMB_DELAYS_L[i], RATE), FEEDBACK, DAMP1);
|
||||||
|
comb_r[i].init(scaledDelay(COMB_DELAYS_L[i] + STEREO_SPREAD, RATE), FEEDBACK, DAMP1);
|
||||||
|
}
|
||||||
|
std::array<Allpass, 4> allpass_l;
|
||||||
|
std::array<Allpass, 4> allpass_r;
|
||||||
|
for (std::size_t i = 0; i < ALLPASS_DELAYS_L.size(); ++i) {
|
||||||
|
allpass_l[i].init(scaledDelay(ALLPASS_DELAYS_L[i], RATE));
|
||||||
|
allpass_r[i].init(scaledDelay(ALLPASS_DELAYS_L[i] + STEREO_SPREAD, RATE));
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t INPUT_LEN = left.size();
|
||||||
|
const std::size_t TOTAL_LEN = INPUT_LEN + TAIL_SAMPLES;
|
||||||
|
std::vector<float> out_l(TOTAL_LEN);
|
||||||
|
std::vector<float> out_r(TOTAL_LEN);
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < TOTAL_LEN; ++i) {
|
||||||
|
const float IN_L = (i < INPUT_LEN) ? left[i] : 0.0F;
|
||||||
|
const float IN_R = (i < INPUT_LEN) ? right[i] : 0.0F;
|
||||||
|
const float MONO_INPUT = (IN_L + IN_R) * FIXED_GAIN;
|
||||||
|
|
||||||
|
// 8 comb filters en paral·lel, sumats.
|
||||||
|
float wet_l = 0.0F;
|
||||||
|
float wet_r = 0.0F;
|
||||||
|
for (std::size_t k = 0; k < comb_l.size(); ++k) {
|
||||||
|
wet_l += comb_l[k].tick(MONO_INPUT);
|
||||||
|
wet_r += comb_r[k].tick(MONO_INPUT);
|
||||||
|
}
|
||||||
|
// 4 allpass en sèrie.
|
||||||
|
for (std::size_t k = 0; k < allpass_l.size(); ++k) {
|
||||||
|
wet_l = allpass_l[k].tick(wet_l);
|
||||||
|
wet_r = allpass_r[k].tick(wet_r);
|
||||||
|
}
|
||||||
|
|
||||||
|
out_l[i] = (DRY * IN_L) + (WET * wet_l);
|
||||||
|
out_r[i] = (DRY * IN_R) + (WET * wet_r);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessedSound result;
|
||||||
|
result.spec = SDL_AudioSpec{.format = SDL_AUDIO_S16, .channels = 2, .freq = RATE};
|
||||||
|
encodeStereoS16(out_l, out_r, result.bytes);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace AudioEffects
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Forward-declaració per no incloure jail_audio.hpp (cicle d'inclusió: aquest
|
||||||
|
// header viu sota els params declarats a jail_audio.hpp, i alhora jail_audio
|
||||||
|
// usa applyEcho/applyReverb).
|
||||||
|
namespace Ja {
|
||||||
|
struct Sound;
|
||||||
|
struct EchoParams;
|
||||||
|
struct ReverbParams;
|
||||||
|
} // namespace Ja
|
||||||
|
|
||||||
|
// Processadors d'efectes per a sons puntuals. Reben un Ja::Sound (qualsevol
|
||||||
|
// format suportat pel decodificador WAV: U8/S16, mono o estèreo) i tornen un
|
||||||
|
// buffer PCM en S16 + el seu spec, llest per empenyer a un SDL_AudioStream.
|
||||||
|
//
|
||||||
|
// El buffer de sortida inclou la cua (decay) generada per l'efecte: per al
|
||||||
|
// reverb, fins a 1500 ms; per a l'eco, fins a 800 ms. Aquests caps eviten
|
||||||
|
// allargar indefinidament la reproducció quan els paràmetres reinjecten molt.
|
||||||
|
//
|
||||||
|
// Si el format del so d'origen no es pot processar, retornen std::nullopt
|
||||||
|
// (el caller ha de fer fallback a reproducció seca).
|
||||||
|
namespace AudioEffects {
|
||||||
|
|
||||||
|
struct ProcessedSound {
|
||||||
|
std::vector<std::uint8_t> bytes; // PCM S16 entrellaçat (LRLRLR... si stereo)
|
||||||
|
SDL_AudioSpec spec; // Format/canals/freqüència del buffer
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] auto applyEcho(const Ja::Sound& src, const Ja::EchoParams& params) -> std::optional<ProcessedSound>;
|
||||||
|
[[nodiscard]] auto applyReverb(const Ja::Sound& src, const Ja::ReverbParams& params) -> std::optional<ProcessedSound>;
|
||||||
|
|
||||||
|
} // namespace AudioEffects
|
||||||
@@ -4,6 +4,10 @@
|
|||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/audio/audio_effects.hpp"
|
||||||
|
|
||||||
// Només declaracions de stb_vorbis: STB_VORBIS_HEADER_ONLY omet el bloc
|
// 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
|
// d'implementació. Les definicions les aporta source/external/stb_vorbis_impl.cpp
|
||||||
@@ -397,13 +401,17 @@ namespace Ja {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Engine::stopChannel(const int channel) {
|
void Engine::stopChannel(const int channel) {
|
||||||
forEachTargetChannel(channel, [](Channel& ch) {
|
forEachTargetChannel(channel, [this](Channel& ch) {
|
||||||
if (ch.state != ChannelState::FREE) {
|
if (ch.state != ChannelState::FREE) {
|
||||||
if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); }
|
if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); }
|
||||||
ch.stream = nullptr;
|
ch.stream = nullptr;
|
||||||
ch.state = ChannelState::FREE;
|
ch.state = ChannelState::FREE;
|
||||||
ch.pos = 0;
|
ch.pos = 0;
|
||||||
ch.sound = nullptr;
|
ch.sound = nullptr;
|
||||||
|
if (ch.has_effect) {
|
||||||
|
ch.has_effect = false;
|
||||||
|
if (effect_channels_active_ > 0) { --effect_channels_active_; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -438,6 +446,55 @@ namespace Ja {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Engine::playSoundWithEcho(const Sound* sound, const EchoParams& params, const int group) -> int {
|
||||||
|
if (sound == nullptr) { return -1; }
|
||||||
|
auto processed = AudioEffects::applyEcho(*sound, params);
|
||||||
|
if (!processed) { return -1; }
|
||||||
|
return playProcessedOnFreeChannel(processed->bytes, processed->spec, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Engine::playSoundWithReverb(const Sound* sound, const ReverbParams& params, const int group) -> int {
|
||||||
|
if (sound == nullptr) { return -1; }
|
||||||
|
auto processed = AudioEffects::applyReverb(*sound, params);
|
||||||
|
if (!processed) { return -1; }
|
||||||
|
return playProcessedOnFreeChannel(processed->bytes, processed->spec, group);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Factories i destructors (permanents) ---
|
// --- Factories i destructors (permanents) ---
|
||||||
|
|
||||||
auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
|
auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
|
||||||
|
|||||||
@@ -48,6 +48,26 @@ namespace Ja {
|
|||||||
// --- Constants ---
|
// --- Constants ---
|
||||||
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20;
|
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20;
|
||||||
inline constexpr int MAX_GROUPS = 2;
|
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
|
// 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
|
// com a valor inicial de Sound/Music. L'spec real d'ús l'imposa el ctor
|
||||||
@@ -72,6 +92,10 @@ namespace Ja {
|
|||||||
int times{0};
|
int times{0};
|
||||||
int group{0};
|
int group{0};
|
||||||
ChannelState state{ChannelState::FREE};
|
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 {
|
struct Music {
|
||||||
@@ -147,6 +171,12 @@ namespace Ja {
|
|||||||
// --- So ---
|
// --- So ---
|
||||||
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
|
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
|
||||||
auto playSoundOnChannel(Sound* sound, int channel, 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 pauseChannel(int channel);
|
||||||
void resumeChannel(int channel);
|
void resumeChannel(int channel);
|
||||||
void stopChannel(int channel);
|
void stopChannel(int channel);
|
||||||
@@ -161,6 +191,10 @@ namespace Ja {
|
|||||||
void updateIncomingFade();
|
void updateIncomingFade();
|
||||||
void updateCurrentMusic();
|
void updateCurrentMusic();
|
||||||
void updateSoundChannels();
|
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>
|
template <typename Fn>
|
||||||
void forEachTargetChannel(int channel, Fn&& fn);
|
void forEachTargetChannel(int channel, Fn&& fn);
|
||||||
@@ -174,6 +208,9 @@ namespace Ja {
|
|||||||
OutgoingMusic outgoing_music_;
|
OutgoingMusic outgoing_music_;
|
||||||
FadeState incoming_fade_;
|
FadeState incoming_fade_;
|
||||||
std::function<void()> on_music_ended_;
|
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 _
|
// NOLINTNEXTLINE(readability-identifier-naming) — convenció projecte: private static amb sufix _
|
||||||
static Engine* active_;
|
static Engine* active_;
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#include "core/audio/sound_effects_config.hpp"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Lector de camp amb fallback: deixa el destí intacte si la clau no
|
||||||
|
// existeix (els defaults dels Ja::*Params s'inicialitzen al ctor del
|
||||||
|
// struct, així que el comportament és "preset parcial = preset complet
|
||||||
|
// amb defaults per als camps que falten").
|
||||||
|
template <typename T>
|
||||||
|
void readField(const fkyaml::node& node, const char* key, T& dst) {
|
||||||
|
if (node.contains(key)) { dst = node[key].get_value<T>(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto SoundEffectsConfig::get() -> SoundEffectsConfig& {
|
||||||
|
static SoundEffectsConfig instance_;
|
||||||
|
return instance_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SoundEffectsConfig::load(const std::string& file_path) {
|
||||||
|
auto bytes = Resource::Helper::loadFile(file_path);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
std::cerr << "SoundEffectsConfig: no s'ha pogut obrir " << file_path << " — cap preset d'efecte disponible\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||||||
|
const auto* end = begin + bytes.size();
|
||||||
|
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||||
|
|
||||||
|
if (yaml.contains("echo") && yaml["echo"].is_mapping()) {
|
||||||
|
for (auto it = yaml["echo"].begin(); it != yaml["echo"].end(); ++it) {
|
||||||
|
const auto NAME = it.key().get_value<std::string>();
|
||||||
|
const auto& node = it.value();
|
||||||
|
Ja::EchoParams params{};
|
||||||
|
readField(node, "delay_ms", params.delay_ms);
|
||||||
|
readField(node, "feedback", params.feedback);
|
||||||
|
readField(node, "wet", params.wet);
|
||||||
|
echoes_[NAME] = params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yaml.contains("reverb") && yaml["reverb"].is_mapping()) {
|
||||||
|
for (auto it = yaml["reverb"].begin(); it != yaml["reverb"].end(); ++it) {
|
||||||
|
const auto NAME = it.key().get_value<std::string>();
|
||||||
|
const auto& node = it.value();
|
||||||
|
Ja::ReverbParams params{};
|
||||||
|
readField(node, "room_size", params.room_size);
|
||||||
|
readField(node, "damping", params.damping);
|
||||||
|
readField(node, "wet", params.wet);
|
||||||
|
reverbs_[NAME] = params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "SoundEffectsConfig: " << echoes_.size() << " preset(s) d'eco i " << reverbs_.size() << " de reverb des de " << file_path << '\n';
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "SoundEffectsConfig: error parsejant " << file_path << ": " << e.what() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SoundEffectsConfig::findEcho(const std::string& name) const -> const Ja::EchoParams* {
|
||||||
|
const auto IT = echoes_.find(name);
|
||||||
|
return (IT == echoes_.end()) ? nullptr : &IT->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SoundEffectsConfig::findReverb(const std::string& name) const -> const Ja::ReverbParams* {
|
||||||
|
const auto IT = reverbs_.find(name);
|
||||||
|
return (IT == reverbs_.end()) ? nullptr : &IT->second;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "core/audio/jail_audio.hpp" // Per a Ja::EchoParams / Ja::ReverbParams
|
||||||
|
|
||||||
|
// Catàleg de presets d'efectes carregat des de data/sounds.yaml. La capa
|
||||||
|
// Audio (playSoundWithEcho/playSoundWithReverb) hi accedeix per nom: si el
|
||||||
|
// preset no existeix, el so es reprodueix sec amb un avís a stderr.
|
||||||
|
//
|
||||||
|
// Patró Meyers idèntic a UiConfig/Locale: un sol load() a l'arrencada, sense
|
||||||
|
// hot-reload. Si el fitxer no existeix, el catàleg queda buit (cap preset
|
||||||
|
// disponible) i tots els playSoundWith* es comporten com playSound dry.
|
||||||
|
class SoundEffectsConfig {
|
||||||
|
public:
|
||||||
|
static auto get() -> SoundEffectsConfig&;
|
||||||
|
|
||||||
|
SoundEffectsConfig(const SoundEffectsConfig&) = delete;
|
||||||
|
SoundEffectsConfig(SoundEffectsConfig&&) = delete;
|
||||||
|
auto operator=(const SoundEffectsConfig&) -> SoundEffectsConfig& = delete;
|
||||||
|
auto operator=(SoundEffectsConfig&&) -> SoundEffectsConfig& = delete;
|
||||||
|
|
||||||
|
void load(const std::string& file_path);
|
||||||
|
|
||||||
|
// Retorna nullptr si el preset no existeix.
|
||||||
|
[[nodiscard]] auto findEcho(const std::string& name) const -> const Ja::EchoParams*;
|
||||||
|
[[nodiscard]] auto findReverb(const std::string& name) const -> const Ja::ReverbParams*;
|
||||||
|
|
||||||
|
private:
|
||||||
|
SoundEffectsConfig() = default;
|
||||||
|
~SoundEffectsConfig() = default;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, Ja::EchoParams> echoes_;
|
||||||
|
std::unordered_map<std::string, Ja::ReverbParams> reverbs_;
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
|
#include "core/audio/sound_effects_config.hpp"
|
||||||
#include "core/input/gamepad_manager.hpp"
|
#include "core/input/gamepad_manager.hpp"
|
||||||
#include "core/input/key_config.hpp"
|
#include "core/input/key_config.hpp"
|
||||||
#include "core/input/mouse.hpp"
|
#include "core/input/mouse.hpp"
|
||||||
@@ -135,6 +136,11 @@ Director::Director() {
|
|||||||
// perquè forma part del preload d'assets, com la font i la paleta.
|
// perquè forma part del preload d'assets, com la font i la paleta.
|
||||||
Resource::Cache::get().loadMusic("pyramid_1.ogg", "data/music/pyramid_1.ogg");
|
Resource::Cache::get().loadMusic("pyramid_1.ogg", "data/music/pyramid_1.ogg");
|
||||||
|
|
||||||
|
// Catàleg de presets d'efectes per als sons. Es carrega abans d'Audio
|
||||||
|
// perquè els primers playSoundWith* (al començament del joc) ja
|
||||||
|
// troben els presets disponibles.
|
||||||
|
SoundEffectsConfig::get().load("data/sounds.yaml");
|
||||||
|
|
||||||
// Audio: singleton manual propietari d'un SDL audio device. Es destrueix
|
// Audio: singleton manual propietari d'un SDL audio device. Es destrueix
|
||||||
// explícitament a ~Director abans que Resource::Cache (Meyers) buide els
|
// explícitament a ~Director abans que Resource::Cache (Meyers) buide els
|
||||||
// JA_Music_t que continga, així que no queden streams orfes.
|
// JA_Music_t que continga, així que no queden streams orfes.
|
||||||
|
|||||||
@@ -697,7 +697,15 @@ auto Player::integrateJump(float delta_time) -> bool {
|
|||||||
state_ = State::LANDING_STUN;
|
state_ = State::LANDING_STUN;
|
||||||
landing_stun_remaining_ms_ = static_cast<float>(jump_config.landing_stun_ms);
|
landing_stun_remaining_ms_ = static_cast<float>(jump_config.landing_stun_ms);
|
||||||
if (!jump_config.land_sound_key.empty() && Audio::get() != nullptr) {
|
if (!jump_config.land_sound_key.empty() && Audio::get() != nullptr) {
|
||||||
|
// Consumeix el preset armat per GameScene al spawn aeri
|
||||||
|
// d'INTRO (single-shot): el primer LAND el sona reverberat,
|
||||||
|
// els salts següents cauen al camí sec.
|
||||||
|
if (next_land_reverb_preset_.empty()) {
|
||||||
Audio::get()->playSound(jump_config.land_sound_key, Audio::Group::GAME);
|
Audio::get()->playSound(jump_config.land_sound_key, Audio::Group::GAME);
|
||||||
|
} else {
|
||||||
|
Audio::get()->playSoundWithReverb(jump_config.land_sound_key, next_land_reverb_preset_, Audio::Group::GAME);
|
||||||
|
next_land_reverb_preset_.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (bus_ != nullptr) {
|
if (bus_ != nullptr) {
|
||||||
bus_->publish(Event{
|
bus_->publish(Event{
|
||||||
|
|||||||
@@ -259,6 +259,15 @@ class Player : public AnimatedSprite {
|
|||||||
// al respawn post-mort: mateixa entrada cinemàtica.
|
// al respawn post-mort: mateixa entrada cinemàtica.
|
||||||
void spawnFromAir(float x, float y);
|
void spawnFromAir(float x, float y);
|
||||||
|
|
||||||
|
// Arma un preset de reverb (catàleg a `data/sounds.yaml`) per al
|
||||||
|
// pròxim aterratge d'aquest jugador. Pensat per a la cinemàtica
|
||||||
|
// d'INTRO de sala: GameScene crida aquesta funció just després de
|
||||||
|
// `spawnFromAir`, i el primer LAND consumeix el preset (queda buit
|
||||||
|
// immediatament). Els salts posteriors del joc, els respawns
|
||||||
|
// post-mort i el reset general no rearmen res. `preset` buit
|
||||||
|
// desactiva el reverb del proper LAND (no afecta cap LAND posterior).
|
||||||
|
void armNextLandReverb(std::string preset) { next_land_reverb_preset_ = std::move(preset); }
|
||||||
|
|
||||||
// Alçada actual del salt (píxels visuals per damunt del sòl).
|
// Alçada actual del salt (píxels visuals per damunt del sòl).
|
||||||
// 0 quan està a terra. L'usa el render i el sistema de col·lisions
|
// 0 quan està a terra. L'usa el render i el sistema de col·lisions
|
||||||
// per altura.
|
// per altura.
|
||||||
@@ -390,6 +399,14 @@ class Player : public AnimatedSprite {
|
|||||||
// gestiona `intended_vx_/vy_` escalats pel resolver.
|
// gestiona `intended_vx_/vy_` escalats pel resolver.
|
||||||
bool being_pushed_{false};
|
bool being_pushed_{false};
|
||||||
|
|
||||||
|
// Preset de reverb (catàleg de `data/sounds.yaml`) que el pròxim
|
||||||
|
// LAND ha de fer servir; buit = so sec. L'arma `armNextLandReverb`
|
||||||
|
// (cridada per GameScene al spawn aeri d'INTRO) i el consumeix
|
||||||
|
// `integrateJump` quan el jugador toca terra. Cap altre punt
|
||||||
|
// (respawn, exit, disable) el toca: així només sona reverberat el
|
||||||
|
// primer LAND de la cinemàtica d'entrada.
|
||||||
|
std::string next_land_reverb_preset_;
|
||||||
|
|
||||||
// Escala d'empenta usada per `snapToLane` quan descobreix un blocker
|
// Escala d'empenta usada per `snapToLane` quan descobreix un blocker
|
||||||
// (mateixa del resolver). 1.0 = sense escalar (comportament si
|
// (mateixa del resolver). 1.0 = sense escalar (comportament si
|
||||||
// GameScene no ha cridat setPushFactor).
|
// GameScene no ha cridat setPushFactor).
|
||||||
|
|||||||
@@ -556,6 +556,11 @@ void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
|
|||||||
float x = SINGLE_X;
|
float x = SINGLE_X;
|
||||||
if (BOTH_ACTIVE) { x = (slot_index == 0) ? SPAWN.p1_x : SPAWN.p2_x; }
|
if (BOTH_ACTIVE) { x = (slot_index == 0) ? SPAWN.p1_x : SPAWN.p2_x; }
|
||||||
players_[slot_index]->spawnFromAir(x, SPAWN.y);
|
players_[slot_index]->spawnFromAir(x, SPAWN.y);
|
||||||
|
// Arma el reverb del primer LAND com a part de la cinemàtica
|
||||||
|
// d'INTRO. Una sola vegada per spawn aeri: el respawn post-mort
|
||||||
|
// (que no passa per ací) cau al camí sec. Si el preset està buit
|
||||||
|
// al YAML, l'arma queda buida i el LAND és sec igualment.
|
||||||
|
players_[slot_index]->armNextLandReverb(transition_config_.intro_land_reverb_preset);
|
||||||
|
|
||||||
std::cout << "[ROOM] spawn cinematic P" << (slot_index + 1) << ": airborne entry\n";
|
std::cout << "[ROOM] spawn cinematic P" << (slot_index + 1) << ": airborne entry\n";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ namespace RoomFsm {
|
|||||||
// El YAML pot trencar la simultaneïtat fixant un valor propi.
|
// El YAML pot trencar la simultaneïtat fixant un valor propi.
|
||||||
cfg.player2_spawn_delay_ms = readChildInt(n, "player2_spawn_delay_ms", cfg.player_spawn_delay_ms);
|
cfg.player2_spawn_delay_ms = readChildInt(n, "player2_spawn_delay_ms", cfg.player_spawn_delay_ms);
|
||||||
cfg.intro_text_line_gap_px = readChildInt(n, "line_gap_px", cfg.intro_text_line_gap_px);
|
cfg.intro_text_line_gap_px = readChildInt(n, "line_gap_px", cfg.intro_text_line_gap_px);
|
||||||
|
if (n.contains("land_reverb")) {
|
||||||
|
cfg.intro_land_reverb_preset = n["land_reverb"].get_value<std::string>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (yaml.contains("room_outro")) {
|
if (yaml.contains("room_outro")) {
|
||||||
const auto& n = yaml["room_outro"];
|
const auto& n = yaml["room_outro"];
|
||||||
|
|||||||
@@ -80,6 +80,13 @@ namespace RoomFsm {
|
|||||||
UiConfig::CurveFn text_enter_curve{nullptr};
|
UiConfig::CurveFn text_enter_curve{nullptr};
|
||||||
UiConfig::CurveFn text_exit_curve{nullptr};
|
UiConfig::CurveFn text_exit_curve{nullptr};
|
||||||
|
|
||||||
|
// Preset de reverb (catàleg a `data/sounds.yaml`) que GameScene
|
||||||
|
// arma al jugador al spawn aeri d'INTRO. Es consumeix al primer
|
||||||
|
// aterratge cinemàtic; els salts posteriors del joc i els
|
||||||
|
// respawns post-mort cauen al so sec. Buit = sense reverb (la
|
||||||
|
// cinemàtica d'entrada sona igual que un salt qualsevol).
|
||||||
|
std::string intro_land_reverb_preset;
|
||||||
|
|
||||||
// Panel translúcid sota el rètol de transició (INTRO i OUTRO).
|
// Panel translúcid sota el rètol de transició (INTRO i OUTRO).
|
||||||
// Centrat verticalment al centre del playfield, ocupa tota
|
// Centrat verticalment al centre del playfield, ocupa tota
|
||||||
// l'amplada del canvas. Creix d'altura 0 → height_px durant la
|
// l'amplada del canvas. Creix d'altura 0 → height_px durant la
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace Texts {
|
namespace Texts {
|
||||||
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
||||||
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
||||||
constexpr const char* VERSION = "0.27.0"; // Single source of truth — Makefile el grep
|
constexpr const char* VERSION = "0.28.0"; // Single source of truth — Makefile el grep
|
||||||
} // namespace Texts
|
} // namespace Texts
|
||||||
|
|
||||||
namespace GameCanvas {
|
namespace GameCanvas {
|
||||||
|
|||||||
Reference in new issue
Block a user