0%

Audio Queue Services Programming Guide:播放音频

当你使用音频队列服务播放音频时,源几乎可以是任意的——磁盘文件、基于软件音频合成器、内存中的对象等。本章介绍最常见的情况:播放磁盘上的文件。

注意:本章介绍了基于ANSI-C的播放实现,并使用了Mac OS X Core Audio SDK的C++类。有关Objective-C的示例,参阅iOS Dev Center中的_SpeakHere_示例代码。

要把播放功能添加到程序中,通常需要执行以下步骤:

  1. 定义一个自定义结构体来管理状态、格式和路径信息。
  2. 编写音频队列回调函数来执行实际的播放。
  3. 编写代码以确定音频队列缓冲区的合适大小。
  4. 打开音频文件进行播放,然后确定其音频数据格式。
  5. 创建一个播放音频队列并进行相关配置。
  6. 分配和排队音频队列缓冲区。告诉音频队列开始播放。完成后,播放回调函数告诉音频队列停止。
  7. 处理音频队列,释放资源。

本章的剩余部分详细介绍了每个步骤。

定义结构体管理状态

首先,定义一个结构体,将用它来管理音频格式和音频队列状态信息,如清单3-1所示:

清单3-1 播放音频队列的自定义结构体

1
2
3
4
5
6
7
8
9
10
11
12
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
};

结构体中大多数字段与用于录制的自定义结构体几乎相同,如Define a Custom Structure to Manage State所述。例如,mDataFormat字段保存正在播放的文件格式。录制时,类似的字段保存了写入磁盘的文件格式。

以下是该结构体各字段介绍:

  1. 设置要使用的音频队列缓冲区数量。如Audio Queue Buffers所述,3个通常是不错的选择。
  2. AudioStreamBasicDescription结构体(来自CoreAudioTypes.h)表示正在播放的文件的音频数据格式。该格式由mQueue字段指定的音频队列使用。 mDataFormat字段通过查询音频文件的kAudioFilePropertyDataFormat属性来填充该字段,如Obtaining a File’s Audio Data Format所述。 有关AudioStreamBasicDescription结构体的详细信息,参阅_Core Audio Data Types Reference_。
  3. 程序创建的播放音频队列。
  4. 一个数组,包含指向音频队列管理的音频队列缓冲区的指针。
  5. 代表程序播放的音频文件的对象。
  6. 每个音频队列缓冲区的大小(以字节为单位)。该值在音频队列创建之后和开始之前,由DeriveBufferSize函数计算。参阅Write a Function to Derive Playback Audio Queue Buffer Size
  7. 音频文件中下一个要播放的数据包索引。
  8. 每次调用音频队列的播放回调函数时,要读取的数据包数量。就像bufferByteSize字段一样,在音频队列创建之后和开始之前,由DeriveBufferSize函数计算该值。
  9. 对于VBR音频数据,该字段是正在播放的文件的数据包描述数组。对于CBR数据,该字段为NULL
  10. 一个布尔值,表示音频队列是否正在运行。

编写播放音频队列回调函数

下面,编写一个播放音频队列回调函数。该回调函数执行三项主要任务:

  • 从音频文件中读取指定数量的数据,并将其放入音频队列缓冲区中。
  • 把音频队列缓冲区排队到缓冲区队列中。
  • 当没有更多数据要从音频文件中读取时,告诉音频队列停止。

本节展示来一个回调声明示例,分别描述各个任务,最后给出完整的播放回调函数。有关播放回调函数的作用,参阅图1-4

播放音频队列回调声明

清单3-2展示了一个播放音频回调函数的示例声明,AudioQueueOutputCallbackAudioQueue.h声明为:

清单3-2 播放音频队列回调声明

1
2
3
4
5
static void HandleOutputBuffer (
void *aqData, // 1
AudioQueueRef inAQ, // 2
AudioQueueBufferRef inBuffer // 3
)

