在iOS和Mac OS X中,音訊佇列Audio Queues是一個用來錄製和播放音訊的軟體物件,也就是說,可以用來錄音和播放,錄音能夠獲取實時的PCM原始音訊資料。
資料介紹
(1)In CBR (constant bit rate) formats, such as linear PCM and IMA/ADPCM, all packets are the same size.
(2)In VBR (variable bit rate) formats, such as AAC, Apple Lossless, and MP3, all packets have the same number of frames but the number of bits in each sample value can vary.
(3)In VFR (variable frame rate) formats, packets have a varying number of frames. There are no commonly used formats of this type.
-(void)copyEncoderCookieToFile
{
// Grab the cookie from the converter and write it to the destination file.
UInt32 cookieSize = 0;
OSStatus error = AudioConverterGetPropertyInfo(_encodeConvertRef, kAudioConverterCompressionMagicCookie, &cookieSize, NULL);
// If there is an error here, then the format doesn't have a cookie - this is perfectly fine as som formats do not.
// log4cplus_info("cookie","cookie status:%d %d",(int)error, cookieSize);
if (error == noErr && cookieSize != 0) {
char *cookie = (char *)malloc(cookieSize * sizeof(char));
// UInt32 *cookie = (UInt32 *)malloc(cookieSize * sizeof(UInt32));
error = AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCompressionMagicCookie, &cookieSize, cookie);
// log4cplus_info("cookie","cookie size status:%d",(int)error);
if (error == noErr) {
error = AudioFileSetProperty(mRecordFile, kAudioFilePropertyMagicCookieData, cookieSize, cookie);
// log4cplus_info("cookie","set cookie status:%d ",(int)error);
if (error == noErr) {
UInt32 willEatTheCookie = false;
error = AudioFileGetPropertyInfo(mRecordFile, kAudioFilePropertyMagicCookieData, NULL, &willEatTheCookie);
printf("Writing magic cookie to destination file: %u\n cookie:%d \n", (unsigned int)cookieSize, willEatTheCookie);
} else {
printf("Even though some formats have cookies, some files don't take them and that's OK\n");
}
} else {
printf("Could not Get kAudioConverterCompressionMagicCookie from Audio Converter!\n");
}
free(cookie);
}
}
複製程式碼
Magic cookie 是一種不透明的資料格式,它和壓縮資料檔案與流聯絡密切,如果檔案資料為CBR格式(無損),則不需要新增頭部資訊,如果為VBR需要新增,// if collect CBR needn't set magic cookie , if collect VBR should set magic cookie, if needn't to convert format that can be setting by audio queue directly.
(5).AudioQueue中註冊的回撥函式
// AudioQueue中註冊的回撥函式
static void inputBufferHandler(void * inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp * inStartTime,
UInt32 inNumPackets,
const AudioStreamPacketDescription* inPacketDesc) {
// 相當於本類物件例項
TVURecorder *recoder = (TVURecorder *)inUserData;
/*
inNumPackets 總包數:音訊佇列緩衝區大小 (在先前估算快取區大小為kXDXRecoderAACFramesPerPacket*2)/ (dataFormat.mFramesPerPacket (採集資料每個包中有多少幀,此處在初始化設定中為1) * dataFormat.mBytesPerFrame(每一幀中有多少個位元組,此處在初始化設定中為每一幀中兩個位元組)),所以可以根據該公式計算捕捉PCM資料時inNumPackets。
注意:如果採集的資料是PCM需要將dataFormat.mFramesPerPacket設定為1,而本例中最終要的資料為AAC,因為本例中使用的轉換器只有每次傳入1024幀才能開始工作,所以在AAC格式下需要將mFramesPerPacket設定為1024.也就是採集到的inNumPackets為1,在轉換器中傳入的inNumPackets應該為AAC格式下預設的1,在此後寫入檔案中也應該傳的是轉換好的inNumPackets,如果有特殊需求需要將採集的資料量小於1024,那麼需要將每次捕捉到的資料先預先儲存在一個buffer中,等到攢夠1024幀再進行轉換。
*/
// collect pcm data,可以在此儲存
// First case : collect data not is 1024 frame, if collect data not is 1024 frame, we need to save data to pcm_buffer untill 1024 frame
memcpy(pcm_buffer+pcm_buffer_size, inBuffer->mAudioData, inBuffer->mAudioDataByteSize);
pcm_buffer_size = pcm_buffer_size + inBuffer->mAudioDataByteSize;
if(inBuffer->mAudioDataByteSize != kXDXRecoderAACFramesPerPacket*2)
NSLog(@"write pcm buffer size:%d, totoal buff size:%d", inBuffer->mAudioDataByteSize, pcm_buffer_size);
frameCount++;
// Second case : If the size of the data collection is not required, we can let mic collect 1024 frame so that don't need to write firtst case, but it is recommended to write the above code because of agility
// if collect data is added to 1024 frame
if(frameCount == totalFrames) {
AudioBufferList *bufferList = convertPCMToAAC(recoder);
pcm_buffer_size = 0;
frameCount = 0;
// free memory
free(bufferList->mBuffers[0].mData);
free(bufferList);
// begin write audio data for record audio only
// 出隊
AudioQueueRef queue = recoder.mQueue;
if (recoder.isRunning) {
AudioQueueEnqueueBuffer(queue, inBuffer, 0, NULL);
}
}
}
複製程式碼
inNumPackets是inPacketDescs引數中包描述符(packet descriptions)的數量,如果你正在錄製一個VBR(可變位元率(variable bitrate))格式, 音訊佇列將會提供這個引數給你的回撥函式,這個引數可以讓你傳遞給AudioFileWritePackets函式. CBR (常量位元率(constant bitrate)) 格式不使用包描述符。對於CBR錄製,音訊佇列會設定這個引數並且將inPacketDescs這個引數設定為NULL,官方解釋為The number of packets of audio data sent to the callback in the inBuffer parameter.
// PCM -> AAC
AudioBufferList* convertPCMToAAC (AudioQueueBufferRef inBuffer, XDXRecorder *recoder) {
UInt32 maxPacketSize = 0;
UInt32 size = sizeof(maxPacketSize);
OSStatus status;
status = AudioConverterGetProperty(_encodeConvertRef,
kAudioConverterPropertyMaximumOutputPacketSize,
&size,
&maxPacketSize);
// log4cplus_info("AudioConverter","kAudioConverterPropertyMaximumOutputPacketSize status:%d \n",(int)status);
// 初始化一個bufferList儲存資料
AudioBufferList *bufferList = (AudioBufferList *)malloc(sizeof(AudioBufferList));
bufferList->mNumberBuffers = 1;
bufferList->mBuffers[0].mNumberChannels = _targetDes.mChannelsPerFrame;
bufferList->mBuffers[0].mData = malloc(maxPacketSize);
bufferList->mBuffers[0].mDataByteSize = pcm_buffer_size;
AudioStreamPacketDescription outputPacketDescriptions;
/*
inNumPackets設定為1表示編碼產生1幀資料即返回,官方:On entry, the capacity of outOutputData expressed in packets in the converter's output format. On exit, the number of packets of converted data that were written to outOutputData. 在輸入表示輸出資料的最大容納能力 在轉換器的輸出格式上,在轉換完成時表示多少個包被寫入
*/
UInt32 inNumPackets = 1;
status = AudioConverterFillComplexBuffer(_encodeConvertRef,
encodeConverterComplexInputDataProc, // 填充資料的回撥函式
pcm_buffer, // 音訊佇列緩衝區中資料
&inNumPackets,
bufferList, // 成功後將值賦給bufferList
&outputPacketDescriptions); // 輸出包包含的一些資訊
log4cplus_info("AudioConverter","set AudioConverterFillComplexBuffer status:%d",(int)status);
if (recoder.needsVoiceDemo) {
// if inNumPackets set not correct, file will not normally play. 將轉換器轉換出來的包寫入檔案中,inNumPackets表示寫入檔案的起始位置
OSStatus status = AudioFileWritePackets(recoder.mRecordFile,
FALSE,
bufferList->mBuffers[0].mDataByteSize,
&outputPacketDescriptions,
recoder.mRecordPacket,
&inNumPackets,
bufferList->mBuffers[0].mData);
// log4cplus_info("write file","write file status = %d",(int)status);
recoder.mRecordPacket += inNumPackets; // Used to record the location of the write file,用於記錄寫入檔案的位置
}
return bufferList;
}
複製程式碼
1). AudioUnit是 iOS提供的為了支援混音,均衡,格式轉換,實時輸入輸出用於錄製,回放,離線渲染和實時回話(VOIP),這讓我們可以動態載入和使用,即從iOS應用程式中接收這些強大而靈活的外掛。它是iOS音訊中最低層,所以除非你需要合成聲音的實時播放,低延遲的I/O,或特定聲音的特定特點。
Audio unit scopes and elements :
上圖是一個AudioUnit的組成結構,A scope 主要使用到的輸入kAudioUnitScope_Input和輸出kAudioUnitScope_Output。Element 是巢狀在audio unit scope的程式設計上下文。
To find an audio unit at runtime, start by specifying its type, subtype, and manufacturer keys in an audio component description data structure. You do this whether using the audio unit or audio processing graph API.
- (void)initBuffer {
// 禁用AudioUnit預設的buffer而使用我們自己寫的全域性BUFFER,用來接收每次採集的PCM資料,Disable AU buffer allocation for the recorder, we allocate our own.
UInt32 flag = 0;
OSStatus status = AudioUnitSetProperty(_audioUnit,
kAudioUnitProperty_ShouldAllocateBuffer,
kAudioUnitScope_Output,
INPUT_BUS,
&flag,
sizeof(flag));
if (status != noErr) {
// log4cplus_info("Audio Recoder", "couldn't AllocateBuffer of AudioUnitCallBack, status : %d \n",status);
}
_buffList = (AudioBufferList*)malloc(sizeof(AudioBufferList));
_buffList->mNumberBuffers = 1;
_buffList->mBuffers[0].mNumberChannels = dataFormat.mChannelsPerFrame;
_buffList->mBuffers[0].mDataByteSize = kTVURecoderPCMMaxBuffSize * sizeof(short);
_buffList->mBuffers[0].mData = (short *)malloc(sizeof(short) * kTVURecoderPCMMaxBuffSize);
}
複製程式碼
解析
本例通過禁用AudioUnit預設的buffer而使用我們自己寫的全域性BUFFER,用來接收每次採集的PCM資料,Disable AU buffer allocation for the recorder, we allocate our own.還有一種寫法是可以使用回撥中提供的ioData儲存採集的資料,這裡使用全域性的buff是為了供其他地方使用,可根據需要自行決定採用哪種方式,若不採用全域性buffer則不可採用上述禁用操作。
// 因為本例只做錄音功能,未實現播放功能,所以沒有設定播放相關設定。
- (void)setAudioUnitPropertyAndFormat {
OSStatus status;
[self setUpRecoderWithFormatID:kAudioFormatLinearPCM];
// 應用audioUnit設定的格式
status = AudioUnitSetProperty(_audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output,
INPUT_BUS,
&dataFormat,
sizeof(dataFormat));
if (status != noErr) {
// log4cplus_info("Audio Recoder", "couldn't set the input client format on AURemoteIO, status : %d \n",status);
}
// 去除回聲開關
UInt32 echoCancellation;
AudioUnitSetProperty(_audioUnit,
kAUVoiceIOProperty_BypassVoiceProcessing,
kAudioUnitScope_Global,
0,
&echoCancellation,
sizeof(echoCancellation));
// AudioUnit輸入端預設是關閉,需要將他開啟
UInt32 flag = 1;
status = AudioUnitSetProperty(_audioUnit,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Input,
INPUT_BUS,
&flag,
sizeof(flag));
if (status != noErr) {
// log4cplus_info("Audio Recoder", "could not enable input on AURemoteIO, status : %d \n",status);
}
}
-(void)setUpRecoderWithFormatID:(UInt32)formatID {
// Notice : The settings here are official recommended settings,can be changed according to specific requirements. 此處的設定為官方推薦設定,可根據具體需求修改部分設定
//setup auido sample rate, channel number, and format ID
memset(&dataFormat, 0, sizeof(dataFormat));
UInt32 size = sizeof(dataFormat.mSampleRate);
AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareSampleRate,
&size,
&dataFormat.mSampleRate);
dataFormat.mSampleRate = kXDXAudioSampleRate;
size = sizeof(dataFormat.mChannelsPerFrame);
AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareInputNumberChannels,
&size,
&dataFormat.mChannelsPerFrame);
dataFormat.mFormatID = formatID;
dataFormat.mChannelsPerFrame = 1;
if (formatID == kAudioFormatLinearPCM) {
if (self.releaseMethod == XDXRecorderReleaseMethodAudioQueue) {
dataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
}elseif (self.releaseMethod == XDXRecorderReleaseMethodAudioQueue) {
dataFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
}
dataFormat.mBitsPerChannel = 16;
dataFormat.mBytesPerPacket = dataFormat.mBytesPerFrame = (dataFormat.mBitsPerChannel / 8) * dataFormat.mChannelsPerFrame;
dataFormat.mFramesPerPacket = kXDXRecoderPCMFramesPerPacket; // 用AudioQueue採集pcm需要這麼設定
}
}
複製程式碼
-(void)copyEncoderCookieToFile
{
// Grab the cookie from the converter and write it to the destination file.
UInt32 cookieSize = 0;
OSStatus error = AudioConverterGetPropertyInfo(_encodeConvertRef, kAudioConverterCompressionMagicCookie, &cookieSize, NULL);
// If there is an error here, then the format doesn't have a cookie - this is perfectly fine as som formats do not.
// log4cplus_info("cookie","cookie status:%d %d",(int)error, cookieSize);
if (error == noErr && cookieSize != 0) {
char *cookie = (char *)malloc(cookieSize * sizeof(char));
// UInt32 *cookie = (UInt32 *)malloc(cookieSize * sizeof(UInt32));
error = AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCompressionMagicCookie, &cookieSize, cookie);
// log4cplus_info("cookie","cookie size status:%d",(int)error);
if (error == noErr) {
error = AudioFileSetProperty(mRecordFile, kAudioFilePropertyMagicCookieData, cookieSize, cookie);
// log4cplus_info("cookie","set cookie status:%d ",(int)error);
if (error == noErr) {
UInt32 willEatTheCookie = false;
error = AudioFileGetPropertyInfo(mRecordFile, kAudioFilePropertyMagicCookieData, NULL, &willEatTheCookie);
printf("Writing magic cookie to destination file: %u\n cookie:%d \n", (unsigned int)cookieSize, willEatTheCookie);
} else {
printf("Even though some formats have cookies, some files don't take them and that's OK\n");
}
} else {
printf("Could not Get kAudioConverterCompressionMagicCookie from Audio Converter!\n");
}
free(cookie);
}
}
複製程式碼
解析
Magic cookie 是一種不透明的資料格式,它和壓縮資料檔案與流聯絡密切,如果檔案資料為CBR格式(無損),則不需要新增頭部資訊,如果為VBR需要新增,// if collect CBR needn't set magic cookie , if collect VBR should set magic cookie, if needn't to convert format that can be setting by audio queue directly.