音频采集相关知识,AduioToolBox 的音频采集原理,音频队列架构,iOS音频录制的过程。
为啥莫名其妙地开始看音频的东西?
工作相关,梳理相关模块需要先了解原理和知识
1. 音频采集原理
音频的采集工作涉及诸多生疏的概念,例如编码
、格式
、缓冲
、队列
等,首先需要将这来生疏的概念梳理清楚。
音频的采集由三阶段工作组成,首先是录音的硬件设备,这部分我们不需要关心,系统提供的API帮我们处理了模拟信号采集到数字信号的过程。
1.1 音频队列 AudioQueue
所有的音频播放和录制都是通过操作 AudioQueue 来完成的,通过 AudioQueueRef
这个数据结构来表示,声明在 AudioQueue.h 中,作用如下:
- 连接音频相关硬件
- 管理内存
- 为不同的压缩格式提供编码器
- 录制
- 播放
录制的音频队列创建使用AudioQueueNewInput
函数,播放的队列创建使用AudioQueueNewOutput
函数。
实际开发需要做的是将 AudioQueue 组合其他的 CoreAduio 接口,已实现符合我们规范的音频方案。
音频队列内部包含三个重要的东西:
- 多个(默认3个)音频队列缓冲区:audio queue buffers
- 一个缓冲队列:buffer queue
- 一个用户自己实现的回调函数:audio queue callback function
下图引用自Audio Queue Services Programming Guide
,展示了这三部分的工作原理
需要注意的是:
AudioQueue 向前的输入设备,默认使用的是用户连接的设备,可 iOS 默认是连接的耳机、内置麦克风等。
AudioQueue 向后的 CallbackFunction 之后的行为完全是用户自定义的,是上传还是发送还是存储到 disk 中用户可以自己实现,这种场景非常多,例如录音完直接发送、录音 app 保存到磁盘、噪音实时监测不需要任何数据保存等...
1.2 音频队列缓冲区 AudioQueueBuffer
音频队列缓冲区
的数据结构,声明于:AudioQueue.h
,如下
typedef struct AudioQueueBuffer {
const UInt32 mAudioDataBytesCapacity;
void *const mAudioData; // 指向缓冲区
UInt32 mAudioDataByteSize;
void *mUserData;
} AudioQueueBuffer;
typedef AudioQueueBuffer *AudioQueueBufferRef;
这个结构中的 mAudioData 指针指向的就是真正的缓冲区数据。
1.3 缓冲区队列 BufferQueue
缓冲区队列就像任务队列一样,它是一个缓冲区的有序列表,这里我们需要知道,队列在实际录制中的作用。
一个音频队列可以使用任意数量的缓冲区,但是一般来说我们使用3个就可以,实际的录制过程是这样的:
音频录制的过程上图中步骤:
- 录音设备开始用捕获的数据填充缓冲区。
- 第一个缓冲区数据填满之后,音频队列调用了 CallBack 将其交出,然后将新的数据向下一个缓冲区进行填充。
- 交出的缓冲区被CallBack函数处理(不一定是写入磁盘,而已程序自定义逻辑)
- Callback 函数交回缓冲区将其重新利用。
- 重复第二步。
- 重复第三步。
1.4 音频队列回调函数 Callback function
大部分的编程工作都是在回调函数上,因为这里是我们编写的录制组件接收数据的地方,也是调用最频繁的地方。
根据缓冲区的设置,根据容量,会计算出调用回调函数的间隔时间,一般在半秒到几秒之间。
另外就是向上图的过程4中记录的那样,我们需要将目前返回的这个缓冲区在函数结束的时候加入到缓冲区的末尾,方法是通过调用函数 AudioQueueEnqueueBuffer
实现将缓冲区加入到缓冲区的末尾。
录制的 CallBack 和播放的 CallBack 结构上不同,录制音频的 CallBack 如下:
AudioQueueInputCallback (
void *inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp *inStartTime,
UInt32 inNumberPacketDescriptions,
const AudioStreamPacketDescription *inPacketDescs
);
各字段解释说明如下:
- inUserData:通常是一个穿件用来保存音频队列和它的缓冲区状态信息的自定义结构、音频文件(AudioFileID类型)代表写入的文件、以及音频格式的信息
- inAQ 调用回调函数的音频队列
- inBuffer 进入回调函数的刚刚被填满的缓冲,根据inUserData中指定的格式格式化。
- inStartTime 采样的参考时间,正常的录制不会使用这个参数
- inNumberPacketDescriptions 是inPacketDescs下一个参数中描述符的数量。加入在录制一个VBR可变比特率音频(variable bitrate),音频队列会提供这个参数给回调函数,这个参数里的值可以让程序传递给AudioFileWritePackets函数。CBR(常量比特率)的录制不使用包描述符,会将inPacketDescs设置为NULL
- inPacketDescs 一组对应于缓冲区采样信息的包描述符。
播放音频时的回调函数:
AudioQueueOutputCallback (
void *inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer
);
1.5 编码和音频数据格式
采集和播放必然涉及编码解码组件,这个处理是在回调函数之前的,所以回调函数不需要处理编码解码的过程。
AudioQueue 的 AudioStreamBasicDescription 中有一个域用来描述音频数据格式。当你在 MFormatID 域中指定了格式之后,音频队列就会使用相应的解码器,然后指定相应的采样率和声道数。
下图是音频录制中进行音频转换的过程
音频转换- 程序告诉音频队列开始录制,并指定数据格式
- 音频队列获取新的音频数据,并根据指定的格式使用编解码器对其进行转换。然后,音频队列将调用该回调,并将其交给包含适当格式的音频数据的缓冲区
- 回调将格式化的音频数据写入磁盘。这样回调不需要了解数据格式
2. 实现步骤
总览:
- 定义一个自定义结构,包括状态、格式、缓冲区大小、保存路劲
- 编写回调函数
- 可选:指定每个缓冲区的大小
- 填充第一步的自定义结构
- 创建AudioQueue以及缓冲区,以及写入的文件
- 通知AudioQueue开始录制
- 录制完毕的时候通知停止录制,然后释放它,以及释放缓冲区
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
};
使用这个结构来管理音频格式和音频队列状态信息。
- 设置使用的音频队列缓冲区的数量,一般是3
- 一个AudioStreamBasicDescription结构(CoreAudioTypes.h),标识写入磁盘的音频数据的格式,音频队列也会使用它来指定mQueue域,mDataFormat域是由app业务测来初始化的。
- 创建的音频队列AudioQueueRef
- 音频队列所管理的音频队列缓冲区的指针数组
- 程序录制音频时写入的文件的音频文件对象
- 每个音频队列缓冲区的字节大小,它的值在随后的例子中的DeriveBufferSize函数中计算出来,它在音频队列创建之后,开始录制音频之前计算出来
- 从当前音频队列缓冲区写入文件的第一个包(packet)的索引
- 布尔值,用来指示音频队列是否在运行中
2.2 编写回调函数
接下来要编写回调函数,这个回调函数做两件事情:
- 接受刚刚填充好的缓冲区,取数据
- 将这个缓冲区返回缓冲队列重新利用
2.2.1 回调函数声明
在AudioQueue.h头文件中声明的AudioQueueInputCallback。
录制用音频队列回调函数声明:
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是由编码器产生的
2.2.2 缓冲区数据写入磁盘
这个回调函数使用AudioFile.h头文件中声明的AudioFileWritePackets函,
AudioFileWritePackets ( // 1
pAqData->mAudioFile, // 2
false, // 3
inBuffer->mAudioDataByteSize, // 4
inPacketDesc, // 5
pAqData->mCurrentPacket, // 6
&inNumPackets, // 7
inBuffer->mAudioData // 8
);
- AudioFileWritePackets 将缓冲区的内容写入音频数据文件
- pAqData表示音频文件对象(类型为:AudioFileID),pAqData变量是指向自定义结构的指针
- false表示在写入时不处理任何缓存
- 正在写入的音频数据的字节数。该inBuffer变量表示音频队列传递给回调的音频队列缓冲区
- 音频数据包描述的副本。值NULL表示不需要数据包描述(例如,对于CBR音频数据)
- 要写入的第一个数据包的数据包索引
- 输入时,要写入的数据包数。输出时,实际写入的数据包数
- 将新的音频数据写入音频文件
2.2.3 缓冲区入队
使用完缓冲区的数据之后,使得缓冲区重新入队
AudioQueueEnqueueBuffer ( // 1
pAqData->mQueue, // 2
inBuffer, // 3
0, // 4
NULL // 5
);
- 该AudioQueueEnqueueBuffer函数将音频体重阈值添加到音频长度的阈值。
- 将指定的音频队列缓冲区添加到的音频队列
- buffer
- 音频为音频数据的数据中的数据包描述数。设置为,0因为此参数未使用记录
- 数据包描述摘要,描述音频编码附件的数据。设置为,NULL因为此参数未使用记录
2.2.4 完整的 CallBack
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
);
}
- 自定义结构的实例化
- 数据包的数量
- 缓冲区的内容写入文件
- 如果成功写入数据,增加音频数据文件的数据包index值,准备写入下一个
- 如果已经停止就返回
- 把缓冲区入队
2.3 计算音频缓冲区大小
音频队列服务要求应用程序为您使用的音频队列缓冲区指定大小。下面的函数展示了一种方法。
它衍生出一个足够大的缓冲区来保存给定的音频数据。
这里的计算考虑了要录制的音频数据格式。格式包括可能影响缓冲区大小的所有因素,例如音频通道的数量。
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,
// 在 Mac OS X v10.5 应该使用:
// kAudioConverterPropertyMaximumOutputPacketSize
&maxPacketSize,
&maxVBRPacketSize
);
}
Float64 numBytesForTime =
ASBDescription.mSampleRate * maxPacketSize * seconds; // 8
*outBufferSize =
UInt32 (numBytesForTime < maxBufferSize ?
numBytesForTime : maxBufferSize); // 9
}
- 需要操作的这个音频队列
- 音频队列的AudioStreamBasicDescription结构
- 为每个音频队列缓冲区指定的大小,以音频的秒数为单位
- 输出时,每个音频队列缓冲区的大小,以字节为单位
- 音频队列缓冲区大小的上限,以字节为单位。在本例中,上限设置为320 KB。这相当于大约5秒钟的立体声、24位音频,采样率为96 kHz
- 对于CBR音频数据,从AudioStreamBasicDescription结构中获取(常量)数据包大小。使用此值作为最大数据包大小。该赋值的副作用是确定要记录的音频数据是CBR还是VBR。如果是VBR,则音频队列的AudioStreamBasicDescription结构将每个数据包的字节值列为0
- 对于VBR音频数据,查询音频队列以获得估计的最大数据包大小
- 派生缓冲区大小(以字节为单位)
- 如果需要,将缓冲区大小限制为先前设置的上限
2.4 特殊格式元数据的处理
一些压缩的音频格式,需要用包含音频元数据的结构,称之为 Magic Cookies,称之为馍干。如果需要录制这种格式的音频文件,必须先处理好 MC 数据结构,先从音频队列中获取MC,然后将其添加到音频文件中,再进行录制的操作。
下面这个函数就是从音频中途获取 MC 的信息,并将其替换为音频文件,代码需要在录制之前就调用此类函数,然后录制之后再调用,某些编码解码器会在录制停止的时候更新MC数据。
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
}
- 正在用于录制的音频队列。
- 你正在录制的音频文件。
- 指示此函数成功或失败的结果变量。
- 一个变量来保存神奇的cookie数据大小。
- 从音频队列获取magic cookie的数据大小,并将其存储在cookieSize变量中。
- 分配一个字节数组来保存魔法cookie信息。
- 通过查询音频队列的kaudioqueproperty\u MagicCookie属性获取magic cookie。
- 设置要录制到的音频文件的魔法cookie。AudioFileSetProperty函数在AudioFile.h头文件中声明。
- 释放临时cookie变量的内存。
- 返回此函数的成功或失败。
2.5 设置音频格式进行录制
如何为音频体制设置音频数据格式。音频格式使用此格式记录到文件。
要设置音频数据格式,需要指定:
- 音频数据格式类型(例如线性PCM,AAC等)
- 采样率(例如44.1 kHz)
- 音频通道数(例如2,用于立体声)
- 位深度(例如16位)
- 每个包的帧数(例如,线性PCM每包使用一帧)
- 音频文件类型(例如,CAF,AIFF等)
- 文件类型所需的音频数据格式的详细信息
下面这段代码说明了如何设置录音的音频格式,为每个属性使用固定的选项。
在产品代码中,通常允许用户指定音频格式的某些或所有方面。
无论哪种方法,目标都是填充 AQRecorderState 自定义结构的 mDataFormat 字段。
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;
- 创建AQRecorderState自定义结构的实例。结构的mDataFormat字段包含AudioStreamBasicDescription结构。mDataFormat字段中设置的值提供音频队列的音频格式的初始定义,该音频队列也是您录制到的文件的音频格式。在清单2-10中,您获得了一个更完整的音频格式规范,核心音频根据格式类型和文件类型提供给您
- 将音频数据格式类型定义为线性PCM。有关可用数据格式的完整列表,请参见核心音频数据类型参考
- 将采样率定义为44.1 kHz
- 将通道数定义为2
- 将每个通道的位深度定义为16
- 将每个数据包的字节数和每帧的字节数定义为4(即,每个采样2个通道乘以2个字节)
- 将每个数据包的帧数定义为1
- 将文件类型定义为AIFF。有关可用文件类型的完整列表,请参见AudioFile.h头文件中的音频文件类型枚举。可以指定已安装编解码器的任何文件类型,如使用编解码器和音频数据格式中所述
- 设置指定文件类型所需的格式标志
2.6 创建音频队列
2.6.1 创建音频队列
创建好了 Callback 和 音频格式 之后,就可以创建用于录制的音频队列了,创建的时候回使用到前面步骤配置好的回调、自定义结构和音频数据格式。
AudioQueueNewInput ( // 1
&aqData.mDataFormat, // 2
HandleInputBuffer, // 3
&aqData, // 4
NULL, // 5
kCFRunLoopCommonModes, // 6
0, // 7
&aqData.mQueue // 8
);
- AudioQueueNewInput 函数创建一个新的录制音频队列
- 音频数据格式
- 回调函数
- 自定义数据结构
- 调用回调的运行循环。使用 NULL 指定默认行为,其中回调将在音频队列内部的线程上调用。这是一个典型的用法,它允许音频队列在应用程序的用户界面线程等待用户输入停止录制时进行录制
- 可以调用回调的运行循环模式。通常使用 kCFRunLoopCommonModes 常量
- 保留。必须为0
- 输出时,新分配的录制音频队列
2.6.2 从音频队列中获取完整的音频格式
音频队列中可能比 AudioStreamBasicDescription 结构更完整,尤其是对于压缩格式。要获得完整的格式描述,调用 AudioQueueGetProperty 函数。创建要录制到的音频文件时使用完整的音频格式。
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
);
- 获取在查询音频队列的音频数据格式时要使用的预期属性值大小
- AudioQueueGetProperty 函数获取音频队列中指定属性的值
- 从中获取音频数据格式的音频队列
- 用于获取音频队列数据格式值的属性ID
- 输出时,以 AudioStreamBasicDescription 结构的形式从音频队列中获取的完整音频数据格式
- 输入时,AudioStreamBasicDescription 结构的预期大小。输出时,实际大小。录制应用程序不需要使用此值
2.7 创建音频文件
音频数据记录到这个文件中,需要使用自定义结构中的文件格式和文件格式规范
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
);
- CFURLCreateFromFileSystemRepresentation函数在CFURL.h头文件中声明,它创建一个CFURL对象,表示要记录到的文件。
- 使用NULL(或kCFAllocatorDefault)使用当前默认内存分配器。
- 要转换为CFURL对象的文件系统路径。在生产代码中,通常会从用户处获取filePath的值。
- 文件系统路径中的字节数。
- 值false表示filePath表示文件,而不是目录。
- AudioFile.h头文件中的AudioFileCreateWithURL函数创建新的音频文件或初始化现有文件。
- 创建新音频文件或在现有文件的情况下初始化的URL。URL是从步骤1中的CFURLCreateFromFileSystemRepresentation派生的。
- 新文件的文件类型。在本章的示例代码中,这是以前通过kAudioFileAIFFType文件类型常量设置为AIFF的。请参见设置录音的音频格式。
- 将记录到文件中的音频的数据格式,指定为AudioStreamBasicDescription结构。在本章的示例代码中,还设置了录音的音频格式。
- 如果文件已经存在,则删除该文件。
- 输出时,表示要录制到的音频文件的音频文件对象(AudioFileID类型)。
2.8 设置音频缓冲区Size
DeriveBufferSize ( // 1
aqData.mQueue, // 2
aqData.mDataFormat, // 3
0.5, // 4
&aqData.bufferByteSize // 5
);
- 调用前面声明的函数,计算缓冲区大小
- 正在为其设置缓冲区大小的音频队列
- 正在录制的文件的音频数据格式。请参见设置录音的音频格式
- 每个音频队列缓冲区应保留的音频秒数。这里设置的半秒通常是一个不错的选择
- 输出时,每个音频队列缓冲区的大小,以字节为单位。此值放置在音频队列的自定义结构中
2.9 准备好一组音频缓冲区
让音频队列准备好一组音频缓冲区。
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
);
}
- 循环缓冲区的数量,分配并入队每个音频队列缓冲区
- AudioQueueAllocateBuffer函数要求音频队列分配音频队列缓冲区
- 执行分配并拥有缓冲区的音频队列
- 正在分配的新音频队列缓冲区的大小(字节)。请参阅编写函数以导出录音音频队列缓冲区大小
- 输出时,新分配的音频队列缓冲区。指向缓冲区的指针放置在音频队列使用的自定义结构中
- audioqueuenbuffer函数将音频队列缓冲区添加到缓冲区队列的末尾
- 要向其添加缓冲区的音频队列
- 正在排队的音频队列缓冲区
- 将缓冲区入队时,此参数未使用
- 将缓冲区入队时,此参数未使用
2.10 录制
前面都已经准备好,录制的时候就会非常简单:
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
- 将数据包索引初始化为0,以便在音频文件开始时开始录制。
- 在自定义结构中设置标志,以指示音频队列正在运行。录制音频队列回调使用此标志。
- AudioQueueStart函数在自己的线程上启动音频队列。
- 要启动的音频队列。
- 使用NULL表示音频队列应立即开始录制。
- AudioQueueStop函数停止并重置录制音频队列。
- 要停止的音频队列。
- 使用true使用同步停止。有关同步和异步停止的说明,请参阅音频队列控制和状态。
- 在自定义结构中设置标志,以指示音频队列未运行。
2.11 善后首位
善后工作:
AudioQueueDispose ( // 1
aqData.mQueue, // 2
true // 3
);
AudioFileClose (aqData.mAudioFile); // 4
- AudioQueueDispose 处理音频队列和所有相关的资源,包括所有缓冲区
- 需要处理的音频队列
- true 表示 synchronously 立即回收
- 管理用于录制的文件,这个 AudioFileClose 函数在 AudioFile.h 头文件中声明
网友评论