本文是作者在学习音频的过程的一些笔记记录,主要目的是为了基于工程的角度学习。没有涉及编码的原理和优化,编解码部分作为了解,没有深入研究。
声音作为波的一种,频率和振幅就成了描述波的重要属性,频率的大小与我们通常所说的音高对应,而振幅影响声音的大小。声音量化的参数有如下参数
- 采样率(sampleRate)
- 量化位数(bits)
- 通道(mono/stereo)
声音存储的原理
我们使用 Audacity 看打开一个 wav 的文件,显示如下
将其放大后可以看到如下波图
再继续放大,可以看到其实是一个一个离散的点图
采样率
世界是连续平滑的,但是我们能采集的信息是有限的。采样率是1s 中做多少次采集。例如G711的采样频率是 8khz,代表 1s 采样8000个点。波形由横轴和纵轴值组成,采样率决定了1s 内横轴的点的密度。
量化位数
量化位数又称作位宽,声音是又幅度,将整个声波的幅度在纵向进行划分,最终决定采样的点在 Y 方向的值。比如使用16bit,纵向可以划分为 2^16 的范围,由于存在正负所以分为 -32768~32767
的范围。
量化的过程是先将采样后的信号按整个声波的幅度划分成有限个区段的集合,把落入某个区段内的样值归为一类,并赋于相同的量化值。参考了这里
声道
声道常用的为 Setreo 双声道或 mono 单声道。这个比较简单就不介绍了。
未经过压缩的 PCM 就可以这样计算大小了,除8是因为需要将位转换为字节。未经过压缩的 PCM 的大小是很容易确定的,这个是重点需要记住。
size = 采样率 * 量化位数 * 声道 * 时长 / 8
其他
- 比特率 每秒传输的位数。
- 时延 发送一次音频数据的大小决定了延长多久听到声音。比如对于 PCM 如果采样率为8000khz,每次发送80个采样值,那么时延就是 1000ms * (80/8000) = 10ms。
- 音频帧 音频帧需要相对于编码方式来说,比如对于 PCM 一帧就是一个采样值16bit,其他的一些音频编码一帧可能是1024个采样。
以上部分可以参考这篇文章进一步加深理解。
编码
这里我不打算讲明白编码的原理,因为作者还没有能力写编码的算法。可以参考一下这篇入门文章的介绍。这里主要介绍一下与工程实现相关的音频编码解构组成。这里介绍了 G711a 和 AAC 的构成。
G711
G711 有 G711a(a-law) 和 G711u (u-law) 两种编码方式。二者很像,以G711a 为例子,实际是取PCM高位的13位,然后转换为8位。实际上也就是将一个16bit 的 PCM 数据压缩成8bit。具体的原理可以参考 G711的 wiki。这里不介绍详细的推导过程,只介绍一下具体的转换规则。
先放一张各个博客都放的表,然而没有一个介绍清楚的。
先介绍一下这个表的含义,s 代表符号位,x 代表该值会被忽略,也就是不会出现在编码里,abcd 代表 PCM 输入的原始值会被复制到编码里。比如PCM为 +52,对应的二进制为 0000 0000 0011 0100, 我们重新排列一下为 0 0000 0000 0110 100,向右移 >> 3 位,将16位变成13位,变成 0 0000 0000 0110,查表为第1行,转化为 10000011,解码后变成 0 0000 0000 0111,然后左移 3位还原。0000 0000 0011 1000 也就是 十进制的56。
在这里放一个G711a 和 G711u 的编码 c 文件。G711.c
G711的默认采样率是 8Khz,比特率就是 8 8 声道数 = 64kbps,量化位数仍然是16bit(注意只是压缩后变成8bit 值和量化位数没关系)。
所以我们可以看到这个转换关系 G711可以将一个16bit 的 PCM 转换为8bit 的值。压缩了一半的空间。所以你可以看到我们写 G711的 buffer 时候可以设置为PCM buffer 的二分之一,原因就是因为这个。
iOS 可以使用 AudioConverterFillComplexBuffer
做转换,也可以使用上面提供的 c 代码,效果是一样的。
AAC
AAC 是一种比较先进的压缩格式,相对 G711 会复杂很多,音质也好很多。AAC 的全名是 Advanced Audio Coding, 早在1997年就被提出,最开始是基于 MPEG-2 的开发的,在2000年在又开发出了 MP3 的后继者MPEG-4 AAC。AAC 的主要扩展名有三种:
- .aac 使用 MPEG-2 Audio Transport Stream (ADTS)的容器封装
- .mp4 使用 MPEG-4 Part14 部分的简化版封装
- .m4a 苹果的纯音频 mp4,用于区分带视频的 mp4文件
查看API 文档可以看到 『众多』的 AAC 名称如 AAC_HE,AAC_LD等。AAC 家族很庞大,一共9种规格,如下
- MPEG-2 AAC LC低复杂度规格(Low Complexity)
- MPEG-2 AAC Main主规格
- MPEG-2 AAC SSR可变采样率规格(Scaleable Sample Rate)
- MPEG-4 AAC LC低复杂度规格(Low Complexity),现在的手机比较常见的MP4文件中的音频部分就包括了该规格音频档案
- MPEG-4 AAC Main主规格
- MPEG-4 AAC SSR可变采样率规格(Scaleable Sample Rate)
- MPEG-4 AAC LTP长时期预测规格(Long Term Predicition)
- MPEG-4 AAC LD低延迟规格(Low Delay)
- MPEG-4 AAC HE高效率规格(High Efficiency)
这么多如何区分呢?
- 主规格(Main)除了增益控制之外全部功能,音质最好
- 低复杂度(LC)比较简单,没有增益控制,编码效率高
- SSR 和 LC 大体相同,但是多了增益控制
- 剩下的 LTP/LD/HE 都是用在低比特率下编码
但是由于 Main 和 LC 的音质相差不打,因此手机上目前多用的是 LC-ACC。
以上内容参考 AAC WIKI。
大小端
大端序和小端序可以参考 WIKI,简单来说就是大端序最高位字节存储在最低的内存地址处,小端序是最低位字节是存储在最低的内存地址处。
格式封装
ADTS
简单介绍一下 AAC 的封装原理。AAC 常用的封装方式有 ADTS 和ADIF 两种方式,前者是每个 Packet 前会增加一个头信息而后者只是在文件头增加头信息,此后都是音频流。因此 ADTS 更适合流,因为我们可以从每个包种分析出如何去解析音频流。
了解一下 ADTS 的头的含义,分成两个部分,一个是固定头,一个是可变头
syncword :同步头 总是0xFFF, all bits must be 1,代表着一个ADTS帧的开始
ID:MPEG Version: 0 for MPEG-4, 1 for MPEG-2
Layer:always: ‘00’
profile:表示使用哪个级别的AAC,可选的值在可以查看这里
sampling_frequency_index:表示使用的采样率下标,可选的值查看这里
private_bit :编码时候设置为0,解码时候会忽略这一位
channel_configuration: 声轨,可选值查看这里
original_copy 和 home 设置为0 即可。
第二部分
copyright_identification_bi 和 copyright_identification_start 设置为0即可。
aac_frame_length:一个ADTS帧的长度包括ADTS头和AAC原始流
adts_buffer_fullness:0x7FF固定值 说明是码率可变的码
number_of_raw_data_blocks_in_frame: 有 number_of_raw_data_blocks_in_frame+1 多少个 aac 帧,注意 这里等于0的时候也表示有1个 aac 帧。一个 AAC 帧包含1024个原始采样。
如下是摘自 LFLiveKit 的一段拼接 ADTS 的代码,注意下面代码由于采用 char 类型,一个 char 类型是8bits,需要进行一些转换,比如 syncword 就拆成两部分拼接。
- (NSData *)adtsData:(NSInteger)channel rawDataLength:(NSInteger)rawDataLength {
int adtsLength = 7;
char *packet = malloc(sizeof(char) * adtsLength);
// Variables Recycled by addADTStoPacket
int profile = 2; //AAC LC
NSInteger freqIdx = [self sampleRateIndex:self.configuration.audioSampleRate]; //44.1KHz
int chanCfg = (int)channel; //MPEG-4 Audio Channel Configuration. 1 Channel front-center
NSUInteger fullLength = adtsLength + rawDataLength;
// fill in ADTS data
packet[0] = (char)0xFF; // 11111111 = syncword
packet[1] = (char)0xF9; // 1111 1 00 1 = syncword MPEG-2 Layer CRC
packet[2] = (char)(((profile-1)<<6) + (freqIdx<<2) +(chanCfg>>2));
packet[3] = (char)(((chanCfg&3)<<6) + (fullLength>>11));
packet[4] = (char)((fullLength&0x7FF) >> 3);
packet[5] = (char)(((fullLength&7)<<5) + 0x1F);
packet[6] = (char)0xFC;
NSData *data = [NSData dataWithBytesNoCopy:packet length:adtsLength freeWhenDone:YES];
return data;
}
FLV
FLV 与 RTMP 协议息息相关。因此了解FLV 格式对 RTMP 的传送的数据也就好理解了。
FLV 格式分为 FLV Header 和 FLV Body。FLV 的格式如下图,参考了极客时间李超的课程和这篇文章。
FLV Header 有9个字节,Body 由 pre tagsize 和 tag 组成。tag 里面包含了Header 和 Data 组成。如果是视频包就是 Video Header 和 Video Data 组成,如果是音频就是 Audio Header 和 Audio Data。
FLV Header
FLV 的头一共由9个字节组成。前三个字节分别为 F
、L
、V
。第四个字节为版本,剩余 TypeFlagsAudio 和 TypeFlagsVideo 代表各一位。代表是否有音频或视频。由于 FLV 的头是固定的9个字节长度,所以 DataOffset 一直是9。其他保留为保留值,都设置为0。
FLV Body
FLV 的 Body 由 4个字节的 PreviousTagSize 和 Tag 组成。Tag 又分成 Tag 的头和 TagData。如下图所示。FLVTag.drawio
在文件格式下 SteamId 是0,后面会看到在 RTMP 流中是有用的。
完整 FLV 的文件是下面这样的一个组成,ScriptTag 紧接着 Flv Header 而且只有一个。ScriptTag 主要包含FLV 的一些Metadata 信息,如视频高宽,时长等。
ScriptTag
解构如下
这里 AMF 是 Adobe 公司的设计一种通用类型格式,在它的很多产品里都有应用。在这里你可以简单认为 AMF 是 Adobe 描述数据的一种语言。AMF 的数据类型定义如下
值 | 类型 | 说明 |
---|---|---|
0x00 | Number type | 8 Bypte Double |
0x01 | Boolean type | 1 Bypte bool |
0x02 | String type | 后面2个字节为长度 |
0x03 | Object type | |
0x04 | MovieClip type | |
0x05 | Null type | |
0x06 | Undefined type | |
0x07 | Reference type | |
0x08 | ECMA array type | 数组,类似Map |
0x09 | Object Type End | |
0x0A | Strict array type | |
0x0B | Date type | |
0x0C | Long string type | 后面4个字节为长度 |
0x0D | Unsport type |
第一个 AMF 包定义如下,第一个字节为 0x02 代表类型是String 类型,后面两个字节为 0x00 0x0a 代表长度为10,最后面十个字节保存的是 “onMetaData” 字符串。这个值目前没有什么作用。
第二个 AMF 包定义如下
第一个字节是0x08代表的是数组(这里其实更像我们用的 map),后面 4 个字节代表数组的长度。 灰色部分就是数组的定义了。从黄色部分开始就是数组的内容,第一个黄色部分代表键的长度,这里的可以用 key 参考下图。第二个也就是 duration , 第三个开始键值的类型,这里参考上面的表为 Double,Double 的是8个字节。后面就是 Duration 的值。依次类推解析出其他的值。
小技巧 一个字节是8位,两个16进制数就是一个字节。
AudioTagData
AudioTagData 的格式如下
TagData 分成音频参数和音频 Data 两部分。比如 G711的采样率为8khz,在这里选一个近似的值0即可,而 AAC 则需要选择 3。
对于除 AAC 之外的音频直接将音频数据放在音频参数之后即可,但是对于 AAC 来说还要说明 PacketType。
所以 AAC 的数据就变成了
AAC 的音频头部信息为 10 << 4 | 3 << 2 | 1 << 1 | 1 ,10 代表 aac,3代表 44.1khz,量化位数为1,最后一个是代表双声道。
对于 G711 的数据就变成了
G711a 的音频参数根据规则
头部音频参数的信息就是 7 << 4 | 0 << 2 | 1 << 1 | 0,7代表 G711a,0代表音频频率选的5.5khz,量化位数为1,最后一个是代表单声道。
音频调试
音频爆音
在调试音频的过程出现电流声,滋滋声或哒哒声,一般是由于音频数据中存在未清零的数据或者数据包没有安格式封装。
- 检查音频 Buffer 初始化的时候是否做了清零处理,避免随机数的产生
- 检查音频数据中是否按格式封装
- 检查播放端是否按格式解析
原始声音变音
- 检查采样频率是否按编码格式要求做的采集
参考文章。