zoukankan      html  css  js  c++  java
  • 音频解析

    本文由三部分组成,第一部分背景介绍 —— 音频类型及本文动机,第二部分类比matlab下wavread()函数的作用,第三部分则给出该函数的C++实现。

    一 背景介绍

    1.1 本文动机

    1)所有wav音频处理的基础就是将wav格式的文件解析出来,解析成数组才能供我们去做后续的处理(fft等等)。

    2)在matlab中直接有一个很好用的函数wavread(' test.wav'),输入是wav音频,输出是数组,如第二章所述。

    3)一般的C++函数读取出来的数据,格式如1.2节所述,然而不管是什么格式,数据之间是可互相转换的。

    4) 我在解决问题的过程中,没有发现一篇详细的参考文献。

          鉴于此,本文将介绍如何用C++完全实现matlab的wavread函数,输出数据格式一模一样,在这个过程中,大家也可以领略文件中数据的本质,及相互间的转换关系。

    1.2 音频类型

    RIFF全称为资源互换文件格式(ResourcesInterchange FileFormat),RIFF文件是windows环境下大部分多媒体文件遵循的一种文件结构,RIFF文件所包含的数据类型由该文件的扩展名来标识,能以RIFF文件存储的数据包括:音频视频交错格式数据(.AVI) 波形格式数据(.WAV) 位图格式数据(.RDI) MIDI格式数据(.RMI)调色板格式(.PAL)多媒体电影(.RMN)动画光标(.ANI)其它RIFF文件(.BND)。

    Chunk是组成RIFF文件的基本单元,它的基本结构如下:

    1
    2
    3
    4
    5
    struct chunk{
      u32 id; //由4个ASCII字符组成,用以识别块中所包含的数据。如:'RIFF','LIST','fmt','data','WAV','AVI'等
      u32 size;    //块大小,是存储在data域中数据的长度,id与size域的大小则不包括在该值内
      u8 dat[size];   //块内容,数据以字(WORD)为单位排列,如果该数据结构长度是奇数,则最后添一个NULL字节
    };
    1.3 wav音频文件

    WAVE 文件作为多媒体中使用的声音波形文件格式之一,它是以RIFF(Resource Interchange File Format)格式为标准的。每个WAVE文件的头四个字节便是“RIFF”。同样的,WAVE 文件由文件头和数据体两大部分组成。其中文件头又分为 RIFF/WAV 文件标识段和声音数据格式说明段两部分。WAVE文件各部分内容及格式见后文。

    常见的声音文件主要有两种,分别对应于单声道(11.025KHz 采样率、8Bit 的采样值)和双声道(44.1KHz 采样率、16Bit 的采样值)。采样率是指:声音信号在“模→数”转换过程中单位时间内采样的次数。采样值是指每一次采样周期      
    内声音模拟信号的积分值。

    对于单声道声音文件,采样数据为八位的短整数(short int 00H-FFH);而对于双声道立体声声音文件,每次采样数据为一个16位的整数(int),高八位和低八位分别代表左右两个声道。

    WAVE 文件数据块包含以脉冲编码调制(PCM)格式表示的样本。WAVE 文件是由样本组织而成的。在单声道 WAVE 文件中,声道0代表左声道,声道1代表右声道。在多声道WAVE文件中,样本是交替出现的。

    WAVE 文件除了前面一小段文件头对数据组织进行说明之外,Data 块就是声音的原始采样数据,WAVE 文件虽然可以压缩,但一般都使用不压缩的格式。44.1KHz 采样率、16Bit的分辨率、双声道,所以WAVE可以保存音质要求非常高的声音文件,CD 采用的也是这种格式,声音方面的专家或是音乐发烧友们应该非常熟悉。但这种文件的体积也非常大,以 44.1KHz 16bit 双声道的数据为例,一分钟的声音数据量为:4100*2byte*2channel*60s/1024/1024=10.09M 。所以不合适在网上传送。


    下面我们具体地分析 WAVE 文件的格式

    endian

    field name

    Size

     
    big ChunkID 4 文件头标识,一般就是" RIFF" 四个字母
    little ChunkSize 4 整个数据文件的大小,不包括上面ID和Size本身
    big Format 4 一般就是" WAVE" 四个字母
    big SubChunk1ID 4 格式说明块,本字段一般就是"fmt "
    little SubChunk1Size 4 本数据块的大小,不包括ID和Size字段本身
    little AudioFormat 2 音频的格式说明
    little NumChannels 2 声道数
    little SampleRate 4 采样率
    little ByteRate 4 比特率,每秒所需要的字节数
    little BlockAlign 2 数据块对齐单元
    little BitsPerSample 2 采样时模数转换的分辨率
    big SubChunk2ID 4 真正的声音数据块,本字段一般是"data"
    little SubChunk2Size 4 本数据块的大小,不包括ID和Size字段本身
    little Data N 音频的采样数据

    以下是对各个字段的详细解说:

    ChunkID 4bytes ASCII 码表示的“RIFF”。(0x52494646)
    ChunkSize 4bytes 36+SubChunk2Size,或是            
    4 + ( 8 + SubChunk1Size ) + ( 8 + SubChunk2Size ),             
    这是整个数据块的大小(不包括ChunkID和ChunkSize的大小)
    Format 4bytes ASCII 码表示的“WAVE”。(0x57415645)
         
    SubChunk1ID   新的数据块(格式信息说明块)            
    ASCII 码表示的“fmt ”——最后是一个空格。(0x666d7420)
    SubChunk1Size 4bytes 本块数据的大小(对于PCM,值为16)。
    AudioFormat 2bytes PCM = 1 (比如,线性采样),如果是其它值的话,则可能是一些压缩形式
    NumChannels 2bytes 1 => 单声道  |  2 => 双声道
    SampleRate 4bytes 采样率,如 8000,44100 等值
    ByteRate 4bytes 等于: SampleRate * numChannels * BitsPerSample / 8
    BlockAlign 2bytes 等于:NumChannels * BitsPerSample / 8
    BitsPerSample 2bytes 采样分辨率,也就是每个样本用几位来表示,一般是 8bits 或是 16bits
    SubChunk2ID 4bytes 新数据块,真正的声音数据            
    ASCII 码表示的“data ”——最后是一个空格。(0x64617461)
    SubChunk2Size 4bytes 数据大小,即,其后跟着的采样数据的大小。
    Data N bytes 真正的声音数据

    对于Data块,根据声道数和采样率的不同情况,布局如下(每列代表8bits):

    1). 8 Bit 单声道:

    采样1 采样2
    数据1 数据2

    2). 8 Bit 双声道

    采样1   采样2  
    声道1数据1 声道2数据1 声道1数据2 声道2数据2

    3). 16 Bit 单声道:

    采样1   采样2  
    数据1低字节 数据1高字节 数据1低字节 数据1高字节

    4). 16 Bit 双声道

    采样1      
    声道1数据1低字节 声道1数据1高字节 声道2数据1低字节 声道2数据1高字节
    采样2      
    声道1数据2低字节 声道1数据2高字节 声道2数据2低字节 声道2数据2高字节

     下面我们看一个具体的例子,wav音频文件如下:(十六进制的形式)

    52 49 46 46 24 08 00 00 57 41 56 45 
    66 6d 74 20 10 00 00 00 01 00 02 00 
    22 56 00 00 88 58 01 00 04 00 10 00 
    64 61 74 61 00 08 00 00 00 00 00 00 
    24 17 1e 3c 13 3c 14 16 18 34 23 3c 24 11 1a 0d

    对应的分析如下图所示:

    untitled

         举例分析数据:形如 'FFFF' 为一个我们需要的完整的数据。如上图中 sample3:3c 和 13是两个数组合在一起是一个我们需要的数, 3c 13,但右端为大端,则应为 3c 13,十六进制数3c按位转换为2进制为0011 1100,同理13按位转换为2进制为0001 0011,则连起来的16bits的二进制数为0011 1100 0001 0011,那么我们可以看到符号位为0,即为正数。

    二 matlab中的wavread( )函数

    1. wavread('testwav.wav' )

      读者试试看输出。例如,取我的一个声音文件'testwav.wav',输出的最后10个数据为:

            -0.0001 -0.0001 -0.0002 -0.0003 -0.0002 -0.0002  -0.0002  -0.0003  -0.0002  -0.0002

       2.  wavread('testwav.wav','native')

            读者可以试试看输出。我的'testwav.wav' 输出的最后10个数据为:

             -4  -2  -8  -9  -7  -8  -8  -11  -5  -7

    1 和 2 的输出数据之间的转换公式为:-0.0002 = -7 / 32768 (其中32768 = 2 ^15,即2的15次幂。这是归一化。因为编码为16bits)

    三 readwav的 C++实现

    上面介绍了这么多,我们来进入主题,怎么用C++实现matlab中的wavread('testwav.wav')函数,且输出一致。

    3.1 编码转换规则

    在介绍之前,我们需要了解这几串数据之间的关系。本章节以test.wav文件的数据为例来分析:

    (1)该wave文件的Data块即原始采样数据的最后20个数据是:

    1
     fc ff fe ff f8 ff f7 ff f9 ff f8 ff f8 ff f5 ff fb ff f9 ff

    (2)在matlab中解析得到的最后10个数据是:

    1
    -0.0001 -0.0001 -0.0002 -0.0003 -0.0002 -0.0002  -0.0002  -0.0003  -0.0002  -0.0002

         这两组数据之间是原码与补码的关系,即(1)是原码而(2)是补码。

    由数据(1)转换为数据(2)的步骤是:先将(1)转换为其补码,再用补码除以32768,则得到(2)。

    原码与补码之间的转换原则:

    (2进制形式的转换):若原码为正数,则补码是其本身。若原码为负数,则补码为符号位不变,数值位按位取反,再加1。

    (数值形式的转换):若原码为正数,则补码是其本身。若原码为负数,补码 = 原码 - 2^16。温馨提示: 为了方便计算数值上有等价替换 2^16 = FFFF - 1。

    为了更好的理解,举例说明:

    步骤一(每次读16字节):由于数据是从X0000到XFFFF的数据。以f9 ff为例,右端为大端,换言之,右端是高位,则应该是fff9。步骤二(转换为补码):按位转换为二进制形式为1111 1111 1111 1001(1位16进制数值对应4位二进制数值),该数据为原码,转换成带符号的十进制形式,先看符号位判断其为负数,则补码为FFF9 - FFFF -1 = -7。步骤三(归一化):用补码数值-7除以32768,取小数点后4位(四舍五入),则等于-0.0002,正确。

    读者可以试着用我的方法算一下(1)中的右起第3第4个数,是否对应等于(2)的右起第2个数。

    3.2 C++实现

    那么C++实现,就是先读取原始采样数据,每次读16字节,然后将16字节的16进制数字转化成十进制数,再转换成其补码,并归一化。转换时注意大小端和符号问题。

    具体的C++代码,我已分享,读者可移步查看:http://www.oschina.net/code/snippet_1768500_39013

    参考文献

    1. http://www.cnblogs.com/liyiwen/archive/2010/04/19/1715715.html

    本文原文:http://my.oschina.net/liusicong/blog/323090

  • 相关阅读:
    Code Forces 650 C Table Compression(并查集)
    Code Forces 645B Mischievous Mess Makers
    POJ 3735 Training little cats(矩阵快速幂)
    POJ 3233 Matrix Power Series(矩阵快速幂)
    PAT 1026 Table Tennis (30)
    ZOJ 3609 Modular Inverse
    Java实现 LeetCode 746 使用最小花费爬楼梯(递推)
    Java实现 LeetCode 745 前缀和后缀搜索(使用Hash代替字典树)
    Java实现 LeetCode 745 前缀和后缀搜索(使用Hash代替字典树)
    Java实现 LeetCode 745 前缀和后缀搜索(使用Hash代替字典树)
  • 原文地址:https://www.cnblogs.com/hwl1023/p/4774330.html
Copyright © 2011-2022 走看看