
Доброго времени суток, друзья!
Хочу поделиться опытом работы с аудио. Под «аудио» я подразумеваю 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(); }
Результат можно посмотреть здесь.
Благодарю за внимание.