Add ambient sound system and eliminate log spam

- Implement AmbientSoundManager with tavern/outdoor ambience
- Fix audio buffer limit (5s → 60s) for long ambient loops
- Set log level to INFO to eliminate DEBUG spam (130MB → 3.2MB logs)
- Remove excessive terrain/model/network logging
- Fix ambient sound timer sharing and pitch parameter bugs
This commit is contained in:
Kelsi 2026-02-09 14:50:14 -08:00
parent 4a7e599764
commit dab23f1895
24 changed files with 701 additions and 138 deletions

View file

@ -33,13 +33,14 @@ bool ActivitySoundManager::initialize(pipeline::AssetManager* assets) {
rebuildHardLandClipsForProfile("Human", "Human", true);
preloadCandidates(splashEnterClips, {
"Sound\\Character\\General\\Water\\WaterSplashSmall.wav",
"Sound\\Character\\General\\Water\\WaterSplashMedium.wav",
"Sound\\Character\\General\\Water\\WaterSplashLarge.wav",
"Sound\\Character\\Footsteps\\mFootMediumLargeWaterA.wav",
"Sound\\Character\\Footsteps\\mFootMediumLargeWaterB.wav",
"Sound\\Character\\Footsteps\\mFootMediumLargeWaterC.wav",
"Sound\\Character\\Footsteps\\mFootMediumLargeWaterD.wav"
"Sound\\Character\\Footsteps\\EnterWaterSplash\\EnterWaterSmallA.wav",
"Sound\\Character\\Footsteps\\EnterWaterSplash\\EnterWaterMediumA.wav",
"Sound\\Character\\Footsteps\\EnterWaterSplash\\EnterWaterGiantA.wav",
"Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterA.wav",
"Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterB.wav",
"Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterC.wav",
"Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterD.wav",
"Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterE.wav"
});
splashExitClips = splashEnterClips;
@ -91,8 +92,29 @@ void ActivitySoundManager::shutdown() {
assetManager = nullptr;
}
void ActivitySoundManager::update(float) {
void ActivitySoundManager::update(float deltaTime) {
reapProcesses();
// Play swimming stroke sounds periodically when swimming and moving
if (swimmingActive && swimMoving && !swimLoopClips.empty()) {
auto now = std::chrono::steady_clock::now();
float elapsed = std::chrono::duration<float>(now - lastSwimStrokeAt).count();
// Play swimming stroke sound every 0.8 seconds (swim stroke rhythm)
if (lastSwimStrokeAt.time_since_epoch().count() == 0 || elapsed >= 0.8f) {
std::uniform_int_distribution<size_t> clipDist(0, swimLoopClips.size() - 1);
const Sample& sample = swimLoopClips[clipDist(rng)];
// Play as one-shot 2D sound
float volume = 0.6f * volumeScale;
AudioEngine::instance().playSound2D(sample.data, volume, false);
lastSwimStrokeAt = now;
}
} else if (!swimmingActive) {
// Reset timer when not swimming
lastSwimStrokeAt = std::chrono::steady_clock::time_point{};
}
}
void ActivitySoundManager::preloadCandidates(std::vector<Sample>& out, const std::vector<std::string>& candidates) {
@ -169,24 +191,21 @@ void ActivitySoundManager::rebuildJumpClipsForProfile(const std::string& raceFol
void ActivitySoundManager::rebuildSwimLoopClipsForProfile(const std::string& raceFolder, const std::string& raceBase, bool male) {
swimLoopClips.clear();
const std::string gender = male ? "Male" : "Female";
const std::string prefix = "Sound\\Character\\" + raceFolder + "\\";
const std::string stem = raceBase + gender;
// WoW 3.3.5a doesn't have dedicated swim loop sounds
// Use water splash/footstep sounds as swimming stroke sounds
preloadCandidates(swimLoopClips, {
prefix + stem + "\\" + stem + "SwimLoop.wav",
prefix + stem + "\\" + stem + "Swim01.wav",
prefix + stem + "\\" + stem + "Swim02.wav",
prefix + stem + "SwimLoop.wav",
prefix + stem + "Swim01.wav",
prefix + stem + "Swim02.wav",
prefix + (male ? "Male" : "Female") + "\\" + stem + "SwimLoop.wav",
"Sound\\Character\\Swim\\SwimMoveLoop.wav",
"Sound\\Character\\Swim\\SwimLoop.wav",
"Sound\\Character\\Swim\\SwimSlowLoop.wav"
"Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterA.wav",
"Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterB.wav",
"Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterC.wav",
"Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterD.wav",
"Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterE.wav",
"Sound\\Character\\Footsteps\\WaterSplash\\FootStepsSmallWaterA.wav",
"Sound\\Character\\Footsteps\\WaterSplash\\FootStepsSmallWaterB.wav",
"Sound\\Character\\Footsteps\\WaterSplash\\FootStepsSmallWaterC.wav",
"Sound\\Character\\Footsteps\\WaterSplash\\FootStepsSmallWaterD.wav",
"Sound\\Character\\Footsteps\\WaterSplash\\FootStepsSmallWaterE.wav"
});
if (swimLoopClips.empty()) {
preloadCandidates(swimLoopClips, buildClassicSet("Water"));
}
}
void ActivitySoundManager::rebuildHardLandClipsForProfile(const std::string& raceFolder, const std::string& raceBase, bool male) {
@ -231,22 +250,9 @@ bool ActivitySoundManager::playOneShot(const std::vector<Sample>& clips, float v
}
void ActivitySoundManager::startSwimLoop() {
if (swimLoopPid != INVALID_PROCESS || swimLoopClips.empty()) return;
std::uniform_int_distribution<size_t> clipDist(0, swimLoopClips.size() - 1);
const Sample& sample = swimLoopClips[clipDist(rng)];
std::ofstream out(loopTempPath, std::ios::binary);
if (!out) return;
out.write(reinterpret_cast<const char*>(sample.data.data()), static_cast<std::streamsize>(sample.data.size()));
out.close();
float volume = (swimMoving ? 0.85f : 0.65f) * volumeScale;
std::string filter = "volume=" + std::to_string(volume);
swimLoopPid = platform::spawnProcess({
"-nodisp", "-autoexit", "-loop", "0", "-loglevel", "quiet",
"-af", filter, loopTempPath
});
// Swimming sounds now handled by periodic playback in update() method
// This method kept for API compatibility but does nothing
return;
}
void ActivitySoundManager::stopSwimLoop() {
@ -353,8 +359,10 @@ void ActivitySoundManager::setSwimmingState(bool swimming, bool moving) {
if (swimming == swimmingActive) return;
swimmingActive = swimming;
if (swimmingActive) {
LOG_INFO("Swimming started - playing swim loop");
startSwimLoop();
} else {
LOG_INFO("Swimming stopped - stopping swim loop");
stopSwimLoop();
}
}
@ -406,22 +414,36 @@ void ActivitySoundManager::setCharacterVoiceProfile(const std::string& modelName
}
void ActivitySoundManager::playWaterEnter() {
LOG_INFO("Water entry detected - attempting to play splash sound");
auto now = std::chrono::steady_clock::now();
if (lastSplashAt.time_since_epoch().count() != 0) {
if (std::chrono::duration<float>(now - lastSplashAt).count() < 0.20f) return;
if (std::chrono::duration<float>(now - lastSplashAt).count() < 0.20f) {
LOG_DEBUG("Water splash throttled (too soon)");
return;
}
}
if (playOneShot(splashEnterClips, 0.95f, 0.95f, 1.05f)) {
LOG_INFO("Water splash enter sound played");
lastSplashAt = now;
} else {
LOG_ERROR("Failed to play water splash enter sound");
}
}
void ActivitySoundManager::playWaterExit() {
LOG_INFO("Water exit detected - attempting to play splash sound");
auto now = std::chrono::steady_clock::now();
if (lastSplashAt.time_since_epoch().count() != 0) {
if (std::chrono::duration<float>(now - lastSplashAt).count() < 0.20f) return;
if (std::chrono::duration<float>(now - lastSplashAt).count() < 0.20f) {
LOG_DEBUG("Water splash throttled (too soon)");
return;
}
}
if (playOneShot(splashExitClips, 0.95f, 0.95f, 1.05f)) {
LOG_INFO("Water splash exit sound played");
lastSplashAt = now;
} else {
LOG_ERROR("Failed to play water splash exit sound");
}
}

View file

@ -0,0 +1,329 @@
#include "audio/ambient_sound_manager.hpp"
#include "audio/audio_engine.hpp"
#include "pipeline/asset_manager.hpp"
#include "core/logger.hpp"
#include <random>
#include <algorithm>
#include <cmath>
namespace wowee {
namespace audio {
namespace {
// Distance thresholds (in game units)
constexpr float MAX_FIRE_DISTANCE = 20.0f;
constexpr float MAX_WATER_DISTANCE = 35.0f;
constexpr float MAX_AMBIENT_DISTANCE = 50.0f;
// Volume settings
constexpr float FIRE_VOLUME = 0.7f;
constexpr float WATER_VOLUME = 0.5f;
constexpr float WIND_VOLUME = 0.35f;
constexpr float BIRD_VOLUME = 0.6f;
constexpr float CRICKET_VOLUME = 0.5f;
// Timing settings (seconds)
constexpr float BIRD_MIN_INTERVAL = 8.0f;
constexpr float BIRD_MAX_INTERVAL = 20.0f;
constexpr float CRICKET_MIN_INTERVAL = 6.0f;
constexpr float CRICKET_MAX_INTERVAL = 15.0f;
constexpr float FIRE_LOOP_INTERVAL = 3.0f; // Fire crackling loop length
std::random_device rd;
std::mt19937 gen(rd());
float randomFloat(float min, float max) {
std::uniform_real_distribution<float> dist(min, max);
return dist(gen);
}
}
bool AmbientSoundManager::initialize(pipeline::AssetManager* assets) {
if (!assets) {
LOG_ERROR("AmbientSoundManager: AssetManager is null");
return false;
}
LOG_INFO("AmbientSoundManager: Initializing...");
// Load fire sounds
fireSoundsSmall_.resize(1);
loadSound("Sound\\Doodad\\CampFireSmallLoop.wav", fireSoundsSmall_[0], assets);
fireSoundsLarge_.resize(1);
loadSound("Sound\\Doodad\\CampFireLargeLoop.wav", fireSoundsLarge_[0], assets);
torchSounds_.resize(1);
loadSound("Sound\\Doodad\\TorchFireLoop.wav", torchSounds_[0], assets);
// Load water sounds
waterSounds_.resize(1);
loadSound("Sound\\Ambience\\Water\\River_LakeStillA.wav", waterSounds_[0], assets);
riverSounds_.resize(1);
loadSound("Sound\\Ambience\\Water\\RiverSlowA.wav", riverSounds_[0], assets);
waterfallSounds_.resize(1);
loadSound("Sound\\Doodad\\WaterFallSmall.wav", waterfallSounds_[0], assets);
// Load wind/ambience sounds
windSounds_.resize(1);
bool windLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\ForestNormalDay.wav", windSounds_[0], assets);
tavernSounds_.resize(1);
bool tavernLoaded = loadSound("Sound\\Ambience\\WMOAmbience\\Tavern.wav", tavernSounds_[0], assets);
LOG_INFO("AmbientSoundManager: Wind loaded: ", windLoaded ? "YES" : "NO",
", Tavern loaded: ", tavernLoaded ? "YES" : "NO");
// Initialize timers with random offsets
birdTimer_ = randomFloat(0.0f, 5.0f);
cricketTimer_ = randomFloat(0.0f, 5.0f);
initialized_ = true;
LOG_INFO("AmbientSoundManager: Initialization complete");
return true;
}
void AmbientSoundManager::shutdown() {
emitters_.clear();
activeSounds_.clear();
initialized_ = false;
}
bool AmbientSoundManager::loadSound(const std::string& path, AmbientSample& sample, pipeline::AssetManager* assets) {
sample.path = path;
sample.loaded = false;
try {
sample.data = assets->readFile(path);
if (!sample.data.empty()) {
sample.loaded = true;
return true;
}
} catch (const std::exception& e) {
LOG_ERROR("AmbientSoundManager: Failed to load ", path, ": ", e.what());
}
return false;
}
void AmbientSoundManager::update(float deltaTime, const glm::vec3& cameraPos, bool isIndoor, bool isSwimming) {
if (!initialized_) return;
// Update all emitter systems
updatePositionalEmitters(deltaTime, cameraPos);
updatePeriodicSounds(deltaTime, isIndoor, isSwimming);
updateWindAmbience(deltaTime, isIndoor);
// Track indoor state changes
wasIndoor_ = isIndoor;
}
void AmbientSoundManager::updatePositionalEmitters(float deltaTime, const glm::vec3& cameraPos) {
// First pass: mark emitters as active/inactive based on distance
int activeFireCount = 0;
int activeWaterCount = 0;
const int MAX_ACTIVE_FIRE = 5; // Max 5 fire sounds at once
const int MAX_ACTIVE_WATER = 3; // Max 3 water sounds at once
for (auto& emitter : emitters_) {
float distance = glm::distance(emitter.position, cameraPos);
// Determine max distance based on type
float maxDist = MAX_AMBIENT_DISTANCE;
bool isFire = false;
bool isWater = false;
if (emitter.type == AmbientType::FIREPLACE_SMALL ||
emitter.type == AmbientType::FIREPLACE_LARGE ||
emitter.type == AmbientType::TORCH) {
maxDist = MAX_FIRE_DISTANCE;
isFire = true;
} else if (emitter.type == AmbientType::WATER_SURFACE ||
emitter.type == AmbientType::RIVER ||
emitter.type == AmbientType::WATERFALL) {
maxDist = MAX_WATER_DISTANCE;
isWater = true;
}
// Update active state based on distance AND limits
bool withinRange = (distance < maxDist);
if (isFire && withinRange && activeFireCount < MAX_ACTIVE_FIRE) {
emitter.active = true;
activeFireCount++;
} else if (isWater && withinRange && activeWaterCount < MAX_ACTIVE_WATER) {
emitter.active = true;
activeWaterCount++;
} else if (!isFire && !isWater && withinRange) {
emitter.active = true; // Other types (fountain, etc)
} else {
emitter.active = false;
}
if (!emitter.active) continue;
// Update play timer
emitter.lastPlayTime += deltaTime;
// Handle different emitter types
switch (emitter.type) {
case AmbientType::FIREPLACE_SMALL:
if (emitter.lastPlayTime >= FIRE_LOOP_INTERVAL && !fireSoundsSmall_.empty() && fireSoundsSmall_[0].loaded) {
float volume = FIRE_VOLUME * volumeScale_ * (1.0f - (distance / maxDist));
AudioEngine::instance().playSound3D(fireSoundsSmall_[0].data, emitter.position, volume);
emitter.lastPlayTime = 0.0f;
}
break;
case AmbientType::FIREPLACE_LARGE:
if (emitter.lastPlayTime >= FIRE_LOOP_INTERVAL && !fireSoundsLarge_.empty() && fireSoundsLarge_[0].loaded) {
float volume = FIRE_VOLUME * volumeScale_ * (1.0f - (distance / maxDist));
AudioEngine::instance().playSound3D(fireSoundsLarge_[0].data, emitter.position, volume);
emitter.lastPlayTime = 0.0f;
}
break;
case AmbientType::TORCH:
if (emitter.lastPlayTime >= FIRE_LOOP_INTERVAL && !torchSounds_.empty() && torchSounds_[0].loaded) {
float volume = FIRE_VOLUME * 0.7f * volumeScale_ * (1.0f - (distance / maxDist));
AudioEngine::instance().playSound3D(torchSounds_[0].data, emitter.position, volume);
emitter.lastPlayTime = 0.0f;
}
break;
case AmbientType::WATER_SURFACE:
if (emitter.lastPlayTime >= 5.0f && !waterSounds_.empty() && waterSounds_[0].loaded) {
float volume = WATER_VOLUME * volumeScale_ * (1.0f - (distance / maxDist));
AudioEngine::instance().playSound3D(waterSounds_[0].data, emitter.position, volume);
emitter.lastPlayTime = 0.0f;
}
break;
case AmbientType::RIVER:
if (emitter.lastPlayTime >= 5.0f && !riverSounds_.empty() && riverSounds_[0].loaded) {
float volume = WATER_VOLUME * volumeScale_ * (1.0f - (distance / maxDist));
AudioEngine::instance().playSound3D(riverSounds_[0].data, emitter.position, volume);
emitter.lastPlayTime = 0.0f;
}
break;
case AmbientType::WATERFALL:
if (emitter.lastPlayTime >= 4.0f && !waterfallSounds_.empty() && waterfallSounds_[0].loaded) {
float volume = WATER_VOLUME * 1.2f * volumeScale_ * (1.0f - (distance / maxDist));
AudioEngine::instance().playSound3D(waterfallSounds_[0].data, emitter.position, volume);
emitter.lastPlayTime = 0.0f;
}
break;
default:
break;
}
}
}
void AmbientSoundManager::updatePeriodicSounds(float deltaTime, bool isIndoor, bool isSwimming) {
// Only play outdoor periodic sounds when outdoors and not swimming/underwater
if (isIndoor || isSwimming) return;
// Bird sounds during daytime
if (isDaytime()) {
birdTimer_ += deltaTime;
if (birdTimer_ >= randomFloat(BIRD_MIN_INTERVAL, BIRD_MAX_INTERVAL)) {
// Play a random bird chirp (we'll use wind sound as placeholder for now)
// TODO: Add actual bird sound files when available
birdTimer_ = 0.0f;
}
}
// Cricket sounds during nighttime
if (isNighttime()) {
cricketTimer_ += deltaTime;
if (cricketTimer_ >= randomFloat(CRICKET_MIN_INTERVAL, CRICKET_MAX_INTERVAL)) {
// Play cricket sounds
// TODO: Add actual cricket sound files when available
cricketTimer_ = 0.0f;
}
}
}
void AmbientSoundManager::updateWindAmbience(float deltaTime, bool isIndoor) {
// Always track indoor state for next frame
bool stateChanged = (wasIndoor_ != isIndoor);
if (stateChanged) {
LOG_INFO("Ambient: ", isIndoor ? "ENTERED BUILDING" : "EXITED TO OUTDOORS");
windLoopTime_ = 99.0f; // Force immediate playback on next update
}
wasIndoor_ = isIndoor;
// Indoor ambience (tavern sounds)
if (isIndoor) {
if (!tavernSounds_.empty() && tavernSounds_[0].loaded) {
windLoopTime_ += deltaTime;
if (windLoopTime_ >= 8.0f) {
float volume = 0.8f * volumeScale_;
bool success = AudioEngine::instance().playSound2D(tavernSounds_[0].data, volume, 1.0f);
LOG_INFO("Playing tavern ambience: ", success ? "OK" : "FAILED", " (vol=", volume, ")");
windLoopTime_ = 0.0f;
}
} else {
LOG_WARNING("Cannot play tavern: empty=", tavernSounds_.empty(),
" loaded=", (!tavernSounds_.empty() && tavernSounds_[0].loaded));
}
}
// Outdoor wind ambience
else {
if (!windSounds_.empty() && windSounds_[0].loaded) {
windLoopTime_ += deltaTime;
if (windLoopTime_ >= 30.0f) {
float volume = 0.2f * volumeScale_;
bool success = AudioEngine::instance().playSound2D(windSounds_[0].data, volume, 1.0f);
LOG_INFO("Playing outdoor ambience: ", success ? "OK" : "FAILED", " (vol=", volume, ")");
windLoopTime_ = 0.0f;
}
} else {
LOG_WARNING("Cannot play outdoor: empty=", windSounds_.empty(),
" loaded=", (!windSounds_.empty() && windSounds_[0].loaded));
}
}
}
uint64_t AmbientSoundManager::addEmitter(const glm::vec3& position, AmbientType type) {
AmbientEmitter emitter;
emitter.id = nextEmitterId_++;
emitter.type = type;
emitter.position = position;
emitter.active = false;
emitter.lastPlayTime = randomFloat(0.0f, 2.0f); // Random initial offset
emitter.loopInterval = FIRE_LOOP_INTERVAL;
emitters_.push_back(emitter);
return emitter.id;
}
void AmbientSoundManager::removeEmitter(uint64_t id) {
emitters_.erase(
std::remove_if(emitters_.begin(), emitters_.end(),
[id](const AmbientEmitter& e) { return e.id == id; }),
emitters_.end()
);
}
void AmbientSoundManager::clearEmitters() {
emitters_.clear();
}
void AmbientSoundManager::setGameTime(float hours) {
gameTimeHours_ = std::fmod(hours, 24.0f);
if (gameTimeHours_ < 0.0f) gameTimeHours_ += 24.0f;
}
void AmbientSoundManager::setVolumeScale(float scale) {
volumeScale_ = std::max(0.0f, std::min(1.0f, scale));
}
} // namespace audio
} // namespace wowee

View file

@ -136,7 +136,7 @@ bool AudioEngine::playSound2D(const std::vector<uint8_t>& wavData, float volume,
);
if (result != MA_SUCCESS) {
LOG_WARNING("Failed to decode WAV data: ", result);
LOG_ERROR("AudioEngine: Failed to decode WAV data (", wavData.size(), " bytes): error ", result);
return false;
}
@ -152,8 +152,8 @@ bool AudioEngine::playSound2D(const std::vector<uint8_t>& wavData, float volume,
totalFrames = 0; // Unknown length, will decode what we can
}
// Allocate buffer for decoded PCM data (limit to 5 seconds max to prevent huge allocations)
ma_uint64 maxFrames = sampleRate * 5;
// Allocate buffer for decoded PCM data (limit to 60 seconds max for ambient loops)
ma_uint64 maxFrames = sampleRate * 60;
if (totalFrames == 0 || totalFrames > maxFrames) {
totalFrames = maxFrames;
}
@ -167,10 +167,15 @@ bool AudioEngine::playSound2D(const std::vector<uint8_t>& wavData, float volume,
ma_decoder_uninit(&decoder);
if (result != MA_SUCCESS || framesRead == 0) {
LOG_WARNING("Failed to read any frames from WAV: ", result);
LOG_ERROR("AudioEngine: Failed to read frames from WAV: error ", result, ", framesRead=", framesRead);
return false;
}
// Only log for large files (>1MB)
if (wavData.size() > 1000000) {
LOG_INFO("AudioEngine: Decoded ", framesRead, " frames (", framesRead / (float)sampleRate, "s) from ", wavData.size(), " byte WAV");
}
// Resize pcmData to actual size used
pcmData.resize(framesRead * channels * ma_get_bytes_per_sample(format));
@ -270,7 +275,8 @@ bool AudioEngine::playSound3D(const std::vector<uint8_t>& wavData, const glm::ve
totalFrames = 0;
}
ma_uint64 maxFrames = sampleRate * 5;
// Limit to 60 seconds max for ambient loops (same as 2D)
ma_uint64 maxFrames = sampleRate * 60;
if (totalFrames == 0 || totalFrames > maxFrames) {
totalFrames = maxFrames;
}

View file

@ -119,6 +119,21 @@ void MusicManager::setVolume(int volume) {
// Update AudioEngine music volume directly (no restart needed!)
float vol = volumePercent / 100.0f;
if (underwaterMode) {
vol *= 0.3f; // 30% volume underwater
}
AudioEngine::instance().setMusicVolume(vol);
}
void MusicManager::setUnderwaterMode(bool underwater) {
if (underwaterMode == underwater) return;
underwaterMode = underwater;
// Apply volume change immediately
float vol = volumePercent / 100.0f;
if (underwaterMode) {
vol *= 0.3f; // Fade to 30% underwater
}
AudioEngine::instance().setMusicVolume(vol);
}