Доброго времени суток, друзья!
Хочу поделиться опытом работы с аудио. Под «аудио» я подразумеваю HTMLAudioElement и Web Audio API.
Что будем делать?
Мы создадим нечто вроде плеера для одного трека (о полноценном проигрывателе — в одной из следующих статей).
Условия:
- Возможность загрузки файла из любого места на жестком диске как по нажатию кнопки, так и перетаскиванием.
- Круговой графический и текстовый индикаторы прогресса.
- Текстовый индикатор громкости звука.
- Визуализация аудио данных.
- Управление плеером с помощью клавиатуры.
В сети полно материалов как по HTMLAudioElement, так и по WAAPI, поэтому я сделаю акцент на практической составляющей. Кроме аудио, мы будем работать с drag-drop и canvas.
Без дальнейших предисловий…
Да, чуть не забыл: за основу «визуалайзера» взял работу одного из хабровчан. Не могу найти его по поиску. Буду признателен за ссылочку.
Вот как выглядит наша разметка:
<p>click or drag</p>
<div dropzone>
<img src="https://thebestcode.ru/media/audioProgress&Visualizer/plus.png" alt="#">
<input type="file" accept="audio/*">
</div>
<canvas></canvas>
У нас есть подсказка (параграф), кнопка (картинка + «инпут» с атрибутом accept в контейнере с атрибутом dropzone)
и холст.
Как видим, ничего необычного.
В стилях также ничего сверхъестественного.
CSS:
@font-face {
font-family: "Nova Mono", monospace;
src: url("https://thebestcode.ru/media/audioProgress&Visualizer/font.ttf");
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height: 100vh;
background: radial-gradient(circle, #666, #222);
display: flex;
justify-content: center;
align-items: center;
}
p {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -70px);
color: #ddd;
text-align: center;
text-transform: uppercase;
font-family: "Nova Mono", monospace;
font-size: 0.8em;
font-weight: bold;
letter-spacing: 2px;
user-select: none;
}
span {
display: block;
font-size: 1.6em;
}
div {
width: 100px;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
border: 1px dashed #ddd;
border-radius: 10%;
cursor: pointer;
}
img {
width: 70px;
height: 70px;
filter: invert();
}
input {
display: none;
}
canvas {
display: none;
}
Переходим к JS.
Объявляем основные переменные:
let dropZone = document.querySelector("div"),
input = document.querySelector("input"),
file,
text,
progress,
volume,
audio,
// массив для частот
frequencyArray;
Работаем с drag-drop:
// "сбрасывание" элемента
dropZone.ondrop = e => {
// отключаем поведение браузера по умолчанию
e.preventDefault();
// осуществляем проверку
if (e.dataTransfer.items[0].kind == "file") {
// получаем файл
file = e.dataTransfer.items[0].getAsFile();
} else return;
// передаем файл
playTrack(file);
};
// элемент над "зоной"
dropZone.ondragover = e => {
// отключаем поведение браузера по умолчанию
e.preventDefault();
};
// клик по зоне
dropZone.onclick = () => {
// кликаем по инпуту
input.click();
// при изменении инпута
input.onchange = () => {
// получаем файл
file = input.files[0];
// передаем файл
playTrack(file);
};
};
Двигаемся дальше.
Объявляем переменные для холста:
let C = document.querySelector("canvas"),
$ = C.getContext("2d"),
W = (C.width = innerWidth),
H = (C.height = innerHeight),
centerX = W / 2,
centerY = H / 2,
radius,
// эту переменную мы будем использовать для определения текущего прогресса
piece,
// количество колонок
bars = 200,
x,
y,
xEnd,
yEnd,
// ширина колонки
barWidth = 2,
// высота колонки
barHeight,
// цвет колонки
lineColor;
Приступаем к основной функции (весь наш дальнейший код будет находиться в этой функции):
function playTrack(file) {
// код плеера
}
Убираем зону (она нам больше не нужна), меняем текст параграфа, инициализируем переменные для звука и прогресса:
dropZone.style.display = "none";
text = document.querySelector("p");
text.style.transform = "translate(-50%,-50%)";
text.innerHTML = `progress: <span class="progress"></span> <br> volume: <span class="volume"></span>`;
volume = document.querySelector(".volume");
progress = document.querySelector(".progress");
Колдуем со звуком:
audio = new Audio();
// аудио контекст представляет собой объект, состоящий из аудио модулей
// он управляет созданием узлов и выполняет обработку (декодирование) аудио данных
context = new AudioContext();
// анализатор представляет собой узел, содержащий актуальную (т.е. постоянно обновляющуюся) информацию о частотах и времени воспроизведения
// он используется для анализа и визуализации аудио данных
analyser = context.createAnalyser();
// метод URL.createObjectURL() создает DOMString, содержащий URL с указанием на объект, заданный как параметр
// он позволяет загружать файлы из любого места на жестком диске
// время жизни URL - сессия браузера
audio.src = URL.createObjectURL(file);
// определяем источник звука
source = context.createMediaElementSource(audio);
// подключаем к источнику звука анализатор
source.connect(analyser);
// подключаем к анализатору "выход" звука - акустическая система устройства
analyser.connect(context.destination);
// получаем так называемый байтовый массив без знака на основе длины буфера
// данный массив содержит информацию о частотах
/*let bufferLength = analyser.frequencyBinCount;
let frequencyArray = new Uint8Array(bufferLength);*/
frequencyArray = new Uint8Array(analyser.frequencyBinCount);
// запускаем воспроизведение
audio.play();
// включаем повтор воспроизведения
audio.loop = true;
Добавляем возможность управления плеером (у нас нет кнопок, поэтому управлять плеером можно только с помощью клавиатуры):
document.addEventListener("keydown", e => {
// необходимость использования try/catch обусловлена странным поведением Chrome, связанным с вычислением громкости звука
// попробуйте убрать try/catch и выводить в консоль громкость звука (console.log(audio.volume)) после каждого изменения
// при приближении к 0 и 1 (согласно спецификации значение громкости звука варьируется от 0 до 1) получаем странные значения, которые нивелируют проверки типа if(audio.volume>0 && audio.volume<1)
// использование в проверках "неточных" значений вроде 0.1 и 0.9 решает проблему исключений, но приводит к некорректному изменению громкости звука
// исключения работе плеера не мешают, но раздражают
try {
// отключаем стандартный функционал клавиатуры
e.preventDefault()
// пробел
if (e.keyCode == 32) {
// пуск/пауза
audio.paused ? audio.play() : audio.pause();
// enter
} else if (e.keyCode == 13) {
// стоп
audio.load();
// стрелка вправо
} else if (e.keyCode == 39) {
// время воспроизведения + 10 секунд
audio.currentTime += 10;
// стрелка влево
} else if (e.keyCode == 37) {
// время воспроизведения - 10 секунд
audio.currentTime -= 10;
// стрелка вниз
} else if (e.keyCode == 40) {
// громкость звука - 10%
audio.volume -= 0.1;
// стрелка вверх
} else if (e.keyCode == 38) {
// громкость звука + 10%
audio.volume += 0.1;
}
// скрываем исключения
} catch {
return;
}
});
// добавляем подсказку
console.log(
" Use Keyboard: \n Space to Play/Pause \n Enter to Stop \n Arrows to Change \n Time and Volume"
);
Следующая часть — анимация. Вызываем соответствующую функцию:
startAnimation();
Сама функция выглядит следующим образом:
function startAnimation() {
// включаем холст
C.style.display = "block";
// определяем текущий прогресс (текущее время воспроизведения / продолжительность трека)
piece = audio.currentTime / audio.duration;
// устанавливаем радиус круга
// мы будем использовать два радиуса: один для прогресса, другой для визуализации частот
radius = 105;
// очищаем холст
$.clearRect(0, 0, W, H);
// рисуем круговой прогресс
$.beginPath();
$.arc(centerX, centerY, radius, 0, Math.PI * (2 * piece));
$.lineWidth = 30;
$.stroke();
// выводим значение громкости
volume.innerText = Math.trunc(audio.volume * 100) + "%";
// выводим значение прогресса
progress.innerText = Math.trunc(piece * 100) + "%";
// копируем данные о частотах в frequencyArray
analyser.getByteFrequencyData(frequencyArray);
// делаем итерацию по количеству колонок
for (let i = 0; i < bars; i++) {
// увеличиваем радиус
radius = 120;
// переводим количество колонок в радианы
rads = Math.PI * 2 / bars;
// определяем высоту колонок
barHeight = frequencyArray[i] * 0.6;
// двигаемся от 0 по часовой стрелке
x = centerX + Math.cos(rads * i) * radius;
y = centerY + Math.sin(rads * i) * radius;
xEnd = centerX + Math.cos(rads * i) * (radius + barHeight);
yEnd = centerY + Math.sin(rads * i) * (radius + barHeight);
// рисуем колонки
drawBar(x, y, xEnd, yEnd, barWidth, frequencyArray[i]);
}
// зацикливаем анимацию
requestAnimationFrame(startAnimation);
}
И в завершение — рендеринг колонок:
// имена переменных можно было не менять, но очень хотелось
function drawBar(x1, y1, x2, y2, width, frequency) {
// цвет колонок меняется от светло-голубого до темно-синего
lineColor = "rgb(" + frequency + ", " + frequency + ", " + 205 + ")";
// рисуем линии
$.strokeStyle = lineColor;
$.lineWidth = width;
$.beginPath();
$.moveTo(x1, y1);
$.lineTo(x2, y2);
$.stroke();
}
Результат можно посмотреть здесь.
Благодарю за внимание.