007-音频AAC编解码详解

2,243 阅读25分钟

前言

大家是否还记得,在004-视频H264编码详解(上)中的八、AVFoundation采集视频数据实现(3)章节下,说到音频和视频捕捉后,所触发的回调方法都是👇🏻 image.png 视频H264原理编解码以及渲染显示都已分析完毕,剩下的就是本篇文章要分析的音频编解码部分了。

一、音频原理知识点

首先我们来了解下,音频相关的几个知识点。

1.1 声音

声音(sound)是由物体振动产生的声波,是通过介质(空气或固体、液体)传播并能被人或动物听觉器官所感知的波动现象。我们都学过初中物理学,知道声音由3要素构成👇🏻

  1. 音调(音频):声音的高低(高音、低音),由“频率”(frequency)决定,频率越高音调越高(频率单位Hz(hertz))
  2. 音量:即声音振动的幅度,人主观上感觉声音的大小。
  3. 音色:又称音品,波形决定了声音的音色。声音因物体材料的特性而不同,音色本身是一种抽象的东西,但波形是把这个抽象直观的表现。波形不同,音色则不同。不同的音色,通过波形,完全可以分辨的。

心理声学模型

image.png

上图中可以看见,人类的听觉范围在20Hz到20000Hz之间,低于20Hz的称作次声波,高于20000Hz的称作超声波次声波超声波我们都听不见,所以在音频流编解码时,我们就可以干掉它们。

1.2 脉冲编码调制(PCM)

那么现在问题来了 👉🏻 如何将现实⽣生活中的声⾳音转换成数字信号?这就是即将介绍的脉冲编码调制(PCM),仅做了解即可。

将声音转变成数字信号的过程,如下图 👇🏻

image.png

大致分3个阶段👇🏻

  1. 抽样
  2. 量化
  3. 编码

image.png

假设一个模拟信号f(t)通过一个开关,则开关的输出与开关的状态有关,

  • 当开关处于闭合状态, 开关的输出就是输入,即y(t) = f(t)
  • 若开关处于断开位置,输出y(t)就为零

可见,如果让开关受一个窄脉冲串(序列)的控制,则脉冲出现时开关闭合,脉冲消失时开关断开,此输出的y(t)就是一个幅值变化的脉冲串(序列),每个脉冲的幅值就是该脉冲出现时刻输入信号f(t)的瞬间值,因此,y(t)就是对f(t)抽样后的信号或称样值信号

image.png

图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个量化值之差

image.png

上图中,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 频域遮蔽

image.png

  1. x轴是频域值。这里是20hz开始的,因为人听不到20hz以下频率的声音
  2. y轴是分贝值40分贝以下的声音,人也是听不到的
  3. 再看紫色柱,这部分声音听得到,但是周边出现了红色柱,柱很高表示声音特别大,把周边的紫色矮柱子遮蔽
  4. 绿色柱的声音,频域值比紫色和红色都小,证明这个声音比较磁性
  5. 绿色红色碰撞后,就好比男生和女生吵架,肯定干不过,注定输,😂
  6. 但是,绿色柱红色柱有段距离,说明他俩的频域值相差比较大,以第三方的角度听,他俩的声音是都能听得到

2.2 时域遮蔽

image.png

  1. 图中,simultaneous这一时段,有一个高音(顶部横线),有一个低音(底部横线),同时发声,那么高音会完全遮蔽低音
  2. 有个特殊情况 👉🏻 pre阶段,开始有个小声音在发声,突然(很短时间内)有个大的声音响了,此时小声音也会被遮蔽掉
  3. 声音是有个传播时间的,有一定的时间影响,大概向前遮蔽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👇🏻

image.png

这个结构体提供了对于音频文件的描述

音频文件的产生是模拟信号 -> PCM以后的数字信号 -> 压缩、编码以后的音频文件

  • PCM时采样频率叫做sample rate
  • 每一次采样可以得到若干采样数据,对应多个channel
  • 每一个采样点得到的若干采样数据组合起来,叫做一个frame
  • 若干frame组合起来叫做一个packet

各个成员释义👇🏻

  • mSampleRate,就是采用频率
  • mBitsPerChannel,就是每个采样数据的位数
  • mChannelsPerFrame,可以理解为声道数,也就是一个采样时刻产生几个采样数据
  • mFramesPerPacket,就是每个packet的中frame的个数,等于这个packet中经历了几次采样间隔
  • mBytesPerPacket,每个packet中数据的字节数
  • mBytesPerFrame,每个frame中数据的字节数

3.2.2 创建converter转换器

相关函数是AudioConverterNewSpecific👇🏻

image.png 各参数释义👇🏻

  • 参数1:输入音频格式描述
  • 参数2:输出音频格式描述
  • 参数3:class desc的数量
  • 参数4:class desc
  • 参数5:创建的转换器

3.2.3 设置转换器属性

相关函数是AudioConverterSetProperty image.png 各参数释义👇🏻

  • 参数1:转换器
  • 参数2:属性的key,可参考枚举AudioConverterPropertyID
  • 参数3:属性值value的数据类型的size大小
  • 参数4:属性值value取值地址

3.2.4 编码器类型描述

编码器类型描述的结构体是AudioClassDescription👇🏻

image.png

各个成员释义👇🏻

  • 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种处理方式👇🏻

  1. 直接播放PCM数据
  2. 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👇🏻 image.png

