diff --git a/engine/include/XCEngine/Audio/AudioSystem.h b/engine/include/XCEngine/Audio/AudioSystem.h index 026c3ac2..e7a71f03 100644 --- a/engine/include/XCEngine/Audio/AudioSystem.h +++ b/engine/include/XCEngine/Audio/AudioSystem.h @@ -51,6 +51,7 @@ public: void SetSpeedOfSound(float metersPerSecond); float GetSpeedOfSound() const { return m_speedOfSound; } + void RenderAudio(float* buffer, uint32 frameCount, uint32 channels, uint32 sampleRate); void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels); void SetListenerTransform(const Math::Vector3& position, const Math::Quaternion& rotation); @@ -79,6 +80,7 @@ private: AudioSystem& operator=(const AudioSystem&) = delete; void ProcessSource(Components::AudioSourceComponent* source, float* buffer, uint32 frameCount, uint32 channels, uint32 sampleRate); + void RenderAudioBlock(uint32 frameCount, uint32 channels, uint32 sampleRate); private: std::unique_ptr m_backend; diff --git a/engine/src/Audio/AudioSystem.cpp b/engine/src/Audio/AudioSystem.cpp index c4b7a8d1..8136b340 100644 --- a/engine/src/Audio/AudioSystem.cpp +++ b/engine/src/Audio/AudioSystem.cpp @@ -140,106 +140,7 @@ void AudioSystem::Update(float deltaTime) { const auto& config = m_backend->GetConfig(); const uint32 frameCount = config.bufferSize; - const uint32 sampleCount = frameCount * config.channels; - - if (m_mixScratchBuffer.size() != sampleCount) { - m_mixScratchBuffer.assign(sampleCount, 0.0f); - } else { - std::fill(m_mixScratchBuffer.begin(), m_mixScratchBuffer.end(), 0.0f); - } - - if (m_sourceScratchBuffer.size() != sampleCount) { - m_sourceScratchBuffer.assign(sampleCount, 0.0f); - } else { - std::fill(m_sourceScratchBuffer.begin(), m_sourceScratchBuffer.end(), 0.0f); - } - - m_activeSourceSnapshot = m_activeSources; - m_mixerScratchChildren.clear(); - m_mixerScratchActiveMixers.clear(); - m_mixerScratchVisiting.clear(); - m_mixerScratchRendered.clear(); - - auto ensureMixerBuffer = [this, sampleCount](AudioMixer* mixer) -> std::vector& { - std::vector& buffer = m_mixerScratchBuffers[mixer]; - const bool firstTouchThisFrame = m_mixerScratchActiveMixers.insert(mixer).second; - if (firstTouchThisFrame) { - if (buffer.size() != sampleCount) { - buffer.assign(sampleCount, 0.0f); - } else { - std::fill(buffer.begin(), buffer.end(), 0.0f); - } - } - return buffer; - }; - - auto registerMixerChain = - [this, &ensureMixerBuffer](AudioMixer* mixer) { - AudioMixer* current = mixer; - while (current != nullptr) { - ensureMixerBuffer(current); - AudioMixer* output = current->GetOutputMixer(); - if (output == nullptr || output == current) { - break; - } - - auto& children = m_mixerScratchChildren[output]; - if (std::find(children.begin(), children.end(), current) == children.end()) { - children.push_back(current); - } - - current = output; - } - }; - - if (m_listenerReverbMixer != nullptr) { - registerMixerChain(m_listenerReverbMixer); - } - - for (auto* source : m_activeSourceSnapshot) { - if (source && source->IsEnabled() && source->IsPlaying()) { - std::fill(m_sourceScratchBuffer.begin(), m_sourceScratchBuffer.end(), 0.0f); - ProcessSource(source, m_sourceScratchBuffer.data(), frameCount, config.channels, config.sampleRate); - - AudioMixer* outputMixer = source->GetOutputMixer(); - if (outputMixer != nullptr) { - registerMixerChain(outputMixer); - MixBufferInto(m_sourceScratchBuffer, ensureMixerBuffer(outputMixer)); - } else { - MixBufferInto(m_sourceScratchBuffer, m_mixScratchBuffer); - } - - if (m_listenerReverbMixer != nullptr && m_listenerReverbLevel > 0.0f) { - const float sendGain = std::clamp( - source->GetReverbZoneMix() * m_listenerReverbLevel, - 0.0f, - 1.0f); - MixScaledBufferInto( - m_sourceScratchBuffer, - ensureMixerBuffer(m_listenerReverbMixer), - sendGain); - } - } - } - - for (AudioMixer* mixer : m_mixerScratchActiveMixers) { - RenderMixerRecursive( - mixer, - frameCount, - config.channels, - config.sampleRate, - m_mixerScratchBuffers, - m_mixerScratchChildren, - m_mixScratchBuffer, - m_mixerScratchVisiting, - m_mixerScratchRendered); - } - - m_masterMixer.ProcessAudio( - m_mixScratchBuffer.data(), - frameCount, - config.channels, - config.sampleRate); + RenderAudioBlock(frameCount, config.channels, config.sampleRate); m_backend->ProcessAudio( m_mixScratchBuffer.data(), frameCount, @@ -311,6 +212,16 @@ void AudioSystem::SetSpeedOfSound(float metersPerSecond) { m_speedOfSound = std::max(1.0f, metersPerSecond); } +void AudioSystem::RenderAudio(float* buffer, uint32 frameCount, uint32 channels, uint32 sampleRate) { + if (buffer == nullptr || frameCount == 0 || channels == 0 || sampleRate == 0) { + return; + } + + RenderAudioBlock(frameCount, channels, sampleRate); + const size_t sampleCount = static_cast(frameCount) * channels; + std::copy_n(m_mixScratchBuffer.data(), sampleCount, buffer); +} + void AudioSystem::ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) { if (m_backend) { const uint32 sampleRate = @@ -365,5 +276,124 @@ void AudioSystem::ProcessSource(Components::AudioSourceComponent* source, float* sampleRate); } +void AudioSystem::RenderAudioBlock(uint32 frameCount, uint32 channels, uint32 sampleRate) { + const size_t sampleCount = static_cast(frameCount) * channels; + if (sampleCount == 0 || sampleRate == 0) { + m_mixScratchBuffer.clear(); + m_sourceScratchBuffer.clear(); + m_stats.activeSources = 0; + m_stats.totalSources = static_cast(m_activeSources.size()); + return; + } + + if (m_mixScratchBuffer.size() != sampleCount) { + m_mixScratchBuffer.assign(sampleCount, 0.0f); + } else { + std::fill(m_mixScratchBuffer.begin(), m_mixScratchBuffer.end(), 0.0f); + } + + if (m_sourceScratchBuffer.size() != sampleCount) { + m_sourceScratchBuffer.assign(sampleCount, 0.0f); + } else { + std::fill(m_sourceScratchBuffer.begin(), m_sourceScratchBuffer.end(), 0.0f); + } + + m_activeSourceSnapshot = m_activeSources; + m_mixerScratchChildren.clear(); + m_mixerScratchActiveMixers.clear(); + m_mixerScratchVisiting.clear(); + m_mixerScratchRendered.clear(); + + auto ensureMixerBuffer = [this, sampleCount](AudioMixer* mixer) -> std::vector& { + std::vector& buffer = m_mixerScratchBuffers[mixer]; + const bool firstTouchThisFrame = m_mixerScratchActiveMixers.insert(mixer).second; + if (firstTouchThisFrame) { + if (buffer.size() != sampleCount) { + buffer.assign(sampleCount, 0.0f); + } else { + std::fill(buffer.begin(), buffer.end(), 0.0f); + } + } + return buffer; + }; + + auto registerMixerChain = + [this, &ensureMixerBuffer](AudioMixer* mixer) { + AudioMixer* current = mixer; + while (current != nullptr) { + ensureMixerBuffer(current); + AudioMixer* output = current->GetOutputMixer(); + if (output == nullptr || output == current) { + break; + } + + auto& children = m_mixerScratchChildren[output]; + if (std::find(children.begin(), children.end(), current) == children.end()) { + children.push_back(current); + } + + current = output; + } + }; + + if (m_listenerReverbMixer != nullptr) { + registerMixerChain(m_listenerReverbMixer); + } + + for (auto* source : m_activeSourceSnapshot) { + if (source && source->IsEnabled() && source->IsPlaying()) { + std::fill(m_sourceScratchBuffer.begin(), m_sourceScratchBuffer.end(), 0.0f); + ProcessSource(source, m_sourceScratchBuffer.data(), frameCount, channels, sampleRate); + + AudioMixer* outputMixer = source->GetOutputMixer(); + if (outputMixer != nullptr) { + registerMixerChain(outputMixer); + MixBufferInto(m_sourceScratchBuffer, ensureMixerBuffer(outputMixer)); + } else { + MixBufferInto(m_sourceScratchBuffer, m_mixScratchBuffer); + } + + if (m_listenerReverbMixer != nullptr && m_listenerReverbLevel > 0.0f) { + const float sendGain = std::clamp( + source->GetReverbZoneMix() * m_listenerReverbLevel, + 0.0f, + 1.0f); + MixScaledBufferInto( + m_sourceScratchBuffer, + ensureMixerBuffer(m_listenerReverbMixer), + sendGain); + } + } + } + + for (AudioMixer* mixer : m_mixerScratchActiveMixers) { + RenderMixerRecursive( + mixer, + frameCount, + channels, + sampleRate, + m_mixerScratchBuffers, + m_mixerScratchChildren, + m_mixScratchBuffer, + m_mixerScratchVisiting, + m_mixerScratchRendered); + } + + m_masterMixer.ProcessAudio( + m_mixScratchBuffer.data(), + frameCount, + channels, + sampleRate); + + uint32 activeCount = 0; + for (auto* source : m_activeSources) { + if (source && source->IsPlaying()) { + activeCount++; + } + } + m_stats.activeSources = activeCount; + m_stats.totalSources = static_cast(m_activeSources.size()); +} + } // namespace Audio } // namespace XCEngine diff --git a/tests/Components/test_audio_system.cpp b/tests/Components/test_audio_system.cpp index 25d9a64a..c7a4b2d1 100644 --- a/tests/Components/test_audio_system.cpp +++ b/tests/Components/test_audio_system.cpp @@ -269,6 +269,32 @@ TEST(AudioSystem, ProcessAudioUsesBackendConfigSampleRateForDirectSubmission) { 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); + + EXPECT_NEAR(buffer[0], 0.25f, 1e-5f); + EXPECT_EQ(system.GetStats().activeSources, 1u); + EXPECT_EQ(system.GetStats().totalSources, 1u); + + source.Stop(); + system.SetMasterVolume(1.0f); + system.Shutdown(); +} + TEST(AudioSystem, AudioListenerComponentSerializeRoundTripPreservesSettings) { AudioSystem& system = AudioSystem::Get(); system.Shutdown();