本文記錄一點工作經歷,探討音頻文件的格式
更多訪問我的博客
前言
最近在整理音視頻編程的知識,回憶起半年多,有一次需求是在後臺播放某來源的 pcm 文件,當時處理方法用了點技巧,記錄下來
背景:業務需求,在web後臺裏播放 pcm 文件,文件不大(約300KB,已知 pcm 的參數採樣率16000,採樣位數16,聲道數1
如何播放
瀏覽器是無法直接播放 pcm 音頻的,因爲 pcm 是比較原始的音頻格式:
PCM(Puls Code Modulation)全稱脈碼調製錄音,PCM錄音就是將聲音的模擬信號表示成0,1標識的數字信號,未經任何編碼和壓縮處理,所以可以認爲PCM是未經壓縮的音頻原始格式。PCM格式文件中不包含頭部信息,播放器無法知道採樣率,聲道數,採樣位數,音頻數據大小等信息,導致無法播放。
如何讓瀏覽器識別 pcm
瀏覽器可以播放另一種音頻格式:WAV格式全稱爲WAVE,前面提到只需要在PCM文件的前面添加WAV文件頭,就可以生成WAV格式文件
所以我的解決方法是給 pcm 添加 wav header,接下來就是 browser javascript 的實踐編碼了
javascript 如何處理文件流
js 在處理文件流、網絡數據,常用到 ArrayBuffer 類型,關於 ArrayBuffer 類型的API調用方法,需要事先多瞭解。
- 第一步,ajax異步獲取網絡 pcm 文件的 ArrayBuffer
const getWebFileArrayBuffer = async (url) => {
return await fetch(url).then(response => response.arrayBuffer())
}
- 第二步,對獲取的 pcm 文件流 ArrayBuffer 添加 wav header,下面以代碼註釋爲大家解釋
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
let buffer = new ArrayBuffer(44 + dataLength)
let view = new DataView(buffer)
function writeString (view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i))
}
}
let offset = 0
/* 資源交換文件標識符 */
writeString(view, offset, 'RIFF'); offset += 4
/* 下個地址開始到文件尾總字節數,即文件大小-8 */
view.setUint32(offset, /* 32 */ 36 + dataLength, true); offset += 4
/* WAV文件標誌 */
writeString(view, offset, 'WAVE'); offset += 4
/* 波形格式標誌 */
writeString(view, offset, 'fmt '); offset += 4
/* 過濾字節,一般爲 0x10 = 16 */
view.setUint32(offset, 16, true); offset += 4
/* 格式類別 (PCM形式採樣數據) */
view.setUint16(offset, 1, true); offset += 2
/* 通道數 */
view.setUint16(offset, channelCount, true); offset += 2
/* 採樣率,每秒樣本數,表示每個通道的播放速度 */
view.setUint32(offset, sampleRateTmp, true); offset += 4
/* 波形數據傳輸率 (每秒平均字節數) 通道數×每秒數據位數×每樣本數據位/8 */
view.setUint32(offset, sampleRateTmp * channelCount * (sampleBits / 8), true); offset += 4
/* 快數據調整數 採樣一次佔用字節數 通道數×每樣本的數據位數/8 */
view.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2
/* 每樣本數據位數 */
view.setUint16(offset, sampleBits, true); offset += 2
/* 數據標識符 */
writeString(view, offset, 'data'); offset += 4
/* 採樣數據總數,即數據總大小-44 */
view.setUint32(offset, dataLength, true); offset += 4
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
}
- 第三步,在瀏覽器播放 pcm 轉出的 wav 的文件流 ArrayBuffer
- 先轉成 base64 格式
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)
}, ''))}`
}
- 將 base64 字符串放入
<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('預載音頻文件失敗')
}
}