From d2017b251cdb2a67a3960b310a8e48c39078bba0 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 19 Apr 2026 00:26:28 +0800 Subject: [PATCH] feat(audio): formalize runtime effects and wav loading --- engine/include/XCEngine/Audio/Equalizer.h | 13 +- engine/include/XCEngine/Audio/FFTFilter.h | 3 +- engine/include/XCEngine/Audio/HRTF.h | 9 +- engine/include/XCEngine/Audio/IAudioEffect.h | 10 +- engine/include/XCEngine/Audio/Reverbation.h | 38 +-- engine/src/Audio/Equalizer.cpp | 79 ++++-- engine/src/Audio/FFTFilter.cpp | 31 ++- engine/src/Audio/HRTF.cpp | 205 +++++++++------- engine/src/Audio/Reverbation.cpp | 229 ++++++++++++------ engine/src/Core/Asset/ResourceManager.cpp | 3 + .../src/Resources/AudioClip/AudioLoader.cpp | 123 +++++++--- tests/Components/CMakeLists.txt | 4 + .../Resources/AudioClip/test_audio_loader.cpp | 25 +- 13 files changed, 518 insertions(+), 254 deletions(-) diff --git a/engine/include/XCEngine/Audio/Equalizer.h b/engine/include/XCEngine/Audio/Equalizer.h index 11ac3631..564adc84 100644 --- a/engine/include/XCEngine/Audio/Equalizer.h +++ b/engine/include/XCEngine/Audio/Equalizer.h @@ -11,7 +11,7 @@ public: Equalizer(); ~Equalizer() override; - void ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) override; + void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) override; void SetBandCount(uint32 count); uint32 GetBandCount() const { return m_bandCount; } @@ -30,13 +30,15 @@ public: void SetWetMix(float wetMix) override; float GetWetMix() const override { return m_wetMix; } + void SetSampleRate(uint32 sampleRate) override; + void ResetState() override; private: - void ProcessBand(float* buffer, uint32 sampleCount, uint32 channel, uint32 band); + void EnsureStateCapacity(uint32 channels); void ComputeCoefficients(uint32 band, float frequency, float q, float gainDb); private: - uint32 m_bandCount = 4; + uint32 m_bandCount = 0; std::vector m_frequencies; std::vector m_gains; std::vector m_qs; @@ -54,11 +56,8 @@ private: }; std::vector m_bandStates; + uint32 m_stateChannels = 0; - float m_wetMix = 1.0f; - bool m_enabled = true; - - uint32 m_sampleRate = 48000; }; } // namespace Audio diff --git a/engine/include/XCEngine/Audio/FFTFilter.h b/engine/include/XCEngine/Audio/FFTFilter.h index fcf4f9e4..3d8f69e8 100644 --- a/engine/include/XCEngine/Audio/FFTFilter.h +++ b/engine/include/XCEngine/Audio/FFTFilter.h @@ -12,13 +12,14 @@ public: explicit FFTFilter(uint32 fftSize); ~FFTFilter() override; - void ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) override; + void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) override; void SetFFTSize(uint32 size); uint32 GetFFTSize() const { return m_fftSize; } void SetSmoothingFactor(float factor); float GetSmoothingFactor() const { return m_smoothingFactor; } + void ResetState() override; const float* GetSpectrumData() const { return m_spectrumData.data(); } size_t GetSpectrumSize() const { return m_spectrumData.size(); } diff --git a/engine/include/XCEngine/Audio/HRTF.h b/engine/include/XCEngine/Audio/HRTF.h index 08c5debd..f39db15e 100644 --- a/engine/include/XCEngine/Audio/HRTF.h +++ b/engine/include/XCEngine/Audio/HRTF.h @@ -24,7 +24,7 @@ public: HRTF(); ~HRTF(); - void ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels, + void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels, const Math::Vector3& sourcePosition, const Math::Vector3& listenerPosition, const Math::Quaternion& listenerRotation); @@ -46,6 +46,9 @@ public: void SetSpeedOfSound(float speed); float GetSpeedOfSound() const { return m_speedOfSound; } + void SetSampleRate(uint32 sampleRate); + uint32 GetSampleRate() const { return m_sampleRate; } + void ResetState(); private: void ComputeDirection(const Math::Vector3& sourcePosition, @@ -56,8 +59,8 @@ private: 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); + void ApplyHRTF(float* buffer, uint32 frameCount, uint32 channels); + void ApplySimplePanning(float* buffer, uint32 frameCount, uint32 channels, float pan); private: bool m_enabled = true; diff --git a/engine/include/XCEngine/Audio/IAudioEffect.h b/engine/include/XCEngine/Audio/IAudioEffect.h index ed48b83e..0f84d054 100644 --- a/engine/include/XCEngine/Audio/IAudioEffect.h +++ b/engine/include/XCEngine/Audio/IAudioEffect.h @@ -9,7 +9,7 @@ class IAudioEffect { public: virtual ~IAudioEffect() = default; - virtual void ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) = 0; + virtual void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) = 0; virtual void SetEnabled(bool enabled) { m_enabled = enabled; } virtual bool IsEnabled() const { return m_enabled; } @@ -17,9 +17,17 @@ public: virtual void SetWetMix(float wetMix) { m_wetMix = wetMix; } virtual float GetWetMix() const { return m_wetMix; } + virtual void SetSampleRate(uint32 sampleRate) { + m_sampleRate = sampleRate > 0 ? sampleRate : 1u; + } + virtual uint32 GetSampleRate() const { return m_sampleRate; } + + virtual void ResetState() {} + protected: bool m_enabled = true; float m_wetMix = 1.0f; + uint32 m_sampleRate = 48000; }; } // namespace Audio diff --git a/engine/include/XCEngine/Audio/Reverbation.h b/engine/include/XCEngine/Audio/Reverbation.h index 601f07dc..4b3d22a1 100644 --- a/engine/include/XCEngine/Audio/Reverbation.h +++ b/engine/include/XCEngine/Audio/Reverbation.h @@ -11,7 +11,7 @@ public: Reverbation(); ~Reverbation() override; - void ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) override; + void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) override; void SetRoomSize(float size); float GetRoomSize() const { return m_roomSize; } @@ -30,19 +30,10 @@ public: void SetFreeze(bool freeze); bool IsFreeze() const { return m_freeze; } + void SetSampleRate(uint32 sampleRate) override; + void ResetState() override; private: - void ProcessCombFilter(float* buffer, uint32 sampleCount, uint32 channel, uint32 combIndex); - void ProcessAllPassFilter(float* buffer, uint32 sampleCount, uint32 channel); - -private: - float m_roomSize = 0.5f; - float m_damping = 0.5f; - float m_wetMix = 0.3f; - float m_dryMix = 0.7f; - float m_width = 1.0f; - bool m_freeze = false; - static constexpr uint32 CombCount = 8; static constexpr uint32 AllPassCount = 4; static constexpr uint32 MaxDelayLength = 2000; @@ -61,13 +52,28 @@ private: std::vector buffer; uint32 bufferSize = 0; uint32 writeIndex = 0; - float feedback = 0.0f; + float feedback = 0.5f; }; - CombFilter m_combFilters[CombCount]; - AllPassFilter m_allPassFilters[AllPassCount]; + struct ChannelState { + CombFilter combFilters[CombCount]; + AllPassFilter allPassFilters[AllPassCount]; + }; - uint32 m_sampleRate = 48000; + void EnsureChannelState(uint32 channels); + void InitializeChannelState(ChannelState& state, uint32 channelIndex); + void UpdateCombParameters(); + float ProcessChannelSample(uint32 channelIndex, float inputSample); + +private: + float m_roomSize = 0.5f; + float m_damping = 0.5f; + float m_dryMix = 0.7f; + float m_width = 1.0f; + bool m_freeze = false; + + std::vector m_channelStates; + uint32 m_stateChannels = 0; }; } // namespace Audio diff --git a/engine/src/Audio/Equalizer.cpp b/engine/src/Audio/Equalizer.cpp index 82012459..f592a50b 100644 --- a/engine/src/Audio/Equalizer.cpp +++ b/engine/src/Audio/Equalizer.cpp @@ -6,7 +6,6 @@ namespace XCEngine { namespace Audio { Equalizer::Equalizer() - : m_sampleRate(48000) { SetBandCount(4); @@ -25,8 +24,8 @@ Equalizer::Equalizer() Equalizer::~Equalizer() { } -void Equalizer::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) { - if (!m_enabled || buffer == nullptr || sampleCount == 0) { +void Equalizer::ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) { + if (!m_enabled || buffer == nullptr || frameCount == 0 || m_wetMix <= 0.0f) { return; } @@ -34,6 +33,15 @@ void Equalizer::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) return; } + EnsureStateCapacity(channels); + + const size_t sampleCount = static_cast(frameCount) * channels; + std::vector dryBuffer; + std::vector wetBuffer(buffer, buffer + sampleCount); + if (m_wetMix < 1.0f) { + dryBuffer.assign(buffer, buffer + sampleCount); + } + for (uint32 ch = 0; ch < channels; ++ch) { for (uint32 band = 0; band < m_bandCount; ++band) { uint32 stateIndex = ch * m_bandCount + band; @@ -45,9 +53,9 @@ void Equalizer::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) float b1 = m_b1[band]; float b2 = m_b2[band]; - for (uint32 i = 0; i < sampleCount; ++i) { + for (uint32 i = 0; i < frameCount; ++i) { uint32 sampleIndex = i * channels + ch; - float x0 = buffer[sampleIndex]; + float x0 = wetBuffer[sampleIndex]; float y0 = a0 * x0 + a1 * state.x1 + a2 * state.x2 - b1 * state.y1 - b2 * state.y2; @@ -56,10 +64,20 @@ void Equalizer::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) state.y2 = state.y1; state.y1 = y0; - buffer[sampleIndex] = y0; + wetBuffer[sampleIndex] = y0; } } } + + if (m_wetMix >= 1.0f) { + std::copy(wetBuffer.begin(), wetBuffer.end(), buffer); + return; + } + + const float dryMix = 1.0f - m_wetMix; + for (size_t i = 0; i < sampleCount; ++i) { + buffer[i] = dryBuffer[i] * dryMix + wetBuffer[i] * m_wetMix; + } } void Equalizer::SetBandCount(uint32 count) { @@ -67,17 +85,18 @@ void Equalizer::SetBandCount(uint32 count) { return; } - m_bandCount = count; - m_frequencies.resize(count, 1000.0f); - m_gains.resize(count, 0.0f); - m_qs.resize(count, 1.0f); - m_a0.resize(count, 1.0f); - m_a1.resize(count, 0.0f); - m_a2.resize(count, 0.0f); - m_b1.resize(count, 0.0f); - m_b2.resize(count, 0.0f); + m_bandCount = std::max(1u, count); + m_frequencies.resize(m_bandCount, 1000.0f); + m_gains.resize(m_bandCount, 0.0f); + m_qs.resize(m_bandCount, 1.0f); + m_a0.resize(m_bandCount, 1.0f); + m_a1.resize(m_bandCount, 0.0f); + m_a2.resize(m_bandCount, 0.0f); + m_b1.resize(m_bandCount, 0.0f); + m_b2.resize(m_bandCount, 0.0f); - m_bandStates.resize(count * 2); + m_bandStates.clear(); + m_stateChannels = 0; } void Equalizer::SetBandFrequency(uint32 band, float frequency) { @@ -136,6 +155,34 @@ void Equalizer::SetWetMix(float wetMix) { m_wetMix = std::max(0.0f, std::min(1.0f, wetMix)); } +void Equalizer::SetSampleRate(uint32 sampleRate) { + const uint32 clampedSampleRate = sampleRate > 0 ? sampleRate : 1u; + if (m_sampleRate == clampedSampleRate) { + return; + } + + IAudioEffect::SetSampleRate(clampedSampleRate); + for (uint32 i = 0; i < m_bandCount; ++i) { + ComputeCoefficients(i, m_frequencies[i], m_qs[i], m_gains[i]); + } + ResetState(); +} + +void Equalizer::ResetState() { + for (BandState& state : m_bandStates) { + state = {}; + } +} + +void Equalizer::EnsureStateCapacity(uint32 channels) { + if (m_stateChannels == channels && m_bandStates.size() == static_cast(channels) * m_bandCount) { + return; + } + + m_stateChannels = channels; + m_bandStates.assign(static_cast(channels) * m_bandCount, BandState{}); +} + void Equalizer::ComputeCoefficients(uint32 band, float frequency, float q, float gainDb) { if (band >= m_bandCount) { return; diff --git a/engine/src/Audio/FFTFilter.cpp b/engine/src/Audio/FFTFilter.cpp index de359984..354771d9 100644 --- a/engine/src/Audio/FFTFilter.cpp +++ b/engine/src/Audio/FFTFilter.cpp @@ -33,8 +33,8 @@ FFTFilter::~FFTFilter() { } } -void FFTFilter::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) { - if (!m_enabled || buffer == nullptr || sampleCount == 0) { +void FFTFilter::ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) { + if (!m_enabled || buffer == nullptr || frameCount == 0) { return; } @@ -42,10 +42,6 @@ void FFTFilter::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) return; } - if (sampleCount < m_fftSize) { - return; - } - uint32 binCount = m_fftSize / 2; if (m_spectrumData.size() != binCount) { m_spectrumData.resize(binCount); @@ -53,9 +49,13 @@ void FFTFilter::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) } std::vector monoBuffer(m_fftSize, 0.0f); - for (uint32 i = 0; i < m_fftSize; ++i) { - uint32 channelIndex = (channels == 1) ? 0 : (i % channels); - monoBuffer[i] = buffer[i * channels + channelIndex]; + const uint32 framesToCopy = std::min(frameCount, m_fftSize); + for (uint32 i = 0; i < framesToCopy; ++i) { + float monoSample = 0.0f; + for (uint32 ch = 0; ch < channels; ++ch) { + monoSample += buffer[i * channels + ch]; + } + monoBuffer[i] = monoSample / static_cast(channels); } ComputeFFT(monoBuffer.data(), m_fftSize); @@ -66,16 +66,21 @@ void FFTFilter::SetFFTSize(uint32 size) { return; } - m_fftSize = size; - m_spectrumData.resize(size / 2); - m_prevSpectrum.resize(size / 2); - InitializeFFT(size); + m_fftSize = std::max(2u, size); + m_spectrumData.assign(m_fftSize / 2, 0.0f); + m_prevSpectrum.assign(m_fftSize / 2, 0.0f); + InitializeFFT(m_fftSize); } void FFTFilter::SetSmoothingFactor(float factor) { m_smoothingFactor = std::max(0.0f, std::min(1.0f, factor)); } +void FFTFilter::ResetState() { + std::fill(m_spectrumData.begin(), m_spectrumData.end(), 0.0f); + std::fill(m_prevSpectrum.begin(), m_prevSpectrum.end(), 0.0f); +} + void FFTFilter::InitializeFFT(uint32 size) { if (m_fftConfig) { kiss_fft_free(static_cast(m_fftConfig)); diff --git a/engine/src/Audio/HRTF.cpp b/engine/src/Audio/HRTF.cpp index 853ee089..7e3b3d4e 100644 --- a/engine/src/Audio/HRTF.cpp +++ b/engine/src/Audio/HRTF.cpp @@ -1,10 +1,22 @@ #include #include #include +#include namespace XCEngine { namespace Audio { +namespace { + +static constexpr float PI_F = 3.14159265f; +static constexpr float HeadRadiusMeters = 0.075f; + +float DegreesToRadians(float degrees) { + return degrees * PI_F / 180.0f; +} + +} // namespace + HRTF::HRTF() : m_leftDelayLine(MaxDelaySamples, 0.0f) , m_rightDelayLine(MaxDelaySamples, 0.0f) @@ -14,11 +26,11 @@ HRTF::HRTF() 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) { +void HRTF::ProcessAudio(float* buffer, uint32 frameCount, uint32 channels, + const Math::Vector3& sourcePosition, + const Math::Vector3& listenerPosition, + const Math::Quaternion& listenerRotation) { + if (!m_enabled || buffer == nullptr || frameCount == 0 || channels < 2) { return; } @@ -28,16 +40,21 @@ void HRTF::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels, ComputeDirection(sourcePosition, listenerPosition, listenerRotation, azimuth, elevation); ComputeITD(azimuth, elevation); ComputeILD(azimuth, elevation); + m_params.headShadowing = std::min(1.0f, std::abs(azimuth) / 90.0f); + m_params.pinnaCues = ComputePinnaEffect(azimuth, elevation); m_params.azimuth = azimuth; m_params.elevation = elevation; if (m_hrtfEnabled) { - ApplyHRTF(buffer, sampleCount, channels); + ApplyHRTF(buffer, frameCount, channels); } else { - float pan = azimuth / 180.0f; - ApplySimplePanning(buffer, sampleCount, channels, pan); + const float pan = std::clamp(azimuth / 90.0f, -1.0f, 1.0f); + ApplySimplePanning(buffer, frameCount, channels, pan); } + + m_prevSourcePosition = sourcePosition; + m_prevListenerPosition = listenerPosition; } void HRTF::SetQualityLevel(uint32 level) { @@ -52,12 +69,24 @@ void HRTF::SetSpeedOfSound(float speed) { m_speedOfSound = std::max(1.0f, speed); } +void HRTF::SetSampleRate(uint32 sampleRate) { + m_sampleRate = std::max(1u, sampleRate); +} + +void HRTF::ResetState() { + std::fill(m_leftDelayLine.begin(), m_leftDelayLine.end(), 0.0f); + std::fill(m_rightDelayLine.begin(), m_rightDelayLine.end(), 0.0f); + m_leftDelayIndex = 0; + m_rightDelayIndex = 0; + m_prevDopplerShift = 1.0f; +} + void HRTF::ComputeDirection(const Math::Vector3& sourcePosition, - const Math::Vector3& listenerPosition, - const Math::Quaternion& listenerRotation, - float& azimuth, float& elevation) { + const Math::Vector3& listenerPosition, + const Math::Quaternion& listenerRotation, + float& azimuth, float& elevation) { Math::Vector3 direction = sourcePosition - listenerPosition; - float distance = Math::Vector3::Magnitude(direction); + const float distance = Math::Vector3::Magnitude(direction); if (distance < 0.001f) { azimuth = 0.0f; @@ -66,111 +95,117 @@ void HRTF::ComputeDirection(const Math::Vector3& sourcePosition, } direction = Math::Vector3::Normalize(direction); + const Math::Vector3 localDirection = listenerRotation.Inverse() * 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; + azimuth = std::atan2(localDirection.x, localDirection.z) * 180.0f / PI_F; + elevation = std::asin(std::clamp(localDirection.y, -1.0f, 1.0f)) * 180.0f / PI_F; - 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)); + azimuth = std::clamp(azimuth, -180.0f, 180.0f); + elevation = std::clamp(elevation, -90.0f, 90.0f); } void HRTF::ComputeITD(float azimuth, float elevation) { - float headRadius = 0.075f; + (void)elevation; - float cosAzimuth = std::cos(azimuth * 3.14159265f / 180.0f); - - float itd = (headRadius / m_speedOfSound) * (cosAzimuth - 1.0f); - - m_params.interauralTimeDelay = itd * m_sampleRate; + const float azimuthRadians = DegreesToRadians(azimuth); + const float itdSeconds = (HeadRadiusMeters / m_speedOfSound) * std::sin(azimuthRadians); + m_params.interauralTimeDelay = itdSeconds * static_cast(m_sampleRate); } void HRTF::ComputeILD(float azimuth, float elevation) { - float absAzimuth = std::abs(azimuth); + (void)elevation; - float ild = (absAzimuth / 90.0f) * 20.0f; - - m_params.interauralLevelDifference = ild; + const float absAzimuth = std::abs(azimuth); + m_params.interauralLevelDifference = (absAzimuth / 90.0f) * 18.0f; } 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; + const float frontalBias = 1.0f - std::min(1.0f, std::abs(azimuth) / 180.0f); + const float elevationBoost = elevation > 0.0f ? (elevation / 90.0f) * 0.25f : 0.0f; + return std::clamp(0.85f + frontalBias * 0.15f + elevationBoost, 0.5f, 1.25f); } -void HRTF::ApplyHRTF(float* buffer, uint32 sampleCount, uint32 channels) { - if (channels < 2) { - return; - } +void HRTF::ApplyHRTF(float* buffer, uint32 frameCount, uint32 channels) { + const float normalizedAzimuth = std::clamp(m_params.azimuth / 90.0f, -1.0f, 1.0f); + const float panAngle = (normalizedAzimuth + 1.0f) * PI_F * 0.25f; + float leftGain = std::cos(panAngle); + float rightGain = std::sin(panAngle); - float leftGain = 1.0f; - float rightGain = 1.0f; - - if (m_params.azimuth < 0.0f) { - rightGain *= (1.0f - std::abs(m_params.azimuth) / 90.0f); + const float ildAttenuation = + std::pow(10.0f, -std::max(0.0f, m_params.interauralLevelDifference) / 20.0f); + if (m_params.azimuth >= 0.0f) { + leftGain *= ildAttenuation; } else { - leftGain *= (1.0f - std::abs(m_params.azimuth) / 90.0f); + rightGain *= ildAttenuation; } - float levelReduction = m_params.interauralLevelDifference / 20.0f; - rightGain *= std::pow(10.0f, -levelReduction / 20.0f); + const float pinnaGain = m_params.pinnaCues; + leftGain *= pinnaGain; + rightGain *= pinnaGain; - for (uint32 i = 0; i < sampleCount; ++i) { - uint32 sampleIndex = i * channels; + const uint32 delaySamples = std::min( + static_cast(std::abs(std::lround(m_params.interauralTimeDelay))), + MaxDelaySamples - 1); + const bool delayLeftEar = m_params.interauralTimeDelay > 0.0f; - float leftSample = buffer[sampleIndex]; - float rightSample = buffer[sampleIndex + 1]; + for (uint32 frame = 0; frame < frameCount; ++frame) { + const uint32 sampleIndex = frame * channels; - float delayedLeft = m_leftDelayLine[m_leftDelayIndex]; - float delayedRight = m_rightDelayLine[m_rightDelayIndex]; + float monoSample = 0.0f; + for (uint32 ch = 0; ch < channels; ++ch) { + monoSample += buffer[sampleIndex + ch]; + } + monoSample /= static_cast(channels); - buffer[sampleIndex] = delayedLeft * leftGain; - buffer[sampleIndex + 1] = delayedRight * rightGain; + m_leftDelayLine[m_leftDelayIndex] = monoSample; + m_rightDelayLine[m_rightDelayIndex] = monoSample; - m_leftDelayLine[m_leftDelayIndex] = leftSample; - m_rightDelayLine[m_rightDelayIndex] = rightSample; + const uint32 delayedLeftIndex = + (m_leftDelayIndex + MaxDelaySamples - (delayLeftEar ? delaySamples : 0)) % MaxDelaySamples; + const uint32 delayedRightIndex = + (m_rightDelayIndex + MaxDelaySamples - (delayLeftEar ? 0 : delaySamples)) % MaxDelaySamples; + + float leftSample = m_leftDelayLine[delayedLeftIndex] * leftGain; + float rightSample = m_rightDelayLine[delayedRightIndex] * rightGain; + + if (m_crossFeed > 0.0f) { + const float cross = std::clamp(m_crossFeed, 0.0f, 1.0f) * 0.5f; + const float mixedLeft = leftSample * (1.0f - cross) + rightSample * cross; + const float mixedRight = rightSample * (1.0f - cross) + leftSample * cross; + leftSample = mixedLeft; + rightSample = mixedRight; + } + + buffer[sampleIndex] = leftSample; + buffer[sampleIndex + 1] = rightSample; + for (uint32 ch = 2; ch < channels; ++ch) { + buffer[sampleIndex + ch] = (leftSample + rightSample) * 0.5f; + } 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; - } +void HRTF::ApplySimplePanning(float* buffer, uint32 frameCount, uint32 channels, float pan) { + pan = std::clamp(pan, -1.0f, 1.0f); + const float panAngle = (pan + 1.0f) * PI_F * 0.25f; + const float leftGain = std::cos(panAngle); + const float rightGain = std::sin(panAngle); - pan = std::max(-1.0f, std::min(1.0f, pan)); + for (uint32 frame = 0; frame < frameCount; ++frame) { + const uint32 sampleIndex = frame * channels; + float monoSample = 0.0f; + for (uint32 ch = 0; ch < channels; ++ch) { + monoSample += buffer[sampleIndex + ch]; + } + monoSample /= static_cast(channels); - 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; + buffer[sampleIndex] = monoSample * leftGain; + buffer[sampleIndex + 1] = monoSample * rightGain; + for (uint32 ch = 2; ch < channels; ++ch) { + buffer[sampleIndex + ch] = monoSample; + } } } diff --git a/engine/src/Audio/Reverbation.cpp b/engine/src/Audio/Reverbation.cpp index 72b269ed..60fb5617 100644 --- a/engine/src/Audio/Reverbation.cpp +++ b/engine/src/Audio/Reverbation.cpp @@ -1,33 +1,27 @@ #include #include -#include +#include namespace XCEngine { namespace Audio { -static const uint32 CombTuning[] = { 1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617 }; -static const uint32 AllPassTuning[] = { 556, 441, 341, 225 }; +namespace { -Reverbation::Reverbation() - : m_sampleRate(48000) -{ - for (uint32 i = 0; i < CombCount; ++i) { - m_combFilters[i].bufferSize = CombTuning[i]; - m_combFilters[i].buffer.resize(m_combFilters[i].bufferSize, 0.0f); - m_combFilters[i].writeIndex = 0; - m_combFilters[i].feedback = 0.0f; - m_combFilters[i].damp1 = 0.0f; - m_combFilters[i].damp2 = 0.0f; - m_combFilters[i].filterStore = 0.0f; - } +static const uint32 CombTuning[] = {1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617}; +static const uint32 AllPassTuning[] = {556, 441, 341, 225}; +static constexpr uint32 StereoCombSpread = 23; +static constexpr uint32 StereoAllPassSpread = 11; +static constexpr float WetScale = 0.015f; - for (uint32 i = 0; i < AllPassCount; ++i) { - m_allPassFilters[i].bufferSize = AllPassTuning[i]; - m_allPassFilters[i].buffer.resize(m_allPassFilters[i].bufferSize, 0.0f); - m_allPassFilters[i].writeIndex = 0; - m_allPassFilters[i].feedback = 0.5f; - } +uint32 ScaleDelayLength(uint32 tuning, uint32 sampleRate) { + const float scaled = static_cast(tuning) * static_cast(sampleRate) / 48000.0f; + return std::max(1u, static_cast(std::lround(scaled))); +} +} // namespace + +Reverbation::Reverbation() { + SetWetMix(0.3f); SetRoomSize(0.5f); SetDamping(0.5f); } @@ -35,72 +29,54 @@ Reverbation::Reverbation() Reverbation::~Reverbation() { } -void Reverbation::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) { - if (!m_enabled || buffer == nullptr || sampleCount == 0) { +void Reverbation::ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) { + if (!m_enabled || buffer == nullptr || frameCount == 0 || channels == 0) { return; } - if (channels == 0) { + if (m_wetMix <= 0.0f && m_dryMix >= 1.0f) { return; } - for (uint32 i = 0; i < sampleCount; ++i) { - float input = buffer[i * channels]; + EnsureChannelState(channels); - float wet = 0.0f; - - for (uint32 c = 0; c < CombCount; ++c) { - float output = m_combFilters[c].buffer[m_combFilters[c].writeIndex]; - - m_combFilters[c].filterStore = output * m_combFilters[c].damp2 + m_combFilters[c].filterStore * m_combFilters[c].damp1; - - m_combFilters[c].buffer[m_combFilters[c].writeIndex] = input + m_combFilters[c].filterStore * m_combFilters[c].feedback; - - m_combFilters[c].writeIndex++; - if (m_combFilters[c].writeIndex >= m_combFilters[c].bufferSize) { - m_combFilters[c].writeIndex = 0; - } - - wet += output; - } - - for (uint32 a = 0; a < AllPassCount; ++a) { - float output = m_allPassFilters[a].buffer[m_allPassFilters[a].writeIndex]; - float temp = output; - - m_allPassFilters[a].buffer[m_allPassFilters[a].writeIndex] = wet + output * m_allPassFilters[a].feedback; - - wet = -wet + output; - wet += temp; - - m_allPassFilters[a].writeIndex++; - if (m_allPassFilters[a].writeIndex >= m_allPassFilters[a].bufferSize) { - m_allPassFilters[a].writeIndex = 0; - } - } - - float outSample = input * m_dryMix + wet * m_wetMix; + const size_t sampleCount = static_cast(frameCount) * channels; + std::vector dryBuffer(buffer, buffer + sampleCount); + std::vector wetBuffer(sampleCount, 0.0f); + for (uint32 frame = 0; frame < frameCount; ++frame) { + const size_t frameOffset = static_cast(frame) * channels; for (uint32 ch = 0; ch < channels; ++ch) { - buffer[i * channels + ch] = outSample; + wetBuffer[frameOffset + ch] = ProcessChannelSample(ch, dryBuffer[frameOffset + ch]); } + + for (uint32 ch = 0; ch + 1 < channels; ch += 2) { + const size_t leftIndex = frameOffset + ch; + const size_t rightIndex = leftIndex + 1; + const float leftWet = wetBuffer[leftIndex]; + const float rightWet = wetBuffer[rightIndex]; + const float width = std::clamp(m_width, 0.0f, 1.0f); + const float leftMix = 0.5f + 0.5f * width; + const float rightMix = 0.5f - 0.5f * width; + + wetBuffer[leftIndex] = leftWet * leftMix + rightWet * rightMix; + wetBuffer[rightIndex] = rightWet * leftMix + leftWet * rightMix; + } + } + + for (size_t i = 0; i < sampleCount; ++i) { + buffer[i] = dryBuffer[i] * m_dryMix + wetBuffer[i] * m_wetMix; } } void Reverbation::SetRoomSize(float size) { m_roomSize = std::max(0.0f, std::min(1.0f, size)); - float roomScale = 0.28f + 0.7f * m_roomSize; - for (uint32 i = 0; i < CombCount; ++i) { - m_combFilters[i].feedback = roomScale; - } + UpdateCombParameters(); } void Reverbation::SetDamping(float damping) { m_damping = std::max(0.0f, std::min(1.0f, damping)); - for (uint32 i = 0; i < CombCount; ++i) { - m_combFilters[i].damp1 = m_damping * 0.4f; - m_combFilters[i].damp2 = 1.0f - m_damping * 0.4f; - } + UpdateCombParameters(); } void Reverbation::SetWetMix(float wetMix) { @@ -119,5 +95,122 @@ void Reverbation::SetFreeze(bool freeze) { m_freeze = freeze; } +void Reverbation::SetSampleRate(uint32 sampleRate) { + const uint32 clampedSampleRate = sampleRate > 0 ? sampleRate : 1u; + if (m_sampleRate == clampedSampleRate) { + return; + } + + IAudioEffect::SetSampleRate(clampedSampleRate); + if (m_stateChannels == 0) { + return; + } + + const uint32 channels = m_stateChannels; + m_stateChannels = 0; + EnsureChannelState(channels); +} + +void Reverbation::ResetState() { + for (ChannelState& state : m_channelStates) { + for (uint32 i = 0; i < CombCount; ++i) { + std::fill(state.combFilters[i].buffer.begin(), state.combFilters[i].buffer.end(), 0.0f); + state.combFilters[i].writeIndex = 0; + state.combFilters[i].filterStore = 0.0f; + } + + for (uint32 i = 0; i < AllPassCount; ++i) { + std::fill(state.allPassFilters[i].buffer.begin(), state.allPassFilters[i].buffer.end(), 0.0f); + state.allPassFilters[i].writeIndex = 0; + } + } +} + +void Reverbation::EnsureChannelState(uint32 channels) { + if (m_stateChannels == channels && m_channelStates.size() == channels) { + return; + } + + m_channelStates.assign(channels, ChannelState{}); + m_stateChannels = channels; + for (uint32 ch = 0; ch < channels; ++ch) { + InitializeChannelState(m_channelStates[ch], ch); + } + UpdateCombParameters(); + ResetState(); +} + +void Reverbation::InitializeChannelState(ChannelState& state, uint32 channelIndex) { + const uint32 combSpread = (channelIndex % 2 == 1) ? StereoCombSpread : 0u; + const uint32 allPassSpread = (channelIndex % 2 == 1) ? StereoAllPassSpread : 0u; + + for (uint32 i = 0; i < CombCount; ++i) { + CombFilter& comb = state.combFilters[i]; + comb.bufferSize = std::min( + MaxDelayLength, + ScaleDelayLength(CombTuning[i] + combSpread, m_sampleRate)); + comb.buffer.assign(comb.bufferSize, 0.0f); + comb.writeIndex = 0; + comb.filterStore = 0.0f; + } + + for (uint32 i = 0; i < AllPassCount; ++i) { + AllPassFilter& allPass = state.allPassFilters[i]; + allPass.bufferSize = std::min( + MaxDelayLength, + ScaleDelayLength(AllPassTuning[i] + allPassSpread, m_sampleRate)); + allPass.buffer.assign(allPass.bufferSize, 0.0f); + allPass.writeIndex = 0; + allPass.feedback = 0.5f; + } +} + +void Reverbation::UpdateCombParameters() { + const float roomScale = 0.28f + 0.7f * m_roomSize; + const float damp1 = m_damping * 0.4f; + const float damp2 = 1.0f - damp1; + + for (ChannelState& state : m_channelStates) { + for (uint32 i = 0; i < CombCount; ++i) { + state.combFilters[i].feedback = roomScale; + state.combFilters[i].damp1 = damp1; + state.combFilters[i].damp2 = damp2; + } + } +} + +float Reverbation::ProcessChannelSample(uint32 channelIndex, float inputSample) { + if (channelIndex >= m_channelStates.size()) { + return 0.0f; + } + + ChannelState& state = m_channelStates[channelIndex]; + const float effectiveInput = m_freeze ? 0.0f : inputSample; + float wet = 0.0f; + + for (uint32 i = 0; i < CombCount; ++i) { + CombFilter& comb = state.combFilters[i]; + const float output = comb.buffer[comb.writeIndex]; + const float damp1 = m_freeze ? 0.0f : comb.damp1; + const float damp2 = m_freeze ? 1.0f : comb.damp2; + const float feedback = m_freeze ? 1.0f : comb.feedback; + + comb.filterStore = output * damp2 + comb.filterStore * damp1; + comb.buffer[comb.writeIndex] = effectiveInput + comb.filterStore * feedback; + comb.writeIndex = (comb.writeIndex + 1) % comb.bufferSize; + wet += output; + } + + for (uint32 i = 0; i < AllPassCount; ++i) { + AllPassFilter& allPass = state.allPassFilters[i]; + const float buffered = allPass.buffer[allPass.writeIndex]; + allPass.buffer[allPass.writeIndex] = wet + buffered * allPass.feedback; + wet = buffered - wet; + allPass.writeIndex = (allPass.writeIndex + 1) % allPass.bufferSize; + } + + return wet * WetScale; +} + } // namespace Audio } // namespace XCEngine diff --git a/engine/src/Core/Asset/ResourceManager.cpp b/engine/src/Core/Asset/ResourceManager.cpp index 37d509b7..225fe16a 100644 --- a/engine/src/Core/Asset/ResourceManager.cpp +++ b/engine/src/Core/Asset/ResourceManager.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +48,7 @@ void RegisterBuiltinLoader(ResourceManager& manager, TLoader& loader) { } GaussianSplatLoader g_gaussianSplatLoader; +AudioLoader g_audioLoader; MaterialLoader g_materialLoader; ModelLoader g_modelLoader; MeshLoader g_meshLoader; @@ -93,6 +95,7 @@ void ResourceManager::EnsureInitialized() { asyncLoader->Initialize(2); RegisterBuiltinLoader(*this, g_gaussianSplatLoader); + RegisterBuiltinLoader(*this, g_audioLoader); RegisterBuiltinLoader(*this, g_materialLoader); RegisterBuiltinLoader(*this, g_modelLoader); RegisterBuiltinLoader(*this, g_meshLoader); diff --git a/engine/src/Resources/AudioClip/AudioLoader.cpp b/engine/src/Resources/AudioClip/AudioLoader.cpp index 0e75aca9..86160328 100644 --- a/engine/src/Resources/AudioClip/AudioLoader.cpp +++ b/engine/src/Resources/AudioClip/AudioLoader.cpp @@ -13,52 +13,89 @@ struct WAVHeader { uint32_t channels = 2; uint32_t bitsPerSample = 16; uint32_t dataSize = 0; - uint32_t dataOffset = 44; + uint32_t dataOffset = 0; }; +uint16_t ReadLE16(const uint8_t* data) { + return static_cast(data[0]) | + (static_cast(data[1]) << 8); +} + +uint32_t ReadLE32(const uint8_t* data) { + return static_cast(data[0]) | + (static_cast(data[1]) << 8) | + (static_cast(data[2]) << 16) | + (static_cast(data[3]) << 24); +} + bool ParseWAVHeader(const uint8_t* data, size_t size, WAVHeader& header) { - if (size < 44) { + if (size < 12) { return false; } - if (data[0] != 'R' || data[1] != 'I' || data[2] != 'F' || data[3] != 'F') { + if (std::memcmp(data, "RIFF", 4) != 0) { return false; } - if (data[8] != 'W' || data[9] != 'A' || data[10] != 'V' || data[11] != 'E') { + if (std::memcmp(data + 8, "WAVE", 4) != 0) { return false; } - if (data[12] != 'f' || data[13] != 'm' || data[14] != 't' || data[15] != ' ') { + bool foundFormatChunk = false; + bool foundDataChunk = false; + size_t offset = 12; + + while (offset + 8 <= size) { + const uint8_t* chunk = data + offset; + const uint32_t chunkSize = ReadLE32(chunk + 4); + const size_t chunkDataOffset = offset + 8; + + if (chunkDataOffset > size || chunkSize > size - chunkDataOffset) { + return false; + } + + if (std::memcmp(chunk, "fmt ", 4) == 0) { + if (chunkSize < 16) { + return false; + } + + const uint16_t audioFormat = ReadLE16(chunk + 8); + if (audioFormat != 1) { + return false; + } + + header.channels = ReadLE16(chunk + 10); + header.sampleRate = ReadLE32(chunk + 12); + header.bitsPerSample = ReadLE16(chunk + 22); + foundFormatChunk = true; + } else if (std::memcmp(chunk, "data", 4) == 0) { + header.dataOffset = static_cast(chunkDataOffset); + header.dataSize = chunkSize; + foundDataChunk = true; + } + + offset = chunkDataOffset + chunkSize + (chunkSize & 1u); + } + + if (!foundFormatChunk || !foundDataChunk) { return false; } - uint32_t subchunk1Size = *reinterpret_cast(&data[16]); - if (subchunk1Size < 16) { + if (header.channels == 0 || header.sampleRate == 0 || header.bitsPerSample == 0) { return false; } - uint16_t audioFormat = *reinterpret_cast(&data[20]); - if (audioFormat != 1) { + if ((header.bitsPerSample % 8u) != 0u) { return false; } - header.channels = *reinterpret_cast(&data[22]); - header.sampleRate = *reinterpret_cast(&data[24]); - header.bitsPerSample = *reinterpret_cast(&data[34]); - - if (data[36] != 'd' || data[37] != 'a' || data[38] != 't' || data[39] != 'a') { + const uint32_t bytesPerFrame = + static_cast(header.channels) * (header.bitsPerSample / 8u); + if (bytesPerFrame == 0 || (header.dataSize % bytesPerFrame) != 0u) { return false; } - header.dataSize = *reinterpret_cast(&data[40]); - header.dataOffset = 44; - - if (header.dataOffset + header.dataSize > size) { - return false; - } - - return true; + return header.dataOffset + header.dataSize <= size; } } // namespace @@ -70,18 +107,12 @@ AudioLoader::~AudioLoader() = default; Containers::Array AudioLoader::GetSupportedExtensions() const { Containers::Array extensions; extensions.PushBack("wav"); - extensions.PushBack("ogg"); - extensions.PushBack("mp3"); - extensions.PushBack("flac"); - extensions.PushBack("aiff"); - extensions.PushBack("aif"); return extensions; } bool AudioLoader::CanLoad(const Containers::String& path) const { Containers::String ext = GetExtension(path); - return ext == "wav" || ext == "ogg" || ext == "mp3" || - ext == "flac" || ext == "aiff" || ext == "aif"; + return ext == "wav"; } LoadResult AudioLoader::Load(const Containers::String& path, const ImportSettings* settings) { @@ -96,18 +127,21 @@ LoadResult AudioLoader::Load(const Containers::String& path, const ImportSetting audioClip->m_guid = ResourceGUID::Generate(path); AudioFormat format = DetectAudioFormat(path, data); - if (format == AudioFormat::WAV) { - if (!ParseWAVData(data, audioClip)) { - delete audioClip; - return LoadResult("Failed to parse WAV data"); - } + if (format != AudioFormat::WAV) { + delete audioClip; + return LoadResult("Unsupported audio format: " + path); } + + if (!ParseWAVData(data, audioClip)) { + delete audioClip; + return LoadResult("Failed to parse WAV data"); + } + audioClip->SetAudioFormat(format); - audioClip->SetAudioData(data); audioClip->m_isValid = true; audioClip->m_memorySize = sizeof(AudioClip) + audioClip->m_name.Length() + - audioClip->m_path.Length() + audioClip->GetAudioData().Size(); + audioClip->m_path.Length() + audioClip->GetPCMData().Size(); return LoadResult(audioClip); } @@ -117,6 +151,10 @@ ImportSettings* AudioLoader::GetDefaultSettings() const { } bool AudioLoader::ParseWAVData(const Containers::Array& data, AudioClip* audioClip) { + if (audioClip == nullptr) { + return false; + } + WAVHeader header; if (!ParseWAVHeader(data.Data(), data.Size(), header)) { return false; @@ -126,10 +164,15 @@ bool AudioLoader::ParseWAVData(const Containers::Array& data, Audio audioClip->SetChannels(header.channels); audioClip->SetBitsPerSample(header.bitsPerSample); - uint32_t bytesPerSample = header.bitsPerSample / 8; - uint32_t totalSamples = header.dataSize / (bytesPerSample * header.channels); - float duration = static_cast(totalSamples) / (header.sampleRate * header.channels); - audioClip->SetDuration(duration); + Containers::Array pcmData; + pcmData.ResizeUninitialized(header.dataSize); + if (header.dataSize > 0) { + std::memcpy( + pcmData.Data(), + data.Data() + header.dataOffset, + header.dataSize); + } + audioClip->SetPCMData(pcmData); return true; } diff --git a/tests/Components/CMakeLists.txt b/tests/Components/CMakeLists.txt index 1060e723..8e06587d 100644 --- a/tests/Components/CMakeLists.txt +++ b/tests/Components/CMakeLists.txt @@ -4,7 +4,11 @@ project(XCEngine_ComponentsTests) set(COMPONENTS_TEST_SOURCES test_component.cpp + test_audio_mixer.cpp + test_audio_system.cpp + test_windows_audio_backend.cpp test_component_factory_registry.cpp + test_audio_source_component.cpp test_transform_component.cpp test_game_object.cpp test_camera_light_component.cpp diff --git a/tests/Resources/AudioClip/test_audio_loader.cpp b/tests/Resources/AudioClip/test_audio_loader.cpp index dff9a8f0..7fe42e09 100644 --- a/tests/Resources/AudioClip/test_audio_loader.cpp +++ b/tests/Resources/AudioClip/test_audio_loader.cpp @@ -65,7 +65,10 @@ TEST(AudioLoader, ParseWAV_Mono44100_16bit) { EXPECT_EQ(clip->GetSampleRate(), 44100u); EXPECT_EQ(clip->GetChannels(), 1u); EXPECT_EQ(clip->GetBitsPerSample(), 16u); - EXPECT_GT(clip->GetDuration(), 0.0f); + EXPECT_EQ(clip->GetPCMData().Size(), 44100u * sizeof(int16_t)); + EXPECT_EQ(clip->GetFrameCount(), 44100u); + EXPECT_EQ(clip->GetSampleCount(), 44100u); + EXPECT_FLOAT_EQ(clip->GetDuration(), 1.0f); } std::remove(testPath); @@ -84,6 +87,10 @@ TEST(AudioLoader, ParseWAV_Stereo48000_16bit) { EXPECT_EQ(clip->GetSampleRate(), 48000u); EXPECT_EQ(clip->GetChannels(), 2u); EXPECT_EQ(clip->GetBitsPerSample(), 16u); + EXPECT_EQ(clip->GetPCMData().Size(), 4800u * 2u * sizeof(int16_t)); + EXPECT_EQ(clip->GetFrameCount(), 4800u); + EXPECT_EQ(clip->GetSampleCount(), 9600u); + EXPECT_FLOAT_EQ(clip->GetDuration(), 0.1f); } std::remove(testPath); @@ -97,14 +104,15 @@ TEST(AudioLoader, GetResourceType) { TEST(AudioLoader, GetSupportedExtensions) { AudioLoader loader; auto extensions = loader.GetSupportedExtensions(); - EXPECT_GE(extensions.Size(), 1u); + ASSERT_EQ(extensions.Size(), 1u); + EXPECT_EQ(extensions[0], XCEngine::Containers::String("wav")); } TEST(AudioLoader, CanLoad) { AudioLoader loader; EXPECT_TRUE(loader.CanLoad("test.wav")); - EXPECT_TRUE(loader.CanLoad("test.mp3")); - EXPECT_TRUE(loader.CanLoad("test.ogg")); + EXPECT_FALSE(loader.CanLoad("test.mp3")); + EXPECT_FALSE(loader.CanLoad("test.ogg")); EXPECT_FALSE(loader.CanLoad("test.txt")); EXPECT_FALSE(loader.CanLoad("test.png")); } @@ -115,4 +123,13 @@ TEST(AudioLoader, LoadInvalidPath) { EXPECT_FALSE(result); } +TEST(AudioLoader, ResourceManagerRegistersAudioLoader) { + ResourceManager& manager = ResourceManager::Get(); + manager.Initialize(); + + EXPECT_NE(manager.GetLoader(ResourceType::AudioClip), nullptr); + + manager.Shutdown(); +} + } // namespace