當你在iPhone上點開一首歌曲,音頻在內置揚聲器中播放出來,此時有電話撥入,音樂會當即中止並處於暫停狀態,此時聽到的是手機呼叫的鈴音。若是此時你掛掉電話,剛纔的音樂聲再次響起,當你插上耳機,音樂播放時音頻輸出到了耳機裏。當聽完這首音樂摘下耳機後,你會發現聲音自動轉回內置揚聲器並處於暫停狀態。objective-c
iOS系統提供了一個可管理的音頻環境(managed audio environment),能夠帶給全部iOS用戶很是好的用戶體驗,這一過程具體是如何實現的呢?這裏會用到音頻會話(audio session)。網絡
音頻會話在應用程序和操做系統之間扮演着中間人的角色。它提供了一種簡單實用的方法使OS得知應用程序應該如何與iOS音頻環境進行交互。你不須要了解與音頻硬件交互的細節,只須要對應用程序的行爲進行語義上的描述便可。這一點使得你能夠指明應用程序的通常音頻行爲,並能夠把對該行爲的管理委託給音頻會話,這樣OS系統就能夠對用戶使用音頻的體驗進行適當的管理。session
全部iOS應用程序都具備音頻會話,不管其是否使用。默認音頻會話來自於如下一些預配置:app
默認音頻會話提供了許多實用功能,可是在大多數狀況下,你須要自定義音頻會話來適配你本身應用程序的需求。oop
音頻會話在應用程序的生命週期中是能夠修改的,但一般咱們只對其配置一次,就是在應用程序啓動時。那麼,配置應用程序的最佳位置就是- (BOOL)application:didFinishLaunchingWithOptions:
方法。測試
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 回去音頻會話單例
AVAudioSession *session = [AVAudioSession sharedInstance];
// 設置音頻會話分類
if (![session setCategory:AVAudioSessionCategoryPlayback error:nil]) {
NSLog(@"設置音頻會話失敗");
}
// 激活音頻會話
if (![session setActive:YES error:nil]) {
NSLog(@"激活音頻會話失敗");
}
return YES;
}
複製代碼
音頻播放時不少應用程序的常見需求,AV Foundation讓這一功能的實現變得很是簡單,這一點要歸功於一個名爲AVAudioPlayer的類。該類的實例提供了一種簡單地從文本或內存中播放音頻的方法。ui
AVAudioPlayer構建與Core Audio中的C-based Audio Queue Services的最頂層。因此它能夠提供全部你在Audio Queue Services中所能找到的核心功能,好比播放、循環甚至音頻計量,但使用的是Objective-C接口。除非你須要從網絡中播放音頻,須要訪問原始音頻樣本或須要很是低的延時,不然AVAudioPlayer都能勝任。atom
有兩種方法能夠建立一個AVAudioPlayer,使用包含播放音頻的內存版本的NSData或本地音頻文件的NSURL。spa
@interface ViewController ()
@property (nonatomic, strong) AVAudioPlayer *player;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 從bundle中獲取資源的NSURL實例
NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"tqsh.mp3" withExtension:nil];
// 根據URL建立一個AVAudioPlayer實例
self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:nil];
if (self.player) {
// 建議開發者,先調用這個方法
// 調用此方法將預加載緩衝區並獲取音頻硬件,這樣作能夠將調用play方法和聽到輸出聲音之間的延時下降到最小
[self.player prepareToPlay];
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.player play];
}
複製代碼
播放實例包含了全部開發者指望的對播放行爲進行控制的方法。調用play方法能夠實現當即播放音頻的功能,pause方法能夠對播放暫停,stop方法能夠中止播放行爲。pause方法和stop方法在應用程序外面看來實現的功能都是中止當前的播放行爲。下一時間咱們調用play方法,經過pause和stop方法中止的音頻都會繼續播放。這二者最主要的區別在於調用stop方法會撤銷調用prepareToPlay時所作的設置,而調用pause方法不會。操作系統
除了前面描述的標準常規方法以外,開發者還可使用其餘一些方法,以下:
需求:同步播放三個播放器,經過控制每一個播放器的音量等級和立體聲方面的pan值將這些音樂混合,進而控制總體播放速率。
@interface AVAudioPlayerManager : NSObject
@property (nonatomic, assign, readonly, getter=isPlaying) BOOL playing;
- (void)play;
- (void)stop;
- (void)adjustRate:(CGFloat)rate;
- (void)adjustPan:(CGFloat)pan forPlayerAtIndex:(NSInteger)index;
- (void)adjustVolume:(CGFloat)volume forPlayerAtIndex:(NSInteger)index;
@end
複製代碼
@interface AVAudioPlayerManager ()
@property (nonatomic, assign) BOOL playing;
@property (nonatomic, strong) NSArray *players;
@end
@implementation AVAudioPlayerManager
- (instancetype)init {
if (self = [super init]) {
AVAudioPlayer *guitarPlayer = [self createPlayerWithFileName:@"guitar"];
AVAudioPlayer *bassPlayer = [self createPlayerWithFileName:@"bass"];
AVAudioPlayer *drumsPlayer = [self createPlayerWithFileName:@"drums"];
_players = @[guitarPlayer, bassPlayer, drumsPlayer];
}
return self;
}
- (AVAudioPlayer *)createPlayerWithFileName:(NSString *)fileName {
NSURL *fileURL = [[NSBundle mainBundle] URLForResource:fileName withExtension:@"caf"];
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:nil];
if (player) {
player.enableRate = YES;
player.numberOfLoops = -1;
[player prepareToPlay];
} else {
NSLog(@"建立player失敗");
}
return player;
}
- (void)play {
if (!self.isPlaying) {
NSTimeInterval delayTime = [self.players[0] deviceCurrentTime] + 0.01;
for (AVAudioPlayer *player in self.players) {
[player playAtTime:delayTime];
}
self.playing = YES;
}
}
- (void)stop {
if (self.isPlaying) {
for (AVAudioPlayer *player in self.players) {
[player stop];
player.currentTime = 0.0;
}
self.playing = NO;
}
}
- (void)adjustPan:(CGFloat)pan forPlayerAtIndex:(NSInteger)index {
if ([self isValidIndex:index]) {
AVAudioPlayer *player = self.players[index];
player.pan = pan;
}
}
- (void)adjustVolume:(CGFloat)volume forPlayerAtIndex:(NSInteger)index {
if ([self isValidIndex:index]) {
AVAudioPlayer *player = self.players[index];
player.volume = volume;
}
}
- (void)adjustRate:(CGFloat)rate {
for (AVAudioPlayer *player in self.players) {
player.rate = rate;
}
}
- (BOOL)isValidIndex:(NSInteger)index {
return index == 0 || index < self.players.count;
}
複製代碼
在上面這個例子中,咱們沒有配置音頻會話,因此咱們使用的系統默認的音頻會話的配置。
以上兩個操做並非咱們但願的,咱們但願切換響鈴/靜音開關繼續播放音頻而且鎖屏後繼續播放音頻,因此咱們要設置音頻會話。
- (BOOL)application:didFinishLaunchingWithOptions:
對音頻會話進行配置,由於咱們的主要功能就是播放因此設置AVAudioSessionCategoryPlayback分類。- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
if (![audioSession setCategory:AVAudioSessionCategoryPlayback error:nil]) {
NSLog(@"設置音頻會話分類失敗");
}
if (![audioSession setActive:YES error:nil]) {
NSLog(@"音頻會話激活失敗");
}
return YES;
}
複製代碼
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
複製代碼
中斷在iOS設備中常常出現,在使用設備的過程當中常常會有諸如電話呼入、鬧鈴響起等狀況。雖然iOS系統自己能夠很好地處理這些事件。不過咱們仍須要針對這些狀況作本身的處理。
按照上述的場景進行測試,你會發現,當中斷髮生時,播放中的音頻會慢慢消失和暫停。這個效果是自動實現的,咱們沒有作任何的處理。當另外一臺手機的電話被掛斷,會出現一些問題,播放/中止功能消失,音頻也再也不繼續播放。
- (instancetype)init {
if (self = [super init]) {
AVAudioPlayer *guitarPlayer = [self createPlayerWithFileName:@"guitar"];
AVAudioPlayer *bassPlayer = [self createPlayerWithFileName:@"bass"];
AVAudioPlayer *drumsPlayer = [self createPlayerWithFileName:@"drums"];
_players = @[guitarPlayer, bassPlayer, drumsPlayer];
// 註冊音頻會話中斷通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterruption:) name:AVAudioSessionInterruptionNotification object:nil];
}
return self;
}
複製代碼
- (void)handleInterruption:(NSNotification *)notification {
NSDictionary *info = notification.userInfo;
NSLog(@"%@", info);
// 獲取音頻會話打斷類型
AVAudioSessionInterruptionType type = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
if (type == AVAudioSessionInterruptionTypeBegan) {
NSLog(@"開始打斷");
[self stop];
// 中斷中止 交給代理處理相關邏輯
if (self.delegate && [self.delegate respondsToSelector:@selector(audioPlayerManagerPlaybackStopped:)]) {
[self.delegate audioPlayerManagerPlaybackStopped:self];
}
} else {
NSLog(@"結束打斷");
AVAudioSessionInterruptionOptions options = [info[AVAudioSessionInterruptionOptionKey] unsignedIntegerValue];
if (options == AVAudioSessionInterruptionOptionShouldResume) { // 音頻會話從新激活
[self play];
// 從新激活 交給代理 處理相關邏輯
if (self.delegate && [self.delegate respondsToSelector:@selector(audioPlayerManagerPlaybackBegan:)]) {
[self.delegate audioPlayerManagerPlaybackBegan:self];
}
}
}
}
複製代碼
@protocol AVAudioPlayerManagerDelegate <NSObject>
@optional
/// 中斷 -> 中止播放
- (void)audioPlayerManagerPlaybackStopped:(AVAudioPlayerManager *)manager;
/// 結束中斷安 -> 開始播放
- (void)audioPlayerManagerPlaybackBegan:(AVAudioPlayerManager *)manager;
@end
複製代碼
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
複製代碼
在iOS設備上添加或移除音頻輸入、輸出線路時,會發生線路改變。好比用戶插入和拔出耳機。當這些事件發生時,音頻會根據狀況改變輸入或輸出線路,同時AVAudioSession會廣播一個描述該變化的通知給全部相關的監聽器。
對咱們的例子進行一個測試,開始播放,並在播放期間插入耳機。音頻的輸出線路變爲耳機並繼續正常播放,這是咱們所指望的結果。保持音頻的播放狀態,斷開耳機的鏈接。音頻線路再次回到設備的內置揚聲器,咱們再次聽到了聲音。雖然線路變化同預期同樣,可是有一個問題,用戶插上耳機多是爲了保持隱私性,耳機斷開鏈接有可能須要繼續保密,因此咱們須要耳機斷開鏈接時候,音樂要中止播放。
當線路發生變化時要有通知,咱們須要註冊AVAudioSession發送的通知,在init方法中。該通知爲AVAdudioSessionRouteChangeNotification。
- (instancetype)init {
if (self = [super init]) {
AVAudioPlayer *guitarPlayer = [self createPlayerWithFileName:@"guitar"];
AVAudioPlayer *bassPlayer = [self createPlayerWithFileName:@"bass"];
AVAudioPlayer *drumsPlayer = [self createPlayerWithFileName:@"drums"];
_players = @[guitarPlayer, bassPlayer, drumsPlayer];
// 註冊音頻會話中斷通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterruption:) name:AVAudioSessionInterruptionNotification object:nil];
// 註冊線路變化通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleRouteChange:) name:AVAudioSessionRouteChangeNotification object:nil];
}
return self;
}
複製代碼
- (void)handleRouteChange:(NSNotification *)notification {
NSDictionary *userInfo = notification.userInfo;
AVAudioSessionRouteChangeReason reason = [userInfo[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { // 線路回到手機端
AVAudioSessionRouteDescription *route = userInfo[AVAudioSessionRouteChangePreviousRouteKey];
AVAudioSessionPortDescription *output = route.outputs.firstObject;
AVAudioSessionPort portType = output.portType;
// 耳機 或 藍牙音頻設備
if ([portType isEqualToString:AVAudioSessionPortHeadphones] ||
[portType isEqualToString:AVAudioSessionPortBluetoothA2DP]) {
[self stop];
if (self.delegate && [self.delegate respondsToSelector:@selector(audioPlayerManagerPlaybackStopped:)]) {
[self.delegate audioPlayerManagerPlaybackStopped:self];
}
}
}
}
複製代碼
如今,當咱們斷開耳機,音頻播放也會中止。以上就是使用AVAudioPlayer完成的一個簡單地播放器功能。實際開發中,咱們只要注意處理咱們真正遇到的場景就能夠了。