Приятное с полезным или разработка под ASIO на C++

nusaiuizluygxwnduxrle_plq_g.jpeg

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

В чем проблема


Я решил использовать ASIO, так как драйвер популярен и удобен, но здесь я столкнулся с проблемой: очень мало статей по теме разработки под данную технологию. По сути, все, чем я мог руководствоваться в самом начале — официальная документация к SDK и несколько англоязычных форумов. Я надеюсь, что если вы имеете подобную проблему, то статья будет хоть немножко полезной для вас.

Библиотека Bass


Код с оффсайта Stainberg с использованием SDK, который просто проигрывает тишину в течении 20 секунд, занимает 500 строк. Однако существует прекрасная библиотека Bass. Любители поработать со звуком знают, как с ее помощью можно создавать звуковые потоки и каналы, как накладывать FX эффекты, микшировать… я на это горячо надеюсь. У Bass есть аддон Bassasio, который, по сути, за несколько строк кода позволяет настроить драйвер ASIO и выполнить идентичную примеру выше задачу.

Установка и настройка библиотек


  1. Качаем библиотеку Bass и ее аддон Bassasio. Это оффсайт
  2. Скидываем .lib файлы в отдельную папку. В данном случае, файлы bass.lib и bassasio.lib я скинул в папку Libs
  3. .dll файлы переносим в папки Release и Debug. (bass.dll и bassasio.dll) Если этого не сделать, то при компиляции кода вашего приложения, программа выдаст ошибку вроде этой:

    5mywe0v4rjjihr1gzqnn4vgjgyg.png

  4. Далее в настройках проекта прописываем компоновщику путь к папке с .lib файлами. То же самое делаем для с/с++.

    ptcp-_ad1tn3cjasbq83zvfb0te.png

    uxx6scmdjlwuf4bjanek2avhbl8.png

  5. Скидываем .h файлы в папку проекта и инклюдим их в проект, а именно файлы bass.h и bassasio.h.
    #include "bass.h"
    #include "bassasio.h"
    


Пример использования


Для примера, создадим небольшое консольное приложение, которое будет проигрывать звук гитары, т.е. программа будет перенаправлять поток из звукового входа на звуковой вывод. Код приложения доступен на гитхабе.Вроде бы все просто. Должно получиться подобие гитарного комбоусилителя (воткнул и играй!), но звук пока что будет чистым (без эффектов).

Библиотеки мы подключили в предыдущем пункте, теперь нужно инициализировать устройства звукового ввода и вывода. Начнем!

BASS_Init () инициализирует устройство звукового вывода, и принимает на вход следующие параметры:

Параметры
int device — идентификатор устройства вывода. Если установить параметр -1, то будет использовано стандартное устройство вывода, если 0 — звук не будет проигрываться вовсе, т.е. не будет предоставлен ни одному устройству вывода.

DWORD freq — частота дискретизации в Гц

DWORD flags — флаги… Их много, прочитать подробнее можно в официальной документации www.un4seen.com/doc/#bass/BASS_Init.html. Если кратко, с помощью флагов можно выбрать глубину кодировния звука, моно или стерео, воспроизведение звука более чем двумя динамиками и т.д.

HWND win — устанавливает основное окно приложения. Для консольных приложений следует установить 0.

GUID *clsid — класс для инициализации объекта, который будет использован для инициализации DirectSound. В остальных случаях устанавливаем Null.

С инициализацией ASIO драйвера все немного попроще. В функцию BASSASIO_Init передаем только 2 параметра:

Параметры
int device — идентификатор «девайса»… технологии, с которой будет работать библиотека. Все дело в том, что если у вас установлено такое ПО, как guitar rig или у вас есть звуковая карта со своим драйвером ASIO, в списке доступных «девайсов» вы увидите несколько пунктов. Список можно получить вызывая для каждого идентификатора (перебирая их) функцию BASS_ASIO_GetDeviceInfo (). Как правило, 0 — наш драйвер ASIO4ALL, который мы и будем использовать в дальнейшем. Значение -1 установит устройство по-умолчанию, об этом уже сказано в документации.
std::cout << "ASIO Devices info:" << std::endl;
a = 0; count = 0;
BASS_ASIO_DEVICEINFO asio_info;
for (a = 0; BASS_ASIO_GetDeviceInfo(a, &asio_info); a++)
	std::cout << "Device " << a << ") " << asio_info.name << std::endl;
std::cout << " ________ " << std::endl;

xrptrepz9w2na1soohx9n8ez1io.png
Список драйверов

DWORD flags — флаги. Их всего 2: BASS_ASIO_THREAD — запускать драйвер в отдельном потоке и BASS_ASIO_JOINORDER — отвечает за порядок работы каналов channels.

Код, инициализации устройства звукового вывода и драйвера ASIO:

try {
	if ( ! BASS_Init(0, 44100, 0, 0, NULL) )
		throw BASS_ErrorGetCode();
	if ( ! BASS_ASIO_Init( 0, NULL ) )
		throw BASS_ASIO_ErrorGetCode();
}
catch ( int err ) {
	std::cout << "Err no - " << err << std::endl;
	system("pause");
	return;	
}


