From 4a7e599764c1321f6306c0ec93fd504b7218820c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 9 Feb 2026 02:22:20 -0800 Subject: [PATCH] Fix NPC voices to use correct WAV format and gender detection WotLK 3.3.5a uses .wav files for NPC voices, not .ogg as shown in retail Wowhead. Fixed audio engine to preserve original sample rate from WAV files (preventing chipmunk playback). Implemented race/gender detection using CreatureDisplayInfo.dbc and CreatureDisplayInfoExtra.dbc to play correct voice types for each NPC. --- include/core/application.hpp | 2 + src/audio/audio_engine.cpp | 15 ++++--- src/audio/npc_voice_manager.cpp | 71 ++++++++++++++------------------- src/core/application.cpp | 44 +++++++++++++++++++- 4 files changed, 84 insertions(+), 48 deletions(-) diff --git a/include/core/application.hpp b/include/core/application.hpp index 2e00e89a..2492e73d 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -16,6 +16,7 @@ namespace ui { class UIManager; } namespace auth { class AuthHandler; } namespace game { class GameHandler; class World; class NpcManager; } namespace pipeline { class AssetManager; } +namespace audio { enum class VoiceType; } namespace core { @@ -91,6 +92,7 @@ private: void despawnOnlineGameObject(uint64_t guid); void buildGameObjectDisplayLookups(); std::string getGameObjectModelPathForDisplayId(uint32_t displayId) const; + audio::VoiceType detectVoiceTypeFromDisplayId(uint32_t displayId) const; static Application* instance; diff --git a/src/audio/audio_engine.cpp b/src/audio/audio_engine.cpp index e6c4feba..93a8a3b6 100644 --- a/src/audio/audio_engine.cpp +++ b/src/audio/audio_engine.cpp @@ -182,6 +182,7 @@ bool AudioEngine::playSound2D(const std::vector& wavData, float volume, pcmData.data(), nullptr // No custom allocator ); + bufferConfig.sampleRate = sampleRate; // Critical: preserve original sample rate! ma_audio_buffer* audioBuffer = new ma_audio_buffer(); result = ma_audio_buffer_init(&bufferConfig, audioBuffer); @@ -242,8 +243,6 @@ bool AudioEngine::playSound3D(const std::vector& wavData, const glm::ve return false; } - (void)pitch; // Pitch not supported yet - // Decode WAV data first ma_decoder decoder; ma_decoder_config decoderConfig = ma_decoder_config_init_default(); @@ -255,6 +254,7 @@ bool AudioEngine::playSound3D(const std::vector& wavData, const glm::ve ); if (result != MA_SUCCESS) { + LOG_WARNING("playSound3D: Failed to decode WAV, error: ", result); return false; } @@ -262,6 +262,8 @@ bool AudioEngine::playSound3D(const std::vector& wavData, const glm::ve ma_uint32 channels = decoder.outputChannels; ma_uint32 sampleRate = decoder.outputSampleRate; + LOG_DEBUG("playSound3D: Decoded WAV - format:", format, " channels:", channels, " sampleRate:", sampleRate, " pitch:", pitch); + ma_uint64 totalFrames; result = ma_decoder_get_length_in_pcm_frames(&decoder, &totalFrames); if (result != MA_SUCCESS) { @@ -286,7 +288,7 @@ bool AudioEngine::playSound3D(const std::vector& wavData, const glm::ve pcmData.resize(framesRead * channels * ma_get_bytes_per_sample(format)); - // Create audio buffer + // Create audio buffer with correct sample rate ma_audio_buffer_config bufferConfig = ma_audio_buffer_config_init( format, channels, @@ -294,6 +296,7 @@ bool AudioEngine::playSound3D(const std::vector& wavData, const glm::ve pcmData.data(), nullptr ); + bufferConfig.sampleRate = sampleRate; // Critical: preserve original sample rate! ma_audio_buffer* audioBuffer = new ma_audio_buffer(); result = ma_audio_buffer_init(&bufferConfig, audioBuffer); @@ -302,17 +305,18 @@ bool AudioEngine::playSound3D(const std::vector& wavData, const glm::ve return false; } - // Create 3D sound (spatialization enabled) + // Create 3D sound (spatialization enabled, pitch enabled) ma_sound* sound = new ma_sound(); result = ma_sound_init_from_data_source( engine_, audioBuffer, - MA_SOUND_FLAG_DECODE | MA_SOUND_FLAG_ASYNC | MA_SOUND_FLAG_NO_PITCH, + MA_SOUND_FLAG_DECODE | MA_SOUND_FLAG_ASYNC, // Removed NO_PITCH flag nullptr, sound ); if (result != MA_SUCCESS) { + LOG_WARNING("playSound3D: Failed to create sound, error: ", result); ma_audio_buffer_uninit(audioBuffer); delete audioBuffer; delete sound; @@ -322,6 +326,7 @@ bool AudioEngine::playSound3D(const std::vector& wavData, const glm::ve // Set 3D position and attenuation ma_sound_set_position(sound, position.x, position.y, position.z); ma_sound_set_volume(sound, volume * masterVolume_); + ma_sound_set_pitch(sound, pitch); // Enable pitch variation ma_sound_set_attenuation_model(sound, ma_attenuation_model_inverse); ma_sound_set_min_gain(sound, 0.0f); ma_sound_set_max_gain(sound, 1.0f); diff --git a/src/audio/npc_voice_manager.cpp b/src/audio/npc_voice_manager.cpp index 6ac22abe..66432f0b 100644 --- a/src/audio/npc_voice_manager.cpp +++ b/src/audio/npc_voice_manager.cpp @@ -20,26 +20,13 @@ bool NpcVoiceManager::initialize(pipeline::AssetManager* assets) { return false; } - // Comprehensive probe - try forward slashes (MPQ internal format) - LOG_INFO("=== Searching for NPC voice files (testing patterns) ==="); + // Files are .WAV not .OGG in WotLK 3.3.5a! + LOG_INFO("=== Probing for NPC voice files (.wav format) ==="); std::vector testPaths = { - // Forward slashes (MPQ internal format) - "Sound/Creature/HumanMaleStandardNPC/HumanMaleStandardNPCGreetings01.ogg", - "Sound/Creature/HumanFemaleStandardNPC/HumanFemaleStandardNPCGreeting01.ogg", - - // Backslashes (Windows format) - "Sound\\Creature\\HumanMaleStandardNPC\\HumanMaleStandardNPCGreetings01.ogg", - "Sound\\Creature\\HumanFemaleStandardNPC\\HumanFemaleStandardNPCGreeting01.ogg", - - // Lowercase with forward slashes - "sound/creature/humanmalestandardnpc/humanmalestandardnpcgreetings01.ogg", - - // PC voice files with forward slashes - "Sound/Character/Human/HumanVocMaleHello01.wav", - "Sound/Character/Human/HumanVocFemaleHello01.wav", - - // PC voice files with backslashes - "Sound\\Character\\Human\\HumanVocMaleHello01.wav", + "Sound\\Creature\\HumanMaleStandardNPC\\HumanMaleStandardNPCGreeting01.wav", + "Sound\\Creature\\HumanFemaleStandardNPC\\HumanFemaleStandardNPCGreeting01.wav", + "Sound\\Creature\\DwarfMaleStandardNPC\\DwarfMaleStandardNPCGreeting01.wav", + "Sound\\Creature\\OrcMaleStandardNPC\\OrcMaleStandardNPCGreeting01.wav", }; for (const auto& path : testPaths) { bool exists = assetManager_->fileExists(path); @@ -76,18 +63,18 @@ void NpcVoiceManager::shutdown() { void NpcVoiceManager::loadVoiceSounds() { if (!assetManager_) return; - // Load all standard NPC greetings from Wowhead database - // Note: Human male uses "Greetings" (plural), others use "Greeting" (singular) + // WotLK 3.3.5a uses .WAV files, not .OGG! + // Files use "Greeting" (singular) not "Greetings" // Generic - mix of all races for variety auto& genericVoices = voiceLibrary_[VoiceType::GENERIC]; for (const auto& path : { - "Sound\\Creature\\HumanMaleStandardNPC\\HumanMaleStandardNPCGreetings01.ogg", - "Sound\\Creature\\HumanFemaleStandardNPC\\HumanFemaleStandardNPCGreeting01.ogg", - "Sound\\Creature\\DwarfMaleStandardNPC\\DwarfMaleStandardNPCGreeting01.ogg", - "Sound\\Creature\\GnomeMaleStandardNPC\\GnomeMaleStandardNPCGreeting01.ogg", - "Sound\\Creature\\NightElfMaleStandardNPC\\NightElfMaleStandardNPCGreeting01.ogg", - "Sound\\Creature\\OrcMaleStandardNPC\\OrcMaleStandardNPCGreeting01.ogg", + "Sound\\Creature\\HumanMaleStandardNPC\\HumanMaleStandardNPCGreeting01.wav", + "Sound\\Creature\\HumanFemaleStandardNPC\\HumanFemaleStandardNPCGreeting01.wav", + "Sound\\Creature\\DwarfMaleStandardNPC\\DwarfMaleStandardNPCGreeting01.wav", + "Sound\\Creature\\GnomeMaleStandardNPC\\GnomeMaleStandardNPCGreeting01.wav", + "Sound\\Creature\\NightElfMaleStandardNPC\\NightElfMaleStandardNPCGreeting01.wav", + "Sound\\Creature\\OrcMaleStandardNPC\\OrcMaleStandardNPCGreeting01.wav", }) { VoiceSample sample; if (loadSound(path, sample)) genericVoices.push_back(std::move(sample)); @@ -96,7 +83,7 @@ void NpcVoiceManager::loadVoiceSounds() { // Human Male auto& humanMale = voiceLibrary_[VoiceType::HUMAN_MALE]; for (int i = 1; i <= 6; ++i) { - std::string path = "Sound\\Creature\\HumanMaleStandardNPC\\HumanMaleStandardNPCGreetings0" + std::to_string(i) + ".ogg"; + std::string path = "Sound\\Creature\\HumanMaleStandardNPC\\HumanMaleStandardNPCGreeting0" + std::to_string(i) + ".wav"; VoiceSample sample; if (loadSound(path, sample)) humanMale.push_back(std::move(sample)); } @@ -104,7 +91,7 @@ void NpcVoiceManager::loadVoiceSounds() { // Human Female auto& humanFemale = voiceLibrary_[VoiceType::HUMAN_FEMALE]; for (int i = 1; i <= 5; ++i) { - std::string path = "Sound\\Creature\\HumanFemaleStandardNPC\\HumanFemaleStandardNPCGreeting0" + std::to_string(i) + ".ogg"; + std::string path = "Sound\\Creature\\HumanFemaleStandardNPC\\HumanFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav"; VoiceSample sample; if (loadSound(path, sample)) humanFemale.push_back(std::move(sample)); } @@ -112,7 +99,7 @@ void NpcVoiceManager::loadVoiceSounds() { // Dwarf Male auto& dwarfMale = voiceLibrary_[VoiceType::DWARF_MALE]; for (int i = 1; i <= 6; ++i) { - std::string path = "Sound\\Creature\\DwarfMaleStandardNPC\\DwarfMaleStandardNPCGreeting0" + std::to_string(i) + ".ogg"; + std::string path = "Sound\\Creature\\DwarfMaleStandardNPC\\DwarfMaleStandardNPCGreeting0" + std::to_string(i) + ".wav"; VoiceSample sample; if (loadSound(path, sample)) dwarfMale.push_back(std::move(sample)); } @@ -120,7 +107,7 @@ void NpcVoiceManager::loadVoiceSounds() { // Gnome Male auto& gnomeMale = voiceLibrary_[VoiceType::GNOME_MALE]; for (int i = 1; i <= 6; ++i) { - std::string path = "Sound\\Creature\\GnomeMaleStandardNPC\\GnomeMaleStandardNPCGreeting0" + std::to_string(i) + ".ogg"; + std::string path = "Sound\\Creature\\GnomeMaleStandardNPC\\GnomeMaleStandardNPCGreeting0" + std::to_string(i) + ".wav"; VoiceSample sample; if (loadSound(path, sample)) gnomeMale.push_back(std::move(sample)); } @@ -128,7 +115,7 @@ void NpcVoiceManager::loadVoiceSounds() { // Gnome Female auto& gnomeFemale = voiceLibrary_[VoiceType::GNOME_FEMALE]; for (int i = 1; i <= 6; ++i) { - std::string path = "Sound\\Creature\\GnomeFemaleStandardNPC\\GnomeFemaleStandardNPCGreeting0" + std::to_string(i) + ".ogg"; + std::string path = "Sound\\Creature\\GnomeFemaleStandardNPC\\GnomeFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav"; VoiceSample sample; if (loadSound(path, sample)) gnomeFemale.push_back(std::move(sample)); } @@ -136,7 +123,7 @@ void NpcVoiceManager::loadVoiceSounds() { // Night Elf Male auto& nelfMale = voiceLibrary_[VoiceType::NIGHTELF_MALE]; for (int i = 1; i <= 8; ++i) { - std::string path = "Sound\\Creature\\NightElfMaleStandardNPC\\NightElfMaleStandardNPCGreeting0" + std::to_string(i) + ".ogg"; + std::string path = "Sound\\Creature\\NightElfMaleStandardNPC\\NightElfMaleStandardNPCGreeting0" + std::to_string(i) + ".wav"; VoiceSample sample; if (loadSound(path, sample)) nelfMale.push_back(std::move(sample)); } @@ -144,7 +131,7 @@ void NpcVoiceManager::loadVoiceSounds() { // Night Elf Female auto& nelfFemale = voiceLibrary_[VoiceType::NIGHTELF_FEMALE]; for (int i = 1; i <= 6; ++i) { - std::string path = "Sound\\Creature\\NightElfFemaleStandardNPC\\NightElfFemaleStandardNPCGreeting0" + std::to_string(i) + ".ogg"; + std::string path = "Sound\\Creature\\NightElfFemaleStandardNPC\\NightElfFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav"; VoiceSample sample; if (loadSound(path, sample)) nelfFemale.push_back(std::move(sample)); } @@ -152,7 +139,7 @@ void NpcVoiceManager::loadVoiceSounds() { // Orc Male auto& orcMale = voiceLibrary_[VoiceType::ORC_MALE]; for (int i = 1; i <= 5; ++i) { - std::string path = "Sound\\Creature\\OrcMaleStandardNPC\\OrcMaleStandardNPCGreeting0" + std::to_string(i) + ".ogg"; + std::string path = "Sound\\Creature\\OrcMaleStandardNPC\\OrcMaleStandardNPCGreeting0" + std::to_string(i) + ".wav"; VoiceSample sample; if (loadSound(path, sample)) orcMale.push_back(std::move(sample)); } @@ -160,7 +147,7 @@ void NpcVoiceManager::loadVoiceSounds() { // Orc Female auto& orcFemale = voiceLibrary_[VoiceType::ORC_FEMALE]; for (int i = 1; i <= 6; ++i) { - std::string path = "Sound\\Creature\\OrcFemaleStandardNPC\\OrcFemaleStandardNPCGreeting0" + std::to_string(i) + ".ogg"; + std::string path = "Sound\\Creature\\OrcFemaleStandardNPC\\OrcFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav"; VoiceSample sample; if (loadSound(path, sample)) orcFemale.push_back(std::move(sample)); } @@ -168,7 +155,7 @@ void NpcVoiceManager::loadVoiceSounds() { // Tauren Male auto& taurenMale = voiceLibrary_[VoiceType::TAUREN_MALE]; for (int i = 1; i <= 5; ++i) { - std::string path = "Sound\\Creature\\TaurenMaleStandardNPC\\TaurenMaleStandardNPCGreeting0" + std::to_string(i) + ".ogg"; + std::string path = "Sound\\Creature\\TaurenMaleStandardNPC\\TaurenMaleStandardNPCGreeting0" + std::to_string(i) + ".wav"; VoiceSample sample; if (loadSound(path, sample)) taurenMale.push_back(std::move(sample)); } @@ -176,7 +163,7 @@ void NpcVoiceManager::loadVoiceSounds() { // Tauren Female auto& taurenFemale = voiceLibrary_[VoiceType::TAUREN_FEMALE]; for (int i = 1; i <= 5; ++i) { - std::string path = "Sound\\Creature\\TaurenFemaleStandardNPC\\TaurenFemaleStandardNPCGreeting0" + std::to_string(i) + ".ogg"; + std::string path = "Sound\\Creature\\TaurenFemaleStandardNPC\\TaurenFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav"; VoiceSample sample; if (loadSound(path, sample)) taurenFemale.push_back(std::move(sample)); } @@ -184,7 +171,7 @@ void NpcVoiceManager::loadVoiceSounds() { // Troll Male auto& trollMale = voiceLibrary_[VoiceType::TROLL_MALE]; for (int i = 1; i <= 6; ++i) { - std::string path = "Sound\\Creature\\TrollMaleStandardNPC\\TrollMaleStandardNPCGreeting0" + std::to_string(i) + ".ogg"; + std::string path = "Sound\\Creature\\TrollMaleStandardNPC\\TrollMaleStandardNPCGreeting0" + std::to_string(i) + ".wav"; VoiceSample sample; if (loadSound(path, sample)) trollMale.push_back(std::move(sample)); } @@ -192,7 +179,7 @@ void NpcVoiceManager::loadVoiceSounds() { // Troll Female auto& trollFemale = voiceLibrary_[VoiceType::TROLL_FEMALE]; for (int i = 1; i <= 5; ++i) { - std::string path = "Sound\\Creature\\TrollFemaleStandardNPC\\TrollFemaleStandardNPCGreeting0" + std::to_string(i) + ".ogg"; + std::string path = "Sound\\Creature\\TrollFemaleStandardNPC\\TrollFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav"; VoiceSample sample; if (loadSound(path, sample)) trollFemale.push_back(std::move(sample)); } @@ -200,7 +187,7 @@ void NpcVoiceManager::loadVoiceSounds() { // Undead Male auto& undeadMale = voiceLibrary_[VoiceType::UNDEAD_MALE]; for (int i = 1; i <= 6; ++i) { - std::string path = "Sound\\Creature\\UndeadMaleStandardNPC\\UndeadMaleStandardNPCGreeting0" + std::to_string(i) + ".ogg"; + std::string path = "Sound\\Creature\\UndeadMaleStandardNPC\\UndeadMaleStandardNPCGreeting0" + std::to_string(i) + ".wav"; VoiceSample sample; if (loadSound(path, sample)) undeadMale.push_back(std::move(sample)); } @@ -208,7 +195,7 @@ void NpcVoiceManager::loadVoiceSounds() { // Undead Female auto& undeadFemale = voiceLibrary_[VoiceType::UNDEAD_FEMALE]; for (int i = 1; i <= 6; ++i) { - std::string path = "Sound\\Creature\\UndeadFemaleStandardNPC\\UndeadFemaleStandardNPCGreeting0" + std::to_string(i) + ".ogg"; + std::string path = "Sound\\Creature\\UndeadFemaleStandardNPC\\UndeadFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav"; VoiceSample sample; if (loadSound(path, sample)) undeadFemale.push_back(std::move(sample)); } diff --git a/src/core/application.cpp b/src/core/application.cpp index 33cc1f65..56c4f087 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -820,7 +820,17 @@ void Application::setupUICallbacks() { if (renderer && renderer->getNpcVoiceManager()) { // Convert canonical to render coords for 3D audio glm::vec3 renderPos = core::coords::canonicalToRender(position); - renderer->getNpcVoiceManager()->playGreeting(guid, audio::VoiceType::GENERIC, renderPos); + + // Detect voice type from NPC display ID + audio::VoiceType voiceType = audio::VoiceType::GENERIC; + auto entity = gameHandler->getEntityManager().getEntity(guid); + if (entity && entity->getType() == game::ObjectType::UNIT) { + auto unit = std::static_pointer_cast(entity); + uint32_t displayId = unit->getDisplayId(); + voiceType = detectVoiceTypeFromDisplayId(displayId); + } + + renderer->getNpcVoiceManager()->playGreeting(guid, voiceType, renderPos); } }); @@ -1926,6 +1936,38 @@ std::string Application::getModelPathForDisplayId(uint32_t displayId) const { return itPath->second; } +audio::VoiceType Application::detectVoiceTypeFromDisplayId(uint32_t displayId) const { + // Look up display data + auto itDisplay = displayDataMap_.find(displayId); + if (itDisplay == displayDataMap_.end() || itDisplay->second.extraDisplayId == 0) { + return audio::VoiceType::GENERIC; // Not a humanoid or no extra data + } + + // Look up humanoid extra data (race/sex info) + auto itExtra = humanoidExtraMap_.find(itDisplay->second.extraDisplayId); + if (itExtra == humanoidExtraMap_.end()) { + return audio::VoiceType::GENERIC; + } + + uint8_t raceId = itExtra->second.raceId; + uint8_t sexId = itExtra->second.sexId; + + // Map (raceId, sexId) to VoiceType + // Race IDs: 1=Human, 2=Orc, 3=Dwarf, 4=NightElf, 5=Undead, 6=Tauren, 7=Gnome, 8=Troll + // Sex IDs: 0=Male, 1=Female + switch (raceId) { + case 1: return (sexId == 0) ? audio::VoiceType::HUMAN_MALE : audio::VoiceType::HUMAN_FEMALE; + case 2: return (sexId == 0) ? audio::VoiceType::ORC_MALE : audio::VoiceType::ORC_FEMALE; + case 3: return (sexId == 0) ? audio::VoiceType::DWARF_MALE : audio::VoiceType::GENERIC; // No dwarf female voices loaded + case 4: return (sexId == 0) ? audio::VoiceType::NIGHTELF_MALE : audio::VoiceType::NIGHTELF_FEMALE; + case 5: return (sexId == 0) ? audio::VoiceType::UNDEAD_MALE : audio::VoiceType::UNDEAD_FEMALE; + case 6: return (sexId == 0) ? audio::VoiceType::TAUREN_MALE : audio::VoiceType::TAUREN_FEMALE; + case 7: return (sexId == 0) ? audio::VoiceType::GNOME_MALE : audio::VoiceType::GNOME_FEMALE; + case 8: return (sexId == 0) ? audio::VoiceType::TROLL_MALE : audio::VoiceType::TROLL_FEMALE; + default: return audio::VoiceType::GENERIC; + } +} + void Application::buildGameObjectDisplayLookups() { if (gameObjectLookupsBuilt_ || !assetManager || !assetManager->isInitialized()) return;