Краткое руководство по работе с Web Audio API

Краткое руководство по работе с Web Audio API
mangohost

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

Начинаем работу

Давайте определимся с терминологией. Все аудио операции в Web Audio API обрабатывается внутри аудио контекста (audio context). Каждая базовая операция выполняется с аудио узлами (nodes), которые связаны между собой и образовывают граф маршрутизации аудио (audio routing graph). Перед воспроизведением любого звука, вам необходимо создать этот аудио контекст. Это очень похоже на то как вы создаёте контекст для рисования внутри элемента <canvas>. Вот как мы создаём аудио контекст:

var context = new (window.AudioContext || window.webkitAudioContext)();

Safari требует WebKit префикс для поддержки AudioContext, поэтому вы должны использовать строчку кода, которая показана выше вместо простого new AudioContext();.

Создание аудио контекста -> создание источника -> создание фильтра узлов -> подключение к цели

Существует три типа источников:

  1. Осциллятор - математически вычисляемые звуки;
  2. Аудио сэмплы - из аудио/видео файлов;
  3. Аудио поток - аудио из веб-камер или микрофонов;

Давайте начнём с осциллятора

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

Формы волны осциллятора

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

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

OscillatorNode.type = 'sine'|'square'|'triangle'|'sawtooth';

Вы также можете создавать собственные формы. Используйте метод setPeriodicWave() чтобы создать формы сигнала, который автоматически установит тип в custom. Давайте посмотрим как различные формы создают различные звуки:

See the Pen Web Audio API: waveforms by Greg Hovanesyan (@gregh) on CodePen.

Кастомные формы сигнала создаются с помощью Преобразований Фурье (Fourier Transforms). Если вы хотите узнать больше о кастомных формах (например как сделать полицейскую сирену) вы можете прочитать эту статью.

Запуск осциллятора

Давайте попробуем сделать какой-нибудь шум. Вот что нам необходимо для этого:

  1. Мы должны создать Web Audio API контекст;
  2. Создать узел осциллятора внутри этого контекста;
  3. Выбрать тип сигнала;
  4. Установить частоту;
  5. Подключить осциллятор;
  6. Запустить осциллятор;

Давайте превратим эти шаги в код:

var context = new (window.AudioContext || window.webkitAudioContext)();

var oscillator = context.createOscillator();

oscillator.type = 'sine';
oscillator.frequency.value = 440;
oscillator.connect(context.destination);
oscillator.start();

Создаём контекст. После этого создаём осциллятор и устанавливаем тип сигнала. Значение по умолчанию для типа сигнала установлено 'sine', поэтому мы могли бы пропустить эту строчку. Частоту устанавливаем в 440, это нота A4 (она тоже установлена по умолчанию). Частоты музыкальных нот от C0 до B8 находятся в диапазоне 16.35 до 7902.13Гц. Позже в этой статье мы рассмотрим пример, в котором будем проигрывать много различных нот.

Теперь когда мы всё это знаем, давайте сделаем возможность регулировать громкость. Для этого нам нужно создать узел усилителя внутри контекста, присоединить его к цепи, и присоединить усилитель к назначению.

var gain = context.createGain();
oscillator.connect(gain);
gain.connect(context.destination);

var now = context.currentTime;
gain.gain.setValueAtTime(1, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.5);
oscillator.start(now);
oscillator.stop(now + 0.5);

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

Расчёт времени Web Audio API

Одна из наиболее важных вещей в разработке программного обеспечения связанного с аудио, это управление временем. Для точности необходимой здесь, использование JavaScript часов не лучшая практика, просто потому что они недостаточно точные. Однако Web Audio API приходит со свойством currentTime, которое вдвое увеличивает аппаратный timestamp и может быть использовано для воспроизведения аудио. Оно начинается с 0, когда аудио контекст объявлен. Попробуйте выполнить console.log(context.currentTime) чтобы увидеть timestamp.

Для примера, если вы хотите чтобы осциллятор заиграл немедленно, вам нужно запустить oscillator.start(0). Однако вы можете захотеть чтобы он начал играть с одной секунды от текущего времени, проиграл 2 секунды и остановился. Вот как это сделать:

var now = context.currentTime;
oscillator.play(now + 1);
oscillator.stop(now + 3);

Есть два метода, которые здесь  необходимо затронуть.

Метод AudioParam.setValueAtTime(value, startTime) планирует изменение значения в точное время. Например, вы хотите изменить значение частоты осциллятора на первой секунде:

oscillator.frequency.setValueAtTime(261.6, context.currentTime + 1);

Однако, вы также можете использовать его когда захотите мгновенно изменить значение .setValueAtTime(value, context.currentTime). Вы можете установить значение изменяя значение свойства AudioParam, но любые изменения значения игнорируются без выброса исключения, если они происходят в тот же момент когда происходят события автоматизации (события запланированные с помощью методов AudioParam).

Метод AudioParam.exponentialRampToValueAtTime(value, endTime)  планирует постепенные изменения значения. Этот код будет экспоненциально уменьшать громкость осциллятора на первой секунде, что собственно является хорошим способом плавного выключения музыки:

