Адаптивный Waveform для вашего аудиосервиса

nwk4n3zrjoklldqz2vfmpq25nzs.gif

Когда мне понадобилось для сайта одной радиопередачи наладить выкладку аудио архива, помимо админки нужен был еще и аудиоплеер. Радиопередача шла 40 минут плюс две музыкальные паузы. Использовать Waveform в таких длинных форматах особенно удобно, поэтому как и многие музыкальные сервисы, я решил использовать это решение в оформлении плеера.

При планируемом будущем редизайне сайта и, возможно, будущих мобильных приложений, растровый waveform тут просто клином упирался. Он не адаптивен, его крайне ресурсоемко редизайнить, если он в растре.
Всем известный SOUNDCLOUD решил этот вопрос на маленьких экранах двиганием всей waveform относительно статического центра. Но я так не хочу.

Заливка радиопередач осуществлялась через админку, и я сразу делал более сжатые копии аудиофайлов через ffmpeg. Было бы глупо отказываться от его возможностей и по генерации waveform.

Алгоритм действий:


1. Генерация waveform в минимальном размере для хранения
2. Перевод в вектор (JSON)
3. Отрисовка плеера по этому массиву
4. Реализация адаптивности: равномерное сокращение массива и возврат к п.3

Генерация waveform


Каким размером выбрать итоговый растровый файл? Если мы возьмем мой дизайн плеера (он здесь уменьшен по ширине), то увидим, что на одну полоску приходится 2 пикселя (плюс 1 пиксель разделитель). Это значит, что 600 px даст нам 1200 px по ширине.

p9dkwfisywv4-irpqxeyteuqpvw.jpeg

Предполагаю, что в будущем крайне маловероятным будет необходимость в большем представлении аудиофайла. Ну если только не тянуть по дизайну на всю ширину 4К монитора, стоит об этом подумать, но останавливаюсь на размере 600×60 px.

А теперь ближе к коду:

shell_exec("ffmpeg -y -i '$name.mp3' -filter_complex 'aformat=channel_layouts=mono,compand,showwavespic=s=600x120,crop=in_w:in_h/2:0:0' -c:v png -pix_fmt pal8 -frames:v 1 '$png_path.png'   > /dev/null 2>/dev/null &");


-filter_complex — подключить фильтры

aformat — работа со звуком

channel_layouts

-mono — режим моно

-compand — это компрессор и экспандер. В этом режиме и тихие и громкие звуки будут выравнены по громкости, что позволяет получать waveform без пиков и перегрузок как на тихих так и на громких записях. Форма волны как бы всегда растянута до максимума.

-showwavespic=s=600×120 — s принимает размер изображения.

-crop=in_w: in_h/2:0:0 — обрезка полученного изображения. Как правило, выходная АЧХ зеркально отображается вокруг оси x. Поэтому мы кропаем, оставляя только верхушку «айсберга».

-c: v png -pix_fmt pal8 -frames: v 1 — формат выходного изображения, цветовая палитра и только первый фрейм (анимация нам не нужна). png8 отлично подходит по качеству (lossless в нашем случае)/месту.

> /dev/null 2>/dev/null & слать выходные и рабочие данные в пропасть. А '&' позволяет php не дожидаться завершения работы консоли, а продолжать дальше.

На выходе мы получаем вот такое изображение:

4fees6nkctphgo1oaxk6j9es7s0.png
Размер итогового файла 2.4 кб

Забавно то, что пару лет назад вместо белого был красный цвет. Разработчики, видимо, поменяли дефолтные значения.

Перевод waveform в вектор


Полученное изображение — это амплитуда по Y и время по X. Ее элементарно перевести в одномерный массив JSON. Где значения будут выступать в роли значений амплитуды, а время — просто их порядковый индекс.

Перевод я решил делать на лету, уж довольно быстро он делается.
Замеряем количество пикселей по Y сверху до первого другого, и переходим к следующему пикселю по X.

 
$a = imagecreatefrompng("test.png");
$i = 0;
$h = '60';
// horizontal movener
while ( $i < 600 ) {

    // vertical movener
    $y  = $h-1;
    $c = 0;
    while ( $c < $h ) {
        //echo imagecolorat($aa, $i, $c ); // test color
        if(imagecolorat($a, $i, $c ) == "255") {
            $arr[$i] =  $c;
            break;
        } else {
            $arr[$i] =  $y;
        }
        $c++;
    } 
    $i++;
};

echo json_encode($arr);


Итоговый массив состоит из 600 значений.

[46,28,34,35,34,35,26,33,39,29,29,30,30,30,33,33,28...]

Отрисовка плеера по JSON


Для удобной работы прогресс бара, я взял либу progressor.js у Elliot Bentley. Он ее сделал для сервиса аудио транскрипций.

github.com/ejb/progressor.js 2.76 KB

Взглянем еще раз на наш плеер.
p9dkwfisywv4-irpqxeyteuqpvw.jpeg

Прогресс бар состоит из двух слоев: фон с серыми столбиками и с зелеными.

Ниже изображения отрисовываются функцией getGraph.
Смысл ее в том, чтобы рисовать столбики нужной толщины и цвета со столбиками разделителями.

