From dc0177589e921d2b561c6492e653c08b93294a4f Mon Sep 17 00:00:00 2001 From: pkv Date: Sat, 7 Sep 2019 18:26:30 +0200 Subject: [PATCH] Juce plugin --- CMakeLists.txt | 79 +- src/_clang-format | 54 + src/asio-input.cpp | 2071 +++++++++++----------------------- src/asio-input.qrc | 6 + src/data/locale/en-US.ini | 18 +- src/images/asiologo.png | Bin 0 -> 14348 bytes src/images/btn_donate_SM.gif | Bin 0 -> 1447 bytes 7 files changed, 746 insertions(+), 1482 deletions(-) create mode 100644 src/_clang-format create mode 100644 src/asio-input.qrc create mode 100644 src/images/asiologo.png create mode 100644 src/images/btn_donate_SM.gif diff --git a/CMakeLists.txt b/CMakeLists.txt index 23cc387..d74e72b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,17 +3,25 @@ cmake_minimum_required(VERSION 3.5) project(obs-asio) # in cmake gui set the following: -# BASS_ASIO_LIB == path of bassasio.lib -# BASS_ASIO_INCLUDE_DIR == path of bassasio.h -# put also bassasio.dll in obs-plugins/bin folder -# be careful there are two versions of the lib and dll (x86 and x64) +# LIBOBS_INCLUDE_DIR = path to obs.h, obs-studio/libobs +# LIBOBS_LIB = filepath to obs.lib, ex: obs-studio/build/libobs/RelWithDebInfo/obs.lib +# OBS_FRONTEND_LIB = filepath to obs-frontend-api.lib , ex: obs-studio/build/UI/obs-frontend-api/RelWithDebInfo/obs-frontend-api.lib +# QTDIR = path to QT , ex: I:/Qt/5.9/msvc2015_64 for x64 +# JUCE_LIBRARY == path of juce.lib +# JUCE_LIBRARY_DEBUG == path of juce_debug.lib (debug version of the lib) +# The Juce lib can be created with ProJucer with either a static lib project or a dynamic lib project. +# JUCE_INCLUDE_DIR == path of Juce includes; if you used ProJucer, this will be the path of JuceLibraryCode. +# If you select to build with a juce dll, you'll have to copy it in the obs-plugins folder. ########################################## # find libobs # ########################################## include(external/FindLibobs.cmake) find_package(Libobs REQUIRED) - +if(NOT DEFINED OBS_FRONTEND_LIB) + set(OBS_FRONTEND_LIB "OBS_FRONTEND_LIB-NOTFOUND" CACHE FILEPATH "OBS frontend library") + message(FATAL_ERROR "Could not find OBS Frontend API\'s library !") +endif() ########################################## # set architecture # ########################################## @@ -35,37 +43,51 @@ endif() ########################################## # QT support # ########################################## +set(CMAKE_PREFIX_PATH "${QTDIR}") # Find includes in corresponding build directories -#set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) # Instruct CMake to run moc automatically when needed. -#set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +find_package(Qt5Core REQUIRED) # Find the QtWidgets library -#find_package(Qt5Widgets) +find_package(Qt5Widgets) include_directories( -# SYSTEM "${CMAKE_SOURCE_DIR}/libobs" - ${BASS_ASIO_INCLUDE_DIR} + SYSTEM "${LIBOBS_INCLUDE_DIR}" + "${LIBOBS_INCLUDE_DIR}/../UI/obs-frontend-api" + "${LIBOBS_INCLUDE_DIR}/../${OBS_BUILDDIR_ARCH}/UI" + ${JUCE_INCLUDE_DIR} + ${JUCE_INCLUDE_DIR}/modules + ${Qt5Core_INCLUDES} + ${Qt5Widgets_INCLUDES} +) ) -set(obs-asio-sdk_HEADERS - ) - -set(obs-asio_HEADERS - ) +set(win-asio_QRC + asio-input.qrc) + set(obs-asio_SOURCES src/asio-input.cpp ) -add_library(obs-asio MODULE - ${obs-asio_SOURCES} - ${obs-asio_HEADERS} - ${obs-asio-sdk_HEADERS} +set(JUCE_LIB optimized ${JUCE_LIBRARY} debug ${JUCE_LIBRARY_DEBUG}) + +qt5_add_resources(win-asio_QRC_SOURCES ${win-asio_QRC}) + +add_library(win-asio MODULE + ${win-asio_SOURCES} + ${win-asio_QRC_SOURCES} ) -target_link_libraries(obs-asio + +target_link_libraries(win-asio libobs - ${BASS_ASIO_LIB} + obs-frontend-api + ${JUCE_LIB} + Qt5::Core + Qt5::Widgets ) #install_obs_plugin_with_data(win-asio data) ==> internal plugin install @@ -88,18 +110,3 @@ add_custom_command(TARGET obs-asio POST_BUILD "$" "${RELEASE_DIR}/obs-plugins/${OBS_ARCH_NAME}") -#Copy to obs-studio dev environment for immediate testing - COMMAND if $==1 ( - "${CMAKE_COMMAND}" -E copy - "$" - "${LIBOBS_INCLUDE_DIR}/../${OBS_BUILDDIR_ARCH}/rundir/$/obs-plugins/${OBS_ARCH_NAME}") - - COMMAND if $==1 ( - "${CMAKE_COMMAND}" -E make_directory - "${LIBOBS_INCLUDE_DIR}/../${OBS_BUILDDIR_ARCH}/rundir/$/data/obs-plugins/obs-asio") - - COMMAND if $==1 ( - "${CMAKE_COMMAND}" -E copy_directory - "${PROJECT_SOURCE_DIR}/data" - "${LIBOBS_INCLUDE_DIR}/../${OBS_BUILDDIR_ARCH}/rundir/$/data/obs-plugins/obs-asio") - ) \ No newline at end of file diff --git a/src/_clang-format b/src/_clang-format new file mode 100644 index 0000000..c21eb86 --- /dev/null +++ b/src/_clang-format @@ -0,0 +1,54 @@ +Language: Cpp +Standard: Cpp11 +AccessModifierOffset: -8 +AlignAfterOpenBracket: DontAlign +AlignConsecutiveAssignments: true +AlignConsecutiveDeclarations: true +AlignEscapedNewlinesLeft: DontAlign +AlignOperands: false +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterFunction: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakStringLiterals: true +ColumnLimit: 120 +ContinuationIndentWidth: 16 +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 8 +Cpp11BracedListStyle: true +DerivePointerAlignment: true +IndentCaseLabels: false +IndentWidth: 8 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: true +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PointerAlignment: Right +ReflowComments: true +SortIncludes: false +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 8 +UseTab: ForContinuationAndIndentation diff --git a/src/asio-input.cpp b/src/asio-input.cpp index 2002374..6215126 100644 --- a/src/asio-input.cpp +++ b/src/asio-input.cpp @@ -1,7 +1,6 @@ /* -Copyright (C) 2017 by pkv , andersama - -Based on Pulse Input plugin by Leonhard Oelke. +Copyright (C) 2019 by andersama +and pkv . 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 @@ -16,1072 +15,703 @@ 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 + +/* For full GPL v2 compatibility it is required to build libs with + * our open source sdk instead of steinberg sdk , see our fork: + * https://github.com/pkviet/portaudio , branch : openasio + * If you build with original asio sdk, you are free to do so to the + * extent that you do not distribute your binaries. + */ #include #include -#include -#include #include +#include #include -#include -#include -#include -#include -#include -#include -#include -#include - -//#include "RtAudio.h" - -//#include -// -//#include -//#include -//#include -//#include -//#include -//#include -//#include -//#include - +#include +#include +#include +#include +#include +#include +#include OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE("win-asio", "en-US") #define blog(level, msg, ...) blog(level, "asio-input: " msg, ##__VA_ARGS__) -#define NSEC_PER_SEC 1000000000LL +static void fill_out_devices(obs_property_t *prop); -#define TEXT_BUFFER_SIZE obs_module_text("BufferSize") -#define TEXT_BUFFER_64_SAMPLES obs_module_text("64_samples") -#define TEXT_BUFFER_128_SAMPLES obs_module_text("128_samples") -#define TEXT_BUFFER_256_SAMPLES obs_module_text("256_samples") -#define TEXT_BUFFER_512_SAMPLES obs_module_text("512_samples") -#define TEXT_BUFFER_1024_SAMPLES obs_module_text("1024_samples") -#define TEXT_BITDEPTH obs_module_text("BitDepth") +static juce::AudioIODeviceType *deviceTypeAsio = AudioIODeviceType::createAudioIODeviceType_ASIO(); -/* ======================================================================= */ -/* conversion between BASS_ASIO and obs */ +class ASIOPlugin; +class AudioCB; -enum audio_format asio_to_obs_audio_format(DWORD format) -{ - switch (format) { - case BASS_ASIO_FORMAT_16BIT: return AUDIO_FORMAT_16BIT; - case BASS_ASIO_FORMAT_32BIT: return AUDIO_FORMAT_32BIT; - case BASS_ASIO_FORMAT_FLOAT: return AUDIO_FORMAT_FLOAT; - default: break; - } - - return AUDIO_FORMAT_UNKNOWN; -} - -int bytedepth_format(audio_format format) -{ - return (int)get_audio_bytes_per_channel(format); -} +static bool asio_device_changed(void *vptr, obs_properties_t *props, obs_property_t *list, obs_data_t *settings); +static bool asio_layout_changed(obs_properties_t *props, obs_property_t *list, obs_data_t *settings); +static bool fill_out_channels_modified(obs_properties_t *props, obs_property_t *list, obs_data_t *settings); -int bytedepth_format(DWORD format) { - return bytedepth_format(asio_to_obs_audio_format(format)); -} +static std::vector callbacks; -DWORD obs_to_asio_audio_format(audio_format format) +enum audio_format string_to_obs_audio_format(std::string format) { - switch (format) { - - case AUDIO_FORMAT_16BIT: - return BASS_ASIO_FORMAT_16BIT; - // obs doesn't have 24 bit - case AUDIO_FORMAT_32BIT: - return BASS_ASIO_FORMAT_32BIT; - - case AUDIO_FORMAT_FLOAT: - default: - return BASS_ASIO_FORMAT_FLOAT; + if (format == "32 Bit Int") { + return AUDIO_FORMAT_32BIT; + } else if (format == "32 Bit Float") { + return AUDIO_FORMAT_FLOAT; + } else if (format == "16 Bit Int") { + return AUDIO_FORMAT_16BIT; } - // default to 32 float samples for best quality + return AUDIO_FORMAT_UNKNOWN; } -enum speaker_layout asio_channels_to_obs_speakers(unsigned int channels) +// returns corresponding planar format on entering some interleaved one +enum audio_format get_planar_format(audio_format format) { - switch (channels) { - case 1: return SPEAKERS_MONO; - case 2: return SPEAKERS_STEREO; - case 3: return SPEAKERS_2POINT1; - case 4: return SPEAKERS_4POINT0; - case 5: return SPEAKERS_4POINT1; - case 6: return SPEAKERS_5POINT1; - /* no layout for 7 channels */ - case 8: return SPEAKERS_7POINT1; - } - return SPEAKERS_UNKNOWN; -} - -/* ======================================================================= */ -/* asio structs and classes */ - -struct asio_source_audio { - uint8_t *data[MAX_AUDIO_CHANNELS]; - uint32_t frames; - - //enum speaker_layout speakers; - volatile long speakers; - enum audio_format format; - uint32_t samples_per_sec; - - uint64_t timestamp; -}; + if (is_audio_planar(format)) + return format; -audio_format get_planar_format(audio_format format) { switch (format) { case AUDIO_FORMAT_U8BIT: return AUDIO_FORMAT_U8BIT_PLANAR; - case AUDIO_FORMAT_16BIT: return AUDIO_FORMAT_16BIT_PLANAR; - case AUDIO_FORMAT_32BIT: return AUDIO_FORMAT_32BIT_PLANAR; - case AUDIO_FORMAT_FLOAT: return AUDIO_FORMAT_FLOAT_PLANAR; + // should NEVER get here + default: + return AUDIO_FORMAT_UNKNOWN; } - - return format; } -audio_format get_interleaved_format(audio_format format) { - switch (format) { - case AUDIO_FORMAT_U8BIT_PLANAR: - return AUDIO_FORMAT_U8BIT; - - case AUDIO_FORMAT_16BIT_PLANAR: - return AUDIO_FORMAT_16BIT; - - case AUDIO_FORMAT_32BIT_PLANAR: - return AUDIO_FORMAT_32BIT; - - case AUDIO_FORMAT_FLOAT_PLANAR: - return AUDIO_FORMAT_FLOAT; - } - - return format; +// returns the size in bytes of a sample from an obs audio_format +int bytedepth_format(audio_format format) +{ + return (int)get_audio_bytes_per_channel(format); } -int bytedepth_format(audio_format format); +// get number of output channels (this is set in obs general audio settings +int get_obs_output_channels() +{ + // get channel number from output speaker layout set by obs + struct obs_audio_info aoi; + obs_get_audio_info(&aoi); + return (int)get_audio_channels(aoi.speakers); +} -#define CAPTURE_INTERVAL INFINITE -struct device_source_audio { - uint8_t **data; - uint32_t frames; - long input_chs; - enum audio_format format; - uint32_t samples_per_sec; - uint64_t timestamp; +int get_max_obs_channels() +{ + static int channels = 0; + if (channels > 0) { + return channels; + } else { + for (int i = 0; i < 1024; i++) { + int c = get_audio_channels((speaker_layout)i); + if (c > channels) + channels = c; + } + return channels; + } +} + +static std::vector known_layouts = { + SPEAKERS_MONO, /**< Channels: MONO */ + SPEAKERS_STEREO, /**< Channels: FL, FR */ + SPEAKERS_2POINT1, /**< Channels: FL, FR, LFE */ + SPEAKERS_4POINT0, /**< Channels: FL, FR, FC, RC */ + SPEAKERS_4POINT1, /**< Channels: FL, FR, FC, LFE, RC */ + SPEAKERS_5POINT1, /**< Channels: FL, FR, FC, LFE, RL, RR */ + SPEAKERS_7POINT1, /**< Channels: FL, FR, FC, LFE, RL, RR, SL, SR */ }; -class device_data; -class asio_data; - -struct listener_pair { - asio_data *asio_listener; - device_data *device; -}; +static std::vector known_layouts_str = {"Mono", "Stereo", "2.1", "4.0", "4.1", "5.1", "7.1"}; -class asio_data { +class AudioCB : public juce::AudioIODeviceCallback { private: - uint8_t* silent_buffer = NULL; - size_t silent_buffer_size = 0; + AudioIODevice * _device = nullptr; + char * _name = nullptr; + int _write_index = 0; + double sample_rate; + TimeSliceThread *_thread = nullptr; + public: - CRITICAL_SECTION settings_mutex; - - obs_source_t *source; - - /*asio device and info */ - const char *device; - uint8_t device_index; - - uint64_t first_ts; //first timestamp - /* channels info */ - DWORD input_channels; //total number of input channels - DWORD output_channels; // number of output channels of device (not used) - DWORD recorded_channels; // number of channels passed from device (including muted) to OBS; is at most 8 - long route[MAX_AUDIO_CHANNELS]; // stores the channel re-ordering info - - std::vector unmuted_chs; - std::vector muted_chs; - std::vector required_signals; - - //signals - WinHandle stop_listening_signal; - - //WinHandle reconnectThread; - WinHandle captureThread; - - bool isASIOActive = false; - bool reconnecting = false; - bool previouslyFailed = false; - bool useDeviceTiming = false; - - std::string get_id() { - const void * address = static_cast(source); - std::stringstream ss; - ss << "0x" << std::uppercase << (int)address; - std::string name = ss.str(); - return name; - //const char * format_id = "0x%x"; - //size_t pad = sizeof(obs_source_t *) * 2; - //char * id_char = (char*)calloc(strlen(format_id) + pad + 1, sizeof(char)); - //sprintf(id_char, format_id, source); - //std::string name = id_char; - //free(id_char); - //return name.c_str(); - } + struct AudioBufferInfo { + AudioBuffer buffer; + obs_source_audio out; + }; - asio_data() : source(NULL), first_ts(0), device_index(-1) { - InitializeCriticalSection(&settings_mutex); + int write_index() + { + return _write_index; + } - memset(&route[0], -1, sizeof(long) * 8); +private: + std::vector buffers; - stop_listening_signal = CreateEvent(nullptr, true, false, nullptr); - } +public: + class AudioListener : public TimeSliceClient { + private: + std::vector _route; + obs_source_audio in; + obs_source_t * source; + + bool active; + int read_index = 0; + int wait_time = 4; + AudioCB *callback; + AudioCB *current_callback; + + size_t silent_buffer_size = 0; + uint8_t *silent_buffer = nullptr; + + bool set_data(AudioBufferInfo *info, obs_source_audio &out, const std::vector &route, + int *sample_rate) + { + out.speakers = in.speakers; + out.samples_per_sec = info->out.samples_per_sec; + out.format = AUDIO_FORMAT_FLOAT_PLANAR; + out.timestamp = info->out.timestamp; + out.frames = info->buffer.getNumSamples(); + + *sample_rate = out.samples_per_sec; + // cache a silent buffer + size_t buffer_size = (out.frames * sizeof(bytedepth_format(out.format))); + if (silent_buffer_size < buffer_size) { + if (silent_buffer) + bfree(silent_buffer); + silent_buffer = (uint8_t *)bzalloc(buffer_size); + silent_buffer_size = buffer_size; + } + + int ichs = info->buffer.getNumChannels(); + int ochs = get_audio_channels(out.speakers); + uint8_t **data = (uint8_t **)info->buffer.getArrayOfWritePointers(); + + bool muted = true; + for (int i = 0; i < ochs; i++) { + if (route[i] >= 0 && route[i] < ichs) { + out.data[i] = data[route[i]]; + muted = false; + } else { + out.data[i] = silent_buffer; + } + } + return !muted; + } - ~asio_data() { - DeleteCriticalSection(&settings_mutex); - if (silent_buffer) { - free(silent_buffer); + public: + AudioListener(obs_source_t *source, AudioCB *cb) : source(source), callback(cb) + { + active = true; } - } - bool disconnect() { - isASIOActive = false; - SetEvent(stop_listening_signal); - if (captureThread.Valid()) { - WaitForSingleObject(captureThread, INFINITE); - //CloseHandle(captureThread); + ~AudioListener() + { + if (silent_buffer) + bfree(silent_buffer); + disconnect(); } - ResetEvent(stop_listening_signal); - return true; - } - bool render_audio(device_source_audio *asio_buffer, long route[]) { + void disconnect() + { + active = false; + } - struct obs_audio_info aoi; - obs_get_audio_info(&aoi); - int index = BASS_ASIO_GetDevice(); - //blog(LOG_INFO, "dv index in render_audio is %i", index); - obs_source_audio out; - out.format = asio_buffer->format; - if (!is_audio_planar(out.format)) { - blog(LOG_ERROR, "that was a goof %i should be %i", out.format, get_planar_format(out.format)); - return false; + void reconnect() + { + active = true; } - if (out.format == AUDIO_FORMAT_UNKNOWN) { - blog(LOG_INFO, "unknown format"); - return false; + + void setOutput(obs_source_audio o) + { + in.format = o.format; + in.samples_per_sec = o.samples_per_sec; + in.speakers = o.speakers; } - out.frames = asio_buffer->frames; - out.samples_per_sec = asio_buffer->samples_per_sec; - out.timestamp = asio_buffer->timestamp; - if (!first_ts) { - first_ts = out.timestamp; - blog(LOG_INFO, "first timestamp"); - return false; + void setCurrentCallback(AudioCB *cb) + { + current_callback = cb; } - //cache a silent buffer - size_t buffer_size = (out.frames * sizeof(bytedepth_format(out.format))); - if (silent_buffer_size < buffer_size) { - if (silent_buffer) { - free(silent_buffer); - } - silent_buffer = (uint8_t*)calloc(buffer_size, sizeof(uint8_t)); - silent_buffer_size = buffer_size; - blog(LOG_INFO, "caching silent buffer"); + + void setCallback(AudioCB *cb) + { + callback = cb; } - if (unmuted_chs.size() == 0) { - blog(LOG_INFO, "all chs muted"); - return 0; + void setReadIndex(int idx) + { + read_index = idx; } - for (short i = 0; i < aoi.speakers; i++) { - if (route[i] >= 0 && route[i] < asio_buffer->input_chs) { - out.data[i] = asio_buffer->data[route[i]]; - } - else if (route[i] == -1) { - out.data[i] = silent_buffer; - } - else { - out.data[i] = silent_buffer; - } + void setRoute(std::vector route) + { + _route = route; } - out.speakers = aoi.speakers; + AudioCB *getCallback() + { + return callback; + } - obs_source_output_audio(source, &out); - //blog(LOG_DEBUG, "output frames %lu", buffer_size); - return true; - } + int useTimeSlice() + { + if (!active || callback != current_callback) + return -1; + int write_index = callback->write_index(); + if (read_index == write_index) + return wait_time; - static std::vector _get_muted_chs(long route_array[]) { - std::vector silent_chs; - silent_chs.reserve(MAX_AUDIO_CHANNELS); - for (short i = 0; i < MAX_AUDIO_CHANNELS; i++) { - if (route_array[i] == -1) { - silent_chs.push_back(i); - } - } - return silent_chs; - } + std::vector route = _route; + int sample_rate = 0; + int max_sample_rate = 1; + int m = (int)callback->buffers.size(); - static std::vector _get_unmuted_chs(long route_array[]) { - std::vector unmuted_chs; - unmuted_chs.reserve(MAX_AUDIO_CHANNELS); - for (short i = 0; i < MAX_AUDIO_CHANNELS; i++) { - if (route_array[i] >= 0) { - unmuted_chs.push_back(i); + while (read_index != write_index) { + obs_source_audio out; + bool unmuted = set_data(&callback->buffers[read_index], out, route, &sample_rate); + if (unmuted && out.speakers) + obs_source_output_audio(source, &out); + if (sample_rate > max_sample_rate) + max_sample_rate = sample_rate; + read_index = (read_index + 1) % m; } + wait_time = ((1000 / 2) * AUDIO_OUTPUT_FRAMES) / max_sample_rate; + return wait_time; } - return unmuted_chs; - } + }; -}; - -class device_data { -private: - size_t write_index; - size_t buffer_count; - - size_t buffer_size; - uint32_t frames; - long input_chs; - audio_format format; - //not in use... - WinHandle *receive_signals; - //create a square tick signal w/ two events - WinHandle all_recieved_signal; - WinHandle all_recieved_signal_2; - //to close out the device - WinHandle stop_listening_signal; - //tell listeners to to reinit - //WinHandle wait_for_reset_signal; - - bool all_prepped = false; - bool buffer_prepped = false; - bool circle_buffer_prepped = false; - bool reallocate_buffer = false; - bool events_prepped = false; - - circlebuf audio_buffer; -public: - uint32_t samples_per_sec; - - const WinHandle * get_handles() { - return receive_signals; + AudioIODevice *getDevice() + { + return _device; } - WinHandle on_buffer() { - return all_recieved_signal; + const char *getName() + { + return _name; } - long get_input_channels() { - return input_chs; + void setDevice(AudioIODevice *device, const char *name) + { + _device = device; + if (_name) + bfree(_name); + _name = bstrdup(name); } - long device_index; - BASS_ASIO_DEVICEINFO device_info; - - device_source_audio* get_writeable_source_audio() { - return (device_source_audio*)circlebuf_data(&audio_buffer, write_index * sizeof(device_source_audio)); + AudioCB(AudioIODevice *device, const char *name) + { + _device = device; + _name = bstrdup(name); } - device_source_audio* get_source_audio(size_t index) { - return (device_source_audio*)circlebuf_data(&audio_buffer, index * sizeof(device_source_audio)); + ~AudioCB() + { + bfree(_name); } - device_data() { - all_prepped = false; - buffer_prepped = false; - circle_buffer_prepped = false; - reallocate_buffer = false; - events_prepped = false; + void audioDeviceIOCallback(const float **inputChannelData, int numInputChannels, float **outputChannelData, + int numOutputChannels, int numSamples) + { + uint64_t ts = os_gettime_ns(); - format = AUDIO_FORMAT_UNKNOWN; - write_index = 0; - buffer_count = 32; + for (int i = 0; i < numInputChannels; i++) + buffers[_write_index].buffer.copyFrom(i, 0, inputChannelData[i], numSamples); + buffers[_write_index].out.timestamp = ts; + buffers[_write_index].out.frames = numSamples; + buffers[_write_index].out.samples_per_sec = (uint32_t)sample_rate; + _write_index = (_write_index + 1) % buffers.size(); - all_recieved_signal = CreateEvent(nullptr, true, false, nullptr); - all_recieved_signal_2 = CreateEvent(nullptr, true, true, nullptr); - stop_listening_signal = CreateEvent(nullptr, true, false, nullptr); + UNUSED_PARAMETER(numOutputChannels); + UNUSED_PARAMETER(outputChannelData); } - device_data(size_t buffers, audio_format audioformat) { - all_prepped = false; - buffer_prepped = false; - circle_buffer_prepped = false; - reallocate_buffer = false; - events_prepped = false; + void add_client(AudioListener *client) + { + if (!_thread) + _thread = new TimeSliceThread(""); - format = audioformat; - write_index = 0; - buffer_count = buffers ? buffers : 32; - - all_recieved_signal = CreateEvent(nullptr, true, false, nullptr); - all_recieved_signal_2 = CreateEvent(nullptr, true, true, nullptr); - stop_listening_signal = CreateEvent(nullptr, true, false, nullptr); + client->setCurrentCallback(this); + client->setReadIndex(_write_index); + _thread->addTimeSliceClient(client); } - ~device_data() { - //free resources? - if (all_prepped) { - delete receive_signals; - for (int i = 0; i < buffer_count; i++) { - device_source_audio* _source_audio = get_source_audio(i); - int input_chs = _source_audio->input_chs; - for (int j = 0; j < input_chs; j++) { - if (_source_audio->data[j]) { - bfree(_source_audio->data[j]); - } - } - bfree(_source_audio->data); - } - circlebuf_free(&audio_buffer); - } - + void remove_client(AudioListener *client) + { + if (_thread) + _thread->removeTimeSliceClient(client); } - //check that all the required device settings have been set - void check_all() { - if (buffer_prepped && circle_buffer_prepped && events_prepped) { - all_prepped = true; + void audioDeviceAboutToStart(juce::AudioIODevice *device) + { + blog(LOG_INFO, "Starting (%s)", device->getName().toStdString().c_str()); + juce::String name = device->getName(); + sample_rate = device->getCurrentSampleRate(); + int buf_size = device->getCurrentBufferSizeSamples(); + int target_size = AUDIO_OUTPUT_FRAMES * 2; + int count = std::max(8, target_size / buf_size); + int ch_count = device->getActiveInputChannels().countNumberOfSetBits(); + + if (buffers.size() < count) + buffers.reserve(count); + int i = 0; + for (; i < buffers.size(); i++) { + buffers[i].buffer = AudioBuffer(ch_count, buf_size); + buffers[i].out.format = AUDIO_FORMAT_FLOAT_PLANAR; + buffers[i].out.samples_per_sec = sample_rate; } - else { - all_prepped = false; + for (; i < count; i++) { + AudioBufferInfo inf; + inf.buffer = AudioBuffer(ch_count, buf_size); + inf.out.format = AUDIO_FORMAT_FLOAT_PLANAR; + inf.out.samples_per_sec = sample_rate; + buffers.push_back(inf); } - } - - void prep_circle_buffer(BASS_ASIO_INFO &info) { - prep_circle_buffer(info.bufpref); - } - void prep_circle_buffer(DWORD bufpref) { - if (!circle_buffer_prepped) { - //create a buffer w/ a minimum of 4 slots and a target of a fraction of 2048 samples - buffer_count = max(4, ceil(2048 / bufpref)); - circlebuf_init(&audio_buffer); - circlebuf_reserve(&audio_buffer, buffer_count * sizeof(device_source_audio)); - for (int i = 0; i < buffer_count; i++) { - circlebuf_push_back(&audio_buffer, &device_source_audio(), sizeof(device_source_audio)); - //initialize # of buffers + if (!_thread) { + _thread = new TimeSliceThread(name); + _thread->startThread(); + } else { + for (int i = 0; i < _thread->getNumClients(); i++) { + AudioListener *l = static_cast(_thread->getClient(i)); + l->setCurrentCallback(this); } - circle_buffer_prepped = true; + _thread->setCurrentThreadName(name); + if (!_thread->isThreadRunning()) + _thread->startThread(); } } - void prep_events(BASS_ASIO_INFO &info) { - prep_events(info.inputs); + void audioDeviceStopped() + { + blog(LOG_INFO, "Stopped (%s)", _device->getName().toStdString().c_str()); } - void prep_events(long input_chs) { - if (!events_prepped) { - receive_signals = (WinHandle*)calloc(input_chs, sizeof(WinHandle)); - for (int i = 0; i < input_chs; i++) { - receive_signals[i] = CreateEvent(nullptr, true, false, nullptr); - } - events_prepped = true; - } + void audioDeviceError(const juce::String &errorMessage) + { + if (_thread) + _thread->stopThread(200); + std::string error = errorMessage.toStdString(); + blog(LOG_ERROR, "Device Error!\n%s", error.c_str()); } +}; - void re_prep_buffers() { - all_prepped = false; - buffer_prepped = false; - BASS_ASIO_INFO info; - bool ret = BASS_ASIO_GetInfo(&info); - prep_buffers(info, format, samples_per_sec); - } - - void re_prep_buffers(BASS_ASIO_INFO &info) { - all_prepped = false; - prep_buffers(info, format, samples_per_sec); - } +static bool show_panel(obs_properties_t *props, obs_property_t *property, void *data); - void update_sample_rate(uint32_t in_samples_per_sec) { - all_prepped = false; - this->samples_per_sec = in_samples_per_sec; - check_all(); - } +class ASIOPlugin { +private: + AudioIODevice * _device = nullptr; + AudioCB::AudioListener *_listener = nullptr; + std::vector _route; + speaker_layout _speakers; - void prep_buffers(BASS_ASIO_INFO &info, audio_format in_format, uint32_t in_samples_per_sec) { - prep_buffers(info.bufpref, info.inputs, in_format, in_samples_per_sec); +public: + AudioIODevice *getDevice() + { + return _device; + } + + ASIOPlugin::ASIOPlugin(obs_data_t *settings, obs_source_t *source) + { + UNUSED_PARAMETER(settings); + _listener = new AudioCB::AudioListener(source, nullptr); + } + + ASIOPlugin::~ASIOPlugin() + { + AudioCB *cb = _listener->getCallback(); + _listener->disconnect(); + if (cb) + cb->remove_client(_listener); + delete _listener; + } + + static void *Create(obs_data_t *settings, obs_source_t *source) + { + ASIOPlugin *plugin = new ASIOPlugin(settings, source); + plugin->update(settings); + return plugin; + } + + static void Destroy(void *vptr) + { + ASIOPlugin *plugin = static_cast(vptr); + delete plugin; + plugin = nullptr; + } + + // version of plugin + static bool credits(obs_properties_t *props, obs_property_t *property, void *data) + { + UNUSED_PARAMETER(props); + UNUSED_PARAMETER(property); + UNUSED_PARAMETER(data); + QMainWindow *main_window = (QMainWindow *)obs_frontend_get_main_window(); + QMessageBox mybox(main_window); + QString text = "v.2.0.0\r\n © 2019, license GPL v3\r\n" + "Based on Juce library\r\n\r\n" + "Authors:\r\n" + "Andersama (main author) & pkv\r\n"; + mybox.setText(text); + mybox.setIconPixmap(QPixmap(":/res/images/asiologo.png")); + mybox.setWindowTitle(QString("Credits: obs-asio")); + mybox.exec(); + return true; } - void prep_buffers(uint32_t frames, long in_chs, audio_format format, uint32_t samples_per_sec) { - if (frames * bytedepth_format(format) > this->buffer_size) { - if (buffer_prepped) { - reallocate_buffer = true; - } - } - else { - reallocate_buffer = false; - } - prep_events(in_chs); - if (circle_buffer_prepped && (!buffer_prepped || reallocate_buffer)) { - this->frames = frames; - this->input_chs = in_chs; - this->format = format; - this->samples_per_sec = samples_per_sec; - this->buffer_size = frames * bytedepth_format(format); - - for (int i = 0; i < buffer_count; i++) { - device_source_audio* _source_audio = get_source_audio(i); - _source_audio->data = (uint8_t **)bzalloc(input_chs * sizeof(uint8_t*))/*calloc(input_chs, sizeof(uint8_t*))*/; - for (int j = 0; j < input_chs; j++) { - if (!buffer_prepped) { - _source_audio->data[j] = (uint8_t*)bzalloc(buffer_size)/*calloc(buffer_size, 1)*/; - } - else if (reallocate_buffer) { - uint8_t* tmp = (uint8_t*)realloc(_source_audio->data[j], buffer_size); - if (tmp == NULL) { - buffer_prepped = false; - all_prepped = false; - return; - } - else if (tmp == _source_audio->data[j]) { - free(tmp); - tmp = NULL; - } - else { - _source_audio->data[j] = tmp; - tmp = NULL; - } - } + static obs_properties_t *Properties(void *vptr) + { + UNUSED_PARAMETER(vptr); + obs_properties_t * props; + obs_property_t * devices; + obs_property_t * format; + obs_property_t * panel; + obs_property_t * button; + int max_channels = get_max_obs_channels(); + std::vector route(max_channels, nullptr); + + props = obs_properties_create(); + devices = obs_properties_add_list(props, "device_id", obs_module_text("Device"), OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_STRING); + obs_property_set_modified_callback2(devices, asio_device_changed, vptr); + fill_out_devices(devices); + obs_property_set_long_description(devices, obs_module_text("ASIO Devices")); + + format = obs_properties_add_list(props, "speaker_layout", obs_module_text("Format"), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + for (size_t i = 0; i < known_layouts.size(); i++) + obs_property_list_add_int(format, known_layouts_str[i].c_str(), known_layouts[i]); + obs_property_set_modified_callback(format, asio_layout_changed); + + for (size_t i = 0; i < max_channels; i++) { + route[i] = obs_properties_add_list(props, ("route " + std::to_string(i)).c_str(), + obs_module_text(("Route." + std::to_string(i)).c_str()), OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_INT); + obs_property_set_long_description( + route[i], obs_module_text(("Route.Desc." + std::to_string(i)).c_str())); + } + + panel = obs_properties_add_button2(props, "ctrl", obs_module_text("Control Panel"), show_panel, vptr); + ASIOPlugin * plugin = static_cast(vptr); + AudioIODevice *device = nullptr; + if (plugin) + device = plugin->getDevice(); + + obs_property_set_visible(panel, device && device->hasControlPanel()); + button = obs_properties_add_button(props, "credits", "CREDITS", credits); + return props; + } + + void update(obs_data_t *settings) + { + std::string name = obs_data_get_string(settings, "device_id"); + speaker_layout layout = (speaker_layout)obs_data_get_int(settings, "speaker_layout"); + AudioCB * callback = nullptr; + + AudioIODevice *selected_device = nullptr; + for (int i = 0; i < callbacks.size(); i++) { + AudioCB * cb = callbacks[i]; + AudioIODevice *device = cb->getDevice(); + std::string n = cb->getName(); + if (n == name) { + if (!device) { + String deviceName = name.c_str(); + device = deviceTypeAsio->createDevice(deviceName, deviceName); + cb->setDevice(device, name.c_str()); } - _source_audio->input_chs = input_chs; - _source_audio->frames = frames; - _source_audio->format = format; - _source_audio->samples_per_sec = samples_per_sec; + selected_device = device; + _device = device; + callback = cb; + break; } - buffer_prepped = true; } - check_all(); - } - void write_buffer_interleaved(void* buffer, DWORD BufSize) { - if (!all_prepped) { - blog(LOG_INFO, "%s device %i is not prepared", __FUNCTION__, device_index); - return; - } - ResetEvent(all_recieved_signal); - SetEvent(all_recieved_signal_2); - //get as much information from the device that called this function - BASS_ASIO_INFO info; - bool ret = BASS_ASIO_GetInfo(&info); - uint8_t * input_buffer = (uint8_t*)buffer; - size_t ch_buffer_size = BufSize / info.inputs; - if (ch_buffer_size > buffer_size) { - blog(LOG_WARNING, "%s device needs to reallocate memory"); - } - int byte_depth = bytedepth_format(format); - size_t interleaved_frame_size = info.inputs * byte_depth; - //calculate on the spot - size_t frames_count = BufSize / interleaved_frame_size; - //use cached value - //size_t frames_count = frames; - - device_source_audio* _source_audio = get_writeable_source_audio(); - if (!_source_audio) { - blog(LOG_INFO, "%s _source_audio = NULL", __FUNCTION__); - return; - } + if (selected_device == nullptr) { + AudioCB *cb = _listener->getCallback(); - audio_format planar_format = get_planar_format(format); - //deinterleave directly into buffer (planar) - for (size_t i = 0; i < frames_count; i++) { - for (size_t j = 0; j < info.inputs; j++) { - memcpy(_source_audio->data[j] + (i * byte_depth), input_buffer + (j * byte_depth) + (i * interleaved_frame_size), byte_depth); - } + _listener->setCurrentCallback(callback); + _listener->disconnect(); + + if (cb) + cb->remove_client(_listener); + return; } - _source_audio->format = planar_format; - _source_audio->frames = frames_count; - _source_audio->input_chs = info.inputs; - _source_audio->samples_per_sec = samples_per_sec; - _source_audio->timestamp = _source_audio->timestamp = os_gettime_ns() - ((_source_audio->frames * NSEC_PER_SEC) / _source_audio->samples_per_sec); - - write_index++; - write_index = write_index % buffer_count; - SetEvent(all_recieved_signal); - ResetEvent(all_recieved_signal_2); - } - static DWORD WINAPI capture_thread(void *data) { - listener_pair *pair = static_cast(data); - asio_data *source = pair->asio_listener;//static_cast(data); - device_data *device = pair->device;//static_cast(data); - struct obs_audio_info aoi; - obs_get_audio_info(&aoi); + StringArray in_chs = _device->getInputChannelNames(); + StringArray out_chs = _device->getOutputChannelNames(); + BigInteger in = 0; + BigInteger out = 0; + in.setRange(0, in_chs.size(), true); + out.setRange(0, out_chs.size(), true); + juce::String err; - std::string thread_name = "asio capture: ";//source->device; - thread_name += source->get_id(); - thread_name += ":"; - thread_name += device->device_info.name;//thread_name += " capture thread"; - os_set_thread_name(thread_name.c_str()); + /* Open Up Particular Device */ + if (!_device->isOpen()) { + err = _device->open(in, out, _device->getCurrentSampleRate(), + _device->getCurrentBufferSizeSamples()); + if (!err.toStdString().empty()) { + blog(LOG_WARNING, "%s", err.toStdString().c_str()); + AudioCB *cb = _listener->getCallback(); - HANDLE signals_1[3] = { device->all_recieved_signal, device->stop_listening_signal, source->stop_listening_signal }; - HANDLE signals_2[3] = { device->all_recieved_signal_2, device->stop_listening_signal, source->stop_listening_signal }; + _listener->setCurrentCallback(callback); + _listener->disconnect(); - long route[MAX_AUDIO_CHANNELS]; - for (short i = 0; i < aoi.speakers; i++) { - route[i] = source->route[i]; + if (cb) + cb->remove_client(_listener); + return; + } } - source->isASIOActive = true; - ResetEvent(source->stop_listening_signal); + AudioCB *cb = _listener->getCallback(); + _listener->setCurrentCallback(callback); - blog(LOG_INFO, "listener for device %lu created: source: %x", device->device_index, source->get_id()); + if (_device->isOpen() && !_device->isPlaying() && callback) + _device->start(callback); - size_t read_index = device->write_index;//0; - int waitResult; + if (callback) { + if (cb != callback) { + _listener->disconnect(); + if (cb) + cb->remove_client(_listener); + } - uint64_t buffer_time = ((device->frames * NSEC_PER_SEC) / device->samples_per_sec); + int recorded_channels = get_audio_channels(layout); + int max_channels = get_max_obs_channels(); + std::vector r; + r.reserve(max_channels); - while (source && device) { - waitResult = WaitForMultipleObjects(3, signals_1, false, INFINITE); - waitResult = WaitForMultipleObjects(3, signals_2, false, INFINITE); - //not entirely sure that all of these conditions are correct (at the very least this is) - if (waitResult == WAIT_OBJECT_0) { - while (read_index != device->write_index) { - device_source_audio* in = device->get_source_audio(read_index);//device->get_writeable_source_audio(); - source->render_audio(in, route); - read_index++; - read_index = read_index % device->buffer_count; - } - if (source->device_index != device->device_index) { - blog(LOG_INFO, "source device index %lu is not device index %lu", source->device_index, device->device_index); - blog(LOG_INFO, "%s closing", thread_name.c_str()); - delete pair; - return 0; - } - else if (!source->isASIOActive) { - blog(LOG_INFO, "%x indicated it wanted to disconnect", source->get_id()); - blog(LOG_INFO, "%s closing", thread_name.c_str()); - delete pair; - return 0; - } - //uint64_t t_stamp = os_gettime_ns(); - //os_sleepto_ns(t_stamp + buffer_time); - //os_sleepto_ns(os_gettime_ns() + ((device->frames * NSEC_PER_SEC) / device->samples_per_sec)); - //Sleep(1); - //microsoft docs on the return codes gives the impression that you're supposed to subtract wait_object_0 - } - else if (waitResult == WAIT_OBJECT_0 + 1) { - blog(LOG_INFO, "device %l indicated it wanted to disconnect", device->device_index); - blog(LOG_INFO, "%s closing", thread_name.c_str()); - delete pair; - return 0; - } - else if (waitResult == WAIT_OBJECT_0 + 2) { - blog(LOG_INFO, "%x indicated it wanted to disconnect", source->get_id()); - blog(LOG_INFO, "%s closing", thread_name.c_str()); - delete pair; - return 0; - } - else if (waitResult == WAIT_ABANDONED_0) { - blog(LOG_INFO, "a mutex for %s was abandoned while listening to", thread_name.c_str(), device->device_index); - blog(LOG_INFO, "%s closing", thread_name.c_str()); - delete pair; - return 0; + for (int i = 0; i < recorded_channels; i++) { + std::string route_str = "route " + std::to_string(i); + r.push_back(obs_data_get_int(settings, route_str.c_str())); } - else if (waitResult == WAIT_ABANDONED_0 + 1) { - blog(LOG_INFO, "a mutex for %s was abandoned while listening to", thread_name.c_str(), device->device_index); - blog(LOG_INFO, "%s closing", thread_name.c_str()); - delete pair; - return 0; + for (int i = recorded_channels; i < max_channels; i++) { + r.push_back(-1); } - else if (waitResult == WAIT_ABANDONED_0 + 2) { - blog(LOG_INFO, "a mutex for %s was abandoned while listening to", thread_name.c_str(), device->device_index); - blog(LOG_INFO, "%s closing", thread_name.c_str()); - delete pair; - return 0; - } - else if (waitResult == WAIT_TIMEOUT) { - blog(LOG_INFO, "%s timed out while listening to %l", thread_name.c_str(), device->device_index); - blog(LOG_INFO, "%s closing", thread_name.c_str()); - delete pair; - return 0; - } - else if (waitResult == WAIT_FAILED) { - blog(LOG_INFO, "listener thread wait %lu failed with 0x%x", device->device_index, GetLastError()); - blog(LOG_INFO, "%s closing", thread_name.c_str()); - delete pair; - return 0; - } - else { - blog(LOG_INFO, "unexpected wait result = %i", waitResult); - blog(LOG_INFO, "%s closing", thread_name.c_str()); - delete pair; - return 0; - } - - } - delete pair; - return 0; - } - //adds a listener thread between an asio_data object and this device - void add_listener(asio_data *listener) { - if (!all_prepped) { - return; - } - listener_pair* parameters = new listener_pair(); - - parameters->asio_listener = listener; - parameters->device = this; - blog(LOG_INFO, "disconnecting any previous connections (source_id: %x)", listener->get_id()); - listener->disconnect(); - //CloseHandle(listener->captureThread); - blog(LOG_INFO, "adding listener for %lu (source: %lu)", device_index, listener->device_index); - listener->captureThread = CreateThread(nullptr, 0, this->capture_thread, parameters, 0, nullptr); - } -}; + _listener->setRoute(r); -std::vector device_list; - -/*****************************************************************************/ -// get number of output channels -DWORD get_obs_output_channels() { - // get channel number from output speaker layout set by obs - struct obs_audio_info aoi; - obs_get_audio_info(&aoi); - DWORD recorded_channels = get_audio_channels(aoi.speakers); - return recorded_channels; -} + obs_source_audio out; + out.speakers = layout; + _listener->setOutput(out); -// get device number -uint8_t getDeviceCount() { - uint8_t a, count = 0; - BASS_ASIO_DEVICEINFO info; - for (a = 0; BASS_ASIO_GetDeviceInfo(a, &info); a++) { - blog(LOG_INFO, "device index is : %i and name is : %s", a, info.name); - count++; - } - - return count; -} - -// get the device index from a device name : the current index can be retrieved from DWORD BASS_ASIO_GetDevice(); -DWORD get_device_index(const char *device_info_name) { - int res; - BASS_ASIO_SetUnicode(false); - BASS_ASIO_DEVICEINFO info; - bool ret; - //int numOfDevices = getDeviceCount(); - uint32_t i; - for (i = 0; BASS_ASIO_GetDeviceInfo(i, &info); i++) { - res = strcmp(info.name, device_info_name); - if (res == 0) { - return i; + _listener->setCallback(callback); + if (cb != callback) { + _listener->reconnect(); + callback->add_client(_listener); + } + } else { + _listener->disconnect(); + if (cb) + cb->remove_client(_listener); } } - return -1; -} -DWORD get_device_index(BASS_ASIO_DEVICEINFO device_info) { - return get_device_index(device_info.name); -} - -bool is_device_index_valid(DWORD index) { - return index < getDeviceCount(); -} - -DWORD get_device_buffer_index(BASS_ASIO_DEVICEINFO device_info) { - uint32_t i; - for (i = 0; i < device_list.size(); i++) { - if (strcmp(device_list[i]->device_info.name, device_info.name) == 0) { - return i; - } + static void Update(void *vptr, obs_data_t *settings) + { + ASIOPlugin *plugin = static_cast(vptr); + if (plugin) + plugin->update(settings); } - return -1; -} -// call the control panel -static bool DeviceControlPanel(obs_properties_t *props, - obs_property_t *property, void *data) { - if (!BASS_ASIO_ControlPanel()) { - switch (BASS_ASIO_ErrorGetCode()) { - case BASS_ERROR_INIT: - blog(LOG_ERROR, "Init not called\n"); - break; - case BASS_ERROR_UNKNOWN: - blog(LOG_ERROR, "Unknown error\n"); + static void Defaults(obs_data_t *settings) + { + struct obs_audio_info aoi; + obs_get_audio_info(&aoi); + int recorded_channels = get_audio_channels(aoi.speakers); + int max_channels = get_max_obs_channels(); + // default is muted channels + for (int i = 0; i < recorded_channels; i++) { + std::string name = "route " + std::to_string(i); + obs_data_set_default_int(settings, name.c_str(), -1); + } + for (int i = recorded_channels; i < max_channels; i++) { + std::string name = "route " + std::to_string(i); + obs_data_set_default_int(settings, name.c_str(), -1); } - return false; - } - else { - int device_index = BASS_ASIO_GetDevice(); - BASS_ASIO_INFO info; - BASS_ASIO_GetInfo(&info); - blog(LOG_INFO, "Console loaded for device %s with index %i\n", - info.name, device_index); - } - return true; -} - -/*****************************************************************************/ - -void asio_update(void *vptr, obs_data_t *settings); -void asio_destroy(void *vptr); -//creates the device list -/* -void fill_out_devices(obs_property_t *list) { - int numOfDevices = (int)getDeviceCount(); - char** names = new char*[numOfDevices]; - blog(LOG_INFO, "ASIO Devices: %i\n", numOfDevices); - BASS_ASIO_SetUnicode(false); - BASS_ASIO_DEVICEINFO info; - for (int i = 0; i < numOfDevices; i++) { - BASS_ASIO_GetDeviceInfo(i, &info); - blog(LOG_INFO, "device %i = %ls\n", i, info.name); - std::string test = info.name; - char* cstr = new char[test.length() + 1]; - strcpy(cstr, test.c_str()); - names[i] = cstr; - blog(LOG_INFO, "Number of ASIO Devices: %i\n", numOfDevices); - blog(LOG_INFO, "device %i = %s added successfully.\n", i, names[i]); - obs_property_list_add_string(list, names[i], names[i]); + obs_data_set_default_int(settings, "speaker_layout", aoi.speakers); } -} -*/ -void fill_out_devices(obs_property_t *list) { - int res; - BASS_ASIO_SetUnicode(false); - BASS_ASIO_DEVICEINFO devinfo; - bool ret; - //int numOfDevices = getDeviceCount(); - uint32_t i; - for (i = 0; BASS_ASIO_GetDeviceInfo(i, &devinfo); i++) { - obs_property_list_add_string(list, devinfo.name, devinfo.name); - } -} -/* Creates list of input channels ; a muted channel has route value -1 and -* is recorded. The user can unmute the channel later. -*/ -static bool fill_out_channels_modified(obs_properties_t *props, obs_property_t *list, obs_data_t *settings) { - BASS_ASIO_INFO info; - bool ret = BASS_ASIO_GetInfo(&info); - BASS_ASIO_DEVICEINFO devinfo; - int index = BASS_ASIO_GetDevice(); - ret = BASS_ASIO_GetDeviceInfo(index, &devinfo); - if (!ret) { - blog(LOG_ERROR, "Unable to retrieve info on the current driver \n" - "error number is : %i \n; check BASS_ASIO_ErrorGetCode\n", - BASS_ASIO_ErrorGetCode()); - } - // DEBUG: check that the current device in bass thread is the correct one - // once code is fine the check can be removed - const char* device = obs_data_get_string(settings, "device_id"); - if (!strcmp(device, devinfo.name)) { - blog(LOG_ERROR, "Device loaded is not the one in settings\n"); + static const char *Name(void *unused) + { + UNUSED_PARAMETER(unused); + return obs_module_text("ASIO"); } - //get the device info - DWORD input_channels = info.inputs; - obs_property_list_clear(list); - obs_property_list_add_int(list, "mute", -1); - BASS_ASIO_CHANNELINFO ch_info; - for (DWORD i = 0; i < input_channels; i++) { - BASS_ASIO_ChannelGetInfo(1, i, &ch_info); - std::string test = info.name; - test = test + " " + std::to_string(i); - test = test + " " + ch_info.name; - obs_property_list_add_int(list, test.c_str(), i); - } - return true; -} +}; -//creates list of input sample rates supported by the device and OBS (obs supports only 44100 and 48000) -static bool fill_out_sample_rates(obs_properties_t *props, obs_property_t *list, obs_data_t *settings) { - BASS_ASIO_DEVICEINFO devinfo; - int index = BASS_ASIO_GetDevice(); - bool ret = BASS_ASIO_GetDeviceInfo(index, &devinfo); - if (!ret) { - blog(LOG_ERROR, "Unable to retrieve info on the current driver \n" - "error number is : %i \n; check BASS_ASIO_ErrorGetCode\n", - BASS_ASIO_ErrorGetCode()); - } - // DEBUG: check that the current device in bass thread is the correct one - // once code is fine the check can be removed - const char* device = obs_data_get_string(settings, "device_id"); - if (!strcmp(device, devinfo.name)) { - blog(LOG_ERROR, "Device loaded is not the one in settings\n"); - } +static bool show_panel(obs_properties_t *props, obs_property_t *property, void *data) +{ + UNUSED_PARAMETER(props); + UNUSED_PARAMETER(property); - obs_property_list_clear(list); - //get the device info - ret = BASS_ASIO_CheckRate(44100); - if (ret) { - std::string rate = "44100 Hz"; - char* cstr = new char[rate.length() + 1]; - strcpy(cstr, rate.c_str()); - obs_property_list_add_int(list, cstr, 44100); - delete cstr; - } - else { - blog(LOG_INFO, "Device loaded does not support 44100 Hz sample rate\n"); - } - ret = BASS_ASIO_CheckRate(48000); - if (ret) { - std::string rate = "48000 Hz"; - char* cstr = new char[rate.length() + 1]; - strcpy(cstr, rate.c_str()); - obs_property_list_add_int(list, cstr, 48000); - delete cstr; - } - else { - blog(LOG_INFO, "Device loaded does not support 48000 Hz sample rate\n"); - } - return true; + if (!data) + return false; + ASIOPlugin * plugin = static_cast(data); + AudioIODevice *device = plugin->getDevice(); + if (device && device->hasControlPanel()) + device->showControlPanel(); + return false; } -//create list of supported audio formats -static bool fill_out_bit_depths(obs_properties_t *props, obs_property_t *list, obs_data_t *settings) { - BASS_ASIO_DEVICEINFO devinfo; - int index = BASS_ASIO_GetDevice(); - bool ret = BASS_ASIO_GetDeviceInfo(index, &devinfo); - if (!ret) { - blog(LOG_ERROR, "Unable to retrieve info on the current driver \n" - "error number is : %i \n; check BASS_ASIO_ErrorGetCode\n", - BASS_ASIO_ErrorGetCode()); - } - // DEBUG: check that the current device in bass thread is the correct one - // once code is fine the check can be removed - const char* device = obs_data_get_string(settings, "device_id"); - if (!strcmp(device, devinfo.name)) { - blog(LOG_ERROR, "Device loaded is not the one in settings\n"); - } - - //get the device channel info - BASS_ASIO_CHANNELINFO channelInfo; - ret = BASS_ASIO_ChannelGetInfo(true, 0, &channelInfo); - if (!ret) { - blog(LOG_ERROR, "Unable to retrieve channel info on the current driver \n" - "error number is : %i \n; check BASS_ASIO_ErrorGetCode\n", - BASS_ASIO_ErrorGetCode()); +static bool fill_out_channels_modified(obs_properties_t *props, obs_property_t *list, obs_data_t *settings) +{ + UNUSED_PARAMETER(props); + std::string name = obs_data_get_string(settings, "device_id"); + AudioCB * _callback = nullptr; + AudioIODevice *_device = nullptr; + + for (int i = 0; i < callbacks.size(); i++) { + AudioCB * cb = callbacks[i]; + AudioIODevice *device = cb->getDevice(); + std::string n = cb->getName(); + if (n == name) { + if (!device) { + String deviceName = name.c_str(); + device = deviceTypeAsio->createDevice(deviceName, deviceName); + cb->setDevice(device, name.c_str()); + } + _device = device; + _callback = cb; + break; + } } obs_property_list_clear(list); - //these settings are ignored, optimal is picked between float and native for least - //amount of processing possible - if (channelInfo.format == BASS_ASIO_FORMAT_16BIT) { - obs_property_list_add_int(list, "16 bit (native)", AUDIO_FORMAT_16BIT); - obs_property_list_add_int(list, "32 bit", AUDIO_FORMAT_32BIT); - obs_property_list_add_int(list, "32 bit float", AUDIO_FORMAT_FLOAT); - } - else if (channelInfo.format == BASS_ASIO_FORMAT_32BIT) { - obs_property_list_add_int(list, "16 bit", AUDIO_FORMAT_16BIT); - obs_property_list_add_int(list, "32 bit (native)", AUDIO_FORMAT_32BIT); - obs_property_list_add_int(list, "32 bit float", AUDIO_FORMAT_FLOAT); - } - else if (channelInfo.format == BASS_ASIO_FORMAT_FLOAT) { - obs_property_list_add_int(list, "16 bit", AUDIO_FORMAT_16BIT); - obs_property_list_add_int(list, "32 bit", AUDIO_FORMAT_32BIT); - obs_property_list_add_int(list, "32 bit float (native)", AUDIO_FORMAT_FLOAT); - } - else { - blog(LOG_ERROR, "Your device uses unsupported bit depth.\n" - "Only 16 bit, 32 bit signed int and 32 bit float are supported.\n" - "Change accordingly your device settings.\n" - "Forcing bit depth to 32 bit float"); - obs_property_list_add_int(list, "32 bit float", AUDIO_FORMAT_FLOAT); - return false; - } - return true; -} + obs_property_list_add_int(list, obs_module_text("Mute"), -1); -//create list of device supported buffer sizes -static bool fill_out_buffer_sizes(obs_properties_t *props, obs_property_t *list, obs_data_t *settings) { - BASS_ASIO_INFO info; - bool ret = BASS_ASIO_GetInfo(&info); - if (!ret) { - blog(LOG_ERROR, "Unable to retrieve info on the current driver \n" - "error number is : %i \n; check BASS_ASIO_ErrorGetCode\n", - BASS_ASIO_ErrorGetCode()); - } + if (!_callback || !_device) + return true; - obs_property_list_clear(list); + juce::StringArray in_names = _device->getInputChannelNames(); + int input_channels = in_names.size(); - if (info.bufgran == -1) { - long long gran_buffer = info.bufmin; - while (gran_buffer <= info.bufmax) { - int n = snprintf(NULL, 0, "%llu%s", gran_buffer, (gran_buffer == info.bufpref ? " (preferred)" : "")); - if (n <= 0) { - //problem...continuing on the loop - gran_buffer *= 2; - continue; - } - char * buf = (char*)bmalloc((n + 1) * sizeof(char)); - if (!buf) { - //problem...continuing on the loop - gran_buffer *= 2; - continue; - } - int c = snprintf(buf, n + 1, "%llu%s", gran_buffer, (gran_buffer == info.bufpref ? " (preferred)" : "")); - buf[n] = '\0'; - obs_property_list_add_int(list, buf, gran_buffer); - bfree(buf); - gran_buffer *= 2; - } - } - else if (info.bufgran == 0) { - size_t gran_buffer = info.bufmin; - int n = snprintf(NULL, 0, "%llu%s", gran_buffer, (gran_buffer == info.bufpref ? " (preferred)" : "")); - char * buf = (char*)bmalloc((n + 1) * sizeof(char)); - int c = snprintf(buf, n + 1, "%llu%s", gran_buffer, (gran_buffer == info.bufpref ? " (preferred)" : "")); - buf[n] = '\0'; - obs_property_list_add_int(list, buf, gran_buffer); - bfree(buf); - } else if (info.bufgran > 0) { - size_t gran_buffer = info.bufmin; - while (gran_buffer <= info.bufmax) { - int n = snprintf(NULL, 0, "%llu%s", gran_buffer, (gran_buffer == info.bufpref ? " (preferred)" : "")); - if (n <= 0) { - //problem...continuing on the loop - gran_buffer += info.bufgran; - continue; - } - char * buf = (char*)bmalloc((n + 1) * sizeof(char)); - if (!buf) { - //problem...continuing on the loop - gran_buffer += info.bufgran; - continue; - } - int c = snprintf(buf, n + 1, "%llu%s", gran_buffer, (gran_buffer == info.bufpref ? " (preferred)" : "")); - buf[n] = '\0'; - obs_property_list_add_int(list, buf, gran_buffer); - bfree(buf); - gran_buffer += info.bufgran; - } - } + int i = 0; + + for (; i < input_channels; i++) + obs_property_list_add_int(list, in_names[i].toStdString().c_str(), i); return true; } -static bool asio_device_changed(obs_properties_t *props, - obs_property_t *list, obs_data_t *settings) +static bool asio_device_changed(void *vptr, obs_properties_t *props, obs_property_t *list, obs_data_t *settings) { - const char *curDeviceId = obs_data_get_string(settings, "device_id"); - obs_property_t *sample_rate = obs_properties_get(props, "sample rate"); - obs_property_t *bit_depth = obs_properties_get(props, "bit depth"); - obs_property_t *buffer_size = obs_properties_get(props, "buffer"); - // be sure to set device as current one + size_t i; + const char * curDeviceId = obs_data_get_string(settings, "device_id"); + int max_channels = get_max_obs_channels(); + std::vector route(max_channels, nullptr); + speaker_layout layout = (speaker_layout)obs_data_get_int(settings, "speaker_layout"); + obs_property_t * panel = obs_properties_get(props, "ctrl"); + + int recorded_channels = get_audio_channels(layout); size_t itemCount = obs_property_list_item_count(list); - bool itemFound = false; + bool itemFound = false; - for (size_t i = 0; i < itemCount; i++) { + for (i = 0; i < itemCount; i++) { const char *DeviceId = obs_property_list_item_string(list, i); if (strcmp(DeviceId, curDeviceId) == 0) { itemFound = true; @@ -1092,565 +722,120 @@ static bool asio_device_changed(obs_properties_t *props, if (!itemFound) { obs_property_list_insert_string(list, 0, " ", curDeviceId); obs_property_list_item_disable(list, 0, true); - } - else { - DWORD device_index = get_device_index(curDeviceId); - bool ret = BASS_ASIO_SetDevice(device_index); - if (!ret) { - blog(LOG_ERROR, "Unable to set device %i\n", device_index); - if (BASS_ASIO_ErrorGetCode() == BASS_ERROR_INIT) { - BASS_ASIO_Init(device_index, BASS_ASIO_THREAD); - BASS_ASIO_SetDevice(device_index); - } - else if (BASS_ASIO_ErrorGetCode() == BASS_ERROR_DEVICE) { - blog(LOG_ERROR, "Device index is invalid\n"); - } - } - else { - obs_property_list_clear(sample_rate); - obs_property_list_clear(bit_depth); - //fill out based on device's settings - obs_property_list_clear(buffer_size); - obs_property_set_modified_callback(sample_rate, fill_out_sample_rates); - obs_property_set_modified_callback(bit_depth, fill_out_bit_depths); - obs_property_set_modified_callback(buffer_size, fill_out_buffer_sizes); - - } - } - // get channel number from output speaker layout set by obs - DWORD recorded_channels = get_obs_output_channels(); - - obs_property_t *route[MAX_AUDIO_CHANNELS]; - if (itemFound) { - for (unsigned int i = 0; i < recorded_channels; i++) { + } else { + for (i = 0; i < max_channels; i++) { std::string name = "route " + std::to_string(i); - route[i] = obs_properties_get(props, name.c_str()); + route[i] = obs_properties_get(props, name.c_str()); obs_property_list_clear(route[i]); -// obs_data_set_default_int(settings, name.c_str(), -1); // default is muted channels obs_property_set_modified_callback(route[i], fill_out_channels_modified); + obs_property_set_visible(route[i], i < recorded_channels); } } - return true; -} - -int mix(uint8_t *inputBuffer, obs_source_audio *out, size_t bytes_per_ch, int route[], unsigned int recorded_device_chs = UINT_MAX) { - DWORD recorded_channels = get_obs_output_channels(); - short j = 0; - for (size_t i = 0; i < recorded_channels; i++) { - if (route[i] > -1 && route[i] < (int)recorded_device_chs) { - out->data[j++] = inputBuffer + route[i] * bytes_per_ch; - } - else if (route[i] == -1) { - uint8_t * silent_buffer; - silent_buffer = (uint8_t *)calloc(bytes_per_ch, 1); - out->data[j++] = silent_buffer; - } + ASIOPlugin *plugin = static_cast(vptr); + if (plugin) { + juce::AudioIODevice *device = plugin->getDevice(); + obs_property_set_visible(panel, device && device->hasControlPanel()); } - return true; -} - -DWORD CALLBACK create_asio_buffer(BOOL input, DWORD channel, void *buffer, DWORD BufSize, void *device_ptr) { - BASS_ASIO_INFO info; - bool ret = BASS_ASIO_GetInfo(&info); - device_data *device = (device_data*)device_ptr; - device->write_buffer_interleaved(buffer, BufSize); - - return 0; -} -void CALLBACK asio_device_setting_changed(DWORD notify, void *device_ptr) { - device_data *device = (device_data*)device_ptr; - BASS_ASIO_INFO info; - bool ret = BASS_ASIO_GetInfo(&info); - uint32_t new_sample_rate; - switch (notify) { - case BASS_ASIO_NOTIFY_RATE: - new_sample_rate = BASS_ASIO_GetRate(); - blog(LOG_WARNING, "device %l changed sample rate to %f", device->device_index , new_sample_rate); - - if (!ret) { - blog(LOG_ERROR, "Unable to retrieve info on the current driver \n" - "error number is : %i; \n check BASS_ASIO_ErrorGetCode\n", - BASS_ASIO_ErrorGetCode()); - } - BASS_ASIO_Stop(); - device->update_sample_rate(new_sample_rate); - device->re_prep_buffers(info); - ret = BASS_ASIO_Start(info.bufpref, info.inputs); - if (!ret) { - switch (BASS_ASIO_ErrorGetCode()) { - case BASS_ERROR_INIT: - blog(LOG_ERROR, "Error: Bass asio not initialized.\n"); - break; - case BASS_ERROR_ALREADY: - blog(LOG_ERROR, "Error: device already started\n"); - //BASS_ASIO_Stop(); - //BASS_ASIO_Start(data->BufferSize, recorded_channels); - break; - case BASS_ERROR_NOCHAN: - blog(LOG_ERROR, "Error: channels have not been enabled so can not start\n"); - break; - case BASS_ERROR_UNKNOWN: - default: - blog(LOG_ERROR, "ASIO init: Unknown error when trying to start the device\n"); - break; - } - } - - break; - case BASS_ASIO_NOTIFY_RESET: - blog(LOG_WARNING, "device %l requested a reset", device->device_index); - // Reset ? - //BASS_ASIO_SetDevice(device->device_index); - if (!ret) { - blog(LOG_ERROR, "Unable to retrieve info on the current driver \n" - "error number is : %i \n; check BASS_ASIO_ErrorGetCode\n", - BASS_ASIO_ErrorGetCode()); - } - BASS_ASIO_Stop(); - device->re_prep_buffers(info); - ret = BASS_ASIO_Start(info.bufpref,info.inputs); - if (!ret) { - switch (BASS_ASIO_ErrorGetCode()) { - case BASS_ERROR_INIT: - blog(LOG_ERROR, "Error: Bass asio not initialized.\n"); - break; - case BASS_ERROR_ALREADY: - blog(LOG_ERROR, "Error: device already started\n"); - //BASS_ASIO_Stop(); - //BASS_ASIO_Start(data->BufferSize, recorded_channels); - break; - case BASS_ERROR_NOCHAN: - blog(LOG_ERROR, "Error: channels have not been enabled so can not start\n"); - break; - case BASS_ERROR_UNKNOWN: - default: - blog(LOG_ERROR, "ASIO init: Unknown error when trying to start the device\n"); - break; - } - } - //BASS_ASIO_Stop(); - break; - } -} - -void asio_init(struct asio_data *data) -{ - // get info, useful for debug - BASS_ASIO_INFO info; - bool ret = BASS_ASIO_GetInfo(&info); - int index = BASS_ASIO_GetDevice(); - BASS_ASIO_DEVICEINFO devinfo; - ret = BASS_ASIO_GetDeviceInfo(index, &devinfo); - if (!ret) { - blog(LOG_ERROR, "Unable to retrieve info on the current driver \n" - "error number is : %i \n; check BASS_ASIO_ErrorGetCode\n", - BASS_ASIO_ErrorGetCode()); - } - - uint8_t deviceNumber = getDeviceCount(); - if (deviceNumber < 1) { - blog(LOG_INFO, "\nNo audio devices found!\n"); - return; - } - //BASS_ASIO_GetCPU - // get channel number from output speaker layout set by obs - DWORD spawn_threads = info.inputs;//get_obs_output_channels(); - - // check buffer size is legit; if not set it to bufpref - // to be implemented : to avoid issues, force to bufpref - // this ignores any setting; bufpref is most likely set in asio control panel - //check channel setup - DWORD checkrate = BASS_ASIO_GetRate(); - blog(LOG_INFO, "sample rate is set in device to %i.\n", checkrate); - DWORD checkbitdepth = BASS_ASIO_ChannelGetFormat(true, 0); - blog(LOG_INFO, "bitdepth is set in device to %i, format: %i.\n", bytedepth_format(checkbitdepth), checkbitdepth); - //audio_format format = asio_to_obs_audio_format(checkbitdepth); - - //get the device_index - DWORD device_index = index; //get_device_index(devinfo.name); - - //start asio device if it hasn't already been - if (!BASS_ASIO_IsStarted()) { - DWORD obs_optimal_format = BASS_ASIO_FORMAT_FLOAT; - DWORD asio_native_format = BASS_ASIO_ChannelGetFormat(true, 0); - DWORD selected_format; - //pick the best format to use (trying to get float if possible) - if (obs_optimal_format == asio_native_format) { - //all good...I don't think this needs to happen - ret = BASS_ASIO_ChannelSetFormat(true, 0, asio_native_format); - if (!ret) { - blog(LOG_ERROR, "ASIO: unable to use native format\n" - "error number: %i \n; check BASS_ASIO_ErrorGetCode\n", - BASS_ASIO_ErrorGetCode()); - return; - } - selected_format = asio_native_format; - } - else { - ret = BASS_ASIO_ChannelSetFormat(true, 0, obs_optimal_format); - if (!ret) { - blog(LOG_ERROR, "ASIO: unable to use optimal format (float)\n" - "error number: %i \n; check BASS_ASIO_ErrorGetCode\n", - BASS_ASIO_ErrorGetCode()); - - ret = BASS_ASIO_ChannelSetFormat(true, 0, asio_native_format); - if (!ret) { - blog(LOG_ERROR, "ASIO: unable to use native format\n" - "error number: %i \n; check BASS_ASIO_ErrorGetCode\n", - BASS_ASIO_ErrorGetCode()); - return; - } - selected_format = asio_native_format; - } - else { - selected_format = obs_optimal_format; - } - } - - blog(LOG_INFO, "(best) bitdepth supported %i", selected_format); - //audio_format format = asio_to_obs_audio_format(selected_format); - audio_format format = get_planar_format(asio_to_obs_audio_format(selected_format)); - - ret = BASS_ASIO_SetNotify(asio_device_setting_changed, device_list[device_index]); - - // enable all chs and link to callback w/ the device buffer class - ret = BASS_ASIO_ChannelEnable(true, 0, &create_asio_buffer, device_list[device_index]);//data - - for (DWORD i = 1; i < info.inputs; i++) { - BASS_ASIO_ChannelJoin(true, i, 0); - } - - /*prep the device buffers*/ - blog(LOG_INFO, "prepping device %lu", device_index); - device_list[device_index]->prep_circle_buffer(info); - device_list[device_index]->prep_events(info); - device_list[device_index]->prep_buffers(info.bufpref, info.inputs, format, checkrate); - - /*start the device w/ # of threads*/ - blog(LOG_INFO, "starting device %lu", device_index); - ret = BASS_ASIO_Start(info.bufpref, spawn_threads); - if(!ret){ - switch (BASS_ASIO_ErrorGetCode()) { - case BASS_ERROR_INIT: - blog(LOG_ERROR, "Error: Bass asio not initialized.\n"); - break; - case BASS_ERROR_ALREADY: - blog(LOG_ERROR, "Error: device already started\n"); - //BASS_ASIO_Stop(); - //BASS_ASIO_Start(data->BufferSize, recorded_channels); - break; - case BASS_ERROR_NOCHAN: - blog(LOG_ERROR, "Error: channels have not been enabled so can not start\n"); - break; - case BASS_ERROR_UNKNOWN: - default: - blog(LOG_ERROR, "ASIO init: Unknown error when trying to start the device\n"); - break; - } - } - } - - //Connect listener thread - //data->captureThread = device_list[device_index]->capture_thread(); - blog(LOG_INFO, "starting listener thread for: %lu", device_index); - device_list[device_index]->add_listener(data); -} - -static void * asio_create(obs_data_t *settings, obs_source_t *source) -{ - asio_data *data = new asio_data; - - data->source = source; - data->first_ts = 0; - data->device = NULL; - - asio_update(data, settings); - - return data; + return true; } -void asio_destroy(void *vptr) +static bool asio_layout_changed(obs_properties_t *props, obs_property_t *list, obs_data_t *settings) { - struct asio_data *data = (asio_data *)vptr; - if (data) { - bfree((void*)data->device); - if (data->device_index < device_list.size()) { - device_data *device = device_list[data->device_index]; - //send disconnect event - //SetEvent(data->stop_listening_signal); - //data->isASIOActive = false; - //HANDLE single_buffer[1] = { device->on_buffer() }; - //WaitForMultipleObjects(1, single_buffer, false, 1000); - //WaitForMultipleObjects(device->get_input_channels(), wait_for_complete_buffer, true, 40); - data->disconnect(); - } + UNUSED_PARAMETER(list); + int max_channels = get_max_obs_channels(); + std::vector route(max_channels, nullptr); + speaker_layout layout = (speaker_layout)obs_data_get_int(settings, "speaker_layout"); + int recorded_channels = get_audio_channels(layout); + int i = 0; + for (i = 0; i < max_channels; i++) { + std::string name = "route " + std::to_string(i); + route[i] = obs_properties_get(props, name.c_str()); + obs_property_list_clear(route[i]); + obs_property_set_modified_callback(route[i], fill_out_channels_modified); + obs_property_set_visible(route[i], i < recorded_channels); } - delete data; + return true; } -/* set all settings to asio_data struct and pass to driver */ -void asio_update(void *vptr, obs_data_t *settings) +static void fill_out_devices(obs_property_t *prop) { - struct asio_data *data = (asio_data *)vptr; - const char *device; - unsigned int rate; - audio_format BitDepth; - uint16_t BufferSize; - unsigned int channels; - BASS_ASIO_INFO info; - int res; - bool ret; - DWORD route[MAX_AUDIO_CHANNELS]; - DWORD device_index; - int numDevices = getDeviceCount(); - bool device_changed = false; - const char *prev_device; - DWORD prev_device_index; - // lock down the settings mutex (protect against a sudden change when reading buffers) - //EnterCriticalSection(&data->settings_mutex); - // get channel number from output speaker layout set by obs - DWORD recorded_channels = get_obs_output_channels(); - data->recorded_channels = recorded_channels; - - // get device from settings - device = obs_data_get_string(settings, "device_id"); - - if (device == NULL || device[0] == '\0') { - blog(LOG_INFO, "Device not yet set \n"); - } - else if (data->device == NULL || data->device[0] == '\0') { - data->device = bstrdup(device); - } - else { - if (strcmp(device, data->device) != 0) { - prev_device = bstrdup(data->device); - data->device = bstrdup(device); - device_changed = true; - } - } - - if (device != NULL && device[0] != '\0') { - device_index = get_device_index(device); - if (!device_changed) { - prev_device_index = device_index; - } - else { - prev_device_index = get_device_index(prev_device); - } - // check if device is already initialized - ret = BASS_ASIO_Init(device_index, BASS_ASIO_THREAD); - bool first_initialization = false; - - if (!ret) { - res = BASS_ASIO_ErrorGetCode(); - switch (res) { - case BASS_ERROR_DEVICE: - blog(LOG_ERROR, "The device number specified is invalid.\n"); - break; - case BASS_ERROR_ALREADY: - blog(LOG_ERROR, "The device has already been initialized\n"); - break; - case BASS_ERROR_DRIVER: - blog(LOG_ERROR, "The driver could not be initialized\n"); + StringArray deviceNames(deviceTypeAsio->getDeviceNames()); + for (int j = 0; j < deviceNames.size(); j++) { + bool found = false; + for (int i = 0; i < callbacks.size(); i++) { + AudioCB * cb = callbacks[i]; + std::string n = cb->getName(); + if (deviceNames[j].toStdString() == n) { + found = true; break; } } - else { - blog(LOG_INFO, "Device %i was successfully initialized\n", device_index); - first_initialization = true; + if (!found) { + char * name = bstrdup(deviceNames[j].toStdString().c_str()); + AudioCB *cb = new AudioCB(nullptr, name); + bfree(name); + callbacks.push_back(cb); } + } - ret = BASS_ASIO_SetDevice(device_index); - if (!ret) { - res = BASS_ASIO_ErrorGetCode(); - switch (res) { - case BASS_ERROR_DEVICE: - blog(LOG_ERROR, "The device number specified is invalid.\n"); - break; - case BASS_ERROR_INIT: - blog(LOG_ERROR, "The device has not been initialized\n"); - break; - } - } - - ret = BASS_ASIO_GetInfo(&info); - if (!ret) { - blog(LOG_ERROR, "Unable to retrieve info on the current driver \n" - "driver is not initialized\n"); - } - - // DEBUG: check that the current device in bass thread is the correct one - // once code is fine the check can be removed - BASS_ASIO_DEVICEINFO devinfo; - int index = BASS_ASIO_GetDevice(); - ret = BASS_ASIO_GetDeviceInfo(index, &devinfo); - if (!strcmp(device, devinfo.name)) { - blog(LOG_ERROR, "Device loaded is not the one in settings\n"); - } - - bool route_changed = false; - for (unsigned int i = 0; i < recorded_channels; i++) { - std::string route_str = "route " + std::to_string(i); - route[i] = (int)obs_data_get_int(settings, route_str.c_str()); - if (data->route[i] != route[i]) { - data->route[i] = route[i]; - route_changed = true; - } - } - - data->input_channels = info.inputs; - data->output_channels = info.outputs; - data->device_index = device_index; - - data->muted_chs = data->_get_muted_chs(data->route); - data->unmuted_chs = data->_get_unmuted_chs(data->route); - - //safe to leave the critical section - //LeaveCriticalSection(&data->settings_mutex); + obs_property_list_clear(prop); - //spin up the asio device if it hasn't already and create a listener thread - /*rate = (double)obs_data_get_int(settings, "sample rate"); - BufferSize = (uint16_t)obs_data_get_int(settings, "buffer"); - BitDepth = (audio_format)obs_data_get_int(settings, "bit depth"); - if ((rate == 44100 || rate == 48000) && BufferSize != 0)*/ - asio_init(data); + for (int i = 0; i < callbacks.size(); i++) { + AudioCB * cb = callbacks[i]; + const char *n = cb->getName(); + obs_property_list_add_string(prop, n, n); } - } -const char * asio_get_name(void *unused) +bool obs_module_load(void) { - UNUSED_PARAMETER(unused); - return obs_module_text("asioInput"); -} + obs_audio_info aoi; + obs_get_audio_info(&aoi); -void asio_get_defaults(obs_data_t *settings) -{ - obs_data_set_default_int(settings, "sample rate", 48000); - obs_data_set_default_int(settings, "bit depth", AUDIO_FORMAT_FLOAT); - DWORD recorded_channels = get_obs_output_channels(); - for (unsigned int i = 0; i < recorded_channels; i++) { - std::string name = "route " + std::to_string(i); - obs_data_set_default_int(settings, name.c_str(), -1); // default is muted channels - } -} + MessageManager::getInstance(); -obs_properties_t * asio_get_properties(void *unused) -{ - obs_properties_t *props; - obs_property_t *devices; - obs_property_t *rate; - obs_property_t *bit_depth; - obs_property_t *buffer_size; - obs_property_t *route[MAX_AUDIO_CHANNELS]; - obs_property_t *console; - int pad_digits = (int)floor(log10(abs(MAX_AUDIO_CHANNELS))) + 1; - - UNUSED_PARAMETER(unused); - - props = obs_properties_create(); - devices = obs_properties_add_list(props, "device_id", - obs_module_text("Device"), OBS_COMBO_TYPE_LIST, - OBS_COMBO_FORMAT_STRING); - obs_property_set_modified_callback(devices, asio_device_changed); - fill_out_devices(devices); - std::string dev_descr = "ASIO devices.\n" - "OBS-Studio supports for now a single ASIO source.\n" - "But duplication of an ASIO source in different scenes is still possible"; - obs_property_set_long_description(devices, dev_descr.c_str()); - // get channel number from output speaker layout set by obs - DWORD recorded_channels = get_obs_output_channels(); - - std::string route_descr = "For each OBS output channel, pick one\n of the input channels of your ASIO device.\n"; - const char* route_name_format = "route %i"; - char* route_name = new char[strlen(route_name_format) + pad_digits]; - - const char* route_obs_format = "Route.%i"; - char* route_obs = new char[strlen(route_obs_format) + pad_digits]; - for (size_t i = 0; i < recorded_channels; i++) { - sprintf(route_name, route_name_format, i); - sprintf(route_obs, route_obs_format, i); - route[i] = obs_properties_add_list(props, route_name, obs_module_text(route_obs), - OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); - obs_property_set_long_description(route[i], route_descr.c_str()); - } + deviceTypeAsio->scanForDevices(); + StringArray deviceNames(deviceTypeAsio->getDeviceNames()); + for (int j = 0; j < deviceNames.size(); j++) { + char *name = bstrdup(deviceNames[j].toStdString().c_str()); - free(route_name); - free(route_obs); - - rate = obs_properties_add_list(props, "sample rate", - obs_module_text("SampleRate"), OBS_COMBO_TYPE_LIST, - OBS_COMBO_FORMAT_INT); - std::string rate_descr = "Sample rate : number of samples per channel in one second.\n"; - obs_property_set_long_description(rate, rate_descr.c_str()); - - bit_depth = obs_properties_add_list(props, "bit depth", TEXT_BITDEPTH, - OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); - std::string bit_descr = "Bit depth : size of a sample in bits and format.\n" - "Float should be preferred."; - obs_property_set_long_description(bit_depth, bit_descr.c_str()); - - buffer_size = obs_properties_add_list(props, "buffer", TEXT_BUFFER_SIZE, - OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); - - //these should be based on the device - obs_property_list_add_int(buffer_size, "64", 64); - obs_property_list_add_int(buffer_size, "128", 128); - obs_property_list_add_int(buffer_size, "256", 256); - obs_property_list_add_int(buffer_size, "512", 512); - obs_property_list_add_int(buffer_size, "1024", 1024); - - std::string buffer_descr = "Buffer : number of samples in a single frame.\n" - "A lower value implies lower latency.\n" - "256 should be OK for most cards.\n" - "Warning: the real buffer returned by the device may differ"; - obs_property_set_long_description(buffer_size, buffer_descr.c_str()); - - console = obs_properties_add_button(props, "console", - obs_module_text("ASIO driver control panel"), DeviceControlPanel); - std::string console_descr = "Make sure your settings in the Driver Control Panel\n" - "for sample rate and buffer are consistent with what you\n" - "have set in OBS."; - obs_property_set_long_description(console, console_descr.c_str()); - - return props; -} + AudioCB *cb = new AudioCB(nullptr, name); + bfree(name); + callbacks.push_back(cb); + } -bool obs_module_load(void) -{ struct obs_source_info asio_input_capture = {}; - asio_input_capture.id = "asio_input_capture"; - asio_input_capture.type = OBS_SOURCE_TYPE_INPUT; - asio_input_capture.output_flags = OBS_SOURCE_AUDIO; - asio_input_capture.create = asio_create; - asio_input_capture.destroy = asio_destroy; - asio_input_capture.update = asio_update; - asio_input_capture.get_defaults = asio_get_defaults; - asio_input_capture.get_name = asio_get_name; - asio_input_capture.get_properties = asio_get_properties; - - uint8_t devices = getDeviceCount(); - device_list.reserve(devices); - for (uint8_t i = 0; i < devices; i++) { - device_data *device = new device_data(); - device->device_index = i; - BASS_ASIO_GetDeviceInfo(i, &device->device_info); - device_list.push_back(device); - } + asio_input_capture.id = "asio_input_capture"; + asio_input_capture.type = OBS_SOURCE_TYPE_INPUT; + asio_input_capture.output_flags = OBS_SOURCE_AUDIO; + asio_input_capture.create = ASIOPlugin::Create; + asio_input_capture.destroy = ASIOPlugin::Destroy; + asio_input_capture.update = ASIOPlugin::Update; + asio_input_capture.get_defaults = ASIOPlugin::Defaults; + asio_input_capture.get_name = ASIOPlugin::Name; + asio_input_capture.get_properties = ASIOPlugin::Properties; obs_register_source(&asio_input_capture); return true; } -void obs_module_unload(void){ - for (uint8_t i = 0; i < device_list.size(); i++) { - //stop streams - BASS_ASIO_SetDevice(i); - BASS_ASIO_Stop(); - BASS_ASIO_Free(); - //clear buffers - delete device_list[i]; +void obs_module_unload(void) +{ + for (int i = 0; i < callbacks.size(); i++) { + AudioCB * cb = callbacks[i]; + AudioIODevice *device = cb->getDevice(); + if (device) { + if (device->isPlaying()) + device->stop(); + if (device->isOpen()) + device->close(); + delete device; + } + device = nullptr; + delete cb; } + + delete deviceTypeAsio; } diff --git a/src/asio-input.qrc b/src/asio-input.qrc new file mode 100644 index 0000000..bc0ed90 --- /dev/null +++ b/src/asio-input.qrc @@ -0,0 +1,6 @@ + + + images/asiologo.png + images/btn_donate_SM.gif + + \ No newline at end of file diff --git a/src/data/locale/en-US.ini b/src/data/locale/en-US.ini index 6fe10e3..c2fabba 100644 --- a/src/data/locale/en-US.ini +++ b/src/data/locale/en-US.ini @@ -1,8 +1,10 @@ AsioInput="ASIO input" Device="Device" -SampleRate="Sample Rate" -BitDepth="Bit depth" -BufferSize="Buffer" +"ASIO Device Settings"="ASIO Device Settings" +"Active Device"="Active Device" +"ASIO Device Control Panel"="ASIO Device Control Panel" +"Settings"="Settings" +Credits="Credits" Route.0="OBS Channel 1" Route.1="OBS Channel 2" Route.2="OBS Channel 3" @@ -11,3 +13,13 @@ Route.4="OBS Channel 5" Route.5="OBS Channel 6" Route.6="OBS Channel 7" Route.7="OBS Channel 8" +Route.Desc.0 = "ASIO Channel 1" +Route.Desc.1 = "ASIO Channel 2" +Route.Desc.2 = "ASIO Channel 3" +Route.Desc.3 = "ASIO Channel 4" +Route.Desc.4 = "ASIO Channel 5" +Route.Desc.5 = "ASIO Channel 6" +Route.Desc.6 = "ASIO Channel 7" +Route.Desc.7 = "ASIO Channel 8" + +Console.Desc = "Make sure your settings in the Device Control Panel\nfor sample rate and buffer are consistent with what you\nhave set in OBS."; \ No newline at end of file diff --git a/src/images/asiologo.png b/src/images/asiologo.png new file mode 100644 index 0000000000000000000000000000000000000000..2d4f5abb87f42e0ff1eb35551b4b9213fef195eb GIT binary patch literal 14348 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

