Звуковой сервер JACK. Пишем простейший клиент: практика
Итак, мы создаем простой клиент, который умеет:
- Загружать волновой файл (например, WAV или FLAC).
- Подключаться ко входным портам JACK.
- В петле (то есть, зацикленно) воспроизводить загруженный файл на входной порт JACK.
Данный код в недалеком прошлом даже входил в набор программ для моего микшерного пульта, оформленного с использованием техники stickerbomb (http://parazitakusok.ru/). Так что это вполне самостоятельны продукт, который вполне сможет стать настоящим старт-апом для начинающего музыканта!
Загрузку файлов возложим на плечи библиотеки libsndfile - все её используют, не грех и нам! Вот файл Qmake-проекта для программы - назовем её test01:
---------------testOl.pro---------------
TARGET = testOl CONFIG += console
link _ pkgconfig
CONFIG -= app _ bundle
TEMPLATE = app
SOURCES += main.cpp
PKGCONFIG += sndfile jack
TARGET = testOl CONFIG += console
link _ pkgconfig
CONFIG -= app _ bundle
TEMPLATE = app
SOURCES += main.cpp
PKGCONFIG += sndfile jack
В этом файле мы добавили в переменную CONFIG ключ link_pkgconfig, который позволит подключать библиотеки через механизм pkgconfig. Далее всего парой строк подключаем к нашей программе библиотеки JACK и libsndfile:
PKGCONFIG += sndfile
jack
jack
Теперь перейдем к коду самой программы. Это будет консольное приложение, поэтому обойдемся только одним файлом -main.cpp. Подразумевается, что вы и так умеете компилировать Qt-программы, однако на всякий случай - собрать приложение можно будет, дав две команды:
qmake
make
make
Теперь - код самого приложения. Вначале приведу его целиком, с второстепенными комментариями, а самые важные вещи мы разберем потом отдельно. И да простит меня читатель за плохой стиль программирования - множество глобальных переменных и тому подобное. Сейчас нам важна простота изложения.
-----------main.cpp-----------
#include
#include
//Подключаем sndfile и JACK
#include
#include
#include
//Задаем путь к файлу, который будем играть
#define fname _ test "/полный _ путь _ к/test.wav"
//Клиентские JACK-порты под левый и правый каналы
jack _ port _t *out_left;
jack _ port _ t *out _ right;
//Буфер для волновых данных, сюда мы прочитаем
WAV float *buffer;
//А сюда - сведения о нем
SF _INFO info;
//Счетчик текущего места в буфере int offset; //Размер буфера int buffer _ len;
//Функция, которая читает весь WAV и возвращает данные из него //в виде буфера, а сведения о файле - в структуру типа SF _INFO float* load _ whole _ sound (const char *fname, SF _ INFO &sf) {
//Откроем файл
SNDFILE *file = sf_open (fname, SFM _ READ, &sf);
//Выделим память под буфер в размере, равном количеству каналов,
//умноженному на количество кадров
float *buffer = new float [sf.channels * sf.frames];
//Прочитаем из файла в буфер
sf _ readf _float (file, buffer, sf.frames);
//Закроем файл
sf _ close (file);
//И возвратим буфер
return buffer;
}
//Эта функция - сердце программы, его мы еще обсудим int process (jack _nframes _t nframes, void *arg) {
float *outl = (float *) jack _ port _ get _ buffer
(out _ left, nframes);
float *outr = (float *) jack _ port _ get _ buffer
(out _ right, nframes);
for (jack _ nframes _ t i = O; i buffer _len) offset = O;
outl[i] = buffer[offset]; offset++;
outr[i] = buffer[offset];; offset++;
}
return O;
}
//Еще одна callback-функция, JACK вызовет её в случае ошибки
void error (const char *desc)
{
qDebug() << "JACK error: " << desc;
}
int main(int argc, char *argv[]) {
QCoreApplication a (argc, argv); //Читаем файл в буфер
buffer = load _ whole _ sound (fname _ test, info);
//Вычисляем длину буфера
buffer _ len = info.frames * info.channels;
//Сбрасываем в ноль счетчик смещения в буфере offset = O;
//Устанавливаем обработчик ошибок jack _ set _ error _ function (error);
//Пытаемся создать новый JACK-клиент
jack _client _t *client = jack _client _new ("testOl");
//Если не вышло - сообщаем и выходим if (! client) {
qDebug() << "jack server is not running"; return l;
}
//Устанавливаем callback, которая дает JACKV порции сигнала
//из буфера
jack _ set _ process _ callback (client, process, O);
//Регистрируем клиентские порты, которые мы подключим
//к входным порта сервера
out _left = jack _ port _register (client, "left",
JACK _ DEFAULT _ AUDIO _ TYPE, JackPortIsOutput, 0);
out _ right = jack _ port _ register (client, "right",
JACK _DEFAULT _AUDIO _TYPE, JackPortIsOutput, O);
//Пытаемся запустить клиент при этом клиент запускаем
//ДО подключения портов
if (jack _activate (client) != O) {
qDebug() << "cannot activate client"; return l;
}
//Массив для списка портов const char **ports;
//Получаем список; запрашиваем у JACK только физические
//(т.е. порты звуковой карты) и только входные - с нашей стороны
//они послужат для вывода звука
ports = jack _ get _ ports (client,
NULL, NULL,
JackPortIsPhysical | JackPortIsInput);
//Если JACK не вернул порты - сообщаем об ошибке и выходим if (! ports) {
qDebug() << "Cannot find physical playback ports"; exit (1);
}
//Соединяем наши клиентские порты с первыми двумя «серверными»
//портами из списка и сообщаем об ошибках в случае неудачи
if (jack _connect (client, jack _port _name
(out _left), ports[O]) != O)
qDebug() << "cannot connect output port O"; if (jack _connect (client, jack _port _name
(out _ right), ports[l]) != O)
qDebug() << "cannot connect output port l";
//Освобождаем память от списка серверных портов free (ports);
//Входим в цикл из 13 итераций во время этого цикла сервер JACK
//будет вызывать нашу callback-функцию process
for (int i = 0; i buffer _len) offset = 0;
#include
#include
//Подключаем sndfile и JACK
#include
#include
#include
//Задаем путь к файлу, который будем играть
#define fname _ test "/полный _ путь _ к/test.wav"
//Клиентские JACK-порты под левый и правый каналы
jack _ port _t *out_left;
jack _ port _ t *out _ right;
//Буфер для волновых данных, сюда мы прочитаем
WAV float *buffer;
//А сюда - сведения о нем
SF _INFO info;
//Счетчик текущего места в буфере int offset; //Размер буфера int buffer _ len;
//Функция, которая читает весь WAV и возвращает данные из него //в виде буфера, а сведения о файле - в структуру типа SF _INFO float* load _ whole _ sound (const char *fname, SF _ INFO &sf) {
//Откроем файл
SNDFILE *file = sf_open (fname, SFM _ READ, &sf);
//Выделим память под буфер в размере, равном количеству каналов,
//умноженному на количество кадров
float *buffer = new float [sf.channels * sf.frames];
//Прочитаем из файла в буфер
sf _ readf _float (file, buffer, sf.frames);
//Закроем файл
sf _ close (file);
//И возвратим буфер
return buffer;
}
//Эта функция - сердце программы, его мы еще обсудим int process (jack _nframes _t nframes, void *arg) {
float *outl = (float *) jack _ port _ get _ buffer
(out _ left, nframes);
float *outr = (float *) jack _ port _ get _ buffer
(out _ right, nframes);
for (jack _ nframes _ t i = O; i buffer _len) offset = O;
outl[i] = buffer[offset]; offset++;
outr[i] = buffer[offset];; offset++;
}
return O;
}
//Еще одна callback-функция, JACK вызовет её в случае ошибки
void error (const char *desc)
{
qDebug() << "JACK error: " << desc;
}
int main(int argc, char *argv[]) {
QCoreApplication a (argc, argv); //Читаем файл в буфер
buffer = load _ whole _ sound (fname _ test, info);
//Вычисляем длину буфера
buffer _ len = info.frames * info.channels;
//Сбрасываем в ноль счетчик смещения в буфере offset = O;
//Устанавливаем обработчик ошибок jack _ set _ error _ function (error);
//Пытаемся создать новый JACK-клиент
jack _client _t *client = jack _client _new ("testOl");
//Если не вышло - сообщаем и выходим if (! client) {
qDebug() << "jack server is not running"; return l;
}
//Устанавливаем callback, которая дает JACKV порции сигнала
//из буфера
jack _ set _ process _ callback (client, process, O);
//Регистрируем клиентские порты, которые мы подключим
//к входным порта сервера
out _left = jack _ port _register (client, "left",
JACK _ DEFAULT _ AUDIO _ TYPE, JackPortIsOutput, 0);
out _ right = jack _ port _ register (client, "right",
JACK _DEFAULT _AUDIO _TYPE, JackPortIsOutput, O);
//Пытаемся запустить клиент при этом клиент запускаем
//ДО подключения портов
if (jack _activate (client) != O) {
qDebug() << "cannot activate client"; return l;
}
//Массив для списка портов const char **ports;
//Получаем список; запрашиваем у JACK только физические
//(т.е. порты звуковой карты) и только входные - с нашей стороны
//они послужат для вывода звука
ports = jack _ get _ ports (client,
NULL, NULL,
JackPortIsPhysical | JackPortIsInput);
//Если JACK не вернул порты - сообщаем об ошибке и выходим if (! ports) {
qDebug() << "Cannot find physical playback ports"; exit (1);
}
//Соединяем наши клиентские порты с первыми двумя «серверными»
//портами из списка и сообщаем об ошибках в случае неудачи
if (jack _connect (client, jack _port _name
(out _left), ports[O]) != O)
qDebug() << "cannot connect output port O"; if (jack _connect (client, jack _port _name
(out _ right), ports[l]) != O)
qDebug() << "cannot connect output port l";
//Освобождаем память от списка серверных портов free (ports);
//Входим в цикл из 13 итераций во время этого цикла сервер JACK
//будет вызывать нашу callback-функцию process
for (int i = 0; i buffer _len) offset = 0;
Переменная offset хранит в себе текущую позицию в буфере. Если позиция больше размера буфера, мы сбрасываем ее в ноль. Таким образом, воспроизведение буфера начнется с нулевого сэмпла - это и есть «петля». Теперь - следующие строчки цикла. Помещаем сэмпл из звукового буфера в буфер левого порта:
outl[i] = buffer[offset];
Смещаем offset на 1, потому что сэмпл правого канала идет в следующей ячейке буфера:
offset++;
Берем сэмпл правого канала, кладем в буфер правого канала:
outr[i] = buffer[offset];
Снова увеличиваем счетчик, чтобы в следующей итерации опять взять сэмпл из левого канала:
offset++;
Смещаем offset на 1, потому что сэмпл правого канала идет в следующей ячейке буфера:
offset++;
Берем сэмпл правого канала, кладем в буфер правого канала:
outr[i] = buffer[offset];
Снова увеличиваем счетчик, чтобы в следующей итерации опять взять сэмпл из левого канала:
offset++;
Всё! А если бы у нас был монофонический сигнал, то надо в итерации один и тот же сэмпл копировать в оба выходных буфера: в левый и правый, не увеличивая счетчик между этим копированием. Вот так:
outl[i] = buffer[offset];
outr[i] = buffer[offset];; offset++;
outr[i] = buffer[offset];; offset++;
Рассмотренный в статье пример - отправная точка для дальнейшего программирования с использованием JACK и, надо полагать, libsndfile: если вашей программе нужно чтение/запись звуковых файлов, то лучшей библиотеки не найти. Тем более, что она есть практически во всех дистрибутивах Linux, а также для Windows, OpenBSD, Solaris, QNX, Mac OS X и даже Irix. Очевидными удобствами libsndfile являются простота API и возможность чтения сразу в формат с плавающей точкой - в буфер типа float*. Писать с нуля свою библиотеку ввода-вывода звуковых файлов - занятие увлекательное и развивающее, но полезно знать и об уже готовом решении.
Напоследок, некоторые замечания. Если в вашем JACK-клиенте нужно микширование нескольких звуковых сигналов, то делать это следует именно в функции process. Микширование заключаются в сложении сэмплов и влиянию на громкость результата - ведь чем больше сэмплов одновременно складывается, тем более громким будет итоговый сэмпл. Для работы с громкостью есть отдельные алгоритмы, хотя на ум первым делом приходит простейшее: разделить значение сэмпла на количество сложенных каналов. Этот способ хоть и действенный, однако испортит звучание самым гнусным образом. Об «отдельных алгоритмах» подробно рассказывать не буду - лишь напомню, что для уменьшения уровня сигнала надо умножать его на дробное число, а для увеличения - делить на дробное.
Также следует обратить внимание на частоту оцифровки ваших звуковых данных и частоту, в которой работает сервер. Вообще по идее все звуки вы должны на ходу переоцифровывать в некую общую выходную частоту. JACK вместо вас не будет этого делать. Благо, есть libsamplerate (http://www.mega-nerd.com/SRC) от того же Эрика де Кастро Лопо (Erik de Castro Lopo), автора libsndfile. Удачи!