var c    = document.createElement("canvas");
c.width  = width;
c.height = height;
var ctx  = c.getContext("2d");

function getGraph(fillStyle1,fillStyle2,fillStyle3) {
                
        if (fillStyle3) {
                //console.log(fillStyle1);
                var grd = ctx.createLinearGradient(0,120,0,0);
                grd.addColorStop(0.5,fillStyle1);
                grd.addColorStop(1,fillStyle2);
                fillStyle1 = grd;
                fillStyle2 = fillStyle3;
        }
        
        json.forEach(function(item, i, arr) {
                  ctx.fillStyle = fillStyle1;
                  ctx.fillRect(i * 3, height, 2, item - height);
                  ctx.fillStyle = fillStyle2;

                        var next = json[i + 1];

                        if( item <= next ) {
                                h2 = next;
                        } else {
                                h2 = item;
                        }               
         
                  ctx.fillRect(i * 3 + 2, height, 1, h2 - height);

        });

        return c.toDataURL();
}

Вот так выглядит рабочий пример без адаптивности
codepen.io/Alexufo/pen/rrjBVx

4. Реализация адаптивности


Теперь нам нужносократить массив JSON на клиенте до нужного размера и вот тебе адаптивность.

План А


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

Модификация waveform через удаление значений массива — тупиковый путь. Когда вы это сделаете, то увидите на сколько форма волны становится обезличенно рваной, потому что вы выкидываете экстремумы и не усредняете соседей по высоте.

Нам нужны алгоритмы ресемплинга. Есть на js реализация алгоритма:

largestTriangleThreeBuckets
flot.base.is/jquery.flot.downsample.js

Работает она хорошо, только просит на вход такой массив, по индексам которого она получит координаты X.Y. У нас массив одномерный, поэтому пришлось чутка покумекать и переделать функцию. Работает это дело вот так:

mfxxwtrk4jf5nddwlkqpmzu3__e.gif

А здесь можно потрогать с адаптивкой как КДПВ:
codepen.io/Alexufo/pen/NNGKGM
Переведите режим просмотра, где фрейм с html будет справа. Тогда можно менять ширину этого окошка.

План Б — пых


Однако, мне все таки не хотелось бы нагружать клиентскую часть. К примеру, я хочу 1000 точек-5000, да на всю ширину экрана. Если у меня будет больше точек, как поведет себя это дело на мобиле? С одной стороны, в этом совершенно нет проблем, это не так вроде бы и накладно если судить по демкам алгоритма, он жует 5000 точек легко. Но с другой стороны — давать надо столько, сколько спрашивают. Вопрос дизайна.

Элементарно, если у вас Node.Js вы можете этот код перенести на сервер. А если у вас php, вы можете найти реализацию этого алгоритма на php но… зачем, подумал я.

Где же алгоритмы ресемплинга? В той же нативной либе GD, которую мы использовали для генерации JSON. Мы просто передаем с клиента параметр в пикселях требуемой ширины и ресайзим нашу waveform перед переводом в JSON.

Поэтому расширю код, написанный в начале.


$h = 60;
$width_new = 600;

$a = imagecreatefrompng("$id.png");
$width_old = imagesx($a); 
$aa = imagecreatetruecolor($width_new, $h); 


imagecopyresized($aa, $a, 0, 0, 0, 0, $width_new, $h, $width_old, $h);
imagetruecolortopalette($aa, false, 2);

$i = 0;
// horizontal movener
while ( $i < $width_new ) {
        // vertical movener
        $y  = $h-1;
        $c = 0;
        while ( $c < $h ){
                //echo imagecolorat($aa, $i, $c ); // search what color is needed
                if(imagecolorat($aa, $i, $c ) == "1"){
                        $arr[$i] =  $c;
                        break;
                } else {
                        $arr[$i] =  $y;
                }
                $c++;
        } 
        $i++;
};

echo json_encode($arr);

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

Код лежит тут:
https://github.com/Alexufo/Responsive-waveform/

.
Пасхалка.
Наверное, это был солнечный день. Окно нашей комнаты выходило на две старые кирпичные 9-ти этажки, которые я помню еще подростком, знаю, что за ними открывается трамвайное кольцо, чуть дальше — старая больница, она сразу за школой, а текущее здание с офисом, где я пытаюсь находиться копаясь в воспоминаниях, это бывшая недостроенная больница, теперь уже чисто офисное помещение. Помню как в детстве здесь тренировались спецназовцы, их показывали по телевизору, бодро штурмующих бетонное сооружение, поросшее вокруг всем, чем только можно. А теперь, оказывается, я бодро бьюсь током о блестящие перила, спускаясь по лестнице, и любуюсь формой искажений этого здания в отражении ближайшего жилого комплекса.(Совсем рядом, по трамвайной линии открывается стена старого большого кладбища. И на ней надпись зеленой краской «Пока Борис у власти» и «Трудовая Россия». Черт знает кто и когда их сделал, но по прошествии пары десятков лет они все так же читаются, но остаются совершенно невидимыми. Я не видел больше из наследия 90-ых более древнего памятника в городе.)

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

© Habrahabr.ru