聊聊 WAV

如果要解码 WAV 文件, 首先需要了解一下 WAV 音频文件的格式.

今天说的是 线性 PCM 对应的 WAV 数据的格式以及如何在 iOS 上面使用 faad2 进行解码和播放改格式的音频数据。

PCM 的种类

  • 线性化 PCM
  • A 律量化的 PCM
  • U 律量化的 PCM
  • AD PCM
  • GSM

WAV 简介

WAV 只是该音频文件的后缀名,其完整名称缩写是 WAVE.

WAVE(Waveform Audio File Format),采用RIFF(Resource Interchange File Format)文件格式结构。

WAV 格式的音频文件通常用来保存 PCM 格式的原始音频数据,通常被称之为无损音频。

WAV 音频文件,粗略来说是 WAV 数据头 + PCM 数据组成的。裸数据 PCM 外面包了一层文件头,WAV 实质为一个 RIFF 文件。

WAV 数据头

关于 WAV 音频文件的数据头定义如下图所示:
聊聊 WAV_第1张图片
最前面的4个字节用来标示是 RIFF 字符串.

可以看出, 一般的 WAV 文件的数据头为 44 个字节, 其后面跟的是 PCM 数据。

分析 WAV 数据头

使用 hexdump 来看一下 WAV 文件的数据头。

在当前路径下, 有个 wav 格式的音频文件 m.wav,使用 hexdump 分析一下。

hexdump -n 44 m.wav

聊聊 WAV_第2张图片
其中,-n 44 表示查看前 44 个字节。

按字节分组的图,如下所示:
聊聊 WAV_第3张图片

52 49 46 46

分别是 RIFF 的 ASCII 码。

跟在 RIFF 后面的四个字节是文件的大小信息,我们先使用 ls 命令看一下该文件的大小。

ls -al

输出文件大小为(字节数): 1080808

staff  1080808 Jan 25 15:44 m.wav

RIFF 后面的四个字节分别是: e0 7d 10 00,由于该存储使用了小端序(Little-Endian 存储,也就是说对其中的数据,低位字节在前,高位字节在后), 所以16进制表示为: 0x00107de0, 对应的字节大小是 1080808.

上面说到, 线性 PCM 其实在该文件头中,第17到第第22个字节(上图红色的5和6组合)标示了 PCM 的类型,即:

10 00 00 00 01 00 

其他类型的 PCM 类型定义为:

A律量化的PCM: 12 00 00 0006 00
U律量化的PCM: 12 00 00 00 07 00
AD PCM: 32 00 00 00 02 00
GSM: 14 00 00 00 31 00

最后4个字节表示真正 PCM 数据的文件大小,即: 0x00107dbc, 其10进制大小为: 1080764, 用总文件大小减去 1080764,就是文件头的大小, 如下:

1080808 - 1080764 = 44

其他对应的数据, 大家可以对照表自行分析。

定义数据头

数据类型

  • char 占用 1 个字节
  • uint32_t 占用 4 个字节
  • uint16_t 占用 2 个字节

这里使用结构体定义 WAV 文件头,其定义如下:

struct MZWavAudioFileHeader
{
    char       riff[4];       // 字符串 "RIFF"
    uint32_t   totalLength; // 文件总大小, 包括PCM 数据大小和该文件头大小
    char       wave[4];     // 字符串 "WAVE"
    char       fmt[4];      // 字符串 "fmt "
    uint32_t   format;      // WAV 头大小, 固定为值 16
    uint16_t   pcm;         // PCM 编码方式, 固定值为 1
    uint16_t   channels;    // 声道数量, 为 2
    uint32_t   frequency;   // 采样频率
    uint32_t   bytes_per_second; // 每秒字节数(码率), 其值=采样率x通道数x位深度/8
    uint16_t   bytes_by_capture; // 采样块大小
    uint16_t   bits_per_sample; // 采样点大小, 这里是 16 位
    char       data[4];         // 字符串 "data"
    uint32_t   bytes_in_pcmdata;  // pcm 数据长度
};

可以使用下面代码来计算该结构体所占的字节数(结果是44):

int wav_header_size = sizeof(struct MZWavAudioFileHeader);

实例简介

上面简单的分析了一下 WAV 的数据头协议,下面以一个实际的例子,使用 faad2 的各个函数来解码 AAC 数据。

主要有以下几个步骤:

  • 获取输入文件
  • 获取 faad 解码器句柄
  • 初始化 faad 解码器
  • 根据文件解析文件帧, 并写入输出文件中
  • 写入文件头将其封装为 WAV 格式的音频文件
  • 关闭 faad 解码器句柄

工程实战

引入 faad2

将编译好的 faad2 导入工程即可,【阅读原文】可以获取编译 fadd2 的方法。
聊聊 WAV_第4张图片
其中关键的 API 在 neaacdec.h 中有描述.

工程效果图:
聊聊 WAV_第5张图片