很明显,我们要将上面获取的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👇🏻 image.png

转换由输入回调函数提供的数据,各参数释义如下👇🏻

  1. 参数1: inAudioConverter 音频转换器
  2. 参数2: inInputDataProc 回调函数.提供要转换的音频数据的回调函数。当转换器准备好接受新的输入数据时,会重复调用此回调.
  3. 参数3: inInputDataProcUserDataself
  4. 参数4: ioOutputDataPacketSize,输出缓冲区的大小
  5. 参数5: outOutputData,需要转换的音频数据
  6. 参数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种处理的方式👇🏻

  1. 写入磁盘文件,前提是添加ADTSheader
  2. 将数据传递到回调队列中,交由调用方去处理解码的流程
AAC音频格式

说到ADTS头,就必须先讲解下AAC音频格式,分2种👇🏻

  1. ADIF:Audio Data Interchange Format 音频数据交换格式。这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行。故这种格式常用在磁盘文件中。

  2. ADTS:Audio Data Transport Stream 音频数据传输流。这种格式的特征是它是一个有同步字的比特流,解码可以在这个流中任何位置开始。它的特征类似于mp3数据流格式

简单说,ADTS可以在任意帧解码,也就是说它每一帧都有头信息ADIF只有一个统一的头,所以必须得到所有的数据后解码。且这两种的header的格式也是不同的,目前一般编码后的和抽取出的都是ADTS格式的音频流👇🏻

image.png

添加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 小结

image.png

如上图,编码总计三大步:

  1. 配置编码器,开始准备编码;
  2. 收集到PCM数据,传给编码器;
  3. 编码完成回调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;
        }
    }

有以下几点区别👇🏻

  1. 输出参数不同:编码是AAC 解码是PCM
  2. 转换器不同,编码是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个阶段:抽样、量化和编码
    • 音频压缩编码原理
      • 音频信号的传输率 = 取样频率 * 样本量化⽐特数 * 通道数
      • 有损编码:消除冗余数据(人耳无法听到的)
      • 无损编码哈弗曼编码,压缩人耳部分听不到的,其他的原样保留
      • 压缩方法
        • 去除采集的音频冗余信息
        • 遮蔽效应:一个声音覆盖另一个声音
      • 音频压缩编码格式: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编码流程
      1. 判断音频转换器是否创建成功。
        1. 成功 👉🏻 直接返回
        2. 失败 👉🏻 配置音频编码参数且创建转码器
          1. 获取输入参数 AudioStreamBasicDescription
          2. 设置输出参数(AAC格式AudioStreamBasicDescription)
          3. AudioFormatGetProperty填充输出参数
          4. 获取编码器的描述信息AudioClassDescription
          5. AudioConverterNewSpecific创建音频转换器
          6. AudioConverterSetProperty配置转换器属性:编解码质量比特率
      2. 音频编码异步队列中处理
        1. 获取BlockBuffer中保存的PCM数据
        2. 将buffer(PCM数据),包装到AudioBufferList
        3. AudioConverterFillComplexBuffer转换由输入回调函数提供的数据
        4. 回调配置成功后,将输出的AAC格式数据转换成NSData
        5. 此时可以分2种场景处理 5.1. 写入磁盘文件,前提是添加ADTSheader 5.2. 将数据传递到回调队列中,交由调用方去处理解码的流程
        6. 释放blockBuffer和sampleBuffer
    • AAC音频格式
      • ADIF:Audio Data Interchange Format 音频数据交换格式。常用在磁盘文件中。
      • ADTS:Audio Data Transport Stream 音频数据传输流。用于网络传输场景。
        • 代码实现添加ADTS头
    • AAC编码回调:将解码的数据(缓存在CCAudioEncoder实例中)填充到AudioBufferList中。
  • 音频AAC解码工具类

    • 2个队列:解码队列 + 回调队列
    • 音频转换器初始化
      • 输出参数pcm的配置 AudioStreamBasicDescription
      • 输入参数aac的配置 AudioStreamBasicDescription
      • AudioFormatGetProperty填充输出相关信息kAudioFormatProperty_FormatInfo
      • 获取解码器的描述信息AudioClassDescription
        • 只能传入kAppleSoftwareAudioCodecManufacturer
      • AudioConverterNewSpecific创建converter
    • 解码前的准备:
      • 自定义结构体CCAudioUserData,包括
        • 数据地址
        • 数据大小
        • 数据通道数(声道数)
        • AudioStreamPacketDescription数据缓冲区的包的描述信息
      • 用于记录aac的信息,并且作为解码回调函数的参数
    • AAC解码流程
      1. 音频转换器未创建成功,直接返回
      2. 解码队列中异步处理
        1. 配置CCAudioUserData,缓存记录aac的信息
        2. 设置输出大小pcmBufferSize和packet个数pcmDataPacketSize
        3. malloc创建临时容器pcm,memset初始化该空间
        4. 输出bufferAudioBufferList的配置
        5. 输出描述AudioStreamPacketDescription结构体初始化
        6. AudioConverterFillComplexBuffer配置填充函数,获取输出数据
        7. 将输出数据转成NSData格式,回调队列中异步delegate出去
    • 解码回调:将缓存的CCAudioUserData中的数据,填充到回调函数的参数AudioStreamPacketDescriptionAudioBufferList之中
  • 音频PCM播放:可参考具体代码。