Коротко
Звук браузерной игры на Web Audio API держится на четырёх решениях: шины громкости на GainNode, запуск AudioContext после первого действия игрока, громкость в децибелах и лимит одновременных голосов. Шины позволяют менять громкость музыки или эффектов одним вызовом. Политика автозапуска держит звук на паузе до клика, касания или нажатия клавиши, поэтому контекст «будят» через resume(). А лимит голосов и кулдаун не дают десяти одинаковым звукам в одном кадре сложиться в скачок до +20 дБ.
Содержание
- Шины громкости: граф из GainNode
- Автозапуск: почему звук молчит до первого клика
- Децибелы и линейная громкость
- Лимит голосов и защита от наложения
- Практический сценарий: монеты, музыка и фанфара победы
- Когда вам это не подойдёт
Шины громкости: граф из GainNode
Шина — это GainNode, через который проходят несколько звуков, поэтому громкость всей группы меняется одним значением. По MDN, GainNode — узел обработки звука с одним входом и одним выходом, а его параметр gain — безразмерный множитель, на который умножается каждый отсчёт сигнала (MDN, GainNode, проверено: ноябрь 2026).
| Шина | Что в неё идёт | Зацикливание | Что с ней делают |
|---|---|---|---|
master | Все шины ниже | — | Общий ползунок громкости и выключение звука |
music | Тема уровня, тема босса | Да | Ползунок «Музыка», приглушение на время фанфары и реплик |
sfx | Прыжок, монета, удар, взрыв | Нет | Ползунок «Эффекты», лимит голосов |
ui | Клик, открытие паузы | Нет | Обычно вместе с эффектами |
// Перевод между децибелами и линейной громкостью (множителем).
function dbToGain(db) { return Math.pow(10, db / 20); }
function gainToDb(g) { return 20 * Math.log10(g); }
// Шины: каждый источник подключают к своей шине, шины — к master, master — к выходу.
function createMixer(ctx) {
const master = ctx.createGain();
master.connect(ctx.destination);
const buses = {};
for (const name of ["music", "sfx", "ui"]) {
const bus = ctx.createGain();
bus.connect(master);
buses[name] = bus;
}
// Плавная смена громкости: за 3 постоянных времени (60 мс) значение проходит около 95% пути.
function setGain(node, value) {
node.gain.setTargetAtTime(value, ctx.currentTime, 0.02);
}
return {
master: master,
buses: buses,
setBusDb: function (name, db) { setGain(name === "master" ? master : buses[name], dbToGain(db)); },
mute: function (on) { setGain(master, on ? 0 : 1); },
};
}
Громкость меняют не присваиванием gain.value, а через setTargetAtTime(): метод планирует плавный переход, и после каждого отрезка длиной timeConstant значение приближается к цели ещё на ≈ 63,2%; для 95% пути MDN советует брать постоянную времени равной трети нужной длительности (MDN, AudioParam: setTargetAtTime(), проверено: ноябрь 2026). Резкий скачок громкости на слух часто даёт щелчок, поэтому плавный переход в 60 мс — наш стартовый выбор.
Автозапуск: почему звук молчит до первого клика
Браузеры не дают странице начать звук, пока пользователь с ней не взаимодействовал, и это касается Web Audio API. MDN пишет, что в Web Audio API звук запускают методом start() у источника, подключённого к AudioContext, и если это происходит вне обработки действия пользователя, применяются правила автозапуска; как правило, звук разрешён, если пользователь уже взаимодействовал с сайтом кликом, касанием или нажатием клавиши (MDN, Autoplay guide for media and Web Audio APIs, страница обновлена 10.09.2026, проверено: ноябрь 2026).
В Chrome политика автозапуска для Web Audio API действует с версии 71. Если AudioContext создан до действия пользователя, он находится в состоянии "suspended", и после действия пользователя нужно вызвать resume() (Chrome for Developers, Autoplay policy in Chrome, проверено: ноябрь 2026). resume() возвращает Promise, который выполняется, когда контекст возобновлён (MDN, AudioContext: resume()).
// Разблокировка звука по первому действию игрока.
function unlockOnFirstGesture(ctx, target, onUnlocked) {
const events = ["pointerdown", "keydown", "touchend"];
function unlock() {
events.forEach(function (e) { target.removeEventListener(e, unlock); });
const p = ctx.state === "suspended" ? ctx.resume() : Promise.resolve();
p.then(function () { if (onUnlocked) onUnlocked(); });
}
events.forEach(function (e) { target.addEventListener(e, unlock); });
}
// В браузере:
// const ctx = new AudioContext();
// const mixer = createMixer(ctx);
// unlockOnFirstGesture(ctx, window, startMusic);
Практический вывод для игры: экран «Нажмите, чтобы начать» — не только дизайн, но и способ получить первое действие игрока, после которого музыка уровня запустится без ошибок.
Децибелы и линейная громкость
Громкость в Web Audio API — множитель амплитуды, а настраивать звук удобнее в децибелах, потому что слух оценивает громкость примерно по логарифмической шкале. Перевод стандартный для амплитудных величин:
дБ = 20 × log10(g) g = 10^(дБ ÷ 20)
g = 1,00 → 0 дБ (исходная громкость файла)
g = 0,50 → −6 дБ
g = 0,25 → −12 дБ
g = 0,10 → −20 дБ
2 одинаковых сигнала в один момент: 20 × log10(2) ≈ +6 дБ
10 одинаковых сигналов: 20 × log10(10) = +20 дБ
Отсюда частая ошибка ползунков: если ползунок 0…1 напрямую задаёт gain, почти весь слышимый диапазон сжимается в правую часть шкалы, а середина 0,5 — это лишь −6 дБ. Удобнее, чтобы ползунок шёл по децибелам:
// Ползунок 0…1 → громкость: линейно по децибелам от −40 дБ до 0 дБ, в нуле — тишина.
function sliderToGain(s, floorDb) {
if (s <= 0) return 0;
return dbToGain((floorDb || -40) * (1 - s));
}
console.log(gainToDb(0.5).toFixed(1), gainToDb(0.25).toFixed(1), gainToDb(0.1).toFixed(1));
// -6.0 -12.0 -20.0
console.log(sliderToGain(1), sliderToGain(0.5), sliderToGain(0.75).toFixed(3), sliderToGain(0));
// 1 0.1 0.316 0
Нижняя граница −40 дБ — наш выбор, а не стандарт. Для общего уровня игры есть индустриальный ориентир: рекомендация ASWG-R001 рабочей группы Sony PlayStation по звуковым стандартам (ASWG, версия 1.2, март 2024) советует нормировать среднюю громкость к −24 LKFS (±2) для домашних платформ и к −18 LKFS (±2) для портативных, а истинный пик — не выше −1 dBTP. Это ориентир для консолей; для браузерной игры он полезен как точка сравнения, а не как требование.
Лимит голосов и защита от наложения
Одинаковый короткий звук нельзя запускать при каждом событии без ограничений: копии складываются, и ряд монет в одном кадре даёт скачок громкости до +20 дБ. Ограничений обычно три: кулдаун между запусками одного звука, лимит копий одного звука и общий лимит голосов с вытеснением наименее важного.
Лимит голосов — старая практика: у звукового блока NES было 5 каналов (два прямоугольных, треугольный, шумовой и канал DPCM-сэмплов), поэтому композиторы заранее решали, какой звук вытесняет какой (Wikipedia, Ricoh 2A03, проверено: ноябрь 2026). В Web Audio API жёсткого лимита нет, но слышимая каша и нагрузка на процессор растут с каждым голосом.
Для каждого запуска нужен новый источник: AudioBufferSourceNode можно запустить только один раз, но такие узлы дешёвы, а сам AudioBuffer можно переиспользовать для многих запусков (MDN, AudioBufferSourceNode, проверено: ноябрь 2026). Событие ended срабатывает, когда источник перестал играть (MDN, AudioScheduledSourceNode: ended event), — по нему голос освобождается.
// Менеджер голосов: кулдаун, лимит копий одного звука, общий лимит с вытеснением по приоритету.
function createVoiceManager(ctx, maxTotal, random) {
const rnd = random || Math.random;
const active = [];
const lastStart = {};
function remove(v) {
const i = active.indexOf(v);
if (i !== -1) active.splice(i, 1);
}
function stopVoice(v) {
remove(v);
try { v.src.stop(); } catch (e) { /* уже остановлен */ }
}
return {
// rule: { maxCopies, cooldown (с), priority (больше — важнее), pitchJitter (доля) }
play: function (name, buffer, bus, rule) {
const now = ctx.currentTime;
if (lastStart[name] !== undefined && now - lastStart[name] < rule.cooldown) return null;
const copies = active.filter(function (v) { return v.name === name; });
if (copies.length >= rule.maxCopies) stopVoice(copies[0]); // самая старая копия
if (active.length >= maxTotal) {
let victim = null;
for (const v of active) {
if (!victim || v.priority < victim.priority ||
(v.priority === victim.priority && v.startedAt < victim.startedAt)) victim = v;
}
if (victim.priority > rule.priority) return null; // новый звук менее важен
stopVoice(victim);
}
const src = ctx.createBufferSource();
src.buffer = buffer;
src.playbackRate.value = 1 + (rnd() * 2 - 1) * rule.pitchJitter;
src.connect(bus);
const voice = { name: name, priority: rule.priority, startedAt: now, src: src };
src.onended = function () { remove(voice); };
active.push(voice);
lastStart[name] = now;
src.start();
return voice;
},
count: function () { return active.length; },
};
}
Разброс скорости воспроизведения ±5% (pitchJitter: 0.05) делает повторяющиеся звуки менее одинаковыми; это наш стартовый параметр, а не стандарт.
| Звук | Копий одновременно | Кулдаун | Приоритет | Разброс скорости |
|---|---|---|---|---|
| Монета | 3 | 0,06 с | 1 | ±5% |
| Выстрел игрока | 4 | 0,05 с | 2 | ±3% |
| Взрыв | 2 | 0,1 с | 3 | ±5% |
| Урон игроку | 1 | 0,2 с | 5 | 0 |
| Фанфара победы | 1 | — | 9 | 0 |
Все значения в таблице — наши стартовые параметры: их подбирают на слух под длину своих звуков.
Практический сценарий: монеты, музыка и фанфара победы
Задача: музыка уровня стартует после «Нажмите, чтобы начать», ряд из 12 монет, собранных за 12 кадров, не даёт скачка громкости, а фанфара победы приглушает музыку и возвращает её после окончания.
- Контекст и шины. Создайте
AudioContext, вызовитеcreateMixer(ctx)иcreateVoiceManager(ctx, 16); общий лимит16голосов — наш выбор для 2D-игры. - Разблокировка. Вызовите
unlockOnFirstGesture(ctx, window, startMusic); вstartMusicсоздайте источник музыки и подключите его к шинеmusic: короткую петлю — какAudioBufferSourceNodeсloop = true, длинный трек — черезMediaElementAudioSourceNode(см. последний раздел). - Громкость музыки.
mixer.setBusDb("music", -6)— музыка на6 дБтише эффектов. - Монеты. На каждую подобранную монету —
voices.play("coin", coinBuffer, mixer.buses.sfx, { maxCopies: 3, cooldown: 0.06, priority: 1, pitchJitter: 0.05 }). При60кадрах в секунду и кулдауне0,06 сиз12монет за12кадров прозвучат3(на 0-м, 4-м и 8-м кадрах), что мы проверили в Node.js с поддельным контекстом. - Фанфара. При касании финиша —
mixer.setBusDb("music", -20), затемvoices.play("fanfare", ...)с приоритетом9; в обработчикеendedфанфары —mixer.setBusDb("music", -6). - Вкладка в фоне. По событию
visibilitychangeпри скрытой вкладке вызывайтеctx.suspend(), при возврате —ctx.resume(), чтобы музыка не играла, пока игра стоит.
| Проверка | Ожидаемый результат |
|---|---|
| Открыть страницу и ничего не нажимать | Тишина, ошибок в консоли нет |
| Нажать «начать» | Музыка стартует с первого действия |
| Собрать ряд монет | Звон без скачка громкости, звуки слегка различаются |
| Дойти до финиша | Музыка уходит на задний план, после фанфары возвращается без щелчка |
| Переключить вкладку и вернуться | Музыка стоит в фоне и продолжается при возврате |
Когда вам это не подойдёт
Схема шин, разблокировки и лимита голосов покрывает звук большинства 2D-игр, но не всё:
- Ритм-игра с точностью до миллисекунды. Синхронизация нажатий с долей такта требует калибровки задержки вывода звука на каждом устройстве; менеджер голосов из статьи этого не решает.
- Длинная музыка в буфере. MDN описывает
AudioBufferкак хранилище коротких фрагментов, обычно до45 с, в формате 32-битных чисел с плавающей точкой, и для длинных звуков советуетMediaElementAudioSourceNode(MDN, AudioBuffer). По нашему расчёту трек3 минстерео при48 кГцзанимает в буфере180 × 48 000 × 2 × 4 байта ≈ 69 МБ. - Пространственный звук. Панорама и шины подходят для 2D-сцены, но не заменяют объёмный звук 3D-игр.
- Нужно создавать звуки с нуля. Статья о воспроизведении и сведении; сами звуки готовят во внешних программах.
Мы разрабатываем J2DRU — браузерный 2D игровой движок с событийной логикой и путём к TypeScript; он в разработке и публично пока не открыт, заявку можно оставить на странице раннего доступа (подача заявки не гарантирует приглашение).
Источники (проверено: ноябрь 2026): MDN, GainNode; MDN, AudioParam: setTargetAtTime(); MDN, Autoplay guide for media and Web Audio APIs (обновлена 10.09.2026); Chrome for Developers, Autoplay policy in Chrome (Web Audio API — с Chrome 71, состояние suspended и resume()); MDN, AudioContext: resume(); MDN, AudioBufferSourceNode; MDN, AudioBuffer (до 45 с, 32-битные числа); MDN, AudioScheduledSourceNode: ended event; Sony ASWG, ASWG-R001 v1.2 (март 2024, репозиторий Sony-ASWG/Loudness на GitHub); Wikipedia, Ricoh 2A03. Формула 20 × log10(g) — стандартное определение уровня для амплитудных величин; таблица стартовых правил, −40 дБ, 60 мс и лимит 16 голосов, расчёт памяти буфера — наши значения.
Читайте также: