Продвинутые темы

Этот раздел покрывает очень важные темы, однако они чуть более сложные, чем в остальной книге. Мы погрузимся в вопросы добавления эффектов к звукам, генерации синтезированных звуковых эффектов без использования аудиобуферов, симуляции эффектов различных акустических сред, а также пространственного звука в 3D-пространстве.

Биквадратные фильтры


Фильтр может усиливать или ослаблять определённые части частотного спектра звука. Визуально это можно показать в виде графика в частотной области, который называется графиком частотной характеристики (см. Рисунок 6-1). Для каждой частоты действует правило: чем выше значение графика, тем сильнее подчёркивается эта часть диапазона. Если график идёт на спад, это значит, что больший акцент делается на низких частотах и меньший — на высоких.

Фильтры Web Audio можно настроить с помощью трёх параметров: усиления (gain), частоты (frequency) и коэффициента качества (quality factor, также известный как Q). Каждый из этих параметров по‑своему влияет на график частотной характеристики.

Существует множество фильтров, которые используются для создания разных эффектов:

Низкочастотный фильтр (Low-pass filter)

Делает звук более приглушённым.

Высокочастотный фильтр (High-pass filter)

Делает звук более «тонким», с акцентом на высоких частотах.

Полосовой фильтр (Band-pass filter)

Отсекает низкие и высокие частоты (например, «телефонный» эффект).

Полочный фильтр нижних частот (Low-shelf filter)

Управляет количеством баса в звуке (как регулятор Bass на стереосистеме).

Полочный фильтр верхних частот (High-shelf filter)

Управляет количеством высоких частот (как регулятор Treble на стереосистеме).

Пиковый фильтр (Peaking filter)

Управляет количеством средних частот (как регулятор Mid на стереосистеме).

Режекторный фильтр (Notch filter)

Удаляет нежелательные звуки в узком диапазоне частот.

Всепропускающий фильтр (All-pass filter)

Создаёт эффекты типа phaser.

Рисунок 6-1. График частотной характеристики низкочастотного фильтра
Рисунок 6-1. График частотной характеристики низкочастотного фильтра

Все эти биквадратные фильтры основаны на общей математической модели, и их графики можно построить так же, как у низкочастотного фильтра на Рисунке 6-1. Более подробную информацию о таких фильтрах можно найти в книгах с более серьёзной математической базой, например, Real Sound Synthesis for Interactive Applications Перри Р. Кука (A K Peters, 2002). Я настоятельно рекомендую её прочесть, если вы интересуетесь фундаментальными основами аудио.

Добавление эффектов с помощью фильтров

Используя Web Audio API, мы можем применять фильтры с помощью узлов BiquadFilterNode. Этот тип аудиоузла очень часто используется для построения эквалайзеров и различных звуковых эффектов. Давайте настроим простой фильтр нижних частот, чтобы убрать низкочастотный шум из звукового семпла:

// Создаём фильтр
var filter = context.createBiquadFilter();

// Примечание: спецификация Web Audio переходит от констант к строкам.
// filter.type = 'lowpass';
filter.type = filter.LOWPASS;
filter.frequency.value = 100;

// Подключаем источник к фильтру, и фильтр к аудиовыходу.
source.connect(filter);
filter.connect(destination);

Узлы BiquadFilterNode поддерживают все часто используемые фильтры второго порядка. Мы можем настроить эти узлы с помощью параметров, о которых мы говорили ранее, а также визуализировать их частотные характеристики, используя метод getFrequencyResponse(). Метод принимает массив частот и возвращает массив значений амплитудных откликов, соответствующих каждой переданной частоте.

Крис Уилсон и Крис Роджерс подготовили отличный пример визуализатора (см. Рисунок 6-2), который показывает частотные характеристики всех типов фильтров, доступных в Web Audio API.

Рисунок 6-2. График частотной характеристики низкочастотного фильтра с параметрами
Рисунок 6-2. График частотной характеристики низкочастотного фильтра с параметрами

Процедурно сгенерированный звук

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

  1. Звуковые файлы будут слишком большими. Это особенно плохо для веба, в котором звуки загружаются не с жёсткого диска, а из интернета (по крайней мере, в первый раз), что примерно на порядок медленнее.

  2. Даже при наличии множества разных ресурсов и изменений в них, их вариативность ограничена.

  3. Вам необходимо найти ресурсы, просматривая библиотеки звуковых эффектов, а затем, возможно, беспокоиться о роялти их авторам. К тому же, скорее всего, тот или иной звуковой эффект будет уже использоваться в других приложениях, и у пользователей могут возникнуть непредвиденные ассоциации.

