The Jerusalem PostUS Space Force electromagnetic weapons played key role in Venezuela, Iran operations - reportESPNFollow live: Bauers, Mitchell smash back-to-back homers as Brewers regain leadESPN DeportesEN VIVO: Dodgers le da vuelta al pizarrón con doble productor de Will SmithPunchTinubu campaign: Accord leaders urge Adeleke to clarify loyalty한겨레‘눈과 귀’인가, ‘감시 카메라’인가… AI안경 확산 속 ‘이중잣대’ 팽배Inquirer EntertainmentComedian Arnell Tamayo dies at 61UOLPamela Anderson, Elle Fanning e Callum Turner estrelam novo filme de Karim Aïnouz nos cinemasZDF heuteEntdecken Sie das ZDF-NachrichtenstudioNew Straits TimesMessi's Inter Miami book playoffs with 2-0 win over New York CityLa NaciónLa foto de Shakira con Antonio de la Rúa en MadridEl ComercioTemblor en Chile: reportes de los últimos sismos del miércoles 14 de octubreGIGAZINE「荷物をお届けできません」「料金が未払いです」などの日本人を狙った詐欺メッセージの送信を支える電子市場をMicrosoftと警察庁が解体
The Daily Newsstand · Free, Always
Thursday, October 15, 2026

Звукорежиссёры сводят фильмы для кинозала, поэтому громкость дома я выравниваю расширением для браузера

Translate

По требованиям Netflix к сведению диалог стоит на −27 LKFS, а пики допускаются до −2 dBTP, и дома на колонках ноутбука я весь фильм крутил громкость. Я написал расширение для Chrome, Edge, Яндекс Браузера, Opera и Firefox, которое каждые 100 мс измеряет громкость видео и подстраивает усиление. На тестовом фрагменте разница между диалогом и экшеном уменьшилась с 16,1 до 3,8 дБ.

Громкость фильмов дома

Фильмы я смотрю на колонках ноутбука и на внешних колонках, и в каждом фильме слышу один и тот же разброс: диалог тихий, взрывы и музыка громкие. Я ставлю громкость по диалогу и на первом же взрыве тянусь к регулятору. Громкие сцены могут мешать соседям, но больше всего разброс раздражает меня самого.

Звукорежиссёр сводит фильм для кинозала. Колонки там откалиброваны по SMPTE RP 200: розовый шум на −20 dBFS даёт 85 дБ SPL от каждого основного канала. Всё, что громче, вплоть до 0 dBFS, остаётся запасом для взрывов и музыки, это 20 дБ над опорным уровнем.

Если звук 5.1 сводится в стерео по ITU-R BS.775 или по правилам из спецификации Web Audio, центральный канал, в котором обычно записан диалог, входит в левый и правый с коэффициентом √0,5, то есть на 3 дБ тише. Левый и правый каналы с музыкой и эффектами входят без ослабления.

Звукорежиссёры сводят по этим стандартам, и выравнивать громкость под колонки ноутбука остаётся зрителю. Я написал для этого расширение.

До него я ставил Equalizer APO. Настраивать его неудобно, и он обрабатывает весь звук Windows. Мне нужна была обработка только видео в браузере, с выключателем для отдельного сайта.

Ещё мне нужно было усиление выше 100%. Ролик на YouTube, который я открыл в Edge 155, расширение измерило на −18 LUFS. Работать всё это должно в плеерах на разных сайтах, включая встроенные плееры с другого домена.

Цепочка обработки

Расширение ждёт событие play у элемента <video> или <audio> и подключает его через createMediaElementSource к цепочке Web Audio. Скрипт запускается в каждом фрейме страницы (all_frames, match_about_blank), и в каждом фрейме у цепочки свой AudioContext:

  1. Два фильтра режима «Чётче речь»: минус 4 дБ ниже 160 Гц и плюс 4 дБ около 2,8 кГц.

  2. Узел GainNode, которым управляет автоматическая регулировка усиления.

  3. Компрессор DynamicsCompressorNode для коротких скачков, которые регулировка не успевает поймать.

  4. Усиление из ползунка, от 100% до 600%.

  5. Лимитер на втором DynamicsCompressorNode с порогом −2 дБ и WaveShaperNode, который мягко ограничивает сигнал выше 0,8 от полной шкалы.

