2 * Copyright (C) 2010-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
9 #include "AESinkXAudio.h"
11 #include "ServiceBroker.h"
12 #include "cores/AudioEngine/AESinkFactory.h"
13 #include "cores/AudioEngine/Sinks/windows/AESinkFactoryWin.h"
14 #include "cores/AudioEngine/Utils/AEDeviceInfo.h"
15 #include "cores/AudioEngine/Utils/AEUtil.h"
16 #include "utils/SystemInfo.h"
17 #include "utils/log.h"
19 #include "platform/win32/CharsetConverter.h"
26 using namespace Microsoft::WRL
;
30 constexpr int XAUDIO_BUFFERS_IN_QUEUE
= 2;
32 HRESULT
KXAudio2Create(IXAudio2
** ppXAudio2
,
33 UINT32 Flags
X2DEFAULT(0),
34 XAUDIO2_PROCESSOR XAudio2Processor
X2DEFAULT(XAUDIO2_DEFAULT_PROCESSOR
))
36 typedef HRESULT(__stdcall
* XAudio2CreateInfoFunc
)(_Outptr_ IXAudio2
**, UINT32
,
38 static HMODULE dll
= NULL
;
39 static XAudio2CreateInfoFunc XAudio2CreateFn
= nullptr;
43 dll
= LoadLibraryEx(L
"xaudio2_9redist.dll", NULL
, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
);
47 dll
= LoadLibraryEx(L
"xaudio2_9.dll", NULL
, LOAD_LIBRARY_SEARCH_SYSTEM32
);
51 // Windows 8 compatibility
52 dll
= LoadLibraryEx(L
"xaudio2_8.dll", NULL
, LOAD_LIBRARY_SEARCH_SYSTEM32
);
55 return HRESULT_FROM_WIN32(GetLastError());
59 XAudio2CreateFn
= (XAudio2CreateInfoFunc
)(void*)GetProcAddress(dll
, "XAudio2Create");
62 return HRESULT_FROM_WIN32(GetLastError());
67 return (*XAudio2CreateFn
)(ppXAudio2
, Flags
, XAudio2Processor
);
74 extern const char* WASAPIErrToStr(HRESULT err
);
76 template <class TVoice
>
77 inline void SafeDestroyVoice(TVoice
**ppVoice
)
81 (*ppVoice
)->DestroyVoice();
86 CAESinkXAudio::CAESinkXAudio()
88 HRESULT hr
= KXAudio2Create(m_xAudio2
.ReleaseAndGetAddressOf(), 0);
91 CLog::LogF(LOGERROR
, "XAudio initialization failed, error {:X}.", hr
);
96 XAUDIO2_DEBUG_CONFIGURATION config
= {};
97 config
.BreakMask
= XAUDIO2_LOG_ERRORS
| XAUDIO2_LOG_WARNINGS
| XAUDIO2_LOG_API_CALLS
| XAUDIO2_LOG_STREAMING
;
98 config
.TraceMask
= XAUDIO2_LOG_ERRORS
| XAUDIO2_LOG_WARNINGS
| XAUDIO2_LOG_API_CALLS
| XAUDIO2_LOG_STREAMING
;
99 config
.LogTiming
= true;
100 config
.LogFunctionName
= true;
101 m_xAudio2
->SetDebugConfiguration(&config
, 0);
105 // Get performance counter frequency for latency calculations
106 QueryPerformanceFrequency(&m_timerFreq
);
109 CAESinkXAudio::~CAESinkXAudio()
115 void CAESinkXAudio::Register()
117 AE::AESinkRegEntry reg
;
118 reg
.sinkName
= "XAUDIO";
119 reg
.createFunc
= CAESinkXAudio::Create
;
120 reg
.enumerateFunc
= CAESinkXAudio::EnumerateDevicesEx
;
121 AE::CAESinkFactory::RegisterSink(reg
);
124 std::unique_ptr
<IAESink
> CAESinkXAudio::Create(std::string
& device
, AEAudioFormat
& desiredFormat
)
126 auto sink
= std::make_unique
<CAESinkXAudio
>();
128 if (!sink
->m_xAudio2
)
130 CLog::LogF(LOGERROR
, "XAudio2 not loaded.");
134 if (sink
->Initialize(desiredFormat
, device
))
140 bool CAESinkXAudio::Initialize(AEAudioFormat
&format
, std::string
&device
)
145 /* Save requested format */
146 AEDataFormat reqFormat
= format
.m_dataFormat
;
148 if (!InitializeInternal(device
, format
))
150 CLog::LogF(LOGINFO
, "could not Initialize voices with format {}",
151 CAEUtil::DataFormatToStr(reqFormat
));
152 CLog::LogF(LOGERROR
, "XAudio initialization failed");
156 m_initialized
= true;
162 void CAESinkXAudio::Deinitialize()
164 if (!m_initialized
&& !m_isDirty
)
171 m_sourceVoice
->Stop();
172 m_sourceVoice
->FlushSourceBuffers();
174 // Stop and FlushSourceBuffers are async, wait for queued buffers to be released by XAudio2.
175 // callbacks don't seem to be called otherwise, with memory leakage.
176 XAUDIO2_VOICE_STATE state
{};
179 if (WAIT_OBJECT_0
!= WaitForSingleObject(m_voiceCallback
.mBufferEnd
.get(), 500))
181 CLog::LogF(LOGERROR
, "timeout waiting for buffer flush - possible buffer memory leak");
184 m_sourceVoice
->GetState(&state
, 0);
185 } while (state
.BuffersQueued
> 0);
188 m_framesInBuffers
= 0;
192 CLog::LogF(LOGERROR
, "invalidated source voice - Releasing");
197 SafeDestroyVoice(&m_sourceVoice
);
198 SafeDestroyVoice(&m_masterVoice
);
200 m_initialized
= false;
203 void CAESinkXAudio::GetDelay(AEDelayStatus
& status
)
207 status
.SetDelay(0.0);
211 XAUDIO2_VOICE_STATE state
;
212 m_sourceVoice
->GetState(&state
, 0);
214 status
.SetDelay(static_cast<double>(m_sinkFrames
- state
.SamplesPlayed
) / m_format
.m_sampleRate
);
218 double CAESinkXAudio::GetCacheTotal()
223 return static_cast<double>(XAUDIO_BUFFERS_IN_QUEUE
* m_format
.m_frames
) / m_format
.m_sampleRate
;
226 double CAESinkXAudio::GetLatency()
228 if (!m_initialized
|| !m_xAudio2
)
231 XAUDIO2_PERFORMANCE_DATA perfData
;
232 m_xAudio2
->GetPerformanceData(&perfData
);
234 return static_cast<double>(perfData
.CurrentLatencyInSamples
) / m_format
.m_sampleRate
;
237 unsigned int CAESinkXAudio::AddPackets(uint8_t **data
, unsigned int frames
, unsigned int offset
)
244 LARGE_INTEGER timerStart
;
245 LARGE_INTEGER timerStop
;
247 XAUDIO2_BUFFER xbuffer
= BuildXAudio2Buffer(data
, frames
, offset
);
249 if (!m_running
) //first time called, pre-fill buffer then start voice
251 m_sourceVoice
->Stop();
252 hr
= m_sourceVoice
->SubmitSourceBuffer(&xbuffer
);
255 CLog::LogF(LOGERROR
, "voice submit buffer failed due to {}", WASAPIErrToStr(hr
));
256 delete xbuffer
.pContext
;
259 hr
= m_sourceVoice
->Start(0, XAUDIO2_COMMIT_NOW
);
262 CLog::LogF(LOGERROR
, "voice start failed due to {}", WASAPIErrToStr(hr
));
263 m_isDirty
= true; //flag new device or re-init needed
264 delete xbuffer
.pContext
;
267 m_sinkFrames
+= frames
;
268 m_framesInBuffers
+= frames
;
269 m_running
= true; //signal that we're processing frames
273 /* Get clock time for latency checks */
274 QueryPerformanceCounter(&timerStart
);
276 /* Wait for Audio Driver to tell us it's got a buffer available */
277 while (m_format
.m_frames
* XAUDIO_BUFFERS_IN_QUEUE
<= m_framesInBuffers
.load())
279 DWORD eventAudioCallback
;
280 eventAudioCallback
= WaitForSingleObjectEx(m_voiceCallback
.mBufferEnd
.get(), 1100, TRUE
);
281 if (eventAudioCallback
!= WAIT_OBJECT_0
)
283 CLog::LogF(LOGERROR
, "voice buffer timed out");
284 delete xbuffer
.pContext
;
292 QueryPerformanceCounter(&timerStop
);
293 const LONGLONG timerDiff
= timerStop
.QuadPart
- timerStart
.QuadPart
;
294 const double timerElapsed
= static_cast<double>(timerDiff
) * 1000.0 / m_timerFreq
.QuadPart
;
295 m_avgTimeWaiting
+= (timerElapsed
- m_avgTimeWaiting
) * 0.5;
297 if (m_avgTimeWaiting
< 3.0)
299 CLog::LogF(LOGDEBUG
, "Possible AQ Loss: Avg. Time Waiting for Audio Driver callback : {}msec",
300 (int)m_avgTimeWaiting
);
303 hr
= m_sourceVoice
->SubmitSourceBuffer(&xbuffer
);
306 CLog::LogF(LOGERROR
, "submiting buffer failed due to {}", WASAPIErrToStr(hr
));
307 delete xbuffer
.pContext
;
311 m_sinkFrames
+= frames
;
312 m_framesInBuffers
+= frames
;
317 void CAESinkXAudio::EnumerateDevicesEx(AEDeviceInfoList
&deviceInfoList
, bool force
)
320 CAEDeviceInfo deviceInfo
;
321 CAEChannelInfo deviceChannels
;
322 WAVEFORMATEXTENSIBLE wfxex
= {};
323 UINT32 eflags
= 0; // XAUDIO2_DEBUG_ENGINE;
324 IXAudio2MasteringVoice
* mMasterVoice
= nullptr;
325 IXAudio2SourceVoice
* mSourceVoice
= nullptr;
326 Microsoft::WRL::ComPtr
<IXAudio2
> xaudio2
;
328 // ForegroundOnlyMedia/BackgroundCapableMedia replaced in Windows 10 by Movie/Media
329 const AUDIO_STREAM_CATEGORY streamCategory
{
330 CSysInfo::IsWindowsVersionAtLeast(CSysInfo::WindowsVersionWin10
)
331 ? AudioCategory_Media
332 : AudioCategory_ForegroundOnlyMedia
};
334 hr
= KXAudio2Create(xaudio2
.ReleaseAndGetAddressOf(), eflags
);
337 CLog::LogF(LOGERROR
, "failed to activate XAudio for capability testing ({})",
342 for (RendererDetail
& details
: CAESinkFactoryWin::GetRendererDetailsWinRT())
344 deviceInfo
.m_channels
.Reset();
345 deviceInfo
.m_dataFormats
.clear();
346 deviceInfo
.m_sampleRates
.clear();
347 deviceChannels
.Reset();
349 for (unsigned int c
= 0; c
< WASAPI_SPEAKER_COUNT
; c
++)
351 if (details
.uiChannelMask
& WASAPIChannelOrder
[c
])
352 deviceChannels
+= AEChannelNames
[c
];
355 const std::wstring deviceId
= KODI::PLATFORM::WINDOWS::ToW(details
.strDeviceId
);
357 /* Test format for PCM format iteration */
358 wfxex
.Format
.cbSize
= sizeof(WAVEFORMATEXTENSIBLE
) - sizeof(WAVEFORMATEX
);
359 wfxex
.Format
.nSamplesPerSec
= 48000;
360 wfxex
.Format
.nChannels
= 2;
361 wfxex
.dwChannelMask
= KSAUDIO_SPEAKER_STEREO
;
362 wfxex
.Format
.wFormatTag
= WAVE_FORMAT_EXTENSIBLE
;
363 wfxex
.SubFormat
= KSDATAFORMAT_SUBTYPE_IEEE_FLOAT
;
365 hr
= xaudio2
->CreateMasteringVoice(&mMasterVoice
, wfxex
.Format
.nChannels
,
366 wfxex
.Format
.nSamplesPerSec
, 0, deviceId
.c_str(), nullptr,
371 CLog::LogF(LOGERROR
, "failed to create mastering voice (:X)", hr
);
375 for (int p
= AE_FMT_FLOAT
; p
> AE_FMT_INVALID
; p
--)
377 if (p
< AE_FMT_FLOAT
)
378 wfxex
.SubFormat
= KSDATAFORMAT_SUBTYPE_PCM
;
379 wfxex
.Format
.wBitsPerSample
= CAEUtil::DataFormatToBits((AEDataFormat
)p
);
380 wfxex
.Format
.nBlockAlign
= wfxex
.Format
.nChannels
* (wfxex
.Format
.wBitsPerSample
>> 3);
381 wfxex
.Format
.nAvgBytesPerSec
= wfxex
.Format
.nSamplesPerSec
* wfxex
.Format
.nBlockAlign
;
382 if (p
== AE_FMT_S24NE4MSB
)
384 wfxex
.Samples
.wValidBitsPerSample
= 24;
386 else if (p
<= AE_FMT_S24NE4
&& p
>= AE_FMT_S24BE4
)
393 wfxex
.Samples
.wValidBitsPerSample
= wfxex
.Format
.wBitsPerSample
;
396 SafeDestroyVoice(&mSourceVoice
);
397 hr
= xaudio2
->CreateSourceVoice(&mSourceVoice
, &wfxex
.Format
);
400 deviceInfo
.m_dataFormats
.push_back((AEDataFormat
)p
);
403 /* Test format for sample rate iteration */
404 wfxex
.Format
.cbSize
= sizeof(WAVEFORMATEXTENSIBLE
) - sizeof(WAVEFORMATEX
);
405 wfxex
.dwChannelMask
= KSAUDIO_SPEAKER_STEREO
;
406 wfxex
.Format
.wFormatTag
= WAVE_FORMAT_EXTENSIBLE
;
407 wfxex
.SubFormat
= KSDATAFORMAT_SUBTYPE_PCM
;
408 wfxex
.Format
.wBitsPerSample
= 16;
409 wfxex
.Samples
.wValidBitsPerSample
= 16;
410 wfxex
.Format
.nChannels
= 2;
411 wfxex
.Format
.nBlockAlign
= wfxex
.Format
.nChannels
* (wfxex
.Format
.wBitsPerSample
>> 3);
412 wfxex
.Format
.nAvgBytesPerSec
= wfxex
.Format
.nSamplesPerSec
* wfxex
.Format
.nBlockAlign
;
414 for (int j
= 0; j
< WASAPISampleRateCount
; j
++)
416 if (WASAPISampleRates
[j
] < XAUDIO2_MIN_SAMPLE_RATE
||
417 WASAPISampleRates
[j
] > XAUDIO2_MAX_SAMPLE_RATE
)
420 SafeDestroyVoice(&mSourceVoice
);
421 SafeDestroyVoice(&mMasterVoice
);
423 wfxex
.Format
.nSamplesPerSec
= WASAPISampleRates
[j
];
424 wfxex
.Format
.nAvgBytesPerSec
= wfxex
.Format
.nSamplesPerSec
* wfxex
.Format
.nBlockAlign
;
426 if (SUCCEEDED(xaudio2
->CreateMasteringVoice(&mMasterVoice
, wfxex
.Format
.nChannels
,
427 wfxex
.Format
.nSamplesPerSec
, 0, deviceId
.c_str(),
428 nullptr, streamCategory
)))
430 hr
= xaudio2
->CreateSourceVoice(&mSourceVoice
, &wfxex
.Format
);
433 deviceInfo
.m_sampleRates
.push_back(WASAPISampleRates
[j
]);
437 SafeDestroyVoice(&mSourceVoice
);
438 SafeDestroyVoice(&mMasterVoice
);
440 deviceInfo
.m_deviceName
= details
.strDeviceId
;
441 deviceInfo
.m_displayName
= details
.strWinDevType
.append(details
.strDescription
);
442 deviceInfo
.m_displayNameExtra
= std::string("XAudio: ").append(details
.strDescription
);
443 deviceInfo
.m_deviceType
= details
.eDeviceType
;
444 deviceInfo
.m_channels
= deviceChannels
;
446 /* Store the device info */
447 deviceInfo
.m_wantsIECPassthrough
= true;
448 deviceInfo
.m_onlyPCM
= true;
450 if (!deviceInfo
.m_streamTypes
.empty())
451 deviceInfo
.m_dataFormats
.push_back(AE_FMT_RAW
);
453 deviceInfoList
.push_back(deviceInfo
);
455 if (details
.bDefault
)
457 deviceInfo
.m_deviceName
= std::string("default");
458 deviceInfo
.m_displayName
= std::string("default");
459 deviceInfo
.m_displayNameExtra
= std::string("");
460 deviceInfo
.m_wantsIECPassthrough
= true;
461 deviceInfo
.m_onlyPCM
= true;
462 deviceInfoList
.push_back(deviceInfo
);
467 bool CAESinkXAudio::InitializeInternal(std::string deviceId
, AEAudioFormat
&format
)
469 const std::wstring device
= KODI::PLATFORM::WINDOWS::ToW(deviceId
);
470 WAVEFORMATEXTENSIBLE wfxex
= {};
472 if ( format
.m_dataFormat
<= AE_FMT_FLOAT
473 || format
.m_dataFormat
== AE_FMT_RAW
)
474 CAESinkFactoryWin::BuildWaveFormatExtensible(format
, wfxex
);
477 // planar formats are currently not supported by this sink
478 format
.m_dataFormat
= AE_FMT_FLOAT
;
479 CAESinkFactoryWin::BuildWaveFormatExtensible(format
, wfxex
);
482 /* Test for incomplete format and provide defaults */
483 if (format
.m_sampleRate
== 0 ||
484 format
.m_channelLayout
== CAEChannelInfo(nullptr) ||
485 format
.m_dataFormat
<= AE_FMT_INVALID
||
486 format
.m_dataFormat
>= AE_FMT_MAX
||
487 format
.m_channelLayout
.Count() == 0)
489 wfxex
.Format
.wFormatTag
= WAVE_FORMAT_EXTENSIBLE
;
490 wfxex
.Format
.nChannels
= 2;
491 wfxex
.Format
.nSamplesPerSec
= 44100L;
492 wfxex
.Format
.wBitsPerSample
= 16;
493 wfxex
.Format
.nBlockAlign
= 4;
494 wfxex
.Samples
.wValidBitsPerSample
= 16;
495 wfxex
.Format
.cbSize
= sizeof(WAVEFORMATEXTENSIBLE
) - sizeof(WAVEFORMATEX
);
496 wfxex
.Format
.nAvgBytesPerSec
= wfxex
.Format
.nBlockAlign
* wfxex
.Format
.nSamplesPerSec
;
497 wfxex
.dwChannelMask
= SPEAKER_FRONT_LEFT
| SPEAKER_FRONT_RIGHT
;
498 wfxex
.SubFormat
= KSDATAFORMAT_SUBTYPE_PCM
;
501 const bool bdefault
= deviceId
.find("default") != std::string::npos
;
504 IXAudio2MasteringVoice
* pMasterVoice
= nullptr;
505 // ForegroundOnlyMedia/BackgroundCapableMedia replaced in Windows 10 by Movie/Media
506 const AUDIO_STREAM_CATEGORY streamCategory
{
507 CSysInfo::IsWindowsVersionAtLeast(CSysInfo::WindowsVersionWin10
)
508 ? AudioCategory_Media
509 : AudioCategory_ForegroundOnlyMedia
};
513 hr
= m_xAudio2
->CreateMasteringVoice(&pMasterVoice
, wfxex
.Format
.nChannels
,
514 wfxex
.Format
.nSamplesPerSec
, 0, device
.c_str(), nullptr,
523 "could not locate the device named \"{}\" in the list of Xaudio endpoint devices. "
524 "Trying the default device...",
525 KODI::PLATFORM::WINDOWS::FromW(device
));
528 // smartphone issue: providing device ID (even default ID) causes E_NOINTERFACE result
529 // workaround: device = nullptr will initialize default audio endpoint
530 hr
= m_xAudio2
->CreateMasteringVoice(&pMasterVoice
, wfxex
.Format
.nChannels
,
531 wfxex
.Format
.nSamplesPerSec
, 0, nullptr, nullptr,
533 if (FAILED(hr
) || !pMasterVoice
)
535 CLog::LogF(LOGINFO
, "Could not retrieve the default XAudio audio endpoint ({}).",
541 m_masterVoice
= pMasterVoice
;
543 int closestMatch
= 0;
544 unsigned int requestedChannels
= 0;
545 unsigned int noOfCh
= 0;
547 hr
= m_xAudio2
->CreateSourceVoice(&m_sourceVoice
, &wfxex
.Format
, 0, XAUDIO2_DEFAULT_FREQ_RATIO
, &m_voiceCallback
);
550 CLog::LogF(LOGINFO
, "Format is Supported - will attempt to Initialize");
554 if (format
.m_dataFormat
== AE_FMT_RAW
) //No sense in trying other formats for passthrough.
557 if (CServiceBroker::GetLogging().CanLogComponent(LOGAUDIO
))
558 CLog::LogFC(LOGDEBUG
, LOGAUDIO
,
559 "CreateSourceVoice failed ({}) - trying to find a compatible format",
562 requestedChannels
= wfxex
.Format
.nChannels
;
564 /* The requested format is not supported by the device. Find something that works */
565 for (int layout
= -1; layout
<= (int)ARRAYSIZE(layoutsList
); layout
++)
567 // if requested layout is not supported, try standard layouts with at least
568 // the number of channels as requested
569 // as the last resort try stereo
570 if (layout
== ARRAYSIZE(layoutsList
))
572 wfxex
.dwChannelMask
= SPEAKER_FRONT_LEFT
| SPEAKER_FRONT_RIGHT
;
573 wfxex
.Format
.nChannels
= 2;
575 else if (layout
>= 0)
577 wfxex
.dwChannelMask
= CAESinkFactoryWin::ChLayoutToChMask(layoutsList
[layout
], &noOfCh
);
578 wfxex
.Format
.nChannels
= noOfCh
;
579 if (noOfCh
< requestedChannels
)
583 for (int j
= 0; j
< sizeof(testFormats
)/sizeof(sampleFormat
); j
++)
587 wfxex
.Format
.wFormatTag
= WAVE_FORMAT_EXTENSIBLE
;
588 wfxex
.SubFormat
= testFormats
[j
].subFormat
;
589 wfxex
.Format
.wBitsPerSample
= testFormats
[j
].bitsPerSample
;
590 wfxex
.Samples
.wValidBitsPerSample
= testFormats
[j
].validBitsPerSample
;
591 wfxex
.Format
.nBlockAlign
= wfxex
.Format
.nChannels
* (wfxex
.Format
.wBitsPerSample
>> 3);
593 for (int i
= 0 ; i
< WASAPISampleRateCount
; i
++)
595 if (WASAPISampleRates
[j
] < XAUDIO2_MIN_SAMPLE_RATE
||
596 WASAPISampleRates
[j
] > XAUDIO2_MAX_SAMPLE_RATE
)
599 SafeDestroyVoice(&m_sourceVoice
);
600 SafeDestroyVoice(&m_masterVoice
);
602 wfxex
.Format
.nSamplesPerSec
= WASAPISampleRates
[i
];
603 wfxex
.Format
.nAvgBytesPerSec
= wfxex
.Format
.nSamplesPerSec
* wfxex
.Format
.nBlockAlign
;
605 hr
= m_xAudio2
->CreateMasteringVoice(&m_masterVoice
, wfxex
.Format
.nChannels
,
606 wfxex
.Format
.nSamplesPerSec
, 0, device
.c_str(),
607 nullptr, streamCategory
);
610 hr
= m_xAudio2
->CreateSourceVoice(&m_sourceVoice
, &wfxex
.Format
, 0, XAUDIO2_DEFAULT_FREQ_RATIO
, &m_voiceCallback
);
613 /* If the current sample rate matches the source then stop looking and use it */
614 if ((WASAPISampleRates
[i
] == format
.m_sampleRate
) && (testFormats
[j
].subFormatType
<= format
.m_dataFormat
))
616 /* If this rate is closer to the source then the previous one, save it */
617 else if (closestMatch
< 0 || abs((int)WASAPISampleRates
[i
] - (int)format
.m_sampleRate
) < abs((int)WASAPISampleRates
[closestMatch
] - (int)format
.m_sampleRate
))
623 CLog::LogF(LOGERROR
, "creating voices failed ({})", WASAPIErrToStr(hr
));
626 if (closestMatch
>= 0)
628 // Closest match may be different from the last successful sample rate tested
629 SafeDestroyVoice(&m_sourceVoice
);
630 SafeDestroyVoice(&m_masterVoice
);
632 wfxex
.Format
.nSamplesPerSec
= WASAPISampleRates
[closestMatch
];
633 wfxex
.Format
.nAvgBytesPerSec
= wfxex
.Format
.nSamplesPerSec
* wfxex
.Format
.nBlockAlign
;
635 if (SUCCEEDED(m_xAudio2
->CreateMasteringVoice(&m_masterVoice
, wfxex
.Format
.nChannels
,
636 wfxex
.Format
.nSamplesPerSec
, 0,
637 device
.c_str(), nullptr, streamCategory
)) &&
638 SUCCEEDED(m_xAudio2
->CreateSourceVoice(&m_sourceVoice
, &wfxex
.Format
, 0,
639 XAUDIO2_DEFAULT_FREQ_RATIO
, &m_voiceCallback
)))
645 CLog::LogF(LOGERROR
, "unable to locate a supported output format for the device. Check the "
646 "speaker settings in the control panel.");
648 /* We couldn't find anything supported. This should never happen */
649 /* unless the user set the wrong speaker setting in the control panel */
654 CAEChannelInfo channelLayout
;
655 CAESinkFactoryWin::AEChannelsFromSpeakerMask(channelLayout
, wfxex
.dwChannelMask
);
656 format
.m_channelLayout
= channelLayout
;
658 /* Set up returned sink format for engine */
659 if (format
.m_dataFormat
!= AE_FMT_RAW
)
661 if (wfxex
.Format
.wBitsPerSample
== 32)
663 if (wfxex
.SubFormat
== KSDATAFORMAT_SUBTYPE_IEEE_FLOAT
)
664 format
.m_dataFormat
= AE_FMT_FLOAT
;
665 else if (wfxex
.Samples
.wValidBitsPerSample
== 32)
666 format
.m_dataFormat
= AE_FMT_S32NE
;
668 format
.m_dataFormat
= AE_FMT_S24NE4MSB
;
670 else if (wfxex
.Format
.wBitsPerSample
== 24)
671 format
.m_dataFormat
= AE_FMT_S24NE3
;
673 format
.m_dataFormat
= AE_FMT_S16NE
;
676 format
.m_sampleRate
= wfxex
.Format
.nSamplesPerSec
; //PCM: Sample rate. RAW: Link speed
677 format
.m_frameSize
= (wfxex
.Format
.wBitsPerSample
>> 3) * wfxex
.Format
.nChannels
;
679 if (format
.m_dataFormat
== AE_FMT_RAW
)
680 format
.m_dataFormat
= AE_FMT_S16NE
;
682 hr
= m_sourceVoice
->Start(0, XAUDIO2_COMMIT_NOW
);
685 CLog::LogF(LOGERROR
, "Voice start failed : {}", WASAPIErrToStr(hr
));
686 CLog::Log(LOGDEBUG
, " Sample Rate : {}", wfxex
.Format
.nSamplesPerSec
);
687 CLog::Log(LOGDEBUG
, " Sample Format : {}", CAEUtil::DataFormatToStr(format
.m_dataFormat
));
688 CLog::Log(LOGDEBUG
, " Bits Per Sample : {}", wfxex
.Format
.wBitsPerSample
);
689 CLog::Log(LOGDEBUG
, " Valid Bits/Samp : {}", wfxex
.Samples
.wValidBitsPerSample
);
690 CLog::Log(LOGDEBUG
, " Channel Count : {}", wfxex
.Format
.nChannels
);
691 CLog::Log(LOGDEBUG
, " Block Align : {}", wfxex
.Format
.nBlockAlign
);
692 CLog::Log(LOGDEBUG
, " Avg. Bytes Sec : {}", wfxex
.Format
.nAvgBytesPerSec
);
693 CLog::Log(LOGDEBUG
, " Samples/Block : {}", wfxex
.Samples
.wSamplesPerBlock
);
694 CLog::Log(LOGDEBUG
, " Format cBSize : {}", wfxex
.Format
.cbSize
);
695 CLog::Log(LOGDEBUG
, " Channel Layout : {}", ((std::string
)format
.m_channelLayout
));
696 CLog::Log(LOGDEBUG
, " Channel Mask : {}", wfxex
.dwChannelMask
);
700 XAUDIO2_PERFORMANCE_DATA perfData
;
701 m_xAudio2
->GetPerformanceData(&perfData
);
702 if (!perfData
.TotalSourceVoiceCount
)
704 CLog::LogF(LOGERROR
, "GetPerformanceData Failed : {}", WASAPIErrToStr(hr
));
708 format
.m_frames
= static_cast<int>(format
.m_sampleRate
* 0.02); // 20 ms chunks
712 CLog::LogF(LOGINFO
, "XAudio Sink Initialized using: {}, {}, {}",
713 CAEUtil::DataFormatToStr(format
.m_dataFormat
), wfxex
.Format
.nSamplesPerSec
,
714 wfxex
.Format
.nChannels
);
716 m_sourceVoice
->Stop();
718 CLog::LogF(LOGDEBUG
, "Initializing XAudio with the following parameters:");
719 CLog::Log(LOGDEBUG
, " Audio Device : {}", KODI::PLATFORM::WINDOWS::FromW(device
));
720 CLog::Log(LOGDEBUG
, " Sample Rate : {}", wfxex
.Format
.nSamplesPerSec
);
721 CLog::Log(LOGDEBUG
, " Sample Format : {}", CAEUtil::DataFormatToStr(format
.m_dataFormat
));
722 CLog::Log(LOGDEBUG
, " Bits Per Sample : {}", wfxex
.Format
.wBitsPerSample
);
723 CLog::Log(LOGDEBUG
, " Valid Bits/Samp : {}", wfxex
.Samples
.wValidBitsPerSample
);
724 CLog::Log(LOGDEBUG
, " Channel Count : {}", wfxex
.Format
.nChannels
);
725 CLog::Log(LOGDEBUG
, " Block Align : {}", wfxex
.Format
.nBlockAlign
);
726 CLog::Log(LOGDEBUG
, " Avg. Bytes Sec : {}", wfxex
.Format
.nAvgBytesPerSec
);
727 CLog::Log(LOGDEBUG
, " Samples/Block : {}", wfxex
.Samples
.wSamplesPerBlock
);
728 CLog::Log(LOGDEBUG
, " Format cBSize : {}", wfxex
.Format
.cbSize
);
729 CLog::Log(LOGDEBUG
, " Channel Layout : {}", ((std::string
)format
.m_channelLayout
));
730 CLog::Log(LOGDEBUG
, " Channel Mask : {}", wfxex
.dwChannelMask
);
731 CLog::Log(LOGDEBUG
, " Frames : {}", format
.m_frames
);
732 CLog::Log(LOGDEBUG
, " Frame Size : {}", format
.m_frameSize
);
737 void CAESinkXAudio::Drain()
746 // Contrary to MS doc, the voice must play a buffer with end of stream flag for the voice
747 // SamplesPlayed counter to be reset.
748 // Per MS doc, Discontinuity() may not take effect until after the entire buffer queue is
749 // consumed, which wouldn't invoke the callback or reset the voice stats.
750 // Solution: submit a 1 sample buffer with end of stream flag and wait for StreamEnd callback
751 // The StreamEnd event is manual reset so that it cannot be missed even if raised before this
752 // code starts waiting for it
754 AddEndOfStreamPacket();
756 constexpr uint32_t waitSafety
= 100; // extra ms wait in case of scheduler issue
758 WaitForSingleObject(m_voiceCallback
.m_StreamEndEvent
,
759 m_framesInBuffers
* 1000 / m_format
.m_sampleRate
+ waitSafety
);
761 if (waitRc
!= WAIT_OBJECT_0
)
763 if (waitRc
== WAIT_FAILED
)
765 //! @todo use FormatMessage for a human readable error message
767 "error WAIT_FAILED while waiting for queued buffers to drain. GetLastError:{}",
772 CLog::LogF(LOGERROR
, "error {} while waiting for queued buffers to drain.", waitRc
);
776 m_sourceVoice
->Stop();
777 ResetEvent(m_voiceCallback
.m_StreamEndEvent
);
780 m_framesInBuffers
= 0;
784 CLog::LogF(LOGERROR
, "invalidated source voice - Releasing");
790 bool CAESinkXAudio::AddEndOfStreamPacket()
792 constexpr unsigned int frames
{1};
794 XAUDIO2_BUFFER xbuffer
= BuildXAudio2Buffer(nullptr, frames
, 0);
795 xbuffer
.Flags
= XAUDIO2_END_OF_STREAM
;
797 HRESULT hr
= m_sourceVoice
->SubmitSourceBuffer(&xbuffer
);
801 CLog::LogF(LOGERROR
, "SubmitSourceBuffer failed due to {:X}", hr
);
802 delete xbuffer
.pContext
;
806 m_sinkFrames
+= frames
;
807 m_framesInBuffers
+= frames
;
811 XAUDIO2_BUFFER
CAESinkXAudio::BuildXAudio2Buffer(uint8_t** data
,
815 const unsigned int dataLength
{frames
* m_format
.m_frameSize
};
817 struct buffer_ctx
* ctx
= new buffer_ctx
;
818 ctx
->data
= new uint8_t[dataLength
];
819 ctx
->frames
= frames
;
823 memcpy(ctx
->data
, data
[0] + offset
* m_format
.m_frameSize
, dataLength
);
825 CAEUtil::GenerateSilence(m_format
.m_dataFormat
, m_format
.m_frameSize
, ctx
->data
, frames
);
827 XAUDIO2_BUFFER xbuffer
{};
828 xbuffer
.AudioBytes
= dataLength
;
829 xbuffer
.pAudioData
= ctx
->data
;
830 xbuffer
.pContext
= ctx
;