Тезисно

  • 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%. Модель не трогали.

Здесь статья бы и кончилась, если бы не следующий раздел.

Раунд второй: на iOS всё сломалось

После той же правки теперь записи с айфонов стали приходить тихими, в один удачный день - семь из восьми. Разборки заняли три дня и три версии.

  1. Кэш Safari. Отпала: добавил телеметрию, которая пишет getSettings() с живого трека вместе с загрузкой, и она показала, что новый код доехал.

  2. Версия Safari. Отпала: тихие записи есть и в 26-й, и в 18-й, и в 17-й.

  3. Версия iOS, состав аудитории, смешение записей с загрузками. Отпали все три.

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

Захват

Громкость

Пик

Уровень шума

по умолчанию

−43,2 дБ

−29,3 дБ

−44,9 дБ

обработка выключена

−61,0 дБ

−48,4 дБ

−62,5 дБ

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

А вот, Сафари, новый IE Safari не сообщает noiseSuppression и autoGainControl вовсе — их просто нет в ответе getSettings(). А echoCancellation работает, только так, что крутит все усиление вниз.

Теперь надо все починить.

Раунд третий: Android всё-таки хуже, но не поэтому

Возвращаемся к исходному вопросу. 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 секунд и потмо нажать кнопку - “Отправить отчет”. Она внизу страницы, спасибо.