如何使用 AudioWorklet 获取麦克风音量

mro*_*man 9 javascript web-audio-api

I'm interested in getting a continuous reading of the microphone volume in Javscript. Many of the existing solutions to this on StackOverflow (see here, here, and here) utilize BaseAudioContext.createScriptProcessor() which is deprecated as of 2014.

I'd like to be using future-proof code in my project, so can anyone share a modern minimal example of how to read the microphone volume using the new AudioWorkletNode?

小智 15

让我们来看看一些需要知道的点:

  • 这一切的改变都是为了避免延迟,创建了自己的线程,也就是说,运行在音频渲染线程(AudioWorkletGlobalScope)上。
  • 这种新的实现方式有两个部分:AudioWorkletProcessor 和 AudioWorkletNode。
  • AudioWorkletNode 至少需要两件事:一个 AudioContext 对象和作为字符串的处理器名称。处理器定义可以通过新的 Audio Worklet 对象的 addModule () 调用加载和注册。
  • 包括 AudioWorklet 在内的 Worklet API 仅在安全上下文中可用。在这种情况下,我们可以使用 localhost,但有必要了解这一点。
  • 我们需要至少从 AudioWorkletProcessor 向 AudioWorkletNode 传达当前值或本例中的音量以对其进行任何操作。
  • 有必要通过以下方式访问计算机的麦克风 navigator.getUserMedia
/** Declare a context for AudioContext object */
let audioContext
// Creating a list of colors for led
const ledColor = [
    "#064dac",
    "#064dac",
    "#064dac",
    "#06ac5b",
    "#15ac06",
    "#4bac06",
    "#80ac06",
    "#acaa06",
    "#ac8b06",
    "#ac5506",
]
let isFirtsClick = true
let listeing = false

function onMicrophoneDenied() {
    console.log('denied')
}

/**
 * This method updates leds
 * depending the volume detected
 * 
 * @param {Float} vol value of volume detected from microphone
 */
function leds(vol) {
    let leds = [...document.getElementsByClassName('led')]
    let range = leds.slice(0, Math.round(vol))

    for (var i = 0; i < leds.length; i++) {
        leds[i].style.boxShadow = "-2px -2px 4px 0px #a7a7a73d, 2px 2px 4px 0px #0a0a0e5e";
        leds[i].style.height = "22px"
    }

    for (var i = 0; i < range.length; i++) {
        range[i].style.boxShadow = `5px 2px 5px 0px #0a0a0e5e inset, -2px -2px 1px 0px #a7a7a73d inset, -2px -2px 30px 0px ${ledColor[i]} inset`;
        range[i].style.height = "25px"
    }
}

/**
 * Method used to create a comunication between
 * AudioWorkletNode, Microphone and AudioWorkletProcessor
 * 
 * @param {MediaStream} stream If user grant access to microphone, this gives you
 * a MediaStream object necessary in this implementation
 */
async function onMicrophoneGranted(stream) {
    // Instanciate just in the first time
    // when button is pressed
    if (isFirtsClick) {
        // Initialize AudioContext object
        audioContext = new AudioContext()

        // Adding an AudioWorkletProcessor
        // from another script with addModule method
        await audioContext.audioWorklet.addModule('vumeter-processor.js')

        // Creating a MediaStreamSource object
        // and sending a MediaStream object granted by 
        // the user
        let microphone = audioContext.createMediaStreamSource(stream)

        // Creating AudioWorkletNode sending
        // context and name of processor registered
        // in vumeter-processor.js
        const node = new AudioWorkletNode(audioContext, 'vumeter')

        // Listing any message from AudioWorkletProcessor in its
        // process method here where you can know
        // the volume level
        node.port.onmessage  = event => {
            let _volume = 0
            let _sensibility = 5 // Just to add any sensibility to our ecuation
            if (event.data.volume)
                _volume = event.data.volume;
            leds((_volume * 100) / _sensibility)
        }

        // Now this is the way to
        // connect our microphone to
        // the AudioWorkletNode and output from audioContext
        microphone.connect(node).connect(audioContext.destination)

        isFirtsClick = false
    }

    // Just to know if button is on or off
    // and stop or resume the microphone listening
    let audioButton = document.getElementsByClassName('audio-control')[0]
    if (listeing) {
        audioContext.suspend()
        audioButton.style.boxShadow = "-2px -2px 4px 0px #a7a7a73d, 2px 2px 4px 0px #0a0a0e5e"
        audioButton.style.fontSize = "25px"
    } else {
        audioContext.resume()
        audioButton.style.boxShadow = "5px 2px 5px 0px #0a0a0e5e inset, -2px -2px 1px 0px #a7a7a73d inset"
        audioButton.style.fontSize = "24px"
    }

    listeing = !listeing
}