下面是该代码的工作方式:

  1. 通常,aqData是包含定义音频队列状态信息的自定义结构体。如Define a Custom Structure to Manage State所述。
  2. 持有该回调函数的音频队列。
  3. 音频队列缓冲区,回调函数通过从音频文件中读取,来填充数据。

从文件读取到音频队列缓冲区

播放音频队列回调函数的第一个操作是从音频文件中读取数据并将其放在音频队列缓冲区中,如清单3-3所示。

清单3-3 从音频文件读取到音频队列缓冲区

1
2
3
4
5
6
7
8
9
AudioFileReadPackets (                        // 1
pAqData->mAudioFile, // 2
false, // 3
&numBytesReadFromFile, // 4
pAqData->mPacketDescs, // 5
pAqData->mCurrentPacket, // 6
&numPackets, // 7
inBuffer->mAudioData // 8
);

下面是该代码的工作方式:

  1. AudioFileReadPackets函数(在AudioFile.h中声明),从音频文件读取数据并将其放入缓冲区中。
  2. 要读取的音频文件。
  3. false表示该函数在读取时不应缓存数据。
  4. 输出时,是从音频文件读取的音频数据字节数。
  5. 输出时,是从音频文件中读取的数据包描述数组。对于CBR数据,该参数输入NULL
  6. 从音频文件中读取第一个数据包的索引。
  7. 输入时,是要从音频文件读取的数据包数量。输出时,是实际读取的包数量。
  8. 在输出时,填充的音频队列缓冲区包含从音频文件读取的数据。

排队音频队列缓冲区

现在已经从音频文件中读取数据并将其放在音频队列缓冲区中,回调函数让缓冲区入队,如清单3-4所示。进入缓冲区队列后,缓冲区的音频数据可用于音频队列发送到输出设备。

清单3-4 从磁盘中读取后排队音频队列缓冲区

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

下面是该代码的工作方式:

  1. AudioQueueEnqueueBuffer函数把音频队列缓冲区添加到缓冲区队列。
  2. 持有缓冲区队列的音频队列。
  3. 要排队的音频队列缓冲区。
  4. 音频队列缓冲区数据中的数据包数量。对于不使用数据包描述的CBR数据,设为0
  5. 对于使用数据描述的压缩音频数据格式,数据包描述在缓冲区中。

停止音频队列

回调函数最后一个操作是检查是否有更多的数据,要从正在播放的音频文件中读取。在发现文件结尾后,回调函数告诉音频队列停止,如清单3-5所示。

清单3-5  Stopping an audio queue

1
2
3
4
5
6
7
if (numPackets == 0) {                          // 1
AudioQueueStop ( // 2
pAqData->mQueue, // 3
false // 4
);
pAqData->mIsRunning = false; // 5
}

下面是该代码的工作方式:

  1. 检查AudioFileReadPackets函数(由之前的回调函数调用)读取的数据包数量是否为0
  2. AudioQueueStop函数停止音频队列。
  3. 要停止的音频队列。
  4. 播放所有排队的缓冲区后,异步停止音频队列。参阅Audio Queue Control and State
  5. 设置结构体标志,表示播放已完成。

完整播放音频队列回调函数

清单3-6展示了完整播放音频队列回调的基本代码。和本文档的其他示例代码一样,该清单代码不包含错误处理。

清单3-6 一个播放音频队列回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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;
}
}

下面是该代码的工作方式:

  1. 实例化后提供给音频队列的自定义结构体,包含要播放的音频文件对象(类型为AudioFileID),以及各种状态数据。参阅Define a Custom Structure to Manage State
  2. 如果音频队列已停止,则立即返回。
  3. 一个变量,用于保存从正在播放的文件中读取的音频数据字节数。
  4. 使用要从正播放的文件中读取的数据包来初始化numPackets变量。
  5. 测试是否从文件中检索了一些音频数据。如果是,则让新填充的缓冲区入队;否则停止音频队列。
  6. 告诉音频队列缓冲区结构体已读取数据的字节数。
  7. 根据读取的数据包数量增加数据包索引。

编写函数计算播放音频队列缓冲区大小

