前言

书接上回,上一篇《音频处理之文件编码与解码(一)》讲的是关于PCM格式的编码与解码实现,本文有些代码用到了上一篇的代码,建议先看完上一篇再看这篇。

其实PCM文件只是最原始的波形音频文件,大部分客户端不能直接播放PCM文件,而我们这次要讲的WAV格式和MP3格式的编码解码实现,这两种文件格式可以说是市面上最常见的两种音频文件格式,有较好的兼容性和认可度。

WAV

WAVE是录音时用的标准的WINDOWS文件格式,文件的扩展名为“WAV”,数据本身的格式为PCM或压缩型,属于无损音乐格式的一种。WAV是最接近无损的音乐格式,所以文件大小相对也比较大。

WAV编码

WAV文件编码与PCM编码类似,说白了就是基于PCM的数据体加入了44字节的文件头`,这段文件头标注了很多信息,可以看下表:

WAV文件结构
WAV文件结构

了解了WAV的文件结构之后就可以知道编码流程应该是编码PCM->设置WAV文件头->加入PCM数据体,下面是代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
* 编码wav,一般wav格式是在pcm文件前增加44个字节的文件头,
* 所以,此处只需要在pcm数据前增加下就行了。
*
* @static
* @param {DataView} bytes pcm编码之后的二进制数据
* @param {Number} inputSampleRate 输入采样率
* @param {Number} outputSampleRate 输出采样率
* @param {Number} numChannels 声道数
* @param {Number} oututSampleBits 输出采样位数
* @returns {DataView} wav二进制数据
* @memberof Recorder
*/
encodeWAV = function (bytes, inputSampleRate, outputSampleRate, numChannels, oututSampleBits) {
var sampleRate = Math.min(inputSampleRate,
outputSampleRate),
sampleBits = oututSampleBits,
buffer = new ArrayBuffer(44 + bytes.byteLength),
data = new DataView(buffer),
channelCount = numChannels, // 声道
offset = 0;
// 资源交换文件标识符
writeString(data, offset, 'RIFF');
offset += 4;
// 下个地址开始到文件尾总字节数,即文件大小-8
data.setUint32(offset, 36 + bytes.byteLength, true);
offset += 4;
// WAV文件标志
writeString(data, offset, 'WAVE');
offset += 4;
// 波形格式标志
writeString(data, offset, 'fmt ');
offset += 4;
// 过滤字节,一般为 0x10 = 16
data.setUint32(offset, 16, true);
offset += 4;
// 格式类别 (PCM形式采样数据)
data.setUint16(offset, 1, true);
offset += 2;
// 声道数
data.setUint16(offset, channelCount, true);
offset += 2;
// 采样率,每秒样本数,表示每个通道的播放速度
data.setUint32(offset, sampleRate, true);
offset += 4;
// 波形数据传输率 (每秒平均字节数) 声道数 × 采样频率 × 采样位数 / 8
data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true);
offset += 4;
// 快数据调整数 采样一次占用字节数 声道数 × 采样位数 / 8
data.setUint16(offset, channelCount * (sampleBits / 8), true);
offset += 2;
// 采样位数
data.setUint16(offset, sampleBits, true);
offset += 2;
// 数据标识符
writeString(data, offset, 'fake'); //填入fake使原视频无法播放,在需要播放时再改成data
offset += 4;
// 采样数据总数,即数据总大小-44
data.setUint32(offset, bytes.byteLength, true);
offset += 4;
// 给wav头增加pcm体
for (var i = 0; i < bytes.byteLength;) {
data.setUint8(offset, bytes.getUint8(i));
offset++;
i++;
}
return data;
};

只要沿用上一篇的encodePCM返回的DataView传入,再把对应的参数传入调用即可

1
2
3
// 输入采样率可以从window.AudioContext.sampleRate获得
// 输出采样率与音频数据的采样率对应,采样位数也是
encodeWAV(encodePCM(), inputSampleRate, outputSampleRate, 1, oututSampleBits)

WAV解码

WAV解码也是类似PCM解码,流程是移除WAV文件头->解码PCM

移除WAV文件头非常简单粗暴,就是去掉这44字节就好了(编码的时候一个一个加那么累,解码直接一刀切😂),代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
decodeWAV = function(arraybuffer, sampleBits) {
var originData = new DataView(arraybuffer);
// 新建一个文件大小减44字节的buffer空间
var buffer = new ArrayBuffer(arraybuffer.byteLength - 44);
// 新建一个dataview
var data = new DataView(buffer);
var offset = 0;
// 加入pcm体
for (var i = 44; i < buffer.byteLength;) {
data.setUint8(offset, originData.getUint8(i));
offset++;
i++;
}
// 沿用上一篇的decodePCM函数,像PCM一样解码就好了
return decodePCM(data.buffer)
};

MP3

MP3是一种音频压缩技术,其全称是动态影像专家压缩标准音频层面3(Moving Picture Experts Group Audio Layer III),简称为MP3。它被设计用来大幅度地降低音频数据量。利用 MPEG Audio Layer 3 的技术,将音乐以1:10 甚至 1:12 的压缩率,压缩成容量较小的文件,而对于大多数用户来说重放的音质与最初的不压缩音频相比没有明显的下降。

MP3编码原理

关于MP3的编码原理非常的复杂,编码算法由混合滤波器组(子带滤波器和MDCT),心理声学模型,量化编码(比特和比特因子分配和哈夫曼编码)组成,这些算法处理的就是我写过的《音频处理之变音变调》提到的波形数据,通过算法分析过滤掉听觉以外的波形数据,由于本人能力有限,就上一篇能看懂的文章给大家了解一下就好了《MP3编码算法分析

基于上述编码算法,目前最主流的编码引擎就是LAME,它有三种编码方式VBRABRCBR,具体解释可以看这篇文章《VBR、ABR和CBR三种编码方式》,它还有很多参数可选,这里就不多赘述了。

关于MP3文件结构,参考文章《MP3格式音频文件结构解析

MP3编码

目前有现成的开源项目lamejs,我可以直接用这个库来编码mp3文件。

编码流程是PCM编码->MP3编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var lamejs = require("lamejs");

/**
* mp3编码
*
* @static
* @param {dataView} bytes encodePCM的数据
* @param {number} sampleBits 采样位数
* @returns {dataview} MP3数据
*/
encodeMP3 = function(bytes, inputSampleRate){
var channels = 1; //1 for mono or 2 for stereo
var sampleRate = inputSampleRate; //44.1khz (normal mp3 samplerate)
var kbps = 128; //encode 128kbps mp3
var mp3encoder = new lamejs.Mp3Encoder(channels, sampleRate, kbps);

var samples = new Int16Array(bytes.buffer); //one second of silence (get your data from the source you have)
var sampleBlockSize = 1152; //can be anything but make it a multiple of 576 to make encoders life easier

var mp3Data = [];
for (var i = 0; i < samples.length; i += sampleBlockSize) {
var sampleChunk = samples.subarray(i, i + sampleBlockSize);
mp3buf = mp3encoder.encodeBuffer(sampleChunk);
if (mp3buf.length > 0) {
mp3Data.push(mp3buf); // fake mp3 fill(0,0,4)
}
}
var mp3buf = mp3encoder.flush(); //finish writing mp3
if (mp3buf.length > 0) {
mp3Data.push(new Int8Array(mp3buf)); //fake mp3
}
return mp3Data;
}

导出MP3文件依然是直接沿用上一篇的download函数:

1
download(data, 'recorder', 'mp3')

实测我录制了一段4分钟左右的音频,导出来的wav文件是22M,而mp3的文件大小是2.2M,足足少了十倍,按这个音频时长和大小来看,与市面上的mp3音乐文件相比基本一致,可以说是达到了商用水准哈哈😁,LAME大法🐂🍺

MP3解码

非常遗憾lamejs没有提供解码的接口,所以我还在找MP3解码的方法,找到了再补充上来。

总结

总的来说,音频的编码解码比起音频滤波算法还是比较好理解的,就是相关的文件格式对应的文件头和文件结构需要自己去查资料处理,但真要涉及MP3编码原理的就要掌握音频信号结合数据结构算法这些高级科目了,最后我还是留下了没技术的眼泪😭