一、前言
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