#include "audio/footstep_manager.hpp" #include "pipeline/asset_manager.hpp" #include "core/logger.hpp" #include #include #include #include #include #include #include namespace wowee { namespace audio { namespace { std::vector buildClassicFootstepSet(const std::string& material) { std::vector 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 buildAltFootstepSet(const std::string& folder, const std::string& stem) { std::vector 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(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& candidates) { if (!assetManager) { return; } auto& list = surfaces[static_cast(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() { if (playerPid > 0) { kill(-playerPid, SIGTERM); kill(playerPid, SIGTERM); int status = 0; waitpid(playerPid, &status, 0); playerPid = -1; } } void FootstepManager::reapFinishedProcess() { if (playerPid <= 0) { return; } int status = 0; pid_t result = waitpid(playerPid, &status, WNOHANG); if (result == playerPid) { playerPid = -1; } } bool FootstepManager::playRandomStep(FootstepSurface surface, bool sprinting) { auto now = std::chrono::steady_clock::now(); if (lastPlayTime.time_since_epoch().count() != 0) { float elapsed = std::chrono::duration(now - lastPlayTime).count(); float minInterval = sprinting ? 0.09f : 0.14f; if (elapsed < minInterval) { return false; } } auto& list = surfaces[static_cast(surface)].clips; if (list.empty()) { list = surfaces[static_cast(FootstepSurface::STONE)].clips; if (list.empty()) { return false; } } // Keep one active step at a time to avoid ffplay process buildup. if (playerPid > 0) { return false; } std::uniform_int_distribution 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(sample.data.data()), static_cast(sample.data.size())); out.close(); // Subtle variation for less repetitive cadence. std::uniform_real_distribution pitchDist(0.97f, 1.05f); std::uniform_real_distribution 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); pid_t pid = fork(); if (pid == 0) { setpgid(0, 0); FILE* outFile = freopen("/dev/null", "w", stdout); FILE* errFile = freopen("/dev/null", "w", stderr); (void)outFile; (void)errFile; execlp("ffplay", "ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", "-af", filter.c_str(), tempFilePath.c_str(), nullptr); _exit(1); } else if (pid > 0) { playerPid = pid; lastPlayTime = now; 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