轉自:http://www.cnblogs.com/iOS-mt/p/4268532.html
感謝作者:夢想通
前言
從事音樂相關的app開發也已經有一段時日了,在這過程中app的播放器幾經修改我也因此對於iOS下的音頻播放實現有了一定的研究。寫這個系列的博客目的一方面希望能夠拋磚引玉,另一方面也是希望能幫助國內其他的iOS開發者和愛好者少走彎路(我自己就遇到了不少的坑=。=)。
本篇為《iOS音頻播放》系列的第一篇,主要將對iOS下實現音頻播放的方法進行概述。
基礎
先來簡單了解一下一些基礎的音頻知識。
目前我們在計算機上進行音頻播放都需要依賴於音頻文件,音頻文件的生成過程是將聲音信息采樣、量化和編碼產生的數字信號的過程,人耳所能聽到的聲音,最低的頻率是從20Hz起一直到最高頻率20KHZ,因此音頻文件格式的最大帶寬是20KHZ。根據奈奎斯特的理論,只有采樣頻率高於聲音信號最高頻率的兩倍時,才能把數字信號表示的聲音還原成為原來的聲音,所以音頻文件的采樣率一般在40~50KHZ,比如最常見的CD音質采樣率44.1KHZ。
對聲音進行采樣、量化過程被稱為脈沖編碼調制(Pulse Code Modulation),簡稱PCM
。PCM數據是最原始的音頻數據完全無損,所以PCM數據雖然音質優秀但體積龐大,為了解決這個問題先后誕生了一系列的音頻格式,這些音頻格式運用不同的方法對音頻數據進行壓縮,其中有無損壓縮(ALAC、APE、FLAC)和有損壓縮(MP3、AAC、OGG、WMA)兩種。
目前最為常用的音頻格式是MP3,MP3是一種有損壓縮的音頻格式,設計這種格式的目的就是為了大幅度的減小音頻的數據量,它舍棄PCM音頻數據中人類聽覺不敏感的部分,從下面的比較圖我們可以明顯的看到MP3數據相比PCM數據明顯矮了一截(圖片引自imp3論壇)。
MP3格式中的碼率(BitRate)代表了MP3數據的壓縮質量,現在常用的碼率有128kbit/s、160kbit/s、320kbit/s等等,這個值越高聲音質量也就越高。MP3編碼方式常用的有兩種固定碼率(Constant bitrate,CBR)和可變碼率(Variable bitrate,VBR)。
MP3格式中的數據通常由兩部分組成,一部分為ID3用來存儲歌名、演唱者、專輯、音軌數等信息,另一部分為音頻數據。音頻數據部分以幀(frame)為單位存儲,每個音頻都有自己的幀頭,如圖所示就是一個MP3文件幀結構圖(圖片同樣來自互聯網)。MP3中的每一個幀都有自己的幀頭,其中存儲了采樣率等解碼必須的信息,所以每一個幀都可以獨立於文件存在和播放,這個特性加上高壓縮比使得MP3文件成為了音頻流播放的主流格式。幀頭之后存儲着音頻數據,這些音頻數據是若干個PCM數據幀經過壓縮算法壓縮得到的,對CBR的MP3數據來說每個幀中包含的PCM數據幀是固定的,而VBR是可變的。
iOS音頻播放概述
了解了基礎概念之后我們就可以列出一個經典的音頻播放流程(以MP3為例):
- 讀取MP3文件
- 解析采樣率、碼率、時長等信息,分離MP3中的音頻幀
- 對分離出來的音頻幀解碼得到PCM數據
- 對PCM數據進行音效處理(均衡器、混響器等,非必須)
- 把PCM數據解碼成音頻信號
- 把音頻信號交給硬件播放
- 重復1-6步直到播放完成
在iOS系統中apple對上述的流程進行了封裝並提供了不同層次的接口(圖片引自官方文檔)。
下面對其中的中高層接口進行功能說明:
- Audio File Services:讀寫音頻數據,可以完成播放流程中的第2步;
- Audio File Stream Services:對音頻進行解碼,可以完成播放流程中的第2步;
- Audio Converter services:音頻數據轉換,可以完成播放流程中的第3步;
- Audio Processing Graph Services:音效處理模塊,可以完成播放流程中的第4步;
- Audio Unit Services:播放音頻數據:可以完成播放流程中的第5步、第6步;
- Extended Audio File Services:Audio File Services和Audio Converter services的結合體;
- AVAudioPlayer/AVPlayer(AVFoundation):高級接口,可以完成整個音頻播放的過程(包括本地文件和網絡流播放,第4步除外);
- Audio Queue Services:高級接口,可以進行錄音和播放,可以完成播放流程中的第3、5、6步;
- OpenAL:用於游戲音頻播放,暫不討論
可以看到apple提供的接口類型非常豐富,可以滿足各種類別類需求:
-
如果你只是想實現音頻的播放,沒有其他需求AVFoundation會很好的滿足你的需求。它的接口使用簡單、不用關心其中的細節;
-
如果你的app需要對音頻進行流播放並且同時存儲,那么AudioFileStreamer加AudioQueue能夠幫到你,你可以先把音頻數據下載到本地,一邊下載一邊用NSFileHandler等接口讀取本地音頻文件並交給AudioFileStreamer或者AudioFile解析分離音頻幀,分離出來的音頻幀可以送給AudioQueue進行解碼和播放。如果是本地文件直接讀取文件解析即可。(這兩個都是比較直接的做法,這類需求也可以用AVFoundation+本地server的方式實現,AVAudioPlayer會把請求發送給本地server,由本地server轉發出去,獲取數據后在本地server中存儲並轉送給AVAudioPlayer。另一個比較trick的做法是先把音頻下載到文件中,在下載到一定量的數據后把文件路徑給AVAudioPlayer播放,當然這種做法在音頻seek后就回有問題了。);
-
如果你正在開發一個專業的音樂播放軟件,需要對音頻施加音效(均衡器、混響器),那么除了數據的讀取和解析以外還需要用到AudioConverter來把音頻數據轉換成PCM數據,再由AudioUnit+AUGraph來進行音效處理和播放(但目前多數帶音效的app都是自己開發音效模塊來坐PCM數據的處理,這部分功能自行開發在自定義性和擴展性上會比較強一些。PCM數據通過音效器處理完成后就可以使用AudioUnit播放了,當然AudioQueue也支持直接使對PCM數據進行播放。)。下圖描述的就是使用AudioFile + AudioConverter + AudioUnit進行音頻播放的流程(圖片引自官方文檔)。
iOS音頻播放 (二):AudioSession
AudioSession這個玩意的主要功能包括以下幾點(圖片來自官方文檔):
- 確定你的app如何使用音頻(是播放?還是錄音?)
- 為你的app選擇合適的輸入輸出設備(比如輸入用的麥克風,輸出是耳機、手機功放或者airplay)
- 協調你的app的音頻播放和系統以及其他app行為(例如有電話時需要打斷,電話結束時需要恢復,按下靜音按鈕時是否歌曲也要靜音等)
AudioSession相關的類有兩個:
AudioToolBox
中的AudioSession
AVFoundation
中的AVAudioSession
其中AudioSession在SDK 7中已經被標注為depracated,而AVAudioSession這個類雖然iOS 3開始就已經存在了,但其中很多方法和變量都是在iOS 6以后甚至是iOS 7才有的。所以各位可以依照以下標准選擇:
- 如果最低版本支持iOS 5,可以使用
AudioSession
,也可以使用AVAudioSession
; - 如果最低版本支持iOS 6及以上,請使用
AVAudioSession
下面以AudioSession
類為例來講述AudioSession相關功能的使用(很不幸我需要支持iOS 5。。T-T,使用AVAudioSession
的同學可以在其頭文件中尋找對應的方法使用即可,需要注意的點我會加以說明).
注意:在使用AVAudioPlayer/AVPlayer時可以不用關心AudioSession的相關問題,Apple已經把AudioSession的處理過程封裝了,但音樂打斷后的響應還是要做的(比如打斷后音樂暫停了UI狀態也要變化,這個應該通過KVO就可以搞定了吧。。我沒試過瞎猜的>_<)。
注意:在使用MPMusicPlayerController
時不必關心AudioSession的問題。
初始化AudioSession
使用AudioSession
類首先需要調用初始化方法:
1
2 3 4 |
|
前兩個參數一般填NULL
表示AudioSession運行在主線程上(但並不代表音頻的相關處理運行在主線程上,只是AudioSession),第三個參數需要傳入一個AudioSessionInterruptionListener
類型的方法,作為AudioSession被打斷時的回調,第四個參數則是代表打斷回調時需要附帶的對象(即回到方法中的inClientData,如下所示,可以理解為UIView animation中的context)。
1
|
|
這才剛開始,坑就來了。這里會有兩個問題:
第一,AudioSessionInitialize可以被多次執行,但AudioSessionInterruptionListener
只能被設置一次,這就意味着這個打斷回調方法是一個靜態方法,一旦初始化成功以后所有的打斷都會回調到這個方法,即便下一次再次調用AudioSessionInitialize並且把另一個靜態方法作為參數傳入,當打斷到來時還是會回調到第一次設置的方法上。
這種場景並不少見,例如你的app既需要播放歌曲又需要錄音,當然你不可能知道用戶會先調用哪個功能,所以你必須在播放和錄音的模塊中都調用AudioSessionInitialize注冊打斷方法,但最終打斷回調只會作用在先注冊的那個模塊中,很蛋疼吧。。。所以對於AudioSession的使用最好的方法是生成一個類單獨進行管理,統一接收打斷回調並發送自定義的打斷通知,在需要用到AudioSession的模塊中接收通知並做相應的操作。
Apple也察覺到了這一點,所以在AVAudioSession中首先取消了Initialize方法,改為了單例方法sharedInstance
。在iOS 5上所有的打斷都需要通過設置id<AVAudioSessionDelegate> delegate
並實現回調方法來實現,這同樣會有上述的問題,所以在iOS 5使用AVAudioSession下仍然需要一個單獨管理AudioSession的類存在。在iOS 6以后Apple終於把打斷改成了通知的形式。。這下科學了。
第二,AudioSessionInitialize方法的第四個參數inClientData,也就是回調方法的第一個參數。上面已經說了打斷回調是一個靜態方法,而這個參數的目的是為了能讓回調時拿到context(上下文信息),所以這個inClientData需要是一個有足夠長生命周期的對象(當然前提是你確實需要用到這個參數),如果這個對象被dealloc了,那么回調時拿到的inClientData會是一個野指針。就這一點來說構造一個單獨管理AudioSession的類也是有必要的,因為這個類的生命周期和AudioSession一樣長,我們可以把context保存在這個類中。
監聽RouteChange事件
如果想要實現類似於“拔掉耳機就把歌曲暫停”的功能就需要監聽RouteChange事件:
1
2 3 4 5 6 7 8 |
|
調用上述方法,AudioSessionPropertyID參數傳kAudioSessionProperty_AudioRouteChange
,AudioSessionPropertyListener參數傳對應的回調方法。inClientData參數同AudioSessionInitialize方法。
同樣作為靜態回調方法還是需要統一管理,接到回調時可以把第一個參數inData轉換成CFDictionaryRef
並從中獲取kAudioSession_AudioRouteChangeKey_Reason鍵值對應的value(應該是一個CFNumberRef),得到這些信息后就可以發送自定義通知給其他模塊進行相應操作(例如kAudioSessionRouteChangeReason_OldDeviceUnavailable
就可以用來做“拔掉耳機就把歌曲暫停”)。
1
2 3 4 5 6 7 8 9 10 11 |
|
1
2 3 4 5 6 7 8 9 10 11 12 |
|
注意:iOS 5下如果使用了AVAudioSession
由於AVAudioSessionDelegate
中並沒有定義相關的方法,還是需要用這個方法來實現監聽。iOS 6下直接監聽AVAudioSession的通知就可以了。
這里附帶兩個方法的實現,都是基於AudioSession
類的(使用AVAudioSession
的同學幫不到你們啦)。
1、判斷是否插了耳機:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
2、判斷是否開了Airplay(來自StackOverflow):
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
設置類別
下一步要設置AudioSession的Category,使用AudioSession
時調用下面的接口
1
2 3 |
|
如果我需要的功能是播放,執行如下代碼
1
2 3 4 |
|
使用AVAudioSession
時調用下面的接口
1
2 3 4 |
|
至於Category的類型在官方文檔中都有介紹,我這里也只羅列一下具體就不贅述了,各位在使用時可以依照自己需要的功能設置Category。
1
2 3 4 5 6 7 8 9 |
|
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
啟用
有了Category就可以啟動AudioSession了,啟動方法:
1
2 3 4 5 6 7 8 |
|
啟動方法調用后必須要判斷是否啟動成功,啟動不成功的情況經常存在,例如一個前台的app正在播放,你的app正在后台想要啟動AudioSession那就會返回失敗。
一般情況下我們在啟動和停止AudioSession調用第一個方法就可以了。但如果你正在做一個即時語音通訊app的話(類似於微信、易信)就需要注意在deactive AudioSession的時候需要使用第二個方法,inFlags參數傳入kAudioSessionSetActiveFlag_NotifyOthersOnDeactivation
(AVAudioSession
給options參數傳入AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
)。當你的app deactive自己的AudioSession時系統會通知上一個被打斷播放app打斷結束(就是上面說到的打斷回調),如果你的app在deactive時傳入了NotifyOthersOnDeactivation參數,那么其他app在接到打斷結束回調時會多得到一個參數kAudioSessionInterruptionType_ShouldResume
否則就是ShouldNotResume(AVAudioSessionInterruptionOptionShouldResume
),根據參數的值可以決定是否繼續播放。
大概流程是這樣的:
- 一個音樂軟件A正在播放;
- 用戶打開你的軟件播放對話語音,AudioSession active;
- 音樂軟件A音樂被打斷並收到InterruptBegin事件;
- 對話語音播放結束,AudioSession deactive並且傳入NotifyOthersOnDeactivation參數;
- 音樂軟件A收到InterruptEnd事件,查看Resume參數,如果是ShouldResume控制音頻繼續播放,如果是ShouldNotResume就維持打斷狀態;
官方文檔中有一張很形象的圖來闡述這個現象:
然而現在某些語音通訊軟件和某些音樂軟件卻無視了NotifyOthersOnDeactivation
和ShouldResume
的正確用法,導致我們經常接到這樣的用戶反饋:
你們的app在使用xx語音軟件聽了一段話后就不會繼續播放了,但xx音樂軟件可以繼續播放啊。
好吧,上面只是吐槽一下。請無視我吧。
2014.7.14補充,7.19更新:
發現即使之前已經調用過AudioSessionInitialize
方法,在某些情況下被打斷之后可能出現AudioSession失效的情況,需要再次調用AudioSessionInitialize
方法來重新生成AudioSession。否則調用AudioSessionSetActive
會返回560557673(其他AudioSession方法也雷同,所有方法調用前必須首先初始化AudioSession),轉換成string后為”!ini”即kAudioSessionNotInitialized
,這個情況在iOS 5.1.x上比較容易發生,iOS 6.x 和 7.x也偶有發生(具體的原因還不知曉好像和打斷時直接調用AudioOutputUnitStop
有關,又是個坑啊)。
所以每次在調用AudioSessionSetActive
時應該判斷一下錯誤碼,如果是上述的錯誤碼需要重新初始化一下AudioSession。
附上OSStatus轉成string的方法:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
打斷處理
正常啟動AudioSession之后就可以播放音頻了,下面要講的是對於打斷的處理。之前我們說到打斷的回調在iOS 5下需要統一管理,在收到打斷開始和結束時需要發送自定義的通知。
使用AudioSession
時打斷回調應該首先獲取kAudioSessionProperty_InterruptionType
,然后發送一個自定義的通知並帶上對應的參數。
1
2 3 4 5 6 7 8 9 10 11 12 13 |
|
收到通知后的處理方法如下(注意ShouldResume參數):
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
小結
關於AudioSession的話題到此結束(碼字果然很累。。)。小結一下:
- 如果最低版本支持iOS 5,可以使用
AudioSession
也可以考慮使用AVAudioSession
,需要有一個類統一管理AudioSession的所有回調,在接到回調后發送對應的自定義通知; - 如果最低版本支持iOS 6及以上,請使用
AVAudioSession
,不用統一管理,接AVAudioSession的通知即可; - 根據app的應用場景合理選擇
Category
; - 在deactive時需要注意app的應用場景來合理的選擇是否使用
NotifyOthersOnDeactivation
參數; - 在處理InterruptEnd事件時需要注意
ShouldResume
的值。
示例代碼
這里有我自己寫的AudioSession
的封裝,如果各位需要支持iOS 5的話可以使用一下。
下篇預告
下一篇將講述如何使用AudioFileStreamer
分離音頻幀,以及如何使用AudioQueue
進行播放。
下一篇將講述如何使用AudioFileStreamer
提取音頻文件格式信息和分離音頻幀。
參考資料
原創文章,版權聲明:自由轉載-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0
Comments
iOS音頻播放 (三):AudioFileStream
Audio Playback in iOS (Part 3) : AudioFileStream
前言
本來說好是要在第三篇中講AudioFileStream
和AudioQueue
,但寫着寫着發現光AudioFileStream
就好多內容,最后還是決定分篇介紹,這篇先來說一下AudioFileStream
,下一篇計划說一下和AudioFileStream
類似的AudioFile
,下下篇再來說AudioQueue
。
在本篇那種將會提到計算音頻時長duration和音頻seek的方法,這些方法對於CBR編碼形式的音頻文件可以做到比較精確而對於VBR編碼形式的會存在較大的誤差(關於CBR和VBR,請看本系列的第一篇),具體講到duration和seek時會再進行說明。
AudioFileStream介紹
在第一篇中說到AudioFileStreamer
時提到它的作用是用來讀取采樣率、碼率、時長等基本信息以及分離音頻幀。那么在官方文檔中Apple是這樣描述的:
To play streamed audio content, such as from a network connection, use Audio File Stream Services in concert with Audio Queue Services. Audio File Stream Services parses audio packets and metadata from common audio file container formats in a network bitstream. You can also use it to parse packets and metadata from on-disk files
根據Apple的描述AudioFileStreamer
用在流播放中,當然不僅限於網絡流,本地文件同樣可以用它來讀取信息和分離音頻幀。AudioFileStreamer
的主要數據是文件數據而不是文件路徑,所以數據的讀取需要使用者自行實現,
支持的文件格式有:
- MPEG-1 Audio Layer 3, used for .mp3 files
- MPEG-2 ADTS, used for the .aac audio data format
- AIFC
- AIFF
- CAF
- MPEG-4, used for .m4a, .mp4, and .3gp files
- NeXT
- WAVE
上述格式是iOS、MacOSX所支持的音頻格式,這類格式可以被系統提供的API解碼,如果想要解碼其他的音頻格式(如OGG、APE、FLAC)就需要自己實現解碼器了。
初始化AudioFileStream
第一步,自然是要生成一個AudioFileStream
的實例:
1
2 3 4 5 |
|
第一個參數和之前的AudioSession的初始化方法一樣是一個上下文對象;
第二個參數AudioFileStream_PropertyListenerProc
是歌曲信息解析的回調,每解析出一個歌曲信息都會進行一次回調;
第三個參數AudioFileStream_PacketsProc
是分離幀的回調,每解析出一部分幀就會進行一次回調;
第四個參數AudioFileTypeID
是文件類型的提示,這個參數來幫助AudioFileStream
對文件格式進行解析。這個參數在文件信息不完整(例如信息有缺陷)時尤其有用,它可以給與AudioFileStream
一定的提示,幫助其繞過文件中的錯誤或者缺失從而成功解析文件。所以在確定文件類型的情況下建議各位還是填上這個參數,如果無法確定可以傳入0(原理上應該和這篇博文近似);
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
第五個參數是返回的AudioFileStream實例對應的AudioFileStreamID
,這個ID需要保存起來作為后續一些方法的參數使用;
返回值用來判斷是否成功初始化(OSStatus == noErr)。
解析數據
在初始化完成之后,只要拿到文件數據就可以進行解析了。解析時調用方法:
1
2 3 4 |
|
第一個參數AudioFileStreamID
,即初始化時返回的ID;
第二個參數inDataByteSize,本次解析的數據長度;
第三個參數inData,本次解析的數據;
第四個參數是說本次的解析和上一次解析是否是連續的關系,如果是連續的傳入0,否則傳入kAudioFileStreamParseFlag_Discontinuity
。
這里需要插入解釋一下何謂“連續”。在第一篇中我們提到過形如MP3的數據都以幀的形式存在的,解析時也需要以幀為單位解析。但在解碼之前我們不可能知道每個幀的邊界在第幾個字節,所以就會出現這樣的情況:我們傳給AudioFileStreamParseBytes的數據在解析完成之后會有一部分數據余下來,這部分數據是接下去那一幀的前半部分,如果再次有數據輸入需要繼續解析時就必須要用到前一次解析余下來的數據才能保證幀數據完整,所以在正常播放的情況下傳入0即可。目前知道的需要傳入kAudioFileStreamParseFlag_Discontinuity
的情況有兩個,一個是在seek完畢之后顯然seek后的數據和之前的數據完全無關;另一個是開源播放器AudioStreamer的作者@Matt Gallagher曾在自己的blog中提到過的:
the Audio File Stream Services hit me with a nasty bug: AudioFileStreamParseBytes will crash when trying to parse a streaming MP3.
In this case, if we pass the kAudioFileStreamParseFlag_Discontinuity flag to AudioFileStreamParseBytes on every invocation between receiving kAudioFileStreamProperty_ReadyToProducePackets and the first successful call to MyPacketsProc, then AudioFileStreamParseBytes will be extra cautious in its approach and won't crash.
Matt發布這篇blog是在2008年,這個Bug年代相當久遠了,而且原因未知,究竟是否修復也不得而知,而且由於環境不同(比如測試用的mp3文件和所處的iOS系統)無法重現這個問題,所以我個人覺得還是按照Matt的work around在回調得到kAudioFileStreamProperty_ReadyToProducePackets
之后,在正常解析第一幀之前都傳入kAudioFileStreamParseFlag_Discontinuity
比較好。
回到之前的內容,AudioFileStreamParseBytes
方法的返回值表示當前的數據是否被正常解析,如果OSStatus的值不是noErr則表示解析不成功,其中錯誤碼包括:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
大多數都可以從字面上理解,需要提一下的是kAudioFileStreamError_NotOptimized
,文檔上是這么說的:
It is not possible to produce output packets because the file's packet table or other defining info is either not present or is after the audio data.
它的含義是說這個音頻文件的文件頭不存在或者說文件頭可能在文件的末尾,當前無法正常Parse,換句話說就是這個文件需要全部下載完才能播放,無法流播。
注意AudioFileStreamParseBytes
方法每一次調用都應該注意返回值,一旦出現錯誤就可以不必繼續Parse了。
解析文件格式信息
在調用AudioFileStreamParseBytes
方法進行解析時會首先讀取格式信息,並同步的進入AudioFileStream_PropertyListenerProc
回調方法
來看一下這個回調方法的定義
1
2 3 4 |
|
回調的第一個參數是Open方法中的上下文對象;
第二個參數inAudioFileStream是和Open方法中第四個返回參數AudioFileStreamID
一樣,表示當前FileStream的ID;
第三個參數是此次回調解析的信息ID。表示當前PropertyID對應的信息已經解析完成信息(例如數據格式、音頻數據的偏移量等等),使用者可以通過AudioFileStreamGetProperty
接口獲取PropertyID對應的值或者數據結構;
1
2 3 4 |
|
第四個參數ioFlags是一個返回參數,表示這個property是否需要被緩存,如果需要賦值kAudioFileStreamPropertyFlag_PropertyIsCached
否則不賦值(這個參數我也不知道應該在啥場景下使用。。一直都沒去理他);
這個回調會進來多次,但並不是每一次都需要進行處理,可以根據需求處理需要的PropertyID進行處理(PropertyID列表如下)。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
這里列幾個我認為比較重要的PropertyID:
1、kAudioFileStreamProperty_BitRate
:
表示音頻數據的碼率,獲取這個Property是為了計算音頻的總時長Duration(因為AudioFileStream沒有這樣的接口。。)。
1
2 3 4 5 6 7 |
|
2014.8.2 補充: 發現在流播放的情況下,有時數據流量比較小時會出現ReadyToProducePackets
還是沒有獲取到bitRate的情況,這時就需要分離一些拼音幀然后計算平均bitRate,計算公式如下:
1
|
|
2、kAudioFileStreamProperty_DataOffset
:
表示音頻數據在整個音頻文件中的offset(因為大多數音頻文件都會有一個文件頭之后才使真正的音頻數據),這個值在seek時會發揮比較大的作用,音頻的seek並不是直接seek文件位置而seek時間(比如seek到2分10秒的位置),seek時會根據時間計算出音頻數據的字節offset然后需要再加上音頻數據的offset才能得到在文件中的真正offset。
1
2 3 4 5 6 7 |
|
3、kAudioFileStreamProperty_DataFormat
表示音頻文件結構信息,是一個AudioStreamBasicDescription的結構
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
4、kAudioFileStreamProperty_FormatList
作用和kAudioFileStreamProperty_DataFormat
是一樣的,區別在於用這個PropertyID獲取到是一個AudioStreamBasicDescription的數組,這個參數是用來支持AAC SBR這樣的包含多個文件類型的音頻格式。由於到底有多少個format我們並不知曉,所以需要先獲取一下總數據大小:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
5、kAudioFileStreamProperty_AudioDataByteCount
顧名思義,音頻文件中音頻數據的總量。這個Property的作用一是用來計算音頻的總時長,二是可以在seek時用來計算時間對應的字節offset。
1
2 3 4 5 6 7 |
|
2014.8.2 補充: 發現在流播放的情況下,有時數據流量比較小時會出現ReadyToProducePackets
還是沒有獲取到audioDataByteCount的情況,這時就需要近似計算audioDataByteCount。一般來說音頻文件的總大小一定是可以得到的(利用文件系統或者Http請求中的contentLength),那么計算方法如下:
1
2 3 |
|
5、kAudioFileStreamProperty_ReadyToProducePackets
這個PropertyID可以不必獲取對應的值,一旦回調中這個PropertyID出現就代表解析完成,接下來可以對音頻數據進行幀分離了。
計算時長Duration
獲取時長的最佳方法是從ID3信息中去讀取,那樣是最准確的。如果ID3信息中沒有存,那就依賴於文件頭中的信息去計算了。
計算duration的公式如下:
1
|
|
音頻數據的字節總量audioDataByteCount可以通過kAudioFileStreamProperty_AudioDataByteCount
獲取,碼率bitRate可以通過kAudioFileStreamProperty_BitRate
獲取也可以通過Parse一部分數據后計算平均碼率來得到。
對於CBR數據來說用這樣的計算方法的duration會比較准確,對於VBR數據就不好說了。所以對於VBR數據來說,最好是能夠從ID3信息中獲取到duration,獲取不到再想辦法通過計算平均碼率的途徑來計算duration。
分離音頻幀
讀取格式信息完成之后繼續調用AudioFileStreamParseBytes
方法可以對幀進行分離,並同步的進入AudioFileStream_PacketsProc
回調方法。
回調的定義:
1
2 3 4 5 |
|
第一個參數,一如既往的上下文對象;
第二個參數,本次處理的數據大小;
第三個參數,本次總共處理了多少幀(即代碼里的Packet);
第四個參數,本次處理的所有數據;
第五個參數,AudioStreamPacketDescription
數組,存儲了每一幀數據是從第幾個字節開始的,這一幀總共多少字節。
1
2 3 4 5 6 7 8 |
|
下面是我按照自己的理解實現的回調方法片段:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|