音频队列服务希望你的程序为使用的音频队列缓冲区指定大小,如清单3-7所示。它得出的缓冲区大小足以容纳给定的音频时长。

创建播放音频队列后,你将在程序中调用DeriveBufferSize函数,作为后续音频队列分配缓冲区的先决条件。参阅Write a Function to Derive Recording Audio Queue Buffer Size。为了播放,还需要:

  • 在每次回调函数调用AudioFileReadPackets函数,得出要读取的数据包数量。
  • 设置缓冲区大小的下限,以避免过多的磁盘访问。

这里的计算考虑了从磁盘读取的音频数据格式。该格式包括了可能影响缓冲区大小的所有因素,例如音频通道数量。

清单3-7 得出播放音频队列缓冲区大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
}

下面是该代码的工作方式:

  1. 音频队列的AudioStreamBasicDescription结构体。
  2. 正在播放的音频文件中最大数据包的预估大小。你可以通过kAudioFilePropertyPacketSizeUpperBound属性ID,使用AudioFileGetProperty函数(在AudioFile.h中声明)得出该值。参阅Set Sizes for a Playback Audio Queue
  3. 为每个音频缓冲区指定大小(以秒为单位)。
  4. 在输出时,是每个音频队列缓冲区的大小(以字节为单位)。
  5. 在输出时,是在每次播放音频队列回调时,从文件读取的音频数据包的数量。
  6. 音频队列缓冲区大小的上限(以字节为单位)。在该示例中,上限设为320 KB。这相等于以96 kHz采样率,大约持续5秒的24位立体声音频。
  7. 音频队列缓冲区大小的下限(以字节为单位)。在该示例中,下限设为16 KB。
  8. 对于定义每个数据包固定帧数的音频数据格式,需要得出音频队列缓冲区大小。
  9. 对于没定义每个数据包固定帧数的音频格式,需要根据最大数据包大小和设置的上限得出合理的音频队列缓冲区大小。
  10. 如果得出的缓冲区大小大于设置的上限,则考虑预估的最大数据包大小,并将其调整为边界值。
  11. 如果得出的缓冲区大小低于设置的下限,则将其调整为下限。
  12. 计算每次调用回调时从音频文件读取的数据包数量。

打开音频文件进行播放

现在,使用以下步骤打开音频文件进行播放:

  1. 获取一个表示要播放的音频文件的CFURL对象。
  2. 打开文件。
  3. 获取文件的音频数据格式。

获取音频文件的CFURL对象

清单3-8展示了如何为要播放的音频文件获取CFURL对象。在下一步中使用CFURL对象,打开文件。

清单3-8 获取音频文件的CFURL对象

1
2
3
4
5
6
7
CFURLRef audioFileURL =
CFURLCreateFromFileSystemRepresentation ( // 1
NULL, // 2
(const UInt8 *) filePath, // 3
strlen (filePath), // 4
false // 5
);

下面是该代码的工作方式:

  1. CFURLCreateFromFileSystemRepresentation函数(在CFURL.h中声明),创建一个CFURL对象,该对象表示要播放的文件。
  2. NULLkCFAllocatorDefault,表示使用当前默认的内存分配器。
  3. 想要转换为CFURL的文件系统路径。在生产代码中,通常会从用户获取filePath值。
  4. 文件系统路径中的字节数。
  5. false值表示filePath代表文件,而不是目录。

打开音频文件

清单3-9展示了如何打开音频文件进行播放。

清单3-9 打开音频文件进行播放

1
2
3
4
5
6
7
8
9
10
11
AQPlayerState aqData;                                   // 1

OSStatus result =
AudioFileOpenURL ( // 2
audioFileURL, // 3
fsRdPerm, // 4
0, // 5
&aqData.mAudioFile // 6
);

CFRelease (audioFileURL); // 7

