diff --git a/rts/Game/Camera.cpp b/rts/Game/Camera.cpp index 8c5ea462a4b..b803215738e 100644 --- a/rts/Game/Camera.cpp +++ b/rts/Game/Camera.cpp @@ -4,6 +4,7 @@ #include "Camera.h" #include "CameraHandler.h" +#include "Game/TraceRay.h" #include "UI/MouseHandler.h" #include "Map/Ground.h" #include "Map/ReadMap.h" @@ -156,6 +157,8 @@ void CCamera::Update(const UpdateParams& p) if (p.updateFrustum) UpdateFrustum(); + TraceToTerrain(); + LoadMatrices(); // not done here // LoadViewPort(); @@ -708,6 +711,24 @@ void CCamera::ClipFrustumLines(const float zmin, const float zmax, bool neg) } } +void CCamera::TraceToTerrain() { + if (camType != CAMTYPE_PLAYER) + return; + + const CUnit* hitUnit = nullptr; + const CFeature* hitFeature = nullptr; + + terrainDistance = TraceRay::GuiTraceRay( + GetPos(), + GetForward(), + 30000, + nullptr, + hitUnit, + hitFeature, + true, + true + ); +} float3 CCamera::GetMoveVectorFromState(bool fromKeyState) const { @@ -716,7 +737,7 @@ float3 CCamera::GetMoveVectorFromState(bool fromKeyState) const if (useInterpolate > 0) camDeltaTime = 1000.0f / std::fmax(globalRendering->FPS, 1.0f); - + float camMoveSpeed = 1.0f; camMoveSpeed *= movState[MOVE_STATE_SLW] ? moveSlowMult : 1.0f; diff --git a/rts/Game/Camera.h b/rts/Game/Camera.h index 7901f2218e2..c0b563490b1 100644 --- a/rts/Game/Camera.h +++ b/rts/Game/Camera.h @@ -202,6 +202,7 @@ class CCamera { float GetNearPlaneDist() const { return frustum.scales.z; } float GetFarPlaneDist() const { return frustum.scales.w; } float GetAspectRatio() const { return aspectRatio; } + float GetTerrainDistance() const { return terrainDistance; } float3 GetMoveVectorFromState(bool fromKeyState) const; @@ -253,6 +254,7 @@ class CCamera { void UpdateDirsFromRot(const float3& r); + void TraceToTerrain(); public: float3 pos; float3 rot; ///< x = inclination, y = azimuth (to the -z axis!), z = roll @@ -309,6 +311,8 @@ class CCamera { uint8_t inViewPlanesMask; + float terrainDistance = 1000; + bool movState[10]; // fwd, back, left, right, up, down, fast, slow, tilt, reset bool rotState[4]; // unused }; diff --git a/rts/System/Sound/AttenuationModels/RtsAttenuationModel.cpp b/rts/System/Sound/AttenuationModels/RtsAttenuationModel.cpp new file mode 100644 index 00000000000..2623973b650 --- /dev/null +++ b/rts/System/Sound/AttenuationModels/RtsAttenuationModel.cpp @@ -0,0 +1,69 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ + +#include "RtsAttenuationModel.h" +#include "Game/Camera.h" +#include "Game/CameraHandler.h" +#include "System/Sound/ISoundAttenuationModel.h" +#include "Game/TraceRay.h" +#include +#include + +SoundAttenuationOutput RtsAttenuationModel::Evaluate(const SoundAttenuationInput& in) const { + + SoundAttenuationOutput out; + + // ----------------------------------------------------------------- + // Convert the soundPosition in to camera space and calculate necessary data + // ----------------------------------------------------------------- + + { + CCamera* playerCamera = CCameraHandler::GetCamera(CCamera::CAMTYPE_PLAYER); + + float terrainDistance = playerCamera->GetTerrainDistance(); + + out.zoomFactor = 1 - std::clamp(terrainDistance == -1 ? 1.0f : terrainDistance / GetForwardAttenuationRange(), 0.0f, 1.0f); + out.tiltFactor = playerCamera->GetForward().dot(float3(0.0f, -1.0f, 0.0f)); + + float3 toSound = in.soundPosition - playerCamera->GetPos(); + + float camForward = playerCamera->GetForward().dot(toSound); + float camRight = playerCamera->GetRight().dot(toSound); + float camUp = playerCamera->GetUp().dot(toSound); + + out.forwardDistance = camForward; + + float hfov = playerCamera->GetHFOV() * math::DEG_TO_RAD; + float vfov = playerCamera->GetVFOV() * math::DEG_TO_RAD; + + float distance = std::abs(camForward); + if (distance <= 0.0f) distance = 1.0f; + + out.frustumWidth = distance * std::tan(hfov * 0.5f); + out.frustumHeight = distance * std::tan(vfov * 0.5f); + + float outsideRight = std::max(0.0f, std::abs(camRight) - out.frustumWidth); + float outsideUp = std::max(0.0f, std::abs(camUp) - out.frustumHeight); + + out.outerDistance = std::sqrt(outsideRight * outsideRight + outsideUp * outsideUp); + } + + // ----------------------------------------------------------------- + // Convert all the positional data to normalized ranges and calculate the final ranges + // ----------------------------------------------------------------- + + { + // Calculate forward attenuation + float forwardValue = std::clamp(out.forwardDistance >= 0 ? + std::lerp(GetMinVolumeAttenuation(), 1.0f, 1.0f - out.forwardDistance / GetForwardAttenuationRange()) : + std::clamp(1.0f - (-out.forwardDistance) / GetBackwardAttenuationRange(), 0.0f, 1.0f), GetMinVolumeAttenuation(), 1.0f); + + // Calculate outer attenuation + float outerValue = std::clamp(std::lerp(GetMinFilterAttenuation(), 1.0f, 1.0f - out.outerDistance / GetOuterAttenuationRange()), GetMinFilterAttenuation(), 1.0f); + + out.volumeFactor = std::pow(forwardValue, 6) * outerValue; + out.filterFactor = std::pow(forwardValue, 1) * outerValue; + } + + return out; +} + diff --git a/rts/System/Sound/AttenuationModels/RtsAttenuationModel.h b/rts/System/Sound/AttenuationModels/RtsAttenuationModel.h new file mode 100644 index 00000000000..3597ef03de2 --- /dev/null +++ b/rts/System/Sound/AttenuationModels/RtsAttenuationModel.h @@ -0,0 +1,22 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ + +#pragma once + +#include "System/Config/ConfigHandler.h" +#include "System/Sound/ISoundAttenuationModel.h" + +class RtsAttenuationModel : public ISoundAttenuationModel +{ +public: + SoundAttenuationOutput Evaluate(const SoundAttenuationInput& in) const override; + +private: + float GetForwardAttenuationRange() const { return configHandler->GetFloat("snd_forwardAttenuationRange"); } + float GetBackwardAttenuationRange() const { return configHandler->GetFloat("snd_backwardAttenuationRange"); } + float GetOuterAttenuationRange() const { return configHandler->GetFloat("snd_outerAttenuationRange"); } + float GetMinVolumeAttenuation() const { return configHandler->GetFloat("snd_minVolumeAttenuation"); } + float GetMinFilterAttenuation() const { return configHandler->GetFloat("snd_minFilterAttenuation"); } +}; + +// "RTS Model: Designed for RTS games with viewport-based attenuation, forward/backward distance handling, off-screen attenuation, and zoom-aware center attenuation. Seen in games like \"Planetary Annihilation\" or \"Supreme Commander 2\"" + diff --git a/rts/System/Sound/CMakeLists.txt b/rts/System/Sound/CMakeLists.txt index a574323e378..8b2c529cbb3 100644 --- a/rts/System/Sound/CMakeLists.txt +++ b/rts/System/Sound/CMakeLists.txt @@ -8,6 +8,7 @@ set(noSoundSources ISound.cpp Null/SoundChannels.cpp Null/NullSound.cpp + AttenuationModels/RtsAttenuationModel.cpp ) add_library(no-sound STATIC EXCLUDE_FROM_ALL ${noSoundSources}) @@ -42,6 +43,7 @@ if (NOT NO_SOUND) OpenAL/SoundItem.cpp OpenAL/SoundSource.cpp OpenAL/VorbisShared.cpp + AttenuationModels/RtsAttenuationModel.cpp ) find_package_static(OpenAL 1.18.2 REQUIRED) diff --git a/rts/System/Sound/ISound.cpp b/rts/System/Sound/ISound.cpp index a8f14595442..ead450c8bd3 100644 --- a/rts/System/Sound/ISound.cpp +++ b/rts/System/Sound/ISound.cpp @@ -40,6 +40,12 @@ CONFIG(float, snd_airAbsorption).defaultValue(0.1f); CONFIG(std::string, snd_device).defaultValue("").description("Sets the used output device. See \"Available Devices\" section in infolog.txt."); +CONFIG(bool, snd_useAttenuationModel).defaultValue(false).description("Use the new AttenuationModel system. Affects how you hear 3D sounds in the game. Recommended: ON"); +CONFIG(float, snd_forwardAttenuationRange).defaultValue(8000).description("Distance in front of the camera over which sound volume and clarity gradually fade."); +CONFIG(float, snd_backwardAttenuationRange).defaultValue(300).description("Distance behind the camera over which sounds are attenuated."); +CONFIG(float, snd_outerAttenuationRange).defaultValue(500).description("How far sounds can be outside the camera view before being strongly attenuated."); +CONFIG(float, snd_minVolumeAttenuation).defaultValue(0.0f).minimumValue(0).maximumValue(1).description("Minimum volume multiplier for attenuated sounds to prevent them from becoming silent. Smaller is quieter"); +CONFIG(float, snd_minFilterAttenuation).defaultValue(0.05f).minimumValue(0).maximumValue(1).description("Minimum clarity level for attenuated sounds, controlling how muffled distant sounds become. Smaller numbers meaning more filtered"); #ifndef NO_SOUND diff --git a/rts/System/Sound/ISound.h b/rts/System/Sound/ISound.h index 6638a4469f1..1ca0900b84b 100644 --- a/rts/System/Sound/ISound.h +++ b/rts/System/Sound/ISound.h @@ -3,6 +3,7 @@ #ifndef _I_SOUND_H_ #define _I_SOUND_H_ +#include "System/Sound/ISoundAttenuationModel.h" #include #include #include @@ -79,6 +80,9 @@ class ISound { private: virtual bool LoadSoundDefsImpl(LuaParser* defsParser) = 0; static bool IsNullAudio(); + +public: + virtual ISoundAttenuationModel* GetAttenuationModel() { return nullptr; } }; #define sound ISound::GetInstance() diff --git a/rts/System/Sound/ISoundAttenuationModel.h b/rts/System/Sound/ISoundAttenuationModel.h new file mode 100644 index 00000000000..2e5be57a3c0 --- /dev/null +++ b/rts/System/Sound/ISoundAttenuationModel.h @@ -0,0 +1,29 @@ +#pragma once + +#include "System/float3.h" +struct SoundAttenuationInput { + float3 soundPosition; + //maybe some sound specific settings like should it be resistant to attenuation +}; + +/// All of the data used to calculate the totalFactor as well as the final totalFactor +struct SoundAttenuationOutput { + float forwardDistance; + float outerDistance; + float frustumHeight; + float frustumWidth; + float volumeFactor; + float filterFactor; + float tiltFactor; + float zoomFactor; +}; + +class ISoundAttenuationModel { +public: + virtual ~ISoundAttenuationModel() = default; + + virtual SoundAttenuationOutput Evaluate( + const SoundAttenuationInput& in + ) const = 0; +}; + diff --git a/rts/System/Sound/OpenAL/Sound.cpp b/rts/System/Sound/OpenAL/Sound.cpp index 832f506bfcc..9659f6baf0a 100644 --- a/rts/System/Sound/OpenAL/Sound.cpp +++ b/rts/System/Sound/OpenAL/Sound.cpp @@ -44,6 +44,8 @@ #include "System/float3.h" +#include "Rendering/GL/glExtra.h" + spring::recursive_mutex soundMutex; @@ -58,7 +60,6 @@ CSound::~CSound() configHandler->RemoveObserver(this); } - void CSound::Init() { std::lock_guard lck(soundMutex); @@ -79,6 +80,8 @@ void CSound::Init() updateListener = false; soundThreadQuit = false; canLoadDefs = false; + + attenuationModel = new RtsAttenuationModel(); } { Channels::General->SetVolume(configHandler->GetInt("snd_volgeneral") * 0.01f); @@ -121,6 +124,9 @@ void CSound::Kill() soundThread.join(); } + delete attenuationModel; + attenuationModel = nullptr; + SoundBuffer::Deinitialise(); } @@ -358,7 +364,7 @@ void CSound::DeviceChanged(uint32_t sdlDeviceIndex) // In these cases, no event is emitted — SDL2 switches the active audio device internally through the OS-specific audio backend (WASAPI, PulseAudio, etc.). // However, with certain device changes a short dropout may occur, and SDL2 will emit the SDL_AUDIODEVICEREMOVED event. // Shortly afterwards, the default device can usually be reinitialized. - + // This behavior can be reproduced on several test systems, for example when switching the Windows default device from monitor audio over HDMI to a sound card (HDMI->analog) std::lock_guard lck(soundMutex); @@ -513,7 +519,7 @@ bool CSound::OpenSdlDevice(const std::string& deviceName, SDL_AudioSpec& obtaine desiredSpec.samples = 4096; desiredSpec.callback = RenderSDLSamples; desiredSpec.userdata = this; - + /* SDL bug: can return devices with >2 channels (3D surround), even if we ask for just 2. * This causes the 2 "primary" channels to be moved in the 3D space compared to their "normal" state * and directional sound doesn't work anymore (though volume change with distance still does). @@ -1108,3 +1114,23 @@ std::vector CSound::GetSoundDevices() } return devices; } + +void CSound::DrawDebug() const +{ + // Only draw 3D sound sources that are currently playing + for (const CSoundSource& source: soundSources) { + // Use your extracted data functions instead of OpenAL calls + if (source.IsPlaying(false) && source.IsIn3D()) { + // Get position using your accessor methods instead of OpenAL calls + float3 pos = source.GetPosition(); // Use your implemented accessor + + // Draw a wireframe sphere at the sound source position + float color[4] = {1.0f, 0.0f, 0.0f, 0.7f}; // Red with some transparency + CMatrix44f matrix; + matrix.Translate(pos.x, pos.y, pos.z); + matrix.Scale(CSoundSource::REFERENCE_DIST * ELMOS_TO_METERS); // Adjusted scale for better visibility + GL::shapes.DrawWireSphere(8, 8, matrix, color); + } + } +} + diff --git a/rts/System/Sound/OpenAL/Sound.h b/rts/System/Sound/OpenAL/Sound.h index c3fa476160e..db84a466514 100644 --- a/rts/System/Sound/OpenAL/Sound.h +++ b/rts/System/Sound/OpenAL/Sound.h @@ -10,6 +10,7 @@ #include #include +#include "System/Sound/AttenuationModels/RtsAttenuationModel.h" #include "System/Sound/ISound.h" #include "System/float3.h" #include "System/UnorderedMap.hpp" @@ -25,6 +26,8 @@ class SoundItem; /// Default sound system implementation (OpenAL) class CSound : public ISound { +public: + void DrawDebug() const; // Debug visualization method public: CSound(); ~CSound(); @@ -73,6 +76,9 @@ class CSound : public ISound int GetFrameSize() const { return frameSize; } std::vector GetSoundDevices() override; + + ISoundAttenuationModel* GetAttenuationModel() override { return attenuationModel; } + private: typedef spring::unordered_map SoundItemNameMap; typedef spring::unordered_map SoundItemDefsMap; @@ -103,6 +109,8 @@ class CSound : public ISound std::string selectedDeviceName = ""; + ISoundAttenuationModel* attenuationModel = nullptr; + spring::thread soundThread; spring::unordered_map soundMap; // spring::unordered_set preloadSet; diff --git a/rts/System/Sound/OpenAL/SoundSource.cpp b/rts/System/Sound/OpenAL/SoundSource.cpp index a66ba495a3c..10220395612 100644 --- a/rts/System/Sound/OpenAL/SoundSource.cpp +++ b/rts/System/Sound/OpenAL/SoundSource.cpp @@ -2,13 +2,17 @@ #include "SoundSource.h" +#include +#include #include #include #include "ALShared.h" #include "EFX.h" +#include "Rendering/GlobalRendering.h" #include "System/Sound/IAudioChannel.h" #include "MusicStream.h" +#include "System/Sound/OpenAL/EFXfuncs.h" #include "System/Sound/SoundLog.h" #include "SoundBuffer.h" #include "SoundItem.h" @@ -17,13 +21,6 @@ #include "Sim/Misc/GlobalConstants.h" #include "System/float3.h" -#include "System/StringUtil.h" -#include "System/SpringMath.h" - - -static constexpr float ROLLOFF_FACTOR = 5.0f; -static constexpr float REFERENCE_DIST = 200.0f; - // used to adjust the pitch to the GameSpeed (optional) float CSoundSource::globalPitch = 1.0f; @@ -34,16 +31,16 @@ float CSoundSource::heightRolloffModifier = 1.0f; void CSoundSource::swap(CSoundSource& r) { std::swap(id, r.id); - std::swap(curChannel, r.curChannel); + std::swap(currentChannel, r.currentChannel); std::swap(curStream, r.curStream); - std::swap(curVolume, r.curVolume); + std::swap(currentVolume, r.currentVolume); std::swap(loopStop, r.loopStop); std::swap(in3D, r.in3D); std::swap(efxEnabled, r.efxEnabled); std::swap(efxUpdates, r.efxUpdates); std::swap(curHeightRolloffModifier, r.curHeightRolloffModifier); - std::swap(curPlayingItem, r.curPlayingItem); + std::swap(currentSoundItem, r.currentSoundItem); std::swap(asyncPlayItem, r.asyncPlayItem); } @@ -59,7 +56,6 @@ CSoundSource::CSoundSource() } } - CSoundSource::CSoundSource(CSoundSource&& src) { // can't use naive/default move because `id` member has to be unique @@ -76,128 +72,123 @@ CSoundSource::~CSoundSource() Delete(); } -void CSoundSource::Update() +float SmoothTowards(float current, float target, float speed, float dt) { - if (asyncPlayItem.id != 0) { - // Sound::Update() holds mutex, soundItems can not be accessed concurrently - Play(asyncPlayItem.channel, sound->GetSoundItem(asyncPlayItem.id), asyncPlayItem.position, asyncPlayItem.velocity, asyncPlayItem.volume, asyncPlayItem.relative); - asyncPlayItem = AsyncSoundItemData(); - } + const float t = 1.0f - std::exp(-speed * dt); + return current + (target - current) * t; +} - if (curPlayingItem.id != 0) { - if (in3D && (efxEnabled != efx.Enabled())) { - alSourcef(id, AL_AIR_ABSORPTION_FACTOR, (efx.Enabled()) ? efx.GetAirAbsorptionFactor() : 0); - alSource3i(id, AL_AUXILIARY_SEND_FILTER, (efx.Enabled()) ? efx.sfxSlot : AL_EFFECTSLOT_NULL, 0, AL_FILTER_NULL); - alSourcei(id, AL_DIRECT_FILTER, (efx.Enabled()) ? efx.sfxFilter : AL_FILTER_NULL); - efxEnabled = efx.Enabled(); - efxUpdates = efx.updates; - } +float Curve(float t, float min, float max, float k) +{ + return min + (max - min) * std::pow(t, k); +} - if (heightRolloffModifier != curHeightRolloffModifier) { - curHeightRolloffModifier = heightRolloffModifier; - alSourcef(id, AL_ROLLOFF_FACTOR, ROLLOFF_FACTOR * curPlayingItem.rolloff * heightRolloffModifier); - } +void CSoundSource::ApplyAttenuationModel(bool smooth) +{ + if (!in3D) + return; - if (!IsPlaying(true) || ((curPlayingItem.loopTime > 0) && (spring_gettime() > loopStop))) - Stop(); - } + if (sound == nullptr) { + LOG_L(L_ERROR, "Could not get sound singleton"); + return; + } - if (curStream) { - if (curStream->IsFinished()) { - Stop(); - } else { - curStream->Update(); - CheckError("CSoundSource::Update"); - } - } + if (sound->GetAttenuationModel() == nullptr) { + LOG_L(L_ERROR, "Could not get attenuation model"); + return; + } - if (efxEnabled && (efxUpdates != efx.updates)) { - // airAbsorption & LowPass aren't auto updated by OpenAL on change, so we need to do it per source - alSourcef(id, AL_AIR_ABSORPTION_FACTOR, efx.GetAirAbsorptionFactor()); - alSourcei(id, AL_DIRECT_FILTER, efx.sfxFilter); - efxUpdates = efx.updates; - } -} + attenuationOutput = sound->GetAttenuationModel()->Evaluate({ currentPosition }); -void CSoundSource::Delete() -{ - if (efxEnabled) { - alSource3i(id, AL_AUXILIARY_SEND_FILTER, AL_EFFECTSLOT_NULL, 0, AL_FILTER_NULL); - alSourcei(id, AL_DIRECT_FILTER, AL_FILTER_NULL); - } + if (smooth) { + currentVolumeValue = SmoothTowards( + currentVolumeValue, + attenuationOutput.volumeFactor, + VIEWPORT_VOLUME_REDUCTION_SPEED, + globalRendering->lastFrameTime); - Stop(); - alDeleteSources(1, &id); - CheckError("CSoundSource::Delete"); -} + currentFilterValue = SmoothTowards( + currentFilterValue, + attenuationOutput.filterFactor, + VIEWPORT_VOLUME_REDUCTION_SPEED, + globalRendering->lastFrameTime); + } + else { + currentVolumeValue = attenuationOutput.volumeFactor; + currentFilterValue = attenuationOutput.filterFactor; + } + efx.Enable(); + efxEnabled = true; -int CSoundSource::GetCurrentPriority() const -{ - if (asyncPlayItem.id != 0) - return asyncPlayItem.priority; + //TODO currentSoundItem.randomVolume is quite overtuned on many things (0.35 for example) + // new system will treat volume as a normalized range (0-1) so 0.35 is like 35% variable volume which is ridiculous + // for now, just ignore the randomVolume in the calculations - if (curStream) - return INT_MAX; + //FIXME 2D events are being affected by filters like the vo - if (curPlayingItem.id == 0) - return INT_MIN; + //FIXME can't just do this as many volumes passed in Play are over 1 + // need to figure out what the best solution is for converting existing values to new system - return (curPlayingItem.priority); -} + // alSourcef(id, AL_GAIN, attenuationOutput.volumeFactor * currentChannel->GetVolume() * currentVolume); -bool CSoundSource::IsPlaying(const bool checkOpenAl) const -{ - if (curStream) - return true; + alSourcef(id, AL_GAIN, std::clamp(attenuationOutput.volumeFactor * currentChannel->GetVolume(), 0.0f, 1.0f)); - if (asyncPlayItem.id != 0) - return true; + float gain; - if (curPlayingItem.id == 0) - return false; + alGetSourcef(id, AL_GAIN, &gain); - // calling OpenAL has a high chance of generating a L2 cache miss, avoid if possible - if (!checkOpenAl) - return true; + alFilterf(attenuationFilter, AL_LOWPASS_GAIN, 1); + alFilterf(attenuationFilter, AL_LOWPASS_GAINHF, currentFilterValue); - CheckError("CSoundSource::IsPlaying"); - ALint state; - alGetSourcei(id, AL_SOURCE_STATE, &state); - CheckError("CSoundSource::IsPlaying"); - return (state == AL_PLAYING); + alSourcei(id, AL_DIRECT_FILTER, attenuationFilter); } - -void CSoundSource::Stop() +void CSoundSource::Update() { - alSourceStop(id); - - { - SoundItem* item = nullptr; - - // callers marked * are mutex-guarded - // ::Delete via ~CSoundSource via CSound::Kill - // ::Play via ::Update (*) - // ::PlayStream via AudioChannel::StreamPlay (*) - // ::StreamStop via AudioChannel::StreamStop (*) - // AudioChannel::FindSourceAndPlay (*) - if (sound != nullptr) - item = sound->GetSoundItem(curPlayingItem.id); - if (item != nullptr) - item->StopPlay(); + if (asyncPlayItem.id != 0) { + // Sound::Update() holds mutex, soundItems can not be accessed concurrently + Play(asyncPlayItem.channel, sound->GetSoundItem(asyncPlayItem.id), asyncPlayItem.position, asyncPlayItem.velocity, asyncPlayItem.volume, asyncPlayItem.relative); + asyncPlayItem = AsyncSoundItemData(); + } - curPlayingItem = {}; + if (currentSoundItem.id != 0) { + if (UseAttenuationModel()) { + ApplyAttenuationModel(true); + } else { + if (in3D && (efxEnabled != efx.Enabled())) { + alSourcef(id, AL_AIR_ABSORPTION_FACTOR, (efx.Enabled()) ? efx.GetAirAbsorptionFactor() : 0); + alSource3i(id, AL_AUXILIARY_SEND_FILTER, (efx.Enabled()) ? efx.sfxSlot : AL_EFFECTSLOT_NULL, 0, AL_FILTER_NULL); + alSourcei(id, AL_DIRECT_FILTER, (efx.Enabled()) ? efx.sfxFilter : AL_FILTER_NULL); + efxEnabled = efx.Enabled(); + efxUpdates = efx.updates; + } + + if (heightRolloffModifier != curHeightRolloffModifier) { + curHeightRolloffModifier = heightRolloffModifier; + alSourcef(id, AL_ROLLOFF_FACTOR, ROLLOFF_FACTOR * currentSoundItem.rolloff * heightRolloffModifier); + } + } + + if (!IsPlaying(true) || ((currentSoundItem.loopTime > 0) && (spring_gettime() > loopStop))) + Stop(); } - curStream.reset(); + if (curStream) { + if (curStream->IsFinished()) { + Stop(); + } else { + curStream->Update(); + CheckError("CSoundSource::Update"); + } + } - if (curChannel != nullptr) { - IAudioChannel* oldChannel = curChannel; - curChannel = nullptr; - oldChannel->SoundSourceFinished(this); + if (!UseAttenuationModel() && efxEnabled && (efxUpdates != efx.updates)) { + // airAbsorption & LowPass aren't auto updated by OpenAL on change, so we need to do it per source + alSourcef(id, AL_AIR_ABSORPTION_FACTOR, efx.GetAirAbsorptionFactor()); + alSourcei(id, AL_DIRECT_FILTER, efx.sfxFilter); + efxUpdates = efx.updates; } - CheckError("CSoundSource::Stop"); } void CSoundSource::Play(IAudioChannel* channel, SoundItem* item, float3 pos, float3 velocity, float volume, bool relative) @@ -212,12 +203,21 @@ void CSoundSource::Play(IAudioChannel* channel, SoundItem* item, float3 pos, flo Stop(); - curVolume = volume; - curPlayingItem = {item->soundItemID, item->loopTime, item->priority, item->GetGain(), item->rolloff}; - curChannel = channel; + currentVolume = volume; + currentSoundItem = { + item->soundItemID, + item->loopTime, + item->priority, + item->GetGain(), + item->rolloff + }; + currentChannel = channel; + currentPosition = pos; + + name = item->name; alSourcei(id, AL_BUFFER, itemBuffer.GetId()); - alSourcef(id, AL_GAIN, volume * item->GetGain() * channel->volume); + alSourcef(id, AL_GAIN, volume * channel->volume); alSourcef(id, AL_PITCH, item->GetPitch() * globalPitch); velocity *= item->dopplerScale * ELMOS_TO_METERS; @@ -243,21 +243,44 @@ void CSoundSource::Play(IAudioChannel* channel, SoundItem* item, float3 pos, flo if (itemBuffer.GetChannels() > 1) LOG_L(L_WARNING, "Can not play non-mono \"%s\" in 3d.", itemBuffer.GetFilename().c_str()); - in3D = true; - if (efx.Enabled()) { - efxEnabled = true; - alSourcef(id, AL_AIR_ABSORPTION_FACTOR, efx.GetAirAbsorptionFactor()); - alSource3i(id, AL_AUXILIARY_SEND_FILTER, efx.sfxSlot, 0, AL_FILTER_NULL); - alSourcei(id, AL_DIRECT_FILTER, efx.sfxFilter); - efxUpdates = efx.updates; - } + in3D = true; alSourcei(id, AL_SOURCE_RELATIVE, AL_FALSE); pos *= ELMOS_TO_METERS; alSource3f(id, AL_POSITION, pos.x, pos.y, pos.z); - curHeightRolloffModifier = heightRolloffModifier; - alSourcef(id, AL_ROLLOFF_FACTOR, ROLLOFF_FACTOR * item->rolloff * heightRolloffModifier); + if (UseAttenuationModel()) { + + efx.Enable(); + if (attenuationFilter == 0) { + alGenFilters(1, &attenuationFilter); + alFilteri(attenuationFilter, AL_FILTER_TYPE, AL_FILTER_LOWPASS); + } + + // Need to disable physics based attenuation + alSourcef(id, AL_AIR_ABSORPTION_FACTOR, 0); + alSourcef(id, AL_REFERENCE_DISTANCE, 0); + alSourcef(id, AL_ROLLOFF_FACTOR, 0); + + alFilterf(attenuationFilter, AL_LOWPASS_GAIN, 1); + alFilterf(attenuationFilter, AL_LOWPASS_GAINHF, 1); + + alSourcei(id, AL_DIRECT_FILTER, attenuationFilter); + + ApplyAttenuationModel(false); + + } else { + if (efx.Enabled()) { + efxEnabled = true; + alSourcef(id, AL_AIR_ABSORPTION_FACTOR, efx.GetAirAbsorptionFactor()); + alSource3i(id, AL_AUXILIARY_SEND_FILTER, efx.sfxSlot, 0, AL_FILTER_NULL); + alSourcei(id, AL_DIRECT_FILTER, efx.sfxFilter); + efxUpdates = efx.updates; + } + + curHeightRolloffModifier = heightRolloffModifier; + alSourcef(id, AL_ROLLOFF_FACTOR, ROLLOFF_FACTOR * item->rolloff * heightRolloffModifier); + } #if defined(__APPLE__) || defined(__OpenBSD__) alSourcef(id, AL_MAX_DISTANCE, 1000000.0f); @@ -280,14 +303,99 @@ void CSoundSource::Play(IAudioChannel* channel, SoundItem* item, float3 pos, flo #endif } + alSourcePlay(id); + float gain; + alGetSourcef(id, AL_GAIN, &gain); + if (itemBuffer.GetId() == 0) LOG_L(L_WARNING, "CSoundSource::Play: Empty buffer for item %s (file %s)", item->name.c_str(), itemBuffer.GetFilename().c_str()); CheckError("CSoundSource::Play"); } +void CSoundSource::Delete() +{ + if (efxEnabled) { + alSource3i(id, AL_AUXILIARY_SEND_FILTER, AL_EFFECTSLOT_NULL, 0, AL_FILTER_NULL); + alSourcei(id, AL_DIRECT_FILTER, AL_FILTER_NULL); + } + + if (attenuationFilter != 0) + alDeleteFilters(1, &attenuationFilter); + + Stop(); + alDeleteSources(1, &id); + CheckError("CSoundSource::Delete"); +} + +int CSoundSource::GetCurrentPriority() const +{ + if (asyncPlayItem.id != 0) + return asyncPlayItem.priority; + + if (curStream) + return INT_MAX; + + if (currentSoundItem.id == 0) + return INT_MIN; + + return (currentSoundItem.priority); +} + +bool CSoundSource::IsPlaying(const bool checkOpenAl) const +{ + if (curStream) + return true; + + if (asyncPlayItem.id != 0) + return true; + + if (currentSoundItem.id == 0) + return false; + + // calling OpenAL has a high chance of generating a L2 cache miss, avoid if possible + if (!checkOpenAl) + return true; + + CheckError("CSoundSource::IsPlaying"); + ALint state; + alGetSourcei(id, AL_SOURCE_STATE, &state); + CheckError("CSoundSource::IsPlaying"); + return (state == AL_PLAYING); +} + +void CSoundSource::Stop() +{ + alSourceStop(id); + + { + SoundItem* item = nullptr; + + // callers marked * are mutex-guarded + // ::Delete via ~CSoundSource via CSound::Kill + // ::Play via ::Update (*) + // ::PlayStream via AudioChannel::StreamPlay (*) + // ::StreamStop via AudioChannel::StreamStop (*) + // AudioChannel::FindSourceAndPlay (*) + if (sound != nullptr) + item = sound->GetSoundItem(currentSoundItem.id); + if (item != nullptr) + item->StopPlay(); + + currentSoundItem = {}; + } + + curStream.reset(); + + if (currentChannel != nullptr) { + IAudioChannel* oldChannel = currentChannel; + currentChannel = nullptr; + oldChannel->SoundSourceFinished(this); + } + CheckError("CSoundSource::Stop"); +} void CSoundSource::PlayAsync(IAudioChannel* channel, size_t id, float3 pos, float3 velocity, float volume, float priority, bool relative) { @@ -303,7 +411,6 @@ void CSoundSource::PlayAsync(IAudioChannel* channel, size_t id, float3 pos, floa asyncPlayItem.relative = relative; } - void CSoundSource::PlayStream(IAudioChannel* channel, const std::string& file, float volume) { // stop any current playback @@ -313,8 +420,8 @@ void CSoundSource::PlayStream(IAudioChannel* channel, const std::string& file, f curStream = std::make_unique (); // OpenAL params - curChannel = channel; - curVolume = volume; + currentChannel = channel; + currentVolume = volume; in3D = false; if (efxEnabled) { @@ -375,15 +482,19 @@ float CSoundSource::GetStreamPlayTime() void CSoundSource::UpdateVolume() { - if (curChannel == nullptr) + if (currentChannel == nullptr) return; if (curStream) { - alSourcef(id, AL_GAIN, curVolume * curChannel->volume); + alSourcef(id, AL_GAIN, currentVolume * currentChannel->volume); return; } - if (curPlayingItem.id != 0) { - alSourcef(id, AL_GAIN, curVolume * curPlayingItem.rndGain * curChannel->volume); + if (currentSoundItem.id != 0) { + if (UseAttenuationModel()) { + ApplyAttenuationModel(false); + return; + } + alSourcef(id, AL_GAIN, currentVolume * currentSoundItem.randomVolume * currentChannel->volume); return; } } diff --git a/rts/System/Sound/OpenAL/SoundSource.h b/rts/System/Sound/OpenAL/SoundSource.h index ecf61527552..b68c993d9f8 100644 --- a/rts/System/Sound/OpenAL/SoundSource.h +++ b/rts/System/Sound/OpenAL/SoundSource.h @@ -8,11 +8,12 @@ #include -#include "System/Misc/NonCopyable.h" +#include "System/Config/ConfigHandler.h" #include "System/Misc/SpringTime.h" +#include "System/Sound/IAudioChannel.h" +#include "System/Sound/ISoundAttenuationModel.h" #include "System/float3.h" -class IAudioChannel; class SoundItem; class MusicStream; @@ -33,9 +34,14 @@ class CSoundSource CSoundSource& operator = (CSoundSource&& src); CSoundSource& operator = (const CSoundSource& src) = delete; + static constexpr float ROLLOFF_FACTOR = 5.0f; + static constexpr float REFERENCE_DIST = 200.0f; + void Update(); void Delete(); + void ApplyAttenuationModel(const bool smooth); + void UpdateVolume(); bool IsValid() const { return (id != 0); }; @@ -51,13 +57,28 @@ class CSoundSource void StreamPause(); float GetStreamTime(); float GetStreamPlayTime(); + void ComputeCameraSpaceData(); static void SetPitch(const float& newPitch) { globalPitch = newPitch; } static void SetHeightRolloffModifer(const float& mod) { heightRolloffModifier = mod; } + bool IsIn3D() const { return in3D; } + float3 GetPosition() const { return currentPosition; } + ALuint GetId() const { return id; } + + static constexpr float VIEWPORT_VOLUME_REDUCTION_SPEED = 0.02f; + private: void swap(CSoundSource& other); + void Initialize(IAudioChannel* channel, SoundItem* item, float3 pos, float3 velocity, float volume, bool relative); + void EnableSpatialization(); + void DisableSpatialization(); + + float GetSummedVolume() { + return (currentChannel ? currentChannel->volume : 1.0f) * currentSoundItem.randomVolume * currentVolume; + } + struct AsyncSoundItemData { IAudioChannel* channel = nullptr; @@ -79,7 +100,7 @@ class CSoundSource unsigned int loopTime = 0; int priority = 0; - float rndGain = 0.0f; + float randomVolume = 0.0f; float rolloff = 0.0f; }; @@ -93,19 +114,36 @@ class CSoundSource private: ALuint id = 0; - SoundItemData curPlayingItem; + ALuint attenuationFilter = 0; + + SoundAttenuationOutput attenuationOutput; + + float cameraZoom; + + float3 currentPosition = float3(0.0f, 0.0f, 0.0f); + + SoundItemData currentSoundItem; AsyncSoundItemData asyncPlayItem; - IAudioChannel* curChannel = nullptr; + IAudioChannel* currentChannel = nullptr; std::unique_ptr curStream; - float curVolume = 1.0f; + float currentVolume = 1.0f; + float currentVolumeValue = 0; + float currentFilterValue = 0; + spring_time loopStop {1e9}; + spring_time lastUpdate = spring_gettime(); bool in3D = false; bool efxEnabled = false; int efxUpdates = 0; + std::string name; + ALfloat curHeightRolloffModifier = 1.0f; + + bool UseAttenuationModel() { return configHandler->GetBool("snd_useAttenuationModel"); } }; #endif +