AudioConvertRef核心API介绍

  1. 创立转化器

    AudioConverterNew(const AudioStreamBasicDescription * inSourceFormat,
                      const AudioStreamBasicDescription * inDestinationFormat,
                      AudioConverterRef __nullable * __nonnull outAudioConverter);
    

    inSourceFormat 源格局,PCM互转或大部分类型(AAC转化)咱们直接指定即可。文件转码需求经过kAudioFilePropertyDataFormat读取。不确定的格局能够预填写采样和声道数其余字段经过kAudioFormatProperty_FormatInfo填充。非PCM转化需求经过kAudioConverterCurrentInputStreamDescription从编码器中从头获取一次以生成的编码器输入输出为准

    inDestinationFormat 同上。非PCM转化需求经过kAudioConverterCurrentInputStreamDescription从编码器中从头获取一次以生成的编码器输入输出为准

    outAudioConverter 生成的转化目标,传入地址即可

    还有一个API同样能创立转化器AudioConverterNewSpecific, 这个API需求传入编码器数量(对应声道数)和信息,能够读取硬编和软编信息传入, 当然这在PCM转化PCM中也不需求(没有硬件支持)。编码器信息能够从kAudioFormatProperty_Decoders中读取然后设置。假如您运用硬件支持,那在iOS设备中需求重视AVAudioSessionInterruptionNotification的告诉,收到此告诉时有或许另一个APP正在运用硬件进行编解码,咱们需求暂停咱们的编码同时问询kAudioConverterPropertyCanResumeFromInterruption是否能够恢复之前的中断(某些格局依靠上下文才干编解码成功),假如不能考虑从头解码或许放弃本次编码。假如咱们不想重视打断事情能够依靠此API显现传入软编码

  2. 转化格局API

    extern OSStatus
    AudioConverterFillComplexBuffer(AudioConverterRef                   inAudioConverter,
                                    AudioConverterComplexInputDataProc  inInputDataProc,
                                    void * __nullable                   inInputDataProcUserData,
                                    UInt32 *                            ioOutputDataPacketSize,
                                    AudioBufferList *                   outOutputData,
                                    AudioStreamPacketDescription * __nullable outPacketDescription)
    

    inAudioConverter 创立好的转化目标实例

    inInputDataProc 一个函数指针,恳求原始数据填充的回调,下面会介绍。

    inInputDataProcUserData 用户流转数据,意图是获取一个用户上下文,一般是自定义结构或许OC/C++的目标指针。目前见过两种写法一种是传操控结构或目标,这个目标一般包括声响源数据(文件符或环形缓冲),源格局,在回调中取出目标操作元数据填充。另一种写法则是直接传AudioBufferList的数据,这种比较有限制,你有必要确保供给的N个数据能一次转化成M个输出,例如不添加滤波器的PCM转PCM,PCM编码AAC数据(一个AAC包包括1024个原始帧)。

    ioOutputDataPacketSize 传入的是本次编码完结后希望得到的packet数量,此值的核算比较简单,PCM转化44100 –>48000 10ms441个原始包,转化后为480个包,PCM转AAC,1024个原始数据转化为1个包。关于非安稳码率的一些格局或许无法清晰核算发生的帧,此刻咱们会提早分配内存,然后经过内存大小反向核算出这些内存最少能包容多少个结果包,此刻需求知道这个格局的最大包size,能够经过kAudioConverterPropertyMaximumOutputPacketSize拿到一个格局的最大包占用。编码完结后传出值被修改为实在转码输出的包数量

    outOutputData 格局转化完结后装载结果的容器,需求用户手动分配内存。体系会操控编码后的包数量不会超出这个内存。关于可核算的输出(PCM 安稳码率),咱们一般分配不小于希望的大小空间。关于不行核算的输出优先分配一段内存,然后经过kAudioConverterPropertyMaximumOutputPacketSize核算出最大包size,进一步估算这个内存能包容的最少包数量

    outPacketDescription 转化后数据的每个packet描绘(音频数据大小开始字节和帧数)。这个是用于转化后编码格局是动态码率(VBR)时,可是关于pcm转pcm或许安稳码率(CBR)转化即使传入体系也不会写值,产物格局为PCM或CBR传NULL即可。假如要运用需求分配不小于希望ioOutputDataPacketSize的内存,核算方法为:ioOutputDataPacketSize * sizeof(AudioStreamPacketDescription)

    苹果为什么不在这个函数供给一个数据入参?
    关于常规转化(安稳码率),定输入数据,要求填充数据的回调只执行一次就能拿到数据,这么看来确实没必要搞一个回调函数。可是一些格局(非安稳码率)或许给编码器添加了__滤波器__后,底层需求你屡次供给数据才干正确编解码,由于这些操作都是和上下文数据相关的,浅显的说下一次解码或抑波依靠上一次数据,当你供给的数据不行本次无法解码或滤波他就会要求你供给更多,所以传一次数据传入是不行行的。

  3. 填充待转化数据回调

    typedef OSStatus
    (*AudioConverterComplexInputDataProc)(AudioConverterRef               inAudioConverter,
                                          UInt32 *                        ioNumberDataPackets,
                                          AudioBufferList *               ioData,
                                          AudioStreamPacketDescription * __nullable * __nullable outDataPacketDescription,
                                          void * __nullable               inUserData);
    

    inAudioConverter 转化目标实例, 一般咱们不运用, 当然假如用户上下文信息不行,咱们能经过此获取一些信息,例如input stream Format和output stream format

    ioNumberDataPackets 这个参数标明本次回调底层希望得到的原始包数量,咱们按照它的值填充数据即可。假如咱们的数据缺乏无法按照它希望的数量填充,此刻咱们能够填充一部分,而且将此值修改为填充的实在包数量,可是这会导致编码器在不久的将来再次动身此回调。浅显的说比如编码器需求10个原始包,假如你第一次供给5个包,那底层在第一次会调后不会完毕AudioConverterFillComplexBuffer,而是在不久再次回调索要剩余的5个包。这种机制关于咱们实时音视频编码是不适宜的,假如下一次回调我仍然无法供给剩余的5个包,这或许会造成底层无限空跑。咱们一般的做法是堵塞编码器等候元数据的填充,当有数据填充时释放堵塞,咱们能够凭借信号量或锁来完结,这就需求编码安稳在一个单独的线程中。关于可核算输出的转化来说(非滤波的PCM转化,PCM转AAC等等),咱们能够不必管这个字段

    ioData 用户需求将待转化音频数据填入此结构, 尽管依据文档这儿的buffer有或许会被体系分配内存空间, 可是这儿咱们一般统一前分配好的内存,然后更改体统的buffer指向,并不会执行数据copy,也便是他分配的缓冲区咱们一般不运用

    outDataPacketDescription 假如你的待转化数据是安稳码率(CBR),此值会是NULL。关于待转化的VBR数据,你有必要供给和传入包数量对应的描绘符以供编码器正确解码原始包,核算方法ioNumberDataPackets * sizeof(AudioStreamPacketDescription)也便是一个数组, 假如outDataPacketDescription不为空,需求将其指向创立好的AudioStreamPacketDescription数组

    inUserData 用户上下文这个便是在AudioConverterFillComplexBuffer中传入的inInputDataProcUserData

    @return 回来0代表成功。假如回来非0值有必要将ioNumberDataPackets值置0。在测试过程中遇到了有意思的一幕,连续三次回来0且供给空数据,编码器永远将不会再触发回调,因而咱们没有数据供给时不仅要将ioNumberDataPackets置0,而且需求回来一个错误码(自定义非0即可)来防止这种bug。官方文档中说,ioNumberDataPackets置0回来错误码,也能够处理编码未完结可是没有源数据的状况

    实测中还发现了一个有趣现象,在做iOS同享体系音时,体系采集的声响为44100采样双声道大端数据,且每次传输1024个帧。再收到一次传输时希望将原数据转化为48000单声道数据,用于和采集声响混合后发送。由于44100到48000运用线性插值会有很多噪音,于是敞开了convert的相位滤波器。经过核算1024个数据并不能完美转化为48000格局的数据, 1024 * 48000 / 44100 = 1114.557, 其时就给了希望1114个字节,也便是每一组数或许丢掉一些帧,此刻假如在回调中将ioNumberDataPackets置0回来-1,编码器简直陷入无限循环最终野指针溃散。封闭滤波器此问题消失。继续探索猜测是不是由于1024个帧无法完整消费,引入一层缓冲区,将传入改为882个帧,则转化后为 882 * 48000 / 44100 = 960,此刻不会有任何问题。

