1

麦克风音量

开发直播类的Web应用时在开播前通常需要检测设备是否正常,本文就来介绍一下如果如何做麦克风音量的可视化。

AudioWorklet出现的背景

做这个功能需要用到 Chrome 的 AudioWorklet。

Web Audio API 中的音频处理运行在一个单独的线程,这样才会比较流畅。之前提议处理音频使用audioContext.createScriptProcessor,但是它被设计成了异步的形式,随之而来的问题就是处理会出现 “延迟”。

所以 AudioWorklet 就诞生了,用来取代 createScriptProcessor。

AudioWorklet 可以很好的把用户提供的JS代码集成到音频处理的线程中,不需要跳到主线程处理音频,这样就保证了0延迟和同步渲染。

使用条件

使用 Audio Worklet 由两个部分组成: AudioWorkletProcessor 和 AudioWorkletNode.

  • AudioWorkletProcessor 代表了真正的处理音频的 JS 代码,运行在 AudioWorkletGlobalScope 中。
  • AudioWorkletNode 与 AudioWorkletProcessor 对应,起到连接主线程 AudioNodes 的作用。

编写代码

首先来写AudioWorkletProcessor,即用于处理音频的逻辑代码,放在一个单独的js文件中,命名为 processor.js,它将运行在一个单独的线程。

// processor.js
const SMOOTHING_FACTOR = 0.8

class VolumeMeter extends AudioWorkletProcessor {
  static get parameterDescriptors() {
    return []
  }

  constructor() {
    super()
    this.volume = 0
    this.lastUpdate = currentTime
  }

  calculateVolume(inputs) {
    const inputChannelData = inputs[0][0]
    let sum = 0

    // Calculate the squared-sum.
    for (let i = 0; i < inputChannelData.length; ++i) {
      sum += inputChannelData[i] * inputChannelData[i]
    }

    // Calculate the RMS level and update the volume.
    const rms = Math.sqrt(sum / inputChannelData.length)

    this.volume = Math.max(rms, this.volume * SMOOTHING_FACTOR)

    // Post a message to the node every 200ms.
    if (currentTime - this.lastUpdate > 0.2) {
      this.port.postMessage({ eventType: "volume", volume: this.volume * 100 })
      // Store previous time
      this.lastUpdate = currentTime
    }
  }

  process(inputs, outputs, parameters) {
    this.calculateVolume(inputs)

    return true
  }
}

registerProcessor('vumeter', VolumeMeter); // 注册一个名为 vumeter 的处理函数 注意:与主线程中的名字对应。

封装成一个继承自AudioWorkletProcessor的类,VolumeMeter(音量表)。

主线程代码

// 告诉用户程序需要使用麦克风
function activeSound () {
    try {
        navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
        
        navigator.getUserMedia({ audio: true, video: false }, onMicrophoneGranted, onMicrophoneDenied);
    } catch(e) {
        alert(e)
    }
}

async function onMicrophoneGranted(stream) {
    // Initialize AudioContext object
    audioContext = new AudioContext()

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

    await audioContext.audioWorklet.addModule('processor.js')
    // 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 => {
        // console.log(event.data.volume) // 在这里就可以获取到processor.js 检测到的音量值
        handleVolumeCellColor(event.data.volume) // 处理页面效果函数
    }

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

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

处理页面展示逻辑

上面的代码我们已经可以获取到系统麦克风的音量了,现在的任务是把它展示在页面上。

准备页面结构和样式代码:

<style>
    .volume-group {
        width: 200px;
        height: 50px;
        background-color: black;
        display: flex;
        align-items: center;
        gap: 5px;
        padding: 0 10px;
    }
    .volume-cell {
        width: 10px;
        height: 30px;
        background-color: #e3e3e5;
    }
</style>

<div class="volume-group">
    <div class="volume-cell"></div>
    <div class="volume-cell"></div>
    <div class="volume-cell"></div>
    <div class="volume-cell"></div>
    <div class="volume-cell"></div>
    <div class="volume-cell"></div>
    <div class="volume-cell"></div>
    <div class="volume-cell"></div>
    <div class="volume-cell"></div>
    <div class="volume-cell"></div>
    <div class="volume-cell"></div>
    <div class="volume-cell"></div>
</div>

渲染逻辑:

/**
 * 该函数用于处理 volume cell 颜色变化
 */
function handleVolumeCellColor(volume) {
    const allVolumeCells = [...volumeCells]
    const numberOfCells = Math.round(volume)
    const cellsToColored = allVolumeCells.slice(0, numberOfCells)

    for (const cell of allVolumeCells) {
        cell.style.backgroundColor = "#e3e3e5"
    }

    for (const cell of cellsToColored) {
        cell.style.backgroundColor = "#79c545"
    }
}

完整代码

下面贴上主线程完整代码,把它和processor.js放在同一目录运行即可。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AudioContext</title>
    <style>
        .volume-group {
            width: 200px;
            height: 50px;
            background-color: black;
            display: flex;
            align-items: center;
            gap: 5px;
            padding: 0 10px;
        }
        .volume-cell {
            width: 10px;
            height: 30px;
            background-color: #e3e3e5;
        }
    </style>
</head>
<body>
    <div class="volume-group">
        <div class="volume-cell"></div>
        <div class="volume-cell"></div>
        <div class="volume-cell"></div>
        <div class="volume-cell"></div>
        <div class="volume-cell"></div>
        <div class="volume-cell"></div>
        <div class="volume-cell"></div>
        <div class="volume-cell"></div>
        <div class="volume-cell"></div>
        <div class="volume-cell"></div>
        <div class="volume-cell"></div>
        <div class="volume-cell"></div>
    </div>
<script>
    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)
        }
    }

    const volumeCells = document.querySelectorAll(".volume-cell")
    
    async function onMicrophoneGranted(stream) {
        // Initialize AudioContext object
        audioContext = new AudioContext()

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

        await audioContext.audioWorklet.addModule('processor.js')
        // 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 => {
            // console.log(event.data.volume)
            handleVolumeCellColor(event.data.volume)
        }

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

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

    /**
     * 该函数用于处理 volume cell 颜色变化
     */
    function handleVolumeCellColor(volume) {
        const allVolumeCells = [...volumeCells]
        const numberOfCells = Math.round(volume)
        const cellsToColored = allVolumeCells.slice(0, numberOfCells)

        for (const cell of allVolumeCells) {
            cell.style.backgroundColor = "#e3e3e5"
        }

        for (const cell of cellsToColored) {
            cell.style.backgroundColor = "#79c545"
        }
    }

    activeSound()
</script>
</body>
</html>

参考文档

Enter Audio Worklet

文章到此结束。如果对你有用的话,欢迎点赞,谢谢。

文章首发于 IICOOM-个人博客|技术博客 - 浏览器检测麦克风音量


来了老弟
508 声望31 粉丝

纸上得来终觉浅,绝知此事要躬行