#include #include #include #include #include #include using namespace XCEngine::Components; using namespace XCEngine::Resources; namespace { AudioClip CreateMono16Clip(std::initializer_list samples, XCEngine::Core::uint32 sampleRate = 4) { AudioClip clip; XCEngine::Containers::Array pcmData; pcmData.ResizeUninitialized(samples.size() * sizeof(int16_t)); size_t byteOffset = 0; for (const int16_t sample : samples) { const uint16_t encoded = static_cast(sample); pcmData[byteOffset++] = static_cast(encoded & 0xFFu); pcmData[byteOffset++] = static_cast((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 samples, XCEngine::Core::uint32 sampleRate = 4) { AudioClip clip; XCEngine::Containers::Array pcmData; pcmData.ResizeUninitialized(samples.size() * sizeof(int16_t)); size_t byteOffset = 0; for (const int16_t sample : samples) { const uint16_t encoded = static_cast(sample); pcmData[byteOffset++] = static_cast(encoded & 0xFFu); pcmData[byteOffset++] = static_cast((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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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