From 348588254c6d5fd1501a205e542c9b0105138f6c Mon Sep 17 00:00:00 2001 From: jp9000 Date: Tue, 4 Mar 2014 07:07:13 -0700 Subject: [PATCH] Add WASAPI audio capture - Add WASAPI audio capture for windows, input and output - Check for null pointer in os_dlopen - Add exception-safe 'WinHandle' and 'CoTaskMemPtr' helper classes that will automatically call CloseHandle on handles and call CoTaskMemFree on certain types of memory returned from windows functions - Changed the wide <-> MBS/UTF8 conversion functions so that you use buffers (like these functions are *supposed* to behave), and changed the ones that allocate to a different naming scheme to be safe --- libobs-d3d11/d3d11-subsystem.cpp | 2 +- libobs/obs-properties.c | 7 + libobs/obs-properties.h | 3 + libobs/util/dstr.c | 4 +- libobs/util/platform-cocoa.m | 4 + libobs/util/platform-nix.c | 4 + libobs/util/platform-windows.c | 11 +- libobs/util/platform.c | 91 ++-- libobs/util/platform.h | 17 +- libobs/util/windows/CoTaskMemPtr.hpp | 43 ++ libobs/util/windows/WinHandle.hpp | 56 ++ obs/window-basic-main.cpp | 5 +- plugins/CMakeLists.txt | 1 + plugins/win-wasapi/CMakeLists.txt | 17 + plugins/win-wasapi/enum-wasapi.cpp | 93 ++++ plugins/win-wasapi/enum-wasapi.hpp | 19 + plugins/win-wasapi/plugin-main.cpp | 13 + plugins/win-wasapi/win-wasapi.cpp | 480 ++++++++++++++++++ vs/2013/OBS.sln | 10 + vs/2013/win-wasapi/win-wasapi.vcxproj | 176 +++++++ vs/2013/win-wasapi/win-wasapi.vcxproj.filters | 33 ++ 21 files changed, 1039 insertions(+), 50 deletions(-) create mode 100644 libobs/util/windows/CoTaskMemPtr.hpp create mode 100644 libobs/util/windows/WinHandle.hpp create mode 100644 plugins/win-wasapi/CMakeLists.txt create mode 100644 plugins/win-wasapi/enum-wasapi.cpp create mode 100644 plugins/win-wasapi/enum-wasapi.hpp create mode 100644 plugins/win-wasapi/plugin-main.cpp create mode 100644 plugins/win-wasapi/win-wasapi.cpp create mode 100644 vs/2013/win-wasapi/win-wasapi.vcxproj create mode 100644 vs/2013/win-wasapi/win-wasapi.vcxproj.filters diff --git a/libobs-d3d11/d3d11-subsystem.cpp b/libobs-d3d11/d3d11-subsystem.cpp index 59b58f992..8fee49ab1 100644 --- a/libobs-d3d11/d3d11-subsystem.cpp +++ b/libobs-d3d11/d3d11-subsystem.cpp @@ -170,7 +170,7 @@ void gs_device::InitDevice(gs_init_data *data, IDXGIAdapter *adapter) L""; char *adapterNameUTF8; - os_wcs_to_utf8(adapterName.c_str(), 0, &adapterNameUTF8); + os_wcs_to_utf8_ptr(adapterName.c_str(), 0, &adapterNameUTF8); blog(LOG_INFO, "Loading up D3D11 on adapter %s", adapterNameUTF8); bfree(adapterNameUTF8); diff --git a/libobs/obs-properties.c b/libobs/obs-properties.c index 50a409cd2..a30b2c9d3 100644 --- a/libobs/obs-properties.c +++ b/libobs/obs-properties.c @@ -190,6 +190,13 @@ static inline void *get_type_data(struct obs_property *prop, return get_property_data(prop); } +void obs_properties_add_bool(obs_properties_t props, const char *name, + const char *desc) +{ + if (!props || has_prop(props, name)) return; + new_prop(props, name, desc, OBS_PROPERTY_BOOL); +} + void obs_properties_add_int(obs_properties_t props, const char *name, const char *desc, int min, int max, int step) { diff --git a/libobs/obs-properties.h b/libobs/obs-properties.h index 94e56ba11..95e3bb324 100644 --- a/libobs/obs-properties.h +++ b/libobs/obs-properties.h @@ -25,6 +25,7 @@ extern "C" { enum obs_property_type { OBS_PROPERTY_INVALID, + OBS_PROPERTY_BOOL, OBS_PROPERTY_INT, OBS_PROPERTY_FLOAT, OBS_PROPERTY_TEXT, @@ -63,6 +64,8 @@ EXPORT obs_property_t obs_properties_get(obs_properties_t props, /* ------------------------------------------------------------------------- */ +EXPORT void obs_properties_add_bool(obs_properties_t props, const char *name, + const char *description); EXPORT void obs_properties_add_int(obs_properties_t props, const char *name, const char *description, int min, int max, int step); EXPORT void obs_properties_add_float(obs_properties_t props, const char *name, diff --git a/libobs/util/dstr.c b/libobs/util/dstr.c index ecd33cba8..d3c1f3571 100644 --- a/libobs/util/dstr.c +++ b/libobs/util/dstr.c @@ -585,13 +585,13 @@ void dstr_right(struct dstr *dst, const struct dstr *str, const size_t pos) void dstr_from_mbs(struct dstr *dst, const char *mbstr) { dstr_free(dst); - dst->len = os_mbs_to_utf8(mbstr, 0, &dst->array); + dst->len = os_mbs_to_utf8_ptr(mbstr, 0, &dst->array); } char *dstr_to_mbs(const struct dstr *str) { char *dst; - os_mbs_to_utf8(str->array, str->len, &dst); + os_mbs_to_utf8_ptr(str->array, str->len, &dst); return dst; } diff --git a/libobs/util/platform-cocoa.m b/libobs/util/platform-cocoa.m index eff7dc514..a70b19ed8 100644 --- a/libobs/util/platform-cocoa.m +++ b/libobs/util/platform-cocoa.m @@ -33,6 +33,10 @@ void *os_dlopen(const char *path) { struct dstr dylib_name; + + if (!path) + return NULL; + dstr_init_copy(&dylib_name, path); if (!dstr_find(&dylib_name, ".so")) dstr_cat(&dylib_name, ".so"); diff --git a/libobs/util/platform-nix.c b/libobs/util/platform-nix.c index f3a68ac9f..30586699d 100644 --- a/libobs/util/platform-nix.c +++ b/libobs/util/platform-nix.c @@ -28,6 +28,10 @@ void *os_dlopen(const char *path) { struct dstr dylib_name; + + if (!path) + return NULL; + dstr_init_copy(&dylib_name, path); if (!dstr_find(&dylib_name, ".so")) dstr_cat(&dylib_name, ".so"); diff --git a/libobs/util/platform-windows.c b/libobs/util/platform-windows.c index 0409c0f1e..3a06f831d 100644 --- a/libobs/util/platform-windows.c +++ b/libobs/util/platform-windows.c @@ -52,11 +52,14 @@ void *os_dlopen(const char *path) wchar_t *wpath; HMODULE h_library = NULL; + if (!path) + return NULL; + dstr_init_copy(&dll_name, path); if (!dstr_find(&dll_name, ".dll")) dstr_cat(&dll_name, ".dll"); - os_utf8_to_wcs(dll_name.array, 0, &wpath); + os_utf8_to_wcs_ptr(dll_name.array, 0, &wpath); h_library = LoadLibraryW(wpath); bfree(wpath); dstr_free(&dll_name); @@ -139,7 +142,7 @@ char *os_get_config_path(const char *name) SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, SHGFP_TYPE_CURRENT, path_utf16); - os_wcs_to_utf8(path_utf16, 0, &ptr); + os_wcs_to_utf8_ptr(path_utf16, 0, &ptr); dstr_init_move_array(&path, ptr); dstr_cat(&path, "\\"); dstr_cat(&path, name); @@ -152,7 +155,7 @@ bool os_file_exists(const char *path) HANDLE hFind; wchar_t *path_utf16; - if (!os_utf8_to_wcs(path, 0, &path_utf16)) + if (!os_utf8_to_wcs_ptr(path, 0, &path_utf16)) return false; hFind = FindFirstFileW(path_utf16, &wfd); @@ -168,7 +171,7 @@ int os_mkdir(const char *path) wchar_t *path_utf16; BOOL success; - if (!os_utf8_to_wcs(path, 0, &path_utf16)) + if (!os_utf8_to_wcs_ptr(path, 0, &path_utf16)) return MKDIR_ERROR; success = CreateDirectory(path_utf16, NULL); diff --git a/libobs/util/platform.c b/libobs/util/platform.c index 24754e452..b736ee384 100644 --- a/libobs/util/platform.c +++ b/libobs/util/platform.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013 Hugh Bailey + * Copyright (c) 2013-2014 Hugh Bailey * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -28,13 +28,13 @@ FILE *os_wfopen(const wchar_t *path, const char *mode) #ifdef _MSC_VER wchar_t *wcs_mode; - os_utf8_to_wcs(mode, 0, &wcs_mode); + os_utf8_to_wcs_ptr(mode, 0, &wcs_mode); file = _wfopen(path, wcs_mode); bfree(wcs_mode); #else char *mbs_path; - os_wcs_to_utf8(path, 0, &mbs_path); + os_wcs_to_utf8_ptr(path, 0, &mbs_path); file = fopen(mbs_path, mode); bfree(mbs_path); #endif @@ -46,7 +46,7 @@ FILE *os_fopen(const char *path, const char *mode) #ifdef _WIN32 wchar_t *wpath = NULL; FILE *file = NULL; - os_utf8_to_wcs(path, 0, &wpath); + os_utf8_to_wcs_ptr(path, 0, &wpath); file = os_wfopen(wpath, mode); bfree(wpath); @@ -95,7 +95,7 @@ size_t os_fread_mbs(FILE *file, char **pstr) } mbstr[size] = 0; - len = os_mbs_to_utf8(mbstr, size, pstr); + len = os_mbs_to_utf8_ptr(mbstr, size, pstr); bfree(mbstr); } @@ -180,7 +180,7 @@ bool os_quick_write_mbs_file(const char *path, const char *str, size_t len) if (!f) return false; - mbs_len = os_utf8_to_mbs(str, len, &mbs); + mbs_len = os_utf8_to_mbs_ptr(str, len, &mbs); if (mbs_len) fwrite(mbs, 1, mbs_len, f); bfree(mbs); @@ -205,74 +205,93 @@ bool os_quick_write_utf8_file(const char *path, const char *str, size_t len, return true; } -size_t os_mbs_to_wcs(const char *str, size_t len, wchar_t **pstr) +size_t os_mbs_to_wcs(const char *str, size_t len, wchar_t *dst) { - size_t out_len = mbstowcs(NULL, str, len); - wchar_t *dst = NULL; + size_t out_len = dst ? len : mbstowcs(NULL, str, len); - if (len) { - dst = bmalloc((out_len+1) * sizeof(wchar_t)); + if (len && dst) { mbstowcs(dst, str, out_len+1); dst[out_len] = 0; } - *pstr = dst; return out_len; } -size_t os_utf8_to_wcs(const char *str, size_t len, wchar_t **pstr) +size_t os_utf8_to_wcs(const char *str, size_t len, wchar_t *dst) { size_t in_len = len ? len : strlen(str); - size_t out_len = utf8_to_wchar(str, in_len, NULL, 0, 0); - wchar_t *dst = NULL; + size_t out_len = dst ? len : utf8_to_wchar(str, in_len, NULL, 0, 0); - if (out_len) { - dst = bmalloc((out_len+1) * sizeof(wchar_t)); + if (out_len && dst) { utf8_to_wchar(str, in_len, dst, out_len+1, 0); dst[out_len] = 0; } - *pstr = dst; return out_len; } -size_t os_wcs_to_mbs(const wchar_t *str, size_t len, char **pstr) +size_t os_wcs_to_mbs(const wchar_t *str, size_t len, char *dst) { - size_t out_len = wcstombs(NULL, str, len); - char *dst = NULL; + size_t out_len = dst ? len : wcstombs(NULL, str, len); - if (len) { - dst = bmalloc(out_len+1); + if (len && dst) { wcstombs(dst, str, out_len+1); dst[out_len] = 0; } - *pstr = dst; return out_len; } -size_t os_wcs_to_utf8(const wchar_t *str, size_t len, char **pstr) +size_t os_wcs_to_utf8(const wchar_t *str, size_t len, char *dst) { size_t in_len = (len != 0) ? len : wcslen(str); - size_t out_len = wchar_to_utf8(str, in_len, NULL, 0, 0); - char *dst = NULL; + size_t out_len = dst ? len : wchar_to_utf8(str, in_len, NULL, 0, 0); - if (out_len) { - dst = bmalloc(out_len+1); + if (out_len && dst) { wchar_to_utf8(str, in_len, dst, out_len+1, 0); dst[out_len] = 0; } - *pstr = dst; return out_len; } -size_t os_utf8_to_mbs(const char *str, size_t len, char **pstr) +size_t os_mbs_to_wcs_ptr(const char *str, size_t len, wchar_t **pstr) +{ + size_t out_len = os_mbs_to_wcs(str, len, NULL); + + *pstr = bmalloc((out_len+1) * sizeof(wchar_t)); + return os_mbs_to_wcs(str, out_len, *pstr); +} + +size_t os_utf8_to_wcs_ptr(const char *str, size_t len, wchar_t **pstr) +{ + size_t out_len = os_utf8_to_wcs(str, len, NULL); + *pstr = bmalloc((out_len+1) * sizeof(wchar_t)); + return os_utf8_to_wcs(str, out_len, *pstr); +} + +size_t os_wcs_to_mbs_ptr(const wchar_t *str, size_t len, char **pstr) +{ + size_t out_len = os_wcs_to_mbs(str, len, NULL); + + *pstr = bmalloc((out_len+1) * sizeof(char)); + return os_wcs_to_mbs(str, out_len, *pstr); +} + +size_t os_wcs_to_utf8_ptr(const wchar_t *str, size_t len, char **pstr) +{ + size_t out_len = os_wcs_to_utf8(str, len, NULL); + + *pstr = bmalloc((out_len+1) * sizeof(char)); + return os_wcs_to_utf8(str, out_len, *pstr); +} + +size_t os_utf8_to_mbs_ptr(const char *str, size_t len, char **pstr) { wchar_t *wstr = NULL; char *dst = NULL; - size_t wlen = os_utf8_to_wcs(str, len, &wstr); - size_t out_len = os_wcs_to_mbs(wstr, wlen, &dst); + size_t wlen = os_utf8_to_wcs_ptr(str, len, &wstr); + size_t out_len = os_wcs_to_mbs_ptr(wstr, wlen, &dst); bfree(wstr); *pstr = dst; @@ -280,12 +299,12 @@ size_t os_utf8_to_mbs(const char *str, size_t len, char **pstr) return out_len; } -size_t os_mbs_to_utf8(const char *str, size_t len, char **pstr) +size_t os_mbs_to_utf8_ptr(const char *str, size_t len, char **pstr) { wchar_t *wstr = NULL; char *dst = NULL; - size_t wlen = os_mbs_to_wcs(str, len, &wstr); - size_t out_len = os_wcs_to_utf8(wstr, wlen, &dst); + size_t wlen = os_mbs_to_wcs_ptr(str, len, &wstr); + size_t out_len = os_wcs_to_utf8_ptr(wstr, wlen, &dst); bfree(wstr); *pstr = dst; diff --git a/libobs/util/platform.h b/libobs/util/platform.h index 35210da2b..846acb71e 100644 --- a/libobs/util/platform.h +++ b/libobs/util/platform.h @@ -45,13 +45,18 @@ EXPORT char *os_quick_read_mbs_file(const char *path); EXPORT bool os_quick_write_mbs_file(const char *path, const char *str, size_t len); -EXPORT size_t os_mbs_to_wcs(const char *str, size_t len, wchar_t **pstr); -EXPORT size_t os_utf8_to_wcs(const char *str, size_t len, wchar_t **pstr); -EXPORT size_t os_wcs_to_mbs(const wchar_t *str, size_t len, char **pstr); -EXPORT size_t os_wcs_to_utf8(const wchar_t *str, size_t len, char **pstr); +EXPORT size_t os_mbs_to_wcs(const char *str, size_t len, wchar_t *dst); +EXPORT size_t os_utf8_to_wcs(const char *str, size_t len, wchar_t *dst); +EXPORT size_t os_wcs_to_mbs(const wchar_t *str, size_t len, char *dst); +EXPORT size_t os_wcs_to_utf8(const wchar_t *str, size_t len, char *dst); -EXPORT size_t os_utf8_to_mbs(const char *str, size_t len, char **pstr); -EXPORT size_t os_mbs_to_utf8(const char *str, size_t len, char **pstr); +EXPORT size_t os_mbs_to_wcs_ptr(const char *str, size_t len, wchar_t **pstr); +EXPORT size_t os_utf8_to_wcs_ptr(const char *str, size_t len, wchar_t **pstr); +EXPORT size_t os_wcs_to_mbs_ptr(const wchar_t *str, size_t len, char **pstr); +EXPORT size_t os_wcs_to_utf8_ptr(const wchar_t *str, size_t len, char **pstr); + +EXPORT size_t os_utf8_to_mbs_ptr(const char *str, size_t len, char **pstr); +EXPORT size_t os_mbs_to_utf8_ptr(const char *str, size_t len, char **pstr); EXPORT void *os_dlopen(const char *path); EXPORT void *os_dlsym(void *module, const char *func); diff --git a/libobs/util/windows/CoTaskMemPtr.hpp b/libobs/util/windows/CoTaskMemPtr.hpp new file mode 100644 index 000000000..4d85f5eb3 --- /dev/null +++ b/libobs/util/windows/CoTaskMemPtr.hpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2014 Hugh Bailey + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#pragma once + +template class CoTaskMemPtr { + T *ptr; + + inline void Clear() {if (ptr) CoTaskMemFree(ptr);} + +public: + inline CoTaskMemPtr() : ptr(NULL) {} + inline CoTaskMemPtr(T *ptr_) : ptr(ptr_) {} + inline ~CoTaskMemPtr() {Clear();} + + inline operator T*() const {return ptr;} + + inline CoTaskMemPtr& operator=(T* val) + { + Clear(); + ptr = val; + } + + inline T** operator&() + { + Clear(); + ptr = NULL; + return &ptr; + } +}; diff --git a/libobs/util/windows/WinHandle.hpp b/libobs/util/windows/WinHandle.hpp new file mode 100644 index 000000000..7112e753c --- /dev/null +++ b/libobs/util/windows/WinHandle.hpp @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2014 Hugh Bailey + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#pragma once + +class WinHandle { + HANDLE handle; + + inline void Clear() + { + if (handle && handle != INVALID_HANDLE_VALUE) + CloseHandle(handle); + } + +public: + inline WinHandle() : handle(NULL) {} + inline WinHandle(HANDLE handle_) : handle(handle_) {} + inline ~WinHandle() {Clear();} + + inline operator HANDLE() const {return handle;} + + inline WinHandle& operator=(HANDLE handle_) + { + if (handle_ != handle) { + Clear(); + handle = handle_; + } + + return *this; + } + + inline HANDLE* operator&() + { + Clear(); + handle = NULL; + return &handle; + } + + inline bool Valid() const + { + return handle && handle != INVALID_HANDLE_VALUE; + } +}; diff --git a/obs/window-basic-main.cpp b/obs/window-basic-main.cpp index 8a19f54e4..42b8c8d62 100644 --- a/obs/window-basic-main.cpp +++ b/obs/window-basic-main.cpp @@ -63,11 +63,14 @@ void OBSBasic::OBSInit() signal_handler_connect(obs_signalhandler(), "channel_change", OBSBasic::ChannelChanged, this); - /* TODO: this is a test */ + /* TODO: this is a test, all modules will be searched for and loaded + * automatically later */ obs_load_module("test-input"); obs_load_module("obs-ffmpeg"); #ifdef __APPLE__ obs_load_module("mac-capture"); +#elif _WIN32 + obs_load_module("win-wasapi"); #endif /* HACK: fixes a qt bug with native widgets with native repaint */ diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 2f3a5a853..e1566613b 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -2,6 +2,7 @@ include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/libobs") if(WIN32) add_subdirectory(dshow) + add_subdirectory(win-wasapi) elseif(APPLE) add_subdirectory(mac-capture) endif() diff --git a/plugins/win-wasapi/CMakeLists.txt b/plugins/win-wasapi/CMakeLists.txt new file mode 100644 index 000000000..6ab27bb96 --- /dev/null +++ b/plugins/win-wasapi/CMakeLists.txt @@ -0,0 +1,17 @@ +project(win-wasapi) + +set(win-wasapi_HEADERS + enum-wasapi.hpp) + +set(win-wasapi_SOURCES + win-wasapi.cpp + enum-wasapi.cpp + plugin-main.cpp) + +add_library(win-wasapi MODULE + ${win-wasapi_SOURCES} + ${win-wasapi_HEADERS}) +target_link_libraries(win-wasapi + libobs) + +install_obs_plugin(win-wasapi) diff --git a/plugins/win-wasapi/enum-wasapi.cpp b/plugins/win-wasapi/enum-wasapi.cpp new file mode 100644 index 000000000..d0ddc45e5 --- /dev/null +++ b/plugins/win-wasapi/enum-wasapi.cpp @@ -0,0 +1,93 @@ +#include "enum-wasapi.hpp" + +#include +#include +#include +#include +#include + +using namespace std; + +string GetDeviceName(IMMDevice *device) +{ + string device_name; + ComPtr store; + HRESULT res; + + if (SUCCEEDED(device->OpenPropertyStore(STGM_READ, store.Assign()))) { + PROPVARIANT nameVar; + + PropVariantInit(&nameVar); + res = store->GetValue(PKEY_Device_FriendlyName, &nameVar); + + if (SUCCEEDED(res)) { + size_t size; + + size = os_wcs_to_utf8(nameVar.pwszVal, 0, nullptr); + if (size) { + device_name.resize(size); + os_wcs_to_utf8(nameVar.pwszVal, size, + &device_name[0]); + } + } + } + + return device_name; +} + +void GetWASAPIAudioDevices_(vector &devices, bool input) +{ + ComPtr enumerator; + ComPtr collection; + UINT count; + HRESULT res; + + res = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, + __uuidof(IMMDeviceEnumerator), + (void**)enumerator.Assign()); + if (!res) + throw HRError("Failed to create enumerator", res); + + res = enumerator->EnumAudioEndpoints(input ? eCapture : eRender, + DEVICE_STATE_ACTIVE, collection.Assign()); + if (!res) + throw HRError("Failed to enumerate devices", res); + + res = collection->GetCount(&count); + if (FAILED(res)) + throw HRError("Failed to get device count", res); + + for (UINT i = 0; i < count; i++) { + ComPtr device; + CoTaskMemPtr w_id; + AudioDeviceInfo info; + size_t size; + + res = collection->Item(i, device.Assign()); + if (FAILED(res)) + continue; + + res = device->GetId(&w_id); + if (FAILED(res) || !w_id || !*w_id) + continue; + + info.name = GetDeviceName(device); + + size = os_wcs_to_utf8(w_id, 0, nullptr); + info.id.resize(size); + os_wcs_to_utf8(w_id, size, &info.id[0]); + } +} + +void GetWASAPIAudioDevices(vector &devices, bool input) +{ + devices.clear(); + + try { + GetWASAPIAudioDevices_(devices, input); + + } catch (HRError error) { + blog(LOG_ERROR, "[GetWASAPIAudioDevices] %s: %lX", + error.str, error.hr); + } +} diff --git a/plugins/win-wasapi/enum-wasapi.hpp b/plugins/win-wasapi/enum-wasapi.hpp new file mode 100644 index 000000000..5041650cc --- /dev/null +++ b/plugins/win-wasapi/enum-wasapi.hpp @@ -0,0 +1,19 @@ +#pragma once + +#define WIN32_MEAN_AND_LEAN +#include +#include +#include +#include +#include + +#include +#include + +struct AudioDeviceInfo { + std::string name; + std::string id; +}; + +std::string GetDeviceName(IMMDevice *device); +void GetWASAPIAudioDevices(std::vector &devices, bool input); diff --git a/plugins/win-wasapi/plugin-main.cpp b/plugins/win-wasapi/plugin-main.cpp new file mode 100644 index 000000000..c8ac6ac79 --- /dev/null +++ b/plugins/win-wasapi/plugin-main.cpp @@ -0,0 +1,13 @@ +#include + +OBS_DECLARE_MODULE() + +extern struct obs_source_info wasapiInput; +extern struct obs_source_info wasapiOutput; + +bool obs_module_load(uint32_t libobs_ver) +{ + obs_register_source(&wasapiInput); + obs_register_source(&wasapiOutput); + return true; +} diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp new file mode 100644 index 000000000..8dcd27bbd --- /dev/null +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -0,0 +1,480 @@ +#include "enum-wasapi.hpp" + +#include +#include +#include +#include +#include +#include + +using namespace std; + +#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; + + obs_source_t source; + string device_id; + string device_name; + bool isInputDevice; + bool useDeviceTiming; + bool isDefaultDevice; + + bool reconnecting; + WinHandle reconnectThread; + + bool active; + 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(); + + void Reconnect(); + + bool InitDevice(IMMDeviceEnumerator *enumerator); + void InitName(); + void InitClient(); + void InitFormat(WAVEFORMATEX *wfex); + void InitCapture(); + void Initialize(); + + bool TryInitialize(); + +public: + WASAPISource(obs_data_t settings, obs_source_t source_, bool input); + inline ~WASAPISource(); + + void UpdateSettings(obs_data_t settings); +}; + +WASAPISource::WASAPISource(obs_data_t settings, obs_source_t source_, + bool input) + : reconnecting (false), + active (false), + reconnectThread (nullptr), + captureThread (nullptr), + source (source_), + isInputDevice (input) +{ + obs_data_set_default_string(settings, "device_id", "default"); + obs_data_set_default_bool(settings, "use_device_timing", true); + 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"; + + if (!TryInitialize()) { + blog(LOG_INFO, "[WASAPISource::WASAPISource] " + "Device '%s' not found. Waiting for device", + device_id.c_str()); + Reconnect(); + } +} + +inline WASAPISource::~WASAPISource() +{ + SetEvent(stopSignal); + + if (active) + WaitForSingleObject(captureThread, INFINITE); + if (reconnecting) + WaitForSingleObject(reconnectThread, INFINITE); +} + +void WASAPISource::UpdateSettings(obs_data_t settings) +{ + device_id = obs_data_getstring(settings, "device_id"); + useDeviceTiming = obs_data_getbool(settings, "useDeviceTiming"); + isDefaultDevice = _strcmpi(device_id.c_str(), "default") == 0; +} + +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); +} + +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(); + InitCapture(); +} + +bool WASAPISource::TryInitialize() +{ + try { + Initialize(); + + } catch (HRError error) { + blog(LOG_ERROR, "[WASAPISource::TryInitialize]:[%s] %s: %lX", + device_name.empty() ? + device_id.c_str() : device_name.c_str(), + error.str, error.hr); + + } catch (const char *error) { + blog(LOG_ERROR, "[WASAPISource::TryInitialize]:[%s] %s", + device_name.empty() ? + device_id.c_str() : device_name.c_str(), + error); + } + + return active; +} + +void WASAPISource::Reconnect() +{ + reconnecting = true; + reconnectThread = CreateThread(nullptr, 0, + WASAPISource::ReconnectThread, this, + 0, nullptr); + + if (!reconnectThread.Valid()) + blog(LOG_ERROR, "[WASAPISource::Reconnect] " + "Failed to intiialize reconnect thread: %d", + 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; + + 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_ERROR, "[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_ERROR, "[WASAPISource::GetCaptureData]" + " capture->GetBuffer" + " failed: %lX", res); + return false; + } + + 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 + }; + + 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(const char *locale) +{ + /* TODO: translate */ + return "Audio Input Capture (WASAPI)"; +} + +static const char *GetWASAPIOutputName(const char *locale) +{ + /* TODO: translate */ + return "Audio Output Capture (WASAPI)"; +} + +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 obs_properties_t GetWASAPIProperties(const char *locale, bool input) +{ + obs_properties_t props = obs_properties_create(); + vector devices; + + /* TODO: translate */ + obs_property_t device_prop = obs_properties_add_list(props, + "device_id", "Device", + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + + GetWASAPIAudioDevices(devices, input); + + for (size_t i = 0; i < devices.size(); i++) { + AudioDeviceInfo &device = devices[i]; + obs_property_list_add_item(device_prop, + device.name.c_str(), device.id.c_str()); + } + + obs_properties_add_bool(props, "use_device_timing", + "Use Device Timing"); + + return props; +} + +static obs_properties_t GetWASAPIPropertiesInput(const char *locale) +{ + return GetWASAPIProperties(locale, true); +} + +static obs_properties_t GetWASAPIPropertiesOutput(const char *locale) +{ + return GetWASAPIProperties(locale, false); +} + +struct obs_source_info wasapiInput { + "wasapi_input_capture", + OBS_SOURCE_TYPE_INPUT, + OBS_SOURCE_AUDIO, + GetWASAPIInputName, + CreateWASAPIInput, + DestroyWASAPISource, + GetWASAPIPropertiesInput, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, +}; + +struct obs_source_info wasapiOutput { + "wasapi_output_capture", + OBS_SOURCE_TYPE_INPUT, + OBS_SOURCE_AUDIO, + GetWASAPIOutputName, + CreateWASAPIOutput, + DestroyWASAPISource, + GetWASAPIPropertiesOutput, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, +}; diff --git a/vs/2013/OBS.sln b/vs/2013/OBS.sln index cc6fabec2..c4a83d630 100644 --- a/vs/2013/OBS.sln +++ b/vs/2013/OBS.sln @@ -39,6 +39,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "obs-ffmpeg", "obs-ffmpeg\ob EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "obs-studio", "obs-studio\obs-studio.vcxproj", "{B12702AD-ABFB-343A-A199-8E24837244A3}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "win-wasapi", "win-wasapi\win-wasapi.vcxproj", "{A3D24C9D-669D-4DDF-91BA-152D7DDD7C04}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Win32 = Debug|Win32 @@ -119,6 +121,14 @@ Global {B12702AD-ABFB-343A-A199-8E24837244A3}.Release|Win32.Build.0 = Release|Win32 {B12702AD-ABFB-343A-A199-8E24837244A3}.Release|x64.ActiveCfg = Release|x64 {B12702AD-ABFB-343A-A199-8E24837244A3}.Release|x64.Build.0 = Release|x64 + {A3D24C9D-669D-4DDF-91BA-152D7DDD7C04}.Debug|Win32.ActiveCfg = Debug|Win32 + {A3D24C9D-669D-4DDF-91BA-152D7DDD7C04}.Debug|Win32.Build.0 = Debug|Win32 + {A3D24C9D-669D-4DDF-91BA-152D7DDD7C04}.Debug|x64.ActiveCfg = Debug|x64 + {A3D24C9D-669D-4DDF-91BA-152D7DDD7C04}.Debug|x64.Build.0 = Debug|x64 + {A3D24C9D-669D-4DDF-91BA-152D7DDD7C04}.Release|Win32.ActiveCfg = Release|Win32 + {A3D24C9D-669D-4DDF-91BA-152D7DDD7C04}.Release|Win32.Build.0 = Release|Win32 + {A3D24C9D-669D-4DDF-91BA-152D7DDD7C04}.Release|x64.ActiveCfg = Release|x64 + {A3D24C9D-669D-4DDF-91BA-152D7DDD7C04}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/vs/2013/win-wasapi/win-wasapi.vcxproj b/vs/2013/win-wasapi/win-wasapi.vcxproj new file mode 100644 index 000000000..290433f8c --- /dev/null +++ b/vs/2013/win-wasapi/win-wasapi.vcxproj @@ -0,0 +1,176 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {A3D24C9D-669D-4DDF-91BA-152D7DDD7C04} + Win32Proj + winwasapi + + + + DynamicLibrary + true + v120 + Unicode + + + DynamicLibrary + true + v120 + Unicode + + + DynamicLibrary + false + v120 + true + Unicode + + + DynamicLibrary + false + v120 + true + Unicode + + + + + + + + + + + + + + + + + + + true + + + true + + + false + + + false + + + + + + Level3 + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;WINCAPTURE_EXPORTS;%(PreprocessorDefinitions) + ../../../libobs + + + Windows + true + libobs.lib;%(AdditionalDependencies) + $(OutDir);%(AdditionalLibraryDirectories) + + + copy "$(OutDir)$(TargetName)$(TargetExt)" "../../../build/obs-plugins/32bit/$(TargetName)$(TargetExt)" + + + + + + + Level3 + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;WINCAPTURE_EXPORTS;%(PreprocessorDefinitions) + ../../../libobs + + + Windows + true + libobs.lib;%(AdditionalDependencies) + $(OutDir);%(AdditionalLibraryDirectories) + + + copy "$(OutDir)$(TargetName)$(TargetExt)" "../../../build/obs-plugins/64bit/$(TargetName)$(TargetExt)" + + + + + Level3 + + + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;WINCAPTURE_EXPORTS;%(PreprocessorDefinitions) + ../../../libobs + + + Windows + true + true + true + libobs.lib;%(AdditionalDependencies) + $(OutDir);%(AdditionalLibraryDirectories) + + + copy "$(OutDir)$(TargetName)$(TargetExt)" "../../../build/obs-plugins/32bit/$(TargetName)$(TargetExt)" + + + + + Level3 + + + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;WINCAPTURE_EXPORTS;%(PreprocessorDefinitions) + ../../../libobs + + + Windows + true + true + true + libobs.lib;%(AdditionalDependencies) + $(OutDir);%(AdditionalLibraryDirectories) + + + copy "$(OutDir)$(TargetName)$(TargetExt)" "../../../build/obs-plugins/64bit/$(TargetName)$(TargetExt)" + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vs/2013/win-wasapi/win-wasapi.vcxproj.filters b/vs/2013/win-wasapi/win-wasapi.vcxproj.filters new file mode 100644 index 000000000..54c46a80f --- /dev/null +++ b/vs/2013/win-wasapi/win-wasapi.vcxproj.filters @@ -0,0 +1,33 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + \ No newline at end of file