Как работать со звуком в JS: собственный аудиоплеер с Web Audio API

Недавно мне довелось поработать со звуком для одного проекта. Моя задача состояла в том, чтобы создать и визуализировать собственный аудиоплеер с React.js и API веб-аудио. Мне пришлось углубиться в эту тему и теперь я хочу поделиться своими знаниями с вами.

Я начну с теории, а затем перейду к примерам из реальной жизни и практическим советам о том, как создавать, манипулировать и визуализировать звук с помощью JavaScript.

Природа звука

Прежде всего, давайте перейдем к основам и поговорим о том, что такое звук. В физике звук — это вибрация, которая обычно распространяется в виде слышимой волны давления через передающую среду, такую ​​как газ, жидкость или твердое тело. Если мы представим звук графически, он будет выглядеть как форма волны f

Звук

Следующий вопрос: как наши устройства воспроизводят эту волну? Для этой цели цифровое аудио — используется способ хранения звука в виде цифрового сигнала. Поскольку звук представляет собой точку в определенный момент, эти моменты можно выбрать и сохранить в образцы (числовые значения точек данных осциллограммы в определенные моменты времени)

звуковая природа

Каждый образец представляет собой набор битов (со значением 0 или 1). Обычно используются 16 или 24 бита. Количество выборок в секунду определяется частотой дискретизации (частотой выборки), измеряемой в герцах. Чем выше частота дискретизации — тем более высокие частоты может содержать звуковой сигнал.

В двух словах звук можно представить как большой массив звуковых колебаний (как в укусах, так и в числовых значениях -N<0>N после расшифровки). Например, [0, -0.018, 0.028, 0.27, … 0.1]. Длина массива зависит от частоты дискретизации. Например, при частоте дискретизации 44400 длина этого массива составляет 44400 элементов на 1 секунду записи.

Как звук сохраняется на устройствах

Теперь, когда вы знаете теорию звуковой волны, давайте посмотрим, как она сохраняется на устройстве. С этой целью формат аудиофайла используется. Каждый аудиофайл состоит из 2 частей: данных и заголовка.

Данные — это наша звуковая волна, массив данных, также известный как .raw формат. Заголовок — это дополнительная информация для расшифровки наших данных. Содержит информацию о частоте дискретизации, количестве каналов записи, авторе альбома, дате записи и т.д.

звуковое хранилище

Разница между .wav а также .mp3 в том, что mp3 является сжатым форматом.

Теперь приступим к практике. Я использовал Express+React для всех примеров, однако основные подходы, которые я упомянул, не привязаны к какому-либо конкретному фреймворку.

Как загрузить звук с сервера

Прежде всего, вам нужно получить файл, с которым вы будете работать. Вы можете получить его либо с клиента, либо с сервера. Для клиента можно использовать элемент ввода файла. Узнайте, как загрузить файл с сервера с помощью Express.js. Вы можете прочитать весь код здесь.

Все примеры для этой статьи сохранены здесь. Код этих примеров хранится в хранилище

...
const express = require('express');
const app = express();
const api = express(); api.get('/track', (req, res, err) => { // generate file path const filePath = path.resolve(__dirname, './private', './track.wav'); // get file size info const stat = fileSystem.statSync(filePath); // set response header info res.writeHead(200, { 'Content-Type': 'audio/mpeg', 'Content-Length': stat.size }); //create read stream const readStream = fileSystem.createReadStream(filePath); // attach this stream with response stream readStream.pipe(res);
}); //register api calls
app.use('/api/v1/', api); const server = http.createServer(app);
server.listen('3001', () => console.log('Server app listening on port 3001!'));

В общем, есть 3 основных шага: чтение файла и информации о нем, превращение аудио/мпега в ответ, загрузка файла. Используя этот подход, вы можете загружать любые файлы. Все, что вам нужно, это сделать запрос по адресу api/v1/track.

Как работать со звуком на клиенте

Теперь, когда вы знаете, как загружать файлы с сервера, следующим шагом будет получение нашего файла на клиенте. Если мы просто сделаем получить запрос в браузере, мы получим наш файл. Однако мы хотим как-то использовать его на нашей странице. Самый простой способ сделать это — использовать аудио элемент следующим образом. См. пример здесь.

<audio controls> <source src="/api/v1/track" type="audio/mpeg" />
</audio>

