Wav文件格式

目录

1、文件整体结构

1.1 RIFF Chunk块

1.2 Format Chunk区块

1.3 DATA块

1.4 文件示例分析

2、Android上Wav录制 

2.1 首先初始化AudioRecord(忽略权限相关代码):

2.2 启动录制

2.3 格式转换(Wav)

3、参考文章


1、文件整体结构

Wav文件格式_第1张图片

        WAV文件的数据体区块一般由3个区块组成:RIFF Chunk、Format Chunk和Data Chunk。如上图三个不同颜色区域。

1.1 RIFF Chunk块

        RIFF数据块长度为12字节,共有三种码段。如下表所示。

名称 偏移地址 字节数 内容
ID 0x00 4 RIFF (0x52494646H)
Size 0x04 4 fileSize - 8
Type 0x08 4 WAVE(0x57415645H)
  • RIFF Chunk类型数据块以RIFF的ID部分为标识,说明数据块类型;
  • Size是整个文件的长度减去ID和Size的长度,表征包含Type字段在内的数据块长度;
  • Type是WAVE表示后面需要Format和Data两个子区块。
1.2 Format Chunk区块
  • 此类型数据块以“fmt”的ID为标识,说明数据块类型;
  • Size表示该区块数据不包含ID和Size的长度;
  • AudioFormat表示Data区块存储的音频数据的格式,PCM音频数据的值为1;
  • NumChannels表示音频数据的声道数,其中1表示单声道,2表示双声道;
  • SampleRate表示音频数据的采样率;
  • ByteRate每秒数据字节数,计算公式为:ByteRate = SampleRate × NumChannels × BitsPerSample / 8
  • BlockAlign每个采样所需的字节数,计算公式为NumChannels*BitsPerSample/8;
  • BitsPerSample每个采样存储的bit数,其中8表示8bit,16表示16bit,32表示32bit。
1.3 DATA块

         该区块标识wav音频文件实际存储的数据

名称 偏移地址 字节数 内容
ID 0x00 4 ‘data’ (0X64617461H)
Size 0x04 4 --
Data 0x08 -- 实际音频数据

        描述WAVE文件的基本单元是"sample"(也就是采样的样本),一个sample代表采样一次得到的数据;因此如果用44KHz采样,将在一秒中得到44000个sample;每个sample可以用8位、24位,甚至32位表示(位数没有限制,只要是8的整数倍即可),位数越高,音频质量越好。

        此处有一个值得注意的细节,8位代表无符号的数值,而16位或16位以上代表有符号的数值;例如,如果有一个10bit的样本,由于sample位数要求是8的倍数,我们就需要把它填充到16位。16位中:0-5位补0,6-15位是原始的10bit数据。这就是左补零对齐原则。

        上述只是单声道,如果要处理多声道,就需要在任意给定时刻给出多个sameple。例如,在多声道中,给出某一时刻,我们需要分辨出哪些sample是左声道的,哪些sample是右声道的。因此我们需要一次读写两个sample。假如以44KHz取样立体声音频,我们需要一秒读写44*2 KHz的sample,给出公式:

        每秒数据大小(字节)= 采样率 * 声道数 * sample比特数 / 8

        处理多声道音频时,每个声道的样本是交叉存储的。我们把左右声道数据交叉存储在一起:先存储第一个sample的左声道数据,然后存储第一个sample的右声道数据。当一个设备需要重现声音时,它需要同时处理多个声道,一个sample中多个声道信息称为一个样本帧。

  • Size表示音频数据的长度,不包含ID和Size数据段,且对于采样率为ByteRate的音频文件数据来说,其计算公式为:Size = ByteRate × seconds
  • Data为实际存储的Wav音频文件数据。
1.4 文件示例分析

        下面是一个数据示例(wav文件):

Wav文件格式_第2张图片

        用表格说明一下文件的格式:

起始地址

占用空间

本地址数字的含义

00H

4byte

RIFF,资源交换文件标志(52494646H)。

04H

4byte

