Pull to refresh

Эквалайзер на JavaScript

Website development *JavaScript *API *
Tutorial
На хабре уже было несколько статей по Web Audio API: создание визуализатора, вокодера и пианино в 30 24 строки. Поиск же по всея интернетам по запросу эквалайзер упорно выдавал туториалы по созданию спектрограмм. (Если заголовок этой статьи сбил вас с толку или вы таки купились на картинку:) и ожидали именно визуализации аудио — вам сюда или вот сюда). Но именно просто эквалайзера я так и не встретил (хотя уверен, что где-то он таки есть). Возможно, это настолько простая задача, что об этом и писать не стоит. Но, в таком случае, почему бы не сделать её ещё проще?




Что хотелось получить?

Пусть мы уже имеем какой-то плеер. В простейшем случае — это голый audio элемент.
<audio controls id="audio" src="path/to/file"></audio>

Хочется, чтобы мы умели прикрутить к нему эквалайзер
var audio = document.getElementById('audio');
equalize(audio); // как-то так, 
чтобы не пришлось думать и это всё никак не сказалось бы на работе самого плеера.
Но, начнем с начала.

API


Любая работа с Web Audio API начинается с создания контекста:
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var context = new AudioContext();

Что важно — такой объект должен быть один. Во-первых, для того, чтобы все связанные объекты могли работать вместе, они должны быть созданны в одном контексте. Во-вторых, если контекстов создать несколько (по наблюдениям — 3-4), то браузер упадёт:)

(UPD: по сосстоянию на 21.09.15 при создании большего количества контекстов возикает ошибка Uncaught NotSupportedError: Failed to construct 'AudioContext': The number of hardware contexts provided (6) is greater than or equal to the maximum bound (6). То есть хром позволяет создать до шести контекстов одновременно.)

Первое, что нам понадобится — это создать обертку для HTMLMediaElement, с которой мы и будем работать:
var source = context.createMediaElementSource(audio);


Метод createMediaElementSource также работает и с элементами <video />

Объект source — это первое звено цепи (в прямом смысле), которую мы строим. В простейшем случае цепь состоит только из двух звеньев — источник сразу подключается к выходу.
source.connect(context.destination);

Здесь context.destination — это, грубо говоря, ваши колонки.
Сам же эквалайзер строится из фильтров, создаваемых с помощью createBiquadFilter.

Код создания фильтра:
var createFilter = function (frequency) {
  var filter = context.createBiquadFilter();
     
  filter.type = 'peaking'; // тип фильтра
  filter.frequency.value = frequency; // частота
  filter.Q.value = 1; // Q-factor
  filter.gain.value = 0;

  return filter;
};

Единственный, в данном случае, параметр — это частота. Остальные параметры совпадают для всех фильтров либо меняются во время работы программы. Это:
  • type — тип фильтра. Может принимать одно из значений: lowpass, highpass, bandpass, lowshelf, highshelf, peaking, notch, allpass. Нам потребуется лишь peaking фильтр — он позволяет выборочно подчеркнуть или ослабить ограниченную полосу звукового спектра. Почитать подробнее.
  • Qдобротность — изменяет ширину полосы частот, на которые фильтр влияет.
  • gain — сила, с которой фильтр влияет на полосу частот.

Необходимо создать фильтры для всего набора частот. Для 10ти-полосного эквалайзера это могут быть 60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000 и 16000 Hz (значения срисованы с winamp'а).
var createFilters = function () {
  var frequencies = [60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000],
    filters;
      
  // создаем фильтры
  filters = frequencies.map(createFilter);
      
  // цепляем их последовательно.
  // Каждый фильтр, кроме первого, соединяется с предыдущим.
  // Удачно, что reduce без начального значения как раз пропускает первый элемент.
  filters.reduce(function (prev, curr) {
    prev.connect(curr);
    return curr;
  });

  return filters;
};

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

Остается только связать это всё воедино:
window.AudioContext = window.AudioContext || window.webkitAudioContext;

var context = new AudioContext(),
  audio = document.getElementById('audio');

var createFilter = function (frequency) {
  var filter = context.createBiquadFilter();
     
  filter.type = 'peaking'; // тип фильтра
  filter.frequency.value = frequency; // частота
  filter.Q.value = 1; // Q-factor
  filter.gain.value = 0;

  return filter;
};

var createFilters = function () {
  var frequencies = [60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000],
    filters = frequencies.map(createFilter);

  filters.reduce(function (prev, curr) {
    prev.connect(curr);
    return curr;
  });

  return filters;
};

var equalize = function (audio) {
  var source = context.createMediaElementSource(audio),
    filters = createFilters();

  // источник цепляем к первому фильтру 
  source.connect(filters[0]);
  // а последний фильтр - к выходу
  filters[filters.length - 1].connect(context.destination);
};

equalize(audio);

Вот так. Эквалайзер в 30 строк. Дальше дело за малым — привязать контролы, но это задача элементарная.
Что-то вроде этого
// схематично
var bindEvents = function (inputs) {
  inputs.forEach(function (item, i) {
    item.addEventListener('change', function (e) {
      filters[i].gain.value = e.target.value;
    }, false);
  });
};


Вот, собственно, демка, где стримится ogg файл и пропускается через наш эквалайзер, но насладиться ей смогут только пользователи Google Chrome, пользователям же других браузеров придется потрудиться открыть локальный файл, да ещё и не абы какой. Потому что…

Момент разочарования


Собрав первую версию плеера, я решил прикрутить к нему soundcloud. Здорово же — прогонять песенки с облака через эквалайзер. В конце концов всё завелось… но только в хроме — мозила упорно отказывался воспроизводить поток. Но локальные файлы при этом запускал на ура. И тут выяснилось страшное:
To prevent this [information leakage], a MediaElementAudioSourceNode must output silence instead of the normal output of the HTMLMediaElement if it has been created using an HTMLMediaElement for which the execution of the fetch algorithm labeled the resource as CORS-cross-origin. (документация)

То есть CORS и Web Audio API несовместимы. А самое интересное, что в хроме эта связка всё-таки работает. Думаю, это всё-таки баг и его, должно быть, скоро закроют (хотя он присутствует уже давно), так что использовать эту особенность не стоит. (upd: по состоянию на 12.07.15 баг закрыт, эквалайзер для CORS-ресуров в хроме не работает)

upd: как справедливо заметили в комментариях, CORS можно настроить с помощью атрибута crossorigin, но для этого в сам поток должен быть добавлен заголовок Access-Control-Allow-Origin.

А для загружаемых файлов, например, можно использовать ObjectURL:
// схематично
fileInput.addEventListener('change', function (e) {
  var url = URL.createObjectURL(e.target.files[0]);
  audio.src = url;
}, false);


Итого


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

Материалы:

Ссылки:


PS Приятно узнать, что статья попала в топ хабра за 2014 год. 2ое место в категории API
Tags:
Hubs:
Total votes 44: ↑39 and ↓5 +34
Views 52K
Comments Comments 21