下面是该代码的工作方式:

  1. 创建AQPlayerState自定义结构体实例(参阅Define a Custom Structure to Manage State)。打开音频文件进行播放时,可以使用该实例存放音频文件对象(类型为AudioFileID)。
  2. AudioFileOpenURL函数(在AudioFile.h中声明),打开要播放的文件。
  3. 要播放文件的引用。
  4. 与正在播放文件一起使用的文件权限。可用权限在文件管理器的File Access Permission Constants枚举中定义。在该示例中,请求读取文件的权限。
  5. 可选文件类型hint。这里的0表示该示例未使用该功能。
  6. 在输出时,对音频文件的引用将放在自定义结构体的mAudioFile字段。
  7. 释放在第一步创建的CFURL对象。

获取文件的音频数据格式

清单3-10展示了如何获取文件的音频数据格式。

清单3-10 获取文件的音频数据格式

1
2
3
4
5
6
7
8
9
UInt32 dataFormatSize = sizeof (aqData.mDataFormat);    // 1

AudioFileGetProperty ( // 2
aqData.mAudioFile, // 3
kAudioFilePropertyDataFormat, // 4
&dataFormatSize, // 5
&aqData.mDataFormat // 6
);

下面是该代码的工作方式:

  1. 获取在查询音频文件有关音频数据格式时要使用的预期属性值大小。
  2. AudioFileGetProperty函数(在AudioFile.h中声明),获取音频文件中指定属性的值。
  3. 音频文件对象(类型为AudioFileID),表示要获取其音频数据格式的文件。
  4. 用户获取音频文件的数据格式的属性ID。
  5. 输入时,是描述音频文件的数据格式的AudioStreamBasicDescription结构体的预期大小。输出时,是其实际大小。播放程序不需要使用该值。
  6. 在输出时,从音频文件获得AudioStreamBasicDescription结构体的完整音频数据格式。该行通过把文件的音频数据格式存储在音频队列的自定义结构体中,将其应用于音频队列。

创建播放音频队列

清单3-11展示了如何创建播放音频队列。注意,AudioQueueNewOutput函数使用了在之前步骤中配置的自定义结构体和回调函数,以及要播放文件的音频数据格式。

清单3-11 创建播放音频队列

1
2
3
4
5
6
7
8
9
AudioQueueNewOutput (                                // 1
&aqData.mDataFormat, // 2
HandleOutputBuffer, // 3
&aqData, // 4
CFRunLoopGetCurrent (), // 5
kCFRunLoopCommonModes, // 6
0, // 7
&aqData.mQueue // 8
);

下面是该代码的工作方式:

  1. AudioQueueNewOutput函数创建一个新的播放音频队列。
  2. 设置要播放音频队列的音频数据格式。参阅Obtaining a File’s Audio Data Format
  3. 和播放音频队列一起使用的回调函数。参阅Write a Playback Audio Queue Callback
  4. 播放音频队列的自定义数据结构体。参阅Define a Custom Structure to Manage State
  5. 当前的run loop,将在其调用音频队列回调函数。
  6. run loop模式。通常设为kCFRunLoopCommonModes
  7. 保留参数,必需为0
  8. 在输出时,新分配的播放音频队列。

设置播放音频队列大小

接下来,设置播放音频队列的一些大小值。在为音频队列分配缓冲区时,以及开始读取音频文件之前,请使用这些大小值。

本节中的代码清单展示了如何设置:

  • 音频队列缓冲区大小。
  • 每次调用播放音频队列回调函数时要读取的数据包数量。
  • 数组大小,用于保存一个缓冲区的音频数据的数据包描述。

设置缓冲区大小和要读取的数据包数量

清单3-12展示了如何使用之前编写的DeriveBufferSize函数(参阅Write a Function to Derive Playback Audio Queue Buffer Size)。这里的目的是为每个音频队列缓冲区设置一个大小(以字节为单位),并确定每次调用播放音频队列回调函数时要读取的包数量。

该代码使用最大数据包大小的保守预估值,Core Audio通过kAudioFilePropertyPacketSizeUpperBound属性提供了该预估值。在大多数情况下,比起花时间读取整个音频文件以获得实际的最大数据包大小,使用这种近似(但快速)的技术更好。

清单3-12 设置播放音频队列缓冲区的大小和要读取的数据包数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
);

