pcm介绍:PCM(Puls Code Modulation)全称脉码调制录音,PCM录音就是将声音的模拟信号表示成0,1标识的数字信号,未经任何编码和压缩处理,所以可以认为PCM是未经压缩的音频原始格式。PCM格式文件中不包含头部信息,播放器无法知道采样率,声道数,采样位数,音频数据大小等信息,导致无法播放。
需求:我需要在websoket中接收实时的pcm音频流原始数据:16位,16k,单通道(意思就是:pcm 的参数采样率16000,采样位数16,声道数1)
重要!:千万不要去运用什么所谓的插件,因为它会使你绕很大一圈的弯路,实时播放的pcm浏览器是支持的,使用插件很可能还会被迫读什么源码,也很容易将你带偏,再多的播放插件底层大部分也是基于AudioContext去开发的。
首先是拿音频文件试,但是却不了解mp3和wav的区别,就首选拿了mp3这是我的第一个深坑,因为mp3经过多层的处理压缩,已经距离原始的文件很远了,通过ajax拿回来的arraybuffer是可以直接塞进去CreateBufferSource.buffer中就能直接播放,原因就是decodeAdioData就直接能处理接口返回的arraybuffer数据
mp3示例:
useEffect(() => {
axios.request({
url: require('@/assets/wholeWorld.mp3'), // 假如这是我们从后端请求回来的音乐文件
responseType: 'arraybuffer', // 必须需要这个
method: 'get'
}).then(res => {
// 创建AudioBufferSourceNode
const arrayBuffer = res.data
const context = new AudioContext()
const source = context.createBufferSource();
context.decodeAudioData(arrayBuffer).then(audioBuffer => {
// 设置buffer属性
source.buffer = audioBuffer;
// 连接到音频上下文并播放
source.connect(context.destination);
})
source.start(0);
})
}, [])
我处理wav的音频去播放,也是可以的使用decodeAudioData去进行播放
wav音频播放示例:
useEffect(() => {
axios.request({
url: require('@/assets/sample.wav'), // 假如这是我们从后端请求回来的音乐文件
responseType: 'arraybuffer', // 必须需要这个
method: 'get'
}).then(res => {
// 创建AudioBufferSourceNode
const arrayBuffer = res.data
const context = new AudioContext()
const source = context.createBufferSource();
context.decodeAudioData(arrayBuffer).then(audioBuffer => {
// 设置buffer属性
source.buffer = audioBuffer;
// 连接到音频上下文并播放
source.connect(context.destination);
})
source.start(0);
})
}, [])
但是都不支持pcm文件,所以我最开始的思路是通过获取到的pcm去处理成wav的文件一样,去搜了很多资料,都说wav的文件比pcm只是多了44字节文件头,我没从深度验证,但是我通过加字节文件头去进行pcm的播放,因为decodeAudioData可以运行其他处理过的音频文件,尽管加了字节文件头是不能解决根本问题的,这段路我绕了很大一个圈。
mp3文件和wav文件的区别示例:
mp3和wav的区别说明:
其实浏览器是可以直接播放pcm数据的,无论是文件,还是socket返回来的原始数据,这过程涉及了Uint8转换Uint16,Uint16转成Float32,了解decodeAudioData和getChannelData究竟处理什么问题等知识。
首先,找一个pcm文件,进行播放调试,pcm的文件能播放成功,那socket就不是问题!
PCM播放代码示例:
(仅播放出声音的调试,自行调整代码规范)
// 解析PCM数据到AudioBuffer
function decodePCM(arrayBuffer: ArrayBufferLike,sampleBits: number, channelCount: number, sampleRate: number,audioContext: any) {
return new Promise((resolve, reject) => {
const dataView = new DataView(arrayBuffer);
const length = (arrayBuffer.byteLength / (sampleBits / 8) / channelCount);
const buffer = audioContext.createBuffer(channelCount, length, sampleRate);
let offset = 0;
for (let channel = 0; channel < channelCount; channel++) {
const channelBuffer = buffer.getChannelData(channel);
for (let i = 0; i < length; i++) {
const sample = dataView.getInt16(offset, true); // 假设PCM数据是16位有符号整数
channelBuffer[i] = sample / 32768; // 标准化到-1到1的范围
offset += 2; // 16位 = 2字节
}
}
resolve(buffer);
});
}
let sourceNode: any = null;
useEffect(() => {
// 加载音频文件
axios.request({
method: 'get',
url: require('@/assets/recorder.pcm'),
responseType: "arraybuffer",
}).then(res => {
const arraybuffer = res.data
const audioContext = new window.AudioContext();
decodePCM(arraybuffer, 16, 1, 16000, audioContext).then(buffer => {
sourceNode = audioContext.createBufferSource();
sourceNode.buffer = buffer;
sourceNode.connect(audioContext.destination);
sourceNode.start(); // 开始播放
}).catch(error => {
console.error('Error decoding PCM:', error);
});
})
}, [])
注:
当声音有杂音,一直是一个杂音就说明,数据错了
当声音没有声音,数据很可能都是0
当声音隐隐有正常,但是杂音很重,一定是需要它:getChannelData和DataView
后端从TCP给我实时的pcm是压缩Uint8Array,前端将Uint8Array的数据解码为Uint16Array,然后合并Uint16Array的所有音频数据,还需要将Uint16Array通过DataView处理成AudioContext可播放的Float32Array,所以不了解AudioCotext API是很难解决这些问题的,更何况仅仅是播放的功能,我后面还要处理更多复杂的应用场景,持续更新实时播放pcm的处理应用场景。
最终在和蔼可亲的同事帮助和自己不辞辛苦的研究下搞出来了,感谢我的同事。
值得参考:
张鑫旭:https://www.zhangxinxu.com/wordpress/2023/10/js-audio-audiobuffer-concat-merge/
MDN:https://developer.mozilla.org/zh-CN/docs/Web/API/BaseAudioContext/createBuffer
推荐工具:
MEIDAINFO:https://mediaarea.net/MediaInfoOnline