Думал, Android пишет двигатель хуже iOS. Передумал. Потом оказалось, что прав, но по своему
- четверг, 20 августа 2026 г. в 00:00:05
Тезисно
getUserMedia({audio:true}) включает подавление шума, настроенное на речь; работающий двигатель для него тот шум, который надо вычесть(sic!). Выключил три флага — доля тишины в записях упала с 36% до 2%.
На iOS та же правка сработала наоборот: выключение обработки роняет весь тракт на 18 дБ — сигнал, пик и уровень шума одинаково.
Дело не в цене телефона и не в версии Android, а в модели устройства: разброс между 170 моделями от 0% до 100% тишины на одном и том же коде.
Я делаю пет-проект автоухо.рф : человек записывает мотор на телефон, модель называет вероятную зону неисправности. В прошлый раз я разбирался, почему модель ставит «поломка» в 71% случаев, и выяснил неприятное: она реагирует не на неисправность, а на параметры записи. Тихие записи с большой долей тишины почти всегда получали «поломка».
Кто именно делает записи тихими, тогда осталось непонятным. Гипотезу про дешёвые телефоны я проверил и отверг: внутри одного квартиля громкости вендоры не различались. Эта статья про то, как искал виновника ещё две недели и трижды менял мнение.
Был сделан анализ корпуса записей не по вендорам, а по контейнеру файла. Контейнер выдаёт тракт записи: webm пишет MediaRecorder в Chrome на Android, m4a приходит с айфонов, wav получается, когда человек загружает готовый файл и браузер декодирует его сам.
Контейнер | Откуда | n | Громкость (медиана) | Тишина | Вердикт «поломка» |
|---|---|---|---|---|---|
webm/Opus | Android, MediaRecorder | 539 | −38,6 дБ | 70% | 84% |
m4a | айфоны | 116 | −24,8 дБ | 0% | 57% |
wav | загрузка файлом | 209 | −22,3 дБ | 1% | 47% |
Пятнадцать децибел разницы в медианной громкости и семьдесят процентных пунктов разницы в доле тишины. Десятый перцентиль громкости у webm это минус шестьдесят четыре с половиной децибела, получается каждая десятая андроидная запись практически неслышна.
Первая мысль была, что файлы битые. Проверил, но нет. Opus в такой записи кодирует 122 кбит/с, столько на тишину не тратят. volumedetect на записи с Tecno даёт средний уровень −64,9 дБ при пике −30,2, у соседнего айфона в том же корпусе −26,7 и −12,6. Звук есть, он просто в сто раз жиже по амплитуде.
Гипотезу про обработку легко проверить: Chromium умеет подставлять вместо микрофона файл.
chromium \ --use-fake-device-for-media-stream \ --use-file-for-fake-audio-capture=engine.wav
Первое, что подтвердилось сразу: track.getSettings() на потоке из {audio:true} возвращает echoCancellation: true, noiseSuppression: true, autoGainControl: true.
Эталон на входе | С обработкой | Без обработки |
|---|---|---|
стук, −21,8 дБ | −27,4 дБ, пик 0 dBFS, тишина 1% | −21,8 дБ, пик −2,7 дБ, тишина 0% |
прогар выпуска, −31,5 дБ | −40,6 дБ, тишина 10% | −31,4 дБ, тишина 0% |
стук, ослабленный до −46,8 дБ | −43,3 дБ, тишина 30% | −46,8 дБ, тишина 23% |
Без обработки запись совпадает с эталоном до десятой доли децибела — все три раза. С обработкой не совпадает ни разу. Причём портит по-разному: громкий сигнал загоняется в потолок с клиппингом, тихий душится на девять децибел и получает десять процентных пунктов тишины, которой в исходнике не было.
Правка — три поля в объекте ограничений:
const MIC = {echoCancellation: false, noiseSuppression: false, autoGainControl: false}; let stream; try { stream = await navigator.mediaDevices.getUserMedia({audio: MIC}); } catch (e1) { // Спецификация разрешает браузеру не удовлетворить ограничения. Без второй // попытки такой отказ выглядел бы как отказ в доступе к микрофону. try { stream = await navigator.mediaDevices.getUserMedia({audio: true}); } catch (e2) { showMicDenied(); return; } }
Через пять дней, 175 новых записей против 102 до правки: громкость с −35,9 до −26,3 дБ, доля тишины с 36% до 2%, вердикт «поломка» с 65% до 39%, «норма» с 14% до 25%. Модель не трогали.
Здесь статья бы и кончилась, если бы не следующий раздел.
После той же правки теперь записи с айфонов стали приходить тихими, в один удачный день - семь из восьми. Разборки заняли три дня и три версии.
Кэш Safari. Отпала: добавил телеметрию, которая пишет getSettings() с живого трека вместе с загрузкой, и она показала, что новый код доехал.
Версия Safari. Отпала: тихие записи есть и в 26-й, и в 18-й, и в 17-й.
Версия iOS, состав аудитории, смешение записей с загрузками. Отпали все три.
Ответ дал отчёт с реального айфона. Для этого пришлось сделать страницу самодиагностики: она делает два захвата подряд, с обработкой по умолчанию и с выключенной, считает по обоим одни и те же метрики и умеет отправить отчёт на сервер.
Захват | Громкость | Пик | Уровень шума |
|---|---|---|---|
по умолчанию | −43,2 дБ | −29,3 дБ | −44,9 дБ |
обработка выключена | −61,0 дБ | −48,4 дБ | −62,5 дБ |
Выключение обработки на iOS роняет весь тракт на восемнадцать децибел разом. Сигнал, пик и уровень шума одинаково. Это не вычитание фона, при котором полезное остаётся, а снижение усиления целиком. Удивительное рядом. А я еще удивлялся, почему же так поперек спецификаций выходит.
А вот, Сафари, новый IE Safari не сообщает
noiseSuppressionиautoGainControlвовсе — их просто нет в ответеgetSettings(). АechoCancellationработает, только так, что крутит все усиление вниз.
Теперь надо все починить.
Возвращаемся к исходному вопросу. Android действительно писал хуже, таки да, но объяснение оказалось не тем, которое напрашивалось - бюджетники пишут плохо, потому что бюджетники.
Что проверил :
бюджетность , но нет до правки самыми громкими были бюджетные Infinix (−32,5 дБ) и Tecno (−35,3), а хуже всех Huawei (−51,3), Realme (−48,0) и Xiaomi (−45,7); связи с ценовым сегментом нет, а если натягивать сову, то только в обратную сторону;
версию Android — с десятой по шестнадцатую везде 68–85% тишины, без тренда;
браузер — Chrome 70%, Яндекс.Браузер 76%: оба на Chromium, оба обращаются к одному системному механизму. Будь дело в браузере, разные сборки вели бы себя по-разному.
Разгадка нашлась на уровне модели устройства. Внутри одного бренда аппараты ведут себя противоположно:
Модель | Тишина |
|---|---|
TECNO LJ8 | 11% |
TECNO LH7n | 85% |
Redmi M2004J19C | 0% |
Redmi 21061119DG | 69% |
SM-A137F | 70% |
SM-S931B | 96% |
Разброс между ста семьюдесятью моделями — от нуля до ста процентов. Усреднение по бренду это прятало.
Механизм как я понимаю такой. Chrome через WebRTC просит систему включить подавление шума, а исполняет запрос реализация производителя: в Android есть механизм AudioEffect, куда вендор подставляет обработку аппаратно, на DSP звукового чипа, и профили захвата в HAL. Ни то, ни другое не обновляется вместе с браузером и привязано к платформе, а не к версии системы. Один аппарат выходит под разными марками, под одной маркой бывают разные чипы.
На модель у меня приходится по четыре-восемнадцать записей, а это, скорее всего, один-три человека. Модель и пользователь тут неразличимы: может, владелец конкретного Tecno просто держал телефон иначе.
Понять это можно одним способом — посмотреть, как ведёт себя та же модель до и после правки. Поведение человека от нашего флага не меняется, а обработка меняется.
Модель | До | После |
|---|---|---|
Realme RMX3938 | −52,0 дБ / 98% тишины | −26,3 дБ / 2% |
Xiaomi 2412DPC0AG | −32,4 дБ / 28% | −26,0 дБ / 0% |
Infinix X6831 | −32,9 дБ / 0% | −12,7 дБ / 0% |
Тот же аппарат, те же владельцы, изменился только флаг.
Третья строка интереснее первых двух: на Infinix X6831 тишины не было и до правки. На этом аппарате шумодав либо не включался, либо широкополосный шум не трогал, и правка дала ему только громкость.
Важно. Моделей с данными по обе стороны правки всего три. Это иллюстрация механизма, а не выборка. Сама правка доказана на полутора тысячах записей, помодельная таблица пока остаётся наблюдением.
Порог тишины стоял на −50 dBFS и смешивал «ничего не записалось» с «записалось тихо». Из-за него я решил, что откат правки для iOS не помог, хотя он помог: запись на −43 дБ формально считалась пустой, потому что попадала под порог.
Теперь тишина считается ещё и относительно собственного уровень шума записи : доля в пределах 6 дБ от него. Абсолютную метрику оставил ради сравнимости с историей.
Общий урок про метрики: если порог абсолютный, а уровень сигнала меняется, метрика измеряет не то, что вы думаете. Я потратил на это три дня, благо это пет проект, а не жесткий прод.
Микрофон через js может жить как хочет, т.к.становится важен запрос к незнакомому железу. Но не для всех случаев : голос ± будет писать хорошо.
Минимальная диагностика занимает минуту - getSettings() и посмотреть, что вернул браузер. Потом записать одно и то же дважды подряд на одном аппарате, с обработкой и без, и сравнить громкость, пик и уровень шума. Если все три меняются одинаково не подавление шума, а дешовые трюки.
Касается это не только диагностики механизмов: запись музыки и репетиций, полевые записи природы, шумомеры, телемедицинская аускультация. В общем всё, где предмет записи и есть стационарный широкополосный шум.
Важно. Речевая обработка в WebRTC это хорошо. Она отлично делает свою работу: убирает фон, чтобы собеседника было слышно. Проблема начинается там, где фон и есть предмет записи.
P.S. Страницу самодиагностики я выложил : автоухо.рф/mic-check. Она считает всё в браузере, ничего не отправляет без нажатия кнопки и показывает, что ваш телефон делает со звуком. Мне нужна статистика, если честно, так что если не очень в лом - зайдите, запишите шум, это займет буквально 10-15 секунд и потмо нажать кнопку - “Отправить отчет”. Она внизу страницы, спасибо.