diff --git a/engine/include/XCEngine/Audio/IAudioBackend.h b/engine/include/XCEngine/Audio/IAudioBackend.h index 68240534..aef2ca88 100644 --- a/engine/include/XCEngine/Audio/IAudioBackend.h +++ b/engine/include/XCEngine/Audio/IAudioBackend.h @@ -19,12 +19,6 @@ public: // Runtime hot-switch is optional and unsupported backends should return false. virtual bool SetDevice(const std::string& deviceName) = 0; - virtual float GetMasterVolume() const = 0; - virtual void SetMasterVolume(float volume) = 0; - - virtual bool IsMuted() const = 0; - virtual void SetMuted(bool muted) = 0; - virtual void Start() = 0; virtual void Stop() = 0; virtual void Suspend() = 0; diff --git a/engine/include/XCEngine/Audio/WindowsAudioBackend.h b/engine/include/XCEngine/Audio/WindowsAudioBackend.h index bdb26876..b44c630b 100644 --- a/engine/include/XCEngine/Audio/WindowsAudioBackend.h +++ b/engine/include/XCEngine/Audio/WindowsAudioBackend.h @@ -32,12 +32,6 @@ public: void GetAvailableDevices(std::vector& devices) override; bool SetDevice(const std::string& deviceName) override; - float GetMasterVolume() const override; - void SetMasterVolume(float volume) override; - - bool IsMuted() const override; - void SetMuted(bool muted) override; - void Start() override; void Stop() override; void Suspend() override; @@ -51,14 +45,10 @@ public: private: static void FillPcm16Buffer(std::vector& output, - const std::vector& input, - float masterVolume, - bool muted) { - const float volume = muted ? 0.0f : std::clamp(masterVolume, 0.0f, 1.0f); - + const std::vector& input) { std::fill(output.begin(), output.end(), 0); for (size_t i = 0; i < output.size() && i < input.size(); ++i) { - float sample = input[i] * volume; + float sample = input[i]; sample = std::clamp(sample, -1.0f, 1.0f); output[i] = static_cast(sample * 32767.0f); } @@ -93,9 +83,6 @@ private: std::string m_deviceName; std::string m_requestedDeviceName; - std::atomic m_masterVolume{1.0f}; - std::atomic m_muted{false}; - static constexpr size_t BufferSize = 8192; std::vector m_audioBuffer1; std::vector m_audioBuffer2; diff --git a/engine/src/Audio/WindowsAudioBackend.cpp b/engine/src/Audio/WindowsAudioBackend.cpp index a4ac2753..50f1a292 100644 --- a/engine/src/Audio/WindowsAudioBackend.cpp +++ b/engine/src/Audio/WindowsAudioBackend.cpp @@ -141,22 +141,6 @@ bool WaveOutBackend::SetDevice(const std::string& deviceName) { return false; } -float WaveOutBackend::GetMasterVolume() const { - return m_masterVolume.load(); -} - -void WaveOutBackend::SetMasterVolume(float volume) { - m_masterVolume.store(std::max(0.0f, std::min(1.0f, volume))); -} - -bool WaveOutBackend::IsMuted() const { - return m_muted.load(); -} - -void WaveOutBackend::SetMuted(bool muted) { - m_muted.store(muted); -} - void WaveOutBackend::Start() { if (m_isRunning.load()) { return; @@ -309,7 +293,7 @@ void WaveOutBackend::AudioThread() { continue; } - FillPcm16Buffer(*targetBuffer, m_pendingMixBuffer, m_masterVolume.load(), m_muted.load()); + FillPcm16Buffer(*targetBuffer, m_pendingMixBuffer); m_hasPendingMix = false; lock.unlock(); diff --git a/tests/Components/test_audio_mixer.cpp b/tests/Components/test_audio_mixer.cpp new file mode 100644 index 00000000..f563046d --- /dev/null +++ b/tests/Components/test_audio_mixer.cpp @@ -0,0 +1,195 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace XCEngine::Audio; + +namespace { + +class OffsetEffect final : public IAudioEffect { +public: + explicit OffsetEffect(float offset) : m_offset(offset) {} + + void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) override { + ++callCount; + const size_t totalSamples = static_cast(frameCount) * channels; + for (size_t i = 0; i < totalSamples; ++i) { + buffer[i] += m_offset; + } + } + + int callCount = 0; + +private: + float m_offset = 0.0f; +}; + +TEST(AudioMixer, AppliesMasterAndChannelVolume) { + AudioMixer mixer; + mixer.SetVolume(0.5f); + mixer.SetChannelVolume(AudioChannel::FrontLeft, 0.25f); + mixer.SetChannelVolume(AudioChannel::FrontRight, 1.0f); + + float buffer[] = {1.0f, 1.0f, 1.0f, 1.0f}; + mixer.ProcessAudio(buffer, 2, 2); + + EXPECT_FLOAT_EQ(buffer[0], 0.125f); + EXPECT_FLOAT_EQ(buffer[1], 0.5f); + EXPECT_FLOAT_EQ(buffer[2], 0.125f); + EXPECT_FLOAT_EQ(buffer[3], 0.5f); +} + +TEST(AudioMixer, MuteSilencesBuffer) { + AudioMixer mixer; + mixer.SetMute(true); + + float buffer[] = {0.5f, -0.5f}; + mixer.ProcessAudio(buffer, 1, 2); + + EXPECT_FLOAT_EQ(buffer[0], 0.0f); + EXPECT_FLOAT_EQ(buffer[1], 0.0f); +} + +TEST(AudioMixer, EffectsRunAfterGain) { + AudioMixer mixer; + mixer.SetVolume(0.5f); + OffsetEffect effect(0.25f); + mixer.AddEffect(&effect); + + float buffer[] = {1.0f, -1.0f}; + mixer.ProcessAudio(buffer, 1, 2); + + EXPECT_EQ(effect.callCount, 1); + EXPECT_FLOAT_EQ(buffer[0], 0.75f); + EXPECT_FLOAT_EQ(buffer[1], -0.25f); +} + +TEST(AudioMixer, EqualizerWetMixControlsProcessedSignal) { + AudioMixer mixer; + Equalizer equalizer; + equalizer.SetBandCount(1); + equalizer.SetBandFrequency(0, 2000.0f); + equalizer.SetBandGain(0, 12.0f); + equalizer.SetBandQ(0, 0.7f); + mixer.AddEffect(&equalizer); + + std::vector dryBuffer(64, 0.0f); + dryBuffer[0] = 1.0f; + equalizer.SetWetMix(0.0f); + mixer.ProcessAudio(dryBuffer.data(), 64, 1, 48000); + + EXPECT_FLOAT_EQ(dryBuffer[0], 1.0f); + EXPECT_TRUE(std::all_of( + dryBuffer.begin() + 1, + dryBuffer.end(), + [](float sample) { return sample == 0.0f; })); + + std::vector wetBuffer(64, 0.0f); + wetBuffer[0] = 1.0f; + equalizer.ResetState(); + equalizer.SetWetMix(1.0f); + mixer.ProcessAudio(wetBuffer.data(), 64, 1, 48000); + + const bool changed = std::any_of( + wetBuffer.begin(), + wetBuffer.end(), + [](float sample) { return std::abs(sample) > 1e-4f && sample != 1.0f; }) || + std::any_of( + wetBuffer.begin() + 1, + wetBuffer.end(), + [](float sample) { return std::abs(sample) > 1e-5f; }); + EXPECT_TRUE(changed); +} + +TEST(AudioMixer, ReverbationProducesAudibleTail) { + AudioMixer mixer; + Reverbation reverb; + reverb.SetWetMix(1.0f); + reverb.SetDryMix(0.0f); + reverb.SetRoomSize(1.0f); + reverb.SetDamping(0.0f); + mixer.AddEffect(&reverb); + + std::vector buffer(2000, 0.0f); + buffer[0] = 1.0f; + mixer.ProcessAudio(buffer.data(), 2000, 1, 48000); + + EXPECT_FLOAT_EQ(buffer[0], 0.0f); + const bool hasTail = std::any_of( + buffer.begin() + 1000, + buffer.end(), + [](float sample) { return std::abs(sample) > 1e-6f; }); + EXPECT_TRUE(hasTail); +} + +TEST(AudioMixer, ReverbationWidthControlsStereoCrossfeed) { + AudioMixer narrowMixer; + Reverbation narrowReverb; + narrowReverb.SetWetMix(1.0f); + narrowReverb.SetDryMix(0.0f); + narrowReverb.SetRoomSize(1.0f); + narrowReverb.SetDamping(0.0f); + narrowReverb.SetWidth(0.0f); + narrowMixer.AddEffect(&narrowReverb); + + std::vector narrowBuffer(2000 * 2, 0.0f); + narrowBuffer[0] = 1.0f; + narrowMixer.ProcessAudio(narrowBuffer.data(), 2000, 2, 48000); + + float narrowMaxDiff = 0.0f; + for (size_t i = 1100; i < 1150; ++i) { + narrowMaxDiff = std::max( + narrowMaxDiff, + std::abs(narrowBuffer[i * 2] - narrowBuffer[i * 2 + 1])); + } + EXPECT_LT(narrowMaxDiff, 1e-6f); + + AudioMixer wideMixer; + Reverbation wideReverb; + wideReverb.SetWetMix(1.0f); + wideReverb.SetDryMix(0.0f); + wideReverb.SetRoomSize(1.0f); + wideReverb.SetDamping(0.0f); + wideReverb.SetWidth(1.0f); + wideMixer.AddEffect(&wideReverb); + + std::vector wideBuffer(2000 * 2, 0.0f); + wideBuffer[0] = 1.0f; + wideMixer.ProcessAudio(wideBuffer.data(), 2000, 2, 48000); + + float wideMaxDiff = 0.0f; + for (size_t i = 1100; i < 1150; ++i) { + wideMaxDiff = std::max( + wideMaxDiff, + std::abs(wideBuffer[i * 2] - wideBuffer[i * 2 + 1])); + } + EXPECT_GT(wideMaxDiff, 1e-6f); +} + +TEST(AudioMixer, FFTFilterAnalyzesWithoutMutatingBuffer) { + AudioMixer mixer; + FFTFilter fft(8); + fft.SetSmoothingFactor(0.0f); + mixer.AddEffect(&fft); + + std::vector buffer = {1.0f, 0.5f, 0.0f, -0.5f}; + const std::vector original = buffer; + mixer.ProcessAudio(buffer.data(), 4, 1, 48000); + + EXPECT_EQ(buffer, original); + ASSERT_EQ(fft.GetSpectrumSize(), 4u); + const bool hasSpectrum = std::any_of( + fft.GetSpectrumData(), + fft.GetSpectrumData() + fft.GetSpectrumSize(), + [](float value) { return value > 0.0f; }); + EXPECT_TRUE(hasSpectrum); +} + +} // namespace diff --git a/tests/Components/test_audio_system.cpp b/tests/Components/test_audio_system.cpp new file mode 100644 index 00000000..25d9a64a --- /dev/null +++ b/tests/Components/test_audio_system.cpp @@ -0,0 +1,309 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace XCEngine::Audio; +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(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& 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(frameCount) * channels); + } + + bool IsRunning() const override { return running; } + AudioConfig GetConfig() const override { return config; } + + AudioConfig config{}; + std::vector captured; + bool running = true; + uint32 lastFrameCount = 0; + uint32 lastChannels = 0; + uint32 lastSampleRate = 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(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(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(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(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, ListenerComponentPublishesVelocityAndDopplerSettings) { + AudioSystem& system = AudioSystem::Get(); + system.Shutdown(); + + GameObject listenerObject("Listener"); + auto* listener = listenerObject.AddComponent(); + + 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); + + EXPECT_FLOAT_EQ(system.GetListenerDopplerLevel(), 2.0f); + EXPECT_FLOAT_EQ(system.GetSpeedOfSound(), 200.0f); + EXPECT_EQ(system.GetListenerPosition(), XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f)); + EXPECT_EQ(system.GetListenerVelocity(), XCEngine::Math::Vector3(20.0f, 0.0f, 0.0f)); + EXPECT_FLOAT_EQ(system.GetListenerRotation().x, 0.0f); + EXPECT_FLOAT_EQ(system.GetListenerRotation().y, 0.0f); + EXPECT_FLOAT_EQ(system.GetListenerRotation().z, 0.0f); + EXPECT_FLOAT_EQ(system.GetListenerRotation().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(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, 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(); +} + +} // namespace diff --git a/tests/Components/test_windows_audio_backend.cpp b/tests/Components/test_windows_audio_backend.cpp index 1a4b1263..ff83e722 100644 --- a/tests/Components/test_windows_audio_backend.cpp +++ b/tests/Components/test_windows_audio_backend.cpp @@ -23,16 +23,6 @@ namespace { #ifdef _WIN32 -TEST(WaveOutBackend, MasterVolumeIsClampedToValidRange) { - WaveOut::WaveOutBackend backend; - - backend.SetMasterVolume(1.5f); - EXPECT_FLOAT_EQ(backend.GetMasterVolume(), 1.0f); - - backend.SetMasterVolume(-0.5f); - EXPECT_FLOAT_EQ(backend.GetMasterVolume(), 0.0f); -} - TEST(WaveOutBackend, ProcessAudioCopiesOnlyRequestedSamplesIntoPendingMixBuffer) { WaveOut::WaveOutBackend backend; backend.m_pendingMixBuffer.assign(8, 1.0f); @@ -71,24 +61,17 @@ TEST(WaveOutBackend, SetDeviceRejectsHotSwitchAfterOpen) { backend.m_hWaveOut = nullptr; } -TEST(WaveOutBackend, FillPcm16BufferAppliesVolumeClampAndMute) { +TEST(WaveOutBackend, FillPcm16BufferClampsSamplesToPcm16Range) { std::vector input = {-2.0f, -0.5f, 0.5f, 2.0f}; std::vector output(4, 123); - WaveOut::WaveOutBackend::FillPcm16Buffer(output, input, 0.5f, false); + WaveOut::WaveOutBackend::FillPcm16Buffer(output, input); ASSERT_EQ(output.size(), 4u); EXPECT_EQ(output[0], -32767); - EXPECT_EQ(output[1], -8191); - EXPECT_EQ(output[2], 8191); + EXPECT_EQ(output[1], -16383); + EXPECT_EQ(output[2], 16383); EXPECT_EQ(output[3], 32767); - - WaveOut::WaveOutBackend::FillPcm16Buffer(output, input, 1.0f, true); - - EXPECT_EQ(output[0], 0); - EXPECT_EQ(output[1], 0); - EXPECT_EQ(output[2], 0); - EXPECT_EQ(output[3], 0); } #endif