Думал, Android пишет двигатель хуже iOS. Передумал. Потом оказалось, что прав, но по своему. android.. android. chrome.. android. chrome. getusermedia.. android. chrome. getusermedia. safari.. android. chrome. getusermedia. safari. обработка звука.. android. chrome. getusermedia. safari. обработка звука. шум.. android. chrome. getusermedia. safari. обработка звука. шум. шумоподавление.

Тезисно

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

Автор: Shadow_ru

Источник