Files
pkv e8e04a20c9 win-asio: Add ASIO source & output
This adds:
- an ASIO Input source, allowing audio capture of devices using
Steinberg ASIO audio SDK on Windows. This allows low latency capture.
- an ASIO output, which will allow routing audio from OBS to any ASIO
device. A per channel routing is provided.
The UI to setup the ASIO output is provided in a later commit.

Signed-off-by: pkv <pkv@obsproject.com>
2026-11-03 11:57:41 -05:00

1439 lines
40 KiB
C

/******************************************************************************
Copyright (C) 2022-2026 pkv <pkv@obsproject.com>
This file is part of win-asio.
It implements an ASIO host for OBS Studio using the Steinberg ASIO SDK,
available under a dual licence including the GNU GPLv3.
Certain parts of the driver initialisation sequence, buffer management
strategy and start/stop flow are inspired by the structure and behaviour
of the JUCE ASIO host (juce_ASIO_windows.cpp), but this file is an
independent C implementation adapted to OBS/libobs. No JUCE source code
is used or included.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "asio-device.h"
#include "asio-callbacks.h"
#include "asio-format.h"
#include "win-asio.h"
#include <obs-frontend-api.h>
#include <media-io/audio-io.h>
#include <util/platform.h>
#include <util/threading.h>
#include <guiddef.h>
#include <limits.h>
#include <objbase.h>
#include <strsafe.h>
#include <string.h>
#include <windows.h>
#define ASIO_OUTPUT_FIFO_BLOCKS 8L
bool com_initialized = false;
os_event_t *shutting_down;
volatile bool shutting_down_atomic = false;
struct asio_device *current_asio_devices[MAX_NUM_ASIO_DEVICES] = {0};
void asio_device_destroy_all();
void OBSEvent(enum obs_frontend_event event, void *none)
{
UNUSED_PARAMETER(none);
if (event == OBS_FRONTEND_EVENT_EXIT || event == OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN) {
os_atomic_set_bool(&shutting_down_atomic, true);
asio_device_destroy_all();
}
}
void asio_device_get_sample_format(int type, char *message)
{
switch (type) {
case ASIOSTInt24LSB:
strcpy(message, "24 bit int");
break;
case ASIOSTInt32LSB:
strcpy(message, "32 bit int");
break;
case ASIOSTFloat32LSB:
strcpy(message, "32 bit float");
break;
case ASIOSTLastEntry:
strcpy(message, "no channels available");
break;
default:
snprintf(message, 64, "uncommon format number (%d)", type);
break;
}
}
struct asio_device *asio_device_find_by_name(const char *name)
{
if (!name || !*name) {
return NULL;
}
for (int i = 0; i < MAX_NUM_ASIO_DEVICES; ++i) {
struct asio_device *dev = current_asio_devices[i];
if (dev && strcmp(dev->device_name, name) == 0) {
return dev;
}
}
return NULL;
}
void asio_device_show_control_panel(struct asio_device *dev)
{
info("Opening ASIO control panel...");
if (dev && dev->asio) {
ASIO_ControlPanel(dev);
}
}
bool asio_device_clear_output_fifos(struct asio_device *dev)
{
if (!dev || !os_atomic_load_bool(&dev->is_started)) {
return false;
}
os_atomic_set_bool(&dev->output_fifos_clear_requested, true);
for (int count = 300; count > 0; --count) {
if (!os_atomic_load_bool(&dev->output_fifos_clear_requested)) {
return true;
}
Sleep(10);
}
return false;
}
static void asio_device_free_output_fifos(struct asio_device *dev)
{
for (int i = 0; i < MAX_DEVICE_CHANNELS; ++i) {
spsc_fifo_free(&dev->output_frames[i]);
}
}
static bool asio_device_init_output_fifos(struct asio_device *dev)
{
long block_frames = dev->current_block_size_samples;
if (block_frames < AUDIO_OUTPUT_FRAMES) {
block_frames = AUDIO_OUTPUT_FRAMES;
}
const long capacity = block_frames * ASIO_OUTPUT_FIFO_BLOCKS + 1;
asio_device_free_output_fifos(dev);
for (int i = 0; i < dev->total_num_output_chans; ++i) {
if (!spsc_fifo_init(&dev->output_frames[i], capacity)) {
asio_device_free_output_fifos(dev);
return false;
}
}
return true;
}
static void asio_device_do_reset_task(void *param)
{
struct asio_device *dev = (struct asio_device *)param;
info("ASIO driver reset");
if (!dev) {
return;
}
struct asio_data *output_client = dev->obs_output_client;
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 (output_client) {
if (dev->is_open && os_atomic_load_bool(&dev->is_started)) {
if (!obs_output_start(output_client->output)) {
error("Failed to restart output after ASIO reset");
}
}
}
if (!dev->is_open) {
if (dev->errorstring[0] != '\0') {
error("Failed to reopen during reset: %s", dev->errorstring);
dev->errorstring[0] = '\0';
} else {
error("Failed to reopen during reset");
}
return;
}
asio_device_reload_channel_names(dev);
info("ASIO reset complete");
}
void asio_device_reset_request(struct asio_device *dev)
{
info("ASIO reset requested.");
if (!dev) {
return;
}
if (os_atomic_exchange_bool(&dev->need_to_reset, true)) {
return;
}
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);
}
static void asio_device_disable_buffer_processing(struct asio_device *dev)
{
os_atomic_set_bool(&dev->is_started, false);
while (os_atomic_load_long(&dev->buffer_callbacks_in_progress) > 0) {
Sleep(1);
}
}
void asio_device_reset_buffers(struct asio_device *dev)
{
if (!dev) {
return;
}
long num_input = dev->total_num_input_chans;
long num_output = dev->total_num_output_chans;
for (int i = 0; i < num_input; ++i) {
dev->buffer_infos[i].isInput = ASIOTrue;
dev->buffer_infos[i].channelNum = i;
dev->buffer_infos[i].buffers[0] = NULL;
dev->buffer_infos[i].buffers[1] = NULL;
}
for (int i = 0; i < num_output; ++i) {
int j = (int)(i + num_input);
dev->buffer_infos[j].isInput = ASIOFalse;
dev->buffer_infos[j].channelNum = i;
dev->buffer_infos[j].buffers[0] = NULL;
dev->buffer_infos[j].buffers[1] = NULL;
}
}
static bool contains_size(struct asio_device *dev, long value)
{
for (size_t i = 0; i < dev->supported_buffer_sizes.num; ++i) {
if (dev->supported_buffer_sizes.array[i] == value) {
return true;
}
}
return false;
}
void asio_device_add_buffer_sizes(struct asio_device *dev, long min_size, long max_size, long preferred_size,
long granularity)
{
if (!dev) {
return;
}
if (granularity >= 0) {
long step = (granularity < 16) ? 16 : granularity;
long size = min_size;
if (size % step) {
size = ((size + step - 1) / step) * step;
}
for (; size <= max_size && size <= 6400; size += step) {
da_push_back(dev->supported_buffer_sizes, &size);
}
} else {
for (int p = 0; p < 18; ++p) {
long size = 1L << p;
if (size >= min_size && size <= max_size) {
da_push_back(dev->supported_buffer_sizes, &size);
}
}
}
if (preferred_size >= min_size && preferred_size <= max_size && !contains_size(dev, preferred_size)) {
da_push_back(dev->supported_buffer_sizes, &preferred_size);
}
}
ASIOError asio_device_refresh_buffer_sizes(struct asio_device *dev)
{
if (!dev || !dev->asio) {
return ASE_NotPresent;
}
ASIOError err = ASIO_GetBufferSize(dev, &dev->min_buffer_size, &dev->max_buffer_size,
&dev->preferred_buffer_size, &dev->buffer_granularity);
if (err == ASE_OK) {
da_clear(dev->supported_buffer_sizes);
asio_device_add_buffer_sizes(dev, dev->min_buffer_size, dev->max_buffer_size,
dev->preferred_buffer_size, dev->buffer_granularity);
}
return err;
}
int asio_device_get_preferred_buffer_size(struct asio_device *dev)
{
if (!dev || !dev->asio) {
return 0;
}
int min_buffer_size = 0;
int max_buffer_size = 0;
int buffer_granularity = 0;
long preferred = 0;
if (ASIO_GetBufferSize(dev, &min_buffer_size, &max_buffer_size, &preferred, &buffer_granularity) == ASE_OK) {
return preferred;
}
return 0;
}
int asio_device_read_buffer_size(struct asio_device *dev, int requested_size)
{
if (!dev || !dev->asio) {
return requested_size;
}
dev->min_buffer_size = 0;
dev->max_buffer_size = 0;
dev->buffer_granularity = 0;
long new_preferred = 0;
if (ASIO_GetBufferSize(dev, &dev->min_buffer_size, &dev->max_buffer_size, &new_preferred,
&dev->buffer_granularity) == ASE_OK) {
if (dev->preferred_buffer_size != 0 && new_preferred != 0 &&
new_preferred != dev->preferred_buffer_size) {
dev->should_use_preferred_size = true;
}
if (requested_size < dev->min_buffer_size || requested_size > dev->max_buffer_size) {
dev->should_use_preferred_size = true;
}
dev->preferred_buffer_size = new_preferred;
}
if (dev->should_use_preferred_size) {
info("Using preferred size for buffer..");
ASIOError err = asio_device_refresh_buffer_sizes(dev);
if (err == ASE_OK) {
requested_size = dev->preferred_buffer_size;
} else {
requested_size = 1024;
warn("getBufferSize1 failed, using 1024 as fallback");
}
dev->should_use_preferred_size = false;
}
return requested_size;
}
static bool asio_device_remove_current_driver(struct asio_device *dev)
{
bool released_ok = true;
if (dev->asio) {
__try {
ASIO_Release(dev);
} __except (EXCEPTION_EXECUTE_HANDLER) {
error("Exception occurred while releasing COM object");
released_ok = false;
}
dev->asio = NULL;
}
return released_ok;
}
static bool asio_device_try_create_driver(struct asio_device *dev, bool *crashed)
{
bool success = false;
__try {
HRESULT hr =
CoCreateInstance(&dev->clsid, NULL, CLSCTX_INPROC_SERVER, &dev->clsid, (void **)&dev->asio);
success = SUCCEEDED(hr);
if (!success) {
error("CoCreateInstance failed (HRESULT 0x%lX)", (unsigned long)hr);
}
return success;
} __except (EXCEPTION_EXECUTE_HANDLER) {
error("Exception occurred during CoCreateInstance");
*crashed = true;
}
return false;
}
bool asio_device_load_driver(struct asio_device *dev)
{
if (!dev) {
return false;
}
if (!asio_device_remove_current_driver(dev)) {
strncpy(dev->errorstring, "** Driver crashed while being closed", 40);
} else {
info("Driver successfully removed");
}
bool crashed = false;
bool ok = asio_device_try_create_driver(dev, &crashed);
if (crashed) {
strncpy(dev->errorstring, "** Driver crashed while being opened", 40);
} else if (ok) {
info("driver com interface opened");
} else {
strncpy(dev->errorstring, "Failed to load driver", 30);
}
return ok;
}
bool asio_device_init_driver(struct asio_device *dev, char *driver_error)
{
if (!dev) {
return false;
}
if (dev->asio == NULL) {
if (driver_error) {
strncpy(driver_error, "No driver", 30);
}
return false;
}
bool init_ok = ASIO_Init(dev, GetDesktopWindow()) == ASIOTrue;
if (!init_ok) {
ASIO_GetErrorMessage(dev, driver_error);
if (!driver_error[0]) {
strncpy(driver_error, "Driver failed to initialize", 30);
}
return false;
}
/* That call seems required by misbehaving drivers although it is not mandated by the SDK. */
if (driver_error && !driver_error[0]) {
char buffer[256] = {0};
ASIO_GetDriverName(dev, buffer);
}
return true;
}
struct asio_device *asio_device_create(const char *name, CLSID clsid, int device_slot_index)
{
struct asio_device *dev = bzalloc(sizeof(struct asio_device));
if (!dev) {
return NULL;
}
dev->driver_failure = false;
if (!com_initialized) {
com_initialized = SUCCEEDED(CoInitialize(NULL));
if (com_initialized) {
dev->com_thread_id = GetCurrentThreadId();
debug("COM initialized in Main thread %lu for device %s", dev->com_thread_id, name);
} else {
error("Failed to initialize COM");
bfree(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);
}
if (device_slot_index < 0 || device_slot_index >= MAX_NUM_ASIO_DEVICES) {
blog(LOG_ERROR, "[ASIO] Invalid slot number %d in asio_device_create", device_slot_index);
bfree(dev);
return NULL;
}
strncpy(dev->device_name, name, sizeof(dev->device_name) - 1);
if (current_asio_devices[device_slot_index] != NULL) {
return NULL;
}
dev->clsid = clsid;
dev->device_slot_index = device_slot_index;
da_init(dev->supported_buffer_sizes);
da_init(dev->supported_sample_rates);
dev->callbacks.bufferSwitch = callback_sets[dev->device_slot_index].buffer_switch;
dev->callbacks.bufferSwitchTimeInfo = callback_sets[dev->device_slot_index].buffer_switch_time_info;
dev->callbacks.asioMessage = callback_sets[dev->device_slot_index].asio_message;
dev->callbacks.sampleRateDidChange = callback_sets[dev->device_slot_index].sample_rate_changed;
os_atomic_set_long(&dev->buffer_callbacks_in_progress, 0);
asio_device_test(dev);
if (dev->asio == NULL || dev->driver_failure) {
error("Failed to load driver");
bfree(dev);
return NULL;
} else {
current_asio_devices[device_slot_index] = dev;
for (int k = 0; k < MAX_DEVICE_CHANNELS; k++) {
dev->output_routes[k].mix_index = -1;
dev->output_routes[k].channel_index = -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;
}
da_free(dev->supported_buffer_sizes);
da_free(dev->supported_sample_rates);
for (int i = 0; i < MAX_NUM_ASIO_DEVICES; ++i) {
if (current_asio_devices[i] == dev) {
current_asio_devices[i] = NULL;
}
}
asio_device_close(dev);
asio_device_free_output_fifos(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;
}
bfree(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");
}
if (com_initialized) {
if (!are_any_devices_still_active()) {
CoUninitialize();
com_initialized = false;
debug("Last ASIO device destroyed — COM uninitialized");
} else {
debug("ASIO device destroyed — COM still in use by others");
}
}
bfree(dev);
}
void asio_device_destroy_all()
{
for (int i = 0; i < MAX_NUM_ASIO_DEVICES; ++i) {
if (current_asio_devices[i]) {
asio_device_destroy(current_asio_devices[i]);
}
}
}
bool get_channel_name(struct asio_device *dev, char channel_names[MAX_DEVICE_CHANNELS][MAX_CH_NAME_LENGTH], int index,
bool is_input)
{
if (!dev) {
return false;
}
if (index < 0 || index >= MAX_DEVICE_CHANNELS) {
return false;
}
ASIOChannelInfo channel_info = {.channel = index, .isInput = is_input ? ASIOTrue : ASIOFalse};
if (ASIO_GetChannelInfo(dev, &channel_info) != ASE_OK) {
return false;
}
strncpy(channel_names[index], channel_info.name, MAX_CH_NAME_LENGTH - 1);
channel_names[index][MAX_CH_NAME_LENGTH - 1] = '\0';
return true;
}
void clear_channel_names(char names[MAX_DEVICE_CHANNELS][MAX_CH_NAME_LENGTH])
{
for (int i = 0; i < MAX_DEVICE_CHANNELS; ++i) {
names[i][0] = '\0';
}
}
double asio_device_get_sample_rate(struct asio_device *dev)
{
double cr = 0;
ASIO_GetSampleRate(dev, &cr);
return cr;
}
bool asio_device_set_sample_rate(struct asio_device *dev, double new_rate)
{
if (!dev || !dev->asio) {
return false;
}
if (dev->current_sample_rate != new_rate) {
ASIOError err = ASIO_SetSampleRate(dev, new_rate);
Sleep(10);
if (err == ASE_NoClock && dev->num_clock_sources > 0) {
debug("trying to set a clock source");
err = ASIO_SetClockSource(dev, dev->clocks[0].index);
if (err != ASE_OK) {
debug("setClockSource failed with code %d", err);
}
Sleep(10);
err = ASIO_SetSampleRate(dev, new_rate);
if (err != ASE_OK) {
debug("setSampleRate (retry) failed with code %d", err);
return false;
}
Sleep(10);
}
if (err != ASE_OK) {
warn("setSampleRate failed with code %d", err);
return false;
} else {
info("rate change: %.0f -> %.0f", dev->current_sample_rate, new_rate);
}
dev->current_sample_rate = new_rate;
}
return true;
}
void asio_device_update_sample_rates_list(struct asio_device *dev)
{
if (!dev || !dev->asio) {
return;
}
static const double common_rates[] = {8000, 11025, 16000, 22050, 24000, 32000, 44100, 48000,
88200, 96000, 176400, 192000, 352800, 384000, 705600, 768000};
DARRAY(double) new_rates;
da_init(new_rates);
for (size_t i = 0; i < sizeof(common_rates) / sizeof(common_rates[0]); ++i) {
double rate = common_rates[i];
if (ASIO_CanSampleRate(dev, rate) == ASE_OK) {
da_push_back(new_rates, &rate);
}
}
if (new_rates.num == 0) {
double current = asio_device_get_sample_rate(dev);
info("No standard sample rates supported - using current rate: %.2f", current);
if (current > 0.0) {
da_push_back(new_rates, &current);
}
}
bool changed = false;
if (dev->supported_sample_rates.num != new_rates.num) {
changed = true;
} else {
for (size_t i = 0; i < dev->supported_sample_rates.num; ++i) {
if (dev->supported_sample_rates.array[i] != new_rates.array[i]) {
changed = true;
break;
}
}
}
if (changed) {
da_move(dev->supported_sample_rates, new_rates);
char buffer[256] = {0};
for (size_t i = 0; i < dev->supported_sample_rates.num; ++i) {
char tmp[32];
snprintf(tmp, sizeof(tmp), "%.0f%s", dev->supported_sample_rates.array[i],
(i < dev->supported_sample_rates.num - 1) ? ", " : "");
strcat_s(buffer, sizeof(buffer), tmp);
}
debug("Supported sample rates: %s", buffer);
}
da_free(new_rates);
}
void asio_device_update_clock_sources(struct asio_device *dev)
{
if (!dev || !dev->asio) {
return;
}
memset(dev->clocks, 0, sizeof(dev->clocks));
long num_sources = MAX_DEVICE_CHANNELS;
ASIOError err = ASIO_GetClockSources(dev, dev->clocks, &num_sources);
dev->num_clock_sources = (int)num_sources;
bool is_source_set = false;
for (int i = 0; i < dev->num_clock_sources; ++i) {
char log_msg[128];
snprintf(log_msg, sizeof(log_msg), "clock: %s", dev->clocks[i].name);
if (dev->clocks[i].isCurrentSource) {
is_source_set = true;
strcat_s(log_msg, sizeof(log_msg), " (cur)");
}
info("%s", log_msg);
}
if (dev->num_clock_sources > 1 && !is_source_set) {
info("setting clock source");
err = ASIO_SetClockSource(dev, dev->clocks[0].index);
if (err != ASE_OK) {
warn("setClockSource failed with code %d", err);
}
Sleep(20);
} else if (dev->num_clock_sources == 0) {
info("no clock sources!");
}
}
void asio_device_read_latencies(struct asio_device *dev)
{
if (!dev) {
return;
}
dev->input_latency = 0;
dev->output_latency = 0;
if (ASIO_GetLatencies(dev, &dev->input_latency, &dev->output_latency) != ASE_OK) {
info("getLatencies() failed");
} else {
info("Latencies (samples): in = %i, out = %i", dev->input_latency, dev->output_latency);
}
}
void asio_device_reload_channel_names(struct asio_device *dev)
{
if (!dev || !dev->asio) {
return;
}
long total_inputs = 0, total_outputs = 0;
ASIOError err = ASIO_GetChannels(dev, &total_inputs, &total_outputs);
if (err != ASE_OK) {
return;
}
if (total_inputs > MAX_DEVICE_CHANNELS || total_outputs > MAX_DEVICE_CHANNELS) {
info("Only up to %d input + %d output channels are enabled. Higher channel counts are disabled.",
MAX_DEVICE_CHANNELS, MAX_DEVICE_CHANNELS);
}
dev->total_num_input_chans = (total_inputs > MAX_DEVICE_CHANNELS) ? MAX_DEVICE_CHANNELS : total_inputs;
dev->total_num_output_chans = (total_outputs > MAX_DEVICE_CHANNELS) ? MAX_DEVICE_CHANNELS : total_outputs;
clear_channel_names(dev->input_channel_names);
clear_channel_names(dev->output_channel_names);
for (int i = 0; i < dev->total_num_input_chans; ++i) {
if (!get_channel_name(dev, dev->input_channel_names, i, true)) {
snprintf(dev->input_channel_names[i], MAX_CH_NAME_LENGTH, "Input %d", i);
}
}
for (int i = 0; i < dev->total_num_output_chans; ++i) {
if (!get_channel_name(dev, dev->output_channel_names, i, false)) {
snprintf(dev->output_channel_names[i], MAX_CH_NAME_LENGTH, "Output %d", i);
}
}
}
bool asio_device_create_dummy_buffers(struct asio_device *dev)
{
if (!dev || !dev->asio) {
return false;
}
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;
debug("Creating dummy buffers: %d channels, size: %d", num_channels, dev->preferred_buffer_size);
if (dev->preferred_buffer_size > 0) {
ASIOError err = ASIO_CreateBuffers(dev, dev->buffer_infos, num_channels, dev->preferred_buffer_size,
&dev->callbacks);
if (err != ASE_OK) {
warn("Dummy buffer creation failed with error %d", err);
} else {
dev->buffers_created = true;
}
}
long new_inputs = 0, new_outputs = 0;
ASIO_GetChannels(dev, &new_inputs, &new_outputs);
if (new_inputs > MAX_DEVICE_CHANNELS || new_outputs > MAX_DEVICE_CHANNELS) {
debug("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;
debug("Detected channels after dummy buffers: %ld input, %ld output", dev->total_num_input_chans,
dev->total_num_output_chans);
asio_device_update_sample_rates_list(dev);
asio_device_reload_channel_names(dev);
for (int i = 0; i < dev->total_num_output_chans; ++i) {
ASIOChannelInfo chinfo = {0};
chinfo.channel = i;
chinfo.isInput = 0;
ASIO_GetChannelInfo(dev, &chinfo);
if (!asio_format_init(&dev->output_format[i], chinfo.type)) {
error("Unsupported ASIO output sample type %ld on channel %d", chinfo.type, i);
return false;
}
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);
}
}
return true;
}
void asio_device_dispose_buffers(struct asio_device *dev)
{
if (!dev) {
return;
}
ASIOError err = ASE_OK;
if (dev->asio != NULL && dev->buffers_created) {
dev->buffers_created = false;
err = ASIO_DisposeBuffers(dev);
}
if (err != ASE_OK) {
info("Device didn't dispose correctly of the buffers; error code %i", err);
}
}
/* Next function exists because of bad drivers which expect some loading sequence which should be unnecessary in
* theory. But following JUCE and for extra safety, we do it anyway 'à la Cubase'. */
void asio_device_test(struct asio_device *dev)
{
if (!dev) {
return;
}
info("opening device: %s", dev->device_name);
os_atomic_set_bool(&dev->need_to_reset, false);
clear_channel_names(dev->input_channel_names);
clear_channel_names(dev->output_channel_names);
da_clear(dev->supported_buffer_sizes);
da_clear(dev->supported_sample_rates);
dev->is_open = false;
dev->total_num_input_chans = 0;
dev->total_num_output_chans = 0;
dev->xruns = 0;
dev->errorstring[0] = '\0';
dev->driver_failure = false;
ASIOError err = 0;
if (asio_device_load_driver(dev)) {
asio_device_init_driver(dev, dev->errorstring);
if (!dev->errorstring[0]) {
dev->total_num_input_chans = 0;
dev->total_num_output_chans = 0;
if (dev->asio != NULL) {
err = ASIO_GetChannels(dev, &dev->total_num_input_chans, &dev->total_num_output_chans);
if (err == ASE_OK) {
info("channels in: %i, channels out: %i", dev->total_num_input_chans,
dev->total_num_output_chans);
if (dev->total_num_input_chans > MAX_DEVICE_CHANNELS ||
dev->total_num_output_chans > MAX_DEVICE_CHANNELS) {
info("Only up to %i input + %i output channels are enabled. Higher channel counts are disabled.",
MAX_DEVICE_CHANNELS, MAX_DEVICE_CHANNELS);
}
dev->total_num_input_chans =
MIN(dev->total_num_input_chans, (long)MAX_DEVICE_CHANNELS);
dev->total_num_output_chans =
MIN(dev->total_num_output_chans, (long)MAX_DEVICE_CHANNELS);
err = asio_device_refresh_buffer_sizes(dev);
if (err == ASE_OK) {
info("buffer sizes: %ld → %ld, preferred: %ld, step: %ld",
dev->min_buffer_size, dev->max_buffer_size,
dev->preferred_buffer_size, dev->buffer_granularity);
double current_rate = asio_device_get_sample_rate(dev);
if (current_rate < 1.0 || current_rate > 192001.0) {
info("setting default sample rate");
err = ASIO_SetSampleRate(dev, 48000.0);
info("force setting sample rate to 48 kHz");
/* sanity check */
current_rate = asio_device_get_sample_rate(dev);
}
dev->current_sample_rate = current_rate;
dev->post_output = ASIO_OutputReady(dev) == ASE_OK;
if (dev->post_output) {
debug("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);
if (!asio_device_create_dummy_buffers(dev)) {
dev->driver_failure = true;
goto fail;
}
asio_device_read_latencies(dev);
err = ASIO_Start(dev);
Sleep(80);
ASIO_Stop(dev);
/* We dispose of buffers here and not in the open function because some
* drivers can't set samplerate while buffers are being prepared.
* (from a discussion with Yamaha).*/
debug("disposing of the dummy buffers");
asio_device_dispose_buffers(dev);
} else {
info("Can't detect buffer sizes");
dev->driver_failure = true;
}
} else {
info("Can't detect asio channels");
dev->driver_failure = true;
}
}
} else {
info("Initialization failure reported by driver:\n %s\nYour device might be used concurrently "
"in another application, but ASIO usually supports a single host.\n"
"Or if you use a USB device, it might be disconnected.",
dev->errorstring);
dev->driver_failure = true;
}
} else {
info("No such device");
dev->driver_failure = true;
}
fail:
if (dev->errorstring[0] != '\0') {
dev->driver_failure = true;
if (!asio_device_remove_current_driver(dev)) {
info("** Driver crashed while being closed");
}
} else {
info("device opened but not yet started");
}
dev->is_open = false;
os_atomic_set_bool(&dev->need_to_reset, false);
}
void asio_device_open(struct asio_device *dev, double sample_rate, long buffer_size_samples)
{
if (!dev) {
blog(LOG_ERROR, "[ASIO] Invalid device handle in open()");
return;
}
if (dev->is_open) {
asio_device_close(dev);
}
DWORD current_thread_id = GetCurrentThreadId();
if (current_thread_id != dev->com_thread_id) {
error("open() called from the wrong thread! Expected COM thread ID: %lu, current: %lu\n",
dev->com_thread_id, current_thread_id);
return;
} else {
debug("open() called from correct COM thread\n");
}
if (dev->asio == NULL) {
asio_device_test(dev);
if (dev->asio == NULL) {
error("[Failed to load driver with error: %s", dev->errorstring);
return;
}
}
dev->errorstring[0] = '\0';
ASIOError err = ASIO_GetChannels(dev, &dev->total_num_input_chans, &dev->total_num_output_chans);
if (dev->total_num_input_chans > MAX_DEVICE_CHANNELS || dev->total_num_output_chans > MAX_DEVICE_CHANNELS) {
info("Only up to %i input + %i output channels are enabled. Higher channel counts are disabled.",
MAX_DEVICE_CHANNELS, MAX_DEVICE_CHANNELS);
}
dev->total_num_input_chans = MIN(dev->total_num_input_chans, (long)MAX_DEVICE_CHANNELS);
dev->total_num_output_chans = MIN(dev->total_num_output_chans, (long)MAX_DEVICE_CHANNELS);
double temptative_rate = sample_rate;
dev->current_sample_rate = sample_rate;
asio_device_update_sample_rates_list(dev);
bool is_listed = false;
for (size_t i = 0; i < dev->supported_sample_rates.num; ++i) {
if (dev->supported_sample_rates.array[i] == sample_rate) {
is_listed = true;
break;
}
}
if (sample_rate == 0.0 || !is_listed) {
temptative_rate = 48000.0;
}
asio_device_update_clock_sources(dev);
dev->current_sample_rate = asio_device_get_sample_rate(dev);
if (!asio_device_set_sample_rate(dev, temptative_rate)) {
goto fail;
}
dev->current_block_size_samples = dev->current_buffer_size =
asio_device_read_buffer_size(dev, buffer_size_samples);
/* a sample rate change might impact the channel count, sigh... */
err = ASIO_GetChannels(dev, &dev->total_num_input_chans, &dev->total_num_output_chans);
if (dev->total_num_input_chans > MAX_DEVICE_CHANNELS || dev->total_num_output_chans > MAX_DEVICE_CHANNELS) {
info("Only up to %d input + %d output channels are enabled. Higher channel counts are disabled.",
MAX_DEVICE_CHANNELS, MAX_DEVICE_CHANNELS);
}
dev->total_num_input_chans = MIN(dev->total_num_input_chans, (long)MAX_DEVICE_CHANNELS);
dev->total_num_output_chans = MIN(dev->total_num_output_chans, (long)MAX_DEVICE_CHANNELS);
if (ASIO_Future(dev, kAsioCanReportOverload, NULL) != ASE_OK) {
dev->xruns = -1;
}
if (os_atomic_load_bool(&dev->need_to_reset)) {
info("Resetting");
if (!asio_device_remove_current_driver(dev)) {
error("** Driver crashed while being closed");
}
if (!asio_device_load_driver(dev)) {
goto fail;
}
char init_error[256] = {0};
if (!asio_device_init_driver(dev, init_error)) {
goto fail;
}
if (init_error[0] != '\0') {
error("ASIOInit: %s", init_error);
goto fail;
} else {
double rate = asio_device_get_sample_rate(dev);
asio_device_set_sample_rate(dev, rate);
}
}
long total_buffers = dev->total_num_input_chans + dev->total_num_output_chans;
asio_device_reset_buffers(dev);
info("creating buffers: %i in-out channels, size: %i samples", total_buffers, dev->current_block_size_samples);
err = ASIO_CreateBuffers(dev, dev->buffer_infos, (long)total_buffers, dev->current_block_size_samples,
&dev->callbacks);
if (err != ASE_OK) {
dev->current_block_size_samples = dev->preferred_buffer_size;
info("createBuffers failed, trying preferred size: %i", dev->current_block_size_samples);
err = ASIO_DisposeBuffers(dev);
err = ASIO_CreateBuffers(dev, dev->buffer_infos, (long)total_buffers, dev->current_block_size_samples,
&dev->callbacks);
if (err != ASE_OK) {
info("createBuffers failed again, when trying preferred size: %i",
dev->current_block_size_samples);
}
}
if (err == ASE_OK) {
dev->buffers_created = true;
info("Buffers created successfully");
bfree(dev->io_buffer_space);
dev->io_buffer_space =
(float *)bzalloc((dev->current_block_size_samples * (total_buffers + 1) + 32) * sizeof(float));
if (!dev->io_buffer_space || !asio_device_init_output_fifos(dev)) {
error("Failed to allocate ASIO output FIFOs");
goto fail;
}
dev->silent_buffers = dev->io_buffer_space + dev->current_block_size_samples * total_buffers;
/* For devices like decklink having input only or output only, default format set to ASIOSTLastEntry */
int input_type = ASIOSTLastEntry;
int output_type = ASIOSTLastEntry;
dev->current_bit_depth = 16;
for (int n = 0; n < dev->total_num_input_chans; ++n) {
dev->in_buffers[n] = dev->io_buffer_space + (dev->current_block_size_samples * n);
ASIOChannelInfo chinfo = {.channel = n, .isInput = ASIOTrue};
ASIO_GetChannelInfo(dev, &chinfo);
if (n == 0) {
input_type = chinfo.type;
}
if (!asio_format_init(&dev->input_format[n], chinfo.type)) {
error("Unsupported ASIO input sample type %ld on channel %d", chinfo.type, n);
goto fail;
}
dev->current_bit_depth = MAX(dev->input_format[n].bit_depth, dev->current_bit_depth);
if (n == 0) {
debug("input sample bitdepth: %i ", dev->current_bit_depth);
}
}
for (int n = 0; n < dev->total_num_output_chans; ++n) {
dev->out_buffers[n] = dev->io_buffer_space +
(dev->current_block_size_samples * (dev->total_num_input_chans + n));
ASIOChannelInfo chinfo = {.channel = n, .isInput = ASIOFalse};
ASIO_GetChannelInfo(dev, &chinfo);
if (n == 0) {
output_type = chinfo.type;
}
if (!asio_format_init(&dev->output_format[n], chinfo.type)) {
error("Unsupported ASIO output sample type %ld on channel %d", chinfo.type, n);
goto fail;
}
dev->current_bit_depth = MAX(dev->output_format[n].bit_depth, dev->current_bit_depth);
if (n == 0) {
debug("output sample bitdepth: %i ", 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);
}
asio_device_read_latencies(dev);
asio_device_refresh_buffer_sizes(dev);
dev->is_open = true;
info("starting");
os_atomic_set_bool(&dev->called_back, false);
os_atomic_set_bool(&dev->is_started, false);
ASIOError err = ASIO_Start(dev);
if (err != ASE_OK) {
dev->is_open = false;
error("stop on failure");
Sleep(10);
ASIO_Stop(dev);
error("Can't start device");
Sleep(10);
goto fail;
}
int count = 300;
while (--count > 0 && !os_atomic_load_bool(&dev->called_back)) {
Sleep(10);
}
if (!os_atomic_load_bool(&dev->called_back)) {
error("Device didn't start correctly. No callbacks - stopping.");
ASIO_Stop(dev);
goto fail;
}
os_atomic_set_bool(&dev->is_started, true);
os_atomic_set_bool(&dev->need_to_reset, false);
return;
}
fail:
asio_device_disable_buffer_processing(dev);
asio_device_dispose_buffers(dev);
Sleep(20);
dev->is_open = false;
asio_device_close(dev);
os_atomic_set_bool(&dev->need_to_reset, false);
}
void asio_device_close(struct asio_device *dev)
{
if (!dev) {
return;
}
if (dev->obs_output_client) {
obs_output_t *output = dev->obs_output_client->output;
if (output) {
os_atomic_set_bool(&dev->capture_started, false);
os_atomic_set_bool(&dev->obs_output_client->stopping, true);
if (obs_output_active(output)) {
obs_output_stop(output);
for (int count = 300; count > 0; --count) {
if (!obs_output_active(output)) {
break;
}
Sleep(10);
}
if (obs_output_active(output)) {
error("Timed out stopping output");
return;
}
}
}
}
if (!dev->asio) {
return;
}
if (dev->is_open) {
asio_device_disable_buffer_processing(dev);
dev->is_open = false;
info(" asio driver stopping");
Sleep(20);
ASIO_Stop(dev);
Sleep(10);
asio_device_dispose_buffers(dev);
Sleep(10);
}
}
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 == kAsioOverload) {
return 1;
}
break;
case kAsioBufferSizeChange:
info("kAsioBufferSizeChange; new buffer is %ld", value);
return 0; /* 0 tells the driver to request a reset to the host */
case kAsioResetRequest:
info("kAsioResetRequest");
asio_device_reset_request(dev);
return 1;
case kAsioResyncRequest:
info("kAsioResyncRequest");
asio_device_reset_request(dev);
return 1;
case kAsioLatenciesChanged:
info("kAsioLatenciesChanged");
return 1;
case kAsioEngineVersion:
return 2;
case kAsioSupportsTimeInfo:
info("Driver queried time-info support: unsupported (returning 0)");
return 0;
case kAsioSupportsTimeCode:
return 0;
case kAsioOverload:
dev->xruns++;
return 1;
}
return 0;
}
void asio_device_process_buffer(struct asio_device *dev, long buffer_index)
{
if (!dev || 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;
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(),
};
for (int idx = 0; idx < MAX_ASIO_CLIENTS; ++idx) {
struct asio_data *client = dev->obs_clients[idx];
if (!client || !client->device_name || !os_atomic_load_bool(&client->active)) {
continue;
}
for (int j = 0; j < output_channels; ++j) {
int mix_idx = client->mix_channels[j];
out.data[j] = (mix_idx >= 0 && !os_atomic_load_bool(&client->stopping))
? (uint8_t *)dev->in_buffers[mix_idx]
: (uint8_t *)dev->silent_buffers;
}
if (!os_atomic_load_bool(&client->stopping) && client->source && os_atomic_load_bool(&client->active)) {
obs_source_output_audio(client->source, &out);
}
}
if (os_atomic_load_bool(&dev->output_fifos_clear_requested)) {
for (int channel = 0; channel < dev->total_num_output_chans; ++channel) {
spsc_fifo_clear(&dev->output_frames[channel]);
}
os_atomic_set_bool(&dev->output_fifos_clear_requested, false);
}
for (int outchan = 0; outchan < dev->total_num_output_chans; ++outchan) {
void *dst = infos[dev->total_num_input_chans + outchan].buffers[buffer_index];
bool play = os_atomic_load_bool(&dev->capture_started) &&
dev->output_routes[outchan].channel_index >= 0 &&
dev->output_routes[outchan].mix_index >= 0;
if (play) {
play = spsc_fifo_read(&dev->output_frames[outchan], dev->out_buffers[outchan], samps) > 0;
}
if (play) {
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);
}
}
if (dev->post_output) {
ASIO_OutputReady(dev);
}
}
void asio_device_callback(struct asio_device *dev, long index)
{
if (!dev) {
return;
}
if (index >= 0 && os_atomic_load_bool(&dev->is_started)) {
os_atomic_inc_long(&dev->buffer_callbacks_in_progress);
if (os_atomic_load_bool(&dev->is_started)) {
if (os_atomic_load_bool(&shutting_down_atomic)) {
os_atomic_set_bool(&dev->capture_started, false);
os_atomic_set_bool(&dev->is_started, false);
os_event_signal(shutting_down);
} else {
asio_device_process_buffer(dev, index);
}
}
os_atomic_dec_long(&dev->buffer_callbacks_in_progress);
} else {
if (dev->post_output && dev->asio) {
ASIO_OutputReady(dev);
}
}
os_atomic_set_bool(&dev->called_back, true);
}