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

Я использовал этот образец кода из репозитория образцов 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/