需求:iOS中使用Audio File 實現音訊檔案錄製.
實現原理: 使用Audio File中的API可以將我們採集到的音訊資料錄製成音訊檔案,這裡採集到的資料包括從Audio Queue/Audio Unit直接採集或Audio Converter間接轉換得到的音訊資料.
閱讀前提:
- 本文需要藉助三種資料來源以實現音訊資料錄製: Audio Queue, Audio Converter
- Core Audio基本原理:簡書,掘金,部落格
- 音訊採集: Audio Queue 簡書,掘金,部落格
- 音訊採集: Audio Unit 簡書,掘金,部落格
- C,C++基本知識
本文直接為實戰篇,如需瞭解理論基礎參考上述連結中的內容,本文側重於實戰中注意點.
本專案需要藉助Audio Queue, Audio Unit的採集,才能實現錄製.所以提供以下兩個Demo.
GitHub地址(附程式碼) : Audio Queue錄製, Audio Unit錄製
簡書地址 : Audio File Record
掘金地址 : Audio File Record
部落格地址 : Audio File Record
具體實現
1. 建立音訊檔案
這裡使用當前格式化時間作為檔名,命名衝突.
下面主要程式碼為建立一個用於存放聲音的音訊檔案,主要是在沙盒中建立一個目錄(名為Voice)存放音訊檔案.注意,我們一定要先將資料夾建立出來,否則在呼叫後面AudioFileCreateWithURL
函式時將報錯.
- (NSString *)createFilePath {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateFormat = @"yyyy_MM_dd__HH_mm_ss";
NSString *date = [dateFormatter stringFromDate:[NSDate date]];
NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask,
YES);
NSString *documentPath = [[searchPaths objectAtIndex:0] stringByAppendingPathComponent:@"Voice"];
// 先建立子目錄. 注意,若果直接呼叫AudioFileCreateWithURL建立一個不存在的目錄建立檔案會失敗
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:documentPath]) {
[fileManager createDirectoryAtPath:documentPath withIntermediateDirectories:YES attributes:nil error:nil];
}
NSString *fullFileName = [NSString stringWithFormat:@"%@.caf",date];
NSString *filePath = [documentPath stringByAppendingPathComponent:fullFileName];
return filePath;
}
複製程式碼
2. 建立Audio File
通過上面建立的url,再加上我們要建立的檔案型別(iOS中CAF格式檔案可以存放任意型別音訊資料),音訊流的ASBD格式,檔案特性的flag,這裡設定kAudioFileFlags_EraseFile
表明CreateURL呼叫將清空現有檔案的內容,如果未設定,則如果檔案已存在則CreateURL呼叫將失敗.
- (AudioFileID)createAudioFileWithFilePath:(NSString *)filePath AudioDesc:(AudioStreamBasicDescription)audioDesc {
CFURLRef url = CFURLCreateWithString(kCFAllocatorDefault, (CFStringRef)filePath, NULL);
NSLog(@"Audio Recorder: record file path:%@",filePath);
AudioFileID audioFile;
// create the audio file
OSStatus status = AudioFileCreateWithURL(url,
kAudioFileCAFType,
&audioDesc,
kAudioFileFlags_EraseFile,
&audioFile);
if (status != noErr) {
NSLog(@"Audio Recorder: AudioFileCreateWithURL Failed, status:%d",(int)status);
}
CFRelease(url);
return audioFile;
}
複製程式碼
3. 設定magic cookie
magic cookie: 可以理解成是檔案的頭資訊,包含音訊檔案播放需要的一些必要資訊, magic cookie塊包含某些音訊資料格式(例如MPEG-4 AAC)所需的補充資料,用於解碼音訊資料。如果CAF檔案中包含的音訊資料格式需要magic cookie資料,則該檔案必須具有此塊。
在這裡分為兩種情況,如果錄製檔案資料CBR(未壓縮資料格式:PCM...),則不需要設定magic cookie, 如果錄製檔案資料VBR(壓縮資料格式:AAC...),則需要設定magic cookie.
注意: 採用不同技術採集到的音訊,設定magic cookie的方式是不同的.
- Audio Queue 設定magic cookie
首先使用kAudioQueueProperty_MagicCookie
屬性獲取當前audio queue是否含有magic cookie,如果有,返回magic cookie長度,然後為它分配一段記憶體就可以呼叫kAudioQueueProperty_MagicCookie
獲取audio queue中的magic cookie,最後,將magic cookie通過kAudioFilePropertyMagicCookieData
屬性設定到audio file中即可.
- (void)copyEncoderCookieToFileByAudioQueue:(AudioQueueRef)inQueue inFile:(AudioFileID)inFile {
OSStatus result = noErr;
UInt32 cookieSize;
result = AudioQueueGetPropertySize (
inQueue,
kAudioQueueProperty_MagicCookie,
&cookieSize
);
if (result == noErr) {
char* magicCookie = (char *) malloc (cookieSize);
result =AudioQueueGetProperty (
inQueue,
kAudioQueueProperty_MagicCookie,
magicCookie,
&cookieSize
);
if (result == noErr) {
result = AudioFileSetProperty (
inFile,
kAudioFilePropertyMagicCookieData,
cookieSize,
magicCookie
);
if (result == noErr) {
NSLog(@"set Magic cookie successful.");
}else {
NSLog(@"set Magic cookie failed.");
}
}else {
NSLog(@"get Magic cookie failed.");
}
free (magicCookie);
}else {
NSLog(@"Magic cookie: get size failed.");
}
}
複製程式碼
- Audio Converter 設定magic cookie
當使用Audio Unit採集音訊資料時,我們無法直接採集AAC型別的資料,需要藉助Audio Converter,原理同上,即從Audio Converter中獲取Magic cookie並設定給audio file.
-(void)copyEncoderCookieToFileByAudioConverter:(AudioConverterRef)audioConverter inFile:(AudioFileID)inFile {
// Grab the cookie from the converter and write it to the destination file.
UInt32 cookieSize = 0;
OSStatus error = AudioConverterGetPropertyInfo(audioConverter, kAudioConverterCompressionMagicCookie, &cookieSize, NULL);
if (error == noErr && cookieSize != 0) {
char *cookie = (char *)malloc(cookieSize * sizeof(char));
error = AudioConverterGetProperty(audioConverter, kAudioConverterCompressionMagicCookie, &cookieSize, cookie);
if (error == noErr) {
error = AudioFileSetProperty(inFile, kAudioFilePropertyMagicCookieData, cookieSize, cookie);
if (error == noErr) {
UInt32 willEatTheCookie = false;
error = AudioFileGetPropertyInfo(inFile, kAudioFilePropertyMagicCookieData, NULL, &willEatTheCookie);
if (error == noErr) {
NSLog(@"%@:%s - Writing magic cookie to destination file: %u cookie:%d \n",kModuleName,__func__, (unsigned int)cookieSize, willEatTheCookie);
}else {
NSLog(@"%@:%s - Could not Writing magic cookie to destination file status:%d \n",kModuleName,__func__,(int)error);
}
} else {
NSLog(@"%@:%s - Even though some formats have cookies, some files don't take them and that's OK,set cookie status:%d \n",kModuleName,__func__,(int)error);
}
} else {
NSLog(@"%@:%s - Could not Get kAudioConverterCompressionMagicCookie from Audio Converter!\n status:%d ",kModuleName,__func__,(int)error);
}
free(cookie);
}else {
// If there is an error here, then the format doesn't have a cookie - this is perfectly fine as som formats do not.
NSLog(@"%@:%s - cookie status:%d, %d \n",kModuleName,__func__,(int)error, cookieSize);
}
}
複製程式碼
4. 將資料寫入檔案.
通過AudioFileWritePackets
可以將音訊資料寫入檔案.
- (void)writeFileWithInNumBytes:(UInt32)inNumBytes ioNumPackets:(UInt32 )ioNumPackets inBuffer:(const void *)inBuffer inPacketDesc:(const AudioStreamPacketDescription*)inPacketDesc {
if (!m_recordFile) {
return;
}
// AudioStreamPacketDescription outputPacketDescriptions;
OSStatus status = AudioFileWritePackets(m_recordFile,
false,
inNumBytes,
inPacketDesc,
m_recordCurrentPacket,
&ioNumPackets,
inBuffer);
if (status == noErr) {
m_recordCurrentPacket += ioNumPackets; // 用於記錄起始位置
}else {
NSLog(@"%@:%s - write file status = %d \n",kModuleName,__func__,(int)status);
}
}
複製程式碼
該函式定義如下.
- inUseCache: 寫入資料時是否快取資料
- inNumBytes: 寫入資料的大小
- inPacketDescriptions: VBR格式下音訊資料包的描述資訊
- inStartingPacket: 每次從第多少個包開始寫入,累加過程,所以需要記錄
- ioNumPackets:當前這次寫入多少個資料包
- inBuffer: 寫入的音訊資料
extern OSStatus
AudioFileWritePackets ( AudioFileID inAudioFile,
Boolean inUseCache,
UInt32 inNumBytes,
const AudioStreamPacketDescription * __nullable inPacketDescriptions,
SInt64 inStartingPacket,
UInt32 *ioNumPackets,
const void *inBuffer) API_AVAILABLE(macos(10.2), ios(2.0), watchos(2.0), tvos(9.0));
複製程式碼
5. 停止錄製
注意: 在開啟與關閉錄製時都需要做一次寫magic cookie操作,開始時做是為了使檔案具備magic cookie可用,結束時呼叫是為了更新與校正magic cookie資訊.
-(void)stopVoiceRecordAudioConverter:(AudioConverterRef)audioConverter needMagicCookie:(BOOL)isNeedMagicCookie {
if (isNeedMagicCookie) {
// reconfirm magic cookie at the end.
[self copyEncoderCookieToFileByAudioConverter:audioConverter
inFile:m_recordFile];
}
AudioFileClose(m_recordFile);
m_recordCurrentPacket = 0;
}
複製程式碼