// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. #if AUDIO_API_XAUDIO2 #include "AudioBackendXAudio2.h" #include "Engine/Audio/AudioBackendTools.h" #include "Engine/Core/Collections/Array.h" #include "Engine/Core/Collections/ChunkedArray.h" #include "Engine/Core/Log.h" #include "Engine/Audio/Audio.h" #include "Engine/Threading/Threading.h" #if PLATFORM_WINDOWS // Tweak Win ver #define _WIN32_WINNT 0x0602 //#include "Engine/Platform/Windows/IncludeWindowsHeaders.h" #endif // Include XAudio library // Documentation: https://docs.microsoft.com/en-us/windows/desktop/xaudio2/xaudio2-apis-portal #include // TODO: implement multi-channel support (eg. 5.1, 7.1) #define MAX_INPUT_CHANNELS 6 #define MAX_OUTPUT_CHANNELS 2 #define MAX_CHANNELS_MATRIX_SIZE (MAX_INPUT_CHANNELS*MAX_OUTPUT_CHANNELS) #if ENABLE_ASSERTION #define XAUDIO2_CHECK_ERROR(method) \ if (hr != 0) \ { \ LOG(Error, "XAudio2 method {0} failed with error 0x{1:X} (at line {2})", TEXT(#method), (uint32)hr, __LINE__ - 1); \ } #else #define XAUDIO2_CHECK_ERROR(method) #endif namespace XAudio2 { struct Listener : AudioBackendTools::Listener { }; class VoiceCallback : public IXAudio2VoiceCallback { COM_DECLSPEC_NOTHROW void STDMETHODCALLTYPE OnVoiceProcessingPassStart(THIS_ UINT32 BytesRequired) override { } COM_DECLSPEC_NOTHROW void STDMETHODCALLTYPE OnVoiceProcessingPassEnd(THIS) override { } COM_DECLSPEC_NOTHROW void STDMETHODCALLTYPE OnStreamEnd(THIS_) override { } COM_DECLSPEC_NOTHROW void STDMETHODCALLTYPE OnBufferStart(THIS_ void* pBufferContext) override { PeekSamples(); } COM_DECLSPEC_NOTHROW void STDMETHODCALLTYPE OnBufferEnd(THIS_ void* pBufferContext) override; COM_DECLSPEC_NOTHROW void STDMETHODCALLTYPE OnLoopEnd(THIS_ void* pBufferContext) override { } COM_DECLSPEC_NOTHROW void STDMETHODCALLTYPE OnVoiceError(THIS_ void* pBufferContext, HRESULT Error) override { #if ENABLE_ASSERTION LOG(Warning, "IXAudio2VoiceCallback::OnVoiceError! Error: 0x{0:x}", Error); #endif } public: uint32 SourceID; void PeekSamples(); }; struct Source : AudioBackendTools::Source { IXAudio2SourceVoice* Voice; WAVEFORMATEX Format; AudioDataInfo Info; XAUDIO2_SEND_DESCRIPTOR Destination; float StartTimeForQueueBuffer; float LastBufferStartTime; uint64 LastBufferStartSamplesPlayed; int32 BuffersProcessed; int32 Channels; bool IsDirty; bool IsPlaying; bool IsLoop; uint32 LastBufferID; VoiceCallback Callback; Source() { Init(); } void Init() { Voice = nullptr; Destination.Flags = 0; Destination.pOutputVoice = nullptr; Pitch = 1.0f; Pan = 0.0f; StartTimeForQueueBuffer = 0.0f; LastBufferStartTime = 0.0f; IsDirty = false; Is3D = false; IsPlaying = false; IsLoop = false; LastBufferID = 0; LastBufferStartSamplesPlayed = 0; BuffersProcessed = 0; } bool IsFree() const { return Voice == nullptr; } }; struct Buffer { AudioDataInfo Info; Array Data; }; class EngineCallback : public IXAudio2EngineCallback { COM_DECLSPEC_NOTHROW void STDMETHODCALLTYPE OnProcessingPassEnd() override { } COM_DECLSPEC_NOTHROW void STDMETHODCALLTYPE OnProcessingPassStart() override { } COM_DECLSPEC_NOTHROW void STDMETHODCALLTYPE OnCriticalError(HRESULT error) override { LOG(Warning, "IXAudio2EngineCallback::OnCriticalError! Error: 0x{0:x}", error); } }; IXAudio2* Instance = nullptr; IXAudio2MasteringVoice* MasteringVoice = nullptr; int32 Channels; DWORD ChannelMask; bool ForceDirty = true; AudioBackendTools::Settings Settings; Listener Listener; CriticalSection Locker; ChunkedArray Sources; ChunkedArray Buffers; // TODO: use ChunkedArray for better performance or use buffers pool? EngineCallback Callback; Source* GetSource(uint32 sourceID) { if (sourceID == 0) return nullptr; return &Sources[sourceID - 1]; // 0 is invalid ID so shift them } void MarkAllDirty() { ForceDirty = true; } void QueueBuffer(Source* aSource, const int32 bufferID, XAUDIO2_BUFFER& buffer) { Buffer* aBuffer = Buffers[bufferID - 1]; buffer.pAudioData = aBuffer->Data.Get(); buffer.AudioBytes = aBuffer->Data.Count(); if (aSource->StartTimeForQueueBuffer > ZeroTolerance) { // Offset start position when playing buffer with a custom time offset const uint32 bytesPerSample = aBuffer->Info.BitDepth / 8 * aBuffer->Info.NumChannels; buffer.PlayBegin = (UINT32)(aSource->StartTimeForQueueBuffer * aBuffer->Info.SampleRate); buffer.PlayLength = (buffer.AudioBytes / bytesPerSample) - buffer.PlayBegin; aSource->LastBufferStartTime = aSource->StartTimeForQueueBuffer; aSource->StartTimeForQueueBuffer = 0; } const HRESULT hr = aSource->Voice->SubmitSourceBuffer(&buffer); XAUDIO2_CHECK_ERROR(SubmitSourceBuffer); } void VoiceCallback::OnBufferEnd(void* pBufferContext) { auto aSource = GetSource(SourceID); if (aSource->IsPlaying) aSource->BuffersProcessed++; } void VoiceCallback::PeekSamples() { auto aSource = GetSource(SourceID); XAUDIO2_VOICE_STATE state; aSource->Voice->GetState(&state); aSource->LastBufferStartSamplesPlayed = state.SamplesPlayed; } } void AudioBackendXAudio2::Listener_Reset() { XAudio2::Listener.Reset(); XAudio2::MarkAllDirty(); } void AudioBackendXAudio2::Listener_VelocityChanged(const Vector3& velocity) { XAudio2::Listener.Velocity = velocity; XAudio2::MarkAllDirty(); } void AudioBackendXAudio2::Listener_TransformChanged(const Vector3& position, const Quaternion& orientation) { XAudio2::Listener.Position = position; XAudio2::Listener.Orientation = orientation; XAudio2::MarkAllDirty(); } void AudioBackendXAudio2::Listener_ReinitializeAll() { // TODO: Implement XAudio2 reinitialization; read HRTF audio value from Audio class } uint32 AudioBackendXAudio2::Source_Add(const AudioDataInfo& format, const Vector3& position, const Quaternion& orientation, float volume, float pitch, float pan, bool loop, bool spatial, float attenuation, float minDistance, float doppler) { ScopeLock lock(XAudio2::Locker); // Get first free source XAudio2::Source* aSource = nullptr; uint32 sourceID = 0; for (int32 i = 0; i < XAudio2::Sources.Count(); i++) { if (XAudio2::Sources[i].IsFree()) { sourceID = i; aSource = &XAudio2::Sources[i]; break; } } if (aSource == nullptr) { // Add new const XAudio2::Source src; sourceID = XAudio2::Sources.Count(); XAudio2::Sources.Add(src); aSource = &XAudio2::Sources[sourceID]; } sourceID++; // 0 is invalid ID so shift them // Initialize audio data format information (from clip) aSource->Info = format; auto& aFormat = aSource->Format; aFormat.wFormatTag = WAVE_FORMAT_PCM; aFormat.nChannels = spatial ? 1 : format.NumChannels; // 3d audio is always mono (AudioClip auto-converts before buffer write if FeatureFlags::SpatialMultiChannel is unset) aFormat.nSamplesPerSec = format.SampleRate; aFormat.wBitsPerSample = format.BitDepth; aFormat.nBlockAlign = (WORD)(aFormat.nChannels * (aFormat.wBitsPerSample / 8)); aFormat.nAvgBytesPerSec = aFormat.nSamplesPerSec * aFormat.nBlockAlign; aFormat.cbSize = 0; // Setup dry effect aSource->Destination.pOutputVoice = XAudio2::MasteringVoice; // Create voice const XAUDIO2_VOICE_SENDS sendList = { 1, &aSource->Destination }; HRESULT hr = XAudio2::Instance->CreateSourceVoice(&aSource->Voice, &aSource->Format, 0, 2.0f, &aSource->Callback, &sendList); XAUDIO2_CHECK_ERROR(CreateSourceVoice); if (FAILED(hr)) return 0; // Prepare source state aSource->Callback.SourceID = sourceID; aSource->IsDirty = true; aSource->IsLoop = loop; aSource->Is3D = spatial; aSource->Pitch = pitch; aSource->Pan = pan; aSource->DopplerFactor = doppler; aSource->Volume = volume; aSource->MinDistance = minDistance; aSource->Attenuation = attenuation; aSource->Channels = aFormat.nChannels; aSource->Position = position; aSource->Orientation = orientation; aSource->Velocity = Vector3::Zero; hr = aSource->Voice->SetVolume(volume); XAUDIO2_CHECK_ERROR(SetVolume); return sourceID; } void AudioBackendXAudio2::Source_Remove(uint32 sourceID) { ScopeLock lock(XAudio2::Locker); auto aSource = XAudio2::GetSource(sourceID); if (!aSource) return; // Free source if (aSource->Voice) { aSource->Voice->DestroyVoice(); } aSource->Init(); } void AudioBackendXAudio2::Source_VelocityChanged(uint32 sourceID, const Vector3& velocity) { auto aSource = XAudio2::GetSource(sourceID); if (aSource) { aSource->Velocity = velocity; aSource->IsDirty = true; } } void AudioBackendXAudio2::Source_TransformChanged(uint32 sourceID, const Vector3& position, const Quaternion& orientation) { auto aSource = XAudio2::GetSource(sourceID); if (aSource) { aSource->Position = position; aSource->Orientation = orientation; aSource->IsDirty = true; } } void AudioBackendXAudio2::Source_VolumeChanged(uint32 sourceID, float volume) { auto aSource = XAudio2::GetSource(sourceID); if (aSource && aSource->Voice) { aSource->Volume = volume; const HRESULT hr = aSource->Voice->SetVolume(volume); XAUDIO2_CHECK_ERROR(SetVolume); } } void AudioBackendXAudio2::Source_PitchChanged(uint32 sourceID, float pitch) { auto aSource = XAudio2::GetSource(sourceID); if (aSource) { aSource->Pitch = pitch; aSource->IsDirty = true; } } void AudioBackendXAudio2::Source_PanChanged(uint32 sourceID, float pan) { auto aSource = XAudio2::GetSource(sourceID); if (aSource) { aSource->Pan = pan; aSource->IsDirty = true; } } void AudioBackendXAudio2::Source_IsLoopingChanged(uint32 sourceID, bool loop) { ScopeLock lock(XAudio2::Locker); auto aSource = XAudio2::GetSource(sourceID); if (!aSource || !aSource->Voice) return; aSource->IsLoop = loop; // Skip if has no buffers (waiting for data or sth) XAUDIO2_VOICE_STATE state; aSource->Voice->GetState(&state); if (state.BuffersQueued == 0) return; // Looping is defined during buffer submission so reset source buffer (this is called only for non-streamable sources that use a single buffer) const uint32 bufferID = aSource->LastBufferID; XAudio2::Buffer* aBuffer = XAudio2::Buffers[bufferID - 1]; HRESULT hr; const bool isPlaying = aSource->IsPlaying; if (isPlaying) { hr = aSource->Voice->Stop(); XAUDIO2_CHECK_ERROR(Stop); } hr = aSource->Voice->FlushSourceBuffers(); XAUDIO2_CHECK_ERROR(FlushSourceBuffers); aSource->LastBufferStartSamplesPlayed = 0; aSource->LastBufferStartTime = 0; aSource->BuffersProcessed = 0; XAUDIO2_BUFFER buffer = { 0 }; buffer.pContext = aBuffer; buffer.Flags = XAUDIO2_END_OF_STREAM; if (loop) buffer.LoopCount = XAUDIO2_LOOP_INFINITE; // Restore play position const UINT32 totalSamples = aBuffer->Info.NumSamples / aBuffer->Info.NumChannels; buffer.PlayBegin = state.SamplesPlayed % totalSamples; buffer.PlayLength = totalSamples - buffer.PlayBegin; aSource->StartTimeForQueueBuffer = 0; XAudio2::QueueBuffer(aSource, bufferID, buffer); if (isPlaying) { hr = aSource->Voice->Start(); XAUDIO2_CHECK_ERROR(Start); } } void AudioBackendXAudio2::Source_SpatialSetupChanged(uint32 sourceID, bool spatial, float attenuation, float minDistance, float doppler) { auto aSource = XAudio2::GetSource(sourceID); if (aSource) { aSource->Is3D = spatial; aSource->MinDistance = minDistance; aSource->Attenuation = attenuation; aSource->DopplerFactor = doppler; aSource->IsDirty = true; } } void AudioBackendXAudio2::Source_Play(uint32 sourceID) { auto aSource = XAudio2::GetSource(sourceID); if (aSource && aSource->Voice && !aSource->IsPlaying) { // Play const HRESULT hr = aSource->Voice->Start(); XAUDIO2_CHECK_ERROR(Start); aSource->IsPlaying = true; } } void AudioBackendXAudio2::Source_Pause(uint32 sourceID) { auto aSource = XAudio2::GetSource(sourceID); if (aSource && aSource->Voice && aSource->IsPlaying) { // Pause const HRESULT hr = aSource->Voice->Stop(); XAUDIO2_CHECK_ERROR(Stop); aSource->IsPlaying = false; } } void AudioBackendXAudio2::Source_Stop(uint32 sourceID) { auto aSource = XAudio2::GetSource(sourceID); if (aSource && aSource->Voice) { aSource->StartTimeForQueueBuffer = 0.0f; aSource->LastBufferStartTime = 0.0f; // Pause HRESULT hr = aSource->Voice->Stop(); XAUDIO2_CHECK_ERROR(Stop); aSource->IsPlaying = false; // Unset streaming buffers to rewind hr = aSource->Voice->FlushSourceBuffers(); XAUDIO2_CHECK_ERROR(FlushSourceBuffers); Platform::Sleep(10); // TODO: find a better way to handle case when VoiceCallback::OnBufferEnd is called after source was stopped thus BuffersProcessed != 0, probably via buffers contexts ptrs aSource->BuffersProcessed = 0; aSource->Callback.PeekSamples(); } } void AudioBackendXAudio2::Source_SetCurrentBufferTime(uint32 sourceID, float value) { const auto aSource = XAudio2::GetSource(sourceID); if (aSource) { // Store start time so next buffer submitted will start from here (assumes audio is stopped) aSource->StartTimeForQueueBuffer = value; } } float AudioBackendXAudio2::Source_GetCurrentBufferTime(uint32 sourceID) { float time = 0; auto aSource = XAudio2::GetSource(sourceID); if (aSource) { const auto& clipInfo = aSource->Info; XAUDIO2_VOICE_STATE state; aSource->Voice->GetState(&state); const uint32 totalSamples = clipInfo.NumSamples / clipInfo.NumChannels; const uint32 sampleRate = clipInfo.SampleRate; // / clipInfo.NumChannels; uint64 lastBufferStartSamplesPlayed = aSource->LastBufferStartSamplesPlayed; if (totalSamples > 0) lastBufferStartSamplesPlayed %= totalSamples; state.SamplesPlayed -= lastBufferStartSamplesPlayed % totalSamples; // Offset by the last buffer start to get time relative to its begin if (totalSamples > 0) state.SamplesPlayed %= totalSamples; time = aSource->LastBufferStartTime + state.SamplesPlayed / static_cast(Math::Max(1U, sampleRate)); } return time; } void AudioBackendXAudio2::Source_SetNonStreamingBuffer(uint32 sourceID, uint32 bufferID) { auto aSource = XAudio2::GetSource(sourceID); if (!aSource) return; aSource->LastBufferID = bufferID; // Use for looping change XAudio2::Locker.Lock(); XAudio2::Buffer* aBuffer = XAudio2::Buffers[bufferID - 1]; XAudio2::Locker.Unlock(); XAUDIO2_BUFFER buffer = { 0 }; buffer.pContext = aBuffer; buffer.Flags = XAUDIO2_END_OF_STREAM; if (aSource->IsLoop) buffer.LoopCount = XAUDIO2_LOOP_INFINITE; // Queue single buffer XAudio2::QueueBuffer(aSource, bufferID, buffer); } void AudioBackendXAudio2::Source_GetProcessedBuffersCount(uint32 sourceID, int32& processedBuffersCount) { processedBuffersCount = 0; auto aSource = XAudio2::GetSource(sourceID); if (aSource && aSource->Voice) { processedBuffersCount = aSource->BuffersProcessed; } } void AudioBackendXAudio2::Source_GetQueuedBuffersCount(uint32 sourceID, int32& queuedBuffersCount) { queuedBuffersCount = 0; auto aSource = XAudio2::GetSource(sourceID); if (aSource && aSource->Voice) { XAUDIO2_VOICE_STATE state; aSource->Voice->GetState(&state, XAUDIO2_VOICE_NOSAMPLESPLAYED); queuedBuffersCount = state.BuffersQueued; } } void AudioBackendXAudio2::Source_QueueBuffer(uint32 sourceID, uint32 bufferID) { auto aSource = XAudio2::GetSource(sourceID); if (!aSource) return; aSource->LastBufferID = bufferID; // Use for looping change XAudio2::Buffer* aBuffer = XAudio2::Buffers[bufferID - 1]; XAUDIO2_BUFFER buffer = { 0 }; buffer.pContext = aBuffer; XAudio2::QueueBuffer(aSource, bufferID, buffer); } void AudioBackendXAudio2::Source_DequeueProcessedBuffers(uint32 sourceID) { auto aSource = XAudio2::GetSource(sourceID); if (aSource && aSource->Voice) { const HRESULT hr = aSource->Voice->FlushSourceBuffers(); XAUDIO2_CHECK_ERROR(FlushSourceBuffers); aSource->BuffersProcessed = 0; } } uint32 AudioBackendXAudio2::Buffer_Create() { uint32 bufferID; ScopeLock lock(XAudio2::Locker); // Get first free buffer slot XAudio2::Buffer* aBuffer = nullptr; for (int32 i = 0; i < XAudio2::Buffers.Count(); i++) { if (XAudio2::Buffers[i] == nullptr) { aBuffer = New(); XAudio2::Buffers[i] = aBuffer; bufferID = i + 1; break; } } if (!aBuffer) { // Add new slot aBuffer = New(); XAudio2::Buffers.Add(aBuffer); bufferID = XAudio2::Buffers.Count(); } aBuffer->Data.Resize(0); return bufferID; } void AudioBackendXAudio2::Buffer_Delete(uint32 bufferID) { ScopeLock lock(XAudio2::Locker); XAudio2::Buffer*& aBuffer = XAudio2::Buffers[bufferID - 1]; aBuffer->Data.Resize(0); Delete(aBuffer); aBuffer = nullptr; } void AudioBackendXAudio2::Buffer_Write(uint32 bufferID, byte* samples, const AudioDataInfo& info) { CHECK(info.NumChannels <= MAX_INPUT_CHANNELS); XAudio2::Locker.Lock(); XAudio2::Buffer* aBuffer = XAudio2::Buffers[bufferID - 1]; XAudio2::Locker.Unlock(); const uint32 samplesLength = info.NumSamples * info.BitDepth / 8; aBuffer->Info = info; aBuffer->Data.Set(samples, samplesLength); } const Char* AudioBackendXAudio2::Base_Name() { return TEXT("XAudio2"); } AudioBackend::FeatureFlags AudioBackendXAudio2::Base_Features() { return FeatureFlags::None; } void AudioBackendXAudio2::Base_OnActiveDeviceChanged() { } void AudioBackendXAudio2::Base_SetDopplerFactor(float value) { XAudio2::Settings.DopplerFactor = value; XAudio2::MarkAllDirty(); } void AudioBackendXAudio2::Base_SetVolume(float value) { if (XAudio2::MasteringVoice) { XAudio2::Settings.Volume = 1.0f; // Volume is applied via MasteringVoice const HRESULT hr = XAudio2::MasteringVoice->SetVolume(value); XAUDIO2_CHECK_ERROR(SetVolume); } } bool AudioBackendXAudio2::Base_Init() { auto& devices = Audio::Devices; // Initialize XAudio backend HRESULT hr = XAudio2Create(&XAudio2::Instance, 0, XAUDIO2_DEFAULT_PROCESSOR); if (FAILED(hr)) { LOG(Error, "Failed to initalize XAudio2. Error: 0x{0:x}", hr); return true; } XAudio2::Instance->RegisterForCallbacks(&XAudio2::Callback); // Initialize master voice hr = XAudio2::Instance->CreateMasteringVoice(&XAudio2::MasteringVoice); if (FAILED(hr)) { LOG(Error, "Failed to initalize XAudio2 mastering voice. Error: 0x{0:x}", hr); return true; } XAUDIO2_VOICE_DETAILS details; XAudio2::MasteringVoice->GetVoiceDetails(&details); #if MAX_OUTPUT_CHANNELS > 2 // TODO: implement multi-channel support (eg. 5.1, 7.1) XAudio2::Channels = details.InputChannels; hr = XAudio2::MasteringVoice->GetChannelMask(&XAudio2::ChannelMask); if (FAILED(hr)) { LOG(Error, "Failed to get XAudio2 mastering voice channel mask. Error: 0x{0:x}", hr); return true; } #else XAudio2::Channels = 2; XAudio2::ChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; #endif LOG(Info, "XAudio2: {0} channels at {1} kHz", XAudio2::Channels, details.InputSampleRate / 1000.0f); // Dummy device devices.Resize(1); devices[0].Name = TEXT("XAudio2 device"); Audio::SetActiveDeviceIndex(0); return false; } void AudioBackendXAudio2::Base_Update() { // Update dirty voices float outputMatrix[MAX_CHANNELS_MATRIX_SIZE]; for (int32 i = 0; i < XAudio2::Sources.Count(); i++) { auto& source = XAudio2::Sources[i]; if (source.IsFree() || !(source.IsDirty || XAudio2::ForceDirty)) continue; auto mix = AudioBackendTools::CalculateSoundMix(XAudio2::Settings, XAudio2::Listener, source, XAudio2::Channels); mix.VolumeIntoChannels(); AudioBackendTools::MapChannels(source.Channels, XAudio2::Channels, mix.Channels, outputMatrix); source.Voice->SetFrequencyRatio(mix.Pitch); source.Voice->SetOutputMatrix(XAudio2::MasteringVoice, source.Channels, XAudio2::Channels, outputMatrix); source.IsDirty = false; } // Clear flag XAudio2::ForceDirty = false; } void AudioBackendXAudio2::Base_Dispose() { // Cleanup stuff if (XAudio2::MasteringVoice) { XAudio2::MasteringVoice->DestroyVoice(); XAudio2::MasteringVoice = nullptr; } if (XAudio2::Instance) { XAudio2::Instance->StopEngine(); XAudio2::Instance->Release(); XAudio2::Instance = nullptr; } } #endif