Визуализация с Web Audio API
Одна из самых интересных фич Web Audio API — возможность извлекать частоту, форму волны и другие данные из звукового источника, которые могут быть использованы для создания визуализаций. Эта статья объясняет, как это можно сделать, и приводит несколько базовых примеров использования.
Примечание: Вы можете найти рабочие примеры всех фрагментов кода в нашей демонстрации автоизменения голоса.
Основные концепции
Чтобы извлечь данные из вашего источника звука, вам понадобится AnalyserNode
, созданный при помощи метода AudioContext.createAnalyser()
, например:
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var analyser = audioCtx.createAnalyser();
Затем этот узел подключается к вашему источнику звука где-то между получением звука и его обработкой, например:
source = audioCtx.createMediaStreamSource(stream);
source.connect(analyser);
analyser.connect(distortion);
distortion.connect(audioCtx.destination);
Примечание: Вам не нужно подключать вывод анализатора к другому узлу для его работы, пока его ввод подключён к источнику, либо напрямую, либо через другой узел.
Затем анализатор захватит аудиоданные, используя быстрое преобразование Фурье (БПФ) в определённой частотной области, в зависимости от того, что вы укажете как значение свойства AnalyserNode.fftSize
(если свойство не задано, то значение по умолчанию равно 2048).
Примечание: Вы так же можете указать значения минимума и максимума для диапазона масштабирования данных БПФ, используя AnalyserNode.minDecibels
и AnalyserNode.maxDecibels
, и разные константы усреднения данных с помощью AnalyserNode.smoothingTimeConstant
. Прочтите эти страницы, чтобы получить больше информации о том как их использовать.
Чтобы получить данные, вам нужно использовать методы AnalyserNode.getFloatFrequencyData()
и AnalyserNode.getByteFrequencyData()
, чтобы получить данные о частоте, и AnalyserNode.getByteTimeDomainData()
и AnalyserNode.getFloatTimeDomainData()
чтобы получить данные о форме волны.
Эти методы копируют данные в указанный массив, поэтому вам необходимо создать новый массив для хранения данных, прежде чем вызывать эти функции . Результат первой из них - числа типа float32, второй и третьей - uint8, поэтому стандартный массив JavaScript не подойдёт для их хранения — следует использовать массивы Float32Array
или Uint8Array
, в зависимости от нужных вам данных.
Например, если параметр AnalyserNode.fftSize установлен на 2048, мы возвращаем значение AnalyserNode.frequencyBinCount
, равное половине fft, затем вызываем Uint8Array() с frequencyBinCount в качестве длины — столько измерений мы произведём для данного размера fft.
analyser.fftSize = 2048;
var bufferLength = analyser.frequencyBinCount;
var dataArray = new Uint8Array(bufferLength);
Чтобы собрать данные и копировать их в массив, мы вызываем подходящий метод сбора данных, с массивом в качестве аргумента. Например:
analyser.getByteTimeDomainData(dataArray);
Теперь в массиве хранятся данные, описывающие звук в данный момент времени, и мы можем визуализировать их любым удобным образом, например с помощью холста HTML5: <canvas>
.
Давайте рассмотрим конкретные примеры:
Создание формы волны/осциллографа
Чтобы визуализировать осциллограф (спасибо Soledad Penadés за код в Voice-change-O-matic), мы сначала следуем шаблону, описанному в предыдущей секции, для создания буфера:
analyser.fftSize = 2048;
var bufferLength = analyser.frequencyBinCount;
var dataArray = new Uint8Array(bufferLength);
Затем, мы очищаем холст:
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
И определяем функцию draw()
:
function draw() {
Здесь мы используем requestAnimationFrame()
чтобы многократно вызывать эту функцию:
drawVisual = requestAnimationFrame(draw);
Затем мы копируем данные в наш массив:
analyser.getByteTimeDomainData(dataArray);
Устанавливаем заливку холста
canvasCtx.fillStyle = "rgb(200, 200, 200)";
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
Затем устанавливаем ширину линий и цвет волны, которую мы хотим нарисовать, и начинаем рисовать путь
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = "rgb(0, 0, 0)";
canvasCtx.beginPath();
Мы определяем ширину каждого отрезка в линии, деля длину холста на длину массива (равную FrequencyBinCount), затем определяем переменную x, задающую позицию, в которую необходимо перенести каждый отрезок.
var sliceWidth = (WIDTH * 1.0) / bufferLength;
var x = 0;
В цикле, мы задаём позицию небольшого отрезка волны для каждой точки в буфере на высоте, основанной на значении массива в этой точке, а затем перемещаем линию туда, где должен быть нарисован следующий отрезок:
for (var i = 0; i < bufferLength; i++) {
var v = dataArray[i] / 128.0;
var y = (v * HEIGHT) / 2;
if (i === 0) {
canvasCtx.moveTo(x, y);
} else {
canvasCtx.lineTo(x, y);
}
x += sliceWidth;
}
Наконец, мы заканчиваем линию в середине правой стороны холста и рисуем, используя установленные цвет и ширину линий:
canvasCtx.lineTo(canvas.width, canvas.height/2);
canvasCtx.stroke();
};
В конце концов, мы вызываем функцию draw()
, запускающую весь процесс:
draw();
Мы получаем изображение волны, обновляющееся несколько раз в секунду:
Создание частотной гистограммы
Ещё одна визуализация, которую можно создать, - это частотные диаграммы (такие, как строит Winamp). В проекте Voice-change-O-matic есть реализация такой диаграммы. Давайте посмотрим на неё.
Сначала мы снова создаём анализатор и массив для данных, затем очищаем холст при помощи clearRect()
. Единственное отличие от того, что мы делали раньше - ы том, что мы можем установить намного меньший размер fft. Таким образом, каждый столбец в диаграмме будет выглядеть как столбец, а не как тонкая полоска.
analyser.fftSize = 256;
var bufferLength = analyser.frequencyBinCount;
console.log(bufferLength);
var dataArray = new Uint8Array(bufferLength);
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
Затем мы запускаем функцию draw()
и задаём цикл при помощи requestAnimationFrame()
таким образом, чтобы в каждом кадре анимации данные обновлялись.
function draw() {
drawVisual = requestAnimationFrame(draw);
analyser.getByteFrequencyData(dataArray);
canvasCtx.fillStyle = 'rgb(0, 0, 0)';
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
Затем мы устанавливаем значение barWidth
, равное ширине холста, делённой на количество столбцов (длину буфера). Однако, мы домножаем ширину на 2.5, поскольку на большинстве частот звука не будет, поскольку большинство звуков, которые мы слышим в повседневной жизни, находятся в определённых, достаточно низких, диапазонах частот. Нам нет смысла показывать множество пустых частот, поэтому мы просто сдвигаем столбцы, соответствующие наиболее распространённым частотам.
Мы также устанавливаем значение переменных barHeight
и x
, задающей то, где на холсте должен быть размещён каждый столбец.
var barWidth = (WIDTH / bufferLength) * 2.5;
var barHeight;
var x = 0;
Как и раньше, мы в цикле проходим по каждому значению в dataArray
. Для каждого значения мы устанавливаем высоту barHeight
на уровне значения в массиве, устанавливаем заливку в зависимости от barHeight
(Чем выше столбец, тем он ярче), и рисуем столбец в x
пикселях от левой стороны холста. Ширина столбца равна barWidth
, а высота - barHeight/2
(чтобы столбцы помещались на холсте, мы уменьшили высоту в два раза)
Одна переменная, требующая объяснения, - это вертикальный сдвиг, с которым мы рисуем каждый столбец: HEIGHT-barHeight/2
. Это делается для того, чтобы столбцы начинались в нижней части холста, а не в верхней, как было бы, если бы вертикальное положение было установлена в 0 0. Поэтому мы каждый раз устанавливаем вертикальное положение в разность высоты холста и barHeight/2
, чтобы столбцы начинались где-то между верхом и низом холста и заканчивались снизу.
for(var i = 0; i < bufferLength; i++) {
barHeight = dataArray[i]/2;
canvasCtx.fillStyle = 'rgb(' + (barHeight+100) + ',50,50)';
canvasCtx.fillRect(x,HEIGHT-barHeight/2,barWidth,barHeight);
x += barWidth + 1;
}
};
Снова, мы вызываем функцию draw() в конце кода, чтобы запустить процесс.
draw();
Этот код даёт нам следующий результат:
Примечание: Примеры, используемые в данной статье, используют AnalyserNode.getByteFrequencyData()
и AnalyserNode.getByteTimeDomainData()
. Примеры работы с AnalyserNode.getFloatFrequencyData()
и AnalyserNode.getFloatTimeDomainData()
можно найти в демо Voice-change-O-matic-float-data (Вы также можете посмотреть исходный код) — это то же самое, что и Voice-change-O-matic, но здесь используются данные типа float, а не unsigned vyte.