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

  • 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
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 21

    +4
    Для наглядности можно ещё прикрутить спектрограмму
      +1
      Могу ещё дать пример пространственных аудио эффектов и нормализации звука: github.com/nazar-pc/CleverStyle-Music/blob/master/js/ac.sound_processing.coffee
      Это аудио плеер для Firefox OS, также он работает на пока что ночных сборках Firefox for Android, и десктопах.
        –9
        В Winamp любил смотреть на скачущие столбцы визуализатора, посему на WebAudio недавно для своего VK-плеера www.inquty.ru добавил визуализацию. Теперь можно добавить эквалайзер и пресеты.
          +5
          Какой отвратный плеер…
            –5
            На вкус и цвет все фломастеры разные. Не нравится эта тема? Форкайте проект github.com/antirek/inquty-player и меняйте. По умолчанию установлена тема darkly от bootswatch установлена, можно сменить на любую другую.
              +1
              Действительно плохой
              добавляет из неизвестного мне места новые треки после каждого запуска песни.image
          +1
          Чего-то я не очень понял следующую фразу:
          Очень важно подключать фильтры именно последовательно. Когда я писал первую версию, у меня фильтры подключались последовательно, и на выходе не было ничего, кроме страшного грохота.

          Важно подключать последовательно, но при последовательном подключении ничего, кроме страшного грохота. Это как?!
            +1
            Спасибо, исправил.
            Разумеется параллельно во втором случае.
              0
              Правильно ли я понимаю, что «эквалайзер» из BandPass'ов нужно наоборот сооружать параллельно.
            +5
              +9
              Потому что на картинке до ката визуализатор спектра, а под катом туториал по реализации эквалайзера… внезапно так.
                +1
                Вопрос был не к конкретному посту, а к сообществу в целом (видимо, зря я не уточнил формулировку)
                Перефразирую:
                — Почему в рунете визуализатор спектра практически всегда называют эквалайзером?
                  0
                  потому что англицизм — equalizer
                  0
                  Попробую предположить, что очень часто на музыкальных центрах и магнитофонах импортного производства есть графический эквалайзер, о чем свидетельствуют небольшой ряд регуляторов и большая блестящая наклейка «Graphic EQ». Ты двигаешь регуляторы эквалайзера, а в маленьком светодиодном окошке меняется отображение амплитудно-частотной характеристики, вау! Поэтому видишь визуализатор, думаешь эквалайзер.: )
              0
              Демка в FF и Safari под маком не работает
                0
                Можно ли написать приложение для Вконтакте эквалайзер? Если да, напишите, плз, буду очень благодарен
                  0
                  То есть CORS и Web Audio API несовместимы.

                  Вроде как вполне совместимы. Можно прокидывать для получения контекста , где выставлен атрибут `crossOrigin`
                    0
                    Извините, не очень понял ваш комментарий — что и куда можно прокидывать?)
                      0
                      в audio элемент можно добавить атрибут crossOrigin (как и с img тегами).
                        0
                        не заметил, что хабр съел часть комментария. Было так:
                        Можно прокидывать [audio] для получения контекста

                    Only users with full accounts can post comments. Log in, please.