Files
ReSkate/Extension/BoardBreak/board_break_audio.cpp
T
Fourth Studio b999f37e7b Board Break: wear and tear, rail snaps, break and body sounds, settings
Boards wear: hard landings, hits onto rails and ledges, grinding and
slams wear the board of the deck in the game, kept per deck. From a fifth
worn it steers lazier and its trucks go soft (the game's own physics
tuning, outside multiplayer sessions only); a worn board breaks from less
and, worn out, can give way under the rider. A board that broke is a new
one once it is whole again, and the game's own board wear is cleared with
it.

A boardslide, lipslide or darkslide that comes down too hard onto a rail
can snap the board, and the rider goes down with it: No Bail can now be
asked for a wipeout, which goes in as a cause before the selector picks.

Sounds play through XAudio2: a crack by the size of the break, metal when
the board broke on metal (the material comes from the game's contact
effects; which are metal is marked in play or listed by a pack), and the
body's hits in a bail by how hard they land.

The settings are a card on the overlay's Skater page, two rows in Mod
Options and console verbs, kept in BoardBreak.settings. A deck may name
itself and set its wheels wider apart in a deck.ini; Old School and Pig
join the decks the feature knows.
2026-10-15 19:22:21 +02:00

167 lines
6.2 KiB
C++

#include "board_break_audio.h"
#include "Engine/Core/Log/logging.h"
#include <Windows.h>
#include <mmsystem.h>
#include <xaudio2.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <condition_variable>
#include <cstring>
#include <deque>
#include <fstream>
#include <mutex>
#include <random>
#include <string>
#include <thread>
#pragma comment(lib, "xaudio2.lib")
#pragma comment(lib, "winmm.lib")
#pragma comment(lib, "ole32.lib")
namespace dingosdk::board_break::audio {
namespace {
// One wave file, read whole: its format as the file gives it and its samples.
struct Sound {
std::filesystem::path file;
std::vector<std::uint8_t> format; // WAVEFORMATEX, or the larger WAVEFORMATEXTENSIBLE
std::vector<std::uint8_t> samples;
};
// A RIFF WAVE file's "fmt " and "data" chunks; false for anything else.
bool read_wave(const std::filesystem::path& file, Sound& out) {
std::ifstream in(file, std::ios::binary);
std::vector<std::uint8_t> bytes((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
if (bytes.size() < 12 || std::memcmp(bytes.data(), "RIFF", 4) || std::memcmp(bytes.data() + 8, "WAVE", 4)) return false;
std::size_t at = 12;
while (at + 8 <= bytes.size()) {
std::uint32_t size{};
std::memcpy(&size, bytes.data() + at + 4, 4);
const auto body = at + 8;
if (body + size > bytes.size()) size = static_cast<std::uint32_t>(bytes.size() - body);
if (!std::memcmp(bytes.data() + at, "fmt ", 4) && size >= 16) out.format.assign(bytes.begin() + body, bytes.begin() + body + size);
else if (!std::memcmp(bytes.data() + at, "data", 4)) out.samples.assign(bytes.begin() + body, bytes.begin() + body + size);
at = body + size + (size & 1);
}
if (out.format.size() < 16 || out.samples.empty()) return false;
std::uint16_t tag{};
std::memcpy(&tag, out.format.data(), 2);
if (tag != WAVE_FORMAT_PCM && tag != WAVE_FORMAT_IEEE_FLOAT && tag != WAVE_FORMAT_EXTENSIBLE) return false;
// XAudio2 reads a whole WAVEFORMATEX (cbSize included).
if (out.format.size() < sizeof(WAVEFORMATEX)) out.format.resize(sizeof(WAVEFORMATEX), 0);
out.file = file;
return true;
}
struct Request {
Kind kind{};
float volume{};
};
struct Engine {
std::mutex mutex;
std::condition_variable wake;
std::deque<Request> queue;
std::array<std::vector<Sound>, kinds> sounds;
std::array<bool, kinds> loaded{};
std::thread worker;
std::atomic<bool> started{}, failed{};
std::mt19937 random{std::random_device{}()};
};
Engine& engine() {
static auto* value = new Engine;
return *value;
}
void run(Engine& e) {
const auto com = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
IXAudio2* xaudio{};
IXAudio2MasteringVoice* master{};
if (FAILED(XAudio2Create(&xaudio, 0, XAUDIO2_DEFAULT_PROCESSOR)) || FAILED(xaudio->CreateMasteringVoice(&master))) {
if (xaudio) xaudio->Release();
xaudio = nullptr;
e.failed.store(true, std::memory_order_release);
logging::log(logging::Level::warning, logging::Channel::skater,
"Board Break: no sound device for its own sounds; the cracks play one at a time.");
}
std::vector<IXAudio2SourceVoice*> voices;
for (;;) {
Request request;
const Sound* sound{};
{
std::unique_lock lock(e.mutex);
e.wake.wait(lock, [&] { return !e.queue.empty(); });
request = e.queue.front();
e.queue.pop_front();
auto& list = e.sounds[static_cast<std::size_t>(request.kind)];
if (list.empty()) continue;
sound = &list[std::uniform_int_distribution<std::size_t>(0, list.size() - 1)(e.random)];
}
if (!xaudio) {
// Without XAudio2: the cracks still sound, one at a time.
if (request.kind <= Kind::heavy) (void)PlaySoundW(sound->file.c_str(), nullptr, SND_FILENAME | SND_ASYNC | SND_NODEFAULT);
continue;
}
// Voices whose sound has played out go; at most a couple of dozen at once.
std::erase_if(voices, [](IXAudio2SourceVoice* voice) {
XAUDIO2_VOICE_STATE state{};
voice->GetState(&state, XAUDIO2_VOICE_NOSAMPLESPLAYED);
if (state.BuffersQueued) return false;
voice->DestroyVoice();
return true;
});
if (voices.size() >= 24) {
voices.front()->DestroyVoice();
voices.erase(voices.begin());
}
IXAudio2SourceVoice* voice{};
if (FAILED(xaudio->CreateSourceVoice(&voice, reinterpret_cast<const WAVEFORMATEX*>(sound->format.data())))) continue;
XAUDIO2_BUFFER buffer{};
buffer.AudioBytes = static_cast<UINT32>(sound->samples.size());
buffer.pAudioData = sound->samples.data();
buffer.Flags = XAUDIO2_END_OF_STREAM;
voice->SetVolume(std::clamp(request.volume, 0.0f, 1.0f));
if (FAILED(voice->SubmitSourceBuffer(&buffer)) || FAILED(voice->Start())) {
voice->DestroyVoice();
continue;
}
voices.push_back(voice);
}
(void)com;
}
}
void load(Kind kind, const std::vector<std::filesystem::path>& files) noexcept {
auto& e = engine();
std::vector<Sound> read;
for (const auto& file : files) {
try {
Sound sound;
if (read_wave(file, sound)) read.push_back(std::move(sound));
else logging::log(logging::Level::warning, logging::Channel::skater, "Board Break: {} is not a wave file it can play.",
file.filename().string());
} catch (...) {}
}
std::lock_guard lock(e.mutex);
const auto at = static_cast<std::size_t>(kind);
if (e.loaded[at]) return;
e.loaded[at] = true;
e.sounds[at] = std::move(read);
}
bool has(Kind kind) noexcept {
auto& e = engine();
std::lock_guard lock(e.mutex);
return !e.sounds[static_cast<std::size_t>(kind)].empty();
}
void play(Kind kind, float volume) noexcept {
auto& e = engine();
try {
if (!e.started.exchange(true)) {
e.worker = std::thread([&e] { run(e); });
e.worker.detach();
}
std::lock_guard lock(e.mutex);
if (e.queue.size() < 32) e.queue.push_back({kind, volume});
} catch (...) { return; }
e.wake.notify_one();
}
}