Я зафиксировал значения громкости со своего устройства и собираюсь показать по частям, что я сделал. То, что у меня сейчас есть для звуковой функциональности, может измениться.
Я использовал этот образец кода из репозитория образцов WebRTC на GitHub, чтобы создать то, что мне нужно было сделать.
Фасилитатор может фиксировать значения громкости для каждой имеющейся у них группы, где значение представляет громкость звука. На странице сеанса группы есть кнопка «Начать запись». Нажав «Начать запись», вы сразу же начнете записывать звук с устройства ведущего.
Кнопка «Начать запись» находится в файле facilitator.html. Также в этом же файле находится тег audio с атрибутом ID, равным громкости, чтобы я мог получить доступ к элементу ‹audio› позже.
<button class="btn sml blue" ng-click="startRec()">Start Recording</button> <audio id="volume"></audio>
Когда нажимается «Начать запись», вызывается функция startRec()
в facilitator.controller.js. В этой функции происходит захват звука. Сначала я присваиваю элемент ‹audio› константе volumeValue
. Метод документа querySelector()
используется для возврата первого элемента, найденного в документе, с идентификатором тома.
const volumeValue = document.querySelector(‘#volume’);
Нам нужно создать новый объект AudioContext
, представляющий граф обработки звука. Это объект, который представляет звуковую систему и управляет звуком. Если AudioContext
API не поддерживается, на экране появится всплывающее окно с сообщением. Объект window представляет окно браузера.
try { window.audioContext = new AudioContext(); } catch (e) { alert('Web Audio API not supported.'); }
Мы можем напрямую получить доступ к микрофону, используя API в спецификации WebRTC под названием getUserMedia()
. getUserMedia()
запросит у пользователя доступ к подключенному микрофону. В случае успеха API вернет поток, содержащий данные с микрофона. Чтобы получить данные с микрофона, мы просто устанавливаем для звука значение true в объекте ограничений, который передается в getUserMedia()
API. Я также установил для видео значение false, потому что мы не записываем видео для ThoughtSwap (пока…).
const constraints = { audio: true, video: false }; navigator.mediaDevices.getUserMedia(constraints).then(handleSuccess).catch(handleError);
Функция handleSuccess()
обрабатывает аудиопоток, поступающий с устройства ведущего, а функция VolumeMeter()
обрабатывает аудиопоток для получения значений громкости. Нам нужно сделать объект VolumeMeter
. Мы передаем объект AudioContext
в качестве аргумента.
const volumeMeter = new VolumeMeter(window.audioContext);
В функции VolumeMeter()
мы создаем объект ScriptProcessorNode
с размером буфера 2048 и одним каналом ввода и вывода. Возвращенный объект назначается script
, что позволяет напрямую генерировать, обрабатывать или анализировать звук. Этот объект используется для обработки нашего микрофонного потока для получения значений громкости.
this.script = context.createScriptProcessor(2048, 1, 1);
Обработчик события onaudioprocess
обрабатывает события для события audioprocess
. Событие audioprocess
запускается, когда входной буфер нашего объекта ScriptProcessorNode
, script
, готов к обработке. Мы получаем входной буфер (наши необработанные аудиоданные) из inputbuffer
, который является доступным только для чтения свойством события audioprocess
и содержит входные аудиоданные для обработки. Метод getChannelData()
вернет массив наших аудиоданных, и мы передаем канал, чтобы получить данные, где значение индекса, равное нулю, представляет первый канал. Float32Array, который возвращает getChannelData()
, представляет собой массив 32-битных чисел с плавающей запятой. Логика внутри функции onaudioprocess()
— это то, что получает значения громкости, когда мы прокручиваем наш входной буфер по одному биту за раз.
function VolumeMeter(context) { this.context = context; this.volume = 0.0; this.script = context.createScriptProcessor(2048, 1, 1); const that = this; this.script.onaudioprocess = function(event) { const input = event.inputBuffer.getChannelData(0); var sum = 0.0; for (var i = 0; i < input.length; ++i) { sum += input[i] * input[i]; } that.volume = Math.sqrt(sum / input.length); }; }
Метод createMediaStreamSource()
объекта AudioContext
создает новый объект, представляющий наш источник звука, состоящий из нашего потока микрофона. Этот новый объект назначен mic
. Подключаем mic
к нашему объектуScriptProcessorNode
, script
. Затем мы соединяем script
с context.destination
, который представляет конечный пункт назначения всего аудио в контексте.
VolumeMeter.prototype.connectToSource = function(stream, callback) { try { this.mic = this.context.createMediaStreamSource(stream); this.mic.connect(this.script); this.script.connect(this.context.destination); if (typeof callback !== 'undefined') { callback(null); } } catch (e) { // what to do on error? } };
В какой-то момент мы также должны прекратить потоковую передачу данных с микрофона. Наш источник звука, mic
, и объект, позволяющий управлять этим источником звука, script
, отключены.
VolumeMeter.prototype.stop = function() { this.mic.disconnect(); this.script.disconnect(); };
Как только мы получим значения громкости для нашего потока аудиоданных, которые исходят от нашего источника звука, мы можем передавать эти значения в режиме реального времени на наш сервер.
ThoughtSocket.emit('new-audio-stream', { volumeValue: volumeValue.value, groupId: $routeParams.groupId, sessionId: $scope.sessionId });
Полная функция handleSuccess()
:
function handleSuccess(stream) { const volumeMeter = new VolumeMeter(window.audioContext); volumeMeter.connectToSource(stream, function() { setInterval(() => { volumeValue.value = volumeMeter.volume.toFixed(2); ThoughtSocket.emit('new-audio-stream', { volumeValue: volumeValue.value, groupId: $routeParams.groupId, sessionId: $scope.sessionId }); }, 200); }); }
Я все еще выясняю, когда аудиоданные должны перестать записываться.
Полная звуковая функциональность, описанная выше:
function VolumeMeter(context) { this.context = context; this.volume = 0.0; this.script = context.createScriptProcessor(2048, 1, 1); const that = this; this.script.onaudioprocess = function(event) { const input = event.inputBuffer.getChannelData(0); var sum = 0.0; for (var i = 0; i < input.length; ++i) { sum += input[i] * input[i]; } that.volume = Math.sqrt(sum / input.length); }; } VolumeMeter.prototype.connectToSource = function(stream, callback) { try { this.mic = this.context.createMediaStreamSource(stream); this.mic.connect(this.script); this.script.connect(this.context.destination); if (typeof callback !== 'undefined') { callback(null); } } catch (e) { // what to do on error? } }; VolumeMeter.prototype.stop = function() { this.mic.disconnect(); this.script.disconnect(); }; $scope.startRec = function () { const volumeValue = document.querySelector('#volume'); try { window.audioContext = new AudioContext(); } catch (e) { alert('Web Audio API not supported.'); } const constraints = { audio: true, video: false }; function handleSuccess(stream) { const volumeMeter = new VolumeMeter(window.audioContext); volumeMeter.connectToSource(stream, function() { setInterval(() => { volumeValue.value = volumeMeter.volume.toFixed(2); ThoughtSocket.emit('new-audio-stream', { volumeValue: volumeValue.value, groupId: $routeParams.groupId, sessionId: $scope.sessionId }); }, 200); }); } function handleError(error) { // what to do on error? } navigator.mediaDevices.getUserMedia(constraints).then(handleSuccess).catch(handleError); }; $scope.stopRec = function() { VolumeMeter.prototype.stop(); }
Я, очевидно, не совсем понял, что делать с ошибкой :p
Спасибо за полезные ресурсы:
https://www.javascripture.com/AudioContext
https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
https://developers.google.com/web/fundamentals/media/recording-audio/