diff --git a/frontend/data/locale/en-US.ini b/frontend/data/locale/en-US.ini
index 908af15890c577..b627e751326992 100644
--- a/frontend/data/locale/en-US.ini
+++ b/frontend/data/locale/en-US.ini
@@ -1309,6 +1309,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 d00b26708fc4a1..2dd35e5dd07dfd 100644
--- a/frontend/data/themes/Yami.obt
+++ b/frontend/data/themes/Yami.obt
@@ -1406,6 +1406,12 @@ QPushButton::menu-indicator {
width: 25px;
}
+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 1d59d537b7dde5..e0f0fcb2766a1a 100644
--- a/frontend/forms/OBSBasicSettings.ui
+++ b/frontend/forms/OBSBasicSettings.ui
@@ -6233,6 +6233,29 @@
+ -
+
+
+ Basic.Settings.Advanced.Audio.AsioMonitoringDevice
+
+
+ asioMonitoring
+
+
+
+ -
+
+
+ Basic.Settings.Audio.AsioMonitoring
+
+
+ asioMonitoring
+
+
+
+
+
+
@@ -8861,6 +8884,7 @@
meterDecayRate
peakMeterType
monitoringDevice
+ asioMonitoring
disableAudioDucking
lowLatencyBuffering
baseResolution
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..b51277e6911902
--- /dev/null
+++ b/frontend/plugins/asio-output-ui/ASIOSettingsDialog.cpp
@@ -0,0 +1,104 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 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
+
+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(bool enabled)
+{
+ SetupPropertiesView(enabled);
+ setVisible(!isVisible());
+}
+
+void ASIOSettingsDialog::SetupPropertiesView(bool enabled)
+{
+ if (propertiesView)
+ delete propertiesView;
+
+ propertiesView = new OBSPropertiesView(settings_, "asio_output",
+ (PropertiesReloadCallback)obs_get_output_properties, 170);
+
+ 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);
+}
+
+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 -> Valid device: start if not running
+ if (!output_running)
+ output_start();
+ } else if (!wasEmpty && nowEmpty) {
+ // Valid device -> 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..0086b1611f48ba
--- /dev/null
+++ b/frontend/plugins/asio-output-ui/ASIOSettingsDialog.h
@@ -0,0 +1,58 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 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 .
+******************************************************************************/
+#ifdef __cplusplus
+#define EXPORT_C extern "C"
+#else
+#define EXPORT_C
+#endif
+
+#pragma once
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include "./forms/ui_output.h"
+
+#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(bool enabled);
+ void SetupPropertiesView(bool enabled);
+ 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..5c150aed2ea8f9
--- /dev/null
+++ b/frontend/plugins/asio-output-ui/asio-ui-main.cpp
@@ -0,0 +1,200 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 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
+#include
+#include
+
+#include
+#include
+#include "ASIOSettingsDialog.h"
+
+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.
+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 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() + 100, 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"));
+ action->setObjectName("asioOutputSetupAction");
+
+ obs_frontend_push_ui_translation(obs_module_get_string);
+ obs_frontend_pop_ui_translation();
+ // 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 *)
+{
+ 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();
+
+ 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);
+}
+
+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.enabled = true;
+ 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");
+ // we add the UI even if there is no output to display a text saying ASIO is disabled
+ addOutputUI();
+ }
+}
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..84c198598fa6f3
--- /dev/null
+++ b/frontend/plugins/asio-output-ui/data/locale/en-US.ini
@@ -0,0 +1,2 @@
+AsioOutput.Menu="ASIO Output"
+AsioOutput.Disabled="No ASIO audio driver was detected in your system. ASIO monitoring is disabled."
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
+
+ -
+
+
+
+
+
+
+
diff --git a/frontend/settings/OBSBasicSettings.cpp b/frontend/settings/OBSBasicSettings.cpp
index 6e47f93ac924df..ecb4244c7c73f6 100644
--- a/frontend/settings/OBSBasicSettings.cpp
+++ b/frontend/settings/OBSBasicSettings.cpp
@@ -718,6 +718,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);
@@ -5287,6 +5295,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);
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 1af88f1d899dfc..3cf59af117b829 100644
--- a/libobs/obs-audio.c
+++ b/libobs/obs-audio.c
@@ -138,7 +138,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 219293413f44df..beb24583c26115 100644
--- a/libobs/obs-internal.h
+++ b/libobs/obs-internal.h
@@ -869,7 +869,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;
@@ -1258,7 +1258,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 c4a1955bb9dfd0..ae42e80e3349ba 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-source-transition.c b/libobs/obs-source-transition.c
index 92b5d107b9d11f..8a21444ff9be86 100644
--- a/libobs/obs-source-transition.c
+++ b/libobs/obs-source-transition.c
@@ -869,7 +869,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 533b91a5b33a5d..c36e3114f5951e 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++) {
@@ -5224,7 +5225,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);
}
@@ -5259,11 +5260,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);
@@ -5276,7 +5278,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];
}
@@ -5293,7 +5295,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)
@@ -5355,7 +5357,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) {
@@ -5435,7 +5437,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];
}
@@ -5519,10 +5521,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 9cfef216637d73..3dc48ec55da930 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];
};
/**
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..aee81faf5c0a2c
--- /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.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..11872325c56288
--- /dev/null
+++ b/plugins/win-asio/asio-callbacks.c
@@ -0,0 +1,79 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 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"
+
+#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..9017146dc3e31b
--- /dev/null
+++ b/plugins/win-asio/asio-callbacks.h
@@ -0,0 +1,30 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 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"
+
+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..11554146d2eb99
--- /dev/null
+++ b/plugins/win-asio/asio-common.h
@@ -0,0 +1,46 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 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 "iasiodrv.h"
+#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_LOG(level, format, ...) \
+ blog(level, "[asio_device '%s']: " format, (dev && dev->device_name) ? dev->device_name : "(null)", ##__VA_ARGS__)
+
+#define ASIO_LOG2(level, format, ...) \
+ blog(level, "[asio_device_list]: " format, ##__VA_ARGS__)
+
+#define debug(format, ...) ASIO_LOG(LOG_DEBUG, format, ##__VA_ARGS__)
+#define warn(format, ...) ASIO_LOG(LOG_WARNING, format, ##__VA_ARGS__)
+#define info(format, ...) ASIO_LOG(LOG_INFO, format, ##__VA_ARGS__)
+#define info2(format, ...) ASIO_LOG2(LOG_INFO, format, ##__VA_ARGS__)
+#define error(format, ...) ASIO_LOG(LOG_ERROR, format, ##__VA_ARGS__)
+#define error2(format, ...) ASIO_LOG2(LOG_ERROR, format, ##__VA_ARGS__)
diff --git a/plugins/win-asio/asio-device-list.c b/plugins/win-asio/asio-device-list.c
new file mode 100644
index 00000000000000..ddb77e79bbe9cd
--- /dev/null
+++ b/plugins/win-asio/asio-device-list.c
@@ -0,0 +1,206 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 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
+
+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)) {
+ info2("Skipping blacklisted driver: %s", subkey);
+ return false;
+ }
+
+ /* Try to open the driver key */
+ err = RegOpenKeyExA(hKey, subkey, 0, KEY_READ, &driverKey);
+ if (err != ERROR_SUCCESS) {
+ info2("Failed to open subkey: %s (err=%ld)", subkey, err);
+ return false;
+ }
+
+ /* Read CLSID */
+ valueSize = sizeof(clsid_str);
+ err = RegGetValueW(driverKey, NULL, L"CLSID", RRF_RT_REG_SZ, NULL, clsid_str, &valueSize);
+ if (err != ERROR_SUCCESS) {
+ info2("Could not read CLSID for %s (err=%ld)", subkey, err);
+ RegCloseKey(driverKey);
+ return false;
+ }
+
+ /* Parse CLSID */
+ if (CLSIDFromString(clsid_str, &entry->clsid) != S_OK) {
+ info2("CLSIDFromString failed for %s ā %ls", subkey, clsid_str);
+ RegCloseKey(driverKey);
+ return false;
+ }
+
+ /* Read description */
+ valueSize = sizeof(desc_str);
+ err = RegGetValueW(driverKey, NULL, L"Description", RRF_RT_REG_SZ, NULL, desc_str, &valueSize);
+ if (err != ERROR_SUCCESS) {
+ info2("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) {
+ error2("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;
+ }
+ error2("You have more than 16 asio devices, that's too many !\nShip me some...");
+ 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];
+ }
+ }
+ 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..9a7152e4763613
--- /dev/null
+++ b/plugins/win-asio/asio-device-list.h
@@ -0,0 +1,64 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 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
+#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);
+
+static inline WCHAR *utf8_to_wide(const char *str)
+{
+ if (!str)
+ return NULL;
+
+ int size_needed = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
+ if (size_needed <= 0)
+ return NULL;
+
+ WCHAR *wstr = (WCHAR *)malloc(size_needed * sizeof(WCHAR));
+ if (!wstr)
+ return NULL;
+
+ if (MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, size_needed) == 0) {
+ free(wstr);
+ return NULL;
+ }
+
+ return wstr;
+}
diff --git a/plugins/win-asio/asio-device.c b/plugins/win-asio/asio-device.c
new file mode 100644
index 00000000000000..241868d723da12
--- /dev/null
+++ b/plugins/win-asio/asio-device.c
@@ -0,0 +1,1255 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 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.h"
+#include "win-asio.h"
+#include "asio-callbacks.h"
+#include "asio-format.h"
+
+#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 bComInitialized = 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;
+}
+
+bool asio_device_is_playing(struct asio_device *dev)
+{
+ return dev->asio != NULL && (dev->current_nb_clients != 0 || dev->obs_output_client != NULL);
+}
+
+void asio_device_show_control_panel(struct asio_device *dev)
+{
+ info("Opening ASIO control panel...");
+ if (dev->asio)
+ dev->asio->lpVtbl->controlPanel(dev->asio);
+}
+
+static void asio_device_do_reset_task(void *param)
+{
+ struct asio_device *dev = (struct asio_device *)param;
+ info("ASIO driver reset");
+ 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->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.");
+ 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)
+{
+ 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 */
+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) {
+ if (granularity < 16)
+ granularity = 16;
+ /* ensure that the buffer size is a multiple of 16, customary with ASIO drivers */
+ int start = (min_size + 15) & ~15;
+ if (granularity > start)
+ start = granularity;
+
+ for (int i = start; i <= max_size && i <= 6400; i += granularity) {
+ long step = granularity * (i / granularity);
+ da_push_back(dev->buffer_sizes, &step);
+ }
+
+ } else {
+ for (int i = 0; i < 18; ++i) {
+ int size = 1 << i;
+ if (size >= min_size && size <= max_size)
+ da_push_back(dev->buffer_sizes, &size);
+ }
+ }
+}
+
+ASIOError asio_device_refresh_buffer_sizes(struct asio_device *dev)
+{
+ if (!dev || !dev->asio)
+ return ASE_NotPresent;
+
+ ASIOError err = dev->asio->lpVtbl->getBufferSize(dev->asio, &dev->min_buffer_size, &dev->max_buffer_size,
+ &dev->preferred_buffer_size, &dev->buffer_granularity);
+
+ if (err == ASE_OK) {
+ da_clear(dev->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 (dev->asio->lpVtbl->getBufferSize(dev->asio, &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 (dev->asio->lpVtbl->getBufferSize(dev->asio, &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;
+ }
+
+ /* Workaround for buggy drivers which crash if you make dynamic changes to the buffer size */
+ if (strstr(dev->device_name, "Digidesign"))
+ dev->should_use_preferred_size = true;
+
+ 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;
+}
+
+static bool asio_device_remove_current_driver(struct asio_device *dev)
+{
+ bool released_ok = true;
+ if (dev->asio) {
+ __try {
+ dev->asio->lpVtbl->Release(dev->asio);
+ } __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 (!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->asio == NULL) {
+ if (driver_error) {
+ strncpy(driver_error, "No driver", 30);
+ }
+ return;
+ }
+ HWND hwnd = GetDesktopWindow();
+ bool init_ok = dev->asio->lpVtbl->init(dev->asio, &hwnd) == ASIOTrue;
+
+ if (!init_ok) {
+ dev->asio->lpVtbl->getErrorMessage(dev->asio, driver_error);
+ if (!driver_error[0]) {
+ strncpy(driver_error, "Driver failed to initialize", 30);
+ }
+ }
+ /* seems expected by sh...y drivers */
+ if (driver_error && !driver_error[0]) {
+ char buffer[256] = {0};
+ dev->asio->lpVtbl->getDriverName(dev->asio, 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;
+
+ /* COM initialization */
+ if (!bComInitialized) {
+ bComInitialized = SUCCEEDED(CoInitialize(NULL));
+ if (bComInitialized) {
+ 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->buffer_sizes);
+ da_init(dev->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->excess_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->excess_frames[j]);
+ }
+
+ da_free(dev->buffer_sizes);
+ da_free(dev->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 (bComInitialized) {
+ if (!are_any_devices_still_active()) {
+ CoUninitialize();
+ bComInitialized = 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(IASIO *asio, char channel_names[MAX_DEVICE_CHANNELS][MAX_CH_NAME_LENGTH], int index,
+ bool is_input)
+{
+ if (index < 0 || index >= MAX_DEVICE_CHANNELS)
+ return false;
+
+ ASIOChannelInfo channel_info = {.channel = index, .isInput = is_input ? ASIOTrue : ASIOFalse};
+
+ if (asio->lpVtbl->getChannelInfo(asio, &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 = dev->asio->lpVtbl->getSampleRate(dev->asio, &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 = dev->asio->lpVtbl->setSampleRate(dev->asio, 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 = dev->asio->lpVtbl->setClockSource(dev->asio, dev->clocks[0].index);
+ if (err != ASE_OK)
+ warn("setClockSource failed with code %d", err);
+
+ Sleep(10);
+
+ err = dev->asio->lpVtbl->setSampleRate(dev->asio, 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 (dev->asio->lpVtbl->canSampleRate(dev->asio, 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->sample_rates.num != new_rates.num) {
+ changed = true;
+ } else {
+ for (size_t i = 0; i < dev->sample_rates.num; ++i) {
+ if (dev->sample_rates.array[i] != new_rates.array[i]) {
+ changed = true;
+ break;
+ }
+ }
+ }
+
+ if (changed) {
+ da_move(dev->sample_rates, new_rates);
+
+ char buffer[256] = {0};
+ for (size_t i = 0; i < dev->sample_rates.num; ++i) {
+ char tmp[32];
+ snprintf(tmp, sizeof(tmp), "%.0f%s", dev->sample_rates.array[i],
+ (i < dev->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 = dev->asio->lpVtbl->getClockSources(dev->asio, 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 = dev->asio->lpVtbl->setClockSource(dev->asio, 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)
+{
+ dev->input_latency = 0;
+ dev->output_latency = 0;
+
+ if (dev->asio->lpVtbl->getLatencies(dev->asio, &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 = dev->asio->lpVtbl->getChannels(dev->asio, &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->asio, 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->asio, dev->output_channel_names, i, false))
+ snprintf(dev->output_channel_names[i], MAX_CH_NAME_LENGTH, "Output %d", i);
+ }
+}
+
+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 = dev->asio->lpVtbl->createBuffers(dev->asio, 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;
+ dev->asio->lpVtbl->getChannels(dev->asio, &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;
+ dev->asio->lpVtbl->getChannelInfo(dev->asio, &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)
+{
+ ASIOError err = ASE_OK;
+ if (dev->asio != NULL && dev->buffers_created) {
+ dev->buffers_created = false;
+ err = dev->asio->lpVtbl->disposeBuffers(dev->asio);
+ }
+ 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 for extra safety, we do it anyway 'Ć la Cubase' */
+void asio_device_test(struct asio_device *dev)
+{
+ 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->buffer_sizes);
+ da_clear(dev->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 = dev->asio->lpVtbl->getChannels(dev->asio, &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 = dev->asio->lpVtbl->setSampleRate(dev->asio, 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 = dev->asio->lpVtbl->outputReady(dev->asio) == 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 = dev->asio->lpVtbl->start(dev->asio);
+ Sleep(80);
+ dev->asio->lpVtbl->stop(dev->asio);
+ } 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;
+ asio_device_dispose_buffers(dev);
+ 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;
+ const IASIOVtbl *iasiotbl = dev->asio->lpVtbl;
+
+ ASIOError err = iasiotbl->getChannels(dev->asio, &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 */
+ 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->sample_rates.num; ++i) {
+ if (dev->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->errorstring[0] = '\0';
+ dev->buffers_created = false;
+
+ dev->current_block_size_samples = dev->current_buffer_size =
+ asio_device_read_buffer_size(dev, dev->current_buffer_size);
+
+ /* need to get this again in case a sample rate change affected the channel count */
+ err = iasiotbl->getChannels(dev->asio, &dev->total_num_input_chans, &dev->total_num_output_chans);
+ 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 (iasiotbl->future(dev->asio, 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; 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("disposing buffers");
+ err = iasiotbl->disposeBuffers(dev->asio);
+ if (err != ASE_OK)
+ info("Device didn't dispose correctly of the buffers; error code %i", err);
+
+ info("creating buffers: %i in-out channels, size: %i samples", total_buffers, dev->current_block_size_samples);
+ err = iasiotbl->createBuffers(dev->asio, 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 = iasiotbl->disposeBuffers(dev->asio);
+ err = iasiotbl->createBuffers(dev->asio, 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 which have only input or only output channels, we set format 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};
+ iasiotbl->getChannelInfo(dev->asio, &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};
+ iasiotbl->getChannelInfo(dev->asio, &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 = iasiotbl->start(dev->asio);
+
+ if (err != ASE_OK) {
+ dev->is_open = false;
+ error("stop on failure");
+ Sleep(10);
+ iasiotbl->stop(dev->asio);
+ 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.");
+ iasiotbl->stop(dev->asio);
+ 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);
+
+ info(" asio driver stopping");
+
+ if (dev->asio != NULL) {
+ Sleep(20);
+ dev->asio->lpVtbl->stop(dev->asio);
+ 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 (dev->asio->lpVtbl->start(dev->asio) != 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) {
+ dev->asio->lpVtbl->stop(dev->asio);
+ 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;
+}
+
+/* ASIO callbacks are quite a pain because they don't store the 'this' pointer. In c++ we could do some template tricks
+ * as done by JUCE. But in C, we're more restricted, hence the use of some macros. */
+
+/* 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; /* tells the driver to request a reset or it won't work per SDK */
+
+ 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 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 */
+ 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->silentBuffers8;
+ }
+ 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 from OBS output --- */
+ int sample_size_byte = dev->output_format[0].bit_depth / 8;
+
+ 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];
+ if (os_atomic_load_bool(&dev->capture_started) &&
+ dev->excess_frames[outchan].size >= samps * sample_size_byte &&
+ dev->obs_track[outchan] >= 0 && dev->obs_track_channel[outchan] >= 0) {
+ deque_pop_front(&dev->excess_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)
+ dev->asio->lpVtbl->outputReady(dev->asio);
+}
+
+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)
+ dev->asio->lpVtbl->outputReady(dev->asio);
+ }
+ 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..b8662aa379aa3f
--- /dev/null
+++ b/plugins/win-asio/asio-device.h
@@ -0,0 +1,158 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 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 .
+******************************************************************************/
+#ifndef ASIO_DEVICE_H
+#define ASIO_DEVICE_H
+#pragma once
+#include "asio-common.h"
+#include "asio-format.h"
+
+#include
+#include
+#include
+#include
+#include
+
+struct asio_data;
+
+struct asio_device {
+ /* Device name */
+ char device_name[64];
+
+ /* COM driver stuff */
+ IASIO *asio;
+ DWORD com_thread_id;
+ CLSID clsid;
+
+ /* channel info : number & names */
+ 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];
+
+ /* Audio buffers: the meat and blood */
+ 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 silentBuffers8[4096];
+
+ /* Device buffer settings (expressed usually as a number of samples e.g. 512, 1024, etc.). This impacts the
+ * latency of the device; at 48 kHz, there are 48000 samples per second, so a buffer size of 512 samples means
+ * a latency of 512/48000 = 0.01067 seconds or 10.67 ms.
+ */
+ int current_buffer_size;
+ int buffer_granularity;
+ DARRAY(int) buffer_sizes; // array holding the buffer values permitted by the device
+ int min_buffer_size;
+ int max_buffer_size;
+ int preferred_buffer_size;
+ bool should_use_preferred_size;
+
+ /* Device sample rate*/
+ DARRAY(double) sample_rates;
+ double current_sample_rate;
+
+ /* Device audio format (32bit ffloat, 16bit int, 24 bit int, 32bit int */
+ 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];
+ ASIOSampleType output_type; // the format to which obs float audio data must be converted to.
+
+ /* clocks */
+ ASIOClockSource clocks[MAX_DEVICE_CHANNELS];
+ int num_clock_sources;
+
+ /* ASIO callbacks */
+ ASIOCallbacks callbacks;
+ volatile bool called_back;
+
+ /* Device state */
+ bool is_open;
+ bool is_started;
+ bool driver_failure;
+ volatile bool need_to_reset;
+ bool buffers_created;
+ bool post_output;
+ volatile bool capture_started;
+
+ /* Misc. info */
+ 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;
+
+ /* ======= ASIO device <==> OBS communication ====== */
+
+ /* 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.
+ * 'excess_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[MAX_DEVICE_CHANNELS]: array which holds the track index for each device output channel.
+ * obs_track_channel[]: array which stores the channel index of an OBS audio track.
+ */
+ struct asio_data *obs_output_client;
+ struct deque excess_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);
+
+#endif // ASIO_DEVICE_H
diff --git a/plugins/win-asio/asio-format.c b/plugins/win-asio/asio-format.c
new file mode 100644
index 00000000000000..cba53d397dab86
--- /dev/null
+++ b/plugins/win-asio/asio-format.c
@@ -0,0 +1,180 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 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-format.h"
+#include "asio.h"
+#include "byteorder.h"
+
+#include
+#include
+#include
+
+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; // unhandled formats
+ }
+}
+
+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..cc895a1e186618
--- /dev/null
+++ b/plugins/win-asio/asio-format.h
@@ -0,0 +1,40 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 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 .
+******************************************************************************/
+#ifndef ASIO_FORMAT_H
+#define ASIO_FORMAT_H
+
+#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);
+
+#endif
diff --git a/plugins/win-asio/asio.h b/plugins/win-asio/asio.h
new file mode 100644
index 00000000000000..9f2c7ce8d0d404
--- /dev/null
+++ b/plugins/win-asio/asio.h
@@ -0,0 +1,139 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 pkv
+
+ This file is part of win-asio.
+ It is a minimal header extracted from the SDK removing anything we don't use in obs ASIO host.
+ 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
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* --------------------- Basic Types --------------------- */
+typedef int32_t ASIOBool;
+typedef int32_t ASIOError;
+typedef double ASIOSampleRate;
+typedef int32_t 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_NotSupported = -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,
+ kAsioCanReportOverload = 16
+};
+
+/* --------------------- 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;
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/plugins/win-asio/byteorder.h b/plugins/win-asio/byteorder.h
new file mode 100644
index 00000000000000..168da9700d5648
--- /dev/null
+++ b/plugins/win-asio/byteorder.h
@@ -0,0 +1,111 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 pkv
+
+ This file is part of win-asio.
+ It is a c-adaptation and rewriting of juce_ByteOrder.h from JUCE 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 .
+******************************************************************************/
+#ifndef BYTEORDER_H
+#define BYTEORDER_H
+
+#include
+#include
+
+// Swap 16-bit and 32-bit using Windows intrinsics
+#define ByteOrder_swap16(x) _byteswap_ushort(x)
+#define ByteOrder_swap32(x) _byteswap_ulong(x)
+
+// 16-bit reads
+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);
+}
+
+// 32-bit reads
+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);
+}
+
+// 24-bit reads (signed)
+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;
+}
+
+// 24-bit write
+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);
+}
+
+// Conditional endian swap macros
+static inline uint16_t ByteOrder_swapIfBigEndian16(uint16_t v)
+{
+ return v; // system is little-endian
+}
+
+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);
+}
+
+#endif // BYTEORDER_H
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..fddecb88945e97
--- /dev/null
+++ b/plugins/win-asio/data/locale/en-US.ini
@@ -0,0 +1,113 @@
+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 messagin 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, ...)"
+
+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"
+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/iasiodrv.h b/plugins/win-asio/iasiodrv.h
new file mode 100644
index 00000000000000..0eb0e631064159
--- /dev/null
+++ b/plugins/win-asio/iasiodrv.h
@@ -0,0 +1,74 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 pkv
+
+ This file is part of win-asio. It adapts to C the IASIO 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 .
+******************************************************************************/
+
+#ifndef IASIODRV_H
+#define IASIODRV_H
+#pragma once
+#include "asio.h"
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+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;
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // IASIODRV_H
diff --git a/plugins/win-asio/plugin-main.c b/plugins/win-asio/plugin-main.c
new file mode 100644
index 00000000000000..e377c6eede5edd
--- /dev/null
+++ b/plugins/win-asio/plugin-main.c
@@ -0,0 +1,61 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 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..af0e20e8adb5a1
--- /dev/null
+++ b/plugins/win-asio/win-asio.c
@@ -0,0 +1,832 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 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"
+
+extern os_event_t *shutting_down;
+extern volatile bool shutting_down_atomic;
+struct asio_data *global_output_asio_data;
+obs_data_t *global_output_settings;
+struct asio_device_list *list = NULL;
+
+/*==================================== DEVICE LIST =====================================*/
+void retrieve_device_list()
+{
+ list = asio_device_list_create();
+ /*DEBUG: check the plugin when no ASIO drivers are installed
+ * list->count = 0;
+ */
+}
+
+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__)
+
+/* This creates the device if it hasn't been created by this or another source or retrieves its pointer if it already
+ * exists. The source or output is also added as a client of the device. The data struct is attached to the device ptr.
+ */
+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 & 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("\nFailed to create device % s ", name);
+ return false;
+ } else if (!dev->asio) {
+ errordata("\nDriver 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 & 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;
+}
+
+/* This detaches the data struct from the device ptr.*/
+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; // both null ā equal
+ if (!a || !b)
+ return true; // one null ā different
+ 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");
+ /* new device is "" (no device) -> return */
+ if (!new_device || !*new_device) {
+ /* we are actually swapping from a 'device' -> "" */
+ 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;
+ /* Reset all mix channels to -1 to avoid leftover routing */
+ 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 + 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) {
+ 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 + 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];
+ 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;
+
+ /* ---- Device selector ---- */
+ 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);
+
+ /* ---- Common controls ---- */
+ 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"));
+ }
+
+ /* ---- Channel routing ---- */
+ 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 + 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];
+ snprintf(label, sizeof(label), "Track%d.%d", j, k);
+ obs_property_list_add_int(lp, obs_module_text(label), idx);
+ }
+ }
+ }
+ }
+ }
+
+ /* ---- Error / no-driver messages ---- */
+ 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 ask 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->excess_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..499b2ad83d7885
--- /dev/null
+++ b/plugins/win-asio/win-asio.h
@@ -0,0 +1,60 @@
+/******************************************************************************
+ Copyright (C) 2022-2025 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.
+};