AGC или как перестать подстраивать громкость вручную

от автора

Предисловие

Я не являюсь профессиональным DSP разработчиком, моя стезя — системное программирование и разработках встраиваемых систем, в частности, специальных систем связи для работы с VoIP. Данная статья рассчитана на тех, кому интересны алгоритмы обработки звука и кто начинает свой путь в их изучении. Здесь я хочу описать свой путь в исследовании и реализации одного из алгоритмов. На Хабре уже выходили статьи на данную тему. Первая касалась аппаратной реализации, а вторая вышла довольно давно, но теория в ней не потеряла актуальности.

Введение

Что такое АРУ, оно же AGC? АРУ/AGC (автоматическая регулировка усиления или англ. automatic gain control) — процесс, при котором входной сигнал автоматически «подгоняется» под заданную мощность, в контексте звука – громкость. Для чего это может понадобиться? Представим ситуацию: вы состоите в радиосети с Машей и Сашей. При выходе в передачу Машу еле слышно, а Саша же «кричит». В такой ситуации разговор превращается в пытку. Для решения проблемы постоянного ручного регулирования громкости и нужен данный алгоритм.

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

Цель: добиться автоматического регулирования усиления звука;

Задача: реализовать алгоритм, который будет нормализовать громкость входящего звука;

Исходные данные: устройство на Linux с ALSA, сервис, похожий на pulseaudio, который принимает звук с нескольких источников и проигрывает в наушники. Формат звука: один канал, формат S16_LE, частота дискретизации 8000 Hz;

Критерии оценивания: разделены на субъективный и объективный:

  • субъективный: на слух звук воспринимается громче на тихих участках аудиодорожки и наоборот, разборчивость речи не стала хуже;

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

Ожидаемый результат: алгоритм работает, не перегружает систему, вносит минимальную задержку (до 500 мс.).

Реализация

AI: не отстаем от трендов

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

  • на выходе у модели частота равна 48 KHz, тогда как по условию задачи должна быть 8 Khz. Можно сделать ресемплинг, но это добавит лишнюю нагрузку и задержку;

  • нельзя использовать модель локально.

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

Сравнение оригинального аудиофайла и обработанного с помощью Nvidia studiovoice

Сравнение оригинального аудиофайла и обработанного с помощью Nvidia studiovoice

ALSA плагины: ищем готовое решение

Разрабатываемые устройства работают на современном Linux и используют ALSA API для работы со звуком. Для его микширования используется собственный звуковой сервис, похожий на pulseaudio. Сервис ожидает на определенных сетевых интерфейсах звуковые данные и проигрывает их на устройство вывода. Вот тут возникла следующая мысль – встать между сервисом и ALSA устройством и обрабатывать данные.

В ALSA есть как встроенные плагины, так и сторонние. Самым подходящим для решения задачи мне показался speex. Данный плагин основан на одноименной библиотеке. Для получения этого плагина включим флаг «—with-speex=lib» в пакете alsa-plugins и добавим зависимость «select BR2_PACKAGE_SPEEX» в buildroot. На развернутом образе системы, по пути /etc/alsa/conf.d/ получаем файл настроек плагина 60-speex.conf, а по пути /usr/lib/alsa-lib/ сам плагин libasound_module_pcm_speex.so. Нас интересуют параметры плагина: agc и agc_level. Первый включает сам функционал, второй задает уровень в диапазоне от 1 до 32768. Подключение плагина производится путем добавления в файл /etc/asound.conf следующих блоков:

pcm.agc_pcm {  type speex  agc on  agc_level 23400  slave.pcm "dev_out"}pcm.test_agc {  type plug  slave.pcm agc_pcm}

В данном конфиге создано PCM устройство test_agc типа плагин, которое управляет непосредственно плагином speex. Параметр agc_level установлен в 23400, PCM устройство для воспроизвения – dev_out. Число 23400 выведено эмпирическим путем. Для проверки была использована следующая команда:

arecord -D dev_in | aplay -D test_agc

Данная команда считывает данные с устройства ввода dev_in и перенаправляет их в устройство test_agc, которое внутри обрабатывает эти данные с помощью плагина speex, и записывает в устройство вывода dev_out.

Из отдела тестирования задача вернулась со следующими комментариями:

В начале передачи звук начинает воспроизводиться тихо, а затем плавно усиливается до достаточно громкого.

и

Иногда возникает звук треснутого стекла и снова начинает играть тихо.

Также была обнаружена следующая проблема: во время записи данных в аудиоустройство, возникает ошибка: «ALSA lib pcm.c:8563:(snd_pcm_recover) underrun occurred», которая приводит к ошибке, описанной в первом комментарии. Изучив исходники плагина pcm_speex в самой библиотеке speexdsp, обнаружил недокументированные опции: frames и filter_length. Первая использовалась для инициализации значений в функции speex_preprocess_state_init, вторая актуальна только для настройки echo. В итоге было решено отказаться от данного плагина.

FFmpeg: решение было на поверхности

Тем, кто не знаком с данным фреймворком, рекомендую прочесть более компетентные статьи, например, раз и два, и официальную страницу проекта. Среди всего многообразия фильтров звука, взгляд остановился на dynaudnorm. Описание этого фильтра будто дублирует постановку задачи.

