audio_unit_mp3_pcm - ShenYj/ShenYj.github.io GitHub Wiki

Audio Unit

通过 Audio Unit 实时解码并播放 MP3 流数据

前言

StreamingKit 笔记

从 StreamingKit 中可以学习到,当播放网络音频时,使用 CFNetwork 获取音频流,并先从 MIME type 获取媒体类型得知原始音频的数据类型

在 StreamingKit 中使用的是 AudioFileStreamOpen 来解析音频

AudioFile 提供了两种 open方法,AudioFileOpenURL 用来读取本地文, AudioFileOpenWithCallbacks 的使用场景比前者要广泛

AudioFile 并不能独立用于音频流的读取,需要配合 AudioStreamFile 使用才能读取流(需要用 AudioStreamFile 来判断文件格式信息可读之后再调用 Open 方法)

AudioFileStream vs AudioFile

AudioFile和AudioFileStream都可以用来解析采样率、码率、时长等信息,分离原始音频数据中的音频帧。这两个都可以使用在流式播放中,当然,不仅限于流播,本地音频也一样可以使用

AudioFile的功能远比AudioFileStream强大,除了共同的解析音频数据分离音频帧之外,它还可以读取音频数据,甚至可以写音频(生成音频文件),而AudioFileStream本身没有读取音频数据的功能。看起来选择AudioFile就OK了,不料AudioFile却需要AudioFileStream来保证数据的完整性,否则会大大增加出错的可能性

这部分对比结论来源于网络 iOS音频播放 (六):简单的音频播放器实现

  • 第一,对于网络流播必须有AudioFileStream的支持,这是因为我们在第四篇中提到过AudioFile在Open时会要求使用者提供数据,如果提供的数据不足会直接跳过并且返回错误码,而数据不足的情况在网络流中很常见,故无法使用AudioFile单独进行网络流数据的解析;

  • 第二,对于本地音乐播放选用AudioFile更为合适,原因如下: AudioFileStream的主要是用在流播放中虽然不限于网络流和本地流,但流数据是按顺序提供的所以AudioFileStream也是顺序解析的,被解析的音频文件还是需要符合流播放的特性,对于不符合的本地文件AudioFileStream会在Parse时返回NotOptimized错误; AudioFile的解析过程并不是顺序的,它会在解析时通过回调向使用者索要某个位置的数据,即使数据在文件末尾也不要紧,所以AudioFile适用于所有类型的音频文件;

基于以上两点我们可以得出这样一个结论:一款完整功能的播放器应当同时使用AudioFileStream和AudioFile,用AudioFileStream来应对可以进行流播放的音频数据,以达到边播放边缓冲的最佳体验,用AudioFile来处理无法流播放的音频数据,让用户在下载完成之后仍然能够进行播放。

音频播放流程

一个经典的音频播放流程(以MP3为例):

  1. 读取MP3文件
  2. 解析采样率、码率、时长等信息,分离MP3中的音频帧
  3. 对分离出来的音频帧解码得到PCM数据
  4. 对PCM数据进行音效处理(均衡器、混响器等,非必须)
  5. 把PCM数据解码成音频信号
  6. 把音频信号交给硬件播放
  7. 重复1-6步直到播放完成

AAC解码流程

  1. 判断文件格式,确定为ADIF或ADTS
  2. 若为ADIF,则解ADIF头信息,跳至第(6)步
  3. 若为ADTS,则寻找同步头
  4. 解ADTS帧头信息
  5. 若有错误检测,则进行错误检测
  6. 解块信息
  7. 解元素信息

创建音频文件流解析器

AudioFileStreamOpen 函数用来创建一个音频文件流解析器

音频文件流解析器的作用是用来读取采样率、码率、时长等基本信息以及分离音频帧