Мы можем использовать Web Audio API для того, чтобы полностью сгенерировать звуки процедурно. Для примера, давайте сэмулируем звук стрельбы. Мы начнём с буфера, содержащего белый шум, для этого сгенерируем шум с помощью узла ScriptProcessorNode (прим. переводчика: этот узел устарел и заменён на AudioWorklet в новых версиях Web Audio API):

function WhiteNoiseScript() {
  this.node = context.createScriptProcessor(1024, 1, 2);
  this.node.onaudioprocess = this.process;
}

WhiteNoiseScript.prototype.process = function(e) {
  var L = e.outputBuffer.getChannelData(0);
  var R = e.outputBuffer.getChannelData(1);

  for (var i = 0; i < L.length; i++) {
    L[i] = ((Math.random() * 2) - 1);
    R[i] = L[i];
  }
};

Для получения дополнительной информации о ScriptProcessorNode смотрите раздел Обработка аудио с помощью JavaScript.

Этот код не слишком производителен, потому что JavaScript вынужден динамически создавать поток белого шума. Чтобы увеличить производительность, мы можем программно сгенерировать моноканальный аудиобуфер с белым шумом, как в следующем примере кода:

function WhiteNoiseGenerated(callback) {
  // Генерируем 5-секундный буфер белого шума
  var lengthInSamples = 5 * context.sampleRate;
  var buffer = context.createBuffer(1, lengthInSamples, context.sampleRate);
  var data = buffer.getChannelData(0);

  for (var i = 0; i < lengthInSamples; i++) {
    data[i] = ((Math.random() * 2) - 1);
  }

  // Создаём источник из буфера
  this.node = context.createBufferSource();
  this.node.buffer = buffer;
  this.node.loop = true;
  this.node.start(0);
}

Далее мы можем смоделировать различные фазы выстрела с помощью огибающей (envelope): атаку (attack), спад (decay) и затухание (release):

function Envelope() {
  this.node = context.createGain()
  this.node.gain.value = 0;
}

Envelope.prototype.addEventToQueue = function() {
  this.node.gain.linearRampToValueAtTime(0, context.currentTime);
  this.node.gain.linearRampToValueAtTime(1, context.currentTime + 0.001);
  this.node.gain.linearRampToValueAtTime(0.3, context.currentTime + 0.101);
  this.node.gain.linearRampToValueAtTime(0, context.currentTime + 0.500);
};

Наконец, мы можем подключить выходы голоса к фильтру, чтобы имитировать эффект расстояния:

this.voices = [];
this.voiceIndex = 0;

var noise = new WhiteNoise();

var filter = context.createBiquadFilter();
filter.type = 0;
filter.Q.value = 1;
filter.frequency.value = 800;

// Инициализация нескольких голосов
for (var i = 0; i < VOICE_COUNT; i++) {
  var voice = new Envelope();

  noise.connect(voice.node);
  voice.connect(filter);

  this.voices.push(voice);
}

var gainMaster = context.createGainNode();
gainMaster.gain.value = 5;
filter.connect(gainMaster);

gainMaster.connect(context.destination);

Этот пример заимствован со страницы BBC, посвящённой звуковым эффектам выстрелов, с небольшими изменениями, включая портирование на JavaScript.

Как видите, этот подход очень мощный, но довольно быстро становится сложным и выходит за рамки этой книги. Для более подробной информации о процедурной генерации звука рекомендую ознакомиться с учебными материалами и книгой Энди Фарнелла Practical Synthetic Sound Design.

Эффекты помещения

Прежде чем звук достигнет наших ушей, он отразится от стен, зданий, мебели, ковров и других объектов. Каждое такое столкновение меняет свойства звука. Например, хлопок в ладоши на улице звучит совсем не так, как хлопок внутри большого собора, где реверберация может быть слышна несколько секунд. Игры с высоким уровнем проработки стремятся имитировать такие эффекты. Создание отдельного набора семплов для каждой акустической среды часто оказывается чрезмерно затратным, так как требует больших усилий от звукового дизайнера, множества файлов ресурсов и, как следствие, значительно увеличивает объём игровых данных.

В Web Audio API есть инструмент для симуляции различных акустических сред — ConvolverNode. С его помощью можно создавать такие эффекты, как хорус (chorus), реверберацию и звук, напоминающий телефонную речь.

