Статьи

Изучение веб-аудио HTML5: визуализация звука

Если вы читали некоторые другие мои статьи в этом блоге, вы наверняка знаете, что я фанат HTML5. С HTML5 мы получаем всю эту интересную функциональность прямо в браузере, таким образом, что, в конце концов, является стандартным для всех браузеров. Одним из новых API-интерфейсов HTML5, который постепенно проходит процесс стандартизации, является API-интерфейс Web Audio . Благодаря этому API, который в настоящее время поддерживается только в Chrome , мы получаем доступ ко всем видам интересных аудиокомпонентов, которые можно использовать для создания, изменения и визуализации звуков (например, следующей спектрограммы).

localhost_Dev_WebstormProjects_webaudio_-1.png

Так почему я начинаю с визуализаций? Это выглядит красиво, это одна из причин, но не важная. Этот API предоставляет ряд более сложных компонентов, поведение которых гораздо проще объяснить, когда вы видите, что происходит. С помощью фильтра вы можете сразу увидеть, отфильтрованы ли некоторые частоты, вместо того, чтобы пытаться прослушать результирующий звук для этих изменений.

Есть много интересных примеров, которые используют этот API. Проблема, однако, в том, что начало работы с этим API и с цифровой обработкой сигналов (DSP) обычно не объясняется. В этой статье я проведу вас через пару шагов, которые показывают, как сделать следующее:

  • Создать измеритель громкости сигнала
  • Визуализируйте частоты с помощью анализатора спектра
  • И показать временную спектрограмму

Мы начнем с базовой настройки, которую мы можем использовать в качестве основы для компонентов, которые мы создадим.

Настройка основных

Если мы хотим поэкспериментировать со звуком, нам нужен источник звука. Мы могли бы использовать микрофон (как мы сделаем позже в этой серии), но для простоты, сейчас мы просто используем mp3 в качестве входных данных. Чтобы заставить это работать, используя веб-аудио, мы должны сделать следующие шаги:

  1. Загрузите данные
  2. Прочитайте его в буферном узле и воспроизведите звук

Загрузите данные

С веб-аудио мы можем использовать различные типы аудио источников. У нас есть MediaElementAudioSourceNode, который можно использовать для использования аудио, предоставляемого медиа-элементом. Также есть MediaStreamAudioSourceNode . С этим узлом аудиоисточника мы можем использовать микрофон в качестве входного сигнала ( см. Мою предыдущую статью о распознавании звука ). Наконец, есть AudioBufferSourceNode . С помощью этого узла мы можем загрузить данные из существующего аудиофайла (например, mp3) и использовать их в качестве входных данных. Для этого примера мы будем использовать этот последний подход.

   // create the audio context (chrome only for now)
    var context = new webkitAudioContext();
    var audioBuffer;
    var sourceNode;
 
    // load the sound
    setupAudioNodes();
    loadSound("wagner-short.ogg");
 
    function setupAudioNodes() {
        // create a buffer source node
        sourceNode = context.createBufferSource();
        // and connect to destination
        sourceNode.connect(context.destination);
    }
 
    // load the specified sound
    function loadSound(url) {
        var request = new XMLHttpRequest();
        request.open('GET', url, true);
        request.responseType = 'arraybuffer';
 
        // When loaded decode the data
        request.onload = function() {
 
            // decode the data
            context.decodeAudioData(request.response, function(buffer) {
                // when the audio is decoded play the sound
                playSound(buffer);
            }, onError);
        }
        request.send();
    }
 
 
    function playSound(buffer) {
        sourceNode.buffer = buffer;
        sourceNode.noteOn(0);
    }
 
    // log if an error occurs
    function onError(e) {
        console.log(e);
    }

В этом примере вы можете увидеть несколько функций. Функция setupAudioNodes создает аудиоузел BufferSource и подключает его к месту назначения. Функция loadSound показывает, как вы можете загрузить аудиофайл. Буфер, который передается в функцию playSound, содержит декодированное аудио, которое может использоваться веб-аудио API.

В этом примере я использую файл .ogg, полный обзор поддерживаемых форматов смотрите по адресу: https://sites.google.com/a/chromium.org/dev/audio-video.

