0%

Audio Queue Services Programming Guide:关于音频队列

本章将学习到音频队列的功能、架构和内部工作原理。本文介绍音频队列用来播放或录制所用的音频队列(audio queues)、音频队列缓冲区(audio queue buffers)和回调函数,你还可以找到关于音频队列状态和参数的信息,截至到本章的结尾,你将会获得有效使用该技术的概念性理解。

什么是音频队列?

在iOS和Mac OS X中,音频队列是一个用来录制和播放音频的软件对象,使用AudioQueueRef不透明数据类型来表示(在AudioQueue.h头文件中声明)。

音频队列完成以下工作:

  • 连接音频硬件
  • 内存管理
  • 根据需要为已压缩的音频格式引入编码器
  • 媒体的录制或播放

你可以将音频队列配合其他Core Audio的接口使用,再加上相对少量的自定义代码就可以在程序中创建一套完整的数字音频录制或播放解决方案。

音频队列架构

所有的音频队列都含有相同的基础结构,包含以下几部分:

  • 一组音频队列缓冲区(audio queue buffers),每个音频队列缓冲区都是一个存储音频数据的临时仓库。
  • 一个缓冲区队列(buffer queue),一个包含音频队列缓冲区的有序列表。
  • 一个你自己编写的音频队列回调函数(audio queue callback)

架构很大程度上依赖于这个音频队列是用来录制还是用来播放的。不同之处在于音频队列如何连接到它的输入和输入,还有它的回调函数所扮演的角色。

用来录制的音频队列

用于录制的的音频队列,使用AudioQueueNewInput函数创建,如图1-1的结构。

图1-1 用于录制的的音频队列

Architecture for a recording audio queue

用于录制的音频队列的输入端一般连接到外部的音频硬件上,比如说麦克风。在iOS中,音频来自于由用户连接的设备:内置的麦克风或者耳机麦克风,如在Mac OS X下,音频来自于由用户在系统首选项中设置的系统默认音频输入设备。

用于录制的音频队列的输入端利用了你自己写的回调函数,当录制音频到磁盘上的时候,回调函数将存有从音频队列中接收到的新的音频数据的缓冲区写入到音频文件中。然而,用于录制的音频队列也可以用其他方法来使用。你也可以使用其中一种,比如说,在一个实时的分析仪中,在这种情况下,你的回调函数会直接向程序提供音频数据,而不是将它写入磁盘。

更多关于该回调的知识,参阅The Recording Audio Queue Callback Function

每一个音频队列,无论是用于录制还是用于播放,都有一个或多个音频队列缓冲区。这些缓冲区排列在一个特殊的被称为缓冲区队列(buffer queue)的序列中。如图所示,音频队列缓冲区是按照他们被填充的顺序编号的——这也是和把他们交付给回调函数的顺序是相同的。有关音频队列是如何使用缓冲区,参阅The Buffer Queue and Enqueuing

用于播放的音频队列

用于播放的音频队列,使用AudioQueueNewOutput函数创建,如图1-2结构。

图1-2 用于播放的音频队列

Architecture for a playback audio queue

在用于播放的音频队列中,回调函数是在输入端的,这个回调函数的职责就是从磁盘(或其他来源)中获取音频数据,然后将它交付给音频队列。当没有更多音频数据需要播放的时候告诉音频队列停止。更多关于这个回调函数的知识,参阅The Playback Audio Queue Callback Function

用于播放的音频队列的输出端一般都是连接到外部的音频设备的,比如说扬声器。在iOS中,音频通过用户选择的设备播放,如接收者是耳机。在Mac OS X中,默认情况下,音频会通过用户在系统首选项中设置的默认音频输出设备中输出。

音频队列缓冲区

音频队列缓冲区(audio queue buffer)是一个AudioQueueBuffer类型的数据结构(在AudioQueue.h头文件中声明):

1
2
3
4
5
6
7
typedef struct AudioQueueBuffer {
const UInt32 mAudioDataBytesCapacity;
void *const mAudioData;
UInt32 mAudioDataByteSize;
void *mUserData;
} AudioQueueBuffer;
typedef AudioQueueBuffer *AudioQueueBufferRef;

