一、前言
Audio Queue Services
提供了一种简单的、低开销的方式来录制、播放iOS和Mac OS X中的音频。用于为iOS或Max OS X应用添加基本的录制和播放功能。
Audio Queue Services
能允许你录制和播放下面这些格式的音频:
- Linear PCM.
- Apple平台上支持的任何压缩格式,如aac、mp3、m4a等
- 用户已安装编码器的任何其他格式
Audio Queue Services
是一个上层服务。它让你的应用使用硬件(如麦克风、扬声器)录制和播放,而无需了解硬件接口。它还允许你使用复杂的编解码器,而无需了解编解码器的工作方式。
二、什么是Audio Queue
Audio Queue
是一个对象,用于录制和播放音频。它用AudioQueueRef
数据类型表示,在AudioQueue.h
头文件中声明
Audio Queue
做以下任务:
- Connecting to audio hardware(连接音频硬件)
- Managing memory(管理内存)
- Employing codecs, as needed, for compressed audio formats(根据需要,为压缩音频格式使用编解码器)
- Mediating recording or playback(调解录音和播放)
三、Audio Queue架构
所有的Audio Queue
都有着相同的结构,由以下部分组成:
- 一组audio queue buffers,每个buffer临时存储着audio data
- 一个buffer queue,有序的管理audio queue buffers
- 一个audio queue callback,这个需要开发者来编写
架构取决于音频队列是用于录制还是回放。 不同之处在于音频队列如何连接其输入和输出,以及回调函数的作用。
3.1 Audio Queues for recording
使用AudioQueueNewInput
函数创建一个recording audio queue
,其结构如下图:
输入方通常连接的是音频硬件,比如麦克风。在iOS中,音频通常来自用户内置的麦克风或耳机麦克风连接的设备 。在Mac OS X的默认情况下,音频来自系统首选项中用户设置的系统默认音频输入设备。
audio queue的输出端,是开发者编写的回调函数。当录制到磁盘时,回调函数会将buffers
中的新数据写入到文件,buffers
中的数据是从audio queue中接收到的。但是,recording audio queue还可以有其它用途。比如,你的回调函数直接向你的应用提供audio data而不是将其写入磁盘。
每个audio queue,无论是recording或playback,都有一个或多个audio queue buffers。这些buffers按特定的顺序排列。在上图中,audio queue buffers按照它们被填充的顺序进行编号,这与它们切换到回调的顺序相同。 后面会详细说明如何使用。
3.2 Audio Queues for Playback
使用AudioQueueNewOutput
函数创建一个playback audio queue
,其结构如下图:
在playback audio queue中,回调函数是在输入端;回调函数负责从磁盘(或其他一些源)获取音频数据并将其交给音频队列。 当没有更多数据可播放时,回调函数应该告知audio queue停止播放。
playback audio queue的输出端通常连接的audio硬件设备,如扬声器
3.3 Audio Queue Buffers
Audio Queue Buffers是一个类型为AudioQueueBuffer
结构体
typedef struct AudioQueueBuffer {
const UInt32 mAudioDataBytesCapacity;
void *const mAudioData;
UInt32 mAudioDataByteSize;
void *mUserData;
} AudioQueueBuffer;
typedef AudioQueueBuffer *AudioQueueBufferRef;
mAudioData:指向了存储音频数据的内存块
mAudioDataByteSize:audio data的字节数,在录制
的时候audio queue会设置此值;在播放
的时候,需要开发者来设置
Audio Queue可以使用任意数量的buffers。 你的应用程序指定了多少。 通常是三个。 一个用来写入磁盘,而另一个用来填充新的音频数据。 如果需要,可以使用第三个缓冲区来补偿磁盘I / O延迟等问题
Audio Queue为其buffers执行内存管理。
- 使用
AudioQueueAllocateBuffer
函数创建一个buffer - 使用
AudioQueueDispose
函数释放一个Audio Queue,Audio Queue会释放掉它的buffers
四、The Buffer Queue and Enqueuing
下面将分析Audio Queue对象如何在录制和播放期间管理buffer queue,以及enqueuing
4.1 录制过程
步骤1:开始录制,audio queue填充数据到buffer1
步骤2:buffer1填充满后,audio queue调用回调函数处理buffer1(步骤3);与此同时audio queue填充数据到buffer2
步骤4:将用过的buffer1重新入队,再将填充好的buffer2给回调使用(步骤6),与此同时audio queue填充数据到其它的buffer(步骤5);依此循环,直到停止录制
4.2 播放过程
播放时,一个audio queue buffer 被发送到输出设备,如扬声器。在queue buffer中的剩余buffers排在当前buffer后面,等待依此播放。
Audio queue会按照播放顺序将播放的音频数据buffer交给回调函数,回调函数将新的音频数据读入buffer,然后将其入队
步骤1:应用程序启动playback audio queue。应用程序为每个audio queue buffers调用一次回调,填充它们并将它们添加到buffer queue。当你的应用程序调用
AudioQueueStart
函数时确保能够启动。步骤3:audio queue 发送buffer1到输出设备
一旦播放了第一个buffer,playback audio queue进入一个循环状态。audio queue开始播放下一个buffer(buffer2 步骤4)并调用回调处理刚刚播放完的buffer(步骤5),填充buffer,并将其入队(步骤6)
4.3 控制播放过程
Audio queue buffers始终按照入队顺序进行播放,但是audio queue可以使用AudioQueueEnqueueBufferWithParameters
函数对播放过程进行一些控制
- Set the precise playback time for a buffer. This lets you support synchronization.
- Trim frames at the start or end of an audio queue buffer. This lets you remove leading or trailing silence.
- Set the playback gain at the granularity of a buffer
五、The Audio Queue Callback Function
通常,使用Audio Queue Services大部分工作是编写回调函数。在录制和播放时,audio queue会重复的调用callback。调用之间的时间取决于audio queue buffers的容量,通常为半秒到几秒
5.1 The Recording Audio Queue Callback Function
typedef void (*AudioQueueInputCallback)(
void * __nullable inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp * inStartTime,
UInt32 inNumberPacketDescriptions,
const AudioStreamPacketDescription * __nullable inPacketDescs
);
当recording audio queue调用callback时,会提供下一组音频数据所需的内容
- inUserData:通常是传入一个包含audio queue及其buffer的状态信息的对象/结构体
- inAQ:调用callback的audio queue
- inBuffer:audio queue buffer,有audio queue刷新填充,其包含了你的回调需要写入磁盘的新数据。该数据已根据在inUserData中指定的格式进行格式化。
- inStartTime:buffer第一个样本的采样时间,对应基本录制,你的callback不使用该参数
- inNumberPacketDescriptions:是inPacketDescs参数中的数据包描述个数。如果要录制为VBR(可变比特率)格式,音频队列会为您的回调提供此参数的值,然后将其传递给AudioFileWritePackets函数。 CBR(恒定比特率)格式不使用数据包描述。 对于CBR记录,音频队列将此设置和inPacketDescs参数设置为NULL。
- inPacketDescs:buffer中的样本对应的数据包描述集。 同样,如果音频数据是VBR格式,音频队列将提供此参数的值,并且您的回调将其传递给AudioFileWritePackets函数
5.2 The Playback Audio Queue Callback Function
typedef void (*AudioQueueOutputCallback)(
void * __nullable inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer
);
在调用callback是,playback audio queue提供callback读取下一组音频数据所需的内容
- inUserData:通常传入一个包含audio queue及其buffers的状态信息对象/结构体
- inAQ:调用callback的audio queue
- inBuffer:由audio queue提供,你的callback将填充从正在播放的文件中读取下一组数据
如果你的应用程序播放VBR数据,callback需要获取audio data的packet information。使用AudioFileReadPacketData
函数,然后将packet information放入inUserData中,以使其可用于playback audio queue
六、使用Codecs and Audio Data Formats
Audio Queue Services根据需要使用编解码器(音频数据编码/解码组件)来在音频格式之间进行转换。 你的录制或播放应用程序可以使用已安装编解码器的任何音频格式。 你无需编写自定义代码来处理各种音频格式。 具体来说,你的回调不需要了解数据格式。
每个Audio queue具有audio data format,使用结构体AudioStreamBasicDescription
来表示。当你指定foramt结构体中的mFromatID字段的格式时,audio queue会使用合适的编解码器。然后,你可以指定采样率和声道数,这就是它的全部内容。
录制使用Codec:
步骤1:应用程序开始recording,并告知audio queue使用何种format来编码
步骤2:根据fromat选择合适的codec得到压缩数据提交给callback
步骤3:callback调用写入磁盘
播放使用Codec:
步骤1:应用程序告知audio queue接收何种format的数据,并启动playing
步骤2:audio queue调用callback,从audio file中读取数据,callback将原始数据传递给audio queue。
步骤3:audio queue使用合适的codec将数据转为未压缩的数据发送到目的地
七、Audio Queue Control and State
Audio queue在creation和disposal之间具有生命周期。你的应用程序可以使用以下函数来管理它的生命周期。
- Start(AudioQueueStart)在初始化recording或playback时调用
- Prime(AudioQueuePrime)对于playback,需要在AudioQueueStart之前调用,以确保立即有可用的数据提供给audio queue播放。该函数与recording无关
- Stop(AudioQueueStop)用来重置audio queue,然后停止recording或playback。当playback的callback没有数据来播放时调用该函数
- Pause(AudioQueuePause)用来暂停recording或playback,不会影响buffers和重置audio queue;调用
AudioQueueStart
函数恢复 - Flush(AudioQueueFlush)在最后一个入队的buffer之后调用,用来确保所有的buffer数据,也包括处理中的数据,得到播放或录制
- Reset(AudioQueueReset)调用该函数会让audio queue立即静音,它会清除掉所有的buffers、重置所有的编解码器和DSP状态
AudioQueuesStop
函数有同步
、异步
两种调用方式:
- 同步调用会立即停止,不考虑已缓冲的audio data
- 异步调用会等到队列中所有的buffer全部播放或录完再停止
八、Audio Queue Parameters
可以给audio queue设置相关的参数,这些参数通常是针对playback,而不是recording
有两种方式来设置参数:
- 对于audio queue,使用
AudioQueueSetParameter
函数 - 对于audio queue buffer,使用
AudioQueueEnqueueBufferWithParameters
函数
九、录制音频
下面以录制一个aac音频格式到本地磁盘为例。
1、创建一个对象LXAudioRecoder来管理audio queue状态、存储dataformat、路径等信息
static const int kNumberBuffers = 3;
@interface LXAudioRecoder () {
// 音频队列
AudioQueueRef queueRef;
// buffers数量
AudioQueueBufferRef buffers[kNumberBuffers];
// 音频数据格式
AudioStreamBasicDescription dataformat;
}
@property (nonatomic, assign) SInt64 currPacket;
// 录制的文件
@property (nonatomic, assign) AudioFileID mAudioFile;
// 当前录制文件的大小
@property (nonatomic, assign) UInt32 bufferBytesSize;
2、配置datafromat
Float64 sampleRate = 44100.0;
UInt32 channel = 2;
// 音频格式
dataformat.mFormatID = kAudioFormatMPEG4AAC;
// 采样率
dataformat.mSampleRate = sampleRate;
// 声道数
dataformat.mChannelsPerFrame = channel;
UInt32 formatSize = sizeof(dataformat);
AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &formatSize, &dataformat);
// 采样位数
// dataformat.mBitsPerChannel = 16;
// // 每个包中的字节数
// dataformat.mBytesPerPacket = channel * sizeof(SInt16);
// // 每个帧中的字节数
// dataformat.mBytesPerFrame = channel * sizeof(SInt16);
// // 每个包中的帧数
// dataformat.mFramesPerPacket = 1;
// // flags
// dataformat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
3、创建record Audio Queue
OSStatus status = AudioQueueNewInput(&dataformat, recoderCallBack, (__bridge void *)self, NULL, NULL, 0, &queueRef);
4、编写recoderCallBack
static void recoderCallBack(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, const AudioTimeStamp *timestamp, UInt32 inNumPackets, const AudioStreamPacketDescription *inPacketDesc) {
LXAudioRecoder *recoder = (__bridge LXAudioRecoder *)aqData;
if (inNumPackets == 0 && recoder->dataformat.mBytesPerPacket != 0) {
inNumPackets = inBuffer->mAudioDataByteSize / recoder->dataformat.mBytesPerPacket;
}
// 将音频数据写入文件
if (AudioFileWritePackets(recoder.mAudioFile, false, inBuffer->mAudioDataByteSize, inPacketDesc, recoder.currPacket, &inNumPackets, inBuffer->mAudioData) == noErr) {
recoder.currPacket += inNumPackets;
}
if (recoder.isRunning) {
// 入队
AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
}
}
5、计算Audio Queue Buffers大小
/**
* 获取AudioQueueBuffer大小
* seconds:每个buffer保存的音频秒数,一般设置为半秒
*/
void deriveBufferSize(AudioQueueRef audioQueue, AudioStreamBasicDescription streamDesc, Float64 seconds, UInt32 *outBufferSize) {
// 音频队列数据大小的上限
static const int maxBufferSize = 0x50000;
int maxPacketSize = streamDesc.mBytesPerPacket;
if (maxPacketSize == 0) { // VBR
UInt32 maxVBRPacketSize = sizeof(maxPacketSize);
AudioQueueGetProperty(audioQueue, kAudioQueueProperty_MaximumOutputPacketSize, &maxPacketSize, &maxVBRPacketSize);
}
// 获取音频数据大小
Float64 numBytesForTime = streamDesc.mSampleRate * maxPacketSize * seconds;
*outBufferSize = (UInt32)(numBytesForTime < maxBufferSize? numBytesForTime : maxBufferSize);
}
6、创建Audio Queue Buffers
deriveBufferSize(queueRef, dataformat, 0.5, &_bufferBytesSize);
// 为Audio Queue准备指定数量的buffer
for (int i = 0; i < kNumberBuffers; i++) {
AudioQueueAllocateBuffer(queueRef, self.bufferBytesSize, &buffers[i]);
AudioQueueEnqueueBuffer(queueRef, buffers[i], 0, NULL);
}
7、创建音频文件
NSURL *fileURL = [NSURL URLWithString:filePath];
AudioFileCreateWithURL((__bridge CFURLRef)fileURL, kAudioFileCAFType, &dataformat, kAudioFileFlags_EraseFile, &_mAudioFile);
8、设置magic cookie for an audio file
默写压缩音频格式,如MPEG 4 AAC,使用一个结构来包含audio的元数据。这种结构叫做magic cookie。当使用audio queue services录制这种格式时,你必须从audio queue中获取magic cookie并在开始录制前添加到音频文件中。
注意:下面方法需在recording之前和在停止recording时调用,因为某些编解码器在recording停止时更新magic cookie数据
- (OSStatus)setupMagicCookie {
UInt32 cookieSize;
OSStatus status = noErr;
if (AudioQueueGetPropertySize(queueRef, kAudioQueueProperty_MagicCookie, &cookieSize) == noErr) {
char *magicCookie = (char *)malloc(cookieSize);
if (AudioQueueGetProperty(queueRef, kAudioQueueProperty_MagicCookie, magicCookie, &cookieSize) == noErr) {
status = AudioFileSetProperty(_mAudioFile, kAudioFilePropertyMagicCookieData, cookieSize, magicCookie);
}
free(magicCookie);
}
return status;
}
9、record audio
- (void)recoder {
if (self.isRunning) {
return;
}
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
[[AVAudioSession sharedInstance] setActive:true error:nil];
OSStatus status = AudioQueueStart(queueRef, NULL);
if (status != noErr) {
NSLog(@"start queue failure");
return;
}
_isRunning = true;
}
- (void)stop {
if (self.isRunning) {
AudioQueueStop(queueRef, true);
_isRunning = false;
[[AVAudioSession sharedInstance] setActive:false withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
}
}
10、clean up
- (void)dealloc {
AudioQueueDispose(queueRef, true);
AudioFileClose(_mAudioFile);
}
十、播放音频
以播放本地音频文件为例
1、创建一个对象用来管理audio queue状态、datafromat、buffers等
static const int kNumberBuffers = 3;
@interface LXAudioPlayer () {
AudioStreamBasicDescription dataFormat;
AudioQueueRef queueRef;
AudioQueueBufferRef mBuffers[kNumberBuffers];
}
@property (nonatomic, assign) AudioFileID mAudioFile;
@property (nonatomic, assign) UInt32 bufferByteSize;
@property (nonatomic, assign) SInt64 mCurrentPacket;
@property (nonatomic, assign) UInt32 mPacketsToRead;
@property (nonatomic, assign) AudioStreamPacketDescription *mPacketDescs;
@property (nonatomic, assign) bool isRunning;
@end
2、打开文件
NSURL *fileURL = [NSURL URLWithString:filePath];
OSStatus status = AudioFileOpenURL((__bridge CFURLRef)fileURL, kAudioFileReadPermission, kAudioFileCAFType, &_mAudioFile);
3、获取文件格式
// 获取文件格式
UInt32 dataFromatSize = sizeof(dataFormat);
AudioFileGetProperty(_mAudioFile, kAudioFilePropertyDataFormat, &dataFromatSize, &dataFormat);
4、创建playback audio queue
// 创建播放音频队列
AudioQueueNewOutput(&dataFormat, playCallback, (__bridge void *)self, CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0, &queueRef);
5、编写play callback
static void playCallback(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) {
LXAudioPlayer *player = (__bridge LXAudioPlayer *)aqData;
UInt32 numBytesReadFromFile = player.bufferByteSize;
UInt32 numPackets = player.mPacketsToRead;
AudioFileReadPacketData(player.mAudioFile, false, &numBytesReadFromFile, player.mPacketDescs, player.mCurrentPacket, &numPackets, inBuffer->mAudioData);
if (numPackets > 0) {
inBuffer->mAudioDataByteSize = numBytesReadFromFile;
player.mCurrentPacket += numPackets;
AudioQueueEnqueueBuffer(player->queueRef, inBuffer, player.mPacketDescs ? numPackets : 0, player.mPacketDescs);
} else {
NSLog(@"play end");
AudioQueueStop(player->queueRef, false);
player.isRunning = false;
}
}
6、计算buffer的大小
void playBufferSize(AudioStreamBasicDescription basicDesc, UInt32 maxPacketSize, Float64 seconds, UInt32 *outBufferSize, UInt32 *outNumPacketsToRead) {
static const int maxBufferSize = 0x50000;
static const int minBufferSize = 0x4000;
if (basicDesc.mFramesPerPacket != 0) {
Float64 numPacketsForTime = basicDesc.mSampleRate / basicDesc.mFramesPerPacket * seconds;
*outBufferSize = numPacketsForTime * maxPacketSize;
} else {
*outBufferSize = maxBufferSize > maxPacketSize ? maxBufferSize : maxPacketSize;
}
if (*outBufferSize > maxBufferSize && *outBufferSize > maxPacketSize) {
*outBufferSize = maxBufferSize;
} else {
if (*outBufferSize < minBufferSize) {
*outBufferSize = minBufferSize;
}
}
*outNumPacketsToRead = *outBufferSize / maxPacketSize;
}
7、为数据包描述分配内存
bool isFormatVBR = dataFormat.mBytesPerPacket == 0 || dataFormat.mFramesPerPacket == 0;
if (isFormatVBR) {
_mPacketDescs = (AudioStreamPacketDescription *)malloc(_mPacketsToRead * sizeof(AudioStreamPacketDescription));
} else {
_mPacketDescs = NULL;
}
8、set magic cookie
- (void)setupMagicCookie {
// magic cookie
UInt32 cookieSize = sizeof(UInt32);
if (AudioFileGetPropertyInfo(_mAudioFile, kAudioFilePropertyMagicCookieData, &cookieSize, NULL) == noErr && cookieSize) {
char *magicCookie = (char *)malloc(cookieSize);
if (AudioFileGetProperty(_mAudioFile, kAudioFilePropertyMagicCookieData, &cookieSize, magicCookie) == noErr) {
AudioQueueSetProperty(queueRef, kAudioQueueProperty_MagicCookie, magicCookie, cookieSize);
}
free(magicCookie);
}
}
9、创建buffer
UInt32 maxPacketSize;
UInt32 propertySize = sizeof(maxPacketSize);
AudioFileGetProperty(_mAudioFile, kAudioFilePropertyPacketSizeUpperBound, &propertySize, &maxPacketSize);
playBufferSize(dataFormat, maxPacketSize, 0.5, &_bufferByteSize, &_mPacketsToRead);
// 分配音频队列
for (int i = 0; i < kNumberBuffers; i++) {
AudioQueueAllocateBuffer(queueRef, _bufferByteSize, &mBuffers[i]);
playCallback((__bridge void *)self, queueRef, mBuffers[i]);
}
10、play audio
- (void)play {
if (self.isRunning) {
return;
}
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
//[[AVAudioSession sharedInstance] setActive:YES error:nil];
[[AVAudioSession sharedInstance] setActive:true withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
OSStatus status = AudioQueueStart(queueRef, NULL);
if (status != noErr) {
NSLog(@"play error");
return;
}
self.isRunning = true;
}
11、stop play
- (void)stop {
if (self.isRunning) {
self.isRunning = false;
AudioQueueStop(queueRef, true);
[[AVAudioSession sharedInstance] setActive:false withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
}
}
12、clean
- (void)dealloc {
AudioFileClose(_mAudioFile);
AudioQueueDispose(queueRef, true);
if (_mPacketDescs) {
free(_mPacketDescs);
}
}
播放和录音demo已上传Github
参考文章:
1、Audio Queue Services Programing Guide