Ознакомившись со всеми параметрами и подобрав оптимальные значения, получилась следующая команда:

ffmpeg -f alsa -i dev_in -filter:a "dynaudnorm=f=20:g=33:m=100:c=1" output.wav

Поясню каждый аргумент фильтра вкратце:

  • f – размер фрейма в мс. Специфика системы подразумевает короткие передачи голоса, поэтому было выбрано значение равное 20 мс;

  • g – окно для фильтра Гаусса. Чем больше число, тем больше задержка, но лучше сглаживание, и наоборот;

  • m – коэффициент усиления. В данном примере используется максимальное усиление, но в итоговой реализации идет привязка к коэффициенту громкости;

  • c – включить DC-коррекцию, то есть убрать смещение относительн нуля.

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

Граф вызовов фильтра dynaudnorm

Граф вызовов фильтра dynaudnorm

После изучения графа вызовов callgrind и листинга libavfilter/af_dynaudnorm.c, была составлена следующая блок схема алгоритма:

Блок схема алгоритма нормализации

Блок схема алгоритма нормализации

На вход алгоритм принимает семпл размером F, смещает постоянную составляющую сигнала ближе к нулю, ищет максимальную амплитуду, вычисляет коэффициент усиления. Если не корректировать постоянную составляющую, не применять DC correction, то звук будет «гудящим», что приведет к худшему результату работы алгоритма. Причиной возникновения смещения постоянной составляющей могут быть аппаратные проблемы, например, наводки на микрофоне. Данный функционал был включен в итоговую реализацию по умолчанию. Далее идет фильтр Гаусса, который сглаживает значения между фреймами, чтобы сделать переходы между кадрами более плавным, избавиться от перегруза звука. Как только нужное количество фреймов накопилось, алгоритм усиливает первый фрейм в очереди и отдает на запись в динамики и так идет до тех пор, пока не закончится буфер. Отмечу, что данный фильтр не убирает шумы в аудио, но такой задачи и не было. Однако хочу поделиться интересными решениями этого вопроса, на которые натыкался по ходу решения задачи:

Собственная реализация

Для прототипирования собственной реализации алгоритма были выбраны язык программирования python и фреймворк Jupyter. Напомню, что в разрабатываемой системе используется следующий формат звука: S16_LE, 8 Кгц, 1 канал, поэтому я опустил универсальность реализации. Неожиданно самым трудным в реализации стала имитация проигрывания аудиофайла так, будто это передача в реальном времени. Если передать на обработку весь файл целиком, то результат будет «усреднен» по всей дорожке. В реальном сценарии использования такой подход неприменим. Ниже представлен результат работы прототипа: верхняя осцилограмма – оригинальная дорожка, нижняя – после обработки прототипом.

Сравнение оригинальной аудиодорожки и обработанной алгоритмом

Сравнение оригинальной аудиодорожки и обработанной алгоритмом

Итоговая реализация была выполнена на языке C++ 20-го стандарта. Алгоритм состоит из двух классов: непосредственно AutomaticGainControl, который занимается сохранением, анализом и усилением/ослаблением громкости, и GaussianFilter. Сам проект был поделен на 3 части:

  1. статическая библиотека libagc – самая главная часть проекта;

  2. утилита для тестирования agc_pipe;

  3. плагин для ALSA libasound_module_pcm_agc.

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

Список опций утилиты:

Allowed options:  -h [ --help ]                 Print help  -m [ --msec ] arg (=20)       Frame length in msec  -c [ --channel ] arg (=1)     Channels to filter  -r [ --rate ] arg (=8000)     Sample rate  -f [ --filter ] arg (=13)     Gaussian filter size  -p [ --peak ] arg (=0.950000) Max peak value  -g [ --gain ] arg (=100)      Max gain value  -v [ --verbose ]              Enable verbose logs

Пример команды для проверки в реальном времени:

arecord -D <input device> -f S16_LE -t raw | ./agc_pipe -f 7 -g 99 | aplay -D <output device> -f S16_LE -t raw

Далее алгоритм был интегрирован в существующий сервис, были произведены замеры производительности. В результате нагрузка на CPU сервиса возрастает не более чем на 3% (в сравнении с показателями без АРУ) во время работы алгоритма с одним источником аудиоданных. Задержка воспроизведения звука не превышает ~400 миллисекунд, при следующих настройках: FrameLenMs = 20, FilterSize = 13.

Сам проект.

Вывод

Хотя итоговый алгоритм похож на реализацию из фреймворка ffmpeg, в нем имеются свои преимущества, например, использование парадигмы ООП, что облегчает поддержку и улучшает восприятие кода, или использование std::span, который снижает количество потенциальных ошибок при работе с C-массивами и указателями. Текущая реализация имеет большой простор для расширения и модификаций, например, поддержку N каналов, фильтр шума. Буду рад комментариям по улучшению алгоритма или другим реализациям, которые я не отразил в этой статье.

ссылка на оригинал статьи https://habr.com/ru/articles/1022424/