From 5f2d0456bcea7a12a5386786348784b76d6c0a81 Mon Sep 17 00:00:00 2001 From: jpark37 Date: Sat, 25 Sep 2021 11:23:40 -0700 Subject: [PATCH 01/16] win-wasapi: Mark GetWASAPIAudioDevices_ as static --- plugins/win-wasapi/enum-wasapi.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/win-wasapi/enum-wasapi.cpp b/plugins/win-wasapi/enum-wasapi.cpp index 8b3a062cb..d65f3a2aa 100644 --- a/plugins/win-wasapi/enum-wasapi.cpp +++ b/plugins/win-wasapi/enum-wasapi.cpp @@ -36,7 +36,7 @@ string GetDeviceName(IMMDevice *device) return device_name; } -void GetWASAPIAudioDevices_(vector &devices, bool input) +static void GetWASAPIAudioDevices_(vector &devices, bool input) { ComPtr enumerator; ComPtr collection; From 8f33e01b84f15f2bd515d23af9096c915b437200 Mon Sep 17 00:00:00 2001 From: jpark37 Date: Sat, 25 Sep 2021 11:33:48 -0700 Subject: [PATCH 02/16] win-wasapi: Fix incorrect log strings --- plugins/win-wasapi/win-wasapi.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index 0dfd9cdd5..44b7fbd76 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -155,9 +155,7 @@ WASAPISource::WASAPISource(obs_data_t *settings, obs_source_t *source_, inline void WASAPISource::Start() { if (!TryInitialize()) { - blog(LOG_INFO, - "[WASAPISource::WASAPISource] " - "Device '%s' not found. Waiting for device", + blog(LOG_INFO, "WASAPI: Device '%s' failed to start", device_id.c_str()); Reconnect(); } @@ -259,7 +257,7 @@ void WASAPISource::InitClient() res = client->Initialize(AUDCLNT_SHAREMODE_SHARED, flags, BUFFER_TIME_100NS, 0, wfex, nullptr); if (FAILED(res)) - throw HRError("Failed to get initialize audio client", res); + throw HRError("Failed to initialize audio client", res); } void WASAPISource::InitRender() @@ -282,7 +280,7 @@ void WASAPISource::InitRender() res = client->Initialize(AUDCLNT_SHAREMODE_SHARED, 0, BUFFER_TIME_100NS, 0, wfex, nullptr); if (FAILED(res)) - throw HRError("Failed to get initialize audio client", res); + throw HRError("Failed to initialize audio client", res); /* Silent loopback fix. Prevents audio stream from stopping and */ /* messing up timestamps and other weird glitches during silence */ @@ -499,7 +497,7 @@ bool WASAPISource::ProcessCaptureData() if (FAILED(res)) { if (res != AUDCLNT_E_DEVICE_INVALIDATED) blog(LOG_WARNING, - "[WASAPISource::GetCaptureData]" + "[WASAPISource::ProcessCaptureData]" " capture->GetNextPacketSize" " failed: %lX", res); @@ -513,7 +511,7 @@ bool WASAPISource::ProcessCaptureData() if (FAILED(res)) { if (res != AUDCLNT_E_DEVICE_INVALIDATED) blog(LOG_WARNING, - "[WASAPISource::GetCaptureData]" + "[WASAPISource::ProcessCaptureData]" " capture->GetBuffer" " failed: %lX", res); From 12442c3e57afc9ef05af65e808b8cd995c1db60f Mon Sep 17 00:00:00 2001 From: jpark37 Date: Sat, 25 Sep 2021 11:47:24 -0700 Subject: [PATCH 03/16] win-wasapi: Make InitDevice throw to log errors --- plugins/win-wasapi/win-wasapi.cpp | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index 44b7fbd76..c7303dc64 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -63,7 +63,7 @@ class WASAPISource { inline void Stop(); void Reconnect(); - bool InitDevice(); + void InitDevice(); void InitName(); void InitClient(); void InitRender(); @@ -204,32 +204,35 @@ void WASAPISource::Update(obs_data_t *settings) Start(); } -bool WASAPISource::InitDevice() +void WASAPISource::InitDevice() { - HRESULT res; - if (isDefaultDevice) { - res = enumerator->GetDefaultAudioEndpoint( + HRESULT res = enumerator->GetDefaultAudioEndpoint( isInputDevice ? eCapture : eRender, isInputDevice ? eCommunications : eConsole, device.Assign()); if (FAILED(res)) - return false; + throw HRError("Failed GetDefaultAudioEndpoint", res); CoTaskMemPtr id; res = device->GetId(&id); + if (FAILED(res)) + throw HRError("Failed to get default id", res); default_id = id; - } else { wchar_t *w_id; os_utf8_to_wcs_ptr(device_id.c_str(), device_id.size(), &w_id); + if (!w_id) + throw "Failed to widen device id string"; - res = enumerator->GetDevice(w_id, device.Assign()); + const HRESULT res = + enumerator->GetDevice(w_id, device.Assign()); bfree(w_id); - } - return SUCCEEDED(res); + if (FAILED(res)) + throw HRError("Failed to enumerate device", res); + } } #define BUFFER_TIME_100NS (5 * 10000000) @@ -370,8 +373,7 @@ void WASAPISource::Initialize() if (FAILED(res)) throw HRError("Failed to create enumerator", res); - if (!InitDevice()) - return; + InitDevice(); device_name = GetDeviceName(device); From 15f9d37aefe3f5a99218b787f6f089a841ff6d2b Mon Sep 17 00:00:00 2001 From: jpark37 Date: Sat, 25 Sep 2021 11:51:14 -0700 Subject: [PATCH 04/16] win-wasapi: Persist objects beyond Start/Stop Initialize IMMNotificationClient and IMMDeviceEnumerator in constructor to simplify cleanup and initializaiton. Both can survive a reset. --- plugins/win-wasapi/win-wasapi.cpp | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index c7303dc64..7cf703d4e 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -23,12 +23,12 @@ static void GetWASAPIDefaults(obs_data_t *settings); (KSAUDIO_SPEAKER_SURROUND | SPEAKER_LOW_FREQUENCY) class WASAPISource { + ComPtr notify; + ComPtr enumerator; ComPtr device; ComPtr client; ComPtr capture; ComPtr render; - ComPtr enumerator; - ComPtr notify; obs_source_t *source; wstring default_id; @@ -149,6 +149,20 @@ WASAPISource::WASAPISource(obs_data_t *settings, obs_source_t *source_, if (!receiveSignal.Valid()) throw "Could not create receive signal"; + notify = new WASAPINotify(this); + if (!notify) + throw "Could not create WASAPINotify"; + + HRESULT res = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, + CLSCTX_ALL, + IID_PPV_ARGS(enumerator.Assign())); + if (FAILED(res)) + throw HRError("Failed to create enumerator", res); + + res = enumerator->RegisterEndpointNotificationCallback(notify); + if (FAILED(res)) + throw HRError("Failed to register endpoint callback", res); + Start(); } @@ -365,23 +379,10 @@ void WASAPISource::InitCapture() void WASAPISource::Initialize() { - HRESULT res; - - res = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, - CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), - (void **)enumerator.Assign()); - if (FAILED(res)) - throw HRError("Failed to create enumerator", res); - InitDevice(); device_name = GetDeviceName(device); - if (!notify) { - notify = new WASAPINotify(this); - enumerator->RegisterEndpointNotificationCallback(notify); - } - HRESULT resSample; IPropertyStore *store = nullptr; PWAVEFORMATEX deviceFormatProperties; From 4503c4fdac5ea40e5e117884f01d7c0ba71209b8 Mon Sep 17 00:00:00 2001 From: jpark37 Date: Sat, 25 Sep 2021 11:59:40 -0700 Subject: [PATCH 05/16] win-wasapi: Remove persistent references Only need IMMDevice and IAudioRenderClient during initialization. --- plugins/win-wasapi/win-wasapi.cpp | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index 7cf703d4e..b1e2fb900 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -25,10 +25,8 @@ static void GetWASAPIDefaults(obs_data_t *settings); class WASAPISource { ComPtr notify; ComPtr enumerator; - ComPtr device; ComPtr client; ComPtr capture; - ComPtr render; obs_source_t *source; wstring default_id; @@ -63,10 +61,10 @@ class WASAPISource { inline void Stop(); void Reconnect(); - void InitDevice(); + ComPtr InitDevice(); void InitName(); - void InitClient(); - void InitRender(); + void InitClient(IMMDevice *device); + void InitRender(IMMDevice *device); void InitFormat(WAVEFORMATEX *wfex); void InitCapture(); void Initialize(); @@ -218,8 +216,10 @@ void WASAPISource::Update(obs_data_t *settings) Start(); } -void WASAPISource::InitDevice() +ComPtr WASAPISource::InitDevice() { + ComPtr device; + if (isDefaultDevice) { HRESULT res = enumerator->GetDefaultAudioEndpoint( isInputDevice ? eCapture : eRender, @@ -247,11 +247,13 @@ void WASAPISource::InitDevice() if (FAILED(res)) throw HRError("Failed to enumerate device", res); } + + return device; } #define BUFFER_TIME_100NS (5 * 10000000) -void WASAPISource::InitClient() +void WASAPISource::InitClient(IMMDevice *device) { CoTaskMemPtr wfex; HRESULT res; @@ -277,7 +279,7 @@ void WASAPISource::InitClient() throw HRError("Failed to initialize audio client", res); } -void WASAPISource::InitRender() +void WASAPISource::InitRender(IMMDevice *device) { CoTaskMemPtr wfex; HRESULT res; @@ -307,8 +309,8 @@ void WASAPISource::InitRender() if (FAILED(res)) throw HRError("Failed to get buffer size", res); - res = client->GetService(__uuidof(IAudioRenderClient), - (void **)render.Assign()); + ComPtr render; + res = client->GetService(IID_PPV_ARGS(render.Assign())); if (FAILED(res)) throw HRError("Failed to get render client", res); @@ -316,7 +318,7 @@ void WASAPISource::InitRender() if (FAILED(res)) throw HRError("Failed to get buffer", res); - memset(buffer, 0, frames * wfex->nBlockAlign); + memset(buffer, 0, (size_t)frames * (size_t)wfex->nBlockAlign); render->ReleaseBuffer(frames, 0); } @@ -379,7 +381,7 @@ void WASAPISource::InitCapture() void WASAPISource::Initialize() { - InitDevice(); + ComPtr device = InitDevice(); device_name = GetDeviceName(device); @@ -403,9 +405,9 @@ void WASAPISource::Initialize() store->Release(); } - InitClient(); + InitClient(device); if (!isInputDevice) - InitRender(); + InitRender(device); InitCapture(); } From 5a2e884d143f6543d2b45cc4238c684f6ff7ebf2 Mon Sep 17 00:00:00 2001 From: jpark37 Date: Sat, 25 Sep 2021 12:05:12 -0700 Subject: [PATCH 06/16] win-wasapi: Simplify sample rate logging --- plugins/win-wasapi/win-wasapi.cpp | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index b1e2fb900..bc2d7ce24 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -10,6 +10,7 @@ #include #include +#include #include using namespace std; @@ -32,7 +33,6 @@ class WASAPISource { wstring default_id; string device_id; string device_name; - string device_sample = "-"; uint64_t lastNotifyTime = 0; bool isInputDevice; bool useDeviceTiming = false; @@ -374,9 +374,6 @@ void WASAPISource::InitCapture() client->Start(); active = true; - - blog(LOG_INFO, "WASAPI: Device '%s' [%s Hz] initialized", - device_name.c_str(), device_sample.c_str()); } void WASAPISource::Initialize() @@ -385,30 +382,13 @@ void WASAPISource::Initialize() device_name = GetDeviceName(device); - HRESULT resSample; - IPropertyStore *store = nullptr; - PWAVEFORMATEX deviceFormatProperties; - PROPVARIANT prop; - resSample = device->OpenPropertyStore(STGM_READ, &store); - if (!FAILED(resSample)) { - resSample = - store->GetValue(PKEY_AudioEngine_DeviceFormat, &prop); - if (!FAILED(resSample)) { - if (prop.vt != VT_EMPTY && prop.blob.pBlobData) { - deviceFormatProperties = - (PWAVEFORMATEX)prop.blob.pBlobData; - device_sample = std::to_string( - deviceFormatProperties->nSamplesPerSec); - } - } - - store->Release(); - } - InitClient(device); if (!isInputDevice) InitRender(device); InitCapture(); + + blog(LOG_INFO, "WASAPI: Device '%s' [%" PRIu32 " Hz] initialized", + device_name.c_str(), sampleRate); } bool WASAPISource::TryInitialize() From 88164affae73df3cb393a353f7c559c53502003c Mon Sep 17 00:00:00 2001 From: jpark37 Date: Sat, 25 Sep 2021 12:06:43 -0700 Subject: [PATCH 07/16] win-wasapi: Rename InitRender to ClearBuffer We no longer persist the render client. --- plugins/win-wasapi/win-wasapi.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index bc2d7ce24..f7b2d78db 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -64,7 +64,7 @@ class WASAPISource { ComPtr InitDevice(); void InitName(); void InitClient(IMMDevice *device); - void InitRender(IMMDevice *device); + void ClearBuffer(IMMDevice *device); void InitFormat(WAVEFORMATEX *wfex); void InitCapture(); void Initialize(); @@ -279,7 +279,7 @@ void WASAPISource::InitClient(IMMDevice *device) throw HRError("Failed to initialize audio client", res); } -void WASAPISource::InitRender(IMMDevice *device) +void WASAPISource::ClearBuffer(IMMDevice *device) { CoTaskMemPtr wfex; HRESULT res; @@ -384,7 +384,7 @@ void WASAPISource::Initialize() InitClient(device); if (!isInputDevice) - InitRender(device); + ClearBuffer(device); InitCapture(); blog(LOG_INFO, "WASAPI: Device '%s' [%" PRIu32 " Hz] initialized", From 49aa9074f8da92d7d717a332acc898c0a868a56a Mon Sep 17 00:00:00 2001 From: jpark37 Date: Sat, 25 Sep 2021 12:08:47 -0700 Subject: [PATCH 08/16] win-wasapi: Remove undefined function InitName --- plugins/win-wasapi/win-wasapi.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index f7b2d78db..4c4e12710 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -62,7 +62,6 @@ class WASAPISource { void Reconnect(); ComPtr InitDevice(); - void InitName(); void InitClient(IMMDevice *device); void ClearBuffer(IMMDevice *device); void InitFormat(WAVEFORMATEX *wfex); From b166b0488b2aeb5861b7342d965d1b9561578234 Mon Sep 17 00:00:00 2001 From: jpark37 Date: Sat, 25 Sep 2021 12:10:08 -0700 Subject: [PATCH 09/16] win-wasapi: Remove unnecessary inline tags --- plugins/win-wasapi/win-wasapi.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index 4c4e12710..1d143406b 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -57,8 +57,8 @@ class WASAPISource { bool ProcessCaptureData(); - inline void Start(); - inline void Stop(); + void Start(); + void Stop(); void Reconnect(); ComPtr InitDevice(); @@ -74,7 +74,7 @@ class WASAPISource { public: WASAPISource(obs_data_t *settings, obs_source_t *source_, bool input); - inline ~WASAPISource(); + ~WASAPISource(); void Update(obs_data_t *settings); @@ -163,7 +163,7 @@ WASAPISource::WASAPISource(obs_data_t *settings, obs_source_t *source_, Start(); } -inline void WASAPISource::Start() +void WASAPISource::Start() { if (!TryInitialize()) { blog(LOG_INFO, "WASAPI: Device '%s' failed to start", @@ -172,7 +172,7 @@ inline void WASAPISource::Start() } } -inline void WASAPISource::Stop() +void WASAPISource::Stop() { SetEvent(stopSignal); @@ -188,7 +188,7 @@ inline void WASAPISource::Stop() ResetEvent(stopSignal); } -inline WASAPISource::~WASAPISource() +WASAPISource::~WASAPISource() { enumerator->UnregisterEndpointNotificationCallback(notify); Stop(); From a504691af83dd8bbb755b5ae8dea4a30c6fe7361 Mon Sep 17 00:00:00 2001 From: jpark37 Date: Sat, 25 Sep 2021 12:14:01 -0700 Subject: [PATCH 10/16] win-wasapi: Clean reset on initialization failure Do not store IAudioClient and IAudioCaptureClient onto the source object until initialization is almost complete, right before the capture thread is created. We don't to retain objects from failed attempts. Also clear them when stopping. --- plugins/win-wasapi/win-wasapi.cpp | 99 ++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 29 deletions(-) diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index 1d143406b..892e068a1 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -61,11 +61,22 @@ class WASAPISource { void Stop(); void Reconnect(); - ComPtr InitDevice(); - void InitClient(IMMDevice *device); - void ClearBuffer(IMMDevice *device); - void InitFormat(WAVEFORMATEX *wfex); - void InitCapture(); + static ComPtr InitDevice(IMMDeviceEnumerator *enumerator, + bool isDefaultDevice, + bool isInputDevice, + const string device_id, + wstring &default_id); + static ComPtr InitClient(IMMDevice *device, + bool isInputDevice, + enum speaker_layout &speakers, + enum audio_format &format, + uint32_t &sampleRate); + static void InitFormat(const WAVEFORMATEX *wfex, + enum speaker_layout &speakers, + enum audio_format &format, uint32_t &sampleRate); + static void ClearBuffer(IMMDevice *device); + static ComPtr InitCapture(IAudioClient *client, + HANDLE receiveSignal); void Initialize(); bool TryInitialize(); @@ -186,6 +197,9 @@ void WASAPISource::Stop() WaitForSingleObject(reconnectThread, INFINITE); ResetEvent(stopSignal); + + capture.Clear(); + client.Clear(); } WASAPISource::~WASAPISource() @@ -215,7 +229,11 @@ void WASAPISource::Update(obs_data_t *settings) Start(); } -ComPtr WASAPISource::InitDevice() +ComPtr WASAPISource::InitDevice(IMMDeviceEnumerator *enumerator, + bool isDefaultDevice, + bool isInputDevice, + const string device_id, + wstring &default_id) { ComPtr device; @@ -252,23 +270,26 @@ ComPtr WASAPISource::InitDevice() #define BUFFER_TIME_100NS (5 * 10000000) -void WASAPISource::InitClient(IMMDevice *device) +ComPtr WASAPISource::InitClient(IMMDevice *device, + bool isInputDevice, + enum speaker_layout &speakers, + enum audio_format &format, + uint32_t &sampleRate) { - CoTaskMemPtr wfex; - HRESULT res; - DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK; - - res = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, - (void **)client.Assign()); + ComPtr client; + HRESULT res = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, + nullptr, (void **)client.Assign()); if (FAILED(res)) throw HRError("Failed to activate client context", res); + CoTaskMemPtr wfex; res = client->GetMixFormat(&wfex); if (FAILED(res)) throw HRError("Failed to get mix format", res); - InitFormat(wfex); + InitFormat(wfex, speakers, format, sampleRate); + DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK; if (!isInputDevice) flags |= AUDCLNT_STREAMFLAGS_LOOPBACK; @@ -276,6 +297,8 @@ void WASAPISource::InitClient(IMMDevice *device) BUFFER_TIME_100NS, 0, wfex, nullptr); if (FAILED(res)) throw HRError("Failed to initialize audio client", res); + + return client; } void WASAPISource::ClearBuffer(IMMDevice *device) @@ -340,7 +363,9 @@ static speaker_layout ConvertSpeakerLayout(DWORD layout, WORD channels) return (speaker_layout)channels; } -void WASAPISource::InitFormat(WAVEFORMATEX *wfex) +void WASAPISource::InitFormat(const WAVEFORMATEX *wfex, + enum speaker_layout &speakers, + enum audio_format &format, uint32_t &sampleRate) { DWORD layout = 0; @@ -350,15 +375,16 @@ void WASAPISource::InitFormat(WAVEFORMATEX *wfex) } /* WASAPI is always float */ - sampleRate = wfex->nSamplesPerSec; - format = AUDIO_FORMAT_FLOAT; speakers = ConvertSpeakerLayout(layout, wfex->nChannels); + format = AUDIO_FORMAT_FLOAT; + sampleRate = wfex->nSamplesPerSec; } -void WASAPISource::InitCapture() +ComPtr WASAPISource::InitCapture(IAudioClient *client, + HANDLE receiveSignal) { - HRESULT res = client->GetService(__uuidof(IAudioCaptureClient), - (void **)capture.Assign()); + ComPtr capture; + HRESULT res = client->GetService(IID_PPV_ARGS(capture.Assign())); if (FAILED(res)) throw HRError("Failed to create capture context", res); @@ -366,25 +392,40 @@ void WASAPISource::InitCapture() if (FAILED(res)) throw HRError("Failed to set event handle", res); - captureThread = CreateThread(nullptr, 0, WASAPISource::CaptureThread, - this, 0, nullptr); - if (!captureThread.Valid()) - throw "Failed to create capture thread"; + res = client->Start(); + if (FAILED(res)) + throw HRError("Failed to start capture client", res); - client->Start(); - active = true; + return capture; } void WASAPISource::Initialize() { - ComPtr device = InitDevice(); + ComPtr device = InitDevice(enumerator, isDefaultDevice, + isInputDevice, device_id, + default_id); device_name = GetDeviceName(device); - InitClient(device); + ComPtr temp_client = + InitClient(device, isInputDevice, speakers, format, sampleRate); if (!isInputDevice) ClearBuffer(device); - InitCapture(); + ComPtr temp_capture = + InitCapture(temp_client, receiveSignal); + + client = std::move(temp_client); + capture = std::move(temp_capture); + + captureThread = CreateThread(nullptr, 0, WASAPISource::CaptureThread, + this, 0, nullptr); + if (!captureThread.Valid()) { + capture.Clear(); + client.Clear(); + throw "Failed to create capture thread"; + } + + active = true; blog(LOG_INFO, "WASAPI: Device '%s' [%" PRIu32 " Hz] initialized", device_name.c_str(), sampleRate); From a995c305b7004ea3c4fe0428dec991954a941108 Mon Sep 17 00:00:00 2001 From: jpark37 Date: Fri, 24 Sep 2021 21:17:12 -0700 Subject: [PATCH 11/16] win-wasapi: Remove bools and persist threads This simplifies synchronization, and fixes several races. --- plugins/win-wasapi/win-wasapi.cpp | 352 ++++++++++++++++++------------ 1 file changed, 217 insertions(+), 135 deletions(-) diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index 892e068a1..03597f7e1 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -10,8 +10,8 @@ #include #include +#include #include -#include using namespace std; @@ -35,18 +35,21 @@ class WASAPISource { string device_name; uint64_t lastNotifyTime = 0; bool isInputDevice; - bool useDeviceTiming = false; + std::atomic useDeviceTiming = false; bool isDefaultDevice = false; - bool reconnecting = false; bool previouslyFailed = false; WinHandle reconnectThread; - bool active = false; WinHandle captureThread; - + WinHandle idleSignal; WinHandle stopSignal; WinHandle receiveSignal; + WinHandle restartSignal; + WinHandle exitSignal; + WinHandle initSignal; + DWORD reconnectDuration = 0; + WinHandle reconnectSignal; speaker_layout speakers; audio_format format; @@ -59,7 +62,6 @@ class WASAPISource { void Start(); void Stop(); - void Reconnect(); static ComPtr InitDevice(IMMDeviceEnumerator *enumerator, bool isDefaultDevice, @@ -149,6 +151,10 @@ WASAPISource::WASAPISource(obs_data_t *settings, obs_source_t *source_, { UpdateSettings(settings); + idleSignal = CreateEvent(nullptr, true, false, nullptr); + if (!idleSignal.Valid()) + throw "Could not create idle signal"; + stopSignal = CreateEvent(nullptr, true, false, nullptr); if (!stopSignal.Valid()) throw "Could not create stop signal"; @@ -157,49 +163,69 @@ WASAPISource::WASAPISource(obs_data_t *settings, obs_source_t *source_, if (!receiveSignal.Valid()) throw "Could not create receive signal"; + restartSignal = CreateEvent(nullptr, true, false, nullptr); + if (!restartSignal.Valid()) + throw "Could not create restart signal"; + + exitSignal = CreateEvent(nullptr, true, false, nullptr); + if (!exitSignal.Valid()) + throw "Could not create exit signal"; + + initSignal = CreateEvent(nullptr, false, false, nullptr); + if (!initSignal.Valid()) + throw "Could not create init signal"; + + reconnectSignal = CreateEvent(nullptr, false, false, nullptr); + if (!reconnectSignal.Valid()) + throw "Could not create reconnect signal"; + + reconnectThread = CreateThread( + nullptr, 0, WASAPISource::ReconnectThread, this, 0, nullptr); + if (!reconnectThread.Valid()) + throw "Failed to create reconnect thread"; + notify = new WASAPINotify(this); if (!notify) throw "Could not create WASAPINotify"; - HRESULT res = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, - CLSCTX_ALL, - IID_PPV_ARGS(enumerator.Assign())); - if (FAILED(res)) - throw HRError("Failed to create enumerator", res); + HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, + CLSCTX_ALL, + IID_PPV_ARGS(enumerator.Assign())); + if (FAILED(hr)) + throw HRError("Failed to create enumerator", hr); - res = enumerator->RegisterEndpointNotificationCallback(notify); - if (FAILED(res)) - throw HRError("Failed to register endpoint callback", res); + hr = enumerator->RegisterEndpointNotificationCallback(notify); + if (FAILED(hr)) + throw HRError("Failed to register endpoint callback", hr); + + captureThread = CreateThread(nullptr, 0, WASAPISource::CaptureThread, + this, 0, nullptr); + if (!captureThread.Valid()) { + enumerator->UnregisterEndpointNotificationCallback(notify); + throw "Failed to create capture thread"; + } Start(); } void WASAPISource::Start() { - if (!TryInitialize()) { - blog(LOG_INFO, "WASAPI: Device '%s' failed to start", - device_id.c_str()); - Reconnect(); - } + SetEvent(initSignal); } void WASAPISource::Stop() { SetEvent(stopSignal); - if (active) { - blog(LOG_INFO, "WASAPI: Device '%s' Terminated", - device_name.c_str()); - WaitForSingleObject(captureThread, INFINITE); - } + blog(LOG_INFO, "WASAPI: Device '%s' Terminated", device_name.c_str()); - if (reconnecting) - WaitForSingleObject(reconnectThread, INFINITE); + WaitForSingleObject(idleSignal, INFINITE); - ResetEvent(stopSignal); + SetEvent(exitSignal); - capture.Clear(); - client.Clear(); + WaitForSingleObject(reconnectThread, INFINITE); + + WaitForSingleObject(captureThread, INFINITE); } WASAPISource::~WASAPISource() @@ -217,16 +243,13 @@ void WASAPISource::UpdateSettings(obs_data_t *settings) void WASAPISource::Update(obs_data_t *settings) { - string newDevice = obs_data_get_string(settings, OPT_DEVICE_ID); - bool restart = newDevice.compare(device_id) != 0; - - if (restart) - Stop(); + const string newDevice = obs_data_get_string(settings, OPT_DEVICE_ID); + const bool restart = newDevice.compare(device_id) != 0; UpdateSettings(settings); if (restart) - Start(); + SetEvent(restartSignal); } ComPtr WASAPISource::InitDevice(IMMDeviceEnumerator *enumerator, @@ -407,6 +430,8 @@ void WASAPISource::Initialize() device_name = GetDeviceName(device); + ResetEvent(receiveSignal); + ComPtr temp_client = InitClient(device, isInputDevice, speakers, format, sampleRate); if (!isInputDevice) @@ -417,93 +442,68 @@ void WASAPISource::Initialize() client = std::move(temp_client); capture = std::move(temp_capture); - captureThread = CreateThread(nullptr, 0, WASAPISource::CaptureThread, - this, 0, nullptr); - if (!captureThread.Valid()) { - capture.Clear(); - client.Clear(); - throw "Failed to create capture thread"; - } - - active = true; - blog(LOG_INFO, "WASAPI: Device '%s' [%" PRIu32 " Hz] initialized", device_name.c_str(), sampleRate); } bool WASAPISource::TryInitialize() { + bool success = false; try { Initialize(); + success = true; } catch (HRError &error) { - if (previouslyFailed) - return active; - - blog(LOG_WARNING, "[WASAPISource::TryInitialize]:[%s] %s: %lX", - device_name.empty() ? device_id.c_str() - : device_name.c_str(), - error.str, error.hr); - + if (!previouslyFailed) { + blog(LOG_WARNING, + "[WASAPISource::TryInitialize]:[%s] %s: %lX", + device_name.empty() ? device_id.c_str() + : device_name.c_str(), + error.str, error.hr); + } } catch (const char *error) { - if (previouslyFailed) - return active; - - blog(LOG_WARNING, "[WASAPISource::TryInitialize]:[%s] %s", - device_name.empty() ? device_id.c_str() - : device_name.c_str(), - error); + if (!previouslyFailed) { + blog(LOG_WARNING, + "[WASAPISource::TryInitialize]:[%s] %s", + device_name.empty() ? device_id.c_str() + : device_name.c_str(), + error); + } } - previouslyFailed = !active; - return active; + previouslyFailed = !success; + return success; } -void WASAPISource::Reconnect() -{ - reconnecting = true; - reconnectThread = CreateThread( - nullptr, 0, WASAPISource::ReconnectThread, this, 0, nullptr); - - if (!reconnectThread.Valid()) - blog(LOG_WARNING, - "[WASAPISource::Reconnect] " - "Failed to initialize reconnect thread: %lu", - GetLastError()); -} - -static inline bool WaitForSignal(HANDLE handle, DWORD time) -{ - return WaitForSingleObject(handle, time) != WAIT_TIMEOUT; -} - -#define RECONNECT_INTERVAL 3000 - DWORD WINAPI WASAPISource::ReconnectThread(LPVOID param) { - WASAPISource *source = (WASAPISource *)param; - os_set_thread_name("win-wasapi: reconnect thread"); - const HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED); - const bool com_initialized = SUCCEEDED(hr); - if (!com_initialized) { - blog(LOG_ERROR, - "[WASAPISource::ReconnectThread]" - " CoInitializeEx failed: 0x%08X", - hr); - } + WASAPISource *source = (WASAPISource *)param; - while (!WaitForSignal(source->stopSignal, RECONNECT_INTERVAL)) { - if (source->TryInitialize()) + const HANDLE sigs[] = { + source->exitSignal, + source->reconnectSignal, + }; + + bool exit = false; + while (!exit) { + const DWORD ret = WaitForMultipleObjects(_countof(sigs), sigs, + false, INFINITE); + switch (ret) { + case WAIT_OBJECT_0: + exit = true; break; + default: + assert(ret == (WAIT_OBJECT_0 + 1)); + if (source->reconnectDuration > 0) { + WaitForSingleObject(source->stopSignal, + source->reconnectDuration); + } + source->Start(); + } } - if (com_initialized) - CoUninitialize(); - - source->reconnectThread = nullptr; - source->reconnecting = false; return 0; } @@ -518,7 +518,6 @@ bool WASAPISource::ProcessCaptureData() while (true) { res = capture->GetNextPacketSize(&captureSize); - if (FAILED(res)) { if (res != AUDCLNT_E_DEVICE_INVALIDATED) blog(LOG_WARNING, @@ -563,44 +562,130 @@ bool WASAPISource::ProcessCaptureData() return true; } -static inline bool WaitForCaptureSignal(DWORD numSignals, const HANDLE *signals, - DWORD duration) -{ - DWORD ret; - ret = WaitForMultipleObjects(numSignals, signals, false, duration); - - return ret == WAIT_OBJECT_0 || ret == WAIT_TIMEOUT; -} +#define RECONNECT_INTERVAL 3000 DWORD WINAPI WASAPISource::CaptureThread(LPVOID param) { - WASAPISource *source = (WASAPISource *)param; - bool reconnect = false; - - /* Output devices don't signal, so just make it check every 10 ms */ - DWORD dur = source->isInputDevice ? RECONNECT_INTERVAL : 10; - - HANDLE sigs[2] = {source->receiveSignal, source->stopSignal}; - os_set_thread_name("win-wasapi: capture thread"); - while (WaitForCaptureSignal(2, sigs, dur)) { - if (!source->ProcessCaptureData()) { - reconnect = true; - break; + const HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED); + const bool com_initialized = SUCCEEDED(hr); + if (!com_initialized) { + blog(LOG_ERROR, + "[WASAPISource::CaptureThread]" + " CoInitializeEx failed: 0x%08X", + hr); + } + + WASAPISource *source = (WASAPISource *)param; + + const HANDLE inactive_sigs[] = { + source->exitSignal, + source->stopSignal, + source->initSignal, + }; + + const HANDLE active_sigs[] = { + source->exitSignal, + source->stopSignal, + source->receiveSignal, + source->restartSignal, + }; + + DWORD sig_count = _countof(inactive_sigs); + const HANDLE *sigs = inactive_sigs; + + bool exit = false; + while (!exit) { + bool idle = false; + bool stop = false; + bool reconnect = false; + do { + /* Windows 7 does not seem to wake up for LOOPBACK */ + const DWORD dwMilliseconds = ((sigs == active_sigs) && + !source->isInputDevice) + ? 10 + : INFINITE; + + const DWORD ret = WaitForMultipleObjects( + sig_count, sigs, false, dwMilliseconds); + switch (ret) { + case WAIT_OBJECT_0: { + exit = true; + stop = true; + idle = true; + break; + } + + case WAIT_OBJECT_0 + 1: + stop = true; + idle = true; + break; + + case WAIT_OBJECT_0 + 2: + case WAIT_TIMEOUT: + if (sigs == inactive_sigs) { + assert(ret != WAIT_TIMEOUT); + + if (source->TryInitialize()) { + sig_count = + _countof(active_sigs); + sigs = active_sigs; + } else { + blog(LOG_INFO, + "WASAPI: Device '%s' failed to start", + source->device_id.c_str()); + stop = true; + reconnect = true; + source->reconnectDuration = + RECONNECT_INTERVAL; + } + } else { + stop = !source->ProcessCaptureData(); + if (stop) { + blog(LOG_INFO, + "Device '%s' invalidated. Retrying", + source->device_name + .c_str()); + stop = true; + reconnect = true; + source->reconnectDuration = + RECONNECT_INTERVAL; + } + } + break; + + default: + assert(sigs == active_sigs); + assert(ret == WAIT_OBJECT_0 + 3); + stop = true; + reconnect = true; + source->reconnectDuration = 0; + ResetEvent(source->restartSignal); + } + } while (!stop); + + sig_count = _countof(inactive_sigs); + sigs = inactive_sigs; + + if (source->client) { + source->client->Stop(); + + source->capture.Clear(); + source->client.Clear(); + } + + if (idle) { + SetEvent(source->idleSignal); + } else if (reconnect) { + blog(LOG_INFO, "Device '%s' invalidated. Retrying", + source->device_name.c_str()); + SetEvent(source->reconnectSignal); } } - source->client->Stop(); - - source->captureThread = nullptr; - source->active = false; - - if (reconnect) { - blog(LOG_INFO, "Device '%s' invalidated. Retrying", - source->device_name.c_str()); - source->Reconnect(); - } + if (com_initialized) + CoUninitialize(); return 0; } @@ -626,12 +711,9 @@ void WASAPISource::SetDefaultDevice(EDataFlow flow, ERole role, LPCWSTR id) if (t - lastNotifyTime < 300000000) return; - std::thread([this]() { - Stop(); - Start(); - }).detach(); - lastNotifyTime = t; + + SetEvent(restartSignal); } /* ------------------------------------------------------------------------- */ From d1230c292e39283aaef66d405d23be3b6f7afb62 Mon Sep 17 00:00:00 2001 From: jpark37 Date: Mon, 27 Sep 2021 12:17:57 -0700 Subject: [PATCH 12/16] win-wasapi: Register capture thread with MMCSS Ensure audio gets more priority to help prevent glitching. --- plugins/win-wasapi/CMakeLists.txt | 1 + plugins/win-wasapi/win-wasapi.cpp | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/plugins/win-wasapi/CMakeLists.txt b/plugins/win-wasapi/CMakeLists.txt index 387efb608..4a5542960 100644 --- a/plugins/win-wasapi/CMakeLists.txt +++ b/plugins/win-wasapi/CMakeLists.txt @@ -15,6 +15,7 @@ add_library(win-wasapi MODULE ${win-wasapi_SOURCES} ${win-wasapi_HEADERS}) target_link_libraries(win-wasapi + Avrt libobs) set_target_properties(win-wasapi PROPERTIES FOLDER "plugins") diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index 03597f7e1..801fd9c10 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -13,6 +13,8 @@ #include #include +#include + using namespace std; #define OPT_DEVICE_ID "device_id" @@ -577,6 +579,9 @@ DWORD WINAPI WASAPISource::CaptureThread(LPVOID param) hr); } + DWORD unused = 0; + const HANDLE handle = AvSetMmThreadCharacteristics(L"Audio", &unused); + WASAPISource *source = (WASAPISource *)param; const HANDLE inactive_sigs[] = { @@ -684,6 +689,9 @@ DWORD WINAPI WASAPISource::CaptureThread(LPVOID param) } } + if (handle) + AvRevertMmThreadCharacteristics(handle); + if (com_initialized) CoUninitialize(); From 86b607154a98a981f277ec33f70776eb52b160af Mon Sep 17 00:00:00 2001 From: jpark37 Date: Sun, 26 Sep 2021 04:19:05 -0700 Subject: [PATCH 13/16] UI: Add support for real-time work queue --- UI/obs-app.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/UI/obs-app.cpp b/UI/obs-app.cpp index 496583fd1..f9d74edd8 100644 --- a/UI/obs-app.cpp +++ b/UI/obs-app.cpp @@ -2689,6 +2689,14 @@ int main(int argc, char *argv[]) SetErrorMode(SEM_FAILCRITICALERRORS); load_debug_privilege(); base_set_crash_handler(main_crash_handler, nullptr); + + const HMODULE hRtwq = LoadLibrary(L"RTWorkQ.dll"); + if (hRtwq) { + typedef HRESULT(STDAPICALLTYPE * PFN_RtwqStartup)(); + PFN_RtwqStartup func = + (PFN_RtwqStartup)GetProcAddress(hRtwq, "RtwqStartup"); + func(); + } #endif base_get_log_handler(&def_log_handler, nullptr); @@ -2828,6 +2836,16 @@ int main(int argc, char *argv[]) curl_global_init(CURL_GLOBAL_ALL); int ret = run_program(logFile, argc, argv); +#ifdef _WIN32 + if (hRtwq) { + typedef HRESULT(STDAPICALLTYPE * PFN_RtwqShutdown)(); + PFN_RtwqShutdown func = + (PFN_RtwqShutdown)GetProcAddress(hRtwq, "RtwqShutdown"); + func(); + FreeLibrary(hRtwq); + } +#endif + blog(LOG_INFO, "Number of memory leaks: %ld", bnum_allocs()); base_set_log_handler(nullptr, nullptr); return ret; From 24d82062aecca2f702dbf1cd7a85e84036b24ccf Mon Sep 17 00:00:00 2001 From: jpark37 Date: Sun, 26 Sep 2021 04:21:22 -0700 Subject: [PATCH 14/16] win-wasapi: Schedule work on real-time work queue MS claims it can schedule audio better if we use their API. --- plugins/win-wasapi/win-wasapi.cpp | 310 +++++++++++++++++++++++++++++- 1 file changed, 302 insertions(+), 8 deletions(-) diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index 801fd9c10..757d9f2c5 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -14,6 +14,7 @@ #include #include +#include using namespace std; @@ -25,6 +26,66 @@ static void GetWASAPIDefaults(obs_data_t *settings); #define OBS_KSAUDIO_SPEAKER_4POINT1 \ (KSAUDIO_SPEAKER_SURROUND | SPEAKER_LOW_FREQUENCY) +typedef HRESULT(STDAPICALLTYPE *PFN_RtwqUnlockWorkQueue)(DWORD); +typedef HRESULT(STDAPICALLTYPE *PFN_RtwqLockSharedWorkQueue)(PCWSTR usageClass, + LONG basePriority, + DWORD *taskId, + DWORD *id); +typedef HRESULT(STDAPICALLTYPE *PFN_RtwqCreateAsyncResult)(IUnknown *, + IRtwqAsyncCallback *, + IUnknown *, + IRtwqAsyncResult **); +typedef HRESULT(STDAPICALLTYPE *PFN_RtwqPutWorkItem)(DWORD, LONG, + IRtwqAsyncResult *); +typedef HRESULT(STDAPICALLTYPE *PFN_RtwqPutWaitingWorkItem)(HANDLE, LONG, + IRtwqAsyncResult *, + RTWQWORKITEM_KEY *); + +class ARtwqAsyncCallback : public IRtwqAsyncCallback { +protected: + ARtwqAsyncCallback(void *source) : source(source) {} + +public: + STDMETHOD_(ULONG, AddRef)() { return ++refCount; } + + STDMETHOD_(ULONG, Release)() { return --refCount; } + + STDMETHOD(QueryInterface)(REFIID riid, void **ppvObject) + { + HRESULT hr = E_NOINTERFACE; + + if (riid == __uuidof(IRtwqAsyncCallback) || + riid == __uuidof(IUnknown)) { + *ppvObject = this; + AddRef(); + hr = S_OK; + } else { + *ppvObject = NULL; + } + + return hr; + } + + STDMETHOD(GetParameters) + (DWORD *pdwFlags, DWORD *pdwQueue) + { + *pdwFlags = 0; + *pdwQueue = queue_id; + return S_OK; + } + + STDMETHOD(Invoke) + (IRtwqAsyncResult *) override = 0; + + DWORD GetQueueId() const { return queue_id; } + void SetQueueId(DWORD id) { queue_id = id; } + +protected: + std::atomic refCount = 1; + void *source; + DWORD queue_id = 0; +}; + class WASAPISource { ComPtr notify; ComPtr enumerator; @@ -35,6 +96,12 @@ class WASAPISource { wstring default_id; string device_id; string device_name; + PFN_RtwqUnlockWorkQueue rtwq_unlock_work_queue = NULL; + PFN_RtwqLockSharedWorkQueue rtwq_lock_shared_work_queue = NULL; + PFN_RtwqCreateAsyncResult rtwq_create_async_result = NULL; + PFN_RtwqPutWorkItem rtwq_put_work_item = NULL; + PFN_RtwqPutWaitingWorkItem rtwq_put_waiting_work_item = NULL; + bool rtwq_supported = false; uint64_t lastNotifyTime = 0; bool isInputDevice; std::atomic useDeviceTiming = false; @@ -43,6 +110,55 @@ class WASAPISource { bool previouslyFailed = false; WinHandle reconnectThread; + class CallbackStartCapture : public ARtwqAsyncCallback { + public: + CallbackStartCapture(WASAPISource *source) + : ARtwqAsyncCallback(source) + { + } + + STDMETHOD(Invoke) + (IRtwqAsyncResult *) override + { + ((WASAPISource *)source)->OnStartCapture(); + return S_OK; + } + + } startCapture; + ComPtr startCaptureAsyncResult; + + class CallbackSampleReady : public ARtwqAsyncCallback { + public: + CallbackSampleReady(WASAPISource *source) + : ARtwqAsyncCallback(source) + { + } + + STDMETHOD(Invoke) + (IRtwqAsyncResult *) override + { + ((WASAPISource *)source)->OnSampleReady(); + return S_OK; + } + } sampleReady; + ComPtr sampleReadyAsyncResult; + + class CallbackRestart : public ARtwqAsyncCallback { + public: + CallbackRestart(WASAPISource *source) + : ARtwqAsyncCallback(source) + { + } + + STDMETHOD(Invoke) + (IRtwqAsyncResult *) override + { + ((WASAPISource *)source)->OnRestart(); + return S_OK; + } + } restart; + ComPtr restartAsyncResult; + WinHandle captureThread; WinHandle idleSignal; WinHandle stopSignal; @@ -94,6 +210,10 @@ public: void Update(obs_data_t *settings); void SetDefaultDevice(EDataFlow flow, ERole role, LPCWSTR id); + + void OnStartCapture(); + void OnSampleReady(); + void OnRestart(); }; class WASAPINotify : public IMMNotificationClient { @@ -149,7 +269,11 @@ public: WASAPISource::WASAPISource(obs_data_t *settings, obs_source_t *source_, bool input) - : source(source_), isInputDevice(input) + : source(source_), + isInputDevice(input), + startCapture(this), + sampleReady(this), + restart(this) { UpdateSettings(settings); @@ -200,11 +324,73 @@ WASAPISource::WASAPISource(obs_data_t *settings, obs_source_t *source_, if (FAILED(hr)) throw HRError("Failed to register endpoint callback", hr); - captureThread = CreateThread(nullptr, 0, WASAPISource::CaptureThread, - this, 0, nullptr); - if (!captureThread.Valid()) { - enumerator->UnregisterEndpointNotificationCallback(notify); - throw "Failed to create capture thread"; + /* OBS will already load DLL on startup if it exists */ + const HMODULE rtwq_module = GetModuleHandle(L"RTWorkQ.dll"); + rtwq_supported = rtwq_module != NULL; + if (rtwq_supported) { + rtwq_unlock_work_queue = + (PFN_RtwqUnlockWorkQueue)GetProcAddress( + rtwq_module, "RtwqUnlockWorkQueue"); + rtwq_lock_shared_work_queue = + (PFN_RtwqLockSharedWorkQueue)GetProcAddress( + rtwq_module, "RtwqLockSharedWorkQueue"); + rtwq_create_async_result = + (PFN_RtwqCreateAsyncResult)GetProcAddress( + rtwq_module, "RtwqCreateAsyncResult"); + rtwq_put_work_item = (PFN_RtwqPutWorkItem)GetProcAddress( + rtwq_module, "RtwqPutWorkItem"); + rtwq_put_waiting_work_item = + (PFN_RtwqPutWaitingWorkItem)GetProcAddress( + rtwq_module, "RtwqPutWaitingWorkItem"); + + hr = rtwq_create_async_result(nullptr, &startCapture, nullptr, + &startCaptureAsyncResult); + if (FAILED(hr)) { + enumerator->UnregisterEndpointNotificationCallback( + notify); + throw HRError( + "Could not create startCaptureAsyncResult", hr); + } + + hr = rtwq_create_async_result(nullptr, &sampleReady, nullptr, + &sampleReadyAsyncResult); + if (FAILED(hr)) { + enumerator->UnregisterEndpointNotificationCallback( + notify); + throw HRError("Could not create sampleReadyAsyncResult", + hr); + } + + hr = rtwq_create_async_result(nullptr, &restart, nullptr, + &restartAsyncResult); + if (FAILED(hr)) { + enumerator->UnregisterEndpointNotificationCallback( + notify); + throw HRError("Could not create restartAsyncResult", + hr); + } + + DWORD taskId = 0; + DWORD id = 0; + hr = rtwq_lock_shared_work_queue(L"Capture", 0, &taskId, &id); + if (FAILED(hr)) { + enumerator->UnregisterEndpointNotificationCallback( + notify); + throw HRError("RtwqLockSharedWorkQueue failed", hr); + } + + startCapture.SetQueueId(id); + sampleReady.SetQueueId(id); + restart.SetQueueId(id); + } else { + captureThread = CreateThread(nullptr, 0, + WASAPISource::CaptureThread, this, + 0, nullptr); + if (!captureThread.Valid()) { + enumerator->UnregisterEndpointNotificationCallback( + notify); + throw "Failed to create capture thread"; + } } Start(); @@ -212,7 +398,12 @@ WASAPISource::WASAPISource(obs_data_t *settings, obs_source_t *source_, void WASAPISource::Start() { - SetEvent(initSignal); + if (rtwq_supported) { + rtwq_put_work_item(startCapture.GetQueueId(), 0, + startCaptureAsyncResult); + } else { + SetEvent(initSignal); + } } void WASAPISource::Stop() @@ -221,13 +412,19 @@ void WASAPISource::Stop() blog(LOG_INFO, "WASAPI: Device '%s' Terminated", device_name.c_str()); + if (rtwq_supported) + SetEvent(receiveSignal); + WaitForSingleObject(idleSignal, INFINITE); SetEvent(exitSignal); WaitForSingleObject(reconnectThread, INFINITE); - WaitForSingleObject(captureThread, INFINITE); + if (rtwq_supported) + rtwq_unlock_work_queue(sampleReady.GetQueueId()); + else + WaitForSingleObject(captureThread, INFINITE); } WASAPISource::~WASAPISource() @@ -444,6 +641,24 @@ void WASAPISource::Initialize() client = std::move(temp_client); capture = std::move(temp_capture); + if (rtwq_supported) { + HRESULT hr = rtwq_put_waiting_work_item( + receiveSignal, 0, sampleReadyAsyncResult, nullptr); + if (FAILED(hr)) { + capture.Clear(); + client.Clear(); + throw HRError("RtwqPutWaitingWorkItem failed", hr); + } + + hr = rtwq_put_waiting_work_item(restartSignal, 0, + restartAsyncResult, nullptr); + if (FAILED(hr)) { + capture.Clear(); + client.Clear(); + throw HRError("RtwqPutWaitingWorkItem failed", hr); + } + } + blog(LOG_INFO, "WASAPI: Device '%s' [%" PRIu32 " Hz] initialized", device_name.c_str(), sampleRate); } @@ -724,6 +939,85 @@ void WASAPISource::SetDefaultDevice(EDataFlow flow, ERole role, LPCWSTR id) SetEvent(restartSignal); } +void WASAPISource::OnStartCapture() +{ + const DWORD ret = WaitForSingleObject(stopSignal, 0); + switch (ret) { + case WAIT_OBJECT_0: + SetEvent(idleSignal); + break; + + default: + assert(ret == WAIT_TIMEOUT); + + if (!TryInitialize()) { + blog(LOG_INFO, "WASAPI: Device '%s' failed to start", + device_id.c_str()); + reconnectDuration = RECONNECT_INTERVAL; + SetEvent(reconnectSignal); + } + } +} + +void WASAPISource::OnSampleReady() +{ + bool stop = false; + bool reconnect = false; + + if (!ProcessCaptureData()) { + stop = true; + reconnect = true; + reconnectDuration = RECONNECT_INTERVAL; + } + + if (WaitForSingleObject(restartSignal, 0) == WAIT_OBJECT_0) { + stop = true; + reconnect = true; + reconnectDuration = 0; + + ResetEvent(restartSignal); + rtwq_put_waiting_work_item(restartSignal, 0, restartAsyncResult, + nullptr); + } + + if (WaitForSingleObject(stopSignal, 0) == WAIT_OBJECT_0) { + stop = true; + reconnect = false; + } + + if (!stop) { + if (FAILED(rtwq_put_waiting_work_item(receiveSignal, 0, + sampleReadyAsyncResult, + nullptr))) { + blog(LOG_ERROR, + "Could not requeue sample receive work"); + stop = true; + reconnect = true; + reconnectDuration = RECONNECT_INTERVAL; + } + } + + if (stop) { + client->Stop(); + + capture.Clear(); + client.Clear(); + + if (reconnect) { + blog(LOG_INFO, "Device '%s' invalidated. Retrying", + device_name.c_str()); + SetEvent(reconnectSignal); + } else { + SetEvent(idleSignal); + } + } +} + +void WASAPISource::OnRestart() +{ + SetEvent(receiveSignal); +} + /* ------------------------------------------------------------------------- */ static const char *GetWASAPIInputName(void *) From ab7d370ada0e71b4b24ee440f169b9bd209740ca Mon Sep 17 00:00:00 2001 From: jpark37 Date: Sat, 2 Oct 2021 21:23:09 -0700 Subject: [PATCH 15/16] win-wasapi Improve default device handling Remove lastNotifyTime throttle. Could ignore last device notification. Will bring back if necessary, but seemed like overkill. Make isInputDevice const for safety. Make isDefaultDevice atomic. Allows it to be accessed simultaneously from settings and notification callback threads. Race between setting change in OBS and Windows is not a problem, a signal will be sent after both to consolidate. Worst that should happen is a redundant reconnect cycle. Only read/write default_id from notification callback for thread safety. --- plugins/win-wasapi/win-wasapi.cpp | 44 ++++++++++++------------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index 757d9f2c5..d5f8d6d04 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -102,10 +102,9 @@ class WASAPISource { PFN_RtwqPutWorkItem rtwq_put_work_item = NULL; PFN_RtwqPutWaitingWorkItem rtwq_put_waiting_work_item = NULL; bool rtwq_supported = false; - uint64_t lastNotifyTime = 0; - bool isInputDevice; + const bool isInputDevice; std::atomic useDeviceTiming = false; - bool isDefaultDevice = false; + std::atomic isDefaultDevice = false; bool previouslyFailed = false; WinHandle reconnectThread; @@ -184,8 +183,7 @@ class WASAPISource { static ComPtr InitDevice(IMMDeviceEnumerator *enumerator, bool isDefaultDevice, bool isInputDevice, - const string device_id, - wstring &default_id); + const string device_id); static ComPtr InitClient(IMMDevice *device, bool isInputDevice, enum speaker_layout &speakers, @@ -454,8 +452,7 @@ void WASAPISource::Update(obs_data_t *settings) ComPtr WASAPISource::InitDevice(IMMDeviceEnumerator *enumerator, bool isDefaultDevice, bool isInputDevice, - const string device_id, - wstring &default_id) + const string device_id) { ComPtr device; @@ -466,12 +463,6 @@ ComPtr WASAPISource::InitDevice(IMMDeviceEnumerator *enumerator, device.Assign()); if (FAILED(res)) throw HRError("Failed GetDefaultAudioEndpoint", res); - - CoTaskMemPtr id; - res = device->GetId(&id); - if (FAILED(res)) - throw HRError("Failed to get default id", res); - default_id = id; } else { wchar_t *w_id; os_utf8_to_wcs_ptr(device_id.c_str(), device_id.size(), &w_id); @@ -624,8 +615,7 @@ ComPtr WASAPISource::InitCapture(IAudioClient *client, void WASAPISource::Initialize() { ComPtr device = InitDevice(enumerator, isDefaultDevice, - isInputDevice, device_id, - default_id); + isInputDevice, device_id); device_name = GetDeviceName(device); @@ -918,24 +908,24 @@ void WASAPISource::SetDefaultDevice(EDataFlow flow, ERole role, LPCWSTR id) if (!isDefaultDevice) return; - EDataFlow expectedFlow = isInputDevice ? eCapture : eRender; - ERole expectedRole = isInputDevice ? eCommunications : eConsole; - + const EDataFlow expectedFlow = isInputDevice ? eCapture : eRender; + const ERole expectedRole = isInputDevice ? eCommunications : eConsole; if (flow != expectedFlow || role != expectedRole) return; - if (id && default_id.compare(id) == 0) - return; + + if (id) { + if (default_id.compare(id) == 0) + return; + default_id = id; + } else { + if (default_id.empty()) + return; + default_id.clear(); + } blog(LOG_INFO, "WASAPI: Default %s device changed", isInputDevice ? "input" : "output"); - /* reset device only once every 300ms */ - uint64_t t = os_gettime_ns(); - if (t - lastNotifyTime < 300000000) - return; - - lastNotifyTime = t; - SetEvent(restartSignal); } From c8d0597358993e2ede83035251e6739f8bc548b9 Mon Sep 17 00:00:00 2001 From: jpark37 Date: Sun, 3 Oct 2021 10:50:45 -0700 Subject: [PATCH 16/16] win-wasapi: Log settings --- plugins/win-wasapi/win-wasapi.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index d5f8d6d04..6dfa9996f 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -436,6 +436,13 @@ void WASAPISource::UpdateSettings(obs_data_t *settings) device_id = obs_data_get_string(settings, OPT_DEVICE_ID); useDeviceTiming = obs_data_get_bool(settings, OPT_USE_DEVICE_TIMING); isDefaultDevice = _strcmpi(device_id.c_str(), "default") == 0; + + blog(LOG_INFO, + "[win-wasapi: '%s'] update settings:\n" + "\tdevice id: %s\n" + "\tuse device timing: %d", + obs_source_get_name(source), device_id.c_str(), + (int)useDeviceTiming); } void WASAPISource::Update(obs_data_t *settings)