概述
Audio Queue Service是Core Audio的Audio Toolbox框架中的基于C语言的一套接口。在iOS和Mac OS X中,Audio Queue Service(音频队列服务)提供了一种直接、低开销的的方式来录制和播放音频。这也是向你的iOS和Mac OS X程序中添加录制和播放功能所推荐使用的技术。
使用场景介绍
相对于上层API(例如:AVAudioRecorder和AVAudioPlayer)而言,使用Audio Queue可以直接获取每一帧音频数据,因此可以对音频帧做一些需要的处理。 但是无法对声音做一些更加精细的处理,如回声消除、混音、降噪等等,如果需要做更底层的操作,则需要使用Audio Unit。
Audio Queue Services它不仅可以在无需了解硬件的基础上使程序与音频硬件(麦克风、扬声器等)之间完成交互,也在无需了解编解码器的原理情况下让我们使用复杂的编解码器。同时,Audio Queue Services还提供了更加精细的定时控制以支持预定的播放与同步任务。可以使用它同步多个音频播放队列或者音视频间进行同步。
音频队列服务允许你录制和播放以下格式的音频:
- 线性PCM(Linear PCM)。
- 任何你正在进行开发的苹果平台所原生支持的压缩格式。
- 任何用户已经安装相应编码器的其他格式。
1.什么是Audio Queues?
在iOS和Mac OS X中,audio queue是一个用来录制和播放音频的软件对象,他用AudioQueueRef这个不透明数据类型来表示,该类型在AudioQueue.h头文件中声明。
Audio Queues作用是什么?
- 连接音频硬件
- 管理相关模块内存
- 根据需要为已压缩的音频格式引入编码器
- 媒体的录制或播放
1.1 Audio Queues架构
所有的音频队列都含有相同的基础结构,包含以下几部分:
-
一组音频队列缓冲区(audio queue buffers):每个音频队列缓冲区都是一个存储音频数据的临时仓库
-
一个缓冲区队列(buffer queue):一个包含音频队列缓冲区的有序列表
-
一个音频队列回调函数(audio queue callback)
它的架构很大程度上依赖于这个音频队列是用来录制还是用来播放的。不同之处在于音频队列如何连接到它的输入和输入,还有它的回调函数所扮演的角色。
1.2 录制
如果要使用audio queue的录制功能,通过AudioQueueNewInput创建录音队列。
录制使用的audio queue的输入端一般连接到外部的音频硬件上,比如说麦克风。在iOS中,音频来自于由用户连接的设备--内置的麦克风或者耳机麦克风。在Mac OS X默认情况下,音频来自于由用户在系统首选项中设置的系统默认音频输入设备。
当硬件设备有音频输出时,录制使用的audio queue的会使用回调函数将存有从音频队列中接收到的新的音频数据的缓冲区返回。你可以将得到的数据写入音频文件或者直播播放。
每一个audio queue无论是录制用还是播放用都有一个或多个音频队列缓冲区。这些缓冲区排列在一个特殊的被称为缓冲区队列(buffer queue)的序列中。如上图所示,音频队列缓冲区是按照他们被填充的顺序编号的,它们最终也将在回调函数中按顺序取出。
1.3 播放
如果要使用audio queue的播放功能,通过AudioQueueNewOutput创建播放队列对象。
在播放使用的audio queue中,回调函数是在输入端的,这个回调函数的职责就是从磁盘(或其他来源)中获取音频数据,然后将它交付给音频队列。当没有更多音频数据需要播放的时候告诉音频队列停止。
播放用音频队列的输出端一般都是连接到外部的音频设备的,比如说扬声器。在iOS中,音频通过用户选择的设备播放。比如说耳机。在Mac OS X中,默认情况下,音频会通过用户在系统首选项中设置的默认音频输出设备中输出。
1.4 音频队列缓冲区
音频队列缓冲区(audio queue buffer)是一个[AudioQueueBuffer](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/c/tdef/AudioQueueBuffer)类型的数据结构,声明于AudioQueue.h头文件。
typedef struct AudioQueueBuffer {
const UInt32 mAudioDataBytesCapacity;
void *const mAudioData;
UInt32 mAudioDataByteSize;
void *mUserData;
} AudioQueueBuffer;
typedef AudioQueueBuffer *AudioQueueBufferRef;
mAudioData: 当前取出的队列中存放的即时的音频数据指针,它指向真正存放音频数据的内存地址.mAudioDataBytesCapacity: 当前音频数据最大存储空间mAudioDataByteSize: 当前存储的音频数据实际的大小mUserData: 开发者可以存放一些自定义的数据
音频队列可以使用任意数量的缓冲区。你的应用程序自定义它的数量。一般情况下是3个。这样就可以让给一个忙于将数据写入磁盘,同时另一个在填充新的音频数据,第三个缓冲区在需要做磁盘I/O延迟补偿的时候可用。
因为Audio Queue的纯C函数,内存需要我们手动管理.
- 初始化Audio Queue时使用
AudioQueueAllocateBuffer分配内存 - 回调函数中用完时使用
AudioQueueDispose回收内存
通过内存管理,可以使录制播放更加稳定,同时优化App资源使用。
1.5. 音频队列与入队
buffer queue: 一个缓冲区队列
audio queue buffers:音频队列缓冲区中存放一个或多个结点数据
- 录制过程
当音频录制的时候,一个音频队列的缓冲区(audio queue buffers)将会被填充从输入设备(如:麦克风)采集的音频数据,而其他audio queue buffers将会一次排队等候被填充。音频队列将按照缓冲区填充的顺序将已填充过音频数据的缓冲区交付给你的回调函数,通过回调函数你就会拿到缓冲区填充的音频数据。下图展示了音频录制的工作流程:
图一:录制开始,音频队列用获取到的数据填充缓冲区。
图二:第一个缓冲区填充完毕,音频队列调用回调函数来处理这个被填充满的缓冲区(缓冲区一)。回调函数(第三步)将缓冲区的内容写到音频文件中。同时,音频队列将另一个缓冲区(缓冲区二)填充新获取到的数据。
图三:回调函数将刚刚写入磁盘的缓冲区(缓冲区一)入队,使它重新重新回到被填充的队列。音频队列再一次调用回调函数(第五步),处理下一个填充完毕的缓冲区(缓冲区二)。回调函数(第六步)将这个缓冲区的内容写入到音频文件。这种稳定状态会一直持续到用户停止录制。
2.播放过程
当进行播放的时候,音频队列缓冲区将被传送到像扬声器这样的输出设备。缓冲区队列中其他的缓冲区讲按顺序排在当前缓冲区后面等待播放。
音频队列将需要播放的音频数据按照他们播放的顺序交付给你的回调函数,回调函数将新的音频数据读取到一个缓冲区中,然后将它入队。下图展示了音频播放的工作流程:
图一:应用程序启动播放用音频队列,应用程序对每一个音频队列缓冲区调用回调函数,填充这些缓冲区并且将它们加入缓冲区队列。启动操作会确保播放可以立即执行当你的应用程序调用[AudioQueueStart](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/c/func/AudioQueueStart)函数之后。
图二:音频队列将第一个缓冲区(缓冲区一)交付给输出。
图三:当第一个缓冲区被播放完毕之后,播放用音频队列就进入了一个稳定的循环状态。音频队列开始播放下一个缓冲区(第四步,缓冲区二)然后调用回调函数(第五步),处理刚刚播放完的那个缓冲区(缓冲区一)。这个回调函数(第六步)从音频文件中读取数据填充缓冲区然后将他们入队用于播放直到音频队列停止
3. 控制播放的过程
音频队列缓冲区总是按照他们入队的顺序进行播放,然而在播放过程中,音频队列服务为你提供了[AudioQueueEnqueueBufferWithParameters](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/c/func/AudioQueueEnqueueBufferWithParameters)函数来进行一些控制,这个函数有以下功能:
-
设置缓冲区的精确播放时间,这可以让你支持同步
-
截断音频队列缓冲区开头或结尾的帧(frame),这可以让你移除开头或结尾的静音
-
在缓冲区的粒度上设置播放增益
1.6 回调函数
一般来说,我们使用音频队列服务时,大部分需要特殊处理的逻辑都在音频队列回调函数上。在录制或播放过程中,音频队列将反复的调用它所拥有的音频队列回调函数。调用的时间间隔取决于音频队列缓冲区的容量,并且一般来一说这个时间在半秒或者几秒。
无论对于录制或者播放,音频队列回调的一个职责就是返回一个缓冲区队列的音频队列缓冲区。回调函数使用[AudioQueueEnqueueBuffer](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/c/func/AudioQueueEnqueueBuffer)函数将一个缓冲区加入到缓冲区队列的末尾。对于播放来说,你也可以使用[AudioQueueEnqueueBufferWithParameters](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/c/func/AudioQueueEnqueueBufferWithParameters)函数来获得更多的控制,就像“控制播放过程”中描述的一样。
1.6.1 录制用音频队列回调函数
AudioQueueInputCallback (
void *inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp *inStartTime,
UInt32 inNumberPacketDescriptions,
const AudioStreamPacketDescription *inPacketDescs
);
当输入端采集到音频数据时就会触发回调,可以从回调函数中取出装有音频数据的audio queue buffer.
- inUserData:自定义的数据,开发者可以传入一些我们需要的数据供回调函数使用。注意:一般情况下我们需要将当前的OC类实例传入,因为回调函数是纯C语言,不能调用OC类中的属性与方法,所以传入OC实例以与本类中属性方法交互。
- inAQ:调用回调函数的音频队列。
- inBuffer:装有音频数据的audio queue buffer。
- inStartTime:当前音频数据的时间戳,主要用于同步。
- inNumberPacketDescriptions:数据包描述参数,如果你正在录制VBR格式,音频队列会提供此参数的值。如果录制文件需要将其传递给
AudioFileWritePackets函数。CBR格式不使用此参数。 - inPacketDescs:音频数据中一组packet描述,如果是VBR格式数据,如果录制文件需要将此值传递给
AudioFileWritePackets函数。
1.6.2. 播放的回调函数
AudioQueueOutputCallback (
void *inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer
);
在回调函数中将读取音频数据以用来播放
- inUserData:自定义的数据,开发者可以传入一些我们需要的数据供回调函数使用。注意:一般情况下我们需要将当前的OC类实例传入,因为回调函数是纯C语言,不能调用OC类中的属性与方法,所以传入OC实例以与本类中属性方法交互。
- inAQ:调用回调函数的音频队列。
- inBuffer:回调将要填充的数据。
如果应用程序正在播放VBR格式数据,这个回调函数需要通过AudioFileReadPackets获取音频数据包信息.然后,回调将数据包信息放入自定义数据结构中,以使其可用于播放音频队列。
1.7 使用编码和音频数据格式
Audio Queue Services可以使用编码器,编码成各种需要的音频数据格式。
每个audio queue有一个自己的音频数据格式,被封装在AudioStreamBasicDescription中,通过mFormatID可以指定音频数据格式,audio queue会自动选择适当编解码器对其进行压缩。开发者可以指定采样率,声道数等等参数自定义音频数据。
上图第一步,你的应用程序告诉音频队列开始录制,同时告诉它所要使用的音频格式为PCM。
第二步,音频队列获取新的音频数据,并且根据你指定的格式使用相应的编码器转换音频数据。然后音频队列调用回调函数,将适当的格式化过的音频数据放进缓冲区中。
第三步,回调函数将格式化后的音频数据写入磁盘。
上图第一步,你的应用程序告诉音频队列开始播放,同时也告诉了它将要播放放的音频文件的数据格式(PCM)。
第二步,音频队列调用回调函数来从音频文件中读取音频数据。回调函数按照它的原始格式将音频数据交付给音频队列。
第三步,音频队列使用对应的解码器将音频交付给目标输出。
1.8 音频队列的控制和状态
音频队列的生命周期通过AudioQueue.h头文件中的六个函数控制
-
Start(
[AudioQueueStart](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/c/func/AudioQueueStart)):调用它来初始化录制或者播放 -
Prime (
[AudioQueuePrime](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/c/func/AudioQueuePrime)):对于播放,,在调用[AudioQueueStart](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/doc/c_ref/AudioQueueStart)之前调用这个函数,用来确定音频队列中立刻就有可用的数据来播放。这个函数不在录制中使用 -
Stop(
[AudioQueueStop](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/c/func/AudioQueueStop)):调用这个函数来重置音频队列 ,然后停止录制或播放。一个播放用音频队列回调函数当它没有更多的数据播放的时候会调用这个函数 -
Pause(
[AudioQueuePause](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/c/func/AudioQueuePause)).调用这个函数可以在不影响缓冲区和不重置音频队列的情况下停止录制或播放。如果需要恢复,调用[AudioQueueStart](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/doc/c_ref/AudioQueueStart)函数 -
Flush (
[AudioQueueFlush](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/c/func/AudioQueueFlush)). 在将最后一个音频队列缓冲区入队之后调用,来确保所有缓存过的数据,也包括处理的中间数据,得到录制或播放 -
Reset (
[AudioQueueReset](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/c/func/AudioQueueReset)). 调用这个函数可以立即让音频队列静音。移除之前调度过的缓冲区,并且重置所有解码器和DSP状态
你可以在同步或异步模式下使用 [AudioQueueStop](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/doc/c_ref/AudioQueueStop) 函数:
-
同步立刻停止,不考虑之前缓冲的音频数据
-
异步在所有已入队的缓冲区播放或录制完毕之后再停止
1.9 参数设置
音频队列有一个称作参数**(parameters)**的可调整设置。每个参数都有一个枚举值作为它的键,一个浮点数作为它的值。参数一般于播放,不用于录制。
在Mac OS X10.5中,只有一个音频队列参数就是播放增益。可以通过使用[kAudioQueueParam_Volume](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/c/econst/kAudioQueueParam_Volume)常量来获取或设置它的值,它的有效范围在0.0(静音)到1.0(单位增益)
你的应用程序可以通过两种方法来设置音频队列参数:
-
对于每一个音频队列,使用
[AudioQueueSetParameter](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/c/func/AudioQueueSetParameter)函数,这可以让你直接改变音频队列的设置,这个改变是立刻生效的 -
对于每一个音频队列缓冲区,调用
[AudioQueueEnqueueBufferWithParameters](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/c/func/AudioQueueEnqueueBufferWithParameters)函数。这可以让你在将音频队列缓冲区入队的时候设置音频队列设置。这种改变只会在播放这个音频队列缓冲区的时候生效。
这两种情况下,音频队列的参数设置会一直保留到你改变它们为止。
你可以通过调用[AudioQueueGetParameter](https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/AudioQueueReference/Reference/reference.html#//apple_ref/c/func/AudioQueueGetParameter)函数来获取音频队列当前的参数。
2. 音频录制
当你使用音频队列服务进行录制的时候,你可以将音频录制到任何地方——磁盘文件、网络连接或内存对象等等。本章将介绍中最常见的一种情况——将音频录制到磁盘文件中。
录制流程
- 自定义一个结构体结构用来管理状态、格式以及路径信息等。
- 编写音频队列回调函数来执行实际的录制工作。
- 选择需要的每个音频数据的大小,如果需要还可以生成magic cookies(元数据信息)。
- 设置自定义音频数据格式,指定文件路径。
- 创建一个录制用的音频队列并且让音频队列创建一系列的音频队列缓冲区,同时创建一个它将要写入的文件。
- 通知音频队列开始录制音频。
- 录制完毕之后,通知音频队列停止录制,然后释放掉它,同时它会释放掉它所拥有的缓冲区。
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
};
- kNumberBuffers: 使用多少个音频队列数据(默认是3个)
- mDataFormat: 指定音频数据格式
- mQueue: 应用程序创建的录制音频队列.
- mBuffers: 音频队列中音频数据指针的数组
- mAudioFile: 录制的文件
- bufferByteSize: 当前录制的文件的大小(单位是bytes)
- mCurrentPacket: 要写入当前录制文件的音频数据包的索引
- mIsRunning: 当前音频队列是否正在运行
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: 自定义的数据,开发者可以传入一些我们需要的数据供回调函数使用。注意:一般情况下我们需要将当前的OC类实例传入,因为回调函数是纯C语言,不能调用OC类中的属性与方法,所以传入OC实例以与本类中属性方法交互。
- inAQ: 调用回调函数的音频队列
- inBuffer: 装有音频数据的audio queue buffer.
- inStartTime: 当前音频数据的时间戳,主要用于同步。
- inNumberPacketDescriptions: 数据包描述参数。如果你正在录制VBR格式,音频队列会提供此参数的值。如果录制文件需要将其传递给
AudioFileWritePackets函数.CBR格式不使用此参数(值为0)。 - inPacketDescs: 音频数据中一组packet描述.如果是VBR格式数据,如果录制文件需要将此值传递给
AudioFileWritePackets函数。
2.3 将数据写入本地文件
使用AudioFileWritePackets将数据写入音频文件
AudioFileWritePackets ( // 1
pAqData->mAudioFile, // 2
false, // 3
inBuffer->mAudioDataByteSize, // 4
inPacketDesc, // 5
pAqData->mCurrentPacket, // 6
&inNumPackets, // 7
inBuffer->mAudioData // 8
);
-
将音频数据写入音频文件
-
要写入的音频文件
-
使用false表示写入文件时不应缓存数据
-
被写入文件的大小
-
一组音频数据包的描述,如2.2中介绍,如果是CBR设置为NULL,如果是VBR需要设置回调函数中的
inPacketDesc参数. -
当前写入的数据包的索引
-
输入(录制)时,要写入的数据包数。输出(播放)时,实际写入的数据包数
-
要写入的音频数据.
2.4 入队
当音频数据在回调函数中用完后,需要重新放回音频队列以便存储新的音频数据
AudioQueueEnqueueBuffer ( // 1
pAqData->mQueue, // 2
inBuffer, // 3
0, // 4
NULL // 5
);
- 将音频数据放入音频队列
- 录制的音频队列
- 等待入队的音频数据
- 音频数据包的描述信息,设置为0因为该参数不用于录制
- 描述音频队列数据的数据包描述数组,设置为NULL因为该参数不用于录制
2.5 完整的录制回调
static void HandleInputBuffer (
void *aqData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp *inStartTime,
UInt32 inNumPackets,
const AudioStreamPacketDescription *inPacketDesc
) {
AQRecorderState *pAqData = (AQRecorderState *) aqData; // 1
if (inNumPackets == 0 && // 2
pAqData->mDataFormat.mBytesPerPacket != 0)
inNumPackets =
inBuffer->mAudioDataByteSize / pAqData->mDataFormat.mBytesPerPacket;
if (AudioFileWritePackets ( // 3
pAqData->mAudioFile,
false,
inBuffer->mAudioDataByteSize,
inPacketDesc,
pAqData->mCurrentPacket,
&inNumPackets,
inBuffer->mAudioData
) == noErr) {
pAqData->mCurrentPacket += inNumPackets; // 4
}
if (pAqData->mIsRunning == 0) // 5
return;
AudioQueueEnqueueBuffer ( // 6
pAqData->mQueue,
inBuffer,
0,
NULL
);
}
-
用于记录音频队列一些信息的结构体,里面包含当前录制文件的信息,状态等等参数
-
如果音频数据是CBR数据,计算当前数据中包含多少个音频数据包.对于VBR数据,可以直接从回调函数中的
inNumPackets参数获取 -
将音频数据写入音频文件
-
如果成功的话,需要将音频数据包索引累加,以便下次可以继续录制
-
如果audio queue已经停止则返回
-
使用完的音频队列数据重新装入音频队列
2.6 获取Audio Queue Buffer大小
void DeriveBufferSize (
AudioQueueRef audioQueue, // 1
AudioStreamBasicDescription &ASBDescription, // 2
Float64 seconds, // 3
UInt32 *outBufferSize // 4
) {
static const int maxBufferSize = 0x50000; // 5
int maxPacketSize = ASBDescription.mBytesPerPacket; // 6
if (maxPacketSize == 0) { // 7
UInt32 maxVBRPacketSize = sizeof(maxPacketSize);
AudioQueueGetProperty (
audioQueue,
kAudioQueueProperty_MaximumOutputPacketSize,
// in Mac OS X v10.5, instead use
// kAudioConverterPropertyMaximumOutputPacketSize
&maxPacketSize,
&maxVBRPacketSize
);
}
Float64 numBytesForTime =
ASBDescription.mSampleRate * maxPacketSize * seconds; // 8
*outBufferSize =
UInt32 (numBytesForTime < maxBufferSize ?
numBytesForTime : maxBufferSize); // 9
}
-
指定的音频队列
-
音频队列配置信息
-
音频数据采集的间隔(可以通过采样率与间隔算出每个采集数据的大小)
-
通过该参数返回计算出的音频数据的大小
-
音频队列数据大小的上限,以字节为单位。在此示例中,上限设置为320 KB。这相当于采样速率为96 kHz的大约5秒的立体声,24位音频
-
对于CBR的数据,可以从ASBD中获取该值大小.如果是VBR数据,ASBD中取出得值为0
-
对于VBR数据,需要手动估算一个最大值
-
获取音频数据大小(字节)
-
如果需要,限制音频数据最大值
2.7 为音频文件设置magin cookie
对于一些压缩音频数据格式,如AAC、MPEG 4 、AAC等,必须包含音频元数据。包含该元数据信息的数据结构称为magic cookies。当你录制压缩音频数据格式的音频文件时,必须从audio queue中获取元数据并将其设置给音频文件。
注意: 我们在录制前与停止录制后两个时间点都设置一次magin cookie,因为有的编码器需要在停止录制后更新magin cookie。
OSStatus SetMagicCookieForFile (
AudioQueueRef inQueue, // 1
AudioFileID inFile // 2
) {
OSStatus result = noErr; // 3
UInt32 cookieSize; // 4
if (
AudioQueueGetPropertySize ( // 5
inQueue,
kAudioQueueProperty_MagicCookie,
&cookieSize
) == noErr
) {
char* magicCookie =
(char *) malloc (cookieSize); // 6
if (
AudioQueueGetProperty ( // 7
inQueue,
kAudioQueueProperty_MagicCookie,
magicCookie,
&cookieSize
) == noErr
)
result = AudioFileSetProperty ( // 8
inFile,
kAudioFilePropertyMagicCookieData,
cookieSize,
magicCookie
);
free (magicCookie); // 9
}
return result; // 10
}
-
录制的音频队列
-
准备录制的文件
-
定义一个变量记录设置是否成功
-
定义一个变量记录magic cookie的大小
-
从audio queue中获取magic cookie的大小.
-
定义一个变量记录magic cookie的内容并为其分配需要的内存
-
从audio queue中获取magic cookie的内容
-
将获取到的magic cookie设置到文件中.
-
释放刚才临时保存的magic cookie变量
-
返回设置的结果
2.8 设置录制音频的格式
主要关注以下参数
-
音频格式(PCM,AAC...)
-
采样率(44.1kHz, 48kHz)
-
声道数(单声道,双声道)
-
采样位数(16bits)
-
每个音频数据包中的帧数(线性PCM通常是1帧,压缩数据通常比较多)
-
音频文件类型(CAF, AIFF...)
AQRecorderState aqData; // 1
aqData.mDataFormat.mFormatID = kAudioFormatLinearPCM; // 2 aqData.mDataFormat.mSampleRate = 44100.0; // 3 aqData.mDataFormat.mChannelsPerFrame = 2; // 4 aqData.mDataFormat.mBitsPerChannel = 16; // 5 aqData.mDataFormat.mBytesPerPacket = // 6 aqData.mDataFormat.mBytesPerFrame = aqData.mDataFormat.mChannelsPerFrame * sizeof (SInt16); aqData.mDataFormat.mFramesPerPacket = 1; // 7
AudioFileTypeID fileType = kAudioFileAIFFType; // 8 aqData.mDataFormat.mFormatFlags = // 9 kLinearPCMFormatFlagIsBigEndian | kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
- 创建一个存放音频状态信息的结构体.(结构体名字自定义)
- 指定音频格式
- 指定采样率
- 指定声道数
- 指定采样位数
- 指定每个包中的字节数
- 指定每个包中的帧数
- 指定文件类型
- 指定文件类型所需要的标志
2.9 创建录制的Audio Queue
AudioQueueNewInput ( // 1
&aqData.mDataFormat, // 2
HandleInputBuffer, // 3
&aqData, // 4
NULL, // 5
kCFRunLoopCommonModes, // 6
0, // 7
&aqData.mQueue // 8
);
- 创建一个录制音频队列
- 指定录制的音频格式
- 指定回调函数
- 可传入自定义的数据结构,可以是本类的实例,可以是记录音频信息的结构体
- 回调函数在哪个循环中被调用.设置为NULL为默认值,即回调函数所在的线程由audio queue内部控制
- 回调函数运行循环模式通常使用kCFRunLoopCommonModes
- 保留值,只能为0
- 输出时新分配的音频队列
2.10 获取完整的音频格式
当audio queue开始工作后,它可能会产生更多音频格式信息比我们初始化设置时,所以我们需要对获取到的音频数据做一个检查。
UInt32 dataFormatSize = sizeof (aqData.mDataFormat); // 1
AudioQueueGetProperty ( // 2
aqData.mQueue, // 3
kAudioQueueProperty_StreamDescription, // 4
// in Mac OS X, instead use
// kAudioConverterCurrentInputStreamDescription
&aqData.mDataFormat, // 5
&dataFormatSize // 6
);
-
查询音频数据格式
-
获取audio queue指定属性的值
-
查询的音频队列
-
音频队列数据格式的ID
-
作为输出,输出完整的音频数据格式
-
在输入时,AudioStreamBasicDescription结构的预期大小。在输出时,实际大小。您的录制应用程序不需要使用此值
2.11 创建一个音频文件
CFURLRef audioFileURL =
CFURLCreateFromFileSystemRepresentation ( // 1
NULL, // 2
(const UInt8 *) filePath, // 3
strlen (filePath), // 4
false // 5
);
AudioFileCreateWithURL ( // 6
audioFileURL, // 7
fileType, // 8
&aqData.mDataFormat, // 9
kAudioFileFlags_EraseFile, // 10
&aqData.mAudioFile // 11
);
-
创建一个
CFURL类型的对象代表录制文件路径 -
使用NULL(kCFAllocatorDefault)使用当前默认的内存分配器
-
设置文件路径
-
文件名长度
-
false表示是一个文件,不是文件夹.
-
创建一个新的文件或初始化一个已经存在的文件.
-
音频文件的路径(即3中创建的)
-
音频文件类型.(CAF,AIFF...)
-
ASBD
-
设置该值表示如果文件已经存在则覆盖
-
代表录制的文件
2.12 设置音频队列数据大小
DeriveBufferSize ( // 1
aqData.mQueue, // 2
aqData.mDataFormat, // 3
0.5, // 4
&aqData.bufferByteSize // 5
);
2.13 为Audio Queue准备指定数量的buffer
for (int i = 0; i < kNumberBuffers; ++i) { // 1
AudioQueueAllocateBuffer ( // 2
aqData.mQueue, // 3
aqData.bufferByteSize, // 4
&aqData.mBuffers[i] // 5
);
AudioQueueEnqueueBuffer ( // 6
aqData.mQueue, // 7
aqData.mBuffers[i], // 8
0, // 9
NULL // 10
);
}
-
一般指定3个,这里为一个简单的循环,为指定数量的buffer分配内存并进行入队操作
-
为每个buffer分配内存
-
指定分配内存的音频队列
-
指定分配内存的Buffer的大小(即2.12中获取的)
-
输出一个分配好内存的buffer
-
音频队列入队
-
将要入队的音频队列
-
将要入队的音频数据
-
对于录制此参数没用
-
1对于录制此参数没用
2.14 录制音频
aqData.mCurrentPacket = 0; // 1
aqData.mIsRunning = true; // 2
AudioQueueStart ( // 3
aqData.mQueue, // 4
NULL // 5
);
// Wait, on user interface thread, until user stops the recording
AudioQueueStop ( // 6
aqData.mQueue, // 7
true // 8
);
aqData.mIsRunning = false; // 9
-
初始化记录当前录制文件packet索引为0
-
表明audio queue正在运行
-
开启一个audio queue
-
指定开启的audio queue
-
设置为NULL表示立即开始采集数据
-
停止并重置当前音频队列
-
指定停止的音频队列
-
true:同步停止, false: 异步停止
-
更新音频队列当前工作状态
2.15 录制完成清理内存
录制完成后,回收音频队列数据,关闭音频文件。
AudioQueueDispose ( // 1
aqData.mQueue, // 2
true // 3
);
AudioFileClose (aqData.mAudioFile); // 4
- 回收音频队列中所有资源
- 指定回收的音频队列
- true: 同步, false:异步
- 关闭录制文件
3. 播放
使用 Audio Queue Services播放音频时,源数据可以是本地文件, 内存中的对象或者其他音频存储方式.本章中仅介绍通过本地文件播放.
- 定义一个结构体管理音频格式状态信息等.
- 实现一个播放回调函数
- 设置音频队列数据大小
- 打开一个音频文件,确定音频数据格式
- 创建并配置一个播放的音频队列
- 为音频队列数据分配内存并入队.告诉音频队列开始播放.完成时,告诉音频队列停止.
- 回收内存,释放资源
3.1 定义一个结构体管理音频状态
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
};
此结构体中的数据基本与录制时相同
- 设置音频队列中可复用的音频数据个数,通常为3
- ASBD
- 播放使用的音频队列
- 管理音频队列中音频数据的数组
- 播放用的音频文件
- 每个音频数据的大小
- 当前准备播放的音频数据包索引
- 每次调用回调函数要读取的音频数据包的个数
- 对于VBR音频数据,表示正在播放的音频数据包描述性数组,对于CBR音频数据可以设为NULL
- 音频队列是否正在运行
3.2 回调函数
作用
- 从音频文件中读取指定数量的音频数据并将其装入音频队列数据
- 将音频队列数据入队
- 文件读取完成后,停止音频队列
3.2.1 定义回调函数
static void HandleOutputBuffer (
void *aqData, // 1
AudioQueueRef inAQ, // 2
AudioQueueBufferRef inBuffer // 3
)
- 同录制,自定义的结构体或类对象,可传入回调函数中使用,即OC类与回调函数间的通信对象
- 当前工作的音频队列
- 通过读取音频文件获取的音频数据
3.2.2 读取音频文件
AudioFileReadPackets ( // 1
pAqData->mAudioFile, // 2
false, // 3
&numBytesReadFromFile, // 4
pAqData->mPacketDescs, // 5
pAqData->mCurrentPacket, // 6
&numPackets, // 7
inBuffer->mAudioData // 8
);
- 读取文件的函数
- 要读取的音频文件
- false:读取时不应缓存数据.
- 作为输出:将从文件读取的字节数
- 作为输出:VBR:从音频文件读取到的数据包描述数组,CBR:NULL
- 当前读取到的索引值,以便下次继续读取
- 作输入时:从音频文件中读取到的音频数据包数,作输出时:实际读取到的音频数据包
- 作输出时:从音频文件中读取的数据
3.2.3 入队
读取完音频数据后,执行入队操作
AudioQueueEnqueueBuffer ( // 1
pAqData->mQueue, // 2
inBuffer, // 3
(pAqData->mPacketDescs ? numPackets : 0), // 4
pAqData->mPacketDescs // 5
);
4. 音频数据包数,CBR的数据使用0
5. 对于压缩数据使用其数据包描述信息
3.2.4 停止音频队列
如果检查到当前音频文件读取完毕,应该停止音频队列.
if (numPackets == 0) { // 1
AudioQueueStop ( // 2
pAqData->mQueue, // 3
false // 4
);
pAqData->mIsRunning = false; // 5
}
1. 通过AudioFileReadPackets检查数据包是否为0
4. true:同步,,false:异步
3.2.5 完整的回调
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;
}
}
3. 记录读取到的字节数
4. 记录读取到音频数据包数
7. 累加音频数据包,使下次触发回调可以接着上次内容继续播放
3.3 计算音频队列数据
我们需要指定一个音频队列buffer的大小,根据计算出来的大小为音频队列数据分配内存。
-
回调函数中调用
AudioFileReadPackets获取读取到的包数 -
设置音频buffer下限值,避免访问过于频繁.
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}
2. 估算当前播放音频文件最大数据包大小,通过调用AudioFileGetProperty查询kAudioFilePropertyPacketSizeUpperBound属性可得
3. 采样时间,根据采样率与采样时间可计算出音频数据大小
4. 每个音频数据的大小
5. 每次从音频播放回调中读取的音频数据包数
6. 音频数据包大小的上限
7. 音频数据包大小的下限
8. 计算音频数据包总大小
9. 根据最大数据包大小和您设置的上限导出合理的音频队列数据大小
10. 设置上限
11. 设置下限
12. 计算读取到的音频数据包数
3.4 打开音频文件
- 获取一个
CFURL对象表示音频文件路径 - 打开音频文件
- 获取文件格式
3.4.1 获取一个CFURL对象表示音频文件路径
CFURLRef audioFileURL =
CFURLCreateFromFileSystemRepresentation ( // 1
NULL, // 2
(const UInt8 *) filePath, // 3
strlen (filePath), // 4
false // 5
);
1. 创建一个CFURL类型的对象代表录制文件路径
2. 使用NULL(kCFAllocatorDefault)使用当前默认的内存分配器
3. 设置文件路径
4. 文件名长度
5. false表示是一个文件,不是文件夹
3.4.2 打开音频文件
AQPlayerState aqData; // 1
OSStatus result =
AudioFileOpenURL ( // 2
audioFileURL, // 3
fsRdPerm, // 4
0, // 5
&aqData.mAudioFile // 6
);
CFRelease (audioFileURL); // 7
2. 打开一个想要播放的音频文件
3. 音频文件路径
4. 文件权限
5. 可选文件类型,0:不使用此参数
6. 作为输出,获取文件对象的引用
3.4.3 获取文件格式
UInt32 dataFormatSize = sizeof (aqData.mDataFormat); // 1
AudioFileGetProperty ( // 2
aqData.mAudioFile, // 3
kAudioFilePropertyDataFormat, // 4
&dataFormatSize, // 5
&aqData.mDataFormat // 6
);
5. 作为输入:输入时,AudioStreamBasicDescription结构体的预期大小,用于描述音频文件的数据格式。在输出时,实际大小。作播放时不需要使用此值。
6. 输出:将文件代表的ASBD数据格式赋给该变量
3.5. 创建播放音频队列
AudioQueueNewOutput ( // 1
&aqData.mDataFormat, // 2
HandleOutputBuffer, // 3
&aqData, // 4
CFRunLoopGetCurrent (), // 5
kCFRunLoopCommonModes, // 6
0, // 7
&aqData.mQueue // 8
);
3. 回调函数
4. 音频队列数据
5. 调用播放回调的的运行循环
6. 调用播放回调运行循环的模式
3.6 设置播放音频队列大小
3.6.1 设置buffer size与读取的音频数据包数量
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
);
3.6.2 为数据包描述数组分配内存
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数据,每个数据包中的帧数(同理每个数据包中的字节数也是一样)是可变的,所以此属性为0.
2. 对于VBR数据,为数据包描述字典分配指定内存.
3. 对于CBR数据,不需要使用该参数,直接设为NULL
3.7 设置magic cookie
对于压缩的音频数据格式(AAC...),我们在播放前必须为音频队列设置magic cookies,即元数据信息。
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. 根据UInt32估算magic cookie数据大小
2. 记录是否能获取magic cookie结果
3. 获取文件中的magic cookie的大小
4. 想要播放的文件
5. key值,代表音频文件的kAudioFilePropertyMagicCookieData
6. 作输入时表示magic cookie估算大小,输出时表示实际大小
7. 设置为NULL表示不关心此属性的读写权限
8. 如果文件包含magic cookie,分配内存去持有它
9. 获取文件中的magic cookie
12. 输入时表示文件中的magic cookie的大小
13. 输出为文件的magic cookie
14. 设置audio queue的函数
3.8 分配音频队列数据
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. 自定义的播放音频回调函
3.9 设置音量
开始播放前,可以设置音量(0~1)
Float32 gain = 1.0; // 1
// Optionally, allow user to override gain setting here
AudioQueueSetParameter ( // 2
aqData.mQueue, // 3
kAudioQueueParam_Volume, // 4
gain // 5
);
3.10 启动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
);
4. 设置为NULL表示马上开始播放
8. 设置运行循环的时间是0.25秒
9. 使用false表示运行循环应该在指定的完整时间内继续
10. 音频队列停止后,运行循环运行一段时间以确保当前播放的音频队列缓冲区有时间完成。
3.11 清理
播放完成后应该回收音频队列,关闭音频文件,释放所有相关资源
AudioQueueDispose ( // 1
aqData.mQueue, // 2
true // 3
);
AudioFileClose (aqData.mAudioFile); // 4
free (aqData.mPacketDescs); // 5
3. true: 同步, false:异步