上述代码中的mAudioData字段,指向了缓冲区本身:一个用来当作暂时存放录制或播放音频数据的容器的内存,其他字段中的数据用来辅助音频队列管理这个缓冲区。

音频队列可以使用任意数量的缓冲区。一般情况下设置为3,这样就可以让一个缓冲区忙于将数据写入磁盘,同时另一个缓冲区在填充新的音频数据,第三个缓冲区在需要做磁盘I/O延迟补偿的时候使用。图1-3展示了这个过程。

音频队列负责对它的缓冲区进行内存管理:

  • 当调用AudioQueueAllocateBuffer函数的时,音频队列创建一个缓冲区。
  • 当通过调用AudioQueueDispose函数释放一个音频队列的时,这个音频队列释放掉它拥有的缓冲区。

这提高了添加到程序中录制和播放功能的健壮性。同时它也帮助你优化资源的使用。

关于AudioQueueBuffer数据结构的完整描述,参阅_Audio Queue Services Reference。_

缓冲区队列和入队

传递给音频队列的缓冲区队列,顾名思义就是音频队列服务(Audio Queue Services),在Audio Queue Architecture中,将提及缓冲区队列,一个缓冲区的有序列表,其中描述了音频队列对象如何配合回调函数在录制或播放的过程中管理缓冲区队列。尤其是入队音频队列,即缓冲区队列对音频队列缓冲区的附加操作。无论是在实现录制或者播放,入队都是你在回调函数中需要执行的任务。

录制过程

当进行录制时,一个音频队列缓冲区填充了从输入设备(如麦克风)中获取的音频数据。缓冲区队列中的其他缓冲区将在当前缓冲区的末尾依次排队等待填充音频数据。

音频队列将按照缓冲区填充的顺序把已填充过音频数据的缓冲区交付给你的回调函数。图1-3展示了当使用音频队列录制时的过程。

图1-3 录制过程

Illustration of the recording process when using an audio queue
  1. 录制开始,音频队列用获取的数据填充缓冲区。
  2. 第一个缓冲区填充完毕,音频队列调用回调函数来处理这个被填充满的缓冲区(缓冲区一)。
  3. 回调函数将缓冲区的内容写到音频文件中。同时,音频队列将另一个缓冲区(缓冲区二)填充新获取的数据。
  4. 回调函数将刚刚写入磁盘的缓冲区(缓冲区一)入队,使它重新重新回到被填充的队列。
  5. 音频队列再一次调用回调函数,处理下一个填充完毕的缓冲区(缓冲区二)。
  6. 回调函数将这个缓冲区的内容写入到音频文件。

这种稳定状态会一直持续到用户停止录制。

播放过程

当进行播放的时候,音频队列缓冲区将被传送到输出设备(如扬声器)。缓冲区队列中其他的缓冲区讲按顺序排在当前缓冲区末尾等待播放。

音频队列将已经播放过的音频数据按照他们播放的顺序交付给你的回调函数,回调函数将新的音频数据读取到一个缓冲区中,然后将它入队。图1-4展示了当使用音频队列播放时的过程。

图1-4 播放过程

Illustration of the playback process when using an audio queue
  1. 程序启动用于播放的音频队列,程序对每一个音频队列缓冲区调用回调函数,填充这些缓冲区并且将它们加入缓冲区队列。
  2. 启动操作会确保当程序调用AudioQueueStart函数之后,播放可以立即执行。
  3. 音频队列将第一个缓冲区(缓冲区一)交付给输出设备。当第一个缓冲区被播放完毕之后,用于播放的音频队列就进入了一个稳定的循环状态。
  4. 音频队列开始播放下一个缓冲区(缓冲区二)。
  5. 调用回调函数,处理刚刚播放完的那个缓冲区(缓冲区一)。
  6. 这个回调函数从音频文件中读取数据填充缓冲区然后入队播放。

控制播放过程

