当你使用音频队列服务进行录制的时候,你可以将音频录制到任何地方:磁盘文件、网络连接或内存对象等等。本章将介绍中最常见的一种情况,将音频录制到磁盘文件中。
注意:本章介绍了基于ANSI-C的录制的实现,并且使用了MAC OS X中Core Audio SDK中了一些C++类,如果想了解基于Objective-C的例子,请参考iOS Dev Center中的_SpeakHere_例子。
要把录制功能添加到程序中,一般都要进行以下几个步骤:
- 定义一个自定义的结构体来管理状态、格式以及路径信息等。
- 编写音频队列回调函数来执行实际的录制工作。
- (可选)编写代码来为音频队列缓冲区选择一个合适的大小。如果你将要录制的格式使用了magic cookies,你需要编写相应的代码来配合使用。
- 填充自定义结构体中的各个字段,包括指定音频队列将要录制到的文件的数据流、文件路径。
- 创建一个用于录制的音频队列并且让音频队列创建一系列的音频队列缓冲区,同时创建一个将要写入的文件。
- 通知音频队列开始录制。
- 录制完毕之后,通知音频队列停止录制,然后释放掉它,同时它会释放掉它所拥有的缓冲区。
本章的剩余部分将详细描述上述的每一个步骤。
定义一个管理状态的结构体
使用音频队列服务来开发一个音频录制解决方案的时候,第一步就是定义一个结构体。将使用这个结构体来管理音频格式和音频队列状态信息。清单2-1展示了这个这样的一个结构体。
清单2-1 一个用于录制的音频队列的结构体
1 | static const int kNumberBuffers = 3; // 1 |
下面是这个结构体中每个字段的说明:
- 要使用的音频队列缓冲区的数量。
- 一个
AudioStreamBasicDescription
结构体(来自CoreAudioTypes.h
),表示将要写入磁盘的音频数据的格式,音频队列缓冲区使用这个格式来指定它的mQueue
字段。mDataFormat
字段是由你的程序初始化的,参阅Set Up an Audio Format for Recording。可以通过查询音频队列的kAudioQueueProperty_StreamDescription
属性来更新这个字段的值,参阅Getting the Full Audio Format from an Audio Queue。在Mac OS X v10.5中要使用kAudioConverterCurrentInputStreamDescription
。 关于AudioStreamBasicDescription
结构体的详细信息,请参阅_Core Audio Data Types Reference_。 - 由程序创建的音频队列。
- 一个指向由音频队列所管理的音频队列缓冲区的指针数组。
- 一个表示程序录制音频时写入的文件的音频文件对象。
- 每个音频队列缓冲区的字节大小。它的值在随后的例子中的
DeriveBufferSize
函数中计算出来,它在音频队列创建后,开始录制音频前计算出来,参阅Write a Function to Derive Recording Audio Queue Buffer Size。 - 从当前音频队列缓冲区写入文件的第一个包(packet)的索引。
- 一个布尔值,表示音频队列是否在运行中。
编写用于录制的音频队列的回调函数
接下来,编写一个用于录制的回调函数,这个函数主要做两个事情:
- 将新填充进音频队列缓冲区的内容写入你正在录制的文件中。
- 将刚才已经将内容写入文件的音频队列缓冲区入队到缓冲区队列。
下面展示了一个回调函数声明的列子,然后分别描述这两个任务,最后展示一个完整的用于录制的回调函数。关于用于录制的音频队列回调函数所扮演的角色,可以参考图1-3。
用于录制的音频队列回调函数的声明
清单2-2是一个用于录制的音频队列回调函数声明,是在AudioQueue.h
中声明的AudioQueueInputCallback
:
清单2-2 用于录制的音频队列回调函数声明
1 | static void HandleInputBuffer ( |
下面是该代码的工作方式:
- 一般来说,
aqData
是一个自定义的数据结构,包含了音频队列的状态信息,参阅Define a Custom Structure to Manage State。 - 拥有该回调函数的音频队列。
- 包含录制数据的音频队列缓冲区。
- 音频队列缓冲区中第一个采样的时间(对于简单的录制是不需要的)。
inPacketDesc
字段中包描述的数量,如果是0,表明这是个CBR数据。- 对于压缩数据格式如果需要包描述,包描述是由编码器产生的。
将音频队列缓冲区中的数据写入磁盘
用于录制的音频队列回调函数要做的第一件事情就是把音频队列缓冲区中的内容写入磁盘。这个缓冲区就是音频队列从输入设备最新输入的音频数据。这个回调函数使用AudioFile.h
中声明的AudioFileWritePackets
函数。如清单2-3所示。
清单2-3 将音频队列缓冲区数据写入磁盘
1 | AudioFileWritePackets ( // 1 |
下面是该代码的工作方式:
AudioFileWritePackets
函数(在AudioFile.h
声明),把缓冲区的内容写入音频数据文件中。- 音频文件对象(类型为
AudioFileID
)表示要写到的音频文件。pAqData
变量是指向清单2-1描述的数据结构的指针。 - 使用
false
值来表达写入是函数不应缓存数据。 - 正在写入的音频数据的字节数。
inBuffer
变量音频队列传递给回调函数的音频队列缓冲区。 - 音频数据包描述数组。
NULL
值表示不需要数据包描述(例如,CBR音频数据)。 - 要写入的第一个数据包的索引。
- 输入时,表示要写入的数据包数量。输出时,表示实际写入的数据包数量。
- 将新的音频数据写入音频文件。
排队音频队列缓冲区
现在,音频队列缓冲区的音频数据已经被写入音频文件,回调对缓冲区进行排队,如清单2-4所示。一旦回到缓冲区队列中,缓冲区就处于排队状态,准备接受更多传入的音频数据。
清单2-4 写入磁盘后排队音频队列缓冲区
1 | AudioQueueEnqueueBuffer ( // 1 |
下面是该代码的工作方式:
AudioQueueEnqueueBuffer
函数把音频队列缓冲区添加到音频队列的缓冲区队列中。- 把指定的音频队列缓冲区添加到音频队列。
pAqData
变量指向清单2-1描述的数据结构指针。 - 要排队的音频队列缓冲区。
- 音频队列缓冲区数据中的数据包描述数量。设为
0
,因为该参数未用于录制。 - 数据包描述数组,描述音频队列缓冲区的数据。设为
NULL
,因为该参数未用于录制。
一个完整的音频录制的音频队列回调函数
清单2-5展示了完整的音频录制中音频队列回调函数的基本形式。与本文档的其他代码一样,该清单不包括错误处理。
清单2-5 一个音频录制的音频队列回调函数
1 | static void HandleInputBuffer ( |
下面是该代码的工作方式:
- 实例化时提供给音频队列对象的结构体,包含代表要记录到其中的音频文件的对象,以及各种状态数据。参阅Define a Custom Structure to Manage State。
- 如果音频队列缓冲区包含CBR数据,则需要计算缓冲区的数据包数量。该数值等于缓冲区中数据的总字节除以每个数据包固定的字节数。对于VBR数据,音频队列在调用回调时会提供缓冲区中的数据包数量。
- 把缓冲区的内容写入到音频数据文件中。有关详细的说明,参阅Writing an Audio Queue Buffer to Disk。
- 如果成功写入音频数据,需要增加音频数据文件的数据包索引,以准备写入下一个缓冲区的音频数据。
- 如果音频队列已停止,则返回。
- 入队该写入音频文件的音频队列缓冲区。有关详细的说明,参阅Enqueuing an Audio Queue Buffer。
编写函数计算用于录制的音频队列缓冲区大小
音频队列服务希望程序为使用的音频队列缓冲区指定大小。清单2-6展示了一种执行该操作的方法。它得出的缓冲区大小足以容纳给定的音频时长。
该计算考虑了要录制到的音频数据格式。该格式包含可能影响缓冲区大小的所有因素,例如音频通道的数量。
清单2-6 得出音频录制的音频队列缓冲区的大小
1 | void DeriveBufferSize ( |
下面是该代码的工作方式:
- 配置缓冲区大小的音频队列。
- 音频队列的
AudioStreamBasicDescription
结构体。 - 为每个音频队列缓冲区指定的大小(以秒为单位)。
- 在输出时,每个音频队列缓冲区的大小(以字节为单位)。
- 音频队列缓冲区大小上限(以字节为单位)。在该示例中,上限设为320 KB。这相当于以96 kHz的采样率采集5秒的24位立体声音频。
- 对于CBR音频数据,则从
AudioStreamBasicDescription
结构体中获取固定的数据包大小。使用该值作为最大数据包大小。 该赋值会有副作用,这取决于要录制的音频数据时CBR还是VBR。如果是VBR,则音频队列的AudioStreamBasicDescription
会把bytes-per-packet设为0
。 - 对于VBR音频数据,查询音频队列以获取最大的数据包估算大小。
- 得出缓冲区大小(以字节为单位)。
- 如果需要,把缓冲区大小限制为之前设置的上限。
设置音频文件Magic Cookie
某些压缩的音频格式(例如MPEG 4 AAC),利用结构体包含音频元数据。这些结构体称为magic cookies。使用音频队列服务以这种格式录制时,必须先从音频队列中获取magic cookie,然后再将其添加到音频文件中,然后开始录制。
清单2-7展示了如何从音频队列中获取magic cookie,并将其应用于音频文件中。代码会在录制之前调用该函数,然后在录制后再次调用,因为某些编解码器会在录制停止时更新magic cookie数据。
清单2-7 给音频文件设置magic cookie
1 | OSStatus SetMagicCookieForFile ( |
下面是该代码的工作方式:
- 用于录制的音频队列。
- 录制到的音频文件。
- 结果变量,表达该函数是成功还是失败。
- 用于保存magic cookie数据大小的变量。
- 从音频队列获取magic cookie数据大小,并存储在
cookieSize
变量中。 - 分配一个字节数据来保存magic cookie信息。
- 通过查询音频队列的
kAudioQueueProperty_MagicCookie
属性获取magic cookie。 - 设置录制到的音频文件的magic cookie。
AudioFileSetProperty
函数在AudioFile.h
头文件中声明。 - 释放临时magic cookie变量的内存。
- 返回该函数的成功或失败状态。
设置音频格式进行录制
本节介绍如何为音频队列设置音频数据格式。音频队列使用该格式记录到文件。
要设置音频数据格式,需要指定:
- 音频数据格式类型(如线性PCM、AAC等)
- 采样率(如44.1 kHz)
- 音频通道数(如2,立体声)
- 位深(如16位)
- 每数据包帧数量(如线性PCM,每包一帧)
- 音频文件类型(如CAF、AIFF等)
- 文件类型所需的音频数据格式的详细信息
清单2-8写死了用来录制的音频格式的每个属性值。在生产代码中,通常允许用户部分或全部指定音频格式。无论采用哪种方式,目标都是填充AQRecorderState
自定义结构体的mDataFormat
字段,参阅Define a Custom Structure to Manage State中的自定义结构体。
清单2-8 指定音频队列的音频数据格式
1 | AQRecorderState aqData; // 1 |
下面是该代码的工作方式:
- 创建
AQRecorderState
结构体实例。结构体的mDataFormat
字段包含一个AudioStreamBasicDescription
结构体。在mDataFormat
字段中设置的值提供了音频队列的初始音频格式,这也是记录到文件的音频格式。在清单2-10中,你可以获得音频格式的完整规范,Core Audio根据格式类型和文件类型提供了相关规范。 - 把音频数据格式类型定义为线性PCM。有关可用数据格式的完整列表,可参阅_Core Audio Data Types Reference_。
- 把采样率设为44.1 kHz。
- 通道数设为2。
- 每个通道位深设为16。
- 每个数据包字节数和每帧字节数设为4(即2个通道乘以每个样本2个字节)。
- 每个数据包帧数量设为1。
- 文件类型设为AIFF。参阅
AudioFile.h
头文件的类型,可获得可用类型的完整列表。可以指定任意已安装的解码器的文件类型,如Using Codecs and Audio Data Formats所述。 - 设置指定文件类型所需的格式标志。
创建一个录制音频队列
现在,在设置了录制黑白你函数和音频数据格式之后,创建和配置用于录制的音频队列。
创建录制音频队列
清单2-9展示了如何创建录制音频队列。注意,AudioQueueNewInput
函数使用在之前步骤中配置的回调函数、自定义结构体和音频数据格式。
清单2-9 创建录制音频队列
1 | AudioQueueNewInput ( // 1 |
下面是该代码的工作方式:
AudioQueueNewInput
函数创建一个新的录制音频队列。- 录制的音频数据格式。参阅Set Up an Audio Format for Recording。
- 于录制音频队列一起使用的回调函数。参阅Write a Recording Audio Queue Callback。
- 录制音频队列的自定义数据结构体。参阅Define a Custom Structure to Manage State。
- 调用回调函数的run loop。使用
NULL
指定默认行为,回调函数将在内部的音频队列中的线程执行。这时典型的用法,允许音频队列在程序的用户界面线程等待用户停止录制的同时进行录制。 - run loop模式。通常使用
kCFRunLoopCommonModes
。 - 保留参数,必须为
0
。 - 在输出时,新分配的录制音频队列。
从音频队列获取完整的音频格式
当音频队列创建后(参阅Creating a Recording Audio Queue),AudioStreamBasicDescription
可能比你填写的更完整,尤其是压缩格式。要获取完整的格式描述,调用清单2-10的AudioQueueGetProperty
函数。创建要录制到的音频文件时,需使用完整的音频格式(参阅Create an Audio File)。
清单2-10 从音频队列获取音频格式
1 | UInt32 dataFormatSize = sizeof (aqData.mDataFormat); // 1 |
下面是该代码的工作方式:
- 获取在查询音频队列有关其音频数据格式时要使用的预期属性值大小。
AudioQueueGetProperty
函数获取音频队列中指定属性的值。- 用于获取音频数据格式的音频队列。
- 用于获取音频队列的数据格式值的属性ID。
- 在输出时,从音频队列获得的
AudioStreamBasicDescription
结构体形式的完整音频数据格式。 - 输入时,是
AudioStreamBasicDescription
的预期大小。输出时,是其实际大小。在录制程序中不需要使用该值。
创建音频文件
创建并配置音频队列后,将创建一个音频文件,把音频数据记录到音频文件中,如清单2-11所示。音频文件使用之前存储在音频队列的自定义结构体中的数据格式和文件格式规范。
清单2-11 创建一个音频文件进行录制
1 | CFURLRef audioFileURL = |
下面是该代码的工作方式:
CFURLCreateFromFileSystemRepresentation
函数(在CFURL.h
头文件中声明),创建一个CFURL对象,该对象表示要录制到其中的文件。- 使用
NULL
或kCFAllocatorDefault
,使用当前默认的内存分配器。 - 想要转换为CFURL的文件系统路径。在生产代码中,通常会从用户获取
filePath
值。 - 文件系统路径中的字节数。
false
值表示filePath
代表文件,而不是目录。AudioFileCreateWithURL
函数(来自AudioFile.h
头文件),创建一个新的音频文件,或初始化一个现有文件。- 用于创建新的音频文件或使用现在文件进行初始化的URL。该URL是从第一步
CFURLCreateFromFileSystemRepresentation
获得的。 - 新文件的文件类型。在本章的示例代码中,之前已通过
kAudioFileAIFFType
设置为AIFF类型。参阅Set Up an Audio Format for Recording。 - 将记录到文件中的音频数据格式,指定为
AudioStreamBasicDescription
结构体。在本章的示例代码中,也已在“Set Up an Audio Format for Recording”中进行了设置。 - 如果文件已存在,则删除该文件。
- 在输出时,音频文件对象(
AudioFileID
类型)表示要录制到的音频文件。
设置音频队列缓冲区大小
在准备在录制是使用一组音频队列缓冲区之前,调用之前的DeriveBufferSize
函数(参阅Write a Function to Derive Recording Audio Queue Buffer Size)。可以把该大小分配给正在使用的录制音频队列,如清单2-12所示:
清单2-12 设置音频队列缓冲区大小
1 | DeriveBufferSize ( // 1 |
下面是该代码的工作方式:
DeriveBufferSize
函数(定义在Write a Function to Derive Recording Audio Queue Buffer Size),设置合适的音频队列缓冲区大小。- 配置缓冲区大小的音频队列。
- 在录制的文件的音频数据格式。参阅Set Up an Audio Format for Recording。
- 每个音频队列缓冲区应保留的秒数。此处设置半秒是个不错的选择。
- 在输出时,每个音频队列缓冲区的大小(以字节为单位)。该值放在音频队列的自定义结构体中。
准备一组音频队列缓冲区
现在,请求音频队列(在Create a Recording Audio Queue创建的)准备一组音频队列缓冲区。清单2-13展示了如何操作。
清单2-13 准备一组音频队列缓冲区
1 | for (int i = 0; i < kNumberBuffers; ++i) { // 1 |
下面是该代码的工作方式:
- 遍历分配和入队每个音频队列缓冲区。
AudioQueueAllocateBuffer
函数请求音频队列分配音频队列缓冲区。- 执行分配并持有缓冲区的音频队列。
- 分配的新音频队列缓冲区的大小(以字节为单位)。参阅Write a Function to Derive Recording Audio Queue Buffer Size。
- 在输出时,是新分配的音频队列缓冲区。指向缓冲区的指针放在和音频队列一起使用的自定义结构体中。
AudioQueueEnqueueBuffer
函数把音频队列缓冲区添加到缓冲区队列的末尾。- 向其添加缓冲区的缓冲区队列的音频队列。
- 正在入队的音频队列缓冲区。
- 缓冲区入队时未使用该参数。
- 缓冲区入队时未使用该参数。
录制音频
有了前面的代码,录制过程显得格外简单,如清单2-14所示。
清单2-14 录制音频
1 | aqData.mCurrentPacket = 0; // 1 |
下面是该代码的工作方式:
- 初始化数据包索引为
0
,在音频文件的开头开始录制。 - 在自定义结构体中设置标志,以指示音频队列正在运行。录制音频队列回调函数使用该标志。
AudioQueueStart
函数在其自己的线程上启动音频队列。- 音频队列开始。
- 使用
NULL
表示音频队列应立即开始录制。 AudioQueueStop
函数停止并重置录制音频队列。- 音频队列停止。
- 使用
true
来同步停止。有关同步和异步的说明,参阅Audio Queue Control and State。 - 在自定义结构体中设置标志,以表示音频队列还没运行。
录制后清理
完成录制后,需要处理音频队列并关闭音频文件,如清单2-15所示。
清单2-15 录制后清理
1 | AudioQueueDispose ( // 1 |
下面是该代码的工作方式:
AudioQueueDispose
函数处理音频队列及其所有资源,包括缓冲区。- 要处理的音频队列。
- 使用
true
来同步(即立即)处理音频队列。 - 关闭用于录制的音频文件。
AudioFileClose
函数在AudioFile.h
头文件中声明。
总结
- 使用录制功能一般步骤:
- 定义自定义结构体来管理状态、格式以及路径信息等。
- 编写回调函数来执行实际的录制数据处理。
- 填充自定义结构体中的各个字段,包括录制到的文件的数据流、文件路径。
- 创建音频队列、音频队列缓冲区、要写入的文件。
- 通知音频队列开始录制。
- 录制完毕后,通知音频队列停止录制,然后释放。这同时会释放它所拥有的所有缓冲区。
- 对于使用OC、Swift代码,可以直接把结构体的字段直接分散到类定义中。
- 录制回调函数任务:
- 把填充进音频队列缓冲区的内容写入到文件。
- 把写入文件的音频队列缓冲区排队到队列中。这样才可以接受更多数据。
- 回调函数中的
const AudioStreamPacketDescription *inPacketDesc
包含VBR包描述的数量(mVariableFramesInPacket
),如果是0则表示这是CBR数据。该数据来自编码器。 - 每个音频队列缓冲区时长可以设置为0.5秒。