Звуковий сервер jack. Пишемо найпростіший клієнт: практика
Відео: Програмування на Java для початківців # 16 (Client-Server)
Отже, ми створюємо простий клієнт, який вміє: - Завантажувати хвильової файл (наприклад, WAV або FLAC). - Підключатися до вхідних портів JACK. - В петлі (тобто, зацикленість) відтворювати долучення на вхідний порт JACK.
Даний код в недалекому минулому навіть входив у набір програм для мого мікшерного пульта, оформленого з використанням техніки stickerbomb (https://parazitakusok.ru/). Так що це цілком самостійні продукт, який цілком зможе стати справжнім старт-апом для музиканта!
Завантаження файлів покладемо на плечі бібліотеки libsndfile - все її використовують, не гріх і нам! Ось файл Qmake-проекту для програми - назвемо її test01:
У цьому файлі ми додали в змінну CONFIG ключ link_pkgconfig, який дозволить підключати бібліотеки через механізм pkgconfig. Далі все парою рядків підключаємо до нашої програми бібліотеки JACK і libsndfile:
PKGCONFIG + = sndfile jack
Тепер перейдемо до коду самої програми. Це буде консольний додаток, тому обійдемося тільки одним файлом -main.cpp. Мається на увазі, що ви і так вмієте компілювати Qt-програми, проте про всяк випадок - зібрати додаток можна буде, давши дві команди:
qmake 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 () lt; lt; "JACK error:" lt; lt; 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 () lt; lt; "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 () lt; lt; "Can not activate client" - return l; } // Масив для списку портів const char ** ports; // Отримуємо спісок- запитуємо у JACK тільки фізичні //(т.е. порти звукової карти) і тільки вхідні - з нашого боку // вони послужать для виведення звуку ports = jack _ get _ ports (client, NULL, NULL, JackPortIsPhysical | JackPortIsInput); // Якщо JACK не повернув порти - повідомляємо про помилку і виходимо if (! Ports) { qDebug () lt; lt; "Can not find physical playback ports" - exit (1); } // З`єднуємо наші клієнтські порти з першими двома «серверними» // портами зі списку і повідомляємо про помилки в разі невдачі if (jack _connect (client, jack _port _name (Out _left), ports [O])! = O) qDebug () lt; lt; "Can not connect output port O" - if (jack _connect (client, jack _port _name (Out _ right), ports [l])! = O) qDebug () lt; lt; "Can not 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 ++ -
Усе! А якби у нас був моно сигнал, то треба в ітерації один і той же семпл копіювати в обидва вихідних буфера: в лівий і правий, не збільшуючи лічильник між цим копіюванням. Ось так:
Розглянутий у статті приклад - відправна точка для подальшого програмування з використанням JACK і, треба думати, libsndfile: якщо вашій програмі потрібно читання / запис звукових файлів, то кращої бібліотеки годі й шукати. Тим більше, що вона є практично у всіх дистрибутивах Linux, а також для Windows, OpenBSD, Solaris, QNX, Mac OS X і навіть Irix. Очевидними зручностями libsndfile є простота API і можливість читання відразу в формат з плаваючою точкою - в буфер типу float *. Писати з нуля свою бібліотеку вводу-виводу звукових файлів - заняття захоплююче і розвиваюче, але корисно знати і про вже готовому рішенні.
Наостанок, деякі зауваження. Якщо у вашому JACK-клієнті потрібно мікшування декількох звукових сигналів, то робити це слід саме в функції process. Мікшування полягають в додаванні семплів і впливу на гучність результату - адже чим більше семплів одночасно складається, тим гучнішим буде підсумковий семпл. Для роботи з гучністю є окремі алгоритми, хоча на розум перш за все приходить найпростіше: розділити значення семпли на кількість складених каналів. Цей спосіб хоч і дієвий, проте зіпсує звучання самим мерзенним чином. Про «окремих алгоритмах» докладно розповідати не буду - лише нагадаю, що для зменшення рівня сигналу треба множити його на дробове число, а для збільшення - ділити на дробове.
Також слід звернути увагу на частоту оцифровки ваших звукових даних і частоту, в якій працює сервер. Взагалі по ідеї все звуки ви повинні на ходу переоціфровивать в якусь загальну вихідну частоту. JACK замість вас не буде цього робити. Благо, є libsamplerate (https://mega-nerd.com/SRC) від того ж Еріка де Кастро Лопо (Erik de Castro Lopo), автора libsndfile. Успіхів!