function activeSound () {
    // Tell user that this
    // program wants to use
    // the microphone
    try {
        navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
        
        navigator.getUserMedia(
            { audio: true, video: false },
            onMicrophoneGranted,
            onMicrophoneDenied
        );
    } catch(e) {
        alert(e)
    }
}

document.getElementById('audio').addEventListener('click', () => {
    activeSound()
})
Run Code Online (Sandbox Code Playgroud)

在本节中,您可以了解麦克风音量的实现:

const SMOOTHING_FACTOR = 0.8;
const MINIMUM_VALUE = 0.00001;

// This is the way to register an AudioWorkletProcessor
// it's necessary to declare a name, in this case
// the name is "vumeter"
registerProcessor('vumeter', class extends AudioWorkletProcessor {

  _volume
  _updateIntervalInMS
  _nextUpdateFrame

  constructor () {
    super();
    this._volume = 0;
    this._updateIntervalInMS = 25;
    this._nextUpdateFrame = this._updateIntervalInMS;
    this.port.onmessage = event => {
      if (event.data.updateIntervalInMS)
        this._updateIntervalInMS = event.data.updateIntervalInMS;
    }
  }

  get intervalInFrames () {
    return this._updateIntervalInMS / 1000 * sampleRate;
  }

  process (inputs, outputs, parameters) {
    const input = inputs[0];

    // Note that the input will be down-mixed to mono; however, if no inputs are
    // connected then zero channels will be passed in.
    if (input.length > 0) {
      const samples = input[0];
      let sum = 0;
      let rms = 0;

      // Calculated the squared-sum.
      for (let i = 0; i < samples.length; ++i)
        sum += samples[i] * samples[i];

      // Calculate the RMS level and update the volume.
      rms = Math.sqrt(sum / samples.length);
      this._volume = Math.max(rms, this._volume * SMOOTHING_FACTOR);

      // Update and sync the volume property with the main thread.
      this._nextUpdateFrame -= samples.length;
      if (this._nextUpdateFrame < 0) {
        this._nextUpdateFrame += this.intervalInFrames;
        this.port.postMessage({volume: this._volume});
      }
    }
    
    return true;
  }
});
Run Code Online (Sandbox Code Playgroud)

最后,这是您可以显示检测到的音量的 html:

<div class="container">
    <span>Microphone</span>
    <div class="volumen-wrapper">
        <div class="led"></div>
        <div class="led"></div>
        <div class="led"></div>
        <div class="led"></div>
        <div class="led"></div>
                
        <div class="led"></div>
        <div class="led"></div>
        <div class="led"></div>
        <div class="led"></div>
        <div class="led"></div>
    </div>

    <div class="control-audio-wrapper">
        <div id="audio" class="audio-control">&#127908;</div>
    </div>
</div>
<script type="module" src="./index.js"></script>
Run Code Online (Sandbox Code Playgroud)

这是结果 在此处输入图片说明

这是我在codepen 中的实现

资料来源:

  • 感谢您的精彩解释和示例代码。目前不应使用“getUserMedia()”,而应使用“MediaDevices.getUserMedia()”。 (2认同)