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);
}
複製代碼