引言:
在iOS中音頻按照播放形式可以分為音效播放和音樂播放。音效主要指的是一些短音頻,通常作為點綴音頻,如游戲中的喊殺聲,對于這類音頻不需要進行進度,循環等控制。音樂主要指的是一些較長的音頻,通常是主音頻,對于這類音頻播放通常需要精確的控制。在iOS中播放一般使用AudioToolbox.framework這個框架,播放音樂一般使用AVFoundation.framework。
音效播放
AudioToolbox.framework是一套基于C語言的框架,使用它來播放音效其本質是將音效注冊到系統聲音服務(System Sound Service)中。System Sound Service是一種簡單,底層的聲音播放服務。但是它本身有一些限制:
- 音效播放時間不能超過30s
- 數據必須是PCM或者IMA4格式
- 音頻文件必須打包成.caf , .aif, .wav中的一種(這是官方的說法,實際測試一些.aac .mp3格式的也可以播放)
使用System Sound Service播放音效的步驟如下:
- 調用AudioServicesCreateSystemSoundID( CFURLRef inFileURL, SystemSoundID* outSystemSoundID)函數獲得系統聲音ID。
- 如果需要監聽播放完成操作,則使用AudioServicesAddSystemSoundCompletion( SystemSoundID inSystemSoundID,
CFRunLoopRef inRunLoop, CFStringRef inRunLoopMode, AudioServicesSystemSoundCompletionProc inCompletionRoutine, void* inClientData)方法注冊回調函數。 - 調用AudioServicesPlaySystemSound(SystemSoundID inSystemSoundID) 或者AudioServicesPlayAlertSound(SystemSoundID inSystemSoundID) 方法播放音效(后者帶有震動效果)。
例如:
void playCallback(SystemSoundID ID, void * clientData){
NSLog(@"播放完成...");
}
NSURL *audioURL=[[NSBundle mainBundle] URLForResource:@"abc" withExtension:@"aac"];
SystemSoundID soundID;
//Creates a system sound object.
AudioServicesCreateSystemSoundID((__bridge CFURLRef)(audioURL), &soundID);
//Registers a callback function that is invoked when a specified system sound finishes playing.
AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, &playCallback, (__bridge void * _Nullable)(self));
// AudioServicesPlayAlertSound(soundID);
AudioServicesPlaySystemSound(soundID);
音樂播放
如果播放較大的音頻或者要對音頻有精確的控制的話,通常會選擇使用AVFoundation.framework中的AVAudioPlayer來實現。AVAudioPlayer可以看成一個播放器,它支持多種音頻格式,而且能夠進行進度、音量、播放速度等控制。首先簡單看一下AVAudioPlayer常用的屬性和方法:
@property(readonly, getter=isPlaying) BOOL playing //是否正在播放,只讀
@property(readonly) NSUInteger numberOfChannels //音頻聲道數,只讀
@property(readonly) NSTimeInterval duration //音頻時長
@property(readonly) NSURL *url //音頻文件路徑,只讀
@property(readonly) NSData *data //音頻數據,只讀
@property float pan //立體聲平衡,如果為-1.0則完全左聲道,如果0.0則左右聲道平衡,如果為1.0則完全為右聲道
@property float volume 音量大小,范圍0-1.0
@property BOOL enableRate 是否允許改變播放速率
@property float rate 播放速率,范圍0.5-2.0,如果為1.0則正常播放,如果要修改播放速率則必須設置enableRate為YES
@property NSTimeInterval currentTime 當前播放時長
@property(readonly) NSTimeInterval deviceCurrentTime 輸出設備播放音頻的時間,注意如果播放中被暫停此時間也會繼續累加
@property NSInteger numberOfLoops 循環播放次數,如果為0則不循環,如果小于0則無限循環,大于0則表示循環次數
@property(readonly) NSDictionary *settings 音頻播放設置信息,只讀
@property(getter=isMeteringEnabled) BOOL meteringEnabled 是否啟用音頻測量,默認為NO,一旦啟用音頻測量可以通過updateMeters方法更新測量值
對象方法
- (instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError 使用文件URL初始化播放器,注意這個URL不能是HTTP URL,AVAudioPlayer不支持加載網絡媒體流,只能播放本地文件
- (instancetype)initWithData:(NSData *)data error:(NSError **)outError 使用NSData初始化播放器,注意使用此方法時必須文件格式和文件后綴一致,否則出錯,所以相比此方法更推薦使用上述方法或- (instancetype)initWithData:(NSData *)data fileTypeHint:(NSString *)utiString error:(NSError **)outError方法進行初始化
- (BOOL)prepareToPlay; 加載音頻文件到緩沖區,注意即使在播放之前音頻文件沒有加載到緩沖區程序也會隱式調用此方法。
- (BOOL)play; 播放音頻文件
- (BOOL)playAtTime:(NSTimeInterval)time 在指定的時間開始播放音頻
- (void)pause; 暫停播放
- (void)stop; 停止播放
- (void)updateMeters 更新音頻測量值,注意如果要更新音頻測量值必須設置meteringEnabled為YES,通過音頻測量值可以即時獲得音頻分貝等信息
- (float)peakPowerForChannel:(NSUInteger)channelNumber; 獲得指定聲道的分貝峰值,注意如果要獲得分貝峰值必須在此之前調用updateMeters方法
- (float)averagePowerForChannel:(NSUInteger)channelNumber 獲得指定聲道的分貝平均值,注意如果要獲得分貝平均值必須在此之前調用updateMeters方法
@property(nonatomic, copy) NSArray *channelAssignments 獲得或設置播放聲道
代理方法 說明
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag 音頻播放完成
- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error 音頻解碼發生錯誤
AVAudioPlayer的使用比較簡單:
- 初始化AVAudioPlayer對象,此時通常指定本地文件路徑。
- 設置播放器屬性,例如重復次數、音量大小等。
- 調用play方法播放。
當然由于AVAudioPlayer一次只能播放一個音頻文件,所有上一曲、下一曲其實可以通過創建多個播放器對象來完成,播放進度的實現主要依靠一個定時器實時計算當前播放時長和音頻總時長的比例。
NSString *urlStr=[[NSBundle mainBundle]pathForResource:@"abc" ofType:@"aac"];
NSURL *url=[NSURL fileURLWithPath:urlStr];
NSError *error=nil;
//初始化播放器,注意這里的Url參數只能時文件路徑,不支持HTTP Url
_audioPlayer=[[AVAudioPlayer alloc]initWithContentsOfURL:url error:&error];
//設置播放器屬性
_audioPlayer.numberOfLoops=0;//設置為0不循環
//audioPlayer.delegate=self;
[_audioPlayer prepareToPlay];//加載音頻文件到緩存
[_audioPlayer play];
音頻會話
在iOS中每個應用都有一個音頻會話,這個會話就通過AVAudioSession來表示。AVAudioSession同樣存在于AVFoundation框架中,它是單例模式設計,通過sharedInstance進行訪問。在使用Apple設備時大家會發現有些應用只要打開其他音頻播放就會終止,而有些應用卻可以和其他應用同時播放,在多種音頻環境中如何去控制播放的方式就是通過音頻會話來完成的。下面是音頻會話的幾種會話模式:
如果要讓一個播放器推到后臺后還繼續播放,則需要做下面幾件事:
- 設置后臺運行模式:在plist文件中添加Required background modes,并且設置item 0=App plays audio or streams audio/video using AirPlay(其實可以直接通過Xcode在Project Targets-Capabilities-Background Modes中設置)
- 設置AVAudioSession的類型為AVAudioSessionCategoryPlayback并且調用setActive::方法啟動會話。
AVAudioSession *audioSession=[AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
[audioSession setActive:YES error:nil];
- 為了能夠讓應用退到后臺之后支持耳機控制,建議添加遠程控制事件(這一步不是后臺播放必須的)
前兩步是后臺播放所必須設置的,第三步主要用于接收遠程事件.如果這一步不設置雖讓也能夠在后臺播放,但是無法獲得音頻控制權(如果在使用當前應用之前使用其他播放器播放音樂的話,此時如果按耳機播放鍵或者控制中心的播放按鈕則會播放前一個應用的音頻),并且不能使用耳機進行音頻控制。第一步操作相信大家都很容易理解,如果應用程序要允許運行到后臺必須設置,正常情況下應用如果進入后臺會被掛起,通過該設置可以上應用程序繼續在后臺運行。此部分參考了這里
音頻隊列服務
大家應該已經注意到了,無論是前面的錄音還是音頻播放均不支持網絡流媒體播放。AVAudioPlayer只能播放本地文件,并且是一次性加載所有音頻數據,初始化AVAudioPlayer時指定的URL也只能是File URL而不能是HTTP URL。當然,將音頻文件下載到本地然后再調用AVAudioPlayer來播放也是一種播放網絡音頻的辦法,但是這種方式最大的弊端就是必須等到整個音頻播放完成才能播放,而不能使用流式播放,這往往在實際開發中是不切實際的。那么在iOS中如何播放網絡流媒體呢?就是使用AudioToolbox框架中的音頻隊列服務Audio Queue Services。
使用音頻隊列服務完全可以做到音頻播放和錄制,首先看一下錄音音頻服務隊列:
一個音頻服務隊列Audio Queue有三部分組成:
三個緩沖器Buffers:每個緩沖器都是一個存儲音頻數據的臨時倉庫。
一個緩沖隊列Buffer Queue:一個包含音頻緩沖器的有序隊列。
一個回調Callback:一個自定義的隊列回調函數。
聲音通過輸入設備進入緩沖隊列中,首先填充第一個緩沖器;當第一個緩沖器填充滿之后自動填充下一個緩沖器,同時會調用回調函數;在回調函數中需要將緩沖器中的音頻數據寫入磁盤,同時將緩沖器放回到緩沖隊列中以便重用。下面是Apple官方關于音頻隊列服務的流程示意圖:
類似的,看一下音頻播放緩沖隊列,其組成部分和錄音緩沖隊列類似:
但是在音頻播放緩沖隊列中,回調函數調用的時機不同于音頻錄制緩沖隊列,流程剛好相反。將音頻讀取到緩沖器中,一旦一個緩沖器填充滿之后就放到緩沖隊列中,然后繼續填充其他緩沖器;當開始播放時,則從第一個緩沖器中讀取音頻進行播放;一旦播放完之后就會觸發回調函數,開始播放下一個緩沖器中的音頻,同時填充第一個緩沖器放;填充滿之后再次放回到緩沖隊列。下面是詳細的流程:
當然,要明白音頻隊列服務的原理并不難,問題是如何實現這個自定義的回調函數,這其中我們有大量的工作要做,控制播放狀態、處理異常中斷、進行音頻編碼等等。由于牽扯內容過多,而且不是本文目的,如果以后有時間將另開一篇文章重點介紹,目前有很多第三方優秀框架可以直接使用,例如AudioStreamer、FreeStreamer。