介绍
本文档介绍了如何使用音频队列服务(Audio Queue Services),这是Core Audio Toolbox框架中的一个C语言编程接口。
什么是音频队列服务(Audio Queue Services)
在iOS和Mac OS X中,音频队列服务提供了一种直接、低开销的的方式来录制和播放音频。这也是向你的iOS和Mac OS X程序中添加录制和播放功能所推荐使用的技术。
音频队列服务允许你录制和播放以下格式的音频:
- 线性PCM(Linear PCM)。
- 任何你正在进行开发的苹果平台所原生支持的压缩格式。
- 任何用户已经安装相应编码器的其他格式。
音频队列服务是高级的。它让你的应用程序使用录音和播放设备(比如麦克风和扬声器)而不需要了解硬件接口的知识。也可以让你使用复杂的编码器而不用了解编码器的工作机制。
同时,音频队列服务也支持一些高级功能。它提供了高精度的时间控制来支持播放进度和同步。你可以使用它来同步多个音频队列以及让视频和音频同步。
注意:音频队列服务提供了一些类似于之前在Mac OS X中Sound Manager提供的功能,它附加了例如同步的功能,Sound Manager在Mac OS X10.5中已经废弃了,并且不能和64位的程序一起工作,苹果建议新的Mac OS X程序使用音频队列服务并且将旧的程序用音频队列服务来替换Sound Manager。 |
音频队列服务是纯C接口的,你可以把它使用在Cocoa和Mac OS X命令行工具中,为了使你更加专注于音频队列服务,本文档中的示例代码通过使用Core Audio SDK中的C++类进行了简化,然而,无论是这个SDK还是C++语言都不是使用音频队列服务所必需的。
关于音频队列(Audio Queues)
本章你将学习到音频队列的功能、架构和内部工作原理。我们将向你介绍音频队列用来播放或录制所用的音频队列(audio queues)、音频队列缓冲区(audio queue buffers)和回调函数(callback functions),你还可以找到关于音频队列状态和参数的信息,截至到本章的结尾,你将会获得有效使用该技术的概念性理解。
什么是音频队列?
在iOS和Mac OS X中,音频队列是一个用来录制和播放音频的软件对象,他用AudioQueueRef
这个不透明数据类型来表示,该类型在AudioQueue.h
头文件中声明。
音频队列完成以下工作:
-
连接音频硬件
-
内存管理
-
根据需要为已压缩的音频格式引入编码器
-
媒体的录制或播放
你可以将音频队列配合其他Core Audio的接口使用,再加上相对少量的自定义代码就可以在你的应用程序中创建一套完整的数字音频录制或播放解决方案。
音频队列架构
所有的音频队列都含有相同的基础结构,包含以下几部分:
-
一组音频队列缓冲区(audio queue buffers),每个音频队列缓冲区都是一个存储音频数据的临时仓库
-
一个缓冲区队列(buffer queue),一个包含音频队列缓冲区的有序列表
-
一个你自己编写的音频队列回调函数(audio queue callback)
用来录制的音频队列
录制用音频队列,用AudioQueueNewInput
函数创建,拥有如图1-1的结构
图示 1-1 录制用音频队列
![A recording audio queue](https://img-my.csdn.net/uploads/201211/19/1353330338_4038.png)
录制用音频队列的输入端一般连接到外部的音频硬件上,比如说麦克风。在iOS中,音频来自于由用户连接的设备--内置的麦克风或者耳机麦克风,比如说,在Mac OS X默认情况下,音频来自于由用户在系统首选项中设置的系统默认音频输入设备。
录制用音频队列的输入端利用了你自己写的回调函数,当录制音频到磁盘上的时候,回调函数将存有从音频队列中接收到的新的音频数据的缓冲区写入到音频文件中。然而,录制用音频队列也可以用其他方法来使用。你也可以使用其中一种,比如说,在一个实时的分析仪中,在这种情况下,你的回调函数会直接向你的应用程序提供音频数据,而不是将它写入磁盘。
你将在“记录用音频队列回调函数”中学到更多关于这个回调的知识。
每一个音频队列——无论是录制用还是播放用——都有一个或多个音频队列缓冲区。这些缓冲区排列在一个特殊的被称为缓冲区队列(buffer queue)的序列中。如图所示,音频队列缓冲区是按照他们被填充的顺序编号的——这也是和把他们交付给回调函数的顺序是相同的。你将在“缓冲区队列和入队”中学到如何音频队列是如何使用它的缓冲区的。
用来播放的音频队列
播放用音频队列(用AudioQueueNewOutput
函数创建)拥有如图1-2所示的结构
图示 1-2 播放用音频队列
![A playback audio queue](https://img-my.csdn.net/uploads/201211/19/1353333082_4508.png)
在播放用音频队列中,回调函数是在输入端的,这个回调函数的职责就是从磁盘(或其他来源)中获取音频数据,然后将它交付给音频队列。当没有更多音频数据需要播放的时候告诉音频队列停止,你将在“播放用音频队列回调函数”中学到更多关于这个回调函数的知识。
播放用音频队列的输出端一般都是连接到外部的音频设备的,比如说扬声器。在iOS中,音频通过用户选择的设备播放——比如说,接受者是耳机。在Mac OS X中,默认情况下,音频会通过用户在系统首选项中设置的默认音频输出设备中输出。
音频队列缓冲区
音频队列缓冲区(audio queue buffer)是一个AudioQueueBuffer
类型的数据结构,声明于AudioQueue.h
头文件。
- <span style="font-size:12px;">typedef struct AudioQueueBuffer {
- const UInt32 mAudioDataBytesCapacity;
- <strong>void *const mAudioData;</strong>
- UInt32 mAudioDataByteSize;
- void *mUserData;
- } AudioQueueBuffer;
- typedef AudioQueueBuffer *AudioQueueBufferRef;</span>
mAudioData
域,指向了缓冲区本身:一个用来当作暂时存放录制或播放音频数据的容器的内存,其他域中的数据用来辅助音频队列管理这个缓冲区。
音频队列可以使用任意数量的缓冲区。你的应用程序制定它的数量。一般情况下这个数字是3。这样就可以让给一个忙于将数据写入磁盘,同时另一个在填充新的音频数据,第三个缓冲区在需要做磁盘I/O延迟补偿的时候可用。图示1-3演示了这个过程。
音频队列负责对它的缓冲区进行内存管理
-
当你调用
AudioQueueAllocateBuffer
函数的时候音频队列创建了一个缓冲区 -
当你通过调用
AudioQueueDispose
函数释放一个音频队列的时候,这个音频队列释放掉它拥有的缓冲区
关于AudioQueueBuffer
数据结构的完整描述,请参照音频队列服务参考(Audio Queue Services Reference)
缓冲区队列和入队
传递给音频队列的缓冲区队列,就如它的名字一样,就是事实上的音频队列服务(Audio Queue Services),你见过缓冲区队列——一个缓冲区的有序列表——在“音频队列架构”中你学到了音频队列对象如何配合回调函数在录制或播放的过程中管理缓冲区队列。特别的,你将学习到入队(enqueuing),缓冲区队列对音频队列缓冲区的附加操作。无论你正在实现录制或者播放,入队都是你在回调函数中需要执行的任务。
录制过程
当进行录制的时候,一个音频队列缓冲区被填充了从列入例如麦克风的输入设备种获取的音频数据。缓冲区队列中的其他缓冲区将在当前缓冲区的后面依次排队等待被填充音频数据。
音频队列将按照缓冲区填充的顺序把已填充过音频数据的缓冲区交付给你的回调函数。图示1-3演示了当使用音频队列的时候这个录制过程是如何工作的。
图示 1-3 录制过程
在图示1-3中的第一步,录制开始,音频队列用获取到的数据填充缓冲区。
第二步,第一个缓冲区填充完毕,音频队列调用回调函数来处理这个被填充满的缓冲区(缓冲区一)。回调函数(第三步)将缓冲区的内容写到音频文件中。同时,音频队列将另一个缓冲区(缓冲区二)填充新获取到的数据。
在第四步,回调函数将刚刚写入磁盘的缓冲区(缓冲区一)入队,使它重新重新回到被填充的队列。音频队列再一次调用回调函数(第五步),处理下一个填充完毕的缓冲区(缓冲区二)。回调函数(第六步)将这个缓冲区的内容写入到音频文件。这种稳定状态会一直持续到用户停止录制。
播放过程
当进行播放的时候,音频队列缓冲区将被传送到像扬声器这样的输出设备。缓冲区队列中其他的缓冲区讲按顺序排在当前缓冲区后面等待播放。
音频队列将已经播放过的音频数据按照他们播放的顺序交付给你的回调函数,回调函数将新的音频数据读取到一个缓冲区中,然后将它入队。图示1-4演示了当使用音频队列时播放是如何工作的
图示 1-4 播放过程
图示1-4中的第一步,应用程序启动播放用音频队列,应用程序对每一个音频队列缓冲区调用回调函数,填充这些缓冲区并且将它们加入缓冲区队列。启动操作会确保播放可以立即执行当你的应用程序调用AudioQueueStart
函数之后。
在第三步,音频队列将第一个缓冲区(缓冲区一)交付给输出。
当第一个缓冲区被播放完毕之后,播放用音频队列就进入了一个稳定的循环状态。音频队列开始播放下一个缓冲区(第四步,缓冲区二)然后调用回调函数(第五步),处理刚刚播放完的那个缓冲区(缓冲区一)。这个回调函数(第六步)从音频文件中读取数据填充缓冲区然后将他们入队用于播放。
控制播放过程
音频队列缓冲区总是按照他们入队的顺序进行播放,然而,在播放过程中,音频队列服务为你提供了AudioQueueEnqueueBufferWithParameters
函数来进行一些控制,这个函数有以下功能:
-
设置缓冲区的精确播放时间,这可以让你支持同步
-
截断音频队列缓冲区开头或结尾的帧(frame),这可以让你移除开头或结尾的静音
-
在缓冲区的粒度上设置播放增益
音频队列回调函数
一般来说,使用音频队列服务的大部分编程任务都在编程音频队列回调函数上。
在录制或播放过程中,音频队列将反复的调用它所拥有的音频队列回调函数。调用的时间间隔取决于音频队列缓冲区的容量,并且一般来一说这个时间在半秒或者几秒。
无论对于录制或者播放,音频队列回调的一个职责就是返回一个缓冲区队列的音频队列缓冲区。回调函数使用AudioQueueEnqueueBuffer
函数将一个缓冲区加入到缓冲区队列的末尾。对于播放来说,你也可以使用AudioQueueEnqueueBufferWithParameters
函数来获得更多的控制,就像“控制播放过程”中描述的一样。
录制用音频队列回调函数
本节介绍了一般情况下——将音频录制到磁盘上,这种情况的回调函数。这里是这个录制用回调函数的原型,就和AudioQueue.h
头文件中声明的一样:
- AudioQueueInputCallback (
- void *inUserData,
- AudioQueueRef inAQ,
- AudioQueueBufferRef inBuffer,
- const AudioTimeStamp *inStartTime,
- UInt32 inNumberPacketDescriptions,
- const AudioStreamPacketDescription *inPacketDescs
- );
-
inUserData,通常是一个你创建用来保存音频队列和它的缓冲区状态信息的自定义结构,一个音频文件对象 (
AudioFileID类型
)代表你正在写入的文件,还有这个文件的音频格式信息。 -
inAQ 是调用回调函数的音频队列
-
inBuffer 是一个被音频队列填充新的音频数据的音频队列缓冲区,它包含了回调函数写入文件所需要的新数据。. 数据已经根据你在自己指定的自定义结构(由inUserData参数传入)中指定的格式格式化。关于此点的更多信息,请参照“使用编码器和音频数据格式”
-
inStartTime 是缓冲区中的一采样的参考时间,对于基本的录制,你的毁掉函数不会使用这个参数
-
inNumberPacketDescriptions 是
inPacketDescs
参数中包描述符(packet descriptions)的数量,如果你正在录制一个VBR(可变比特率(variable bitrate))格式, 音频队列将会提供这个参数给你的回调函数,这个参数可以让你传递给AudioFileWritePackets
函数. CBR (常量比特率(constant bitrate)) 格式不使用包描述符。对于CBR录制,音频队列会设置这个参数并且将inPacketDescs这个参数设置为NULL -
inPacketDescs 是一组对应于缓冲区中采样的包描述符,音频队列提供了这个参数的值,如果音频文件是VBR格式的,你的回调函数可以将这个值传递给
AudioFileWritePackets
函数(声明于AudioFile.h
头文件中)
如果要了解更多关于录制用回调函数的信息,请参照本文档的 “录制音频” ,并且参照音频队列服务参考(Audio Queue Services Reference.)
播放用音频队列回调函数
本节介绍了一般情况下——从磁盘文件播放音频,这种情况的回调函数。 这里是这个播放用回调函数的原型,就和AudioQueue.h
头文件中声明的一样:
- AudioQueueOutputCallback (
- void *inUserData,
- AudioQueueRef inAQ,
- AudioQueueBufferRef inBuffer
- );
播放用音频队列,在调用回调函数的时候,提供了回调函数将下一组音频数据进行读取进行播放的信息。
-
inUserData域,一般来说是一个你创建的包含音频队列和它的缓冲区的的状态信息的自定义结构,一个音频文件对象 (
AudioFileID类型
) 代表了你要写入的文件和文件的音频数据格式信息。在播放音频队列的情况下,回调函数会在这个结构中用一个域保持对当前包的索引
-
inAQ域是调用这个回调函数的音频队列
-
inBuffer域是一个音频队列缓冲区,是一个有音频队列变成可用状态的音频队列缓冲区,你的回调函数将把它填充上下一组要进行播放的音频数据。
AudioFileReadPackets
函数来完成这个任务,这个函数声明于
AudioFile.h
头文件,回调函数随后将包信息放到自定义的数据结构中以使得它对播放用音频队列可用。
关于播放回调的更多信息,请看本文档的“播放音频”(Playing Audio),并且参照音频队列服务参考(Audio Queue Services Reference.)
使用编码和音频数据格式
音频队列服务根据在不同的音频格式之间转换的时候会根据需要使用编码器(音频数据编码/解码组件)。你的录制或播放程序可以使用任意已经安装过相应编码器的格式,不需要写自定义的代码来处理各种各样的音频格式。特别的,你的回调函数不需要知道数据格式。
现在来讲解一下这是如何工作的,每一个音频队列在AudioStreamBasicDescription
结构中都有一个域代表了音频数据格式。当你在mFormatID
域中指定了它的格式的时候——音频队列会使用相应的解码器。然后你指定采样率和声道数,这些就是所有你需要做的。你将会在“录制音频”和“播放音频”中看到如何设置音频数据格式的示例。
录制用音频队列按照图示1-5中的流程使用已安装的编码器。
![Using a code when recording with an audio queue](https://img-my.csdn.net/uploads/201211/23/1353641122_3816.png)
在图示1-5中的第一步,你的应用程序告诉音频队列开始录制,同时也告诉它所要使用的音频格式。在第二部,音频队列获取新的音频数据,并且根据你指定的格式使用相应的编码器转换音频数据。然后音频队列调用回调函数,将适当的格式化过的音频数据放进缓冲区中。第三步,回调函数将格式化后的音频数据写入磁盘。再次,你的回调函数不需要了解数据格式。
播放用音频队列按照图示1-6的流程使用已安装的编码器。
图示 1-6 在播放过程中进行音频格式转换
在图示1-6的第一步中,你的应用程序告诉音频队列开始播放,同时也告诉了它将要播放放的音频文件的数据格式。在第二步,音频队列调用回调函数来从音频文件中读取音频数据。回调函数按照它的原始格式将音频数据交付给音频队列。在第三步,音频队列使用对应的解码器将音频交付给目标输出。
音频队列可以使用任意已安装的编码器,无论是Mac OS X原生的还是第三方的。你可以通过指定音频队列的AudioStreamBasicDescription
结构中四字节的编码ID来指定将要使用的编码器。你将会在“录制音频”中看到这个字段的使用示例。
Mac OS X包含大量的编码器,他们都在CoreAudioTypes.h
头文件中的format IDs枚举值中列出了,并且记录在核心音频数据类型参考(Core Audio Data Types Reference)中。你可以通过调用AudioFormat.h
头文件中的接口来查询当前系统可用的编码器。在Audio Toolbox框架中。你可以使用Fiendishthngs应用程序来显示系统的编码器,相应的示例代码在网址http://developer.apple.com/samplecode/Fiendishthngs/.
音频队列的控制和状态
音频队列的生命周期在创建和处理之间。你的应用程序管理它的生命周期——并且控制音频队列的状态——通过使用AudioQueue.h
头文件中的六个函数:
-
Start(
AudioQueueStart
). 调用它来初始化录制或者播放 -
Prime (
AudioQueuePrime
). 对于播放, 在调用AudioQueueStart
之前调用这个函数,用来确定音频队列中立刻就有可用的数据来播放。这个函数不在录制中使用 -
Stop(
AudioQueueStop
).调用这个函数来重置音频队列 (参考下面对AudioQueueReset
的描述),然后停止录制或播放。一个播放用音频队列回调函数当它没有更多的数据播放的时候会调用这个函数
-
Pause(
AudioQueuePause
).调用这个函数可以在不影响缓冲区和不重置音频队列的情况下停止录制或播放。如果需要恢复,调用AudioQueueStart
函数 -
Flush (
AudioQueueFlush
). 在将最后一个音频队列缓冲区入队之后调用,来确保所有缓存过的数据,也包括处理的中间数据,得到录制或播放 -
Reset (
AudioQueueReset
). 调用这个函数可以立即让音频队列静音。移除之前调度过的缓冲区,并且重置所有解码器和DSP状态
AudioQueueStop
函数:
-
同步立刻停止,不考虑之前缓冲的音频数据
-
异步在所有已入队的缓冲区播放或录制完毕之后再停止
参照 Audio Queue Services Reference 来获取所有这些函数的完整描述,也包含了更多关于同步和异步停止音频队列的信息
音频队列参数(Audio Queue Parameters)
音频队列有一个称作参数(parameters)的可调整设置。每个参数都有一个枚举值作为它的键,一个浮点数作为它的值。参数一般于播放,不用于录制。
在Mac OS X10.5中,只有一个音频队列参数就是播放增益。可以通过使用kAudioQueueParam_Volume
常量来获取或设置它的值,它的有效范围在0.0(静音)到1.0(单位增益)
你的应用程序可以通过两种方法来设置音频队列参数:
-
对于每一个音频队列,使用
AudioQueueSetParameter
函数,这可以让你直接改变音频队列的设置,这个改变是立刻生效的 -
对于每一个音频队列缓冲区,调用
AudioQueueEnqueueBufferWithParameters
函数。这可以让你在将音频队列缓冲区入队的时候设置音频队列设置。这种改变只会在播放这个音频队列缓冲区的时候生效。
这两种情况下,音频队列的参数设置会一直保留到你改变它们为止。
你可以通过调用AudioQueueGetParameter
函数来获取音频队列当前的参数。参考Audio Queue Services Reference 来获得这个函数的完整描述和获取和设置参数值的方法
音频录制
注意: 本章介绍了基于ANSI-C的录制的实现,并且使用了MAC OS X中Core Audio SDK中了一些C++类,如果想了解基于Objective-C的例子,请参考iOS Dev Center中的SpeakHere 例子。 |
- 定义一个自定义的结构来管理状态、格式以及路径信息等。
- 编写音频队列回调函数来执行实际的录制工作。
- 可选的,你可以编写代码来为音频队列缓冲区选择一个合适的大小。如果你将要录制的格式使用了magic cookies,你需要编写相应的代码来配合使用。
- 填充自定义结构中的各个域,包括制定音频队列将要录制到的文件的数据流、还有这个文件的路径。
- 创建一个录制用的音频队列并且让音频队列创建一系列的音频队列缓冲区,同时创建一个它将要写入的文件。
- 通知音频队列开始录制音频。
- 录制完毕之后,通知音频队列停止录制,然后释放掉它,同时它会释放掉它所拥有的缓冲区。
定义一个用来管理状态的自定义结构
使用音频队列服务来开发一个音频录制解决方案的时候,第一步就是定义一个自定义的结构。你将使用这个结构来管理音频格式和音频队列状态信息。清单2-1演示了这个这样的一个结构。
清单 2-1 一个录制用音频队列的自定义结构
- static const int kNumberBuffers = 3; // 1
- struct AQRecorderState {
- AudioStreamBasicDescription mDataFormat; // 2
- AudioQueueRef mQueue; // 3
- AudioQueueBufferRef mBuffers[kNumberBuffers]; // 4
- AudioFileID mAudioFile; // 5
- UInt32 bufferByteSize; // 6
- SInt64 mCurrentPacket; // 7
- bool mIsRunning; // 8
- };
下面是这个结构体中每个域的说明:
- 设置要使用的音频队列缓冲区的数量。
- 一个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 X10.5中要使用the
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 录制用音频队列回调函数声明
- static void HandleInputBuffer (
- void *aqData, // 1
- AudioQueueRef inAQ, // 2
- AudioQueueBufferRef inBuffer, // 3
- const AudioTimeStamp *inStartTime, // 4
- UInt32 inNumPackets, // 5
- const AudioStreamPacketDescription *inPacketDesc // 6
- )
下面就是这段代码如何工作的:
- 一般来说,
aqData
是一个自定义的数据结构,他包含了音频队列的状态信息,就像“Define a Custom Structure to Manage State.”中的一样。 - 拥有这个回调函数的音频队列
- 包含录制数据的音频队列缓冲区
- 音频队列缓冲区中第一个采样的的时间(对于简单的录制,这个是不需要的)
- inPacketDesc域中packet descriptions的数量,如果是0,表明这是个CBR数据
- 对于压缩数据格式如果需要packet descriptions,这个packet descriptions是由编码器产生的
将音频队列缓冲区中的数据写入磁盘
录制用音频队列回调函数要做的第一件事情就是讲音频队列缓冲区中的内容写入磁盘。这个缓冲区就是音频队列从输入设备最新输入的音频数据。这个回调函数使用AudioFile.h头文件中声明的AudioFileWritePackets
函。如列表2-3所示
列表2-3 将音频队列缓冲区数据写入磁盘
- AudioFileWritePackets ( // 1
- pAqData->mAudioFile, // 2
- false, // 3
- inBuffer->mAudioDataByteSize, // 4
- inPacketDesc, // 5
- pAqData->mCurrentPacket, // 6
- &inNumPackets, // 7
- inBuffer->mAudioData // 8
- );