gain.gain.exponentialRampToValueAtTime(0.001, context.currentTime + 1);

Мы не можем использовать значение 0 потому что оно должно быть положительным, поэтому мы используем очень маленькое значение.

Создание класса Sound

После того как вы остановили осциллятор, вы не можете запустить его снова. Вы не сделали ничего плохого, просто это особенность Web Auidio API, которая оптимизирует производительность. Мы можем создать класс Sound, который будет отвечать за создание узлов осциллятора, а также проигрывать и останавливать звуки. Таким образом мы сможем проигрывать звук несколько раз. Для этого  я собираюсь использовать синтаксис ES6:

class Sound {

  constructor(context) {
    this.context = context;
  }

  init() {
    this.oscillator = this.context.createOscillator();
    this.gainNode = this.context.createGain();

    this.oscillator.connect(this.gainNode);
    this.gainNode.connect(this.context.destination);
    this.oscillator.type = 'sine';
  }

  play(value, time) {
    this.init();

    this.oscillator.frequency.value = value;
    this.gainNode.gain.setValueAtTime(1, this.context.currentTime);
            
    this.oscillator.start(time);
    this.stop(time);

  }

  stop(time) {
    this.gainNode.gain.exponentialRampToValueAtTime(0.001, time + 1);
    this.oscillator.stop(time + 1);
  }

}

Мы передаём контекст в конструкторе, таким образом мы можем создавать экземпляры класса Sound в этом же контексте. У нас есть метод init, который создаёт осциллятор и все необходимые фильтрующие узлы, соединяет их, и т.д. Метод Play принимает значение (частоту ноты в Герцах, которую будет играть) и время когда оно должно быть воспроизведено. Но сначала, он создаёт осциллятор, и это происходит каждый раз когда вызывается метод play. Метод stop экспоненциально уменьшает громкость с каждой секундой, пока осциллятор полностью не остановится. Поэтому когда нам нужно воспроизвести звук снова, мы создаём новый экземпляр класса Sound и вызываем метод play. Теперь мы можем воспроизвести некоторые ноты:

let context = new (window.AudioContext || window.webkitAudioContext)();
let note = new Sound(context);
let now = context.currentTime;
note.play(261.63, now);
note.play(293.66, now + 0.5);
note.play(329.63, now + 1);
note.play(349.23, now + 1.5);
note.play(392.00, now + 2);
note.play(440.00, now + 2.5);
note.play(493.88, now + 3);
note.play(523.25, now + 3.5);

Код выше будет играть ноты C D E F G A B C, все в одном контексте. Если вы хотите больше узнать о частоте нот в Герцах, вы можете найти информацию здесь.

Знание всего этого делает нас способными разработать что-то вроде ксилофона! Он создаёт новый экземпляр класса Sound и проигрывает его при событии mouseenter. Вы можете посмотреть пример и попытаться сделать его в качестве упражнения.

See the Pen Play the Xylophone (Web Audio API) by Greg Hovanesyan (@gregh) on CodePen.

Я создал площадку содержащую весь необходимый HTML, CSS и класс Sound, который мы создали. Используйте атрибут data-frequency для получения значения нот. Попробуйте здесь.

Работа с записанным звуком

Теперь когда вы создали что-то с осциллятором, давайте посмотрим как работать с уже записанным звуком. Некоторые звуки очень сложные для воспроизведения их с помощью осциллятора. Для того чтобы использовать реалистичные звуки, в большинстве случаев, вы должны работать с записанными звуками. Это могут быть ".mp3", ".ogg", ".wav", и т.д. Взгляните на полный список, чтобы получить больше информации. Мне нравится использовать ".mp3" так как он легковесный, широко поддерживаемый и с довольно неплохим качеством звука.

Вы не можете просто получить звук через URL, так как вы делаете это с изображениями. Мы должны запустить XMLHttpRequest чтобы получить файлы, декодировать данные и положить их в буфер.

class Buffer {

  constructor(context, urls) {  
    this.context = context;
    this.urls = urls;
    this.buffer = [];
  }

  loadSound(url, index) {
    let request = new XMLHttpRequest();
    request.open('get', url, true);
    request.responseType = 'arraybuffer';
    let thisBuffer = this;
    request.onload = function() {
      thisBuffer.context.decodeAudioData(request.response, function(buffer) {
        thisBuffer.buffer[index] = buffer;
        updateProgress(thisBuffer.urls.length);
        if(index == thisBuffer.urls.length-1) {
          thisBuffer.loaded();
        }       
      });
    };
    request.send();
  };

  loadAll() {
    this.urls.forEach((url, index) => {
      this.loadSound(url, index);
    })
  }

  loaded() {
    // what happens when all the files are loaded
  }

  getSoundByIndex(index) {
    return this.buffer[index];
  }

}

Давайте взглянем на конструктор. Мы получаем наш контекст также как это делали в классе Sound, получаем список URL, который будет загружен и пустой массив для буфера.

У нас есть два метода: loadSound и loadAll. loadAll обходит список URL адресов и вызывает метод loadSound. Важно передавать индекс, поэтому мы складываем буферизированный звук в нужный элемент массива, независимо от того какой запрос загружается первым. Также это позволяет нам видеть который запрос последний, а это означает что буфер полностью загружен.

