Audio Queue录制 播放原理

5,269 阅读28分钟

阅读前提:

  • C语言基础
  • 音视频基础
  • Core Audio基本数据结构
  • Audio Session

Audio Queue Services是官方推荐的方式以一种直接的,低开销的方式在iOS与Mac OS X中完成录制与播放的操作.不像上层的API,它可以通过回调拿到音频帧数据,以完成更加精细的操作.

使用场景:

比上层API而言,可以直接获取每一帧音频数据,因此可以对音频帧做一些需要的处理. 但是无法对声音做一些更加精细的处理,如回声消除,混音,降噪等等,如果需要做更底层的操作,需要使用Audio Unit.

Overview

Audio Queue Service是Core Audio的Audio Toolbox框架中的基于C语言的一套接口.

Audio Queue Services是一套高级的API. 它不仅可以在无需了解硬件的基础上使程序与音频硬件(麦克风,扬声器等)之间完成交互,也在无需了解编解码器的原理情况下让我们使用复杂的编解码器.

同时,Audio Queue Services还提供了更加精细的定时控制以支持预定的播放与同步任务.可以使用它同步多个音频播放队列或者音视频间进行同步.

支持以下格式

  • 线性PCM
  • Apple提供的本机支持的任何压缩格式
  • 用户使用编解码器生成的任何格式

注意: Audio Queue Services是一套纯C的接口,所以基础的C,C++需要有一定了解.

1. Audio Queues概述

在iOS, Mac OS X中audio queue是一个软件层面的对象,可以用来做录制与播放操作.使用AudioQueueRef代表其数据结构.

作用

  • 连接音频硬件
  • 管理相关模块内存
  • 使用编解码器
  • 调解录制与播放

1.1. Audio Queue架构

  • 一组音频队列数据,队列中每个结点都是音频数据的临时存储库.
  • 队列中数据是严格按照顺序排列
  • 回调函数

1.2. 录制

如果要使用audio queue的录制功能,通过AudioQueueNewInput创建录音队列.

1.record

录制使用的audio queue的输入端通常是当前设备连接的音频设备,如内置的麦克风,或外置的带麦克风功能的输入设备.输出端是我们定义的回调函数.如果将音频数据录制成文件,可以在回调函数中将从audio queue中取出的音频数据写入文件.当然录制的音频数据也可以直接送给当前App以实现边录制边播放的功能.

每个audio queue,不管是用于录制或播放,都至少有一个或多个音频数据.所有的音频数据被放在一个被称为音频队列buffer特殊的数据结构中,可以理解成队列中的结点.如上图所示,指定数量的buffer按顺序依次被放入音频队列中,它们最终也将在回调函数中按顺序取出.

1.3. 播放

如果要使用audio queue的播放功能,通过AudioQueueNewOutput创建播放队列对象.

2.play

播放使用的音频队列,回调函数在输入端.该回调函数将从本地或其他音频数据源获取到的数据交给音频队列中.当没有数据装入播放回调函数也会告诉音频队列停止播放.

用于播放的音频队列的输出端则连接着音频输出硬件,如扬声器或外接的具有扬声器功能的音频设备(如:耳机,音响等).

1.4. 音频队列数据

AudioQueueBuffer用于存放音频队列数据.

typedef struct AudioQueueBuffer {
    const UInt32   mAudioDataBytesCapacity;
    void *const    mAudioData;
    UInt32         mAudioDataByteSize;
    void           *mUserData;
} AudioQueueBuffer;
typedef AudioQueueBuffer *AudioQueueBufferRef;
  • mAudioData: 当前取出的队列中存放的即时的音频数据指针,它指向真正存放音频数据的内存地址.
  • mAudioDataBytesCapacity: 当前音频数据最大存储空间
  • mAudioDataByteSize: 当前存储的音频数据实际的大小
  • mUserData: 开发者可以存放一些自定义的数据

音频队列可以使用任意数量的音频数据结点,但一般建议用3个即可.因为如果太少则存取过于频繁,太多则增加应用程序内存消耗,正常情况下两个即可,我们可以使用第三个当有延迟情况出现时作为补偿数据.

因为Audio Queue的纯C函数,内存需要我们手动管理.

  • 初始化Audio Queue时使用AudioQueueAllocateBuffer分配内存
  • 回调函数中用完时使用AudioQueueDispose回收内存

通过内存管理,可以使录制播放更加稳定,同时优化App资源使用.

1.5. 音频队列与入队操作

audio queue: 音频队列, 即Audio Queue Services的名字

audio queue buffer : 音频队列中存放一个或多个结点数据

  • 录制过程

做录制操作时,一个audio queue buffer将被从输入设备(如:麦克风)采集的音频数据填充.音频队列中剩余的buffer按顺序排列在当前填充数据的buffer之后,依次等待被填充数据.在输出端,回调函数将按照指定时间间隔依次接收音频队列中按顺序排列好的音频数据.工作原理如下图:

3.recording_process

图一: 录制开始,音频队列中填充需要的音频数据.

图二: 第一个buffer被填充,对调函数取出buffer 1并将其写入文件,同时buffer2也被填充完数据.

图三: 在第4步,回调函数将用完的buffer 1重新放回音频队列,随后第五步回调函数再次取出音频数据buffer2,最终将其写入文件而后重新放回音频队列此后循环往复直到录制停止.

  • 播放过程

做播放操作时,一个audio queue buffer需要交给输出设备(如:扬声器).剩余的音频数据也将按顺序排列在当前取出播放的音频数据之后,等待播放.回调函数将按顺序取出音频队列中的数据交给扬声器,随后将用完的audio queue buffer重新放入音频队列.

