Перейти к содержимому

Аудиовизуализатор

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

Доступ к аудиоданным SignalRGB в коде осуществляется через несколько свойств:

  • engine.audio.level — возвращает число от -100 до 0, представляющее общую громкость трека. 0 — громко, -100 — очень тихо.
  • engine.audio.density — возвращает число от 0 до 1, представляющее «чистоту» тона: цифровые тоны близки к 0, белый шум — к 1.
  • engine.audio.freq — возвращает массив из 200 элементов с частотными данными трека.

Каждое свойство потребует различного уровня нормализации или корректировки перед использованием.

Начнём с базовой анимации частот.

Частота представляет тональность воспринимаемого звука и является наиболее важным свойством для аудиовизуализаторов. Здесь берётся 200 срезов частотной волны за каждый кадр и преобразуется в визуальную форму. Основной процесс:

  • Создать массив и заполнить его частотными данными.
  • Отредактировать массив под свои нужды (фильтрация, отображение, сокращение и т. д.).
  • Создать класс «звуковой полосы» для представления каждого элемента.
  • Связывать данные с классом звуковой полосы при каждом кадре.

Важный момент с частотой: необходимо сделать две корректировки необработанных данных. Иногда элемент может иметь отрицательное значение, что визуально неприятно. Высота элементов также поступает некорректно для данного примера. Поскольку положительные значения в параметре «height» прямоугольника рисуются вниз от начала координат фигуры, нужно инвертировать их, чтобы получить вид типичного визуализатора.

Пример — необработанные данные:

Обработанные данные:

Пример кода с обработанными данными:

<head>
<title>Visualizer Tutorial</title>
<meta description="Basic Effects" />
<meta publisher="SignalRgb" />
</head>
<body style="margin: 0; padding: 0; background: #000;">
<canvas id="exCanvas" width="320" height="200"></canvas>
</body>
<script>
canvas = document.getElementById('exCanvas');
ctx = canvas.getContext('2d');
var effects = [];
var reducedFreq = [];
function update() {
// "frequency" represents the full 200 elements from our frequency data
var frequency = new Int8Array(engine.audio.freq)
// "reducedFreq" filters the data down to every fourth result to save some CPU load
reducedFreq = frequency.filter((element, index) => {
return index % 4 === 0;
})
// Create the effect if it does not yet exist
if(effects.length < 1){
effects.push(new soundBars(20, 100))
}
// Background color
DrawRect(0, 0, 320, 200, "black")
// Play effect
effects.forEach((ele, i) => {
ele.draw();
if (ele.lifetime <= 0) {
effects.splice(i, 1);
}
});
window.requestAnimationFrame(update);
}
function soundBars (x, y){
this.x = x;
this.y = y;
this.draw = function(){
for(let i = 0; i < reducedFreq.length; i++){
var x = this.x + 5 * i
var y = this.y
// Data processing occurs for "height". Find the absolute value of each element, then flip negative
var height = -Math.abs(reducedFreq[i])
DrawRect(x, y, 5, height, "white")
}
}
}
function DrawRect(x, y, width, height, color) {
ctx.beginPath();
ctx.fillStyle = color;
ctx.fillRect(x, y, width, height);
};
window.requestAnimationFrame(update);
</script>

С данным визуализатором всё ещё есть проблемы. Хотя музыка звучит сбалансированно через наушники, видно, что данные значительно предпочитают одни звуковые полосы и часто делают другие полностью невидимыми. С художественной точки зрения это не идеально, поэтому нужно «нормализовать» данные от SignalRGB. Нормализация предполагает равномерное распределение данных между максимальным и минимальным значениями. Определив максимум и минимум в массиве частот, уравнение простое: (x - min) / (max - min), где «x» — текущий элемент.

Нормализованные обработанные данные:

Теперь добавим эффектности, расположив звуковые полосы по кругу:

function soundBars (x, y){
this.x = x;
this.y = y;
this.draw = function(){
var max = Math.max(...reducedFreq)
var min = Math.min(...reducedFreq)
for(let i = 0; i < reducedFreq.length; i++){
//Save the current state of the canvas
ctx.save()
//Add the circular component to your bars. "50" here is the radius of the circle
var x = this.x + Math.cos(i) * 50
var y = this.y + Math.sin(i) * 50
var height = ((Math.abs(reducedFreq[i])) - min) / (max - min) * -40
// Determine the angle of the bar, to make it perpindicular to the circle
var rotate = Math.atan2(y - this.y, x - this.x) + Math.PI / 2;
//Translate to circle center
ctx.translate(x, y)
//Rotate the canvas
ctx.rotate(rotate)
//Translate back for drawing
ctx.translate(-x, -y)
DrawRect(x, y, 5, height, "white")
//Restore the old state of the canvas after drawing to prevent positive feedback loops
ctx.restore()
}
}
}

Плотность проста в использовании. Возвращаемое значение — число от 0 до 1, представляющее «чистоту» тона. Цифровые тоны будут ближе к 0, аналоговые — к 1. В этом примере она будет редактировать цвет звуковых полос, придавая различным тонам в музыке различную окраску.

function soundBars (x, y){
this.x = x;
this.y = y;
this.rotate = 0;
this.draw = function(){
//Hue is calculated each frame, the resulting amount will be a proper hue option for hsl
var hue = engine.audio.density * 360
var max = Math.max(...reducedFreq)
var min = Math.min(...reducedFreq)
for(let i = 0; i < reducedFreq.length; i++){
ctx.save()
var x = this.x + Math.cos(i) * 50
var y = this.y + Math.sin(i) * 50
//Height slightly edited for visability
var height = ((Math.abs(reducedFreq[i])) - min) / (max - min) * -50 - 5
this.rotate = Math.atan2(y - this.y, x - this.x) + Math.PI / 2;
ctx.translate(x, y)
ctx.rotate(this.rotate)
ctx.translate(-x, -y)
//Insert the edited hue value each frame
DrawRect(x, y, 5, height, `hsl(${hue}, 100%, 50%)`)
this.rotate = 0;
ctx.restore()
}
}
}

Это свойство просто возвращает громкость трека в децибелах, но с особенностью: числа находятся в диапазоне от -100 до 0. -100 — очень тихо, 0 — очень громко. Потребуется небольшая корректировка данных. В этом примере уровень трека будет редактировать яркость внутреннего чёрного круга. Фигура рисуется в функции update.

function update() {
var frequency = new Int8Array(engine.audio.freq)
reducedFreq = frequency.filter((element, index) => {
return index % 4 === 0;
})
if(effects.length < 1){
effects.push(new soundBars(160, 100))
}
DrawRect(0, 0, 320, 200, "black")
effects.forEach((ele, i) => {
ele.draw();
if (ele.lifetime <= 0) {
effects.splice(i, 1);
}
});
// Hue calcualtion to match the rest of the visualizer
var hue = engine.audio.density * 360
//The level has been multiplied by 10, then added to 100. This is arbitrary, but some combination of actions here will give you the result you want
DrawCircle(160, 100, 50, `hsl(${hue}, 100%, ${100 + engine.audio.level * 10}%)`)
window.requestAnimationFrame(update);
}