Идея создания эффектов помещения заключается в том, чтобы воспроизвести опорный звук в комнате, записать его, а затем (образно говоря) вычесть из записанного оригинала. Результатом такого процесса является импульсная характеристика, которая фиксирует влияние помещения на звук. Эти импульсные характеристики тщательно записываются в специальных студийных условиях, и для того, чтобы сделать это самостоятельно, требуется серьёзная подготовка. К счастью, существуют сайты, где можно найти множество уже готовых файлов импульсных характеристик (хранящихся в виде аудиофайлов) для удобного использования.

Web Audio API предоставляет простой способ применить такие импульсные характеристики к вашим звукам с помощью ConvolverNode. Этот узел принимает буфер импульсной характеристики, представляющий собой обычный AudioBuffer, в который загружен соответствующий файл. По сути, конволвер — это очень сложный фильтр (подобно BiquadFilterNode), но вместо выбора из набора готовых типов эффектов его можно настроить на произвольную частотную характеристику:

var impulseResponseBuffer = null;

function loadImpulseResponse() {
  loadBuffer('impulse.wav', function(buffer) {
    impulseResponseBuffer = buffer;
  });
}

function play() {
  // Создаём узел-источник для семпла
  var source = context.createBufferSource();
  source.buffer = this.buffer;

  // Создаём узел Convolver для импульсной характеристики
  var convolver = context.createConvolver();

  // Устанавливаем буфер импульсной характеристики
  convolver.buffer = impulseResponseBuffer;

  // Соединяем граф
  source.connect(convolver);
  convolver.connect(context.destination);
}

Узел ConvolverNode «смешивает» входной звук с его импульсной характеристикой, выполняя свёртку — вычислительно сложную математическую операцию. В результате получается звук, который звучит так, словно он был записан в том помещении, где была сделана импульсная характеристика. На практике часто имеет смысл смешивать исходный сигнал (называемый dry mix) с обработанным сигналом (wet mix), используя кроссфейд с равной мощностью (equal‑power crossfade), чтобы контролировать, какую долю эффекта вы хотите применить.

Также можно синтезировать такие импульсные характеристики, но эта тема выходит за рамки данной книги.

Пространственный звук

Игры часто существуют в мире, где объекты имеют позиции в пространстве — будь то в 2D или в 3D. В таких случаях пространственный звук может значительно повысить эффект погружения. К счастью, в Web Audio API есть встроенные возможности позиционирования звука (пока только в стерео), и использовать их достаточно просто.

Экспериментируя с пространственным звуком, убедитесь, что вы слушаете его через стереоколонки (лучше — в наушниках). Так вы получите более точное представление о том, как левый и правый каналы изменяются в зависимости от выбранного вами метода пространственной обработки.

Модель Web Audio API включает три уровня сложности (во многом заимствованные из OpenAL):

  1. Позиция и ориентация источников и слушателей.

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

  3. Относительные скорости источников и слушателей.

В Web Audio API есть единственный слушатель (узел AudioListener), прикреплённый к контексту, который можно настраивать в пространстве, задавая его позицию и ориентацию. Каждый источник можно пропустить через панорамирующий узел AudioPannerNode, который выполняет пространственную обработку входного аудио. На основе относительного положения источников и слушателя Web Audio API рассчитывает необходимые изменения усиления.

Есть несколько вещей, которые важно знать об используемых в API допущениях. Во-первых, по умолчанию слушатель находится в начале координат (0, 0, 0). Координаты позиционирования в API не имеют единиц измерения, поэтому на практике нужно вручную подбирать множители так, чтобы звук воспринимался, как вы хотите. Во-вторых, ориентация задаётся векторами направления (длиной 1). И, наконец, в этой системе координат положительная ось Y направлена вверх, что противоположно большинству систем компьютерной графики.

Учитывая это, вот пример того, как можно изменить позицию узла‑источника, который пространственно обрабатывается в 2D с помощью панорамирующего узла PannerNode:

// Размещаем слушателя в начале координат (по умолчанию, просто для ясности)
context.listener.setPosition(0, 0, 0);

// Размещаем узел панорамирования
// Предполагаем, что X и Y заданы в координатах экрана,
// а слушатель находится в центре экрана
var panner = context.createPanner();
var centerX = WIDTH/2;
var centerY = HEIGHT/2;

