AudioConvertRef核心API介绍
-
创建转换器
objectivecAudioConverterNew(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显示传入软编码 -
转换格式API
objectivecextern 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)
苹果为什么不在这个函数提供一个数据入参? 对于常规转换(恒定码率),定输入数据,要求填充数据的回调只执行一次就能拿到数据,这么看来确实没必要搞一个回调函数。但是一些格式(非恒定码率)或者给编码器添加了__滤波器__后,底层需要你多次提供数据才能正确编解码,因为这些操作都是和上下文数据相关的,通俗的说下一次解码或抑波依赖上一次数据,当你提供的数据不够本次无法解码或滤波他就会要求你提供更多,所以传一次数据传入是不可行的。
-
填充待转换数据回调
objectivectypedef OSStatus (*AudioConverterComplexInputDataProc)(AudioConverterRef inAudioConverter, UInt32 * ioNumberDataPackets, AudioBufferList * ioData, AudioStreamPacketDescription * __nullable * __nullable outDataPacketDescription, void * __nullable inUserData);
inAudioConverter
转换对象实例, 一般我们不使用, 当然如果用户上下文信息不够,我们能通过此获取一些信息,例如input stream Format和output stream formatioNumberDataPackets
这个参数表明本次回调底层期望得到的原始包数量,我们按照它的值填充数据即可。如果我们的数据不足无法按照它期望的数量填充,此时我们可以填充一部分,并且将此值修改为填充的真实包数量,但是这会导致编码器在不久的将来再次出发此回调。通俗的说比如编码器需要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位深数据输出:
- 创建编码器和环形缓冲区,开启采集,同时开启编码线程
- 采集中如果有数据回调直接将数据存放入环形缓冲,并且释放信号量阻塞
- 编码线程中循环读取固定数量的数据(更为合理的是固定期望阻塞编码器回调,这样能稳定得到固定数量的输出,这样更有利于固定数据回调,例如webrtc 10ms数据回调),如果数据不足则使用信号量阻塞一段时间(阻塞时长应不小于数据回调周期,避免反复触发阻塞)
- 编码完成将数据存入缓冲即可
此逻辑详细可参考webrtc
源码 audio_device_mac.cc
, 他从coreAudio提供的IOProc种读取硬件采集数据,然后转换为48000 1 16的格式,固定期望480帧也就是10ms,然后送入device buffer进一步送往transport做3A处理
我们再简述一个场景,文件转码aif转换为aac
- 读取源文件AudioFileID,根据fileID读取源asbd
- 确定目的输出格式asbd,如果非PCM则需要从kAudioFormatProperty_FormatInfo填充
- 根据源和目的asbd创建转换器,将原始文件的MagicCookieData填入编码器
- 根据convert重新换获取源asbd和目的asbd
- 设置aac格式的吞码率,创建目的文件AudioFileID,将MagicCookieData从convert写入目的文件,如果声道数大于2需要将channelLayout写入文件
- 创建32768字节的输入缓冲,估算缓冲能承载的最少原始包数量,并且为些原始包创建包描述数组,将这些组成一个结构体用于用户上下文
- 创建32768字节的目的缓冲,估算所能容纳的最少期望包数量
- 无限循环填充编码器,在回调函数中从源fileID 读取最少原始包数量然后填充,如果不足就填充实际值,如果没有数据返回-1
- 单次编码结束,判定是否为-1,如果为-1则编码结束跳出循环
此逻辑参考苹果官方源码AudioConverterFileConvertTest
注意事项
- 44100转换为48000需要插入合适的滤波器,否则噪音过大,并且每次编码输入必须刚好转换为输出
- iOS使用硬件编码器时需要关注AVAudioSessionInterruptionNotification,此时硬件可能被停用或被其他app占用,需要停止convert,在收到中断结束时依赖kAudioConverterPropertyCanResumeFromInterruption获取能否恢复。如果不想关注显示使用软编即可
- 从一种复杂格式到另一种复杂格式(无损格式除外,无损格式一般就是pcm加数据头),最好使用中间层pcm过度,也就是先解码再编码