В задачах обработки сигналов часто возникает необходимость фильтрации сигналов, когда сигнал разбивается на узкополосные диапазоны. В бытовом плане мы с этим сталкиваемся при воспроизведении музыки через акустические системы, в которых каждый громкоговоритель (динамик) воспроизводит свою полосу частот, которых обычно три — низкие (НЧ), средние (СЧ) и высокие (ВЧ); для воспроизведения сверхнизких частот иногда выделяют отдельную акустическую систему под названием «сабвуфер». Конкретные границы частот зависят от реализации и ориентировочно находятся на границах 100 Гц, 1 кГц и 5 кГц. Для того, чтобы не было резких скачков громкости между динамиками, используют частичное перекрытие — когда амплитуда воспроизводимой полосы частот плавно спадает на одном, одновременно нарастая на другом.

Наиболее популярными фильтрами для такого разбиения являются фильтры Линквитца-Рейли 4-го порядка, представляющих из себя два последовательно соединённых фильтра Баттерворта, изображение АЧХ которых многим хорошо знакомо:


Популярность их обусловлена простотой схемотехнической реализации и гладкой АЧХ с отсутствием пульсаций. Порядок фильтра, обуславливающий скорость затухания, обычно выбирают 4-ым.

У всех аналоговых фильтров есть недостатки — нестабильность параметров и сложность в настройке. Другой немаловажным недостатком является неустранимый сдвиг фаз. Реализовать фазолинейный фильтр в аналоге невозможно, потому что это нарушает причинно-следственную связь выходного сигнала от входного.

Появление цифровой техники в целом и цифровых источников сигналов в частности позволило избавиться от недостатков аналоговых фильтров путём реализации их непосредственно программным образом, в цифре. Такой подход требует «мультиампинг» — когда используется отдельный усилитель мощности для каждой полосы частот — в отличие от классического подхода, когда сначала усиливается широкополосный сигнал, который разбивается на частотные полосы (обычно) пассивными фильтрами. Применение нескольких усилителей вместо одного очевидным образом удорожает технику, поэтому такое решение выбирают для особо качественных систем.

▍ Краткое введение в цифровые фильтры


Цифровой фильтр — это функция, которую можно рассматривать как во временной, так и в частотной области. Во временной области она определяет импульсную характеристику, получаемой при реакции некоторого устройства или его мат. модели на единичный импульс (на слух воспринимаемый как щелчок). В частотной области она определяет затухание или усиление амплитуды и сдвиг фазы на отдельно взятой частоте. Переход между этими областями (в англоязычной литературе используется слово domain) осуществляется через преобразование Фурье, которое между функциями во временной и в частотной области задаёт однозначное соответствие.

Разделяют два типа цифровых фильтров:

IIR (Infinite Impulse Response) — фильтры с бесконечной импульсной характеристикой. По сути, представляют собой всё те же аналоговые фильтры, но «моделируемые» в цифре — математический аппарат в обоих случаях идентичен (в основе которого лежит преобразование Лапласа). Отсюда следует бесконечность импульсной характеристики, которую можно представить в виде суммы экспоненциально затухающих синусоид.

Вот так выглядит (обрезанная справа) импульсная характеристика рекурсивного фильтра низких частот с высокой добротностью



Их преимуществом является низкая вычислительная сложность — каждое новое значение вычисляется рекурсивно в зависимости от предыдущих. Они также чувствительны к погрешностям вычисления и ошибкам округления — поэтому вопрос устойчивости (гарантированного затухания при отсутствии сигнала на входе) таких фильтров в теории рассматривается отдельно.

Наиболее простым является фильтр низких частот первого порядка:

$y_{n+1}=k \left(x_n-y_n\right)+y_n$


FIR (Finite Impulse Response) — фильтры с конечной импульсной характеристикой. Самый известный фильтр такого рода — это скользящее среднее, а сама фильтрация осуществляется посредством линейной свёртки отсчётов фильтра с отсчётами входного сигнала. Здесь сложность уже квадратичная — однако её можно уменьшить до $n \cdot log(n)$, если использовать алгоритм быстрой свёртки с использованием быстрого преобразования Фурье (FFT). Конечность количества отсчётов накладывает свои ограничения на форму АЧХ, приводя к пульсациям.

Примечание
На самом деле математически и IIR-фильтры, и FIR-фильтры описываются одинаково — через отношение полиномов. Разница в том, что у IIR-фильтров степень и у числителя, и знаменателя небольшая, а у FIR-фильтров высокая степень числителя компенсирует единичный знаменатель. Эта тема достаточно сложная и занимает немалую часть в теории обработки сигналов; для практических задач в неё углубляться не обязательно, интересующим же можно порекомендовать вспомнить про ряд Тейлора, затем перейти к производящей функции и уже после разбираться с z-преобразованием.

А так выглядит импульсная характеристика фазолинейного FIR-фильтра низких частот



Довольно популярной практикой при проектировании фильтров является линеаризация — когда берётся АЧХ известного IIR фильтра и формируется из неё FIR с линейной фазой.

▍ Постановка задачи


Итак, нашей задачей является сформировать пару фильтров, делящих полосу частот на две с перекрытием. При этом они должны обладать следующими характеристиками:

  1. Гладкая АЧХ без пульсаций;
  2. Фазолинейность (сдвиг фаз на всех частотах равен нулю);
  3. Симметричность АЧХ в логарифмическом масштабе частот.

