Начало работы с API веб-аудио

Борис Смус
Boris Smus

До появления элемента <audio> в HTML5 для нарушения «тишины» в интернете требовались Flash или другие плагины. Хотя сейчас для воспроизведения аудио в интернете плагины не нужны, тег <audio> накладывает существенные ограничения на реализацию сложных игр и интерактивных приложений.

Web Audio API — это высокоуровневый JavaScript API для обработки и синтеза звука в веб-приложениях. Цель этого API — включить возможности, присущие современным игровым аудиодвижкам, а также некоторые задачи микширования, обработки и фильтрации, которые встречаются в современных настольных приложениях для создания звука. Ниже представлено краткое введение в использование этого мощного API.

Начало работы с AudioContext

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

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

Следующий фрагмент кода создает объект AudioContext :

var context;
window.addEventListener('load', init, false);
function init() {
    try {
    context = new AudioContext();
    }
    catch(e) {
    alert('Web Audio API is not supported in this browser');
    }
}

Для более старых браузеров на основе WebKit используйте префикс webkit , как в случае с webkitAudioContext .

Многие интересные функции Web Audio API, такие как создание AudioNodes и декодирование данных аудиофайлов, являются методами AudioContext .

Звуки загрузки

API Web Audio использует AudioBuffer для коротких и средних по длине звуков. Основной подход заключается в использовании XMLHttpRequest для получения звуковых файлов.

API поддерживает загрузку аудиофайлов в различных форматах, таких как WAV, MP3, AAC, OGG и другие . Поддержка различных аудиоформатов браузерами может различаться .

Следующий фрагмент кода демонстрирует загрузку звукового сэмпла:

var dogBarkingBuffer = null;
var context = new AudioContext();

function loadDogSound(url) {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.responseType = 'arraybuffer';

    // Decode asynchronously
    request.onload = function() {
    context.decodeAudioData(request.response, function(buffer) {
        dogBarkingBuffer = buffer;
    }, onError);
    }
    request.send();
}

Данные аудиофайла имеют двоичный формат (не текстовый), поэтому мы устанавливаем responseType запроса в значение 'arraybuffer' . Для получения дополнительной информации об ArrayBuffers см. эту статью об XHR2 .

После получения (недекодированных) данных аудиофайла их можно сохранить для последующего декодирования или декодировать сразу же с помощью метода decodeAudioData() объекта AudioContext. Этот метод принимает ArrayBuffer с данными аудиофайла, хранящимися в request.response , и декодирует их асинхронно (не блокируя основной поток выполнения JavaScript).

После завершения decodeAudioData() вызывается функция обратного вызова, которая предоставляет декодированные аудиоданные PCM в виде объекта AudioBuffer .

Воспроизводятся звуки

После загрузки одного или нескольких AudioBuffers мы готовы воспроизводить звуки. Предположим, мы только что загрузили AudioBuffer со звуком собачьего лая, и загрузка завершилась. Тогда мы можем воспроизвести этот буфер с помощью следующего кода.

var context = new AudioContext();

function playSound(buffer) {
    var source = context.createBufferSource(); // creates a sound source
    source.buffer = buffer;                    // tell the source which sound to play
    source.connect(context.destination);       // connect the source to the context's destination (the speakers)
    source.noteOn(0);                          // play the source now
}

Функцию playSound() можно вызывать каждый раз, когда кто-то нажимает клавишу или щелкает мышью.

Функция noteOn(time) позволяет легко планировать точное воспроизведение звука для игр и других приложений, критичных ко времени. Однако для корректной работы этого планирования убедитесь, что ваши звуковые буферы предварительно загружены.

Абстрагирование API веб-аудио

Конечно, было бы лучше создать более универсальную систему загрузки, которая не была бы жестко привязана к загрузке именно этого звука. Существует множество подходов к обработке многочисленных коротких и средних по длине звуков, которые используются в аудиоприложениях или играх — вот один из способов с использованием BufferLoader (не входящий в веб-стандарт).

Ниже приведён пример использования класса BufferLoader . Давайте создадим два AudioBuffers ; и, как только они будут загружены, давайте воспроизведём их одновременно.

