版本记录
版本号 | 时间 |
---|---|
V1.0 | 2017.12.28 |
前言
ios系统中有很多方式可以播放音频文件,这里我们就详细的说明下播放音乐文件的原理和实例。感兴趣的可以看我写的上面几篇。
1. 几种播放音频文件的方式(一) —— 播放本地音乐
2. 几种播放音频文件的方式(二) —— 音效播放
3. 几种播放音频文件的方式(三) —— 网络音乐播放
4. 几种播放音频文件的方式(四) —— 音频队列服务(Audio Queue Services)(一)
5. 几种播放音频文件的方式(五) —— 音频队列服务(Audio Queue Services)简介(二)
6. 几种播放音频文件的方式(六) —— 音频队列服务(Audio Queue Services)之关于音频队列(三)
7. 几种播放音频文件的方式(七) —— 音频队列服务(Audio Queue Services)之录制音频(四)
Playing Audio - 播放音频
使用音频队列服务播放音频时,音源可以是任何东西 - 磁盘上文件,基于软件的音频合成器,内存中的对象等等。本章介绍最常见的情况:播放磁盘上的文件。
注意:本章介绍了一个基于ANSI-C的播放实现,并且使用了
Mac OS X Core Audio SDK
中的C ++类。对于基于Objective-C的示例,请参阅iOS Dev Center的SpeakHere
示例代码。
要为您的应用程序添加播放功能,通常需要执行以下步骤:
- 定义一个自定义结构来管理状态,格式和路径信息。
- 编写音频队列回调函数来执行实际播放。
- 编写代码来确定音频队列缓冲区的大小。
- 打开音频文件进行播放并确定其音频数据格式。
- 创建一个播放音频队列并将其配置为播放。
- 分配和入队音频队列缓冲区。告诉音频队列开始播放。完成后,回放回调告知音频队列停止。
- 销毁音频队列,释放资源。
本章的其余部分将详细介绍这些步骤。
Define a Custom Structure to Manage State - 定义一个自定义结构管理状态
首先,定义一个用来管理音频格式和音频队列状态信息的自定义结构。 Listing 3-1
说明了这样一个结构:
// Listing 3-1 A custom structure for a playback audio queue
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
字段在此用于保存正在播放的文件的格式。录制时,类似字段保存正在写入磁盘的文件的格式。
以下是这个结构中的字段的描述:
- 设置要使用的音频队列缓冲区的数量。如Audio Queue Buffers中所述,“三”通常是一个很好的数字。
- 表示正在播放的文件的音频数据格式的
AudioStreamBasicDescription
结构(来自CoreAudioTypes.h
)。该格式被mQueue
字段中指定的音频队列使用。通过查询音频文件的kAudioFilePropertyDataFormat
属性来填充mDataFormat
字段,如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。
- 对于
- 指示音频队列是否正在运行的布尔值。
Write a Playback Audio Queue Callback - 编写播放音频队列回调
接下来,写一个回放音频队列回调函数。 这个回调有三个主要的事情:
- 从音频文件读取指定数量的数据并将其放入音频队列缓冲区
- 将音频队列缓冲区排入缓冲队列
- 当没有更多的数据要从音频文件读取,告诉音频队列停止
本部分显示了一个示例回调声明,分别描述了这些任务,并最终呈现整个回放回调。 有关回放回调角色的说明,请参阅图Figure 1-4。
1. The Playback Audio Queue Callback Declaration - 播放音频队列回调函数声明
Listing 3-2
显示了一个播放音频队列回调函数的示例声明,声明为AudioQueue.h
头文件中的AudioQueueOutputCallback
// Listing 3-2 The playback audio queue callback declaration
static void HandleOutputBuffer (
void *aqData, // 1
AudioQueueRef inAQ, // 2
AudioQueueBufferRef inBuffer // 3
)
以下是这段代码的工作原理:
- 通常,
aqData
是包含音频队列状态信息的自定义结构,如Define a Custom Structure to Manage State所述。
- 通常,
- 拥有此回调的音频队列。
- 一个音频队列缓冲区,回调将通过读取音频文件来填充数据。
2. Reading From a File into an Audio Queue Buffer - 从文件中读取数据到音频队列缓冲区
回放音频队列回调的第一个动作是从音频文件读取数据并将其放入音频队列缓冲区。Listing 3-3
显示了如何做到这一点。
// Listing 3-3 Reading from an audio file into an audio queue buffer
AudioFileReadPackets ( // 1
pAqData->mAudioFile, // 2
false, // 3
&numBytesReadFromFile, // 4
pAqData->mPacketDescs, // 5
pAqData->mCurrentPacket, // 6
&numPackets, // 7
inBuffer->mAudioData // 8
);
以下是这段代码的工作原理:
- 在
AudioFile.h
头文件中声明的AudioFileReadPackets
函数从音频文件读取数据并将其放入缓冲区。
- 在
- 要从中读取的音频文件。
- 使用值为false来指示函数在读取时不应该缓存数据。
- 输出时,从音频文件中读取的音频数据的字节数。
- 在输出上,从音频文件中读取数据的数据包描述数组。 对于CBR数据,此参数的输入值为NULL。
- 从音频文件中读取的第一个数据包数据包索引。
- 输入时,从音频文件中读取的数据包数量。 输出时,实际读取的数据包数量。
- 输出时,填充的音频队列缓冲区包含从音频文件中读取的数据。
3. Enqueuing an Audio Queue Buffer - 声频队列缓冲入队
现在,数据已经从音频文件中读取并放入音频队列缓冲区,回调将缓冲区排入队列中,如Listing 3-4
所示。 一旦进入缓冲区队列,缓冲区中的音频数据就可供音频队列发送到输出设备。
// Listing 3-4 Enqueuing an audio queue buffer after reading from disk
AudioQueueEnqueueBuffer ( // 1
pAqData->mQueue, // 2
inBuffer, // 3
(pAqData->mPacketDescs ? numPackets : 0), // 4
pAqData->mPacketDescs // 5
);
以下是这段代码的工作原理:
-
AudioQueueEnqueueBuffer
函数将音频队列缓冲区添加到缓冲区队列中。
-
- 拥有缓冲队列
buffer queue
的音频队列audio queue
。
- 拥有缓冲队列
- 要排队的音频队列缓冲区
- 音频队列缓冲区数据中表示的数据包数量。 对于不使用数据包描述的CBR数据,使用0。
- 对于使用数据包描述的压缩音频数据格式,缓冲区中数据包的数据包描述。
4. Stopping an Audio Queue - 停止音频队列
你回调的最后一件事是检查是否没有更多的数据从你正在播放的音频文件中读取。 一旦发现文件结束,你的回调告诉播放音频队列停止。 Listing 3-5
说明了这一点。
// Listing 3-5 Stopping an audio queue
if (numPackets == 0) { // 1
AudioQueueStop ( // 2
pAqData->mQueue, // 3
false // 4
);
pAqData->mIsRunning = false; // 5
}
以下是这段代码的工作原理:
- 检查
AudioFileReadPackets
函数读取的数据包的数量是否为0。
- 检查
-
AudioQueueStop
函数停止音频队列。
-
- 要停止的音频队列。
- 当所有排队的缓冲区都被播放时,异步停止音频队列。 请参阅Audio Queue Control and State。
- 在自定义结构中设置一个标志来指示播放完成。
5. A Full Playback Audio Queue Callback - 播放音频队列回调函数完整版
Listing 3-6
显示了完整播放音频队列回调的基本版本。 与本文档中的其他代码示例一样,此列表不包括错误处理。
// Listing 3-6 A playback audio queue callback function
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;
}
}
以下是这段代码的工作原理:
- 实例化时提供给音频队列的自定义数据,包括表示要播放的文件的音频文件对象(类型
AudioFileID
)以及各种状态数据。 请参阅 Define a Custom Structure to Manage State。
- 实例化时提供给音频队列的自定义数据,包括表示要播放的文件的音频文件对象(类型
- 如果音频队列停止,立即返回。
- 保存正在播放的文件中读取的音频数据字节数的变量。
- 使用要从正在播放的文件中读取的数据包数初始化
numPackets
变量。
- 使用要从正在播放的文件中读取的数据包数初始化
- 测试是否从文件中检索到某些音频数据。 如果是,排队新填充的缓冲区。 如果不是,则停止音频队列。
- 告诉音频队列缓冲区结构读取数据的字节数。
- 根据读取的数据包数量递增数据包索引。
Write a Function to Derive Playback Audio Queue Buffer Size - 编写函数获取播放音频队列缓冲区的大小
音频队列服务期望您的应用程序指定您使用的音频队列缓冲区的大小。 Listing 3-7
显示了一种方法。 它产生足够大的缓冲区大小来保存给定持续时间的音频数据。
在创建一个回放音频队列之后,您将在您的应用程序中调用此DeriveBufferSize
函数,作为要求音频队列分配缓冲区的先决条件。 请参阅Set Sizes for a Playback Audio Queue。
与你在Write a Function to Derive Recording Audio Queue Buffer Size类似的函数相比,这里的代码做了两个额外的事情, 对于回放同样如此:
- 每次您的回调调用
AudioFileReadPackets
函数时,要读取数据包的数量
- 每次您的回调调用
- 设置缓冲区大小的下限,以避免过度频繁的磁盘访问
这里的计算考虑了您从磁盘读取的音频数据格式。 格式包括可能影响缓冲区大小的所有因素,例如音频通道的数量。
// Listing 3-7 Deriving a playback audio queue buffer size
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
}
以下是这段代码的工作原理:
- 音频队列的
AudioStreamBasicDescription
结构。
- 音频队列的
- 您正在播放的音频文件中数据的估计最大数据包大小。您可以通过调用
AudioFileGetProperty
函数(在AudioFile.h
头文件中声明)使用属性IDkAudioFilePropertyPacketSizeUpperBound
来确定此值。请参阅Set Sizes for a Playback Audio Queue。
- 您正在播放的音频文件中数据的估计最大数据包大小。您可以通过调用
- 按照音频的秒数指定每个音频队列缓冲区的大小。
- 输出时,每个音频队列缓冲区的大小(以字节为单位)。
- 输出时,每次调用回放音频队列回调时从文件中读取的音频数据包的数量。
- 音频队列缓冲区大小的上限(以字节为单位)。在这个例子中,上限设置为320 KB。这对应于大约五秒钟的立体声,采样率为96kHz的24位音频。
- 音频队列缓冲区大小的下限,以字节为单位。在这个例子中,下限设置为16 KB。
- 对于定义每个数据包固定数量帧的音频数据格式,获取音频队列缓冲区大小。
- 对于没有为每个数据包定义固定数量帧的音频数据格式,根据最大数据包大小和您设置的上限获取合理的音频队列缓冲区大小。
- 如果导出的缓冲区大小高于您设置的上限,则根据估计的最大数据包大小调整边界考虑。
- 如果导出的缓冲区大小低于您设置的下限,则将其调整到界限。
- 计算每次调用回调时从音频文件中读取的数据包数量。
Open an Audio File for Playback - 打开用于播放的音频文件
现在您打开一个音频文件进行播放,使用以下三个步骤:
- 获取表示要播放的音频文件的
CFURL
对象。
- 获取表示要播放的音频文件的
- 打开文件。
- 获取文件的音频数据格式
1. Obtaining a CFURL Object for an Audio File - 从音频文件中获取CFURL对象
Listing 3-8
演示了如何获取要播放的音频文件的CFURL对象。 在下一步中使用CFURL对象,打开文件。
// Listing 3-8 Obtaining a CFURL object for an audio file
CFURLRef audioFileURL =
CFURLCreateFromFileSystemRepresentation ( // 1
NULL, // 2
(const UInt8 *) filePath, // 3
strlen (filePath), // 4
false // 5
);
以下是这段代码的工作原理:
- 在
CFURL.h
头文件中声明的CFURLCreateFromFileSystemRepresentation
函数创建一个代表要播放文件的CFURL对象。
- 在
- 使用
NULL
(或kCFAllocatorDefault
)来使用当前的默认内存分配器。
- 使用
- 您要转换为CFURL对象的文件系统路径。 在生产代码中,通常会从用户获取
filePath
的值。
- 您要转换为CFURL对象的文件系统路径。 在生产代码中,通常会从用户获取
- 文件系统路径中的字节数。
- 值为
false
表示filePath
表示文件,而不是文件夹directory
。
- 值为
2. Opening an Audio File - 打开音频文件
Listing 3-9
演示了如何打开一个音频文件进行播放。
// Listing 3-9 Opening an audio file for playback
AQPlayerState aqData; // 1
OSStatus result =
AudioFileOpenURL ( // 2
audioFileURL, // 3
fsRdPerm, // 4
0, // 5
&aqData.mAudioFile // 6
);
CFRelease (audioFileURL); // 7
以下是这段代码的工作原理:
- 创建
AQPlayerState
自定义结构的实例(请参阅Define a Custom Structure to Manage State)。 当您打开音频文件进行播放时,您可以使用此实例作为放置表示音频文件的音频文件对象(AudioFileID
类型)的位置。
- 创建
- 在
AudioFile.h
头文件中声明的AudioFileOpenURL
函数打开你想要播放的文件。
- 在
- 对要播放的文件的引用。
- 您想要与您正在播放的文件一起使用的文件权限。 可用权限在文件管理器的
File Access Permission Constants
枚举中定义。 在这个例子中你要求读取文件的权限。
- 您想要与您正在播放的文件一起使用的文件权限。 可用权限在文件管理器的
- 一个可选的文件类型提示。 此处的值为0表示该示例不使用此功能。
- 在输出时,对音频文件的引用被放置在自定义结构的
mAudioFile
字段中。
- 在输出时,对音频文件的引用被放置在自定义结构的
- 释放在步骤1中创建的
CFURL
对象。
- 释放在步骤1中创建的
3. Obtaining a File’s Audio Data Format - 获取文件音频数据格式
Listing 3-10
显示了如何获取文件的音频数据格式
Listing 3-10 Obtaining a file’s audio data format
UInt32 dataFormatSize = sizeof (aqData.mDataFormat); // 1
AudioFileGetProperty ( // 2
aqData.mAudioFile, // 3
kAudioFilePropertyDataFormat, // 4
&dataFormatSize, // 5
&aqData.mDataFormat // 6
);
以下是这段代码的工作原理:
- 获取预期的属性值大小,用于查询音频文件的音频数据格式。
- 在
AudioFile.h
头文件中声明的AudioFileGetProperty
函数获取音频文件中指定属性的值。
- 在
- 音频文件对象(类型为
AudioFileID
),代表要获取其音频数据格式的文件。
- 音频文件对象(类型为
- 用于获取音频文件的数据格式的值的属性ID。
- 输入时,描述音频文件数据格式的
AudioStreamBasicDescription
结构的预期大小。 在输出上,实际的大小。 您的回放应用程序不需要使用此值。
- 输入时,描述音频文件数据格式的
- 在输出时,以音频文件的形式从
AudioStreamBasicDescription
结构中获得完整的音频数据格式。 该行将文件的音频数据格式存储到音频队列的自定义结构中,以将其应用于音频队列。
- 在输出时,以音频文件的形式从
Create a Playback Audio Queue - 创建播放音频队列
Listing 3-11
显示了如何创建一个回放音频队列。 请注意,AudioQueueNewOutput
函数使用前面步骤中配置的自定义结构和回调,以及要播放的文件的音频数据格式。
// Listing 3-11 Creating a playback audio queue
AudioQueueNewOutput ( // 1
&aqData.mDataFormat, // 2
HandleOutputBuffer, // 3
&aqData, // 4
CFRunLoopGetCurrent (), // 5
kCFRunLoopCommonModes, // 6
0, // 7
&aqData.mQueue // 8
);
以下是这段代码的工作原理:
-
AudioQueueNewOutput
函数创建一个新的播放音频队列。
-
- 音频队列正在设置播放的文件的音频数据格式。 请参阅 Obtaining a File’s Audio Data Format。
- 与回放音频队列一起使用的回调函数。 请参阅Write a Playback Audio Queue Callback。
- 播放音频队列的自定义数据结构。 请参阅Define a Custom Structure to Manage State。
- 当前运行循环,以及将调用音频队列回放回调的循环。
- 可以调用回调的运行循环模式。 通常,在这里使用
kCFRunLoopCommonModes
常量。
- 可以调用回调的运行循环模式。 通常,在这里使用
- 保留。 必须为0。
- 输出时,新分配的播放音频队列。
Set Sizes for a Playback Audio Queue - 设置回放音频队列的大小
接下来,您为播放音频队列设置一些大小。 在为音频队列分配缓冲区之前以及在开始读取音频文件之前,请使用这些大小。
本节中的代码清单显示如何设置:
- 音频队列缓冲区大小
- 每次调用回放音频队列回调时要读取的数据包数量
- 数组大小,用于保存一个缓冲区的音频数据的数据包描述
1. Setting Buffer Size and Number of Packets to Read - 设置缓存大小和要读取的包的数量
Listing 3-12
演示了如何使用之前编写的DeriveBufferSize
函数(请参阅 Write a Function to Derive Playback Audio Queue Buffer Size)。 此处的目标是为每个音频队列缓冲区设置一个以字节为单位的大小,并确定每次调用回放音频队列回调时要读取的数据包数量。
此代码使用保守估计的最大数据包大小,Core Audio
通过kAudioFilePropertyPacketSizeUpperBound
属性提供。 在大多数情况下,与比花费时间读取整个音频文件来获得实际的最大数据包大小相比,最好使用这种技术 - 这是近似而快速的 。
// Listing 3-12 Setting playback audio queue buffer size and number of packets to read
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
);
以下是这段代码的工作原理:
- 在
AudioFile.h
头文件中声明的AudioFileGetProperty
函数获取音频文件的指定属性的值。在这里,你用它来获得一个保守的上限,以字节为单位,你想播放的文件中的音频数据包的大小。
- 在
- 表示要播放的文件的音频文件对象(类型为
AudioFileID
)。请参阅打Opening an Audio File。
- 表示要播放的文件的音频文件对象(类型为
- 用于获取音频文件中数据包大小保守上限的属性ID。
- 在输出上,
kAudioFilePropertyPacketSizeUpperBound
属性的大小(以字节为单位)。
- 在输出上,
- 在输出上,对于要播放的文件,数据包大小的保守上限(以字节为单位)。
- 在Write a Function to Derive Playback Audio Queue Buffer Size中描述的
DeriveBufferSize
函数设置每次调用回放音频队列回调时要读取的缓冲区大小和数据包的数量。
- 在Write a Function to Derive Playback Audio Queue Buffer Size中描述的
- 您要播放的文件的音频数据格式。请参阅Obtaining a File’s Audio Data Format。
- 音频文件中估计的最大数据包大小,来自此列表的第5行。
- 每个音频队列缓冲区应该容纳的音频的秒数。半秒钟,如这里设置,通常是一个不错的选择。
- 输出时,每个音频队列缓冲区的大小(以字节为单位)。该值放置在音频队列的自定义结构中。
- 输出时,每次调用播放音频队列回调时要读取的数据包数量。该值也放置在音频队列的自定义结构中。
2. Allocating Memory for a Packet Descriptions Array - 为包的描述数组分配内存
现在,您为数组分配内存,以保存一个缓冲区的音频数据的数据包描述。 恒定比特率数据不使用数据包描述,因此Listing 3-13
中的CBR情况步骤3非常简单。
// Listing 3-13 Allocating memory for a packet descriptions array
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;
}
以下是这段代码的工作原理:
- 确定音频文件的数据格式是VBR还是CBR。 在VBR数据中,每个字节数据包或帧每个数据包值中的一个或两个是可变的,因此在音频队列的
AudioStreamBasicDescription
结构中将被列为0。
- 确定音频文件的数据格式是VBR还是CBR。 在VBR数据中,每个字节数据包或帧每个数据包值中的一个或两个是可变的,因此在音频队列的
- 对于包含VBR数据的音频文件,为数据包描述数组分配内存。 根据每次调用回放回调时要读取的音频数据包的数量来计算所需的内存。 请参阅Setting Buffer Size and Number of Packets to Read。
- 对于包含CBR数据(如线性PCM)的音频文件,音频队列不使用数据包描述数组。
Set a Magic Cookie for a Playback Audio Queue - 为播放音频队列设置Magic Cookie
某些压缩音频格式(如MPEG 4 AAC
)利用结构来包含音频元数据。 这些结构被称为Magic Cookie
。 当您使用音频队列服务以这种格式播放文件时,您将从音频文件中获取Magic Cookie,并在开始播放之前将其添加到音频队列中。
Listing 3-14
展示了如何从一个文件中获得一个Magic Cookie并将其应用到一个音频队列中。 开始播放之前,您的代码会调用此函数。
// Listing 3-14 Setting a magic cookie for a playback audio queue
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
}
以下是这段代码的工作原理:
- 为magic cookie数据设置估计大小。
- 捕获
AudioFileGetPropertyInfo
函数的结果。如果成功,则此函数返回NoErr
的值,相当于布尔值false。
- 捕获
- 在
AudioFile.h
头文件中声明的AudioFileGetPropertyInfo
函数获取指定属性值的大小。您可以使用它来设置保存属性值的变量的大小。
- 在
- 音频文件对象(类型为
AudioFileID
),表示要播放的音频文件。
- 音频文件对象(类型为
- 属性ID代表音频文件的magic cookie数据。
- 在输入时,magic cookie数据的估计大小。在输出上,实际的大小。
- 使用
NULL
来表示您不关心属性的读/写访问权限。
- 使用
- 如果音频文件包含一个magic cookie,分配内存来持有它。
- 在
AudioFile.h
头文件中声明的AudioFileGetProperty
函数获取指定属性的值。在这种情况下,它会获取音频文件的magic cookie。
- 在
- 音频文件对象(类型为
AudioFileID
),表示您要播放的音频文件,以及您获取的magic cookie。
- 音频文件对象(类型为
- 表示音频文件magic cookie数据的属性ID。
- 在输入上,使用
AudioFileGetPropertyInfo
函数获得的magicCookie
变量的大小。在输出上,根据写入到magicCookie
变量的字节数来计算magic cookie的实际大小。
- 在输入上,使用
- 输出时,音频文件的magic cookie。
-
AudioQueueSetProperty
函数在音频队列中设置一个属性。在这种情况下,它为音频队列设置一个magic cookie,匹配要播放的音频文件中的magic cookie。
-
- 您要为其设置magic cookie的音频队列。
- 属性ID代表音频队列的magic cookie。
- 您要播放的音频文件中的magic cookie。
- magic cookie的大小,以字节为单位。
- 释放为magic cookie分配的内存。
Allocate and Prime Audio Queue Buffers - 分配和填充音频队列缓冲区
现在您可以询问您创建的音频队列(在Create a Playback Audio Queue中)以准备一组音频队列缓冲区。 Listing 3-15
演示了如何做到这一点。
// Listing 3-15 Allocating and priming audio queue buffers for playback
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
);
}
以下是这段代码的工作原理:
- 将数据包索引设置为0,以便当音频队列回调开始填充缓冲区时(步骤7),它将从音频文件的开始处开始。
- 分配和填充一组音频队列缓冲区。 (您可以在Define a Custom Structure to Manage State。将此编号
kNumberBuffers
设置为3)。
- 分配和填充一组音频队列缓冲区。 (您可以在Define a Custom Structure to Manage State。将此编号
-
AudioQueueAllocateBuffer
函数通过为其分配内存来创建音频队列缓冲区。
-
- 正在分配音频队列缓冲区的音频队列。
- 新的音频队列缓冲区的大小(以字节为单位)。
- 在输出上,将新的音频队列缓冲区添加到自定义结构中的
mBuffers
数组中。
- 在输出上,将新的音频队列缓冲区添加到自定义结构中的
-
HandleOutputBuffer
函数是你写的回放音频队列回调函数。 请参阅 Write a Playback Audio Queue Callback。
-
- 音频队列的自定义结构。
- 您正在调用的回调的音频队列。
- 您传递给音频队列回调的音频队列缓冲区。
Set an Audio Queue’s Playback Gain - 设置音频队列的播放增益
在您告诉音频队列开始播放之前,您需要通过音频队列参数机制设置其增益。 Listing 3-16
显示了如何做到这一点。 有关参数机制的更多信息,请参阅Audio Queue Parameters。
// Listing 3-16 Setting an audio queue’s playback gain
Float32 gain = 1.0; // 1
// Optionally, allow user to override gain setting here
AudioQueueSetParameter ( // 2
aqData.mQueue, // 3
kAudioQueueParam_Volume, // 4
gain // 5
);
以下是这段代码的工作原理:
- 在0(用于静音)和1(用于单位增益)之间设置与音频队列一起使用的增益。
-
AudioQueueSetParameter
函数设置音频队列的参数值。
-
- 您正在设置参数的音频队列。
- 您正在设置的参数的ID。
kAudioQueueParam_Volume
常量让您设置音频队列的增益。
- 您正在设置的参数的ID。
- 您正在应用到音频队列的增益设置。
Start and Run an Audio Queue - 开始和运行音频队列
所有前面的代码都说明了播放文件的过程。 这包括启动音频队列并在文件播放时保持运行循环,如Listing 3-17
所示
// Listing 3-17 Starting and running an 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
);
以下是这段代码的工作原理:
- 在自定义结构中设置一个标志来指示音频队列正在运行。
-
AudioQueueStart
函数在其自己的线程上启动音频队列。
-
- 音频队列开始。
- 使用
NULL
来指示音频队列应该立即开始播放。
- 使用
- 定期轮询自定义结构的
mIsRunning
字段,以检查音频队列是否已停止。
- 定期轮询自定义结构的
-
CFRunLoopRunInMode
函数运行包含音频队列线程的运行循环。
-
- 使用运行循环的默认模式。
- 将运行循环的运行时间设置为0.25秒。
- 使用false来指示运行循环应该持续指定的全部时间。
- 音频队列停止后,再运行一次运行循环,以确保当前正在播放的音频队列缓冲区有时间完成。
Clean Up After Playing - 播放完后的清理
当您完成播放文件时,请释放音频队列,关闭音频文件并释放剩余的资源。 Listing 3-18
说明了这些步骤。
// Listing 3-18 Cleaning up after playing an audio file
AudioQueueDispose ( // 1
aqData.mQueue, // 2
true // 3
);
AudioFileClose (aqData.mAudioFile); // 4
free (aqData.mPacketDescs); // 5
以下是这段代码的工作原理:
-
AudioQueueDispose
函数销毁音频队列及其所有资源,包括其缓冲区。
-
- 您想要处理的音频队列。
- 使用true来同步销毁音频队列。
- 关闭播放的音频文件。
AudioFileClose
函数在AudioFile.h
头文件中声明。
- 关闭播放的音频文件。
- 释放用来保存数据包描述的内存。
后记
未完,待续~~~