AudioWorklet я не использовал. Модуль для него загружается отдельным скриптом, и на сайтах со строгой политикой CSP, например на YouTube, браузер может его заблокировать. Встроенные узлы Web Audio от CSP не зависят.

Некоторые элементы расширение не трогает. У элемента с srcObject идёт поток WebRTC, это звонки в Google Meet и Discord. Если страница уже подключила элемент к своему графу Web Audio, второй вызов createMediaElementSource для того же элемента заканчивается исключением, и такой элемент тоже остаётся без обработки.

Регулировка усиления

Громкость я измеряю по ITU-R BS.1770. Сигнал проходит K-фильтр из двух BiquadFilterNode: полка +4 дБ на 1681 Гц и срез ниже 38 Гц. После фильтра стоит AnalyserNode с окном 8192 отсчёта. Каждые 100 мс расширение считает средний квадрат сигнала и усредняет последние четыре замера, то есть громкость за 400 мс, как momentary loudness в EBU R128.

AnalyserNode сводит стерео в моно усреднением каналов. При одинаковых каналах средний квадрат получается на 3 дБ ниже суммы каналов, которую берёт BS.1770, и я прибавляю к результату 3,01 дБ.

const level = this.momentary;                       // LUFS за последние 400 мс
const desired = clamp(p.target - level, -p.maxCut, p.maxBoost);
// далеко от нужного усиления (диалог сразу после взрыва) возвращаемся быстрее
const tc = desired < this.gainDb ? p.attack : p.release * (desired - this.gainDb > 8 ? 0.35 : 1);
this.gainDb += (desired - this.gainDb) * (1 - Math.exp(-dt / tc));
this.agc.gain.setTargetAtTime(dbToGain(this.gainDb), now, 0.04);

Цель регулировки −20 LUFS. Если громкость ниже −50 LUFS, усиление остаётся прежним, чтобы в паузах не поднимались тишина и шум. Режимы отличаются диапазоном и скоростью:

Режим

Подъём тихого, до

Снижение громкого, до

Время снижения

Время возврата

Компрессор

Мягко

+6 дБ

−6 дБ

0,5 с

3 с

порог −16 дБ, 2:1

Авто

+12 дБ

−12 дБ

0,3 с

2 с

порог −22 дБ, 3:1

Ночь

+20 дБ

−20 дБ

0,2 с

1,5 с

порог −28 дБ, 6:1

В разделе «Тонкая настройка» эти параметры можно поменять ползунками: целевую громкость, подъём и снижение, время возврата, порог и степень сжатия компрессора, потолок лимитера.

Окно расширения с открытой тонкой настройкой

Окно расширения с открытой тонкой настройкой

В первой версии регулировку вёл трёхсекундный замер, как short-term loudness в EBU R128. На тестовом фрагменте после 12 секунд экшена усиление поднималось с −10 до +10 дБ около десяти секунд, и первые фразы диалога после взрыва звучали тише остальных. Я перевёл регулировку на замер за 400 мс, и в первые две секунды диалога после экшена громкость стала на 5,8 дБ ниже остального диалога. Потом я добавил ускоренный возврат: если до нужного усиления больше 8 дБ, постоянная времени умножается на 0,35. Провал сократился до 3,6 дБ.

Компенсирующее усиление в DynamicsCompressorNode

DynamicsCompressorNode сам добавляет компенсирующее усиление, это прописано в спецификации Web Audio. Я заметил его на тестовом стенде: тихий синус на −50 дБ проходил через компрессоры громче, чем входил.

Узел

Добавочное усиление на тихом сигнале

Компрессор режима «Мягко»

+3,6 дБ

Компрессор режима «Авто»

+6,9 дБ

Компрессор режима «Ночь»

+11,8 дБ

Лимитер с порогом −2 дБ

+1,1 дБ

В компрессорах режимов я его оставил: режим «Ночь» звучит громче «Мягкого», и для ночного просмотра это подходит. В лимитере оно мешало. При потолке −6 дБ пики на выходе доходили до −1,3 дБ. По формуле из спецификации прибавка равна (1 / curve(0 дБ))^0.6, и при нулевом колене кривая в нуле равна порог × (1 − 1/ratio). Я поставил после лимитера GainNode, который вычитает эту прибавку, и пики при потолке −6 дБ опустились до −4,7 дБ.

Политика автозапуска звука