音频队列缓冲区总是按照他们入队的顺序进行播放,然而,在播放过程中,音频队列服务提供了AudioQueueEnqueueBufferWithParameters函数来进行一些控制,这个函数有以下功能:

  • 设置缓冲区的精确播放时间,这可以实现音频同步。
  • 截断音频队列缓冲区开头或结尾的帧,这可以让你去除开头或结尾的静音。
  • 在缓冲区的粒度上设置播放增益。

关于更多播放增益的信息,参阅Audio Queue Parameters,如果要了解对AudioQueueEnqueueBufferWithParameters函数的完整描述,参阅_Audio Queue Services Reference_。

音频队列回调函数

一般来说,使用音频队列服务的大部分编程任务都在编程音频队列回调函数上。

在录制或播放过程中,音频队列将反复调用它所拥有的音频队列回调函数。调用的时间间隔取决于音频队列缓冲区的容量,一般来一说这个时间在半秒到几秒。

无论对于录制或者播放,音频队列回调的一个职责就是返回一个缓冲区队列的音频队列缓冲区。回调函数使用AudioQueueEnqueueBuffer函数将一个缓冲区加入到缓冲区队列的末尾。对于播放来说,你也可以使用AudioQueueEnqueueBufferWithParameters函数来获得更多的控制。

用于录制的音频队列的回调函数

本节介绍了一般情况下(将音频录制到磁盘上)的回调函数。以下是用于录制的回调函数的原型(在AudioQueue.h头文件中声明):

1
2
3
4
5
6
7
8
AudioQueueInputCallback (
void *inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp *inStartTime,
UInt32 inNumberPacketDescriptions,
const AudioStreamPacketDescription *inPacketDescs
);

用于录制的音频队列,在调用回调函数的时候,提供了把下一组音频数据写入到文件的一切信息:

  • inUserData:通常是一个用来保存音频队列和它的缓冲区状态信息的自定义结构,或者一个音频文件对象 (AudioFileID类型)表示正在写入的文件,或者该文件的音频格式信息。
  • inAQ:是调用回调函数的音频队列。
  • inBuffer:是一个被音频队列填充新的音频数据的音频队列缓冲区,它包含了回调函数写入文件所需要的新数据。数据已经根据你在自己指定的自定义结构(由inUserData参数传入)中指定的格式格式化。更多信息,可参阅Using Codecs and Audio Data Formats
  • inStartTime:是缓冲区中的首个采样的参考时间,对于基本的录制,你的回调函数不会使用这个参数。
  • inNumberPacketDescriptions:是inPacketDescs参数中包描述符(packet descriptions)的数量,如果你正在录制一个VBR(可变比特率(variable bitrate))格式,音频队列将回调该参数给你,这个参数可以让你传递给AudioFileWritePackets函数。CBR(常量比特率(constant bitrate)) 格式不使用包描述。对于CBR录制,音频队列会设置这个参数并且将inPacketDescs这个参数设置为NULL
  • inPacketDescs:是一组对应于缓冲区中采样的包描述符,音频队列提供了这个参数的值,如果音频文件是VBR格式的,回调函数可以将这个值传递给AudioFileWritePackets函数(在AudioFile.h头文件中声明)。

如果要了解更多关于用于录制的回调函数的信息,参阅Recording AudioAudio Queue Services Reference

用于播放的音频队列的回调函数