extern OSStatus AudioFileStreamOpen(void * inClientData, 
                                    AudioFileStream_PropertyListenerProc inPropertyListenerProc, 
                                    AudioFileStream_PacketsProc inPacketsProc, 
                                    AudioFileTypeID inFileTypeHint, 
                                    AudioFileStreamID * outAudioFileStream);
  • 参数说明

    • inClientData, 指向要传递给回调函数的值或结构的指针。
    • inPropertyListenerProc, 属性侦听器回调。
      每当解析器在数据流中找到属性值时,它都会使用属性ID调用您的属性侦听器。
      然后可以调用AudioFile和Audio函数来获取属性的值。
    • inPacketsProc, 音频数据回调。
      每当解析器在数据流中找到音频数据包时,它都会将数据传递到您的音频数据回调。
    • inFileTypeHint, 音频文件类型提示。
      如果打算传递给解析器的音频文件流是解析器无法从数据中轻松或唯一确定的类型(如ADTS或AC3),
      可以使用此参数来指示类型。可能的值列在音频文件服务的Audio枚举中。
      如果不知道音频文件类型,传递 0。
    • outAudioFileStream, 输出时,一个表示音频文件流解析器的不透明对象.
      为音频文件流解析器ID。您需要将此ID传递给音频文件流API中的其他函数。

解析数据

调用 AudioFileStreamParseBytes 函数,将数据传递给解析器

extern OSStatus AudioFileStreamParseBytes(AudioFileStreamID inAudioFileStream, 
                                            UInt32 inDataByteSize, 
                                            const void * inData, 
                                            AudioFileStreamParseFlags inFlags);
  • 参数说明

    • inAudioFileStream 您希望将数据传递给的解析器的ID。Audio函数返回解析器ID。
    • inDataByteSize 要解析的数据字节数。
    • inData 要解析的数据。
    • inFlags 音频文件流标志。
      这次的解析和上一次解析是否是连续的关系,如果是连续的传入0,否则传入 kAudioFileStreamParseFlag_Discontinuity

解析文件格式信息

调用 AudioFileStreamParseBytes 函数时,会先读取格式信息,并同步的进入 AudioFileStream_PropertyListenerProc 回调方法

AudioFileStream_PropertyListenerProc

这个回调函数就是我们在调用AudioFileStreamOpen函数创建解析器的时候传递的第二个参数

AudioFileStream_PropertyListenerProc 是一个别名, 真实函数类型是:

typedef void (*AudioFileStream_PropertyListenerProc)(void * inClientData,
                                                    AudioFileStreamID inAudioFileStream,
                                                    AudioFileStreamPropertyID inPropertyID,
                                                    UInt32 * ioFlags);

如果回调得到 kAudioFileStreamProperty_ReadyToProducePackets 表示解析格式信息完成

  • 参数说明

    • inClientData Open方法中的上下文对象;
    • inAudioFileStream 表示当前解析器 FileStream 的ID;
    • inPropertyID 是此次回调解析的信息 ID。
      表示当前 PropertyID 对应的信息已经解析完成信息(例如数据格式、音频数据的偏移量等等)
      使用者可以通过 AudioFileStreamGetProperty 接口获取 PropertyID 对应的值或者数据结构;
    • ioFlags 是一个返回参数,表示这个 property 是否需要被缓存
      如果需要赋值 kAudioFileStreamPropertyFlag_PropertyIsCached 否则不赋值 这个回调会进来多次,但并不是每一次都需要进行处理,可以根据需求处理需要的PropertyID进行处理(PropertyID列表如下)。

分离音频帧

读取格式信息完成之后继续调用 AudioFileStreamParseBytes 函数可以对帧进行分离,并同步的进入 AudioFileStream_PacketsProc 回调方法

AudioFileStream_PacketsProc

这个回调函数就是我们在调用AudioFileStreamOpen函数创建解析器的时候传递的第三个参数

typedef void (*AudioFileStream_PacketsProc)(void *inClientData,
                                            UInt32 inNumberBytes,
                                            UInt32 inNumberPackets,
                                            const void * inInputData,
                                            AudioStreamPacketDescription * __nullable inPacketDescriptions);
  • 参数说明

    • inClientData,上下文对象;
    • inNumberBytes,本次处理的数据大小;
    • inNumberPackets,本次总共处理了多少帧(即代码里的Packet);
    • inInputData,本次处理的所有数据;
    • inPacketDescriptions,AudioStreamPacketDescription数组,存储了每一帧数据是从第几个字节开始的,这一帧总共多少字节。

AudioConverter编解码

