Merge pull request #26350 from jjd-uk/estuary_media_align
[xbmc.git] / xbmc / cores / AudioEngine / Sinks / AESinkDirectSound.cpp
blob98c99a9ebf947ff8acc00bbd197e122ef8943086
1 /*
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.
7 */
9 #define INITGUID
12 #include "AESinkDirectSound.h"
14 #include "cores/AudioEngine/AESinkFactory.h"
15 #include "cores/AudioEngine/Sinks/windows/AESinkFactoryWin.h"
16 #include "cores/AudioEngine/Utils/AEUtil.h"
17 #include "utils/StringUtils.h"
18 #include "utils/XTimeUtils.h"
19 #include "utils/log.h"
21 #include "platform/win32/CharsetConverter.h"
22 #include "platform/win32/WIN32Util.h"
24 #include <algorithm>
25 #include <mutex>
27 #include <Mmreg.h>
28 #include <initguid.h>
30 // include order is important here
31 // clang-format off
32 #include <mmdeviceapi.h>
33 #include <Functiondiscoverykeys_devpkey.h>
34 // clang-format on
36 extern HWND g_hWnd;
38 DEFINE_GUID( _KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, WAVE_FORMAT_IEEE_FLOAT, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 );
39 DEFINE_GUID( _KSDATAFORMAT_SUBTYPE_DOLBY_AC3_SPDIF, WAVE_FORMAT_DOLBY_AC3_SPDIF, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 );
41 #define EXIT_ON_FAILURE(hr, reason) \
42 if (FAILED(hr)) \
43 { \
44 CLog::LogF(LOGERROR, reason " - error {}", hr, CWIN32Util::FormatHRESULT(hr)); \
45 goto failed; \
48 namespace
50 constexpr unsigned int DS_SPEAKER_COUNT = 8U;
51 constexpr unsigned int DSChannelOrder[] = {
52 SPEAKER_FRONT_LEFT, SPEAKER_FRONT_RIGHT, SPEAKER_FRONT_CENTER, SPEAKER_LOW_FREQUENCY,
53 SPEAKER_BACK_LEFT, SPEAKER_BACK_RIGHT, SPEAKER_SIDE_LEFT, SPEAKER_SIDE_RIGHT};
54 constexpr enum AEChannel AEChannelNamesDS[] = {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_BL,
55 AE_CH_BR, AE_CH_SL, AE_CH_SR, AE_CH_NULL};
56 } // namespace
58 using namespace Microsoft::WRL;
60 CAESinkDirectSound::CAESinkDirectSound() :
61 m_pBuffer (nullptr),
62 m_pDSound (nullptr),
63 m_encodedFormat (AE_FMT_INVALID),
64 m_AvgBytesPerSec(0 ),
65 m_dwChunkSize (0 ),
66 m_dwFrameSize (0 ),
67 m_dwBufferLen (0 ),
68 m_BufferOffset (0 ),
69 m_CacheLen (0 ),
70 m_BufferTimeouts(0 ),
71 m_running (false),
72 m_initialized (false),
73 m_isDirtyDS (false)
75 m_channelLayout.Reset();
78 CAESinkDirectSound::~CAESinkDirectSound()
80 Deinitialize();
83 void CAESinkDirectSound::Register()
85 AE::AESinkRegEntry reg;
86 reg.sinkName = "DIRECTSOUND";
87 reg.createFunc = CAESinkDirectSound::Create;
88 reg.enumerateFunc = CAESinkDirectSound::EnumerateDevicesEx;
89 AE::CAESinkFactory::RegisterSink(reg);
92 std::unique_ptr<IAESink> CAESinkDirectSound::Create(std::string& device,
93 AEAudioFormat& desiredFormat)
95 auto sink = std::make_unique<CAESinkDirectSound>();
96 if (sink->Initialize(desiredFormat, device))
97 return sink;
99 return {};
102 bool CAESinkDirectSound::Initialize(AEAudioFormat& format, std::string& device)
104 if (m_initialized)
105 return false;
107 bool deviceIsBluetooth = false;
108 GUID deviceGUID = {};
109 HRESULT hr = E_FAIL;
110 std::string strDeviceGUID = device;
111 std::string deviceFriendlyName = "unknown";
113 if (StringUtils::EndsWithNoCase(device, "default"))
114 strDeviceGUID = CAESinkFactoryWin::GetDefaultDeviceId();
116 hr = CLSIDFromString(KODI::PLATFORM::WINDOWS::ToW(strDeviceGUID).c_str(), &deviceGUID);
117 if (FAILED(hr))
118 CLog::LogF(LOGERROR, "Failed to convert the device '{}' to a GUID, with error {}.",
119 strDeviceGUID, CWIN32Util::FormatHRESULT(hr));
121 for (const auto& detail : CAESinkFactoryWin::GetRendererDetails())
123 if (StringUtils::CompareNoCase(detail.strDeviceId, strDeviceGUID) != 0)
124 continue;
126 if (detail.strDeviceEnumerator == "BTHENUM")
128 deviceIsBluetooth = true;
129 CLog::LogF(LOGDEBUG, "Audio device '{}' is detected as Bluetooth device", deviceFriendlyName);
132 deviceFriendlyName = detail.strDescription;
134 break;
137 hr = DirectSoundCreate(&deviceGUID, m_pDSound.ReleaseAndGetAddressOf(), nullptr);
139 if (FAILED(hr))
141 CLog::LogF(
142 LOGERROR,
143 "Failed to create the DirectSound device {} with error {}, trying the default device.",
144 deviceFriendlyName, CWIN32Util::FormatHRESULT(hr));
146 deviceFriendlyName = "default";
148 hr = DirectSoundCreate(nullptr, m_pDSound.ReleaseAndGetAddressOf(), nullptr);
149 if (FAILED(hr))
151 CLog::LogF(LOGERROR, "Failed to create the default DirectSound device with error {}.",
152 CWIN32Util::FormatHRESULT(hr));
153 return false;
157 /* Dodge the null handle on first init by using desktop handle */
158 HWND tmp_hWnd = g_hWnd == nullptr ? GetDesktopWindow() : g_hWnd;
159 CLog::LogF(LOGDEBUG, "Using Window handle: {}", fmt::ptr(tmp_hWnd));
161 hr = m_pDSound->SetCooperativeLevel(tmp_hWnd, DSSCL_PRIORITY);
163 if (FAILED(hr))
165 CLog::LogF(LOGERROR, "Failed to create the DirectSound device cooperative level with error {}.",
166 CWIN32Util::FormatHRESULT(hr));
167 m_pDSound = nullptr;
168 return false;
171 // clamp samplerate between 44100 and 192000
172 if (format.m_sampleRate < 44100)
173 format.m_sampleRate = 44100;
175 if (format.m_sampleRate > 192000)
176 format.m_sampleRate = 192000;
178 // fill waveformatex
179 WAVEFORMATEXTENSIBLE wfxex = {};
180 wfxex.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE)-sizeof(WAVEFORMATEX);
181 wfxex.Format.nChannels = format.m_channelLayout.Count();
182 wfxex.Format.nSamplesPerSec = format.m_sampleRate;
183 if (format.m_dataFormat == AE_FMT_RAW)
185 wfxex.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
186 wfxex.Format.wFormatTag = WAVE_FORMAT_DOLBY_AC3_SPDIF;
187 wfxex.SubFormat = _KSDATAFORMAT_SUBTYPE_DOLBY_AC3_SPDIF;
188 wfxex.Format.wBitsPerSample = 16;
189 wfxex.Format.nChannels = 2;
191 else
193 wfxex.dwChannelMask = SpeakerMaskFromAEChannels(format.m_channelLayout);
194 wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
195 wfxex.SubFormat = _KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
196 wfxex.Format.wBitsPerSample = 32;
199 wfxex.Samples.wValidBitsPerSample = wfxex.Format.wBitsPerSample;
200 wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
201 wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
203 m_AvgBytesPerSec = wfxex.Format.nAvgBytesPerSec;
205 const unsigned int chunkDurationMs = deviceIsBluetooth ? 50 : 20;
206 const unsigned int uiFrameCount = format.m_sampleRate * chunkDurationMs / 1000U;
207 m_dwFrameSize = wfxex.Format.nBlockAlign;
208 m_dwChunkSize = m_dwFrameSize * uiFrameCount;
210 // BT: 500ms total buffer (50 * 10); non-BT: 200ms total buffer (20 * 10)
211 m_dwBufferLen = m_dwChunkSize * 10;
213 // fill in the secondary sound buffer descriptor
214 DSBUFFERDESC dsbdesc = {};
215 dsbdesc.dwSize = sizeof(DSBUFFERDESC);
216 dsbdesc.dwFlags = DSBCAPS_GETCURRENTPOSITION2 /** Better position accuracy */
217 | DSBCAPS_TRUEPLAYPOSITION /** Vista+ accurate position */
218 | DSBCAPS_GLOBALFOCUS; /** Allows background playing */
220 dsbdesc.dwBufferBytes = m_dwBufferLen;
221 dsbdesc.lpwfxFormat = (WAVEFORMATEX *)&wfxex;
223 // now create the stream buffer
224 HRESULT res = m_pDSound->CreateSoundBuffer(&dsbdesc, m_pBuffer.ReleaseAndGetAddressOf(), nullptr);
225 if (res != DS_OK)
227 if (dsbdesc.dwFlags & DSBCAPS_LOCHARDWARE)
229 CLog::LogF(LOGDEBUG, "Couldn't create secondary buffer ({}). Trying without LOCHARDWARE.",
230 CWIN32Util::FormatHRESULT(res));
231 // Try without DSBCAPS_LOCHARDWARE
232 dsbdesc.dwFlags &= ~DSBCAPS_LOCHARDWARE;
233 res = m_pDSound->CreateSoundBuffer(&dsbdesc, m_pBuffer.ReleaseAndGetAddressOf(), nullptr);
235 if (res != DS_OK)
237 m_pBuffer = nullptr;
238 CLog::LogF(LOGERROR, "cannot create secondary buffer ({})", CWIN32Util::FormatHRESULT(res));
239 return false;
242 CLog::LogF(LOGDEBUG, "secondary buffer created");
244 m_pBuffer->Stop();
246 AEChannelsFromSpeakerMask(wfxex.dwChannelMask);
247 format.m_channelLayout = m_channelLayout;
248 m_encodedFormat = format.m_dataFormat;
249 format.m_frames = uiFrameCount;
250 format.m_frameSize = ((format.m_dataFormat == AE_FMT_RAW) ? (wfxex.Format.wBitsPerSample >> 3) : sizeof(float)) * format.m_channelLayout.Count();
251 format.m_dataFormat = (format.m_dataFormat == AE_FMT_RAW) ? AE_FMT_S16NE : AE_FMT_FLOAT;
253 m_format = format;
254 m_device = device;
256 m_BufferOffset = 0;
257 m_CacheLen = 0;
258 m_initialized = true;
259 m_isDirtyDS = false;
261 CLog::LogF(LOGDEBUG, "Initializing DirectSound with the following parameters:");
262 CLog::Log(LOGDEBUG, " Audio Device : {}", ((std::string)deviceFriendlyName));
263 CLog::Log(LOGDEBUG, " Sample Rate : {}", wfxex.Format.nSamplesPerSec);
264 CLog::Log(LOGDEBUG, " Sample Format : {}", CAEUtil::DataFormatToStr(format.m_dataFormat));
265 CLog::Log(LOGDEBUG, " Bits Per Sample : {}", wfxex.Format.wBitsPerSample);
266 CLog::Log(LOGDEBUG, " Valid Bits/Samp : {}", wfxex.Samples.wValidBitsPerSample);
267 CLog::Log(LOGDEBUG, " Channel Count : {}", wfxex.Format.nChannels);
268 CLog::Log(LOGDEBUG, " Block Align : {}", wfxex.Format.nBlockAlign);
269 CLog::Log(LOGDEBUG, " Avg. Bytes Sec : {}", wfxex.Format.nAvgBytesPerSec);
270 CLog::Log(LOGDEBUG, " Samples/Block : {}", wfxex.Samples.wSamplesPerBlock);
271 CLog::Log(LOGDEBUG, " Format cBSize : {}", wfxex.Format.cbSize);
272 CLog::Log(LOGDEBUG, " Channel Layout : {}", ((std::string)format.m_channelLayout));
273 CLog::Log(LOGDEBUG, " Channel Mask : {}", wfxex.dwChannelMask);
274 CLog::Log(LOGDEBUG, " Frames : {}", format.m_frames);
275 CLog::Log(LOGDEBUG, " Frame Size : {}", format.m_frameSize);
277 return true;
280 void CAESinkDirectSound::Deinitialize()
282 if (!m_initialized)
283 return;
285 CLog::LogF(LOGDEBUG, "Cleaning up");
287 if (m_pBuffer)
289 m_pBuffer->Stop();
292 m_initialized = false;
293 m_pBuffer = nullptr;
294 m_pDSound = nullptr;
295 m_BufferOffset = 0;
296 m_CacheLen = 0;
297 m_dwChunkSize = 0;
298 m_dwBufferLen = 0;
301 unsigned int CAESinkDirectSound::AddPackets(uint8_t **data, unsigned int frames, unsigned int offset)
303 if (!m_initialized)
304 return 0;
306 DWORD total = m_dwFrameSize * frames;
307 DWORD len = total;
308 unsigned char* pBuffer = (unsigned char*)data[0]+offset*m_format.m_frameSize;
310 DWORD bufferStatus = 0;
311 if (m_pBuffer->GetStatus(&bufferStatus) != DS_OK)
313 CLog::LogF(LOGERROR, "GetStatus() failed");
314 return 0;
316 if (bufferStatus & DSBSTATUS_BUFFERLOST)
318 CLog::LogF(LOGDEBUG, "Buffer allocation was lost. Restoring buffer.");
319 m_pBuffer->Restore();
322 while (GetSpace() < total)
324 if(m_isDirtyDS)
325 return INT_MAX;
326 else
328 KODI::TIME::Sleep(
329 std::chrono::milliseconds(static_cast<int>(total * 1000 / m_AvgBytesPerSec)));
333 while (len)
335 void* start = nullptr, *startWrap = nullptr;
336 DWORD size = 0, sizeWrap = 0;
337 if (m_BufferOffset >= m_dwBufferLen) // Wrap-around manually
338 m_BufferOffset = 0;
339 DWORD dwWriteBytes = std::min((int)m_dwChunkSize, (int)len);
340 HRESULT res = m_pBuffer->Lock(m_BufferOffset, dwWriteBytes, &start, &size, &startWrap, &sizeWrap, 0);
341 if (DS_OK != res)
343 CLog::LogF(LOGERROR, "Unable to lock buffer at offset {}. HRESULT: {:#08x}", m_BufferOffset,
344 res);
345 m_isDirtyDS = true;
346 return INT_MAX;
349 memcpy(start, pBuffer, size);
351 pBuffer += size;
352 len -= size;
354 m_BufferOffset += size;
355 if (startWrap) // Write-region wraps to beginning of buffer
357 memcpy(startWrap, pBuffer, sizeWrap);
358 m_BufferOffset = sizeWrap;
360 pBuffer += sizeWrap;
361 len -= sizeWrap;
364 m_CacheLen += size + sizeWrap; // This data is now in the cache
365 m_pBuffer->Unlock(start, size, startWrap, sizeWrap);
368 CheckPlayStatus();
370 return (total - len) / m_dwFrameSize; // Frames used
373 void CAESinkDirectSound::Stop()
375 if (m_pBuffer)
376 m_pBuffer->Stop();
379 void CAESinkDirectSound::Drain()
381 if (!m_initialized || m_isDirtyDS)
382 return;
384 m_pBuffer->Stop();
385 HRESULT res = m_pBuffer->SetCurrentPosition(0);
386 if (DS_OK != res)
388 CLog::LogF(LOGERROR,
389 "SetCurrentPosition failed. Unable to determine buffer status. HRESULT = {:#08x}",
390 res);
391 m_isDirtyDS = true;
392 return;
394 m_BufferOffset = 0;
395 UpdateCacheStatus();
398 void CAESinkDirectSound::GetDelay(AEDelayStatus& status)
400 if (!m_initialized)
402 status.SetDelay(0);
403 return;
406 /* Make sure we know how much data is in the cache */
407 if (!UpdateCacheStatus())
408 m_isDirtyDS = true;
410 /** returns current cached data duration in seconds */
411 status.SetDelay((double)m_CacheLen / (double)m_AvgBytesPerSec);
414 double CAESinkDirectSound::GetCacheTotal()
416 /** returns total cache capacity in seconds */
417 return (double)m_dwBufferLen / (double)m_AvgBytesPerSec;
420 void CAESinkDirectSound::EnumerateDevicesEx(AEDeviceInfoList &deviceInfoList, bool force)
422 CAEDeviceInfo deviceInfo{};
424 for (RendererDetail& detail : CAESinkFactoryWin::GetRendererDetails())
426 deviceInfo.m_channels.Reset();
427 deviceInfo.m_dataFormats.clear();
428 deviceInfo.m_sampleRates.clear();
429 deviceInfo.m_streamTypes.clear();
431 if (detail.nChannels)
432 deviceInfo.m_channels =
433 layoutsByChCount[std::max(std::min(detail.nChannels, DS_SPEAKER_COUNT), 2U)];
434 deviceInfo.m_dataFormats.push_back(AEDataFormat(AE_FMT_FLOAT));
436 if (detail.eDeviceType != AE_DEVTYPE_PCM)
438 deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
439 // DTS is played with the same infrastructure as AC3
440 deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
441 deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
442 deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
443 deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
444 // signal that we can doe AE_FMT_RAW
445 deviceInfo.m_dataFormats.push_back(AE_FMT_RAW);
448 if (detail.m_samplesPerSec)
449 deviceInfo.m_sampleRates.push_back(std::min(detail.m_samplesPerSec, 192000UL));
450 deviceInfo.m_deviceName = detail.strDeviceId;
451 deviceInfo.m_displayName = detail.strWinDevType.append(detail.strDescription);
452 deviceInfo.m_displayNameExtra = std::string("DIRECTSOUND: ").append(detail.strDescription);
453 deviceInfo.m_deviceType = detail.eDeviceType;
454 deviceInfo.m_wantsIECPassthrough = true;
455 deviceInfoList.push_back(deviceInfo);
457 // add the default device with m_deviceName = default
458 if (detail.bDefault)
460 deviceInfo.m_deviceName = std::string("default");
461 deviceInfo.m_displayName = std::string("default");
462 deviceInfo.m_displayNameExtra = std::string("");
463 deviceInfo.m_wantsIECPassthrough = true;
464 deviceInfoList.push_back(deviceInfo);
469 ///////////////////////////////////////////////////////////////////////////////
471 void CAESinkDirectSound::CheckPlayStatus()
473 DWORD status = 0;
474 if (m_pBuffer->GetStatus(&status) != DS_OK)
476 CLog::LogF(LOGERROR, "GetStatus() failed");
477 return;
480 if (!(status & DSBSTATUS_PLAYING) &&
481 m_CacheLen != 0) // If we have some data, see if we can start playback
483 HRESULT hr = m_pBuffer->Play(0, 0, DSBPLAY_LOOPING);
484 CLog::LogF(LOGDEBUG, "Resuming Playback");
485 if (FAILED(hr))
486 CLog::LogF(LOGERROR, "Failed to play the DirectSound buffer: {}",
487 CWIN32Util::FormatHRESULT(hr));
491 bool CAESinkDirectSound::UpdateCacheStatus()
493 std::unique_lock<CCriticalSection> lock(m_runLock);
495 DWORD playCursor = 0, writeCursor = 0;
496 HRESULT res = m_pBuffer->GetCurrentPosition(&playCursor, &writeCursor); // Get the current playback and safe write positions
497 if (DS_OK != res)
499 CLog::LogF(LOGERROR, "GetCurrentPosition failed. Unable to determine buffer status ({})",
500 CWIN32Util::FormatHRESULT(res));
501 m_isDirtyDS = true;
502 return false;
505 // Check the state of the ring buffer (P->O->W == underrun)
506 // These are the logical situations that can occur
507 // O: CurrentOffset W: WriteCursor P: PlayCursor
508 // | | | | | | | | | |
509 // ***O----W----P***** < underrun P > W && O < W (1)
510 // | | | | | | | | | |
511 // ---P****O----W----- < underrun O > P && O < W (2)
512 // | | | | | | | | | |
513 // ---W----P****O----- < underrun P > W && P < O (3)
514 // | | | | | | | | | |
515 // ***W****O----P***** P > W && P > O (4)
516 // | | | | | | | | | |
517 // ---P****W****O----- P < W && O > W (5)
518 // | | | | | | | | | |
519 // ***O----P****W***** P < W && O < P (6)
521 // Check for underruns
522 if ((playCursor > writeCursor && m_BufferOffset < writeCursor) || // (1)
523 (playCursor < m_BufferOffset && m_BufferOffset < writeCursor) || // (2)
524 (playCursor > writeCursor && playCursor < m_BufferOffset)) // (3)
526 CLog::Log(LOGWARNING, "CWin32DirectSound::GetSpace - buffer underrun - W:{}, P:{}, O:{}.",
527 writeCursor, playCursor, m_BufferOffset);
528 m_BufferOffset = writeCursor; // Catch up
529 //m_pBuffer->Stop(); // Wait until someone gives us some data to restart playback (prevents glitches)
530 m_BufferTimeouts++;
531 if (m_BufferTimeouts > 10)
533 m_isDirtyDS = true;
534 return false;
537 else
538 m_BufferTimeouts = 0;
540 // Calculate available space in the ring buffer
541 if (playCursor == m_BufferOffset && m_BufferOffset == writeCursor) // Playback is stopped and we are all at the same place
542 m_CacheLen = 0;
543 else if (m_BufferOffset > playCursor)
544 m_CacheLen = m_BufferOffset - playCursor;
545 else
546 m_CacheLen = m_dwBufferLen - (playCursor - m_BufferOffset);
548 return true;
551 unsigned int CAESinkDirectSound::GetSpace()
553 std::unique_lock<CCriticalSection> lock(m_runLock);
554 if (!UpdateCacheStatus())
555 m_isDirtyDS = true;
556 unsigned int space = m_dwBufferLen - m_CacheLen;
558 // We can never allow the internal buffers to fill up complete
559 // as we get confused between if the buffer is full or empty
560 // so never allow the last chunk to be added
561 if (space > m_dwChunkSize)
562 return space - m_dwChunkSize;
563 else
564 return 0;
567 void CAESinkDirectSound::AEChannelsFromSpeakerMask(DWORD speakers)
569 m_channelLayout.Reset();
571 for (int i = 0; i < DS_SPEAKER_COUNT; i++)
573 if (speakers & DSChannelOrder[i])
574 m_channelLayout += AEChannelNamesDS[i];
578 DWORD CAESinkDirectSound::SpeakerMaskFromAEChannels(const CAEChannelInfo &channels)
580 DWORD mask = 0;
582 for (unsigned int i = 0; i < channels.Count(); i++)
584 for (unsigned int j = 0; j < DS_SPEAKER_COUNT; j++)
585 if (channels[i] == AEChannelNamesDS[j])
586 mask |= DSChannelOrder[j];
589 return mask;