VSTi-плагин ASIO-хоста для подключения входа дополнительного ASIO-драйвера в DAW

Столкнулся я как-то с ситуацией, когда, при наличии основной звуковой карты со своим ASIO-драйвером, необходимо было в DAW подключить USB-микрофон со своим ASIO-драйвером. А DAW не поддерживает подключение двух ASIO-драйверов одновременно. Поиском наткнулся на VST-плагин «VST interfaced ASIO-Host», написанный на Delphi. К сожалению, хоть и плагин увиделся через jBridge, нормально он так и не заработал. Таким образом, пришлось писать подобный плагин самому. В итоге, за 10 дней был написан плагин ASIOInput с открытым исходным кодом. В этой статье я расскажу о некоторых особенностях его разработки и архитектуры.

d5e925698cb5fc75b76624100d744a97.png

Сам VST-плагин технически является .dll, которая экспортирует единственную функцию

SVSTPlugin* VSTPluginMain(void*)

Эту функцию вызывает DAW и получает указатель на структуру данных, в которой хранится вся информация о загруженном плагине. Чтобы плагин функционировал, нужно заполнить как минимум следующие поля.

  • InputCount и OutputCount — количество входных и выходных каналов. Количество каналов — это строго заданная величина на этапе загрузки плагина, и в дальнейшем она меняться не может. Поэтому здесь я задаю 0 входных каналов и 2 выходных канала. Если потребуется 1 реальный канал, то он будет продублирован по обоим выходам плагина.

  • PluginProperties — флаги свойств плагина. Здесь задаем 3 флага: наличие собственного интерфейса, поддержка колбэка UpdateBufferData () и то, что плагин является инструментом — в некоторых DAW для инструментов заводятся отдельные дорожки в микшере, что удобно.

  • Колбэк RequestFromHost () — эту функцию будет вызывать DAW, чтобы проинформировать плагин о некоторых событиях. Минимально реализованный в плагине набор событий следующий: инициализация и де-инициализация плагина, приостановка и возобновление работы плагина, изменение частоты семплирования, изменение размера буфера семплов, а также события для графического интерфейса плагина: показать и скрыть окно редактора, получить размер окна редактора, и событие простоя — для перерисовки элементов интерфейса окна редактора.

  • Колбэк UpdateBufferData () — DAW вызывает эту функцию, когда ему нужно получить значения очередного набора звуковых семплов. Здесь же, в параметрах, указывается, сколько именно семплов хочет получить DAW при каждом вызове этой функции.

Дочерний же ASIO драйвер является Windows COM-объектом. Доступ к нему осуществляется по GUID-ам. Список всех ASIO-драйверов (имена и GUID-ы) хранится в реестре по адресу HKEYLOCALMACHINE\SOFTWARE\ASIO. Для работы с ASIO-драйвером, после инициализации COM-объекта по GUID-у функцией CoCreateInstance (), нужно инициализировать сам ASIO-драйвер, создать буферы для семплов, и непосредственно запустить ASIO-драйвер. Причем при вызове функции создания буферов семплов, необходимо также передать указатели на колбэки обновления данных в буфере и запросов от ASIO-драйвера к хосту.

Таким образом получается, что у плагина вызываются извне две функции: DAW вызывает функцию, где плагин должен заполнить данные выходных семплов от плагина в DAW — VSTPluginCallUpdateBufferData (), и дочерний ASIO-драйвер вызывает функцию, где отдает плагину очередную порцию данных семплов со входа звуковой карты — ASIOHostCallUpdateBufferDataEx (). Причем эти функции вызываются независимо друг от друга. А в случае, если размеры буферов семплов DAW и дочернего ASIO-драйвера разные, то еще и разное количество раз в секунду. Нам же необходимо передать данные от дочернего ASIO-драйвера в DAW, причем сделать это с минимальной задержкой.

В случае с одинаковым размером буфера семплов я применил синхронизацию двух вызовов. Таким образом, при запросе DAW-ом семплов от плагина, вызов функции приостанавливается до ближайшего вызова из дочернего ASIO-драйвера, в котором происходит обновление буфера DAW напрямую, только после чего функция запроса DAW-ом семплов от плагина отпускается и завершается. Технически синхронизация обеспечивается ожиданием событий ОС Windows (функции CreateEvent (), SetEvent () и WaitForSingleObject ()).

Если же размер буферов DAW и дочернего ASIO-драйвера разный, то применяется кольцевой буфер. Колбэк дочернего ASIO-драйвера записывает данные в кольцевой буфер, а колбэк DAW-а считывает данные. Чтобы чтение не происходило во время записи, используется мютекс OC Windows. Размер кольцевого буфера выбирается как максимальный из размеров буферов DAW и дочернего ASIO-драйвера, и может настраиваться пользователем х2, х3 и х4.

61ff6f494f89e3d2b848b56989aecf20.png

Подводные камни при разработке плагина.

  • У COM-объекта ASIO-драйвера все функции возвращают 0 в случае успеха. Все, кроме функции инициализации. Она, в случае успеха, возвращает 1. Минус один вечер отладки.

  • DAW передает в плагин дескриптор родительского окна. Если после события скрытия окна редактора, не снять привязку окна редактора с этим дескриптором, то окно редактора плагина продолжает отображаться, но, в случае с DAW Cubase 9.5, поверх системного меню DAW — т.е. поверх «Файл, Правка» и т.д.

  • Если при создании буферов семплов указать ASIO-драйверу некорректное значение, то буферы создадутся с нужными ASIO-драйверу корректными размерами, но частота вызова колбэка будет рассчитана некорректно, и колбэк будет вызываться неверное количество раз в секунду. При этом функция создания буферов вернет 0. Еще минус один вечер отладки.

  • Не стоит де-инициализировать COM-объект дочернего ASIO-драйвера из колбэка самого ASIO-драйвера.

Скриншот ранней версии плагина с дополнительной отладочной информацией.

fe27717a4967e5b10c4d3ff26ac0e46b.png

Скачать последнюю версию плагина (VST2, x86 и x64): тыц.

Исходники на гитхабе: тыц.

© Habrahabr.ru