From b69d54a5ca620f87eaacb71c7b5fd87e26da91a8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 5 Feb 2024 16:03:31 +0100 Subject: [PATCH] Add `AudioBackendTools` for sharing spatial audio impl for consoles --- Source/Engine/Audio/AudioBackendTools.h | 128 ++++++++++++++++++ .../Audio/XAudio2/AudioBackendXAudio2.cpp | 2 +- 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 Source/Engine/Audio/AudioBackendTools.h diff --git a/Source/Engine/Audio/AudioBackendTools.h b/Source/Engine/Audio/AudioBackendTools.h new file mode 100644 index 000000000..3229490ed --- /dev/null +++ b/Source/Engine/Audio/AudioBackendTools.h @@ -0,0 +1,128 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Core/Math/Transform.h" + +/// +/// The helper class for that handles active audio backend operations. +/// +class AudioBackendTools +{ +public: + struct Settings + { + float Volume = 1.0f; + float DopplerFactor = 1.0f; + }; + + struct Listener + { + Vector3 Velocity; + Vector3 Position; + Quaternion Orientation; + }; + + struct Source + { + bool Is3D; + float Volume; + float Pitch; + float Pan; + float MinDistance; + float Attenuation; + float DopplerFactor; + Vector3 Velocity; + Vector3 Position; + Quaternion Orientation; + }; + + enum class Channels + { + FrontLeft = 0, + FrontRight = 1, + Center = 2, + BackLeft = 3, + BackRight = 4, + SideLeft = 5, + SideRight = 6, + MAX + }; + + struct SoundMix + { + float Pitch; + float Volume; + float Channels[(int32)Channels::MAX]; + }; + + static SoundMix CalculateSoundMix(const Settings& settings, const Listener& listener, const Source& source, int32 channelCount = 2) + { + SoundMix mix; + mix.Pitch = source.Pitch; + mix.Volume = source.Volume * settings.Volume; + if (source.Is3D) + { + const Transform listenerTransform(listener.Position, listener.Orientation); + float distance = (float)Vector3::Distance(listener.Position, source.Position); + float gain = 1; + + // Calculate attenuation (OpenAL formula for mode: AL_INVERSE_DISTANCE_CLAMPED) + // [https://www.openal.org/documentation/openal-1.1-specification.pdf] + distance = Math::Clamp(distance, source.MinDistance, MAX_float); + const float dst = source.MinDistance + source.Attenuation * (distance - source.MinDistance); + if (dst > 0) + gain = source.MinDistance / dst; + mix.Volume *= Math::Saturate(gain); + + // Calculate panning + // Ramy Sadek and Chris Kyriakakis, 2004, "A Novel Multichannel Panning Method for Standard and Arbitrary Loudspeaker Configurations" + // [https://www.researchgate.net/publication/235080603_A_Novel_Multichannel_Panning_Method_for_Standard_and_Arbitrary_Loudspeaker_Configurations] + //TODO: hardcoded main speaker directions for 2, 3.1, 5.1 and 7.1 setups - these are simplified and could also be made configurable + static const Float3 ChannelDirections[(int32)Channels::MAX] = + { + Float3(-1.0, 0.0, -1.0).GetNormalized(), + Float3(1.0, 0.0, -1.0).GetNormalized(), + Float3(0.0, 0.0, -1.0).GetNormalized(), + Float3(-1.0, 0.0, 1.0).GetNormalized(), + Float3(1.0, 0.0, 1.0).GetNormalized(), + Float3(-1.0, 0.0, 0.0).GetNormalized(), + Float3(1.0, 0.0, 0.0).GetNormalized(), + }; + const Float3 sourceInListenerSpace = (Float3)listenerTransform.WorldToLocal(source.Position); + const Float3 sourceToListener = Float3::Normalize(sourceInListenerSpace); + float sqGainsSum = 0.0f; + for (int32 i = 0; i < channelCount; i++) + { + float othersSum = 0.0f; + for (int32 j = 0; j < channelCount; j++) + othersSum += (1.0f + Float3::Dot(ChannelDirections[i], ChannelDirections[j])) * 0.5f; + const float sqGain = Math::Square(0.5f * Math::Pow(1.0f + Float3::Dot(ChannelDirections[i], sourceToListener), 2.0f) / othersSum); + sqGainsSum += sqGain; + mix.Channels[i] = sqGain; + } + for (int32 i = 0; i < channelCount; i++) + mix.Channels[i] = Math::Sqrt(mix.Channels[i] / sqGainsSum); + + // Calculate doppler + const Float3 velocityInListenerSpace = (Float3)listenerTransform.WorldToLocalVector(source.Velocity - listener.Velocity); + const float velocity = velocityInListenerSpace.Length(); + const float dopplerFactor = settings.DopplerFactor * source.DopplerFactor; + if (dopplerFactor > 0.0f && velocity > 0.0f) + { + constexpr float speedOfSound = 343.3f * 100.0f * 100.0f; // in air, in Flax units + const float approachingFactor = Float3::Dot(sourceToListener, velocityInListenerSpace.GetNormalized()); + const float dopplerPitch = speedOfSound / (speedOfSound + velocity * approachingFactor); + mix.Pitch *= Math::Clamp(dopplerPitch, 0.1f, 10.0f); + } + } + else + { + mix.Channels[(int32)Channels::FrontLeft] = Math::Min(1.0f - source.Pan, 1.0f); + mix.Channels[(int32)Channels::FrontRight] = Math::Min(1.0f + source.Pan, 1.0f); + for (int32 i = 2; i < channelCount; i++) + mix.Channels[i] = 0.0f; + } + return mix; + } +}; diff --git a/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp b/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp index 3092a2229..dc3a7e7ad 100644 --- a/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp +++ b/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp @@ -387,7 +387,7 @@ void AudioBackendXAudio2::Source_OnAdd(AudioSource* source) const auto& header = clip->AudioHeader; auto& format = aSource->Format; format.wFormatTag = WAVE_FORMAT_PCM; - format.nChannels = clip->Is3D() ? 1 : header.Info.NumChannels; // 3d audio is always mono (AudioClip auto-converts before buffer write) + format.nChannels = clip->Is3D() ? 1 : header.Info.NumChannels; // 3d audio is always mono (AudioClip auto-converts before buffer write if FeatureFlags::SpatialMultiChannel is unset) format.nSamplesPerSec = header.Info.SampleRate; format.wBitsPerSample = header.Info.BitDepth; format.nBlockAlign = (WORD)(format.nChannels * (format.wBitsPerSample / 8));