window.onload = init;
var context;
var bufferLoader;

function init() {
    context = new AudioContext();

    bufferLoader = new BufferLoader(
    context,
    [
        '../sounds/hyper-reality/br-jam-loop.wav',
        '../sounds/hyper-reality/laughter.wav',
    ],
    finishedLoading
    );

    bufferLoader.load();
}

function finishedLoading(bufferList) {
    // Create two sources and play them both together.
    var source1 = context.createBufferSource();
    var source2 = context.createBufferSource();
    source1.buffer = bufferList[0];
    source2.buffer = bufferList[1];

    source1.connect(context.destination);
    source2.connect(context.destination);
    source1.noteOn(0);
    source2.noteOn(0);
}

Управление временем: воспроизведение звуков в ритме

API Web Audio позволяет разработчикам точно планировать воспроизведение. Чтобы это продемонстрировать, давайте создадим простую ритм-дорожку. Вероятно, наиболее известный паттерн ударных инструментов выглядит следующим образом:

Простой ритм для рок-барабанов

в которой хай-хэт играется через каждые восемь нот, а бас-барабан и малый барабан играют попеременно через каждые четверть, в размере 4/4.

Предположим, мы загрузили буферы kick , snare и hihat . Код для этого очень прост:

for (var bar = 0; bar < 2; bar++) {
    var time = startTime + bar * 8 * eighthNoteTime;
    // Play the bass (kick) drum on beats 1, 5
    playSound(kick, time);
    playSound(kick, time + 4 * eighthNoteTime);

    // Play the snare drum on beats 3, 7
    playSound(snare, time + 2 * eighthNoteTime);
    playSound(snare, time + 6 * eighthNoteTime);

    // Play the hi-hat every eighth note.
    for (var i = 0; i < 8; ++i) {
    playSound(hihat, time + i * eighthNoteTime);
    }
}

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

function playSound(buffer, time) {
    var source = context.createBufferSource();
    source.buffer = buffer;
    source.connect(context.destination);
    source.noteOn(time);
}

Изменение громкости звука

Одна из самых простых операций, которые вы можете захотеть выполнить со звуком, — это изменить его громкость. Используя Web Audio API, мы можем направить наш источник звука к месту назначения через AudioGainNode , чтобы регулировать громкость:

Простая блок-схема, изображающая поток аудиосигнала от источника к узлу усиления и далее к месту назначения.

Настроить это соединение можно следующим образом:

// Create a gain node.
var gainNode = context.createGainNode();
// Connect the source to the gain node.
source.connect(gainNode);
// Connect the gain node to the destination.
gainNode.connect(context.destination);

После настройки графика вы можете программно изменять громкость, манипулируя параметром gainNode.gain.value следующим образом:

// Reduce the volume.
gainNode.gain.value = 0.5;

Плавное затухание между двумя звуками

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

Это можно сделать с помощью следующего аудиографа:

Аудиограф с двумя источниками, подключенными к отдельным узлам усиления, которые затем направляются в одно и то же место назначения.

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

function createSource(buffer) {
    var source = context.createBufferSource();
    // Create a gain node.
    var gainNode = context.createGainNode();
    source.buffer = buffer;
    // Turn on looping.
    source.loop = true;
    // Connect source to gain.
    source.connect(gainNode);
    // Connect gain to destination.
    gainNode.connect(context.destination);

    return {
    source: source,
    gainNode: gainNode
    };
}

Перекрёстное затухание с равной мощностью

Простой линейный метод кроссфейда демонстрирует снижение громкости при переходе между сэмплами.

Линейное перекрестное затухание, изображенное в виде двух взаимодополняющих прямых линий, отображаемых в виде уровней амплитуды во времени.
Линейный перекрестный переход

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

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

плавный переход между плейлистами

Еще одно распространенное применение кроссфейдера — в приложениях для воспроизведения музыки. При смене песни нам нужно плавно затухать текущий трек и плавно набирать новый, чтобы избежать резкого перехода. Для этого нужно запланировать кроссфейд на будущее. Хотя для этого можно использовать setTimeout , это не обеспечивает достаточной точности . С помощью Web Audio API мы можем использовать интерфейс AudioParam для планирования будущих значений параметров, таких как значение усиления AudioGainNode .

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

