Как стать автором
Обновить

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

Время на прочтение6 мин
Количество просмотров8.6K


Когда мне понадобилось для сайта одной радиопередачи наладить выкладку аудио архива, помимо админки нужен был еще и аудиоплеер. Радиопередача шла 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 лет из трансцендентного сюда заглядывал только мойщик окон, а из имманентного — бухгалтера с безумными глазами, которые стучат по всем дверям на этаже в поисках кого-либо, кто объяснит как им подписать платежку через безумный плагин интернет-банка из-за очередного обновления браузера.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 20: ↑19 и ↓1+18
Комментарии19

Публикации

Истории

Работа

PHP программист
148 вакансий
React разработчик
60 вакансий

Ближайшие события