Как работать со звуком в фоновом режиме

Здорово, что API браузера дает нам такие простые элементы из коробки. Однако было бы неплохо иметь больший контроль над звуком внутри нашего кода. Узнайте, как создать собственное аудио, например Вот этот.

Это когда API веб-аудио — на помощь приходит набор инструментов для работы со звуком в браузере. С чего начать? Начнем с ответов на несколько вопросов.

Как воспроизвести аудиофайл

Прежде всего, вам нужно загрузить его с сервера. Для этой цели можно использовать принести метод или другие библиотеки (например, я использую аксиомы).

const response = await axios.get(url, { responseType: 'arraybuffer', // <- important param
});

Примечание. Тип responseType: 'arraybuffer' в заголовок, чтобы браузер знал, что он загружает buffer но нет json

Для воспроизведения файла необходимо создать аудиоконтекст учебный класс.

const getAudioContext = () => { AudioContext = window.AudioContext || window.webkitAudioContext; const audioContent = new AudioContext(); return audioContent;
};

Здесь важно помнить. Некоторые браузеры позволяют использовать Audio Context только после взаимодействия пользователя со страницей. Если пользователь не будет совершать никаких действий на странице, произойдет ошибка. Вот почему вам нужно getAudioContext.

Для воспроизведения файла необходимо создать BufferSource. Вы можете использовать метод создать источник буфера в аудиоконтексте.

После этого, BufferSource требует audioBuffer. Мы можем взять его из нашего файла с помощью декодировать аудиоданные метод. Вот как все это будет выглядеть:

// load audio file from server
const response = await axios.get(url, { responseType: 'arraybuffer',
});
// create audio context
const audioContext = getAudioContext();
// create audioBuffer (decode audio file)
const audioBuffer = await audioContext.decodeAudioData(response.data); // create audio source
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination); // play audio
source.start();

После этого вам останется только позвонить source.start() метод.

Как остановить воспроизведение

Чтобы остановить воспроизведение, просто позвоните source.stop() метод. Вам также необходимо сохранить время, когда вы нажали кнопку остановки. Это пригодится, если вы воспроизведете звук из паузы. В этом случае необходимо позвонить source.start() уже с параметром.

// start play
let startedAt = Date.now();
let pausedAt = null;
source.start(); // stop play
source.stop();
pausedAt = Date.now() - startedAt; // resume from where we stop
source.start();
startedAt = Date.now() - pausedAt;
source.start(0, audionState.pausedAt / 1000);

Как отобразить процесс воспроизведения

Здесь вы можете выбрать два подхода. Один из них заключается в использовании метода createScriptProcessor и его обратный вызов onaudioprocess.

const audioBuffer = await audioContext.decodeAudioData(response.data);
// create progress source
const scriptNode = audioContext.createScriptProcessor(4096, audioBuffer.numberOfChannels, audioBuffer.numberOfChannels);
scriptNode.connect(audioContext.destination);
scriptNode.onaudioprocess = (e) => { const rate = parseInt((e.playbackTime * 100) / audioBuffer.duration, 10);
};

Чтобы увидеть процент воспроизведенного аудио, вам нужны две вещи: — продолжительность песни audioBuffer.duration и текущий e.playbackTime.

Одним из недостатков является то, что при вызове source.stop(), вам нужно аннулировать этот обратный вызов. Другой подход — экономить время воспроизведения и запускать обновление каждую секунду.

const audioBuffer = await audioContext.decodeAudioData(response.data);
...
const startedAt = Date.now();
const duration = audioBuffer.duration;
source.start(); setInterval(() => { const playbackTime = (Date.now() - startedAt) / 1000; const rate = parseInt((playbackTime * 100) / duration, 10);
},1000)

Как перемотать звук на определенный момент

Здесь ситуация несколько обратная. Во-первых, необходимо определитьrateа затем, исходя из этого, рассчитать playbackTime. Чтобы определить rateвы можете подсчитать длину элемента прогресса и положение мыши относительно точки, где пользователь щелкнул.

onProgressClick: (e) => { const rate = (e.clientX * 100) / e.target.offsetWidth; const playbackTime = (audioBuffer.duration * rate) / 100; source.stop(); source.start(o, playbackTime); // dont forger change startedAt time // startedAt = Date.now() - playbackTime * 1000;
}