>6Y3EMvC;p1P{7J@Ll2 zsLwh`=BTX%Oi4~fZLu}87PF~=w|mdD$^^?C>1k~%S2n8!bG_g5Z1B{2H8+DNielFe z`6f~(z4^iuEw6RiN)a;U{EygT1Vw^l!um8z>mw5|UWZ&^!x$)FfPcu9+29*lB#x9E=TJnUXQvswk0W3exV@}WO8}dA}e(*yGv4Ef-C&|-DL?Vvk zTLVw4`zsfu(2tw6`C%ka9LFtgtue-LAqW@7yB(ZcxA92YKJpuKYuYtpx2LDc=8;G#|7)pK{?tA9-k<&M z_n+V+AO0z_+023@dY6rAwaT77dl;XXV$IrhoIG)YU-^|^e!IQB{hEyw#B5te>!zj# z2?>1s_=(>d7##XHZ@u+4)g3>OAZ`3_{Wt>^{2agdNA75e;tJdL;77YJl|(# zdYUI5f1E2Wv8%Z7^1n173<-G8k^8V%X!{-wm5+o2RLIDym*8^B| zLW@o?aD#2p3+XFRFREB51mv$8&TiQ)DDPrSr)-V_pm}~N*IU~UV)o=|5G%KE0vHp3$_hn$e7^BAS z`3+RFK+}%FYW7#H;MHx;VD%F9UC+gNQqUjjd7yFYSS=Uq2(|PnCmVeu#RNxtt9h;T zW20c?4mUoGgtUSf)l8}rGDLu1?AY|BMc_t8A>a_!N^5X1hlGb)%!yyyB=gOF3tDji z3wQq_W9G^Bm>}ZFubj`1{XRGQRh@F1Z}pE3By!v_6LtJTC1s(-@9r6QOON8&FRaUl zs9kF2!efC85wWNbIHe#T-{Jh{b)UmgPMg=-eQe-^y44>V_+QO?rtb6YY+#`Re1dEl zpN@G~%Ny13gsZToGH=aUkB4~-(6C{xx6O&2F0;-4Q%&mw`L@H49;cT~get?*8db#WqCwO#R=Zj#RF zH@&*e{-O&=+vTFWyy{WD=Q?j2F&h``YisF=wZ*_NC}>7J84d%?<=#U#IUS{51qUf# zwZfuQ>Zj&_E&lf46LJ0l3fK_>_8%X*#b(m1n=g5ODzOWSk%uJND90sYWn7t+DZW!JEX>F( z?kp`h$H*}t)*0ic?P%>Vin|FqGX=?ndnKKv@CcXaZ`Hf@ItnW~BHyaQq2!cYF{>{KD+yU+V2xDHHvrb z-L$rxK4xoW3Nb$W4C2W3zWs5EpmPcO4fT4M12Z$R`@kYNM!WjnO+LEYUo(rnFxbp~ z%`5D!lG`TTFBM00JukUsG>L|Dp+7iMDiHRDx-1B?dRT=#w12wG1zkD2lN(z^Rmtl2 znPrG(eO=V)&-%%uoiwV2`5_+q>6eahfyG|=~y&fiu%ly}%I9Oh#{!{PSP z6Vqb5F5dW?!tfx*XwkM%xgKe#QNi(np5pabQ8r^cmPZ%o)7?ZKZ%b48;VN1WFB29^ zP)-@~