AudioConverter 提供了三个函数用于编解码

  • AudioConverterConvertBuffer

    OSStatus AudioConverterConvertBuffer(AudioConverterRef inAudioConverter,
                                            UInt32 inInputDataSize,
                                            const void *inInputData,
                                            UInt32 *ioOutputDataSize,
                                            void *outOutputData);
    

    discussion:
    此功能用于将一种线性PCM格式转换为另一种特殊情况的转换。此功能无法执行采样率转换,也不能用于转换为或从大多数压缩格式。要执行这些类型的转换,请使用AudioConverterFillComplexBuffer。

  • AudioConverterConvertComplexBuffer

    extern OSStatus AudioConverterConvertComplexBuffer(AudioConverterRef inAudioConverter, 
                                                        UInt32 inNumberPCMFrames, 
                                                        const AudioBufferList * inInputData, 
                                                        AudioBufferList * outOutputData);
    

    discussion:
    此功能适用于无采样率转换的线性PCM到线性PCM音频数据格式转换。

  • AudioConverterFillComplexBuffer

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

    discussion:
    使用此功能进行所有音频数据格式转换,除非是特殊情况,即从一种线性PCM格式转换为另一种格式且不进行采样率转换。

前两个函数功能类似,都只支持PCM之间的转换,并且两种PCM的采样率必须一致。也就是说无法从PCM转换成其他压缩格式或者从压缩格式转换成PCM
在 StreamingKit 中也是使用了 AudioConverterFillComplexBuffer 来实现解码,

AudioConverterFillComplexBuffer

实现非 PCM之间转换,重点就是学习这个函数的使用

关于函数的参数说明
  • 第一个参数,inAudioConverter 是初始化得到的 AudioConverter对象。
  • 第二个参数,inInputDataProc 是提供音频数据进行转换的回调函数。
    当 AudioConverter 准备好新的输入数据时,这个回调被重复调用。
  • 第三个参数,inInputDataProcUserData 是上下文对象。
  • 第四个参数,ioOutputDataPacketSize,在输入时代表另一个参数 outOutputData 的大小(以音频包表示),在输出时会写入已经转换了的数据包数。
    如果调用完毕 ioOutputDataPacketSize == 0,说明 EOF(end of file)。
  • 第五个参数,outOutputData 代表转换后的数据输出。
  • 第六个参数,outPacketDescription 在输入时,必须指向能够保存 ioOutputDataPacketSize * sizeof(AudioStreamPacketDescription) 内存块。
    在输出时如果非空,并且 AudioConverter 的输出格式使用 AudioStreamPacketDescription 来描述,则会被写入一个 AudioStreamPacketDescription 数组。
AudioConverterComplexInputDataProc
  • AudioConverterComplexInputDataProc

    typedef int (*)(struct OpaqueAudioConverter *, 
                    unsigned int *, 
                    struct AudioBufferList *, 
                    struct AudioStreamPacketDescription **, void *) AudioConverterComplexInputDataProc;
    
  • 参数说明

    • inAudioConverter

      调用此回调以获取新数据以进行转换的音频转换器对象。

    • ioNumberDataPackets

      在输入时,转换器在其当前转换周期中需要的最小输入音频数据包数。在输出时,提供的音频数据包数以进行转换,如果没有更多数据要转换,则为0。

    • ioData

      在输出时,将此参数传递的 AudioBufferList 结构字段指向您提供的要转换的音频数据。

    • outDataPacketDescription

      如果输入时不为NULL,音频转换器期望此回调在输出时提供 AudioStreamPacketDescription 结构数组的数组,每个数组对应于您在ioData 参数中提供的每个音频数据包。

    • inUserData

      在输入时,您提供给 AudioConverterFillComplexBuffer 函数的自定义应用程序数据。

参考资料

详细的博客资料

AudiosExample-AudioUnitRecorderDemo 这个 Demo 中,使用了 Audio Unit 实现了一个录音和播放 PCM 文件的功能,同时使用到了 Extended Audio File Services,在录音的同时,将录音写入本地沙盒目录下

三个成员变量分别是

  • mDataByteSize 数据包中的字节数。
  • mStartOffset 从缓冲区开头到数据包开头的字节数。
  • mVariableFramesInPacket 数据包中数据样本帧的数量。
⚠️ **GitHub.com Fallback** ⚠️