From 5628a9bd23229a413528b6957371c79730d168ab Mon Sep 17 00:00:00 2001 From: pkv Date: Thu, 27 Oct 2022 09:37:15 +0200 Subject: [PATCH 1/9] win-asio: Add ASIO source & output This adds: - an ASIO Input source, allowing audio capture of devices using Steinberg ASIO audio SDK on windows. This allows low latency capture. - an ASIO output, which will allow routing audio from OBS to any ASIO device. A per channel routing is provided. The UI to setup the ASIO output is provided in a later commit. Signed-off-by: pkv --- plugins/CMakeLists.txt | 1 + plugins/win-asio/CMakeLists.txt | 19 + plugins/win-asio/asio-callbacks.c | 85 + plugins/win-asio/asio-callbacks.h | 33 + plugins/win-asio/asio-common.h | 47 + plugins/win-asio/asio-compat.h | 138 ++ plugins/win-asio/asio-device-list.c | 222 +++ plugins/win-asio/asio-device-list.h | 45 + plugins/win-asio/asio-device.c | 1372 +++++++++++++++++ plugins/win-asio/asio-device.h | 144 ++ plugins/win-asio/asio-format.c | 182 +++ plugins/win-asio/asio-format.h | 38 + plugins/win-asio/byteorder.h | 102 ++ .../win-asio/cmake/windows/obs-module.rc.in | 24 + plugins/win-asio/data/locale/en-US.ini | 105 ++ plugins/win-asio/iasiodrv.h | 102 ++ plugins/win-asio/plugin-main.c | 62 + plugins/win-asio/win-asio.c | 861 +++++++++++ plugins/win-asio/win-asio.h | 62 + 19 files changed, 3644 insertions(+) create mode 100644 plugins/win-asio/CMakeLists.txt create mode 100644 plugins/win-asio/asio-callbacks.c create mode 100644 plugins/win-asio/asio-callbacks.h create mode 100644 plugins/win-asio/asio-common.h create mode 100644 plugins/win-asio/asio-compat.h create mode 100644 plugins/win-asio/asio-device-list.c create mode 100644 plugins/win-asio/asio-device-list.h create mode 100644 plugins/win-asio/asio-device.c create mode 100644 plugins/win-asio/asio-device.h create mode 100644 plugins/win-asio/asio-format.c create mode 100644 plugins/win-asio/asio-format.h create mode 100644 plugins/win-asio/byteorder.h create mode 100644 plugins/win-asio/cmake/windows/obs-module.rc.in create mode 100644 plugins/win-asio/data/locale/en-US.ini create mode 100644 plugins/win-asio/iasiodrv.h create mode 100644 plugins/win-asio/plugin-main.c create mode 100644 plugins/win-asio/win-asio.c create mode 100644 plugins/win-asio/win-asio.h diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index c12f015c8b85ae..1ab43cddb828b6 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -85,6 +85,7 @@ add_obs_plugin(rtmp-services) add_obs_plugin(sndio PLATFORMS LINUX FREEBSD OPENBSD) add_obs_plugin(text-freetype2) add_obs_plugin(vlc-video WITH_MESSAGE) +add_obs_plugin(win-asio PLATFORMS WINDOWS) add_obs_plugin(win-capture PLATFORMS WINDOWS) add_obs_plugin(win-dshow PLATFORMS WINDOWS) add_obs_plugin(win-wasapi PLATFORMS WINDOWS) diff --git a/plugins/win-asio/CMakeLists.txt b/plugins/win-asio/CMakeLists.txt new file mode 100644 index 00000000000000..fc573b1f3e8461 --- /dev/null +++ b/plugins/win-asio/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.28...3.30) + +add_library(win-asio MODULE) +add_library(OBS::asio ALIAS win-asio) +set(MODULE_DESCRIPTION "OBS ASIO module") + +target_sources( + win-asio + PRIVATE asio-device.c asio-format.c asio-device-list.c asio-callbacks.c plugin-main.c win-asio.c + PUBLIC asio-device.h asio-format.h asio-device-list.h asio-callbacks.h iasiodrv.h byteorder.h asio-compat.h +) + +target_link_libraries(win-asio PRIVATE OBS::libobs OBS::frontend-api) + +configure_file(cmake/windows/obs-module.rc.in win-asio.rc) +target_sources(win-asio PRIVATE win-asio.rc) + +set_property(TARGET win-asio APPEND PROPERTY AUTORCC_OPTIONS --format-version 1) +set_target_properties_obs(win-asio PROPERTIES FOLDER plugins PREFIX "") diff --git a/plugins/win-asio/asio-callbacks.c b/plugins/win-asio/asio-callbacks.c new file mode 100644 index 00000000000000..d29c53f81e6318 --- /dev/null +++ b/plugins/win-asio/asio-callbacks.c @@ -0,0 +1,85 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of win-asio. + It uses the Steinberg ASIO SDK, which is licensed under the GNU GPL v3. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "asio-callbacks.h" + +#include "asio-device.h" + +/* ASIO callbacks are quite a pain because they don't store the 'this' pointer of the COM driver. In C++ we could do + * some template tricks as done by JUCE. But in C, we're more restricted, hence the use of these macros. + * The number of callback sets is the number of supported devices. So up to 16, which is plenty enough! + */ +#define DEFINE_CALLBACK_SET(N) \ + void buffer_switch_##N(long index, long directProcess) {{ \ + if (current_asio_devices[N]) { \ + asio_device_callback(current_asio_devices[N], index); } \ + }} \ + ASIOTime* buffer_switch_time_info_##N(ASIOTime* params, long index, long directProcess) {{ \ + UNUSED_PARAMETER(params); \ + UNUSED_PARAMETER(index); \ + UNUSED_PARAMETER(directProcess); \ + return NULL; \ + }} \ + long asio_message_callback_##N(long selector, long value, void* message, double* opt) {{ \ + if (current_asio_devices[N]) { \ + return asio_device_asio_message_callback(current_asio_devices[N], selector, value, message, opt); } \ + return 0; \ + }} \ + void sample_rate_changed_callback_##N(ASIOSampleRate rate) {{ \ + UNUSED_PARAMETER(rate); \ + if (current_asio_devices[N]) { \ + asio_device_reset_request(current_asio_devices[N]); } \ + }} + +DEFINE_CALLBACK_SET(0) +DEFINE_CALLBACK_SET(1) +DEFINE_CALLBACK_SET(2) +DEFINE_CALLBACK_SET(3) +DEFINE_CALLBACK_SET(4) +DEFINE_CALLBACK_SET(5) +DEFINE_CALLBACK_SET(6) +DEFINE_CALLBACK_SET(7) +DEFINE_CALLBACK_SET(8) +DEFINE_CALLBACK_SET(9) +DEFINE_CALLBACK_SET(10) +DEFINE_CALLBACK_SET(11) +DEFINE_CALLBACK_SET(12) +DEFINE_CALLBACK_SET(13) +DEFINE_CALLBACK_SET(14) +DEFINE_CALLBACK_SET(15) + +const struct asio_callback_set callback_sets[MAX_NUM_ASIO_DEVICES] = { + {buffer_switch_0, buffer_switch_time_info_0, asio_message_callback_0, sample_rate_changed_callback_0}, + {buffer_switch_1, buffer_switch_time_info_1, asio_message_callback_1, sample_rate_changed_callback_1}, + {buffer_switch_2, buffer_switch_time_info_2, asio_message_callback_2, sample_rate_changed_callback_2}, + {buffer_switch_3, buffer_switch_time_info_3, asio_message_callback_3, sample_rate_changed_callback_3}, + {buffer_switch_4, buffer_switch_time_info_4, asio_message_callback_4, sample_rate_changed_callback_4}, + {buffer_switch_5, buffer_switch_time_info_5, asio_message_callback_5, sample_rate_changed_callback_5}, + {buffer_switch_6, buffer_switch_time_info_6, asio_message_callback_6, sample_rate_changed_callback_6}, + {buffer_switch_7, buffer_switch_time_info_7, asio_message_callback_7, sample_rate_changed_callback_7}, + {buffer_switch_8, buffer_switch_time_info_8, asio_message_callback_8, sample_rate_changed_callback_8}, + {buffer_switch_9, buffer_switch_time_info_9, asio_message_callback_9, sample_rate_changed_callback_9}, + {buffer_switch_10, buffer_switch_time_info_10, asio_message_callback_10, sample_rate_changed_callback_10}, + {buffer_switch_11, buffer_switch_time_info_11, asio_message_callback_11, sample_rate_changed_callback_11}, + {buffer_switch_12, buffer_switch_time_info_12, asio_message_callback_12, sample_rate_changed_callback_12}, + {buffer_switch_13, buffer_switch_time_info_13, asio_message_callback_13, sample_rate_changed_callback_13}, + {buffer_switch_14, buffer_switch_time_info_14, asio_message_callback_14, sample_rate_changed_callback_14}, + {buffer_switch_15, buffer_switch_time_info_15, asio_message_callback_15, sample_rate_changed_callback_15}, +}; diff --git a/plugins/win-asio/asio-callbacks.h b/plugins/win-asio/asio-callbacks.h new file mode 100644 index 00000000000000..cc0d70ce3e515e --- /dev/null +++ b/plugins/win-asio/asio-callbacks.h @@ -0,0 +1,33 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of win-asio. + It uses the Steinberg ASIO SDK, which is licensed under the GNU GPL v3. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "asio-common.h" +#include "asio-compat.h" + +struct asio_callback_set { + void (*buffer_switch)(long index, long directProcess); + ASIOTime *(*buffer_switch_time_info)(ASIOTime *params, long index, long directProcess); + long (*asio_message)(long selector, long value, void *opt, double *message); + void (*sample_rate_changed)(ASIOSampleRate); +}; + +extern const struct asio_callback_set callback_sets[MAX_NUM_ASIO_DEVICES]; diff --git a/plugins/win-asio/asio-common.h b/plugins/win-asio/asio-common.h new file mode 100644 index 00000000000000..0ee335dc265a25 --- /dev/null +++ b/plugins/win-asio/asio-common.h @@ -0,0 +1,47 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of win-asio. + It uses the Steinberg ASIO SDK, which is licensed under the GNU GPL v3. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +#define MAX_DEVICE_CHANNELS 32 +#define MAX_CH_NAME_LENGTH 32 +#define MAX_NUM_ASIO_DEVICES 16 + +#ifndef MIN +#define MIN(x, y) ((x) < (y) ? (x) : (y)) +#endif +#ifndef MAX +#define MAX(a, b) (((a) > (b)) ? (a) : (b)) +#endif + +#define ASIO_DEVICE_LOG(level, format, ...) \ + blog(level, "[asio_device '%s']: " format, (dev && dev->device_name) ? dev->device_name : "(null)", ##__VA_ARGS__) + +#define ASIO_SCANNER_LOG(level, format, ...) \ + blog(level, "[asio_device_list]: " format, ##__VA_ARGS__) + +#define debug(format, ...) ASIO_DEVICE_LOG(LOG_DEBUG, format, ##__VA_ARGS__) +#define warn(format, ...) ASIO_DEVICE_LOG(LOG_WARNING, format, ##__VA_ARGS__) +#define info(format, ...) ASIO_DEVICE_LOG(LOG_INFO, format, ##__VA_ARGS__) +#define infoscanner(format, ...) ASIO_SCANNER_LOG(LOG_INFO, format, ##__VA_ARGS__) +#define error(format, ...) ASIO_DEVICE_LOG(LOG_ERROR, format, ##__VA_ARGS__) +#define errorscanner(format, ...) ASIO_SCANNER_LOG(LOG_ERROR, format, ##__VA_ARGS__) diff --git a/plugins/win-asio/asio-compat.h b/plugins/win-asio/asio-compat.h new file mode 100644 index 00000000000000..af01c704122dc8 --- /dev/null +++ b/plugins/win-asio/asio-compat.h @@ -0,0 +1,138 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of win-asio. + Minimal ASIO compatibility header for the OBS ASIO host. + It defines the subset of ASIO types, enums, and structs required to + communicate with ASIO drivers via the IASIO COM interface. + This file contains a reduced set of definitions derived from the + Steinberg ASIO SDK (GPL v3), limited to what is required by this host. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +/* --------------------- Basic Types --------------------- */ +typedef long ASIOBool; +typedef long ASIOError; +typedef double ASIOSampleRate; +typedef long ASIOSampleType; + +/* --------------------- Boolean Values ------------------ */ +#ifndef ASIOTrue +#define ASIOTrue 1 +#define ASIOFalse 0 +#endif + +/* --------------------- Error Codes --------------------- */ +enum { + ASE_OK = 0, + ASE_SUCCESS = 0x3f4847a0, + ASE_NotPresent = -1000, + ASE_HWMalfunction = -999, + ASE_InvalidParameter = -998, + ASE_InvalidMode = -997, + ASE_SPNotAdvancing = -996, + ASE_NoClock = -995, + ASE_NoMemory = -994 +}; + +/* --------------------- Sample Types -------------------- */ +enum { + ASIOSTInt16MSB = 0, + ASIOSTInt24MSB = 1, + ASIOSTInt32MSB = 2, + ASIOSTFloat32MSB = 3, + ASIOSTFloat64MSB = 4, + ASIOSTInt32MSB16 = 8, + ASIOSTInt32MSB18 = 9, + ASIOSTInt32MSB20 = 10, + ASIOSTInt32MSB24 = 11, + ASIOSTInt16LSB = 16, + ASIOSTInt24LSB = 17, + ASIOSTInt32LSB = 18, + ASIOSTFloat32LSB = 19, + ASIOSTFloat64LSB = 20, + ASIOSTInt32LSB16 = 24, + ASIOSTInt32LSB18 = 25, + ASIOSTInt32LSB20 = 26, + ASIOSTInt32LSB24 = 27, + ASIOSTLastEntry +}; + +/* --------------------- Selector Constants --------------- */ +enum { + kAsioSelectorSupported = 1, + kAsioEngineVersion = 2, + kAsioResetRequest = 3, + kAsioBufferSizeChange = 4, + kAsioResyncRequest = 5, + kAsioLatenciesChanged = 6, + kAsioSupportsTimeInfo = 7, + kAsioSupportsTimeCode = 8, + kAsioMMCCommand = 9, + kAsioSupportsInputMonitor = 10, + kAsioSupportsInputGain = 11, + kAsioSupportsOutputGain = 12, + kAsioSupportsInputMeter = 13, + kAsioSupportsOutputMeter = 14, + kAsioOverload = 15, +}; + +/* --------------------- ASIOFuture selector --------------------- */ +#define kAsioCanReportOverload 0x24042012 + +/* --------------------- Channel Info --------------------- */ +typedef struct ASIOChannelInfo { + long channel; + long isInput; + long isActive; + long channelGroup; + ASIOSampleType type; + char name[64]; +} ASIOChannelInfo; + +/* --------------------- Clock Source --------------------- */ +typedef struct ASIOClockSource { + long index; + long associatedChannel; + long associatedGroup; + long isCurrentSource; + char name[32]; +} ASIOClockSource; + +/* --------------------- Buffer Info ---------------------- */ +typedef struct ASIOBufferInfo { + long isInput; + long channelNum; + void *buffers[2]; +} ASIOBufferInfo; + +/* --------------------- Time Info (stub) ----------------- */ +typedef struct ASIOTime { + double sampleRate; + int64_t samplePosition; + int64_t systemTime; +} ASIOTime; + +/* --------------------- Callbacks ------------------------ */ +typedef struct ASIOCallbacks { + void (*bufferSwitch)(long doubleBufferIndex, long directProcess); + ASIOTime *(*bufferSwitchTimeInfo)(ASIOTime *params, long doubleBufferIndex, long directProcess); + long (*asioMessage)(long selector, long value, void *message, double *opt); + void (*sampleRateDidChange)(ASIOSampleRate sRate); +} ASIOCallbacks; diff --git a/plugins/win-asio/asio-device-list.c b/plugins/win-asio/asio-device-list.c new file mode 100644 index 00000000000000..52f40d1c9beb0f --- /dev/null +++ b/plugins/win-asio/asio-device-list.c @@ -0,0 +1,222 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of win-asio. + It uses the Steinberg ASIO SDK, which is licensed under the GNU GPL v3. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "asio-device-list.h" + +#include + +#include +#include +#include +#include + +// Blacklist taken from JUCE to which I added Realtek ASIO. +static const char *blacklisted_drivers[] = {"ASIO DirectX Full Duplex", "ASIO Multimedia Driver", "Realtek ASIO", NULL}; + +static bool is_blacklisted_driver(const char *name) +{ + for (int i = 0; blacklisted_drivers[i]; ++i) { + if (strcmp(name, blacklisted_drivers[i]) == 0) { + return true; + } + } + return false; +} + +static bool read_asio_driver_info(HKEY hKey, const char *subkey, struct asio_driver_entry *entry) +{ + HKEY driverKey; + LONG err; + DWORD valueSize; + wchar_t clsid_str[256]; + wchar_t desc_str[256]; + + if (is_blacklisted_driver(subkey)) { + infoscanner("Skipping blacklisted driver: %s", subkey); + return false; + } + + err = RegOpenKeyExA(hKey, subkey, 0, KEY_READ, &driverKey); + if (err != ERROR_SUCCESS) { + infoscanner("Failed to open subkey: %s (err=%ld)", subkey, err); + return false; + } + + valueSize = sizeof(clsid_str); + err = RegGetValueW(driverKey, NULL, L"CLSID", RRF_RT_REG_SZ, NULL, clsid_str, &valueSize); + if (err != ERROR_SUCCESS) { + infoscanner("Could not read CLSID for %s (err=%ld)", subkey, err); + RegCloseKey(driverKey); + return false; + } + + if (CLSIDFromString(clsid_str, &entry->clsid) != S_OK) { + infoscanner("CLSIDFromString failed for %s → %ls", subkey, clsid_str); + RegCloseKey(driverKey); + return false; + } + + valueSize = sizeof(desc_str); + err = RegGetValueW(driverKey, NULL, L"Description", RRF_RT_REG_SZ, NULL, desc_str, &valueSize); + if (err != ERROR_SUCCESS) { + infoscanner("Missing Description for %s, using subkey as name", subkey); + StringCchCopyA(entry->name, sizeof(entry->name), subkey); + } else { + WideCharToMultiByte(CP_UTF8, 0, desc_str, -1, entry->name, sizeof(entry->name), NULL, NULL); + } + + entry->loadable = true; + + RegCloseKey(driverKey); + return true; +} + +struct asio_device_list *asio_device_list_create(void) +{ + HKEY hKey; + LONG result = RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\ASIO", 0, KEY_READ, &hKey); + if (result != ERROR_SUCCESS) { + blog(LOG_ERROR, "[ASIO] Failed to open registry key SOFTWARE\\ASIO (error code: %ld)", result); + return NULL; + } + + blog(LOG_INFO, "[ASIO] Successfully opened registry key SOFTWARE\\ASIO"); + + struct asio_device_list *list = calloc(1, sizeof(struct asio_device_list)); + if (!list) { + blog(LOG_ERROR, "[ASIO] Failed to allocate memory for ASIO device list"); + RegCloseKey(hKey); + return NULL; + } + + DWORD index = 0; + char subkey[256]; + DWORD subkey_len; + + while (true) { + subkey_len = sizeof(subkey); + LONG enum_result = RegEnumKeyExA(hKey, index++, subkey, &subkey_len, NULL, NULL, NULL, NULL); + if (enum_result != ERROR_SUCCESS) { + break; + } + + if (list->count >= MAX_NUM_ASIO_DEVICES) { + errorscanner("Max number of drivers (%d) reached, skipping others.", MAX_NUM_ASIO_DEVICES); + break; + } + + struct asio_driver_entry *entry = &list->drivers[list->count]; + if (read_asio_driver_info(hKey, subkey, entry)) { + blog(LOG_INFO, "[ASIO] Found ASIO driver: %s", entry->name); + list->count++; + } else { + blog(LOG_WARNING, "[ASIO] Failed to read driver info for subkey: %s", subkey); + } + } + + blog(LOG_INFO, "[ASIO] Total drivers found: %zu", list->count); + list->has_scanned = true; + + RegCloseKey(hKey); + return list; +} + +void asio_device_list_destroy(struct asio_device_list *list) +{ + if (list) { + free(list); + } +} + +size_t asio_device_list_get_count(const struct asio_device_list *list) +{ + return list ? list->count : 0; +} + +const char *asio_device_list_get_name(const struct asio_device_list *list, size_t index) +{ + if (!list || index >= list->count) { + return NULL; + } + + return list->drivers[index].name; +} + +int find_free_driver_slot() +{ + for (int i = 0; i < MAX_NUM_ASIO_DEVICES; ++i) { + if (current_asio_devices[i] == NULL) { + return i; + } + } + errorscanner("You have more than %i asio devices, which is the maximum supported by OBS.", + MAX_NUM_ASIO_DEVICES); + return -1; +} + +int asio_device_list_get_index_from_driver_name(const struct asio_device_list *list, const char *name) +{ + if (!list || !name) { + return -1; + } + + if (!list->has_scanned) { + return -1; + } + + for (size_t i = 0; i < list->count; ++i) { + if (strcmp(list->drivers[i].name, name) == 0) { + return (int)i; + } + } + return -1; +} + +struct asio_device *asio_device_list_attach_device(struct asio_device_list *list, const char *name) +{ + if (!list || !name) { + return NULL; + } + + if (!list->has_scanned) { + return NULL; + } + + int index = asio_device_list_get_index_from_driver_name(list, name); + if (index >= 0) { + for (int j = 0; j < MAX_NUM_ASIO_DEVICES; j++) { + if (current_asio_devices[j]) { + if (strcmp(name, current_asio_devices[j]->device_name) == 0) { + return current_asio_devices[j]; + } + } + } + const int free_slot = find_free_driver_slot(); + if (free_slot >= 0) { + struct asio_device *dev = asio_device_create(name, list->drivers[index].clsid, free_slot); + if (!dev) { + list->drivers[index].loadable = false; + } + + return dev; + } + } + return NULL; +} diff --git a/plugins/win-asio/asio-device-list.h b/plugins/win-asio/asio-device-list.h new file mode 100644 index 00000000000000..f444ca3148f82b --- /dev/null +++ b/plugins/win-asio/asio-device-list.h @@ -0,0 +1,45 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of win-asio. + It uses the Steinberg ASIO SDK, which is licensed under the GNU GPL v3. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "asio-device.h" + +#include +#include + +struct asio_driver_entry { + CLSID clsid; + char name[256]; + bool loadable; +}; + +struct asio_device_list { + struct asio_driver_entry drivers[MAX_NUM_ASIO_DEVICES]; + bool has_scanned; + size_t count; +}; + +struct asio_device_list *asio_device_list_create(void); +void asio_device_list_destroy(struct asio_device_list *list); +size_t asio_device_list_get_count(const struct asio_device_list *list); +const char *asio_device_list_get_name(const struct asio_device_list *list, size_t index); +struct asio_device *asio_device_list_attach_device(struct asio_device_list *list, const char *name); +int asio_device_list_get_index_from_driver_name(const struct asio_device_list *list, const char *name); diff --git a/plugins/win-asio/asio-device.c b/plugins/win-asio/asio-device.c new file mode 100644 index 00000000000000..f9c4e14a8e882e --- /dev/null +++ b/plugins/win-asio/asio-device.c @@ -0,0 +1,1372 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of win-asio. + It implements an ASIO host for OBS Studio using the Steinberg ASIO SDK, + available under a dual licence including the GNU GPLv3. + Certain parts of the driver initialisation sequence, buffer management + strategy and start/stop flow are inspired by the structure and behaviour + of the JUCE ASIO host (juce_ASIO_windows.cpp), but this file is an + independent C implementation adapted to OBS/libobs. No JUCE source code + is used or included. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "asio-device.h" + +#include "asio-callbacks.h" +#include "asio-format.h" +#include "win-asio.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +const IID IID_IASIO = {0x261a7a60, 0xa003, 0x11d1, {0xa3, 0x90, 0x00, 0x80, 0x5f, 0x08, 0x38, 0x75}}; + +/* global vars for com initialization in Main thread */ +bool com_initialized = false; +os_event_t *shutting_down; +volatile bool shutting_down_atomic = false; +struct asio_device *current_asio_devices[MAX_NUM_ASIO_DEVICES] = {0}; + +void asio_device_destroy_all(); +void OBSEvent(enum obs_frontend_event event, void *none) +{ + UNUSED_PARAMETER(none); + if (event == OBS_FRONTEND_EVENT_EXIT || event == OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN) { + os_atomic_set_bool(&shutting_down_atomic, true); + asio_device_destroy_all(); + } +} + +void asio_device_get_sample_format(int type, char *message) +{ + switch (type) { + case 17: + strcpy(message, "24 bit int"); + break; + case 18: + strcpy(message, "32 bit int"); + break; + case 19: + strcpy(message, "32 bit float"); + break; + case 41: + strcpy(message, "no channels available"); + break; + default: + snprintf(message, 64, "uncommon format number (%d)", type); + break; + } +} + +struct asio_device *asio_device_find_by_name(const char *name) +{ + if (!name || !*name) { + return NULL; + } + + for (int i = 0; i < MAX_NUM_ASIO_DEVICES; ++i) { + struct asio_device *dev = current_asio_devices[i]; + if (dev && strcmp(dev->device_name, name) == 0) { + return dev; + } + } + + return NULL; +} + +void asio_device_show_control_panel(struct asio_device *dev) +{ + info("Opening ASIO control panel..."); + if (dev && dev->asio) { + ASIO_ControlPanel(dev); + } +} + +/* ASIO driver reset must be done in the UI thread */ +static void asio_device_do_reset_task(void *param) +{ + struct asio_device *dev = (struct asio_device *)param; + info("ASIO driver reset"); + if (!dev) { + return; + } + + asio_device_close(dev); + os_atomic_set_bool(&dev->need_to_reset, true); + asio_device_open(dev, dev->current_sample_rate, dev->current_buffer_size); + if (dev->obs_output_client) { + os_atomic_set_bool(&dev->obs_output_client->stopping, false); + } + + if (dev->errorstring[0] != '\0') { + error("Failed to reopen during reset: %s", dev->errorstring); + dev->errorstring[0] = '\0'; + return; + } + asio_device_reload_channel_names(dev); + info("ASIO reset complete"); +} + +void asio_device_reset_request(struct asio_device *dev) +{ + info("ASIO reset requested."); + if (!dev) { + return; + } + + os_atomic_set_bool(&dev->need_to_reset, true); + + const DWORD cur = GetCurrentThreadId(); + if (cur != dev->com_thread_id) { + Sleep(500); + obs_queue_task(OBS_TASK_UI, asio_device_do_reset_task, dev, false); + return; + } + + asio_device_do_reset_task((void *)dev); +} + +void asio_device_reset_buffers(struct asio_device *dev) +{ + if (!dev) { + return; + } + + long num_input = dev->total_num_input_chans; + long num_output = dev->total_num_output_chans; + + for (int i = 0; i < num_input; ++i) { + dev->buffer_infos[i].isInput = ASIOTrue; + dev->buffer_infos[i].channelNum = i; + dev->buffer_infos[i].buffers[0] = NULL; + dev->buffer_infos[i].buffers[1] = NULL; + } + + for (int i = 0; i < num_output; ++i) { + int j = (int)(i + num_input); + dev->buffer_infos[j].isInput = ASIOFalse; + dev->buffer_infos[j].channelNum = i; + dev->buffer_infos[j].buffers[0] = NULL; + dev->buffer_infos[j].buffers[1] = NULL; + } +} + +/* functions related to the 'buffer' setting in the ASIO driver, measured as a number of samples */ +static bool contains_size(struct asio_device *dev, long value) +{ + for (size_t i = 0; i < dev->supported_buffer_sizes.num; ++i) { + if (dev->supported_buffer_sizes.array[i] == value) { + return true; + } + } + + return false; +} + +void asio_device_add_buffer_sizes(struct asio_device *dev, long min_size, long max_size, long preferred_size, + long granularity) +{ + if (!dev) { + return; + } + + if (granularity >= 0) { + long step = (granularity < 16) ? 16 : granularity; + long size = min_size; + if (size % step) { + size = ((size + step - 1) / step) * step; + } + + for (; size <= max_size && size <= 6400; size += step) { + da_push_back(dev->supported_buffer_sizes, &size); + } + } else { + for (int p = 0; p < 18; ++p) { + long size = 1L << p; + if (size >= min_size && size <= max_size) { + da_push_back(dev->supported_buffer_sizes, &size); + } + } + } + + if (preferred_size >= min_size && preferred_size <= max_size && !contains_size(dev, preferred_size)) { + da_push_back(dev->supported_buffer_sizes, &preferred_size); + } +} + +ASIOError asio_device_refresh_buffer_sizes(struct asio_device *dev) +{ + if (!dev || !dev->asio) { + return ASE_NotPresent; + } + + ASIOError err = ASIO_GetBufferSize(dev, &dev->min_buffer_size, &dev->max_buffer_size, + &dev->preferred_buffer_size, &dev->buffer_granularity); + + if (err == ASE_OK) { + da_clear(dev->supported_buffer_sizes); + asio_device_add_buffer_sizes(dev, dev->min_buffer_size, dev->max_buffer_size, + dev->preferred_buffer_size, dev->buffer_granularity); + } + + return err; +} + +int asio_device_get_preferred_buffer_size(struct asio_device *dev) +{ + if (!dev || !dev->asio) { + return 0; + } + + int min_buffer_size = 0; + int max_buffer_size = 0; + int buffer_granularity = 0; + long preferred = 0; + if (ASIO_GetBufferSize(dev, &min_buffer_size, &max_buffer_size, &preferred, &buffer_granularity) == ASE_OK) { + return preferred; + } + return 0; +} + +int asio_device_read_buffer_size(struct asio_device *dev, int requested_size) +{ + if (!dev || !dev->asio) { + return requested_size; + } + + dev->min_buffer_size = 0; + dev->max_buffer_size = 0; + dev->buffer_granularity = 0; + long new_preferred = 0; + + if (ASIO_GetBufferSize(dev, &dev->min_buffer_size, &dev->max_buffer_size, &new_preferred, + &dev->buffer_granularity) == ASE_OK) { + if (dev->preferred_buffer_size != 0 && new_preferred != 0 && + new_preferred != dev->preferred_buffer_size) { + dev->should_use_preferred_size = true; + } + + if (requested_size < dev->min_buffer_size || requested_size > dev->max_buffer_size) { + dev->should_use_preferred_size = true; + } + + dev->preferred_buffer_size = new_preferred; + } + + if (dev->should_use_preferred_size) { + info("Using preferred size for buffer.."); + ASIOError err = asio_device_refresh_buffer_sizes(dev); + + if (err == ASE_OK) { + requested_size = dev->preferred_buffer_size; + } else { + requested_size = 1024; + warn("getBufferSize1 failed, using 1024 as fallback"); + } + + dev->should_use_preferred_size = false; + } + + return requested_size; +} + +/* driver loading & removal */ +static bool asio_device_remove_current_driver(struct asio_device *dev) +{ + bool released_ok = true; + if (dev->asio) { + __try { + ASIO_Release(dev); + } __except (EXCEPTION_EXECUTE_HANDLER) { + error("Exception occurred while releasing COM object"); + released_ok = false; + } + dev->asio = NULL; + } + return released_ok; +} + +static bool asio_device_try_create_driver(struct asio_device *dev, bool *crashed) +{ + bool success = false; + __try { + HRESULT hr = + CoCreateInstance(&dev->clsid, NULL, CLSCTX_INPROC_SERVER, &dev->clsid, (void **)&dev->asio); + success = SUCCEEDED(hr); + if (!success) { + error("CoCreateInstance failed (HRESULT 0x%lX)", (unsigned long)hr); + } + return success; + } __except (EXCEPTION_EXECUTE_HANDLER) { + error("Exception occurred during CoCreateInstance"); + *crashed = true; + } + return false; +} + +bool asio_device_load_driver(struct asio_device *dev) +{ + if (!dev) { + return false; + } + + if (!asio_device_remove_current_driver(dev)) { + strncpy(dev->errorstring, "** Driver crashed while being closed", 40); + } else { + info("Driver successfully removed"); + } + + bool crashed = false; + bool ok = asio_device_try_create_driver(dev, &crashed); + + if (crashed) { + strncpy(dev->errorstring, "** Driver crashed while being opened", 40); + } else if (ok) { + info("driver com interface opened"); + } else { + strncpy(dev->errorstring, "Failed to load driver", 30); + } + + return ok; +} + +void asio_device_init_driver(struct asio_device *dev, char *driver_error) +{ + if (!dev) { + return; + } + + if (dev->asio == NULL) { + if (driver_error) { + strncpy(driver_error, "No driver", 30); + } + return; + } + HWND hwnd = GetDesktopWindow(); + bool init_ok = ASIO_Init(dev, &hwnd) == ASIOTrue; + + if (!init_ok) { + ASIO_GetErrorMessage(dev, driver_error); + if (!driver_error[0]) { + strncpy(driver_error, "Driver failed to initialize", 30); + } + } + /* that call seems required by sh...y drivers */ + if (driver_error && !driver_error[0]) { + char buffer[256] = {0}; + ASIO_GetDriverName(dev, buffer); + } + return; +} + +struct asio_device *asio_device_create(const char *name, CLSID clsid, int slot_number) +{ + struct asio_device *dev = calloc(1, sizeof(struct asio_device)); + if (!dev) { + return NULL; + } + + dev->driver_failure = false; + + if (!com_initialized) { + com_initialized = SUCCEEDED(CoInitialize(NULL)); + if (com_initialized) { + dev->com_thread_id = GetCurrentThreadId(); + debug("COM initialized in Main thread %lu for device %s", dev->com_thread_id, name); + } else { + error("Failed to initialize COM"); + free(dev); + return NULL; + } + } else { + dev->com_thread_id = GetCurrentThreadId(); + debug("COM already initialized in Main thread %lu for device %s", dev->com_thread_id, name); + } + + strncpy(dev->device_name, name, sizeof(dev->device_name) - 1); + if (current_asio_devices[slot_number] != NULL) { + return NULL; + } + + dev->clsid = clsid; + dev->slot_number = slot_number; + + da_init(dev->supported_buffer_sizes); + da_init(dev->supported_sample_rates); + + if (slot_number < 0 || slot_number >= MAX_NUM_ASIO_DEVICES) { + blog(LOG_ERROR, "[ASIO] Invalid slot number %d in asio_device_create", slot_number); + free(dev); + return NULL; + } + + dev->callbacks.bufferSwitch = callback_sets[dev->slot_number].buffer_switch; + dev->callbacks.bufferSwitchTimeInfo = callback_sets[dev->slot_number].buffer_switch_time_info; + dev->callbacks.asioMessage = callback_sets[dev->slot_number].asio_message; + dev->callbacks.sampleRateDidChange = callback_sets[dev->slot_number].sample_rate_changed; + + /* we load the driver, retrieve the COM pointer and do some basics tests */ + asio_device_test(dev); + if (dev->asio == NULL || dev->driver_failure) { + error("Failed to load driver"); + free(dev); + return NULL; + } else { + current_asio_devices[slot_number] = dev; + for (int i = 0; i < MAX_DEVICE_CHANNELS; i++) { + deque_init(&dev->output_frames[i]); + } + for (int k = 0; k < MAX_DEVICE_CHANNELS; k++) { + dev->obs_track[k] = -1; + dev->obs_track_channel[k] = -1; + } + dev->current_nb_clients = 0; + os_atomic_set_bool(&dev->capture_started, false); + } + + return dev; +} + +bool are_any_devices_still_active() +{ + for (int i = 0; i < MAX_NUM_ASIO_DEVICES; ++i) { + if (current_asio_devices[i] != NULL) { + return true; + } + } + return false; +} + +void asio_device_destroy(struct asio_device *dev) +{ + if (!dev) { + return; + } + + for (int j = 0; j < MAX_DEVICE_CHANNELS; j++) { + deque_free(&dev->output_frames[j]); + } + + da_free(dev->supported_buffer_sizes); + da_free(dev->supported_sample_rates); + + for (int i = 0; i < MAX_NUM_ASIO_DEVICES; ++i) { + if (current_asio_devices[i] == dev) { + current_asio_devices[i] = NULL; + } + } + + asio_device_close(dev); + + dev->current_nb_clients = 0; + memset(dev->obs_clients, 0, sizeof(dev->obs_clients)); + + if (dev->obs_output_client) { + blog(LOG_INFO, + "[asio_output]:\n\tDevice % s removed;\n" + "\tnumber of xruns: % i;\n\tincrease your buffer if you get a high count & hear cracks, pops or" + " else !\n-1 means your device doesn't report xruns.", + dev->device_name, dev->xruns); + dev->obs_output_client = NULL; + } + free(dev->io_buffer_space); + dev->io_buffer_space = NULL; + + if (!asio_device_remove_current_driver(dev)) { + info("** Driver crashed while being closed"); + } else { + info("Closed ASIO device"); + } + + /* close COM if this is the last device */ + if (com_initialized) { + if (!are_any_devices_still_active()) { + CoUninitialize(); + com_initialized = false; + debug("Last ASIO device destroyed — COM uninitialized"); + } else { + debug("ASIO device destroyed — COM still in use by others"); + } + } + + free(dev); +} + +void asio_device_destroy_all() +{ + for (int i = 0; i < MAX_NUM_ASIO_DEVICES; ++i) { + if (current_asio_devices[i]) { + asio_device_destroy(current_asio_devices[i]); + } + } +} + +bool get_channel_name(struct asio_device *dev, char channel_names[MAX_DEVICE_CHANNELS][MAX_CH_NAME_LENGTH], int index, + bool is_input) +{ + if (!dev) { + return false; + } + + if (index < 0 || index >= MAX_DEVICE_CHANNELS) { + return false; + } + + ASIOChannelInfo channel_info = {.channel = index, .isInput = is_input ? ASIOTrue : ASIOFalse}; + + if (ASIO_GetChannelInfo(dev, &channel_info) != ASE_OK) { + return false; + } + + strncpy(channel_names[index], channel_info.name, MAX_CH_NAME_LENGTH - 1); + channel_names[index][MAX_CH_NAME_LENGTH - 1] = '\0'; + return true; +} + +void clear_channel_names(char names[MAX_DEVICE_CHANNELS][MAX_CH_NAME_LENGTH]) +{ + for (int i = 0; i < MAX_DEVICE_CHANNELS; ++i) { + names[i][0] = '\0'; + } +} + +double asio_device_get_sample_rate(struct asio_device *dev) +{ + double cr = 0; + auto err = ASIO_GetSampleRate(dev, &cr); + return cr; +} + +void asio_device_set_sample_rate(struct asio_device *dev, double new_rate) +{ + if (!dev || !dev->asio) { + return; + } + + if (dev->current_sample_rate != new_rate) { + info("rate change: %.0f → %.0f", dev->current_sample_rate, new_rate); + + ASIOError err = ASIO_SetSampleRate(dev, new_rate); + if (err != ASE_OK) { + warn("setSampleRate failed with code %d", err); + } + + Sleep(10); + + if (err == ASE_NoClock && dev->num_clock_sources > 0) { + info("trying to set a clock source.."); + err = ASIO_SetClockSource(dev, dev->clocks[0].index); + if (err != ASE_OK) { + warn("setClockSource failed with code %d", err); + } + + Sleep(10); + + err = ASIO_SetSampleRate(dev, new_rate); + if (err != ASE_OK) { + warn("setSampleRate (retry) failed with code %d", err); + } + + Sleep(10); + } + + if (err == ASE_OK) { + dev->current_sample_rate = new_rate; + } + } +} + +void asio_device_update_sample_rates_list(struct asio_device *dev) +{ + if (!dev || !dev->asio) { + return; + } + + static const double common_rates[] = {8000, 11025, 16000, 22050, 24000, 32000, 44100, 48000, + 88200, 96000, 176400, 192000, 352800, 384000, 705600, 768000}; + + DARRAY(double) new_rates; + da_init(new_rates); + + for (size_t i = 0; i < sizeof(common_rates) / sizeof(common_rates[0]); ++i) { + double rate = common_rates[i]; + if (ASIO_CanSampleRate(dev, rate) == ASE_OK) { + da_push_back(new_rates, &rate); + } + } + + if (new_rates.num == 0) { + double current = asio_device_get_sample_rate(dev); + info("No standard sample rates supported - using current rate: %.2f", current); + if (current > 0.0) { + da_push_back(new_rates, ¤t); + } + } + + bool changed = false; + if (dev->supported_sample_rates.num != new_rates.num) { + changed = true; + } else { + for (size_t i = 0; i < dev->supported_sample_rates.num; ++i) { + if (dev->supported_sample_rates.array[i] != new_rates.array[i]) { + changed = true; + break; + } + } + } + + if (changed) { + da_move(dev->supported_sample_rates, new_rates); + + char buffer[256] = {0}; + for (size_t i = 0; i < dev->supported_sample_rates.num; ++i) { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%.0f%s", dev->supported_sample_rates.array[i], + (i < dev->supported_sample_rates.num - 1) ? ", " : ""); + strcat_s(buffer, sizeof(buffer), tmp); + } + debug("Supported sample rates: %s", buffer); + } + + da_free(new_rates); +} + +void asio_device_update_clock_sources(struct asio_device *dev) +{ + if (!dev || !dev->asio) { + return; + } + + memset(dev->clocks, 0, sizeof(dev->clocks)); + long num_sources = MAX_DEVICE_CHANNELS; + ASIOError err = ASIO_GetClockSources(dev, dev->clocks, &num_sources); + dev->num_clock_sources = (int)num_sources; + + bool is_source_set = false; + + for (int i = 0; i < dev->num_clock_sources; ++i) { + char log_msg[128]; + snprintf(log_msg, sizeof(log_msg), "clock: %s", dev->clocks[i].name); + + if (dev->clocks[i].isCurrentSource) { + is_source_set = true; + strcat_s(log_msg, sizeof(log_msg), " (cur)"); + } + + info("%s", log_msg); + } + + if (dev->num_clock_sources > 1 && !is_source_set) { + info("setting clock source"); + err = ASIO_SetClockSource(dev, dev->clocks[0].index); + if (err != ASE_OK) { + warn("setClockSource1 failed with code %d", err); + } + + Sleep(20); + } else if (dev->num_clock_sources == 0) { + info("no clock sources!"); + } +} + +void asio_device_read_latencies(struct asio_device *dev) +{ + if (!dev) { + return; + } + + dev->input_latency = 0; + dev->output_latency = 0; + + if (ASIO_GetLatencies(dev, &dev->input_latency, &dev->output_latency) != ASE_OK) { + info("getLatencies() failed"); + } else { + info("Latencies (samples): in = %i, out = %i", dev->input_latency, dev->output_latency); + } +} + +void asio_device_reload_channel_names(struct asio_device *dev) +{ + if (!dev || !dev->asio) { + return; + } + + long total_inputs = 0, total_outputs = 0; + ASIOError err = ASIO_GetChannels(dev, &total_inputs, &total_outputs); + if (err != ASE_OK) { + return; + } + + if (total_inputs > MAX_DEVICE_CHANNELS || total_outputs > MAX_DEVICE_CHANNELS) { + info("Only up to %d input + %d output channels are enabled. Higher channel counts are disabled.", + MAX_DEVICE_CHANNELS, MAX_DEVICE_CHANNELS); + } + + dev->total_num_input_chans = (total_inputs > MAX_DEVICE_CHANNELS) ? MAX_DEVICE_CHANNELS : total_inputs; + dev->total_num_output_chans = (total_outputs > MAX_DEVICE_CHANNELS) ? MAX_DEVICE_CHANNELS : total_outputs; + + clear_channel_names(dev->input_channel_names); + clear_channel_names(dev->output_channel_names); + + for (int i = 0; i < dev->total_num_input_chans; ++i) { + if (!get_channel_name(dev, dev->input_channel_names, i, true)) { + snprintf(dev->input_channel_names[i], MAX_CH_NAME_LENGTH, "Input %d", i); + } + } + + for (int i = 0; i < dev->total_num_output_chans; ++i) { + if (!get_channel_name(dev, dev->output_channel_names, i, false)) { + snprintf(dev->output_channel_names[i], MAX_CH_NAME_LENGTH, "Output %d", i); + } + } +} + +/* we test the driver with buffers with up to 2 channels */ +void asio_device_create_dummy_buffers(struct asio_device *dev) +{ + if (!dev || !dev->asio) { + return; + } + + const int num_dummy_inputs = dev->total_num_input_chans < 2 ? dev->total_num_input_chans : 2; + const int num_dummy_outputs = dev->total_num_output_chans < 2 ? dev->total_num_output_chans : 2; + + for (int i = 0; i < num_dummy_inputs; ++i) { + dev->buffer_infos[i].isInput = ASIOTrue; + dev->buffer_infos[i].channelNum = i; + dev->buffer_infos[i].buffers[0] = NULL; + dev->buffer_infos[i].buffers[1] = NULL; + } + int output_buffer_index = num_dummy_inputs; + for (int i = 0; i < num_dummy_outputs; ++i) { + dev->buffer_infos[output_buffer_index + i].isInput = ASIOFalse; + dev->buffer_infos[output_buffer_index + i].channelNum = i; + dev->buffer_infos[output_buffer_index + i].buffers[0] = NULL; + dev->buffer_infos[output_buffer_index + i].buffers[1] = NULL; + } + + int num_channels = output_buffer_index + num_dummy_outputs; + info("Creating dummy buffers: %d channels, size: %d", num_channels, dev->preferred_buffer_size); + + if (dev->preferred_buffer_size > 0) { + ASIOError err = ASIO_CreateBuffers(dev, dev->buffer_infos, num_channels, dev->preferred_buffer_size, + &dev->callbacks); + if (err != ASE_OK) { + warn("Dummy buffer creation failed with error %d", err); + } else { + dev->buffers_created = true; + } + } + + long new_inputs = 0, new_outputs = 0; + ASIO_GetChannels(dev, &new_inputs, &new_outputs); + + if (new_inputs > MAX_DEVICE_CHANNELS || new_outputs > MAX_DEVICE_CHANNELS) { + info("Limiting to %d input + %d output channels max", MAX_DEVICE_CHANNELS, MAX_DEVICE_CHANNELS); + } + dev->total_num_input_chans = new_inputs > MAX_DEVICE_CHANNELS ? MAX_DEVICE_CHANNELS : new_inputs; + dev->total_num_output_chans = new_outputs > MAX_DEVICE_CHANNELS ? MAX_DEVICE_CHANNELS : new_outputs; + + info("Detected channels after dummy buffers: %ld input, %ld output", dev->total_num_input_chans, + dev->total_num_output_chans); + + asio_device_update_sample_rates_list(dev); + asio_device_reload_channel_names(dev); + + for (int i = 0; i < dev->total_num_output_chans; ++i) { + ASIOChannelInfo chinfo = {0}; + chinfo.channel = i; + chinfo.isInput = 0; + ASIO_GetChannelInfo(dev, &chinfo); + asio_format_init(&dev->output_format[i], chinfo.type); + + if (i < num_dummy_outputs) { + asio_format_clear(&dev->output_format[i], dev->buffer_infos[output_buffer_index + i].buffers[0], + dev->preferred_buffer_size); + asio_format_clear(&dev->output_format[i], dev->buffer_infos[output_buffer_index + i].buffers[1], + dev->preferred_buffer_size); + } + } +} + +void asio_device_dispose_buffers(struct asio_device *dev) +{ + if (!dev) { + return; + } + + ASIOError err = ASE_OK; + if (dev->asio != NULL && dev->buffers_created) { + dev->buffers_created = false; + err = ASIO_DisposeBuffers(dev); + } + if (err != ASE_OK) { + info("Device didn't dispose correctly of the buffers; error code %i", err); + } +} + +/* Next function exists because of shi..y drivers which expect some loading sequence which should be unnecessary in + * theory. But following JUCE & for extra safety, we do it anyway 'à la Cubase' */ +void asio_device_test(struct asio_device *dev) +{ + if (!dev) { + return; + } + + info("opening device: %s", dev->device_name); + os_atomic_set_bool(&dev->need_to_reset, false); + + clear_channel_names(dev->input_channel_names); + clear_channel_names(dev->output_channel_names); + + da_clear(dev->supported_buffer_sizes); + da_clear(dev->supported_sample_rates); + + dev->is_open = false; + dev->total_num_input_chans = 0; + dev->total_num_output_chans = 0; + dev->xruns = 0; + dev->errorstring[0] = '\0'; + + ASIOError err = 0; + + if (asio_device_load_driver(dev)) { + asio_device_init_driver(dev, dev->errorstring); + if (!dev->errorstring[0]) { + dev->total_num_input_chans = 0; + dev->total_num_output_chans = 0; + if (dev->asio != NULL) { + err = ASIO_GetChannels(dev, &dev->total_num_input_chans, &dev->total_num_output_chans); + if (err == ASE_OK) { + info("channels in: %i, channels out: %i", dev->total_num_input_chans, + dev->total_num_output_chans); + if (dev->total_num_input_chans > MAX_DEVICE_CHANNELS || + dev->total_num_output_chans > MAX_DEVICE_CHANNELS) { + info("Only up to %i input + %i output channels are enabled. Higher channel counts are disabled.", + MAX_DEVICE_CHANNELS, MAX_DEVICE_CHANNELS); + } + + dev->total_num_input_chans = + MIN(dev->total_num_input_chans, (long)MAX_DEVICE_CHANNELS); + dev->total_num_output_chans = + MIN(dev->total_num_output_chans, (long)MAX_DEVICE_CHANNELS); + + if (err = asio_device_refresh_buffer_sizes(dev) == ASE_OK) { + info("buffer sizes: %ld → %ld, preferred: %ld, step: %ld", + dev->min_buffer_size, dev->max_buffer_size, + dev->preferred_buffer_size, dev->buffer_granularity); + + double current_rate = asio_device_get_sample_rate(dev); + if (current_rate < 1.0 || current_rate > 192001.0) { + info("setting default sample rate"); + err = ASIO_SetSampleRate(dev, 48000.0); + info("force setting sample rate to 48 kHz"); + /* sanity check */ + current_rate = asio_device_get_sample_rate(dev); + } + dev->current_sample_rate = current_rate; + + dev->post_output = ASIO_OutputReady(dev) == ASE_OK; + if (dev->post_output) { + info("outputReady true"); + } + + asio_device_update_sample_rates_list(dev); + + /* series of steps inspired by cubase loading sequence because + * otherwise some devices fail to load ...*/ + asio_device_read_latencies(dev); + asio_device_create_dummy_buffers(dev); + asio_device_read_latencies(dev); + err = ASIO_Start(dev); + Sleep(80); + ASIO_Stop(dev); + /* We dispose of buffers here and not in the open function bc some + * drivers can't set samplerate while buffers are being prepared. */ + info("disposing of the dummy buffers"); + asio_device_dispose_buffers(dev); + } else { + info("Can't detect buffer sizes"); + } + } else { + info("Can't detect asio channels"); + } + } + } else { + info("Initialization failure reported by driver:\n %s\nYour device is likely used concurrently " + "in another application, but ASIO usually supports a single host.\n", + dev->errorstring); + } + } else { + info("No such device"); + } + + if (dev->errorstring[0] != '\0') { + dev->driver_failure = true; + if (!asio_device_remove_current_driver(dev)) { + info("** Driver crashed while being closed"); + } + } else { + info("device opened but not yet started"); + } + + dev->is_open = false; + os_atomic_set_bool(&dev->need_to_reset, false); +} + +void asio_device_open(struct asio_device *dev, double sample_rate, long buffer_size_samples) +{ + if (!dev) { + blog(LOG_ERROR, "[ASIO] Invalid device handle in open()"); + return; + } + if (dev->is_open) { + asio_device_close(dev); + } + + DWORD current_thread_id = GetCurrentThreadId(); + if (current_thread_id != dev->com_thread_id) { + error("open() called from the wrong thread! Expected COM thread ID: %lu, current: %lu\n", + dev->com_thread_id, current_thread_id); + return; + } else { + debug("open() called from correct COM thread\n"); + } + + if (dev->asio == NULL) { + asio_device_test(dev); + if (dev->asio == NULL) { + error("[Failed to load driver with error: %s", dev->errorstring); + return; + } + } + dev->is_started = false; + dev->errorstring[0] = '\0'; + + ASIOError err = ASIO_GetChannels(dev, &dev->total_num_input_chans, &dev->total_num_output_chans); + if (dev->total_num_input_chans > MAX_DEVICE_CHANNELS || dev->total_num_output_chans > MAX_DEVICE_CHANNELS) { + info("Only up to %i input + %i output channels are enabled. Higher channel counts are disabled.", + MAX_DEVICE_CHANNELS, MAX_DEVICE_CHANNELS); + } + + dev->total_num_input_chans = MIN(dev->total_num_input_chans, (long)MAX_DEVICE_CHANNELS); + dev->total_num_output_chans = MIN(dev->total_num_output_chans, (long)MAX_DEVICE_CHANNELS); + + /* Check if the driver supports the requested sample rate & set the rate; then set the clocks */ + double temptative_rate = sample_rate; + dev->current_sample_rate = sample_rate; + + asio_device_update_sample_rates_list(dev); + bool is_listed = false; + for (size_t i = 0; i < dev->supported_sample_rates.num; ++i) { + if (dev->supported_sample_rates.array[i] == sample_rate) { + is_listed = true; + break; + } + } + + if (sample_rate == 0.0 || !is_listed) { + temptative_rate = 48000.0; + } + + asio_device_update_clock_sources(dev); + dev->current_sample_rate = asio_device_get_sample_rate(dev); + asio_device_set_sample_rate(dev, temptative_rate); + + dev->current_block_size_samples = dev->current_buffer_size = + asio_device_read_buffer_size(dev, buffer_size_samples); + + /* a sample rate change might impact the channel count, sigh... */ + err = ASIO_GetChannels(dev, &dev->total_num_input_chans, &dev->total_num_output_chans); + /* DEBUG check : assert(err == ASE_OK); */ + + if (dev->total_num_input_chans > MAX_DEVICE_CHANNELS || dev->total_num_output_chans > MAX_DEVICE_CHANNELS) { + info("Only up to %d input + %d output channels are enabled. Higher channel counts are disabled.", + MAX_DEVICE_CHANNELS, MAX_DEVICE_CHANNELS); + } + dev->total_num_input_chans = MIN(dev->total_num_input_chans, (long)MAX_DEVICE_CHANNELS); + dev->total_num_output_chans = MIN(dev->total_num_output_chans, (long)MAX_DEVICE_CHANNELS); + + if (ASIO_Future(dev, kAsioCanReportOverload, NULL) != ASE_OK) { + dev->xruns = -1; + } + + if (os_atomic_load_bool(&dev->need_to_reset)) { + info("Resetting"); + + if (!asio_device_remove_current_driver(dev)) { + error("** Driver crashed while being closed"); + } + + asio_device_load_driver(dev); + + char init_error[256] = {0}; + asio_device_init_driver(dev, init_error); + + if (init_error[0] != '\0') { + error("ASIOInit: %s", init_error); + } else { + double rate = asio_device_get_sample_rate(dev); + asio_device_set_sample_rate(dev, rate); + } + os_atomic_set_bool(&dev->need_to_reset, false); + } + + /* buffers creation routine; if this fails, try a second time with preferredBufferSize*/ + long total_buffers = dev->total_num_input_chans + dev->total_num_output_chans; + asio_device_reset_buffers(dev); + + info("creating buffers: %i in-out channels, size: %i samples", total_buffers, dev->current_block_size_samples); + err = ASIO_CreateBuffers(dev, dev->buffer_infos, (long)total_buffers, dev->current_block_size_samples, + &dev->callbacks); + + if (err != ASE_OK) { + dev->current_block_size_samples = dev->preferred_buffer_size; + info("createBuffers failed, trying preferred size: %i", dev->current_block_size_samples); + err = ASIO_DisposeBuffers(dev); + err = ASIO_CreateBuffers(dev, dev->buffer_infos, (long)total_buffers, dev->current_block_size_samples, + &dev->callbacks); + if (err != ASE_OK) { + info("createBuffers failed again, when trying preferred size: %i", + dev->current_block_size_samples); + } + } + + if (err == ASE_OK) { + dev->buffers_created = true; + info("Buffers created successfully"); + /* allocation of input and output buffers */ + free(dev->io_buffer_space); + dev->io_buffer_space = + (float *)calloc(dev->current_block_size_samples * total_buffers + 32, sizeof(float)); + + /* For devices like decklink having input only or output only, default format set to ASIOSTLastEntry */ + int input_type = ASIOSTLastEntry; + int output_type = ASIOSTLastEntry; + dev->current_bit_depth = 16; + + /* Set up input buffers and formats */ + for (int n = 0; n < dev->total_num_input_chans; ++n) { + dev->in_buffers[n] = dev->io_buffer_space + (dev->current_block_size_samples * n); + ASIOChannelInfo chinfo = {.channel = n, .isInput = ASIOTrue}; + ASIO_GetChannelInfo(dev, &chinfo); + if (n == 0) { + input_type = chinfo.type; + } + + asio_format_init(&dev->input_format[n], chinfo.type); + dev->current_bit_depth = MAX(dev->input_format[n].bit_depth, dev->current_bit_depth); + } + + for (int n = 0; n < dev->total_num_output_chans; ++n) { + dev->out_buffers[n] = dev->io_buffer_space + + (dev->current_block_size_samples * (dev->total_num_input_chans + n)); + ASIOChannelInfo chinfo = {.channel = n, .isInput = ASIOFalse}; + ASIO_GetChannelInfo(dev, &chinfo); + + if (n == 0) { + output_type = chinfo.type; + } + + asio_format_init(&dev->output_format[n], chinfo.type); + dev->current_bit_depth = MAX(dev->output_format[n].bit_depth, dev->current_bit_depth); + } + + char in_fmt[32], out_fmt[32]; + asio_device_get_sample_format(input_type, in_fmt); + asio_device_get_sample_format(output_type, out_fmt); + info("input sample format: %s, output sample format: %s\n ", in_fmt, out_fmt); + + for (int i = 0; i < dev->total_num_output_chans; ++i) { + asio_format_clear(&dev->output_format[i], + dev->buffer_infos[dev->total_num_input_chans + i].buffers[0], + dev->current_block_size_samples); + asio_format_clear(&dev->output_format[i], + dev->buffer_infos[dev->total_num_input_chans + i].buffers[1], + dev->current_block_size_samples); + } + + /* start sequence */ + asio_device_read_latencies(dev); + asio_device_refresh_buffer_sizes(dev); + dev->is_open = true; + + info("starting"); + dev->called_back = false; + + ASIOError err = ASIO_Start(dev); + + if (err != ASE_OK) { + dev->is_open = false; + error("stop on failure"); + Sleep(10); + ASIO_Stop(dev); + error("Can't start device"); + Sleep(10); + goto fail; + } else { + int count = 300; + while (--count > 0 && !dev->called_back) { + Sleep(10); + } + + dev->is_started = true; + + if (!dev->called_back) { + error("Device didn't start correctly\nNo callbacks - stopping."); + ASIO_Stop(dev); + goto fail; + } + dev->need_to_reset = false; + return; + } + } +fail: + asio_device_dispose_buffers(dev); + Sleep(20); + dev->is_started = false; + dev->is_open = false; + asio_device_close(dev); + dev->need_to_reset = false; +} + +void asio_device_close(struct asio_device *dev) +{ + if (!dev || !dev->asio) { + return; + } + + if (dev->asio != NULL && dev->is_open) { + + dev->is_open = false; + dev->is_started = false; + os_atomic_set_bool(&dev->need_to_reset, false); + + if (dev->obs_output_client) { + os_atomic_set_bool(&dev->obs_output_client->stopping, true); + for (int j = 0; j < MAX_DEVICE_CHANNELS; j++) { + deque_free(&dev->output_frames[j]); + } + } + + info(" asio driver stopping"); + + if (dev->asio != NULL) { + Sleep(20); + ASIO_Stop(dev); + Sleep(10); + asio_device_dispose_buffers(dev); + } + + Sleep(10); + } +} + +bool asio_device_start(struct asio_device *dev) +{ + if (!dev || !dev->asio) { + return false; + } + + if (dev->is_started) { + return true; + } + + if (ASIO_Start(dev) != ASE_OK) { + return false; + } + + dev->is_started = true; + return true; +} + +void asio_device_stop(struct asio_device *dev) +{ + if (dev && dev->asio && dev->is_started) { + ASIO_Stop(dev); + dev->is_started = false; + } +} + +void asio_device_set_output_client(struct asio_device *dev, struct asio_data *client) +{ + if (dev) { + dev->obs_output_client = client; + } +} + +struct asio_data *asio_device_get_output_client(struct asio_device *dev) +{ + return dev ? dev->obs_output_client : NULL; +} + +/* message calback */ +long asio_device_asio_message_callback(struct asio_device *dev, long selector, long value, void *message, double *opt) +{ + UNUSED_PARAMETER(message); + UNUSED_PARAMETER(opt); + switch (selector) { + case kAsioSelectorSupported: + if (value == kAsioResetRequest || value == kAsioEngineVersion || value == kAsioResyncRequest || + value == kAsioLatenciesChanged || value == kAsioSupportsInputMonitor || value == kAsioOverload) { + return 1; + } + break; + + case kAsioBufferSizeChange: + info("kAsioBufferSizeChange; new buffer is %ld", value); + return 0; /* 0 tells the driver to request a reset to the host */ + + case kAsioResetRequest: + info("kAsioResetRequest"); + asio_device_reset_request(dev); + return 1; + + case kAsioResyncRequest: + info("kAsioResyncRequest"); + asio_device_reset_request(dev); + return 1; + + case kAsioLatenciesChanged: + info("kAsioLatenciesChanged"); + return 1; + + case kAsioEngineVersion: + return 2; + + case kAsioSupportsTimeInfo: + case kAsioSupportsTimeCode: + return 0; + + case kAsioOverload: + dev->xruns++; + return 1; + } + + return 0; +} + +/* main audio callback: split in 2 ==> asio_device_callback which implements some signalling and process_buffer which + * does the actual processing. */ +void asio_device_process_buffer(struct asio_device *dev, long buffer_index) +{ + if (!dev || !dev->is_started || buffer_index < 0) { + return; + } + + if (dev->in_buffers[0] == NULL && dev->out_buffers[0] == NULL) { + return; + } + + ASIOBufferInfo *infos = dev->buffer_infos; + int samps = dev->current_block_size_samples; + + /* input: convert ASIO samples to float and feed OBS sources */ + for (int i = 0; i < dev->total_num_input_chans; ++i) { + const void *src = infos[i].buffers[buffer_index]; + if (dev->in_buffers[i] && src) { + asio_format_convert_to_float(&dev->input_format[i], infos[i].buffers[buffer_index], + dev->in_buffers[i], samps); + } + } + + struct obs_audio_info aoi; + obs_get_audio_info(&aoi); + int output_channels = (int)audio_output_get_channels(obs_get_audio()); + + struct obs_source_audio out = { + .speakers = aoi.speakers, + .format = AUDIO_FORMAT_FLOAT_PLANAR, + .samples_per_sec = (uint32_t)dev->current_sample_rate, + .frames = samps, + .timestamp = os_gettime_ns(), + }; + /* pass audio to obs clients & then to the audio pipeline */ + for (int idx = 0; idx < dev->current_nb_clients; ++idx) { + struct asio_data *client = dev->obs_clients[idx]; + if (!client || !client->device_name || !client->active) { + continue; + } + + for (int j = 0; j < output_channels; ++j) { + int mix_idx = client->mix_channels[j]; + out.data[j] = (mix_idx >= 0 && !os_atomic_load_bool(&client->stopping)) + ? (uint8_t *)dev->in_buffers[mix_idx] + : (uint8_t *)dev->silent_buffers8; + } + if (!os_atomic_load_bool(&client->stopping) && client->source && os_atomic_load_bool(&client->active)) { + obs_source_output_audio(client->source, &out); + } + } + + /* output: feed ASIO buffers with OBS output, ch per ch; the obs_output selects track channels */ + if (dev->obs_output_client) { + for (int outchan = 0; outchan < dev->total_num_output_chans; ++outchan) { + void *dst = infos[dev->total_num_input_chans + outchan].buffers[buffer_index]; + bool play = os_atomic_load_bool(&dev->capture_started) && dev->obs_track[outchan] >= 0 && + dev->obs_track_channel[outchan] >= 0 && + dev->output_frames[outchan].size >= samps * sizeof(float); + if (play) { + deque_pop_front(&dev->output_frames[outchan], dev->out_buffers[outchan], + samps * sizeof(float)); + asio_format_convert_from_float(&dev->output_format[outchan], dev->out_buffers[outchan], + dst, samps); + } else { + asio_format_clear(&dev->output_format[outchan], dst, samps); + } + } + } else { + for (int i = 0; i < dev->total_num_output_chans; ++i) { + void *dst = infos[dev->total_num_input_chans + i].buffers[buffer_index]; + asio_format_clear(&dev->output_format[i], dst, samps); + } + } + + if (dev->post_output) { + ASIO_OutputReady(dev); + } +} + +void asio_device_callback(struct asio_device *dev, long index) +{ + if (!dev) { + return; + } + + if (dev->is_started && index >= 0) { + if (os_atomic_load_bool(&shutting_down_atomic)) { + os_atomic_set_bool(&dev->capture_started, false); + os_event_signal(shutting_down); + dev->is_started = false; + return; + } else { + asio_device_process_buffer(dev, index); + } + } else { + if (dev->post_output && dev->asio) { + ASIO_OutputReady(dev); + } + } + os_atomic_set_bool(&dev->called_back, true); +} diff --git a/plugins/win-asio/asio-device.h b/plugins/win-asio/asio-device.h new file mode 100644 index 00000000000000..4d4b97f3e40723 --- /dev/null +++ b/plugins/win-asio/asio-device.h @@ -0,0 +1,144 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of win-asio. + It uses the Steinberg ASIO SDK, which is licensed under the GNU GPL v3. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "asio-common.h" +#include "asio-compat.h" +#include "asio-format.h" +#include "iasiodrv.h" + +#include +#include + +#include +#include +#include + +struct asio_data; + +struct asio_device { + char device_name[64]; + + IASIO *asio; + DWORD com_thread_id; + CLSID clsid; + + long total_num_input_chans; + long total_num_output_chans; + char input_channel_names[MAX_DEVICE_CHANNELS][MAX_CH_NAME_LENGTH]; + char output_channel_names[MAX_DEVICE_CHANNELS][MAX_CH_NAME_LENGTH]; + + ASIOBufferInfo buffer_infos[MAX_DEVICE_CHANNELS * 2]; + float *in_buffers[MAX_DEVICE_CHANNELS]; + float *out_buffers[MAX_DEVICE_CHANNELS]; + float *io_buffer_space; + uint8_t silent_buffers8[4096]; + + int current_buffer_size; + int buffer_granularity; + DARRAY(int) supported_buffer_sizes; + int min_buffer_size; + int max_buffer_size; + int preferred_buffer_size; + bool should_use_preferred_size; + + DARRAY(double) supported_sample_rates; + double current_sample_rate; + + int current_bit_depth; + int current_block_size_samples; + asio_sample_format input_format[MAX_DEVICE_CHANNELS]; + asio_sample_format output_format[MAX_DEVICE_CHANNELS]; + + ASIOClockSource clocks[MAX_DEVICE_CHANNELS]; + int num_clock_sources; + + ASIOCallbacks callbacks; + volatile bool called_back; + + bool is_open; + bool is_started; + bool driver_failure; + volatile bool need_to_reset; + bool buffers_created; + bool post_output; + volatile bool capture_started; + + long input_latency; + long output_latency; + int xruns; + char errorstring[256]; + + /* Device slot number in the list of devices. This is used to identify the device in the list of devices + * created by the host. */ + int slot_number; + + /* Capture Audio (device ==> OBS) + * Each device will stream audio to a number of OBS ASIO sources acting as audio clients. + * The clients are listed in this darray which stores the asio_data struct ptr. + */ + struct asio_data *obs_clients[32]; + int current_nb_clients; + + /* Output Audio (OBS ==> device) + * Each device can be a client to a single OBS output which outputs audio to the said device. + * 'output_frames': circular buffer to store the frames which are passed to ASIO devices + * Any of the 6 tracks in OBS mixer can be streamed to the device. The plugin also allows to select a channel + * for each track. Therefore one has to specify a track index and a channel index for each output channel of + * the device. -1 means no track or a mute channel. + * obs_track[]: array which holds the track index played for each device output channel + * obs_track_channel[]: array which stores the channel index of an OBS audio track played on an output channel + * Ex: obs_track[ch=2] = 2 & obs_track_channel[2]=1 means we are playing on the third channel (ch +1) channel 2 + * ( = obs_track_channel[2] + 1) from track 3 ( = obs_track[ch=2]+1) + */ + struct asio_data *obs_output_client; + struct deque output_frames[MAX_DEVICE_CHANNELS]; + int obs_track[MAX_DEVICE_CHANNELS]; + int obs_track_channel[MAX_DEVICE_CHANNELS]; +}; + +extern struct asio_device *current_asio_devices[MAX_NUM_ASIO_DEVICES]; + +struct asio_device *asio_device_create(const char *name, CLSID clsid, int slot_number); +void asio_device_destroy(struct asio_device *dev); +void asio_device_test(struct asio_device *dev); + +void asio_device_open(struct asio_device *dev, double sample_rate, long buffer_size_samples); +void asio_device_close(struct asio_device *dev); + +bool asio_device_start(struct asio_device *dev); +void asio_device_stop(struct asio_device *dev); + +void asio_device_dispose_buffers(struct asio_device *dev); + +void asio_device_set_output_client(struct asio_device *dev, struct asio_data *client); +struct asio_data *asio_device_get_output_client(struct asio_device *dev); +void asio_device_reload_channel_names(struct asio_device *dev); + +void asio_device_reset_request(struct asio_device *dev); +long asio_device_asio_message_callback(struct asio_device *dev, long selector, long value, void *message, double *opt); +void asio_device_callback(struct asio_device *dev, long buffer_index); + +double asio_device_get_sample_rate(struct asio_device *dev); +int asio_device_get_preferred_buffer_size(struct asio_device *dev); + +void asio_device_show_control_panel(struct asio_device *dev); +struct asio_device *asio_device_find_by_name(const char *name); diff --git a/plugins/win-asio/asio-format.c b/plugins/win-asio/asio-format.c new file mode 100644 index 00000000000000..0b1f7f63940257 --- /dev/null +++ b/plugins/win-asio/asio-format.c @@ -0,0 +1,182 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of win-asio. + It uses the Steinberg ASIO SDK, which is licensed under the GNU GPL v3. + It is partly based on JUCE ASIoSampleFormat struct in juce_ASIO_windows.cpp + licensed under GPL v3. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "asio-format.h" + +#include "asio-compat.h" +#include "byteorder.h" + +static double jlimit(double lower, double upper, double val) +{ + return val < lower ? lower : (val > upper ? upper : val); +} + +void asio_format_init(asio_sample_format *fmt, long type) +{ + *fmt = (asio_sample_format){.bit_depth = 24, .byte_stride = 4, .format_is_float = false, .little_endian = true}; + + switch (type) { + case ASIOSTInt16MSB: + fmt->bit_depth = 16; + fmt->byte_stride = 2; + fmt->little_endian = false; + break; + case ASIOSTInt24MSB: + fmt->bit_depth = 24; + fmt->byte_stride = 3; + fmt->little_endian = false; + break; + case ASIOSTInt32MSB: + fmt->bit_depth = 32; + fmt->little_endian = false; + break; + case ASIOSTFloat32MSB: + fmt->bit_depth = 32; + fmt->format_is_float = true; + fmt->little_endian = false; + break; + case ASIOSTFloat64MSB: + fmt->bit_depth = 64; + fmt->byte_stride = 8; + fmt->format_is_float = true; + fmt->little_endian = false; + break; + case ASIOSTInt16LSB: + fmt->bit_depth = 16; + fmt->byte_stride = 2; + break; + case ASIOSTInt24LSB: + fmt->bit_depth = 24; + fmt->byte_stride = 3; + break; + case ASIOSTInt32LSB: + fmt->bit_depth = 32; + break; + case ASIOSTFloat32LSB: + fmt->bit_depth = 32; + fmt->format_is_float = true; + break; + case ASIOSTFloat64LSB: + fmt->bit_depth = 64; + fmt->byte_stride = 8; + fmt->format_is_float = true; + break; + default: + break; + } +} + +void asio_format_convert_to_float(const asio_sample_format *fmt, const void *src, float *dst, int samps) +{ + const char *s = (const char *)src; + if (fmt->format_is_float) { + memcpy(dst, src, samps * sizeof(float)); + return; + } + + const double g16 = 1.0 / 32768.0; + const double g24 = 1.0 / 0x7FFFFF; + const double g32 = 1.0 / 0x7FFFFFFF; + + switch (fmt->bit_depth) { + case 16: + while (--samps >= 0) { + int16_t val = fmt->little_endian ? ByteOrder_littleEndianShort(s) : ByteOrder_bigEndianShort(s); + *dst++ = (float)(g16 * val); + s += fmt->byte_stride; + } + break; + case 24: + while (--samps >= 0) { + int32_t val = fmt->little_endian ? ByteOrder_littleEndian24Bit(s) : ByteOrder_bigEndian24Bit(s); + *dst++ = (float)(g24 * val); + s += fmt->byte_stride; + } + break; + case 32: + while (--samps >= 0) { + int32_t val = fmt->little_endian ? ByteOrder_littleEndianInt(s) : ByteOrder_bigEndianInt(s); + *dst++ = (float)(g32 * val); + s += fmt->byte_stride; + } + break; + default: + break; + } +} + +void asio_format_convert_from_float(const asio_sample_format *fmt, const float *src, void *dst, int samps) +{ + char *d = (char *)dst; + if (fmt->format_is_float) { + memcpy(dst, src, samps * sizeof(float)); + return; + } + + const double max16 = 32767.0; + const double max24 = 0x7FFFFF; + const double max32 = 0x7FFFFFFF; + + switch (fmt->bit_depth) { + case 16: + while (--samps >= 0) { + int16_t val = (int16_t)jlimit(-max16, max16, max16 * *src++); + uint16_t word = (uint16_t)val; + word = fmt->little_endian ? ByteOrder_swapIfBigEndian16(word) + : ByteOrder_swapIfLittleEndian16(word); + memcpy(d, &word, 2); + d += fmt->byte_stride; + } + break; + case 24: + while (--samps >= 0) { + int32_t val = (int32_t)jlimit(-max24, max24, max24 * *src++); + uint32_t uval = (uint32_t)val; + if (fmt->little_endian) { + ByteOrder_littleEndian24BitToChars(uval, d); + } else { + ByteOrder_bigEndian24BitToChars(uval, d); + } + d += fmt->byte_stride; + } + break; + case 32: + while (--samps >= 0) { + int32_t val = (int32_t)jlimit(-max32, max32, max32 * *src++); + uint32_t uval = (uint32_t)val; + uval = fmt->little_endian ? ByteOrder_swapIfBigEndian32(uval) + : ByteOrder_swapIfLittleEndian32(uval); + memcpy(d, &uval, 4); + d += fmt->byte_stride; + } + break; + default: + break; + } +} + +void asio_format_clear(const asio_sample_format *fmt, void *dst, int samps) +{ + if (dst) { + memset(dst, 0, samps * fmt->byte_stride); + } +} diff --git a/plugins/win-asio/asio-format.h b/plugins/win-asio/asio-format.h new file mode 100644 index 00000000000000..c9e4da2a1556bb --- /dev/null +++ b/plugins/win-asio/asio-format.h @@ -0,0 +1,38 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of win-asio. + It uses the Steinberg ASIO SDK, which is licensed under the GNU GPL v3. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +typedef struct asio_sample_format { + int bit_depth; + int byte_stride; + bool format_is_float; + bool little_endian; +} asio_sample_format; + +void asio_format_init(asio_sample_format *fmt, long type); + +void asio_format_convert_to_float(const asio_sample_format *fmt, const void *src, float *dst, int samps); + +void asio_format_convert_from_float(const asio_sample_format *fmt, const float *src, void *dst, int samps); + +void asio_format_clear(const asio_sample_format *fmt, void *dst, int samps); diff --git a/plugins/win-asio/byteorder.h b/plugins/win-asio/byteorder.h new file mode 100644 index 00000000000000..db2f5d9b7e50e2 --- /dev/null +++ b/plugins/win-asio/byteorder.h @@ -0,0 +1,102 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of win-asio. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include + +#define ByteOrder_swap16(x) _byteswap_ushort(x) +#define ByteOrder_swap32(x) _byteswap_ulong(x) + +static inline int16_t ByteOrder_littleEndianShort(const void *p) +{ + uint16_t v; + memcpy(&v, p, sizeof(v)); + return (int16_t)v; +} + +static inline int16_t ByteOrder_bigEndianShort(const void *p) +{ + uint16_t v; + memcpy(&v, p, sizeof(v)); + return (int16_t)ByteOrder_swap16(v); +} + +static inline int32_t ByteOrder_littleEndianInt(const void *p) +{ + uint32_t v; + memcpy(&v, p, sizeof(v)); + return (int32_t)v; +} + +static inline int32_t ByteOrder_bigEndianInt(const void *p) +{ + uint32_t v; + memcpy(&v, p, sizeof(v)); + return (int32_t)ByteOrder_swap32(v); +} + +static inline int32_t ByteOrder_littleEndian24Bit(const void *p) +{ + const uint8_t *b = (const uint8_t *)p; + return (int32_t)((b[2] << 24) | (b[1] << 16) | (b[0] << 8)) >> 8; +} + +static inline int32_t ByteOrder_bigEndian24Bit(const void *p) +{ + const uint8_t *b = (const uint8_t *)p; + return (int32_t)((b[0] << 24) | (b[1] << 16) | (b[2] << 8)) >> 8; +} + +static inline void ByteOrder_littleEndian24BitToChars(uint32_t val, void *p) +{ + uint8_t *b = (uint8_t *)p; + b[0] = (uint8_t)(val & 0xFF); + b[1] = (uint8_t)((val >> 8) & 0xFF); + b[2] = (uint8_t)((val >> 16) & 0xFF); +} + +static inline void ByteOrder_bigEndian24BitToChars(uint32_t val, void *p) +{ + uint8_t *b = (uint8_t *)p; + b[0] = (uint8_t)((val >> 16) & 0xFF); + b[1] = (uint8_t)((val >> 8) & 0xFF); + b[2] = (uint8_t)(val & 0xFF); +} + +static inline uint16_t ByteOrder_swapIfBigEndian16(uint16_t v) +{ + return v; +} + +static inline uint16_t ByteOrder_swapIfLittleEndian16(uint16_t v) +{ + return ByteOrder_swap16(v); +} + +static inline uint32_t ByteOrder_swapIfBigEndian32(uint32_t v) +{ + return v; +} + +static inline uint32_t ByteOrder_swapIfLittleEndian32(uint32_t v) +{ + return ByteOrder_swap32(v); +} diff --git a/plugins/win-asio/cmake/windows/obs-module.rc.in b/plugins/win-asio/cmake/windows/obs-module.rc.in new file mode 100644 index 00000000000000..c16e698097cb7b --- /dev/null +++ b/plugins/win-asio/cmake/windows/obs-module.rc.in @@ -0,0 +1,24 @@ +1 VERSIONINFO +FILEVERSION ${OBS_VERSION_MAJOR},${OBS_VERSION_MINOR},${OBS_VERSION_PATCH},0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "${OBS_COMPANY_NAME}" + VALUE "FileDescription", "OBS ASIO module" + VALUE "FileVersion", "${OBS_VERSION_CANONICAL}" + VALUE "ProductName", "${OBS_PRODUCT_NAME}" + VALUE "ProductVersion", "${OBS_VERSION_CANONICAL}" + VALUE "Comments", "${OBS_COMMENTS}" + VALUE "LegalCopyright", "${OBS_LEGAL_COPYRIGHT}" + VALUE "InternalName", "win-asio" + VALUE "OriginalFilename", "win-asio" + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 0x04B0 + END +END diff --git a/plugins/win-asio/data/locale/en-US.ini b/plugins/win-asio/data/locale/en-US.ini new file mode 100644 index 00000000000000..e93eacbc464c68 --- /dev/null +++ b/plugins/win-asio/data/locale/en-US.ini @@ -0,0 +1,105 @@ +ASIO.Driver="ASIO Drivers" +ASIO.Driver.Error="The driver failed to load. Consult the log for possible hints." +ASIO.Driver.None="No ASIO audio driver installed." +Control.Panel="ASIO Device Control Panel" +Control.Panel.Hint="If you change any settings of the device, like the buffer, while it's streaming, hit the reload button to reload the driver in case of glitches (due to bad drivers not messaging changes to hosts)." +Reset.Device="Reload driver" +Reset.Device.Hint="Reloads manually the driver. This is useful in case of driver settings being modified (sample rate, buffer, etc.)." + +ASIO.Input.Capture="ASIO input" +ASIO.Output="ASIO Output" +ASIO.Output.Hint="Any channel from the 6 audio tracks can be selected.\nAn additional monitoring track is provided which mixes all sources which are not set to 'Monitor Off'.\n" +Select.Device="Select a device" + +Mute="Mute" + +OBS.Channels.0="OBS Channel 1" +OBS.Channels.1="OBS Channel 2" +OBS.Channels.2="OBS Channel 3" +OBS.Channels.3="OBS Channel 4" +OBS.Channels.4="OBS Channel 5" +OBS.Channels.5="OBS Channel 6" +OBS.Channels.6="OBS Channel 7" +OBS.Channels.7="OBS Channel 8" + +Device_ch.0="Device Channel 1" +Device_ch.1="Device Channel 2" +Device_ch.2="Device Channel 3" +Device_ch.3="Device Channel 4" +Device_ch.4="Device Channel 5" +Device_ch.5="Device Channel 6" +Device_ch.6="Device Channel 7" +Device_ch.7="Device Channel 8" +Device_ch.8="Device Channel 9" +Device_ch.9="Device Channel 10" +Device_ch.10="Device Channel 11" +Device_ch.11="Device Channel 12" +Device_ch.12="Device Channel 13" +Device_ch.13="Device Channel 14" +Device_ch.14="Device Channel 15" +Device_ch.15="Device Channel 16" +Device_ch.16="Device Channel 17" +Device_ch.17="Device Channel 18" +Device_ch.18="Device Channel 19" +Device_ch.19="Device Channel 20" +Device_ch.20="Device Channel 21" +Device_ch.21="Device Channel 22" +Device_ch.22="Device Channel 23" +Device_ch.23="Device Channel 24" +Device_ch.24="Device Channel 25" +Device_ch.25="Device Channel 26" +Device_ch.26="Device Channel 27" +Device_ch.27="Device Channel 28" +Device_ch.28="Device Channel 29" +Device_ch.29="Device Channel 30" +Device_ch.30="Device Channel 31" +Device_ch.31="Device Channel 32" + +Track0.0="Track 1 Channel 1" +Track0.1="Track 1 Channel 2" +Track0.2="Track 1 Channel 3" +Track0.3="Track 1 Channel 4" +Track0.4="Track 1 Channel 5" +Track0.5="Track 1 Channel 6" +Track0.6="Track 1 Channel 7" +Track1.7="Track 1 Channel 8" +Track1.0=" Track 2 Channel 1" +Track1.1=" Track 2 Channel 2" +Track1.2=" Track 2 Channel 3" +Track1.3=" Track 2 Channel 4" +Track1.4=" Track 2 Channel 5" +Track1.5=" Track 2 Channel 6" +Track1.6=" Track 2 Channel 7" +Track1.7=" Track 2 Channel 8" +Track2.0=" Track 3 Channel 1" +Track2.1=" Track 3 Channel 2" +Track2.2=" Track 3 Channel 3" +Track2.3=" Track 3 Channel 4" +Track2.4=" Track 3 Channel 5" +Track2.5=" Track 3 Channel 6" +Track2.6=" Track 3 Channel 7" +Track2.7=" Track 3 Channel 8" +Track3.0=" Track 4 Channel 1" +Track3.1=" Track 4 Channel 2" +Track3.2=" Track 4 Channel 3" +Track3.3=" Track 4 Channel 4" +Track3.4=" Track 4 Channel 5" +Track3.5=" Track 4 Channel 6" +Track3.6=" Track 4 Channel 7" +Track3.7=" Track 4 Channel 8" +Track4.0=" Track 5 Channel 1" +Track4.1=" Track 5 Channel 2" +Track4.2=" Track 5 Channel 3" +Track4.3=" Track 5 Channel 4" +Track4.4=" Track 5 Channel 5" +Track4.5=" Track 5 Channel 6" +Track4.6=" Track 5 Channel 7" +Track4.7=" Track 5 Channel 8" +Track5.0=" Track 6 Channel 1" +Track5.1=" Track 6 Channel 2" +Track5.2=" Track 6 Channel 3" +Track5.3=" Track 6 Channel 4" +Track5.4=" Track 6 Channel 5" +Track5.5=" Track 6 Channel 6" +Track5.6=" Track 6 Channel 7" +Track5.7=" Track 6 Channel 8" diff --git a/plugins/win-asio/iasiodrv.h b/plugins/win-asio/iasiodrv.h new file mode 100644 index 00000000000000..f0b665de7d42c2 --- /dev/null +++ b/plugins/win-asio/iasiodrv.h @@ -0,0 +1,102 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of win-asio. It adapts to C the IASIO COM interface. + It uses the Steinberg ASIO SDK, which is licensed under the GNU GPL v3. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "asio-compat.h" + +#include + +extern const IID IID_IASIO; + +typedef struct IASIO IASIO; + +typedef struct IASIOVtbl { + HRESULT(STDMETHODCALLTYPE *QueryInterface)(IASIO *This, REFIID riid, void **ppvObject); + ULONG(STDMETHODCALLTYPE *AddRef)(IASIO *This); + ULONG(STDMETHODCALLTYPE *Release)(IASIO *This); + + ASIOBool(STDMETHODCALLTYPE *init)(IASIO *This, void *sysHandle); + void(STDMETHODCALLTYPE *getDriverName)(IASIO *This, char *name); + long(STDMETHODCALLTYPE *getDriverVersion)(IASIO *This); + void(STDMETHODCALLTYPE *getErrorMessage)(IASIO *This, char *string); + ASIOError(STDMETHODCALLTYPE *start)(IASIO *This); + ASIOError(STDMETHODCALLTYPE *stop)(IASIO *This); + ASIOError(STDMETHODCALLTYPE *getChannels)(IASIO *This, long *numInputChannels, long *numOutputChannels); + ASIOError(STDMETHODCALLTYPE *getLatencies)(IASIO *This, long *inputLatency, long *outputLatency); + ASIOError(STDMETHODCALLTYPE *getBufferSize)(IASIO *This, long *minSize, long *maxSize, long *preferredSize, + long *granularity); + ASIOError(STDMETHODCALLTYPE *canSampleRate)(IASIO *This, double sampleRate); + ASIOError(STDMETHODCALLTYPE *getSampleRate)(IASIO *This, double *sampleRate); + ASIOError(STDMETHODCALLTYPE *setSampleRate)(IASIO *This, double sampleRate); + ASIOError(STDMETHODCALLTYPE *getClockSources)(IASIO *This, void *clocks, long *numSources); + ASIOError(STDMETHODCALLTYPE *setClockSource)(IASIO *This, long reference); + ASIOError(STDMETHODCALLTYPE *getSamplePosition)(IASIO *This, void *sPos, void *tStamp); + ASIOError(STDMETHODCALLTYPE *getChannelInfo)(IASIO *This, void *info); + ASIOError(STDMETHODCALLTYPE *createBuffers)(IASIO *This, void *bufferInfos, long numChannels, long bufferSize, + void *callbacks); + ASIOError(STDMETHODCALLTYPE *disposeBuffers)(IASIO *This); + ASIOError(STDMETHODCALLTYPE *controlPanel)(IASIO *This); + ASIOError(STDMETHODCALLTYPE *future)(IASIO *This, long selector, void *opt); + ASIOError(STDMETHODCALLTYPE *outputReady)(IASIO *This); +} IASIOVtbl; + +struct IASIO { + const IASIOVtbl *lpVtbl; +}; + +#define ASIO_QueryInterface(dev, riid, ppv) (dev)->asio->lpVtbl->QueryInterface((dev)->asio,(riid),(ppv)) +#define ASIO_AddRef(dev) (dev)->asio->lpVtbl->AddRef((dev)->asio) +#define ASIO_Release(dev) (dev)->asio->lpVtbl->Release((dev)->asio) + +#define ASIO_Init(dev, sys) (dev)->asio->lpVtbl->init((dev)->asio,(sys)) +#define ASIO_GetDriverName(dev, name) (dev)->asio->lpVtbl->getDriverName((dev)->asio,(name)) +#define ASIO_GetDriverVersion(dev) (dev)->asio->lpVtbl->getDriverVersion((dev)->asio) +#define ASIO_GetErrorMessage(dev, str) (dev)->asio->lpVtbl->getErrorMessage((dev)->asio,(str)) + +#define ASIO_Start(dev) (dev)->asio->lpVtbl->start((dev)->asio) +#define ASIO_Stop(dev) (dev)->asio->lpVtbl->stop((dev)->asio) + +#define ASIO_GetChannels(dev, in, out) (dev)->asio->lpVtbl->getChannels((dev)->asio,(in),(out)) +#define ASIO_GetLatencies(dev, in, out) (dev)->asio->lpVtbl->getLatencies((dev)->asio,(in),(out)) + +#define ASIO_GetBufferSize(dev, min, max, pref, gran) (dev)->asio->lpVtbl->getBufferSize((dev)->asio,(min),(max),(pref),(gran)) + +#define ASIO_CanSampleRate(dev, rate) (dev)->asio->lpVtbl->canSampleRate((dev)->asio,(rate)) +#define ASIO_GetSampleRate(dev, rate) (dev)->asio->lpVtbl->getSampleRate((dev)->asio,(rate)) +#define ASIO_SetSampleRate(dev, rate) (dev)->asio->lpVtbl->setSampleRate((dev)->asio,(rate)) + +#define ASIO_GetClockSources(dev, clocks, num) (dev)->asio->lpVtbl->getClockSources((dev)->asio,(clocks),(num)) + +#define ASIO_SetClockSource(dev, idx) (dev)->asio->lpVtbl->setClockSource((dev)->asio,(idx)) + +#define ASIO_GetSamplePosition(dev, pos, ts) (dev)->asio->lpVtbl->getSamplePosition((dev)->asio,(pos),(ts)) + +#define ASIO_GetChannelInfo(dev, info) (dev)->asio->lpVtbl->getChannelInfo((dev)->asio,(info)) + +#define ASIO_CreateBuffers(dev, infos, n, size, cb) (dev)->asio->lpVtbl->createBuffers((dev)->asio,(infos),(n),(size),(cb)) + +#define ASIO_DisposeBuffers(dev) (dev)->asio->lpVtbl->disposeBuffers((dev)->asio) + +#define ASIO_ControlPanel(dev) (dev)->asio->lpVtbl->controlPanel((dev)->asio) + +#define ASIO_Future(dev, sel, opt) (dev)->asio->lpVtbl->future((dev)->asio,(sel),(opt)) + +#define ASIO_OutputReady(dev) (dev)->asio->lpVtbl->outputReady((dev)->asio) diff --git a/plugins/win-asio/plugin-main.c b/plugins/win-asio/plugin-main.c new file mode 100644 index 00000000000000..1f442875419800 --- /dev/null +++ b/plugins/win-asio/plugin-main.c @@ -0,0 +1,62 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of win-asio. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include +#include +#include + +const char *PLUGIN_VERSION = "1.0.0"; + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("win-asio", "en-US") + +MODULE_EXPORT const char *obs_module_description(void) +{ + return "ASIO audio plugin"; +} + +extern os_event_t *shutting_down; +extern struct obs_source_info asio_input_capture; +extern struct obs_output_info asio_output; +void retrieve_device_list(); +void free_device_list(); +void OBSEvent(enum obs_frontend_event event, void *); + +bool obs_module_load(void) +{ + retrieve_device_list(); + + obs_register_source(&asio_input_capture); + blog(LOG_INFO, "ASIO plugin loaded successfully (version %s)", PLUGIN_VERSION); + + if (os_event_init(&shutting_down, OS_EVENT_TYPE_AUTO)) { + return false; + } + + obs_frontend_add_event_callback(OBSEvent, NULL); + obs_register_output(&asio_output); + return true; +} + +void obs_module_unload() +{ + free_device_list(); + os_event_destroy(shutting_down); + obs_frontend_remove_event_callback(OBSEvent, NULL); +} diff --git a/plugins/win-asio/win-asio.c b/plugins/win-asio/win-asio.c new file mode 100644 index 00000000000000..cc4e4ea9d81aee --- /dev/null +++ b/plugins/win-asio/win-asio.c @@ -0,0 +1,861 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of win-asio. + It uses the Steinberg ASIO SDK, which is licensed under the GNU GPL v3. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "win-asio.h" + +#include "asio-device-list.h" + +#include + +#include +#include + +extern os_event_t *shutting_down; +extern volatile bool shutting_down_atomic; +struct asio_data *global_output_asio_data = NULL; +obs_data_t *global_output_settings = NULL; +struct asio_device_list *list = NULL; + +/*==================================== DEVICE SCAN =====================================*/ +void retrieve_device_list() +{ + list = asio_device_list_create(); +} + +void free_device_list() +{ + asio_device_list_destroy(list); +} + +/*============================== COMMON TO INPUT & OUTPUT ==============================*/ + +#define ASIODATA_LOG(level, fmt, ...) \ + blog(level, "[%s '%s']: " fmt, \ + (data)->is_output ? "asio_output" : "asio_input", \ + (data)->is_output \ + ? ((data)->device_name ? (data)->device_name : "(none)") \ + : ((data)->source ? obs_source_get_name((data)->source) : "(null)"), \ + ##__VA_ARGS__) + +#define debugdata(fmt, ...) ASIODATA_LOG(LOG_DEBUG, fmt, ##__VA_ARGS__) +#define infodata(fmt, ...) ASIODATA_LOG(LOG_INFO, fmt, ##__VA_ARGS__) +#define errordata(fmt, ...) ASIODATA_LOG(LOG_ERROR, fmt, ##__VA_ARGS__) +#define warndata(fmt, ...) ASIODATA_LOG(LOG_WARNING, fmt, ##__VA_ARGS__) + +bool attach_device(struct asio_data *data, const char *name) +{ + if (!name || !*name) { + return false; + } + + data->device_index = asio_device_list_get_index_from_driver_name(list, name); + if (data->device_index < 0) { + errordata("This driver was not found in the registry: %s", name); + data->driver_loaded = false; + data->device_name = NULL; + return false; + } + + /* We set the name even if the driver fails to load in order to be able to deal with disconnected devices. */ + if (data->device_name) { + bfree((void *)data->device_name); + } + data->device_name = bstrdup(name); + + /* Retrieve the struct asio_device ptr and create the asio_device ptr if NULL. */ + data->asio_device = asio_device_list_attach_device(list, name); + struct asio_device *dev = data->asio_device; + if (!dev) { + errordata("Failed to create device %s ", name); + return false; + } else if (!dev->asio) { + errordata( + "Driver could not find a connected device or device might already be in use by another host."); + data->asio_device = NULL; + return false; + } + + if (!data->is_output) { + /* Update the device client list if the src was never a client and add src ptr as a client of ASIO device. */ + int nb_clients = dev->current_nb_clients; + data->asio_client_index[data->device_index] = nb_clients; + dev->obs_clients[nb_clients] = data; + dev->current_nb_clients++; + } else { + dev->obs_output_client = data; + } + + return true; +} + +static void detach_device(struct asio_data *data) +{ + if (!data || !data->asio_device) { + return; + } + + struct asio_device *dev = data->asio_device; + const bool is_output = data->is_output; + + /*--- Output path ---*/ + if (is_output) { + data->device_index = -1; + debugdata("Detached device %s", data->device_name); + + dev->obs_output_client = NULL; + infodata("Device removed; xruns=%d. (-1 means device doesn't report xruns)", dev->xruns); + + /* Close device if no clients remain */ + if (dev->current_nb_clients == 0) { + asio_device_close(dev); + } + + return; + } + + /*--- Input path ---*/ + int prev_dev_idx = asio_device_list_get_index_from_driver_name(list, data->device_name); + if (prev_dev_idx < 0) { + return; + } + + int prev_client_idx = data->asio_client_index[prev_dev_idx]; + if (!dev->is_open || dev->current_nb_clients <= 0 || prev_client_idx < 0) { + return; + } + + dev->obs_clients[prev_client_idx] = NULL; + dev->current_nb_clients--; + data->device_index = -1; + + if (dev->current_nb_clients == 0 && !dev->obs_output_client) { + infodata("Device %s removed; xruns=%d. (-1 means xruns not reported). " + "Increase buffer if you hear cracks/pops.", + data->device_name, dev->xruns); + if (dev->xruns > 0) { + infodata("XRuns detected: %d. Increase your buffer size if needed.", dev->xruns); + } + asio_device_close(dev); + } +} + +static inline bool strdiff(const char *a, const char *b) +{ + if (!a && !b) { + return false; + } + if (!a || !b) { + return true; + } + return strcmp(a, b) != 0; +} + +static void asio_input_update(void *vptr, obs_data_t *settings); +static void asio_output_update(void *vptr, obs_data_t *settings); +static void asio_update(void *vptr, obs_data_t *settings, bool is_output) +{ + struct asio_data *data = NULL; + + if (is_output && !global_output_settings) { + global_output_settings = settings; + obs_data_addref(global_output_settings); + } + + if (!is_output) { + data = (struct asio_data *)vptr; + } else { + UNUSED_PARAMETER(vptr); + data = global_output_asio_data; + } + + const char *new_device = obs_data_get_string(settings, "device_name"); + /* if new device is "" (== no device), return */ + if (!new_device || !*new_device) { + /* we might actually be swapping from a 'device' to "" */ + if (data->device_name && *data->device_name && data->driver_loaded) { + detach_device(data); + bfree((void *)data->device_name); + data->device_name = NULL; + data->driver_loaded = false; + data->update_channels = false; + } + data->asio_device = NULL; + data->initial_update = false; + for (int i = 0; i < MAX_AUDIO_CHANNELS; i++) { + data->mix_channels[i] = -1; + } + for (int i = 0; i < MAX_DEVICE_CHANNELS; i++) { + data->out_mix_channels[i] = -1; + } + return; + } + + /* we are loading an asio_data which had already settings */ + if (data->initial_update && new_device) { + data->driver_loaded = attach_device(data, new_device); + /* If the driver fails to load, we keep the name but the driver will be greyed out on next properties call. */ + if (!data->driver_loaded) { + data->asio_device = NULL; + data->initial_update = false; + return; + } + } + + /* we swap from a 'device' to a 'new device' */ + if (!data->initial_update && strdiff(data->device_name, new_device)) { + if (data->device_name && *data->device_name && data->driver_loaded) { + detach_device(data); + } + + data->driver_loaded = attach_device(data, new_device); + data->update_channels = data->driver_loaded; + if (!data->driver_loaded) { + data->asio_device = NULL; + return; + } + if (data->driver_loaded && is_output) { + /* Reset all mix channels to -1 to avoid leftover routing */ + for (int i = 0; i < MAX_DEVICE_CHANNELS; i++) { + data->out_mix_channels[i] = -1; + data->asio_device->obs_track[i] = -1; + data->asio_device->obs_track_channel[i] = -1; + } + } + if (data->driver_loaded && !is_output) { + for (int i = 0; i < MAX_AUDIO_CHANNELS; i++) { + data->mix_channels[i] = -1; + } + } + } + + struct asio_device *dev = data->asio_device; + if (!dev) { + data->initial_update = false; + return; + } + + if (!dev->is_open) { + double obs_sr = audio_output_get_sample_rate(obs_get_audio()); + + int buffer_size = asio_device_get_preferred_buffer_size(dev); + if (buffer_size < 16) { + buffer_size = 512; + } + + asio_device_open(dev, obs_sr, buffer_size); + if (!dev->is_open) { + infodata("\nconnected to device %s;" + "\n\tcurrent sample rate: %f," + "\n\tcurrent buffer: %i," + "\n\tinput latency: %f ms\n", + data->device_name, dev->current_sample_rate, dev->current_buffer_size, + 1000.0f * (float)dev->input_latency / dev->current_sample_rate); + } + } + data->initial_update = false; + + /*--- Input path ---*/ + if (!is_output) { + /* update the routing */ + for (int i = 0; i < data->out_channels; i++) { + char key[32]; + snprintf(key, sizeof(key), "OBS.Channels.%d", i); + data->mix_channels[i] = (int)obs_data_get_int(settings, key); + } + return; + } + + /*--- Output path ---*/ + if (dev->is_started) { + os_atomic_set_bool(&dev->capture_started, true); + } + + data->out_channels = (uint8_t)data->asio_device->total_num_output_chans; + + /* update the routing data for each output device channels & pass the info to the device */ + for (int i = 0; i < data->out_channels; ++i) { + char key[32]; + snprintf(key, sizeof(key), "device_ch%d", i); + if (data->out_mix_channels[i] != (int)obs_data_get_int(settings, key)) { + data->out_mix_channels[i] = (int)obs_data_get_int(settings, key); + data->asio_device->obs_track[i] = -1; // device does not use track i + data->asio_device->obs_track_channel[i] = -1; // device does not use any channel from track i + if (data->out_mix_channels[i] >= 0) { + for (int j = 0; j < MAX_AUDIO_MIXES; j++) { + for (int k = 0; k < data->obs_track_channels; k++) { + int64_t idx = (int64_t)k + (1LL << (j + 4)); + if (data->out_mix_channels[i] == idx) { + data->asio_device->obs_track[i] = j; + data->asio_device->obs_track_channel[i] = k; + blog(LOG_DEBUG, + "[asio_output]:\nDevice output channel n° %i: Track %i, Channel %i", + i, j + 1, k + 1); + } + } + } + } + } + } +} + +static void *asio_create_internal(obs_data_t *settings, void *owner, bool is_output) +{ + struct asio_data *data = bzalloc(sizeof(struct asio_data)); + data->asio_device = NULL; + data->device_name = NULL; + data->update_channels = false; + data->driver_loaded = false; + data->initial_update = true; + data->is_output = is_output; + os_atomic_set_bool(&data->stopping, false); + os_atomic_set_bool(&data->active, true); + + for (int i = 0; i < MAX_NUM_ASIO_DEVICES; i++) { + data->asio_client_index[i] = -1; // not a client if negative; + } + + if (is_output) { + /* ---- OUTPUT SETUP ---- */ + data->source = NULL; + data->output = (obs_output_t *)owner; + data->obs_track_channels = (uint8_t)audio_output_get_channels(obs_get_audio()); + + /* default value is negative, which implies no processing */ + for (int i = 0; i < MAX_DEVICE_CHANNELS; i++) { + data->out_mix_channels[i] = -1; + } + + /* allow all tracks for asio output + extra monitoring track */ + obs_output_set_mixers(data->output, (1 << 7) - 1); + + if (global_output_asio_data) { + infodata("issue with asio output code! multiple outputs opened!"); + } + + global_output_asio_data = data; + } else { + /* ---- INPUT SETUP ---- */ + data->output = NULL; + data->source = (obs_source_t *)owner; + data->out_channels = (uint8_t)audio_output_get_channels(obs_get_audio()); + + for (int i = 0; i < MAX_AUDIO_CHANNELS; i++) { + data->mix_channels[i] = -1; + } + + infodata("Source created successfully."); + } + is_output ? asio_output_update(data, settings) : asio_input_update(data, settings); + + return data; +} + +static void asio_destroy(void *vptr) +{ + struct asio_data *data = (struct asio_data *)vptr; + + if (!data) { + return; + } + + os_atomic_set_bool(&data->stopping, true); + + if (!os_atomic_load_bool(&shutting_down_atomic)) { + if (data->asio_device) { + detach_device(data); + } + } + + if (data->device_name) { + bfree((void *)data->device_name); + } + + data->asio_device = NULL; + + if (data->is_output) { + obs_data_release(global_output_settings); + global_output_asio_data = NULL; + global_output_settings = NULL; + } + + bfree(data); +} + +static bool display_control_panel_input(obs_properties_t *props, obs_property_t *property, void *vptr); +static bool display_control_panel_output(obs_properties_t *props, obs_property_t *property, void *vptr); +static bool display_control_panel(obs_properties_t *props, obs_property_t *property, void *vptr, bool is_output) +{ + UNUSED_PARAMETER(props); + UNUSED_PARAMETER(property); + struct asio_data *data = NULL; + struct asio_device *dev = NULL; + + if (!is_output) { + if (!vptr) { + return false; + } + + data = (struct asio_data *)vptr; + } else { + UNUSED_PARAMETER(vptr); + data = global_output_asio_data; + } + + dev = data->asio_device; + if (dev) { + asio_device_show_control_panel(dev); + } else { + return false; + } + + return true; +} + +static bool on_reset_input_device_clicked(obs_properties_t *props, obs_property_t *property, void *vptr); +static bool on_reset_output_device_clicked(obs_properties_t *props, obs_property_t *property, void *vptr); +static bool on_reset_device_clicked(obs_properties_t *props, obs_property_t *property, void *vptr, bool is_output) +{ + UNUSED_PARAMETER(props); + UNUSED_PARAMETER(property); + struct asio_data *data = NULL; + struct asio_device *dev = NULL; + + if (!is_output) { + if (!vptr) { + return false; + } + + data = (struct asio_data *)vptr; + } else { + UNUSED_PARAMETER(vptr); + data = global_output_asio_data; + } + + dev = data->asio_device; + + if (dev) { + asio_device_reset_request(dev); + } else { + return false; + } + + return true; +} + +static void asio_defaults(obs_data_t *settings) +{ + if (!settings || !list || list->count == 0) { + return; + } + + obs_data_set_default_string(settings, "device_name", NULL); + + /* Set default channel routing (-1 means unassigned/muted) */ + for (int i = 0; i < MAX_DEVICE_CHANNELS; i++) { + char key[32]; + snprintf(key, sizeof(key), "OBS.Channels.%d", i); + snprintf(key, sizeof(key), "device_ch%d", i); + obs_data_set_default_int(settings, key, -1); + } +} + +static bool on_asio_device_changed(void *priv, obs_properties_t *props, obs_property_t *list_prop, + obs_data_t *cur_settings) +{ + struct asio_data *data = (struct asio_data *)priv; + const bool is_output = data->is_output; + obs_data_t *settings = cur_settings; + + /* For output, ignore transient settings and use the global one */ + if (is_output) { + if (!global_output_settings) { + return false; + } + settings = global_output_settings; + } + + const char *device_name = obs_data_get_string(settings, "device_name"); + if (!device_name) { + return false; + } + + struct asio_device *dev = asio_device_find_by_name(device_name); + const size_t count = list ? list->count : 0; + + /* === Case 1: driver missing or device not found === */ + /* For input, show only "Mute" for each output channel. For output, show nothing. */ + if (!dev) { + const int max_channels = is_output ? MAX_DEVICE_CHANNELS : MAX_AUDIO_CHANNELS; + + for (int i = 0; i < max_channels; i++) { + char key[64]; + snprintf(key, sizeof(key), is_output ? "device_ch%d" : "OBS.Channels.%d", i); + obs_data_set_int(settings, key, -1); + obs_property_t *p = obs_properties_get(props, key); + if (!p) { + continue; + } + + if (is_output) { + obs_properties_remove_by_name(props, key); + } else { + obs_property_list_clear(p); + obs_property_list_add_int(p, obs_module_text("Mute"), -1); + } + } + + obs_property_t *error = obs_properties_get(props, "error"); + obs_property_set_visible(error, (count && *device_name)); + + asio_update(data, settings, is_output); + return true; + } + + /* === Case 2: device present and channels need update === */ + if (data->update_channels) { + if (is_output) { + /* --- OUTPUT: rebuild all device_ch lists --- */ + for (int i = 0; i < MAX_DEVICE_CHANNELS; ++i) { + char key[32]; + snprintf(key, sizeof(key), "device_ch%d", i); + obs_data_set_int(settings, key, -1); + obs_properties_remove_by_name(props, key); + } + + for (int i = 0; i < dev->total_num_output_chans; ++i) { + char key[32]; + snprintf(key, sizeof(key), "device_ch%d", i); + obs_property_t *p = obs_properties_add_list(props, key, dev->output_channel_names[i], + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + + obs_property_list_add_int(p, obs_module_text("Mute"), -1); + + for (int j = 0; j < MAX_AUDIO_MIXES; j++) { + for (int k = 0; k < global_output_asio_data->obs_track_channels; k++) { + long long idx = k + (1ULL << (j + 4)); + char label[32]; + snprintf(label, sizeof(label), "Track%d.%d", j, k); + obs_property_list_add_int(p, obs_module_text(label), idx); + } + } + } + } else { + /* --- INPUT: rebuild all OBS.Channels lists --- */ + for (int i = 0; i < MAX_AUDIO_CHANNELS; i++) { + char key[32]; + snprintf(key, sizeof(key), "OBS.Channels.%d", i); + obs_data_set_int(settings, key, -1); + obs_property_t *p = obs_properties_get(props, key); + if (!p) { + continue; + } + + obs_property_list_clear(p); + obs_property_list_add_int(p, obs_module_text("Mute"), -1); + for (int j = 0; j < dev->total_num_input_chans; ++j) { + obs_property_list_add_int(p, dev->input_channel_names[j], j); + } + } + } + + obs_property_t *error = obs_properties_get(props, "error"); + obs_property_set_visible(error, false); + + data->update_channels = false; + asio_update(data, settings, is_output); // required to update to the mute values ! + } + + return true; +} + +static obs_properties_t *asio_properties_internal(void *vptr, bool is_output) +{ + obs_properties_t *props = obs_properties_create(); + size_t count = list ? list->count : 0; + + struct asio_data *data = NULL; + if (is_output) { + UNUSED_PARAMETER(vptr); + data = global_output_asio_data; + } else if (!is_output) { + data = (struct asio_data *)vptr; + } + + struct asio_device *dev = data ? data->asio_device : NULL; + + obs_property_t *p = obs_properties_add_list(props, "device_name", obs_module_text("ASIO.Driver"), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(p, obs_module_text("Select.Device"), ""); + + for (size_t i = 0; i < count; ++i) { + const char *name = asio_device_list_get_name(list, i); + obs_property_list_add_string(p, name, name); + } + + obs_property_set_modified_callback2(p, on_asio_device_changed, data); + + obs_property_t *panel = obs_properties_add_button2( + props, "ctrl", obs_module_text("Control.Panel"), + is_output ? display_control_panel_output : display_control_panel_input, vptr); + obs_property_set_long_description(panel, obs_module_text("Control.Panel.Hint")); + + obs_property_t *reset = obs_properties_add_button2( + props, "reset_button", obs_module_text("Reset.Device"), + is_output ? on_reset_output_device_clicked : on_reset_output_device_clicked, vptr); + obs_property_set_long_description(reset, obs_module_text("Reset.Device.Hint")); + + if (is_output) { + obs_property_t *warning = obs_properties_add_text(props, "hint", NULL, OBS_TEXT_INFO); + obs_property_text_set_info_type(warning, OBS_TEXT_INFO_WARNING); + obs_property_set_long_description(warning, obs_module_text("ASIO.Output.Hint")); + } + + if (!is_output) { + int obs_channels = (int)audio_output_get_channels(obs_get_audio()); + for (int i = 0; i < obs_channels; ++i) { + char key[64]; + snprintf(key, sizeof(key), "OBS.Channels.%d", i); + obs_property_t *lp = obs_properties_add_list(props, key, obs_module_text(key), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(lp, obs_module_text("Mute"), -1); + if (dev) { + for (int j = 0; j < dev->total_num_input_chans; ++j) { + obs_property_list_add_int(lp, dev->input_channel_names[j], j); + } + } + } + } else { + int dev_out_channels = dev ? dev->total_num_output_chans : -1; + if (dev_out_channels >= 0) { + for (int i = 0; i < dev_out_channels; i++) { + char key[32]; + snprintf(key, sizeof(key), "device_ch%d", i); + obs_property_t *lp = obs_properties_add_list(props, key, dev->output_channel_names[i], + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(lp, obs_module_text("Mute"), -1); + + for (int j = 0; j < MAX_AUDIO_MIXES; j++) { + for (int k = 0; k < global_output_asio_data->obs_track_channels; k++) { + long long idx = k + (1ULL << (j + 4)); + char label[32]; + snprintf(label, sizeof(label), "Track%d.%d", j, k); + obs_property_list_add_int(lp, obs_module_text(label), idx); + } + } + } + } + } + + obs_property_t *error = obs_properties_add_text(props, "error", NULL, OBS_TEXT_INFO); + obs_property_text_set_info_type(error, OBS_TEXT_INFO_ERROR); + obs_property_set_long_description(error, obs_module_text("ASIO.Driver.Error")); + obs_property_set_visible(error, false); + + obs_property_t *no_asio = obs_properties_add_text(props, "noasio", NULL, OBS_TEXT_INFO); + obs_property_text_set_info_type(no_asio, OBS_TEXT_INFO_ERROR); + obs_property_set_long_description(no_asio, obs_module_text("ASIO.Driver.None")); + obs_property_set_visible(no_asio, !count); + + return props; +} + +/*===================================== ASIO INPUT =====================================*/ +static const char *asio_input_getname(void *unused) +{ + UNUSED_PARAMETER(unused); + return obs_module_text("ASIO.Input.Capture"); +} + +static void asio_input_update(void *vptr, obs_data_t *settings) +{ + + asio_update(vptr, settings, false); +} + +static void *asio_input_create(obs_data_t *settings, obs_source_t *source) +{ + return asio_create_internal(settings, source, false); +} + +static bool display_control_panel_input(obs_properties_t *props, obs_property_t *property, void *vptr) +{ + return display_control_panel(props, property, vptr, false); +} + +static bool on_reset_input_device_clicked(obs_properties_t *props, obs_property_t *property, void *vptr) +{ + return on_reset_device_clicked(props, property, vptr, false); +} + +static obs_properties_t *asio_input_properties(void *vptr) +{ + return asio_properties_internal(vptr, false); +} + +static void asio_input_activate(void *vptr) +{ + struct asio_data *data = (struct asio_data *)vptr; + os_atomic_set_bool(&data->active, true); +} + +static void asio_input_deactivate(void *vptr) +{ + struct asio_data *data = (struct asio_data *)vptr; + os_atomic_set_bool(&data->active, false); +} + +struct obs_source_info asio_input_capture = { + .id = "asio_input_capture", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE, + .get_name = asio_input_getname, + .create = asio_input_create, + .destroy = asio_destroy, + .update = asio_input_update, + .get_defaults = asio_defaults, + .get_properties = asio_input_properties, + .icon_type = OBS_ICON_TYPE_AUDIO_INPUT, + .activate = asio_input_activate, + .deactivate = asio_input_deactivate, +}; + +/*==================================== ASIO OUTPUT =====================================*/ + +static const char *asio_output_getname(void *unused) +{ + UNUSED_PARAMETER(unused); + return obs_module_text("ASIO.Output"); +} + +static void asio_output_update(void *vptr, obs_data_t *settings) +{ + + asio_update(vptr, settings, true); +} + +static void *asio_output_create(obs_data_t *settings, obs_output_t *output) +{ + return asio_create_internal(settings, output, true); +} + +static bool asio_output_start(void *vptr) +{ + struct asio_data *data = vptr; + + if (!data) { + return false; + } + + if (!data->asio_device) { + return false; + } + + if (!obs_output_can_begin_data_capture(data->output, 0)) { + return false; + } + + struct obs_audio_info oai; + obs_get_audio_info(&oai); + /* Audio is always planar for asio so we need obs to convert to planar format. */ + struct audio_convert_info aci = {.format = AUDIO_FORMAT_FLOAT_PLANAR, + .speakers = oai.speakers, + .samples_per_sec = (uint32_t)data->asio_device->current_sample_rate}; + + obs_output_set_audio_conversion(data->output, &aci); + + struct asio_device *dev = data->asio_device; + os_atomic_set_bool(&dev->capture_started, true); + + return obs_output_begin_data_capture(data->output, 0); +} + +static void asio_output_stop(void *vptr, uint64_t ts) +{ + struct asio_data *data = vptr; + if (data) { + obs_output_end_data_capture(data->output); + } +} + +static void asio_receive_audio(void *vptr, size_t mix_idx, struct audio_data *frame) +{ + UNUSED_PARAMETER(vptr); + struct asio_data *data = global_output_asio_data; + struct audio_data in = *frame; + struct asio_device *dev = data->asio_device; + + if (os_atomic_load_bool(&shutting_down_atomic)) { + return; + } + + if (os_atomic_load_bool(&data->stopping) || !frame) { + return; + } + + if (!dev) { + return; + } + + if (!os_atomic_load_bool(&dev->capture_started)) { + return; + } + + for (int i = 0; i < dev->total_num_output_chans; ++i) { + for (int j = 0; j < data->obs_track_channels; ++j) { + if (dev->obs_track[i] == (int)mix_idx && dev->obs_track_channel[i] == j) { + deque_push_back(&dev->output_frames[i], in.data[j], in.frames * sizeof(float)); + } + } + } +} + +static uint64_t asio_output_total_bytes(void *data) +{ + return 0; +} + +static bool display_control_panel_output(obs_properties_t *props, obs_property_t *property, void *vptr) +{ + return display_control_panel(props, property, vptr, true); +} + +static bool on_reset_output_device_clicked(obs_properties_t *props, obs_property_t *property, void *vptr) +{ + return on_reset_device_clicked(props, property, vptr, true); +} + +static obs_properties_t *asio_output_properties(void *vptr) +{ + return asio_properties_internal(vptr, true); +} + +struct obs_output_info asio_output = { + .id = "asio_output", + .flags = OBS_OUTPUT_AUDIO | OBS_OUTPUT_MULTI_TRACK, + .get_name = asio_output_getname, + .create = asio_output_create, + .destroy = asio_destroy, + .start = asio_output_start, + .stop = asio_output_stop, + .update = asio_output_update, + .get_defaults = asio_defaults, + .get_properties = asio_output_properties, + .raw_audio2 = asio_receive_audio, +}; diff --git a/plugins/win-asio/win-asio.h b/plugins/win-asio/win-asio.h new file mode 100644 index 00000000000000..fa4b51bb17f451 --- /dev/null +++ b/plugins/win-asio/win-asio.h @@ -0,0 +1,62 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of win-asio. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "asio-common.h" + +#include +#include +#include +#include +#include + +struct asio_device; + +struct asio_data { + /* common */ + struct asio_device *asio_device; // ASIO device (source plugin: input; output plugin: output) + int asio_client_index[MAX_NUM_ASIO_DEVICES]; // index of OBS source in device client list + const char *device_name; // device name + uint8_t device_index; // device index in the driver list + bool update_channels; // bool to track the change of driver + enum speaker_layout speakers; // speaker layout + int sample_rate; // 44100 or 48000 Hz + uint8_t in_channels; // number of device input channels + uint8_t out_channels; // output:number of device output channels; + // source: number of OBS output channels set in OBS Audio Settings + volatile bool stopping; // signals the source is stopping + bool initial_update; // initial update right after creation + bool driver_loaded; // driver was loaded correctly + bool is_output; // true if it is an output; false if it is an input capture + /* source */ + obs_source_t *source; + int mix_channels[MAX_AUDIO_CHANNELS]; // stores the channel re-ordering info + volatile bool active; // tracks whether the device is streaming + /* output */ + obs_output_t *output; + uint8_t obs_track_channels; // number of OBS output channels + int out_mix_channels[MAX_DEVICE_CHANNELS]; // Stores which OBS track and which track channel has been picked. + // 3 bits are reserved for the track channel (0-7) since OBS + // supports up to 8 audio channels. 1 more bit is reserved to + // allow for up to 16 channels, should there be a need later to + // expand the channel count (presumbably for broadcast setups). + // Track_index is then stored as 1 << track_index + 4 + // so: track 0 = 16, track 1 = 32, etc. +}; From 12f49133795b29e33f091f7278531a1e10918631 Mon Sep 17 00:00:00 2001 From: pkv Date: Sun, 3 Mar 2024 16:14:55 +0100 Subject: [PATCH 2/9] UI: Add ASIO output frontend plugin This adds an ASIO output entry in Tools Menu allowing to setup audio output from obs to an ASIO device. Signed-off-by: pkv --- frontend/plugins/CMakeLists.txt | 1 + .../asio-output-ui/ASIOSettingsDialog.cpp | 101 +++++++++++ .../asio-output-ui/ASIOSettingsDialog.h | 49 ++++++ .../plugins/asio-output-ui/CMakeLists.txt | 27 +++ .../plugins/asio-output-ui/asio-ui-main.cpp | 166 ++++++++++++++++++ .../cmake/windows/obs-module.rc.in | 24 +++ .../asio-output-ui/data/locale/en-US.ini | 1 + .../plugins/asio-output-ui/forms/output.ui | 39 ++++ 8 files changed, 408 insertions(+) create mode 100644 frontend/plugins/asio-output-ui/ASIOSettingsDialog.cpp create mode 100644 frontend/plugins/asio-output-ui/ASIOSettingsDialog.h create mode 100644 frontend/plugins/asio-output-ui/CMakeLists.txt create mode 100644 frontend/plugins/asio-output-ui/asio-ui-main.cpp create mode 100644 frontend/plugins/asio-output-ui/cmake/windows/obs-module.rc.in create mode 100644 frontend/plugins/asio-output-ui/data/locale/en-US.ini create mode 100644 frontend/plugins/asio-output-ui/forms/output.ui diff --git a/frontend/plugins/CMakeLists.txt b/frontend/plugins/CMakeLists.txt index fdaf40b6325763..f4da4a04a9e9c8 100644 --- a/frontend/plugins/CMakeLists.txt +++ b/frontend/plugins/CMakeLists.txt @@ -1,4 +1,5 @@ add_subdirectory(aja-output-ui) +add_obs_plugin(asio-output-ui PLATFORMS WINDOWS) add_subdirectory(decklink-captions) add_subdirectory(decklink-output-ui) add_subdirectory(frontend-tools) diff --git a/frontend/plugins/asio-output-ui/ASIOSettingsDialog.cpp b/frontend/plugins/asio-output-ui/ASIOSettingsDialog.cpp new file mode 100644 index 00000000000000..3e84a9b892cb6f --- /dev/null +++ b/frontend/plugins/asio-output-ui/ASIOSettingsDialog.cpp @@ -0,0 +1,101 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of asio-output-ui which requires win-asio. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ASIOSettingsDialog.h" +#include +#include +#include + +extern void output_start(); +extern void output_stop(); +extern bool output_running; +extern std::string g_currentDeviceName; + +ASIOSettingsDialog::ASIOSettingsDialog(QWidget *parent, obs_output_t *output, OBSData settings) + : QDialog(parent), + ui(new Ui::Output), + output_(output), + settings_(settings), + currentDeviceName("") +{ + ui->setupUi(this); + setSizeGripEnabled(true); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + propertiesView = nullptr; +} + +void ASIOSettingsDialog::ShowHideDialog() +{ + SetupPropertiesView(); + setVisible(!isVisible()); +} + +void ASIOSettingsDialog::SetupPropertiesView() +{ + if (propertiesView) { + delete propertiesView; + } + + propertiesView = new OBSPropertiesView(settings_, "asio_output", + (PropertiesReloadCallback)obs_get_output_properties, 170); + + ui->propertiesLayout->addWidget(propertiesView); + currentDeviceName = g_currentDeviceName; + + connect(propertiesView, &OBSPropertiesView::Changed, this, &ASIOSettingsDialog::PropertiesChanged); +} + +void ASIOSettingsDialog::SaveSettings() +{ + BPtr modulePath = obs_module_get_config_path(obs_current_module(), ""); + os_mkdirs(modulePath); + BPtr path = obs_module_get_config_path(obs_current_module(), "asioOutputProps.json"); + obs_data_t *settings = propertiesView->GetSettings(); + + if (settings) { + obs_data_save_json_safe(settings, path, "tmp", "bak"); + } +} + +void ASIOSettingsDialog::PropertiesChanged() +{ + obs_output_update(output_, settings_); + SaveSettings(); + const char *dev = obs_data_get_string(settings_, "device_name"); + const std::string newDevice = (dev && *dev) ? dev : std::string{}; + + const bool wasEmpty = currentDeviceName.empty(); + const bool nowEmpty = newDevice.empty(); + + if (wasEmpty && !nowEmpty) { + // No device swapped to a valid device: start if not running + if (!output_running) { + output_start(); + } + } else if (!wasEmpty && nowEmpty) { + // Valid device swapped to None: stop if running + if (output_running) { + output_stop(); + } + } else if (!nowEmpty && newDevice != currentDeviceName) { + // Output was already started so do nothing + } + + currentDeviceName = newDevice; + g_currentDeviceName = newDevice; +} diff --git a/frontend/plugins/asio-output-ui/ASIOSettingsDialog.h b/frontend/plugins/asio-output-ui/ASIOSettingsDialog.h new file mode 100644 index 00000000000000..e0720cc0beb13b --- /dev/null +++ b/frontend/plugins/asio-output-ui/ASIOSettingsDialog.h @@ -0,0 +1,49 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of asio-output-ui which requires win-asio. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "ui_output.h" + +#include +#include + +#include + +#include + +class ASIOSettingsDialog : public QDialog { + Q_OBJECT + +public: + explicit ASIOSettingsDialog(QWidget *parent = 0, obs_output_t *output = nullptr, OBSData settings = nullptr); + std::unique_ptr ui; + void ShowHideDialog(); + void SetupPropertiesView(); + void SaveSettings(); + OBSData settings_; + obs_output_t *output_; + std::string currentDeviceName; + +public slots: + void PropertiesChanged(); + +private: + OBSPropertiesView *propertiesView; +}; diff --git a/frontend/plugins/asio-output-ui/CMakeLists.txt b/frontend/plugins/asio-output-ui/CMakeLists.txt new file mode 100644 index 00000000000000..9b2ebde4a7865d --- /dev/null +++ b/frontend/plugins/asio-output-ui/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.28...3.30) + +find_package(Qt6 REQUIRED Widgets) + +add_library(asio-output-ui MODULE) +add_library(OBS::asio-output-ui ALIAS asio-output-ui) + +target_sources(asio-output-ui PRIVATE asio-ui-main.cpp ASIOSettingsDialog.cpp ASIOSettingsDialog.h) + +target_sources(asio-output-ui PRIVATE forms/output.ui) + +target_link_libraries(asio-output-ui PRIVATE OBS::libobs OBS::frontend-api OBS::properties-view Qt::Widgets) + +configure_file(cmake/windows/obs-module.rc.in asio-output-ui.rc) +target_sources(asio-output-ui PRIVATE asio-output-ui.rc) + +set_property(TARGET asio-output-ui APPEND PROPERTY AUTORCC_OPTIONS --format-version 1) + +set_target_properties_obs( + asio-output-ui + PROPERTIES FOLDER frontend + PREFIX "" + AUTOMOC ON + AUTOUIC ON + AUTORCC ON + AUTOUIC_SEARCH_PATHS forms +) diff --git a/frontend/plugins/asio-output-ui/asio-ui-main.cpp b/frontend/plugins/asio-output-ui/asio-ui-main.cpp new file mode 100644 index 00000000000000..d9a426f373cf10 --- /dev/null +++ b/frontend/plugins/asio-output-ui/asio-ui-main.cpp @@ -0,0 +1,166 @@ +/****************************************************************************** + Copyright (C) 2022-2026 pkv + + This file is part of win-asio. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "ASIOSettingsDialog.h" + +#include +#include +#include +#include + +#include + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("asio-output-ui", "en-US") + +struct asio_ui_output { + bool enabled; + obs_output_t *output; + OBSData settings; +}; + +// We use a global context for asio output because we allow a single output device. +struct asio_ui_output context = {0}; +bool output_running = false; +ASIOSettingsDialog *settingsDialog_ = nullptr; +std::string g_currentDeviceName; + +OBSData load_settings() +{ + BPtr path = obs_module_get_config_path(obs_current_module(), "asioOutputProps.json"); + BPtr jsonData = os_quick_read_utf8_file(path); + if (!!jsonData) { + obs_data_t *data = obs_data_create_from_json(jsonData); + OBSData dataRet(data); + obs_data_release(data); + return dataRet; + } + return nullptr; +} + +#define MAX_DEVICE_CHANNELS 32 + +void save_default_settings(obs_data_t *settings) +{ + BPtr modulePath = obs_module_get_config_path(obs_current_module(), ""); + os_mkdirs(modulePath); + BPtr path = obs_module_get_config_path(obs_current_module(), "asioOutputProps.json"); + obs_data_t *data = obs_data_create(); + obs_data_set_string(data, "device_name", obs_data_get_string(settings, "device_name")); + for (int i = 0; i < MAX_DEVICE_CHANNELS; i++) { + char key[32]; + snprintf(key, sizeof(key), "device_ch%d", i); + obs_data_set_int(data, key, -1); + } + obs_data_save_json_safe(data, path, "tmp", "bak"); +} + +void output_stop() +{ + if (context.output) { + obs_output_stop(context.output); + } + output_running = false; +} + +void output_start() +{ + if (context.output != nullptr) { + output_running = obs_output_start(context.output); + if (!output_running) { + output_stop(); + } + } +} + +void addOutputUI(void) +{ + QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction(obs_module_text("AsioOutput.Menu")); + + QMainWindow *mainWindow = (QMainWindow *)obs_frontend_get_main_window(); + + obs_frontend_push_ui_translation(obs_module_get_string); + settingsDialog_ = new ASIOSettingsDialog(mainWindow, context.output, context.settings); + obs_frontend_pop_ui_translation(); + + auto cb = []() { + settingsDialog_->ShowHideDialog(); + }; + + action->connect(action, &QAction::triggered, cb); +} + +static void OBSEvent(enum obs_frontend_event event, void *) +{ + if (event == OBS_FRONTEND_EVENT_FINISHED_LOADING) { + if (context.settings) { + const char *device = obs_data_get_string(context.settings, "device_name"); + if (device && *device) { + g_currentDeviceName = device; + if (!output_running) { + output_start(); + } + } + } + } else if (event == OBS_FRONTEND_EVENT_EXIT) { + if (output_running) { + output_stop(); + } + } +} + +bool obs_module_load(void) +{ + return true; +} + +void obs_module_unload(void) +{ + if (output_running) { + output_stop(); + } + + obs_output_release(context.output); + context.output = nullptr; + obs_data_release(context.settings); + context.settings = nullptr; + obs_frontend_remove_event_callback(OBSEvent, nullptr); +} + +void obs_module_post_load(void) +{ + if (!obs_get_module("win-asio")) { + return; + } + + context.settings = load_settings(); + obs_output_t *const output = obs_output_create("asio_output", "asio_output", context.settings, NULL); + if (output != nullptr) { + context.output = output; + + if (!context.settings) { + context.settings = obs_output_get_settings(output); + save_default_settings(context.settings); + } + addOutputUI(); + obs_frontend_add_event_callback(OBSEvent, nullptr); + } else { + blog(LOG_INFO, "Failed to create ASIO output"); + } +} diff --git a/frontend/plugins/asio-output-ui/cmake/windows/obs-module.rc.in b/frontend/plugins/asio-output-ui/cmake/windows/obs-module.rc.in new file mode 100644 index 00000000000000..b468e116c8f2f2 --- /dev/null +++ b/frontend/plugins/asio-output-ui/cmake/windows/obs-module.rc.in @@ -0,0 +1,24 @@ +1 VERSIONINFO +FILEVERSION ${OBS_VERSION_MAJOR},${OBS_VERSION_MINOR},${OBS_VERSION_PATCH},0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "${OBS_COMPANY_NAME}" + VALUE "FileDescription", "OBS ASIO Output UI" + VALUE "FileVersion", "${OBS_VERSION_CANONICAL}" + VALUE "ProductName", "${OBS_PRODUCT_NAME}" + VALUE "ProductVersion", "${OBS_VERSION_CANONICAL}" + VALUE "Comments", "${OBS_COMMENTS}" + VALUE "LegalCopyright", "${OBS_LEGAL_COPYRIGHT}" + VALUE "InternalName", "asio-output-ui" + VALUE "OriginalFilename", "asio-output-ui" + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 0x04B0 + END +END diff --git a/frontend/plugins/asio-output-ui/data/locale/en-US.ini b/frontend/plugins/asio-output-ui/data/locale/en-US.ini new file mode 100644 index 00000000000000..28246e7972e1e1 --- /dev/null +++ b/frontend/plugins/asio-output-ui/data/locale/en-US.ini @@ -0,0 +1 @@ +AsioOutput.Menu="ASIO Output" \ No newline at end of file diff --git a/frontend/plugins/asio-output-ui/forms/output.ui b/frontend/plugins/asio-output-ui/forms/output.ui new file mode 100644 index 00000000000000..95b7f8c26be643 --- /dev/null +++ b/frontend/plugins/asio-output-ui/forms/output.ui @@ -0,0 +1,39 @@ + + + Output + + + + 0 + 0 + 785 + 484 + + + + + 0 + 0 + + + + ASIO Output + + + true + + + false + + + + QLayout::SetDefaultConstraint + + + + + + + + + From d9f6fec006d57423c0f3b7bcf1ddfc15995be923 Mon Sep 17 00:00:00 2001 From: pkv Date: Tue, 20 Feb 2024 16:52:09 +0100 Subject: [PATCH 3/9] libobs: Add ASIO monitoring track On windows, this adds an additional audio track (Track 7) which is used to output a mix of monitored sources. Currently the mixing is left to WASAPI on windows for WASAPI monitoring devices. But ASIO SDK doesn't allow multiple clients (although individual drivers might allow it) and therefore can not mix them. So we have to do the mixing on obs side. Signed-off-by: pkv --- libobs/media-io/audio-io.c | 20 ++++++++--------- libobs/media-io/audio-io.h | 9 +++++++- libobs/obs-audio.c | 2 +- libobs/obs-internal.h | 4 ++-- libobs/obs-output.c | 8 +++---- libobs/obs-scene.c | 2 +- libobs/obs-source-transition.c | 2 +- libobs/obs-source.c | 40 ++++++++++++++++++++++++++-------- libobs/obs-source.h | 2 +- 9 files changed, 59 insertions(+), 30 deletions(-) diff --git a/libobs/media-io/audio-io.c b/libobs/media-io/audio-io.c index 26d1604b39d285..10d7a005b8054e 100644 --- a/libobs/media-io/audio-io.c +++ b/libobs/media-io/audio-io.c @@ -76,7 +76,7 @@ struct audio_output { audio_input_callback_t input_cb; void *input_param; pthread_mutex_t input_mutex; - struct audio_mix mixes[MAX_AUDIO_MIXES]; + struct audio_mix mixes[MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES]; }; /* ------------------------------------------------------------------------- */ @@ -133,7 +133,7 @@ static inline void clamp_audio_output(struct audio_output *audio, size_t bytes) { size_t float_size = bytes / sizeof(float); - for (size_t mix_idx = 0; mix_idx < MAX_AUDIO_MIXES; mix_idx++) { + for (size_t mix_idx = 0; mix_idx < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); mix_idx++) { struct audio_mix *mix = &audio->mixes[mix_idx]; /* do not process mixing if a specific mix is inactive */ @@ -160,7 +160,7 @@ static inline void clamp_audio_output(struct audio_output *audio, size_t bytes) static void input_and_output(struct audio_output *audio, uint64_t audio_time, uint64_t prev_time) { size_t bytes = AUDIO_OUTPUT_FRAMES * audio->block_size; - struct audio_output_data data[MAX_AUDIO_MIXES]; + struct audio_output_data data[MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES]; uint32_t active_mixes = 0; uint64_t new_ts = 0; bool success; @@ -173,14 +173,14 @@ static void input_and_output(struct audio_output *audio, uint64_t audio_time, ui /* get mixers */ pthread_mutex_lock(&audio->input_mutex); - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + for (size_t i = 0; i < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); i++) { if (audio->mixes[i].inputs.num) active_mixes |= (1 << i); } pthread_mutex_unlock(&audio->input_mutex); /* clear mix buffers */ - for (size_t mix_idx = 0; mix_idx < MAX_AUDIO_MIXES; mix_idx++) { + for (size_t mix_idx = 0; mix_idx < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); mix_idx++) { struct audio_mix *mix = &audio->mixes[mix_idx]; memset(mix->buffer, 0, sizeof(mix->buffer)); @@ -198,7 +198,7 @@ static void input_and_output(struct audio_output *audio, uint64_t audio_time, ui clamp_audio_output(audio, bytes); /* output */ - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) + for (size_t i = 0; i < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); i++) do_audio_output(audio, i, new_ts, AUDIO_OUTPUT_FRAMES); } @@ -291,7 +291,7 @@ bool audio_output_connect(audio_t *audio, size_t mi, const struct audio_convert_ { bool success = false; - if (!audio || mi >= MAX_AUDIO_MIXES) + if (!audio || mi >= (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES)) return false; pthread_mutex_lock(&audio->input_mutex); @@ -330,7 +330,7 @@ bool audio_output_connect(audio_t *audio, size_t mi, const struct audio_convert_ void audio_output_disconnect(audio_t *audio, size_t mix_idx, audio_output_callback_t callback, void *param) { - if (!audio || mix_idx >= MAX_AUDIO_MIXES) + if (!audio || mix_idx >= (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES)) return; pthread_mutex_lock(&audio->input_mutex); @@ -403,7 +403,7 @@ void audio_output_close(audio_t *audio) pthread_mutex_destroy(&audio->input_mutex); } - for (size_t mix_idx = 0; mix_idx < MAX_AUDIO_MIXES; mix_idx++) { + for (size_t mix_idx = 0; mix_idx < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); mix_idx++) { struct audio_mix *mix = &audio->mixes[mix_idx]; for (size_t i = 0; i < mix->inputs.num; i++) @@ -424,7 +424,7 @@ bool audio_output_active(const audio_t *audio) if (!audio) return false; - for (size_t mix_idx = 0; mix_idx < MAX_AUDIO_MIXES; mix_idx++) { + for (size_t mix_idx = 0; mix_idx < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); mix_idx++) { const struct audio_mix *mix = &audio->mixes[mix_idx]; if (mix->inputs.num != 0) diff --git a/libobs/media-io/audio-io.h b/libobs/media-io/audio-io.h index 6f2d9274528395..4f771806a7c896 100644 --- a/libobs/media-io/audio-io.h +++ b/libobs/media-io/audio-io.h @@ -24,13 +24,20 @@ #ifdef __cplusplus extern "C" { #endif +#ifdef _WIN32 +// extra track for ASIO monitoring on windows only +#define MAX_AUDIO_MONITORING_MIXES 1 +#else +#define MAX_AUDIO_MONITORING_MIXES 0 +#endif #define MAX_AUDIO_MIXES 6 + #define MAX_AUDIO_CHANNELS 8 #define MAX_DEVICE_INPUT_CHANNELS 64 #define AUDIO_OUTPUT_FRAMES 1024 -#define TOTAL_AUDIO_SIZE (MAX_AUDIO_MIXES * MAX_AUDIO_CHANNELS * AUDIO_OUTPUT_FRAMES * sizeof(float)) +#define TOTAL_AUDIO_SIZE ((MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES) * MAX_AUDIO_CHANNELS * AUDIO_OUTPUT_FRAMES * sizeof(float)) /* * Base audio output component. Use this to create an audio output track diff --git a/libobs/obs-audio.c b/libobs/obs-audio.c index f016c0505e4800..d8a3888b24ef05 100644 --- a/libobs/obs-audio.c +++ b/libobs/obs-audio.c @@ -104,7 +104,7 @@ static inline void mix_audio(struct audio_output_data *mixes, obs_source_t *sour total_floats -= start_point; } - for (size_t mix_idx = 0; mix_idx < MAX_AUDIO_MIXES; mix_idx++) { + for (size_t mix_idx = 0; mix_idx < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); mix_idx++) { for (size_t ch = 0; ch < channels; ch++) { register float *mix = mixes[mix_idx].data[ch]; register float *aud = source->audio_output_buf[mix_idx][ch]; diff --git a/libobs/obs-internal.h b/libobs/obs-internal.h index 9974c0ac76727e..3d9d1a5075ee3a 100644 --- a/libobs/obs-internal.h +++ b/libobs/obs-internal.h @@ -868,7 +868,7 @@ struct obs_source { struct deque audio_input_buf[MAX_AUDIO_CHANNELS]; size_t last_audio_input_buf_size; DARRAY(struct audio_action) audio_actions; - float *audio_output_buf[MAX_AUDIO_MIXES][MAX_AUDIO_CHANNELS]; + float *audio_output_buf[MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES][MAX_AUDIO_CHANNELS]; float *audio_mix_buf[MAX_AUDIO_CHANNELS]; struct resample_info sample_info; audio_resampler_t *resampler; @@ -1257,7 +1257,7 @@ struct obs_output { struct pause_data pause; - struct deque audio_buffer[MAX_AUDIO_MIXES][MAX_AV_PLANES]; + struct deque audio_buffer[MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES][MAX_AV_PLANES]; uint64_t audio_start_ts; uint64_t video_start_ts; size_t audio_size; diff --git a/libobs/obs-output.c b/libobs/obs-output.c index fae54a00a89693..1e166811f778a7 100644 --- a/libobs/obs-output.c +++ b/libobs/obs-output.c @@ -269,7 +269,7 @@ static inline void free_packets(struct obs_output *output) static inline void clear_raw_audio_buffers(obs_output_t *output) { - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + for (size_t i = 0; i < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); i++) { for (size_t j = 0; j < MAX_AV_PLANES; j++) { deque_free(&output->audio_buffer[i][j]); } @@ -918,7 +918,7 @@ audio_t *obs_output_audio(const obs_output_t *output) static inline size_t get_first_mixer(const obs_output_t *output) { - for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + for (size_t i = 0; i < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); i++) { if ((((size_t)1 << i) & output->mixer_mask) != 0) { return i; } @@ -2448,7 +2448,7 @@ static inline void start_video_encoders(struct obs_output *output, encoded_callb static inline void start_raw_audio(obs_output_t *output) { if (output->info.raw_audio2) { - for (int idx = 0; idx < MAX_AUDIO_MIXES; idx++) { + for (int idx = 0; idx < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); idx++) { if ((output->mixer_mask & ((size_t)1 << idx)) != 0) { audio_output_connect(output->audio, idx, get_audio_conversion(output), default_raw_audio_callback, output); @@ -2824,7 +2824,7 @@ static inline void stop_video_encoders(obs_output_t *output, encoded_callback_t static inline void stop_raw_audio(obs_output_t *output) { if (output->info.raw_audio2) { - for (int idx = 0; idx < MAX_AUDIO_MIXES; idx++) { + for (int idx = 0; idx < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); idx++) { if ((output->mixer_mask & ((size_t)1 << idx)) != 0) { audio_output_disconnect(output->audio, idx, default_raw_audio_callback, output); } diff --git a/libobs/obs-scene.c b/libobs/obs-scene.c index 02fa144a26b63d..0bdb8668cca6c1 100644 --- a/libobs/obs-scene.c +++ b/libobs/obs-scene.c @@ -1699,7 +1699,7 @@ static bool scene_audio_render(void *data, uint64_t *ts_out, struct obs_source_a obs_source_get_audio_mix(source, &child_audio); if (!source->audio_is_duplicated) { - for (size_t mix = 0; mix < MAX_AUDIO_MIXES; mix++) { + for (size_t mix = 0; mix < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); mix++) { if ((mixers & (1 << mix)) == 0) continue; diff --git a/libobs/obs-source-transition.c b/libobs/obs-source-transition.c index 73980006a8a23a..c79d042123033a 100644 --- a/libobs/obs-source-transition.c +++ b/libobs/obs-source-transition.c @@ -889,7 +889,7 @@ static void process_audio(obs_source_t *transition, obs_source_t *child, struct if (pos > AUDIO_OUTPUT_FRAMES) return; - for (size_t mix_idx = 0; mix_idx < MAX_AUDIO_MIXES; mix_idx++) { + for (size_t mix_idx = 0; mix_idx < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); mix_idx++) { struct audio_output_data *output = &audio->output[mix_idx]; struct audio_output_data *input = &child_audio.output[mix_idx]; diff --git a/libobs/obs-source.c b/libobs/obs-source.c index 0ed8e7c9a92d3c..a0966cf1246a1f 100644 --- a/libobs/obs-source.c +++ b/libobs/obs-source.c @@ -172,10 +172,11 @@ enum obs_module_load_state obs_source_load_state(const char *id) static void allocate_audio_output_buffer(struct obs_source *source) { - size_t size = sizeof(float) * AUDIO_OUTPUT_FRAMES * MAX_AUDIO_CHANNELS * MAX_AUDIO_MIXES; + size_t size = sizeof(float) * AUDIO_OUTPUT_FRAMES * MAX_AUDIO_CHANNELS * + (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); float *ptr = bzalloc(size); - for (size_t mix = 0; mix < MAX_AUDIO_MIXES; mix++) { + for (size_t mix = 0; mix < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); mix++) { size_t mix_pos = mix * AUDIO_OUTPUT_FRAMES * MAX_AUDIO_CHANNELS; for (size_t i = 0; i < MAX_AUDIO_CHANNELS; i++) { @@ -5289,7 +5290,7 @@ static void apply_audio_actions(obs_source_t *source, size_t channels, size_t sa pthread_mutex_unlock(&source->audio_actions_mutex); - for (size_t mix = 0; mix < MAX_AUDIO_MIXES; mix++) { + for (size_t mix = 0; mix < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); mix++) { if ((source->audio_mixers & (1 << mix)) != 0) multiply_vol_data(source, mix, channels, vol_data); } @@ -5324,11 +5325,12 @@ static void apply_audio_volume(obs_source_t *source, uint32_t mixers, size_t cha if (vol == 0.0f || mixers == 0) { memset(source->audio_output_buf[0][0], 0, - AUDIO_OUTPUT_FRAMES * sizeof(float) * MAX_AUDIO_CHANNELS * MAX_AUDIO_MIXES); + AUDIO_OUTPUT_FRAMES * sizeof(float) * MAX_AUDIO_CHANNELS * + (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES)); return; } - for (size_t mix = 0; mix < MAX_AUDIO_MIXES; mix++) { + for (size_t mix = 0; mix < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); mix++) { uint32_t mix_and_val = (1 << mix); if ((source->audio_mixers & mix_and_val) != 0 && (mixers & mix_and_val) != 0) multiply_output_audio(source, mix, channels, vol); @@ -5341,7 +5343,7 @@ static void custom_audio_render(obs_source_t *source, uint32_t mixers, size_t ch bool success; uint64_t ts; - for (size_t mix = 0; mix < MAX_AUDIO_MIXES; mix++) { + for (size_t mix = 0; mix < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); mix++) { for (size_t ch = 0; ch < channels; ch++) { audio_data.output[mix].data[ch] = source->audio_output_buf[mix][ch]; } @@ -5358,7 +5360,7 @@ static void custom_audio_render(obs_source_t *source, uint32_t mixers, size_t ch if (!success || !source->audio_ts || !mixers) return; - for (size_t mix = 0; mix < MAX_AUDIO_MIXES; mix++) { + for (size_t mix = 0; mix < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); mix++) { uint32_t mix_bit = 1 << mix; if ((mixers & mix_bit) == 0) @@ -5420,7 +5422,7 @@ static inline void process_audio_source_tick(obs_source_t *source, uint32_t mixe pthread_mutex_unlock(&source->audio_buf_mutex); - for (size_t mix = 1; mix < MAX_AUDIO_MIXES; mix++) { + for (size_t mix = 1; mix < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); mix++) { uint32_t mix_and_val = (1 << mix); if (audio_submix) { @@ -5503,7 +5505,7 @@ void obs_source_get_audio_mix(const obs_source_t *source, struct obs_source_audi if (!obs_ptr_valid(audio, "audio")) return; - for (size_t mix = 0; mix < MAX_AUDIO_MIXES; mix++) { + for (size_t mix = 0; mix < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); mix++) { for (size_t ch = 0; ch < MAX_AUDIO_CHANNELS; ch++) { audio->output[mix].data[ch] = source->audio_output_buf[mix][ch]; } @@ -5587,10 +5589,30 @@ void obs_source_set_monitoring_type(obs_source_t *source, enum obs_monitoring_ty } source->monitoring_type = type; + +#ifdef _WIN32 + /* On Windows, assign to the extra ASIO monitoring track (track 7) all sources which have not type + * OBS_MONITORING_TYPE_NONE. */ + if (type != OBS_MONITORING_TYPE_NONE) { + source->audio_mixers |= 1 << (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES - 1); + } else { + source->audio_mixers &= ~(1 << (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES - 1)); + } +#endif } enum obs_monitoring_type obs_source_get_monitoring_type(const obs_source_t *source) { +#ifdef _WIN32 + /* If type is OBS_MONITORING_TYPE_NONE, unselect the extra ASIO monitoring track (track 7) on Windows. */ + uint32_t mixers = obs_source_get_audio_mixers(source); + if (source->monitoring_type == OBS_MONITORING_TYPE_NONE && mixers) { + if (mixers & 1 << (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES - 1)) { + mixers &= ~(1 << (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES - 1)); + obs_source_set_audio_mixers((obs_source_t *)source, mixers); + } + } +#endif return obs_source_valid(source, "obs_source_get_monitoring_type") ? source->monitoring_type : OBS_MONITORING_TYPE_NONE; } diff --git a/libobs/obs-source.h b/libobs/obs-source.h index fcfdc1bc2b32d3..59ae600cf5459a 100644 --- a/libobs/obs-source.h +++ b/libobs/obs-source.h @@ -213,7 +213,7 @@ enum obs_media_state { typedef void (*obs_source_enum_proc_t)(obs_source_t *parent, obs_source_t *child, void *param); struct obs_source_audio_mix { - struct audio_output_data output[MAX_AUDIO_MIXES]; + struct audio_output_data output[MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES]; }; /** From a2ca3368a86570bb5c80d51e4fe553e6157f55db Mon Sep 17 00:00:00 2001 From: pkv Date: Thu, 29 Jan 2026 20:48:44 +0100 Subject: [PATCH 4/9] libobs: Audio routing of MONITOR_ONLY sources Currently, sources which have the monitoring_type MONITOR_ONLY are not pushed into audio_input_buf so they are not available to outputs. So this makes them unavailable to ASIO monitoring. To fix that, we systematically push them to audio_input_buf but silence the source for all tracks except the extra ASIO monitoring track in the apply_volume call, further down in the audio pipeline. Signed-off-by: pkv --- libobs/obs-source.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/libobs/obs-source.c b/libobs/obs-source.c index a0966cf1246a1f..49f7b12c30cb59 100644 --- a/libobs/obs-source.c +++ b/libobs/obs-source.c @@ -1648,11 +1648,10 @@ static void source_output_audio_data(obs_source_t *source, const struct audio_da source->last_sync_offset = sync_offset; } - if (source->monitoring_type != OBS_MONITORING_TYPE_MONITOR_ONLY) { - if (push_back && source->audio_ts) - source_output_audio_push_back(source, &in); - else - source_output_audio_place(source, &in); + if (push_back && source->audio_ts) { + source_output_audio_push_back(source, &in); + } else { + source_output_audio_place(source, &in); } pthread_mutex_unlock(&source->audio_buf_mutex); @@ -5323,7 +5322,7 @@ static void apply_audio_volume(obs_source_t *source, uint32_t mixers, size_t cha if (vol == 1.0f) return; - if (vol == 0.0f || mixers == 0) { + if (vol == 0.0f || mixers == 0 || source->monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) { memset(source->audio_output_buf[0][0], 0, AUDIO_OUTPUT_FRAMES * sizeof(float) * MAX_AUDIO_CHANNELS * (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES)); From 341953f3273f8e93b1fb013c16d100f00c723cc9 Mon Sep 17 00:00:00 2001 From: pkv Date: Wed, 10 Dec 2025 19:47:20 +0100 Subject: [PATCH 5/9] libobs: Don't silence the extra monitoring track for ASIO On windows, we provide an extra track which mixes all monitored sources. If a monitored source is muted, it is removed from the usual monitoring callbacks (wasapi-output.c ...) which can make sense given the possible interferences with Desktop Audio. But for ASIO drivers, there is no such issue since there is no play capture. We therefore enable a muted source to be monitored. Since it is muted, it won't appear in the mix delivered to streams or recordings but it will still be heard on the monitoring ASIO device. This is quite in line with a usage where one prepares some audio before putting it online. Currently with wasapi, this requires either a hardware mixer or a sound card with several outputs, or several devices. Signed-off-by: pkv --- libobs/obs-source.c | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/libobs/obs-source.c b/libobs/obs-source.c index 49f7b12c30cb59..7fb05adbf9f38e 100644 --- a/libobs/obs-source.c +++ b/libobs/obs-source.c @@ -5324,8 +5324,13 @@ static void apply_audio_volume(obs_source_t *source, uint32_t mixers, size_t cha if (vol == 0.0f || mixers == 0 || source->monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) { memset(source->audio_output_buf[0][0], 0, - AUDIO_OUTPUT_FRAMES * sizeof(float) * MAX_AUDIO_CHANNELS * - (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES)); + AUDIO_OUTPUT_FRAMES * sizeof(float) * MAX_AUDIO_CHANNELS * MAX_AUDIO_MIXES); + /* We don't silence the extra monitoring mixes so that they can be heard at all times. */ + for (size_t mix = MAX_AUDIO_MIXES; mix < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); mix++) { + uint32_t mix_and_val = (1 << mix); + if ((source->audio_mixers & mix_and_val) != 0 && (mixers & mix_and_val) != 0) + multiply_output_audio(source, mix, channels, source->volume); + } return; } @@ -5365,7 +5370,8 @@ static void custom_audio_render(obs_source_t *source, uint32_t mixers, size_t ch if ((mixers & mix_bit) == 0) continue; - if ((source->audio_mixers & mix_bit) == 0) { + if ((source->audio_mixers & mix_bit) == 0 || + (mix < MAX_AUDIO_MIXES && source->monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY)) { memset(source->audio_output_buf[mix][0], 0, sizeof(float) * AUDIO_OUTPUT_FRAMES * channels); } } @@ -5432,7 +5438,8 @@ static inline void process_audio_source_tick(obs_source_t *source, uint32_t mixe mix_and_val = 1; } - if ((source->audio_mixers & mix_and_val) == 0 || (mixers & mix_and_val) == 0) { + if ((source->audio_mixers & mix_and_val) == 0 || (mixers & mix_and_val) == 0 || + (mix < MAX_AUDIO_MIXES && source->monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY)) { memset(source->audio_output_buf[mix][0], 0, size * channels); continue; } @@ -5446,7 +5453,8 @@ static inline void process_audio_source_tick(obs_source_t *source, uint32_t mixe return; } - if ((source->audio_mixers & 1) == 0 || (mixers & 1) == 0) + if ((source->audio_mixers & 1) == 0 || (mixers & 1) == 0 || + source->monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) memset(source->audio_output_buf[0][0], 0, size * channels); apply_audio_volume(source, mixers, channels, sample_rate); From 42ffee958db6334d43aada1ea9cfecb40b80a276 Mon Sep 17 00:00:00 2001 From: pkv Date: Tue, 20 Feb 2024 19:22:09 +0100 Subject: [PATCH 6/9] win-asio: Enable monitoring track for output This enables a monitoring track for output. Signed-off-by: pkv --- plugins/win-asio/data/locale/en-US.ini | 8 ++++++++ plugins/win-asio/win-asio.c | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/plugins/win-asio/data/locale/en-US.ini b/plugins/win-asio/data/locale/en-US.ini index e93eacbc464c68..f60b4b895c7eb0 100644 --- a/plugins/win-asio/data/locale/en-US.ini +++ b/plugins/win-asio/data/locale/en-US.ini @@ -103,3 +103,11 @@ Track5.4=" Track 6 Channel 5" Track5.5=" Track 6 Channel 6" Track5.6=" Track 6 Channel 7" Track5.7=" Track 6 Channel 8" +Track6.0=" Monitoring Track Channel 1" +Track6.1=" Monitoring Track Channel 2" +Track6.2=" Monitoring Track Channel 3" +Track6.3=" Monitoring Track Channel 4" +Track6.4=" Monitoring Track Channel 5" +Track6.5=" Monitoring Track Channel 6" +Track6.6=" Monitoring Track Channel 7" +Track6.7=" Monitoring Track Channel 8" diff --git a/plugins/win-asio/win-asio.c b/plugins/win-asio/win-asio.c index cc4e4ea9d81aee..1438a28f14ed78 100644 --- a/plugins/win-asio/win-asio.c +++ b/plugins/win-asio/win-asio.c @@ -298,7 +298,7 @@ static void asio_update(void *vptr, obs_data_t *settings, bool is_output) data->asio_device->obs_track[i] = -1; // device does not use track i data->asio_device->obs_track_channel[i] = -1; // device does not use any channel from track i if (data->out_mix_channels[i] >= 0) { - for (int j = 0; j < MAX_AUDIO_MIXES; j++) { + for (int j = 0; j < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); j++) { for (int k = 0; k < data->obs_track_channels; k++) { int64_t idx = (int64_t)k + (1LL << (j + 4)); if (data->out_mix_channels[i] == idx) { @@ -547,7 +547,7 @@ static bool on_asio_device_changed(void *priv, obs_properties_t *props, obs_prop obs_property_list_add_int(p, obs_module_text("Mute"), -1); - for (int j = 0; j < MAX_AUDIO_MIXES; j++) { + for (int j = 0; j < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); j++) { for (int k = 0; k < global_output_asio_data->obs_track_channels; k++) { long long idx = k + (1ULL << (j + 4)); char label[32]; @@ -651,7 +651,7 @@ static obs_properties_t *asio_properties_internal(void *vptr, bool is_output) OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); obs_property_list_add_int(lp, obs_module_text("Mute"), -1); - for (int j = 0; j < MAX_AUDIO_MIXES; j++) { + for (int j = 0; j < (MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES); j++) { for (int k = 0; k < global_output_asio_data->obs_track_channels; k++) { long long idx = k + (1ULL << (j + 4)); char label[32]; From 6d50ce014cfde7587764367f85b99504c689e96d Mon Sep 17 00:00:00 2001 From: pkv Date: Sun, 18 May 2025 21:10:09 +0200 Subject: [PATCH 7/9] UI: Add to Settings > Audio ASIO monitoring This adds a QToolButton to the Audio Settings on Windows. This triggers in turn a dedicated settings for the ASIO monitoring output. If no ASIO driver is detected in the system, the panel displays an explanatory message. The panel allows to: - select an ASIO monitoring device; - for each output channel of the ASIO device, one can select any channel from any of the 6 tracks or from the monitoring mix. Signed-off-by: pkv --- frontend/data/locale/en-US.ini | 2 + frontend/data/themes/Yami.obt | 6 ++ frontend/forms/OBSBasicSettings.ui | 24 ++++++ .../asio-output-ui/ASIOSettingsDialog.cpp | 30 +++++-- .../asio-output-ui/ASIOSettingsDialog.h | 8 +- .../plugins/asio-output-ui/asio-ui-main.cpp | 85 ++++++++++++++----- .../asio-output-ui/data/locale/en-US.ini | 3 +- frontend/settings/OBSBasicSettings.cpp | 25 ++++++ frontend/settings/OBSBasicSettings.hpp | 3 + 9 files changed, 153 insertions(+), 33 deletions(-) diff --git a/frontend/data/locale/en-US.ini b/frontend/data/locale/en-US.ini index a86d2a729a33c6..a9012c1ced2b7a 100644 --- a/frontend/data/locale/en-US.ini +++ b/frontend/data/locale/en-US.ini @@ -1328,6 +1328,8 @@ Basic.Settings.Advanced.Video.HdrNominalPeakLevel="HDR Nominal Peak Level" Basic.Settings.Advanced.Audio.MonitoringDevice="Monitoring Device" Basic.Settings.Advanced.Audio.MonitoringDevice.Default="Default" Basic.Settings.Advanced.Audio.DisableAudioDucking="Disable Windows audio ducking" +Basic.Settings.Advanced.Audio.AsioMonitoringDevice="ASIO Monitoring Device" +Basic.Settings.Audio.AsioMonitoring="Setup Panel" Basic.Settings.Advanced.StreamDelay="Stream Delay" Basic.Settings.Advanced.StreamDelay.Duration="Duration" Basic.Settings.Advanced.StreamDelay.Preserve="Preserve cutoff point (increase delay) when reconnecting" diff --git a/frontend/data/themes/Yami.obt b/frontend/data/themes/Yami.obt index 739a0329e0084d..4c4cfa5a10fa1c 100644 --- a/frontend/data/themes/Yami.obt +++ b/frontend/data/themes/Yami.obt @@ -1473,6 +1473,12 @@ QPushButton::menu-indicator { right: -2px; } +QWidget QFormLayout > QToolButton#asioMonitoring { + min-width: 0px; + max-width: 16777215px; + text-align: center; +} + QToolButton { border: 1px solid var(--button_border); } diff --git a/frontend/forms/OBSBasicSettings.ui b/frontend/forms/OBSBasicSettings.ui index a1f6e6320e1199..b4cd7c96008c33 100644 --- a/frontend/forms/OBSBasicSettings.ui +++ b/frontend/forms/OBSBasicSettings.ui @@ -6327,6 +6327,29 @@ + + + + Basic.Settings.Advanced.Audio.AsioMonitoringDevice + + + asioMonitoring + + + + + + + Basic.Settings.Audio.AsioMonitoring + + + asioMonitoring + + + + + + @@ -8955,6 +8978,7 @@ meterDecayRate peakMeterType monitoringDevice + asioMonitoring disableAudioDucking lowLatencyBuffering baseResolution diff --git a/frontend/plugins/asio-output-ui/ASIOSettingsDialog.cpp b/frontend/plugins/asio-output-ui/ASIOSettingsDialog.cpp index 3e84a9b892cb6f..57ab88e488200f 100644 --- a/frontend/plugins/asio-output-ui/ASIOSettingsDialog.cpp +++ b/frontend/plugins/asio-output-ui/ASIOSettingsDialog.cpp @@ -16,11 +16,15 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ + #include "ASIOSettingsDialog.h" + #include #include #include +#include + extern void output_start(); extern void output_stop(); extern bool output_running; @@ -39,13 +43,13 @@ ASIOSettingsDialog::ASIOSettingsDialog(QWidget *parent, obs_output_t *output, OB propertiesView = nullptr; } -void ASIOSettingsDialog::ShowHideDialog() +void ASIOSettingsDialog::showHideDialog(bool enabled) { - SetupPropertiesView(); + setupPropertiesView(enabled); setVisible(!isVisible()); } -void ASIOSettingsDialog::SetupPropertiesView() +void ASIOSettingsDialog::setupPropertiesView(bool enabled) { if (propertiesView) { delete propertiesView; @@ -54,13 +58,21 @@ void ASIOSettingsDialog::SetupPropertiesView() propertiesView = new OBSPropertiesView(settings_, "asio_output", (PropertiesReloadCallback)obs_get_output_properties, 170); - ui->propertiesLayout->addWidget(propertiesView); - currentDeviceName = g_currentDeviceName; + if (enabled) { + ui->propertiesLayout->addWidget(propertiesView); + currentDeviceName = g_currentDeviceName; + } else { + QLabel *noAsioLabel = new QLabel(obs_module_text("AsioOutput.Disabled"), this); + noAsioLabel->setWordWrap(true); + noAsioLabel->setAlignment(Qt::AlignCenter); + ui->propertiesLayout->addWidget(noAsioLabel); + adjustSize(); + } - connect(propertiesView, &OBSPropertiesView::Changed, this, &ASIOSettingsDialog::PropertiesChanged); + connect(propertiesView, &OBSPropertiesView::Changed, this, &ASIOSettingsDialog::propertiesChanged); } -void ASIOSettingsDialog::SaveSettings() +void ASIOSettingsDialog::saveSettings() { BPtr modulePath = obs_module_get_config_path(obs_current_module(), ""); os_mkdirs(modulePath); @@ -72,10 +84,10 @@ void ASIOSettingsDialog::SaveSettings() } } -void ASIOSettingsDialog::PropertiesChanged() +void ASIOSettingsDialog::propertiesChanged() { obs_output_update(output_, settings_); - SaveSettings(); + saveSettings(); const char *dev = obs_data_get_string(settings_, "device_name"); const std::string newDevice = (dev && *dev) ? dev : std::string{}; diff --git a/frontend/plugins/asio-output-ui/ASIOSettingsDialog.h b/frontend/plugins/asio-output-ui/ASIOSettingsDialog.h index e0720cc0beb13b..2fa738d57b5fd1 100644 --- a/frontend/plugins/asio-output-ui/ASIOSettingsDialog.h +++ b/frontend/plugins/asio-output-ui/ASIOSettingsDialog.h @@ -34,15 +34,15 @@ class ASIOSettingsDialog : public QDialog { public: explicit ASIOSettingsDialog(QWidget *parent = 0, obs_output_t *output = nullptr, OBSData settings = nullptr); std::unique_ptr ui; - void ShowHideDialog(); - void SetupPropertiesView(); - void SaveSettings(); + void showHideDialog(bool enabled); + void setupPropertiesView(bool enabled); + void saveSettings(); OBSData settings_; obs_output_t *output_; std::string currentDeviceName; public slots: - void PropertiesChanged(); + void propertiesChanged(); private: OBSPropertiesView *propertiesView; diff --git a/frontend/plugins/asio-output-ui/asio-ui-main.cpp b/frontend/plugins/asio-output-ui/asio-ui-main.cpp index d9a426f373cf10..157f3fa6d3e1ed 100644 --- a/frontend/plugins/asio-output-ui/asio-ui-main.cpp +++ b/frontend/plugins/asio-output-ui/asio-ui-main.cpp @@ -1,7 +1,7 @@ /****************************************************************************** Copyright (C) 2022-2026 pkv - This file is part of win-asio. + This file is part of asio-output-ui which requires win-asio. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -25,6 +25,7 @@ #include #include +#include OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE("asio-output-ui", "en-US") @@ -35,11 +36,16 @@ struct asio_ui_output { OBSData settings; }; +bool output_running = false; +std::string g_currentDeviceName; + +namespace { +constexpr int MAX_ASIO_DEVICE_CHANNELS = 32; +constexpr int SHIFT_WINDOW = 100; // We use a global context for asio output because we allow a single output device. struct asio_ui_output context = {0}; -bool output_running = false; ASIOSettingsDialog *settingsDialog_ = nullptr; -std::string g_currentDeviceName; +} // namespace OBSData load_settings() { @@ -54,8 +60,6 @@ OBSData load_settings() return nullptr; } -#define MAX_DEVICE_CHANNELS 32 - void save_default_settings(obs_data_t *settings) { BPtr modulePath = obs_module_get_config_path(obs_current_module(), ""); @@ -63,7 +67,7 @@ void save_default_settings(obs_data_t *settings) BPtr path = obs_module_get_config_path(obs_current_module(), "asioOutputProps.json"); obs_data_t *data = obs_data_create(); obs_data_set_string(data, "device_name", obs_data_get_string(settings, "device_name")); - for (int i = 0; i < MAX_DEVICE_CHANNELS; i++) { + for (int i = 0; i < MAX_ASIO_DEVICE_CHANNELS; i++) { char key[32]; snprintf(key, sizeof(key), "device_ch%d", i); obs_data_set_int(data, key, -1); @@ -89,21 +93,53 @@ void output_start() } } +void callback() +{ + QMainWindow *mainWindow = (QMainWindow *)obs_frontend_get_main_window(); + QWidget *obsSettingsDialog = nullptr; + const auto topLevels = QApplication::topLevelWidgets(); + for (QWidget *widget : topLevels) { + if (widget->isVisible() && QString(widget->metaObject()->className()).contains("OBSBasicSettings")) { + obsSettingsDialog = widget; + break; + } + } + if (!settingsDialog_) { + if (!obsSettingsDialog) { + settingsDialog_ = new ASIOSettingsDialog(mainWindow, context.output, context.settings); + } else { + settingsDialog_ = new ASIOSettingsDialog(obsSettingsDialog, context.output, context.settings); + } + + settingsDialog_->setAttribute(Qt::WA_DeleteOnClose); + QObject::connect(settingsDialog_, &QObject::destroyed, []() { settingsDialog_ = nullptr; }); + } + + settingsDialog_->showHideDialog(context.enabled); + if (obsSettingsDialog) { + QRect settingsRect = obsSettingsDialog->geometry(); + QRect asioRect = settingsDialog_->geometry(); + QPoint newPos(settingsRect.right() + SHIFT_WINDOW, settingsRect.top()); + QScreen *screen = obsSettingsDialog->screen(); + QRect desktopRect = screen->availableGeometry(); + if (newPos.x() + asioRect.width() > desktopRect.right()) { + newPos.setX(desktopRect.right() - asioRect.width()); + } + + settingsDialog_->move(newPos); + } +} + void addOutputUI(void) { QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction(obs_module_text("AsioOutput.Menu")); - - QMainWindow *mainWindow = (QMainWindow *)obs_frontend_get_main_window(); + action->setObjectName("asioOutputSetupAction"); obs_frontend_push_ui_translation(obs_module_get_string); - settingsDialog_ = new ASIOSettingsDialog(mainWindow, context.output, context.settings); obs_frontend_pop_ui_translation(); - - auto cb = []() { - settingsDialog_->ShowHideDialog(); - }; - - action->connect(action, &QAction::triggered, cb); + // The UI is added through the callback, which is triggered in OBS Audio Settings. + action->connect(action, &QAction::triggered, callback); + action->setVisible(false); } static void OBSEvent(enum obs_frontend_event event, void *) @@ -136,10 +172,15 @@ void obs_module_unload(void) output_stop(); } - obs_output_release(context.output); - context.output = nullptr; - obs_data_release(context.settings); - context.settings = nullptr; + if (context.output) { + obs_output_release(context.output); + context.output = nullptr; + } + + if (context.settings) { + obs_data_release(context.settings); + context.settings = nullptr; + } obs_frontend_remove_event_callback(OBSEvent, nullptr); } @@ -150,8 +191,11 @@ void obs_module_post_load(void) } context.settings = load_settings(); + obs_output_t *const output = obs_output_create("asio_output", "asio_output", context.settings, NULL); + if (output != nullptr) { + context.enabled = true; context.output = output; if (!context.settings) { @@ -162,5 +206,8 @@ void obs_module_post_load(void) obs_frontend_add_event_callback(OBSEvent, nullptr); } else { blog(LOG_INFO, "Failed to create ASIO output"); + // We add the UI even if there is no output to display a text saying ASIO is disabled + // so that users know that ASIO exists. + addOutputUI(); } } diff --git a/frontend/plugins/asio-output-ui/data/locale/en-US.ini b/frontend/plugins/asio-output-ui/data/locale/en-US.ini index 28246e7972e1e1..84c198598fa6f3 100644 --- a/frontend/plugins/asio-output-ui/data/locale/en-US.ini +++ b/frontend/plugins/asio-output-ui/data/locale/en-US.ini @@ -1 +1,2 @@ -AsioOutput.Menu="ASIO Output" \ No newline at end of file +AsioOutput.Menu="ASIO Output" +AsioOutput.Disabled="No ASIO audio driver was detected in your system. ASIO monitoring is disabled." diff --git a/frontend/settings/OBSBasicSettings.cpp b/frontend/settings/OBSBasicSettings.cpp index f974a1defa0513..60719d19acaafd 100644 --- a/frontend/settings/OBSBasicSettings.cpp +++ b/frontend/settings/OBSBasicSettings.cpp @@ -719,6 +719,14 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) if (obs_audio_monitoring_available()) FillAudioMonitoringDevices(); +#ifdef _WIN32 + connect(ui->asioMonitoring, &QPushButton::clicked, this, &OBSBasicSettings::AsioMonitoringShow); + ui->asioMonitoring->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + ui->formLayout_56->setAlignment(ui->asioMonitoring, Qt::AlignLeft); +#else + ui->asioMonitoring->hide(); + ui->asioDeviceLabel->hide(); +#endif connect(ui->channelSetup, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::SurroundWarning); connect(ui->channelSetup, &QComboBox::currentIndexChanged, this, &OBSBasicSettings::SpeakerLayoutChanged); connect(ui->lowLatencyBuffering, &QCheckBox::clicked, this, &OBSBasicSettings::LowLatencyBufferingChanged); @@ -5281,6 +5289,23 @@ void OBSBasicSettings::SimpleRecordingEncoderChanged() ui->simpleOutInfoLayout->addWidget(simpleOutRecWarning); } +#ifdef _WIN32 +void OBSBasicSettings::AsioMonitoringShow() +{ + QList actions = main->ui->menuTools->actions(); + QAction *asioAction = nullptr; + for (QAction *action : actions) { + if (action->objectName() == "asioOutputSetupAction") { + asioAction = action; + break; + } + } + if (asioAction) { + asioAction->trigger(); + } +} +#endif + void OBSBasicSettings::SurroundWarning(int idx) { if (idx == lastChannelSetupIdx || idx == -1) diff --git a/frontend/settings/OBSBasicSettings.hpp b/frontend/settings/OBSBasicSettings.hpp index 38412505398bf6..e79985b9204477 100644 --- a/frontend/settings/OBSBasicSettings.hpp +++ b/frontend/settings/OBSBasicSettings.hpp @@ -396,6 +396,9 @@ private slots: void AudioChanged(); void AudioChangedRestart(); void ReloadAudioSources(); +#ifdef _WIN32 + void AsioMonitoringShow(); +#endif void SurroundWarning(int idx); void SpeakerLayoutChanged(int idx); void LowLatencyBufferingChanged(bool checked); From 8ee58b659385eab43e5c7043f6a7e1cdbb272748 Mon Sep 17 00:00:00 2001 From: pkv Date: Fri, 16 Jan 2026 16:29:04 +0100 Subject: [PATCH 8/9] plugins: Enable extra ASIO monitoring track for transitions and slideshow Modifies obs-transitions and image-source. This allows the audio of transitions such as stingers and slideshows to be heard on the ASIO monitoring track. Signed-off-by: pkv --- plugins/image-source/obs-slideshow-mk2.c | 2 +- plugins/image-source/obs-slideshow.c | 2 +- plugins/obs-transitions/transition-stinger.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/image-source/obs-slideshow-mk2.c b/plugins/image-source/obs-slideshow-mk2.c index 4d756961de4651..c3807473a791f5 100644 --- a/plugins/image-source/obs-slideshow-mk2.c +++ b/plugins/image-source/obs-slideshow-mk2.c @@ -896,7 +896,7 @@ static inline bool ss_audio_render_(obs_source_t *transition, uint64_t *ts_out, return false; obs_source_get_audio_mix(transition, &child_audio); - for (size_t mix = 0; mix < MAX_AUDIO_MIXES; mix++) { + for (size_t mix = 0; mix < MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES; mix++) { if ((mixers & (1 << mix)) == 0) continue; diff --git a/plugins/image-source/obs-slideshow.c b/plugins/image-source/obs-slideshow.c index 7d61cf0c30b5ea..b05465ee738b15 100644 --- a/plugins/image-source/obs-slideshow.c +++ b/plugins/image-source/obs-slideshow.c @@ -783,7 +783,7 @@ static inline bool ss_audio_render_(obs_source_t *transition, uint64_t *ts_out, return false; obs_source_get_audio_mix(transition, &child_audio); - for (size_t mix = 0; mix < MAX_AUDIO_MIXES; mix++) { + for (size_t mix = 0; mix < MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES; mix++) { if ((mixers & (1 << mix)) == 0) continue; diff --git a/plugins/obs-transitions/transition-stinger.c b/plugins/obs-transitions/transition-stinger.c index fddeb9302055aa..369297e64a1795 100644 --- a/plugins/obs-transitions/transition-stinger.c +++ b/plugins/obs-transitions/transition-stinger.c @@ -504,7 +504,7 @@ static bool stinger_audio_render(void *data, uint64_t *ts_out, struct obs_source struct obs_source_audio_mix child_audio; obs_source_get_audio_mix(s->media_source, &child_audio); - for (size_t mix = 0; mix < MAX_AUDIO_MIXES; mix++) { + for (size_t mix = 0; mix < MAX_AUDIO_MIXES + MAX_AUDIO_MONITORING_MIXES; mix++) { if ((mixers & (1 << mix)) == 0) continue; From 3336c2cfa189b438317398203eebcc82309dc668 Mon Sep 17 00:00:00 2001 From: pkv Date: Sat, 31 Jan 2026 13:28:02 +0100 Subject: [PATCH 9/9] obs-transitions: Set tracks for stinger Currently the stinger is output on all tracks. When MONITOR_ONLY is picked, we explicitly set the mixers bitwise to 0 so that the audio never goes to outputs except the extra ASIO track. Signed-off-by: pkv --- plugins/obs-transitions/transition-stinger.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/obs-transitions/transition-stinger.c b/plugins/obs-transitions/transition-stinger.c index 369297e64a1795..c7513928c3dc42 100644 --- a/plugins/obs-transitions/transition-stinger.c +++ b/plugins/obs-transitions/transition-stinger.c @@ -137,6 +137,13 @@ static void stinger_update(void *data, obs_data_t *settings) } s->monitoring_type = (int)obs_data_get_int(settings, "audio_monitoring"); + /* If the stinger is set to monitor only, don't send audio to output mixers. */ + if (s->monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) { + obs_source_set_audio_mixers(s->media_source, 0); + } else { + obs_source_set_audio_mixers(s->media_source, 0xFF); + } + obs_source_set_monitoring_type(s->media_source, s->monitoring_type); s->fade_style = (enum fade_style)obs_data_get_int(settings, "audio_fade_style");