В череде дней «длинных» майских праздников решил развернуть голосовой помощник на сервере домашней автоматизации 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 микрофона, нет возможности направить аудиопоток в модуль шумоподавления, так как он не принимает поток;

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

  1. Программный – написать сценарий WirePlumber на языке Lua, который учтет все возможные состояния сеанса работы со звуком, от подключения bluetooth микрофона до распознавания речи и построит правильный маршрут аудиопотока в каждом из возможных состояний. Описание такого скрипта выходит за рамки данной статьи;

  2. Конфигурационный – задание маршрута аудиопотока с помощью конфигурационных файлов PipeWire. Так как этот способ гораздо проще Lua скриптов WirePlumber, в качестве базового плана я выбрал этот путь;

Базовые правила создания конфигурации.

Для планирования внесения изменений в конфигурацию приведу немного справочной информации про работу с конфигурационными файлами PipeWire. В Ubuntu конфигурационный файл, по умолчанию, расположен по пути /usr/share/pipewire/pipewire.conf В этот файл, созданный при установке PipeWire, не стоит вносить никаких изменений. Я придерживался следующего плана изменения конфигурации PipeWire:

  1. Необходимо определить пользователя Ubuntu - владельца звукового потока. В контексте разработки голосового помощника необходимо планировать, сколько будет микрофонов, раздельно ли будет обрабатываться звук с каждого микрофона или будет микшироваться со всех источников. Качество звука немаловажно при его распознавании, и поэтому рекомендуется не микшировать  звук, а использовать один микрофон для управления всем умным домом или создать группу Audio и создать столько пользователей ОС Ubuntu в этой группе, сколько микрофонов планируется установить;

  2. Для каждого пользователя – владельца звукового потока необходимо создать папку pipewire по пути ~/.config;

  3. Для каждого пользователя, в папке ~/.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 В этом проекте созданы сконфигурированные файлы плагина, которые достаточно скачать и использовать. Я придерживался следующего порядка действий:

  1. Скачиваю и распаковываю архив normalize-supress.zip в проекте Pipewire-Filter-Chains_Normalize-Audio-and-Noise-Suppression

  2. Файлы, находящиеся в архиве, по пути /normalize-supress/normalize-supress/.local/share/ladspa, копирую в папку  ~/.local/share/ladspa

  3. Файл input-denoising.conf, находящийся в архиве по пути normalize-supress/normalize-supress/.config/pipewire/pipewire.conf.d копирую в папку ~/.config/pipewire/pipewire.conf.d

  4. Открываю файл input-denoising.conf в редакторе (я использую nano) и корректирую пути, указанные в параметре plugin. Если все сделано по описанию выше, то достаточно заменить username на имя пользователя, под которым будет осуществляться обработка аудиопотока.

  5. Перезапускаю PipeWire и WirePlumber.

  6. Проверяю результат, запрашивая статус аудио/видео командой 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. По завершении процесса подключения микрофон должен появится на графе аудио/видео.

Граф с отображением bluetooth устройства
Граф с отображением bluetooth устройства

На этом скриншоте видно, что в устройствах появилось новое устройство 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 модуля поступит такой звук, который максимально соответствует «ожиданиям» нейросети.

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


  1. V1tol
    22.05.2024 10:33

    В своё время настраивал шумоподавление и компрессор для микрофона с помощью https://github.com/wwmm/easyeffects.


  1. nshmyrev
    22.05.2024 10:33

    Для синтеза тоже можно попробовать vosk-tts