本文記錄一點工做經歷,探討音頻文件的格式
更多訪問 個人博客
最近在整理音視頻編程的知識,回憶起半年多,有一次需求是在後臺播放某來源的 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('預載音頻文件失敗') } }