4.playback_process

图1: 应用程序启动音频播放队列,每调用依次回调函数填充一个audio queue buffers,填充完后将其放入音频队列. 当应用程序调用AudioQueueStart立即开始播放.

图2: 音频队列输出第一个音频数据

图3: 用完的audio queue buffer重新放入音频队列.一旦播放了第一个音频数据,音频队列会进入一个循环稳定的状态,即开始播放下一个buffer2(第4步)然后调用回调函数准备填充数据(第5步),最后(第6步)buffer1重新被填充并装入音频队列依次循环直到音频队列停止.

  • 控制播放的过程

Audio queue buffers始终按照入队顺序进行播放.然而可以使用AudioQueueEnqueueBufferWithParameters函数做一些额外控制

a. 设置缓冲区精确的播放时间,用于同步

b. 可以裁剪开始或结尾的audio queue buffer,这使我们可以做到开始或结尾的静音效果.

c. 增加播放的声音

后文播放章节中将具体介绍.

1.6. 回调函数

无论录制还是播放,一旦注册好回调函数,它将频繁的被调用.调用时间取决于我们的设置.回调函数的一个重要职责是将用完的数据重新交给音频队列.使用AudioQueueEnqueueBuffer入队.

1.6.1. 录制的回调函数
AudioQueueInputCallback (
    void                               *inUserData,
    AudioQueueRef                      inAQ,
    AudioQueueBufferRef                inBuffer,
    const AudioTimeStamp               *inStartTime,
    UInt32                             inNumberPacketDescriptions,
    const AudioStreamPacketDescription *inPacketDescs
);

当输入端采集到音频数据时就会触发回调,可以从回调函数中取出装有音频数据的audio queue buffer.

  • inUserData: 自定义的数据,开发者可以传入一些我们需要的数据供回调函数使用.注意:一般情况下我们需要将当前的OC类实例传入,因为回调函数是纯C语言,不能调用OC类中的属性与方法,所以传入OC实例以与本类中属性方法交互.
  • inAQ: 调用回调函数的音频队列
  • inBuffer: 装有音频数据的audio queue buffer.
  • inStartTime: 当前音频数据的时间戳.主要用于同步.
  • inNumberPacketDescriptions: 数据包描述参数.如果你正在录制VBR格式,音频队列会提供此参数的值.如果录制文件需要将其传递给AudioFileWritePackets函数.CBR格式不使用此参数.
  • inPacketDescs: 音频数据中一组packet描述.如果是VBR格式数据,如果录制文件需要将此值传递给AudioFileWritePackets函数
1.6.2. 播放的回调函数
AudioQueueOutputCallback (
    void                  *inUserData,
    AudioQueueRef         inAQ,
    AudioQueueBufferRef   inBuffer
);

在回调函数中将读取音频数据以用来播放

  • inUserData:自定义的数据,开发者可以传入一些我们需要的数据供回调函数使用.注意:一般情况下我们需要将当前的OC类实例传入,因为回调函数是纯C语言,不能调用OC类中的属性与方法,所以传入OC实例以与本类中属性方法交互.
  • inAQ:调用回调函数的音频队列
  • inBuffer:回调将要填充的数据。

如果应用程序正在播放VBR格式数据,这个回调函数需要通过AudioFileReadPackets获取音频数据包信息.然后,回调将数据包信息放入自定义数据结构中,以使其可用于播放音频队列。

1.7. 使用编解码器

Audio Queue Services使音频编解码器用于转换音频数据格式.你的录制或播放可以使用编解码器支持的任意格式.

每个audio queue有一个自己的音频数据格式,被封装在AudioStreamBasicDescription中,通过mFormatID可以指定音频数据格式,audio queue会自动选择适当编解码器对其压缩.开发者可以指定采样率,声道数等等参数自定义音频数据.

5.record_convert

如上图,应用程序告诉音频队列使用指定格式开始录制,音频队列在获取到原生的PCM数据后使用编码器将其转换为AAC类型数据,然后音频队列通知回调函数,将转换好的数据放入audio queue buffer中传给回调函数.最后,回调函数拿到转换好的AAC数据进行使用.

6.play_convert

如上图,应用程序告诉音频队列播放指定的格式(AAC)的文件,音频队列调用回调函数从音频文件中读取音频数据,回调函数将原始格式的数据传给音频队列.最后,音频队列使用合适的解码器将音频数据(PCM)交给扬声器.

音频队列可以利用任何编解码器无论是系统自带的还是第三方安装的(仅Mac OS)

1.7. 生命周期

音频队列在创建与销毁间的活动范围称为它的声明周期.

  • Start (AudioQueueStart): 初始化
  • Prime (AudioQueuePrime): 仅用于播放,在调用AudioQueueStart前调用它确保当有可用的音频数据时能够立即播放.
  • Stop (AudioQueueStop): 重置音频队列,停止播放与录制.
  • Pause (AudioQueuePause): 暂停录制,播放不会影响音频队列中已有的数据.调用AudioQueueStart恢复.
  • Flush (AudioQueueFlush): 在音频队列最后一个buffer入队时调用,确保所有的音频数据处理完毕.
  • Reset (AudioQueueReset): 调用后会立即静音,音频队列移除所有数据并且重置编解码器与DSP状态.

AudioQueueStop可以选择以同步或异步的方式停止.

  • Synchronous: 立即停止,忽略队列中的数据
  • Asynchronous: 当队列中所有数据被取出用完后再停止.

1.8. 参数设置

音频队列有一个可以调节的设置称为参数,每个参数都有一个枚举常量作为其键,一个浮点型作为其值,该值仅用于播放.

