Learning AV Foundation(二)AVAudioPlayer
開篇
最近在學習 AV Foundation
試圖把學習內容記錄下來 並參考一些部落格文章
本期的內容是 AVAudioPlayer
音訊知識基礎
音訊檔案的生成過程是將聲音資訊 取樣 、 量化 和 編碼 產生的數字訊號的過程, 人耳所能聽到的聲音,最低的頻率是從20Hz起一直到最高頻率20KHZ ,因此音訊檔案格式的最大頻寬是20KHZ。根據 奈奎斯特
的理論,只有取樣頻率高於聲音訊號最高頻率的兩倍時,才能把數字訊號表示的聲音還原成為原來的聲音,所以音訊檔案的取樣率一般在 40~50KHZ ,比如最常見的CD音質取樣率 44.1KHZ 。 (所以一般大家都覺得CD音質是最好的.) 對聲音進行取樣、量化過程被稱為 脈衝編碼調製
(Pulse Code Modulation),簡稱PCM。PCM資料是最原始的音訊資料完全無損,所以PCM資料雖然音質優秀但體積龐大,為了解決這個問題先後誕生了一系列的音訊格式,這些音訊格式運用不同的方法對音訊資料進行壓縮,其中有無失真壓縮(ALAC、APE、FLAC)和有失真壓縮(MP3、AAC、OGG、WMA)兩種 來源: iOS音訊播放 (一):概述 by 碼農人生
–
我覺得程寅大牛的處理音訊說的很明白
大神列出一個經典的音訊播放流程(以MP3為例)
- 讀取MP3檔案
- 解析取樣率、位元速率、時長等資訊,分離MP3中的音訊幀
- 對分離出來的音訊幀解碼得到PCM資料
- 對PCM資料進行音效處理(均衡器、混響器等,非必須)
- 把PCM資料解碼成音訊訊號
- 把音訊訊號交給硬體播放
- 重複1-6步直到播放完成
在iOS系統中apple對上述的流程進行了封裝並提供了不同層次的介面

這是CoreAudio的介面層次
下面對其中的中高層介面進行功能說明:
- 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進行音訊播放的流程

image
以上內容均轉自 碼農人生 希望大神不要介意 如果有問題 我可立即清除
使用 AVAudioPlayer
之前對AudioSession簡介
AVAudioSession
負責管理音訊會話 它是個單例 在應用程式和作業系統之間負責中間人的角色 AudioSession參考
AVAudioSession
主要功能包括以下幾點:
- app是如何使用的音訊服務 播放 還是錄製 之類的
- 控制協調app輸入輸出裝置(比如 麥克風,耳機、手機外放比如藍芽連線一個外接音響 或airplay)
- 協調你的app的音訊播放和系統以及其他app行為(例如有電話時需要打斷,電話結束時需要恢復,按下靜音按鈕時是否歌曲也要靜音等)

注:AVAudioSession iOS6以後使用 以前叫AudioSession
如何使用 AVAudioPlayer
在我的部落格裡面我儘量使用code勝過千言萬語
使用AVAudioPlayer
之前需要在
AppDelegate
裡面匯入
#import <AVFoundation/AVFoundation.h>
並且啟動音訊會話
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { AVAudioSession *session = [AVAudioSession sharedInstance]; NSError *error; if (![session setCategory:AVAudioSessionCategoryPlayback error:&error]) { NSLog(@"Category Error: %@", [error localizedDescription]); } if (![session setActive:YES error:&error]) { NSLog(@"Activation Error: %@", [error localizedDescription]); } return YES; }
上邊已經介紹了 AVAudioSession
這裡面說一下 [session setCategory:AVAudioSessionCategoryPlayback error:&error]
裡面的 AVAudioSessionCategoryPlayback

音訊會話分類
這是這幾種分類的列表大家可以看下
記得開啟後臺播放

或者在plist裡面修改

