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 const wchar_t* pDevice
= device
.c_str();
506 // ForegroundOnlyMedia/BackgroundCapableMedia replaced in Windows 10 by Movie/Media
507 const AUDIO_STREAM_CATEGORY streamCategory
{
508 CSysInfo::IsWindowsVersionAtLeast(CSysInfo::WindowsVersionWin10
)
509 ? AudioCategory_Media
510 : AudioCategory_ForegroundOnlyMedia
};
514 hr
= m_xAudio2
->CreateMasteringVoice(&pMasterVoice
, wfxex
.Format
.nChannels
,
515 wfxex
.Format
.nSamplesPerSec
, 0, pDevice
, nullptr,
524 "could not locate the device named \"{}\" in the list of Xaudio endpoint devices. "
525 "Trying the default device...",
526 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
531 hr
= m_xAudio2
->CreateMasteringVoice(&pMasterVoice
, wfxex
.Format
.nChannels
,
532 wfxex
.Format
.nSamplesPerSec
, 0, pDevice
, nullptr,
534 if (FAILED(hr
) || !pMasterVoice
)
536 CLog::LogF(LOGINFO
, "Could not retrieve the default XAudio audio endpoint ({}).",
542 m_masterVoice
= pMasterVoice
;
544 int closestMatch
= 0;
545 unsigned int requestedChannels
= 0;
546 unsigned int noOfCh
= 0;
548 hr
= m_xAudio2
->CreateSourceVoice(&m_sourceVoice
, &wfxex
.Format
, 0, XAUDIO2_DEFAULT_FREQ_RATIO
, &m_voiceCallback
);
551 CLog::LogF(LOGINFO
, "Format is Supported - will attempt to Initialize");
555 if (format
.m_dataFormat
== AE_FMT_RAW
) //No sense in trying other formats for passthrough.
558 if (CServiceBroker::GetLogging().CanLogComponent(LOGAUDIO
))
559 CLog::LogFC(LOGDEBUG
, LOGAUDIO
,
560 "CreateSourceVoice failed ({}) - trying to find a compatible format",
563 requestedChannels
= wfxex
.Format
.nChannels
;
565 /* The requested format is not supported by the device. Find something that works */
566 for (int layout
= -1; layout
<= (int)ARRAYSIZE(layoutsList
); layout
++)
568 // if requested layout is not supported, try standard layouts with at least
569 // the number of channels as requested
570 // as the last resort try stereo
571 if (layout
== ARRAYSIZE(layoutsList
))
573 wfxex
.dwChannelMask
= SPEAKER_FRONT_LEFT
| SPEAKER_FRONT_RIGHT
;
574 wfxex
.Format
.nChannels
= 2;
576 else if (layout
>= 0)
578 wfxex
.dwChannelMask
= CAESinkFactoryWin::ChLayoutToChMask(layoutsList
[layout
], &noOfCh
);
579 wfxex
.Format
.nChannels
= noOfCh
;
580 if (noOfCh
< requestedChannels
)
584 for (int j
= 0; j
< sizeof(testFormats
)/sizeof(sampleFormat
); j
++)
588 wfxex
.Format
.wFormatTag
= WAVE_FORMAT_EXTENSIBLE
;
589 wfxex
.SubFormat
= testFormats
[j
].subFormat
;
590 wfxex
.Format
.wBitsPerSample
= testFormats
[j
].bitsPerSample
;
591 wfxex
.Samples
.wValidBitsPerSample
= testFormats
[j
].validBitsPerSample
;
592 wfxex
.Format
.nBlockAlign
= wfxex
.Format
.nChannels
* (wfxex
.Format
.wBitsPerSample
>> 3);
594 for (int i
= 0 ; i
< WASAPISampleRateCount
; i
++)
596 if (WASAPISampleRates
[j
] < XAUDIO2_MIN_SAMPLE_RATE
||
597 WASAPISampleRates
[j
] > XAUDIO2_MAX_SAMPLE_RATE
)
600 SafeDestroyVoice(&m_sourceVoice
);
601 SafeDestroyVoice(&m_masterVoice
);
603 wfxex
.Format
.nSamplesPerSec
= WASAPISampleRates
[i
];
604 wfxex
.Format
.nAvgBytesPerSec
= wfxex
.Format
.nSamplesPerSec
* wfxex
.Format
.nBlockAlign
;
606 hr
= m_xAudio2
->CreateMasteringVoice(&m_masterVoice
, wfxex
.Format
.nChannels
,
607 wfxex
.Format
.nSamplesPerSec
, 0, pDevice
, nullptr,
611 hr
= m_xAudio2
->CreateSourceVoice(&m_sourceVoice
, &wfxex
.Format
, 0, XAUDIO2_DEFAULT_FREQ_RATIO
, &m_voiceCallback
);
614 /* If the current sample rate matches the source then stop looking and use it */
615 if ((WASAPISampleRates
[i
] == format
.m_sampleRate
) && (testFormats
[j
].subFormatType
<= format
.m_dataFormat
))
617 /* If this rate is closer to the source then the previous one, save it */
618 else if (closestMatch
< 0 || abs((int)WASAPISampleRates
[i
] - (int)format
.m_sampleRate
) < abs((int)WASAPISampleRates
[closestMatch
] - (int)format
.m_sampleRate
))
624 CLog::LogF(LOGERROR
, "creating voices failed ({})", WASAPIErrToStr(hr
));
627 if (closestMatch
>= 0)
629 // Closest match may be different from the last successful sample rate tested
630 SafeDestroyVoice(&m_sourceVoice
);
631 SafeDestroyVoice(&m_masterVoice
);
633 wfxex
.Format
.nSamplesPerSec
= WASAPISampleRates
[closestMatch
];
634 wfxex
.Format
.nAvgBytesPerSec
= wfxex
.Format
.nSamplesPerSec
* wfxex
.Format
.nBlockAlign
;
636 if (SUCCEEDED(m_xAudio2
->CreateMasteringVoice(&m_masterVoice
, wfxex
.Format
.nChannels
,
637 wfxex
.Format
.nSamplesPerSec
, 0, pDevice
,
638 nullptr, streamCategory
)) &&
639 SUCCEEDED(m_xAudio2
->CreateSourceVoice(&m_sourceVoice
, &wfxex
.Format
, 0,
640 XAUDIO2_DEFAULT_FREQ_RATIO
, &m_voiceCallback
)))
646 CLog::LogF(LOGERROR
, "unable to locate a supported output format for the device. Check the "
647 "speaker settings in the control panel.");
649 /* We couldn't find anything supported. This should never happen */
650 /* unless the user set the wrong speaker setting in the control panel */
655 CAEChannelInfo channelLayout
;
656 CAESinkFactoryWin::AEChannelsFromSpeakerMask(channelLayout
, wfxex
.dwChannelMask
);
657 format
.m_channelLayout
= channelLayout
;
659 /* Set up returned sink format for engine */
660 if (format
.m_dataFormat
!= AE_FMT_RAW
)
662 if (wfxex
.Format
.wBitsPerSample
== 32)
664 if (wfxex
.SubFormat
== KSDATAFORMAT_SUBTYPE_IEEE_FLOAT
)
665 format
.m_dataFormat
= AE_FMT_FLOAT
;
666 else if (wfxex
.Samples
.wValidBitsPerSample
== 32)
667 format
.m_dataFormat
= AE_FMT_S32NE
;
669 format
.m_dataFormat
= AE_FMT_S24NE4MSB
;
671 else if (wfxex
.Format
.wBitsPerSample
== 24)
672 format
.m_dataFormat
= AE_FMT_S24NE3
;
674 format
.m_dataFormat
= AE_FMT_S16NE
;
677 format
.m_sampleRate
= wfxex
.Format
.nSamplesPerSec
; //PCM: Sample rate. RAW: Link speed
678 format
.m_frameSize
= (wfxex
.Format
.wBitsPerSample
>> 3) * wfxex
.Format
.nChannels
;
680 if (format
.m_dataFormat
== AE_FMT_RAW
)
681 format
.m_dataFormat
= AE_FMT_S16NE
;
683 hr
= m_sourceVoice
->Start(0, XAUDIO2_COMMIT_NOW
);
686 CLog::LogF(LOGERROR
, "Voice start failed : {}", WASAPIErrToStr(hr
));
687 CLog::Log(LOGDEBUG
, " Sample Rate : {}", wfxex
.Format
.nSamplesPerSec
);
688 CLog::Log(LOGDEBUG
, " Sample Format : {}", CAEUtil::DataFormatToStr(format
.m_dataFormat
));
689 CLog::Log(LOGDEBUG
, " Bits Per Sample : {}", wfxex
.Format
.wBitsPerSample
);
690 CLog::Log(LOGDEBUG
, " Valid Bits/Samp : {}", wfxex
.Samples
.wValidBitsPerSample
);
691 CLog::Log(LOGDEBUG
, " Channel Count : {}", wfxex
.Format
.nChannels
);
692 CLog::Log(LOGDEBUG
, " Block Align : {}", wfxex
.Format
.nBlockAlign
);
693 CLog::Log(LOGDEBUG
, " Avg. Bytes Sec : {}", wfxex
.Format
.nAvgBytesPerSec
);
694 CLog::Log(LOGDEBUG
, " Samples/Block : {}", wfxex
.Samples
.wSamplesPerBlock
);
695 CLog::Log(LOGDEBUG
, " Format cBSize : {}", wfxex
.Format
.cbSize
);
696 CLog::Log(LOGDEBUG
, " Channel Layout : {}", ((std::string
)format
.m_channelLayout
));
697 CLog::Log(LOGDEBUG
, " Channel Mask : {}", wfxex
.dwChannelMask
);
701 XAUDIO2_PERFORMANCE_DATA perfData
;
702 m_xAudio2
->GetPerformanceData(&perfData
);
703 if (!perfData
.TotalSourceVoiceCount
)
705 CLog::LogF(LOGERROR
, "GetPerformanceData Failed : {}", WASAPIErrToStr(hr
));
709 format
.m_frames
= static_cast<int>(format
.m_sampleRate
* 0.02); // 20 ms chunks
713 CLog::LogF(LOGINFO
, "XAudio Sink Initialized using: {}, {}, {}",
714 CAEUtil::DataFormatToStr(format
.m_dataFormat
), wfxex
.Format
.nSamplesPerSec
,
715 wfxex
.Format
.nChannels
);
717 m_sourceVoice
->Stop();
719 CLog::LogF(LOGDEBUG
, "Initializing XAudio with the following parameters:");
720 CLog::Log(LOGDEBUG
, " Audio Device : {}", KODI::PLATFORM::WINDOWS::FromW(device
));
721 CLog::Log(LOGDEBUG
, " Sample Rate : {}", wfxex
.Format
.nSamplesPerSec
);
722 CLog::Log(LOGDEBUG
, " Sample Format : {}", CAEUtil::DataFormatToStr(format
.m_dataFormat
));
723 CLog::Log(LOGDEBUG
, " Bits Per Sample : {}", wfxex
.Format
.wBitsPerSample
);
724 CLog::Log(LOGDEBUG
, " Valid Bits/Samp : {}", wfxex
.Samples
.wValidBitsPerSample
);
725 CLog::Log(LOGDEBUG
, " Channel Count : {}", wfxex
.Format
.nChannels
);
726 CLog::Log(LOGDEBUG
, " Block Align : {}", wfxex
.Format
.nBlockAlign
);
727 CLog::Log(LOGDEBUG
, " Avg. Bytes Sec : {}", wfxex
.Format
.nAvgBytesPerSec
);
728 CLog::Log(LOGDEBUG
, " Samples/Block : {}", wfxex
.Samples
.wSamplesPerBlock
);
729 CLog::Log(LOGDEBUG
, " Format cBSize : {}", wfxex
.Format
.cbSize
);
730 CLog::Log(LOGDEBUG
, " Channel Layout : {}", ((std::string
)format
.m_channelLayout
));
731 CLog::Log(LOGDEBUG
, " Channel Mask : {}", wfxex
.dwChannelMask
);
732 CLog::Log(LOGDEBUG
, " Frames : {}", format
.m_frames
);
733 CLog::Log(LOGDEBUG
, " Frame Size : {}", format
.m_frameSize
);
738 void CAESinkXAudio::Drain()
747 // Contrary to MS doc, the voice must play a buffer with end of stream flag for the voice
748 // SamplesPlayed counter to be reset.
749 // Per MS doc, Discontinuity() may not take effect until after the entire buffer queue is
750 // consumed, which wouldn't invoke the callback or reset the voice stats.
751 // Solution: submit a 1 sample buffer with end of stream flag and wait for StreamEnd callback
752 // The StreamEnd event is manual reset so that it cannot be missed even if raised before this
753 // code starts waiting for it
755 AddEndOfStreamPacket();
757 constexpr uint32_t waitSafety
= 100; // extra ms wait in case of scheduler issue
759 WaitForSingleObject(m_voiceCallback
.m_StreamEndEvent
,
760 m_framesInBuffers
* 1000 / m_format
.m_sampleRate
+ waitSafety
);
762 if (waitRc
!= WAIT_OBJECT_0
)
764 if (waitRc
== WAIT_FAILED
)
766 //! @todo use FormatMessage for a human readable error message
768 "error WAIT_FAILED while waiting for queued buffers to drain. GetLastError:{}",
773 CLog::LogF(LOGERROR
, "error {} while waiting for queued buffers to drain.", waitRc
);
777 m_sourceVoice
->Stop();
778 ResetEvent(m_voiceCallback
.m_StreamEndEvent
);
781 m_framesInBuffers
= 0;
785 CLog::LogF(LOGERROR
, "invalidated source voice - Releasing");
791 bool CAESinkXAudio::AddEndOfStreamPacket()
793 constexpr unsigned int frames
{1};
795 XAUDIO2_BUFFER xbuffer
= BuildXAudio2Buffer(nullptr, frames
, 0);
796 xbuffer
.Flags
= XAUDIO2_END_OF_STREAM
;
798 HRESULT hr
= m_sourceVoice
->SubmitSourceBuffer(&xbuffer
);
802 CLog::LogF(LOGERROR
, "SubmitSourceBuffer failed due to {:X}", hr
);
803 delete xbuffer
.pContext
;
807 m_sinkFrames
+= frames
;
808 m_framesInBuffers
+= frames
;
812 XAUDIO2_BUFFER
CAESinkXAudio::BuildXAudio2Buffer(uint8_t** data
,
816 const unsigned int dataLength
{frames
* m_format
.m_frameSize
};
818 struct buffer_ctx
* ctx
= new buffer_ctx
;
819 ctx
->data
= new uint8_t[dataLength
];
820 ctx
->frames
= frames
;
824 memcpy(ctx
->data
, data
[0] + offset
* m_format
.m_frameSize
, dataLength
);
826 CAEUtil::GenerateSilence(m_format
.m_dataFormat
, m_format
.m_frameSize
, ctx
->data
, frames
);
828 XAUDIO2_BUFFER xbuffer
{};
829 xbuffer
.AudioBytes
= dataLength
;
830 xbuffer
.pAudioData
= ctx
->data
;
831 xbuffer
.pContext
= ctx
;