SAKA'S BLOG

音频(三)-wav格式与处理

wav格式与处理

pcm格式简介

PCM(脉冲编码调制)就是把一个时间连续,取值连续的模拟信号变换成时间离散,取值离散的数字信号后在信道中传输。脉冲编码调制就是对模拟信号先抽样,再对样值幅度量化,编码的过程。也就是前边的说过的采样-量化过程,这个过程可以较好的存储原始的音频模拟信号,并真实还原。但是PCM并不是一种友好的存储格式,所以一些机构定制了一些准则来记录PCM信号,包括声道、采样率、位宽等信息,比较通用的就是windows平台下的wav格式和osx平台下的aiff格式,这两种都可以以一种友好可读的方式保存PCM信息。

wav格式简介

以下内容来源百度:
WAV为微软公司(Microsoft)开发的一种声音文件格式,它符合RIFF(Resource Interchange File Format)文件规范,用于保存Windows平台的音频信息资源,被Windows平台及其应用程序所广泛支持,该格式也支持MSADPCM,CCITT A LAW等多种压缩运算法,支持多种音频数字,取样频率和声道,标准格式化的WAV文件和CD格式一样,也是44.1K的取样频率,16位量化数字,因此在声音文件质量和CD相差无几! WAV打开工具是WINDOWS的媒体播放器。
通常使用三个参数来表示声音,量化位数,取样频率和采样点振幅。量化位数分为8位,16位,24位三种,声道有单声道和立体声之分,单声道振幅数据为n1矩阵点,立体声为n2矩阵点,取样频率一般有11025Hz(11kHz) ,22050Hz(22kHz)和44100Hz(44kHz) 三种,不过尽管音质出色,但在压缩后的文件体积过大!相对其他音频格式而言是一个缺点,其文件大小的计算方式为:WAV格式文件所占容量(B) = (取样频率 X量化位数X 声道) X 时间 / 8 (字节= 8bit) 每一分钟WAV格式的音频文件的大小为10MB,其大小不随音量大小及清晰度的变化而变化。
WAV是最接近无损的音乐格式,所以文件大小相对也比较大。

一张经典的wav格式图来简单讲解一下:
wav格式

这张图表明了wav个基本结构和存储信息的格式:

  1. 所有的字符采用big-endian存储,所有的数字采用little-endian存储。
  2. headerchunk下包含fmtchunk和datachunk
  3. 每个chunk包含chunkId,chunkSize和chunkData
  4. headerchunk指明了RIFF格式的具体格式,wav格式必须是“WAVE”
  5. fmtchunk指明了pcm文件的一些基本格式,采样率、声道、位宽等
  6. datachunk是用来存储具体的PCM数据

1. headerchunk介绍

headerchunk是一个总章,包含了最基本的wav格式的信息:

字段名称 字段长度 大小端 表示信息
chunkId 4byte big-endian 这个地方必须是“RIFF”
chunkSize 4byte little-endian 表示该文件除了chunkId和chunkSize以外的文件剩余大小
Format 4byte big-endian wav文件必须是“WAVE”

上表可以看出来headerchunk总共占24byte

2.formatchunk介绍

formatchunk是wav格式中最重要的信息,包含了该文件在读取时的所有信息。

字段名称 字段长度 大小端 表示信息
chunkId 4byte big-endian 这个地方必须是“fmt ”(注意最后是空格补齐)
chunkSize 4byte little-endian 表示该文件除了chunkId和chunkSize以外的文件剩余大小
AudioFormat 2byte little-endian 表示音频的编码格式,一般pcm编码用1表示见表
Num channels 2byte little-endian 表示音频的声道个数,1表示单声道(MONO),2表示双声道(STEREO)
SampleRate 4byte little-endian 表示音频的采样率,比如44100Hz
ByteRate 4byte little-endian 表示音频的比特率
BlockAlign 2byte little-endian 表示音频的块长度,也就是单元长度
bitPerSample 2byte little-endian 表示位宽,一般是8或者16,或者更高
extensionChunk 长度不定 由chunkSize决定

chunksize一般是是16,表示出chunkid和chunksize以外的所有formatchunk的字节。假如是16的时候则不会有extensionchunk,大于16的时候则会产生这个字段,这个字段的长度就是chunksize-16,后边有介绍这个字段。