Примечание
пункт 1) означает, что мы не закладываем в фильтр пульсации так, как это делается, например, в фильтре Чебышёва. В теории, идеально гладкая АЧХ доступна только в IIR-фильтре бесконечной длины. На практике нам достаточно лишь, чтобы уровень пульсаций не превышал уровень шума входного сигнала, который для аудио-сигналов составляет -120 дБ в целом и -90 дБ для компакт-дисков и прочих 16-битных записей.

Cимметричность (зеркальность) АЧХ решает две задачи:
1) ВЧ-составляющую сигнала можно получить вычитанием из исходного НЧ-составляющей — как в частотной, так и во временной области;
2) избавляет от мучительного выбора, для какого фильтра — НЧ или ВЧ — предпочтительнее более пологий или крутой спад АЧХ.

Похожее требование есть у квадратурных зеркальных фильтров, в которых задаётся симметрия в линейном масштабе частот. Но нас интересует именно логарифмический масштаб, как более естественный для человеческого слуха.

Кроме того,
стиль изложения в описании квадратурных зеркальных фильтров что в википедии, что в типичных научных работах мало способствует понимаю того, как их нужно реализовывать на практике даже не очень далёкому от математики программисту.


▍ Общий алгоритм построения


Существует несколько подходов к проектированию КИХ-фильтров, из которых наиболее простым и интуитивно-понятным является следующий:

  1. В частотном домене задаётся желаемая АЧХ фильтра;
  2. Производится обратное преобразование Фурье:
  3. Накладывается оконная функция.

Его можно выполнить как чисто аналитически, так и численно, используя дискретное преобразование Фурье (далее FFT). Аналитическое решение сложнее и имеет очевидное ограничение на использование только тех функций, для которых известны Фурье-образы.

▍ Аналитическое решение


Озвученным выше требованиям гладкости и симметрии (но не фазолинейности) соответствует фильтр Линквитца-Рейли. Зная формулу АЧХ $\frac{1}{w^{2 n}+1}$, где $n$ — порядок фильтра, формулу импульсной характеристики можно получить аналитически через интеграл Фурье, а конкретные отсчёты FIR фильтра считать непосредственным её вычислением.

График фильтра в логарифмическом масштабе:



Лучший способ для вычисления интеграла Фурье — это использовать какую-нибудь систему компьютерной алгебры. Например, в Wolfram Mathematica это будет выглядеть так:

InverseFourierTransform[1/(1 + w^2), w, x] // FullSimplify

$\sqrt{\frac{\pi }{2}} e^{-\left| x\right| }$

График получившейся функции, она же импульсная характеристика:



Как видно из формулы и графика, импульсная характеристика бесконечна — стремится к нулю, но не достигает его; а также симметрична относительно нуля (за счёт фазолинейности). Ограничение её во времени осуществляется путём умножения на оконную функцию, за счёт чего значения функции за пределами окна приобретают нулевые значения, не требующих участия в расчётах. Побочным эффектом этого становится искажения исходного спектра фильтра — поскольку при умножении функций их спектры сворачиваются. Итоговый спектр можно увидеть также через преобразование Фурье. Например, для прямоугольного окна получим


в результате чего АЧХ фильтра изменится на


код
FourierTransform[E^-Abs[x] Sqrt[Pi/2] UnitBox[x/5], x,
w] // FullSimplify


$\frac{w \sin \left(\frac{5 w}{2}\right)-\cos \left(\frac{5 w}{2}\right)+e^{5/2}}{e^{5/2} \left(w^2+1\right)}$

Пульсации, которые мы видим, являются следствием от наличия «боковых лепестков» оконной функции. Их можно уменьшить, взяв более гладкое и широкое окно. Среди множества разработанных окон одним из наиболее оптимальных и удобным для аналитических вычислений является окно Нуттала, определяемое как