以下有两种方式设置参数

  • 对于每个audio queue, 使用AudioQueueSetParameter:立即改变
  • 对于每个audio queue buffer,使用AudioQueueEnqueueBufferWithParameters,在入队时进行设置,播放时,此类更改将生效。

使用kAudioQueueParam_Volume可以调节播放音量(0.0~1.0)

2. 录制

使用Audio Queue Services进行录制,输出端可以是一个文件,网络协议传输,拷贝给一个对象等等.这里仅介绍输出到文件.

流程

  • 自定义一个结构体去管理音频格式,状态,文件路径等等...
  • 使用audio queue做录制
  • 选择需要的每个音频数据的大小,如果需要还可以生成magic cookies(元数据信息).
  • 设置自定义音频数据格式,指定文件路径.
  • 创建audio queue,分配audio queue buffer内存,执行入队操作.
  • 告诉audio queue开始录制
  • 完成时停止audio queue并且回收audio queue buffer的内存.

2.1. 使用自定义结构体管理状态信息

第一步是自定义一个结构体管理音频格式及状态信息.

static const int kNumberBuffers = 3;                            // 1
struct AQRecorderState {
    AudioStreamBasicDescription  mDataFormat;                   // 2
    AudioQueueRef                mQueue;                        // 3
    AudioQueueBufferRef          mBuffers[kNumberBuffers];      // 4
    AudioFileID                  mAudioFile;                    // 5
    UInt32                       bufferByteSize;                // 6
    SInt64                       mCurrentPacket;                // 7
    bool                         mIsRunning;                    // 8
};
  • kNumberBuffers: 使用多少个音频队列数据.
  • mDataFormat: 指定音频数据格式
  • mQueue: 应用程序创建的录制音频队列.
  • mBuffers: 音频队列中音频数据指针的数组
  • mAudioFile: 录制的文件
  • bufferByteSize: 当前录制的文件的大小(单位是bytes)
  • mCurrentPacket: 要写入当前录制文件的音频数据包的索引
  • mIsRunning: 当前音频队列是否正在运行.

2.2. 回调函数

static void HandleInputBuffer (
    void                                *aqData,             // 1
    AudioQueueRef                       inAQ,                // 2
    AudioQueueBufferRef                 inBuffer,            // 3
    const AudioTimeStamp                *inStartTime,        // 4
    UInt32                              inNumPackets,        // 5
    const AudioStreamPacketDescription  *inPacketDesc        // 6
)
  • aqData: 自定义的数据,开发者可以传入一些我们需要的数据供回调函数使用.注意:一般情况下我们需要将当前的OC类实例传入,因为回调函数是纯C语言,不能调用OC类中的属性与方法,所以传入OC实例以与本类中属性方法交互.
  • inAQ: 调用回调函数的音频队列
  • inBuffer: 装有音频数据的audio queue buffer.
  • inStartTime: 当前音频数据的时间戳.主要用于同步.
  • inNumberPacketDescriptions: 数据包描述参数.如果你正在录制VBR格式,音频队列会提供此参数的值.如果录制文件需要将其传递给AudioFileWritePackets函数.CBR格式不使用此参数(值为0).
  • inPacketDescs: 音频数据中一组packet描述.如果是VBR格式数据,如果录制文件需要将此值传递给AudioFileWritePackets函数

2.3. 将数据写入本地文件

使用AudioFileWritePackets将数据写入音频文件.

AudioFileWritePackets (                     // 1
    pAqData->mAudioFile,                    // 2
    false,                                  // 3
    inBuffer->mAudioDataByteSize,           // 4
    inPacketDesc,                           // 5
    pAqData->mCurrentPacket,                // 6
    &inNumPackets,                          // 7
    inBuffer->mAudioData                    // 8
);

  • 1.将音频数据写入音频文件
  • 2.要写入的音频文件
  • 3.使用false表示写入文件时不应缓存数据
  • 4.被写入文件的大小
  • 5.一组音频数据包的描述,如2.2中介绍,如果是CBR设置为NULL,如果是VBR需要设置回调函数中的inPacketDesc参数.
  • 6.当前写入的数据包的索引
  • 7.输入(录制)时,要写入的数据包数。输出(播放)时,实际写入的数据包数
  • 8.要写入的音频数据.

2.4. 入队

当音频数据在回调函数中用完后,需要重新放回音频队列以便存储新的音频数据

AudioQueueEnqueueBuffer (                    // 1
    pAqData->mQueue,                         // 2
    inBuffer,                                // 3
    0,                                       // 4
    NULL                                     // 5
);
  • 1.将音频数据放入音频队列
  • 2.录制的音频队列
  • 3.等待入队的音频数据
  • 4.音频数据包的描述信息,设置为0因为该参数不用于录制.
  • 5.描述音频队列数据的数据包描述数组。设置为NULL因为该参数不用于录制.

2.5. 完整的录制回调