从下一个地址开始到文件尾的总字节数。高位字节在后面,这里就是00012C24H,换成十进制是76836byte,算上这之前的8byte就正好76844byte了。

08H

4byte

WAVE,代表wav文件格式(57415645H)。

0CH

4byte

FMT ,波形格式标志(666D7420H)。

10H

4byte

00000010H,SubChunk1Siz标识第二个区块数据大小,不包含ID和Size的长度(24 - 8 = 16byte)。

14H

2byte

为1时表示线性PCM编码,大于1时表示有压缩的编码。这里是0001H。

16H

2byte

1为单声道,2为双声道,这里是0001H。

18H

4byte

采样频率,这里是00003E80H,也就是16000Hz。

1CH

4byte

Byte率=采样频率*音频通道数*每次采样得到的样本位数/8,00007D00H,也就是32000Byte/s=16000*1*16/2。

20H

2byte

块对齐=通道数*每次采样得到的样本位数/8,0002H,也就是2=1*16/8。

22H

2byte

样本数据位数,0010H即16,一个量化样本占2byte。

24H

4byte

data,一个标志而已(64617461H)。

28H

4byte

Wav文件实际音频数据所占的大小,这里是00012C00H即76800,再加上2CH就正好是76844,整个文件的大小。

2CH

不定

量化数据。

2、Android上Wav录制 

2.1 首先初始化AudioRecord(忽略权限相关代码):
    private byte[] buffer;
    private void init() {
        int sampleRateInHz = 16000;
        int channelConfig = AudioFormat.CHANNEL_IN_DEFAULT;
        int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
        int recordMinBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
        //指定 AudioRecord 缓冲区大小
        buffer = new byte[recordMinBufferSize];
        //根据录音参数构造AudioRecord实体对象
        audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConfig, audioFormat, recordMinBufferSize);
    }
2.2 启动录制
    /**
     * 开始录制
     */
    public void start(String path, String name) {
        if (audioRecord.getState() == AudioRecord.RECORDSTATE_STOPPED) {
            recorderState = true;
            audioRecord.startRecording();
            new RecordThread(path, name).start();
        } else {
            Log.i(TAG, "start: " + audioRecord.getState());
        }
    }

 录制线程:

private class RecordThread extends Thread {


        private String cachePath;
        private String name;
        private String path;


        public RecordThread(String path, String name) {
            this.path = path;
            this.name = name;
            this.cachePath = path + "cache.pcm";
        }

