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

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

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

AudioContext предназначен для управления и воспроизведения всех звуков. Чтобы создать звук с помощью 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 веб-аудио использует 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 .

После получения (некодированных) данных аудиофайла их можно сохранить для последующего декодирования или можно декодировать сразу с помощью метода AudioContext decodeAudioData() . Этот метод принимает 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 веб-аудио позволяет разработчикам точно планировать воспроизведение. Чтобы продемонстрировать это, давайте настроим простой ритм-трек. Вероятно, наиболее широко известным паттерном ударной установки является следующий:

Простой рисунок рок-барабана
Простой рисунок рок-барабана

в котором хэт исполняется каждую восьмую ноту, а кик и малый барабан исполняются попеременно каждую четверть, в размере 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);
}

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

Одна из самых основных операций, которые вы, возможно, захотите выполнить со звуком, — это изменение его громкости. Используя 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;

Переход между двумя звуками

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

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

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

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

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 для такого планирования, это неточно . С помощью 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 веб-аудио предоставляет удобный набор методов RampToValue для постепенного изменения значения параметра, например linearRampToValueAtTime и exponentialRampToValueAtTime .

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

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

Аудиографик с BiquadFilterNode
Аудиографик с BiquadFilterNode

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

Один из способов сделать это — разместить 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, включая загрузку и воспроизведение аудиосэмплов. Мы построили аудиографики с узлами усиления и фильтрами, а также запланировали настройки звуков и параметров звука, чтобы включить некоторые распространенные звуковые эффекты. На этом этапе вы готовы приступить к созданию замечательных веб-аудиоприложений!

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

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