feat(audio): so del sarcòfag amb pitch aleatori per obertura

This commit is contained in:
JailDesigner committed 2026-05-11 20:46:18 +02:00
1 parent 3e31a88f78
commit 932d195eb0
11 files changed
+112 -11

No files matched your search

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

After

Width:  |  Height:  |  Size: 1.6 KiB

+14 -2
View File
@@ -20,11 +20,23 @@ flags:
interactable: true interactable: true
animations: animations:
closed: { frames: [0], fps: 1, loop: true } closed: { frames: [0], fps: 1, loop: true }
opening: { frames: [0], fps: 6, loop: false } opening: { frames: [1, 2], fps: 6, loop: false }
open: { frames: [0], fps: 1, loop: true } open: { frames: [3], fps: 1, loop: true }
behaviour: behaviour:
states: [CLOSED, OPENING, OPEN] states: [CLOSED, OPENING, OPEN]
initial_state: CLOSED initial_state: CLOSED
on_perimeter_complete: OPENING on_perimeter_complete: OPENING
on_opening_end: OPEN on_opening_end: OPEN
emits_event_on_open: sarcophagus_opened emits_event_on_open: sarcophagus_opened
# Sons opcionals. `opening` es dispara un sol cop en la transició
# CLOSED → OPENING (quan el jugador completa el perímetre). Si la clau
# manca, no sona res. El basename val tant de clau al Resource::Cache
# com de path (`Defaults::Paths::AUDIO_SOUNDS_DIR + key`).
#
# `opening_pitch_spread`: variació uniforme del ratio de reproducció
# centrada a 1.0. Amb 0.05 cada obertura sona entre 0.95× i 1.05× la
# velocitat normal (puja/baixa també el to, efecte arcade clàssic).
# 0 ⇒ sempre 1.0 (sense variació).
sound:
opening: open_sarc.wav
opening_pitch_spread: 0.5
+16
View File
@@ -136,6 +136,22 @@ void Audio::playSound(Ja::Sound* sound, Group group) {
} }
} }
// Variant amb velocitat (i to) escalats. Apliquem el ratio al canal
// just retornat per `playSound`: així el `SDL_AudioStream` recent creat
// processa tot el sample amb el ratio des del primer pull del callback.
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
// la crida al ratio — sense efectes col·laterals.
// NOLINTNEXTLINE(readability-make-member-function-const)
void Audio::playSound(const std::string& name, Group group, float speed) {
if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name);
if (sound == nullptr) { return; }
const int CH = engine_->playSound(sound, 0, static_cast<int>(group));
if (CH >= 0 && speed != 1.0F) {
engine_->setChannelSpeed(CH, speed);
}
}
// Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no // 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 // 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. // sec — l'usuari sent el so encara que la cua no s'apliqui.
+6
View File
@@ -85,6 +85,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 amb la velocitat (i to) escalats per `speed`:
// 1.0 = normal, 0.95 ≈ -5% (més greu i lent), 1.05 ≈ +5% (més
// agut i ràpid). Mateixa semàntica que `setMusicSpeed`. Útil per a
// variacions subtils que eviten la fatiga d'escoltar el mateix
// sample idèntic (p.ex. obertures de sarcòfag, picks d'ítems).
void playSound(const std::string& name, Group group, float speed);
// Reprodueix un so processat per un efecte definit a data/config/sounds.yaml // Reprodueix un so processat per un efecte definit a data/config/sounds.yaml
// (preset_name busca a SoundEffectsConfig). Si el preset no existeix // (preset_name busca a SoundEffectsConfig). Si el preset no existeix
// o el motor està al cap de canals amb efecte, fa fallback a playSound // o el motor està al cap de canals amb efecte, fa fallback a playSound
+6
View File
@@ -430,6 +430,12 @@ namespace Ja {
return channel; return channel;
} }
void Engine::setChannelSpeed(const int channel, const float ratio) {
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return; }
if (channels_[channel].stream == nullptr) { return; }
SDL_SetAudioStreamFrequencyRatio(channels_[channel].stream, ratio);
}
void Engine::pauseChannel(const int channel) { void Engine::pauseChannel(const int channel) {
forEachTargetChannel(channel, [](Channel& ch) { forEachTargetChannel(channel, [](Channel& ch) {
if (ch.state == ChannelState::PLAYING) { if (ch.state == ChannelState::PLAYING) {
+8
View File
@@ -192,6 +192,14 @@ 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;
// Ajusta la velocitat de reproducció d'un canal actiu via
// `SDL_SetAudioStreamFrequencyRatio`. 1.0 = normal. Igual que a
// `setMusicSpeed`, puja/baixa el to junt amb la velocitat
// (efecte "chipmunk"); per a SFX curts arcade és el que volem.
// No-op si el canal no està actiu. Cridar-lo just després de
// `playSound`/`playSoundOnChannel` perquè el ratio cobreixi
// tota la reproducció.
void setChannelSpeed(int channel, float ratio);
// Reproducció amb so processat per un efecte. Retorna el canal // Reproducció amb so processat per un efecte. Retorna el canal
// assignat o -1 si no queden slots d'efecte (MAX_EFFECT_CHANNELS). // 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 // El sound original només s'usa per consultar el spec/buffer; el
@@ -1,14 +1,26 @@
#include "game/entities/concrete/sarcophagus.hpp" #include "game/entities/concrete/sarcophagus.hpp"
#include <random>
#include <string> #include <string>
#include <vector> #include <vector>
#include "core/audio/audio.hpp"
#include "core/resources/resource_cache.hpp"
#include "game/entities/entity_factory.hpp" #include "game/entities/entity_factory.hpp"
#include "game/entities/entity_manager.hpp" #include "game/entities/entity_manager.hpp"
#include "game/entities/event_bus.hpp" #include "game/entities/event_bus.hpp"
#include "utils/defaults.hpp"
namespace { namespace {
// RNG local al mòdul, mateix patró que `mummy.cpp`: llavor fixa per
// facilitar la depuració. Si algun dia volem aleatoritat real entre
// partides, prou amb llegir-la de std::random_device.
auto rng() -> std::mt19937& {
static std::mt19937 k_rng_{0xCAFEU};
return k_rng_;
}
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent { auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
std::vector<int> frames = {0}; std::vector<int> frames = {0};
float fps = 1.0F; float fps = 1.0F;
@@ -57,6 +69,20 @@ Sarcophagus::Sarcophagus(const fkyaml::node& yaml)
interactable_ = true; interactable_ = true;
readTileSize(yaml, tile_size_w_, tile_size_h_); readTileSize(yaml, tile_size_w_, tile_size_h_);
anim_ = anim_closed_; anim_ = anim_closed_;
// Sons opcionals (bloc `sound:` del YAML). El basename val tant de
// clau al Resource::Cache com de path dins `Defaults::Paths::AUDIO_SOUNDS_DIR`.
// Mateix patró que `mummy.hurt.sound` i `player.jump.sound`.
if (yaml.contains("sound")) {
const auto& s = yaml["sound"];
if (s.contains("opening")) {
opening_sound_key_ = s["opening"].get_value<std::string>();
Resource::Cache::get().loadSound(opening_sound_key_, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + opening_sound_key_);
}
if (s.contains("opening_pitch_spread")) {
opening_pitch_spread_ = s["opening_pitch_spread"].get_value<float>();
}
}
} }
void Sarcophagus::place(int anchor_col, int anchor_row, const BlockedPredicate& is_blocked) { void Sarcophagus::place(int anchor_col, int anchor_row, const BlockedPredicate& is_blocked) {
@@ -122,6 +148,18 @@ void Sarcophagus::notifyTilePainted(int col, int row) {
state_ = State::OPENING; state_ = State::OPENING;
anim_ = anim_opening_; anim_ = anim_opening_;
if (anim_.has_value()) { anim_->reset(); } if (anim_.has_value()) { anim_->reset(); }
if (!opening_sound_key_.empty() && Audio::get() != nullptr) {
// Variació de pitch/velocitat per evitar la fatiga d'escoltar
// el mateix sample idèntic a cada obertura. Si l'spread és 0
// (default), reproduïm amb ratio 1.0; en cas contrari sortegem
// uniformement [1 - spread, 1 + spread].
if (opening_pitch_spread_ > 0.0F) {
std::uniform_real_distribution<float> dist(1.0F - opening_pitch_spread_, 1.0F + opening_pitch_spread_);
Audio::get()->playSound(opening_sound_key_, Audio::Group::GAME, dist(rng()));
} else {
Audio::get()->playSound(opening_sound_key_, Audio::Group::GAME);
}
}
} }
} }
@@ -2,6 +2,7 @@
#include <cstdint> #include <cstdint>
#include <functional> #include <functional>
#include <string>
#include <unordered_set> #include <unordered_set>
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
@@ -83,5 +84,12 @@ class Sarcophagus : public Interactable {
AnimationComponent anim_opening_; AnimationComponent anim_opening_;
AnimationComponent anim_open_; AnimationComponent anim_open_;
// Clau (basename del fitxer) al Resource::Cache del so que es dispara
// a la transició CLOSED → OPENING. Buit ⇒ no sona res.
std::string opening_sound_key_;
// Variació uniforme del ratio de reproducció al voltant de 1.0. Cada
// obertura sorteja [1 - spread, 1 + spread]. 0 ⇒ ratio fix a 1.0.
float opening_pitch_spread_{0.0F};
bool opened_event_emitted_{false}; bool opened_event_emitted_{false};
}; };
+3
View File
@@ -18,6 +18,9 @@ auto Audio::get() -> Audio* {
void Audio::playSound(const std::string& /*name*/, Audio::Group /*group*/) { void Audio::playSound(const std::string& /*name*/, Audio::Group /*group*/) {
} }
void Audio::playSound(const std::string& /*name*/, Audio::Group /*group*/, float /*speed*/) {
}
namespace Resource { namespace Resource {
auto Cache::get() -> Cache& { auto Cache::get() -> Cache& {
+13 -9
View File
@@ -166,9 +166,11 @@ namespace {
REQUIRE(sarc->getVisitedCount() == 8); REQUIRE(sarc->getVisitedCount() == 8);
REQUIRE(sarc->getState() == Sarcophagus::State::OPENING); REQUIRE(sarc->getState() == Sarcophagus::State::OPENING);
// Un update amb l'animació `opening` d'1 frame loop=false completa // Tickem prou per consumir l'animació `opening` (4 frames @ 6fps =
// la transició a OPEN. // ~0.67s). Quan completa, la transició OPENING → OPEN es dispara.
sarc->update(0.0F); for (int i = 0; i < 16 && sarc->getState() == Sarcophagus::State::OPENING; ++i) {
sarc->update(0.1F);
}
REQUIRE(sarc->getState() == Sarcophagus::State::OPEN); REQUIRE(sarc->getState() == Sarcophagus::State::OPEN);
} }
@@ -208,12 +210,14 @@ namespace {
forceOpen(s1); forceOpen(s1);
forceOpen(s2); forceOpen(s2);
// Un primer tick: cada sarc transiciona OPENING → OPEN i publica; // Tickem prou per consumir l'animació `opening` dels sarcs (4
// l'handler del manager compta i, en arribar al total, crida // frames @ 6fps) i la de la porta. Cada sarc transiciona
// door->open() — que en aquest mateix tick (la iteració sobre // OPENING → OPEN i publica; l'handler del manager compta i, en
// entities continua) farà transitar la porta OPENING → OPEN perquè // arribar al total, crida `door->open()`, que també necessita
// l'animació és d'1 frame loop=false. // tickar fins acabar la seua animació d'obertura.
manager.update(0.0F); for (int i = 0; i < 20 && door_ptr->getState() != Door::State::OPEN; ++i) {
manager.update(0.1F);
}
REQUIRE(manager.openedSarcophagusCount() == 2); REQUIRE(manager.openedSarcophagusCount() == 2);
REQUIRE(door_ptr->getState() == Door::State::OPEN); REQUIRE(door_ptr->getState() == Door::State::OPEN);