$NuttallWindow(x)= \left\{ \begin{array}{ll}\frac{121849 \cos (2 \pi x)+36058 \cos (4 \pi x)+3151 \cos (6 \pi x)+88942}{250000}& \frac{1}{2} < x < \frac{1}{2} \\ 0& \left | x \right | \geq \frac{1}{2} \end{array} \right.$


Примечание
Используя полиномы Чебышёва, окно Нуттала также можно вычислить как $\frac{3151 t^3+18029 t^2+28099 t+13221}{62500}$, где $t = \cos (2 \pi x)$

Помножив её на импульсную характеристику фильтра, получим:


а АЧХ примет вид


код
frnw = FourierTransform[E^-Abs[x] Sqrt[Pi/2] NuttallWindow[x/20],
x, w] // Simplify




Как видно, величина пульсаций значительно уменьшилась. По графику видно, что «полка» фильтра слегка опустилась — в идеале это тоже нужно учитывать и компенсировать. В данном случае она составила (на нулевой частоте):

FourierTransform[E^-Abs[x] Sqrt[Pi/2] NuttallWindow[x/20], x, w] /.w->0.0

$0.909464 +1.66533 \times 10^{-16} i$

Мнимая часть здесь получилась вследствие погрешности при численных вычислениях и её можно смело отбрасывать (об этом говорит значение $10^{-16}$, поскольку точность вычислений в формате double и представляет примерно 16 десятичных цифр).

Аналогичным образом можно посчитать импульсные характеристики и для более высоких (но только целых) порядков, например:

InverseFourierTransform[1/(1+w^4), w, x] // FullSimplify

$\left(\frac{1}{4}+\frac{i}{4}\right) \sqrt{\pi } e^{-(-1)^{1/4} x} \left(e^{\sqrt{2} x} \left(e^{i \sqrt{2} x}-i\right) \theta (-x)-i e^{i \sqrt{2} x} \theta (x)+\theta (x)\right)$

Как видно, автоматического упрощения уже недостаточно и здесь требуется ручная работа по приведению формулы к удобочитаемому виду. В итоге получается следующая формула (в комплексных числах) для импульсной характеристики фильтра Линквитца-Рейли произвольного порядка:

$\frac{\sqrt{\frac{\pi }{2}} \sum _{k=1}^n i^{\frac{-2 k+n+1}{n}} e^{\left| x\right| \left(-i^{\frac{-2 k+n+1}{n}}\right)}}{n}$


При вычислении за счёт сложения комплексно-сопряжённых чисел мнимая часть обнуляется и в результате получится чисто действительная функция. Эту формулу можно выписать и непосредственно в действительных числах:

$\frac{\sqrt{\frac{\pi }{2}} \sum _{k=1}^n e^{\left| x\right| \sin \left(\frac{\pi -2 \pi k}{2 n}\right)} \cos \left(\frac{\pi (-2 k+n+1)}{2 n}-\left| x\right| \cos \left(\frac{\pi -2 \pi k}{2 n}\right)\right)}{n}$



▍ Переход в дискретную область


Синтез фильтра в дискретном виде осуществляется сходным образом. Принципиальное отличие состоит в том, что:

  1. Частотный диапазон фильтра ограничен частотой дискретизации по определению;
  2. На выходе мы получаем периодический сигнал — в отличие от аналитического решения, в которой функция во времени затухает к бесконечности. Этот момент особенно важен, когда проектируются фазосдвигающие фильтры. Из этого также следует, что даже если мы не будем явно накладывать оконную функцию, по факту у нас в любом случае будет наложено прямоугольное окно.

Попробуем реализовать тот же фильтр НЧ. Первым делом необходимо определиться с частотой дискретизации и размером массива. Размер массива ограничивает нижнюю частоту фильтра (но не постоянную составляющую). Если не ставить задачей минимизацию размера, то 2048 отсчётов обычно достаточно. Размер выбирается кратным степени двойки исходя из ограничений стандартной реализации БПФ.

Примечание
Быстрые алгоритмы существуют и для степеней тройки, и для взаимно простых чисел, и для некоторых других частных случаев — но для степеней двойки, четвёрки и восьмёрки они наиболее быстрые.

Также имеются различия (но не результат) при работе с классическим (комплексным) FFT, Real-FFT, который работает только с половиной спектра, предполагая его симметрию, и FHT (быстрое преобразование Хартли), в котором изначально и данные, и спектр определены в поле действительных чисел. Здесь будет использоваться классический FFT.

Итак, определим формулу для фильтра — для примера 4-го порядка:

FilterAmps[w_] := 1/(1+w^8);

Выберем частоту дискретизации (в герцах), стандартную для современных ЦАП:

samplerate = 48000;

Выберем частоту среза, на которой подавления будет составлять 0.5 (-6 дБ) и соответствовать нормированной частоте w=1:

fc = 1000;

Выберем размер массива (маленький, для наглядности):

size = 128;

Далее необходимо заполнить массив. Каждому элементу в нём будет соответствовать частота от 0 (постоянная составляющая) до $samplerate/2$ (частота Найквиста), равномерно распределённых в линейном масштабе. Определим функцию, которая в зависимости от индекса будет считать нормированную частоту:

w[index_] := (index*samplerate)/(size*fc)

Заполняем первую половину (положительную часть спектра) массива:

halfspectrumdata =
Table[FilterAmps[w[index]]*(-1)^index, {index, 0, size/2}];


Здесь фазу на каждой нечётной частоте мы повернули на 180° для того чтобы после БПФ максимум импульса был расположен по центру массива.

Вторую половину (отрицательную часть спектра) получаем реверсом первой, за исключением крайних частот (нулевой — постоянной составляющей и максимальной — частоты Найквиста, потому что для их описания достаточно одного числа).

spectrumdata =
Join[halfspectrumdata, Reverse[halfspectrumdata[[2;;size/2]]]];


Получили следующее:


Теперь делаем обратное преобразование Фурье:

wavedata = InverseFourier[spectrumdata];


И накладываем оконную функцию — Нутталла, как и в прошлый раз:


В качестве заключительного аккорда можно нормировать полку фильтра к единице. Для этого снова потребуется прямое FFT, чтобы узнать значение постоянной составляющей:

Fourier[wavedata][[1]]

$0.977615 +3.80129 \times 10^{-17} i$

и затем просто поделить на него значение каждого отсчёта в импульсе

wavedata /= Fourier[wavedata][[1]];

Сделав FFT ещё раз, можно убедиться, что постоянная составляющая стала равной единице:

Fourier[wavedata][[1]]

$1. +3.81315 \times 10^{-33} i$

Если бы мы проектировали фильтр высоких частот, но нормализацию нужно было бы делать по амплитуде не на нулевой частоте, а на частоте Найквиста.

▍ Кусочно-непрерывные фильтры


Исходя из изначально поставленной задачи, описывать АЧХ удобнее кусочно-непрерывной функцией, в которой полосы пропускания и подавления константны и равны 1 и 0 соответственно, заданной в логарифмическом масштабе. Выводу таких функций была посвящена отдельная статья, здесь для примера мы возьмём стандартную smooth-step функцию, построенной из кубического полинома 3-го порядка:

$FilterCubicClip(x)= \left\{ \begin{array}{ll} \frac{1}{4} \left(x^3-3 x+2\right) & -1 < x < 1 \\ 1 & x \leq -1 \\ 0 & x \geq 1 \end{array} \right.$


Из её графика в линейном масштабе симметрия, обеспечивающая суммирование в единицу, хорошо видна:


А вот в логарифмическом масштабе — уже нет, зато хорошо видно главное отличие от фильтра Линквитца-Рейли — спад не пологий, а имеет выраженную границу справа, за которой громкость (в децибелах) равна минус бесконечности. Дополнение до единицы даёт ВЧ-фильтр (жёлтым цветом), который и обладает желаемой симметрией:


На большем диапазоне громкости это свойство фильтров выглядит ещё более наглядным:


В отличие от классических фильтров, где параметризация крутизны спада АЧХ через порядок обусловлена их схемотехнической реализацией, здесь мы можем оперировать непосредственно шириной полосы сопряжения, задавая её в октавах от частоты раздела в -6 дБ. Для этого потребуется дополнительная функция для перевода логарифмического масштаба в линейный, значение которой будет передаваться в smooth-step функцию для определения амплитуды на частоте $f$. Её можно определить несколькими путями:

1) через граничные частоты

$LogFreqLH(f,flo,fhi)=\frac{2 \log \left(\frac{f}{\sqrt{fhi \cdot flo}}\right)}{\log \left(\frac{fhi}{flo}\right)}$



2) через центральную частоту и ширину (в октавах)

