EasyEffects не нужно, или PipeWire для продвинутых
Posted on Чт 23 января 2025 in misc
ВСТУПЛЕНИЕ
Звуковая система PipeWire принесла, без преувеличения, революцию в мир десктопно-мультимедийного Linux. Наконец то эта ОС получила продуманную, сделанную по уму понимающими в теме людьми, и сделанную качественно звуковую систему. Многие ее возможности присутствовали и раньше, в других более старых звуковых системах, но в PipeWire они собраны воедино и сформировали целую единую платформу, на которой можно реализовать огромное количество самых разных задач.
Но в пользовательской среде, эта революция практически не заметна, часто можно встретить мнение вида «ну играет ютуб и играет, мне хватает». Поэтому я решил сделать небольшой цикл статей, посвященный более продвинутому использованию PipeWire, раскрывающему его, без преувеличения, поразительные возможности. (Ну и чтобы изучить их самому в процессе, хехехе).
ЧАСТЬ 1
Начнем с самого простого и нужного. На ноутбуках мы часто или практически всегда имеем плохие динамики, которые требуют эквалайзера. Для пользователя удобнее всего, если такой эквалайзер будет системным, независимым от приложения, потому что, ну попробуйте добавить эквалайзер в Google Chrome )) да и синхронизировать настройки эквалайзера по всем приложениям никому не нужно.
Здесь выходит на сцену известное приложение EasyEffects, которое предоставляет и эквалайзер, и много чего кроме него, как раз общесистемно. НО! Если вы пробовали его использовать, то видите сразу его недостатки -- это довольно тяжелое для своей задачи приложение, на GTK4, запускается и работает в фоне тоже как GUI приложение со скрытым окном, потребляет излишне много памяти и ресурсов. Заглянув чуть больше под капот, мы увидим еще более пугающие вещи -- EasyEffects принимает на себя задачу линковки (соединения нод в графе звуковой системы), то есть он сам начинает подключать приложения к тем нодам, к каким он хочет. Но, это же задача менеджера сессии PipeWire, то есть WirePlumber!
Проще говоря, EasyEffects берет на себя прямую функцию WirePlumber и они борются друг с другом. Надо ли говорить, что подобная вакханалия и тяжеловесность для простейшей задачи «просто сделать эквалайзер» попадает в полной мере под термин bloatware! Но как же быть?
А что, если я скажу, что PipeWire, сам голый, непосредственно, УЖЕ содержит все возможности для реализации эквалайзера, и не только эквалайзера, а вообще многого, что только может понадобиться делать со звуком? Что если я скажу, что вам достаточно создать один текстовый файл, и САМ PipeWire будет выполнять всю работу, прямо в своем процессе, заменяя не то что EasyEffects, а даже, например, софтовые гитарные процессоры типа Guitarix и KPP?? Для этого не потребуется запуск дополнительных приложений, демонов, хостов плагинов. Только PipeWire. One love. Как тебе такое, ~~Илон Маск~~ R_He_Po6oT?
Сегодня мы сделаем общесистемный эквалайзер. Немного теории, в двух предложениях. Все приложения, работающие с PipeWire, создают ноды -- узлы, через которые идет обмен звуковыми потоками. Через ноду приложение может получать либо выводить звук. Все ноды соединены в граф -- «схему» из нод, соединенных связями. Нода, В КОТОРУЮ можно выводить звук, называется Sink, а ноду, которая сама что-то выдает из себя, назовем Source или Stream.
Тогда, чтобы сделать эквалайзер, нам надо добавить в граф две ноды -- Equalizer Sink к которому все приложения будут подключаться и выводить в него звук, и Stream который будет выдавать обработанный, эквализованный звук в звуковую карту. Звуковая карта -- это тоже нода типа Sink, она уже есть изначально, и по умолчанию все приложения подключаются именно к ней. А мы создадим Equalizer Sink и назначим его «синком по умолчанию». Тогда WirePlumber, менеджер сессии, начнет подключать все приложения на наш Equalizer Sink.
Осталось решить, что же, какое приложение или какая сущность, будет выполнять саму обработку, что будет связано с этими двумя нодами, получать, обрабатывать и отдавать звук. Ноды может создавать любое приложение, используя PipeWire API, что и делает EasyEffects, например. Но мы же хотим от него избавиться! Помимо приложений, это могут делать модули PipeWire. Модули -- это плагины, библиотеки формата .so, загружаемые в процесс PipeWire. В стандартной поставке PipeWire есть модуль filter-chain, который мы рассмотрим подробнее.
filter-chain создает две ноды, Sink и Stream, и обрабатывает звуковой поток, идущий между ними. Это именно то, что нам нужно -- filter-chain позволяет применить к звуку любую обработку. filter-chain позволяет использовать внутри себя плагины LADSPA, плагины LV2, и самое главное -- уже содержит набор встроенных фильтров, для которых не нужно ничего внешнего. Уже понятно, что если мы подключим LV2 плагины из набора LSP например, то можно вытворять со звуком все что угодно, т. к. это студийные плагины для DAW системы профессионального уровня. НО! НО! НО! Оказывается, что даже встроенными эффектами, без дополнительных плагинов вообще, уже можно делать очень многое!
И для начала мы сделаем графический эквалайзер на 15 полос. Для этого, надо создать вот такой конфиг-файл, по адресу ~.config/pipewire/pipewire.conf.d/eq.conf
:
context.modules = [
{ name = libpipewire-module-filter-chain
args = {
node.description = "Equalizer Sink"
media.name = "Equalizer Sink"
filter.graph = {
nodes = [
{
type = builtin
name = eq_preamp
label = bq_highshelf
control = { "Freq" = 0 "Q" = 1.0 "Gain" = 0.0 }
}
{
type = builtin
name = eq_band_1
label = bq_peaking
control = { "Freq" = 25.0 "Q" = 1.7 "Gain" = -10.0 }
}
{
type = builtin
name = eq_band_2
label = bq_peaking
control = { "Freq" = 40.0 "Q" = 1.7 "Gain" = -10.0 }
}
{
type = builtin
name = eq_band_3
label = bq_peaking
control = { "Freq" = 63.0 "Q" = 1.7 "Gain" = -9.0}
}
{
type = builtin
name = eq_band_4
label = bq_peaking
control = { "Freq" = 100.0 "Q" = 1.7 "Gain" = 3.0 }
}
{
type = builtin
name = eq_band_5
label = bq_peaking
control = { "Freq" = 160.0 "Q" = 1.7 "Gain" = 3.0 }
}
{
type = builtin
name = eq_band_6
label = bq_peaking
control = { "Freq" = 250.0 "Q" = 1.7 "Gain" = -2.0 }
}
{
type = builtin
name = eq_band_7
label = bq_peaking
control = { "Freq" = 400.0 "Q" = 1.7 "Gain" = -5.0 }
}
{
type = builtin
name = eq_band_8
label = bq_peaking
control = { "Freq" = 630.0 "Q" = 1.7 "Gain" = -5.0 }
}
{
type = builtin
name = eq_band_9
label = bq_peaking
control = { "Freq" = 1000.0 "Q" = 1.7 "Gain" = -3.0 }
}
{
type = builtin
name = eq_band_10
label = bq_peaking
control = { "Freq" = 1600.0 "Q" = 1.7 "Gain" = -3.0 }
}
{
type = builtin
name = eq_band_11
label = bq_peaking
control = { "Freq" = 2500.0 "Q" = 1.7 "Gain" = -3.0 }
}
{
type = builtin
name = eq_band_12
label = bq_peaking
control = { "Freq" = 4000.0 "Q" = 1.7 "Gain" = -5.0 }
}
{
type = builtin
name = eq_band_13
label = bq_peaking
control = { "Freq" = 6300.0 "Q" = 1.7 "Gain" = -3.0 }
}
{
type = builtin
name = eq_band_14
label = bq_peaking
control = { "Freq" = 10000.0 "Q" = 1.7 "Gain" = 0.0 }
}
{
type = builtin
name = eq_band_15
label = bq_peaking
control = { "Freq" = 16000.0 "Q" = 1.7 "Gain" = 0.0 }
}
]
links = [
{ output = "eq_preamp:Out" input = "eq_band_1:In" }
{ output = "eq_band_1:Out" input = "eq_band_2:In" }
{ output = "eq_band_2:Out" input = "eq_band_3:In" }
{ output = "eq_band_3:Out" input = "eq_band_4:In" }
{ output = "eq_band_4:Out" input = "eq_band_5:In" }
{ output = "eq_band_5:Out" input = "eq_band_6:In" }
{ output = "eq_band_6:Out" input = "eq_band_7:In" }
{ output = "eq_band_7:Out" input = "eq_band_8:In" }
{ output = "eq_band_8:Out" input = "eq_band_9:In" }
{ output = "eq_band_9:Out" input = "eq_band_10:In" }
{ output = "eq_band_10:Out" input = "eq_band_11:In" }
{ output = "eq_band_11:Out" input = "eq_band_12:In" }
{ output = "eq_band_12:Out" input = "eq_band_13:In" }
{ output = "eq_band_13:Out" input = "eq_band_14:In" }
{ output = "eq_band_14:Out" input = "eq_band_15:In" }
]
inputs = [ "eq_preamp:In" ]
outputs = [ "eq_band_15:Out" ]
}
capture.props = {
node.name = "effect_input.eq6"
media.class = Audio/Sink
audio.channels = 2
audio.position=[FL FR]
}
playback.props = {
node.name = "effect_output.eq6"
node.passive = true
audio.channels = 2
audio.position=[FL FR]
}
}
}
]
Разберем его подробнее. name = libpipewire-module-filter-chain
-- подключаем волшебный модуль filter-chain. media.name = "Equalizer Sink"
- название ноды, которое мы увидим в графе pipewire, и в GUI типа pavucontrol или kmix. Далее в массиве nodes перечислены фильтры, через которые пройдет звук. Здесь мы используем эффект eq_preamp как общий регулятор громкости эквалайзера, и 15 раз bq_peaking - каждый из них реализует одну из 15 полос нашего графического эквалайзера. Тут же задаем параметры полос -- Freq это средняя частота полосы в герцах, Q - добротность, Gain - уровень этой полосы в децибелах. Частоты и добротности я задал примерно стандартные для 2/3 октавного эквалайзера (свиснул из qmmp). Значения Gain настройте как угодно, под ваши потребности.
Далее идет массив links - это соединения между нодами эффектов, которых у нас 16. Они должны быть соединены по цепочке, друг за другом, что и сделано.
И в конце, описываются параметры двух нод, которые добавятся в граф PipeWire, о чем я писал выше. capture.props это поисание ноды Equalizer Sink, на которую будут подключаться все пиложения. playback.props -- свойства выходной ноды, которая будет выводить обработанный звук в звуковую карту.
После того, как вы добавите этот конфиг-файл на его место, надо перезапустить демон pipewire или перезагрузиться, или выйти-войти в GUI-сеанс. Перезапустить PipeWire можно командой systemctl --user restart pipewire
.
После перезапуска PipeWire, в pavucontrol на вкладке «Устройства вывода» должно добавиться устройство Equalizer Sink. Если включить птичку «Установить по умолчанию» напротив него, то все приложения начнут выводить звук через наш эквалайзер. Для того, чтобы отключить эквалайзер, надо включить эту птичку на любое другое устройство, то которое там было до добавления эквалайзера, у меня это «Встроенное аудио Аналоговый стерео».
То же самое можно делать через плазмоид регулятора громкости в KDE Plasma. Там добавится устройство Equalizer Sink и можно будет переключить радиокнопку на него.
Основной недостаток этого всего один -- нет GUI для руления настройками! Чтобы их изменить, надо, в простейшем случае на сегодня, изменить числа в конфиге и перезапустить PipeWire.
Вот и все! В следующий раз мы создадим более сложные эффекты, и разберем вопрос, можно ли рулить настройками по-живому, без перезапуска PipeWire.
ЧАСТЬ 2
В первой части мы создали конфиг-файл, который добавляет 15-полосный эквалайзер в граф PipeWire. Теперь разберем вопрос, а как же изменять его параметры на лету, без перезапуска PipeWire. Вопрос очень животрепещущий, редакция завалена письмами с ним. ))
В этой части мы разберем задачу на «низком» уровне CLI-утилит. На этой базе несложно сделать любой GUI-фронтенд, который позволит управлять параметрами уже из GUI.
Сначала получим текущие настройки эквалайзера из PipeWire. Эти настройки хранятся в ноде Equalizer Sink, надо просто знать как их извлечь оттуда. Для этого есть два способа и две CLI утилиты -- pw-cli
и pw-dump
. Начнем с первого варианта, пригодного для ручной работы (но он неудобен для скрипта).
Сначала надо получить номер ноды Equalizer Sink. Для этого выполним команду pw-cli ls Node
. Получаем список всех нод, и в нем видим ноду с именем node.name = "effect_input.eq6"
, это как раз то имя, которое мы задали в конфиг-файле.
id 39, type PipeWire:Interface:Node/3
object.serial = "39"
factory.id = "19"
client.id = "38"
node.description = "Equalizer Sink"
node.name = "effect_input.eq6"
media.class = "Audio/Sink"
Следовательно, наш номер 39. запомним его. Теперь получим список параметров filter-chain, с их значениями, командой pw-cli enum-params 39 Props
, где 39 -- номер ноды, найденный ранее. В выводе этой команды, посмотрев чуть дальше начала, видим такое:
String "eq_preamp:Freq"
Float 0,000000
String "eq_preamp:Q"
Float 1,000000
String "eq_preamp:Gain"
Float 5,000000
String "eq_preamp:b0"
Float 1,778279
String "eq_preamp:b1"
Float 0,000000
...
И так далее. Это и есть параметры, заданные нами в конфиг-файле, с их текущими значениями. Тут есть еще параметры вида a0, b0 которые мы не задавали, это особенность эффекта bq_peak, он их сам добавляет, нам это не сильно интересно. Нам нужны параметры, заканчивающиеся на «:Gain».
Мы получили то что хотели, но в такой форме оно не удобно для человека и еще менее удобно для обработки в скрипте. Поэтому есть второй способ --- выполним команду pw-dump
. Эта команда выводит все параметры всех объектов pipewire в JSON формате. В этой огромной простыне, видим вот что:
{
"params": [
"eq_preamp:Freq",
0.000000,
"eq_preamp:Q",
1.000000,
"eq_preamp:Gain",
5.000000,
"eq_preamp:b0",
1.778279,
"eq_preamp:b1",
0.000000,
"eq_preamp:b2",
0.000000,
"eq_preamp:a0",
1.000000,
"eq_preamp:a1",
0.000000,
"eq_preamp:a2",
и так далее, все это внутри ноды 39 c node.name=«effect_input.eq6», то есть все то же самое, только в JSON формате, который уже удобно парсить в скрипте либо в приложении. Как извлечь оттуда нужную информацию в python скрипте, показано в конце статьи.
Теперь разберем главный вопрос -- как эти параметры изменять. Это тоже позволяет делать утилита pw-cli
, команда имеет следующую форму:
pw-cli s 39 Props '{params = [ "eq_band_6:Gain" 5.0 ]}'
s -- значит set, нода 39, массив Props, имя параметра eq_band_6:Gain
, установить значение 5.0. Таким простым образом можно изменить любой параметр, если мы знаем его имя и номер ноды!
Теперь, реализуем простейший Python скрипт, который будет работать как CLI утилита управления эквалайзером. Пользоваться им так:
eqcli.py get
-- получить текущие значения параметров, выводит вот что (номер полосы эквалайзера, ее частота, ее значение):
0\. Freq: 0.0 Gain: 5.0
1. Freq: 25.0 Gain: -10.0
2. Freq: 40.0 Gain: -10.0
3. Freq: 63.0 Gain: -9.0
4. Freq: 100.0 Gain: 3.0
5. Freq: 160.0 Gain: 3.0
6. Freq: 250.0 Gain: 3.0
7. Freq: 400.0 Gain: -5.0
8. Freq: 630.0 Gain: -5.0
9. Freq: 1000.0 Gain: -3.0
10. Freq: 1600.0 Gain: -3.0
11. Freq: 2500.0 Gain: -3.0
12. Freq: 4000.0 Gain: -5.0
13. Freq: 6300.0 Gain: 10.0
14. Freq: 10000.0 Gain: 0.0
15. Freq: 16000.0 Gain: 0.0
eqcli.py set 6 -3.5
-- установить полосу номер 6 на значение -3.5 децибел. Эта команда тоже выведет значения параметров, как и команда get
, но уже измененных.
Вот листинг скрипта, надо сохранить в файл eqcli.py
#!/usr/bin/python
import sys
import subprocess
import json
def parse_pw_data():
result = subprocess.run(["pw-dump"], capture_output=True)
parsed_data = json.loads(result.stdout.decode())
eq_item_id = 0
eq_item = 0
for item in parsed_data:
try:
if item['info']['props']['node.name'] == 'effect_input.eq6':
eq_item_id = int(item['id'])
eq_item = item
except:
pass
return eq_item_id, eq_item
def get_func(eq_item):
eq_params = eq_item['info']['params']['Props'][1]['params']
i = 0
while i < len(eq_params):
print(str(int(i / 18)) + ". Freq: " + str(eq_params[i + 1]) + " Gain: " + str(eq_params[i + 5]))
i = i + 18
def set_func(eq_item_id, eq_item):
try:
band_num = int(sys.argv[2])
band_gain = float(sys.argv[3])
set_command_line = ""
if band_num == 0:
set_command_line = "{params = [ \"eq_preamp:Gain\" " + str(band_gain) + " ]}"
else:
set_command_line = "{params = [ \"eq_band_" + str(band_num) + ":Gain\" " + str(band_gain) + " ]}"
result = subprocess.run(["pw-cli", "s", str(eq_item_id), "Props", set_command_line], capture_output=True)
print(result.stdout.decode())
eq_item_id, eq_item = parse_pw_data()
get_func(eq_item)
except:
print("Wrong parameters")
eq_item_id, eq_item = parse_pw_data()
if eq_item_id == 0:
print("No equalizer found, exiting...")
exit()
try:
if (sys.argv[1] == 'get'):
get_func(eq_item)
elif (sys.argv[1] == 'set'):
set_func(eq_item_id, eq_item)
else:
print("Unknown command")
except:
print("No command")
Как это работает? Сначала скрипт получает JSON-описание всех объектов командой pw-dump
. Находит нужную ноду. Извлекает из нее значения параметров и печатает их, если дана команда get
. Формирует и вызывает pw-cli s ...
если дана команда set
.
Я думаю, очевидно, что этот пример не сложно расширить до GUI-варианта на PyQt, к примеру, и получить графическую «морду» для настройки нашего эквалайзера. К этому мы вернемся позднее.
ЧАСТЬ 3
Во второй части мы разобрали вопрос, как управлять параметрами эффектов в реальном времени, без перезапуска PipeWire.
Теперь рассмотрим, как можно добавить сторонний плагин, реализующий эффект, который не встроен в PipeWire, и как управлять его параметрами.
Модуль filter-chain, помимо встроенных (builtin) эффектов, поддерживает два самых распространенных в Linux стандарта DSP плагинов - LADSPA и LV2. Прежде всего, эти плагины предназначены для использования в DAW системе (цифровая звуковая рабочая станция, в которой «делают музыку»), такой как Ardour, Qtractor, LMMS. Они реализуют множество эффектов, таких как эквалайзер, компрессор, лимитер и тому подобное.
LADSPA это более простой и более старый стандарт, LV2 более развитый, но большинство популярных в Linux наборов LV2 плагинов поставляются и в LADSPA варианте. PipeWire может использовать плагины обоих стандартов, поэтому используем сразу LV2 вариант.
Реализуем очень нужный многим эффект -- нормализацию громкости, или компрессию. То есть, нам нужно, чтобы тихие звуки стали громче, и были слышны в зашумлённой обстановке (на работе, к примеру) из плохих динамиков ноутбука. Также эта проблема очень актуальна при просмотре фильмов, часто есть огромная разница в громкости между диалогами и активными сценами (когда начинают бить морды). Надо эту разницу уменьшить.
Для этого мы используем замечательный набор плагинов LSP. Эти плагины доступны во всех основных форматах, мы же используем LV2 версию. В составе набора есть плагин Compressor Mono, который хорошо подходит для нашей задачи -- там предусмотрен «обратный» режим работы, когда тихие звуки делаются более громкими (а не только громкие -- более тихими, как в большинстве аналогичных плагинов). Это именно то, что нужно.
Добавим этот плагин в цепочку фильтров filter-chain. Для этого, сначала надо узнать такую вещь, как URI плагина. Это такая форма его названия, в виде веб-адреса. Но это не настоящая веб-ссылка, просто такая форма ))). В общем, это тяжело объяснить.
Чтобы узнать URI плагина, выполним команду lv2ls
. Команда выдаст URI всех плагинов, которые установлены в системе. Среди них должен быть нужный нам плагин http://lsp-plug.in/plugins/lv2/compressor_mono
. Если такого нет, надо установить набор плагинов LSP, при помощи пакетного менеджера дистрибутива.
Теперь добавим в конфиг-файл из предыдущих частей вот такой кусок, в массив nodes (полная версия конфига будет в конце статьи):
{
type = lv2
name = compressor
label = compressor
plugin = "http://lsp-plug.in/plugins/lv2/compressor_mono"
control = { "cm" = 1 "cr" = 4.0}
}
Как видно, это очень похоже на добавление builtin эффекта. Только теперь тип будет lv2
, plugin
- указываем URI плагина. name
, label
-- название, которое мы сами даём этому эффекту, может быть любое. По нему потом всё будет доступно через pw-cli
для изменения параметров. Добавляем два параметра - "cm" = 1
это режим работы, который делает тихие звуки громче (а не громкие -- тише). "cr" = 4.0
-- это степень компрессии, во сколько раз. Чем больше это значение, тем больше будет выравниваться громкость.
Здесь важно, что параметров у этого плагина намного больше, а нам нужно указать только те, которые мы изменяем -- остальные будут иметь значение по умолчанию.
Очень тяжелый вопрос: откуда же брать названия параметров плагина, допустимые значения этих параметров, чтобы прописать сюда? Как узнать, что есть вот именно два этих параметра, и они называются вот так?
Для этого используем утилиту lv2info
. Выполним команду
lv2info http://lsp-plug.in/plugins/lv2/compressor_mono
В выводе команды видим всю информацию о плагине, нас интересуют блоки вида Port N:
. Порты у плагина, упрощенно, есть двух видов - это входы/выходы, через которые передается звук, и параметры самого плагина, которые можно задавать и изменять. Находим среди портов те, которые соответствуют нужным нам параметрам. Ищем, ориентируясь на поле Name
, оно говорящее, сразу понятно что это.
Port 17:
Type:
http://lv2plug.in/ns/lv2core#ControlPort
http://lv2plug.in/ns/lv2core#InputPort
Scale Points:
0 = "Down"
1 = "Up"
2 = "Boot"
Symbol: cm
Name: Compression mode
Minimum: 0.000000
Maximum: 2.000000
Default: 0.000000
Properties: http://lv2plug.in/ns/ext/port-props#hasStrictBounds
http://lv2plug.in/ns/lv2core#integer
http://lv2plug.in/ns/lv2core#enumeration
Это описание параметра cm
, который мы меняем в конфиг-файле. Из этого куска мы узнаем, что есть параметр Name: Compression mode
, он имеет имя Symbol: cm
, может принимать значения 0, 1 или 2.
Scale Points:
0 = "Down"
1 = "Up"
2 = "Boot"
Нам нужен режим Up (делать громче тихий звук), значит мы должны установить этот параметр в значение 1.
Далее, надо «залинковать» этот дополнительный эффект к уже добавленному ранее эквалайзеру. Для этого добавляем в конфиг-файле, в массив links:
{ output = "eq_band_15:Out" input = "compressor:in" }
то есть подключить вход компрессора к выходу 15 полосы эквалайзера. И последний штрих: надо сделать выходом всей цепочки эффектов -- выход компрессора.
outputs = [ "compressor:out" ]
Теперь, помимо эквалайзера, у нас в цепочке обработки будет компрессор. Какими параметрами может потребоваться управлять? Можно включать/отключать компрессор
pw-cli s 36 Props '{params = ["compressor:enabled" 0]}'
pw-cli s 36 Props '{params = ["compressor:enabled" 1]}'
Не забываем, что 36 -- это номер ноды Equalizer Sink, у вас он может быть совсем другим. Как его определить - см. Часть 2.
Ещё можно подрегулировать степень компрессии, параметр cr
.
pw-cli s 36 Props '{params = ["compressor:cr" 10.0]}'
Вот и всё, два основных эффекта мы реализовали )). Полная версия получившегося конфиг-файла будет выглядеть так:
context.modules = [
{ name = libpipewire-module-filter-chain
args = {
node.description = "Equalizer Sink"
media.name = "Equalizer Sink"
filter.graph = {
nodes = [
{
type = builtin
name = eq_preamp
label = bq_highshelf
control = { "Freq" = 0 "Q" = 1.0 "Gain" = 5.0 }
}
{
type = builtin
name = eq_band_1
label = bq_peaking
control = { "Freq" = 25.0 "Q" = 1.7 "Gain" = -10.0 }
}
{
type = builtin
name = eq_band_2
label = bq_peaking
control = { "Freq" = 40.0 "Q" = 1.7 "Gain" = -10.0 }
}
{
type = builtin
name = eq_band_3
label = bq_peaking
control = { "Freq" = 63.0 "Q" = 1.7 "Gain" = -9.0}
}
{
type = builtin
name = eq_band_4
label = bq_peaking
control = { "Freq" = 100.0 "Q" = 1.7 "Gain" = 3.0 }
}
{
type = builtin
name = eq_band_5
label = bq_peaking
control = { "Freq" = 160.0 "Q" = 1.7 "Gain" = 3.0 }
}
{
type = builtin
name = eq_band_6
label = bq_peaking
control = { "Freq" = 250.0 "Q" = 1.7 "Gain" = 3.0 }
}
{
type = builtin
name = eq_band_7
label = bq_peaking
control = { "Freq" = 400.0 "Q" = 1.7 "Gain" = -5.0 }
}
{
type = builtin
name = eq_band_8
label = bq_peaking
control = { "Freq" = 630.0 "Q" = 1.7 "Gain" = -5.0 }
}
{
type = builtin
name = eq_band_9
label = bq_peaking
control = { "Freq" = 1000.0 "Q" = 1.7 "Gain" = -3.0 }
}
{
type = builtin
name = eq_band_10
label = bq_peaking
control = { "Freq" = 1600.0 "Q" = 1.7 "Gain" = -3.0 }
}
{
type = builtin
name = eq_band_11
label = bq_peaking
control = { "Freq" = 2500.0 "Q" = 1.7 "Gain" = -3.0 }
}
{
type = builtin
name = eq_band_12
label = bq_peaking
control = { "Freq" = 4000.0 "Q" = 1.7 "Gain" = -5.0 }
}
{
type = builtin
name = eq_band_13
label = bq_peaking
control = { "Freq" = 6300.0 "Q" = 1.7 "Gain" = -3.0 }
}
{
type = builtin
name = eq_band_14
label = bq_peaking
control = { "Freq" = 10000.0 "Q" = 1.7 "Gain" = 0.0 }
}
{
type = builtin
name = eq_band_15
label = bq_peaking
control = { "Freq" = 16000.0 "Q" = 1.7 "Gain" = 0.0 }
}
{
type = lv2
name = compressor
label = compressor
plugin = "http://lsp-plug.in/plugins/lv2/compressor_mono"
control = { "cm" = 1 "cr" = 4.0}
}
]
links = [
{ output = "eq_preamp:Out" input = "eq_band_1:In" }
{ output = "eq_band_1:Out" input = "eq_band_2:In" }
{ output = "eq_band_2:Out" input = "eq_band_3:In" }
{ output = "eq_band_3:Out" input = "eq_band_4:In" }
{ output = "eq_band_4:Out" input = "eq_band_5:In" }
{ output = "eq_band_5:Out" input = "eq_band_6:In" }
{ output = "eq_band_6:Out" input = "eq_band_7:In" }
{ output = "eq_band_7:Out" input = "eq_band_8:In" }
{ output = "eq_band_8:Out" input = "eq_band_9:In" }
{ output = "eq_band_9:Out" input = "eq_band_10:In" }
{ output = "eq_band_10:Out" input = "eq_band_11:In" }
{ output = "eq_band_11:Out" input = "eq_band_12:In" }
{ output = "eq_band_12:Out" input = "eq_band_13:In" }
{ output = "eq_band_13:Out" input = "eq_band_14:In" }
{ output = "eq_band_14:Out" input = "eq_band_15:In" }
{ output = "eq_band_15:Out" input = "compressor:in" }
]
inputs = [ "eq_preamp:In" ]
outputs = [ "compressor:out" ]
}
capture.props = {
node.name = "effect_input.eq6"
media.class = Audio/Sink
audio.channels = 2
audio.position=[FL FR]
}
playback.props = {
node.name = "effect_output.eq6"
node.passive = true
audio.channels = 2
audio.position=[FL FR]
}
}
}
]
ЧАСТЬ 4
Спатиалайзер для наушников
Вы когда-нибудь задумывались, чем прослушивание музыки через наушники, отличается от прослушивания музыки через колонки? Основное отличие - когда мы в наушниках, то левое ухо слышит только звук левого наушника (левый стерео канал), и совсем не слышит звук правого. И наоборот. Когда мы слушаем колонки, то оба уха слышат обе колонки, но по-разному. Из-за этого простого факта, восприятие стерео-сцены в наушниках и через колонки, радикально отличается.
И возникает существенная проблема - а на что должна быть расчитана запись, на прослушивание в наушниках, или в колонках? Индустрия выбирает колонки, как основной источник звука, и все делается в расчете на них. А в наушниках мы будем слышать неправильное стерео, сильно искаженное.
Но - у нас же есть pipewire, поэтому не беда, сейчас мы это исправим! В этой и следующей статье, мы заставим звучать наушники как колонки, а колонки как наушники (ну почти)!
ТЕОРИЯ
Поможет нам в этом эффект, который называется спатиалайзер. С наушниками его реализация даже проще, потому что конструктивно мы левым ухом слышим левый стерео канал без примеси правого, правым ухом слышим правый без примеси левого. Теперь, нам нужно добавить эти примеси, чтобы имитировать ситуацию с колонками, когда есть crosstalk - то есть левый стерео канал проникает в правое ухо, и наоборот.
В простейшем случае, можно просто подмешать левый сигнал в правый с пониженной громкостью, и наоборот. Есть готовые устройства для наушников (кроссфидер) которые это делают. В наушниках станет возможно слушать первые альбомы Beatles)) но радикально ничего не изменится - звуки все равно будут внутри головы, а не вокруг, как с настоящими колонками.
Для того, чтобы имитировать звучание колонок на более серьезном уровне, давайте разберемся, а как вообще человек определяет, с какой стороны идет звук - слева, справа, спереди, сзади? Как работает локализация источника звука?
Форма наших ушей и форма нашей головы такова, что она оказывает влияние на звук в зависимости от того, с какой стороны идет звук! Проще говоря, воспринимаемый ухом баланс низких, средних, высоких частот будет меняться, если колонку перемещать вокруг вашей головы! Ушная раковина работает как эквалайзер, который меняет звук в зависимости от положения источника звука.
Второй фактор - задержки между звуком в правом и левом ухе. Если источник ближе к левому уху, оно будет получать звуковую волну раньше, и наоборот.
Третий фактор - разница в громкости между левым и правым ухом.
Вот эти три фактора определяют для нас ощущение в пространстве места источника звука. Теперь, если мы хотим сделать спатиалайзер, то есть имитатор источника звука, расположенного где-то вокруг нас с заданными координатами, нам нужна математическая функция, которая на вход получает необработанный, исходный звук, имеет параметры - направление (азимут) и расстояние до источника звука, и на выход выдает обработанный звук, измененный так же, как его бы изменила наша ушная раковина и форма черепа, если бы он шел из заданной точки! Причем, на выходе мы должны получать два канала - для левого наушника, и для правого, потому что все источники звука мы слышим двумя ушами.
Такая функция называется HRTF - Head Related Transfer Function, или ее подвариант DTF - Directional Transfer Function (содержит только разницу между ушами, по сравнению с HRTF, это не сильно важные пока тонкости реализации). Откуда ее взять, как получить?
Берем вас. Вставляем вам в ушные каналы микрофоны. Теперь через ваши уши будете слышать не вы, а шайтан-машина! Сажаем вас в заглушенную лабораторию, где вокруг вас расставлено огромное количество колонок, по кругу. И начинаем воспроизводить тестовый сигнал с каждой колонки по очереди и записывать звук с микрофонов. Для каждой записи, помечаем под каким азимутом стояла колонка. Так мы получаем набор импульсных откликов уха-головы (HRIR, Head Related Impulse Response), для каждого направления на колонку, когда она перед вами, влево под 5 градусов, влево под 10 градусов и так далее. Это запись того, что слышат ваши уши.
Дальше эти HRIR записи обрабатываются, пересчитываются и получается HRTF или DTF. Это все сохраняется в файл по стандарту SOFA.
Теперь мы можем сделать обратный процесс, если у нас есть этот SOFA файл! Мы можем взять музыку, левый стерео канал, задать координаты виртуальной левой колонки, пропустить звук через HRTF, и получить измененный, окрашенный звук для левого и правого наушника, который обманывает уши, заставляет их думать, что источник звука - вот там вот! Имитируем правую колонку так же, можно задние для 5.1 системы, боковые для 7.1, что угодно! И вы услышите окружающий звук через простые наушники!
Фантастика! Но есть ложка дегтя. Стоить такой обмер вашей головы будет столько, что вы не захотите это делать)) Поэтому, максимум что мы можем иметь - чужая HRTF другого человека, или какая-то обобщенная HRTF «для всех». Которую заморочились и сняли. Вы будете слышать через чужие уши! А уши это такая зараза, что у каждого человека они разные. Поэтому, идеальной точной локализации вы не получите, но эффект все равно достаточно сильный и крутой.
ПРАКТИКА
Теперь мы можем реализовать спатиалайзер - он должен загружать HRTF (или DTF) из SOFA файла, пропускать исходный стерео звук через HRTF, задаем для него координаты виртуальных стерео колонок перед нами, и получаем звук в наушниках, как из колонок (в какой-то мере). И намного более правильную стерео сцену.
Такой спатиалайзер уже есть во всех основных ОС, в андроид смартфонах это называется 3D Effects, в Windows не помню как, но начиная с Windows 10 это доступно в системном микшере просто. В Linux - хм хм, вы видели? Нет? Что-то не находите? Как всегда...
Хорошая новость - на самом деле теперь и в Linux есть все средства для того чтобы сделать спатиалайзер, ведь его встроили в pipewire! Плагин filter-chain, которому посвящены эти статьи, содержит эффект SOFA, который загружает SOFA файлы и делает нужную нам обработку. В параметрах эффекта, можно задать азимут направления на виртуальный источник, и расстояние до него.
Плохая новость - никто не включил в pipewire сами SOFA файлы! Их просто нет в комплекте, ищи где хочешь. Благо, найти их не сложно. Есть ресурс https://www.sofaconventions.org/ и там море ссылок на свободно доступные HRTF и DTF, снятые учеными в лабораториях для нас!
Я использую вот этот файл https://sofacoustics.org/data/database/ari/dtf b_nh2.sofa (прямая ссылка). Там рядом их еще море, отличаются тем, что их сняли с ушей разных людей. Выбор файла - поле для экспериментов.
Теперь напишем конфиг файл, который создает две виртуальные колонки перед нами. Азимут у них будет 30 градусов от направления «вперед», это теоретически идеальная расстановка стерео колонок по принципу равностороннего треугольника. Еще создадим две задние колонки, для ощущения окружающего звука и глубины, на них подадим тот же стерео звук, только тише.
{
type = sofa
label = spatializer
name = spFL
config = {
filename = "/home/curufinwe/dtf_b_nh2.sofa"
}
control = {
"Azimuth" = 30.0
"Elevation" = 0.0
"Radius" = 1.0
}
}
В filename указываем путь, где лежит sofa файл. Азимут - направление на виртуальный источник, прямо вперед это 0 градусов, влево это 90 градусов, сзади 180, справа 270. Elevation - угол подъема, не имеет значения, в нашем sofa файле нет информации о вертикальной плоскости. Radius - расстояние до источника, тоже не имеет значения, этот файл не содержит информации о расстоянии. Работает только азимут.
В начале цепочки нам понадобятся эффекты copy
, потому что у нас будет по два sofa эффекта для каждого канала - для передних виртуальных колонок, и для задних. После sofa эффектов мы получаем по 4 сигнала на каждый канал наушников, смешиваем их при помощи mixer
, задаем громкости для передних «колонок» 0.5, для задних «колонок» 0.3.
Если громкость задних сделать 0, то будем слышать чистую имитацию стерео колонок впереди вас.
Практика прослушивания показала, что конечно, ощущение колонок далеко не полностью реалистичное, звук только немного выходит вперед и в стороны из головы, но при сравнении с реализациями в других ОС - оно работает не хуже, точно так же! Идеала пока нигде нет, к нему можно приблизиться, если снять HRTF с вашей личной головы.
Полный конфиг-файл:
context.modules = [
{ name = libpipewire-module-filter-chain
args = {
node.description = "Spatializer"
media.name = "Spatializer"
filter.graph = {
nodes = [
{
type = builtin
label = copy
name = copyL
}
{
type = builtin
label = copy
name = copyR
}
{
type = sofa
label = spatializer
name = spFL
config = {
filename = "/home/curufinwe/dtf_b_nh2.sofa"
}
control = {
"Azimuth" = 30.0
"Elevation" = 0.0
"Radius" = 1.0
}
}
{
type = sofa
label = spatializer
name = spFR
config = {
filename = "/home/curufinwe/dtf_b_nh2.sofa"
}
control = {
"Azimuth" = 330.0
"Elevation" = 0.0
"Radius" = 1.0
}
}
{
type = sofa
label = spatializer
name = spRL
config = {
filename = "/home/curufinwe/dtf_b_nh2.sofa"
}
control = {
"Azimuth" = 150.0
"Elevation" = 0.0
"Radius" = 1.0
}
}
{
type = sofa
label = spatializer
name = spRR
config = {
filename = "/home/curufinwe/dtf_b_nh2.sofa"
}
control = {
"Azimuth" = 210.0
"Elevation" = 0.0
"Radius" = 1.0
}
}
{
type = builtin
label = mixer
name = mixL
control = {
"Gain 1" = 0.5
"Gain 2" = 0.5
"Gain 3" = 0.3
"Gain 4" = 0.3
}
}
{
type = builtin
label = mixer
name = mixR
control = {
"Gain 1" = 0.5
"Gain 2" = 0.5
"Gain 3" = 0.3
"Gain 4" = 0.3
}
}
]
links = [
{ output = "copyL:Out" input="spFL:In" }
{ output = "copyR:Out" input="spFR:In" }
{ output = "copyL:Out" input="spRL:In" }
{ output = "copyR:Out" input="spRR:In" }
{ output = "spFL:Out L" input="mixL:In 1" }
{ output = "spFL:Out R" input="mixR:In 1" }
{ output = "spFR:Out L" input="mixL:In 2" }
{ output = "spFR:Out R" input="mixR:In 2" }
{ output = "spRL:Out L" input="mixL:In 3" }
{ output = "spRL:Out R" input="mixR:In 3" }
{ output = "spRR:Out L" input="mixL:In 4" }
{ output = "spRR:Out R" input="mixR:In 4" }
]
inputs = [ "copyL:In" "copyR:In" ]
outputs = [ "mixL:Out" "mixR:Out" ]
}
capture.props = {
node.name = "effect_input.spatializer"
media.class = Audio/Sink
audio.channels = 2
audio.position=[FL FR]
}
playback.props = {
node.name = "effect_output.spatializer"
node.passive = true
audio.channels = 2
audio.position=[FL FR]
}
}
}
]