下面就是建立音訊播放器程式碼
#import "ViewController.h" #import <Masonry/Masonry.h> #import "THControlKnob.h" #import "THPlayButton.h" #import <AVFoundation/AVFoundation.h> @interface ViewController () //三個控制推子 @property (weak, nonatomic) IBOutlet THOrangeControlKnob *panKnob; @property (weak, nonatomic) IBOutlet THOrangeControlKnob *volumnKnob; @property (weak, nonatomic) IBOutlet THGreenControlKnob *rateKnob; @property (weak, nonatomic) IBOutlet THPlayButton *playButton; //音樂播放器 @property (nonatomic, strong) AVAudioPlayer *musicPlayer; @property (nonatomic, getter = isPlaying) BOOL playing; //播放狀態 //無關程式碼 @property (weak, nonatomic) IBOutlet UILabel *LeftRightRoundDec; @property (weak, nonatomic) IBOutlet UILabel *voiceDec; @property (weak, nonatomic) IBOutlet UILabel *rateDec; @property (weak, nonatomic) IBOutlet UILabel *trackDescrption; @end
匯入幾個第三方控制元件的類用於音樂播放

這上邊的三個旋鈕就是匯入的開源庫
下面建立播放器 AVAudioPlayer
建立時需要一個 NSURL
代表要播放的檔案路徑 這裡簡單從bundle中拖了一首歌進去了
#pragma mark - #pragma mark - 建立AVAudioPlayer與播放狀態控制 /** 建立音樂播放器 @param fileName 檔名 @param fileExtension 副檔名 @return 播放器例項 */ - (AVAudioPlayer *)createPlayForFile:(NSString *)fileName withExtension:(NSString *)fileExtension{ NSURL *url = [[NSBundle mainBundle] URLForResource:fileName withExtension:fileExtension]; NSError *error = nil; AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error]; if (audioPlayer) { audioPlayer.numberOfLoops = -1; //-1無限迴圈 audioPlayer.enableRate = YES; //啟動倍速控制 [audioPlayer prepareToPlay]; } else { NSLog(@"Error creating player: %@",[error localizedDescription]); } return audioPlayer; }
numberOfLoops
= -1; 代表本首歌 無限迴圈 其它常數代表迴圈次數
enableRate
代表是否啟用倍速調節 0.5x 1.0x 2.0x 等倍速 1.0代表正常速度
這裡說一下 [audioPlayer prepareToPlay]
呼叫這個函式是為了取得需要的音訊硬體並預載入 Audio Queue
的緩衝區. 當然也可以不呼叫這個方法直接呼叫 [audioPlayer play]
,但當 呼叫 play
方法時也會隱性啟用 ,呼叫 prepareToPlay
是為了減少 建立播放器時預設載入和聽到聲音輸出之間的延時
@implementation ViewController - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { if (self.musicPlayer == nil) { self.musicPlayer = [self createPlayForFile:@"384551_1438267683" withExtension:@"mp3"]; } [self setupNotifications]; } return self; } - (void)awakeFromNib{ [super awakeFromNib]; if (self.musicPlayer == nil) { self.musicPlayer = [self createPlayForFile:@"384551_1438267683" withExtension:@"mp3"]; } [self setupNotifications]; }
在 initWithNibName
或 awakeFromNib
時候呼叫一下建立播放器的程式碼
這個 [self setupNotifications];
後面說
先新增一些常見的方法封裝 比如 播放、暫停、停止
- (void)play { if (self.musicPlayer == nil) { return; } if (!self.playing) { NSTimeInterval delayTime = [self.musicPlayer deviceCurrentTime] + 0.01; [self.musicPlayer playAtTime:delayTime]; self.playing = YES; } self.trackDescrption.text = [self.musicPlayer.url absoluteString]; [self configNowPlayingInfoCenter]; //配置後臺播放的頁面資訊 } - (void)stop { if (self.musicPlayer == nil) { return; } if (self.playing) { [self.musicPlayer stop]; self.musicPlayer.currentTime = 0.0f; self.playing = NO; } } - (void)pause { if (self.musicPlayer == nil) { return; } if (self.playing) { [self.musicPlayer pause]; self.playing = NO; } }
這裡看到 [self.musicPlayer deviceCurrentTime] + 0.01
加了 -0.01的延時, 是為了以後大家做播放器的時候 有可能暫停或者歌曲切換時 有可能 向前向後做片段銜接, 也是為了使用 playAtTime
去播放 指定位置的音樂用於 意外暫停或者播放上次播放的配置資訊使用 這裡看到我寫了一個
[self configNowPlayingInfoCenter];
配置後臺播放的頁面資訊
這個主要用於播放音樂在後臺時 鎖屏顯示的螢幕資訊 請看下面程式碼
//設定鎖屏狀態,顯示的歌曲資訊 -(void)configNowPlayingInfoCenter{ if (NSClassFromString(@"MPNowPlayingInfoCenter")) { NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; //歌曲名稱 [dict setObject:@"歌曲名稱" forKey:MPMediaItemPropertyTitle]; //演唱者 [dict setObject:@"演唱者" forKey:MPMediaItemPropertyArtist]; //專輯名 [dict setObject:@"專輯名" forKey:MPMediaItemPropertyAlbumTitle]; //專輯縮圖 UIImage *image = [UIImage imageNamed:@"sunyazhou"]; MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithImage:image]; [dict setObject:artwork forKey:MPMediaItemPropertyArtwork]; //音樂剩餘時長 [dict setObject:@20 forKey:MPMediaItemPropertyPlaybackDuration]; //音樂當前播放時間 在計時器中修改 // [dict setObject:[NSNumber numberWithDouble:100.0] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime]; //設定鎖屏狀態下螢幕顯示播放音樂資訊 [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict]; } }
如果需要在計時器中不斷重新整理鎖屏狀態下的播放進度條請寫如下程式碼
//計時器修改進度 - (void)changeProgress:(NSTimer *)sender{ if(self.player){ //當前播放時間 NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:[[MPNowPlayingInfoCenter defaultCenter] nowPlayingInfo]]; [dict setObject:[NSNumber numberWithDouble:self.player.currentTime] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime]; //音樂當前已經過時間 [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict]; } }
下面我們來介紹一下
[self setupNotifications];
註冊監聽 音訊意外中斷和耳機拔出時要暫停音樂播放
實現程式碼如下
/** 播放的通知處理 */ - (void)setupNotifications { NSNotificationCenter *nsnc = [NSNotificationCenter defaultCenter]; //新增意外中斷音訊播放的通知 [nsnc addObserver:self selector:@selector(handleInterruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]]; //新增線路變化通知 [nsnc addObserver:self selector:@selector(hanldeRouteChange:) name:AVAudioSessionRouteChangeNotification object:[AVAudioSession sharedInstance]]; }
注:記得在delloc裡面 [[NSNotificationCenter defaultCenter] removeObserver:self]
意外中斷音訊發生的場景 例如 聽歌過程中來電話或者 按住home鍵使用siri
下面是具體方法實現
/** 音訊意外打斷處理 @param notification 通知資訊 */ - (void)handleInterruption:(NSNotification *)notification { NSDictionary *info = notification.userInfo; AVAudioSessionInterruptionType type = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue]; if (type == AVAudioSessionInterruptionTypeBegan) { //Handle AVAudioSessionInterruptionTypeBegan [self pause]; } else { //Handle AVAudioSessionInterruptionTypeEnded AVAudioSessionInterruptionOptions options = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue]; NSError *error = nil; //啟用音訊會話 允許外接音響 [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:nil]; [[AVAudioSession sharedInstance] setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error]; if (options == AVAudioSessionInterruptionOptionShouldResume) { [self play]; } else { [self play]; } self.playButton.selected = YES; if (error) { NSLog(@"AVAudioSessionInterruptionOptionShouldResume失敗:%@",[error localizedDescription]); } } }
先說 handleInterruption
意外情況下中斷比如我按住home鍵使用siri
我會收到意外打斷的通知當 type == AVAudioSessionInterruptionTypeBegan
時 我們停止音樂播放或者暫停.
當type != AVAudioSessionInterruptionTypeBegan
的時候一定是 AVAudioSessionInterruptionTypeEnded
這個時候 notification.userInfo
裡面包含一個 AVAudioSessionInterruptionOptions
值來表明音訊會話是否已經重新啟用以及是否可以再次播放
注:這個地方遇到個坑 當意外中斷時候有時音訊會話會很不靈敏 後來發現這種情況下需要重新啟用會話 如下程式碼:
[[AVAudioSession sharedInstance] setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
這裡 AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
是為了通知其它應用會話被我激活了 很多播放器開發者很不講究 每次從來不用這個方法導致每次別人播放完音訊 自己都收不到音訊重新播放的資訊 建議大家以和為貴, 寫良心程式碼.
因為我外接的小米藍芽音響發現還是不好使 最後又補上了 AVAudioSessionCategoryOptionAllowBluetooth
這個
啟用音訊會話 允許外接音響
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:nil];
就好使了
下面說一下耳機插拔或者USB麥克風斷開 Apple有個什麼 Human Interface Guidelines(HIG)
相關定義 意思是說當硬體耳機拔出時建議 暫停播放音樂或者麥克風斷開時。就是處於靜音狀態。是為了保密播放內容不被外界聽到,不管蘋果啥規定 我們都得照辦 否則就得被拒。
- (void)hanldeRouteChange:(NSNotification *)notification { NSDictionary *info = notification.userInfo; AVAudioSessionRouteChangeReason reason = [info[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue]; //老裝置不可用 if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { AVAudioSessionRouteDescription *previousRoute = info[AVAudioSessionRouteChangePreviousRouteKey]; AVAudioSessionPortDescription *previousOutput = previousRoute.outputs[0]; NSString *portType = previousOutput.portType; if ([portType isEqualToString:AVAudioSessionPortHeadphones]) { [self stop]; self.playButton.selected = NO; } } }
這需要用 AVAudioSessionRouteChangeReasonKey
取出線路切換的原因 AVAudioSessionRouteChangeReason
原因有這麼多
typedef NS_ENUM(NSUInteger, AVAudioSessionRouteChangeReason) { AVAudioSessionRouteChangeReasonUnknown = 0, AVAudioSessionRouteChangeReasonNewDeviceAvailable = 1, AVAudioSessionRouteChangeReasonOldDeviceUnavailable = 2, AVAudioSessionRouteChangeReasonCategoryChange = 3, AVAudioSessionRouteChangeReasonOverride = 4, AVAudioSessionRouteChangeReasonWakeFromSleep = 6, AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory = 7, AVAudioSessionRouteChangeReasonRouteConfigurationChange NS_ENUM_AVAILABLE_IOS(7_0) = 8 } NS_AVAILABLE_IOS(6_0);
我們需要這個 AVAudioSessionRouteChangeReasonOldDeviceUnavailable
判斷是否是舊裝置
通過 AVAudioSessionRouteChangePreviousRouteKey
拿出
AVAudioSessionRouteDescription
描述資訊
previousRoute
在通過
previousRoute.outputs[0]
拿出 AVAudioSessionPortDescription
拿出 NSString *portType = previousOutput.portType
如果 [portType isEqualToString:AVAudioSessionPortHeadphones]
如果是耳機 AVAudioSessionPortHeadphones
則暫停播放
以上就是中斷和線路切換的一些程式碼邏輯
下面我介紹一些好玩的

前面說的一些後臺設定資訊顯示的內容就是上圖所示 在鎖屏的時候顯示
但是大家一定很奇怪的是怎麼實現接收 鎖屏狀態下 點選 上一曲 暫停/播放 下一曲等操作
需要在AppDelegate裡面寫上
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { AVAudioSession *session = [AVAudioSession sharedInstance]; NSError *error; if (![session setCategory:AVAudioSessionCategoryPlayback error:&error]) { NSLog(@"Category Error: %@", [error localizedDescription]); } if (![session setActive:YES error:&error]) { NSLog(@"Activation Error: %@", [error localizedDescription]); } [[UIApplication sharedApplication] beginReceivingRemoteControlEvents]; [self becomeFirstResponder]; return YES; }
這 [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
行程式碼 以及呼叫自己為 [self becomeFirstResponder];
第一響應者 這樣寫是為了應用響應音訊播放 後臺切換或者中斷的時候更靈敏.
- (BOOL)canBecomeFirstResponder { return YES; }
然後 寫上如下程式碼 處理 鎖屏狀態下 點選 上一曲 暫停/播放 下一曲等操作
- (void)remoteControlReceivedWithEvent:(UIEvent *)event { if (event.type == UIEventTypeRemoteControl) { switch (event.subtype) { case UIEventSubtypeRemoteControlPlay: NSLog(@"暫停播放"); break; case UIEventSubtypeRemoteControlPause: NSLog(@"繼續播放"); break; case UIEventSubtypeRemoteControlNextTrack: NSLog(@"下一曲"); break; case UIEventSubtypeRemoteControlPreviousTrack: NSLog(@"上一曲"); break; default: break; } } }
剩餘邏輯大家自己填充吧我就不介紹了.
好了AVAudioPlayer就到這吧!有啥疑問大家可以評論留言都能看到或者指正我的錯誤。我會及時改正.
全文完
文章最終的Demo獲取:加iOS高階技術交流群: 624212887
,獲取Demo,以及更多iOS學習資料
文章來源於網路,如有侵權請聯絡小編刪除