function playHelper(bufferNow, bufferLater) {
    var playNow = createSource(bufferNow);
    var source = playNow.source;
    var gainNode = playNow.gainNode;
    var duration = bufferNow.duration;
    var currTime = context.currentTime;
    // Fade the playNow track in.
    gainNode.gain.linearRampToValueAtTime(0, currTime);
    gainNode.gain.linearRampToValueAtTime(1, currTime + ctx.FADE_TIME);
    // Play the playNow track.
    source.noteOn(0);
    // At the end of the track, fade it out.
    gainNode.gain.linearRampToValueAtTime(1, currTime + duration-ctx.FADE_TIME);
    gainNode.gain.linearRampToValueAtTime(0, currTime + duration);
    // Schedule a recursive track change with the tracks swapped.
    var recurse = arguments.callee;
    ctx.timer = setTimeout(function() {
    recurse(bufferLater, bufferNow);
    }, (duration - ctx.FADE_TIME) - 1000);
}

API Web Audio предоставляет удобный набор методов RampToValue для постепенного изменения значения параметра, таких как linearRampToValueAtTime и exponentialRampToValueAtTime .

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

Применение простого эффекта фильтрации к звуку.

Простая блок-схема, изображающая поток аудиосигнала от источника к узлу биквадратного фильтра и далее к месту назначения.

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

Один из способов сделать это — разместить узлы BiquadFilterNode между источником и приемником звука. Этот тип аудиоузла может создавать различные фильтры низкого порядка, которые можно использовать для построения графических эквалайзеров и даже более сложных эффектов, в основном связанных с выбором того, какие части частотного спектра звука следует подчеркнуть, а какие — приглушить.

Поддерживаются следующие типы фильтров:

  • Фильтр нижних частот
  • Фильтр верхних частот
  • Полосовой фильтр
  • Фильтр с низкой полкой
  • Фильтр с высокой полкой
  • Пиковый фильтр
  • Режекторный фильтр
  • Фильтр всех проходов

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

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

// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
source.connect(filter);
filter.connect(context.destination);
// Create and specify parameters for the low-pass filter.
filter.type = 0; // Low-pass filter. See BiquadFilterNode docs
filter.frequency.value = 440; // Set cutoff to 440 HZ
// Playback the sound.
source.noteOn(0);

В целом, настройки частоты необходимо производить в логарифмическом масштабе, поскольку человеческий слух работает по тому же принципу (то есть, A4 — это 440 Гц, а A5 — 880 Гц). Более подробную информацию см. в функции FilterSample.changeFrequency по ссылке на исходный код выше.

Наконец, обратите внимание, что приведенный пример кода позволяет подключать и отключать фильтр, динамически изменяя граф AudioContext. Мы можем отключить AudioNodes от графа, вызвав метод node.disconnect(outputNumber) . Например, чтобы перенаправить граф с прохождения через фильтр на прямое соединение, мы можем сделать следующее:

// Disconnect the source and filter.
source.disconnect(0);
filter.disconnect(0);
// Connect the source directly.
source.connect(context.destination);

Дальнейшее прослушивание

Мы рассмотрели основы API, включая загрузку и воспроизведение аудиосэмплов. Мы создали аудиографы с узлами усиления и фильтрами, а также запланировали воспроизведение звуков и настроили параметры звука для включения некоторых распространенных звуковых эффектов. На этом этапе вы готовы создавать отличные веб-приложения для работы со звуком!

Если вы ищете вдохновение, многие разработчики уже создали замечательные работы, используя Web Audio API. Вот некоторые из моих любимых:

  • AudioJedit — это встроенный в браузер инструмент для склейки звуков, использующий постоянные ссылки SoundCloud.
  • ToneCraft — это звуковой секвенсор, в котором звуки создаются путем наложения 3D-блоков.
  • Plink — это игра для совместного создания музыки с использованием Web Audio и Web Sockets.