chromium/tools/perf/page_sets/webrtc_cases/audio-processing.js

/*
 *  Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */

'use strict';

/* global MediaStreamTrackProcessor, MediaStreamTrackGenerator */
if (typeof MediaStreamTrackProcessor === 'undefined' ||
    typeof MediaStreamTrackGenerator === 'undefined') {
  alert(
      'Your browser does not support the experimental MediaStreamTrack API ' +
      'for Insertable Streams of Media. See the note at the bottom of the ' +
      'page.');
}

try {
  new MediaStreamTrackGenerator('audio');
  console.log('Audio insertable streams supported');
} catch (e) {
  alert(
      'Your browser does not support insertable audio streams. See the note ' +
        'at the bottom of the page.');
}

// Put variables in global scope to make them available to the browser console.

// Audio element
let audio;

// Buttons
let startButton;
let stopButton;

// Transformation chain elements
let processor;
let generator;
let transformer;

// Stream from getUserMedia
let stream;
// Output from the transform
let processedStream;

// Adjust this value to increase/decrease the amount of filtering.
// eslint-disable-next-line prefer-const
let cutoff = 100;

// An AbortController used to stop the transform.
let abortController;

// Initialize on page load.
async function init() {
  audio = document.getElementById('audioOutput');
  startButton = document.getElementById('startButton');
  stopButton = document.getElementById('stopButton');

  startButton.onclick = start;
  stopButton.onclick = stop;
}

const constraints = window.constraints = {
  audio: true,
  video: false
};

// Returns a low-pass transform function for use with TransformStream.
function lowPassFilter() {
  let lastValuePerChannel = undefined;
  return (frame, controller) => {
    const rc = 1.0 / (cutoff * 2 * Math.PI);
    const dt = 1.0 / frame.buffer.sampleRate;
    const alpha = dt / (rc + dt);
    const nChannels = frame.buffer.numberOfChannels;
    if (!lastValuePerChannel) {
      console.log(`Audio stream has ${nChannels} channels.`);
      lastValuePerChannel = Array(nChannels).fill(0);
    }
    for (let c = 0; c < nChannels; c++) {
      const samples = frame.buffer.getChannelData(c);
      let lastValue = lastValuePerChannel[c];

      // Apply low-pass filter to samples.
      for (let i = 0; i < samples.length; ++i) {
        lastValue = lastValue + alpha * (samples[i] - lastValue);
        samples[i] = lastValue;
      }

      frame.buffer.copyToChannel(samples, c);
      lastValuePerChannel[c] = lastValue;
    }
    controller.enqueue(frame);
  };
}

async function start() {
  startButton.disabled = true;
  try {
    stream = await navigator.mediaDevices.getUserMedia(constraints);
  } catch (error) {
    const errorMessage = 'navigator.MediaDevices.getUserMedia error: ' + error.message + ' ' + error.name;
    document.getElementById('errorMsg').innerText = errorMessage;
    console.log(errorMessage);
  }
  const audioTracks = stream.getAudioTracks();
  console.log('Using audio device: ' + audioTracks[0].label);
  stream.oninactive = () => {
    console.log('Stream ended');
  };

  processor = new MediaStreamTrackProcessor(audioTracks[0]);
  generator = new MediaStreamTrackGenerator('audio');
  const source = processor.readable;
  const sink = generator.writable;
  transformer = new TransformStream({transform: lowPassFilter()});
  abortController = new AbortController();
  const signal = abortController.signal;
  const promise = source.pipeThrough(transformer, {signal}).pipeTo(sink);
  promise.catch((e) => {
    if (signal.aborted) {
      console.log('Shutting down streams after abort.');
    } else {
      console.error('Error from stream transform:', e);
    }
    source.cancel(e);
    sink.abort(e);
  });

  processedStream = new MediaStream();
  processedStream.addTrack(generator);
  audio.srcObject = processedStream;
  stopButton.disabled = false;
  await audio.play();
}

async function stop() {
  stopButton.disabled = true;
  audio.pause();
  audio.srcObject = null;
  stream.getTracks().forEach(track => {
    track.stop();
  });
  abortController.abort();
  abortController = null;
  startButton.disabled = false;
}

window.onload = init;