Теперь мы можем вызывать метод loaded(), который может делать что-то на подобии скрытия индикатора загрузки. И в заключении метод getSoundByIndex(index) загружает звук из буфера по индексу для воспроизведения.

У метода decodeAudioData есть более новый синтаксис основанный на промисах, но он ещё не работает в Safari.

context.decodeAudioData(audioData).then(function(decodedData) {
  // use the decoded data here
});

Теперь нам нужно создать класс для звука. У нас уже есть завершённый класс для работы с записанным звуком:

class Sound() {

  constructor(context, buffer) {
    this.context = context;
    this.buffer = buffer;
  }

  init() {
    this.gainNode = this.context.createGain();
    this.source = this.context.createBufferSource();
    this.source.buffer = this.buffer;
    this.source.connect(this.gainNode);
    this.gainNode.connect(this.context.destination);
  }

  play() {
    this.setup();
    this.source.start(this.context.currentTime);
  }  

  stop() {
    this.gainNode.gain.exponentialRampToValueAtTime(0.001, this.context.currentTime + 0.5);
    this.source.stop(this.context.currentTime + 0.5);
  }

}

Конструктор принимает контекст и буфер. Создаём мы с помощью метода createBufferSource(), вместо createOscillator как мы делали это раньше. Буфер это нота (элемент из массива буфера), который мы получаем с помощью метода getSoundByIndex(). Теперь вместо осциллятора мы создаём источник буфера, устанавливаем буфер и соединяем его с целью (или усилителем или другими фильтрами).

let buffer = new Buffer(context, sounds);
buffer.loadAll();

sound = new Sound(context, buffer.getSoundByIndex(id));
sound.play();

Теперь мы должны создать экземпляр буфера и вызвать метод loadAll чтобы загрузить все звуки в буфер. У нас также есть метод getSoundById чтобы захватить звук, который нам нужен, поэтому мы передаём звук в Sound и вызываем play(). id может быть сохранён как атрибут data на кнопке, которую вы нажимаете для воспроизведения звука.

Вот проект, который использует всё это: буфер, записанные ноты и т.д.

See the Pen The Bluesman - You Can Play The Blues (Web Audio API) by Greg Hovanesyan (@gregh) on CodePen.

Вы можете использовать этот пример для справки, но чтобы выполнить упражнение воспользуйтесь этой площадкой. В ней есть необходимый HTML, CSS и URL адреса нот, которые я записал на настоящей электрической гитаре. Попробуйте написать свой код!чс

Введение в фильтры

Web Auidio API позволяет вам добавлять различные фильтрующие узлы между вашим источником звука и назначением. BiquadFilterNode - это простой фильтр низкого порядка, который предоставляет вам возможность контролировать какие части должны быть выражены, а какие должны быть ослаблены. Это позволяет вам разрабатывать приложения-эквалайзеры и другие штуки. Есть 8 типов биквадратных фильтров: highpass, lowpass, bandpass, lowshelf, highshelf, peaking, notch, и allpass.

Highpass - это фильтр, который пропускает высокие частоты, но ослабляет низкочастотные составляющие сигналов. Lowpass пропускает низкие частоты, но ослабляет высокие. Они также называются "low cut" и "hight cut" фильтрами, потому что это объясняет, что происходит с сигналами.

Highshelf и Lowshelf - это фильтры, которые используются для управления низкими и высокими частотами звуков. Они используются для подчёркивания или понижения сигналов, выше или ниже заданной частоты.

Вы можете найти свойство Q интерфейса BiquadFilterNode, который представляет собой двойную составляющую Q-фактора. Фактор качества или Q-фактор контролирует пропускную способность, количество затронутых частот. Чем ниже Q-фактор, тем больше пропускная способность, и это означает, что больше частот будут затронуты. Чем выше Q-фактор, тем меньше пропускная способность.

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

Давайте разработаем настраиваемый эквалайзер.

See the Pen Web Audio API: parametric equalizer by Greg Hovanesyan (@gregh) on CodePen.

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

See the Pen Web Audio API: distortion example by Greg Hovanesyan (@gregh) on CodePen.

Послесловие

Теперь, когда мы узнали как  нужно работать с Web Audio API,  я рекомендую поиграться  с ним самостоятельно и сделать собственные проекты!

Вот несколько библиотек для работы с аудио в вебе:

  • Pizzicato.js - Pizzicato призван упростить создания и обработку звуков с Web Audio API;
  • webaudiox.js - это куча хелперов, которые упростят работу с WebAudio API;
  • howler.js - Javascript аудио библиотека для современного веба;
  • WAD - Использование HTML5 Web Audio API для динамического синтеза звука. Это как jQuery для ваших ушей;
  • Tone.js - Web Audio фреймворк для создания интерактивной музыки в браузере;

Комментарии

9 марта 2017 01:32
Дима Гашко

like

11 июля 2017 21:49
Евгений

Спасибо за понятное объяснение! Очень помогло! :)

11 июля 2017 22:40
Администратор

Всегда пожалуйста!)

mangohost