---
title: "Звук в браузерной игре на Web Audio API: шины громкости на GainNode, автозапуск, лимит голосов и децибелы"
description: "Как устроить звук браузерной 2D-игры на Web Audio API: шины master, музыка, эффекты и интерфейс на GainNode, запуск AudioContext после первого действия игрока по политике автозапуска, перевод между децибелами и линейной громкостью по формуле 20 × log10(g), лимит одновременных голосов, кулдаун и вытеснение по приоритету. Код на JavaScript проверен в Node.js."
slug: zvuk-v-brauzernoj-igre-web-audio-api-shiny-i-golosa
date: 2026-11-03
updated: 2026-11-03
scheduled_at: "2026-11-03T14:00:00+03:00"
rubric: gejmdizajn-i-mehaniki
tags: ["zvuk", "web-audio-api", "javascript", "brauzer", "novichkam"]
reading_time: 9
draft: false
summary: "Звук браузерной игры на Web Audio API держится на четырёх решениях. Все звуки идут через шины на GainNode (общая, музыка, эффекты, интерфейс), поэтому громкость группы меняется одним вызовом. AudioContext запускают через resume() по первому клику или нажатию клавиши, потому что до действия игрока браузер держит звук на паузе. Громкость хранят в децибелах и переводят в множитель по формуле 20 × log10(g). А одинаковые звуки ограничивают кулдауном и лимитом голосов, иначе десять монет в одном кадре дадут до +20 дБ."
---

## Коротко

Звук браузерной игры на Web Audio API держится на четырёх решениях: шины громкости на `GainNode`, запуск `AudioContext` после первого действия игрока, громкость в децибелах и лимит одновременных голосов. Шины позволяют менять громкость музыки или эффектов одним вызовом. Политика автозапуска держит звук на паузе до клика, касания или нажатия клавиши, поэтому контекст «будят» через `resume()`. А лимит голосов и кулдаун не дают десяти одинаковым звукам в одном кадре сложиться в скачок до `+20 дБ`.

## Содержание

1. Шины громкости: граф из GainNode
2. Автозапуск: почему звук молчит до первого клика
3. Децибелы и линейная громкость
4. Лимит голосов и защита от наложения
5. Практический сценарий: монеты, музыка и фанфара победы
6. Когда вам это не подойдёт

## Шины громкости: граф из GainNode

Шина — это `GainNode`, через который проходят несколько звуков, поэтому громкость всей группы меняется одним значением. По MDN, `GainNode` — узел обработки звука с одним входом и одним выходом, а его параметр `gain` — безразмерный множитель, на который умножается каждый отсчёт сигнала (MDN, *GainNode*, проверено: ноябрь 2026).

Таблица: Типовая схема шин 2D-игры

| Шина | Что в неё идёт | Зацикливание | Что с ней делают |
|---|---|---|---|
| `master` | Все шины ниже | — | Общий ползунок громкости и выключение звука |
| `music` | Тема уровня, тема босса | Да | Ползунок «Музыка», приглушение на время фанфары и реплик |
| `sfx` | Прыжок, монета, удар, взрыв | Нет | Ползунок «Эффекты», лимит голосов |
| `ui` | Клик, открытие паузы | Нет | Обычно вместе с эффектами |

```js
// Перевод между децибелами и линейной громкостью (множителем).
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()*).

```js
// Разблокировка звука по первому действию игрока.
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 — множитель амплитуды, а настраивать звук удобнее в децибелах, потому что слух оценивает громкость примерно по логарифмической шкале. Перевод стандартный для амплитудных величин:

```text
дБ = 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 дБ`. Удобнее, чтобы ползунок шёл по децибелам:

```js
// Ползунок 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*), — по нему голос освобождается.

```js
// Менеджер голосов: кулдаун, лимит копий одного звука, общий лимит с вытеснением по приоритету.
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` кадров, не даёт скачка громкости, а фанфара победы приглушает музыку и возвращает её после окончания.

1. **Контекст и шины.** Создайте `AudioContext`, вызовите `createMixer(ctx)` и `createVoiceManager(ctx, 16)`; общий лимит `16` голосов — наш выбор для 2D-игры.
2. **Разблокировка.** Вызовите `unlockOnFirstGesture(ctx, window, startMusic)`; в `startMusic` создайте источник музыки и подключите его к шине `music`: короткую петлю — как `AudioBufferSourceNode` с `loop = true`, длинный трек — через `MediaElementAudioSourceNode` (см. последний раздел).
3. **Громкость музыки.** `mixer.setBusDb("music", -6)` — музыка на `6 дБ` тише эффектов.
4. **Монеты.** На каждую подобранную монету — `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 с поддельным контекстом.
5. **Фанфара.** При касании финиша — `mixer.setBusDb("music", -20)`, затем `voices.play("fanfare", ...)` с приоритетом `9`; в обработчике `ended` фанфары — `mixer.setBusDb("music", -6)`.
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; он в разработке и публично пока не открыт, заявку можно оставить на [странице раннего доступа](/early-access?utm_source=blog&utm_medium=article&utm_campaign=zvuk-v-brauzernoj-igre-web-audio-api-shiny-i-golosa) (подача заявки не гарантирует приглашение).

**Источники (проверено: ноябрь 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 голосов, расчёт памяти буфера — наши значения.

**Читайте также:**
- [Камера, параллакс и «джус» (Game Feel)](/blog/kamera-parallaks-i-game-feel-v-2d-igre)
- [Как найти просадку FPS в браузерной 2D-игре](/blog/prosadki-fps-v-brauzernoj-igre-chrome-devtools-performance)
- [Сохранения в браузерной игре: localStorage или IndexedDB](/blog/sohraneniya-v-brauzernoj-igre-localstorage-indexeddb)
