feat(title): música title.ogg amb fade-out, SFX player_start al START i bombament continu del stream sortint per evitar talls quan SDL drena a 2x
This commit is contained in:
1 parent
36fc496226
commit
921e10bbb3
10 files changed
+109
-1
No files matched your search
Binary file not shown.
Binary file not shown.
@@ -98,6 +98,9 @@ pause:
|
||||
sounds:
|
||||
menu_select: "menu_select.wav"
|
||||
menu_accept: "menu_accept.wav"
|
||||
# Disparat quan un jugador prem START a la TITLE (primera pulsació o
|
||||
# la del segon jugador dins de la finestra de STARTING).
|
||||
player_start: "player_start.wav"
|
||||
|
||||
# Escena TITLE (placeholder visual mentre no hi haja artwork).
|
||||
# `bg_color` i `text_color` són índexs sobre la paleta "ui" (uzebox.pal):
|
||||
@@ -129,6 +132,9 @@ title:
|
||||
# és la mida màxima de bloc en píxels al final del fade.
|
||||
fade_out_ms: 800
|
||||
fade_out_max_block: 32
|
||||
# Fade-out de `title.ogg` en entrar a STARTING. Independent del visual:
|
||||
# la música pot acabar abans o després del mosaic dissolve.
|
||||
music_fade_out_ms: 3000
|
||||
# Distorsió sinusoidal del fons (efecte de calor del desert).
|
||||
# `amplitude_px`: desplaçament màxim de cada fila en píxels.
|
||||
# `wavelength_px`: distància vertical entre dos pics de l'ona.
|
||||
|
||||
@@ -127,11 +127,44 @@ namespace Ja {
|
||||
void Engine::updateOutgoingFade() {
|
||||
if (outgoing_music_.stream == nullptr || !outgoing_music_.fade.active) { return; }
|
||||
|
||||
// Mentre la fosa està activa, mantenim el stream amb una reserva
|
||||
// de samples per davant del cursor (mateix patró que pumpMusic
|
||||
// per al current_music_). Així el stream no es buida ni quan SDL
|
||||
// drena més ràpid del previst en haver sounds bound a la mateixa
|
||||
// device. Si l'OGG arriba a EOF, rebobina (la fosa pot ser més
|
||||
// llarga que la pista).
|
||||
if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
|
||||
const Music& music = *outgoing_music_.music;
|
||||
const int BYTES_PER_SECOND = music.spec.freq * music.spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||
const int LOW_WATER = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));
|
||||
while (SDL_GetAudioStreamAvailable(outgoing_music_.stream) < LOW_WATER) {
|
||||
short chunk[MUSIC_CHUNK_SHORTS];
|
||||
const int SAMPLES = stb_vorbis_get_samples_short_interleaved(
|
||||
music.vorbis,
|
||||
music.spec.channels,
|
||||
static_cast<short*>(chunk),
|
||||
MUSIC_CHUNK_SHORTS);
|
||||
if (SAMPLES <= 0) {
|
||||
stb_vorbis_seek_start(music.vorbis);
|
||||
continue;
|
||||
}
|
||||
const int BYTES = SAMPLES * music.spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||
SDL_PutAudioStreamData(outgoing_music_.stream, static_cast<const void*>(chunk), BYTES);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
// Deixem el Vorbis del Music original en un estat conegut per
|
||||
// a la pròxima reproducció. (playMusic també fa seek_start,
|
||||
// però fer-ho ací evita estats intermedis si algú consulta.)
|
||||
if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
|
||||
stb_vorbis_seek_start(outgoing_music_.music->vorbis);
|
||||
}
|
||||
outgoing_music_.music = nullptr;
|
||||
} else {
|
||||
SDL_SetAudioStreamGain(outgoing_music_.stream, outgoing_music_.fade.initial_volume * (1.0F - PROGRESS));
|
||||
}
|
||||
@@ -191,7 +224,14 @@ namespace Ja {
|
||||
}
|
||||
|
||||
preFillOutgoing(current_music_, duration_ms);
|
||||
|
||||
outgoing_music_.stream = current_music_->stream;
|
||||
// Guardem la referència al Music perquè updateOutgoingFade puga
|
||||
// seguir bombant Vorbis cap al stream durant tota la fosa. NO fem
|
||||
// seek_start ací: la decompressió ha de continuar des d'on estava
|
||||
// perquè el so siga continu. El seek_start es farà quan la fosa
|
||||
// acabe (o quan playMusic la interrompi via stopMusic).
|
||||
outgoing_music_.music = current_music_;
|
||||
outgoing_music_.fade = {
|
||||
.active = true,
|
||||
.start_time = SDL_GetTicks(),
|
||||
@@ -200,7 +240,6 @@ namespace Ja {
|
||||
};
|
||||
current_music_->stream = nullptr;
|
||||
current_music_->state = MusicState::STOPPED;
|
||||
if (current_music_->vorbis != nullptr) { stb_vorbis_seek_start(current_music_->vorbis); }
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
@@ -265,6 +304,10 @@ namespace Ja {
|
||||
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||
outgoing_music_.stream = nullptr;
|
||||
outgoing_music_.fade.active = false;
|
||||
if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
|
||||
stb_vorbis_seek_start(outgoing_music_.music->vorbis);
|
||||
}
|
||||
outgoing_music_.music = nullptr;
|
||||
}
|
||||
incoming_fade_.active = false;
|
||||
|
||||
|
||||
@@ -125,6 +125,13 @@ namespace Ja {
|
||||
|
||||
struct OutgoingMusic {
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
// Referència al Music original perquè updateOutgoingFade puga
|
||||
// continuar descomprimint des de Vorbis cap al stream durant
|
||||
// tota la fosa. Sense això, només tenim el pre-fill puntual i
|
||||
// SDL drena el stream més ràpid del previst quan hi ha sounds
|
||||
// bound a la mateixa device (~2x), buidant-lo a meitat del
|
||||
// fade i sentint-se com un tall sec.
|
||||
Music* music{nullptr};
|
||||
FadeState fade;
|
||||
};
|
||||
|
||||
|
||||
@@ -153,6 +153,9 @@ Director::Director() {
|
||||
Resource::Cache::get().loadMusic("room_intro.ogg", "data/music/room_intro.ogg");
|
||||
Resource::Cache::get().loadMusic("playing.ogg", "data/music/playing.ogg");
|
||||
Resource::Cache::get().loadMusic("room_outro.ogg", "data/music/room_outro.ogg");
|
||||
// Música de la TITLE: arrenca al constructor de l'escena i es fa
|
||||
// fade-out quan l'escena entra a STARTING.
|
||||
Resource::Cache::get().loadMusic("title.ogg", "data/music/title.ogg");
|
||||
|
||||
// Sons del menú de servei. Els noms venen de `data/ui.yaml` (amb
|
||||
// fallback a `Defaults::UI::MENU_*_SOUND`); el ServiceMenu els
|
||||
@@ -161,6 +164,10 @@ Director::Director() {
|
||||
const auto& menu_accept = UiConfig::get().menuAcceptSound();
|
||||
Resource::Cache::get().loadSound(menu_select, "data/sounds/" + menu_select);
|
||||
Resource::Cache::get().loadSound(menu_accept, "data/sounds/" + menu_accept);
|
||||
// So del START a la TITLE: el dispara TitleScene quan un jugador
|
||||
// prem START (vore enterStarting i el reset del segon jugador).
|
||||
const auto& player_start = UiConfig::get().playerStartSound();
|
||||
Resource::Cache::get().loadSound(player_start, "data/sounds/" + player_start);
|
||||
|
||||
// Catàleg de presets d'efectes per als sons. Es carrega abans d'Audio
|
||||
// perquè els primers playSoundWith* (al començament del joc) ja
|
||||
|
||||
@@ -106,6 +106,7 @@ UiConfig::UiConfig()
|
||||
title_starting_blink_period_ms_(Defaults::UI::Title::STARTING_BLINK_PERIOD_MS),
|
||||
title_fade_out_ms_(Defaults::UI::Title::FADE_OUT_MS),
|
||||
title_fade_out_max_block_(Defaults::UI::Title::FADE_OUT_MAX_BLOCK),
|
||||
title_music_fade_out_ms_(Defaults::UI::Title::MUSIC_FADE_OUT_MS),
|
||||
title_heat_haze_{
|
||||
.enabled = Defaults::UI::Title::HeatHaze::ENABLED,
|
||||
.amplitude_px = Defaults::UI::Title::HeatHaze::AMPLITUDE_PX,
|
||||
@@ -114,6 +115,7 @@ UiConfig::UiConfig()
|
||||
},
|
||||
menu_select_sound_(Defaults::UI::MENU_SELECT_SOUND),
|
||||
menu_accept_sound_(Defaults::UI::MENU_ACCEPT_SOUND),
|
||||
player_start_sound_(Defaults::UI::PLAYER_START_SOUND),
|
||||
hud_font_{.name = Defaults::UI::Font::HUD_NAME, .gif = Defaults::UI::Font::HUD_GIF, .fnt = Defaults::UI::Font::HUD_FNT, .kerning = Defaults::UI::Font::HUD_KERNING},
|
||||
placeholder_font_{.name = Defaults::UI::Font::PLACEHOLDER_NAME, .gif = Defaults::UI::Font::PLACEHOLDER_GIF, .fnt = Defaults::UI::Font::PLACEHOLDER_FNT, .kerning = Defaults::UI::Font::PLACEHOLDER_KERNING},
|
||||
room_text_font_{.name = Defaults::UI::Font::ROOM_TEXT_NAME, .gif = Defaults::UI::Font::ROOM_TEXT_GIF, .fnt = Defaults::UI::Font::ROOM_TEXT_FNT, .kerning = Defaults::UI::Font::ROOM_TEXT_KERNING},
|
||||
@@ -218,6 +220,7 @@ void UiConfig::load(const std::string& file_path) {
|
||||
const auto& s = yaml["sounds"];
|
||||
readField(s, "menu_select", menu_select_sound_);
|
||||
readField(s, "menu_accept", menu_accept_sound_);
|
||||
readField(s, "player_start", player_start_sound_);
|
||||
}
|
||||
|
||||
if (yaml.contains("title")) {
|
||||
@@ -232,6 +235,7 @@ void UiConfig::load(const std::string& file_path) {
|
||||
readField(t, "starting_blink_period_ms", title_starting_blink_period_ms_);
|
||||
readField(t, "fade_out_ms", title_fade_out_ms_);
|
||||
readField(t, "fade_out_max_block", title_fade_out_max_block_);
|
||||
readField(t, "music_fade_out_ms", title_music_fade_out_ms_);
|
||||
if (t.contains("heat_haze")) {
|
||||
const auto& hh = t["heat_haze"];
|
||||
readField(hh, "enabled", title_heat_haze_.enabled);
|
||||
|
||||
@@ -99,6 +99,7 @@ class UiConfig {
|
||||
[[nodiscard]] auto titleStartingBlinkPeriodMs() const -> int { return title_starting_blink_period_ms_; }
|
||||
[[nodiscard]] auto titleFadeOutMs() const -> int { return title_fade_out_ms_; }
|
||||
[[nodiscard]] auto titleFadeOutMaxBlock() const -> int { return title_fade_out_max_block_; }
|
||||
[[nodiscard]] auto titleMusicFadeOutMs() const -> int { return title_music_fade_out_ms_; }
|
||||
[[nodiscard]] auto titleHeatHaze() const -> const TitleHeatHaze& { return title_heat_haze_; }
|
||||
|
||||
// Corbes d'easing triades al YAML. Si el nom no és conegut, s'usa
|
||||
@@ -116,6 +117,7 @@ class UiConfig {
|
||||
// fitxers viuen a `data/sounds/`).
|
||||
[[nodiscard]] auto menuSelectSound() const -> const std::string& { return menu_select_sound_; }
|
||||
[[nodiscard]] auto menuAcceptSound() const -> const std::string& { return menu_accept_sound_; }
|
||||
[[nodiscard]] auto playerStartSound() const -> const std::string& { return player_start_sound_; }
|
||||
|
||||
// Fonts de UI.
|
||||
[[nodiscard]] auto hudFont() const -> const Font& { return hud_font_; }
|
||||
@@ -175,10 +177,12 @@ class UiConfig {
|
||||
int title_starting_blink_period_ms_;
|
||||
int title_fade_out_ms_;
|
||||
int title_fade_out_max_block_;
|
||||
int title_music_fade_out_ms_;
|
||||
TitleHeatHaze title_heat_haze_;
|
||||
|
||||
std::string menu_select_sound_;
|
||||
std::string menu_accept_sound_;
|
||||
std::string player_start_sound_;
|
||||
|
||||
Font hud_font_;
|
||||
Font placeholder_font_;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/input/gamepad_bindings.hpp"
|
||||
#include "core/input/gamepad_manager.hpp"
|
||||
#include "core/input/global_inputs.hpp"
|
||||
@@ -69,6 +70,15 @@ namespace {
|
||||
return font.getLineHeight();
|
||||
}
|
||||
|
||||
// Reproducció del so de START. Centralitzat ací per disparar-lo des
|
||||
// dels punts on una pulsació té efecte real (entrada a STARTING o
|
||||
// upgrade a BOTH); l'attract timeout no l'ha de tocar.
|
||||
void playStartSound() {
|
||||
if (auto* audio = Audio::get(); audio != nullptr) {
|
||||
audio->playSound(UiConfig::get().playerStartSound(), Audio::Group::INTERFACE);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TitleScene::TitleScene(std::shared_ptr<Text> display_font, std::shared_ptr<Text> condensed_font, std::shared_ptr<Surface> background, std::shared_ptr<Surface> mini_logo, SceneFactory next_factory)
|
||||
@@ -90,6 +100,13 @@ TitleScene::TitleScene(std::shared_ptr<Text> display_font, std::shared_ptr<Text>
|
||||
fade_out_.emplace(Transitions::Mosaic::Params{
|
||||
.max_block_size = UiConfig::get().titleFadeOutMaxBlock(),
|
||||
});
|
||||
|
||||
// Música de fons en bucle. Es preloda al Director, així que la
|
||||
// primera crida és immediata. El fade-out arrenca en entrar a
|
||||
// STARTING (vore enterStarting).
|
||||
if (auto* audio = Audio::get(); audio != nullptr) {
|
||||
audio->playMusic("title.ogg", -1);
|
||||
}
|
||||
}
|
||||
|
||||
TitleScene::~TitleScene() = default;
|
||||
@@ -117,8 +134,10 @@ void TitleScene::update(int delta_ms) {
|
||||
case Phase::IDLE: {
|
||||
timer_ms_ -= delta_ms;
|
||||
if (P1_START) {
|
||||
playStartSound();
|
||||
enterStarting(DebugConfig::StartingPlayers::P1);
|
||||
} else if (P2_START) {
|
||||
playStartSound();
|
||||
enterStarting(DebugConfig::StartingPlayers::P2);
|
||||
} else if (timer_ms_ <= 0) {
|
||||
// Attract: ningú ha premut. Entrem a STARTING amb BOTH
|
||||
@@ -140,6 +159,7 @@ void TitleScene::update(int delta_ms) {
|
||||
(pending_players_ == DebugConfig::StartingPlayers::P2 && P1_START);
|
||||
const bool FADE_LOCKED = fade_out_ && (fade_out_->isActive() || fade_out_->isFinished());
|
||||
if (OTHER_PRESSED && !FADE_LOCKED) {
|
||||
playStartSound();
|
||||
pending_players_ = DebugConfig::StartingPlayers::BOTH;
|
||||
starting_timer_ms_ = UiConfig::get().titleStartingMs();
|
||||
}
|
||||
@@ -172,6 +192,12 @@ void TitleScene::enterStarting(DebugConfig::StartingPlayers players) {
|
||||
// STARTING comenci visible — sense això, el modulus amb el nou
|
||||
// període podria caure mig cicle i donar un primer "off" estrany.
|
||||
blink_elapsed_ms_ = 0;
|
||||
// Fade-out de la música. Si l'altre jugador prem START i el timer
|
||||
// es reseteja, no rearranquem la música: es manté la fosa iniciada
|
||||
// ací (consistent amb el fade visual, que tampoc es reverteix).
|
||||
if (auto* audio = Audio::get(); audio != nullptr) {
|
||||
audio->fadeOutMusic(UiConfig::get().titleMusicFadeOutMs());
|
||||
}
|
||||
std::cout << "[scene] " << SCENE_NAME << " STARTING (players=" << DebugConfig::toString(players) << ")\n";
|
||||
}
|
||||
|
||||
|
||||
@@ -245,6 +245,12 @@ namespace Defaults {
|
||||
constexpr const char* MENU_SELECT_SOUND = "menu_select.wav";
|
||||
constexpr const char* MENU_ACCEPT_SOUND = "menu_accept.wav";
|
||||
|
||||
// So disparat quan un jugador prem START a la TITLE (tant la
|
||||
// primera pulsació que entra a STARTING com la del segon
|
||||
// jugador que reseteja a BOTH dins de la finestra). Fitxer a
|
||||
// `data/sounds/`.
|
||||
constexpr const char* PLAYER_START_SOUND = "player_start.wav";
|
||||
|
||||
// Escena TITLE (provisional fins que tinguem artwork). Colors
|
||||
// referits a la paleta "ui" (uzebox.pal): BG_COLOR és el to més
|
||||
// proper al negre sense ser-ho (índex 1 = 36,0,0); TEXT_COLOR és
|
||||
@@ -276,6 +282,11 @@ namespace Defaults {
|
||||
constexpr int FADE_OUT_MS = 800;
|
||||
constexpr int FADE_OUT_MAX_BLOCK = 32;
|
||||
|
||||
// Fade-out de la música de la TITLE en entrar a STARTING.
|
||||
// Independent del visual: no cal que coincidisca amb FADE_OUT_MS
|
||||
// (la música pot acabar abans/després del mosaic).
|
||||
constexpr int MUSIC_FADE_OUT_MS = 2000;
|
||||
|
||||
// Distorsió sinusoidal horitzontal de l'imatge de fons
|
||||
// (efecte de calor del desert). `amplitude_px` és el
|
||||
// desplaçament màxim per fila; `wavelength_px` la distància
|
||||
|
||||
Reference in new issue
Block a user