From 9d1616a11bb1f68ec3551be5c04436b0787e9460 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 9 Mar 2026 21:04:24 -0700 Subject: [PATCH] audio: stop precast sound on spell completion, failure, or interrupt Add AudioEngine::playSound2DStoppable() + stopSound() so callers can hold a handle and cancel playback early. SpellSoundManager::playPrecast() now stores the handle in activePrecastId_; stopPrecast() cuts the sound. playCast() calls stopPrecast() before playing the release sound, so the channeling audio never bleeds past cast time. SMSG_SPELL_FAILURE and SMSG_CAST_FAILED both call stopPrecast() so interrupted casts silence immediately. --- include/audio/audio_engine.hpp | 7 +++ include/audio/spell_sound_manager.hpp | 2 + src/audio/audio_engine.cpp | 68 ++++++++++++++++++++++++++- src/audio/spell_sound_manager.cpp | 14 +++++- src/game/game_handler.cpp | 12 +++++ 5 files changed, 100 insertions(+), 3 deletions(-) diff --git a/include/audio/audio_engine.hpp b/include/audio/audio_engine.hpp index 20015330..c6d5e723 100644 --- a/include/audio/audio_engine.hpp +++ b/include/audio/audio_engine.hpp @@ -45,6 +45,11 @@ public: bool playSound2D(const std::vector& wavData, float volume = 1.0f, float pitch = 1.0f); bool playSound2D(const std::string& mpqPath, float volume = 1.0f, float pitch = 1.0f); + // Stoppable 2D sound — returns a non-zero handle, or 0 on failure + uint32_t playSound2DStoppable(const std::vector& wavData, float volume = 1.0f); + // Stop a sound started with playSound2DStoppable (no-op if already finished) + void stopSound(uint32_t id); + // 3D positional sound playback bool playSound3D(const std::vector& wavData, const glm::vec3& position, float volume = 1.0f, float pitch = 1.0f, float maxDistance = 100.0f); @@ -70,8 +75,10 @@ private: ma_sound* sound; void* buffer; // ma_audio_buffer* - Keep audio buffer alive std::shared_ptr> pcmDataRef; // Keep decoded PCM alive + uint32_t id = 0; // 0 = anonymous (not stoppable) }; std::vector activeSounds_; + uint32_t nextSoundId_ = 1; // Music track state ma_sound* musicSound_ = nullptr; diff --git a/include/audio/spell_sound_manager.hpp b/include/audio/spell_sound_manager.hpp index 1933a7aa..d0273c82 100644 --- a/include/audio/spell_sound_manager.hpp +++ b/include/audio/spell_sound_manager.hpp @@ -45,6 +45,7 @@ public: // Spell casting sounds void playPrecast(MagicSchool school, SpellPower power); // Channeling/preparation + void stopPrecast(); // Stop precast sound early void playCast(MagicSchool school); // When spell fires void playImpact(MagicSchool school, SpellPower power); // When spell hits target @@ -96,6 +97,7 @@ private: // State tracking float volumeScale_ = 1.0f; bool initialized_ = false; + uint32_t activePrecastId_ = 0; // Handle from AudioEngine::playSound2DStoppable() // Helper methods bool loadSound(const std::string& path, SpellSample& sample, pipeline::AssetManager* assets); diff --git a/src/audio/audio_engine.cpp b/src/audio/audio_engine.cpp index 7fdcb952..f15b161a 100644 --- a/src/audio/audio_engine.cpp +++ b/src/audio/audio_engine.cpp @@ -288,11 +288,77 @@ bool AudioEngine::playSound2D(const std::vector& wavData, float volume, } // Track this sound for cleanup (decoded PCM shared across plays) - activeSounds_.push_back({sound, audioBuffer, decoded.pcmData}); + activeSounds_.push_back({sound, audioBuffer, decoded.pcmData, 0u}); return true; } +uint32_t AudioEngine::playSound2DStoppable(const std::vector& wavData, float volume) { + if (!initialized_ || !engine_ || wavData.empty()) return 0; + if (masterVolume_ <= 0.0f) return 0; + + DecodedWavCacheEntry decoded; + if (!decodeWavCached(wavData, decoded) || !decoded.pcmData || decoded.frames == 0) return 0; + + ma_audio_buffer_config bufferConfig = ma_audio_buffer_config_init( + decoded.format, decoded.channels, decoded.frames, decoded.pcmData->data(), nullptr); + bufferConfig.sampleRate = decoded.sampleRate; + + ma_audio_buffer* audioBuffer = static_cast(std::malloc(sizeof(ma_audio_buffer))); + if (!audioBuffer) return 0; + if (ma_audio_buffer_init(&bufferConfig, audioBuffer) != MA_SUCCESS) { + std::free(audioBuffer); + return 0; + } + + ma_sound* sound = static_cast(std::malloc(sizeof(ma_sound))); + if (!sound) { + ma_audio_buffer_uninit(audioBuffer); + std::free(audioBuffer); + return 0; + } + ma_result 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_NO_SPATIALIZATION, + nullptr, sound); + if (result != MA_SUCCESS) { + ma_audio_buffer_uninit(audioBuffer); + std::free(audioBuffer); + std::free(sound); + return 0; + } + + ma_sound_set_volume(sound, volume); + if (ma_sound_start(sound) != MA_SUCCESS) { + ma_sound_uninit(sound); + ma_audio_buffer_uninit(audioBuffer); + std::free(audioBuffer); + std::free(sound); + return 0; + } + + uint32_t id = nextSoundId_++; + if (nextSoundId_ == 0) nextSoundId_ = 1; // Skip 0 (sentinel) + activeSounds_.push_back({sound, audioBuffer, decoded.pcmData, id}); + return id; +} + +void AudioEngine::stopSound(uint32_t id) { + if (id == 0) return; + for (auto it = activeSounds_.begin(); it != activeSounds_.end(); ++it) { + if (it->id == id) { + ma_sound_stop(it->sound); + ma_sound_uninit(it->sound); + std::free(it->sound); + ma_audio_buffer* buffer = static_cast(it->buffer); + ma_audio_buffer_uninit(buffer); + std::free(buffer); + activeSounds_.erase(it); + return; + } + } +} + bool AudioEngine::playSound2D(const std::string& mpqPath, float volume, float pitch) { if (!assetManager_) { LOG_WARNING("AudioEngine::playSound2D(path): no AssetManager set"); diff --git a/src/audio/spell_sound_manager.cpp b/src/audio/spell_sound_manager.cpp index 4c024b88..c72f6d7c 100644 --- a/src/audio/spell_sound_manager.cpp +++ b/src/audio/spell_sound_manager.cpp @@ -220,12 +220,22 @@ void SpellSoundManager::playPrecast(MagicSchool school, SpellPower power) { return; } - if (library) { - playSound(*library); + if (library && !library->empty() && (*library)[0].loaded) { + stopPrecast(); // Stop any previous precast still playing + float volume = 0.75f * volumeScale_; + activePrecastId_ = AudioEngine::instance().playSound2DStoppable((*library)[0].data, volume); + } +} + +void SpellSoundManager::stopPrecast() { + if (activePrecastId_ != 0) { + AudioEngine::instance().stopSound(activePrecastId_); + activePrecastId_ = 0; } } void SpellSoundManager::playCast(MagicSchool school) { + stopPrecast(); // Ensure precast doesn't overlap the cast sound switch (school) { case MagicSchool::FIRE: playSound(castFireSounds_); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 5146e98d..7d63d4db 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2516,6 +2516,11 @@ void GameHandler::handlePacket(network::Packet& packet) { // Spell failed mid-cast casting = false; currentCastSpellId = 0; + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* ssm = renderer->getSpellSoundManager()) { + ssm->stopPrecast(); + } + } break; case Opcode::SMSG_SPELL_COOLDOWN: handleSpellCooldown(packet); @@ -12368,6 +12373,13 @@ void GameHandler::handleCastFailed(network::Packet& packet) { currentCastSpellId = 0; castTimeRemaining = 0.0f; + // Stop precast sound — spell failed before completing + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* ssm = renderer->getSpellSoundManager()) { + ssm->stopPrecast(); + } + } + // Add system message about failed cast with readable reason int powerType = -1; auto playerEntity = entityManager.getEntity(playerGuid);