开发直播类的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.8class VolumeMeter extends AudioWorkletProcessor {static get parameterDescriptors() {return []}constructor() {super()this.volume = 0this.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 timethis.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 objectaudioContext = new AudioContext()// Creating a MediaStreamSource object and sending a MediaStream object granted by the userlet microphone = audioContext.createMediaStreamSource(stream)await audioContext.audioWorklet.addModule('processor.js')// Creating AudioWorkletNode sending// context and name of processor registered// in vumeter-processor.jsconst node = new AudioWorkletNode(audioContext, 'vumeter')// Listing any message from AudioWorkletProcessor in its// process method here where you can know// the volume levelnode.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 audioContextmicrophone.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 microphonetry {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 objectaudioContext = new AudioContext()// Creating a MediaStreamSource object and sending a MediaStream object granted by the userlet microphone = audioContext.createMediaStreamSource(stream)await audioContext.audioWorklet.addModule('processor.js')// Creating AudioWorkletNode sending// context and name of processor registered// in vumeter-processor.jsconst node = new AudioWorkletNode(audioContext, 'vumeter')// Listing any message from AudioWorkletProcessor in its// process method here where you can know// the volume levelnode.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 audioContextmicrophone.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-个人博客|技术博客 - 浏览器检测麦克风音量