编码实现

在 音视频编程: 简单分析 WAV 文件 中已经定义了 WAV 的数据头.

struct MZWavAudioFileHeader
{
    char       riff[4];       // 字符串 "RIFF"
    uint32_t   totalLength; // 文件总大小, 包括PCM 数据大小和该文件头大小
    char       wave[4];     // 字符串 "WAVE"
    char       fmt[4];      // 字符串 "fmt "
    uint32_t   format;      // WAV 头大小, 固定为值 16
    uint16_t   pcm;         // PCM 编码方式, 固定值为 1
    uint16_t   channels;    // 声道数量, 为 2
    uint32_t   frequency;   // 采样频率
    uint32_t   bytes_per_second; // 每秒字节数(码率), 其值=采样率x通道数x位深度/8
    uint16_t   bytes_by_capture; // 采样块大小
    uint16_t   bits_per_sample; // 采样点大小, 这里是 16 位
    char       data[4];         // 字符串 "data"
    uint32_t   bytes_in_pcmdata;  // pcm 数据长度
};

现在实现写入数据头的方法 mz_write_wav_header

/**
 * 写入 wav 头数据.
 *
 *  @param file  wav 文件指针.
 *  @param total_samples_per_channel 每个声道的采样数.
 *  @param samplerate 采样率.
 *  @param channels 声道数.
 */
void mz_write_wav_header(FILE *file, int total_samples_per_channel, int samplerate, int channels) {
    
    if (NULL == file) {
        return;
    }
    
    if (total_samples_per_channel <= 0) {
        return;
    }
    
    printf("FAAD. total_samples_per_channel: %i, samplerate: %i, channels: %i\n",
           total_samples_per_channel, samplerate, channels);
    
    struct MZWavAudioFileHeader wavHeader;
    
    // 写入 RIFF
    strcpy(wavHeader.riff, "RIFF");
    
    wavHeader.bits_per_sample = 16;
    
    wavHeader.totalLength = (total_samples_per_channel * channels * wavHeader.bits_per_sample/8) + sizeof(wavHeader) - 8;
    
    // 写入 WAVE 和 fmt
    strcpy(wavHeader.wave, "WAVE");
    strcpy(wavHeader.fmt, "fmt ");
    
    wavHeader.format = 16;
    wavHeader.pcm = 1;
    wavHeader.channels = channels;
    wavHeader.frequency = samplerate;
    
    // 每秒的字节数(码率)=采样率x通道数x位深度/8
    wavHeader.bytes_per_second = wavHeader.channels * wavHeader.frequency * wavHeader.bits_per_sample/8;
    
    wavHeader.bytes_by_capture = wavHeader.channels*wavHeader.bits_per_sample/8;
    
    wavHeader.bytes_in_pcmdata = total_samples_per_channel * wavHeader.channels * wavHeader.bits_per_sample/8;
    
    // 写入 data
    strcpy(wavHeader.data, "data");
    
    fwrite(&wavHeader, 1, sizeof(wavHeader), file);
}

解码主要用到了 FAAD2 中的 NeAACDecDecode 函数. 函数原型如下:

void* NEAACDECAPI NeAACDecDecode(NeAACDecHandle hDecoder,
                                 NeAACDecFrameInfo *hInfo,
                                 unsigned char *buffer,
                                 unsigned long buffer_size);

对应帧定义的结构体: NeAACDecFrameInfo, 定义如下:

typedef struct NeAACDecFrameInfo
{
    unsigned long bytesconsumed;
    unsigned long samples;
    unsigned char channels;
    unsigned char error;
    unsigned long samplerate;

    /* SBR: 0: off, 1: on; upsample, 2: on; downsampled, 3: off; upsampled */
    unsigned char sbr;

    /* MPEG-4 ObjectType */
    unsigned char object_type;

    /* AAC header type; MP4 will be signalled as RAW also */
    unsigned char header_type;

    /* multichannel configuration */
    unsigned char num_front_channels;
    unsigned char num_side_channels;
    unsigned char num_back_channels;
    unsigned char num_lfe_channels;
    unsigned char channel_position[64];

    /* PS: 0: off, 1: on */
    unsigned char ps;
} NeAACDecFrameInfo;

具体的解码实现, 我放到了 Github 上面了, 大家可以去 这里 查看.

可能会遇到的问题

解码 aac, 解决采样频率和通道数不对的问题

//防止采样频率加倍
NeAACDecConfigurationPtr conf = NeAACDecGetCurrentConfiguration(decoder);
conf->dontUpSampleImplicitSBR = 1;
NeAACDecSetConfiguration(decoder, conf);
//从双声道的数据中提取单通道  
for(i=0,j=0; i<4096 && j<2048; i+=4, j+=2) {

	frame_mono[j]=pcm_data[i];
	frame_mono[j+1]=pcm_data[i+1];
}  

你可能感兴趣的:(iOS,C/C++)