2026-02-03 14:55:32 -08:00
|
|
|
#include "audio/footstep_manager.hpp"
|
|
|
|
|
#include "pipeline/asset_manager.hpp"
|
|
|
|
|
#include "core/logger.hpp"
|
2026-02-03 22:24:17 -08:00
|
|
|
#include "platform/process.hpp"
|
2026-02-03 14:55:32 -08:00
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
#include <fstream>
|
|
|
|
|
#include <string>
|
|
|
|
|
|
|
|
|
|
namespace wowee {
|
|
|
|
|
namespace audio {
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> buildClassicFootstepSet(const std::string& material) {
|
|
|
|
|
std::vector<std::string> out;
|
|
|
|
|
for (char c = 'A'; c <= 'L'; ++c) {
|
|
|
|
|
out.push_back("Sound\\Character\\Footsteps\\mFootMediumLarge" + material + std::string(1, c) + ".wav");
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> buildAltFootstepSet(const std::string& folder, const std::string& stem) {
|
|
|
|
|
std::vector<std::string> out;
|
|
|
|
|
for (int i = 1; i <= 8; ++i) {
|
|
|
|
|
char index[4];
|
|
|
|
|
std::snprintf(index, sizeof(index), "%02d", i);
|
|
|
|
|
out.push_back("Sound\\Character\\Footsteps\\" + folder + "\\" + stem + "_" + index + ".wav");
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
FootstepManager::FootstepManager() : rng(std::random_device{}()) {}
|
|
|
|
|
|
|
|
|
|
FootstepManager::~FootstepManager() {
|
|
|
|
|
shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool FootstepManager::initialize(pipeline::AssetManager* assets) {
|
|
|
|
|
assetManager = assets;
|
|
|
|
|
sampleCount = 0;
|
|
|
|
|
for (auto& surface : surfaces) {
|
|
|
|
|
surface.clips.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!assetManager) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
preloadSurface(FootstepSurface::STONE, buildClassicFootstepSet("Stone"));
|
|
|
|
|
preloadSurface(FootstepSurface::DIRT, buildClassicFootstepSet("Dirt"));
|
|
|
|
|
preloadSurface(FootstepSurface::GRASS, buildClassicFootstepSet("Grass"));
|
|
|
|
|
preloadSurface(FootstepSurface::WOOD, buildClassicFootstepSet("Wood"));
|
|
|
|
|
preloadSurface(FootstepSurface::SNOW, buildClassicFootstepSet("Snow"));
|
|
|
|
|
preloadSurface(FootstepSurface::WATER, buildClassicFootstepSet("Water"));
|
|
|
|
|
|
|
|
|
|
// Alternate naming seen in some builds (especially metals).
|
|
|
|
|
preloadSurface(FootstepSurface::METAL, buildAltFootstepSet("MediumLargeMetalFootsteps", "MediumLargeFootstepMetal"));
|
|
|
|
|
if (surfaces[static_cast<size_t>(FootstepSurface::METAL)].clips.empty()) {
|
|
|
|
|
preloadSurface(FootstepSurface::METAL, buildClassicFootstepSet("Metal"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Footstep manager initialized (", sampleCount, " clips)");
|
|
|
|
|
return sampleCount > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FootstepManager::shutdown() {
|
|
|
|
|
stopCurrentProcess();
|
|
|
|
|
std::remove(tempFilePath.c_str());
|
|
|
|
|
for (auto& surface : surfaces) {
|
|
|
|
|
surface.clips.clear();
|
|
|
|
|
}
|
|
|
|
|
sampleCount = 0;
|
|
|
|
|
assetManager = nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FootstepManager::update(float) {
|
|
|
|
|
reapFinishedProcess();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FootstepManager::playFootstep(FootstepSurface surface, bool sprinting) {
|
|
|
|
|
if (!assetManager || sampleCount == 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
reapFinishedProcess();
|
|
|
|
|
playRandomStep(surface, sprinting);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FootstepManager::preloadSurface(FootstepSurface surface, const std::vector<std::string>& candidates) {
|
|
|
|
|
if (!assetManager) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto& list = surfaces[static_cast<size_t>(surface)].clips;
|
|
|
|
|
for (const std::string& path : candidates) {
|
|
|
|
|
if (!assetManager->fileExists(path)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
auto data = assetManager->readFile(path);
|
|
|
|
|
if (data.empty()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
list.push_back({path, std::move(data)});
|
|
|
|
|
sampleCount++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!list.empty()) {
|
|
|
|
|
LOG_INFO("Footsteps ", surfaceName(surface), ": loaded ", list.size(), " clips");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FootstepManager::stopCurrentProcess() {
|
2026-02-03 22:24:17 -08:00
|
|
|
platform::killProcess(playerPid);
|
2026-02-03 14:55:32 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FootstepManager::reapFinishedProcess() {
|
2026-02-03 22:24:17 -08:00
|
|
|
if (playerPid == INVALID_PROCESS) {
|
2026-02-03 14:55:32 -08:00
|
|
|
return;
|
|
|
|
|
}
|
2026-02-03 22:24:17 -08:00
|
|
|
platform::isProcessRunning(playerPid);
|
2026-02-03 14:55:32 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool FootstepManager::playRandomStep(FootstepSurface surface, bool sprinting) {
|
2026-02-03 17:21:04 -08:00
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
|
if (lastPlayTime.time_since_epoch().count() != 0) {
|
|
|
|
|
float elapsed = std::chrono::duration<float>(now - lastPlayTime).count();
|
|
|
|
|
float minInterval = sprinting ? 0.09f : 0.14f;
|
|
|
|
|
if (elapsed < minInterval) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 14:55:32 -08:00
|
|
|
auto& list = surfaces[static_cast<size_t>(surface)].clips;
|
|
|
|
|
if (list.empty()) {
|
|
|
|
|
list = surfaces[static_cast<size_t>(FootstepSurface::STONE)].clips;
|
|
|
|
|
if (list.empty()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keep one active step at a time to avoid ffplay process buildup.
|
2026-02-03 22:24:17 -08:00
|
|
|
if (playerPid != INVALID_PROCESS) {
|
2026-02-03 14:55:32 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::uniform_int_distribution<size_t> clipDist(0, list.size() - 1);
|
|
|
|
|
const Sample& sample = list[clipDist(rng)];
|
|
|
|
|
|
|
|
|
|
std::ofstream out(tempFilePath, std::ios::binary);
|
|
|
|
|
if (!out) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
out.write(reinterpret_cast<const char*>(sample.data.data()), static_cast<std::streamsize>(sample.data.size()));
|
|
|
|
|
out.close();
|
|
|
|
|
|
|
|
|
|
// Subtle variation for less repetitive cadence.
|
|
|
|
|
std::uniform_real_distribution<float> pitchDist(0.97f, 1.05f);
|
|
|
|
|
std::uniform_real_distribution<float> volumeDist(0.92f, 1.00f);
|
|
|
|
|
float pitch = pitchDist(rng);
|
|
|
|
|
float volume = volumeDist(rng) * (sprinting ? 1.0f : 0.88f);
|
|
|
|
|
if (volume > 1.0f) volume = 1.0f;
|
|
|
|
|
if (volume < 0.1f) volume = 0.1f;
|
|
|
|
|
|
|
|
|
|
std::string filter = "asetrate=44100*" + std::to_string(pitch) +
|
|
|
|
|
",aresample=44100,volume=" + std::to_string(volume);
|
|
|
|
|
|
2026-02-03 22:24:17 -08:00
|
|
|
playerPid = platform::spawnProcess({
|
|
|
|
|
"-nodisp", "-autoexit", "-loglevel", "quiet",
|
|
|
|
|
"-af", filter, tempFilePath
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (playerPid != INVALID_PROCESS) {
|
2026-02-03 17:21:04 -08:00
|
|
|
lastPlayTime = now;
|
2026-02-03 14:55:32 -08:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char* FootstepManager::surfaceName(FootstepSurface surface) {
|
|
|
|
|
switch (surface) {
|
|
|
|
|
case FootstepSurface::STONE: return "stone";
|
|
|
|
|
case FootstepSurface::DIRT: return "dirt";
|
|
|
|
|
case FootstepSurface::GRASS: return "grass";
|
|
|
|
|
case FootstepSurface::WOOD: return "wood";
|
|
|
|
|
case FootstepSurface::METAL: return "metal";
|
|
|
|
|
case FootstepSurface::WATER: return "water";
|
|
|
|
|
case FootstepSurface::SNOW: return "snow";
|
|
|
|
|
default: return "unknown";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace audio
|
|
|
|
|
} // namespace wowee
|