前言
大家是否还记得,在004-视频H264编码详解(上)中的八、AVFoundation采集视频数据实现(3)
章节下,说到音频和视频捕捉后,所触发的回调方法都是👇🏻
视频H264
的原理
、编解码
以及渲染显示
都已分析完毕,剩下的就是本篇文章要分析的音频编解码
部分了。
一、音频原理知识点
首先我们来了解下,音频相关的几个知识点。
1.1 声音
声音(sound)
是由物体振动产生的声波,是通过介质
(空气或固体、液体)传播并能被人或动物听觉器官所感知的波动
现象。我们都学过初中物理学
,知道声音由3要素构成👇🏻
- 音调(音频):声音的
高低
(高音、低音),由“频率”(frequency)
决定,频率越高音调越高(频率单位Hz(hertz)
) - 音量:即声音振动的幅度,人主观上感觉声音的大小。
- 音色:又称
音品
,波形决定了声音的音色。声音因物体材料
的特性而不同,音色本身是一种抽象的东西,但波形
是把这个抽象直观的表现。波形不同,音色则不同。
不同的音色,通过波形,完全可以分辨的。
心理声学模型
上图中可以看见,人类的听觉范围在20Hz到20000Hz
之间,低于20Hz的称作次声波
,高于20000Hz的称作超声波
。次声波
和超声波
我们都听不见,所以在音频流编解码时,我们就可以干掉它们。
1.2 脉冲编码调制(PCM)
那么现在问题来了 👉🏻 如何将现实⽣生活中的声⾳音转换成数字信号?
这就是即将介绍的脉冲编码调制(PCM)
,仅做了解即可。
将声音转变成数字信号的过程,如下图 👇🏻
大致分3个阶段👇🏻
- 抽样
- 量化
- 编码
假设一个模拟信号f(t)
通过一个开关,则开关的输出与开关的状态有关,
- 当开关处于
闭合状态
, 开关的输出就是输入,即y(t) = f(t) - 若开关处于
断开位置
,输出y(t)就为零
可见,如果让开关受一个窄脉冲串(序列)
的控制,则脉冲出现时开关闭合,脉冲消失时开关断开,此输出的y(t)
就是一个幅值变化的脉冲串(序列),每个脉冲的幅值就是该脉冲出现时刻输入信号f(t)
的瞬间值,因此,y(t)就是对f(t)抽样后的信号
或称样值信号
。
图3-2(a),是一个以Ts
为时间间隔的窄脉冲序列p(t)
.因为要⽤用它进行抽样,所以称为抽样脉冲
。
图3-2(b),v(t)
是待抽样的模拟电压信号
, 抽样后的离散信号k(t)
的取值分别为 k(0) =
0.2 , k(Ts) = 0.4 , k(2Ts) = 1.8 , k(3Ts) = 2.8 , k(4Ts) = 3.6 , k(5Ts) = 5.1 , k(6Ts) = 6.0 , k(7Ts) = 5.7 , K(8Ts) = 3.9 , K(9Ts) = 2.0 , k(10Ts) = 1.2; 可⻅取值在0-6
之间是随机
的,也就是说可以有无穷个可能的取值。
图3-2(c),为了把无穷个可能取值变成有限
个,我们必须对k(t)
的取值进行量化(即四舍 五入)
,得到m(t)
。则m(t)的取值变为,m(0) =
0.0 , m(Ts) = 0.0 , m(2Ts) = 2.0 , m(3Ts) = 3.0 , m(4Ts) = 4.0 , m(5Ts) = 5.0 , m(6Ts) =
6.0 , m(7Ts) = 6.0 , m(8Ts) = 4.0 , m(9Ts) = 2.0 , m(10Ts) = 1.0; 总共只有0-6
等7个可能的取值。
图3-2(d),m(t)
已经变成数字信号
,但还不是实际应⽤中的二进制数字信号,因此对m(t) ⽤用3位二进制编码元进⾏自然编码就得到图3- 2(d)的数字信号d(t)
。从⽽完成了A/D转换
,实 现了脉冲编码调制。即PCM全过程。
1.3 了解量量化过程
量化 👉🏻 将一个连续函数
的⽆限个数值
的集合映射
为一个离散函 数
的有限个数值
的集合。
量化值
👉🏻 确定的量化后的取值量化级
👉🏻 量化值的个数量化间隔
👉🏻 相邻的2个量化值之差
上图中,v(t)
的样值信号k(t)
和 量化后的量化信号m(t)
是不一样的,例如:k(0) = 0.2 而 m(0) = 0。
接收端
只能是量化后的信号m(t)
,⽽不能恢复出k(t)
.这样就使得收发信号之间产生误差
,这称为量化误差或量量化噪声
。
采用的是“四舍五入”
进来量化,因此量化噪声的最⼤值是0.5
。一般来说,量化噪声,最大绝对误差值是0.5个量化间隔
。这个量化间隔都一样
的量化叫做均匀量化
。
1.4 音频压缩编码原理 & 标准
数字音频的质量取决于:采样频率
和 量化位数
这2个参数,为了保证在时间变化方向上取样点尽量密,取样频率要高;在幅度取值上尽量细,量化⽐特率要高。那么直接的结果就是存储容量
及传输信道容量
要求的压力
。
音频信号的传输率 = 取样频率 * 样本量化⽐特数 * 通道数
音频压缩编码原理
-
有损编码
有损编码即消除了冗余数据
,因为采集过程中,各种频率的声音都有,其中我们可以丢弃人耳无法听到
的那部分声音数据,直接从数据源中干掉!这样就能大大减少数据的存储。 -
无损编码
无损编码也称作哈夫曼编码
,除了人耳部分听不到的声音
压缩之外,其他的声音数据(即人耳听得到的声音
)都原样保留
!并且压缩后的数据能完全复原
!(短码高频,长码低频) -
压缩的方法
- 去除采集的
音频冗余信息
!包括人耳听觉范围以外
的数据,被遮蔽
的音频信号等。 遮蔽效应
👉🏻 一个较弱的声音会被一个较强的声音所影响!遮蔽效应
的信号表现 👉🏻频域遮蔽
和时域遮蔽
。
- 去除采集的
-
音频压缩编码格式
- MPEG-1
- 杜比AC-3
- MPEG-2
- MPEG-4
- AAC
标准
- 取样频率 = 44.1kHz
- 样本值的量化比特数 = 16
- 普通立体声的信号通道数 = 2
- 数字信号传输码流⼤约 1.4M bit/s
- 一秒钟的数据量为 1.4Mbit / (8/Byte) 达176.4Byte(字节),等于88200个汉字的数据量
二、频域遮蔽与时域遮蔽
接下来,我们重点看看频域遮蔽
与时域遮蔽
具体表示什么?
2.1 频域遮蔽
x轴是频域值
。这里是20hz开始的,因为人听不到20hz以下
频率的声音y轴是分贝值
。40分贝以下
的声音,人也是听不到的- 再看
紫色柱
,这部分声音听得到,但是周边出现了红色柱
,柱很高表示声音特别大,把周边的紫色矮柱子
都遮蔽
了 - 而
绿色柱
的声音,频域值比紫色和红色都小,证明这个声音比较磁性
绿色
和红色
碰撞后,就好比男生和女生吵架,肯定干不过,注定输,😂- 但是,
绿色柱
离红色柱
有段距离
,说明他俩的频域值相差比较大
,以第三方的角度听,他俩的声音是都能听得到
的
2.2 时域遮蔽
- 图中,
simultaneous
这一时段,有一个高音
(顶部横线),有一个低音
(底部横线),同时发声,那么高音会完全遮蔽低音
- 有个特殊情况 👉🏻
pre
阶段,开始有个小声音在发声,突然(很短时间内)有个大的声音响了,此时小声音也会被遮蔽掉 - 声音是有个
传播时间
的,有一定的时间影响
,大概向前遮蔽50ms左右
,向后遮蔽100ms左右
三、音频AAC编码
音频AAC编码
和视频的完全不同,千万不要参考
之前的视频编码
的流程!视频编码使用的是VideoToolBox
,而音频则是AudioToolBox
。
我们还是一样,对音频的编码也封装一个工具类。
3.1 编码工具类头文件 & 初始化
音频参数配置类
首先,初始化同视频编码
的处理方式一样,我们也定义一个音频参数配置类
,命名为CCAudioConfig
👇🏻
/**音频配置*/
@interface CCAudioConfig : NSObject
/**码率*/
@property (nonatomic, assign) NSInteger bitrate;//96000)
/**声道*/
@property (nonatomic, assign) NSInteger channelCount;//(1)
/**采样率*/
@property (nonatomic, assign) NSInteger sampleRate;//(默认44100)
/**采样点量化*/
@property (nonatomic, assign) NSInteger sampleSize;//(16)
+ (instancetype)defaultConifg;
@end
实现部分👇🏻
@implementation CCAudioConfig
+ (instancetype)defaultConifg {
return [[CCAudioConfig alloc] init];
}
- (instancetype)init
{
self = [super init];
if (self) {
self.bitrate = 96000;
self.channelCount = 1;
self.sampleSize = 16;
self.sampleRate = 44100;
}
return self;
}
@end
工具类回调
/**AAC编码器代理*/
@protocol CCAudioEncoderDelegate <NSObject>
- (void)audioEncodeCallback:(NSData *)aacData;
@end
音频同视频不同,我们音频编码后,直接输出的是二进制数据NSData
。
工具类头文件
工具类命名为CCAudioEncoder
,头文件中应包含初始化方法
和编码方法
👇🏻
/**AAC硬编码器*/
@interface CCAudioEncoder : NSObject
/**编码器配置*/
@property (nonatomic, strong) CCAudioConfig *config;
@property (nonatomic, weak) id<CCAudioEncoderDelegate> delegate;
/**初始化传入编码器配置*/
- (instancetype)initWithConfig:(CCAudioConfig*)config;
/**编码*/
- (void)encodeAudioSamepleBuffer: (CMSampleBufferRef)sampleBuffer;
@end
初始化流程
和视频编码
一样,我们也需要定义2个队列 👉🏻 编码队列
+ 回调队列
,分别异步
处理音频编码和回调结果👇🏻
@property (nonatomic, strong) dispatch_queue_t encoderQueue;
@property (nonatomic, strong) dispatch_queue_t callbackQueue;
还需要音频编码相关的属性👇🏻
//对音频转换器对象
@property (nonatomic, unsafe_unretained) AudioConverterRef audioConverter;
//PCM缓存区
@property (nonatomic) char *pcmBuffer;
//PCM缓存区大小
@property (nonatomic) size_t pcmBufferSize;
初始化方法中,就是针对以上这些属性的处理👇🏻
- (instancetype)initWithConfig:(CCAudioConfig*)config {
self = [super init];
if (self) {
//音频编码队列
_encoderQueue = dispatch_queue_create("aac hard encoder queue", DISPATCH_QUEUE_SERIAL);
//音频回调队列
_callbackQueue = dispatch_queue_create("aac hard encoder callback queue", DISPATCH_QUEUE_SERIAL);
//音频转换器
_audioConverter = NULL;
_pcmBufferSize = 0;
_pcmBuffer = NULL;
_config = config;
if (config == nil) {
_config = [[CCAudioConfig alloc] init];
}
}
return self;
}
3.2 编码前的准备
在对音频编码之前,我们需要配置音频编码的参数
,其中涉及到音频相关的结构体,创建转换器 和 配置属性等函数。
3.2.1 音频参数结构体
音频参数结构体是AudioStreamBasicDescription
👇🏻
这个结构体提供了对于音频文件的描述
。
音频文件的产生是
模拟信号
-> PCM以后的数字信号
-> 压缩、编码以后的音频文件
。
- PCM时采样频率叫做
sample rate
。 - 每一次采样可以得到若干采样数据,对应多个
channel
。 - 每一个采样点得到的若干采样数据组合起来,叫做一个
frame
。 若干frame
组合起来叫做一个packet
。
各个成员释义👇🏻
mSampleRate
,就是采用频率mBitsPerChannel
,就是每个采样数据的位数mChannelsPerFrame
,可以理解为声道数,也就是一个采样时刻产生几个采样数据mFramesPerPacket
,就是每个packet的中frame的个数,等于这个packet中经历了几次采样间隔mBytesPerPacket
,每个packet中数据的字节数mBytesPerFrame
,每个frame中数据的字节数
3.2.2 创建converter转换器
相关函数是AudioConverterNewSpecific
👇🏻
各参数释义👇🏻
- 参数1:输入音频格式描述
- 参数2:输出音频格式描述
- 参数3:class desc的数量
- 参数4:class desc
- 参数5:创建的转换器
3.2.3 设置转换器属性
相关函数是AudioConverterSetProperty
各参数释义👇🏻
- 参数1:转换器
- 参数2:属性的key,可参考枚举
AudioConverterPropertyID
- 参数3:
属性值value
的数据类型的size大小
- 参数4:
属性值value
的取值地址
3.2.4 编码器类型描述
编码器类型描述的结构体是AudioClassDescription
👇🏻
各个成员释义👇🏻
mType
编码输出的格式,例如AAC
mSubType
编码输出的子格式mManufacturer
编码的方式软编码
或硬编码
3.3 编码
接着我们来到最关键的编码流程,我们对外的编码方法是- (void)encodeAudioSamepleBuffer: (CMSampleBufferRef)sampleBuffer;
,调用的地方就是采集类CCSystemCapture
的采集回调方法中👇🏻
//捕获音视频回调
- (void)captureSampleBuffer:(CMSampleBufferRef)sampleBuffer type: (CCSystemCaptureType)type {
if (type == CCSystemCaptureTypeAudio) {
//音频数据
//1.直接播放PCM数据
NSData *pcmData = [self convertAudioSamepleBufferToPcmData:sampleBuffer];
[_pcmPlayer playPCMData:pcmData];
//2.AAC编码
[_audioEncoder encodeAudioSamepleBuffer:sampleBuffer];
}else {
[_videoEncoder encodeVideoSampleBuffer:sampleBuffer];
}
}
采集的音频,有2种处理方式👇🏻
- 直接播放PCM数据
- AAC编码
直接播放PCM数据
后面再讲,我们先看AAC编码
👇🏻
- (void)encodeAudioSamepleBuffer:(CMSampleBufferRef)sampleBuffer {
CFRetain(sampleBuffer);
// 判断音频转换器是否创建成功.如果未创建成功.则配置音频编码参数且创建转码器
if (!_audioConverter) {
[self setupEncoderWithSampleBuffer:sampleBuffer];
}
// 来到音频编码异步队列
dispatch_async(_encoderQueue, ^{
});
首先,看看配置音频编码参数且创建转码器
的流程👇🏻
- (void)setupEncoderWithSampleBuffer: (CMSampleBufferRef)sampleBuffer {
//获取输入参数
AudioStreamBasicDescription inputAduioDes = *CMAudioFormatDescriptionGetStreamBasicDescription( CMSampleBufferGetFormatDescription(sampleBuffer));
//设置输出参数
AudioStreamBasicDescription outputAudioDes = {0};
outputAudioDes.mSampleRate = (Float64)_config.sampleRate; //采样率
outputAudioDes.mFormatID = kAudioFormatMPEG4AAC; //输出格式
outputAudioDes.mFormatFlags = kMPEG4Object_AAC_LC; // 如果设为0 代表无损编码
outputAudioDes.mBytesPerPacket = 0; //自己确定每个packet 大小
outputAudioDes.mFramesPerPacket = 1024; //每一个packet帧数 AAC-1024;
outputAudioDes.mBytesPerFrame = 0; //每一帧大小
outputAudioDes.mChannelsPerFrame = (uint32_t)_config.channelCount; //输出声道数
outputAudioDes.mBitsPerChannel = 0; //数据帧中每个通道的采样位数。
outputAudioDes.mReserved = 0; //对其方式 0(8字节对齐)
//填充输出相关信息
UInt32 outDesSize = sizeof(outputAudioDes);
AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &outDesSize, &outputAudioDes);
//获取编码器的描述信息(只能传入software)
AudioClassDescription *audioClassDesc = [self getAudioCalssDescriptionWithType:outputAudioDes.mFormatID fromManufacture:kAppleSoftwareAudioCodecManufacturer];
// 创建converter
OSStatus status = AudioConverterNewSpecific(&inputAduioDes, &outputAudioDes, 1, audioClassDesc, &_audioConverter);
if (status != noErr) {
NSLog(@"Error!:硬编码AAC创建失败, status= %d", (int)status);
return;
}
// 设置编解码质量
/*
kAudioConverterQuality_Max = 0x7F,
kAudioConverterQuality_High = 0x60,
kAudioConverterQuality_Medium = 0x40,
kAudioConverterQuality_Low = 0x20,
kAudioConverterQuality_Min = 0
*/
UInt32 temp = kAudioConverterQuality_High;
//编解码器的呈现质量
AudioConverterSetProperty(_audioConverter, kAudioConverterCodecQuality, sizeof(temp), &temp);
//设置比特率
uint32_t audioBitrate = (uint32_t)self.config.bitrate;
uint32_t audioBitrateSize = sizeof(audioBitrate);
status = AudioConverterSetProperty(_audioConverter, kAudioConverterEncodeBitRate, audioBitrateSize, &audioBitrate);
if (status != noErr) {
NSLog(@"Error!:硬编码AAC 设置比特率失败");
}
}
接着我们来到编码的异步队列
中,需要处理的流程👇🏻
- 获取BlockBuffer中保存的PCM数据
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
CFRetain(blockBuffer);
//获取BlockBuffer中音频数据大小以及音频数据地址
OSStatus status = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &_pcmBufferSize, &_pcmBuffer);
//判断status状态
NSError *error = nil;
if (status != kCMBlockBufferNoErr) {
error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
NSLog(@"Error: ACC encode get data point error: %@",error);
return;
}
- 输出buffer,包装到
AudioBufferList
中
先看看AudioBufferList
👇🏻
很明显,我们要将上面获取的blockBuffer
存储到AudioBufferList
的成员AudioBuffer mBuffers[1]
之中,这么处理👇🏻
//开辟_pcmBuffsize大小的pcm内存空间
uint8_t *pcmBuffer = malloc(_pcmBufferSize);
//将_pcmBufferSize数据set到pcmBuffer中.
memset(pcmBuffer, 0, _pcmBufferSize);
AudioBufferList outAudioBufferList = {0};
outAudioBufferList.mNumberBuffers = 1;
outAudioBufferList.mBuffers[0].mNumberChannels = (uint32_t)_config.channelCount;
outAudioBufferList.mBuffers[0].mDataByteSize = (UInt32)_pcmBufferSize;
outAudioBufferList.mBuffers[0].mData = pcmBuffer;
- 配置填充函数,获取输出数据
这个
填充函数
,就是编码完成时
的回调函数,通过AudioConverterFillComplexBuffer
来配置,先看看AudioConverterFillComplexBuffer
👇🏻
转换由输入回调函数提供的数据,各参数释义如下👇🏻
- 参数1:
inAudioConverter
音频转换器 - 参数2:
inInputDataProc
回调函数.提供要转换的音频数据的回调函数。当转换器准备好接受新的输入数据时,会重复调用此回调. - 参数3:
inInputDataProcUserData
即self
- 参数4:
ioOutputDataPacketSize
,输出缓冲区的大小 - 参数5:
outOutputData
,需要转换的音频数据 - 参数6:
outPacketDescription
,输出包信息
代码如下👇🏻
//输出包大小为1
UInt32 outputDataPacketSize = 1;
//转换由输入回调函数提供的数据
status = AudioConverterFillComplexBuffer(_audioConverter, aacEncodeInputDataProc, (__bridge void * _Nullable)(self), &outputDataPacketSize, &outAudioBufferList, NULL);
if (status == noErr) {
//获取数据
NSData *rawAAC = [NSData dataWithBytes: outAudioBufferList.mBuffers[0].mData length:outAudioBufferList.mBuffers[0].mDataByteSize];
//释放pcmBuffer
free(pcmBuffer);
//添加ADTS头,想要获取裸流时,请忽略添加ADTS头,写入文件时,必须添加
// NSData *adtsHeader = [self adtsDataForPacketLength:rawAAC.length];
// NSMutableData *fullData = [NSMutableData dataWithCapacity:adtsHeader.length + rawAAC.length];;
// [fullData appendData:adtsHeader];
// [fullData appendData:rawAAC];
//将数据传递到回调队列中
dispatch_async(_callbackQueue, ^{
[_delegate audioEncodeCallback:rawAAC];
});
} else {
error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
}
//释放
CFRelease(blockBuffer);
CFRelease(sampleBuffer);
if (error) {
NSLog(@"error: AAC编码失败 %@",error);
}
其中,编码完成的回调函数指定的是aacEncodeInputDataProc
,具体流程请参考3.4 编码回调
。
接着继续,我们注意到,配置函数成功后,有2种处理的方式👇🏻
- 写入
磁盘文件
,前提是添加ADTS
header - 将数据传递到回调队列中,交由调用方去处理
解码
的流程
AAC音频格式
说到ADTS
头,就必须先讲解下AAC音频格式
,分2种👇🏻
-
ADIF
:Audio Data Interchange Format 音频数据交换格式。这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行。故这种格式常用在磁盘文件
中。 -
ADTS
:Audio Data Transport Stream 音频数据传输流。这种格式的特征是它是一个有同步字的比特流,解码可以在这个流中任何位置开始。它的特征类似于mp3数据流格式
。
简单说,ADTS
可以在任意帧解码
,也就是说它每一帧都有头信息
。ADIF
只有一个统一的头
,所以必须得到所有的数据后
解码。且这两种的header的格式也是不同的,目前一般编码后的和抽取出的都是ADTS
格式的音频流👇🏻
添加ADTS头
这里补充下添加ADTS头
的处理流程👇🏻(仅做了解即可)
/**
* Add ADTS header at the beginning of each and every AAC packet.
* This is needed as MediaCodec encoder generates a packet of raw
* AAC data.
*
* AAC ADtS头
* Note the packetLen must count in the ADTS header itself.
* See: http://wiki.multimedia.cx/index.php?title=ADTS
* Also: http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Channel_Configurations
**/
- (NSData*)adtsDataForPacketLength:(NSUInteger)packetLength {
int adtsLength = 7;
char *packet = malloc(sizeof(char) * adtsLength);
// Variables Recycled by addADTStoPacket
int profile = 2; //AAC LC
//39=MediaCodecInfo.CodecProfileLevel.AACObjectELD;
int freqIdx = 4; //3: 48000 Hz、4:44.1KHz、8: 16000 Hz、11: 8000 Hz
int chanCfg = 1; //MPEG-4 Audio Channel Configuration. 1 Channel front-center
NSUInteger fullLength = adtsLength + packetLength;
// 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;
}
3.4 编码回调
最后我们看看编码回调函数aacEncodeInputDataProc
的流程 👇🏻
static OSStatus aacEncodeInputDataProc(AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, void *inUserData) {
//获取self
CCAudioEncoder *aacEncoder = (__bridge CCAudioEncoder *)(inUserData);
//判断pcmBuffsize大小
if (!aacEncoder.pcmBufferSize) {
*ioNumberDataPackets = 0;
return - 1;
}
//填充
ioData->mBuffers[0].mData = aacEncoder.pcmBuffer;
ioData->mBuffers[0].mDataByteSize = (uint32_t)aacEncoder.pcmBufferSize;
ioData->mBuffers[0].mNumberChannels = (uint32_t)aacEncoder.config.channelCount;
//填充完毕,则清空数据
aacEncoder.pcmBufferSize = 0;
*ioNumberDataPackets = 1;
return noErr;
}
主要就是将解码的数据
(缓存在CCAudioEncoder
实例中)填充到AudioBufferList
中。
3.5 小结
如上图,编码总计三大步:
- 配置编码器,开始准备编码;
- 收集到PCM数据,传给编码器;
- 编码完成
回调callback
,或写入文件
。
四、音频AAC解码
分析玩编码的流程,接下来自然是解码
👉🏻 我们也封装一个工具类CCAudioDecoder
。
4.1 解码工具类头文件
解码和编码差不多,直接上代码。
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
@class CCAudioConfig;
/**AAC解码回调代理*/
@protocol CCAudioDecoderDelegate <NSObject>
- (void)audioDecodeCallback:(NSData *)pcmData;
@end
@interface CCAudioDecoder : NSObject
@property (nonatomic, strong) CCAudioConfig *config;
@property (nonatomic, weak) id<CCAudioDecoderDelegate> delegate;
//初始化 传入解码配置
- (instancetype)initWithConfig:(CCAudioConfig *)config;
/**解码aac*/
- (void)decodeAudioAACData: (NSData *)aacData;
@end
.m中的扩展类👇🏻
@interface CCAudioDecoder()
@property (nonatomic, strong) dispatch_queue_t decoderQueue;
@property (nonatomic, strong) dispatch_queue_t callbackQueue;
//对音频转换器对象
@property (nonatomic) AudioConverterRef audioConverter;
//AAC缓存区
@property (nonatomic) char *aacBuffer;
//AAC缓存区大小
@property (nonatomic) UInt32 aacBufferSize;
//音频流包的描述信息
@property (nonatomic) AudioStreamPacketDescription *packetDesc;
@end
4.2 初始化
接着看看初始化👇🏻
- (instancetype)initWithConfig:(CCAudioConfig *)config {
self = [super init];
if (self) {
_decoderQueue = dispatch_queue_create("aac hard decoder queue", DISPATCH_QUEUE_SERIAL);
_callbackQueue = dispatch_queue_create("aac hard decoder callback queue", DISPATCH_QUEUE_SERIAL);
_audioConverter = NULL;
_aacBufferSize = 0;
_aacBuffer = NULL;
_config = config;
if (_config == nil) {
_config = [[CCAudioConfig alloc] init];
}
AudioStreamPacketDescription desc = {0};
_packetDesc = &desc;
[self setupEncoder];
}
return self;
}
然后是setupEncoder
👇🏻
- (void)setupEncoder {
//输出参数pcm
AudioStreamBasicDescription outputAudioDes = {0};
outputAudioDes.mSampleRate = (Float64)_config.sampleRate; //采样率
outputAudioDes.mChannelsPerFrame = (UInt32)_config.channelCount; //输出声道数(左声道、右声道)
outputAudioDes.mFormatID = kAudioFormatLinearPCM; //输出格式
outputAudioDes.mFormatFlags = (kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked); //编码 12
outputAudioDes.mFramesPerPacket = 1; //每一个packet帧数 ;
outputAudioDes.mBitsPerChannel = 16; //数据帧中每个通道的采样位数。
outputAudioDes.mBytesPerFrame = outputAudioDes.mBitsPerChannel / 8 *outputAudioDes.mChannelsPerFrame; //每一帧大小(采样位数 / 8 *声道数)
outputAudioDes.mBytesPerPacket = outputAudioDes.mBytesPerFrame * outputAudioDes.mFramesPerPacket; //每个packet大小(帧大小 * 帧数)
outputAudioDes.mReserved = 0; //对其方式 0(8字节对齐)
//输入参数aac
AudioStreamBasicDescription inputAduioDes = {0};
inputAduioDes.mSampleRate = (Float64)_config.sampleRate;
inputAduioDes.mFormatID = kAudioFormatMPEG4AAC;
inputAduioDes.mFormatFlags = kMPEG4Object_AAC_LC;
inputAduioDes.mFramesPerPacket = 1024;
inputAduioDes.mChannelsPerFrame = (UInt32)_config.channelCount;
//填充输出相关信息
UInt32 inDesSize = sizeof(inputAduioDes);
AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &inDesSize, &inputAduioDes);
//获取解码器的描述信息(只能传入software)
AudioClassDescription *audioClassDesc = [self getAudioCalssDescriptionWithType:outputAudioDes.mFormatID fromManufacture:kAppleSoftwareAudioCodecManufacturer];
//创建converter
OSStatus status = AudioConverterNewSpecific(&inputAduioDes, &outputAudioDes, 1, audioClassDesc, &_audioConverter);
if (status != noErr) {
NSLog(@"Error!:硬解码AAC创建失败, status= %d", (int)status);
return;
}
}
和编码的- (void)setupEncoderWithSampleBuffer: (CMSampleBufferRef)sampleBuffer
流程基本上一模一样。区别在于创建解码器
的方法getAudioCalssDescriptionWithType:fromManufacture:
👇🏻
- (AudioClassDescription *)getAudioCalssDescriptionWithType:(AudioFormatID)type fromManufacture:(uint32_t)manufacture {
static AudioClassDescription desc;
UInt32 decoderSpecific = type;
//获取满足AAC解码器的总大小
UInt32 size;
OSStatus status = AudioFormatGetPropertyInfo(kAudioFormatProperty_Decoders, sizeof(decoderSpecific), &decoderSpecific, &size);
if (status != noErr) {
NSLog(@"Error!:硬解码AAC get info 失败, status= %d", (int)status);
return nil;
}
//计算aac解码器的个数
unsigned int count = size / sizeof(AudioClassDescription);
//创建一个包含count个解码器的数组
AudioClassDescription description[count];
//将满足aac解码的解码器的信息写入数组
status = AudioFormatGetProperty(kAudioFormatProperty_Encoders, sizeof(decoderSpecific), &decoderSpecific, &size, &description);
if (status != noErr) {
NSLog(@"Error!:硬解码AAC get propery 失败, status= %d", (int)status);
return nil;
}
for (unsigned int i = 0; i < count; i++) {
if (type == description[i].mSubType && manufacture == description[i].mManufacturer) {
desc = description[i];
return &desc;
}
}
有以下几点区别👇🏻
- 输出参数不同:编码是
AAC
解码是PCM
- 转换器不同,编码是
kAudioFormatProperty_Encoders
,解码是kAudioFormatProperty_Decoders
4.3 解码前的准备
我们封装了个结构体CCAudioUserData
,用于记录aac的信息
作为解码回调函数
的参数👇🏻
typedef struct {
char * data;
UInt32 size;
UInt32 channelCount;
AudioStreamPacketDescription packetDesc;
} CCAudioUserData;
4.4 解码
接着就是解码流程
了👇🏻
- (void)decodeAudioAACData:(NSData *)aacData {
if (!_audioConverter) { return; }
dispatch_async(_decoderQueue, ^{
//CCAudioUserData记录aac的信息 作为参数参入解码回调函数
CCAudioUserData userData = {0};
userData.channelCount = (UInt32)_config.channelCount;
userData.data = (char *)[aacData bytes];
userData.size = (UInt32)aacData.length;
userData.packetDesc.mDataByteSize = (UInt32)aacData.length;
userData.packetDesc.mStartOffset = 0;
userData.packetDesc.mVariableFramesInPacket = 0;
//输出大小和packet个数
UInt32 pcmBufferSize = (UInt32)(2048 * _config.channelCount);
UInt32 pcmDataPacketSize = 1024;
//创建临时容器pcm
uint8_t *pcmBuffer = malloc(pcmBufferSize);
memset(pcmBuffer, 0, pcmBufferSize);
//输出buffer
AudioBufferList outAudioBufferList = {0};
outAudioBufferList.mNumberBuffers = 1;
outAudioBufferList.mBuffers[0].mNumberChannels = (uint32_t)_config.channelCount;
outAudioBufferList.mBuffers[0].mDataByteSize = (UInt32)pcmBufferSize;
outAudioBufferList.mBuffers[0].mData = pcmBuffer;
//输出描述
AudioStreamPacketDescription outputPacketDesc = {0};
//配置填充函数,获取输出数据
OSStatus status = AudioConverterFillComplexBuffer(_audioConverter, &AudioDecoderConverterComplexInputDataProc, &userData, &pcmDataPacketSize, &outAudioBufferList, &outputPacketDesc);
if (status != noErr) {
NSLog(@"Error: AAC Decoder error, status=%d",(int)status);
return;
}
//如果获取到数据
if (outAudioBufferList.mBuffers[0].mDataByteSize > 0) {
NSData *rawData = [NSData dataWithBytes:outAudioBufferList.mBuffers[0].mData length:outAudioBufferList.mBuffers[0].mDataByteSize];
dispatch_async(_callbackQueue, ^{
[_delegate audioDecodeCallback:rawData];
});
}
free(pcmBuffer);
});
}
其中解码的回调函数是AudioDecoderConverterComplexInputDataProc
。
4.5 解码回调
static OSStatus AudioDecoderConverterComplexInputDataProc( AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, void *inUserData) {
CCAudioUserData *audioDecoder = (CCAudioUserData *)(inUserData);
if (audioDecoder->size <= 0) {
ioNumberDataPackets = 0;
return -1;
}
//填充数据
*outDataPacketDescription = &audioDecoder->packetDesc;
(*outDataPacketDescription)[0].mStartOffset = 0;
(*outDataPacketDescription)[0].mDataByteSize = audioDecoder->size;
(*outDataPacketDescription)[0].mVariableFramesInPacket = 0;
ioData->mBuffers[0].mData = audioDecoder->data;
ioData->mBuffers[0].mDataByteSize = audioDecoder->size;
ioData->mBuffers[0].mNumberChannels = audioDecoder->channelCount;
return noErr;
}
解码中你会发现,不需要桥接self对象
,因为我们把数据缓存在自定义的结构体CCAudioUserData
之中。这点也是和编码的区别。
至此,解码的工具类已封装完毕!🍺🍺🍺🍺🍺🍺
五、音频PCM播放
最后,再补充一点 👉🏻 音频PCM播放
。可能有的场景不需要编解码,捕获到音频后就直接播放了。
我们可以在编码工具类中定义一个方法👇🏻
- (NSData *)convertAudioSamepleBufferToPcmData: (CMSampleBufferRef)sampleBuffer {
//获取pcm数据大小
size_t size = CMSampleBufferGetTotalSampleSize(sampleBuffer);
//分配空间
int8_t *audio_data = (int8_t *)malloc(size);
memset(audio_data, 0, size);
//获取CMBlockBuffer, 这里面保存了PCM数据
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
//将数据copy到我们分配的空间中
CMBlockBufferCopyDataBytes(blockBuffer, 0, size, audio_data);
NSData *data = [NSData dataWithBytes:audio_data length:size];
free(audio_data);
return data;
}
将sampleBuffer
数据提取出PCM
数据,然后回调返回给ViewController
,接着就可以直接播放PCM
数据了。
播放肯定需要播放器类CCAudioPCMPlayer
,她就好比是解码器
👇🏻
@class CCAudioConfig;
@interface CCAudioPCMPlayer : NSObject
- (instancetype)initWithConfig:(CCAudioConfig *)config;
/**播放pcm*/
- (void)playPCMData:(NSData *)data;
/** 设置音量增量 0.0 - 1.0 */
- (void)setupVoice:(Float32)gain;
/**销毁 */
- (void)dispose;
@end
实现部分👇🏻
#import "CCAudioPCMPlayer.h"
#import <AudioToolbox/AudioToolbox.h>
#import <AVFoundation/AVFoundation.h>
#import "CCAVConfig.h"
#import "CCAudioDataQueue.h"
#define MIN_SIZE_PER_FRAME 2048 //每帧最小数据长度
static const int kNumberBuffers_play = 3; // 1
typedef struct AQPlayerState
{
AudioStreamBasicDescription mDataFormat; // 2
AudioQueueRef mQueue; // 3
AudioQueueBufferRef mBuffers[kNumberBuffers_play]; // 4
AudioStreamPacketDescription *mPacketDescs; // 9
}AQPlayerState;
@interface CCAudioPCMPlayer ()
@property (nonatomic, assign) AQPlayerState aqps;
@property (nonatomic, strong) CCAudioConfig *config;
@property (nonatomic, assign) BOOL isPlaying;
@end
@implementation CCAudioPCMPlayer
static void TMAudioQueueOutputCallback(void * inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) {
AudioQueueFreeBuffer(inAQ, inBuffer);
}
- (instancetype)initWithConfig:(CCAudioConfig *)config
{
self = [super init];
if (self) {
_config = config;
//配置
AudioStreamBasicDescription dataFormat = {0};
dataFormat.mSampleRate = (Float64)_config.sampleRate; //采样率
dataFormat.mChannelsPerFrame = (UInt32)_config.channelCount; //输出声道数
dataFormat.mFormatID = kAudioFormatLinearPCM; //输出格式
dataFormat.mFormatFlags = (kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked); //编码 12
dataFormat.mFramesPerPacket = 1; //每一个packet帧数 ;
dataFormat.mBitsPerChannel = 16; //数据帧中每个通道的采样位数。
dataFormat.mBytesPerFrame = dataFormat.mBitsPerChannel / 8 *dataFormat.mChannelsPerFrame; //每一帧大小(采样位数 / 8 *声道数)
dataFormat.mBytesPerPacket = dataFormat.mBytesPerFrame * dataFormat.mFramesPerPacket; //每个packet大小(帧大小 * 帧数)
dataFormat.mReserved = 0;
AQPlayerState state = {0};
state.mDataFormat = dataFormat;
_aqps = state;
[self setupSession];
//创建播放队列
OSStatus status = AudioQueueNewOutput(&_aqps.mDataFormat, TMAudioQueueOutputCallback, NULL, NULL, NULL, 0, &_aqps.mQueue);
if (status != noErr) {
NSError *error = [[NSError alloc] initWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
NSLog(@"Error: AudioQueue create error = %@", [error description]);
return self;
}
[self setupVoice:1];
_isPlaying = false;
}
return self;
}
- (void)setupSession {
NSError *error = nil;
//将会话设置为活动或非活动。请注意,激活音频会话是一个同步(阻塞)操作
[[AVAudioSession sharedInstance] setActive:YES error:&error];
if (error) {
NSLog(@"Error: audioQueue palyer AVAudioSession error, error: %@", error);
}
//设置会话类别
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
if (error) {
NSLog(@"Error: audioQueue palyer AVAudioSession error, error: %@", error);
}
}
- (void)playPCMData:(NSData *)data {
//指向音频队列缓冲区
AudioQueueBufferRef inBuffer;
/*
要求音频队列对象分配音频队列缓冲区。
参数1:要分配缓冲区的音频队列
参数2:新缓冲区所需的容量(字节)
参数3:输出,指向新分配的音频队列缓冲区
*/
AudioQueueAllocateBuffer(_aqps.mQueue, MIN_SIZE_PER_FRAME, &inBuffer);
//将data里的数据拷贝到inBuffer.mAudioData中
memcpy(inBuffer->mAudioData, data.bytes, data.length);
//设置inBuffer.mAudioDataByteSize
inBuffer->mAudioDataByteSize = (UInt32)data.length;
//将缓冲区添加到录制或播放音频队列的缓冲区队列。
/*
参数1:拥有音频队列缓冲区的音频队列
参数2:要添加到缓冲区队列的音频队列缓冲区。
参数3:inBuffer参数中音频数据包的数目,对于以下任何情况,请使用值0:
* 播放恒定比特率(CBR)格式时。
* 当音频队列是录制(输入)音频队列时。
* 当使用audioqueueallocateBufferWithPacketDescriptions函数分配要重新排队的缓冲区时。在这种情况下,回调应该描述缓冲区的mpackedDescriptions和mpackedDescriptionCount字段中缓冲区的数据包。
参数4:一组数据包描述。对于以下任何情况,请使用空值
* 播放恒定比特率(CBR)格式时。
* 当音频队列是输入(录制)音频队列时。
* 当使用audioqueueallocateBufferWithPacketDescriptions函数分配要重新排队的缓冲区时。在这种情况下,回调应该描述缓冲区的mpackedDescriptions和mpackedDescriptionCount字段中缓冲区的数据包
*/
OSStatus status = AudioQueueEnqueueBuffer(_aqps.mQueue, inBuffer, 0, NULL);
if (status != noErr) {
NSLog(@"Error: audio queue palyer enqueue error: %d",(int)status);
}
//开始播放或录制音频
/*
参数1:要开始的音频队列
参数2:音频队列应开始的时间。
要指定相对于关联音频设备时间线的开始时间,请使用audioTimestamp结构的msampletime字段。使用NULL表示音频队列应尽快启动
*/
AudioQueueStart(_aqps.mQueue, NULL);
}
//不需要该函数,
//- (void)pause {
// AudioQueuePause(_aqps.mQueue);
//}
//设置音量增量//0.0 - 1.0
- (void)setupVoice:(Float32)gain {
Float32 gain0 = gain;
if (gain < 0) {
gain0 = 0;
}else if (gain > 1) {
gain0 = 1;
}
//设置播放音频队列参数值
/*
参数1:要开始的音频队列
参数2:属性
参数3:value
*/
AudioQueueSetParameter(_aqps.mQueue, kAudioQueueParam_Volume, gain0);
}
//销毁
- (void)dispose {
AudioQueueStop(_aqps.mQueue, true);
AudioQueueDispose(_aqps.mQueue, true);
}
@end
代码中有详细的注释,这里就不在赘述了。
总结
-
音频原理
- 声音三要素:
音调、音量和音色
- 人类听觉范围:
20hz-20kHz
,低于20hz是次声波
,高于20kHz是超声波
- 脉冲编码调制(PCM)
- 分3个阶段:
抽样、量化和编码
- 分3个阶段:
- 音频压缩编码原理
- 音频信号的传输率 = 取样频率 * 样本量化⽐特数 * 通道数
有损编码
:消除冗余数据(人耳无法听到的)无损编码
:哈弗曼编码
,压缩人耳部分听不到的,其他的原样保留- 压缩方法
- 去除采集的
音频冗余信息
- 遮蔽效应:一个声音覆盖另一个声音
- 去除采集的
- 音频压缩编码格式:MPEG-1、杜比AC-3、MPEG-2、MPEG-4和AAC
- 音频的标准参数
- 取样频率 = 44.1kHz
- 样本值的量化比特数 = 16
- 普通立体声的信号通道数 = 2
- 数字信号传输码流⼤约 1.4M bit/s
- 一秒钟的数据量为 1.4Mbit / (8/Byte) 达176.4Byte(字节),等于88200个汉字的数据量
- 声音三要素:
-
频域遮蔽与时域遮蔽
- 频域遮蔽
- 一个较弱的声音会被临近的一个较强的声音所覆盖
- 不同频域、不同分贝的声音,相互之间影响不大
- 时域遮蔽
- 同时段的,高音会完全遮蔽低音
- 特殊遮蔽:开始小音量发声,短时间(50ms)内出现另一个大音量的声音,则后者会遮蔽前者
- 声音传播时间:
向前遮蔽50ms左右
,向后遮蔽100ms左右
- 频域遮蔽
-
音频AAC编码工具类
- 2个队列:
编码队列
+回调队列
- 编码前的准备
- 音频参数结构体
AudioStreamBasicDescription
- 创建converter转换器:函数
AudioConverterNewSpecific
- 设置转换器属性:函数
AudioConverterSetProperty
- 编码器类型描述结构体
AudioClassDescription
- 音频参数结构体
- AAC编码流程
- 判断音频转换器是否创建成功。
- 成功 👉🏻 直接返回
- 失败 👉🏻 配置音频编码参数且创建转码器
- 获取输入参数
AudioStreamBasicDescription
- 设置输出参数(AAC格式
AudioStreamBasicDescription
) AudioFormatGetProperty
填充输出参数- 获取编码器的描述信息
AudioClassDescription
AudioConverterNewSpecific
创建音频转换器AudioConverterSetProperty
配置转换器属性:编解码质量
和比特率
等
- 获取输入参数
- 音频编码异步队列中处理
- 获取
BlockBuffer
中保存的PCM数据 - 将buffer(PCM数据),包装到
AudioBufferList
中 AudioConverterFillComplexBuffer
转换由输入回调函数提供的数据- 回调配置成功后,将输出的AAC格式数据转换成NSData
- 此时可以分2种场景处理
5.1. 写入
磁盘文件
,前提是添加ADTS
header 5.2. 将数据传递到回调队列中,交由调用方去处理解码
的流程 - 释放blockBuffer和sampleBuffer
- 获取
- 判断音频转换器是否创建成功。
- AAC音频格式
ADIF
:Audio Data Interchange Format 音频数据交换格式。常用在磁盘文件
中。ADTS
:Audio Data Transport Stream 音频数据传输流。用于网络传输
场景。- 代码实现添加ADTS头
- AAC编码回调:将
解码的数据
(缓存在CCAudioEncoder
实例中)填充到AudioBufferList
中。
- 2个队列:
-
音频AAC解码工具类
- 2个队列:
解码队列
+回调队列
- 音频转换器初始化
- 输出参数pcm的配置
AudioStreamBasicDescription
- 输入参数aac的配置
AudioStreamBasicDescription
AudioFormatGetProperty
填充输出相关信息kAudioFormatProperty_FormatInfo
- 获取解码器的描述信息
AudioClassDescription
- 只能传入
kAppleSoftwareAudioCodecManufacturer
- 只能传入
AudioConverterNewSpecific
创建converter
- 输出参数pcm的配置
- 解码前的准备:
- 自定义结构体
CCAudioUserData
,包括- 数据地址
- 数据大小
- 数据通道数(声道数)
AudioStreamPacketDescription
数据缓冲区的包的描述信息
- 用于
记录aac的信息
,并且作为解码回调函数
的参数
- 自定义结构体
- AAC解码流程
- 音频转换器未创建成功,直接返回
- 解码队列中异步处理
- 配置
CCAudioUserData
,缓存记录aac的信息 - 设置输出大小
pcmBufferSize
和packet个数pcmDataPacketSize
malloc
创建临时容器pcm,memset
初始化该空间- 输出buffer
AudioBufferList
的配置 - 输出描述
AudioStreamPacketDescription
结构体初始化 AudioConverterFillComplexBuffer
配置填充函数,获取输出数据- 将输出数据转成
NSData
格式,回调队列中异步delegate出去
- 配置
- 解码回调:将缓存的
CCAudioUserData
中的数据,填充到回调函数的参数AudioStreamPacketDescription
和AudioBufferList
之中
- 2个队列:
-
音频PCM播放:可参考具体代码。