Когда мне понадобилось для сайта одной радиопередачи наладить выкладку аудио архива, помимо админки нужен был еще и аудиоплеер. Радиопередача шла 40 минут плюс две музыкальные паузы. Использовать Waveform в таких длинных форматах особенно удобно, поэтому как и многие музыкальные сервисы, я решил использовать это решение в оформлении плеера.
При планируемом будущем редизайне сайта и, возможно, будущих мобильных приложений, растровый waveform тут просто клином упирался. Он не адаптивен, его крайне ресурсоемко редизайнить, если он в растре.
Всем известный SOUNDCLOUD решил этот вопрос на маленьких экранах двиганием всей waveform относительно статического центра. Но я так не хочу.
Заливка радиопередач осуществлялась через админку, и я сразу делал более сжатые копии аудиофайлов через ffmpeg. Было бы глупо отказываться от его возможностей и по генерации waveform.
Алгоритм действий:
1. Генерация waveform в минимальном размере для хранения
2. Перевод в вектор (JSON)
3. Отрисовка плеера по этому массиву
4. Реализация адаптивности: равномерное сокращение массива и возврат к п.3
Генерация waveform
На момент реализации этого подхода, товарищи из BBC еще не релизили в своей утилите вывод в JSON, на сколько я помню. А на текущий момент, я бы вам рекомендовал пересобрать их утилиту, чтобы убрать бесполезный вывод отрицательных чисел и доп. инфу о каналах битности и прочей ерунде.
А пока, продожим:
Если мы возьмем мой дизайн плеера (он здесь уменьшен по ширине), то увидим, что на одну полоску приходится 2 пикселя (плюс 1 пиксель разделитель). Это значит, что 600px даст нам 1200px по ширине.
Предполагаю, что в будущем крайне маловероятным будет необходимость в большем представлении аудиофайла. Ну если только не тянуть по дизайну на всю ширину 4К монитора, стоит об этом подумать, но останавливаюсь на размере 600x60px.
А теперь ближе к коду:
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 monob -frames:v 1 '$png_path.png' > /dev/null 2>/dev/null &");
-filter_complex — подключить фильтры
aformat — работа со звуком
channel_layouts
-mono — режим моно
-compand — это компрессор и экспандер. В этом режиме и тихие и громкие звуки будут выравнены по громкости, что позволяет получать waveform без пиков и перегрузок как на тихих так и на громких записях. Форма волны как бы всегда растянута до максимума.
-showwavespic=s=600x120 — s принимает размер изображения.
-crop=in_w:in_h/2:0:0 — обрезка полученного изображения. Как правило, выходная АЧХ зеркально отображается вокруг оси x. Поэтому мы кропаем, оставляя только верхушку «айсберга».
-c:v png -pix_fmt monob -frames:v 1 — формат выходного изображения, цветовая палитра чб и только первый фрейм (анимация нам не нужна). png8 отлично подходит по качеству(lossless в нашем случае)/месту.
> /dev/null 2>/dev/null & слать выходные и рабочие данные в пропасть. А '&' позволяет php не дожидаться завершения работы консоли, а продолжать дальше.
На выходе мы получаем вот такое изображение:
Размер итогового файла 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
Взглянем еще раз на наш плеер.
Прогресс бар состоит из двух слоев: фон с серыми столбиками и с зелеными.
Ниже изображения отрисовываются функцией 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();
}
Вот так выглядит рабочий пример без адаптивности
4. Реализация адаптивности
Теперь нам нужносократить массив JSON на клиенте до нужного размера и вот тебе адаптивность.
План А
Самый первый приходящий в голову способ, это убрать в цикле каждую вторую, третью, четв… подождите, таким образом меньше чем в два раза сократить массив нельзя, да и тут нельзя добиться пиксельной точности.
Модификация waveform через удаление значений массива — тупиковый путь. Когда вы это сделаете, то увидите на сколько форма волны становится обезличенно рваной, потому что вы выкидываете экстремумы и не усредняете соседей по высоте.
Нам нужны алгоритмы ресемплинга. Есть на js реализация алгоритма:
largestTriangleThreeBuckets
Работает она хорошо, только просит на вход такой массив, по индексам которого она получит координаты X.Y. У нас массив одномерный, поэтому пришлось чутка покумекать и переделать функцию. Работает это дело вот так:
А здесь можно потрогать с адаптивкой как КДПВ.
Переведите режим просмотра, где фрейм с 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);
После этого можно не переживать, если нужно будет поменять дизайн, ширину плеера, расширить в мобильное приложение. Все выглядит довольно гибким и очень шустрым.
Код лежит тут
.
Пасхалка.
Наверное, это был солнечный день. Окно нашей комнаты выходило на две старые кирпичные 9-ти этажки, которые я помню еще подростком, знаю, что за ними открывается трамвайное кольцо, чуть дальше — старая больница, она сразу за школой, а текущее здание с офисом, где я пытаюсь находиться копаясь в воспоминаниях, это бывшая недостроенная больница, теперь уже чисто офисное помещение. Помню как в детстве здесь тренировались спецназовцы, их показывали по телевизору, бодро штурмующих бетонное сооружение, поросшее вокруг всем, чем только можно. А теперь, оказывается, я бодро бьюсь током о блестящие перила, спускаясь по лестнице, и любуюсь формой искажений этого здания в отражении ближайшего жилого комплекса. (Совсем рядом, по трамвайной линии открывается стена старого большого кладбища. И на ней надпись зеленой краской «Пока Борис у власти» и «Трудовая Россия». Черт знает кто и когда их сделал, но по прошествии пары десятков лет они все так же читаются, но остаются совершенно невидимыми. Я не видел больше из наследия 90-ых более древнего памятника в городе.)
На нашем верхнем этаже пусто, как бывает пусто в начатом пакете с гречкой: внизу куча всего и плотно: какие-то крутачи из спецгеоразведки, офис 2gis, потом очередные сеошники, а сверху — почти нет зерен. Думаешь, вот должно же прорасти что-то сквозь этажи что-то сюда, но за эти 5 лет из трансцендентного сюда заглядывал только мойщик окон, а из имманентного — бухгалтера с безумными глазами, которые стучат по всем дверям на этаже в поисках кого-либо, кто объяснит как им подписать платежку через безумный плагин интернет-банка из-за очередного обновления браузера.
Наверное, это был солнечный день. Окно нашей комнаты выходило на две старые кирпичные 9-ти этажки, которые я помню еще подростком, знаю, что за ними открывается трамвайное кольцо, чуть дальше — старая больница, она сразу за школой, а текущее здание с офисом, где я пытаюсь находиться копаясь в воспоминаниях, это бывшая недостроенная больница, теперь уже чисто офисное помещение. Помню как в детстве здесь тренировались спецназовцы, их показывали по телевизору, бодро штурмующих бетонное сооружение, поросшее вокруг всем, чем только можно. А теперь, оказывается, я бодро бьюсь током о блестящие перила, спускаясь по лестнице, и любуюсь формой искажений этого здания в отражении ближайшего жилого комплекса. (Совсем рядом, по трамвайной линии открывается стена старого большого кладбища. И на ней надпись зеленой краской «Пока Борис у власти» и «Трудовая Россия». Черт знает кто и когда их сделал, но по прошествии пары десятков лет они все так же читаются, но остаются совершенно невидимыми. Я не видел больше из наследия 90-ых более древнего памятника в городе.)
На нашем верхнем этаже пусто, как бывает пусто в начатом пакете с гречкой: внизу куча всего и плотно: какие-то крутачи из спецгеоразведки, офис 2gis, потом очередные сеошники, а сверху — почти нет зерен. Думаешь, вот должно же прорасти что-то сквозь этажи что-то сюда, но за эти 5 лет из трансцендентного сюда заглядывал только мойщик окон, а из имманентного — бухгалтера с безумными глазами, которые стучат по всем дверям на этаже в поисках кого-либо, кто объяснит как им подписать платежку через безумный плагин интернет-банка из-за очередного обновления браузера.