本文记录一点工做经历,探讨音频文件的格式
更多访问 个人博客
最近在整理音视频编程的知识,回忆起半年多,有一次需求是在后台播放某来源的 pcm 文件,当时处理方法用了点技巧,记录下来javascript
背景:业务需求,在web后台里播放 pcm 文件,文件不大(约300KB,已知 pcm 的参数采样率16000,采样位数16,声道数1java
浏览器是没法直接播放 pcm 音频的,由于 pcm 是比较原始的音频格式:react
PCM(Puls Code Modulation)全称脉码调制录音,PCM录音就是将声音的模拟信号表示成0,1标识的数字信号,未经任何编码和压缩处理,因此能够认为PCM是未经压缩的音频原始格式。PCM格式文件中不包含头部信息,播放器没法知道采样率,声道数,采样位数,音频数据大小等信息,致使没法播放。web
浏览器能够播放另外一种音频格式:WAV格式全称为WAVE,前面提到只须要在PCM文件的前面添加WAV文件头,就能够生成WAV格式文件ajax
因此个人解决方法是给 pcm 添加 wav header,接下来就是 browser javascript 的实践编码了编程
js 在处理文件流、网络数据,经常使用到 ArrayBuffer 类型,关于 ArrayBuffer 类型的API调用方法,须要事先多了解。浏览器
const getWebFileArrayBuffer = async (url) => { return await fetch(url).then(response => response.arrayBuffer()) }
看以上图片,咱们须要将获取到的 pcm data 添加 44 bytes 的 header,根据 header 的结构,对齐、紧凑地填充信息,在 javascript 中,须要使用 DataView
类型帮助咱们进行字节填充的操做,注意DataView
提供的 API 默认使用 little end
的数据格式,须要额外定义 big end
格式填充字节的方法。网络
如下以代码来讲明如何一步一步填充字节信息:异步
const getWebPcm2WavArrayBuffer = async (url) => { const bytes = await getWebFileArrayBuffer(url) return addWavHeader(bytes, 16000, 16, 1) // 这里是当前业务需求,特定的参数,采样率16000,采样位数16,声道数1 } const addWavHeader = function (samples, sampleRateTmp, sampleBits, channelCount) { let dataLength = samples.byteLength /* 新的buffer类,预留 44 bytes 的 heaer 空间 */ let buffer = new ArrayBuffer(44 + dataLength) /* 转为 Dataview, 利用 API 来填充字节 */ let view = new DataView(buffer) /* 定义一个内部函数,以 big end 数据格式填充字符串至 DataView */ function writeString (view, offset, string) { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)) } } let offset = 0 /* ChunkID, 4 bytes, 资源交换文件标识符 */ writeString(view, offset, 'RIFF'); offset += 4 /* ChunkSize, 4 bytes, 下个地址开始到文件尾总字节数,即文件大小-8 */ view.setUint32(offset, /* 32 */ 36 + dataLength, true); offset += 4 /* Format, 4 bytes, WAV文件标志 */ writeString(view, offset, 'WAVE'); offset += 4 /* Subchunk1 ID, 4 bytes, 波形格式标志 */ writeString(view, offset, 'fmt '); offset += 4 /* Subchunk1 Size, 4 bytes, 过滤字节,通常为 0x10 = 16 */ view.setUint32(offset, 16, true); offset += 4 /* Audio Format, 2 bytes, 格式类别 (PCM形式采样数据) */ view.setUint16(offset, 1, true); offset += 2 /* Num Channels, 2 bytes, 通道数 */ view.setUint16(offset, channelCount, true); offset += 2 /* SampleRate, 4 bytes, 采样率,每秒样本数,表示每一个通道的播放速度 */ view.setUint32(offset, sampleRateTmp, true); offset += 4 /* ByteRate, 4 bytes, 波形数据传输率 (每秒平均字节数) 通道数×每秒数据位数×每样本数据位/8 */ view.setUint32(offset, sampleRateTmp * channelCount * (sampleBits / 8), true); offset += 4 /* BlockAlign, 2 bytes, 快数据调整数 采样一次占用字节数 通道数×每样本的数据位数/8 */ view.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2 /* BitsPerSample, 2 bytes, 每样本数据位数 */ view.setUint16(offset, sampleBits, true); offset += 2 /* Subchunk2 ID, 4 bytes, 数据标识符 */ writeString(view, offset, 'data'); offset += 4 /* Subchunk2 Size, 4 bytes, 采样数据总数,即数据总大小-44 */ view.setUint32(offset, dataLength, true); offset += 4 /* 数据流须要以大端的方式存储,定义不一样采样比特的 API */ function floatTo32BitPCM (output, offset, input) { input = new Int32Array(input) for (let i = 0; i < input.length; i++, offset += 4) { output.setInt32(offset, input[i], true) } } function floatTo16BitPCM (output, offset, input) { input = new Int16Array(input) for (let i = 0; i < input.length; i++, offset += 2) { output.setInt16(offset, input[i], true) } } function floatTo8BitPCM (output, offset, input) { input = new Int8Array(input) for (let i = 0; i < input.length; i++, offset++) { output.setInt8(offset, input[i], true) } } if (sampleBits == 16) { floatTo16BitPCM(view, 44, samples) } else if (sampleBits == 8) { floatTo8BitPCM(view, 44, samples) } else { floatTo32BitPCM(view, 44, samples) } return view.buffer }
const getWebPcm2WavBase64 = async (url) => { let bytes = await getWebPcm2WavArrayBuffer(url) return `data:audio/wav;base64,${btoa(new Uint8Array(bytes).reduce((data, byte) => { return data + String.fromCharCode(byte) }, ''))}` }
<audio>
组件中,这里以 react/ant design
的组件为例,封装一个方法const playWebPcm = async (url) => { try { let pcmBase64 = await fileServer.getWebPcm2WavBase64(url) Modal.info({ title: '播放音频', content: ( <audio controls src={pcmBase64} type="audio/wav" autoPlay /> ), onOk () {}, okText: '关闭', }) } catch (err) { console.error(err) message.error('预载音频文件失败') } }