- Published on
pcm 转 wav 与播放
- Authors
- Name
- Magikarp
前面我们已经实现了 js 录制 pcm 编码的功能了,但是很遗憾,浏览器并不能播放 pcm 音频,但可以支持与他比较相近的 wav 格式的文件,来琢磨下这两者的转化吧。
Wav
Waveform Audio File Format,是微软与 IBM 公司所开发在个人计算机存储音频流的编码格式。
wav 可以使用多种音频编码来压缩其音频流,不过我们常见的都是音频流被 pcm 编码处理的 wav。但这不表示 wav 只能使用 pcm 编码,mp3 编码同样也可以运用在 wav 中。简单来说,pcm 是无损 wav 文件中音频数据的一种编码方式,但 wav 还可以用其它方式编码。
wav 是一种无损的音频文件格式,由于此音频格式未经过压缩,所以在音质方面不会出现失真的情况,但文件的体积因而在众多音频格式中较为大。
一般情况下,wav 数据实际上就是裸数据 pcm 外面包了一层文件头。在前面的文章中,我们已经拿到了 pcm 数据了,只要在其前部增加 44 个字节的 wav 头就行了。
wav 头
先上这张图吧,
ChunkID
偏移量 0,占用了 4 字节,大端字节序,表示资源交换文件标识符,一般固定是"RIFF"。
ChunkSize
偏移量 4,占用 4 字节,小端字节序,下个地址开始到文件尾总字节数,即文件大小-8。
Format
偏移量 8,占用 4 字节,大端字节序,表示 wav 文件标志,一般固定为"WAVE"。
Subchunk1 ID
偏移量 12,占用 4 字节,大端字节序,表示波形格式标志,一般固定为"fmt ",注意最后有空格。
Subchunk1 Size
偏移量 16,占用 4 字节,小端字节序,表示过滤字节,一般为 0x10 = 16。
AudioFormat
偏移量 20,占用 2 字节,小端字节序,表示格式类别,1 是 PCM 形式采样数据,故此处填 1。
Num Channels
偏移量 22,占用 2 字节,小端字节序,表示声道数。
SampleRate
偏移量 24,占用 4 字节,小端字节序,表示采样率。
ByteRate
偏移量 28,占用 4 字节,小端字节序,表示波特率,即声道数 × 采样频率 × 采样位数 / 8。
BlockAlign
偏移量 32,占用 2 字节,小端字节序,声道数 × 采样位数 / 8。
Bits Per Sample
偏移量 34,占用 2 字节,小端字节序,采样位数.
Subchunk2 Id
偏移量 36,占用 4 字节,大端字节序,数据标识符,一般固定为"data"。
Subchunk2 Size
偏移量 40,占用 4 字节,小端字节序,表示采样数据总数,即数据总大小-44。
data
偏移量 44,小端字节序,pcm 数据。
js 拼装
给 pcm 的数据加上 wav 头就可以播放了,简单的处理如下:
/**
* 编码wav,一般wav格式是在pcm文件前增加44个字节的文件头,
* 所以,此处只需要在pcm数据前增加下就行了。
*
* @param {DataView} bytes pcm二进制数据
* @param {number} inputSampleRate 输入采样率
* @param {number} outputSampleRate 输出采样率
* @param {number} numChannels 声道数
* @param {number} oututSampleBits 输出采样位数
* @param {boolean} littleEdian 是否是小端字节序
* @returns {DataView} wav二进制数据
*/
export const encodeWAV = (
bytes: DataView,
inputSampleRate: number,
outputSampleRate: number,
numChannels: number,
oututSampleBits: number,
littleEdian: boolean = true
) => {
const sampleRate = outputSampleRate > inputSampleRate ? inputSampleRate : outputSampleRate // 输出采样率较大时,仍使用输入的值,
const sampleBits = oututSampleBits
const buffer = new ArrayBuffer(44 + bytes.byteLength)
const data = new DataView(buffer)
const channelCount = numChannels // 声道
let offset = 0
// 资源交换文件标识符
writeString(data, offset, 'RIFF')
offset += 4
// 下个地址开始到文件尾总字节数,即文件大小-8
data.setUint32(offset, 36 + bytes.byteLength, littleEdian)
offset += 4
// WAV文件标志
writeString(data, offset, 'WAVE')
offset += 4
// 波形格式标志
writeString(data, offset, 'fmt ')
offset += 4
// 过滤字节,一般为 0x10 = 16
data.setUint32(offset, 16, littleEdian)
offset += 4
// 格式类别 (PCM形式采样数据)
data.setUint16(offset, 1, littleEdian)
offset += 2
// 声道数
data.setUint16(offset, channelCount, littleEdian)
offset += 2
// 采样率,每秒样本数,表示每个通道的播放速度
data.setUint32(offset, sampleRate, littleEdian)
offset += 4
// 波形数据传输率 (每秒平均字节数) 声道数 × 采样频率 × 采样位数 / 8
data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), littleEdian)
offset += 4
// 快数据调整数 采样一次占用字节数 声道数 × 采样位数 / 8
data.setUint16(offset, channelCount * (sampleBits / 8), littleEdian)
offset += 2
// 采样位数
data.setUint16(offset, sampleBits, littleEdian)
offset += 2
// 数据标识符
writeString(data, offset, 'data')
offset += 4
// 采样数据总数,即数据总大小-44
data.setUint32(offset, bytes.byteLength, littleEdian)
offset += 4
// 给wav头增加pcm体
for (let i = 0; i < bytes.byteLength; ) {
data.setUint8(offset, bytes.getUint8(i))
offset += 1
i += 1
}
return data
}
给 pcm 嵌入上 wav 头,那么浏览器就可以播放了。
播放
audio 方式
可以创建 audio 标签,外加 window.createObjectURL 方法返回资源路径给 audio 标签,再通过 audio 的播放就 ok 了。