Воспроизвести звук

Чтобы воспроизвести этот аудиофайл, все что нам нужно сделать, это включить исходный узел, это делается в функции playSound:

    function playSound(buffer) {
        sourceNode.buffer = buffer;
        sourceNode.noteOn(0);
    }

Вы можете проверить это на следующей странице:

Когда вы откроете эту страницу, вы услышите немного музыки. Ничего особенного, но тем не менее простой способ загрузить звук, который будет использоваться до конца этой статьи. Первым пунктом в нашем списке был измеритель объема.

Создать измеритель объема

Одним из основных сценариев, и часто одним из первых шагов, который пытается создать кто-то новичок в этом API, является простой измеритель объема сигнала (или УФ-измеритель). Я ожидал, что это будет стандартный компонент в этом API, где я мог бы просто считать силу сигнала как свойство. Но такого узла не существует. Но не волнуйтесь, с доступными компонентами довольно легко (не просто, но все же легко) получить информацию о силе сигнала вашего аудиофайла. В этом разделе мы создадим следующий простой измеритель объема:

Загрузка и воспроизведение звука с помощью Web Audio API-1.png

Как видите, это простой измеритель громкости, где мы измеряем силу сигнала для левого и правого аудиоканала. Это нарисовано на холсте, но вы могли бы также использовать divs или svg для визуализации этого. Давайте начнем с одного измерителя громкости вместо одного для каждого канала. Для этого нам нужно сделать следующее:

  1. Создать узел анализатора: с помощью этого узла мы получаем в реальном времени информацию о данных, которые обрабатываются. Эти данные мы используем для определения силы сигнала
  2. Создать узел javascript: этот узел используется как таймер для обновления измерителей объема новой информацией.
  3. Соедините все вместе

Узел анализатора

С помощью узла анализатора мы можем выполнять анализ частоты и времени в реальном времени. Из спецификации:

узел, который способен предоставлять информацию о частоте в реальном времени и анализ во временной области. Аудиопоток будет передаваться необработанным от входа до выхода.

Я не буду вдаваться в математические подробности этого узла, так как есть много статей, в которых объясняется, как это работает (хорошая из них — глава о преобразовании Фурье отсюда ). Что вы должны теперь сказать об этом узле, так это о том, что он разделяет сигнал на частотные сегменты, и мы получаем амплитуду (мощность сигнала) для каждого набора частот (сегмента). Лучший способ понять это — пропустить немного вперед в этой статье и посмотреть на распределение частот, которое мы создадим позже.

localhost_Dev_WebstormProjects_webaudio_example3.html.png

Это изображение отображает результат из узла анализатора. Частоты увеличиваются слева направо, а высота столбца показывает силу этой конкретной области частот. Подробнее об этом позже в статье. На данный момент мы не хотим видеть силу отдельных частотных сегментов, но силу общего сигнала. Для этого мы просто добавим все силы из каждого сегмента и разделим его на количество сегментов.

Сначала нам нужно создать узел анализатора

       // setup a analyzer
        analyser = context.createAnalyser();
        analyser.smoothingTimeConstant = 0.3;
        analyser.fftSize = 1024;

Это создает узел анализатора, чей результат будет использоваться для создания измерителя объема. Мы используем smoothingTimeConstant, чтобы сделать счетчик менее нервным. С помощью этой переменной мы используем входные данные из более длительного периода времени для расчета амплитуд, что приводит к более плавному измерению. FftSize определяет, сколько мы получаем блоков, содержащих информацию о частоте. Если у нас fftSize 1024, мы получим 512 сегментов (подробнее об этом в книге о DPS и преобразованиях Фурье).

Когда этот узел получает поток данных, он анализирует этот поток и предоставляет нам информацию о частотах в этом сигнале и их сильных сторонах. Теперь нам нужен таймер для регулярного обновления счетчика. Мы могли бы использовать стандартную функцию javascript setInterval, но поскольку мы смотрим на API Web Audio, мы можем использовать один из его узлов. JavaScriptNode.

Узел JavaScript

