Захвати и визуализируй! Или гистограмма с микрофона средствами Web Audio API



    Я очень люблю «живые» графики. Смертельная скука — смотреть на статичные картинки с цифрами. Мне хочется, чтобы график завораживал, чтобы заставлял человека, который смотрит на него, взаимодействовать и открывать для себя новые грани всех данных на нем. Поэтому любой пример, что попадает мне в руки, и любая библиотека визуализации, которой не повезло оказаться на моей машине, проходит испытание “оживлением”. Вот и в очередной раз, раздумывая, как же еще я могу раскорячить визуализационные виджеты из DevExtreme библиотеки, я задумалась об отображении звука. «Интересно и живо» — подумала я в тот день, запаслась чаем с печеньками и засела за эту задачу. Что у меня в итоге вышло — узнаете под катом.

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



    Захват аудио с микрофона. Один из самых простых способов получить данные с микрофона в браузере — использовать getUserMedia метод, что находится в объекте общедоступных браузерных методов — в navigator. Принимает он три аргумента:
    1. Настройки захвата аудио и видео:
      { audio: true/false, video: true/false }
      

    2. Функцию, выполняющуюся в случае успеха
    3. Функцию, выполняющуюся в случае ошибки

    То есть, в общем случае вызов этого метода выглядит вот так:
    navigator.getUserMedia(
        { audio: true, video: true },
        function (stream) {
            //stream processing
        },
        function (error) {
            //error processing
        }
    );
    

    Но! Нужно помнить, что метод в разных браузерах различен, поэтому будет не лишним перед вызовом getUserMedia провернуть следующую штуку, и обеспечить поддержку метода во всех распространенных браузерах:
    navigator.getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);
    

    Анализ захваченного аудио. В случае успеха выполнения метода getUserMedia, мы получаем объект типа MediaStream, который уже можно использовать для создания источника аудио с помощью Web Audio API. Но чтобы воспользоваться этим способом, для начала нужно создать AudioContext — наш проводник в мир Web Audio API. Создание очень простое:
    var ctx = new AudioContext();
    

    Опять же, нужно не забывать про кроссбраузерность:
    var AudioContext = window.AudioContext || window.webkitAudioContext;
    

    Теперь, с помощью аудио контекста ctx, есть возможность создать все необходимые элементы для анализа нашего звука. Что же это за элементы?



    Элемент Destination нам доступен по-умолчанию, уже после создания аудио контекста. По сути, это то, куда будет уходить наш звук, а использование конструкции ctx.destination дает нам доступ к стандартному выводу звука для системы, например, к колонкам или наушникам. Остальные же элементы из этой схемы требуется создать и настроить.
    • Source — это источник звука, наше пойманное аудио с микрофона. Необходимо лишь преобразовать поток MediaStream в элемент, с которым мы сможем работать, и для этого подойдет метод аудио контекста createMediaStreamSource:
      var source = ctx.createMediaStreamSource(stream);
      

    • Analyser — это узел для анализа аудио, многие параметры звука можно вытянуть именно с его помощью. Создается он незатейливым методом аудио контекста createAnalyser:
      var analyser = ctx.createAnalyser();
      

    • ScriptProcessor — это модуль обработки аудио. Он необходим для отслеживания момента изменения звука с помощью события onaudioprocess. Создать его можно, используя метод аудио контекста createScriptProcessor с параметрами размера буфера, количеством входов и выходов:
      var processor = ctx.createScriptProcessor(2048, 1, 1);
      

    Все созданные элементы необходимо соединить между собой, как показано на схеме. Web Audio API предоставляет пару методов connect/disconnect для связывания и развязывания узлов:
    source.connect(analyser);
    source.connect(processor);
    analyser.connect(ctx.destination);
    processor.connect(ctx.destination);
    

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



    Последние штрихи — разбор звука по кирпичикам. Для анализатора есть возможность задать размерность преобразования Фурье — опцию fftSize. Зачем она сдалась? А затем, что в итоге получится количество элементов данных равное fftSize/2, то есть количество точек для графика:
    analyser.fftSize = 128;
    

    Осталось лишь получать данные о частотах звука от анализатора, сохраняя их в специально созданную для этого переменную data. Изменение звука отслеживается благодаря уже знакомому нам событию onaudioprocess:
    var data = new Uint8Array(analyser.frequencyBinCount);
    processor.onaudioprocess = function (){
        analyser.getByteFrequencyData(data);
        console.log(data);
    }
    

    Та-дам! И вот они, заветные циферки с микрофона в консоли (пример):



    Визуализация полученных данных. Непосредственно “оживляющая” часть, превращение циферок в график, для которого понадобится библиотека DevExtreme. Первым делом, конечно же, требуется создать график:
    <div id="chart" style="width: 800px; height: 250px"></div>
    

    var chart = chart = $("#chart").dxChart({ 
        //chart options
    }).dxChart("instance");
    

    Такой созданный график будет пустым, потому что для него нужны данные — опция dataSource, и серия, которую нужно отобразить — опция series. При этом данные — динамические, и их нужно менять при срабатывания события onaudioprocess:
    var dataSource = $.map(data, function (item, index) {
        return { arg: index, val: item };
    });
    chart.option("dataSource", dataSource);
    

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



    А итоговый код будет выглядеть вот так, просто и лаконично:
    Итоговый код
    navigator.getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);
    
    navigator.getUserMedia(
        { audio: true, video: false },
        function (stream) {
            var AudioContext = window.AudioContext || window.webkitAudioContext,
                ctx = new AudioContext(),
                source = ctx.createMediaStreamSource(stream),
                analyser = ctx.createAnalyser(),
                processor = ctx.createScriptProcessor(2048, 1, 1),
                data,
                chart,
                dataSource;
            
            source.connect(analyser);
            source.connect(processor);
            //analyser.connect(ctx.destination);
            processor.connect(ctx.destination);
            
            chart = $("#chart").dxChart({
                dataSource: [],
     		      legend: {
                    visible: false
                },
                argumentAxis: {
                    label: {
                        visible: false
                    }
                },
                valueAxis: {
                    grid: {
                        visible: false
                    },
                    label: {
                        visible: false
                    }
                },
                series: {
                    hoverMode: "none",
                    type: "bar",
                    color: "#1E90FF"
                }
            }).dxChart("instance");
            data = new Uint8Array(analyser.frequencyBinCount);
            processor.onaudioprocess = function (){
                 analyser.getByteFrequencyData(data);
                 dataSource = $.map(data, function (item, index) {
                    return { arg: index, val: item };
                 });
                 chart.option("dataSource", dataSource);
            }
        },
        function (error) {
            //error processing
        }
    );
    

    Вот здесь можно найти готовый пример. При желании можно собрать пример локально, или даже заменить визуализацию на любую другую понравившуюся. Главное — это насколько быстро и просто с помощью Web Audio API и библиотеки DevExtreme собирается подобный пример.

    Как всегда, жду ваши замечания и предложения в комментариях к статье. Всем хорошего дня!
    Developer Soft
    74,00
    Компания
    Поделиться публикацией

    Комментарии 15

      0
      Сравнил со спектроанализатором из Аблетона, не сильно похоже — http://screencast.com/t/sw9kEutqs4
        0
        как вы проводили эксперимент?
          0
          1. Открыл ссылку из статьи и разрешил хрому использовать микрофон. Появилось отображение сигнала.
          2. Открыл аблетон, кинул на аудио дорожку эквалайзер. На вход дорожки назначил тот же микрофон.
          3. Пододвинул аблетон так, чтобы было видно и его эквалайзер и отображение на сайте одновременно, сделал скриншот.
            0
            Хм… просто я думал что доступ к микрофону всегда монопольный.
              +1
              В Лисе проверить не могу — очень тормозит.
                0
                Согласен с вами, я быстренько включил профайлер, чтобы успеть заснять, где мой браузер проседает, оказалось, что смена источника данных — дорогая операция, особенно в условиях реального времени.
                Надо посмотреть почему, возможно очень много данных. Вот скриншот (убрал под спойлер)

                Profiling
                image
              +1
              Ради интереса, а "мозильный" пример совпадает? Вот ссылка — https://mdn.github.io/voice-change-o-matic/
                0
                мозильный просит закрыть приложение так как "приложение не отвечает" :-)
          +6
          Возможно, разница в шкалах: линейная/логарифмическая.
            +1
            У вас в Аблетоне логарифмический масштаб, поэтому графики так сильно различаются
              0
              Спасибо. Если есть время/желание, моежт объясните практическое применение линейного графика. В Аблетоне я вижу какая частота звучит с какой громкостью, а что можно увидеть из линейного графика? Быстрое гугление не дало исчерпывающего ответа.
                +2
                Логарифмический масштаб позволяет наблюдать более детально низкочастотную часть спектра.

                Помогает в анализе частотных характеристик голоса.
                Спектр звука [А]
                Сверху линейный масштаб, а снизу логарифмический.image


                И ещё много для чего.
                  0
                  Извиняюсь не так прочитал вопрос.
                  Линейный масштаб просто показывает зависимость громкости от частоты. Тот же график, что и при логарифмическом масштабе, только немного в другом формате.
                  При анализе звука иногда нужно анализировать весь спектр равномерно.
                  Выбор масштаба зависит от задачи.
              +1
              А ещё можно визуализировать через WebAudio амплитудно-временной график.

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

              Самое читаемое