• 浏览器检测麦克风音量


    在这里插入图片描述

    开发直播类的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 的处理函数 注意:与主线程中的名字对应。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    封装成一个继承自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')
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    处理页面展示逻辑

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

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

    <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    渲染逻辑:

    /**
     * 该函数用于处理 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"
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    完整代码

    下面贴上主线程完整代码,把它和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>AudioContexttitle>
        <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105

    参考文档

    Enter Audio Worklet

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

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

  • 相关阅读:
    大模型时代的具身智能系列专题(九)
    基于RabbitMQ构建延迟队列
    OAuth2密码模式已死,最先进的Spring Cloud认证授权方案在这里
    LeetCode刷题--思路总结记录
    Centos7安装docker-compose
    git命令提交代码到远程仓库
    [应用推荐]Web Scraper——轻量数据爬取利器
    html iframe 框架有哪些优缺点?
    四十二、路由层
    【JS进阶】防抖与节流
  • 原文地址:https://blog.csdn.net/IICOOM/article/details/126854479