下面是该代码的工作方式:

  1. AudioFileGetProperty函数(在AudioFile.h中声明),获取音频文件的指定属性的值。这里,可以用它来获取要播放文件中音频数据包大小的保守上限值(以字节为单位)。
  2. 要播放的音频文件对象(类型为AudioFileID)。参阅Opening an Audio File
  3. 用于获取音频文件中数据包大小的保守上限的属性ID。
  4. 输出时,kAudioFilePropertyPacketSizeUpperBound属性的大小(以字节为单位)。
  5. 输出时,要播放的文件的数据包大小的保守上限(以字节为单位)。
  6. DeriveBufferSize函数(在Write a Function to Derive Playback Audio Queue Buffer Size中描述),设置来缓冲区大小和每次调用回调函数时要读取的数据包数量。
  7. 要播放的文件的音频数据格式。参阅Obtaining a File’s Audio Data Format
  8. 来自第5行的音频文件最大数据包大小的预估值。
  9. 每次音频队列缓冲区应保留的音频时长(以秒为单位)。此处设置半秒是个不错的选择。
  10. 在输出时,每个音频队列缓冲区大小(以字节为单位)。该值放在音频队列的自定义结构体中。
  11. 在输出时,是在每次播放音频队列回调时要读取的数据包数量。该值也放在音频队列的自定义结构体中。

给数据包描述数组分配内存

现在,给数组分配内存,以保存一个缓冲区的音频数据的数据包描述。CBR数据不使用数据包描述,因此CBR的情况(清单3-13中的步骤3)非常简单。

清单3-13 给数据包描述数组分配内存

1
2
3
4
5
6
7
8
9
10
11
12
13
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数据中,bytes-per-packet或frames-per-packet值的一个或两个是可变的,因此列出在音频队列的AudioStreamBasicDescription结构体中这两个值为0的情况。
  2. 对于包含VBR数据的音频文件,则为数据包描述数组分配内存。根据每次播放回调调用时要读取的音频数据包数量,计算所需内存。参阅Setting Buffer Size and Number of Packets to Read
  3. 对于包含CBR数据的音频文件(例如线性PCM),音频队列不使用数据包描述数组。

某些压缩音频格式(例如MPEG 4 AAC)利用结构体来包含音频元数据。这些结构体称为magic cookies。使用音频队列服务以这种格式播放文件时,需要从音频文件中获取magic cookie,然后在开始播放之前应用到音频队列中。

清单3-14展示了如何从文件中获取magic cookie并将其应用到音频队列。你需要在开始播放之前调用该函数。

清单3-14 为播放音频队列设置magic cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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. 设置magic cookie数据的预估大小。
  2. 接收AudioFileGetPropertyInfo函数的结果。如果成功,该函数返回NoErr,等同于布尔值false
  3. AudioFileGetPropertyInfo函数(在AudioFile.h中声明),获取指定属性值的大小。可以用它来设置保存属性值的变量大小。
  4. 音频文件对象(类型为AudioFileID),表示要播放的音频文件。
  5. 表示音频文件的magic cookie数据的属性ID。
  6. 输入时,magic cookie数据的预估大小。输出时,是其实际大小。
  7. NULL表示不关心该属性的读/写访问权限。
  8. 如果音频文件确实包含magic cookie,则分配内存保存它。
  9. AudioFileGetProperty函数(在AudioFile.h中声明),获取指定属性的值。在这里,它将获取音频文件的magic cookie。
  10. 音频文件对象(类型为AudioFileID),表示要播放的以及要获取magic cookie的音频文件。
  11. 音频文件的magic cookie数据的属性ID。
  12. 输入时,magicCookie使用AudioFileGetPropertyInfo函数获得变量的大小。输出时,将是magic cookie的实际大小(以写入magicCookie变量的字节数为单位)。
  13. 输出时,音频文件的magic cookie。
  14. AudioQueueSetProperty函数给音频队列设置属性。在这里,它将给音频队列设置magic cookie,使其于要播放的音频文件中的magic cookie相匹配。
  15. 要为其设置magic cookie的音频队列。
  16. 音频队列的magic cookie的属性ID。
  17. 要播放文件中的magic cookie。
  18. magic cookie的大小(以字节为单位)。
  19. 释放分配给magic cookie的内存。

