From 0fed931aa0a2bfccf8d87a7f185b99db1a4a8700 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 9 Feb 2026 01:19:35 -0800 Subject: [PATCH] Implement mount ambient sounds Added full mount sound system with: - Wing flap sounds for flying mounts (gryphon/wyvern) when moving - Wing idle/hovering sounds when stationary in air - Breathing/snorting sounds for ground mounts when idle - Occasional whinny sounds for ground mounts when moving Sounds are loaded from MPQ files and played via AudioEngine with randomized pitch/volume variation. Mount sound manager tracks mount type, movement state, and flying state to play appropriate ambient sounds at natural intervals. Updated setMounted() to accept creature display ID and notify the mount sound manager, which uses display ID ranges to detect mount type (flying vs ground). --- include/audio/mount_sound_manager.hpp | 17 +++ include/rendering/renderer.hpp | 2 +- src/audio/mount_sound_manager.cpp | 199 +++++++++++++++++++++++--- src/core/application.cpp | 2 +- src/rendering/renderer.cpp | 24 +++- 5 files changed, 221 insertions(+), 23 deletions(-) diff --git a/include/audio/mount_sound_manager.hpp b/include/audio/mount_sound_manager.hpp index 5a586379..6a603e89 100644 --- a/include/audio/mount_sound_manager.hpp +++ b/include/audio/mount_sound_manager.hpp @@ -2,6 +2,8 @@ #include #include +#include +#include namespace wowee { namespace pipeline { class AssetManager; } @@ -15,6 +17,11 @@ enum class MountType { SWIMMING // Sea turtle, etc. }; +struct MountSample { + std::string path; + std::vector data; +}; + class MountSoundManager { public: MountSoundManager(); @@ -40,6 +47,8 @@ private: MountType detectMountType(uint32_t creatureDisplayId) const; void updateMountSounds(); void stopAllMountSounds(); + void loadMountSounds(); + bool loadSound(const std::string& path, MountSample& sample); pipeline::AssetManager* assetManager_ = nullptr; bool mounted_ = false; @@ -49,9 +58,17 @@ private: uint32_t currentDisplayId_ = 0; float volumeScale_ = 1.0f; + // Mount sound samples (loaded from MPQ) + std::vector wingFlapSounds_; + std::vector wingIdleSounds_; + std::vector horseBreathSounds_; + std::vector horseMoveSounds_; + // Sound state tracking bool playingMovementSound_ = false; bool playingIdleSound_ = false; + std::chrono::steady_clock::time_point lastSoundUpdate_; + float soundLoopTimer_ = 0.0f; }; } // namespace audio diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 69f65ae9..a4222822 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -127,7 +127,7 @@ public: void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId = 0; } // Mount rendering - void setMounted(uint32_t mountInstId, float heightOffset); + void setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset); void setTaxiFlight(bool onTaxi) { taxiFlight_ = onTaxi; } void setMountPitchRoll(float pitch, float roll) { mountPitch_ = pitch; mountRoll_ = roll; } void clearMount(); diff --git a/src/audio/mount_sound_manager.cpp b/src/audio/mount_sound_manager.cpp index 85908d7a..27671755 100644 --- a/src/audio/mount_sound_manager.cpp +++ b/src/audio/mount_sound_manager.cpp @@ -2,11 +2,14 @@ #include "audio/audio_engine.hpp" #include "pipeline/asset_manager.hpp" #include "core/logger.hpp" +#include namespace wowee { namespace audio { -MountSoundManager::MountSoundManager() = default; +MountSoundManager::MountSoundManager() { + lastSoundUpdate_ = std::chrono::steady_clock::now(); +} MountSoundManager::~MountSoundManager() { shutdown(); @@ -14,23 +17,125 @@ MountSoundManager::~MountSoundManager() { bool MountSoundManager::initialize(pipeline::AssetManager* assets) { assetManager_ = assets; - LOG_INFO("Mount sound manager initialized"); + if (!assetManager_) { + LOG_WARNING("Mount sound manager: no asset manager"); + return false; + } + + loadMountSounds(); + + int totalSamples = wingFlapSounds_.size() + wingIdleSounds_.size() + + horseBreathSounds_.size() + horseMoveSounds_.size(); + LOG_INFO("Mount sound manager initialized (", totalSamples, " clips)"); return true; } void MountSoundManager::shutdown() { stopAllMountSounds(); mounted_ = false; + wingFlapSounds_.clear(); + wingIdleSounds_.clear(); + horseBreathSounds_.clear(); + horseMoveSounds_.clear(); assetManager_ = nullptr; } -void MountSoundManager::update(float deltaTime) { - (void)deltaTime; +void MountSoundManager::loadMountSounds() { + if (!assetManager_) return; + // Flying mount wing flaps (movement) + std::vector wingFlapPaths = { + "Sound\\Creature\\Gryphon\\GryphonWingFlap1.wav", + "Sound\\Creature\\Gryphon\\GryphonWingFlap2.wav", + "Sound\\Creature\\Gryphon\\GryphonWingFlap3.wav", + "Sound\\Creature\\WindRider\\WindRiderWingFlap1.wav", + "Sound\\Creature\\WindRider\\WindRiderWingFlap2.wav", + }; + + for (const auto& path : wingFlapPaths) { + MountSample sample; + if (loadSound(path, sample)) { + wingFlapSounds_.push_back(std::move(sample)); + } + } + + // Flying mount idle/hovering + std::vector wingIdlePaths = { + "Sound\\Creature\\Gryphon\\GryphonIdle1.wav", + "Sound\\Creature\\Gryphon\\GryphonIdle2.wav", + "Sound\\Creature\\WindRider\\WindRiderIdle1.wav", + }; + + for (const auto& path : wingIdlePaths) { + MountSample sample; + if (loadSound(path, sample)) { + wingIdleSounds_.push_back(std::move(sample)); + } + } + + // Ground mount breathing/idle + std::vector horseBreathPaths = { + "Sound\\Creature\\Horse\\HorseBreath1.wav", + "Sound\\Creature\\Horse\\HorseBreath2.wav", + "Sound\\Creature\\Horse\\HorseSnort1.wav", + }; + + for (const auto& path : horseBreathPaths) { + MountSample sample; + if (loadSound(path, sample)) { + horseBreathSounds_.push_back(std::move(sample)); + } + } + + // Ground mount movement ambient + std::vector horseMovePaths = { + "Sound\\Creature\\Horse\\HorseWhinny1.wav", + "Sound\\Creature\\Horse\\HorseWhinny2.wav", + }; + + for (const auto& path : horseMovePaths) { + MountSample sample; + if (loadSound(path, sample)) { + horseMoveSounds_.push_back(std::move(sample)); + } + } + + if (!wingFlapSounds_.empty()) { + LOG_INFO("Loaded ", wingFlapSounds_.size(), " wing flap sounds"); + } + if (!wingIdleSounds_.empty()) { + LOG_INFO("Loaded ", wingIdleSounds_.size(), " wing idle sounds"); + } + if (!horseBreathSounds_.empty()) { + LOG_INFO("Loaded ", horseBreathSounds_.size(), " horse breath sounds"); + } + if (!horseMoveSounds_.empty()) { + LOG_INFO("Loaded ", horseMoveSounds_.size(), " horse move sounds"); + } +} + +bool MountSoundManager::loadSound(const std::string& path, MountSample& sample) { + if (!assetManager_ || !assetManager_->fileExists(path)) { + return false; + } + + auto data = assetManager_->readFile(path); + if (data.empty()) { + return false; + } + + sample.path = path; + sample.data = std::move(data); + return true; +} + +void MountSoundManager::update(float deltaTime) { if (!mounted_) { + soundLoopTimer_ = 0.0f; return; } + soundLoopTimer_ += deltaTime; updateMountSounds(); } @@ -104,29 +209,83 @@ void MountSoundManager::updateMountSounds() { return; } - // TODO: Implement actual mount sound playback - // For now, just log state changes - static bool lastMoving = false; - static bool lastFlying = false; + static std::mt19937 rng(std::random_device{}()); - if (moving_ != lastMoving || flying_ != lastFlying) { - LOG_INFO("Mount sound state: moving=", moving_, " flying=", flying_, - " type=", static_cast(currentMountType_)); - lastMoving = moving_; - lastFlying = flying_; + // Flying mounts + if (currentMountType_ == MountType::FLYING && flying_) { + if (moving_ && !wingFlapSounds_.empty()) { + // Wing flaps when moving (play periodically for continuous flapping sound) + if (soundLoopTimer_ >= 1.2f) { + std::uniform_int_distribution dist(0, wingFlapSounds_.size() - 1); + const auto& sample = wingFlapSounds_[dist(rng)]; + std::uniform_real_distribution volumeDist(0.4f, 0.5f); + std::uniform_real_distribution pitchDist(0.95f, 1.05f); + AudioEngine::instance().playSound2D( + sample.data, + volumeDist(rng) * volumeScale_, + pitchDist(rng) + ); + soundLoopTimer_ = 0.0f; + playingMovementSound_ = true; + } + } else if (!moving_ && !wingIdleSounds_.empty()) { + // Idle/hovering sounds (less frequent) + if (soundLoopTimer_ >= 3.5f) { + std::uniform_int_distribution dist(0, wingIdleSounds_.size() - 1); + const auto& sample = wingIdleSounds_[dist(rng)]; + std::uniform_real_distribution volumeDist(0.3f, 0.4f); + std::uniform_real_distribution pitchDist(0.98f, 1.02f); + AudioEngine::instance().playSound2D( + sample.data, + volumeDist(rng) * volumeScale_, + pitchDist(rng) + ); + soundLoopTimer_ = 0.0f; + playingIdleSound_ = true; + } + } + } + // Ground mounts + else if (currentMountType_ == MountType::GROUND && !flying_) { + if (moving_ && !horseMoveSounds_.empty()) { + // Occasional whinny/ambient sounds while moving + if (soundLoopTimer_ >= 8.0f) { + std::uniform_int_distribution dist(0, horseMoveSounds_.size() - 1); + const auto& sample = horseMoveSounds_[dist(rng)]; + std::uniform_real_distribution volumeDist(0.35f, 0.45f); + std::uniform_real_distribution pitchDist(0.97f, 1.03f); + AudioEngine::instance().playSound2D( + sample.data, + volumeDist(rng) * volumeScale_, + pitchDist(rng) + ); + soundLoopTimer_ = 0.0f; + playingMovementSound_ = true; + } + } else if (!moving_ && !horseBreathSounds_.empty()) { + // Breathing/snorting when idle + if (soundLoopTimer_ >= 4.5f) { + std::uniform_int_distribution dist(0, horseBreathSounds_.size() - 1); + const auto& sample = horseBreathSounds_[dist(rng)]; + std::uniform_real_distribution volumeDist(0.25f, 0.35f); + std::uniform_real_distribution pitchDist(0.98f, 1.02f); + AudioEngine::instance().playSound2D( + sample.data, + volumeDist(rng) * volumeScale_, + pitchDist(rng) + ); + soundLoopTimer_ = 0.0f; + playingIdleSound_ = true; + } + } } - - // TODO: Load and play appropriate looping sounds: - // - Flying + moving: wing flaps (fast loop) - // - Flying + idle: wing flaps (slow loop) or hovering sound - // - Ground + moving: galloping/hoofbeats (pace based on speed) - // - Ground + idle: breathing, fidgeting sounds (occasional) } void MountSoundManager::stopAllMountSounds() { - // TODO: Stop any active looping mount sounds + // Reset state flags playingMovementSound_ = false; playingIdleSound_ = false; + soundLoopTimer_ = 0.0f; } } // namespace audio diff --git a/src/core/application.cpp b/src/core/application.cpp index 3df7783c..9c9a94b2 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -2803,7 +2803,7 @@ void Application::processPendingMount() { } } - renderer->setMounted(instanceId, heightOffset); + renderer->setMounted(instanceId, mountDisplayId, heightOffset); charRenderer->playAnimation(instanceId, 0, true); LOG_INFO("processPendingMount: DONE displayId=", mountDisplayId, " model=", m2Path, " heightOffset=", heightOffset); diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 123e7861..9e80c518 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -507,7 +507,7 @@ void Renderer::setCharacterFollow(uint32_t instanceId) { } } -void Renderer::setMounted(uint32_t mountInstId, float heightOffset) { +void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset) { mountInstanceId_ = mountInstId; mountHeightOffset_ = heightOffset; charAnimState = CharAnimState::MOUNT; @@ -515,6 +515,12 @@ void Renderer::setMounted(uint32_t mountInstId, float heightOffset) { cameraController->setMounted(true); cameraController->setMountHeightOffset(heightOffset); } + + // Notify mount sound manager + if (mountSoundManager) { + bool isFlying = taxiFlight_; // Taxi flights are flying mounts + mountSoundManager->onMount(mountDisplayId, isFlying); + } } void Renderer::clearMount() { @@ -527,6 +533,11 @@ void Renderer::clearMount() { cameraController->setMounted(false); cameraController->setMountHeightOffset(0.0f); } + + // Notify mount sound manager + if (mountSoundManager) { + mountSoundManager->onDismount(); + } } uint32_t Renderer::resolveMeleeAnimId() { @@ -1349,6 +1360,17 @@ void Renderer::update(float deltaTime) { } } + // Mount ambient sounds: wing flaps, breathing, etc. + if (mountSoundManager) { + mountSoundManager->update(deltaTime); + if (cameraController && isMounted()) { + bool moving = cameraController->isMoving(); + bool flying = taxiFlight_ || !cameraController->isGrounded(); // Flying if taxi or airborne + mountSoundManager->setMoving(moving); + mountSoundManager->setFlying(flying); + } + } + // Update M2 doodad animations (pass camera for frustum-culling bone computation) if (m2Renderer && camera) { m2Renderer->update(deltaTime, camera->getPosition(),