本节介绍了一般情况下(从磁盘文件播放音频的回调函数。 下面是用于播放的回调函数的原型(在AudioQueue.h头文件中声明):

1
2
3
4
5
AudioQueueOutputCallback (
void *inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer
);

用于播放的音频队列,在调用回调函数的时候,提供了从文件读取下一组音频数据所需的信息:

  • inUserData:一般来说是一个你创建的包含音频队列和它的缓冲区的的状态信息的自定义结构;或者一个音频文件对象 (AudioFileID类型)表示要写入的文件;或者文件的音频数据格式信息。 在播放音频队列的情况下,回调函数会在这个结构体中用一个字段保持对当前包的索引。

  • inAQ:调用这个回调函数的音频队列。

  • inBuffer:一个音频队列缓冲区,由音频队列提供,回调将填充从正在播放的文件中读取的下一组数据。

如果程序在播放VBR数据,回调函数需要得到正在播放的音频数据的包数据,它通过调用AudioFileReadPackets函数来实现,这个函数声明于AudioFile.h头文件,回调函数随后把包信息放到自定义的数据结构中,以供音频队列使用。

关于播放回调的更多信息,参阅Playing AudioAudio Queue Services Reference

使用编码和音频数据格式

音频队列服务根据采用的编解码器在音频格式之间进行转换。录制或播放程序可以使用任意已经安装过相应编码器的格式,不需要写自定义的代码来处理各种音频格式。尤其是你的回调函数不需要知道其数据格式。

每个音频队列在AudioStreamBasicDescription结构体中都有一个字段表示音频数据格式。当你在mFormatID字段中指定格式时,音频队列会使用相应的解码器。然后指定采样率和声道数,这些就是所有你需要做的。设置音频数据格式的示例,参阅Recording AudioPlaying Audio

用于录制的音频队列按照图1-5中的流程使用已安装的编码器。

图1-5 在录制音频的时候进行音频格式转换

Using a code when recording with an audio queue
  1. 程序告诉音频队列开始录制,同时也告诉它所要使用的音频格式。
  2. 音频队列获取新的音频数据,并且根据你指定的格式使用相应的编码器转换音频数据。然后音频队列调用回调函数,将适当的格式化过的音频数据放进缓冲区中。
  3. 回调函数将格式化后的音频数据写入磁盘。回调函数不需要知道数据格式。

用于播放的音频队列按照图1-6的流程使用已安装的编码器。

图1-6 在播放过程中进行音频格式转换

Using a codec when playing a file with an audio queue
  1. 程序告诉音频队列开始播放,同时也告诉了它将要播放放的音频文件的数据格式。
  2. 音频队列调用回调函数来从音频文件中读取音频数据。回调函数按照它的原始格式将音频数据交付给音频队列。
  3. 音频队列使用对应的解码器将音频交付给目标输出设备。

音频队列可以使用任意已安装的编码器,无论是Mac OS X原生的还是第三方的。你可以通过指定音频队列的AudioStreamBasicDescription结构体中四字节编码ID来指定将要使用的编码器。该字段的使用示例,参阅Recording Audio

Mac OS X包含大量的编码器,在CoreAudioTypes.h头文件中的format IDs枚举值中列出,并且记录在_Core Audio Data Types Reference_中。你可以使用Audio Toolbox框架中AudioFormat.h头文件中的接口来查询当前系统可用的编码器。你可以使用Fiendishthngs程序来显示系统的编码器,该示例代码可以从http://developer.apple.com/samplecode/Fiendishthngs/获得。

音频队列控制和状态

音频队列的生命周期从创建到废弃。程序管理器生命周期,且控制音频队列的状态,通过使用AudioQueue.h头文件中的六个函数:

  • StartAudioQueueStart):初始化录制或者播放。
  • PrimeAudioQueuePrime):对于播放, 在调用AudioQueueStart之前调用这个函数,以确保有数据可立即用于音频队列的播放。这个函数不在录制中使用。
  • StopAudioQueueStop):调用这个函数来重置音频队列 (参考下面对AudioQueueReset的描述),然后停止录制或播放。当没有更多的数据要播放时,播放音频队列回调调用该函数。
  • PauseAudioQueuePause):调用这个函数可以在不影响缓冲区和不重置音频队列的情况下停止录制或播放。如果需要恢复,调用AudioQueueStart函数。
  • FlushAudioQueueFlush):在对最后一个音频队列缓冲区进行排队后调用,以确保所有缓冲的数据以及所有正在处理的音频数据被记录或播放。
  • ResetAudioQueueReset):调用这个函数可以立即让音频队列静音。移除之前调度过的缓冲区,并且重置所有解码器和DSP状态。

你可以在同步或异步模式下使用AudioQueueStop函数:

  • 同步:立刻停止,不考虑之前缓冲的音频数据。
  • 异步:在所有已入队的缓冲区播放或录制完毕之后再停止。

