概述
如果要解码 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 音频文件的数据头定义如下图所示:
最前面的4个字节用来标示是 RIFF 字符串.
可以看出, 一般的 WAV 文件的数据头为 44 个字节, 其后面跟的是 PCM 数据。
分析 WAV 数据头
使用 hexdump 来看一下 WAV 文件的数据头。
在当前路径下, 有个 wav 格式的音频文件 m.wav
,使用 hexdump 分析一下。
hexdump -n 44 m.wav
其中,-n 44
表示查看前 44 个字节。
按字节分组的图,如下所示:
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 的方法。
其中关键的 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: %in",
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];
}
最后
以上就是碧蓝小鸽子为你收集整理的聊聊 WAV的全部内容,希望文章能够帮你解决聊聊 WAV所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复