Pull to refresh

Пробуем Audio API на примере написания визуализатора

Reading time 6 min
Views 50K

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

Но прежде чем начать разбираться непосредственно с Audio API, нам необходимо набросать заготовку нашего визуализатора и делать мы будем это при помощи canvas.


Создание заготовки


Если вам не интересна эта часть то можете просто пропустить её.
Создание заготовки
Итак создаем холст:
var canva = document.createElement('canvas');
var ctx = canva.getContext('2d');
//Делаем наш холст на всю страницу
canva.width = window.innerWidth;
canva.height = window.innerHeight;
//Добавляем его на страницу
document.body.appendChild(canvas);

Отлично, холст у нас есть и далее нам надо создать элементы, которые и будут отвечать за визуализацию звукового сигнала. В нашем случае это будут обычные круги:

var particles = [];//Тут будут храниться все созданные частицы
var createParticles = function () {
    var particle = null;
    for (var i = 0; i < 50; i++) {
        particle = new Particle();
        particles.push(particle);
    }
    //Тут запускаем непосредственно ф-ю отрисовки 
    setInterval(draw,33);
}

Функция draw, которая будет рисовать наши круги, выглядит следующим образом:

var draw = function () {
    //Очищаем холст перед новой отрисовкой 
    ctx.clearRect(0, 0, canva.width, canva.height);
    for (var i = 0; i < 50; i++) {
        var loc = particles[i];
        loc.draw();
    }
}


Собственно, наша заготовка практически готова, остался только конструктор частицы Particle.

var Particle = function () {
       this.init();
};

Particle.prototype = {
    init: function () {
         this.x = random(canva.width);
         this.y = random(canva.height);
         this.level = 1 * random(4);
         this.speed = random(0.2, 1);
         this.radius = random(10, 70); //радиус частиц
         this.color = random(['#69D2E7', '#A7DBD8', '#E0E4CC', '#F38630', '#FA6900', '#FF4E50', '#F9D423']); //цвет частицы
         this.opacity = random(0.2, 1);
         this.band = Math.floor(random(128));
     },
     draw: function () {
         var pulsar, scale;
         pulsar = Math.exp(this.pulse);
         scale = pulsar * this.radius || this.radius;
         ctx.save();
         ctx.beginPath(); //Начинает отрисовку фигуры
         ctx.arc(this.x, this.y, scale, 0, Math.PI * 2);
         ctx.fillStyle = this.color; //цвет
         ctx.globalAlpha = this.opacity / this.level; //прозрачность
         ctx.closePath();
         ctx.fill();
         ctx.strokeStyle = this.color; //цвет рамки
         ctx.stroke();
         ctx.restore();

          this.move();
      },
      move: function () {
          this.y -= this.speed * this.level;
           
          //Возвращаем в начало частицы которые ушли за пределы холста
          if (this.y < -100) {
              this.y = canva.height;
          }
      }
}
//И напишем вспомогательные ф-и 
var random: function( min, max ) {
    if (this.isArray( min )) {
        return min[ ~~( Math.random() * min.length ) ];
    }
    if (!this.isNumber(max)) {
        max = min || 1, min = 0;
    }
    return min + Math.random() * ( max - min ); 
},
//Проверка на массив
var isArray: function(object) {
    return Object.prototype.toString.call( object ) == '[object Array]';
},
//Проверка на число
var isNumber: function(object) {
    return typeof object == 'number';
}


Создаем анализатор


Итак, у нас есть все, чтобы начать писать визуализатор. Главную роль будет играть понятие AudioContext. У одной страницы может быть только один контекст, но этого вполне достаточно, чтобы реализовать все, что вашей душе угодно. Практически все методы для создания модулей являются методами AudioContext. Полный их список можно посмотреть тут. Так как спецификация AudioContext до конца еще не одобрена, то создавать мы его будет вот таким образом:

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


Этого достаточно, чтобы создать AudioContext в Opera, Chrome и Firefox. От созданного нами контекста нам понадобятся следующие методы:

  1. createScriptProcessor (Бывший createJavaScriptNode) — Этот метод позволяет создать интерфейс для сбора, обработки или анализа аудио-данных при помощи js. У этого интерфейса есть свой обработчик событий и нас будет интересовать событие onaudioprocess, которое наступает, когда на вход передаются новые данные.
    При вызове метод принимает три аргумента bufferSize — Размер буфера, numInputChannels — кол-во входных каналов в потоке, numOutputChannels — кол-во выходных каналов.
  2. createMediaElementSource — Создает интерфейс, который представляет собой источник звука от аудио или видео элемента. После вызова данного метода аудио поток с аудио элемента будет перенаправляться на обработку в AudioContext.
  3. createAnalyser — Данный метод позволяет получить информацию о частотных и временных параметрах сигнала в виде массива данных. Мы должны будем присоединить наш анализатор к источнику аудио сигнала и к получателю звука.


Итак, используя то, что мы выяснили по поводу AudioContext, набросаем конструктор нашего анализатора:

var Analyze = function () {
    var AudioContext = w.AudioContext || w.webkitAudioContext;
   
    this.context = new AudioContext();
    this.node = this.context.createScriptProcessor(2048, 1, 1);
    this.analyser = this.context.createAnalyser();
    this.analyser.smoothingTimeConstant = 0.3;
    this.analyser.fftSize = 512;
    
    this.bands = new Uint8Array(this.analyser.frequencyBinCount);
}