转码操作简述

再次之前咱们需求约好好一些事:

元数据的存储: 关于文件转码需求的便是文件操作符,关于实时音视频编程需求的便是无锁的环形缓冲(依据内存排序),元数据供给的能力便是有能力供给任意数量的原始包

能堵塞当前线程必定时间的机制: 在c++或许iOS中均供给了信号量机制,运用wait同步等候一段时间,而且能在任意时间撤销堵塞。一些锁也能完结此机制

常驻线程: 实时编码中咱们一般会发动一个线程处理编码,创立线程while(true){}跑圈即可。iOS中依靠NSThread即可,尽量不要依靠GCD,GCD确实有创立线程的能力,可是也有一套杂乱的复用逻辑,假如咱们堵塞GCD的异步block同时也或许堵塞其他操作

咱们简单描绘一下操作大致作业流程:

录音采集16000采样 单声道 16位深数据而且转化为48000 单声道 16位深数据输出:

  1. 创立编码器和环形缓冲区,敞开采集,同时敞开编码线程
  2. 采集中假如有数据回调直接将数据存放入环形缓冲,而且释放信号量堵塞
  3. 编码线程中循环读取固定数量的数据(更为合理的是固定希望堵塞编码器回调,这样能安稳得到固定数量的输出,这样更有利于固定数据回调,例如webrtc 10ms数据回调),假如数据缺乏则运用信号量堵塞一段时间(堵塞时长应不小于数据回调周期,防止重复触发堵塞)
  4. 编码完结将数据存入缓冲即可