AudioContext, созданный без действия пользователя, браузер оставляет в состоянии suspended. Если подключить видео к такому контексту, звук пропадёт совсем. Расширение вызывает resume() и ждёт 300 мс. Если контекст не запустился, элемент ждёт первого pointerdown или keydown на странице и подключается после него.

Видео с другого сервера

Многие плееры берут файл видео с сервера раздачи на другом домене. Если ответ сервера не разрешает чтение заголовками CORS, MediaElementAudioSourceNode выдаёт тишину. Видео при этом продолжает играть, но без звука.

В версии 1.0.0 расширение делало пробный запрос к файлу с mode: 'cors'. Если сервер разрешал чтение, расширение перезагружало элемент с атрибутом crossorigin с той же секунды и подключало к обработке. Если нет, видео оставалось без обработки, а окно расширения писало об этом. На встроенном плеере VK Видео расширение так и написало: «Видео грузится с другого сервера, который запрещает обработку». Сервер VK с видеофайлами отдаёт заголовок Access-Control-Allow-Origin: https://vkvideo.ru, и встроенный плеер на другом домене это разрешение не получает.

В версии 1.1.0 расширение добавляет заголовки само, правилом declarativeNetRequest на время сессии браузера:

await chrome.declarativeNetRequest.updateSessionRules({  removeRuleIds: [id],  addRules: [{    id,    priority: 1,    action: {      type: 'modifyHeaders',      responseHeaders: [        { header: 'access-control-allow-origin', operation: 'set', value: pageOrigin },        { header: 'access-control-allow-credentials', operation: 'set', value: 'true' },      ],    },    condition: { requestDomains: [mediaHost], initiatorDomains: [pageHost], resourceTypes: ['media'] },  }],
});

Правило действует только для пары «страница и сервер с видео» и только для запросов типа media, которые делают элементы <video> и <audio>. Номер правила я считаю хешем FNV-1a от этой пары, поэтому повторный запрос для той же пары заменяет правило и не плодит новые. После этого расширение ставит элементу crossorigin="use-credentials", чтобы cookie уходили на сервер так же, как до перезагрузки, и возвращает воспроизведение на ту же секунду и с той же скоростью. Если за 8 секунд метаданные не загрузились или элемент выдал ошибку, расширение снимает атрибут и перезагружает видео в прежнем виде.

Встроенные плееры VK Видео на версии 1.1.0 я проверил сам в Firefox, звук обрабатывается.

Плееры в Shadow DOM

Плееры, собранные из веб-компонентов, держат <video> внутри Shadow DOM. Событие play не всплывает и не выходит за границу теневого дерева (composed: false), и обработчик на document его не видит. Расширение раз в 2 секунды обходит страницу, заходит в теневые деревья и вешает на каждое свой обработчик play.

Закрытое теневое дерево из скрипта страницы недоступно, но расширению браузеры его отдают, и делают это по-разному. В Chromium это функция chrome.dom.openOrClosedShadowRoot(el). В Firefox это свойство el.openOrClosedShadowRoot. Сначала я вызывал его как функцию в обоих браузерах, и тест в Firefox упал с TypeError: el.openOrClosedShadowRoot is not a function.

Выключатель для сайта во фреймах

Переключатель «На этом сайте» хранит адрес вкладки, а видео часто играет во фрейме плеера с другого домена. Скрипт во фрейме узнаёт адрес вкладки так: сначала читает window.top.location, а если фрейм с другого домена и браузер это запрещает, берёт последний элемент location.ancestorOrigins. Если и этого свойства нет, скрипт берёт document.referrer.

DRM и захват вкладки

Видео с защитой DRM браузер отдаёт в Web Audio тишиной, и элементы с mediaKeys расширение не подключает. Для них в браузерах на Chromium есть захват звука всей вкладки: chrome.tabCapture.getMediaStreamId в фоновом скрипте и getUserMedia в offscreen-документе, где звук проходит ту же цепочку. Пока захват включён, вкладка звучит только через расширение, а скрипты на странице пропускают свои элементы без обработки, чтобы звук не прошёл цепочку дважды.

Без действия пользователя tabCapture не работает. Захват включается, когда пользователь открывает окно расширения на вкладке с таким видео, кнопкой в окне или сочетанием Alt+Shift+V: нажатие сочетания браузер тоже считает обращением к расширению.

