Files
XCEngine/tests/Components/test_audio_source_component.cpp

512 lines
16 KiB
C++

#include <gtest/gtest.h>
#include <XCEngine/Components/AudioSourceComponent.h>
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Core/Math/Quaternion.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <sstream>
using namespace XCEngine::Components;
using namespace XCEngine::Resources;
namespace {
AudioClip CreateMono16Clip(std::initializer_list<int16_t> samples, XCEngine::Core::uint32 sampleRate = 4) {
AudioClip clip;
XCEngine::Containers::Array<XCEngine::Core::uint8> pcmData;
pcmData.ResizeUninitialized(samples.size() * sizeof(int16_t));
size_t byteOffset = 0;
for (const int16_t sample : samples) {
const uint16_t encoded = static_cast<uint16_t>(sample);
pcmData[byteOffset++] = static_cast<XCEngine::Core::uint8>(encoded & 0xFFu);
pcmData[byteOffset++] = static_cast<XCEngine::Core::uint8>((encoded >> 8) & 0xFFu);
}
clip.SetSampleRate(sampleRate);
clip.SetChannels(1);
clip.SetBitsPerSample(16);
clip.SetAudioFormat(AudioFormat::WAV);
clip.SetPCMData(pcmData);
clip.m_isValid = true;
return clip;
}
AudioClip CreateStereo16Clip(std::initializer_list<int16_t> samples, XCEngine::Core::uint32 sampleRate = 4) {
AudioClip clip;
XCEngine::Containers::Array<XCEngine::Core::uint8> pcmData;
pcmData.ResizeUninitialized(samples.size() * sizeof(int16_t));
size_t byteOffset = 0;
for (const int16_t sample : samples) {
const uint16_t encoded = static_cast<uint16_t>(sample);
pcmData[byteOffset++] = static_cast<XCEngine::Core::uint8>(encoded & 0xFFu);
pcmData[byteOffset++] = static_cast<XCEngine::Core::uint8>((encoded >> 8) & 0xFFu);
}
clip.SetSampleRate(sampleRate);
clip.SetChannels(2);
clip.SetBitsPerSample(16);
clip.SetAudioFormat(AudioFormat::WAV);
clip.SetPCMData(pcmData);
clip.m_isValid = true;
return clip;
}
TEST(AudioSourceComponent, MonoClipMapsToStereoOutput) {
AudioClip clip = CreateMono16Clip({32767, -32768}, 4);
AudioSourceComponent source;
source.SetSpatialize(false);
source.SetClip(&clip);
source.Play();
float buffer[4] = {};
source.ProcessAudio(
buffer,
2,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_NEAR(buffer[0], 32767.0f / 32768.0f, 1e-5f);
EXPECT_NEAR(buffer[1], 32767.0f / 32768.0f, 1e-5f);
EXPECT_NEAR(buffer[2], -1.0f, 1e-5f);
EXPECT_NEAR(buffer[3], -1.0f, 1e-5f);
EXPECT_FALSE(source.IsPlaying());
EXPECT_FLOAT_EQ(source.GetTime(), 0.0f);
}
TEST(AudioSourceComponent, PauseSilencesUntilResumed) {
AudioClip clip = CreateMono16Clip({32767, 16384}, 4);
AudioSourceComponent source;
source.SetSpatialize(false);
source.SetClip(&clip);
source.Play();
source.Pause();
float pausedBuffer[1] = {};
source.ProcessAudio(
pausedBuffer,
1,
1,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_FLOAT_EQ(pausedBuffer[0], 0.0f);
source.Play();
float resumedBuffer[1] = {};
source.ProcessAudio(
resumedBuffer,
1,
1,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_NEAR(resumedBuffer[0], 32767.0f / 32768.0f, 1e-5f);
}
TEST(AudioSourceComponent, LoopingWrapsAtClipEnd) {
AudioClip clip = CreateMono16Clip({32767, -32768}, 4);
AudioSourceComponent source;
source.SetSpatialize(false);
source.SetLooping(true);
source.SetClip(&clip);
source.Play();
float buffer[4] = {};
source.ProcessAudio(
buffer,
4,
1,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_NEAR(buffer[0], 32767.0f / 32768.0f, 1e-5f);
EXPECT_NEAR(buffer[1], -1.0f, 1e-5f);
EXPECT_NEAR(buffer[2], 32767.0f / 32768.0f, 1e-5f);
EXPECT_NEAR(buffer[3], -1.0f, 1e-5f);
EXPECT_TRUE(source.IsPlaying());
}
TEST(AudioSourceComponent, PitchSupportsFractionalFramePlayback) {
AudioClip clip = CreateMono16Clip({0, 32767, 0}, 4);
AudioSourceComponent source;
source.SetSpatialize(false);
source.SetPitch(0.5f);
source.SetClip(&clip);
source.Play();
float buffer[3] = {};
source.ProcessAudio(
buffer,
3,
1,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
const float peak = 32767.0f / 32768.0f;
EXPECT_NEAR(buffer[0], 0.0f, 1e-5f);
EXPECT_NEAR(buffer[1], peak * 0.5f, 1e-4f);
EXPECT_NEAR(buffer[2], peak, 1e-5f);
EXPECT_TRUE(source.IsPlaying());
EXPECT_NEAR(source.GetTime(), 0.375f, 1e-5f);
}
TEST(AudioSourceComponent, PanControlsStereoBalanceForMonoClip) {
AudioClip clip = CreateMono16Clip({32767}, 4);
AudioSourceComponent leftSource;
leftSource.SetSpatialize(false);
leftSource.SetPan(-1.0f);
leftSource.SetClip(&clip);
leftSource.Play();
float leftBuffer[2] = {};
leftSource.ProcessAudio(
leftBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_NEAR(leftBuffer[0], 32767.0f / 32768.0f, 1e-5f);
EXPECT_NEAR(leftBuffer[1], 0.0f, 1e-5f);
AudioSourceComponent rightSource;
rightSource.SetSpatialize(false);
rightSource.SetPan(1.0f);
rightSource.SetClip(&clip);
rightSource.Play();
float rightBuffer[2] = {};
rightSource.ProcessAudio(
rightBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_NEAR(rightBuffer[0], 0.0f, 1e-5f);
EXPECT_NEAR(rightBuffer[1], 32767.0f / 32768.0f, 1e-5f);
}
TEST(AudioSourceComponent, DopplerAdjustsPlaybackRateFromListenerVelocity) {
AudioClip clip = CreateMono16Clip({0, 32767, 0, 0, 0}, 4);
GameObject sourceObject("AudioSourceObject");
sourceObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
auto* source = sourceObject.AddComponent<AudioSourceComponent>();
XCEngine::Audio::Audio3DParams params;
params.minDistance = 100.0f;
params.maxDistance = 100000.0f;
source->Set3DParams(params);
source->SetClip(&clip);
source->Play();
float buffer[2] = {};
source->ProcessAudio(
buffer,
2,
1,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity(),
XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f),
1.0f,
20.0f,
4);
const float peak = 32767.0f / 32768.0f;
EXPECT_NEAR(buffer[0], 0.0f, 1e-5f);
EXPECT_NEAR(buffer[1], peak * 0.5f, 1e-3f);
EXPECT_TRUE(source->IsPlaying());
}
TEST(AudioSourceComponent, SourceVelocityAdjustsDopplerWhenMovingAway) {
AudioClip clip = CreateMono16Clip({0, 32767, 0}, 4);
GameObject sourceObject("MovingAudioSource");
sourceObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
auto* source = sourceObject.AddComponent<AudioSourceComponent>();
XCEngine::Audio::Audio3DParams params;
params.minDistance = 100.0f;
params.maxDistance = 100000.0f;
source->Set3DParams(params);
source->SetClip(&clip);
source->Play();
source->Update(0.0f);
sourceObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(20.0f, 0.0f, 0.0f));
source->Update(1.0f);
float buffer[2] = {};
source->ProcessAudio(
buffer,
2,
1,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity(),
XCEngine::Math::Vector3::Zero(),
1.0f,
20.0f,
4);
const float peak = 32767.0f / 32768.0f;
EXPECT_NEAR(buffer[0], 0.0f, 1e-5f);
EXPECT_NEAR(buffer[1], peak * (20.0f / 30.0f), 1e-3f);
}
TEST(AudioSourceComponent, SpatialPanUsesListenerRotationAndPanLevel) {
AudioClip clip = CreateMono16Clip({32767}, 4);
GameObject rightSourceObject("RightSource");
rightSourceObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
auto* rightSource = rightSourceObject.AddComponent<AudioSourceComponent>();
XCEngine::Audio::Audio3DParams params;
params.minDistance = 0.0f;
params.maxDistance = 100000.0f;
params.panLevel = 1.0f;
rightSource->Set3DParams(params);
rightSource->SetClip(&clip);
rightSource->Play();
float identityBuffer[2] = {};
rightSource->ProcessAudio(
identityBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_GT(identityBuffer[1], identityBuffer[0]);
GameObject rotatedSourceObject("RotatedRightSource");
rotatedSourceObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
auto* rotatedSource = rotatedSourceObject.AddComponent<AudioSourceComponent>();
rotatedSource->Set3DParams(params);
rotatedSource->SetClip(&clip);
rotatedSource->Play();
float rotatedBuffer[2] = {};
rotatedSource->ProcessAudio(
rotatedBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::FromEulerAngles(0.0f, XCEngine::Math::PI, 0.0f));
EXPECT_GT(rotatedBuffer[0], rotatedBuffer[1]);
GameObject neutralSourceObject("NeutralPanSource");
neutralSourceObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
auto* neutralSource = neutralSourceObject.AddComponent<AudioSourceComponent>();
params.panLevel = 0.0f;
neutralSource->Set3DParams(params);
neutralSource->SetClip(&clip);
neutralSource->Play();
float neutralBuffer[2] = {};
neutralSource->ProcessAudio(
neutralBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_NEAR(neutralBuffer[0], neutralBuffer[1], 1e-5f);
}
TEST(AudioSourceComponent, SpreadControlsStereoWidthWhenSpatialized) {
AudioClip clip = CreateStereo16Clip({32767, 0}, 4);
GameObject narrowSourceObject("NarrowStereoSource");
narrowSourceObject.GetTransform()->SetPosition(XCEngine::Math::Vector3::Zero());
auto* narrowSource = narrowSourceObject.AddComponent<AudioSourceComponent>();
XCEngine::Audio::Audio3DParams narrowParams;
narrowParams.minDistance = 0.0f;
narrowParams.maxDistance = 100000.0f;
narrowParams.spread = 0.0f;
narrowSource->Set3DParams(narrowParams);
narrowSource->SetClip(&clip);
narrowSource->Play();
float narrowBuffer[2] = {};
narrowSource->ProcessAudio(
narrowBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
const float peak = 32767.0f / 32768.0f;
EXPECT_NEAR(narrowBuffer[0], peak * 0.5f, 1e-5f);
EXPECT_NEAR(narrowBuffer[1], peak * 0.5f, 1e-5f);
GameObject wideSourceObject("WideStereoSource");
wideSourceObject.GetTransform()->SetPosition(XCEngine::Math::Vector3::Zero());
auto* wideSource = wideSourceObject.AddComponent<AudioSourceComponent>();
XCEngine::Audio::Audio3DParams wideParams = narrowParams;
wideParams.spread = 1.0f;
wideSource->Set3DParams(wideParams);
wideSource->SetClip(&clip);
wideSource->Play();
float wideBuffer[2] = {};
wideSource->ProcessAudio(
wideBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_NEAR(wideBuffer[0], peak, 1e-5f);
EXPECT_NEAR(wideBuffer[1], 0.0f, 1e-5f);
}
TEST(AudioSourceComponent, HRTFSpatializesMonoSourceOnStereoOutput) {
AudioClip clip = CreateMono16Clip({32767}, 4);
XCEngine::Audio::Audio3DParams params;
params.minDistance = 100.0f;
params.maxDistance = 100000.0f;
params.panLevel = 1.0f;
GameObject baselineObject("BaselineSpatialSource");
baselineObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
auto* baselineSource = baselineObject.AddComponent<AudioSourceComponent>();
baselineSource->Set3DParams(params);
baselineSource->SetClip(&clip);
baselineSource->Play();
float baselineBuffer[2] = {};
baselineSource->ProcessAudio(
baselineBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_NEAR(baselineBuffer[0], 0.0f, 1e-5f);
EXPECT_GT(baselineBuffer[1], 0.0f);
GameObject hrtfObject("HRTFSpatialSource");
hrtfObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
auto* hrtfSource = hrtfObject.AddComponent<AudioSourceComponent>();
hrtfSource->Set3DParams(params);
hrtfSource->SetHRTFEnabled(true);
hrtfSource->SetHRTFCrossFeed(0.25f);
hrtfSource->SetClip(&clip);
hrtfSource->Play();
float hrtfBuffer[2] = {};
hrtfSource->ProcessAudio(
hrtfBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_GT(hrtfBuffer[0], baselineBuffer[0]);
EXPECT_GT(hrtfBuffer[0], 0.0f);
EXPECT_GT(hrtfBuffer[1], hrtfBuffer[0]);
}
TEST(AudioSourceComponent, MultipleSourcesCanReuseSameClipDecodedCache) {
AudioClip clip = CreateMono16Clip({32767, -32768}, 4);
const float* decodedBuffer = clip.GetDecodedPCMData().data();
AudioSourceComponent sourceA;
sourceA.SetSpatialize(false);
sourceA.SetClip(&clip);
sourceA.Play();
AudioSourceComponent sourceB;
sourceB.SetSpatialize(false);
sourceB.SetClip(&clip);
sourceB.Play();
float bufferA[2] = {};
float bufferB[2] = {};
sourceA.ProcessAudio(
bufferA,
2,
1,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
sourceB.ProcessAudio(
bufferB,
2,
1,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_EQ(decodedBuffer, clip.GetDecodedPCMData().data());
EXPECT_NEAR(bufferA[0], 32767.0f / 32768.0f, 1e-5f);
EXPECT_NEAR(bufferA[1], -1.0f, 1e-5f);
EXPECT_NEAR(bufferB[0], 32767.0f / 32768.0f, 1e-5f);
EXPECT_NEAR(bufferB[1], -1.0f, 1e-5f);
}
TEST(AudioSourceComponent, SerializeRoundTripPreservesClipPathAndSpatialSettings) {
AudioClip clip = CreateMono16Clip({32767}, 4);
clip.m_path = "test://audio/runtime.wav";
clip.m_name = "runtime.wav";
AudioSourceComponent source;
source.SetClip(&clip);
source.SetVolume(0.75f);
source.SetPitch(1.5f);
source.SetPan(-0.25f);
source.SetLooping(true);
source.SetSpatialize(true);
source.SetHRTFEnabled(true);
source.SetHRTFCrossFeed(0.4f);
source.SetHRTFQuality(3);
XCEngine::Audio::Audio3DParams params;
params.dopplerLevel = 2.0f;
params.speedOfSound = 280.0f;
params.minDistance = 2.0f;
params.maxDistance = 64.0f;
params.panLevel = 0.6f;
params.spread = 0.3f;
params.reverbZoneMix = 0.2f;
source.Set3DParams(params);
std::stringstream stream;
source.Serialize(stream);
AudioSourceComponent target;
target.Deserialize(stream);
EXPECT_EQ(target.GetClip(), nullptr);
EXPECT_EQ(target.GetClipPath(), "test://audio/runtime.wav");
EXPECT_FLOAT_EQ(target.GetVolume(), 0.75f);
EXPECT_FLOAT_EQ(target.GetPitch(), 1.5f);
EXPECT_FLOAT_EQ(target.GetPan(), -0.25f);
EXPECT_TRUE(target.IsLooping());
EXPECT_TRUE(target.IsSpatialize());
EXPECT_TRUE(target.IsHRTFEnabled());
EXPECT_FLOAT_EQ(target.GetHRTFCrossFeed(), 0.4f);
EXPECT_EQ(target.GetHRTFQuality(), 3u);
EXPECT_FLOAT_EQ(target.GetDopplerLevel(), 2.0f);
EXPECT_FLOAT_EQ(target.Get3DParams().speedOfSound, 280.0f);
EXPECT_FLOAT_EQ(target.Get3DParams().minDistance, 2.0f);
EXPECT_FLOAT_EQ(target.Get3DParams().maxDistance, 64.0f);
EXPECT_FLOAT_EQ(target.Get3DParams().panLevel, 0.6f);
EXPECT_FLOAT_EQ(target.GetSpread(), 0.3f);
EXPECT_FLOAT_EQ(target.GetReverbZoneMix(), 0.2f);
}
} // namespace