此逻辑具体可参阅webrtc源码 audio_device_mac.cc, 他从coreAudio供给的IOProc种读取硬件采集数据,然后转化为48000 1 16的格局,固定希望480帧也便是10ms,然后送入device buffer进一步送往transport做3A处理

咱们再简述一个场景,文件转码aif转化为aac

  1. 读取源文件AudioFileID,依据fileID读取源asbd
  2. 确定意图输出格局asbd,假如非PCM则需求从kAudioFormatProperty_FormatInfo填充
  3. 依据源和意图asbd创立转化器,将原始文件的MagicCookieData填入编码器
  4. 依据convert从头换获取源asbd和意图asbd
  5. 设置aac格局的吞码率,创立意图文件AudioFileID,将MagicCookieData从convert写入意图文件,假如声道数大于2需求将channelLayout写入文件
  6. 创立32768字节的输入缓冲,估算缓冲能承载的最少原始包数量,而且为些原始包创立包描绘数组,将这些组成一个结构体用于用户上下文
  7. 创立32768字节的意图缓冲,估算所能包容的最少希望包数量
  8. 无限循环填充编码器,在回调函数中从源fileID 读取最少原始包数量然后填充,假如缺乏就填充实践值,假如没有数据回来-1
  9. 单次编码完毕,判定是否为-1,假如为-1则编码完毕跳出循环

此逻辑参阅苹果官方源码AudioConverterFileConvertTest

注意事项

  1. 44100转化为48000需求刺进适宜的滤波器,否则噪音过大,而且每次编码输入有必要刚好转化为输出
  2. iOS运用硬件编码器时需求重视AVAudioSessionInterruptionNotification,此刻硬件或许被停用或被其他app占用,需求停止convert,在收到中断完毕时依靠kAudioConverterPropertyCanResumeFromInterruption获取能否恢复。假如不想重视显现运用软编即可
  3. 从一种杂乱格局到另一种杂乱格局(无损格局除外,无损格局一般便是pcm加数据头),最好运用中间层pcm过度,也便是先解码再编码