Расширение стереоэффекта для динамиков ноутбука
Предыдущие части:
ВАЖНО! Если вы не читали Часть 4, то настоятельно рекомендуется прочитать сначала ее, а то из этой ничего не будет понятно!
В предыдущих четырех статьях цикла мы рассмотрели возможности модуля Pipewire под названием filter-chain. Этот модуль позволяет добавлять цепочки фильтров, реализующих общесистемный эквалайзер, компрессор, лимитер и прочее. В общем, все то что обычно делают при помощи EasyEffects, но без дополнительных сервисов и фоновых приложений, силами самого процесса Pipewire, с минимальным потреблением ресурсов.
Помимо этих, привычных эффектов, сейчас во всех основных коммерческих ОС набирает популярность еще один – спатиалайзер. Этот эффект делает 3D для устройства стереовоспроизведения, создает иллюзию расположения источника звука в пространстве, спереди, сбоку, сзади, притом, что звук воспроизводится через обычные наушники или стереодинамики.
В Части 4 мы рассмотрели, как реализовать спатиалайзер для наушников, там же изложена теория и принципы работы этого эффекта. А сейчас мы реализуем вторую часть этой задачи – расширение стерео эффекта для динамиков ноутбука!
Строго говоря, для настоящих, полноценных колонок этот эффект практически не нужен (для стереомузыки по крайней мере, о которой речь в этих статьях пока). Хорошая стереосистема дает ощущение пространства просто на исходной стерео записи. Но если у нас встроенные динамики ноутбука, то необработанный звук из них нельзя назвать хорошим ))) Как минимум, динамики ноутбука расположены близко, под слишком острым углом к ушам слушателя, и нормального стерео они, сами по себе, дать не смогут. Что же делать?
Обратимся к тому, что мы делали в Части 4. Там мы имитировали два источника звука в пространстве, расположенные под углами 30 градусов к направлению «прямо». Такое расположение дает оптимальный стереоэффект. А если мы сделаем то же самое с динамиками ноутбука? Заставим звук восприниматься не из самих динамиков, а из пространства впереди с более широкой и правильной стереобазой?
Тут есть одна серьезная проблема – динамики это не наушники. А функции HRTF (смотри часть 4), которые мы использовали, подготовлены для наушников! В чем же разница? Звук из наушников поступает в левое и правое ухо независимо, два стерео канала не смешиваются. Наши HRTF-функции рассчитаны именно на это. Если теперь мы тот же обработанный сигнал пропускаем через динамики и слушаем, то левый и правый каналы СИЛЬНО смешиваются в воздухе перед нами, и мы слышим не то, что нужно! По-английски это явление называется crosstalk. И у нас возникает дополнительная задача – подавить crosstalk, заставить левое ухо слышать только левый канал, а правое ухо – только правый канал, при том что звук идет из близко расположенных динамиков ноутбука!
Вообще, науке известен целый ряд способов подавления crosstalk. Например, если подмешать каналы друг в друга с нужным фазовым сдвигом, то в точке нахождения головы произойдет фазовое гашение лишних составляющих звука. Но это очень сложно, работает только в одной точке, зависит от конфигурации динамиков конкретного ноутбука. Поэтому мы поступим намного проще – решим задачу не идеально теоретически, но достаточно для конкретной нашей практической задачи.
Стерео сигнал можно представить в другой форме – не левый/правый канал, а средний/боковой канал. Если сложить левый и правый, то получится средний. Если из левого вычесть правый – получится боковой. То есть боковой канал содержит тот звук, который должен идти с боков, а средний – то что мы ощущаем посередине (вокал, ударные). Это называется Mid-Side техника, хорошо известная в звукорежиссуре.
Так вот, к чему приводит crosstalk, то есть смешение левого и правого канала звука из колонок, в представлении Mid-Side? Очевидно что, в целом, этот эффект смешивания усиливает средний канал, и ослабляет боковой! Потому что crosstalk добавляет в звук сумму левого и правого канала. А мы сделаем наоборот – ослабим средний канал, не тронув боковой! Это даст обратный эффект, в какой-то степени подавит crosstalk, компенсирует его действие! Не идеально, но вполне достаточно.
После этого, надо звук из Mid-Side формата преобразовать обратно в левый/правый. Сделать это не составит труда, средний плюс боковой дает левый, средний минус боковой дает правый.
Вот так просто, не применяя сложных фильтров, просто операциями сложения-вычитания, мы можем адаптировать звук, обработанный спатиалайзером, к выводу через динамики ноутбука вместо наушников! В filter-chain доступны все элементы, реализующие арифметические операции с сигналами.
Вот эти фильтры подавляют средний канал в два раза, перед преобразованием обратно в левый/правый формат (это часть полного конфига, который будет ниже):
{
type = builtin
label = mixer
name = enhMixL
control = {
"Gain 1" = 0.5
}
}
{
type = builtin
label = mixer
name = enhMixR
control = {
"Gain 1" = 0.5
}
}
Как всегда, привожу полный конфиг-файл, содержимое этого листинга надо поместить в файл по адресу ~/.config/pipewire/pipewire.conf.d/stereoenhancer.conf и перезапустить pipewire. Также обращаю внимание, что этот конфиг, как и приведенный в Части 4, использует сторонний sofa-файл! Его надо скачать, например отсюда https://sofacoustics.org/data/database/ari/dtf%20b_nh2.sofa, а в конфиге указать правильный путь к нему в вашей системе (в моем конфиге, приведенном ниже, прописан путь где он лежит у меня).
context.modules = [
{ name = libpipewire-module-filter-chain
args = {
node.description = "StereoEnhancer"
media.name = "StereoEnhancer"
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.03
"Gain 4" = 0.03
}
}
{
type = builtin
label = mixer
name = mixR
control = {
"Gain 1" = 0.5
"Gain 2" = 0.5
"Gain 3" = 0.03
"Gain 4" = 0.03
}
}
{
type = builtin
label = copy
name = enhcopyL
}
{
type = builtin
label = copy
name = enhcopyR
}
{
type = builtin
label = mixer
name = enhAdd
}
{
type = builtin
label = mixer
name = enhSub
}
{
type = builtin
label = copy
name = enhcopyA
}
{
type = builtin
label = copy
name = enhcopyS
}
{
type = builtin
label = invert
name = enhinv1
}
{
type = builtin
label = invert
name = enhinv2
}
{
type = builtin
label = mixer
name = enhMixL
control = {
"Gain 1" = 0.5
}
}
{
type = builtin
label = mixer
name = enhMixR
control = {
"Gain 1" = 0.5
}
}
]
links = [
{ output = "convL:Out" input="copyL:In" }
{ output = "convR:Out" input="copyR:In" }
{ 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" }
{ output = "mixL:Out" input="enhcopyL:In" }
{ output = "mixR:Out" input="enhcopyR:In" }
{ output = "enhcopyL:Out" input="enhAdd:In 1" }
{ output = "enhcopyR:Out" input="enhAdd:In 2" }
{ output = "enhcopyL:Out" input="enhSub:In 1" }
{ output = "enhcopyR:Out" input="enhinv1:In" }
{ output = "enhinv1:Out" input="enhSub:In 2" }
{ output = "enhAdd:Out" input="enhcopyA:In" }
{ output = "enhSub:Out" input="enhcopyS:In" }
{ output = "enhcopyA:Out" input="enhMixL:In 1" }
{ output = "enhcopyS:Out" input="enhMixL:In 2" }
{ output = "enhcopyA:Out" input="enhMixR:In 1" }
{ output = "enhcopyS:Out" input="enhinv2:In" }
{ output = "enhinv2:Out" input="enhMixR:In 2" }
]
inputs = [ "copyL:In" "copyR:In" ]
outputs = [ "enhMixL:Out" "enhMixR:Out" ]
}
capture.props = {
node.name = "effect_input.stereoenhancer"
media.class = Audio/Sink
audio.channels = 2
audio.position=[FL FR]
}
playback.props = {
node.name = "effect_output.stereoenhancer"
node.passive = true
audio.channels = 2
audio.position=[FL FR]
}
}
}
]