В череде дней «длинных» майских праздников решил развернуть голосовой помощник на сервере домашней автоматизации Home Assistant. Мой домашний сервер работает под управлением ОС Ubuntu Server 23.10 и не имеет никаких предустановленных источников или приемников аудио и видео информации. В Ubuntu были установлены аудиосервер PipeWire и менеджер сеансов WirePlumber. Такой выбор был продиктован тем, что эти приложения являются стандартным ПО по обработке аудио и видео потоков в Linux. В сети мне не удалось обнаружить какого-либо полного описания процесса настройки, и эта статья, в некоторой степени, восполняет этот пробел.
Шаг 1. Проверка начальной конфигурации.
На старте работ проверяем, какие аудио и видео устройства зарегистрированы в системе. Для этого я использую утилиту wpctl, входящую в состав WirePlumber. Выполняю команду wpctl status
В моей конфигурации отображается пустой граф аудио/видео, который говорит о том, что в системе нет никаких источников/приемников аудио/видео. Это неудивительно для серверной конфигурации ОС, так как никакого прослушивания или просмотра аудио или видео, на данном сервере, не ведется. В другом случае граф может содержать те или иные данные, присущие индивидуальной конфигурации.
Шаг 2. Планирование конечной конфигурации
Постановка задачи:
Планируется создать DIY конструкцию «умной колонки» с bluetooth подключением к домашнему серверу. На этом сервере развернут Home Assistant и, через него, будет осуществляться голосовое управление без использования каких-либо облачных подключений. В качестве инструмента распознавания "речь-текст" планируется использовать VOSK. В качестве инструмента перевода текста в речь планируется использовать Silero Speech. Аудиопоток с микрофона, для качественного распознавания речи, должен иметь средства шумоподавления.
Физические устройства передачи и приема звука
В качестве модулей для DIY конструкции умной колонки были приобретены:
Адаптер Bluetooth 5.3 на чипе Realtek (стандарт 5.3 допускает одновременное подключение нескольких устройств, что важно именно для серверной конфигурации);
Проводной микрофон для аудио наблюдения (потокового аудио) с питанием 12В. В стандартном случае такие микрофоны предназначены для подключения к камере видеонаблюдения для одновременной записи видео/аудио окружающей обстановки;
Проводную стереоколонку с питанием 5В и 3.5 мм разъемом для приема звука;
-
Bluetooth аудиошлюз, который решает несколько задач:
обеспечивает микрофон питанием 12В, от сети 220В;
обеспечивает стереоколонку питанием 5В от сети 220В;
обеспечивает передачу звука, записываемого через микрофон, по bluetooth каналу;
Bluetooth приемник для подключения аудиоколонки к серверу по bluetooth каналу (аудиошлюз обеспечивает как передачу так и прием аудио по bluetooth, но переключение режимов в аудиошлюзе - ручное, поэтому я решил приобрести отдельный приемник);
Я специально не указываю никаких торговых марок приобретенных модулей. В настоящее время, у меня экспериментальный интерес к этому вопросу, поэтому были приобретены самые дешевые устройства, какие мне удалось найти с требуемыми характеристиками. Если купить модули подороже, например, собрать микрофон с аппаратным шумоподавлением, то система заработает из коробки, и все нижеописанное будет представлять только теоретический интерес. Но и общая стоимость такой системы будет гораздо дороже чем та, что применена в этом эксперименте.
Технические нюансы настройки аудиосервера и менеджера сеансов
Для того, чтобы вся планируемая система заработала, необходимо учитывать следующие нюансы:
Устройства, которыми управляет WirePlumber, можно разделить на статические и динамические. К статическим устройствам относятся устройства, видимые в системе при загрузке PipeWire. Если настроены службы, загрузка PipeWire обычно совпадает с загрузкой ОС. Например, к статическим устройствам относятся стереодинамики, подключённые по проводу к звуковой карте. К динамическим устройствам можно отнести все устройства, подключаемые через радиоканалы. Эти устройства являются динамическими, если, в момент загрузки Pipewire, системе о них почти ничего неизвестно;
У микрофона, как источника звука, обязательно должен быть приемник звука, который этот звук воспроизведет. Если в системе нет статического приемника звука (например динамика, подключенного по проводу к звуковой карте), то WirePlumber создаст приемник динамически, в момент подключения микрофона. Такой динамический приемник называется «холостым выходом»;
Так как планируется сделать микрофон с шумоподавлением, то динамическое создание «холостого выхода» для микрофона будет неправильно направлять аудиопоток. В идеале, аудиопоток с микрофона должен идти в приемник модуля шумоподавления, и только пройдя модуль шумоподавления, должен попадать в «холостой выход» А при создании «холостого выхода» система не будет учитывать наличие модуля шумоподавления. В итоге получим звук не того «качества», и, как результат, слишком большой процент ошибок при распознавании речи;
Алгоритмы WirePlumber работают так, что при отсутствии статического источника звука, у модуля шумоподавления не будет создано приемника. Это приводит к тому, что впоследствии, когда этот источник появится, например, после динамического подключения bluetooth микрофона, нет возможности направить аудиопоток в модуль шумоподавления, так как он не принимает поток;
Настроить правильно работающую систему, с учетом вышеописанных нюансов, можно двумя путями:
Программный – написать сценарий WirePlumber на языке Lua, который учтет все возможные состояния сеанса работы со звуком, от подключения bluetooth микрофона до распознавания речи и построит правильный маршрут аудиопотока в каждом из возможных состояний. Описание такого скрипта выходит за рамки данной статьи;
Конфигурационный – задание маршрута аудиопотока с помощью конфигурационных файлов PipeWire. Так как этот способ гораздо проще Lua скриптов WirePlumber, в качестве базового плана я выбрал этот путь;
Базовые правила создания конфигурации.
Для планирования внесения изменений в конфигурацию приведу немного справочной информации про работу с конфигурационными файлами PipeWire. В Ubuntu конфигурационный файл, по умолчанию, расположен по пути /usr/share/pipewire/pipewire.conf
В этот файл, созданный при установке PipeWire, не стоит вносить никаких изменений. Я придерживался следующего плана изменения конфигурации PipeWire:
Необходимо определить пользователя Ubuntu - владельца звукового потока. В контексте разработки голосового помощника необходимо планировать, сколько будет микрофонов, раздельно ли будет обрабатываться звук с каждого микрофона или будет микшироваться со всех источников. Качество звука немаловажно при его распознавании, и поэтому рекомендуется не микшировать звук, а использовать один микрофон для управления всем умным домом или создать группу Audio и создать столько пользователей ОС Ubuntu в этой группе, сколько микрофонов планируется установить;
Для каждого пользователя – владельца звукового потока необходимо создать папку pipewire по пути
~/.config
;Для каждого пользователя, в папке
~/.config/pipewire
необходимо создать папку pipewire.conf.d. В этой папке создается пользовательский конфигурационный файл с необходимыми пользовательскими настройками;
Далее, в этой статье, показана настройка конфигурации с одним пользователем и одним микрофоном. При желании настройки можно масштабировать на любое количество микрофонов и пользователей.
Шаг 3. Установка модуля (плагина) шумоподавления.
В качестве модуля шумоподавления используется PipeWire плагин Noise Suppression С этим плагином можно ознакомиться по ссылке https://github.com/werman/noise-suppression-for-voice Для упрощения использования этого плагина, я рекомендую ознакомиться с проектом https://gitlab.com/echoa/pipewire-guides/-/tree/Pipewire-Filter-Chains_Normalize-Audio-and-Noise-Suppression В этом проекте созданы сконфигурированные файлы плагина, которые достаточно скачать и использовать. Я придерживался следующего порядка действий:
Скачиваю и распаковываю архив normalize-supress.zip в проекте Pipewire-Filter-Chains_Normalize-Audio-and-Noise-Suppression
Файлы, находящиеся в архиве, по пути
/normalize-supress/normalize-supress/.local/share/ladspa
, копирую в папку~/.local/share/ladspa
Файл input-denoising.conf, находящийся в архиве по пути
normalize-supress/normalize-supress/.config/pipewire/pipewire.conf.d
копирую в папку~/.config/pipewire/pipewire.conf.d
Открываю файл input-denoising.conf в редакторе (я использую nano) и корректирую пути, указанные в параметре plugin. Если все сделано по описанию выше, то достаточно заменить username на имя пользователя, под которым будет осуществляться обработка аудиопотока.
Перезапускаю PipeWire и WirePlumber.
Проверяю результат, запрашивая статус аудио/видео командой
wpctl status
. Если все сделано правильно, граф аудио/видео отобразит модуль шумоподавления со своим аудиопотоком;
Шаг 4. Статическая маршрутизация аудиопотока.
Для обработки звука и видео сервер управляет физическими и виртуальным устройствами, которые, в терминологии PipeWire, называются узлами(node) аудиографа. Для маршрутизации звукового потока автоматически создаются программные порты (ports) у каждого объекта, работающего с аудио/видео. По направлению потока порты делятся на входящие (input) и исходящие (output). Для управления аудиопотоком можно использовать утилиту pw-link, входящую в состав PipeWire. Команда pw-link -o
позволяет увидеть исходящие порты. В моей текущей конфигурации есть четыре исходящих порта
capture.rnnoise_source:monitor_FL
capture.rnnoise_source:monitor_FR
rnnoise_source:capture_FL
rnnoise_source:capture_FR
Исходящие порты типа «monitor» автоматически создаются у любого приемника звука. Описание назначения таких портов выходят за рамки данной статьи. Для поставленной задачи более интересны исходящие порты rnnoise_source:capture_FL и rnnoise_source:capture_FR, которые являются, соответственно, левым и правым стереовыходом установленного модуля шумоподавления.
Команда pw-link -i
позволяет увидеть входящие порты.
В моей текущей конфигурации система не видит ни одного входящего порта, соответственно, обработка звука, в такой конфигурации, невозможна. Необходимо подключение источника звука. Но, так как источник звука подключаем динамически, то и порты будут формироваться динамически. К сожалению, при динамическом формировании портов, WirePlumber не учитывает наличие модуля шумоподавления и звук идет «мимо». Чтобы это исправить, создадим статический виртуальный микрофон и статический виртуальный динамик и построим статический маршрут для аудио: виртуальный микрофон ->модуль шумоподавления ->виртуальный динамик. Для создания виртуального микрофона вношу следующие изменения в файл input-denoising.conf
context.objects = [
{ factory = adapter
args = {
factory.name = "my-sink"
media.class = Audio/Sink
audio.position = [MONO]
adapter.auto-port-config = {
mode = dsp
monitor = true
position = preserve
}
}
}
{ factory = adapter
args = {
factory.name = "my-mic"
media.class = Audio/Source/Virtual
audio.position = [MONO]
adapter.auto-port-config = {
mode = dsp
position = preserve
}
}
}
]
context.modules = [
{ name = libpipewire-module-filter-chain
args = {
node.description = "Noise Canceling Source"
media.name = "Noise Canceling Source"
filter.graph = {
nodes = [
{
type = ladspa
name = rnnoise
plugin = /home/home-server/.local/share/ladspa/librnnoise_ladspa.so
label = noise_suppressor_mono
control = { "VAD Threshold (%)" = 85.0 "VAD Grace Period (ms)" = 200 "Retroactive VAD Grace (ms)" = 30 }
}
{
type = ladspa
name = gate
plugin = /home/home-server/.local/share/ladspa/gate_1410.so
label = gate
control = { "Threshold (dB)" = -36 "Attack (ms)" = 10 "Hold (ms)" = 200 "Decay (ms)" = 60 "Range (dB)" = -6 }
}
]
links = [
{ output = "rnnoise:Output" input = "gate:Input" }
]
inputs = [ "rnnoise:Input" ]
outputs = [ "gate:Output" ]
}
capture.props = {
node.name = "capture.rnnoise_source"
node.passive = true
audio.rate = 48000
target.object = "my-mic"
}
playback.props = {
node.name = "rnnoise_source"
media.class = Audio/Source
audio.rate = 48000
target.object = "my-sink"
}
}
}
]
Было добавлено два объекта – my-sink (виртуальный динамик) и my-mic (виртуальный микрофон), а также, в настройках модуля шумоподавления, указали эти новые объекты в качестве целевых, отредактировав параметр target.object. На основании такой конфигурации WirePlumber должен создать и автоматически сконфигурировать порты и статические маршруты. Перезапускаю PipeWire и WirePlumber и проверяю результат командой wpctl status
На графе отобразились добавленные виртуальные устройства my-sink и my-mic, и, самое главное, отобразился статический маршрут capture_mono -> input_mono, где captute_mono – это, созданный алгоритмами WirePlumber, исходящий порт виртуального микрофона my-mic, а input_mono – входящий порт модуля шумоподавления (Noise Canceling Source). Эти статические порты, в дальнейшем, понадобятся для динамически подключаемого источника звука.
Шаг 5. Создание маршрута для bluetooth микрофона.
Микрофон подключается с использованием стандартного инструмента bluetoothctl. Я не буду описывать сам процесс подключения, он описан в руководстве bluetoothctl. По завершении процесса подключения микрофон должен появится на графе аудио/видео.
На этом скриншоте видно, что в устройствах появилось новое устройство AC692x_Bluetooth. С таким названием, в моей конфигурации, отображается bluetooth микрофон, который был подключен к серверу. Также видно, что WirePlumber построил динамический маршрут с порта output_MONO на порт playback_MONO, где output_MONO – это исходящий порт bluetooth микрофона, а playback_MONO – входящий порт виртуального динамика my_sink. Как и говорилось выше, при построении динамического маршрута, WirePlumber полностью проигнорировал наличие модуля шумоподавления и нужно решить эту проблему. Для этого буду использовать утилиту pw-link, входящую в состав PipeWire. Эта утилита позволяет создать динамический маршрут, указав нужные порты. К сожалению, этот маршрут не сохраняется и, при перезагрузке PipeWire, маршрут необходимо задавать повторно. Такого недостатка лишены правильно написанные Lua скрипты WirePlumber, но такой путь более сложный и выходят за рамки данной статьи.
В первую очередь, необходимо определить названия портов для динамического маршрута и здесь помогут команды pw-link -o
и pw-link -i
, описанные выше. Запрашиваю список исходящих портов:
home-server@irkin:~$ pw-link -o
Получаю следующий список:
my-sink:monitor_MONO
my-mic:capture_MONO
capture.rnnoise_source:monitor_MONO
rnnoise_source:capture_MONO
bluez_input.DB_6C_BE_D6_B5_95.2:output_MONO
Нужный исходящий порт называется bluez_input.DB_6C_BE_D6_B5_95.2:output_MONO – это исходящий порт bluetooth микрофона.
Запрашиваю список входящих портов:
home-server@irkin:~$ pw-link -i
Получаю следующий список:
my-sink:playback_MONO
my-mic:input_MONO
capture.rnnoise_source:input_MONO
Нужный входящий порт называется capture.rnnoise_source:input_MONO – это входящий порт модуля шумоподавления. Задаю команду и строю маршрут:
home-server@irkin:~$ pw-link bluez_input.DB_6C_BE_D6_B5_95.2:output_MONO capture.rnnoise_source:input_MONO
На графе динамически созданный маршрут не видно, поэтому запрашиваю список маршрутов с помощью утилиты pw-link c опцией -l
home-server@irkin:~$ pw-link -l
Получаю список маршрутов:
my-sink:playback_MONO
|<- bluez_input.DB_6C_BE_D6_B5_95.2:output_MONO
my-mic:capture_MONO
|-> capture.rnnoise_source:input_MONO
capture.rnnoise_source:input_MONO
|<- my-mic:capture_MONO
|<- bluez_input.DB_6C_BE_D6_B5_95.2:output_MONO
bluez_input.DB_6C_BE_D6_B5_95.2:output_MONO
|-> my-sink:playback_MONO
|-> capture.rnnoise_source:input_MONO
Необходимый результат получен. Утилита показывает, что bluetooth микрофон bluez_input.DB_6C_BE_D6_B5_95.2:output_MONO передает звук на два порта: my-sink:playback_MONO и capture.rnnoise_source:input_MONO. Для того, чтобы можно было тестировать качество звука, маршрут bluez_input.DB_6C_BE_D6_B5_95.2:output_MONO -> my-sink:playback_MONO не нужен и его отключаем, используя опцию -d (disconnect)
home-server@irkin:~$ pw-link -d bluez_input.DB_6C_BE_D6_B5_95.2:output_MONO my-sink:playback_MONO
Проверяю результат:
home-server@irkin:~$ pw-link -l
Получаю список маршрутов:
my-mic:capture_MONO
|-> capture.rnnoise_source:input_MONO
capture.rnnoise_source:input_MONO
|<- my-mic:capture_MONO
|<- bluez_input.DB_6C_BE_D6_B5_95.2:output_MONO
bluez_input.DB_6C_BE_D6_B5_95.2:output_MONO
|-> capture.rnnoise_source:input_MONO
По этому списку видно, что единственным приемником звука с bluetooth микрофона является модуль шумоподавления. Для завершающего штриха необходимо устройство воспроизведения звука с учетом шумоподавления. В нашей конфигурации таким устройством является виртуальный динамик, поэтому строю маршрут с модуля шумоподавления на виртуальный динамик:
home-server@irkin:~$ pw-link rnnoise_source:capture_MONO my-sink:playback_MONO
Проверяю результат:
home-server@irkin:~$ pw-link -l
Получаю список маршрутов:
my-sink:playback_MONO
|<- rnnoise_source:capture_MONO
my-mic:capture_MONO
|-> capture.rnnoise_source:input_MONO
capture.rnnoise_source:input_MONO
|<- my-mic:capture_MONO
|<- bluez_input.DB_6C_BE_D6_B5_95.2:output_MONO
rnnoise_source:capture_MONO
|-> my-sink:playback_MONO
bluez_input.DB_6C_BE_D6_B5_95.2:output_MONO
|-> capture.rnnoise_source:input_MONO
Заданный маршрут появился в списке. Также можно отключить все маршруты, в которых задействован my-mic. Но это необязательно, так как эти маршруты никак не влияют на нашу цель – получить звук с bluetooth микрофона с шумоподавлением. Цель достигнута.
Шаг 6. Практическое использование результата.
Вышеописанные настройки были предприняты с целью улучшить результаты распознавания звукового потока, подаваемого с микрофона на вход модуля VOSK. Для того, чтобы звук с устройств PipeWire был доступен в Python, можно использовать модуль PyAudio. Например, метод p.get_device_info_by_index модуля PyAudio, в вышеописанной конфигурации PipeWire, дает следующий результат:
{'index': 0, 'structVersion': 2, 'name': 'pipewire', 'hostApi': 0, 'maxInputChannels': 64, 'maxOutputChannels': 64, 'defaultLowInputLatency': 0.008 684 807 256 235 827, 'defaultLowOutputLatency': 0.008 684 807 256 235 827, 'defaultHighInputLatency': 0.034 807 256 235 827 665, 'defaultHighOutputLatency': 0.034 807 256 235 827 665, 'defaultSampleRate': 44 100.0}
{'index': 1, 'structVersion': 2, 'name': 'default', 'hostApi': 0, 'maxInputChannels': 64, 'maxOutputChannels': 64, 'defaultLowInputLatency': 0.008 684 807 256 235 827, 'defaultLowOutputLatency': 0.008 684 807 256 235 827, 'defaultHighInputLatency': 0.034 807 256 235 827 665, 'defaultHighOutputLatency': 0.034 807 256 235 827 665, 'defaultSampleRate': 44 100.0}
{'index': 2, 'structVersion': 2, 'name': 'Noise Canceling Source', 'hostApi': 2, 'maxInputChannels': 2, 'maxOutputChannels': 1, 'defaultLowInputLatency': 0.010 666 666 666 666 666, 'defaultLowOutputLatency': 0.0, 'defaultHighInputLatency': 0.010 666 666 666 666 666, 'defaultHighOutputLatency': 0.0, 'defaultSampleRate': 48 000.0}
{'index': 3, 'structVersion': 2, 'name': 'AC692x_Bluetooth', 'hostApi': 2, 'maxInputChannels': 1, 'maxOutputChannels': 0, 'defaultLowInputLatency': 0.021 333 333 333 333 333, 'defaultLowOutputLatency': 0.0, 'defaultHighInputLatency': 0.021 333 333 333 333 333, 'defaultHighOutputLatency': 0.0, 'defaultSampleRate': 48 000.0}
{'index': 4, 'structVersion': 2, 'name': 'my‑sink', 'hostApi': 2, 'maxInputChannels': 1, 'maxOutputChannels': 1, 'defaultLowInputLatency': 0.010 666 666 666 666 666, 'defaultLowOutputLatency': 0.0, 'defaultHighInputLatency': 0.010 666 666 666 666 666, 'defaultHighOutputLatency': 0.0, 'defaultSampleRate': 48 000.0}
{'index': 5, 'structVersion': 2, 'name': 'my‑mic', 'hostApi': 2, 'maxInputChannels': 1, 'maxOutputChannels': 1, 'defaultLowInputLatency': 0.0, 'defaultLowOutputLatency': 0.0, 'defaultHighInputLatency': 0.0, 'defaultHighOutputLatency': 0.0, 'defaultSampleRate': 48 000.0}
Звук с шумоподавлением доступен на устройстве с индексом 2 ('Noise Canceling Source') или на устройстве с индексом 4 ('my-sink'). Соответственно, с этими потоками должен работать VOSK и параметры распознавания должны сильно улучшиться, по сравнению с отсутствием шумоподавления.
Примечание: Качество распознавания можно еще улучшать. Если обратить внимание на параметры звука в этой конфигурации, то можно заметить одну деталь: частота дискретизации равна 48000 Гц. Такое значение возникает потому, что использовался микрофон, предназначенный для присоединения к видеокамере. Такие микрофоны предназначены для потокового аудио и имеют частоту дискретизации 48000 Гц, так как звук должен правильно ложиться на видеодорожку (24 кадра/сек). Нейросеть, которую использует VOSK, обучалась на данных с частотой дискретизации 16000 Гц. Поэтому распознавание будет не самым качественным. Проблему можно исправить программно, снизив частоту с 48000 до 16000 Гц, и, в этом случае, на вход speech-to-text модуля поступит такой звук, который максимально соответствует «ожиданиям» нейросети.
V1tol
В своё время настраивал шумоподавление и компрессор для микрофона с помощью https://github.com/wwmm/easyeffects.