当你使用音频队列服务播放音频时,源几乎可以是任意的——磁盘文件、基于软件音频合成器、内存中的对象等。本章介绍最常见的情况:播放磁盘上的文件。
注意:本章介绍了基于ANSI-C的播放实现,并使用了Mac OS X Core Audio SDK的C++类。有关Objective-C的示例,参阅iOS Dev Center中的_SpeakHere_示例代码。
要把播放功能添加到程序中,通常需要执行以下步骤:
- 定义一个自定义结构体来管理状态、格式和路径信息。
- 编写音频队列回调函数来执行实际的播放。
- 编写代码以确定音频队列缓冲区的合适大小。
- 打开音频文件进行播放,然后确定其音频数据格式。
- 创建一个播放音频队列并进行相关配置。
- 分配和排队音频队列缓冲区。告诉音频队列开始播放。完成后,播放回调函数告诉音频队列停止。
- 处理音频队列,释放资源。
本章的剩余部分详细介绍了每个步骤。
定义结构体管理状态
首先,定义一个结构体,将用它来管理音频格式和音频队列状态信息,如清单3-1所示:
清单3-1 播放音频队列的自定义结构体
1 | static const int kNumberBuffers = 3; // 1 |
结构体中大多数字段与用于录制的自定义结构体几乎相同,如Define a Custom Structure to Manage State所述。例如,mDataFormat
字段保存正在播放的文件格式。录制时,类似的字段保存了写入磁盘的文件格式。
以下是该结构体各字段介绍:
- 设置要使用的音频队列缓冲区数量。如Audio Queue Buffers所述,3个通常是不错的选择。
AudioStreamBasicDescription
结构体(来自CoreAudioTypes.h
)表示正在播放的文件的音频数据格式。该格式由mQueue
字段指定的音频队列使用。mDataFormat
字段通过查询音频文件的kAudioFilePropertyDataFormat
属性来填充该字段,如Obtaining a File’s Audio Data Format所述。 有关AudioStreamBasicDescription
结构体的详细信息,参阅_Core Audio Data Types Reference_。- 程序创建的播放音频队列。
- 一个数组,包含指向音频队列管理的音频队列缓冲区的指针。
- 代表程序播放的音频文件的对象。
- 每个音频队列缓冲区的大小(以字节为单位)。该值在音频队列创建之后和开始之前,由
DeriveBufferSize
函数计算。参阅Write a Function to Derive Playback Audio Queue Buffer Size。 - 音频文件中下一个要播放的数据包索引。
- 每次调用音频队列的播放回调函数时,要读取的数据包数量。就像
bufferByteSize
字段一样,在音频队列创建之后和开始之前,由DeriveBufferSize
函数计算该值。 - 对于VBR音频数据,该字段是正在播放的文件的数据包描述数组。对于CBR数据,该字段为
NULL
。 - 一个布尔值,表示音频队列是否正在运行。
编写播放音频队列回调函数
下面,编写一个播放音频队列回调函数。该回调函数执行三项主要任务:
- 从音频文件中读取指定数量的数据,并将其放入音频队列缓冲区中。
- 把音频队列缓冲区排队到缓冲区队列中。
- 当没有更多数据要从音频文件中读取时,告诉音频队列停止。
本节展示来一个回调声明示例,分别描述各个任务,最后给出完整的播放回调函数。有关播放回调函数的作用,参阅图1-4。
播放音频队列回调声明
清单3-2展示了一个播放音频回调函数的示例声明,AudioQueueOutputCallback
在AudioQueue.h
声明为:
清单3-2 播放音频队列回调声明
1 | static void HandleOutputBuffer ( |
下面是该代码的工作方式:
- 通常,
aqData
是包含定义音频队列状态信息的自定义结构体。如Define a Custom Structure to Manage State所述。 - 持有该回调函数的音频队列。
- 音频队列缓冲区,回调函数通过从音频文件中读取,来填充数据。
从文件读取到音频队列缓冲区
播放音频队列回调函数的第一个操作是从音频文件中读取数据并将其放在音频队列缓冲区中,如清单3-3所示。
清单3-3 从音频文件读取到音频队列缓冲区
1 | AudioFileReadPackets ( // 1 |
下面是该代码的工作方式:
AudioFileReadPackets
函数(在AudioFile.h
中声明),从音频文件读取数据并将其放入缓冲区中。- 要读取的音频文件。
- 用
false
表示该函数在读取时不应缓存数据。 - 输出时,是从音频文件读取的音频数据字节数。
- 输出时,是从音频文件中读取的数据包描述数组。对于CBR数据,该参数输入
NULL
。 - 从音频文件中读取第一个数据包的索引。
- 输入时,是要从音频文件读取的数据包数量。输出时,是实际读取的包数量。
- 在输出时,填充的音频队列缓冲区包含从音频文件读取的数据。
排队音频队列缓冲区
现在已经从音频文件中读取数据并将其放在音频队列缓冲区中,回调函数让缓冲区入队,如清单3-4所示。进入缓冲区队列后,缓冲区的音频数据可用于音频队列发送到输出设备。
清单3-4 从磁盘中读取后排队音频队列缓冲区
1 | AudioQueueEnqueueBuffer ( // 1 |
下面是该代码的工作方式:
AudioQueueEnqueueBuffer
函数把音频队列缓冲区添加到缓冲区队列。- 持有缓冲区队列的音频队列。
- 要排队的音频队列缓冲区。
- 音频队列缓冲区数据中的数据包数量。对于不使用数据包描述的CBR数据,设为
0
。 - 对于使用数据描述的压缩音频数据格式,数据包描述在缓冲区中。
停止音频队列
回调函数最后一个操作是检查是否有更多的数据,要从正在播放的音频文件中读取。在发现文件结尾后,回调函数告诉音频队列停止,如清单3-5所示。
清单3-5 Stopping an audio queue
1 | if (numPackets == 0) { // 1 |
下面是该代码的工作方式:
- 检查
AudioFileReadPackets
函数(由之前的回调函数调用)读取的数据包数量是否为0
。 AudioQueueStop
函数停止音频队列。- 要停止的音频队列。
- 播放所有排队的缓冲区后,异步停止音频队列。参阅Audio Queue Control and State。
- 设置结构体标志,表示播放已完成。
完整播放音频队列回调函数
清单3-6展示了完整播放音频队列回调的基本代码。和本文档的其他示例代码一样,该清单代码不包含错误处理。
清单3-6 一个播放音频队列回调函数
1 | static void HandleOutputBuffer ( |
下面是该代码的工作方式:
- 实例化后提供给音频队列的自定义结构体,包含要播放的音频文件对象(类型为
AudioFileID
),以及各种状态数据。参阅Define a Custom Structure to Manage State。 - 如果音频队列已停止,则立即返回。
- 一个变量,用于保存从正在播放的文件中读取的音频数据字节数。
- 使用要从正播放的文件中读取的数据包来初始化
numPackets
变量。 - 测试是否从文件中检索了一些音频数据。如果是,则让新填充的缓冲区入队;否则停止音频队列。
- 告诉音频队列缓冲区结构体已读取数据的字节数。
- 根据读取的数据包数量增加数据包索引。
编写函数计算播放音频队列缓冲区大小
音频队列服务希望你的程序为使用的音频队列缓冲区指定大小,如清单3-7所示。它得出的缓冲区大小足以容纳给定的音频时长。
创建播放音频队列后,你将在程序中调用DeriveBufferSize
函数,作为后续音频队列分配缓冲区的先决条件。参阅Write a Function to Derive Recording Audio Queue Buffer Size。为了播放,还需要:
- 在每次回调函数调用
AudioFileReadPackets
函数,得出要读取的数据包数量。 - 设置缓冲区大小的下限,以避免过多的磁盘访问。
这里的计算考虑了从磁盘读取的音频数据格式。该格式包括了可能影响缓冲区大小的所有因素,例如音频通道数量。
清单3-7 得出播放音频队列缓冲区大小
1 | void DeriveBufferSize ( |
下面是该代码的工作方式:
- 音频队列的
AudioStreamBasicDescription
结构体。 - 正在播放的音频文件中最大数据包的预估大小。你可以通过
kAudioFilePropertyPacketSizeUpperBound
属性ID,使用AudioFileGetProperty
函数(在AudioFile.h
中声明)得出该值。参阅Set Sizes for a Playback Audio Queue。 - 为每个音频缓冲区指定大小(以秒为单位)。
- 在输出时,是每个音频队列缓冲区的大小(以字节为单位)。
- 在输出时,是在每次播放音频队列回调时,从文件读取的音频数据包的数量。
- 音频队列缓冲区大小的上限(以字节为单位)。在该示例中,上限设为320 KB。这相等于以96 kHz采样率,大约持续5秒的24位立体声音频。
- 音频队列缓冲区大小的下限(以字节为单位)。在该示例中,下限设为16 KB。
- 对于定义每个数据包固定帧数的音频数据格式,需要得出音频队列缓冲区大小。
- 对于没定义每个数据包固定帧数的音频格式,需要根据最大数据包大小和设置的上限得出合理的音频队列缓冲区大小。
- 如果得出的缓冲区大小大于设置的上限,则考虑预估的最大数据包大小,并将其调整为边界值。
- 如果得出的缓冲区大小低于设置的下限,则将其调整为下限。
- 计算每次调用回调时从音频文件读取的数据包数量。
打开音频文件进行播放
现在,使用以下步骤打开音频文件进行播放:
- 获取一个表示要播放的音频文件的CFURL对象。
- 打开文件。
- 获取文件的音频数据格式。
获取音频文件的CFURL对象
清单3-8展示了如何为要播放的音频文件获取CFURL对象。在下一步中使用CFURL对象,打开文件。
清单3-8 获取音频文件的CFURL对象
1 | CFURLRef audioFileURL = |
下面是该代码的工作方式:
CFURLCreateFromFileSystemRepresentation
函数(在CFURL.h
中声明),创建一个CFURL对象,该对象表示要播放的文件。- 用
NULL
或kCFAllocatorDefault
,表示使用当前默认的内存分配器。 - 想要转换为CFURL的文件系统路径。在生产代码中,通常会从用户获取
filePath
值。 - 文件系统路径中的字节数。
false
值表示filePath
代表文件,而不是目录。
打开音频文件
清单3-9展示了如何打开音频文件进行播放。
清单3-9 打开音频文件进行播放
1 | AQPlayerState aqData; // 1 |
下面是该代码的工作方式:
- 创建
AQPlayerState
自定义结构体实例(参阅Define a Custom Structure to Manage State)。打开音频文件进行播放时,可以使用该实例存放音频文件对象(类型为AudioFileID
)。 AudioFileOpenURL
函数(在AudioFile.h
中声明),打开要播放的文件。- 要播放文件的引用。
- 与正在播放文件一起使用的文件权限。可用权限在文件管理器的
File Access Permission Constants
枚举中定义。在该示例中,请求读取文件的权限。 - 可选文件类型hint。这里的
0
表示该示例未使用该功能。 - 在输出时,对音频文件的引用将放在自定义结构体的
mAudioFile
字段。 - 释放在第一步创建的CFURL对象。
获取文件的音频数据格式
清单3-10展示了如何获取文件的音频数据格式。
清单3-10 获取文件的音频数据格式
1 | UInt32 dataFormatSize = sizeof (aqData.mDataFormat); // 1 |
下面是该代码的工作方式:
- 获取在查询音频文件有关音频数据格式时要使用的预期属性值大小。
AudioFileGetProperty
函数(在AudioFile.h
中声明),获取音频文件中指定属性的值。- 音频文件对象(类型为
AudioFileID
),表示要获取其音频数据格式的文件。 - 用户获取音频文件的数据格式的属性ID。
- 输入时,是描述音频文件的数据格式的
AudioStreamBasicDescription
结构体的预期大小。输出时,是其实际大小。播放程序不需要使用该值。 - 在输出时,从音频文件获得
AudioStreamBasicDescription
结构体的完整音频数据格式。该行通过把文件的音频数据格式存储在音频队列的自定义结构体中,将其应用于音频队列。
创建播放音频队列
清单3-11展示了如何创建播放音频队列。注意,AudioQueueNewOutput
函数使用了在之前步骤中配置的自定义结构体和回调函数,以及要播放文件的音频数据格式。
清单3-11 创建播放音频队列
1 | AudioQueueNewOutput ( // 1 |
下面是该代码的工作方式:
AudioQueueNewOutput
函数创建一个新的播放音频队列。- 设置要播放音频队列的音频数据格式。参阅Obtaining a File’s Audio Data Format。
- 和播放音频队列一起使用的回调函数。参阅Write a Playback Audio Queue Callback。
- 播放音频队列的自定义数据结构体。参阅Define a Custom Structure to Manage State。
- 当前的run loop,将在其调用音频队列回调函数。
- run loop模式。通常设为
kCFRunLoopCommonModes
。 - 保留参数,必需为
0
。 - 在输出时,新分配的播放音频队列。
设置播放音频队列大小
接下来,设置播放音频队列的一些大小值。在为音频队列分配缓冲区时,以及开始读取音频文件之前,请使用这些大小值。
本节中的代码清单展示了如何设置:
- 音频队列缓冲区大小。
- 每次调用播放音频队列回调函数时要读取的数据包数量。
- 数组大小,用于保存一个缓冲区的音频数据的数据包描述。
设置缓冲区大小和要读取的数据包数量
清单3-12展示了如何使用之前编写的DeriveBufferSize
函数(参阅Write a Function to Derive Playback Audio Queue Buffer Size)。这里的目的是为每个音频队列缓冲区设置一个大小(以字节为单位),并确定每次调用播放音频队列回调函数时要读取的包数量。
该代码使用最大数据包大小的保守预估值,Core Audio通过kAudioFilePropertyPacketSizeUpperBound
属性提供了该预估值。在大多数情况下,比起花时间读取整个音频文件以获得实际的最大数据包大小,使用这种近似(但快速)的技术更好。
清单3-12 设置播放音频队列缓冲区的大小和要读取的数据包数量
1 | UInt32 maxPacketSize; |
下面是该代码的工作方式:
AudioFileGetProperty
函数(在AudioFile.h
中声明),获取音频文件的指定属性的值。这里,可以用它来获取要播放文件中音频数据包大小的保守上限值(以字节为单位)。- 要播放的音频文件对象(类型为
AudioFileID
)。参阅Opening an Audio File。 - 用于获取音频文件中数据包大小的保守上限的属性ID。
- 输出时,
kAudioFilePropertyPacketSizeUpperBound
属性的大小(以字节为单位)。 - 输出时,要播放的文件的数据包大小的保守上限(以字节为单位)。
DeriveBufferSize
函数(在Write a Function to Derive Playback Audio Queue Buffer Size中描述),设置来缓冲区大小和每次调用回调函数时要读取的数据包数量。- 要播放的文件的音频数据格式。参阅Obtaining a File’s Audio Data Format。
- 来自第5行的音频文件最大数据包大小的预估值。
- 每次音频队列缓冲区应保留的音频时长(以秒为单位)。此处设置半秒是个不错的选择。
- 在输出时,每个音频队列缓冲区大小(以字节为单位)。该值放在音频队列的自定义结构体中。
- 在输出时,是在每次播放音频队列回调时要读取的数据包数量。该值也放在音频队列的自定义结构体中。
给数据包描述数组分配内存
现在,给数组分配内存,以保存一个缓冲区的音频数据的数据包描述。CBR数据不使用数据包描述,因此CBR的情况(清单3-13中的步骤3)非常简单。
清单3-13 给数据包描述数组分配内存
1 | bool isFormatVBR = ( // 1 |
下面是该代码的工作方式:
- 确定音频文件的数据格式是VBR还是CBR。在VBR数据中,bytes-per-packet或frames-per-packet值的一个或两个是可变的,因此列出在音频队列的
AudioStreamBasicDescription
结构体中这两个值为0
的情况。 - 对于包含VBR数据的音频文件,则为数据包描述数组分配内存。根据每次播放回调调用时要读取的音频数据包数量,计算所需内存。参阅Setting Buffer Size and Number of Packets to Read。
- 对于包含CBR数据的音频文件(例如线性PCM),音频队列不使用数据包描述数组。
给播放音频队列设置Magic Cookie
某些压缩音频格式(例如MPEG 4 AAC)利用结构体来包含音频元数据。这些结构体称为magic cookies。使用音频队列服务以这种格式播放文件时,需要从音频文件中获取magic cookie,然后在开始播放之前应用到音频队列中。
清单3-14展示了如何从文件中获取magic cookie并将其应用到音频队列。你需要在开始播放之前调用该函数。
清单3-14 为播放音频队列设置magic cookie
1 | UInt32 cookieSize = sizeof (UInt32); // 1 |
下面是该代码的工作方式:
- 设置magic cookie数据的预估大小。
- 接收
AudioFileGetPropertyInfo
函数的结果。如果成功,该函数返回NoErr
,等同于布尔值false
。 AudioFileGetPropertyInfo
函数(在AudioFile.h
中声明),获取指定属性值的大小。可以用它来设置保存属性值的变量大小。- 音频文件对象(类型为
AudioFileID
),表示要播放的音频文件。 - 表示音频文件的magic cookie数据的属性ID。
- 输入时,magic cookie数据的预估大小。输出时,是其实际大小。
- 用
NULL
表示不关心该属性的读/写访问权限。 - 如果音频文件确实包含magic cookie,则分配内存保存它。
AudioFileGetProperty
函数(在AudioFile.h
中声明),获取指定属性的值。在这里,它将获取音频文件的magic cookie。- 音频文件对象(类型为
AudioFileID
),表示要播放的以及要获取magic cookie的音频文件。 - 音频文件的magic cookie数据的属性ID。
- 输入时,
magicCookie
使用AudioFileGetPropertyInfo
函数获得变量的大小。输出时,将是magic cookie的实际大小(以写入magicCookie
变量的字节数为单位)。 - 输出时,音频文件的magic cookie。
AudioQueueSetProperty
函数给音频队列设置属性。在这里,它将给音频队列设置magic cookie,使其于要播放的音频文件中的magic cookie相匹配。- 要为其设置magic cookie的音频队列。
- 音频队列的magic cookie的属性ID。
- 要播放文件中的magic cookie。
- magic cookie的大小(以字节为单位)。
- 释放分配给magic cookie的内存。
分配和准备音频队列缓冲区
现在,请求之前创建的(参阅Create a Playback Audio Queue)音频队列来准备一组音频队列缓冲区,如清单3-15所示。
清单3-15 分配和准备音频队列缓冲区进行播放
1 | aqData.mCurrentPacket = 0; // 1 |
下面是该代码的工作方式:
- 数据包索引设为
0
,以便当前音频队列回调函数填充缓冲区(步骤7)时,是从音频文件的开头开始。 - 分配和准备一组音频队列缓冲区(
kNumberBuffers
设置为3
,参阅Define a Custom Structure to Manage State)。 AudioQueueAllocateBuffer
函数通过为其分配内存来创建音频队列缓冲区。- 分配缓冲区的音频队列。
- 新音频队列缓冲区大小(以字节为单位)。
- 输出时,把新的音频队列缓冲区添加到自定义结构体的
mBuffers
数组中。 HandleOutputBuffer
是播放音频队列的回调函数。参阅Write a Playback Audio Queue Callback。- 音频队列的自定义结构体。
- 要调用其回调的音频队列。
- 要传递给音频队列回调的缓冲区。
设置音频队列播放增益
在音频队列开始播放之前,通过音频队列参数机制设置其增益,如清单3-16所示。有关参数机制的更多信息,可参阅Audio Queue Parameters。
清单3-16 设置音频队列的播放增益
1 | Float32 gain = 1.0; // 1 |
下面是该代码的工作方式:
- 在
0
(静音)和1
(单元增益)之间设置增益。 AudioQueueSetParameter
函数设置音频队列的参数值。- 要设置参数的音频队列。
- 要设置的参数ID。
kAudioQueueParam_Volume
用于设置音频队列增益。 - 要应用于音频队列的增益设置。
启动和运行音频队列
前面的代码已经为播放文件做了准备。下面是启动音频队列和维护run loop,如清单3-17所示。
清单3-17 启动和运行音频队列
1 | aqData.mIsRunning = true; // 1 |
下面是该代码的工作方式:
- 设置自定义结构体标志,表示音频队列正在运行。
AudioQueueStart
函数在其自身的线程上启动音频队列。- 要开始的音频队列。
- 用
NULL
表示因队列应立即开始播放。 - 定义轮询自定义结构体的
mIsRunning
字段,以检查音频队列是否已经停止。 CFRunLoopRunInMode
函数运行包含音频队列线程的run loop。- 对run loop使用默认模式。
- 把run loop的运行时间设置为
0.25
秒。 - 用
false
表示run loop应在指定的时间内继续。 - 音频队列停止后,再运行一次run loop,以确保当前正在播放的音频队列缓冲区有足够时间完成。
播放后的清理
播放文件后,处理音频队列,关闭音频文件,并释放所有剩余资源,如清单3-18所示。
清单3-18 播放音频文件后清理
1 | AudioQueueDispose ( // 1 |
下面是该代码的工作方式:
AudioQueueDispose
函数处理音频队列及其所有资源,包括缓冲区。- 要处理的音频队列。
- 用
true
表示同步处理音频队列。 - 关闭播放的音频文件。
AudioFileClose
函数在AudioFile.h
中声明。 - 释放用于保存数据包描述的内存。
总结
- 使用AudioQueue实现播放功能,一般步骤:
- 定义一个自定义结构体来管理状态、格式和路径信息。
- 编写音频队列回调函数来执行实际的播放。
- 编写代码以确定音频队列缓冲区的合适大小。
- 打开音频文件进行播放,然后确定其音频数据格式。
- 创建一个播放音频队列并进行相关配置。
- 分配和排队音频队列缓冲区。告诉音频队列开始播放。完成后,播放回调函数告诉音频队列停止。
- 处理音频队列,释放资源。