这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战
WebRTC 互动时,由于自己的声音只有别人能够听到,需要一个音量仪表盘来表示麦克风正在正常工作。在 React 项目中使用 AudioWorklet 获取麦克风音量代码已经放到 github 上,有需要的可以参考。
通过使用 AudioContext 可以检查 MediaStream 的音量,参考 WebRTC samples 和 cwilso/volume-meter 完成了第一个版本。
使用 AudioContext 的 ScriptProcessor 检测音量
使用 AudioContext 的 ScriptProcessor 进行音量检测代码比较简单,虽然没有看懂原理,但是可以正常工作了。
const audioContext = new AudioContext();
const mediaStreamSource = audioContext.createMediaStreamSource(mediaStream);
const scriptProcessor = ref.current.audioContext.createScriptProcessor(2048, 1, 1);
scriptProcessor.onaudioprocess = () => {
const input = event.inputBuffer.getChannelData(0);
let sum = 0.0;
for (let i = 0; i < input.length; i += 1) {
sum += input[i] * input[i];
}
setVolume(Math.round(Math.sqrt(sum / input.length) * 100));
const volume = Math.round(Math.sqrt(sum / input.length) * 100);
console.log(`volume: ${volume}`);
};
scriptProcessor.connect(audioContext.destination);
mediaStreamSource.connect(scriptProcessor);
使用 AudioWorklet 检测音量原因
使用 ScriptProcessor 可以正常工作,21 年春节的时候,更新 chrome 后浏览器 console 中有了下面的警告信息。
看了下 MDN 中的浏览器兼容性,浏览器基本上都已经支持 AudioWorklet 了,后续肯定是要迁移过去的。
参考 Audio Worklet 的说明,使用 Audio Worklet 可以将音频处理相关的计算从主线程中移出去,理论上可以提升性能,当时互动页面的性能也是需要提升,开始看资料学习怎么使用 AudioWorklet 来检测音量大小。
Web Audio API 中的音频处理在与主 UI 线程不同的线程中运行,因此运行流畅。为了在 JavaScript 中启用自定义音频处理,Web Audio API 提出了一个 ScriptProcessorNode,它使用事件处理程序在主 UI 线程中调用用户脚本。
这种设计有两个问题:事件处理是异步设计的,代码执行发生在主线程上。前者导致延迟,后者给主线程带来压力,主线程通常挤满了各种 UI 和 DOM 相关任务,导致 UI“卡顿”或音频“故障”。由于这个基本的设计缺陷,
ScriptProcessorNode从规范中弃用并替换为 AudioWorklet。
Audio Worklet 很好地将用户提供的 JavaScript 代码保存在音频处理线程中——也就是说,它不必跳到主线程来处理音频。这意味着用户提供的脚本代码可以与其他内置 AudioNode 一起在音频渲染线程 (AudioWorkletGlobalScope) 上运行,从而确保零额外延迟和同步渲染。
AudioWorklet 检测音量实现
查资料的时候看到有提问 How to get microphone volume using AudioWorklet,回答里面有具体实现也有具体的 demo,实际测试了一下没有问题。
使用 AudioWorklet 有一很重要的一点需要了解,AudioWorklet 的代码时运行在 AudioWorkletGlobalScope 作用域下的,执行的上下文不是 window,AudioWorkletGlobalScope 下有几个全局变量 currentFrame、 currentTime、 sampleRate,就像主线程里面的 window、document 这些一样可以直接使用。第一次看 stackoverflow 上的代码非常疑惑,感觉代码都是错误的怎么可以正常工作,后面继续学习了才解除心中的疑问。
使用 AudioWorklet 检测音量的代码也不算复杂,麻烦的是代码需要分开两个文件,而且 AudioWorklet 需要通过网络加载,在 React 项目中使用时需要放到 public 目录中。
// volume-meter.js
const audioContext = new AudioContext();
await audioContext.audioWorklet.addModule('./worklet/vumeter.js');
const source = audioContext.createMediaStreamSource(mediaStream);
const node = new AudioWorkletNode(audioContext, 'vumeter');
node.port.onmessage = (event) => {
if (event.data.volume) {
console.log(Math.round(event.data.volume * 200));
}
};
source.connect(node).connect(audioContext.destination);
// /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;
constructor() {
super();
this._volume = 0;
this._updateIntervalInMS = 25;
this._nextUpdateFrame = this._updateIntervalInMS;
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;
this.port.postMessage({ volume: this._volume });
}
}
return true;
}
},
);
遗留问题
使用 AudioWorklet 将声音相关的计算从主线程中走了,理论上可以提升页面的性能,但是用 performance 实际测量感受并不明显。
优化思路:
- 代码中可以看到,没过 25 毫秒就上报一次检测结果,实际使用并不需要这么频繁
- 修改 _updateIntervalInMS 为 125,导致声音大小检测的没那么准确,思考原因应该是检测间隔大了后,音量大小被平均下来了
- 缓存一下每次 postMessage 的 currentTime,下次上报的时候检查时间间隔是否大于 0.125 秒,超过才 postMessage
但是由于自己对于 performance 工具理解不够,只能从理论上分析性能上有改善,实际分析等以后更加精通 performance 分析了再说。