如果要解码 WAV 文件, 首先需要了解一下 WAV 音频文件的格式.
今天说的是 线性 PCM
对应的 WAV 数据的格式以及如何在 iOS 上面使用 faad2 进行解码和播放改格式的音频数据。
PCM 的种类
WAV 只是该音频文件的后缀名,其完整名称缩写是 WAVE.
WAVE(Waveform Audio File Format),采用RIFF(Resource Interchange File Format)文件格式结构。
WAV 格式的音频文件通常用来保存 PCM 格式的原始音频数据,通常被称之为无损音频。
WAV 音频文件,粗略来说是 WAV 数据头 + PCM 数据组成的。裸数据 PCM 外面包了一层文件头,WAV 实质为一个 RIFF 文件。
关于 WAV 音频文件的数据头定义如下图所示:
最前面的4个字节用来标示是 RIFF 字符串.
可以看出, 一般的 WAV 文件的数据头为 44 个字节, 其后面跟的是 PCM 数据。
使用 hexdump 来看一下 WAV 文件的数据头。
在当前路径下, 有个 wav 格式的音频文件 m.wav
,使用 hexdump 分析一下。
hexdump -n 44 m.wav
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
其他对应的数据, 大家可以对照表自行分析。
数据类型
这里使用结构体定义 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 数据。
主要有以下几个步骤:
将编译好的 faad2 导入工程即可,【阅读原文】可以获取编译 fadd2 的方法。
其中关键的 API 在 neaacdec.h 中有描述.
在 音视频编程: 简单分析 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];
}