diff --git a/src/xrEngine/device.cpp b/src/xrEngine/device.cpp index b161e00a9e..12d32dbef8 100644 --- a/src/xrEngine/device.cpp +++ b/src/xrEngine/device.cpp @@ -121,7 +121,9 @@ void CRenderDevice::End(void) #endif // #ifdef INGAME_EDITOR if (dwPrecacheFrame) { - ::Sound->set_master_volume(0.f); + // Level precache mutes the OpenAL listener; skip while persistent PDA/music plays + if (::Sound && !::Sound->has_playing_persistent()) + ::Sound->set_master_volume(0.f); dwPrecacheFrame--; if (!dwPrecacheFrame) @@ -366,6 +368,11 @@ void CRenderDevice::on_idle() if (g_loading_events.front()()) g_loading_events.pop_front(); } + // Keep listener snapshot fresh; SoundRender_UpdateThread refills OpenAL buffers +#ifndef DEDICATED_SERVER + if (::Sound) + ::Sound->update(Device.vCameraPosition, Device.vCameraDirection, Device.vCameraTop); +#endif PROF_EVENT("LoadDraw"); pApp->LoadDraw(); return; diff --git a/src/xrEngine/x_ray.cpp b/src/xrEngine/x_ray.cpp index e8f247476a..543ffd851e 100644 --- a/src/xrEngine/x_ray.cpp +++ b/src/xrEngine/x_ray.cpp @@ -1395,8 +1395,8 @@ CApplication::CApplication() // Register us Device.seqFrame.Add(this, REG_PRIORITY_HIGH + 1000); - if (psDeviceFlags.test(mtSound)) Device.seqFrameMT.Add(&SoundProcessor); - else Device.seqFrame.Add(&SoundProcessor); + // OpenAL updates run on SoundRender_UpdateThread; main thread only refreshes listener snapshot + Device.seqFrame.Add(&SoundProcessor); Console->Show(); @@ -1414,7 +1414,6 @@ CApplication::~CApplication() // font xr_delete(pFontSystem); - Device.seqFrameMT.Remove(&SoundProcessor); Device.seqFrame.Remove(&SoundProcessor); Device.seqFrame.Remove(this); diff --git a/src/xrEngine/xr_ioc_cmd.cpp b/src/xrEngine/xr_ioc_cmd.cpp index e0b65ee369..89796cc49e 100644 --- a/src/xrEngine/xr_ioc_cmd.cpp +++ b/src/xrEngine/xr_ioc_cmd.cpp @@ -12,6 +12,7 @@ #include "CustomHUD.h" #include "../Include/xrRender/RenderDeviceRender.h" +#include "../xrSound/Sound.h" #include "xr_object.h" #include "MonitorList.h" @@ -599,6 +600,18 @@ class CCC_SND_Restart : public IConsole_Command } }; +class CCC_SndStopPersistent : public IConsole_Command +{ +public: + CCC_SndStopPersistent(LPCSTR N) : IConsole_Command(N) { bEmptyArgsHandled = TRUE; }; + + virtual void Execute(LPCSTR args) + { + if (Sound) + Sound->stop_persistent_emitters(); + } +}; + //----------------------------------------------------------------------- float ps_gamma = 1.f, ps_brightness = 1.f, ps_contrast = 1.f; @@ -953,6 +966,19 @@ class CCC_SoundParamsSmoothing : public CCC_Integer } }; +class CCC_SndMT : public CCC_Mask +{ +public: + CCC_SndMT(LPCSTR N, Flags32* V, u32 M) : CCC_Mask(N, V, M) {}; + + virtual void Execute(LPCSTR args) + { + CCC_Mask::Execute(args); + if (Sound) + Sound->set_thread_enabled(GetValue() != FALSE); + } +}; + class CCC_Editor : public IConsole_Command { public: @@ -1022,6 +1048,7 @@ void CCC_Register() CMD1(CCC_LoadCFG, "cfg_load"); CMD3(CCC_Mask, "mt_particles", &psDeviceFlags, mtParticles); + CMD3(CCC_SndMT, "mt_sound", &psDeviceFlags, mtSound); #ifdef DEBUG CMD1(CCC_MotionsStat, "stat_motions"); @@ -1038,7 +1065,6 @@ void CCC_Register() CMD1(CCC_DbgStrCheck, "dbg_str_check"); CMD1(CCC_DbgStrDump, "dbg_str_dump"); - CMD3(CCC_Mask, "mt_sound", &psDeviceFlags, mtSound); CMD3(CCC_Mask, "mt_physics", &psDeviceFlags, mtPhysics); CMD3(CCC_Mask, "mt_network", &psDeviceFlags, mtNetwork); @@ -1118,6 +1144,7 @@ void CCC_Register() CMD2(CCC_Float, "snd_volume_eff", &psSoundVEffects); CMD2(CCC_Float, "snd_volume_music", &psSoundVMusic); CMD1(CCC_SND_Restart, "snd_restart"); + CMD1(CCC_SndStopPersistent, "snd_stop_persistent"); CMD3(CCC_Mask, "snd_acceleration", &psSoundFlags, ss_Hardware); CMD3(CCC_Mask, "snd_efx", &psSoundFlags, ss_EFX); CMD4(CCC_Float, "snd_efx_environment_change_time", &snd_efx_environment_change_time, 0.f, 3.f); diff --git a/src/xrGame/script_sound.cpp b/src/xrGame/script_sound.cpp index c44ad35ad1..2da3ef3a21 100644 --- a/src/xrGame/script_sound.cpp +++ b/src/xrGame/script_sound.cpp @@ -12,6 +12,7 @@ #include "gameobject.h" #include "ai_space.h" #include "script_engine.h" +#include "../xrSound/Sound.h" CScriptSound::CScriptSound(LPCSTR caSoundName, ESoundTypes sound_type) { @@ -50,18 +51,37 @@ Fvector CScriptSound::GetPosition() const } } +void CScriptSound::apply_pending_persistent() +{ + CSound_emitter* emitter = active_emitter(false); + if (!m_bPersistentPending || !emitter) + return; + emitter->set_persistent_in_menu(m_bPersistentInMenuPending); + emitter->set_persistent(true); + m_bPersistentPending = false; +} + +CSound_emitter* CScriptSound::active_emitter(bool reconcile) +{ + if (m_sound._feedback()) + return m_sound._feedback(); + if (reconcile) + m_sound.reconcile_feedback(); + return m_sound._feedback(); +} + void CScriptSound::Play(CScriptGameObject* object, float delay, int flags) { THROW3(m_sound._handle(), "There is no sound", *m_caSoundToPlay); - // Msg ("%6d : CScriptSound::Play (%s), delay %f, flags %d",Device.dwTimeGlobal,m_sound._handle()->file_name(),delay,flags); m_sound.play((object) ? &object->object() : NULL, flags, delay); + apply_pending_persistent(); } void CScriptSound::PlayAtPos(CScriptGameObject* object, const Fvector& position, float delay, int flags) { THROW3(m_sound._handle(), "There is no sound", *m_caSoundToPlay); - // Msg ("%6d : CScriptSound::Play (%s), delay %f, flags %d",m_sound._handle()->file_name(),delay,flags); m_sound.play_at_pos((object) ? &object->object() : NULL, position, flags, delay); + apply_pending_persistent(); } void CScriptSound::PlayNoFeedback(CScriptGameObject* object, u32 flags/*!< Looping */, float delay/*!< Delay */, @@ -69,4 +89,45 @@ void CScriptSound::PlayNoFeedback(CScriptGameObject* object, u32 flags/*!< Loopi { THROW3(m_sound._handle(), "There is no sound", *m_caSoundToPlay); m_sound.play_no_feedback((object) ? &object->object() : NULL, flags, delay, &pos, &vol, &freq); + apply_pending_persistent(); +} + +void CScriptSound::set_persistent(bool bPersist) +{ + set_persistent(bPersist, m_bPersistentInMenuPending); +} + +void CScriptSound::set_persistent(bool bPersist, bool bPersistInMenu) +{ + m_bPersistentInMenuPending = bPersistInMenu; + CSound_emitter* emitter = active_emitter(); + if (!emitter) + { + m_bPersistentPending = bPersist; + if (!bPersist && Sound && m_sound._p) + Sound->stop_emitters_for_owner(m_sound._p._get()); + return; + } + emitter->set_persistent_in_menu(bPersistInMenu); + emitter->set_persistent(bPersist); + m_bPersistentPending = false; +} + +void CScriptSound::Stop() +{ + m_sound.stop(); +} + +bool CScriptSound::is_persistent() const +{ + if (CSound_emitter* emitter = m_sound._feedback()) + return emitter->is_persistent(); + return m_bPersistentPending; +} + +bool CScriptSound::is_persistent_in_menu() const +{ + if (CSound_emitter* emitter = m_sound._feedback()) + return emitter->is_persistent_in_menu(); + return m_bPersistentInMenuPending; } diff --git a/src/xrGame/script_sound.h b/src/xrGame/script_sound.h index 4b661b7bea..d4925cd021 100644 --- a/src/xrGame/script_sound.h +++ b/src/xrGame/script_sound.h @@ -12,6 +12,7 @@ #include "ai_sounds.h" class CScriptGameObject; +class CSound_emitter; class CScriptSound { @@ -33,7 +34,7 @@ class CScriptSound void PlayNoFeedback(CScriptGameObject* object, u32 flags/*!< Looping */, float delay/*!< Delay */, Fvector pos, float vol, float freq); IC void AttachTail(LPCSTR caSoundName); - IC void Stop(); + void Stop(); IC void StopDeffered(); IC void SetPosition(const Fvector& position); IC void SetFrequency(float frequency); @@ -48,6 +49,16 @@ class CScriptSound IC const float GetMaxDistance() const; IC const float GetVolume() const; IC bool IsPlaying() const; + void set_persistent(bool bPersist); + void set_persistent(bool bPersist, bool bPersistInMenu); + bool is_persistent() const; + bool is_persistent_in_menu() const; + +private: + void apply_pending_persistent(); + CSound_emitter* active_emitter(bool reconcile = true); + bool m_bPersistentPending = false; + bool m_bPersistentInMenuPending = true; DECLARE_SCRIPT_REGISTER_FUNCTION }; diff --git a/src/xrGame/script_sound_inline.h b/src/xrGame/script_sound_inline.h index 28aa102357..00236d4465 100644 --- a/src/xrGame/script_sound_inline.h +++ b/src/xrGame/script_sound_inline.h @@ -74,7 +74,7 @@ IC bool CScriptSound::IsPlaying() const { // commented for comfort work with -nosound command line option // VERIFY (m_sound._handle()); - return (!!m_sound._feedback()); + return m_sound.has_playing_emitter(); } IC void CScriptSound::AttachTail(LPCSTR caSoundName) @@ -82,12 +82,6 @@ IC void CScriptSound::AttachTail(LPCSTR caSoundName) m_sound.attach_tail(caSoundName); } -IC void CScriptSound::Stop() -{ - VERIFY(m_sound._handle()); - m_sound.stop(); -} - IC void CScriptSound::StopDeffered() { VERIFY(m_sound._handle()); diff --git a/src/xrGame/script_sound_script.cpp b/src/xrGame/script_sound_script.cpp index e28ccb4929..37bc3ace6a 100644 --- a/src/xrGame/script_sound_script.cpp +++ b/src/xrGame/script_sound_script.cpp @@ -54,5 +54,9 @@ void CScriptSound::script_register(lua_State* L) .def("playing", &CScriptSound::IsPlaying) .def("length", &CScriptSound::Length) .def("attach_tail", &CScriptSound::AttachTail) + .def("set_persistent", (void (CScriptSound::*)(bool))(&CScriptSound::set_persistent)) + .def("set_persistent", (void (CScriptSound::*)(bool, bool))(&CScriptSound::set_persistent)) + .def("is_persistent", &CScriptSound::is_persistent) + .def("is_persistent_in_menu", &CScriptSound::is_persistent_in_menu) ]; } diff --git a/src/xrSound/Sound.h b/src/xrSound/Sound.h index 1c14f68c20..e650f8fdf9 100644 --- a/src/xrSound/Sound.h +++ b/src/xrSound/Sound.h @@ -45,7 +45,6 @@ XRSOUND_API extern float psSpeedOfSound; XRSOUND_API extern int psSoundCacheSizeMB; XRSOUND_API extern xr_token* snd_devices_token; XRSOUND_API extern xr_string snd_device_name; - // reverb overwrite extern BOOL reverb_overwrite; @@ -213,6 +212,8 @@ struct ref_sound IC void stop(); IC void stop_deffered(); + IC void reconcile_feedback(); + IC bool has_playing_emitter() const; IC void set_position(const Fvector& pos); IC void set_frequency(float freq); IC void set_range(float min, float max); @@ -333,6 +334,12 @@ class XRSOUND_API CSound_emitter virtual void stop(BOOL bDeffered) = 0; virtual const CSound_params* get_params() = 0; virtual u32 play_time() = 0; + + // Survive level unload (implementation forces 2D when enabled). + virtual void set_persistent(bool bPersist) = 0; + virtual bool is_persistent() const = 0; + virtual void set_persistent_in_menu(bool bPersistInMenu) = 0; + virtual bool is_persistent_in_menu() const = 0; }; /// definition (Sound Stream Interface) @@ -411,6 +418,13 @@ class XRSOUND_API CSound_manager_interface virtual void clone(ref_sound& S, const ref_sound& from, esound_type sound_type, int game_type) = 0; virtual void destroy(ref_sound& S) = 0; virtual void stop_emitters() = 0; + virtual void stop_persistent_emitters() = 0; // force-stops ALL persistent emitters + virtual void stop_emitters_for_owner(ref_sound_data* owner) = 0; + virtual bool has_playing_emitter_for_owner(ref_sound_data* owner) const = 0; + virtual bool reconcile_emitter_feedback(ref_sound_data* owner) = 0; + virtual bool has_playing_persistent() const = 0; + virtual void set_thread_enabled(bool enabled) = 0; // mt_sound: run OpenAL updates on the worker thread (1) or the main thread (0) + virtual bool thread_enabled() const = 0; virtual int pause_emitters(bool val) = 0; virtual void play(ref_sound& S, CObject* O, u32 flags = 0, float delay = 0.f) = 0; @@ -535,10 +549,31 @@ IC void ref_sound::set_priority(float p) if (_feedback()) _feedback()->set_priority(p); } +IC void ref_sound::reconcile_feedback() +{ + if (!_p || _p->feedback || !::Sound) + return; + VERIFY(!::Sound->i_locked()); + ::Sound->reconcile_emitter_feedback(_p._get()); +} + +IC bool ref_sound::has_playing_emitter() const +{ + if (!_p) + return false; + if (_p->feedback) + return true; + return ::Sound && ::Sound->has_playing_emitter_for_owner(_p._get()); +} + IC void ref_sound::stop() { VERIFY(!::Sound->i_locked()); - if (_feedback()) _feedback()->stop(FALSE); + reconcile_feedback(); + if (_feedback()) + _feedback()->stop(FALSE); + else if (_p && ::Sound) + ::Sound->stop_emitters_for_owner(_p._get()); } IC void ref_sound::stop_deffered() diff --git a/src/xrSound/SoundRender_Core.cpp b/src/xrSound/SoundRender_Core.cpp index 79cfd90970..98de936a32 100644 --- a/src/xrSound/SoundRender_Core.cpp +++ b/src/xrSound/SoundRender_Core.cpp @@ -6,6 +6,8 @@ #include "SoundRender_Core.h" #include "SoundRender_Source.h" #include "SoundRender_Emitter.h" +#include "SoundRender_CoreA.h" +#include "../xrCore/_math.h" #include "NotificationClient.h" @@ -47,6 +49,11 @@ CSoundRender_Core::CSoundRender_Core() bListenerMoved = FALSE; bReady = FALSE; bLocked = FALSE; + m_bUpdateThreadRun = FALSE; + m_bUpdateThreadExited = TRUE; + m_snap_P.set(0, 0, 0); + m_snap_D.set(0, 0, 1); + m_snap_N.set(0, 1, 0); fTimer_Value = Timer.GetElapsed_sec(); fTimer_Delta = 0.0f; m_iPauseCounter = 1; @@ -55,6 +62,7 @@ CSoundRender_Core::CSoundRender_Core() CSoundRender_Core::~CSoundRender_Core() { + update_thread_stop(); #ifdef _EDITOR ETOOLS::destroy_model (geom_ENV); ETOOLS::destroy_model (geom_SOM); @@ -95,25 +103,198 @@ void CSoundRender_Core::_clear() cache.destroy(); env_unload(); - // remove sources - for (auto& kv : s_sources) - xr_delete(kv.second); - - s_sources.clear(); - - // remove emmiters + // remove non-persistent emitters; rescue persistent ones + xr_vector survivors; + xr_vector persistent_sources; for (u32 eit = 0; eit < s_emitters.size(); eit++) - xr_delete(s_emitters[eit]); + { + CSoundRender_Emitter* E = s_emitters[eit]; + if (E->is_persistent()) + { + // Detach from its current target — the target pool is about + // to be destroyed too. The emitter enters "simulating" mode + // so the FSM keeps ticking even without a render target. + if (E->target) + E->cancel(); // switches to stSimulating/stSimulatingLooped, releases target + + survivors.push_back(E); + if (E->owner_data && E->owner_data->handle) + { + CSoundRender_Source* src = (CSoundRender_Source*)E->owner_data->handle; + bool already_saved = false; + for (u32 i = 0; i < persistent_sources.size(); ++i) + { + if (persistent_sources[i] == src) + { + already_saved = true; + break; + } + } + if (!already_saved) + persistent_sources.push_back(src); + } + } + else + { + // Ensure script-side ref_sound no longer points to this emitter. + // Without this, gameplay sound owners can keep dangling feedback + // pointers after level transition and crash on next update. + E->stop(FALSE); + xr_delete(E); + } + } s_emitters.clear(); + for (u32 i = 0; i < survivors.size(); ++i) + { + CSoundRender_Emitter* E = survivors[i]; + s_emitters.push_back(E); + if (E->owner_data) + reconcile_emitter_feedback(E->owner_data._get()); + } + + // Remove sources not used by persistent survivors. + xr_unordered_map kept_sources; + for (auto& kv : s_sources) + { + bool keep = false; + for (u32 i = 0; i < persistent_sources.size(); ++i) + { + if (persistent_sources[i] == kv.second) + { + keep = true; + break; + } + } + if (keep) + kept_sources[kv.first] = kv.second; + else + xr_delete(kv.second); + } + s_sources.clear(); + s_sources = std::move(kept_sources); g_target_temp_data.clear(); g_target_temp_data_16.clear(); } +void CSoundRender_Core::update(const Fvector& P, const Fvector& D, const Fvector& N) +{ + m_snap_P = P; + m_snap_D = D; + m_snap_N = N; + + if (use_background_update()) + return; + + sound_api_enter(); + update_impl(P, D, N); + sound_api_leave(); +} + void CSoundRender_Core::stop_emitters() { + sound_api_enter(); for (u32 eit = 0; eit < s_emitters.size(); eit++) - s_emitters[eit]->stop(FALSE); + { + CSoundRender_Emitter* E = s_emitters[eit]; + if (!E->is_persistent()) + E->stop(FALSE); + } + sound_api_leave(); +} +// Force-stop all persistent emitters (use at main menu / full game quit). +void CSoundRender_Core::stop_persistent_emitters() +{ + sound_api_enter(); + for (u32 eit = 0; eit < s_emitters.size(); eit++) + { + CSoundRender_Emitter* E = s_emitters[eit]; + if (E->is_persistent()) + { + E->set_persistent(false); // release anchor + disarm + E->stop(FALSE); + } + } + sound_api_leave(); +} + +bool CSoundRender_Core::emitter_belongs_to_owner(CSoundRender_Emitter* E, ref_sound_data* owner) +{ + return owner && E && E->owner_data && E->owner_data._get() == owner; +} + +CSoundRender_Emitter* CSoundRender_Core::find_emitter_for_owner(ref_sound_data* owner, bool playing_only) const +{ + if (!owner) + return nullptr; + + for (u32 it = 0; it < s_emitters.size(); it++) + { + CSoundRender_Emitter* E = s_emitters[it]; + if (emitter_belongs_to_owner(E, owner) && (!playing_only || E->isPlaying())) + return E; + } + return nullptr; +} + +void CSoundRender_Core::stop_emitters_for_owner(ref_sound_data* owner) +{ + if (!owner) + return; + + sound_api_enter(); + for (u32 it = 0; it < s_emitters.size(); it++) + { + CSoundRender_Emitter* E = s_emitters[it]; + if (!emitter_belongs_to_owner(E, owner)) + continue; + if (E->is_persistent()) + E->set_persistent(false); + E->stop(FALSE); + } + sound_api_leave(); +} + +bool CSoundRender_Core::has_playing_emitter_for_owner(ref_sound_data* owner) const +{ + return find_emitter_for_owner(owner, true) != nullptr; +} + +bool CSoundRender_Core::reconcile_emitter_feedback(ref_sound_data* owner) +{ + if (!owner) + return false; + + sound_api_enter(); + CSoundRender_Emitter* E = find_emitter_for_owner(owner, true); + if (!E && owner->handle) + { + // Level transition can recreate Lua sound objects (new owner pointer) while + // a persistent emitter keeps playing. Reattach by source handle so control + // (stop/volume/state) continues to work after load without restarting audio. + for (u32 it = 0; it < s_emitters.size(); it++) + { + CSoundRender_Emitter* candidate = s_emitters[it]; + if (!candidate || !candidate->is_persistent() || !candidate->isPlaying()) + continue; + if (!candidate->owner_data || candidate->owner_data->handle != owner->handle) + continue; + + ref_sound_data_ptr prev_owner = candidate->owner_data; + if (prev_owner && prev_owner._get() != owner) + prev_owner->feedback = nullptr; + + release_persistent(candidate); + candidate->owner_data = owner; + anchor_persistent(candidate); + E = candidate; + break; + } + } + if (E) + owner->feedback = E; + sound_api_leave(); + return E != nullptr; } void CSoundRender_Core::restart_emitters() @@ -123,15 +304,120 @@ void CSoundRender_Core::restart_emitters() i_start(s_emitters[eit]); } +void CSoundRender_Core::set_thread_enabled(bool enabled) +{ + // Toggle the dedicated sound thread at runtime. When disabled the main thread + // performs the OpenAL updates directly (original, single-threaded behaviour). + if (enabled) + { + if (bPresent && bReady) + update_thread_start(); + } + else + update_thread_stop(); +} + +bool CSoundRender_Core::thread_enabled() const +{ + return m_bUpdateThreadRun != FALSE; +} + +bool CSoundRender_Core::use_background_update() const +{ + // Dedicated sound thread owns OpenAL buffer updates whenever it is running + return m_bUpdateThreadRun != FALSE; +} + +void CSoundRender_Core::sound_api_enter() +{ + m_api_cs.Enter(); + if (SoundRenderA) + SoundRenderA->bind_context(); +} + +void CSoundRender_Core::sound_api_leave() +{ + m_api_cs.Leave(); +} + +void CSoundRender_Core::update_thread_start() +{ + if (m_bUpdateThreadRun) + return; + m_bUpdateThreadExited = FALSE; + m_bUpdateThreadRun = TRUE; + thread_spawn(SoundRender_UpdateThread, "X-Ray Sound Update", 0, nullptr); +} + +void CSoundRender_Core::update_thread_stop() +{ + if (!m_bUpdateThreadRun) + return; + m_bUpdateThreadRun = FALSE; + // Wait until the worker has finished its current iteration and exited, so the + // caller (e.g. _clear) can safely tear down OpenAL targets/context afterwards. + while (!m_bUpdateThreadExited) + Sleep(1); +} + +bool CSoundRender_Core::has_playing_persistent() const +{ + for (u32 it = 0; it < s_emitters.size(); it++) + { + CSoundRender_Emitter* E = s_emitters[it]; + if (E->is_persistent() && E->isPlaying()) + return true; + } + return false; +} + int CSoundRender_Core::pause_emitters(bool val) { + sound_api_enter(); m_iPauseCounter += val ? +1 : -1; VERIFY(m_iPauseCounter>=0); for (u32 it = 0; it < s_emitters.size(); it++) - ((CSoundRender_Emitter*)s_emitters[it])->pause(val, val ? m_iPauseCounter : m_iPauseCounter + 1); + { + CSoundRender_Emitter* E = (CSoundRender_Emitter*)s_emitters[it]; + if (E->is_persistent() && E->is_persistent_in_menu()) + continue; + E->pause(val, val ? m_iPauseCounter : m_iPauseCounter + 1); + } + + const int counter = m_iPauseCounter; + sound_api_leave(); + return counter; +} + +// Called when an emitter is marked persistent. +// The core grabs a strong ref to owner_data so the sound survives Lua GC. +void CSoundRender_Core::anchor_persistent(CSoundRender_Emitter* E) +{ + if (!E || !E->owner_data) + return; + + // Only add once + for (const auto& ref : s_persistent_refs) + if (ref._get() == E->owner_data._get()) + return; - return m_iPauseCounter; + s_persistent_refs.push_back(E->owner_data); +} + +// Called when an emitter is un-marked persistent. +// The core drops its strong ref; the emitter is now mortal again. +void CSoundRender_Core::release_persistent(CSoundRender_Emitter* E) +{ + if (!E || !E->owner_data) + return; + + auto it = std::find_if( + s_persistent_refs.begin(), s_persistent_refs.end(), + [E](const ref_sound_data_ptr& p) { return p._get() == E->owner_data._get(); } + ); + if (it != s_persistent_refs.end()) + s_persistent_refs.erase(it); } void CSoundRender_Core::env_load() @@ -337,7 +623,9 @@ void CSoundRender_Core::clone(ref_sound& S, const ref_sound& from, esound_type s void CSoundRender_Core::play(ref_sound& S, CObject* O, u32 flags, float delay) { if (!bPresent || (0==S._handle())) return; + sound_api_enter(); S._p->g_object = O; + S.reconcile_feedback(); if (S._feedback()) ((CSoundRender_Emitter*)S._feedback())->rewind(); else i_play(&S, flags & sm_Looped, delay); @@ -348,11 +636,13 @@ void CSoundRender_Core::play(ref_sound& S, CObject* O, u32 flags, float delay) { S._feedback()->switch_to_Intro(); } + sound_api_leave(); } void CSoundRender_Core::play_no_feedback(ref_sound& S, CObject* O, u32 flags, float delay, Fvector* pos, float* vol, float* freq, Fvector2* range) { if (!bPresent || (0 == S._handle())) return; + sound_api_enter(); ref_sound_data_ptr orig = S._p; S._p = xr_new(); S._p->handle = orig->handle; @@ -378,12 +668,15 @@ void CSoundRender_Core::play_no_feedback(ref_sound& S, CObject* O, u32 flags, fl if (range) S._feedback()->set_range((*range)[0], (*range)[1]); if (vol) S._feedback()->set_volume(*vol); S._p = orig; + sound_api_leave(); } void CSoundRender_Core::play_at_pos(ref_sound& S, CObject* O, const Fvector &pos, u32 flags, float delay) { if (!bPresent || (0 == S._handle())) return; + sound_api_enter(); S._p->g_object = O; + S.reconcile_feedback(); if (S._feedback()) ((CSoundRender_Emitter*)S._feedback())->rewind(); else i_play(&S, flags & sm_Looped, delay); @@ -395,16 +688,21 @@ void CSoundRender_Core::play_at_pos(ref_sound& S, CObject* O, const Fvector &pos { S._feedback()->switch_to_Intro(); } + sound_api_leave(); } void CSoundRender_Core::destroy(ref_sound& S) { + sound_api_enter(); if (S._feedback()) { CSoundRender_Emitter* E = (CSoundRender_Emitter*)S._feedback(); - E->stop(FALSE); + if (!E->is_persistent()) + E->stop(FALSE); + // else: let it keep playing; the core holds ownership } S._p = 0; + sound_api_leave(); } void CSoundRender_Core::_create_data(ref_sound_data& S, LPCSTR fName, esound_type sound_type, int game_type) @@ -427,9 +725,15 @@ void CSoundRender_Core::_destroy_data(ref_sound_data& S) if (S.feedback) { CSoundRender_Emitter* E = (CSoundRender_Emitter*)S.feedback; - E->stop(FALSE); + if (!E->is_persistent()) + E->stop(FALSE); + // For persistent emitters: do NOT stop. The core holds a strong + // ref in s_persistent_refs so owner_data stays alive; the emitter + // keeps playing. The script-side ref_sound_data* is going away but + // the emitter's owner_data ptr still points to the core-held copy. } - R_ASSERT(0==S.feedback); + if (!S.feedback || !((CSoundRender_Emitter*)S.feedback)->is_persistent()) + R_ASSERT(0 == S.feedback); SoundRender->i_destroy_source((CSoundRender_Source*)S.handle); S.handle = NULL; @@ -437,7 +741,7 @@ void CSoundRender_Core::_destroy_data(ref_sound_data& S) CSoundRender_Environment* CSoundRender_Core::get_environment(const Fvector& P) { - PROF_EVENT(); + PROF_EVENT("CSoundRender_Core::get_environment"); static CSoundRender_Environment identity; if (bUserEnvironment) diff --git a/src/xrSound/SoundRender_Core.h b/src/xrSound/SoundRender_Core.h index 97aaae02c7..678a49d7aa 100644 --- a/src/xrSound/SoundRender_Core.h +++ b/src/xrSound/SoundRender_Core.h @@ -3,6 +3,7 @@ #include "SoundRender.h" #include "SoundRender_Environment.h" #include "SoundRender_Cache.h" +#include "../xrCore/xrSyncronize.h" class CNotificationClient; @@ -56,6 +57,28 @@ class CSoundRender_Core : public CSound_manager_interface CSoundRender_Environment s_user_environment; int m_iPauseCounter; + + // Persistent sound support + // One entry per persistent emitter: keeps owner_data alive across level GC. + xr_vector s_persistent_refs; + + // Background OpenAL update (level load blocks main thread for seconds at a time) + xrCriticalSection m_api_cs; + volatile BOOL m_bUpdateThreadRun; + volatile BOOL m_bUpdateThreadExited; + Fvector m_snap_P; + Fvector m_snap_D; + Fvector m_snap_N; + + void update_impl(const Fvector& P, const Fvector& D, const Fvector& N); + void sound_api_enter(); + void sound_api_leave(); + bool use_background_update() const; + + static bool emitter_belongs_to_owner(CSoundRender_Emitter* E, ref_sound_data* owner); + CSoundRender_Emitter* find_emitter_for_owner(ref_sound_data* owner, bool playing_only) const; + + friend void SoundRender_UpdateThread(void*); public: // Cache CSoundRender_Cache cache; @@ -82,6 +105,17 @@ class CSoundRender_Core : public CSound_manager_interface virtual void stop_emitters(); virtual void restart_emitters(); virtual int pause_emitters(bool val); + virtual void stop_persistent_emitters() override; + virtual void stop_emitters_for_owner(ref_sound_data* owner) override; + virtual bool has_playing_emitter_for_owner(ref_sound_data* owner) const override; + virtual bool reconcile_emitter_feedback(ref_sound_data* owner) override; + virtual bool has_playing_persistent() const override; + virtual void set_thread_enabled(bool enabled) override; + virtual bool thread_enabled() const override; + + // Called by CSoundRender_Emitter::set_persistent + void anchor_persistent(CSoundRender_Emitter* E); + void release_persistent(CSoundRender_Emitter* E); virtual void play(ref_sound& S, CObject* O, u32 flags = 0, float delay = 0.f); virtual void play_at_pos(ref_sound& S, CObject* O, const Fvector& pos, u32 flags = 0, float delay = 0.f); @@ -93,7 +127,9 @@ class CSoundRender_Core : public CSound_manager_interface virtual void set_geometry_occ(CDB::MODEL* M); virtual void set_handler(sound_event* E); - virtual void update(const Fvector& P, const Fvector& D, const Fvector& N); + virtual void update(const Fvector& P, const Fvector& D, const Fvector& N) override; + void update_thread_start(); + void update_thread_stop(); virtual void update_events(); virtual void statistic(CSound_stats* dest, CSound_stats_ext* ext); diff --git a/src/xrSound/SoundRender_CoreA.cpp b/src/xrSound/SoundRender_CoreA.cpp index 538f44d1de..8f0e73a947 100644 --- a/src/xrSound/SoundRender_CoreA.cpp +++ b/src/xrSound/SoundRender_CoreA.cpp @@ -7,6 +7,8 @@ #include "../xrEngine/pure.h" #include "../xrEngine/XR_IOConsole.h" +#include "../xrEngine/defines.h" +#include "../xrCore/_math.h" #include @@ -57,6 +59,37 @@ extern CConsole* Console; CSoundRender_CoreA* SoundRenderA = nullptr; +static const u32 SOUND_BG_SLEEP_MS = 20; + +void SoundRender_UpdateThread(void*) +{ + VERIFY(SoundRenderA && SoundRenderA->pContext); + alcMakeContextCurrent(SoundRenderA->pContext); + + while (SoundRender->m_bUpdateThreadRun) + { + Sleep(SOUND_BG_SLEEP_MS); + if (!SoundRender->bReady) + continue; + + SoundRender->sound_api_enter(); + SoundRender->update_impl( + SoundRender->m_snap_P, + SoundRender->m_snap_D, + SoundRender->m_snap_N); + SoundRender->sound_api_leave(); + } + + alcMakeContextCurrent(nullptr); + SoundRender->m_bUpdateThreadExited = TRUE; +} + +void CSoundRender_CoreA::bind_context() +{ + if (pContext && alcGetCurrentContext() != pContext) + alcMakeContextCurrent(pContext); +} + CSoundRender_CoreA::CSoundRender_CoreA() : CSoundRender_Core() { pDevice = nullptr; @@ -505,6 +538,10 @@ void CSoundRender_CoreA::_initialize(int stage) break; } } +#ifndef _EDITOR + if (psDeviceFlags.test(mtSound)) + update_thread_start(); +#endif } } @@ -512,12 +549,15 @@ void CSoundRender_CoreA::set_master_volume(float f) { if (bPresent) { + sound_api_enter(); A_CHK(alListenerf (AL_GAIN,f)); + sound_api_leave(); } } void CSoundRender_CoreA::_clear() { + update_thread_stop(); inherited::_clear(); // remove targets CSoundRender_Target* T = nullptr; diff --git a/src/xrSound/SoundRender_CoreA.h b/src/xrSound/SoundRender_CoreA.h index 7d2052e7f8..8784fb718c 100644 --- a/src/xrSound/SoundRender_CoreA.h +++ b/src/xrSound/SoundRender_CoreA.h @@ -19,6 +19,7 @@ class CSoundRender_CoreA : public CSoundRender_Core { typedef CSoundRender_Core inherited; friend class CNotificationClient; + friend void SoundRender_UpdateThread(void*); ALCdevice* pDevice; ALCcontext* pContext; @@ -98,6 +99,8 @@ class CSoundRender_CoreA : public CSoundRender_Core virtual void set_master_volume(float f); + void bind_context(); + virtual const Fvector& listener_position() { return Listener.position; } // EFX listener diff --git a/src/xrSound/SoundRender_Core_Processor.cpp b/src/xrSound/SoundRender_Core_Processor.cpp index f353887309..e33e626838 100644 --- a/src/xrSound/SoundRender_Core_Processor.cpp +++ b/src/xrSound/SoundRender_Core_Processor.cpp @@ -18,7 +18,7 @@ CSoundRender_Emitter* CSoundRender_Core::i_play(ref_sound* S, BOOL _loop, float return E; } -void CSoundRender_Core::update(const Fvector& P, const Fvector& D, const Fvector& N) +void CSoundRender_Core::update_impl(const Fvector& P, const Fvector& D, const Fvector& N) { u32 it; diff --git a/src/xrSound/SoundRender_Core_SourceManager.cpp b/src/xrSound/SoundRender_Core_SourceManager.cpp index 51938d7c20..8f84b79a87 100644 --- a/src/xrSound/SoundRender_Core_SourceManager.cpp +++ b/src/xrSound/SoundRender_Core_SourceManager.cpp @@ -32,7 +32,7 @@ void CSoundRender_Core::i_destroy_source(CSoundRender_Source* S) void CSoundRender_Core::i_create_all_sources() { - PROF_EVENT(); + PROF_EVENT("CSoundRender_Core::i_create_all_sources"); CTimer T; T.Start(); diff --git a/src/xrSound/SoundRender_Core_StartStop.cpp b/src/xrSound/SoundRender_Core_StartStop.cpp index 8d29954a3e..910541ce5d 100644 --- a/src/xrSound/SoundRender_Core_StartStop.cpp +++ b/src/xrSound/SoundRender_Core_StartStop.cpp @@ -9,24 +9,39 @@ void CSoundRender_Core::i_start(CSoundRender_Emitter* E) { R_ASSERT(E); + if (!E->owner_data || !E->owner_data->handle) + { + Msg("! sound: i_start skipped emitter without valid source handle"); + E->stop(FALSE); + return; + } - // Search lowest-priority target + // Search lowest-priority target (never steal targets from other persistent streams) float Ptest = E->priority(); float Ptarget = flt_max; CSoundRender_Target* T = 0; for (u32 it = 0; it < s_targets.size(); it++) { CSoundRender_Target* Ttest = s_targets[it]; + CSoundRender_Emitter* cur = Ttest->get_emitter(); + if (cur && cur != E && cur->is_persistent()) + continue; if (Ttest->priority < Ptarget) { T = Ttest; Ptarget = Ttest->priority; } } + if (!T) + return; - // Stop currently playing + // Stop currently playing (never cancel persistent emitters) if (T->get_emitter()) - T->get_emitter()->cancel(); + { + CSoundRender_Emitter* victim = T->get_emitter(); + if (victim != E && !victim->is_persistent()) + victim->cancel(); + } // Associate E->target = T; @@ -67,6 +82,9 @@ BOOL CSoundRender_Core::i_allow_play(CSoundRender_Emitter* E) for (u32 it = 0; it < s_targets.size(); it++) { CSoundRender_Target* T = s_targets[it]; + CSoundRender_Emitter* cur = T->get_emitter(); + if (cur && cur != E && cur->is_persistent()) + continue; if (T->priority < Ptest) return TRUE; } return FALSE; diff --git a/src/xrSound/SoundRender_Emitter.cpp b/src/xrSound/SoundRender_Emitter.cpp index da5dfc3f44..d752e34b00 100644 --- a/src/xrSound/SoundRender_Emitter.cpp +++ b/src/xrSound/SoundRender_Emitter.cpp @@ -38,6 +38,8 @@ CSoundRender_Emitter::CSoundRender_Emitter(void) set_cursor(0); bMoved = TRUE; b2D = FALSE; + b_persistent = false; + b_persistent_in_menu = true; bStopping = FALSE; bRewind = FALSE; iPaused = 0; @@ -156,3 +158,25 @@ void CSoundRender_Emitter::move_cursor(int offset) { set_cursor(get_cursor(true) + offset); } + +void CSoundRender_Emitter::set_persistent(bool bPersist) +{ + b_persistent = bPersist; + + if (bPersist) + { + // Persistent sounds MUST be 2D — they have no world position after + // the level is torn down, so spatial rolloff would mute them. + if (!b2D) + switch_to_2D(); + + // Tell the core to anchor this emitter's owner_data so that even + // if the Lua ref_sound_data refcount drops to zero the audio keeps + // playing. The core's s_persistent_refs holds the strong reference. + SoundRender->anchor_persistent(this); + } + else + { + SoundRender->release_persistent(this); + } +} diff --git a/src/xrSound/SoundRender_Emitter.h b/src/xrSound/SoundRender_Emitter.h index 96f5434290..f878ec24dc 100644 --- a/src/xrSound/SoundRender_Emitter.h +++ b/src/xrSound/SoundRender_Emitter.h @@ -31,7 +31,8 @@ class CSoundRender_Emitter : public CSound_emitter private: bool need_preplay_update; - + bool b_persistent; // survives level unload when true + bool b_persistent_in_menu; public: #ifdef DEBUG u32 dbg_ID; @@ -59,6 +60,11 @@ class CSoundRender_Emitter : public CSound_emitter BOOL bMoved; BOOL b2D; bool bIntro; + // CSound_emitter overrides + virtual void set_persistent(bool bPersist) override; + virtual bool is_persistent() const override { return b_persistent; } + virtual void set_persistent_in_menu(bool bPersistInMenu) override { b_persistent_in_menu = bPersistInMenu; } + virtual bool is_persistent_in_menu() const override { return b_persistent_in_menu; } BOOL bStopping; BOOL bRewind; float fTimeStarted; // time of "Start" diff --git a/src/xrSound/SoundRender_Emitter_StartStop.cpp b/src/xrSound/SoundRender_Emitter_StartStop.cpp index 96a56798a5..d36b374827 100644 --- a/src/xrSound/SoundRender_Emitter_StartStop.cpp +++ b/src/xrSound/SoundRender_Emitter_StartStop.cpp @@ -37,6 +37,11 @@ void CSoundRender_Emitter::start(ref_sound* _owner, BOOL _loop, float delay) void CSoundRender_Emitter::i_stop() { bRewind = FALSE; + if (b_persistent) + { + SoundRender->release_persistent(this); + b_persistent = false; + } if (target) SoundRender->i_stop(this); if (owner_data) { diff --git a/src/xrSound/sound.cpp b/src/xrSound/sound.cpp index 273f73af44..330abf2bf6 100644 --- a/src/xrSound/sound.cpp +++ b/src/xrSound/sound.cpp @@ -29,6 +29,8 @@ void CSound_manager_interface::_create(int stage) void CSound_manager_interface::_destroy() { + if (Sound) + Sound->stop_persistent_emitters(); Sound->_clear(); xr_delete(SoundRender); Sound = 0;