static void HandleInputBuffer (
    void                                 *aqData,
    AudioQueueRef                        inAQ,
    AudioQueueBufferRef                  inBuffer,
    const AudioTimeStamp                 *inStartTime,
    UInt32                               inNumPackets,
    const AudioStreamPacketDescription   *inPacketDesc
) {
    AQRecorderState *pAqData = (AQRecorderState *) aqData;               // 1
 
    if (inNumPackets == 0 &&                                             // 2
          pAqData->mDataFormat.mBytesPerPacket != 0)
       inNumPackets =
           inBuffer->mAudioDataByteSize / pAqData->mDataFormat.mBytesPerPacket;
 
    if (AudioFileWritePackets (                                          // 3
            pAqData->mAudioFile,
            false,
            inBuffer->mAudioDataByteSize,
            inPacketDesc,
            pAqData->mCurrentPacket,
            &inNumPackets,
            inBuffer->mAudioData
        ) == noErr) {
            pAqData->mCurrentPacket += inNumPackets;                     // 4
    }
   if (pAqData->mIsRunning == 0)                                         // 5
      return;
 
    AudioQueueEnqueueBuffer (                                            // 6
        pAqData->mQueue,
        inBuffer,
        0,
        NULL
    );
}
  • 1.用于记录音频队列一些信息的结构体,里面包含当前录制文件的信息,状态等等参数.
  • 2.如果音频数据是CBR数据,计算当前数据中包含多少个音频数据包.对于VBR数据,可以直接从回调函数中的inNumPackets参数获取.
  • 3.将音频数据写入音频文件
  • 4.如果成功的话,需要将音频数据包索引累加,以便下次可以继续录制
  • 5.如果audio queue已经停止则返回.
  • 6.使用完的音频队列数据重新装入音频队列.

2.6. 获取Audio Queue Buffer大小

void DeriveBufferSize (
    AudioQueueRef                audioQueue,                  // 1
    AudioStreamBasicDescription  &ASBDescription,             // 2
    Float64                      seconds,                     // 3
    UInt32                       *outBufferSize               // 4
) {
    static const int maxBufferSize = 0x50000;                 // 5
 
    int maxPacketSize = ASBDescription.mBytesPerPacket;       // 6
    if (maxPacketSize == 0) {                                 // 7
        UInt32 maxVBRPacketSize = sizeof(maxPacketSize);
        AudioQueueGetProperty (
                audioQueue,
                kAudioQueueProperty_MaximumOutputPacketSize,
                // in Mac OS X v10.5, instead use
                //   kAudioConverterPropertyMaximumOutputPacketSize
                &maxPacketSize,
                &maxVBRPacketSize
        );
    }
 
    Float64 numBytesForTime =
        ASBDescription.mSampleRate * maxPacketSize * seconds; // 8
    *outBufferSize =
    UInt32 (numBytesForTime < maxBufferSize ?
        numBytesForTime : maxBufferSize);                     // 9
}
  • 1.指定的音频队列
  • 2.音频队列配置信息
  • 3.音频数据采集的间隔(可以通过采样率与间隔算出每个采集数据的大小)
  • 4.通过该参数返回计算出的音频数据的大小
  • 5.音频队列数据大小的上限,以字节为单位。在此示例中,上限设置为320 KB。这相当于采样速率为96 kHz的大约5秒的立体声,24位音频。
  • 6.对于CBR的数据,可以从ASBD中获取该值大小.如果是VBR数据,ASBD中取出得值为0.
  • 7.对于VBR数据,需要手动估算一个最大值.
  • 8.获取音频数据大小(字节)
  • 9.如果需要,限制音频数据最大值.

2.7. 为音频文件设置magin cookie

对于一些压缩音频数据格式,如AAC,MPEG 4 AAC等,必须包含音频元数据.包含该元数据信息的数据结构称为magic cookies.当你录制压缩音频数据格式的音频文件时,必须从audio queue中获取元数据并将其设置给音频文件.

注意: 我们在录制前与停止录制后两个时间点都设置一次magin cookie,因为有的编码器需要在停止录制后更新magin cookie.

OSStatus SetMagicCookieForFile (
    AudioQueueRef inQueue,                                      // 1
    AudioFileID   inFile                                        // 2
) {
    OSStatus result = noErr;                                    // 3
    UInt32 cookieSize;                                          // 4
 
    if (
            AudioQueueGetPropertySize (                         // 5
                inQueue,
                kAudioQueueProperty_MagicCookie,
                &cookieSize
            ) == noErr
    ) {
        char* magicCookie =
            (char *) malloc (cookieSize);                       // 6
        if (
                AudioQueueGetProperty (                         // 7
                    inQueue,
                    kAudioQueueProperty_MagicCookie,
                    magicCookie,
                    &cookieSize
                ) == noErr
        )
            result =    AudioFileSetProperty (                  // 8
                            inFile,
                            kAudioFilePropertyMagicCookieData,
                            cookieSize,
                            magicCookie
                        );
        free (magicCookie);                                     // 9
    }
    return result;                                              // 10
}
  • 1.录制的音频队列
  • 2.准备录制的文件
  • 3.定义一个变量记录设置是否成功
  • 4.定义一个变量记录magic cookie的大小
  • 5.从audio queue中获取magic cookie的大小.
  • 6.定义一个变量记录magic cookie的内容并为其分配需要的内存
  • 7.从audio queue中获取magic cookie的内容
  • 8.将获取到的magic cookie设置到文件中.
  • 9.释放刚才临时保存的magic cookie变量
  • 10.返回设置的结果

2.8.设置录制音频的格式.

主要关注以下参数

  • 音频格式(PCM,AAC...)
  • 采样率(44.1kHz, 48kHz)
  • 声道数(单声道,双声道)
  • 采样位数(16bits)
  • 每个音频数据包中的帧数(线性PCM通常是1帧,压缩数据通常比较多)
  • 音频文件类型(CAF, AIFF...)
AQRecorderState aqData;                                       // 1
 
aqData.mDataFormat.mFormatID         = kAudioFormatLinearPCM; // 2
aqData.mDataFormat.mSampleRate       = 44100.0;               // 3
aqData.mDataFormat.mChannelsPerFrame = 2;                     // 4
aqData.mDataFormat.mBitsPerChannel   = 16;                    // 5
aqData.mDataFormat.mBytesPerPacket   =                        // 6
   aqData.mDataFormat.mBytesPerFrame =
      aqData.mDataFormat.mChannelsPerFrame * sizeof (SInt16);
