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(),