Занялись шумоподавлением в БОЛТУНе. Хотелось убрать из разговора клавиатуру, фон и, если получится, чужие голоса рядом с микрофоном. При этом оставить в покое голос самого игрока и не отъесть у игры процессор.
Сначала прошлись по основным подходам и инструментам: штатная обработка браузера, пороговые фильтры, SpeexDSP, RNNoise, DeepFilterNet, выделение голоса по образцу. Потом собрали стенды и начали проверять то, что подходило под нашу задачу. Попробовали нейросеть в браузере, вынесли её в нативный процесс, посмотрели на нагрузку от нескольких пользователей. А сейчас занимаемся более простой обработкой пауз.
Сразу обозначу, где мы находимся: всё это пока отдельные локальные эксперименты. В опубликованный БОЛТУН их не добавляли. Уже есть результаты, которыми можно поделиться, но проверка с настоящими микрофонами и запущенной игрой ещё впереди.
Что посмотрели
Слово «шумоподавление» оказалось слишком общим. Гул вентилятора, стук по столу и человек, разговаривающий рядом, требуют разного подхода. Особенно человек: его голос тоже является речью, и обычному детектору голосовой активности всё равно, кого именно мы собирались слушать.
Для себя разложили варианты так:
|
Инструмент или подход |
Для чего нужен |
До чего дошли у нас |
|---|---|---|
|
Gate и expander |
Закрывать передачу или ослаблять тихие участки |
Собрали свой лёгкий вариант и A/B-стенд |
|
Находить участки речи; сам звук не очищает |
Изучили документацию |
|
|
Запрашивать шумоподавление при захвате микрофона |
Уже используется в нашей цепочке захвата |
|
|
Шумоподавление, регулировка усиления и другая предобработка |
Изучили документацию |
|
|
Подавлять шум с помощью нейросетевой обработки |
Посмотрели устройство и способы подключения |
|
|
Выделять нужный голос по его образцу |
Запустили, замерили, попробовали несколько одновременных входов |
Кто настраивал микрофон в OBS, наверняка встречал отдельные фильтры gate, expander и noise suppression. Мы тоже стали рассматривать их отдельно. Например, gate может убрать фон, пока человек молчит. Но когда передача открылась, вместе с голосом через неё проходит и остальной звук.
Посмотрели и на готовые приложения вроде NVIDIA Broadcast. Там есть удаление шума и комнатного эха с использованием поддерживаемых RTX GPU. Для владельца подходящей видеокарты это отдельный вариант, а нам нужна обработка внутри своего приложения, с учётом веба и Android.
В итоге до собственных тестов довели VoxLock и лёгкий пороговый обработчик. Остальные инструменты остались кандидатами. RNNoise против DeepFilterNet мы пока не гоняли, и расставлять их по местам по чужим демо не будем.
Попробовали убрать чужой голос
С VoxLock собрали отдельную страницу. Сначала записываешь шесть секунд своего голоса в тишине, из них берётся трёхсекундный образец. Затем записываешь или загружаешь звук с фоном. После обработки можно переключать дорожки и слушать разницу.
Образец сделали отдельным шагом: нужного говорящего задаём сами, модель не выбирает его из общей записи.
Для захвата взяли наш AudioWorklet — 16 кГц, блоки по 20 мс. У браузера запрашивали шумоподавление и эхоподавление, автоматическую регулировку усиления выключили. Здесь есть тонкость с обозначением «до обработки»: при записи микрофона браузер уже мог обработать сигнал. Импортированные файлы шли без этого этапа.
Первый прототип использовал FP16-модель через ONNX Runtime Web и WASM. Получили выходную запись, сделали прослушивание и экспорт WAV. Дальше проверили скорость работы по блокам.
На Intel Core i5-11400H под Windows 12 секунд звука обрабатывались 25,92 секунды. Размер блока — 80 мс, четыре потока WASM. Во время разговора с такой скоростью мы будем всё сильнее отставать от входящего звука.
Из браузера — в нативный процесс
Следом попробовали ONNX Runtime Node, CPU и модель INT8 full. Вычисления шли в отдельном локальном процессе, браузер общался с ним через WebSocket на 127.0.0.1. До рабочего сервера БОЛТУНа звук не доходил.
По сохранённым прогонам получилось так:
|
Вариант |
Блок |
Время на 12 секунд звука |
RTF |
|---|---|---|---|
|
WASM, FP16, 4 потока |
80 мс |
25,92 с |
2,16 |
|
Native CPU, INT8 full, 2 потока, после настройки |
40 мс |
7,58 с |
0,63 |
|
Та же нативная конфигурация, повторный прогон |
40 мс |
10,51 с |
0,88 |
RTF — время обработки, делённое на длительность звука. Меньше единицы — запись обработана быстрее, чем она звучит. Отдельные блоки при этом всё ещё могут опаздывать.
В этой таблице меняется сразу несколько вещей: рантайм, модель, число потоков, размер блока. По ней нельзя сказать, что native сам по себе быстрее WASM во столько-то раз. Это последовательность наших попыток ускорить конкретный прототип.
С потоками тоже поэкспериментировали. В предварительной серии переход с двух на четыре сократил время с 7,26 до 6,70 секунды. Оставили два. Ещё ограничили активное ожидание между задачами — время, когда поток продолжает занимать CPU в ожидании следующей работы.
Затем подавали тестовую запись через виртуальный микрофон в реальном времени. В 20-секундном и минутном прогонах прерываний из-за нехватки данных для воспроизведения не было.
В минутном тесте 95% ответов локального обработчика укладывались в 39,4 мс. К этому ещё добавляются накопление входного блока, сдвиг модели, буфер воспроизведения и работа звукового устройства. Сквозную задержку «микрофон — наушники» этой цифрой мы не измерили.
За минуту обработчик потратил около 106 CPU-секунд, то есть в среднем 1,76 CPU-секунды на секунду работы. Память процесса до и после теста — примерно 276 и 266 МиБ. Это только нативный обработчик, без браузера и игры.
И в повторном прогоне обработка той же записи заняла уже 10,51 секунды вместо 7,58. Причину разброса пока не нашли. Для нас это повод проверить запас под нагрузкой: у пользователя параллельно будет работать игра, а не только наш стенд.
Сервер тоже попробовали
Возникла следующая мысль: если не хочется нагружать компьютер игрока, можно перенести модель на сервер.
Сначала проверили такую схему на том же ПК. Подняли локальный обработчик и несколько клиентов, которые отправляли звук по расписанию. Генератор не ждал ответа на предыдущий блок: если обработчик тормозил, входящая нагрузка от этого не уменьшалась.
Образец голоса и состояние модели у каждого входа были свои. Веса переиспользовались внутри каждого worker. В экономичной конфигурации взяли блоки по 80 мс, до шести workers и по одному вычислительному потоку на worker.
На ответ отвели 250 мс с момента готовности блока. Вот сколько блоков уложилось в этот срок за 20 секунд:
|
Непрерывных входов |
Обработано вовремя |
|---|---|
|
2 |
100% |
|
5 |
100% |
|
10 |
51,6% |
|
40 |
10,99% |
Проценты посчитаны от всех отправленных блоков — отвергнутые и опоздавшие тоже учтены. В эти 250 мс не входят накопление блока, интернет, Opus и воспроизведение у собеседника. Это выбранный допуск нагрузочного стенда.
Пять входов затем прогнали в течение минуты. Все 3750 блоков обработались вовремя, расход составил 4,61 CPU-секунды на секунду работы, пик памяти — около 796 МиБ.
С десятью непрерывными входами эта конфигурация уже не справлялась. Перенос на сервер избавил бы игрока от локальных вычислений, но нам пришлось бы обеспечить ресурс для всех активных потоков.
Здесь важно не перепутать входы с участниками комнаты. Сорок человек не обязательно говорят одновременно. В тесте были непрерывные потоки одной открытой записи, сдвинутые относительно друг друга. Проверяли вычислительную нагрузку, а не разговор сорока разных людей. GPU и объединение нескольких клиентов в один вызов модели в этот стенд не добавляли.
Пока занялись обработкой попроще
После этих замеров тяжёлую изоляцию отложили. Решили отдельно разобраться с поведением микрофона в паузах и на тихих словах.
В новом стенде сравниваются существующий WebVoiceEngine БОЛТУНа и экспериментальная пороговая обработка. На вход обоим приходит одна запись после браузерных NS/AEC. Обычный вариант запускаем с усилением 100%, чувствительностью 94 и включённым шумоподавлением. Сохранённые настройки пользователя не читаем, чтобы исходные условия не менялись от запуска к запуску.
Начали с калибровки. Две секунды записываем фон, затем ждём нажатия второй кнопки и записываем пять секунд речи. Между этапами можно спокойно подготовиться. Если голос по уровню почти не отличается от фона, просим повторить настройку ближе к микрофону: повышение порога здесь может отрезать тихую речь.
Автоматическое увеличение усиления убрали. В обычной ветке при включённом шумоподавлении есть компенсация ×1,18; в экспериментальной её нет. Во время калибровки усиление можно уменьшить при риске перегрузки, но поднимать его вместе с фоном не стали.
Сам обработчик устроен так:
-
Для открытия нужны два последовательных блока выше порога — 40 мс.
-
Порог закрытия ниже порога открытия. Это позволяет не закрывать передачу сразу, как только голос стал немного тише.
-
Храним до 100 мс предыдущего звука. После открытия возвращаем этот запас, чтобы сохранить начало слова.
-
После падения уровня есть удержание на 240 мс и плавное ослабление тихих участков.
Запас предыдущего звука, pre-roll, сейчас проверяется в офлайн-записи: сохранённые блоки возвращаются на свои места. Как это повлияет на задержку и воспроизведение в живой комнате, нужно будет проверить при подключении к звонку.
Громкий чужой голос такой обработчик пропустит. Два одновременно говорящих человека останутся в одной дорожке. Со стуком тоже нет гарантии: короткий всплеск может не открыть передачу, но достаточно громкий или длинный пройдёт. Тихие слова ниже порога всё ещё могут пострадать. Это те случаи, ради которых и нужно прослушивание.
Сломали даже настройку микрофона
При попытке настроить лёгкий вариант получили «Не удалось уверенно услышать речь». После этой ошибки стенд не давал даже записать микрофон и послушать, что с ним происходит: запись была завязана на обязательную калибровку.
Пришлось переделать. Теперь запись доступна сразу, в том числе после неудачной настройки. Добавили название выбранного устройства и индикатор уровня. Можно сначала услышать микрофон, а уже потом разбираться с порогами.
Почему именно тот микрофон присылал низкий уровень, пока не установили. Но диагностику хотя бы больше не блокирует сама калибровка.
Что получилось на лёгком варианте
В A/B-плеере можно менять вариант обработки, не перематывая запись. Для двух обработанных дорожек выравниваем применяемое усиление речи, чтобы разница в громкости не мешала сравнению.
В сохранённом браузерном прогоне лёгкий DSP обработал 15 секунд записи за 56,6 мс. Это вычисления над готовой записью, без времени браузерного шумоподавления и без измерения задержки звонка.
Напрямую сравнивать эти 56,6 мс с VoxLock бессмысленно: лёгкий вариант управляет уровнем сигнала в паузах, а VoxLock пытается выделить нужный голос из смеси.
Автотестами проверили калибровку, короткий всплеск, возврат начала речи из pre-roll, окончания слов, выравнивание усиления и ограничение памяти. В браузере — запись, переключение дорожек, экспорт WAV, отмену и отключение микрофона. Вычислительная часть работает; качество на разных микрофонах предстоит оценить ушами.
Пока на этом и остановились. Изоляцию по образцу сохранили отдельным экспериментом. Лёгкую обработку будем проверять на тихой русской речи, с паузами, короткими словами и меняющимся фоном. Потом — во время игры.
RNNoise и DeepFilterNet из списка не вычёркиваем. До собственного сравнения с ними ещё не дошли, поэтому утверждать, что выбрали лучший вариант, оснований нет.
Для БОЛТУНа сейчас важнее не потерять половину тихой фразы и оставить игре её ресурсы. С шумом продолжим разбираться. Пилим дальше)))
Замеры сделаны 23–26 октября 2026 года на локальном ПК с Intel Core i5-11400H под Windows. Браузерные автотесты запускались в Edge headless. Нейросетевые прогоны использовали открытые записи, а потоковые браузерные проверки — виртуальный микрофон. Рабочие комнаты, Android и Tauri/WebView2 в этих тестах не участвовали.
Тестовый звук взят из демо VoxLock автора Arbaz Siddiqui, ревизия 08ed37cbb79f449ca6b499bc23fb14138b207bfd. Источник речи — LibriSpeech test-clean, CC BY 4.0, с моделированием комнаты автором демо. Индивидуальные записи пользователей в статье не используются.
Автор: PAPADAN


