Vue3 中使用 AudioWorklet 获取麦克风音量

Vue3 中使用 AudioWorklet 获取麦克风音量

最近公司在开发音视频项目,其中有个功能是用户监测麦克风讲话是否有波动;于是从各种技术网站想找关于Vue3的示例项目想着借鉴一番,但是功夫不负有心人,没有找到Vue的示例,但是在github中找到了一个大佬用React开发的麦克风监测功能;于是在他代码基础上迁移适配了Vue3并封装成hooks函数,上手即用非常简单

示例代码:gitee Demo示例

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>

最后效果的预览

Vue3 中使用 AudioWorklet 获取麦克风音量_第1张图片

你可能感兴趣的:(javascript,vue.js,前端)