AudioConvertRef

AudioConvertRef核心API介绍

  1. 创建转换器

    objectivec 复制代码
    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

    objectivec 复制代码
    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. 填充待转换数据回调

    objectivec 复制代码
    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过度,也就是先解码再编码
相关推荐
百万蹄蹄向前冲3 小时前
2024不一样的VUE3期末考查
前端·javascript·程序员
陈哥聊测试1 天前
软件格局在变,谁能扛起国产替代的大旗?
安全·程序员·产品
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭2 天前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
少年姜太公2 天前
从零开始详解js中的this(下)
前端·javascript·程序员
凌虚2 天前
Kubernetes APF(API 优先级和公平调度)简介
后端·程序员·kubernetes
小华同学ai2 天前
ShowDoc:Star12.3k,福利项目,个人小团队的在线文档“简单、易用、轻量化”还专门针对API文档、技术文档做了优化
前端·程序员·github
小青鱼4 天前
AI编程-Cursor从入门到精通系列之常用概念及解释(二)
人工智能·程序员
捡田螺的小男孩5 天前
参数校验的十个建议!收藏好,别再给测试机会提bug~
java·后端·程序员
哔哩哔哩技术5 天前
B站装机系统实践:从初创到规模化的演进
前端·程序员
程序员鱼皮5 天前
没事别想不开去创业!
计算机·面试·程序员·项目