С помощью javascriptnode мы можем обрабатывать необработанные аудиоданные напрямую из javascript. Мы можем использовать это для написания наших собственных анализаторов или сложных компонентов. Мы не собираемся этого делать. При создании узла javascript вы можете указать интервал, с которым он вызывается. Мы будем использовать эту функцию для обновления счетчика с регулярными интервалами.

Создать узел javascript очень просто.

        // setup a javascript node
        javascriptNode = context.createJavaScriptNode(2048, 1, 1);

Это создаст javascriptnode, который вызывается всякий раз, когда были выбраны 2048 кадров. Поскольку наши данные выбираются на скорости 44,1 Кб, эта функция будет вызываться примерно 21 раз в секунду. Теперь, что происходит, когда эта функция вызывается:

   // when the javascript node is called
    // we use information from the analyzer node
    // to draw the volume
    javascriptNode.onaudioprocess = function() {
 
        // get the average, bincount is fftsize / 2
        var array =  new Uint8Array(analyser.frequencyBinCount);
        analyser.getByteFrequencyData(array);
        var average = getAverageVolume(array)
 
        // clear the current state
        ctx.clearRect(0, 0, 60, 130);
 
        // set the fill style
        ctx.fillStyle=gradient;
 
        // create the meters
        ctx.fillRect(0,130-average,25,130);
    }
 
    function getAverageVolume(array) {
        var values = 0;
        var average;
 
        var length = array.length;
 
        // get all the frequency amplitudes
        for (var i = 0; i < length; i++) {
            values += array[i];
        }
 
        average = values / length;
        return average;
    }

В этих двух функциях мы вычисляем среднее значение и рисуем метр прямо на холсте (используя градиент, чтобы у нас были хорошие цвета). Теперь все, что нам нужно сделать, — это подключить выход аудиосигнала к анализатору, анализатор к узлу javasource (и если мы хотим, чтобы звук слышал, нам также нужно что-то подключить к описанию).

Соедините все вместе

Соединить все вместе легко:

 function setupAudioNodes() {
 
        // setup a javascript node
        javascriptNode = context.createJavaScriptNode(2048, 1, 1);
        // connect to destination, else it isn't called
        javascriptNode.connect(context.destination);
 
        // setup a analyzer
        analyser = context.createAnalyser();
        analyser.smoothingTimeConstant = 0.3;
        analyser.fftSize = 1024;
 
        // create a buffer source node
        sourceNode = context.createBufferSource();
 
        // connect the source to the analyser
        sourceNode.connect(analyser);
 
        // we use the javascript node to draw at a specific interval.
        analyser.connect(javascriptNode);
 
        // and connect to destination, if you want audio
       sourceNode.connect(context.destination);
    }

Вот и все. Это нарисует один измеритель объема, для полного сигнала. Теперь, что мы делаем, когда мы хотим иметь измеритель громкости для каждого канала. Для этого мы используем ChannelSplitter. Давайте погрузимся прямо в код, чтобы соединить все:

    function setupAudioNodes() {
 
        // setup a javascript node
        javascriptNode = context.createJavaScriptNode(2048, 1, 1);
        // connect to destination, else it isn't called
        javascriptNode.connect(context.destination);
 
        // setup a analyzer
        analyser = context.createAnalyser();
        analyser.smoothingTimeConstant = 0.3;
        analyser.fftSize = 1024;
 
        analyser2 = context.createAnalyser();
        analyser2.smoothingTimeConstant = 0.0;
        analyser2.fftSize = 1024;
 
        // create a buffer source node
        sourceNode = context.createBufferSource();
        splitter = context.createChannelSplitter();
 
        // connect the source to the analyser and the splitter
        sourceNode.connect(splitter);
 
        // connect one of the outputs from the splitter to
        // the analyser
        splitter.connect(analyser,0,0);
        splitter.connect(analyser2,1,0);
 
        // we use the javascript node to draw at a
        // specific interval.
        analyser.connect(javascriptNode);
 
        // and connect to destination
        sourceNode.connect(context.destination);
    }

