音频优化:SharedArrayBuffer共享 --> 二进制传输
前记
在音频使用中,我使用的是Web Audio
,这是一个内置于浏览器的JavaScript API,用于处理和操作音频。这个API提供了一套很强大的功能,使身为开发者的我们能够创建和控制音频流,实现音频的录制、播放、混合、合成和特效处理等。这篇文章是在使用Web Worker
进行解码传输,在主线程上进行播放音频,这样做的原因是:Web Worker
是后台独立运行的独立线程,无法直接访问或操作浏览器的DOM元素,包括音频播放器,而将音频解码操作放进worker中是为了防止堵塞住线程运行,提高用户体验。
因为一开始用的 SharedArrayBuffer 将worker解码出来的音频数据进行共享,后来考虑到一些因素(下面也会说)决定转 二进制 传输,如果想了解Web Audio
的使用,请点击这里。
直接传输
VS SharedArrayBuffer共享
直接传输
当我一开始使用Web Worker
来处理解码音频数据的时候,我很自然的不对音频数据进行任何处理而直接将它post
到主线程,"小"音频还好,但如果碰上了内存大的音频很多问题就渐渐浮出水面,比如:
- 数据传输延迟:由于音频数据通常是以较大的块进行解码的,直接将大量的解码后的音频数据传输给主线程可能会导致传输延迟。大量数据的传输需要消耗一定的时间,可能导致主线程在接收和处理音频数据时发生延迟,从而影响音频播放的实时性和流畅性.
- 主线程负载增加:直接将解码后的音频数据传输给主线程,主线程需要处理较大的数据量。如果音频数据的解码速度快于主线程处理数据的速度,可能会导致主线程的负载增加,造成主线程阻塞或卡顿的情况.
- 内存占用:解码后的音频数据可能占用较大的内存空间,如果直接传输给主线程,会增加主线程的内存占用。特别是在处理长时间的音频流或大型音频文件时,可能会导致主线程的内存压力增大.
- 线程间通信开销:将解码后的音频数据通过postMessage()传输给主线程需要进行线程间通信,这涉及到数据的复制和序列化操作。这些操作可能会引起一定的开销,特别是在频繁传输大量数据时,可能会对性能产生一定的影响.
SharedArrayBuffer共享(优点、用法和弊端)
使用 SharedArrayBuffer
进行共享可以很好的解决上述直接传输的问题,但是仍然存在一些弊端,接着往下看。
使用此方法的好处:
- 零拷贝(Zero-copy):SharedArrayBuffer允许多个线程(例如主线程和Web Worker)在共享内存空间中访问相同的数据,而无需进行数据复制。这意味着数据可以直接在内存中共享,避免了传输和复制数据的额外开销,提高了性能和效率。
- 高效的并发访问:SharedArrayBuffer提供了原子操作和锁机制,确保多个线程对共享数据的并发访问是安全和有序的。这使得多个线程可以同时读取和写入共享的数据,而不会发生竞态条件或数据不一致的问题。
- 实时性:由于零拷贝和高效的并发访问特性,使用SharedArrayBuffer进行数据共享可以实现实时性的要求。例如,在音频或视频处理应用中,可以将音频或视频数据共享给Web Worker进行处理,然后实时返回处理后的结果,以实现实时的音视频处理和渲染。
- 灵活性和扩展性:SharedArrayBuffer可以在多个线程之间共享任意类型的数据,不仅限于音频或视频数据。这使得它非常适用于各种并行计算和数据处理任务,如图像处理、计算密集型算法等。通过共享数据,可以将工作负载分摊到多个线程,提高整体的性能和扩展性。
用法(关键代码)
- 主线程中的代码
js
// 创建Web Worker
const worker = new Worker('worker.js');
// 监听Web Worker的消息事件
worker.addEventListener('message', (event) => {
// 获取共享的音频数据
const sharedBuffer = event.data;
// 在主线程中创建Float32Array,读取共享的音频数据
const sharedArray = new Float32Array(sharedBuffer);
// 创建AudioBufferSourceNode
const audioContext = new AudioContext();
const audioBuffer = audioContext.createBuffer(1, sharedArray.length, audioContext.sampleRate);
audioBuffer.getChannelData(0).set(sharedArray);
const sourceNode = audioContext.createBufferSource();
sourceNode.buffer = audioBuffer;
sourceNode.connect(audioContext.destination);
sourceNode.start();
});
// 导入音频文件
const audioFileInput = document.getElementById('audio-file-input');
audioFileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
const fileReader = new FileReader();
fileReader.onload = (event) => {
// 将音频数据传递给Web Worker
worker.postMessage(event.target.result);
};
fileReader.readAsArrayBuffer(file);
});
- worker中的代码
js
// 监听消息事件
self.addEventListener('message', (event) => {
const audioData = event.data;
// 解码音频数据
self.decodeAudioData(audioData, (decodedAudioData) => {
// 创建SharedArrayBuffer,用于存储解码后的音频数据
const sharedBuffer = new SharedArrayBuffer(decodedAudioData.length * 4); // 4 bytes per float32
// 在SharedArrayBuffer中创建Float32Array,存储解码后的音频数据
const sharedArray = new Float32Array(sharedBuffer);
sharedArray.set(decodedAudioData);
// 将SharedArrayBuffer传输给主线程
self.postMessage(sharedBuffer);
});
});
使用此方法的弊端:
此方法最大的问题来自于安全性,要在浏览器中开启ShareArrayBuffer
的支持,需要:
- 使用HTTPS协议:ShareArrayBuffer 在未加密的HTTP连接上被禁用,因此需要使用HTTPS协议来提供安全的通信.
- 添加合适的Cross-Origin-Opener-Policy(COOP)和Cross-Origin-Embedder-Policy(COEP)头部:
makefile
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
这两个请求头,具有一定的兼容性问题,配置复杂性较高,安全性考虑及跨域等问题.
二进制传输(关键代码)
worker代码
js
// Web Worker 代码(audio-decode-worker.js)
// 监听主线程传递的音频数据
self.onmessage = function(event) {
const arrayBuffer = event.data;
// 在Web Worker中解码音频数据
decodeAudioData(arrayBuffer)
.then(audioBuffer => {
// 将解码后的音频数据转换为二进制数据
const channelData = audioBuffer.getChannelData(0);
const float32Array = new Float32Array(channelData);
const binaryData = float32Array.buffer;
// 将二进制数据发送给主线程
self.postMessage(binaryData, [binaryData]);
})
.catch(error => {
console.error('音频解码失败:', error);
});
};
// 解码音频数据的函数
function decodeAudioData(arrayBuffer) {
return new Promise((resolve, reject) => {
// 创建音频上下文
const audioContext = new AudioContext();
// 解码音频数据为音频缓冲区
audioContext.decodeAudioData(arrayBuffer, function(audioBuffer) {
resolve(audioBuffer);
}, function(error) {
reject(error);
});
});
}
- 监听主线程传递的音频数据,并调用
decodeAudioData()
函数解码音频数据。 - 在解码成功后,将解码后的音频数据转换为二进制数据,这可以使用
AudioBuffer
对象的getChannelData()
方法来获取音频数据的浮点型数组。 - 将二进制数据通过
postMessage()
方法发送回主线程。
主线程代码
js
// 主线程代码
// 创建Web Worker实例
const audioWorker = new Worker('audio-decode-worker.js');
// 监听Web Worker的消息事件
audioWorker.onmessage = function(event) {
const binaryData = event.data;
// 创建音频上下文
const audioContext = new AudioContext();
// 将二进制数据转换为音频缓冲区
audioContext.decodeAudioData(binaryData, function(audioBuffer) {
// 创建音频源节点
const audioSource = audioContext.createBufferSource();
audioSource.buffer = audioBuffer;
// 连接音频源节点到音频目标节点(扬声器)
audioSource.connect(audioContext.destination);
// 播放音频
audioSource.start();
});
};
// 发送音频数据给Web Worker进行解码
fetch('audio-file.mp3')
.then(response => response.arrayBuffer())
.then(arrayBuffer => {
// 将音频数据发送给Web Worker
audioWorker.postMessage(arrayBuffer);
});
- 监听Web Worker传递的消息,接收解码后的二进制音频数据。
- 创建
AudioContext
对象。 - 使用
AudioContext
的decodeAudioData()
方法将二进制音频数据解码为AudioBuffer
对象。 - 创建
AudioBufferSourceNode
对象,并将解码后的AudioBuffer
对象设置为其buffer
属性。 - 将
AudioBufferSourceNode
连接到AudioContext
的目标节点(如扬声器)。 - 调用
AudioBufferSourceNode
的start()
方法开始播放音频。