ByteRate表示音频数据的比特率,也就是每秒的数据大小,这个值=Numchannels*SampleRate*BitPerSample/8.

BlockAlign表示一个音频数据块的长度,取决于位宽(bitPerSample)和声道(NumChannels),这个值=NumbleChannels*bitPerSample/8.

下表表示AudioFormat的详细内容,注意写入的时候小端字节序

formatcode data fact
0x0001 PCM
0x0003 IEEE float yes
0x0006 8-bitITU G.711 A-law yes
0x0007 8-bitITU G.711 µ-law yes
0xFFFE 由extensionchunk中的subformat决定

3.datachunk介绍

datachunk记录了真实的pcm数据并将它简单封装

字段名称 字段长度 大小端 表示信息
chunkId 4byte big-endian 这个地方必须是“data”
chunkSize 4byte little-endian 表示data的数据长度
data

4.extensionchunk介绍

该部分是可选部分,由formatchunk部分的chunksize决定,当chunksize大于16的时候,必然包含该块。它本身是属于formatchunk,但是我为了方便专门提取出来。

字段名称 字段长度 大小端 表示信息
chunksize 2byte little-endian 可以是0或者22
ValidBitsPerSample 2byte little-endian
ChannelMask 4byte big-endian 表示扬声器的位置
SubFormat 16byte GUID,包含数据的编码格式,最后14byte是固定的\x00\x00\x00\x00\x10\x00\x80\x00\x00\xAA\x00\x38\x9B\x71.

5.factchunk介绍

在Rev.3之后,所有的非PCM压缩格式都必须包含factchunk。这个部分最少包含一个值,就是采样数量

字段名称 字段长度 大小端 表示信息
chunkId 4byte big-endian 这个地方必须是“fact”
chunkSize 4byte little-endian 最小值是4
samplelength 4byte big-endian 每个声道的采样数量

其实在Rev.3之后,所有的新wav文件建议有fac这个字段,IEEE float格式必须有这个字段,但PCM格式的编码并不一定必须有这个字段。

6.peakchunk介绍

这个字段基本很少见了,
|字段名称|字段长度|大小端|表示信息
|—|—|—|—
|chunkId|4byte|big-endian|这个地方必须是“PEAK”
|chunkSize|4byte|little-endian|表示该文件除了chunkId和chunkSize以外的文件剩余大小
|data|不定|我也不知道啥意思

实例分析

我现在从cd上考过来一首音乐-17.wav,这个文件总共有48627746字节,是wav格式编码的。具体内容如下:

1
2
3
4
00000000: 5249 4646 1a00 e602 5741 5645 666d 7420  RIFF....WAVEfmt
00000010: 1000 0000 0100 0200 44ac 0000 10b1 0200 ........D.......
00000020: 0400 1000 6461 7461 90ff e502 0000 0000 ....data........
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................

这是通过vim打开并转换为16进制查看格式来表示的一段代码。
我们来详细分析这一段:

5249 4646 1a00 e602 5741 5645这段是headerchunk字段,前四个字节和后四个字节可以转换为asc码

1
2
3
52 - R 49 - I 46 -F  46 - F //这段表示RIFF
1a 00 e6 02 //转换int后为48627738,正好是文件大小减去8,8就是RIFF和这个长度的字节
57 - W 41 - A 56 - V 45 - E //这段表示WAVE

666d 7420 1000 0000 0100 0200 44ac 0000 10b1 0200 0400 1000这段表示fmt字段:

1
2
3
4
5
6
7
8
666d 7420   //转换为asc为fmt 
1000 0000 //表示chunksize,转为大端后为16
0100 //表示编码格式,此处为1,表示是PCM编码
0200 //表示声道数量,此处为2,表示双声道
44ac 0000 //表示采样率,转换后等于44100Hz
10b1 0200 //表示比特率,每秒传送的字节数,此处为176400
0040 //表示单元块长度,此处为4,双声道,位宽为16
1000 //表示位宽,或者说是每次采样的bit,转换后为16

PCM数据的单声道和双声道

音频在存储的时候可以根据mic的数量来决定录入的轨道是双声道还是单声道,但是现代技术也可以用1个mic录制双声道音频。
单声道PCM数据是按采样的时间顺序排列,双声道的PCM数据是按采样的时间交替存储两个声道的数据,在存储的时候是按下边的图排列的:

1
2
3
4
5
6
7

