#include "enum-wasapi.hpp" #include #include #include #include #include #include #include using namespace std; #define OPT_DEVICE_ID "device_id" #define OPT_USE_DEVICE_TIMING "use_device_timing" static void GetWASAPIDefaults(obs_data_t *settings); #define KSAUDIO_SPEAKER_4POINT1 (KSAUDIO_SPEAKER_QUAD|SPEAKER_LOW_FREQUENCY) #define KSAUDIO_SPEAKER_2POINT1 (KSAUDIO_SPEAKER_STEREO|SPEAKER_LOW_FREQUENCY) class WASAPISource { ComPtr device; ComPtr client; ComPtr capture; ComPtr render; obs_source_t *source; string device_id; string device_name; bool isInputDevice; bool useDeviceTiming = false; bool isDefaultDevice = false; bool reconnecting = false; bool previouslyFailed = false; WinHandle reconnectThread; bool active = false; WinHandle captureThread; WinHandle stopSignal; WinHandle receiveSignal; speaker_layout speakers; audio_format format; uint32_t sampleRate; static DWORD WINAPI ReconnectThread(LPVOID param); static DWORD WINAPI CaptureThread(LPVOID param); bool ProcessCaptureData(); inline void Start(); inline void Stop(); void Reconnect(); bool InitDevice(IMMDeviceEnumerator *enumerator); void InitName(); void InitClient(); void InitRender(); void InitFormat(WAVEFORMATEX *wfex); void InitCapture(); void Initialize(); bool TryInitialize(); void UpdateSettings(obs_data_t *settings); public: WASAPISource(obs_data_t *settings, obs_source_t *source_, bool input); inline ~WASAPISource(); void Update(obs_data_t *settings); }; WASAPISource::WASAPISource(obs_data_t *settings, obs_source_t *source_, bool input) : source (source_), isInputDevice (input) { UpdateSettings(settings); stopSignal = CreateEvent(nullptr, true, false, nullptr); if (!stopSignal.Valid()) throw "Could not create stop signal"; receiveSignal = CreateEvent(nullptr, false, false, nullptr); if (!receiveSignal.Valid()) throw "Could not create receive signal"; Start(); } inline void WASAPISource::Start() { if (!TryInitialize()) { blog(LOG_INFO, "[WASAPISource::WASAPISource] " "Device '%s' not found. Waiting for device", device_id.c_str()); Reconnect(); } } inline void WASAPISource::Stop() { SetEvent(stopSignal); if (active) { blog(LOG_INFO, "WASAPI: Device '%s' Terminated", device_name.c_str()); WaitForSingleObject(captureThread, INFINITE); } if (reconnecting) WaitForSingleObject(reconnectThread, INFINITE); ResetEvent(stopSignal); } inline WASAPISource::~WASAPISource() { Stop(); } 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; } 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(); UpdateSettings(settings); if (restart) Start(); } bool WASAPISource::InitDevice(IMMDeviceEnumerator *enumerator) { HRESULT res; if (isDefaultDevice) { res = enumerator->GetDefaultAudioEndpoint( isInputDevice ? eCapture : eRender, isInputDevice ? eCommunications : eConsole, device.Assign()); } else { wchar_t *w_id; os_utf8_to_wcs_ptr(device_id.c_str(), device_id.size(), &w_id); res = enumerator->GetDevice(w_id, device.Assign()); bfree(w_id); } return SUCCEEDED(res); } #define BUFFER_TIME_100NS (5*10000000) void WASAPISource::InitClient() { CoTaskMemPtr wfex; HRESULT res; DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK; res = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, (void**)client.Assign()); if (FAILED(res)) throw HRError("Failed to activate client context", res); res = client->GetMixFormat(&wfex); if (FAILED(res)) throw HRError("Failed to get mix format", res); InitFormat(wfex); if (!isInputDevice) flags |= AUDCLNT_STREAMFLAGS_LOOPBACK; 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); } void WASAPISource::InitRender() { CoTaskMemPtr wfex; HRESULT res; LPBYTE buffer; UINT32 frames; ComPtr client; res = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, (void**)client.Assign()); if (FAILED(res)) throw HRError("Failed to activate client context", res); res = client->GetMixFormat(&wfex); if (FAILED(res)) throw HRError("Failed to get mix format", res); 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); /* Silent loopback fix. Prevents audio stream from stopping and */ /* messing up timestamps and other weird glitches during silence */ /* by playing a silent sample all over again. */ res = client->GetBufferSize(&frames); if (FAILED(res)) throw HRError("Failed to get buffer size", res); res = client->GetService(__uuidof(IAudioRenderClient), (void**)render.Assign()); if (FAILED(res)) throw HRError("Failed to get render client", res); res = render->GetBuffer(frames, &buffer); if (FAILED(res)) throw HRError("Failed to get buffer", res); memset(buffer, 0, frames*wfex->nBlockAlign); render->ReleaseBuffer(frames, 0); } static speaker_layout ConvertSpeakerLayout(DWORD layout, WORD channels) { switch (layout) { case KSAUDIO_SPEAKER_QUAD: return SPEAKERS_QUAD; case KSAUDIO_SPEAKER_2POINT1: return SPEAKERS_2POINT1; case KSAUDIO_SPEAKER_4POINT1: return SPEAKERS_4POINT1; case KSAUDIO_SPEAKER_SURROUND: return SPEAKERS_SURROUND; case KSAUDIO_SPEAKER_5POINT1: return SPEAKERS_5POINT1; case KSAUDIO_SPEAKER_5POINT1_SURROUND: return SPEAKERS_5POINT1_SURROUND; case KSAUDIO_SPEAKER_7POINT1: return SPEAKERS_7POINT1; case KSAUDIO_SPEAKER_7POINT1_SURROUND: return SPEAKERS_7POINT1_SURROUND; } return (speaker_layout)channels; } void WASAPISource::InitFormat(WAVEFORMATEX *wfex) { DWORD layout = 0; if (wfex->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { WAVEFORMATEXTENSIBLE *ext = (WAVEFORMATEXTENSIBLE*)wfex; layout = ext->dwChannelMask; } /* WASAPI is always float */ sampleRate = wfex->nSamplesPerSec; format = AUDIO_FORMAT_FLOAT; speakers = ConvertSpeakerLayout(layout, wfex->nChannels); } void WASAPISource::InitCapture() { HRESULT res = client->GetService(__uuidof(IAudioCaptureClient), (void**)capture.Assign()); if (FAILED(res)) throw HRError("Failed to create capture context", res); res = client->SetEventHandle(receiveSignal); 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"; client->Start(); active = true; blog(LOG_INFO, "WASAPI: Device '%s' initialized", device_name.c_str()); } void WASAPISource::Initialize() { ComPtr enumerator; HRESULT res; res = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)enumerator.Assign()); if (FAILED(res)) throw HRError("Failed to create enumerator", res); if (!InitDevice(enumerator)) return; device_name = GetDeviceName(device); InitClient(); if (!isInputDevice) InitRender(); InitCapture(); } bool WASAPISource::TryInitialize() { try { Initialize(); } 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); } 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); } previouslyFailed = !active; return active; } void WASAPISource::Reconnect() { reconnecting = true; reconnectThread = CreateThread(nullptr, 0, WASAPISource::ReconnectThread, this, 0, nullptr); if (!reconnectThread.Valid()) blog(LOG_WARNING, "[WASAPISource::Reconnect] " "Failed to intiialize 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"); while (!WaitForSignal(source->stopSignal, RECONNECT_INTERVAL)) { if (source->TryInitialize()) break; } source->reconnectThread = nullptr; source->reconnecting = false; return 0; } bool WASAPISource::ProcessCaptureData() { HRESULT res; LPBYTE buffer; UINT32 frames; DWORD flags; UINT64 pos, ts; UINT captureSize = 0; while (true) { res = capture->GetNextPacketSize(&captureSize); if (FAILED(res)) { if (res != AUDCLNT_E_DEVICE_INVALIDATED) blog(LOG_WARNING, "[WASAPISource::GetCaptureData]" " capture->GetNextPacketSize" " failed: %lX", res); return false; } if (!captureSize) break; res = capture->GetBuffer(&buffer, &frames, &flags, &pos, &ts); if (FAILED(res)) { if (res != AUDCLNT_E_DEVICE_INVALIDATED) blog(LOG_WARNING, "[WASAPISource::GetCaptureData]" " capture->GetBuffer" " failed: %lX", res); return false; } obs_source_audio data = {}; data.data[0] = (const uint8_t*)buffer; data.frames = (uint32_t)frames; data.speakers = speakers; data.samples_per_sec = sampleRate; data.format = format; data.timestamp = useDeviceTiming ? ts*100 : os_gettime_ns(); obs_source_output_audio(source, &data); capture->ReleaseBuffer(frames); } 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; } 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 ? INFINITE : 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; } } 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(); } return 0; } /* ------------------------------------------------------------------------- */ static const char *GetWASAPIInputName(void*) { return obs_module_text("AudioInput"); } static const char *GetWASAPIOutputName(void*) { return obs_module_text("AudioOutput"); } static void GetWASAPIDefaultsInput(obs_data_t *settings) { obs_data_set_default_string(settings, OPT_DEVICE_ID, "default"); obs_data_set_default_bool(settings, OPT_USE_DEVICE_TIMING, false); } static void GetWASAPIDefaultsOutput(obs_data_t *settings) { obs_data_set_default_string(settings, OPT_DEVICE_ID, "default"); obs_data_set_default_bool(settings, OPT_USE_DEVICE_TIMING, true); } static void *CreateWASAPISource(obs_data_t *settings, obs_source_t *source, bool input) { try { return new WASAPISource(settings, source, input); } catch (const char *error) { blog(LOG_ERROR, "[CreateWASAPISource] %s", error); } return nullptr; } static void *CreateWASAPIInput(obs_data_t *settings, obs_source_t *source) { return CreateWASAPISource(settings, source, true); } static void *CreateWASAPIOutput(obs_data_t *settings, obs_source_t *source) { return CreateWASAPISource(settings, source, false); } static void DestroyWASAPISource(void *obj) { delete static_cast(obj); } static void UpdateWASAPISource(void *obj, obs_data_t *settings) { static_cast(obj)->Update(settings); } static obs_properties_t *GetWASAPIProperties(bool input) { obs_properties_t *props = obs_properties_create(); vector devices; obs_property_t *device_prop = obs_properties_add_list(props, OPT_DEVICE_ID, obs_module_text("Device"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); GetWASAPIAudioDevices(devices, input); if (devices.size()) obs_property_list_add_string(device_prop, obs_module_text("Default"), "default"); for (size_t i = 0; i < devices.size(); i++) { AudioDeviceInfo &device = devices[i]; obs_property_list_add_string(device_prop, device.name.c_str(), device.id.c_str()); } obs_properties_add_bool(props, OPT_USE_DEVICE_TIMING, obs_module_text("UseDeviceTiming")); return props; } static obs_properties_t *GetWASAPIPropertiesInput(void *) { return GetWASAPIProperties(true); } static obs_properties_t *GetWASAPIPropertiesOutput(void *) { return GetWASAPIProperties(false); } void RegisterWASAPIInput() { obs_source_info info = {}; info.id = "wasapi_input_capture"; info.type = OBS_SOURCE_TYPE_INPUT; info.output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE; info.get_name = GetWASAPIInputName; info.create = CreateWASAPIInput; info.destroy = DestroyWASAPISource; info.update = UpdateWASAPISource; info.get_defaults = GetWASAPIDefaultsInput; info.get_properties = GetWASAPIPropertiesInput; obs_register_source(&info); } void RegisterWASAPIOutput() { obs_source_info info = {}; info.id = "wasapi_output_capture"; info.type = OBS_SOURCE_TYPE_INPUT; info.output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE | OBS_SOURCE_DO_NOT_MONITOR; info.get_name = GetWASAPIOutputName; info.create = CreateWASAPIOutput; info.destroy = DestroyWASAPISource; info.update = UpdateWASAPISource; info.get_defaults = GetWASAPIDefaultsOutput; info.get_properties = GetWASAPIPropertiesOutput; obs_register_source(&info); }