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).
This commit is contained in:
Kelsi 2026-02-09 01:19:35 -08:00
parent 0b6b403848
commit 0fed931aa0
5 changed files with 221 additions and 23 deletions

View file

@ -2,6 +2,8 @@
#include <string>
#include <cstdint>
#include <vector>
#include <chrono>
namespace wowee {
namespace pipeline { class AssetManager; }
@ -15,6 +17,11 @@ enum class MountType {
SWIMMING // Sea turtle, etc.
};
struct MountSample {
std::string path;
std::vector<uint8_t> 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<MountSample> wingFlapSounds_;
std::vector<MountSample> wingIdleSounds_;
std::vector<MountSample> horseBreathSounds_;
std::vector<MountSample> horseMoveSounds_;
// Sound state tracking
bool playingMovementSound_ = false;
bool playingIdleSound_ = false;
std::chrono::steady_clock::time_point lastSoundUpdate_;
float soundLoopTimer_ = 0.0f;
};
} // namespace audio

View file

@ -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();

View file

@ -2,11 +2,14 @@
#include "audio/audio_engine.hpp"
#include "pipeline/asset_manager.hpp"
#include "core/logger.hpp"
#include <random>
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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<int>(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<size_t> dist(0, wingFlapSounds_.size() - 1);
const auto& sample = wingFlapSounds_[dist(rng)];
std::uniform_real_distribution<float> volumeDist(0.4f, 0.5f);
std::uniform_real_distribution<float> 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<size_t> dist(0, wingIdleSounds_.size() - 1);
const auto& sample = wingIdleSounds_[dist(rng)];
std::uniform_real_distribution<float> volumeDist(0.3f, 0.4f);
std::uniform_real_distribution<float> 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<size_t> dist(0, horseMoveSounds_.size() - 1);
const auto& sample = horseMoveSounds_[dist(rng)];
std::uniform_real_distribution<float> volumeDist(0.35f, 0.45f);
std::uniform_real_distribution<float> 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<size_t> dist(0, horseBreathSounds_.size() - 1);
const auto& sample = horseBreathSounds_[dist(rng)];
std::uniform_real_distribution<float> volumeDist(0.25f, 0.35f);
std::uniform_real_distribution<float> 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

View file

@ -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);

View file

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