В Chromium фоновый скрипт работает как service worker, который браузер останавливает между событиями, поэтому список вкладок с захватом лежит в storage.session.

Я проверил захват на примере dash.js с шифрованием ClearKey. Это тот же механизм EME, что у Netflix, но с открытым ключом. Через захват в обработку пришёл звук громкостью −12,4 LUFS, и регулировка опустила его на 7,6 дБ.

Одна сборка на два браузера

Манифест в репозитории написан для Chromium. Скрипт сборки делает из него версию для Firefox: фоновый скрипт объявлен через background.scripts и работает как event page, из разрешений уходят tabCapture и offscreen, из манифеста уходит сочетание клавиш, из архива offscreen-документ. Для AMO в манифесте есть browser_specific_settings.gecko с data_collection_permissions: none, без этого поля новые расширения в каталог не принимают.

Версию для Firefox проверяет web-ext lint с предупреждениями в роли ошибок. Из-за него обращение к chrome.dom в коде записано как chrome['dom']: в Firefox такого API нет, и на прямой вызов линтер выдавал предупреждение. По тегу v* GitHub Actions собирает оба архива и прикладывает их к релизу. Если в секретах репозитория лежат ключи API addons.mozilla.org, тот же процесс отправляет новую версию в каталог через web-ext sign --channel listed. Эти ключи я пока не добавил. Версию 1.2.1 Mozilla одобрила 13 октября 2026 года.

Как я проверял

Тестовый стенд прогоняет через цепочку в OfflineAudioContext синтетический фрагмент: 12 секунд тихого диалога, 12 секунд экшена со взрывами и снова 12 секунд диалога. Регулировку я вызываю через suspend() каждые 100 мс, как таймер в расширении. Уровни в таблице посчитаны как медиана среднеквадратичного уровня по окнам 400 мс, в дБ от полной шкалы, при громкости 100%.

Уровень тестового фрагмента по окнам 400 мс без расширения и в режиме «Авто»

Уровень тестового фрагмента по окнам 400 мс без расширения и в режиме «Авто»

Режим

Диалог

Экшен

Разница

Пик

Без расширения

−40,9

−24,8

16,1 дБ

+0,7 дБ

Выравнивание выключено

−41,1

−24,8

16,2 дБ

−1,4 дБ

Мягко

−33,9

−23,5

10,4 дБ

−1,2 дБ

Авто

−25,2

−21,4

3,8 дБ

−1,2 дБ

Ночь

−22,8

−19,8

3,0 дБ

−2,7 дБ

Сквозные тесты ставят расширение в Edge и Firefox. Edge 155 я запускал через Playwright, Firefox 157 через WebDriver BiDi: команда webExtension.install ставит расширение временно, без подписи. Тесты проверяют подъём диалога и снижение экшена, подключение видео с чужого сервера с CORS и без, плеер в закрытом Shadow DOM и захват вкладки. Для видео с чужого сервера тест сравнивает уровень на входе обработки с порогом −50 LUFS, чтобы отличить звук от тишины.

На YouTube видео идёт через MSE с адресом blob:, и расширение подключается к нему без правил CORS. На ролике, который я открыл в Edge 155, регулировка опустила громкость на 4 дБ. Сам я смотрю через расширение в Firefox RuTube, YouTube и VK Видео, и на всех трёх сайтах оно подключается к плееру.

Ограничения

Обработка Web Audio добавляет задержку вывода звука, при захвате вкладки задержка больше. Я её не замерял, на тестовых видео рассинхрон губ и звука я на слух не заметил. Firefox для Android поддерживается манифестом, на телефоне я расширение не проверял.

Код и установка

Расширение открытое, лицензия MIT. Большую часть кода написал Claude от Anthropic, я ставил задачи и проверял результат в своих браузерах.

Расширение не собирает данные и само не делает сетевых запросов. Настройки лежат в storage.sync. Окно переведено на 18 языков.

YouTube я проверял в Edge 155 и Firefox, RuTube и VK Видео вручную в Firefox, встроенный плеер VK Видео в Firefox на версии расширения 1.1.0, тестовую страницу в Edge 155 и Firefox 157 на Windows 10. Если на каком-то сайте расширение не подключилось, напишите в комментариях или в issue адрес страницы, браузер с версией и строку из нижней части окна расширения во время воспроизведения.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.