单声道只包含声道0,双声道包含声道0(左)和声道1(右)

MONO 8bitPCM---->|声道0|声道0|...
STEREO 8bitPCM---->|声道0|声道1|声道0|声道1|...
MONO 16bitPCM--->|声道0(低字节)|声道0(高字节)|声道0(低字节)|声道0(高字节)|...
STERO 16bitPCM--->|声道0(低字节)|声道0(高字节)|声道1(低字节)|声道1(高字节)|声道0(低字节)|声道0(高字节)|声道1(低字节)|声道1(高字节)|...

在播放的时候回根据设置好的blockalign读取一块数据,这块数据会根据对应的声道数分发给扬声器。利用这个特性我们可以提取双声道的数据为单声道,也可以将单声道数据合并为双声道数据,同时也可以将双声道设置为只播放左耳和只播放右耳。
这里主要讲的是声音的采集,暂时不讲声音的播放。

安卓实现PCM录制并封装为wav格式

上一篇中讲解的是利用MediaRecord来录制音频,但是这种录制方式得到的是最终的文件,假如我们需要录制无损音频,然后包装为wav格式,这种方式就不支持了。AudioRecorder是系统提供的一个api,这个api虽然不是很好用,但是用来录制音频还算可以。

AudioRecord类是在sdk提供的java层的音频录制工具。在api21的时候这个类的方法还比较少。当我们使用这个类的时候,必须使用read方法来从缓冲区不断的拉出数据来,假如未能及时从缓冲区取出数据,则数据在缓冲区会积累并抛出异常。我曾经尝试在read的时候讲左声道的数值全部置为0,并且采用的是同步的处理方式,左声道置零的操作时间过长,导致数据溢出抛出异常,根本不能工作。
简单贴一下代码:

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
private void startRecord() throws IOException {
Log.e("---", "start");
int bufferSize = AudioRecord.getMinBufferSize(22050,
AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, 22050,
AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);
byte[] audio = new byte[bufferSize];
audioRecord.startRecording();

Log.e("---", "samplerate=" + audioRecord.getSampleRate() + ",channelcount" + audioRecord.getChannelCount());
isRecording = true;
fileName = "" + System.currentTimeMillis() + ".wav";
File file = new File(Environment.getExternalStorageDirectory(), fileName);
if (!file.exists()) {
file.createNewFile();
}
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file, true));
pcmToWav(bos);
while (isRecording) {
audioRecord.read(audio, 0, bufferSize);
bos.write(audio);
}
audioRecord.stop();
bos.flush();
bos.close();
long length = file.length() - 44;
Log.e("-----", "" + length);
RandomAccessFile raf = new RandomAccessFile(file, "rwd");
raf.seek(4);
raf.write(ByteUtil.intToLittleByte((int) length + 36));
raf.seek(40);
raf.write(ByteUtil.intToLittleByte((int) length));
raf.close();

}

我是在最开始的时候将chunk写入文件,录制完成后计算差值来更改chunk部分的数值大小。

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

private void pcmToWav(BufferedOutputStream fileOutputStream) {
try {
fileOutputStream.write(HEADER_CHUNK.getBytes());
fileOutputStream.write(ByteUtil.intToLittleByte(36));
fileOutputStream.write(FORMAT.getBytes());

fileOutputStream.write(FMT_CHUNK.getBytes());
fileOutputStream.write(ByteUtil.intToLittleByte(16));
fileOutputStream.write(ByteUtil.shortToLittleByte((short) 1));
fileOutputStream.write(ByteUtil.shortToLittleByte((short) (audioRecord.getChannelCount())));
fileOutputStream.write(ByteUtil.intToLittleByte(audioRecord.getSampleRate()));
fileOutputStream.write(ByteUtil.intToLittleByte(2 * 22050 * 16 / 8));
fileOutputStream.write(ByteUtil.shortToLittleByte((short) (2 * 16 / 8)));
fileOutputStream.write(ByteUtil.shortToLittleByte((short) 16));

fileOutputStream.write(DATA_CHUNK.getBytes());
fileOutputStream.write(0);
fileOutputStream.write(0);
fileOutputStream.write(0);
fileOutputStream.write(0);
fileOutputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}

在上述的代码中主要是注意大小端的问题,关于这个问题可以参考我的另一篇文章:

java中大小端问题研究
参考资料

  1. Audio File Format Specifications;