aqData.mDataFormat.mFramesPerPacket  = 1;                     // 7
 
AudioFileTypeID fileType             = kAudioFileAIFFType;    // 8
aqData.mDataFormat.mFormatFlags =                             // 9
    kLinearPCMFormatFlagIsBigEndian
    | kLinearPCMFormatFlagIsSignedInteger
    | kLinearPCMFormatFlagIsPacked;

  • 1.创建一个存放音频状态信息的结构体.(结构体名字自定义)
  • 2.指定音频格式
  • 3.指定采样率
  • 4.指定声道数
  • 5.指定采样位数
  • 6.指定每个包中的字节数
  • 7.指定每个包中的帧数
  • 8.指定文件类型
  • 9.指定文件类型所需要的标志

2.9. 创建录制的Audio Queue

AudioQueueNewInput (                              // 1
    &aqData.mDataFormat,                          // 2
    HandleInputBuffer,                            // 3
    &aqData,                                      // 4
    NULL,                                         // 5
    kCFRunLoopCommonModes,                        // 6
    0,                                            // 7
    &aqData.mQueue                                // 8
);
  • 1.创建一个录制音频队列
  • 2.指定录制的音频格式
  • 3.指定回调函数
  • 4.可传入自定义的数据结构,可以是本类的实例,可以是记录音频信息的结构体
  • 5.回调函数在哪个循环中被调用.设置为NULL为默认值,即回调函数所在的线程由audio queue内部控制.
  • 6.回调函数运行循环模式通常使用kCFRunLoopCommonModes.
  • 7.保留值,只能为0.
  • 8.输出时新分配的音频队列.

2.10. 获取完整的音频格式.

当audio queue开始工作后,它可能会产生更多音频格式信息比我们初始化设置时,所以我们需要对获取到的音频数据做一个检查.

UInt32 dataFormatSize = sizeof (aqData.mDataFormat);       // 1
 
AudioQueueGetProperty (                                    // 2
    aqData.mQueue,                                         // 3
    kAudioQueueProperty_StreamDescription,                 // 4
    // in Mac OS X, instead use
    //    kAudioConverterCurrentInputStreamDescription
    &aqData.mDataFormat,                                   // 5
    &dataFormatSize                                        // 6
);
  • 1.查询音频数据格式
  • 2.获取audio queue指定属性的值
  • 3.查询的音频队列
  • 4.音频队列数据格式的ID
  • 5.作为输出,输出完整的音频数据格式
  • 6.在输入时,AudioStreamBasicDescription结构的预期大小。在输出时,实际大小。您的录制应用程序不需要使用此值。

2.11. 创建一个音频文件

CFURLRef audioFileURL =
    CFURLCreateFromFileSystemRepresentation (            // 1
        NULL,                                            // 2
        (const UInt8 *) filePath,                        // 3
        strlen (filePath),                               // 4
        false                                            // 5
    );
 
AudioFileCreateWithURL (                                 // 6
    audioFileURL,                                        // 7
    fileType,                                            // 8
    &aqData.mDataFormat,                                 // 9
    kAudioFileFlags_EraseFile,                           // 10
    &aqData.mAudioFile                                   // 11
);
  • 1.创建一个CFURL类型的对象代表录制文件路径
  • 2.使用NULL(kCFAllocatorDefault)使用当前默认的内存分配器
  • 3.设置文件路径
  • 4.文件名长度
  • 5.false表示是一个文件,不是文件夹.
  • 6.创建一个新的文件或初始化一个已经存在的文件.
  • 7.音频文件的路径(即3中创建的)
  • 8.音频文件类型.(CAF,AIFF...)
  • 9.ASBD
  • 10.设置该值表示如果文件已经存在则覆盖
  • 11.代表录制的文件.

2.12. 设置音频队列数据大小

使用2.6.章节中的函数设置音频队列数据的大小以便后续使用.

DeriveBufferSize (                               // 1
    aqData.mQueue,                               // 2
    aqData.mDataFormat,                          // 3
    0.5,                                         // 4
    &aqData.bufferByteSize                       // 5
);

2.13. 为Audio Queue准备指定数量的buffer

for (int i = 0; i < kNumberBuffers; ++i) {           // 1
    AudioQueueAllocateBuffer (                       // 2
        aqData.mQueue,                               // 3
        aqData.bufferByteSize,                       // 4
        &aqData.mBuffers[i]                          // 5
    );
 
    AudioQueueEnqueueBuffer (                        // 6
        aqData.mQueue,                               // 7
        aqData.mBuffers[i],                          // 8
        0,                                           // 9
        NULL                                         // 10
    );
}
  • 1.一般指定3个,这里为一个简单的循环,为指定数量的buffer分配内存并进行入队操作
  • 2.为每个buffer分配内存
  • 3.指定分配内存的音频队列
  • 4.指定分配内存的Buffer的大小(即2.12中获取的)
  • 5.输出一个分配好内存的buffer
  • 6.音频队列入队
  • 7.将要入队的音频队列
  • 8.将要入队的音频数据
  • 9.对于录制此参数没用
  • 10.对于录制此参数没用

2.14. 录制音频

aqData.mCurrentPacket = 0;                           // 1
aqData.mIsRunning = true;                            // 2
 
