460 lines
14 KiB
C++
460 lines
14 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <XCEngine/Audio/AudioSystem.h>
|
|
#include <XCEngine/Audio/IAudioBackend.h>
|
|
#include <XCEngine/Audio/AudioMixer.h>
|
|
#include <XCEngine/Components/AudioListenerComponent.h>
|
|
#include <XCEngine/Components/GameObject.h>
|
|
#include <XCEngine/Components/AudioSourceComponent.h>
|
|
#include <XCEngine/Core/Math/Quaternion.h>
|
|
#include <XCEngine/Core/Math/Vector3.h>
|
|
#include <XCEngine/Resources/AudioClip/AudioClip.h>
|
|
#include <sstream>
|
|
|
|
using namespace XCEngine::Audio;
|
|
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(XCEngine::Resources::AudioFormat::WAV);
|
|
clip.SetPCMData(pcmData);
|
|
clip.m_isValid = true;
|
|
return clip;
|
|
}
|
|
|
|
class CaptureBackend final : public IAudioBackend {
|
|
public:
|
|
explicit CaptureBackend(const AudioConfig& inConfig) : config(inConfig) {}
|
|
|
|
bool Initialize(const AudioConfig& inConfig) override {
|
|
config = inConfig;
|
|
return true;
|
|
}
|
|
|
|
void Shutdown() override {}
|
|
|
|
std::string GetDeviceName() const override { return "CaptureBackend"; }
|
|
void GetAvailableDevices(std::vector<std::string>& devices) override {
|
|
devices = {"CaptureBackend"};
|
|
}
|
|
bool SetDevice(const std::string& deviceName) override { return deviceName == "CaptureBackend"; }
|
|
|
|
void Start() override { running = true; }
|
|
void Stop() override { running = false; }
|
|
void Suspend() override {}
|
|
void Resume() override {}
|
|
|
|
void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels, uint32 sampleRate) override {
|
|
lastFrameCount = frameCount;
|
|
lastChannels = channels;
|
|
lastSampleRate = sampleRate;
|
|
captured.assign(buffer, buffer + static_cast<size_t>(frameCount) * channels);
|
|
}
|
|
|
|
bool IsRunning() const override { return running; }
|
|
AudioConfig GetConfig() const override { return config; }
|
|
|
|
AudioConfig config{};
|
|
std::vector<float> captured;
|
|
bool running = true;
|
|
uint32 lastFrameCount = 0;
|
|
uint32 lastChannels = 0;
|
|
uint32 lastSampleRate = 0;
|
|
};
|
|
|
|
class PullCaptureBackend final : public IAudioBackend {
|
|
public:
|
|
explicit PullCaptureBackend(const AudioConfig& inConfig) : config(inConfig) {}
|
|
|
|
bool Initialize(const AudioConfig& inConfig) override {
|
|
config = inConfig;
|
|
return true;
|
|
}
|
|
|
|
void Shutdown() override {}
|
|
|
|
std::string GetDeviceName() const override { return "PullCaptureBackend"; }
|
|
void GetAvailableDevices(std::vector<std::string>& devices) override {
|
|
devices = {"PullCaptureBackend"};
|
|
}
|
|
bool SetDevice(const std::string& deviceName) override { return deviceName == "PullCaptureBackend"; }
|
|
|
|
void Start() override { running = true; }
|
|
void Stop() override { running = false; }
|
|
void Suspend() override {}
|
|
void Resume() override {}
|
|
|
|
void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels, uint32 sampleRate) override {
|
|
(void)buffer;
|
|
(void)frameCount;
|
|
(void)channels;
|
|
(void)sampleRate;
|
|
submissionCount++;
|
|
}
|
|
|
|
bool UsesPullModel() const override { return true; }
|
|
void SetRenderCallback(RenderCallback callback) override { renderCallback = callback; }
|
|
|
|
bool IsRunning() const override { return running; }
|
|
AudioConfig GetConfig() const override { return config; }
|
|
|
|
bool RenderOnce() {
|
|
if (!renderCallback) {
|
|
return false;
|
|
}
|
|
|
|
const size_t sampleCount = static_cast<size_t>(config.bufferSize) * config.channels;
|
|
captured.assign(sampleCount, 0.0f);
|
|
renderCallback(captured.data(), config.bufferSize, config.channels, config.sampleRate);
|
|
return true;
|
|
}
|
|
|
|
AudioConfig config{};
|
|
std::vector<float> captured;
|
|
RenderCallback renderCallback;
|
|
bool running = true;
|
|
uint32 submissionCount = 0;
|
|
};
|
|
|
|
TEST(AudioSystem, MasterMixerProcessesDirectSources) {
|
|
AudioSystem& system = AudioSystem::Get();
|
|
system.Shutdown();
|
|
|
|
AudioConfig config;
|
|
config.sampleRate = 4;
|
|
config.channels = 1;
|
|
config.bufferSize = 1;
|
|
|
|
auto backend = std::make_unique<CaptureBackend>(config);
|
|
CaptureBackend* backendPtr = backend.get();
|
|
system.SetBackend(std::move(backend));
|
|
system.GetMasterMixer().SetVolume(0.5f);
|
|
system.GetMasterMixer().SetMute(false);
|
|
system.GetMasterMixer().ClearEffects();
|
|
|
|
AudioClip clip = CreateMono16Clip({16384}, 4);
|
|
AudioSourceComponent source;
|
|
source.SetSpatialize(false);
|
|
source.SetClip(&clip);
|
|
source.Play();
|
|
|
|
system.Update(0.0f);
|
|
|
|
ASSERT_EQ(backendPtr->captured.size(), 1u);
|
|
EXPECT_NEAR(backendPtr->captured[0], 0.25f, 1e-5f);
|
|
|
|
source.Stop();
|
|
system.GetMasterMixer().SetVolume(1.0f);
|
|
system.Shutdown();
|
|
}
|
|
|
|
TEST(AudioSystem, SourceOutputMixerRoutesIntoBackend) {
|
|
AudioSystem& system = AudioSystem::Get();
|
|
system.Shutdown();
|
|
|
|
AudioConfig config;
|
|
config.sampleRate = 4;
|
|
config.channels = 1;
|
|
config.bufferSize = 1;
|
|
|
|
auto backend = std::make_unique<CaptureBackend>(config);
|
|
CaptureBackend* backendPtr = backend.get();
|
|
system.SetBackend(std::move(backend));
|
|
system.GetMasterMixer().SetVolume(1.0f);
|
|
system.GetMasterMixer().SetMute(false);
|
|
system.GetMasterMixer().ClearEffects();
|
|
|
|
AudioMixer mixer;
|
|
mixer.SetVolume(0.5f);
|
|
|
|
AudioClip clip = CreateMono16Clip({16384}, 4);
|
|
AudioSourceComponent source;
|
|
source.SetSpatialize(false);
|
|
source.SetOutputMixer(&mixer);
|
|
source.SetClip(&clip);
|
|
source.Play();
|
|
|
|
system.Update(0.0f);
|
|
|
|
ASSERT_EQ(backendPtr->captured.size(), 1u);
|
|
EXPECT_NEAR(backendPtr->captured[0], 0.25f, 1e-5f);
|
|
|
|
source.Stop();
|
|
system.Shutdown();
|
|
}
|
|
|
|
TEST(AudioSystem, SetMasterVolumeControlsMasterMixerOutput) {
|
|
AudioSystem& system = AudioSystem::Get();
|
|
system.Shutdown();
|
|
|
|
AudioConfig config;
|
|
config.sampleRate = 4;
|
|
config.channels = 1;
|
|
config.bufferSize = 1;
|
|
|
|
auto backend = std::make_unique<CaptureBackend>(config);
|
|
CaptureBackend* backendPtr = backend.get();
|
|
system.SetBackend(std::move(backend));
|
|
system.SetMasterVolume(0.25f);
|
|
system.SetMuted(false);
|
|
system.GetMasterMixer().ClearEffects();
|
|
|
|
AudioClip clip = CreateMono16Clip({16384}, 4);
|
|
AudioSourceComponent source;
|
|
source.SetSpatialize(false);
|
|
source.SetClip(&clip);
|
|
source.Play();
|
|
|
|
system.Update(0.0f);
|
|
|
|
ASSERT_EQ(backendPtr->captured.size(), 1u);
|
|
EXPECT_NEAR(backendPtr->captured[0], 0.125f, 1e-5f);
|
|
EXPECT_FLOAT_EQ(system.GetMasterVolume(), 0.25f);
|
|
EXPECT_FALSE(system.IsMuted());
|
|
|
|
source.Stop();
|
|
system.SetMasterVolume(1.0f);
|
|
system.Shutdown();
|
|
}
|
|
|
|
TEST(AudioSystem, ListenerReverbMixerReceivesSend) {
|
|
AudioSystem& system = AudioSystem::Get();
|
|
system.Shutdown();
|
|
|
|
AudioConfig config;
|
|
config.sampleRate = 4;
|
|
config.channels = 1;
|
|
config.bufferSize = 1;
|
|
|
|
auto backend = std::make_unique<CaptureBackend>(config);
|
|
CaptureBackend* backendPtr = backend.get();
|
|
system.SetBackend(std::move(backend));
|
|
system.SetMasterVolume(1.0f);
|
|
system.SetMuted(false);
|
|
system.GetMasterMixer().ClearEffects();
|
|
|
|
AudioMixer reverbMixer;
|
|
reverbMixer.SetVolume(1.0f);
|
|
system.SetListenerReverbMixer(&reverbMixer);
|
|
system.SetListenerReverbLevel(0.5f);
|
|
|
|
AudioClip clip = CreateMono16Clip({16384}, 4);
|
|
AudioSourceComponent source;
|
|
source.SetSpatialize(false);
|
|
source.SetReverbZoneMix(0.5f);
|
|
source.SetClip(&clip);
|
|
source.Play();
|
|
|
|
system.Update(0.0f);
|
|
|
|
ASSERT_EQ(backendPtr->captured.size(), 1u);
|
|
EXPECT_NEAR(backendPtr->captured[0], 0.625f, 1e-5f);
|
|
|
|
source.Stop();
|
|
system.SetListenerReverbMixer(nullptr);
|
|
system.SetListenerReverbLevel(1.0f);
|
|
system.Shutdown();
|
|
}
|
|
|
|
TEST(AudioSystem, DestroyingMixerClearsDependentRoutes) {
|
|
AudioSystem& system = AudioSystem::Get();
|
|
system.Shutdown();
|
|
|
|
AudioSourceComponent source;
|
|
AudioListenerComponent listener;
|
|
auto parentMixer = std::make_unique<AudioMixer>();
|
|
auto childMixer = std::make_unique<AudioMixer>();
|
|
|
|
source.SetOutputMixer(parentMixer.get());
|
|
listener.SetReverb(parentMixer.get());
|
|
childMixer->SetOutputMixer(parentMixer.get());
|
|
|
|
parentMixer.reset();
|
|
|
|
EXPECT_EQ(source.GetOutputMixer(), nullptr);
|
|
EXPECT_EQ(listener.GetReverb(), nullptr);
|
|
EXPECT_EQ(system.GetListenerReverbMixer(), nullptr);
|
|
EXPECT_EQ(childMixer->GetOutputMixer(), nullptr);
|
|
|
|
system.Shutdown();
|
|
}
|
|
|
|
TEST(AudioSystem, ListenerComponentPublishesVelocityAndDopplerSettings) {
|
|
AudioSystem& system = AudioSystem::Get();
|
|
system.Shutdown();
|
|
|
|
GameObject listenerObject("Listener");
|
|
auto* listener = listenerObject.AddComponent<AudioListenerComponent>();
|
|
|
|
listener->SetDopplerLevel(2.0f);
|
|
listener->SetSpeedOfSound(200.0f);
|
|
|
|
listenerObject.GetTransform()->SetPosition(XCEngine::Math::Vector3::Zero());
|
|
listener->Update(0.0f);
|
|
|
|
listenerObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
|
|
listener->Update(0.5f);
|
|
|
|
const XCEngine::Math::Vector3 listenerPosition = system.GetListenerPosition();
|
|
const XCEngine::Math::Vector3 listenerVelocity = system.GetListenerVelocity();
|
|
const XCEngine::Math::Quaternion listenerRotation = system.GetListenerRotation();
|
|
EXPECT_FLOAT_EQ(system.GetListenerDopplerLevel(), 2.0f);
|
|
EXPECT_FLOAT_EQ(system.GetSpeedOfSound(), 200.0f);
|
|
EXPECT_EQ(listenerPosition, XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
|
|
EXPECT_EQ(listenerVelocity, XCEngine::Math::Vector3(20.0f, 0.0f, 0.0f));
|
|
EXPECT_FLOAT_EQ(listenerRotation.x, 0.0f);
|
|
EXPECT_FLOAT_EQ(listenerRotation.y, 0.0f);
|
|
EXPECT_FLOAT_EQ(listenerRotation.z, 0.0f);
|
|
EXPECT_FLOAT_EQ(listenerRotation.w, 1.0f);
|
|
|
|
system.Shutdown();
|
|
}
|
|
|
|
TEST(AudioSystem, ProcessAudioUsesBackendConfigSampleRateForDirectSubmission) {
|
|
AudioSystem& system = AudioSystem::Get();
|
|
system.Shutdown();
|
|
|
|
AudioConfig config;
|
|
config.sampleRate = 22050;
|
|
config.channels = 1;
|
|
config.bufferSize = 1;
|
|
|
|
auto backend = std::make_unique<CaptureBackend>(config);
|
|
CaptureBackend* backendPtr = backend.get();
|
|
system.SetBackend(std::move(backend));
|
|
|
|
float buffer[1] = {0.5f};
|
|
system.ProcessAudio(buffer, 1, 1);
|
|
|
|
ASSERT_EQ(backendPtr->captured.size(), 1u);
|
|
EXPECT_FLOAT_EQ(backendPtr->captured[0], 0.5f);
|
|
EXPECT_EQ(backendPtr->lastSampleRate, 22050u);
|
|
|
|
system.Shutdown();
|
|
}
|
|
|
|
TEST(AudioSystem, RenderAudioProducesMixedBlockWithoutBackendSubmission) {
|
|
AudioSystem& system = AudioSystem::Get();
|
|
system.Shutdown();
|
|
|
|
system.SetMasterVolume(0.5f);
|
|
system.SetMuted(false);
|
|
system.GetMasterMixer().ClearEffects();
|
|
|
|
AudioClip clip = CreateMono16Clip({16384}, 4);
|
|
AudioSourceComponent source;
|
|
source.SetSpatialize(false);
|
|
source.SetClip(&clip);
|
|
source.Play();
|
|
|
|
float buffer[1] = {};
|
|
system.RenderAudio(buffer, 1, 1, 4);
|
|
|
|
const AudioSystem::Stats stats = system.GetStats();
|
|
EXPECT_NEAR(buffer[0], 0.25f, 1e-5f);
|
|
EXPECT_EQ(stats.activeSources, 1u);
|
|
EXPECT_EQ(stats.totalSources, 1u);
|
|
|
|
source.Stop();
|
|
system.SetMasterVolume(1.0f);
|
|
system.Shutdown();
|
|
}
|
|
|
|
TEST(AudioSystem, PullBackendRendersViaCallbackWithoutPushSubmission) {
|
|
AudioSystem& system = AudioSystem::Get();
|
|
system.Shutdown();
|
|
|
|
AudioConfig config;
|
|
config.sampleRate = 4;
|
|
config.channels = 1;
|
|
config.bufferSize = 1;
|
|
|
|
auto backend = std::make_unique<PullCaptureBackend>(config);
|
|
PullCaptureBackend* backendPtr = backend.get();
|
|
system.SetBackend(std::move(backend));
|
|
system.SetMasterVolume(0.5f);
|
|
system.SetMuted(false);
|
|
system.GetMasterMixer().ClearEffects();
|
|
|
|
AudioClip clip = CreateMono16Clip({16384}, 4);
|
|
AudioSourceComponent source;
|
|
source.SetSpatialize(false);
|
|
source.SetClip(&clip);
|
|
source.Play();
|
|
|
|
system.Update(0.0f);
|
|
|
|
EXPECT_EQ(backendPtr->submissionCount, 0u);
|
|
ASSERT_TRUE(backendPtr->RenderOnce());
|
|
ASSERT_EQ(backendPtr->captured.size(), 1u);
|
|
EXPECT_NEAR(backendPtr->captured[0], 0.25f, 1e-5f);
|
|
|
|
source.Stop();
|
|
system.SetMasterVolume(1.0f);
|
|
system.Shutdown();
|
|
}
|
|
|
|
TEST(AudioSystem, AudioListenerComponentSerializeRoundTripPreservesSettings) {
|
|
AudioSystem& system = AudioSystem::Get();
|
|
system.Shutdown();
|
|
|
|
AudioListenerComponent source;
|
|
source.SetMasterVolume(0.6f);
|
|
source.SetMute(true);
|
|
source.SetDopplerLevel(2.5f);
|
|
source.SetSpeedOfSound(250.0f);
|
|
source.SetReverbLevel(0.35f);
|
|
|
|
std::stringstream stream;
|
|
source.Serialize(stream);
|
|
|
|
AudioListenerComponent target;
|
|
target.Deserialize(stream);
|
|
|
|
EXPECT_FLOAT_EQ(target.GetMasterVolume(), 0.6f);
|
|
EXPECT_TRUE(target.IsMute());
|
|
EXPECT_FLOAT_EQ(target.GetDopplerLevel(), 2.5f);
|
|
EXPECT_FLOAT_EQ(target.GetSpeedOfSound(), 250.0f);
|
|
EXPECT_FLOAT_EQ(target.GetReverbLevel(), 0.35f);
|
|
|
|
EXPECT_FLOAT_EQ(system.GetMasterVolume(), 0.6f);
|
|
EXPECT_TRUE(system.IsMuted());
|
|
EXPECT_FLOAT_EQ(system.GetListenerDopplerLevel(), 2.5f);
|
|
EXPECT_FLOAT_EQ(system.GetSpeedOfSound(), 250.0f);
|
|
EXPECT_FLOAT_EQ(system.GetListenerReverbLevel(), 0.35f);
|
|
|
|
system.SetMasterVolume(1.0f);
|
|
system.SetMuted(false);
|
|
system.SetListenerDopplerLevel(1.0f);
|
|
system.SetSpeedOfSound(343.0f);
|
|
system.SetListenerReverbLevel(1.0f);
|
|
system.Shutdown();
|
|
}
|
|
|
|
TEST(AudioSystem, AudioListenerFrequencySnapshotDefaultsToEmpty) {
|
|
AudioListenerComponent listener;
|
|
|
|
const std::vector<float> frequencyData = listener.GetFrequencyData();
|
|
|
|
EXPECT_TRUE(frequencyData.empty());
|
|
EXPECT_EQ(listener.GetFrequencyDataSize(), 0u);
|
|
}
|
|
|
|
} // namespace
|