如何在AudioWorkletProcessor.process函数中使用await或等待Promise?
如何在AudioWorkletProcessor.process函数中正确处理异步任务?
问题背景
需要在AudioWorkletProcessor的process函数中执行异步任务(比如机器学习模型推理),但尝试以下三种方案均失败:
- 直接在
process里用await:触发SyntaxError: Unexpected reserved word - 把
process声明为async函数:页面卡顿,无法处理麦克风输入 - 用
then回调加循环等待:CPU占用飙升,异步函数根本没执行
核心原因
AudioWorklet的process函数运行在实时音频线程中,要求必须同步执行、快速返回,不能有任何阻塞操作(包括异步等待)。一旦process阻塞,会导致音频卡顿、丢帧,甚至浏览器直接冻结。
正确解决方案:异步任务离线处理+消息队列
把异步任务移到非实时线程处理,通过消息队列传递音频数据和处理结果,具体步骤如下:
在AudioWorkletProcessor中维护数据队列与结果缓存
- 每次
process调用时,收集需要处理的音频数据,加入待处理队列 - 缓存异步任务返回的结果,在下一次
process调用时应用到音频流中
- 每次
通过
port通信触发异步任务- 从
process里把音频数据通过port.postMessage发送到主线程(或其他Worker)处理 - 主线程完成异步任务后,再通过
port.postMessage把结果发回Processor
- 从
在后续
process调用中应用结果- Processor收到结果后缓存,等下一次
process执行时,用缓存的结果处理当前音频帧
- Processor收到结果后缓存,等下一次
修改后的代码示例
<!doctype html> <html> <head> <script type="text/javascript" src="https://code.jquery.com/jquery-2.1.1.min.js"></script> </head> <div class="p-3 mb-2 bg-slate-100 w-1/3 rounded text-sm"> <input id="volume-meter" class="w-full bg-white" type="range" min="0" max="100" value="0" step="1" disabled> </div> <button id="button-start" disabled>START</button> <script> const audioContext = new AudioContext(); let process_code = ` const SMOOTHING_FACTOR = 0.8; const FRAME_PER_SECOND = 60; const FRAME_INTERVAL = 1 / FRAME_PER_SECOND; class VolumeMeter extends AudioWorkletProcessor { constructor() { super(); this._lastUpdate = currentTime; this._volume = 0; this._pendingTasks = []; // 待处理的音频数据队列 this._cachedResult = null; // 缓存异步任务结果 // 监听主线程发回的异步结果 this.port.onmessage = (event) => { if (event.data.type === 'async-result') { this._cachedResult = event.data.value; } }; } calculateRMS(inputChannelData) { let sum = 0; for (let i = 0; i < inputChannelData.length; i++) { sum += inputChannelData[i] * inputChannelData[i]; } let rms = Math.sqrt(sum / inputChannelData.length); this._volume = Math.max(rms, this._volume * SMOOTHING_FACTOR); } process(inputs, outputs) { const inputChannelData = inputs[0][0]; if (currentTime - this._lastUpdate > FRAME_INTERVAL) { this.calculateRMS(inputChannelData); this.port.postMessage({type: 'volume-data', value: this._volume}); this._lastUpdate = currentTime; } // 1. 收集音频数据,发送到主线程执行异步任务 if (this._pendingTasks.length === 0) { // 克隆音频数据(避免内存共享问题) const dataClone = new Float32Array(inputChannelData); this._pendingTasks.push(dataClone); this.port.postMessage({type: 'process-data', data: dataClone}); } // 2. 如果有缓存的异步结果,应用到输出(这里只是示例,根据实际需求修改) if (this._cachedResult) { // 比如把结果打印到控制台,或者修改输出音频 console.log('异步任务结果:', this._cachedResult); this._cachedResult = null; // 重置缓存,避免重复使用 this._pendingTasks.shift(); // 移除已处理的任务 } return true; } } registerProcessor("volume-meter", VolumeMeter); `; const startAudio = async (context, meterElement) => { let blob = new Blob([process_code], {type: 'application/javascript'}); await context.audioWorklet.addModule(URL.createObjectURL(blob)); const mediaStream = await navigator.mediaDevices.getUserMedia({audio: true}); const micNode = context.createMediaStreamSource(mediaStream); const volumeMeterNode = new AudioWorkletNode(context, 'volume-meter'); // 主线程处理异步任务 volumeMeterNode.port.onmessage = (event) => { if (event.data.type === 'volume-data') { meterElement.value = event.data.value * 500; } else if (event.data.type === 'process-data') { // 模拟异步任务(比如ML模型推理) const asyncTask = async (data) => { // 这里替换成实际的异步操作 await new Promise(resolve => setTimeout(resolve, 10)); return data.reduce((a,b) => a+b, 0) / data.length; // 计算平均值作为示例结果 }; asyncTask(event.data.data).then(result => { volumeMeterNode.port.postMessage({type: 'async-result', value: result}); }); } }; micNode.connect(volumeMeterNode).connect(context.destination); }; window.addEventListener('load', async () => { const buttonEl = document.getElementById('button-start'); const meterEl = document.getElementById('volume-meter'); buttonEl.disabled = false; meterEl.disabled = false; buttonEl.addEventListener('click', async () => { await startAudio(audioContext, meterEl); audioContext.resume(); buttonEl.disabled = true; buttonEl.textContent = 'Playing...'; }, false); }); </script> </html>
关键注意点
- 音频数据传递时要注意内存管理,避免内存泄漏,建议使用
Float32Array克隆数据 - 异步任务的处理速度要尽量跟上音频帧的生成速度,避免队列积压导致延迟
- 如果异步任务耗时过长,可以考虑批量处理音频数据,减少通信次数
内容的提问来源于stack exchange,提问作者JK-Hu
相关产品推荐
相关产品推荐