var x = (X - centerX)  / WIDTH;
// Координата Y инвертируется, чтобы совпадать с системой координат canvas
var y = (Y - centerY) / HEIGHT;
// Координату Z размещаем немного позади слушателя
var z = -0.5;

// Подбираем множитель по необходимости
var scaleFactor = 2;
panner.setPosition(x * scaleFactor, y * scaleFactor, z);
// Преобразуем угол в единичный вектор
panner.setOrientation(Math.cos(angle), -Math.sin(angle), 1);

// Подключаем узел, который нужно пространственно
// обрабатывать, к панорамирующему узлу
source.connect(panner);

Помимо учёта относительных позиций и ориентаций, у каждого источника есть настраиваемый аудиоконус, как показано на Рисунке 6-3.

Рисунок 6-3. Диаграмма панорамирующих узлов и слушателя в 2D пространстве
Рисунок 6-3. Диаграмма панорамирующих узлов и слушателя в 2D пространстве

После того как вы задали внутренний и внешний конус, пространство делится на три зоны, как на Рисунке 6-3:

  1. Внутренний конус.

  2. Внешний конус.

  3. Зона вне обоих конусов.

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

panner.coneInnerAngle = 5;
panner.coneOuterAngle = 10;
panner.coneGain = 0.5;
panner.coneOuterGain = 0.2;

Рассеянный звук может иметь совершенно иной набор параметров. У всенаправленного источника внутренний конус составляет 360 градусов, и его ориентация не оказывает влияния на пространственную обработку:

panner.coneInnerAngle = 180;
panner.coneGain = 0.5;

Помимо позиции, ориентации и аудиоконусов, для источников и слушателя можно задать скорость. Этот параметр важен для имитации изменения высоты тона в результате доплеровского эффекта.

Обработка аудио с помощью JavaScript

Web Audio API стремится предоставить достаточный набор примитивов (в основном через аудиоузлы), чтобы можно было выполнять большинство стандартных задач работы со звуком. Эти модули написаны на C++ и работают значительно быстрее, чем аналогичный код на JavaScript.

Однако Web Audio API также предоставляет ScriptProcessorNode (прим. переводчика: этот узел устарел и заменён на AudioWorklet в новых версиях Web Audio API), позволяющий веб‑разработчикам напрямую синтезировать и обрабатывать звук на JavaScript. Например, с его помощью можно прототипировать собственные DSP‑эффекты или наглядно демонстрировать концепции в образовательных приложениях.

Для начала создайте ScriptProcessorNode. Этот узел обрабатывает звук кусками, размер которых задаётся параметром bufferSize. Размер буфера должен быть равен степени двойки. Лучше использовать буфер побольше, так как это даст больше запаса прочности для предотвращения сбоев, если основной поток занят другими задачами — такими, как переразметка страницы, сборка мусора или вызовы функций JavaScript:

// Создаём ScriptProcessorNode
var processor = context.createScriptProcessor(2048);

// Назначаем функцию onProcess для вызова при обработке каждого буфера
processor.onaudioprocess = onProcess;

// Подключаем существующий источник к ScriptProcessorNode
source.connect(processor);

Когда аудиоданные начнут поступать в функцию процессора, вы можете начать анализировать поток, исследуя входной буфер, или напрямую изменять выход, модифицируя выходной буфер. Например, мы можем легко поменять местами левый и правый каналы, реализовав следующий скриптовый процессор:

function onProcess(e) {
  var leftIn = e.inputBuffer.getChannelData(0);
  var rightIn = e.inputBuffer.getChannelData(1);
  var leftOut = e.outputBuffer.getChannelData(0);
  var rightOut = e.outputBuffer.getChannelData(1);

  for (var i = 0; i < leftIn.length; i++) {
    // Меняем местами левый и правый каналы
    leftOut[i] = rightIn[i];
    rightOut[i] = leftIn[i];
  }
}

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

function onProcess(e) {
  var leftOut = e.outputBuffer.getChannelData(0);
  var rightOut = e.outputBuffer.getChannelData(1);

  for (var i = 0; i < leftOut.length; i++) {
    // Добавляем немного шума
    leftOut[i] += (Math.random() - 0.5) * NOISE_FACTOR;
    rightOut[i] += (Math.random() - 0.5) * NOISE_FACTOR;
  }
}

Основная проблема использования узлов скриптовой обработки — это производительность. Реализация таких вычислительно сложных алгоритмов на JavaScript работает значительно медленнее, чем их выполнение напрямую в нативном коде браузера.