512 lines
16 KiB
C++
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
|