1 前言

本文基于 Vue3 + TypeScript 实现实时音量检测,其他语言可修改相应写法

2 功能实现

2.1 新建 vumeter.js

在 public/static 目录下新建 vumeter.js

const SMOOTHING_FACTOR = 0.8;
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;
}
};
}

get intervalInFrames() {
return (this._updateIntervalInMS / 1000) * sampleRate;
}

process(inputs) {
const input = inputs[0];
if (input.length > 0) {
const samples = input[0];
let sum = 0;
let rms = 0;
for (let i = 0; i < samples.length; i += 1) {
sum += samples[i] * samples[i];
}
rms = Math.sqrt(sum / samples.length);
this._volume = Math.max(rms, this._volume * SMOOTHING_FACTOR);
this._nextUpdateFrame -= samples.length;
if (0 > this._nextUpdateFrame) {
this._nextUpdateFrame += this.intervalInFrames;
if (!this._currentTime || 0.125 < currentTime - this._currentTime) {
this._currentTime = currentTime;
this.port.postMessage({ volume: this._volume });
}
}
}

return true;
}
},
);

2.2 新建 useVolume.ts

在 src/hooks 目录下新建 vumeter.js

const volume = ref(0)
let audioContext: AudioContext
let node: AudioWorkletNode
let stream: MediaStream | null = null
let source: MediaStreamAudioSourceNode | null = null

const getStaticPath = (path: string) => `${import.meta.env.VITE_PUBLIC_PATH}static/${path}`

const init = async () => {
audioContext = new AudioContext()
await audioContext.audioWorklet.addModule(getStaticPath('vumeter.js'))
node = new AudioWorkletNode(audioContext, 'vumeter')
node.port.onmessage = (event) => {
if (event.data.volume) {
volume.value = Math.round(event.data.volume * 200)
}
}
}

const disconnectAudioContext = () => {
stream?.getTracks().forEach((track) => track.stop()) // 取消麦克风占用
source?.disconnect() // 取消 onmessage 监听
stream = null
source = null
volume.value = 0
}

const connectAudioContext = async () => {
if (!audioContext) await init()
navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then(async (mediaStream) => {
stream = mediaStream
source = audioContext.createMediaStreamSource(stream)
source.connect(node)
})
}

export default function () {
return {
volume,
connectAudioContext,
disconnectAudioContext
}
}

2.3 新增音量检测展示容器

<template>
<div class="volume">
<div>当前麦克风</div>
<div>输入音量大小如下</div>
<el-icon><i-ep-Microphone /></el-icon>
<span
v-for="i in 20"
:key="i"
class="volume-span"
:style="`${Math.ceil(volume / 5) >= i ? 'background: #2559e5' : ''}`"
></span>
</div>
</template>

<script setup lang="ts">
import useVolume from '@/hooks/useVolume'
const { volume, connectAudioContext, disconnectAudioContext } = useVolume()

// 开始音量检测
connectAudioContext()

// 暂停音量检测
// disconnectAudioContext()
</script>

<style lang="scss" scoped>
.volume {
border-top: 1px solid #dcdfe6;
border-bottom: 1px solid #dcdfe6;
padding: 16px 0;
text-align: center;
font-size: 12px;
:nth-child(2) {
margin-bottom: 6px;
}
div {
color: #909399;
}
.volume-span {
display: inline-block;
margin-left: 1px;
width: 3px;
height: 8px;
background: #c6e2ff;
}
}
</style>