AudioQueueStart (                                    // 3
    aqData.mQueue,                                   // 4
    NULL                                             // 5
);
// Wait, on user interface thread, until user stops the recording
AudioQueueStop (                                     // 6
    aqData.mQueue,                                   // 7
    true                                             // 8
);
 
aqData.mIsRunning = false;                           // 9
  • 初始化记录当前录制文件packet索引为0
  • 表明audio queue正在运行
  • 开启一个audio queue
  • 指定开启的audio queue
  • 设置为NULL表示立即开始采集数据
  • 停止并重置当前音频队列
  • 指定停止的音频队列
  • true:同步停止, false: 异步停止
  • 更新音频队列当前工作状态.

2.15. 录制完成清理内存

录制完成后,回收音频队列数据,关闭音频文件.

AudioQueueDispose (                                 // 1
    aqData.mQueue,                                  // 2
    true                                            // 3
);
 
AudioFileClose (aqData.mAudioFile);                 // 4
  • 1.回收音频队列中所有资源
  • 2.指定回收的音频队列
  • 3.true: 同步, false:异步
  • 4.关闭录制文件.

3. 播放

使用 Audio Queue Services播放音频时,源数据可以是本地文件, 内存中的对象或者其他音频存储方式.本章中仅介绍通过本地文件播放.

  • 定义一个结构体管理音频格式状态信息等.
  • 实现一个播放回调函数
  • 设置音频队列数据大小
  • 打开一个音频文件,确定音频数据格式
  • 创建并配置一个播放的音频队列
  • 为音频队列数据分配内存并入队.告诉音频队列开始播放.完成时,告诉音频队列停止.
  • 回收内存,释放资源

3.1. 定义一个结构体管理音频状态

static const int kNumberBuffers = 3;                              // 1
struct AQPlayerState {
    AudioStreamBasicDescription   mDataFormat;                    // 2
    AudioQueueRef                 mQueue;                         // 3
    AudioQueueBufferRef           mBuffers[kNumberBuffers];       // 4
    AudioFileID                   mAudioFile;                     // 5
    UInt32                        bufferByteSize;                 // 6
    SInt64                        mCurrentPacket;                 // 7
    UInt32                        mNumPacketsToRead;              // 8
    AudioStreamPacketDescription  *mPacketDescs;                  // 9
    bool                          mIsRunning;                     // 10
};

此结构体中的数据基本与录制时相同.

  • 1.设置音频队列中可复用的音频数据个数,通常为3
  • 2.ASBD
  • 3.播放使用的音频队列
  • 4.管理音频队列中音频数据的数组
  • 5.播放用的音频文件
  • 6.每个音频数据的大小
  • 7.当前准备播放的音频数据包索引
  • 8.每次调用回调函数要读取的音频数据包的个数
  • 9.对于VBR音频数据,表示正在播放的音频数据包描述性数组,对于CBR音频数据可以设为NULL.
  • 10.音频队列是否正在运行.

3.2.回调函数

作用

  • 从音频文件中读取指定数量的音频数据并将其装入音频队列数据.
  • 将音频队列数据入队
  • 文件读取完成后,停止音频队列
3.2.1. 定义回调函数
static void HandleOutputBuffer (
    void                 *aqData,                 // 1
    AudioQueueRef        inAQ,                    // 2
    AudioQueueBufferRef  inBuffer                 // 3
)

  • 1.同录制,自定义的结构体或类对象,可传入回调函数中使用,即OC类与回调函数间的通信对象
  • 2.当前工作的音频队列
  • 3.通过读取音频文件获取的音频数据
3.2.2. 读取音频文件
AudioFileReadPackets (                        // 1
    pAqData->mAudioFile,                      // 2
    false,                                    // 3
    &numBytesReadFromFile,                    // 4
    pAqData->mPacketDescs,                    // 5
    pAqData->mCurrentPacket,                  // 6
    &numPackets,                              // 7
    inBuffer->mAudioData                      // 8
);
  • 1.读取文件的函数
  • 2.要读取的音频文件
  • 3.false:读取时不应缓存数据.
  • 4.作为输出:将从文件读取的字节数
  • 5.作为输出:VBR:从音频文件读取到的数据包描述数组,CBR:NULL
  • 6.当前读取到的索引值,以便下次继续读取
  • 7.作输入时:从音频文件中读取到的音频数据包数,作输出时:实际读取到的音频数据包
  • 8.作输出时:从音频文件中读取的数据
3.2.3. 入队

读取完音频数据后,执行入队操作.

AudioQueueEnqueueBuffer (                      // 1
    pAqData->mQueue,                           // 2
    inBuffer,                                  // 3
    (pAqData->mPacketDescs ? numPackets : 0),  // 4
    pAqData->mPacketDescs                      // 5
);

  • 4.音频数据包数,CBR的数据使用0
  • 5.对于压缩数据使用其数据包描述信息
3.2.4. 停止音频队列

如果检查到当前音频文件读取完毕,应该停止音频队列.

if (numPackets == 0) {                          // 1
    AudioQueueStop (                            // 2
        pAqData->mQueue,                        // 3
        false                                   // 4
    );
    pAqData->mIsRunning = false;                // 5
}
  • 1.通过AudioFileReadPackets检查数据包是否为0
  • 4.true:同步, false:异步