        @Override
        public void run() {

            Log.i(TAG, "run: pcm目录=path" + cachePath);

            deleteFile(cachePath);

            File pcmFile = new File(path + "cache.pcm");
            boolean file = createFile(pcmFile);
            if (file)
                Log.i(TAG, "run: 创建缓存文件成功:" + cachePath);
            else {
                Log.i(TAG, "run: 创建缓存文件失败:" + cachePath);
                return;
            }
            FileOutputStream fos = null;
            try {
                fos = new FileOutputStream(cachePath);
            } catch (FileNotFoundException e) {
                e.printStackTrace();

            }

            if (fos == null) {
                Log.i(TAG, "run: 未找到缓存文件" + cachePath);
                return;
            }

            //获取到的pcm数据就是buffer
            int read;
            while (recorderState && !isInterrupted()) {
                read = audioRecord.read(buffer, 0, buffer.length);
                if (AudioRecord.ERROR_INVALID_OPERATION != read) {

                    try {
                        fos.write(buffer);
                        Log.i(TAG, "run: 写录音数据->" + read);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }
            }

            try {
                fos.flush();
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            PcmToWavUtil.getInstance().pcmToWav(cachePath, path + name);

        }
    }
2.3 格式转换(Wav)

录制完成后,会把得到的pcm原始数据转换为wav格式的音频,工具类如下:

public class PcmToWavUtil {
    private int mBufferSize; //缓存的音频大小
    private int mSampleRate = 44100;// 此处的值必须与录音时的采样率一致
    private int mChannel = AudioFormat.CHANNEL_IN_STEREO; //立体声
    private int mEncoding = AudioFormat.ENCODING_PCM_16BIT;

    private static class SingleHolder {
        static PcmToWavUtil mInstance = new PcmToWavUtil();
    }

    public static PcmToWavUtil getInstance() {
        return SingleHolder.mInstance;
    }


    public PcmToWavUtil() {
        mSampleRate = AudioRecordManager.sampleRateInHz;
        mChannel = AudioRecordManager.channelConfig;
        mEncoding = AudioRecordManager.audioFormat;
        mBufferSize = AudioRecord.getMinBufferSize(mSampleRate, mChannel, mEncoding);
        GFLog.d("AudioRecordUtil", "mChannel = "+mChannel +", mEncoding: "+ mEncoding+", mSampleRate:"+mSampleRate + ", mBufferSize = " + mBufferSize);
    }



    /**
     * pcm文件转wav文件
     *
     * @param inFilename  源文件路径
     * @param outFilename 目标文件路径
     * @param deleteOrg   是否删除源文件
     */
    public void pcmToWav(String inFilename, String outFilename, boolean deleteOrg) {
        FileInputStream in;
        FileOutputStream out;
        long totalAudioLen;
        long totalDataLen;
        long longSampleRate = 16_000;
        int channels = AudioFormat.CHANNEL_IN_DEFAULT;
        //ByteRate = SampleRate × NumChannels × BitsPerSample / 8
        long byteRate = (longSampleRate * channels * 16L) / 8;
        byte[] data = new byte[mBufferSize];
        try {
            in = new FileInputStream(inFilename);
            out = new FileOutputStream(outFilename);
            totalAudioLen = in.getChannel().size();
            totalDataLen = totalAudioLen + 36;

            writeWaveFileHeader(out, totalAudioLen, totalDataLen,
                    longSampleRate, channels, byteRate);
            while (in.read(data) != -1) {
                out.write(data);
            }
            in.close();
            out.flush();
            out.close();
            if (deleteOrg) {
                new File(inFilename).delete();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void pcmToWav(String inFilename, String outFilename) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                pcmToWav(inFilename, outFilename, false);
            }
        }).start();
    }

    /**
     * 加入wav文件头
     */
    private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
                                     long totalDataLen, long longSampleRate, int channels, long byteRate)
            throws IOException {
        byte[] header = new byte[44];
        // ChunkID, RIFF, 占4bytes(big)
        header[0] = 'R';
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        // ChunkSize, pcmLen + 36, 占4bytes
        header[4] = (byte) (totalDataLen & 0xff);
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        // Format, WAVE, 占4bytes(big)
        header[8] = 'W';
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        // Subchunk1ID, 'fmt ', 占4bytes(big)
        header[12] = 'f'; // 'fmt ' chunk
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';
        // Subchunk1Size, 16, 占4bytes(Size表示该区块(fmt)数据不包含ID和Size的长度: 24 - 8 = 16)
        header[16] = 16;
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        // AudioFormat, pcm = 1, 占2bytes
        header[20] = 1;
        header[21] = 0;
        // NumChannels, mono = 1, stereo = 2, 占2bytes
        header[22] = (byte) channels;
        header[23] = 0;
        // SampleRate, 占4bytes
        header[24] = (byte) (longSampleRate & 0xff);
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
        // ByteRate = SampleRate * NumChannels * BitsPerSample / 8, 占4bytes
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        // BlockAlign = NumChannels * BitsPerSample / 8, 占2bytes
        header[32] = (byte) (channels * 16 / 8);
        header[33] = 0;
        // BitsPerSample, 占2bytes
        header[34] = 16;
        header[35] = 0;
        // Subhunk2ID, data, 占4bytes(big)
        header[36] = 'd';
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        // Subchunk2Size, 占4bytes
        header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
        out.write(header, 0, 44);
    }
}

其中文件头的写入是重点,可参考上面的说明进行对比阅读。

3、参考文章

1)Wav文件直观展示

2)wav文件格式分析 - Dsp Tian - 博客园 (cnblogs.com) 

你可能感兴趣的:(多媒体,Wav,Android)