Vue3 中使用 AudioWorklet 获取麦克风音量
最近公司在开发音视频项目,其中有个功能是用户监测麦克风讲话是否有波动;于是从各种技术网站想找关于Vue3的示例项目想着借鉴一番,但是功夫不负有心人,没有找到Vue的示例,但是在github中找到了一个大佬用React开发的麦克风监测功能;于是在他代码基础上迁移适配了Vue3并封装成hooks函数,
上手即用非常简单
AudioWorklet 简介
- AudioWorklet 用于提供在单独线程中执行的自定义音频处理脚本,以提供非常低延迟的音频处理。
- 之前提议处理音频使用
audioContext.createScriptProcessor,但是它被设计成了异步的形式,随之而来的问题就是处理会出现 “延迟”。 - AudioWorklet 的代码在 AudioWorkletGlobalScope 全局执行上下文中运行,使用由工作集和其他音频节点共享的单独的 Web 音频线程。
如何在Vue中监听麦克风音量大小
使用 navigator.mediaDevices.getUserMedia 来获取音频流。这里方便复用写成hook的形式。
-
获取到stream后,创建一个
AudioContext, 音频中的AudioContext可以类比于canvas中的context,其中包含了一系列用来处理音频的API,简而言之,就是可以用来控制音频的各种行为,比如播放、暂停、音量大小等等 -
创建处理器脚本,也就是下面的
vumeter.js,然后在脚本主文件中一个 AudioWorkletNode 实例,并传递处理器的名称,然后通过addModule将该实例连接。
注意:在调用
addModule时,AudioWorklet需要通过网络加载,要将文件路径放 Vue 项目中的pubilc文件夹中。
实现代码
hooks/useVolume.js
代码直接复制到hooks目录即可
import {reactive,ref,watch} from 'vue'
export default function useVolume(stream){
const volume = ref(0)
const newRef = reactive({current:{}})
const onmessage = (event) => {
if (!newRef.current.audioContext) {
return;
}
if (event.data.volume) {
volume.value = Math.round(event.data.volume * 200)
}
};
const disconnectAudioContext = () =>{
if (newRef.current.node) {
try {
newRef.current.node.disconnect();
} catch (errMsg) {}
}
if (newRef.current.source) {
try {
newRef.current.source.disconnect();
} catch (errMsg) {}
}
newRef.current.node = null;
newRef.current.source = null;
newRef.current.audioContext = null;
volume.value = 0
}
const connectAudioContext = async (mediaStream) =>{
if (newRef.current.audioContext) {
disconnectAudioContext();
}
try {
newRef.current.audioContext = new AudioContext();
await newRef.current.audioContext.audioWorklet.addModule('./worklet/vumeter.js');
if (!newRef.current.audioContext) {
return;
}
newRef.current.source = newRef.current.audioContext.createMediaStreamSource(mediaStream);
newRef.current.node = new AudioWorkletNode(newRef.current.audioContext, 'vumeter');
newRef.current.node.port.onmessage = onmessage;
newRef.current.source.connect(newRef.current.node).connect(newRef.current.audioContext.destination);
} catch (errMsg) {
disconnectAudioContext();
}
}
watch(()=>stream,(nVal,oVal)=>{
if (!nVal) {
return () => {};
}
connectAudioContext(nVal);
return () => {
disconnectAudioContext(nVal);
};
},{
immediate:true
})
return volume
}
AudioWorklet 插件代码
此处为项目路径文件名:
public/worklet/vumeter.js
/* eslint-disable no-underscore-dangle */
const SMOOTHING_FACTOR = 0.8;
// eslint-disable-next-line no-unused-vars
const MINIMUM_VALUE = 0.00001;
registerProcessor(
'vumeter',
class extends AudioWorkletProcessor {
_volume;
_updateIntervalInMS;
_nextUpdateFrame;
_currentTime;
constructor() {
super();
this._volume = 0;
this._updateIntervalInMS = 25;
this._nextUpdateFrame = this._updateIntervalInMS;
this._currentTime = 0;
this.port.onmessage = (event) => {
if (event.data.updateIntervalInMS) {
this._updateIntervalInMS = event.data.updateIntervalInMS;
// console.log(event.data.updateIntervalInMS);
}
};
}
get intervalInFrames() {
// eslint-disable-next-line no-undef
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 (0 < input.length) {
const samples = input[0];
let sum = 0;
let rms = 0;
// Calculated the squared-sum.
for (let i = 0; i < samples.length; i += 1) {
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 (0 > this._nextUpdateFrame) {
this._nextUpdateFrame += this.intervalInFrames;
// const currentTime = currentTime ;
// eslint-disable-next-line no-undef
if (!this._currentTime || 0.125 < currentTime - this._currentTime) {
// eslint-disable-next-line no-undef
this._currentTime = currentTime;
// console.log(`currentTime: ${currentTime}`);
this.port.postMessage({ volume: this._volume });
}
}
}
return true;
}
},
);
使用方式
引入使用 hooks 函数
<script setup>
import {onMounted, ref} from "vue";
import useVolume from "@/hooks/useVolume/useVolume";
const volume = ref(0)
onMounted(()=>{
navigator.mediaDevices.getUserMedia({ video: false, audio: true }).then((s) => {
volume.value = useVolume(s);
});
})
</script>
接下来是在页面中使用的效果
<div style="display: flex;justify-content: center">
<div class="audio-box">
<label for="">麦克风监测</label>
<n-progress type="line" :percentage="volume.value" :show-indicator="false" />
{{volume}}
</div>
</div>