3.2.5. 完整的回调
static void HandleOutputBuffer (
    void                *aqData,
    AudioQueueRef       inAQ,
    AudioQueueBufferRef inBuffer
) {
    AQPlayerState *pAqData = (AQPlayerState *) aqData;        // 1
    if (pAqData->mIsRunning == 0) return;                     // 2
    UInt32 numBytesReadFromFile;                              // 3
    UInt32 numPackets = pAqData->mNumPacketsToRead;           // 4
    AudioFileReadPackets (
        pAqData->mAudioFile,
        false,
        &numBytesReadFromFile,
        pAqData->mPacketDescs, 
        pAqData->mCurrentPacket,
        &numPackets,
        inBuffer->mAudioData 
    );
    if (numPackets > 0) {                                     // 5
        inBuffer->mAudioDataByteSize = numBytesReadFromFile;  // 6
       AudioQueueEnqueueBuffer ( 
            pAqData->mQueue,
            inBuffer,
            (pAqData->mPacketDescs ? numPackets : 0),
            pAqData->mPacketDescs
        );
        pAqData->mCurrentPacket += numPackets;                // 7 
    } else {
        AudioQueueStop (
            pAqData->mQueue,
            false
        );
        pAqData->mIsRunning = false; 
    }
}
  • 3.记录读取到的字节数
  • 4.记录读取到音频数据包数
  • 7.累加音频数据包,使下次触发回调可以接着上次内容继续播放

3.3. 计算音频队列数据

我们需要指定一个音频队列buffer的大小.根据计算出来的大小为音频队列数据分配内存.

  • 回调函数中调用AudioFileReadPackets获取读取到的包数
  • 设置音频buffer下限值,避免访问过于频繁.
void DeriveBufferSize (
    AudioStreamBasicDescription &ASBDesc,                            // 1
    UInt32                      maxPacketSize,                       // 2
    Float64                     seconds,                             // 3
    UInt32                      *outBufferSize,                      // 4
    UInt32                      *outNumPacketsToRead                 // 5
) {
    static const int maxBufferSize = 0x50000;                        // 6
    static const int minBufferSize = 0x4000;                         // 7
 
    if (ASBDesc.mFramesPerPacket != 0) {                             // 8
        Float64 numPacketsForTime =
            ASBDesc.mSampleRate / ASBDesc.mFramesPerPacket * seconds;
        *outBufferSize = numPacketsForTime * maxPacketSize;
    } else {                                                         // 9
        *outBufferSize =
            maxBufferSize > maxPacketSize ?
                maxBufferSize : maxPacketSize;
    }
 
    if (                                                             // 10
        *outBufferSize > maxBufferSize &&
        *outBufferSize > maxPacketSize
    )
        *outBufferSize = maxBufferSize;
    else {                                                           // 11
        if (*outBufferSize < minBufferSize)
            *outBufferSize = minBufferSize;
    }
 
    *outNumPacketsToRead = *outBufferSize / maxPacketSize;           // 12
}
  • 2.估算当前播放音频文件最大数据包大小,通过调用AudioFileGetProperty查询kAudioFilePropertyPacketSizeUpperBound属性可得
  • 3.采样时间,根据采样率与采样时间可计算出音频数据大小
  • 4.每个音频数据的大小
  • 5.每次从音频播放回调中读取的音频数据包数
  • 6.音频数据包大小的上限
  • 7.音频数据包大小的下限
  • 8.计算音频数据包总大小
  • 9.根据最大数据包大小和您设置的上限导出合理的音频队列数据大小
  • 10.设置上限
  • 11.设置下限
  • 12.计算读取到的音频数据包数

3.4. 打开音频文件

  • 获取一个CFURL对象表示音频文件路径
  • 打开音频文件
  • 获取文件格式
3.4.1. 获取一个CFURL对象表示音频文件路径
CFURLRef audioFileURL =
    CFURLCreateFromFileSystemRepresentation (           // 1
        NULL,                                           // 2
        (const UInt8 *) filePath,                       // 3
        strlen (filePath),                              // 4
        false                                           // 5
    );
  • 1.创建一个CFURL类型的对象代表录制文件路径
  • 2.使用NULL(kCFAllocatorDefault)使用当前默认的内存分配器
  • 3.设置文件路径
  • 4.文件名长度
  • 5.false表示是一个文件,不是文件夹.
3.4.2. 打开音频文件
AQPlayerState aqData;                                   // 1
 
OSStatus result =
    AudioFileOpenURL (                                  // 2
        audioFileURL,                                   // 3
        fsRdPerm,                                       // 4
        0,                                              // 5
        &aqData.mAudioFile                              // 6
    );
 
CFRelease (audioFileURL);                               // 7
  • 2.打开一个想要播放的音频文件
  • 3.音频文件路径
  • 4.文件权限
  • 5.可选文件类型,0:不使用此参数
  • 6.作为输出,获取文件对象的引用
3.4.3. 获取文件格式
UInt32 dataFormatSize = sizeof (aqData.mDataFormat);    // 1
 
AudioFileGetProperty (                                  // 2
    aqData.mAudioFile,                                  // 3
    kAudioFilePropertyDataFormat,                       // 4
    &dataFormatSize,                                    // 5
    &aqData.mDataFormat                                 // 6
);
  • 5.作为输入:输入时,AudioStreamBasicDescription结构体的预期大小,用于描述音频文件的数据格式。在输出时,实际大小。作播放时不需要使用此值。
  • 6.输出:将文件代表的ASBD数据格式赋给该变量

3.5. 创建播放音频队列

AudioQueueNewOutput (                                // 1
    &aqData.mDataFormat,                             // 2
    HandleOutputBuffer,                              // 3
    &aqData,                                         // 4
    CFRunLoopGetCurrent (),                          // 5
    kCFRunLoopCommonModes,                           // 6
    0,                                               // 7
    &aqData.mQueue                                   // 8
);
  • 3.回调函数
  • 4.音频队列数据
  • 5.调用播放回调的的运行循环
  • 6.调用播放回调运行循环的模式

