Работаем с аудио: загрузка файлов, звук, прогресс, визуализация

—

от автора


Доброго времени суток, друзья!

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

Результат можно посмотреть здесь.

На этом у меня все. Благодарю за внимание. До новых встреч.

ссылка на оригинал статьи https://habr.com/ru/post/487574/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *