iOS中使用Audio Queue实现音频数据采集,直接采集PCM无损数据或AAC及其余压缩格式数据.ios
使用Audio Queue采集硬件输入端,如麦克风,其余外置具有麦克风功能设备(带麦的耳机,话筒等,前提是其自己要和苹果兼容).git
如上所示,咱们整体分为两大类,一个是负责采集的类,一个是负责作音频录制的类,你能够根据需求在适当时机启动,关闭Audio Queue, 而且在Audio Queue已经启动的状况下能够进行音频文件录制,前面需求仅仅须要以下四个API便可完成.github
// Start / Stop Audio Queue
[[XDXAudioQueueCaptureManager getInstance] startAudioCapture];
[[XDXAudioQueueCaptureManager getInstance] stopAudioCapture];
// Start / Stop Audio Record
[[XDXAudioQueueCaptureManager getInstance] startRecordFile];
[[XDXAudioQueueCaptureManager getInstance] stopRecordFile];
复制代码
#define kXDXAudioPCMFramesPerPacket 1
#define kXDXAudioPCMBitsPerChannel 16
复制代码
struct XDXRecorderInfo {
AudioStreamBasicDescription mDataFormat;
AudioQueueRef mQueue;
AudioQueueBufferRef mBuffers[kNumberBuffers];
};
typedef struct XDXRecorderInfo *XDXRecorderInfoType;
复制代码
@property (nonatomic, assign, readonly) BOOL isRunning;
@property (nonatomic, assign) BOOL isRecordVoice;
复制代码
由于Audio Queue中自己就是用纯C语言实现的,因此它会直接调用一些函数,咱们必需要理解函数跟OC方法的区别,以及指针的概念,由于函数中会出现一些相似&
运算符,这里能够简单给你们介绍下以便小白阅读. &
就是获取某个对象的内存地址,使用它主要为了知足让Audio Queue的API能够将其查询到的值直接赋给这段内存地址,好比下面会讲到的AudioSessionGetProperty
查询方法中就是这样将查询出来的值赋值给咱们定义的全局静态变量的.macos
SingletonH
,实现文件中使用SingletonM
便可,关于单例的实现自行百度.为何使用单例,由于iPhone中输入端只能接收一个音频输入设备,因此若是使用Audio Queue采集,该采集对象在应用程序声明周期内应该是单一存在的,因此使用单例实现.缓存
+ (void)initialize {
m_audioInfo = malloc(sizeof(struct XDXRecorderInfo));
}
复制代码
- (void)startAudioCapture {
[self startAudioCaptureWithAudioInfo:m_audioInfo
formatID:kAudioFormatMPEG4AAC // kAudioFormatLinearPCM
sampleRate:44100
channelCount:1
durationSec:0.05
isRunning:&_isRunning];
}
复制代码
须要注意的是,音频数据格式与硬件直接相关,若是想获取最高性能,最好直接使用硬件自己的采样率,声道数等音频属性,因此,如采样率,当咱们手动进行更改后,Audio Queue会在内部自行转换一次,虽然代码上没有感知,但必定程序上仍是下降了性能.bash
iOS中不支持直接设置双声道,若是想模拟双声道,能够自行填充音频数据,具体会在之后的文章中讲到,喜欢请持续关注.数据结构
理解AudioSessionGetProperty
函数,该函数代表查询当前硬件指定属性的值,以下,kAudioSessionProperty_CurrentHardwareSampleRate
为查询当前硬件采样率,kAudioSessionProperty_CurrentHardwareInputNumberChannels
为查询当前采集的声道数.由于本例中使用手动赋值方式更加灵活,因此没有使用查询到的值.函数
首先,你必须了解未压缩格式(PCM...)与压缩格式(AAC...). 使用iOS直接采集未压缩数据是能够直接拿到硬件采集到的数据,而若是直接设置如AAC这样的压缩数据格式,其原理是Audio Queue在内部帮咱们作了一次转换,具体原理在本文开篇中的阅读前提中去查阅.oop
使用PCM数据格式必须设置采样值的flag:mFormatFlags
,每一个声道中采样的值换算成二进制的位宽mBitsPerChannel
,iOS中每一个声道使用16位的位宽,每一个包中有多少帧mFramesPerPacket
,对于PCM数据而言,由于其未压缩,因此每一个包中仅有1帧数据.每一个包中有多少字节数(即每一帧中有多少字节数),能够根据以下简单计算得出post
注意,若是是其余压缩数据格式,大多数不须要单独设置以上参数,默认为0.这是由于对于压缩数据而言,每一个音频采样包中压缩的帧数以及每一个音频采样包压缩出来的字节数多是不一样的,因此咱们没法预知进行设置,就像mFramesPerPacket
参数,由于压缩出来每一个包具体有多少帧只有压缩完成后才能得知.
audioInfo->mDataFormat = [self getAudioFormatWithFormatID:formatID
sampleRate:sampleRate
channelCount:channelCount];
-(AudioStreamBasicDescription)getAudioFormatWithFormatID:(UInt32)formatID sampleRate:(Float64)sampleRate channelCount:(UInt32)channelCount {
AudioStreamBasicDescription dataFormat = {0};
UInt32 size = sizeof(dataFormat.mSampleRate);
// Get hardware origin sample rate. (Recommended it)
Float64 hardwareSampleRate = 0;
AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareSampleRate,
&size,
&hardwareSampleRate);
// Manual set sample rate
dataFormat.mSampleRate = sampleRate;
size = sizeof(dataFormat.mChannelsPerFrame);
// Get hardware origin channels number. (Must refer to it)
UInt32 hardwareNumberChannels = 0;
AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareInputNumberChannels,
&size,
&hardwareNumberChannels);
dataFormat.mChannelsPerFrame = channelCount;
// Set audio format
dataFormat.mFormatID = formatID;
// Set detail audio format params
if (formatID == kAudioFormatLinearPCM) {
dataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
dataFormat.mBitsPerChannel = kXDXAudioPCMBitsPerChannel;
dataFormat.mBytesPerPacket = dataFormat.mBytesPerFrame = (dataFormat.mBitsPerChannel / 8) * dataFormat.mChannelsPerFrame;
dataFormat.mFramesPerPacket = kXDXAudioPCMFramesPerPacket;
}else if (formatID == kAudioFormatMPEG4AAC) {
dataFormat.mFormatFlags = kMPEG4Object_AAC_Main;
}
NSLog(@"Audio Recorder: starup PCM audio encoder:%f,%d",sampleRate,channelCount);
return dataFormat;
}
复制代码
上面步骤中咱们已经拿到音频流数据格式,使用AudioQueueNewInput
函数能够将建立出来的Audio Queue对象赋值给咱们定义的全局变量,另外还指定了CaptureAudioDataCallback
采集音频数据回调函数的名称.回调函数的定义必须听从以下格式.由于系统会将采集到值赋值给此函数中的参数,函数名称能够本身指定.
typedef void (*AudioQueueInputCallback)(
void * __nullable inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp * inStartTime,
UInt32 inNumberPacketDescriptions,
const AudioStreamPacketDescription * __nullable inPacketDescs);
复制代码
// New queue
OSStatus status = AudioQueueNewInput(&audioInfo->mDataFormat,
CaptureAudioDataCallback,
(__bridge void *)(self),
NULL,
kCFRunLoopCommonModes,
0,
&audioInfo->mQueue);
if (status != noErr) {
NSLog(@"Audio Recorder: AudioQueueNewInput Failed status:%d \n",(int)status);
return NO;
}
复制代码
如下是AudioQueueNewInput
函数的定义
extern OSStatus
AudioQueueNewInput( const AudioStreamBasicDescription *inFormat,
AudioQueueInputCallback inCallbackProc,
void * __nullable inUserData,
CFRunLoopRef __nullable inCallbackRunLoop,
CFStringRef __nullable inCallbackRunLoopMode,
UInt32 inFlags,
AudioQueueRef __nullable * __nonnull outAQ) API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
复制代码
用如下方法验证获取到音频格式是否与咱们设置的相符.
// Set audio format for audio queue
UInt32 size = sizeof(audioInfo->mDataFormat);
status = AudioQueueGetProperty(audioInfo->mQueue,
kAudioQueueProperty_StreamDescription,
&audioInfo->mDataFormat,
&size);
if (status != noErr) {
NSLog(@"Audio Recorder: get ASBD status:%d",(int)status);
return NO;
}
复制代码
该计算要区分压缩与未压缩数据.
只能进行估算,即用采样率与采样时间相乘,可是须要注意由于直接设置采集压缩数据(如AAC),至关因而Audio Queue在内部本身进行一次转换,而像AAC这样的压缩数据,每次至少须要1024个采样点(即采样时间最小为23.219708 ms)才能完成一个压缩,因此咱们不能将buffer size设置太小,不信能够本身尝试,若是设置太小直接crash.
而咱们计算出来的这个大小只是原始数据的大小,通过压缩后每每低于咱们计算出来的这个值.能够在回调中打印查看.
对于未压缩数据,咱们时能够经过计算精确得出采样的大小. 即以下公式
// Set capture data size
UInt32 bufferByteSize;
if (audioInfo->mDataFormat.mFormatID == kAudioFormatLinearPCM) {
int frames = (int)ceil(durationSec * audioInfo->mDataFormat.mSampleRate);
bufferByteSize = frames*audioInfo->mDataFormat.mBytesPerFrame*audioInfo->mDataFormat.mChannelsPerFrame;
}else {
// AAC durationSec MIN: 23.219708 ms
bufferByteSize = durationSec * audioInfo->mDataFormat.mSampleRate;
if (bufferByteSize < 1024) {
bufferByteSize = 1024;
}
}
复制代码
关于audio queue,能够理解为一个队列的数据结构,buffer就是队列中的每一个结点.具体设计请参考文中阅读前提中的概念篇.
官方建议咱们将audio queue中的buffer设置为3个,由于,一个用于准备去装数据,一个正在使用的数据以及若是出现I/0缓存时还留有一个备用数据,设置过少,采集效率可能变低,设置过多浪费内存,3个刚恰好.
以下操做就是先为队列中每一个buffer分配内存,而后将分配好内存的buffer作入队操做,准备接收音频数据
// Allocate and Enqueue
for (int i = 0; i != kNumberBuffers; i++) {
status = AudioQueueAllocateBuffer(audioInfo->mQueue,
bufferByteSize,
&audioInfo->mBuffers[i]);
if (status != noErr) {
NSLog(@"Audio Recorder: Allocate buffer status:%d",(int)status);
}
status = AudioQueueEnqueueBuffer(audioInfo->mQueue,
audioInfo->mBuffers[i],
0,
NULL);
if (status != noErr) {
NSLog(@"Audio Recorder: Enqueue buffer status:%d",(int)status);
}
}
复制代码
第二个参数设置为NULL表示当即开始采集数据.
status = AudioQueueStart(audioInfo->mQueue, NULL);
if (status != noErr) {
NSLog(@"Audio Recorder: Audio Queue Start failed status:%d \n",(int)status);
return NO;
}else {
NSLog(@"Audio Recorder: Audio Queue Start successful");
*isRunning = YES;
return YES;
}
复制代码
若是上面的操做所有执行成功,最终系统会将采集到的音频数据以回调函数形式返回给开发者,以下.
经过回调函数,就能够拿到当前采集到的音频数据,你能够对数据作你须要的任何自定义操做.如下以写入文件为例,咱们在拿到音频数据后,将其写入音频文件.
static void CaptureAudioDataCallback(void * inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp * inStartTime,
UInt32 inNumPackets,
const AudioStreamPacketDescription* inPacketDesc) {
XDXAudioQueueCaptureManager *instance = (__bridge XDXAudioQueueCaptureManager *)inUserData;
/* Test audio fps
static Float64 lastTime = 0;
Float64 currentTime = CMTimeGetSeconds(CMClockMakeHostTimeFromSystemUnits(inStartTime->mHostTime))*1000;
NSLog(@"Test duration - %f",currentTime - lastTime);
lastTime = currentTime;
*/
// NSLog(@"Test data: %d,%d,%d,%d",inBuffer->mAudioDataByteSize,inNumPackets,inPacketDesc->mDataByteSize,inPacketDesc->mVariableFramesInPacket);
if (instance.isRecordVoice) {
UInt32 bytesPerPacket = m_audioInfo->mDataFormat.mBytesPerPacket;
if (inNumPackets == 0 && bytesPerPacket != 0) {
inNumPackets = inBuffer->mAudioDataByteSize / bytesPerPacket;
}
[[XDXAudioFileHandler getInstance] writeFileWithInNumBytes:inBuffer->mAudioDataByteSize
ioNumPackets:inNumPackets
inBuffer:inBuffer->mAudioData
inPacketDesc:inPacketDesc];
}
if (instance.isRunning) {
AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
}
}
复制代码
AudioQueueStop
: 中止当前audio queueAudioQueueFreeBuffer
: 释放audio queue中每一个bufferAudioQueueDispose
: 释放audio queue如下函数调用具备前后顺序,咱们必须先停掉audio queue,才能释放其中buffer的内存,最后再将整个audio queue完全释放.
-(BOOL)stopAudioQueueRecorderWithAudioInfo:(XDXRecorderInfoType)audioInfo isRunning:(BOOL *)isRunning {
if (*isRunning == NO) {
NSLog(@"Audio Recorder: Stop recorder repeat \n");
return NO;
}
if (audioInfo->mQueue) {
OSStatus stopRes = AudioQueueStop(audioInfo->mQueue, true);
if (stopRes == noErr){
for (int i = 0; i < kNumberBuffers; i++)
AudioQueueFreeBuffer(audioInfo->mQueue, audioInfo->mBuffers[i]);
}else{
NSLog(@"Audio Recorder: stop AudioQueue failed.");
return NO;
}
OSStatus status = AudioQueueDispose(audioInfo->mQueue, true);
if (status != noErr) {
NSLog(@"Audio Recorder: Dispose failed: %d",status);
return NO;
}else {
audioInfo->mQueue = NULL;
*isRunning = NO;
// AudioFileClose(mRecordFile);
NSLog(@"Audio Recorder: stop AudioQueue successful.");
return YES;
}
}
return NO;
}
复制代码
此部分可参考另外一篇文章: 音频文件录制
当音频数据为压缩数据时,原本能够经过一个函数求出每一个音频数据包中最大的音频数据大小,以进一步求出buffer size,但不知为什么调用一直失败,因此在上述第6步中我才换了种方式估算.若是有人知道能够评论补充下,感谢.
UInt32 propertySize = sizeof(maxPacketSize);
OSStatus status = AudioQueueGetProperty(audioQueue,
kAudioQueueProperty_MaximumOutputPacketSize,
&maxPacketSize,
&propertySize);
if (status != noErr) {
NSLog(@"%s: get max output packet size failed:%d",__func__,status);
}
复制代码