所有这些函数的完整描述和同步异步停止音频队列的更多信息,参阅_Audio Queue Services Reference_。

音频队列参数

音频队列通过参数(parameters)调整配置。每个参数都使用枚举值作为键,浮点数作为值。参数一般于播放,不用于录制。

在Mac OS X v10.5中,只有播放增益参数。可以通过使用kAudioQueueParam_Volume常量来获取或设置它的值,它的有效范围在0.0(静音)到1.0(单位增益)。

程序可以通过以下两种方法来设置音频队列参数:

  • 对于每一个音频队列,使用AudioQueueSetParameter函数,这可以让你直接改变音频队列的设置,这个改变是立刻生效的。
  • 对于每一个音频队列缓冲区,调用AudioQueueEnqueueBufferWithParameters函数。这可以让你在将音频队列缓冲区入队的时候设置音频队列设置。这种修改只会在播放音频队列缓冲区的时候生效。

这两种情况下,音频队列的参数设置会一直保留到你改变它们为止。

可以通过调用AudioQueueGetParameter函数来获取音频队列当前的参数。该函数的完整描述和获取和设置参数值的方法,参阅_Audio Queue Services Reference_。

总结

  • 音频队列工作:
    • 连接音频硬件
    • 内存管理
    • 根据需要为已压缩的音频格式引入编码器
    • 媒体的录制或播放
  • 使用音频队列的基本组成:
    • 一组音频队列缓冲区,每个缓冲区临时存储音频数据。
    • 缓冲区队列。
    • 音频队列回调函数。
  • 音频队列按用途分类:
    • 录制
      • 输入端:音频输入硬件。
      • 输出/回调:音频数据
    • 播放
      • 输入/回调:获取音频数据并交付给音频队列。且当没有更多音频数据要播放时停止音频队列。
      • 输出:音频输出设备。
  • 音频队列缓冲区数量一般设置为3,对应录制:一个用于写入磁盘,一个填充新音频数据,一个在需要磁盘I/O延迟补偿时使用。
  • 音频队列管理了音频队列缓冲区的生命周期/内存:AudioQueueAllocateBuffer创建,AudioQueueDispose释放音频队列时也一起释放其缓冲区。
  • 播放过程通过AudioQueueEnqueueBufferWithParameters来实现播放控制:
    • 设置缓冲区的精确播放时间,这可以实现音频同步。
    • 截断音频队列缓冲区开头或结尾的帧,这可以让你去除开头或结尾的静音。
    • 在缓冲区的粒度上设置播放增益。
  • 音频队列服务的编码大部分都在其回调函数上。录制使用AudioQueueInputCallback函数原型,播放使用AudioQueueOutputCallback函数原型。
  • 回调函数调用的间隔取决于缓冲区的容量。
  • 音频队列回调的任务是返回队列缓冲区。使用AudioQueueEnqueueBuffer入队缓冲区。
  • 音频队列在录制和播放过程中都可以进行格式转换。回调函数不需要知道音频格式,因为音频编码器都是提前给音频队列配置的。
  • 音频队列的状态控制:
    • StartAudioQueueStart):初始化录制或者播放。
    • PrimeAudioQueuePrime):仅用于播放, 在调用AudioQueueStart之前调用这个函数,以确保有数据可立即用于音频队列的播放。
    • StopAudioQueueStop):调用这个函数来重置音频队列 (参考下面对AudioQueueReset的描述),然后停止录制或播放。当没有更多的数据要播放时,回调中调用该函数。
    • PauseAudioQueuePause):调用这个函数可以在不影响缓冲区和不重置音频队列的情况下停止录制或播放。如果需要恢复,调用AudioQueueStart函数。
    • FlushAudioQueueFlush):在对最后一个音频队列缓冲区进行排队后调用,以确保所有缓冲的数据以及所有正在处理的音频数据被记录或播放。
    • ResetAudioQueueReset):调用这个函数可以立即让音频队列静音。移除之前调度过的缓冲区,并且重置所有解码器和DSP状态。

欢迎关注我的其它发布渠道