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

в 4:43, , рубрики: api, javascript, Веб-разработка

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

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

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

Пусть мы уже имеем какой-то плеер. В простейшем случае — это голый 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), то браузер упадёт:)

Первое, что нам понадобится — это создать обертку для 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(function (frequency) {
    return createFilter(frequency);
  });
      
  // цепляем их последовательно.
  // Каждый фильтр, кроме первого, соединяется с предыдущим.
  // Удачно, что 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;
      
  filters = frequencies.map(function (frequency) {
    return createFilter(frequency);
  });

  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 несовместимы. А самое интересное, что в хроме эта связка всё-таки работает. Думаю, это всё-таки баг и его, должно быть, скоро закроют (хотя он присутствует уже давно), так что использовать эту особенность не стоит.
Для загружаемых файлов, например, можно использовать ObjectURL:

// схематично
fileInput.addEventListener('change', function (e) {
  var url = URL.createObjectURL(e.target.files[0]);
  audio.src = url;
}, false);

Итого

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

Материалы:

Ссылки:

Автор: linoleum

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js