分配和准备音频队列缓冲区

现在,请求之前创建的(参阅Create a Playback Audio Queue)音频队列来准备一组音频队列缓冲区,如清单3-15所示。

清单3-15 分配和准备音频队列缓冲区进行播放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)时,是从音频文件的开头开始。
  2. 分配和准备一组音频队列缓冲区(kNumberBuffers设置为3,参阅Define a Custom Structure to Manage State)。
  3. AudioQueueAllocateBuffer函数通过为其分配内存来创建音频队列缓冲区。
  4. 分配缓冲区的音频队列。
  5. 新音频队列缓冲区大小(以字节为单位)。
  6. 输出时,把新的音频队列缓冲区添加到自定义结构体的mBuffers数组中。
  7. HandleOutputBuffer是播放音频队列的回调函数。参阅Write a Playback Audio Queue Callback
  8. 音频队列的自定义结构体。
  9. 要调用其回调的音频队列。
  10. 要传递给音频队列回调的缓冲区。

设置音频队列播放增益

在音频队列开始播放之前,通过音频队列参数机制设置其增益,如清单3-16所示。有关参数机制的更多信息,可参阅Audio Queue Parameters

清单3-16 设置音频队列的播放增益

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

下面是该代码的工作方式:

  1. 0(静音)和1(单元增益)之间设置增益。
  2. AudioQueueSetParameter函数设置音频队列的参数值。
  3. 要设置参数的音频队列。
  4. 要设置的参数ID。kAudioQueueParam_Volume用于设置音频队列增益。
  5. 要应用于音频队列的增益设置。

启动和运行音频队列

前面的代码已经为播放文件做了准备。下面是启动音频队列和维护run loop,如清单3-17所示。

清单3-17 启动和运行音频队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
);

下面是该代码的工作方式:

  1. 设置自定义结构体标志,表示音频队列正在运行。
  2. AudioQueueStart函数在其自身的线程上启动音频队列。
  3. 要开始的音频队列。
  4. NULL表示因队列应立即开始播放。
  5. 定义轮询自定义结构体的mIsRunning字段,以检查音频队列是否已经停止。
  6. CFRunLoopRunInMode函数运行包含音频队列线程的run loop。
  7. 对run loop使用默认模式。
  8. 把run loop的运行时间设置为0.25秒。
  9. false表示run loop应在指定的时间内继续。
  10. 音频队列停止后,再运行一次run loop,以确保当前正在播放的音频队列缓冲区有足够时间完成。

播放后的清理

播放文件后,处理音频队列,关闭音频文件,并释放所有剩余资源,如清单3-18所示。

清单3-18 播放音频文件后清理

1
2
3
4
5
6
7
8
AudioQueueDispose (                            // 1
aqData.mQueue, // 2
true // 3
);

AudioFileClose (aqData.mAudioFile); // 4

free (aqData.mPacketDescs); // 5

下面是该代码的工作方式:

  1. AudioQueueDispose函数处理音频队列及其所有资源,包括缓冲区。
  2. 要处理的音频队列。
  3. true表示同步处理音频队列。
  4. 关闭播放的音频文件。AudioFileClose函数在AudioFile.h中声明。
  5. 释放用于保存数据包描述的内存。

总结

  • 使用AudioQueue实现播放功能,一般步骤:
    1. 定义一个自定义结构体来管理状态、格式和路径信息。
    2. 编写音频队列回调函数来执行实际的播放。
    3. 编写代码以确定音频队列缓冲区的合适大小。
    4. 打开音频文件进行播放,然后确定其音频数据格式。
    5. 创建一个播放音频队列并进行相关配置。
    6. 分配和排队音频队列缓冲区。告诉音频队列开始播放。完成后,播放回调函数告诉音频队列停止。
    7. 处理音频队列,释放资源。

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