Тут главное не забыть поменять startedAt или ваш прогресс не будет отображаться должным образом.

Как контролировать громкость

Для этого необходимо создать Узел усиления позвонив audioContext.createGain(); метод. После этого вы можете легко написать метод setVolume.

const gainNode = audioContext.createGain();
...
source.connect(gainNode);
const setVolume = (level) => { gainNode.gain.setValueAtTime(level, audioContext.currentTime);
};
setVolume(-1); // mute
setVolume(1); // speek

Итак, вы знаете все о том, как написать свой компонент для воспроизведения аудиофайлов. Теперь вам нужно только настроить его и включить такие функции, как воспроизведение, название файла, переход к следующему треку и так далее.

Как создать свой собственный звук

Как насчет создания собственных звуков? Здесь вы можете увидеть небольшой пример того, как генерировать звук разной частоты. Проверьте весь код здесь. Для генерации звука другой частоты используйте метод создатьOscillato.

const getOscillator = (startFrequency) => { const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const oscillator = audioCtx.createOscillator(); oscillator.type="square"; oscillator.frequency.setValueAtTime(startFrequency, audioCtx.currentTime); oscillator.connect(audioCtx.destination); const start = () => oscillator.start(); const stop = () => oscillator.stop(); const change = frequency => oscillator.frequency.setValueAtTime(frequency, audioCtx.currentTime); // value in hertz return { start, stop, change };
}; let frequency = 100;
const oscillator = getOscillator(frequency);
oscillator.start(); const interval = setInterval(() => { frequency = frequency + 100; oscillator.change(frequency);
}, 100);
setTimeout(() => { clearInterval(interval); oscillator.stop();
}, 2000);

Как пользоваться микрофоном

Использование микрофона в браузере в наши дни стало обычным делом. Для этого нужно вызвать метод getUserMedia принадлежащий окно.навигатор. Вы получите доступ к объекту stream. С его помощью вы можете сделать audioSource. Чтобы получить фрагмент данных с микрофона, вы можете использовать createScriptProcessor и его метод onaudioprocess.

// get permission to use mic
window.navigator.getUserMedia({ audio:true }, (stream) => { const audioContext = new AudioContext(); // get mic stream const source = audioContext.createMediaStreamSource( stream ); const scriptNode = audioContext.createScriptProcessor(4096, 1, 1); source.connect(scriptNode); scriptNode.connect(audioContext.destination); // output to speaker // source.connect(audioContext.destination); // on process event scriptNode.onaudioprocess = (e) => { // get mica data console.log(e.inputBuffer.getChannelData(0)) };
}, console.log);

Вы также можете использовать audioContext.createAnalyser() с микрофоном, чтобы получить спектральные характеристики сигнала.

Как визуализировать звук

В этой главе я покажу, как улучшить наш аудиоплеер, добавив визуализацию формы звукового сигнала (синусоида) и спектральных характеристик (частота) или эквалайзер (назовем это аудиобарами). Вы можете увидеть пример здесьи весь код правильно здесь

Итак, с чего следует начать? Вам понадобится два полотна. Напишите их в html и получите к ним доступ в js.

// in hmml <div className="bars-wrapper"> <canvas className="frequency-bars" width="1024" height="100"></canvas> <canvas className="sinewave" width="1024" height="100"></canvas>
</div>
....
// in js const frequencyC = document.querySelector('.frequency-bars'); const sinewaveC = document.querySelector('.sinewave');

Мы рассмотрим их позже. Мы уже узнали, как использовать AudioContext для декодирования файла и его воспроизведения. Для получения более подробной информации мы используем Аудиоанализатор. Итак, мы должны изменить метод getAudioContext немного.

const getAudioContext = () => { AudioContext = window.AudioContext || window.webkitAudioContext; const audioContext = new AudioContext(); const analyser = audioContext.createAnalyser(); return { audioContext, analyser };
};

Теперь давайте сделаем то же самое с нашим методом loadFile:

