使用 AudioWorklet 获取麦克风音量
AudioWorklet 简介
AudioWorklet 用于提供在单独线程中执行的自定义音频处理脚本,以提供非常低延迟的音频处理。
之前提议处理音频使用 audioContext.createScriptProcessor,但是它被设计成了异步的形式,随之而来的问题就是处理会出现 “延迟”。
AudioWorklet 的代码在 AudioWorkletGlobalScope 全局执行上下文中运行,使用由工作集和其他音频节点共享的单独的 Web 音频线程。
在 Ract 中实现监听麦克风音量大小
使用 navigator.mediaDevices.getUserMedia
来获取音频流。这里方便复用写成hook的形式。
- 获取到stream后,创建一个
AudioContext
, 音频中的AudioContext
可以类比于canvas
中的context
,其中包含了一系列用来处理音频的API
,简而言之,就是可以用来控制音频的各种行为,比如播放、暂停、音量大小等等。
也能做一些高级的操作,比如声道的合并与分割、混响、音调、声相控制和音频振幅压缩等等。
- 创建处理器脚本,也就是下面的
vumeterProcessor.js
,然后在脚本主文件中一个 AudioWorkletNode 实例,并传递处理器的名称,然后通过addModule
将该实例连接。
注意点: 在调用
addModule
时,AudioWorklet
需要通过网络加载,要将文件路径放到 pubilc 文件夹中。
然后创建
AudioWorkletNode
,创建时传递的参数为(audioContext,‘处理脚本的名称’),也就是registerProcessor('vumeter', VolumeMeter)
与这里的一致。实时监听音频的变化,设置音量。
最后,在不需要使用时调用
audioContext.close()
来进行销毁。
实现代码
useMedia.ts
import { useState, useEffect } from 'react';
const constraintsDefault: MediaStreamConstraints = {
video: true,
audio: {
channelCount: 1, // 单声道
noiseSuppression: true, // 降噪
echoCancellation: true, // 回音消除
},
};
export const useMedia = (constraints: MediaStreamConstraints = constraintsDefault) => {
const [stream, setStream] = useState<MediaStream | null>(null);
const [mediaError, setMediaError] = useState<any>(null);
const [microphoneVolume, setMicrophoneVolume] = useState(0);
const [audioContext, setAudioContext] = useState<AudioContext>(
() => new AudioContext()
);
const closeStream = async () => {
if (stream && stream.getTracks()) {
stream.getTracks().forEach((track: MediaStreamTrack) => {
track.stop();
});
}
setStream(null);
await audioContext?.close();
}
const getMicrophoneVolume = async (mediaStream: MediaStream) => {
/**
* In this case the audioWorklet.addModule() method expects the path to point to your public folder.
* It can also be an external URL for example a link to Github repository that loads the JS file.
* https://stackoverflow.com/questions/49972336/audioworklet-error-domexception-the-user-aborted-a-request
*/
await audioContext?.audioWorklet.addModule('js/vumeterProcessor.js');
const microphone = audioContext?.createMediaStreamSource(mediaStream);
const node = new AudioWorkletNode(audioContext, 'vumeter');
node.port.onmessage = event => {
let volume = 0;
if (event.data.volume) volume = Math.round(event.data.volume);
setMicrophoneVolume(volume);
}
microphone?.connect(node).connect(audioContext.destination);
}
useEffect(() => {
navigator.mediaDevices.getUserMedia(constraints).then(mediaStream => {
setStream(mediaStream);
getMicrophoneVolume(mediaStream);
}).catch(error => {
setMediaError(error);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return {
stream,
mediaError,
microphoneVolume,
closeStream,
};
}
vumeterProcessor.js
// vumeterProcessor.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 的处理函数 注意:与主线程中的名字对应。
使用方式
import { useMedia } from '@/hooks/useMedia';
const { stream, microphoneVolume, closeStream } = useMedia();
我们在页面中,可以加一些样式来查看效果
<div className={styles.volume}>
{
[1, 2, 3, 4, 5].map(item => {
return (
<>
<div
className={styles.volumeItem}
style={{
backgroundColor: microphoneVolume > item ? '#1967d2' : '#fff'
}}
></div>
</>
)
})
}
</div>
css
.volume {
margin-top: 20px;
display: flex;
align-items: center;
}
.volumeItem {
width: 12px;
height: 8px;
border: 1px solid #d4d4d4;
box-shadow: 0 1px 2px 0 rgba(149, 157, 163, 0.3);
margin-right: 2px;
}
最后看一下预览效果