Audio Worklet 现已登陆 Firefox
AudioWorklet
首次在 2018 年引入 Web。从那时起,Mozilla 一直在探索如何在 WebAudio API 中实现“无妥协”的实现。本周,Audio Worklet 登陆 Firefox 76 版本。我们已准备好开始弥合原生应用程序中音频功能与 Web 上可用音频功能之间的差距。
现在,开发人员可以利用 AudioWorklet
来编写任意音频处理代码,从而创建以前无法实现的 Web 应用。这项激动人心的新功能提高了 3D 游戏、VR 和音乐制作等新兴 Web 体验的标准。
音频工作线程为通用的实时音频合成和处理带来了强大功能和灵活性。这从使用 addModule()
方法指定一个脚本开始,该脚本可以动态生成音频或执行任意音频处理。现在,各种类型的源可以通过 Web Audio API 连接到 AudioWorkletNode
以进行即时处理。源示例包括 HTMLMediaElement
资源、本地麦克风 或 远程音频。或者,AudioWorklet
脚本本身可以作为音频源。
优点
音频处理代码在用于音频处理的专用实时系统线程上运行。这使音频免受过去可能由浏览器中发生的各种其他事件引起的暂停影响。
一个由脚本 process()
方法 注册 的方法会在实时线程上以规律的间隔被调用。每次调用都会提供与单个 AudioContext
渲染块相对应的 PCM (脉冲编码调制) 音频样本的输入和输出缓冲区。对输入样本的处理会同步生成输出样本。由于音频管道中没有添加延迟,因此我们可以构建响应性更强的应用程序。这种方法对于熟悉原生音频 API 的开发人员来说会很熟悉。在原生开发中,这种注册回调的模型无处不在。代码会注册一个回调函数,该函数会被系统调用以填充缓冲区。
通过其 AudioWorklet
属性在 AudioContext
中加载工作线程脚本
<button>Play</button>
<audio src="t.mp3" controls></audio>
<input type=range min=0.5 max=10 step=0.1 value=0.5></input>
<script>
let ac = new AudioContext;
let audioElement = document.querySelector("audio");
let source = ac.createMediaElementSource(audioElement);
async function play() {
await ac.audioWorklet.addModule('clipper.js');
ac.resume();
audioElement.play();
let softclipper = new AudioWorkletNode(ac, 'soft-clipper-node');
source.connect(softclipper).connect(ac.destination);
document.querySelector("input").oninput = function(e) {
console.log("Amount is now " + e.target.value);
softclipper.parameters.get("amount").value = e.target.value;
}
};
document.querySelector("button").onclick = function() {
play();
}
</script>
clipper.js: 实现一个能够产生可配置失真效果的软限幅器。使用 Audio Worklet 很简单,但如果不使用它,就会消耗大量内存。
class SoftClipper extends AudioWorkletProcessor {
constructor() {
super()
}
static get parameterDescriptors() {
return [{
name: 'amount',
defaultValue: 0.5,
minValue: 0,
maxValue: 10,
automationRate: "k-rate"
}];
}
process(input, output, parameters) {
// `input` is an array of input ports, each having multiple channels.
// For each channel of each input port, a Float32Array holds the audio
// input data.
// `output` is an array of output ports, each having multiple channels.
// For each channel of each output port, a Float32Array must be filled
// to output data.
// `parameters` is an object having a property for each parameter
// describing its value over time.
let amount = parameters["amount"][0];
let inputPortCount = input.length;
for (let portIndex = 0; portIndex < input.length; portIndex++) {
let channelCount = input[portIndex].length;
for (let channelIndex = 0; channelIndex < channelCount; channelIndex++) {
let sampleCount = input[portIndex][channelIndex].length;
for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++) {
output[0][channelIndex][sampleIndex] =
Math.tanh(amount * input[portIndex][channelIndex][sampleIndex]);
}
}
}
return true;
}
}
registerProcessor('soft-clipper-node', SoftClipper);
实时性能
但是,低延迟也带来了重大的责任。让我们从图形世界中做一个对比,在图形世界中,60 Hz 是移动设备和台式机设备的常见默认屏幕刷新率。确定显示内容的代码预计在以下时间内运行
1000 / 60 = 16.6̇ ms
以确保不会出现丢帧现象。
音频世界也有类似的期望。典型的音频系统每秒输出 48000 个音频帧,Web Audio API 以 128 个帧的块来处理帧。因此,128 帧(Web Audio API 中块的当前大小)的所有音频计算必须在以下时间内完成
128 * 1000 / 48000 ≅ 3 ms.
这包括 Web Audio API 图中所有 AudioWorkletProcessor
的所有 process()
调用,以及所有原生 AudioNode
处理。
在现代计算机和移动设备上,3 毫秒足够充足,但某些编程模式比其他模式更适合此任务。错过此截止时间会导致音频输出出现卡顿,这比显示屏上偶尔出现丢帧现象要令人更加反感。
为了始终保持在时间预算内,实时音频编程的第一条规则是“避免任何可能导致非确定性计算时间的事情”。最小化或避免除算术运算、其他数学函数以及从缓冲区读写之外的任何操作。
特别是,为了确保一致的处理时间,脚本应该将内存分配的频率降至最低。如果需要工作缓冲区,那么只需分配一次,并在每次处理块时重复使用相同的缓冲区。MessagePort
通信涉及内存分配,因此建议您最小化复制数据结构的复杂性。仅在绝对必要时才在实时 AudioWorklet
线程上执行操作。
垃圾回收
最后,由于 JavaScript 是一种垃圾回收语言,而当今 Web 浏览器中的垃圾回收器不是实时安全的,因此有必要最大限度地减少对可被垃圾回收的对象的创建。这将最大限度地减少实时线程上的非确定性行为。
话虽如此,当前一代 JavaScript 引擎的 JavaScript JIT 编译器和垃圾回收器已经足够先进,可以允许许多工作负载可靠地运行,只需对代码编写进行最少的注意即可。反过来,这允许快速原型设计或快速演示。
Firefox 的实现
最大限度地减少内存分配,只在音频处理中执行严格必要的操作这一原则也适用于浏览器的 AudioWorklet
实现。
Web Audio API 规范中的一个错误意外地要求在每次调用 process()
时为其参数创建新对象。为了性能的缘故,此要求将从规范中 删除。为了让开发人员能够最大限度地提高其应用程序的性能,Firefox 不会为 process()
调用创建新对象,除非配置更改需要这样做。目前,Firefox 是唯一提供此功能的主要浏览器。
如果开发人员注意编写不创建可被垃圾回收对象的 JavaScript,那么 Firefox 中的垃圾回收器将永远不会在实时音频处理线程上触发。这比听起来更简单,并且对性能非常有益。您可以使用类型化数组,并重复使用对象,但不要使用像 Promise 这样的高级功能。这些简单的建议非常有用,并且只适用于在实时音频线程上运行的代码。
在构建 Firefox 的 AudioWorklet
实现时,我们对涉及音频处理的原生代码路径进行了严格的评估。我们非常注重让开发人员能够在 Web 上发布可靠的音频应用程序。我们的目标是在 Firefox 可用的所有操作系统上提供尽可能快、尽可能稳定的体验。
一些技术调查支持了我们的性能目标。以下是一些值得注意的调查:分析 Firefox 的原生内存分配速度;仅在音频关键路径上使用具有实时优先级的线程;以及调查 SpiderMonkey 的内部工作原理。(SpiderMonkey 是 Firefox 的 JavaScript 虚拟机。)这确保了我们的 JavaScript 引擎不会在实时音频线程上执行任何无限制的操作。
WASM 和工作线程
WebAssembly (WASM) 的性能和潜力非常适合复杂的音频处理或合成。WASM 可与 AudioWorklet
一起使用。在专业音频行业中,现有的信号处理代码绝大多数是用编译成 WASM 的语言实现的。由于此代码只是进行音频处理,因此通常可以轻松地将其编译成 WASM 并运行在 Web 上。此外,它通常设计用于类似于 AudioWorklet
提供的回调接口。
对于需要大量批处理的算法,以及涵盖远远超过 128 帧块的数据,最好将处理拆分为多个块,或者在单独的 Web 工作线程 中执行。在 传递 工作线程和 AudioWorklet
脚本之间特别大的 ArrayBuffer 时,请务必将所有权转移给对方,以避免进行大型复制操作。然后将数组传回,以避免在实时线程上释放内存。这种方法还可以避免每次都需要分配新缓冲区的需求。
网络音频处理的未来
AudioWorklet
是三个功能中的第一个,它将弥合本地应用程序和网络应用程序之间低延迟音频处理的差距。 SharedArrayBuffer 和 WebAssembly SIMD 是另外两个即将推出 Firefox 的功能,它们与 AudioWorklet
结合起来非常有趣。前者,SharedArrayBuffer
,支持 Web 上的无锁编程,这是一种音频程序员经常依赖的技术,用于减少实时代码的不确定性。后者,WebAssembly SIMD,将允许加速各种音频处理算法。这是一种在音频软件中非常常见的技术。
想要更深入地了解如何在 Web 开发工作中使用 AudioWorklet
?您可以在 MDN 上找到文档和规范的详细信息。要分享规范的想法,您可以 访问 GitHub 上的 WebAudio 仓库。如果您想更多地参与 WebAudio 社区,有一个 活跃的 WebAudio Slack 可以加入。
2 条评论