From 91291b20759d5b7495c5b2dab3f411e5783832ce Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 21 Mar 2026 12:25:42 +0800 Subject: [PATCH] Add HRTF 3D spatialization audio effect --- engine/CMakeLists.txt | 2 + engine/include/XCEngine/Audio/HRTF.h | 87 +++++++++++++ engine/src/Audio/HRTF.cpp | 178 +++++++++++++++++++++++++++ 3 files changed, 267 insertions(+) create mode 100644 engine/include/XCEngine/Audio/HRTF.h create mode 100644 engine/src/Audio/HRTF.cpp diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 9c88cc76..a44661c9 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -250,6 +250,8 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/Audio/Reverbation.cpp ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Audio/Equalizer.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Audio/Equalizer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Audio/HRTF.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/Audio/HRTF.cpp # Third-party (KissFFT) ${CMAKE_CURRENT_SOURCE_DIR}/third_party/kissfft/kiss_fft.h diff --git a/engine/include/XCEngine/Audio/HRTF.h b/engine/include/XCEngine/Audio/HRTF.h new file mode 100644 index 00000000..6e3c43f5 --- /dev/null +++ b/engine/include/XCEngine/Audio/HRTF.h @@ -0,0 +1,87 @@ +#pragma once + +#include "AudioTypes.h" +#include +#include +#include +#include + +namespace XCEngine { +namespace Audio { + +struct HRTFParams { + float azimuth = 0.0f; + float elevation = 0.0f; + float interauralTimeDelay = 0.0f; + float interauralLevelDifference = 0.0f; + float headShadowing = 0.0f; + float pinnaCues = 0.0f; + float torsoShoulderRotation = 0.0f; +}; + +class HRTF { +public: + HRTF(); + ~HRTF(); + + void ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels, + const Math::Vector3& sourcePosition, + const Math::Vector3& listenerPosition, + const Math::Quaternion& listenerRotation); + + void SetEnabled(bool enabled) { m_enabled = enabled; } + bool IsEnabled() const { return m_enabled; } + + void SetHRTFEnabled(bool enabled) { m_hrtfEnabled = enabled; } + bool IsHRTFEnabled() const { return m_hrtfEnabled; } + + void SetQualityLevel(uint32 level); + uint32 GetQualityLevel() const { return m_qualityLevel; } + + void SetCrossFeed(float crossFeed); + float GetCrossFeed() const { return m_crossFeed; } + + void SetDopplerShiftEnabled(bool enabled) { m_dopplerEnabled = enabled; } + bool IsDopplerShiftEnabled() const { return m_dopplerEnabled; } + + void SetSpeedOfSound(float speed); + float GetSpeedOfSound() const { return m_speedOfSound; } + +private: + void ComputeDirection(const Math::Vector3& sourcePosition, + const Math::Vector3& listenerPosition, + const Math::Quaternion& listenerRotation, + float& azimuth, float& elevation); + void ComputeITD(float azimuth, float elevation); + void ComputeILD(float azimuth, float elevation); + float ComputePinnaEffect(float azimuth, float elevation); + + void ApplyHRTF(float* buffer, uint32 sampleCount, uint32 channels); + void ApplySimplePanning(float* buffer, uint32 sampleCount, uint32 channels, float pan); + +private: + bool m_enabled = true; + bool m_hrtfEnabled = true; + bool m_dopplerEnabled = true; + + uint32 m_qualityLevel = 2; + float m_crossFeed = 0.0f; + float m_speedOfSound = 343.0f; + + HRTFParams m_params; + + Math::Vector3 m_prevSourcePosition = Math::Vector3::Zero(); + Math::Vector3 m_prevListenerPosition = Math::Vector3::Zero(); + float m_prevDopplerShift = 1.0f; + + static constexpr uint32 MaxDelaySamples = 1024; + std::vector m_leftDelayLine; + std::vector m_rightDelayLine; + uint32 m_leftDelayIndex = 0; + uint32 m_rightDelayIndex = 0; + + uint32 m_sampleRate = 48000; +}; + +} // namespace Audio +} // namespace XCEngine diff --git a/engine/src/Audio/HRTF.cpp b/engine/src/Audio/HRTF.cpp new file mode 100644 index 00000000..853ee089 --- /dev/null +++ b/engine/src/Audio/HRTF.cpp @@ -0,0 +1,178 @@ +#include +#include +#include + +namespace XCEngine { +namespace Audio { + +HRTF::HRTF() + : m_leftDelayLine(MaxDelaySamples, 0.0f) + , m_rightDelayLine(MaxDelaySamples, 0.0f) +{ +} + +HRTF::~HRTF() { +} + +void HRTF::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels, + const Math::Vector3& sourcePosition, + const Math::Vector3& listenerPosition, + const Math::Quaternion& listenerRotation) { + if (!m_enabled || buffer == nullptr || sampleCount == 0) { + return; + } + + float azimuth = 0.0f; + float elevation = 0.0f; + + ComputeDirection(sourcePosition, listenerPosition, listenerRotation, azimuth, elevation); + ComputeITD(azimuth, elevation); + ComputeILD(azimuth, elevation); + + m_params.azimuth = azimuth; + m_params.elevation = elevation; + + if (m_hrtfEnabled) { + ApplyHRTF(buffer, sampleCount, channels); + } else { + float pan = azimuth / 180.0f; + ApplySimplePanning(buffer, sampleCount, channels, pan); + } +} + +void HRTF::SetQualityLevel(uint32 level) { + m_qualityLevel = std::max(1u, std::min(3u, level)); +} + +void HRTF::SetCrossFeed(float crossFeed) { + m_crossFeed = std::max(0.0f, std::min(1.0f, crossFeed)); +} + +void HRTF::SetSpeedOfSound(float speed) { + m_speedOfSound = std::max(1.0f, speed); +} + +void HRTF::ComputeDirection(const Math::Vector3& sourcePosition, + const Math::Vector3& listenerPosition, + const Math::Quaternion& listenerRotation, + float& azimuth, float& elevation) { + Math::Vector3 direction = sourcePosition - listenerPosition; + float distance = Math::Vector3::Magnitude(direction); + + if (distance < 0.001f) { + azimuth = 0.0f; + elevation = 0.0f; + return; + } + + direction = Math::Vector3::Normalize(direction); + + Math::Quaternion conjugateRotation = listenerRotation.Inverse(); + Math::Vector3 localDirection; + localDirection.x = conjugateRotation.x * direction.x + conjugateRotation.y * direction.y + conjugateRotation.z * direction.z; + localDirection.y = conjugateRotation.x * direction.y - conjugateRotation.y * direction.x + conjugateRotation.w * direction.z; + localDirection.z = conjugateRotation.x * direction.z - conjugateRotation.y * direction.y - conjugateRotation.w * direction.x; + + Math::Vector3 forward(0.0f, 0.0f, 1.0f); + Math::Vector3 up(0.0f, 1.0f, 0.0f); + + float dotForward = Math::Vector3::Dot(forward, localDirection); + float dotUp = Math::Vector3::Dot(up, localDirection); + + azimuth = std::atan2(localDirection.x, localDirection.z) * 180.0f / 3.14159265f; + elevation = std::asin(dotUp) * 180.0f / 3.14159265f; + + azimuth = std::max(-180.0f, std::min(180.0f, azimuth)); + elevation = std::max(-90.0f, std::min(90.0f, elevation)); +} + +void HRTF::ComputeITD(float azimuth, float elevation) { + float headRadius = 0.075f; + + float cosAzimuth = std::cos(azimuth * 3.14159265f / 180.0f); + + float itd = (headRadius / m_speedOfSound) * (cosAzimuth - 1.0f); + + m_params.interauralTimeDelay = itd * m_sampleRate; +} + +void HRTF::ComputeILD(float azimuth, float elevation) { + float absAzimuth = std::abs(azimuth); + + float ild = (absAzimuth / 90.0f) * 20.0f; + + m_params.interauralLevelDifference = ild; +} + +float HRTF::ComputePinnaEffect(float azimuth, float elevation) { + float pinnaGain = 1.0f; + + if (elevation > 0.0f) { + pinnaGain += elevation / 90.0f * 3.0f; + } else if (elevation < -30.0f) { + pinnaGain -= 3.0f; + } + + pinnaGain = std::max(0.5f, std::min(2.0f, pinnaGain)); + + return pinnaGain; +} + +void HRTF::ApplyHRTF(float* buffer, uint32 sampleCount, uint32 channels) { + if (channels < 2) { + return; + } + + float leftGain = 1.0f; + float rightGain = 1.0f; + + if (m_params.azimuth < 0.0f) { + rightGain *= (1.0f - std::abs(m_params.azimuth) / 90.0f); + } else { + leftGain *= (1.0f - std::abs(m_params.azimuth) / 90.0f); + } + + float levelReduction = m_params.interauralLevelDifference / 20.0f; + rightGain *= std::pow(10.0f, -levelReduction / 20.0f); + + for (uint32 i = 0; i < sampleCount; ++i) { + uint32 sampleIndex = i * channels; + + float leftSample = buffer[sampleIndex]; + float rightSample = buffer[sampleIndex + 1]; + + float delayedLeft = m_leftDelayLine[m_leftDelayIndex]; + float delayedRight = m_rightDelayLine[m_rightDelayIndex]; + + buffer[sampleIndex] = delayedLeft * leftGain; + buffer[sampleIndex + 1] = delayedRight * rightGain; + + m_leftDelayLine[m_leftDelayIndex] = leftSample; + m_rightDelayLine[m_rightDelayIndex] = rightSample; + + m_leftDelayIndex = (m_leftDelayIndex + 1) % MaxDelaySamples; + m_rightDelayIndex = (m_rightDelayIndex + 1) % MaxDelaySamples; + } +} + +void HRTF::ApplySimplePanning(float* buffer, uint32 sampleCount, uint32 channels, float pan) { + if (channels < 2) { + return; + } + + pan = std::max(-1.0f, std::min(1.0f, pan)); + + float leftGain = (1.0f - pan) / 2.0f; + float rightGain = (1.0f + pan) / 2.0f; + + for (uint32 i = 0; i < sampleCount; ++i) { + uint32 sampleIndex = i * channels; + float sample = (buffer[sampleIndex] + buffer[sampleIndex + 1]) / 2.0f; + + buffer[sampleIndex] = sample * leftGain; + buffer[sampleIndex + 1] = sample * rightGain; + } +} + +} // namespace Audio +} // namespace XCEngine