Эта ф-я при вызове создаст нам аудио контекст и интерфейс для анализа данных. Тут видно, что при создании анализатора мы устанавливаем значения для параметров smoothingTimeConstant — частота опроса с которой анализатор будет требовать данные и fftSize — размерность преобразования Фурье (грубо говоря, этот параметр указывает, сколько данных мы хотим получить в результате частотного анализа сигнала, это кол-во будет равно fftSize/2). Функцию Uint8Array мы используем для создания массива с четким указанием границ, в нашем случае его длина будет равна 256.

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …]


Пока в массиве у нас шаром покати, но это и не удивительно, ведь нет источника сигнала, который надо анализировать. Добавим в наш конструктор создание audio — элемента, а заодно и подпишемся на событие canplay для него, которое возникает, когда браузер считает, что получил достаточно данных для того, чтобы начать воспроизведение. С учетом этого наш конструктор примет вид:

var Analyze = function () {
    var AudioContext = w.AudioContext || w.webkitAudioContext;
   
    //Создание источника
    this.audio = new Audio();
    this.audio.src = "test1.ogg";
    this.controls = true;
    //Создаем аудио-контекст
    this.context = new AudioContext();
    this.node = this.context.createScriptProcessor(2048, 1, 1);
    //Создаем анализатор
    this.analyser = this.context.createAnalyser();
    this.analyser.smoothingTimeConstant = 0.3;
    this.analyser.fftSize = 512;
    this.bands = new Uint8Array(this.analyser.frequencyBinCount);

    //Подписываемся на событие
    this.audio.addEventListener("canplay", function () {});
}


Нам осталось отправить наш созданный аудио поток в AudioContext и связать анализатор с источником и приемником. Сделать это довольно легко, ведь умные люди, которые создавали Web Audio API, позаботились об этом и благодаря им у каждого audio — модуля есть метод connect, который в качестве параметра принимает значение, к которому и надо присоединиться. Итого, конечный вид будет таким:

var Analyse = function () {
          var an= this,
          AudioContext = w.AudioContext || w.webkitAudioContext;

          //Создание источника
          this.audio = new Audio();
          this.audio.src = "test1.ogg";
          this.controls = true;
          //Создаем аудио-контекст
          this.context = new AudioContext();
          this.node = this.context.createScriptProcessor(2048, 1, 1);
          //Создаем анализатор
          this.analyser = this.context.createAnalyser();
          this.analyser.smoothingTimeConstant = 0.3;
          this.analyser.fftSize = 512;
          this.bands = new Uint8Array(this.analyser.frequencyBinCount);
          //Подписываемся на событие
          this.audio.addEventListener('canplay', function () {
                     //отправляем на обработку в  AudioContext 
                    an.source = an.context.createMediaElementSource(an.audio);
                    //связываем источник и анализатором
                    an.source.connect(an.analyser);
                    //связываем анализатор с интерфейсом, из которого он будет получать данные
                    an.analyser.connect(an.node);
                    //Связываем все с выходом
                    an.node.connect(an.context.destination);
                    an.source.connect(an.context.destination);
                    //подписываемся на событие изменения входных данных
                    an.node.onaudioprocess = function () {
                        an.analyser.getByteFrequencyData(an.bands);
                        if (!an.audio.paused) {
                            if (typeof an.update === "function") {
                                return an.update(an.bands);
                            } else {
                                return 0;
                            }
                        }
                    };
            });

            return this;
        };


Тут стоит упомянуть о AudioContext.destination — это системный звуковой выход по умолчанию (обычно это колонки). Метод getByteFrequencyData — этот метод получает данные от анализатора и копирует их в переданный массив, который мы в итоге и возвращаем, благодаря великой магии замыканий.

Допишем в нашу функцию createParticles создание анализатора, в итоге получим:

var createParticles = function () {
    var particle = null, audio = null;
    for (var i = 0; i < 50; i++) {
        particle = new Particle();
        particles.push(particle);
    }
    //Создаем анализатор
    elem = new Analyse();
   //Добавляем элемент audio на страницу
   document.body.appendChild(elem.audio);
   //Добавляем данные полученные анализатором к созданным частицам
   audio.update = function (bands) {
       var ln = 50;
        while (ln--) {
            var loc = particles[ln];
            loc.pulse = bands[loc.band] / 256;
        }
   };
    //Тут запускаем непосредственно функцию отрисовки 
    setInterval(draw,33);
}


Вот и все, мы получили свой простенький визуализатор и немного приоткрыли завесу над Web Audio API. Вся эта прелесть будет работать в Chrome, Opera, Firefox. IE, как и всегда, остается за бортом.
Немного более навороченная демка, в которой использовался данный код: demo
Код демки на github: Analyser
Код на codepen: Analyser

Конечно это только маленькая часть тех возможностей, на которые способен Audio API, но надо же с чего-то начинать. Уже сейчас можно применять Audio API для:
  • Объемный звук для игр
  • Приложения для обработки звука
  • Аудио синтез

и для многого другого.

Полезное чтиво:
Tags:
Hubs:
+30
Comments 13
Comments Comments 13

Articles