3.6. 设置播放音频队列大小

3.6.1. 设置buffer size与读取的音频数据包数量
UInt32 maxPacketSize;
UInt32 propertySize = sizeof (maxPacketSize);
AudioFileGetProperty (                               // 1
    aqData.mAudioFile,                               // 2
    kAudioFilePropertyPacketSizeUpperBound,          // 3
    &propertySize,                                   // 4
    &maxPacketSize                                   // 5
);
 
DeriveBufferSize (                                   // 6
    aqData.mDataFormat,                              // 7
    maxPacketSize,                                   // 8
    0.5,                                             // 9
    &aqData.bufferByteSize,                          // 10
    &aqData.mNumPacketsToRead                        // 11
);
3.6.2. 为数据包描述数组分配内存
bool isFormatVBR = (                                       // 1
    aqData.mDataFormat.mBytesPerPacket == 0 ||
    aqData.mDataFormat.mFramesPerPacket == 0
);
 
if (isFormatVBR) {                                         // 2
    aqData.mPacketDescs =
      (AudioStreamPacketDescription*) malloc (
        aqData.mNumPacketsToRead * sizeof (AudioStreamPacketDescription)
      );
} else {                                                   // 3
    aqData.mPacketDescs = NULL;
}
  • 1.判断音频文件数据是VBR还是CBR.对于VBR数据,每个数据包中的帧数(同理每个数据包中的字节数也是一样)是可变的,所以此属性为0.
  • 2.对于VBR数据,为数据包描述字典分配指定内存.
  • 3.对于CBR数据,不需要使用该参数,直接设为NULL

3.7. 设置magic cookie

对于压缩的音频数据格式(AAC...),我们在播放前必须为音频队列设置magic cookies,即元数据信息.

UInt32 cookieSize = sizeof (UInt32);                   // 1
bool couldNotGetProperty =                             // 2
    AudioFileGetPropertyInfo (                         // 3
        aqData.mAudioFile,                             // 4
        kAudioFilePropertyMagicCookieData,             // 5
        &cookieSize,                                   // 6
        NULL                                           // 7
    );
 
if (!couldNotGetProperty && cookieSize) {              // 8
    char* magicCookie =
        (char *) malloc (cookieSize);
 
    AudioFileGetProperty (                             // 9
        aqData.mAudioFile,                             // 10
        kAudioFilePropertyMagicCookieData,             // 11
        &cookieSize,                                   // 12
        magicCookie                                    // 13
    );
 
    AudioQueueSetProperty (                            // 14
        aqData.mQueue,                                 // 15
        kAudioQueueProperty_MagicCookie,               // 16
        magicCookie,                                   // 17
        cookieSize                                     // 18
    );
 
    free (magicCookie);                                // 19
}
  • 1.根据UInt32估算magic cookie数据大小
  • 2.记录是否能获取magic cookie结果
  • 3.获取文件中的magic cookie的大小。
  • 4.想要播放的文件
  • 5.key值,代表音频文件的kAudioFilePropertyMagicCookieData
  • 6.作输入时表示magic cookie估算大小,输出时表示实际大小
  • 7.设置为NULL表示不关心此属性的读写权限
  • 8.如果文件包含magic cookie,分配内存去持有它
  • 9.获取文件中的magic cookie
  • 12.输入时表示文件中的magic cookie的大小
  • 13.输出为文件的magic cookie
  • 14.设置audio queue的函数

3.8. 分配音频队列数据

aqData.mCurrentPacket = 0;                                // 1
 
for (int i = 0; i < kNumberBuffers; ++i) {                // 2
    AudioQueueAllocateBuffer (                            // 3
        aqData.mQueue,                                    // 4
        aqData.bufferByteSize,                            // 5
        &aqData.mBuffers[i]                               // 6
    );
 
    HandleOutputBuffer (                                  // 7
        &aqData,                                          // 8
        aqData.mQueue,                                    // 9
        aqData.mBuffers[i]                                // 10
    );
}
  • 1.初始化读取音频数据包索引为0
  • 7.自定义的播放音频回调函

3.9. 设置音量

开始播放前,可以设置音量(0~1)

Float32 gain = 1.0;                                       // 1
    // Optionally, allow user to override gain setting here
AudioQueueSetParameter (                                  // 2
    aqData.mQueue,                                        // 3
    kAudioQueueParam_Volume,                              // 4
    gain                                                  // 5
);

3.10. 启动Audio Queue

aqData.mIsRunning = true;                          // 1
 
AudioQueueStart (                                  // 2
    aqData.mQueue,                                 // 3
    NULL                                           // 4
);
 
do {                                               // 5
    CFRunLoopRunInMode (                           // 6
        kCFRunLoopDefaultMode,                     // 7
        0.25,                                      // 8
        false                                      // 9
    );
} while (aqData.mIsRunning);
 
CFRunLoopRunInMode (                               // 10
    kCFRunLoopDefaultMode,
    1,
    false
);

  • 4.设置为NULL表示马上开始播放
  • 8.设置运行循环的时间是0.25秒
  • 9.使用false表示运行循环应该在指定的完整时间内继续
  • 10.音频队列停止后,运行循环运行一段时间以确保当前播放的音频队列缓冲区有时间完成。

3.11. 清理

播放完成后应该回收音频队列,关闭音频文件,释放所有相关资源

AudioQueueDispose (                            // 1
    aqData.mQueue,                             // 2
    true                                       // 3
);
 
AudioFileClose (aqData.mAudioFile);            // 4
 
free (aqData.mPacketDescs);                    // 5
  • 3:true: 同步, false:异步

Apple官方文档