Как видите, мы не сильно меняемся. Мы вводим новый узел, узел сплиттера. Этот узел разбивает звук на левый и правый канал. Эти каналы могут обрабатываться отдельно. С этим макетом происходит следующее:

  1. Аудиоисточник создает сигнал на основе буферизованного аудио.
  2. Этот сигнал отправляется разделителю, который разделяет сигнал на левый и правый поток.
  3. Каждый из этих двух потоков обрабатывается собственным анализатором в реальном времени.
  4. Из узла javascript мы теперь получаем информацию от обоих анализаторов и строим графики на обоих счетчиках

Я показал шаги с 1 по 3, давайте быстро перейдем к шагу 4. Для этого мы просто добавим следующее к узлу onaudioprocess:

    javascriptNode.onaudioprocess = function() {
 
        // get the average for the first channel
        var array =  new Uint8Array(analyser.frequencyBinCount);
        analyser.getByteFrequencyData(array);
        var average = getAverageVolume(array);
 
        // get the average for the second channel
        var array2 =  new Uint8Array(analyser2.frequencyBinCount);
        analyser2.getByteFrequencyData(array2);
        var average2 = getAverageVolume(array2);
 
        // clear the current state
        ctx.clearRect(0, 0, 60, 130);
 
        // set the fill style
        ctx.fillStyle=gradient;
 
        // create the meters
        ctx.fillRect(0,130-average,25,130);
        ctx.fillRect(30,130-average2,25,130);
    }

И теперь у нас есть два измерителя сигнала, по одному на каждый канал.

Или посмотреть результат на YouTube:

Теперь давайте посмотрим, как мы можем получить представление о частотах, которые я показывал ранее.

Создать частотный спектр

Со всей работой, которую мы уже проделали в предыдущем разделе, создание обзора частотного спектра теперь очень просто. Мы собираемся стремиться к этому:

Загрузка и воспроизведение звука с помощью Web Audio API-2.png

Мы настраиваем узлы так же, как и в первом примере:

    function setupAudioNodes() {
 
        // setup a javascript node
        javascriptNode = context.createJavaScriptNode(2048, 1, 1);
        // connect to destination, else it isn't called
        javascriptNode.connect(context.destination);
 
        // setup a analyzer
        analyser = context.createAnalyser();
        analyser.smoothingTimeConstant = 0.3;
        analyser.fftSize = 512;
 
        // create a buffer source node
        sourceNode = context.createBufferSource();
        sourceNode.connect(analyser);
        analyser.connect(javascriptNode);
 
//        sourceNode.connect(context.destination);
    }

Поэтому на этот раз мы не разделяем каналы и устанавливаем fftSize равным 512. Это означает, что мы получаем 256 баров, которые представляют нашу частоту. Теперь нам просто нужно изменить метод onaudioprocess и используемый нами градиент:

    var gradient = ctx.createLinearGradient(0,0,0,300);
    gradient.addColorStop(1,'#000000');
    gradient.addColorStop(0.75,'#ff0000');
    gradient.addColorStop(0.25,'#ffff00');
    gradient.addColorStop(0,'#ffffff');
 
    // when the javascript node is called
    // we use information from the analyzer node
    // to draw the volume
    javascriptNode.onaudioprocess = function() {
 
        // get the average for the first channel
        var array =  new Uint8Array(analyser.frequencyBinCount);
        analyser.getByteFrequencyData(array);
 
        // clear the current state
        ctx.clearRect(0, 0, 1000, 325);
 
        // set the fill style
        ctx.fillStyle=gradient;
        drawSpectrum(array);
 
    }
    function drawSpectrum(array) {
    for ( var i = 0; i < (array.length); i++ ){
            var value = array[i];
            ctx.fillRect(i*5,325-value,3,325);
        }
    };

В функции drawSpectrum мы перебираем массив и рисуем вертикальную черту в зависимости от значения. Вот и все. Для живого примера, нажмите на следующую ссылку:

Или посмотрите на YouTube:

И затем последний. Спектрограмма.

Временная спектрограмма