$LogFreq0W(f,f0,width)=\frac{2 \log \left(\frac{f}{f0}\right)}{width \log (2)}$



3) через центральную и верхнюю граничную частоту

$LogFreq0H(f,f0,fhi)=\frac{\log \left(\frac{f}{f0}\right)}{\log \left(\frac{fhi}{f0}\right)}$



4) через центральную и нижнюю граничную частоту

$LogFreqL0(f,flo,f0)=\frac{\log \left(\frac{f}{f0}\right)}{\log \left(\frac{f0}{flo}\right)}$


Теперь, используя некоторую функцию фильтра, можно получить его импульсную характеристику по вышеописанному алгоритму, который можно определить отдельной функцией:

SymmetricFilterImpulseResponse[f0_, width_, size_, samplerate_,
ClipFunction_, WindowFunction_] :=
Table[ClipFunction[(2 Log[(index samplerate)/(f0 size)])/
(width Log[2]) ] (-1)^index, {index, 0, size/2}] //
Join[#, Reverse[#[[2 ;; size/2]]]] & // InverseFourier[#]
Table[WindowFunction[(index-1)/size-1/2], {index, 1, size}] & // #/
Fourier[#, FourierParameters -> {1, -1}][[1]] & // Re


Примечание
В разных реализациях свёртки через ДПФ существуют разные подходы к нормализации импульсной характеристики по амплитуде. Здесь она делается не отдельной операцией, а заданием опции FourierParameters при выполнении ДПФ.

Передав в эту функцию наш кубический фильтр и окно Нуттала получим

SymmetricFilterImpulseResponse[1000, 0.5, 512, 48000, FilterCubicClip, NuttallWindow]



▍ Несколько интересных фильтр-функций


Помимо уже рассмотренной кубической, можно придумать множество и других функций для фильтров, основанных на какой-нибудь математической идее. Также имеет значение, насколько легко может быть вычислена (и может ли быть вычислена вообще) обратная функция.

Параболическая, наименее вычислительно затратная. При необходимости, обратная к ней функция находится довольно элементарно:

$FilterParabolClip(x)= \left\{ \begin{array}{ll} \frac{1}{2} \left(-x^2-2 x+1\right) & -1 < x < 0 \\ \frac{1}{2} (x-1)^2 & 0 \leq x < 1\\ 1 & x \leq -1 \\ 0 & x \geq 1 \end{array} \right.$

$\small FilterParabolClipInverse(y)= \left\{ \begin{array}{ll} 1-\sqrt{2 y} & y \leq \frac{1}{2} \\\sqrt{2-2 y}-1&y> \frac{1}{2} \end{array} \right.$




Кубическая. Обратная к ней функция уже немного контринтуитивна (а если вам интересно, откуда здесь взялись тригонометрические функции — тогда сюда):

$FilterCubicClip(x)= \left\{ \begin{array}{ll} \frac{1}{4} \left(x^3-3 x+2\right) & -1 < x < 1 \\ 1 & x \leq -1 \\ 0 & x \geq 1 \end{array} \right.$

$\small Filter CubicClipInverse(y)=-2 \cos \left(\frac{1}{3} \left(\cos ^{-1}(2 y-1)+\pi \right)\right)$




Используя полином 5-ой степени, с двумя нулевыми производными на границах сопряжения. Обратная функция здесь уже в элементарных функциях не выражается (потому и не приведена):

$FilterQuinticClip(x)= \left\{ \begin{array}{ll} \frac{1}{16} \left(-3 x^5+10 x^3-15 x+8\right) & -1 < x < 1 \\ 1 & x \leq -1 \\ 0 & x \geq 1 \end{array} \right.$




Используя полином 13-ой степени — с дополнительным «выпрямлением»:

$FilterThirteenthClip(x)= \left\{ \begin{array}{ll}\frac{1}{256} \left(15 x^{13}-65 x^9+117 x^5-195 x+128\right) & -1 < x < 1 \\ 1 & x \leq -1 \\ 0 & x \geq 1 \end{array} \right.$




Используя рациональный полином — даёт более гладкую характеристику и более простую обратную функцию, чем у кубической:

$FilterRatClip(x)= \left\{ \begin{array}{ll}\frac{(x-1)^2}{2 \left(x^2+1\right)} & -1 < x < 1 \\ 1 & x \leq -1 \\ 0 & x \geq 1 \end{array} \right.$

$\small FilterRatClipInverse(y)= \frac{\sqrt{1-y}-\sqrt{y}}{\sqrt{1-y}+\sqrt{y}}$




С заданным количеством нулевых производных в точках стыковки, частным случаем которой является предыдущая функция. Обратная к ней функция также легко находится:

$FilterNZClip(x,n)= \left\{ \begin{array}{ll}\frac{(1-x)^n}{(1-x)^n+(x+1)^n} & -1 < x < 1 \\ 1 & x \leq -1 \\ 0 & x \geq 1 \end{array} \right.$

$\small FilterNZClipInverse(y,n)= \frac{(1-y)^{1/n}-y^{1/n}}{(1-y)^{1/n}+y^{1/n}}$




Линейно спадающая в логарифмическом масштабе:

$FilterEdgeClip(x)= \left\{ \begin{array}{ll} 2^{-x-1} & -1 < x < 0 \\ 1-2^{x-1} & 0 \leq x < 1\\ 1 & x \leq -1 \\ 0 & x \geq 1 \end{array} \right.$

$\small FilterEdgeClipInverse(y)= \left\{ \begin{array}{ll} \log _2(2-2 y) & y \leq \frac{1}{2} \\ \log _2\left(\frac{1}{2 y}\right) & y> \frac{1}{2} \end{array} \right.$




Более плавный вариант предыдущего с возможностью регулировки «жёсткости». Здесь уже обратная функция для произвольного n в элементарных функциях не выражается:

$FilterSinhClip(x, n)= \left\{ \begin{array}{ll}\frac{1}{2}-\frac{n \text{csch}(1) \sinh (x)}{\text{csch}^{2 n}(1) \sinh ^2(x)^n+2 n-1} & -1 < x < 1 \\ 1 & x \leq -1 \\ 0 & x \geq 1 \end{array} \right.$




С бесконечным количеством нулевых производных в точках стыковки, что обеспечивает идеальное сопряжение. А здесь обратная функция легко находится:

$FilterTanhInfClip(x,n)= \left\{ \begin{array}{ll}\frac{1}{2} \left(1-\tanh \left(\frac{n x}{\sqrt{1-x^2}}\right)\right) & -1 < x < 1 \\ 1 & x \leq -1 \\ 0 & x \geq 1 \end{array} \right.$

$\small FilterTanhInfClipInverse(y,n)= \frac{\tanh ^{-1}(1-2 y)}{\sqrt{n^2+\tanh ^{-1}(1-2 y)^2}}$




С максимально быстро затухающей импульсной характеристикой — что при достаточно больших $n$ ($>5$) позволяет обойтись без оконной функции вообще. Сама формула получена модуляцией аргумента кубической функции, т.е. $FilterCubicClip\left(\frac{\text{erf}(n x)}{\text{erf}(n)}\right)$:

$FilterErfClip(x,n)= \left\{ \begin{array}{ll}\frac{\text{erf}(n x)^3}{4 \text{erf}(n)^3}-\frac{3 \text{erf}(n x)}{4 \text{erf}(n)}+\frac{1}{2} & -1 < x < 1 \\ 1 & x \leq -1 \\ 0 & x \geq 1 \end{array} \right.$

$\small FilterErfClipInverse(y,n)=\frac{\text{erf}^{-1}\left(-2 \text{erf}(n) \cos \left(\frac{1}{3} \left(\cos ^{-1}(2 y-1)+\pi \right)\right)\right)}{n}$



Здесь $erf$ — это специальная функция (функция ошибок), определяемая как интеграл от гауссианы. Вместо неё можно использовать функцию гиперболического тангенса — импульсная характеристика также будет достаточно быстро затухать, хоть и не так быстро, как в предыдущем случае. Причём при больших n визуально она будет походить на Линквитца-Рейли и это совсем не случайно —
потому что
Сделав в функции $\frac{1}{2} (1-\tanh (k x))$ замену $x\to \frac{2 \log (f)}{\text{width} \log (2)}$ и сократив, получим не что иное, как $\frac{1}{1+\left(f^2\right)^{\frac{2 k}{\text{width} \log (2)}}}$

$FilterTanhClip(x,n)= \left\{ \begin{array}{ll}\frac{\tanh(n x)^3}{4 \tanh(n)^3}-\frac{3 \tanh(n x)}{4 \tanh(n)}+\frac{1}{2} & -1 < x < 1 \\ 1 & x \leq -1 \\ 0 & x \geq 1 \end{array} \right.$

$\small FilterTanhClipInverse(y,n)=\frac{\tanh^{-1}\left(-2 \tanh(n) \cos \left(\frac{1}{3} \left(\cos ^{-1}(2 y-1)+\pi \right)\right)\right)}{n}$



▍ Заключение


Несмотря на очевидность постановки задачи, мне не встречалось ничего подобного ни в специализированных решениях, ни в мат. пакетах общего назначения. Возможно, что это связано с инерцией мышления в стиле «всё, что можно придумать, уже придумано». Однако это не так хотя бы потому, что сейчас доступны технологии и вычислительные мощности, о которых всего лишь 50 лет назад инженеры и математики не могли и мечтать.

В качестве proof of concept фильтры по такой методике были реализованы в плагине для популярного в аудиофильских кругах плеера foobar, извлекающего канал сабвуфера из стереосигнала (официальный репозиторий); и ещё в одном, просто для демонстрации звучания (неофициальный репозиторий). Для реализации таких фильтров в «мультиампинг»-системах можно использовать готовые решения, позволяющие загружать заранее посчитанные импульсные файлы. При отсутствии мат.пакетов их можно посчитать даже в excel-е (используя преобразование Фурье из пакета анализа).

Насколько такие фильтры «звучат» лучше классических решений — вопрос уже аудиофильский, но, как минимум, разницу в звучании обеспечивают вполне объективную и обеспечивают возможность для более точной настройки.

Комментарии (20)


  1. quaer
    09.09.2021 13:47
    +3

    Было бы интересно узнать результаты прослушивания на слышимость артефактов. Например, пре-эхо.


    1. Refridgerator Автор
      09.09.2021 14:07

      В заключении есть ссылка на плагин для фубара, чтобы послушать самостоятельно — единственно, что нельзя на лету с обычными фильтрами сравнить. Что касается слышимости пре-эха — слепые тесты этого не обнаруживают, я как-то на вегалабе предлагал поучаствовать в слепом сравнении всем желающим. Но при сильном желании такие фильтры тоже можно преобразовать в минимально фазовые — правда тогда потеряется свойство суммирования в единицу из-за сдвига фаз.


      1. quaer
        09.09.2021 17:22

        Есть ещё один момент: не показана переходная характеристика. Обычно для того, чтобы она была гладкой, в звуковых трактах используют фильтры Бесселя, а не Баттерворта.


    1. Refridgerator Автор
      10.09.2021 10:21

      Можно сравнить визуально. Возьмём высокодобротный фильтр низких частот,

      АЧХ

      Тогда его фазолинейная импульсная характеристика будет выглядеть так

      а минимально-фазовая — так:


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


  1. valeramikhaylovsky
    09.09.2021 17:07

    Несмотря на очевидность постановки задачи, мне не встречалось ничего подобного

    Не там искали.

    КИХ фильтры в обработке звука обычно не используют, так как ухо нечувствительно к фазе сигнала, только в редких случаях прибегают к Allpass фильтрам для глубокого баса, но это редкость и мало кто это способен хоть как-то услышать.


  1. iShrimp
    09.09.2021 19:44

    Спасибо за интересный и полезный материал!

    Позвольте пару вопросов от неспециалиста:

    1. Как эффективно применять полученный КИХ-фильтр?
      Простейшая реализация - прямая свёртка входного сигнала с фильтром. Это даёт почти нулевую задержку, но само вычисление занимает слишком много времени. Ускорить вычисление можно с помощью БПФ (умножая Фурье-образ фрагмента входного сигнала на Фурье-образ фильтра). Но алгоритм БПФ предполагает, что входной сигнал зациклен, а в нашем случае это не так, т.к. мы просто выдёргиваем произвольный фрагмент из временного ряда. В результате такой фильтрации конец и начало фрагмента будут "смешаны" друг с другом. Как этого избежать? Использовать оконную функцию (например, Ганна)? А как потом восстановить фильтрованный сигнал из фрагментов?

    2. Пробовали ли Вы в качестве функции пропускания (для АЧХ) использовать интеграл атомарной функции? Она обладает двумя замечательными свойствами: её производная представляет собой сумму двух таких же функций (с точностью до масштаба и смещения); в точках "стыковки" все её производные равны нулю, т.е. стык получается идеально гладким.


    1. Refridgerator Автор
      09.09.2021 20:16

      1.а. Чтобы сделать свёртку через FFT, не нужны оконные функции, а нужен метод Overlap–add. Его идея в следующем: допустим, у нас FIR из 3-х отсчётов, а использовать будем FFT на 8 отсчётов. Тогда мы этот FIR дополняем 5-ю нулями до 8-и, а входной сигнал делим на порции из 5-и отсчётов, каждый из которых дополняем 3-мя нулями (тоже до 8). Спектры перемножаем, и результаты складываем с нахлёстом в 3 отсчёта.

      1.б. Для свёртки без задержки есть алгоритм zero-delay convolution, идея которого тоже проста. Допустим, у нас FIR в 16 отсчётов. Тогда мы его разбиваем на 4,4 и 8 отсчётов. Первые 4 вычисляем прямой свёрткой, остальные — через FFT (что даст задержку соответственно в 4 и 8 отсчётов). Результаты складываем.

      2. Нет, не слышал о таких функциях.


  1. interrupt
    10.09.2021 00:51

    Спасибо за интересную и доступную подачу материала. Раз вы упомянули про QMF, может знаете что кроме научных статей можно почитать про расчет FIR фильтра для полифазного фильтрбанка?


    1. Refridgerator Автор
      10.09.2021 05:40

      Почитать не могу ничего посоветовать, кроме того что искать надо на англоязычных источниках. А у вас есть какая-то конкретная задача или просто интересно?


      1. interrupt
        10.09.2021 10:57

        Ух. Есть у меня pet project - открытая реализация atrac кодека. Для atrac3plus нужно реализовать анализирующий фильтрбанк для вот такой реализации синтезирующего https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/atrac3plusdsp.c#L502-L645

        Попытка "с наскоку" взять и адаптировать код pqf из другого кодека для моего случая (M=16, 384 fir коэффициента) не очень сработала - хотя для DC или синуса результат очень похож на правду, и после обратного синтеза получилось что то похожее на near perfect reconstruction, но для дельта функции или просто прямоугольного фронта после синтеза получается уже совсем не похожий на входной сигнал.

        Что хочется для себя прояснить.

        1. Как по прототипу понять является ли фильтрбанк near perfect reconstruction или perfect reconstruction?

        2. Из разных статей понял, что есть случаи когда анализирующий и синтезирующий фильтрбанк используют разные прототипы. Есть подозрения что это именно мой случай. Можно ли это как то узнать из анализа прототипа? И как получить из одного другой?

        Ну и в целом тема интересная, есть много вопросов, но к сожалению математику происходящего понимаю плохо (


        1. Refridgerator Автор
          10.09.2021 12:55

          Тема действительно интересная, но я с ней ещё не сталкивался, к сожалению. Решал похожую задачу, но во временной области — для нелинейной обработки сигнала через FFT. Тут, как я понимаю, аналогичная ситуация, только восстанавливать в единицу нужен. Ещё, поскольку кодек со сжатием, часть высокочастотного сигнала безвозвратно теряется и полного восстановления сигнала получить всё равно не получится. Поэтому для тестирования нужно брать сигнал с идеально ровным спектром и после анализа-синтеза смотреть его на предмет появления пульсаций в рабочей полосе спектра.

          В данном случае у меня сразу возникло непонимание того, как из этих коэффициентов получается результирующий FIR-фильтр. Попробовал их визуализировать в том порядке, в котором они описываются — получилось следующее:

          спойлер


          Выглядит так, как будто авторы специально усложнили алгоритм для реверс-инжиниринга.


          1. interrupt
            10.09.2021 15:20

            Анализирующий pqf это первое преобразование при кодировании, и соответственно синтезирующий pqf - последнее при декодировании. Все потери происходят сильно позже, уже после перехода в частотную область. Таким образом потерь я пока просто не допускаю, просто подаю результат работы своего анализирующего фильтрбанка на вход синтезирующего из ffmpeg. И ожидаю как минимум "near perfect reconstruction".

            Коэффициенты часто записываются не последовательно для более простого кода их применения, так почти во всех кодеках, в том числе и opensource. Там же хитрое прореживание совмещенное со сверткой с частью прототипа. В dcaenc боле понятно https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/dcaenc.c#L319-L367 И вот форма этого прототипа сильно влияет на свойства фильтрбанка. В том же dca есть 2 фильтрбанка, для PR и NPR.

            NPR обеспечивает подавление алиасинга только в соседних полосах, часто этого достаточно, так как за пределами соседних полос ослабление уже обычно более 90db, и этим можно пренебречь для задач аудио. Например вот тут https://www.researchgate.net/publication/221380083_A_method_to_convert_near-perfect_into_perfect_reconstruction_FIR_prototype_filters_for_modulated_filter_banks
            описано получение из near perfect reconstruction, perfect reconstruction. Забавно, что perfect reconstruction уже не выглядит как sinc(x)/x а имеет некоторые "резкие изломы".

            Почитаю еще раз, вашу предыдущую статью может какие идеи возникнут...


        1. Refridgerator Автор
          10.09.2021 20:14

          А предыстория у вашего кода есть? Нельзя же просто так с нуля взять и аудио-кодек написать, ещё и совместимый с проприетарным.


          1. interrupt
            10.09.2021 23:51

            Предыстория? Я как то порывался статью написать, но все не мог себя заставить)) Аудио и видео кодеки всегда были мне интересны, еще до того как программировать стал. Потом как то сидел в отпуске и захотелось попробовать себя в чем то новом (по работе я не связан с DSP никак), стал читать код ffmpeg, увидел что там есть декодер для ATRAC, а когда то я был фанатом минидиска, ну тут все и совпало - решено написать енкодер по коду декодера из ffmpeg. ATRAC1 мне показался достаточно простым (стек из 2х QMF + MDCT + квантование в частотной области, ну еще опционально переключение размера окна), ну и стал постепенно изучать область и реализовывать. Особых сложностей с ATRAC1 в общем то не было, и достаточно быстро получился рабочий код, дающий в целом не плохой результат. Ну так и втянулся.

            Потом взялся за ATRAC3 - он не сильно сложнее, другой фильтрбанк, но все еще имеющий в основе стек из QMF + MDCT, есть кодирование хаффмана. Из интересного, нет переключения размера окна, но есть gain modulation - возможность изменить уровень сигнала в пределах окна перед mdct в кодировщике и соответственно обратным образом изменить его после imdct в декодере, эта фича, кстати, до конца мной не была реализована. Тем не менее для LP2 режима (это что то в районе 132Kbit/s) качество получилось сравнимым с mp3 - 128.

            Тем временем код на github заметили другие любители минидиска, и с удивлением для себя я обнаружил, что оказывается проект используют в том числе для заливки треков на минидиск теми кто по каким то причинам до сих пор ими пользуются)) А кто то собрал его под WebAssembly и предлагал как web сервис))

            Тем не менее мне интереснее разбираться дальше, и ATRAC3Plus тут как раз интересен тем что использует Generalized Harmonic Analysis (GHA) для извлечения информации о тональных составляющих - в других кодеках такого подхода я не встречал, и было бы интересно поиграться с этим. Но ATRAC3Plus использует pqf для разбивки входного сигнала на 16 суб-полос перед mdct, и подозреваю что разные прототипы для синтеза и анализа, и тут я застреваю, поскольку дальше без понимания математики происходящего в pqf никак.

            Вот такая история)


        1. blacklion
          08.10.2021 18:22

          но для дельта функции или просто прямоугольного фронта после синтеза получается уже совсем не похожий на входной сигнал.

          А должен? Вы же понимаете, что такой сигнал невозможен, если мы говорим о результате работы АЦП. Т.е. он в принципе не в домене звукового кодека лежит.


          1. interrupt
            22.10.2021 01:13

            Эксперименты с фильтрбанками для которых я могу найти заведомо корректную реализацию говорят что да. Для фильтрбанка DTS кодека (реализация есть в ffmpeg), например, ошибка где то в 5м знаке после запятой была.


  1. Fly_Cam
    10.09.2021 12:52

    В статье повстречались мелкие ошибки по синтаксису и орфографии:

    1) -"Переход между этими областями осуществляется через преобразование Фурье, которая между функциями во временной и в частотной области задаёт однозначное соответствие." Здесь определение "которая" женского рода, а должно быть среднего, так как относится к слову "преобразование", которое среднего рода. Тут бы не помешало буковку "а" заменить на "о", в окончании слова "которая".

    2) -"По сути, представляют собой всё те аналоговые фильтры, но «моделируемые» в цифре..." Здесь недостаёт частички "же" после "всё те", для более удобоваримого прочтения.

    3) -"Среди множества разработанных окон одним из наиболее оптимальных и удобным для аналитических вычислений является окно Нуттала, определяемая как..." Тут "определяемая" - по смыслу соотносится к существительному среднего рода "окно", а раз так, то и окончание должно быть так же среднего рода: "определяемое", а не женского - "определяемая". Ну или как вариант, можно добавить слово "функция".

    4) -"Используя полином 5-ой степени, с двумя нулевыми производными на границах сопряжения." Здесь недосказанный непорядок в первом слове. "Используя", подразумевает некое дальнейшее действие. Но его нет до самой точки в окончании предложения. А раз нет дальнейшего действия, то целесообразнее изменить окончание у первого слова, например на вариант "используем", тогда получится предложение с констатацией факта. А иначе смысл как бы повисает в воздухе, а логичного завершения в данном предложении не следует.

    5) "-Используя рациональный полином — даёт более гладкую характеристику и более простую обратную функцию, чем у кубической:" Здесь первому слову "используя" (что делая здесь и сейчас), глагол "даёт" не соответствует. Просится изменение первого слова на "использование", с коррекцией окончаний в последующих двух словах: -Использование рационального полинома — даёт более гладкую характеристику...

    6) -"Для реализации таких фильтров в «мультиампинг»-системах можно использовать готовые решения, позволяющих загружать заранее посчитанные импульсные файлы." Здесь следует изменить окончание в слове "позволяющих", на "позволяющие", так как оно относится к существительному женского рода во множественном числе: "решения". Готовые решения позволяющие загружать... (так получится правильно). А пока читается несколько коряво: -"Готовые решения позволяющих загружать..."


    1. Refridgerator Автор
      10.09.2021 13:04

      Большое спасибо за замечания! Все эти глупые ошибки — следствие злоупотребления автодополнением в современных IDE при программировании.


  1. Nikolo-73
    12.09.2021 13:19

    Выберем размер массива...

    Массива чего?

    Каждому элементу в нём будет соответствовать частота от 0 (постоянная составляющая) до samplerate/2 (частота Найквиста), равномерно распределённых в линейном масштабе.

    Переведите это на русский, пожалуйста...

    Здесь каждую нечётную частоту мы повернули на 180° ...

    Частоты можно вращать? (Да вращал я ваши частоты знаете ли...)

    ...для того чтобы после БПФ максимум импульса был расположен по центру.

    Центру чего, простите?


  1. Refridgerator Автор
    12.09.2021 13:55

    Массива чего?
    Ответ дан в следующей Вашей же цитате:
    Каждому элементу в нём будет соответствовать частота
    Алгоритмы FFT обычно работают inplace, то есть результаты преобразования сохраняются в массив с исходными данными. Поэтому вопрос о том, находятся там отсчёты во временной или частотной области — это вопрос интерпретации.

    Частоты можно вращать?
    Мне это казалось допустимой формулировкой, потому что речь идёт о комплексных числах. Исправил на фазу.

    Центру чего, простите?
    Центру массива.