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
9
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
8

单声道只包含声道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
27
28
29
30
31
32
33

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中大小端问题研究](https://rangaofei.github.io/2018/04/16/java中大小端问题研究/#more)
参考资料

1. [Audio File Format Specifications](http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html);