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;