const loadFile = (url, { frequencyC, sinewaveC }) => new Promise(async (resolve, reject) => { const response = await axios.get(url, { responseType: 'arraybuffer' }); const { audioContext, analyser } = getAudioContext(); const audioBuffer = await audioContext.decodeAudioData(response.data); ... let source = audioContext.createBufferSource(); source.buffer = audioBuffer; source.start();

Как видите, этот метод получает в качестве параметров наши холсты. Мы должны подключить analyser к source чтобы использовать его для нашего аудиофайла. Теперь вы можете вызвать два метода drawFrequency а также drawSinewave для создания аудиобаров.

source.connect(analyser);
drawFrequency();
drawSinewave();

Чтобы построить Sinewave, вы должны знать две вещи: как получать данные и визуализировать их. В первой главе я описал концепцию звука и то, как он сохраняется на устройствах. Я упомянул, что звук — это определенное значение в определенный момент времени, также известное как цифровое аудио. Давайте теперь расшифруем и отобразим наши точки.

Для этого воспользуемся методом getBytheTimeDomainData. Поскольку этот метод работает с массивами, давайте сначала создадим наш массив.

... const audioBuffer = await audioContext.decodeAudioData(response.data); analyser.fftSize = 1024; let sinewaveDataArray = new Uint8Array(analyser.fftSize); // draw Sinewave const drawSinewave = function() { // get sinewave data analyser.getByteTimeDomainData(sinewaveDataArray); requestAnimationFrame(drawSinewave); // canvas config sinewaveСanvasCtx.fillStyle = styles.fillStyle; sinewaveСanvasCtx.fillRect(0, 0, sinewaveC.width, sinewaveC.height); sinewaveСanvasCtx.lineWidth = styles.lineWidth; sinewaveСanvasCtx.strokeStyle = styles.strokeStyle; sinewaveСanvasCtx.beginPath(); // draw wave const sliceWidth = sinewaveC.width * 1.0 / analyser.fftSize; let x = 0; for(let i = 0; i < analyser.fftSize; i++) { const v = sinewaveDataArray[i] / 128.0; // byte / 2 || 256 / 2 const y = v * sinewaveC.height / 2; if(i === 0) { sinewaveСanvasCtx.moveTo(x, y); } else { sinewaveСanvasCtx.lineTo(x, y); } x += sliceWidth; } sinewaveСanvasCtx.lineTo(sinewaveC.width, sinewaveC.height / 2); sinewaveСanvasCtx.stroke(); };

Вот две основные вещи, которые нужно помнить.

Первый – это параметр Analyzer.fftSize. Он указывает на точность, с которой декодируются аудиоданные или, другими словами, на длину массива sinewaveDataArray. Попробуйте изменить это значение и посмотрите, как изменится тип волны. Второй requestAnimationFrame (рисоватьSinewave) — значит, наша функция будет работать до обновления рамок экрана в браузере.

Все остальное — простой код для работы с канвасом. Для построения эквалайзера напишем функцию drawFrequency. Реализация похожа на предыдущую, разница только в вызове метода analyser.getByteFrequencyData(frequencyDataArray) и код холста (теперь мы строим прямоугольники, а не линию).

analyser.fftSize = styles.fftSize;
let frequencyDataArray = new Uint8Array(analyser.frequencyBinCount); const drawFrequency = function() { // get equalizer data analyser.getByteFrequencyData(frequencyDataArray); requestAnimationFrame(drawFrequency); // canvas config frequencyСanvasCtx.fillStyle = styles.fillStyle; frequencyСanvasCtx.fillRect(0, 0, frequencyC.width, frequencyC.height); frequencyСanvasCtx.beginPath(); // draw frequency - bar const barWidth = (frequencyC.width / analyser.frequencyBinCount) * 2.5; let barHeight; let x = 0; for(let i = 0; i < analyser.frequencyBinCount; i++) { barHeight = frequencyDataArray[i]; frequencyСanvasCtx.fillStyle = styles.strokeStyle; frequencyСanvasCtx.fillRect(x, frequencyC.height - barHeight / 2, barWidth, barHeight / 2); x += barWidth + 1; } };

Теперь вы знаете, как построить простую звуковую визуализацию. Если вы уже работали с холстом раньше, добавление новых эффектов не составит большого труда.

Во второй части статьи вы узнаете полезные советы и рекомендации по потоковой передаче аудиофайла. Не стесняйтесь проверить это прямо сейчас!

Часть вторая: Как работать со звуком в JS: Потоковое аудио

Похожие записи

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

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