Может возникнуть вопрос: почему в BASS_Init устройство вывода не используется (0). Дело в том, что звуковой вывод будет производиться через ASIO с помощью аддона BASSASIO. Стандартный звуковой вывод с помощью библиотеки BASS нам для реализации поставленной задачи совершенно не нужен — поэтому 0. Однако стоит отметить пару моментов:

  1. Можно осуществить работу драйвера ASIO на одном устройстве и вывод звука стандартными средствами на другом (скажем… будет играть музыка). Для этого достаточно выбрать разные устройства при инициализации BASS и настройке драйвера ASIO (Во время работы будет его иконка в области уведомлений).
  2. Чтобы получить список доступных устройств используйте BASS_GetDeviceInfo.
    setlocale(LC_ALL, "Rus");
    
    std::cout << "Devices info:" << std::endl;
    int a, count = 0;
    BASS_DEVICEINFO info;
    for (a = 1; BASS_GetDeviceInfo(a, &info); a++) {
    	if (info.flags&BASS_DEVICE_ENABLED) // device is enabled
    		std::cout << "Device " << a << ") " << info.name << " is availible" << std::endl;
    else
    	std::cout << "Device " << a << ") " << info.name << " is unable" << std::endl;
    }
    std::cout << " ________ " << std::endl;
    

    udbctanifrlzwz-5qxwhvf2q_cq.png

    BASS работает только с устройствами, активными на данный момент. Отключенные устройства попросту не будут видны.

    r3cojmwyulzibmp3iurlkv7pc-i.png
    Активные устройства звукового вывода

    BASSASIO абсолютно все равно на то, было ли отключено устройство операционной системой или нет. В настройках драйвера выбирайте нужные вам устройства звукового ввода и вывода.

  3. Нет смысла пытаться выводить звук с помощью BASS и BASSASIO одновременно в одно устройство — технология ASIO расчитана именно на то, что звук не будет микшироваться операционкой и пойдет сразу на звуковую карту для последующего вывода. Т.е. вы услышите только звук от приложения, использующего ASIO.


Далее, нам необходимо звуковой поток с микрофона направить на устройство звукового вывода. Это можно сделать несколькими способами. Самый просто — «отзеркалить» канал.

Мы имеем 3 канала: микрофон, левый наушник и правый наушник. Чтобы сигнал с микрофона перенести на каналы вывода, воспользуемся функцией

BOOL BASS_ASIO_ChannelEnableMirror(
    DWORD channel - идентификатор канала
    BOOL input2 - входной или выходной? (0 или 1)
    DWORD channel2 – идентификатор исходного канала (с сигналом)
);


Здесь все просто. Нам нужно передать сигнал в output-каналы 0 и 1 из input-канала 0:

BASS_ASIO_ChannelEnableMirror( 0, 1, 0 );
BASS_ASIO_ChannelEnableMirror( 1, 1, 0 ); 


Тогда возникает вопрос: откуда я узнал в какие каналы мне нужно передавать сигнал. Ответ — информацию по каналам можно узнать с помощью функции BASS_ASIO_ ChannelGetInfo.

a = 0; BASS_ASIO_CHANNELINFO channel_info;
std::cout << "inputs: " << std::endl;	
for (a = 0; BASS_ASIO_ChannelGetInfo(0, a, &channel_info ); a++ )
	std::cout << a << ") " << channel_info.name << " format: " << channel_info.format << std::endl;	
std::cout << "Outputs: " << std::endl;
for (a = 0; BASS_ASIO_ChannelGetInfo(1, a, &channel_info); a++)
	std::cout << a << ") " << channel_info.name << " format: " << channel_info.format << std::endl;
std::cout << "__________" << std::endl;

bew9h9ti4r0p9ddjbhyyfxk8oyg.png

Все настройки готовы. Старт — BASS_ASIO_Start. Можно дать на вход параметры для максимальной длины сэмпла и количества потоков, но для нашей задачи можно оставить эти параметры по-умолчанию (заполним нулями).

BASS_ASIO_Start( 0, 0 ); 


Фактически, это все. Однако, это консольное приложение, не забываем про system («pause»), чтобы оно не закрывалось сразу после запуска, и сразу после этого, освобождаем память и прекращаем работу с устройствами и драйвером.

BASS_ASIO_Stop();
BASS_ASIO_Free();
BASS_Stop();
BASS_Free();
return;


Итоги


Исходный код оставлю на гитхабе.
Вот мы и получили простое консольное приложение, использующее технологию ASIO. Подключая инструмент к звуковой карте, мы не услышим той задержки, что могла быть при стандартном подключении с помощью стандартных средств Windows. Конечно, возможности как библиотеки BASS, так и аддона BASSASIO шире представленных выше, однако не хватит ни формата статьи, ни моих знаний, чтобы изложить абсолютно все.
Если эта тема была для вас хоть немного интересной — был рад стараться. В свою очередь постараюсь украсть немного времени для написания продолжения, в котором мы сможем работать со стандартными FX-эффектами из DX8 и получим подобие простенького гитарного процессора.

Спасибо за внимание!

© Habrahabr.ru