Когда вы запускаете предыдущую демонстрацию, вы видите силу различных частотных интервалов в реальном времени. Хотя это хорошая визуализация, она не позволяет анализировать информацию в течение определенного периода времени. Если вы хотите сделать это, вы можете создать спектрограмму.

localhost_Dev_WebstormProjects_webaudio_-1.png

С помощью спектрограммы мы строим одну линию для каждого измерения. Ось Y представляет частоту, а ось X — время, а цвет пикселя — силу этой частоты. Его можно использовать для анализа полученного звука, а также для создания красивых изображений.

Хорошо, что для вывода этих данных нам не нужно сильно отличаться от того, что у нас уже есть. Единственная функция, которая изменится — это узел onaudioprocess, и мы создадим немного другой анализатор.

        analyser = context.createAnalyser();
        analyser.smoothingTimeConstant = 0;
        analyser.fftSize = 1024;

Созданный нами анализатор имеет fftSize 1024, это означает, что мы получаем 512 частотных интервалов с сильными сторонами. Таким образом, мы можем нарисовать спектрограмму высотой 512 пикселей. Также обратите внимание, что smoothingTimeConstant имеет значение 0. Это означает, что мы не используем никаких предыдущих результатов в анализе. Мы хотим показать реальную информацию, а не обеспечить плавный объемный анализ или анализ частотного спектра.

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

    // create a temp canvas we use for copying and scrolling
    var tempCanvas = document.createElement("canvas"),
        tempCtx = tempCanvas.getContext("2d");
    tempCanvas.width=800;
    tempCanvas.height=512;
 
    // used for color distribution
    var hot = new chroma.ColorScale({
        colors:['#000000', '#ff0000', '#ffff00', '#ffffff'],
        positions:[0, .25, .75, 1],
        mode:'rgb',
        limits:[0, 300]
    });
 
   ...
 
    // when the javascript node is called
    // we use information from the analyzer node
    // to draw the volume
    javascriptNode.onaudioprocess = function () {
 
        // get the average for the first channel
        var array = new Uint8Array(analyser.frequencyBinCount);
        analyser.getByteFrequencyData(array);
 
        // draw the spectrogram
        if (sourceNode.playbackState == sourceNode.PLAYING_STATE) {
            drawSpectrogram(array);
        }
    }
 
    function drawSpectrogram(array) {
 
        // copy the current canvas onto the temp canvas
        var canvas = document.getElementById("canvas");
 
        tempCtx.drawImage(canvas, 0, 0, 800, 512);
 
        // iterate over the elements from the array
        for (var i = 0; i < array.length; i++) {
            // draw each pixel with the specific color
            var value = array[i];
            ctx.fillStyle = hot.getColor(value).hex();
 
            // draw the line at the right side of the canvas
            ctx.fillRect(800 - 1, 512 - i, 1, 1);
        }
 
        // set translate on the canvas
        ctx.translate(-1, 0);
        // draw the copied image
        ctx.drawImage(tempCanvas, 0, 0, 800, 512, 0, 0, 800, 512);
 
        // reset the transformation matrix
        ctx.setTransform(1, 0, 0, 1, 0, 0);
    }

Чтобы нарисовать спектрограмму, мы делаем следующее:

  1. Мы копируем то, что в настоящее время рисуется на скрытый холст
  2. Затем мы рисуем линию текущих значений в крайнем правом углу холста
  3. Мы устанавливаем перевод на холсте на -1
  4. Мы копируем скопированную информацию обратно на исходный холст (который теперь рисуется на 1 пиксель слева)
  5. И сбросить матрицу преобразования

Смотрите бегущий пример здесь:

Или посмотрите это здесь:

Последнее, что я хотел бы упомянуть в отношении кода, это библиотека chroma.js, которую я использовал для цветов. Если вам когда-нибудь понадобится нарисовать что-нибудь связанное с цветом или градиентом (например, карты, сильные стороны, уровни), вы можете легко создать цветовые шкалы с помощью этой библиотеки.

Два последних указателя, я знаю, я получу вопросы о:

  • Объем мог быть представлен как величина, просто не хотел усложнять вопросы для этого.
  • Спектограмма не использует логарифмические шкалы. Еще раз, не хотел усложнять вещи