Work with user data and global information這個類可以用來建立、初始化音頻檔案;讀寫音頻資料;對音頻檔案進行最佳化;讀取和寫入音頻格式資訊等等,功能十分強大,可見它不但可以用來支援音頻播放,甚至可以用來產生音頻檔案。當然,在本篇文章中只會涉及一些和音頻播放相關的內容(開啟音頻檔案、讀取格式資訊、讀取音頻資料,其實我也只對這些方法有一點瞭解,其餘的功能沒用過。。>_<).
AudioFile的開啟“姿勢”AudioFile提供了兩個開啟檔案的方法:
1、 AudioFileOpenURL
12345678910 |
enum { kAudioFileReadPermission = 0x01, kAudioFileWritePermission = 0x02, kAudioFileReadWritePermission = 0x03};extern OSStatus AudioFileOpenURL (CFURLRef inFileRef, SInt8 inPermissions, AudioFileTypeID inFileTypeHint, AudioFileID * outAudioFile);
|
從方法的定義上來看是用來讀取本地檔案的:
第一個參數,檔案路徑;
第二個參數,檔案的允許使用方式,是讀、寫還是讀寫,如果開啟檔案後進行了允許使用方式以外的操作,就得到kAudioFilePermissionsError錯誤碼(比如Open時聲明是kAudioFileReadPermission但卻調用了AudioFileWriteBytes);
第三個參數,和AudioFileStream的open方法中一樣是一個協助AudioFile解析檔案的類型提示,如果檔案類型確定的話應當傳入;
第四個參數,返回AudioFile執行個體對應的AudioFileID,這個ID需要儲存起來作為後續一些方法的參數使用;
返回值用來判斷是否成功開啟檔案(OSSStatus == noErr)。
2、 AudioFileOpenWithCallbacks
1234567 |
extern OSStatus AudioFileOpenWithCallbacks (void * inClientData, AudioFile_ReadProc inReadFunc, AudioFile_WriteProc inWriteFunc, AudioFile_GetSizeProc inGetSizeFunc, AudioFile_SetSizeProc inSetSizeFunc, AudioFileTypeID inFileTypeHint, AudioFileID * outAudioFile);
|
看過第一個Open方法後,這個方法乍看上去讓人有點迷茫,沒有URL的參數如何告訴AudioFile該開啟哪個檔案?還是先來看一下參數的說明吧:
第一個參數,上下文資訊,不再多做解釋;
第二個參數,當AudioFile需要讀音頻資料時進行的回調(調用Open和Read方式後同步回調);
第三個參數,當AudioFile需要寫音頻資料時進行的回調(寫音頻檔案功能時使用,暫不討論);
第四個參數,當AudioFile需要用到檔案的總大小時回調(調用Open和Read方式後同步回調);
第五個參數,當AudioFile需要設定檔案的大小時回調(寫音頻檔案功能時使用,暫不討論);
第六、七個參數和返回值同AudioFileOpenURL方法;
這個方法的重點在於AudioFile_ReadProc這個回調。換一個角度理解,這個方法相比於第一個方法自由度更高,AudioFile需要的只是一個資料來源,無論是磁碟上的檔案、記憶體裡的資料甚至是網路流只要能在AudioFile需要資料時(Open和Read時)通過AudioFile_ReadProc回調為AudioFile提供合適的資料就可以了,也就是說使用方法不僅僅可以讀取本地檔案也可以如AudioFileStream一樣以流的形式讀取資料。
下面來看一下AudioFile_GetSizeProc和AudioFile_ReadProc這兩個讀取功能相關的回調
1234567 |
typedef SInt64 (*AudioFile_GetSizeProc)(void * inClientData);typedef OSStatus (*AudioFile_ReadProc)(void * inClientData, SInt64 inPosition, UInt32 requestCount, void * buffer, UInt32 * actualCount);
|
首先是AudioFile_GetSizeProc回調,這個回調很好理解,返迴文件總長度即可,總長度的擷取途徑自然是檔案系統或者httpResponse等等。
接下來是AudioFile_ReadProc回調:
第一個參數,內容物件,不再贅述;
第二個參數,需要讀取第幾個位元組開始的資料;
第三個參數,需要讀取的資料長度;
第四個參數,返回參數,是一個資料指標並且其空間已經被分配,我們需要做的是把資料memcpy到buffer中;
第五個參數,實際提供的資料長度,即memcpy到buffer中的資料長度;
返回值,如果沒有任何異常產生就返回noErr,如果有異常可以根據異常類型選擇需要的error常量返回(一般用不到其他返回值,返回noErr就足夠了);
這裡需要解釋一下這個回調方法的工作方式。AudioFile需要資料時會調用回調方法,需要資料的時間點有兩個:
Open方法調用時,由於AudioFile的Open方法調用過程中就會對音頻格式資訊進行解析,只有符合要求的音頻格式才能被成功開啟否則Open方法就會返回錯誤碼(換句話說,Open方法一旦調用成功就相當於AudioStreamFile在Parse後返回ReadyToProducePackets一樣,只要Open成功就可以開始讀取音頻資料,詳見第三篇),所以在Open方法調用的過程中就需要提供一部分音頻資料來進行解析;
Read相關方法調用時,這個不需要多說很好理解;
通過回調提供資料時需要注意inPosition和requestCount參數,這兩個參數指明了本次回調需要提供的資料範圍是從inPosition開始requestCount個位元組的資料。這裡又可以分為兩種情況:
有充足的資料:那麼我們需要把這個範圍內的資料拷貝到buffer中,並且給actualCount賦值requestCount,最後返回noError;
資料不足:沒有充足資料的話就只能把手頭有的資料拷貝到buffer中,需要注意的是這部分被拷貝的資料必須是從inPosition開始的連續資料,拷貝完成後給actualCount賦值實際拷貝進buffer中的資料長度後返回noErr,這個過程可以用下面的代碼來表示:
1234567891011121314151617 |
static OSStatus MyAudioFileReadCallBack(void *inClientData, SInt64 inPosition, UInt32 requestCount, void *buffer, UInt32 *actualCount){ __unsafe_unretained MyContext *context = (__bridge MyContext *)inClientData; *actualCount = [context availableDataLengthAtOffset:inPosition maxLength:requestCount]; if (*actualCount > 0) { NSData *data = [context dataAtOffset:inPosition length:*actualCount]; memcpy(buffer, [data bytes], [data length]); } return noErr;}
|
說到這裡又需要分兩種情況:
2.1. Open方法調用時的回調資料不足:AudioFile的Open方法會根據檔案格式類型分幾步進行資料讀取以解析確定是否是一個合法的檔案格式,其中每一步的inPosition和requestCount都不一樣,如果某一步不成功就會直接進行下一步,如果幾部下來都失敗了,那麼Open方法就會失敗。簡單的說就是在調用Open之前首先需要保證音頻檔案的格式資訊完整,這就意味著AudioFile並不能獨立用於音頻流的讀取,在流播放時首先需要使用AudioStreamFile來得到ReadyToProducePackets標誌位來保證資訊完整;
2.2. Read方法調用時的回調資料不足:這種情況下inPosition和requestCount的數值與Read方法調用時傳入的參數有關,資料不足對於Read方法本身沒有影響,只要回調返回noErr,Read就成功,只是實際交給Read方法的調用方的資料會不足,那麼就把這個問題的處理交給了Read的調用方;
讀取音頻格式資訊成功開啟音頻檔案後就可以讀取其中的格式資訊了,讀取用到的方法如下:
123456789 |
extern OSStatus AudioFileGetPropertyInfo(AudioFileID inAudioFile, AudioFilePropertyID inPropertyID, UInt32 * outDataSize, UInt32 * isWritable); extern OSStatus AudioFileGetProperty(AudioFileID inAudioFile, AudioFilePropertyID inPropertyID, UInt32 * ioDataSize, void * outPropertyData);
|
AudioFileGetPropertyInfo方法用來擷取某個屬性對應的資料的大小(outDataSize)以及該屬性是否可以被write(isWritable),而AudioFileGetProperty則用來擷取屬性對應的資料。對於一些大小可變的屬性需要先使用AudioFileGetPropertyInfo擷取資料大小才能取擷取資料(例如formatList),而有些確定類型單個屬性則不必先調用AudioFileGetPropertyInfo直接調用AudioFileGetProperty即可(比如BitRate),例子如下:
12345678910111213141516171819202122232425262728 |
AudioFileID fileID; //Open方法返回的AudioFileID//擷取格式資訊UInt32 formatListSize = 0;OSStatus status = AudioFileGetPropertyInfo(_fileID, kAudioFilePropertyFormatList, &formatListSize, NULL);if (status == noErr){ AudioFormatListItem *formatList = (AudioFormatListItem *)malloc(formatListSize); status = AudioFileGetProperty(fileID, kAudioFilePropertyFormatList, &formatListSize, formatList); if (status == noErr) { for (int i = 0; i * sizeof(AudioFormatListItem) < formatListSize; i += sizeof(AudioFormatListItem)) { AudioStreamBasicDescription pasbd = formatList[i].mASBD; //選擇需要的格式。。 } } free(formatList);}//擷取碼率UInt32 bitRate;UInt32 bitRateSize = sizeof(bitRate);status = AudioFileGetProperty(fileID, kAudioFilePropertyBitRate, &size, &bitRate);if (status != noErr){ //錯誤處理}
|
可以擷取的屬性有下面這些,大家可以參考文檔來擷取自己需要的資訊(注意到這裡有EstimatedDuration,可以得到Duration了):
12345678910111213141516171819202122232425262728 |
enum{ kAudioFilePropertyFileFormat = 'ffmt', kAudioFilePropertyDataFormat = 'dfmt', kAudioFilePropertyIsOptimized = 'optm', kAudioFilePropertyMagicCookieData = 'mgic', kAudioFilePropertyAudioDataByteCount = 'bcnt', kAudioFilePropertyAudioDataPacketCount = 'pcnt', kAudioFilePropertyMaximumPacketSize = 'psze', kAudioFilePropertyDataOffset = 'doff', kAudioFilePropertyChannelLayout = 'cmap', kAudioFilePropertyDeferSizeUpdates = 'dszu', kAudioFilePropertyMarkerList = 'mkls', kAudioFilePropertyRegionList = 'rgls', kAudioFilePropertyChunkIDs = 'chid', kAudioFilePropertyInfoDictionary = 'info', kAudioFilePropertyPacketTableInfo = 'pnfo', kAudioFilePropertyFormatList = 'flst', kAudioFilePropertyPacketSizeUpperBound = 'pkub', kAudioFilePropertyReserveDuration = 'rsrv', kAudioFilePropertyEstimatedDuration = 'edur', kAudioFilePropertyBitRate = 'brat', kAudioFilePropertyID3Tag = 'id3t', kAudioFilePropertySourceBitDepth = 'sbtd', kAudioFilePropertyAlbumArtwork = 'aart', kAudioFilePropertyAudioTrackCount = 'atct', kAudioFilePropertyUseAudioTrack = 'uatk'};
|
讀取音頻資料讀取音頻資料的方法分為兩類:
1、直接讀取音頻資料:
12345 |
extern OSStatus AudioFileReadBytes (AudioFileID inAudioFile, Boolean inUseCache, SInt64 inStartingByte, UInt32 * ioNumBytes, void * outBuffer);
|
第一個參數,FileID;
第二個參數,是否需要cache,一般來說傳false;
第三個參數,從第幾個byte開始讀取資料
第四個參數,這個參數在調用時作為輸入參數表示需要讀取讀取多少資料,調用完成後作為輸出參數表示實際讀取了多少資料(即Read回調中的requestCount和actualCount);
第五個參數,buffer指標,需要事先分配好足夠大的記憶體(ioNumBytes大,即Read回調中的buffer,所以Read回調中不需要再分配記憶體);
返回值表示是否讀取成功,EOF時會返回kAudioFileEndOfFileError;
使用這個方法得到的資料都是沒有進行過幀分離的資料,如果想要用來播放或者解碼還必須通過AudioFileStream進行幀分離;
2、按幀(Packet)讀取音頻資料:
12345678910111213141516 |
extern OSStatus AudioFileReadPacketData (AudioFileID inAudioFile, Boolean inUseCache, UInt32 * ioNumBytes, AudioStreamPacketDescription * outPacketDescriptions, SInt64 inStartingPacket, UInt32 * ioNumPackets, void * outBuffer); extern OSStatus AudioFileReadPackets (AudioFileID inAudioFile, Boolean inUseCache, UInt32 * outNumBytes, AudioStreamPacketDescription * outPacketDescriptions, SInt64 inStartingPacket, UInt32 * ioNumPackets, void * outBuffer);
|
按幀讀取的方法有兩個,這兩個方法看上去差不多,就連參數也幾乎相同,但使用情境和效率上卻有所不同,官方文檔中如此描述這兩個方法:
AudioFileReadPacketData is memory efficient when reading variable bit-rate (VBR) audio data;
AudioFileReadPacketData is more efficient than AudioFileReadPackets when reading compressed file formats that do not have packet tables, such as MP3 or ADTS. This function is a good choice for reading either CBR (constant bit-rate) or VBR data if you do not need to read a fixed duration of audio.
- Use
AudioFileReadPackets only when you need to read a fixed duration of audio data, or when you are reading only uncompressed audio.只有當需要讀取固定時間長度音頻或者非壓縮音頻時才會用到AudioFileReadPackets,其餘時候使用AudioFileReadPacketData會有更高的效率並且更省記憶體;
下面來看看這些參數:
第一、二個參數,同AudioFileReadBytes;
第三個參數,對於AudioFileReadPacketData來說ioNumBytes這個參數在輸入輸出時都要用到,在輸入時表示outBuffer的size,輸出時表示實際讀取了多少size的資料。而對AudioFileReadPackets來說outNumBytes只在輸出時使用,表示實際讀取了多少size的資料;
第四個參數,幀資訊數組指標,在輸入前需要分配記憶體,大小必須足夠存在ioNumPackets個幀資訊(ioNumPackets * sizeof(AudioStreamPacketDescription));
第五個參數,在輸入時表示需要讀取多少個幀,在輸出時表示實際讀取了多少幀;
第六個參數,outBuffer資料指標,在輸入前就需要分配好空間,這個參數看上去兩個方法一樣但其實並非如此。對於AudioFileReadPacketData來說只要分配近似幀大小 * 幀數的記憶體空間即可,方法本身會針對給定的記憶體空間大小來決定最後輸出多少個幀,如果空間不夠會適當減少出的幀數;而對於AudioFileReadPackets來說則需要分配最大幀大小(或幀大小上界) * 幀數的記憶體空間才行(最大幀大小和幀大小上界的區別等下會說);這也就是為何第三個參數一個是輸入輸出雙向使用的,而另一個只是輸出時使用的原因。就這點來說兩個方法中前者在使用的過程中要比後者更省記憶體;
返回值,同AudioFileReadBytes;
這兩個方法讀取後的資料為幀分離後的資料,可以直接用來播放或者解碼。
下面給出兩個方法的使用代碼(以MP3為例):
123456789101112131415161718192021 |
AudioFileID fileID; //Open方法返回的AudioFileIDUInt32 ioNumPackets = ...; //要讀取多少個packetSInt64 inStartingPacket = ...; //從第幾個Packet開始讀取UInt32 bitRate = ...; //AudioFileGetProperty讀取kAudioFilePropertyBitRateUInt32 sampleRate = ...; //AudioFileGetProperty讀取kAudioFilePropertyDataFormat或kAudioFilePropertyFormatListUInt32 byteCountPerPacket = 144 * bitRate / sampleRate; //MP3資料每個Packet的近似大小UInt32 descSize = sizeof(AudioStreamPacketDescription) * ioNumPackets;AudioStreamPacketDescription * outPacketDescriptions = (AudioStreamPacketDescription *)malloc(descSize);UInt32 ioNumBytes = byteCountPerPacket * ioNumPackets;void * outBuffer = (void *)malloc(ioNumBytes);OSStatus status = AudioFileReadPacketData(fileID, false, &ioNumBytes, outPacketDescriptions, inStartingPacket, &ioNumPackets, outBuffer);
|
12345678910111213141516171819202122 |
AudioFileID fileID; //Open方法返回的AudioFileIDUInt32 ioNumPackets = ...; //要讀取多少個packetSInt64 inStartingPacket = ...; //從第幾個Packet開始讀取UInt32 maxByteCountPerPacket = ...; //AudioFileGetProperty讀取kAudioFilePropertyMaximumPacketSize,最大的packet大小//也可以用://UInt32 byteCountUpperBoundPerPacket = ...; //AudioFileGetProperty讀取kAudioFilePropertyPacketSizeUpperBound,當前packet大小上界(未掃描全檔案的情況下)UInt32 descSize = sizeof(AudioStreamPacketDescription) * ioNumPackets;AudioStreamPacketDescription * outPacketDescriptions = (AudioStreamPacketDescription *)malloc(descSize);UInt32 outNumBytes = 0;UInt32 ioNumBytes = maxByteCountPerPacket * ioNumPackets;void * outBuffer = (void *)malloc(ioNumBytes);OSStatus status = AudioFileReadPackets(fileID, false, &outNumBytes, outPacketDescriptions, inStartingPacket, &ioNumPackets, outBuffer);
|
Seekseek的思路和之前講AudioFileStream時講到的是一樣的,區別在於AudioFile沒有方法來協助修正seek的offset和seek的時間:
- 使用
AudioFileReadBytes時需要計算出approximateSeekOffset
- 使用
AudioFileReadPacketData或者AudioFileReadPackets時需要計算出seekToPacketapproximateSeekOffset和seekToPacket的計算方法參見第三篇。
關閉AudioFileAudioFile使用完畢後需要調用AudioFileClose進行關閉,沒啥特別需要注意的。
1 |
extern OSStatus AudioFileClose (AudioFileID inAudioFile);
|
小結本篇針對AudioFile的音頻讀取功能做了介紹,小結一下:
AudioFile有兩個Open方法,需要針對自身的使用情境選擇不同的方法;
AudioFileOpenURL用來讀取本地檔案
AudioFileOpenWithCallbacks的使用情境比前者要廣泛,使用時需要注意AudioFile_ReadProc,這個回調方法在Open方法本身和Read方法被調用時會被同步調用
必須保證音頻檔案格式資訊可讀時才能使用AudioFile的Open方法,AudioFile並不能獨立用於音頻流的讀取,需要配合AudioStreamFile使用才能讀取流(需要用AudioStreamFile來判斷檔案格式資訊可讀之後再調用Open方法);
使用AudioFileGetProperty讀取格式資訊時需要判斷所讀取的資訊是否需要先調用AudioFileGetPropertyInfo獲得資料大小後再進行讀取;
讀取音頻資料應該根據使用的情境選擇不同的音頻讀取方法,對於不同的讀取方法seek時需要計算的變數也不相同;
AudioFile使用完畢後需要調用AudioFileClose進行關閉;
範例程式碼對於本地檔案用AudioFile讀取比較簡單就不在這裡提供demo了,對於流播放中的AudioFile使用推薦大家閱讀豆瓣的開源播放器代碼DOUAudioStreamer。
下篇預告下一篇將講述如何使用AudioQueue。