AV Foundation使用AVAudioPlayer播放音頻

2.1 iOS的音頻環境

當你在iPhone上點開一首歌曲,音頻在內置揚聲器中播放出來,此時有電話撥入,音樂會當即中止並處於暫停狀態,此時聽到的是手機呼叫的鈴音。若是此時你掛掉電話,剛纔的音樂聲再次響起,當你插上耳機,音樂播放時音頻輸出到了耳機裏。當聽完這首音樂摘下耳機後,你會發現聲音自動轉回內置揚聲器並處於暫停狀態。objective-c

iOS系統提供了一個可管理的音頻環境(managed audio environment),能夠帶給全部iOS用戶很是好的用戶體驗,這一過程具體是如何實現的呢?這裏會用到音頻會話(audio session)。網絡

2.2 理解音頻會話

音頻會話在應用程序和操做系統之間扮演着中間人的角色。它提供了一種簡單實用的方法使OS得知應用程序應該如何與iOS音頻環境進行交互。你不須要了解與音頻硬件交互的細節,只須要對應用程序的行爲進行語義上的描述便可。這一點使得你能夠指明應用程序的通常音頻行爲,並能夠把對該行爲的管理委託給音頻會話,這樣OS系統就能夠對用戶使用音頻的體驗進行適當的管理。session

全部iOS應用程序都具備音頻會話,不管其是否使用。默認音頻會話來自於如下一些預配置:app

  • 支持音頻播放,不支持音頻錄製
  • 在iOS中,當用戶切換響鈴/靜音開關到「靜音」模式時,應用程序正在被播放的全部音頻都會消失
  • 在iOS中,當設備鎖屏時,應用程序的音頻將處於靜音狀態
  • 當應用程序播放音頻時,全部其餘後臺播放音頻,例如音樂的應用程序都會被靜音。

默認音頻會話提供了許多實用功能,可是在大多數狀況下,你須要自定義音頻會話來適配你本身應用程序的需求。oop

2.2.1 音頻會話的分類

  • AVAudioSessionCategoryAmbient:支持混音,鎖屏和響鈴/靜音開關會使音頻靜音,只容許輸出(播放)音頻。
  • AVAudioSessionCategorySoloAmbient:默認設置,不支持混音,鎖屏和響鈴/靜音開關會使音頻靜音,只容許輸出(播放)音頻。
  • AVAudioSessionCategoryPlayback:默認不支持混音,若是想要支持混音,可使用AVAudioSessionCategoryOptionMixWithOthers這個option。鎖屏和響鈴/靜音開關不會使音頻靜音,爲了支持應用程序轉到後臺能夠繼續在後臺播放音頻,能夠在info.plist文件中添加UIBackgroundModes的key和audio的值。
  • AVAudioSessionCategoryRecord:只要該會話處於激活狀態,會使系統中全部輸出靜音。爲了支持應用程序轉到後臺能夠繼續在後臺錄製音頻,須要在info.plist文件中添加UIBackgroundModes的key和audio的值。而且用戶必須容許,才能夠進行錄製。
  • AVAudioSessionCategoryPlayAndRecord:這個分類能夠同時用來播放和錄製音頻。鎖屏和響鈴/靜音開關不會使音頻靜音,要在應用程序轉到後臺能夠繼續播放音頻須要在info.plist文件中添加UIBackgroundModes的key和audio的值。該分類支持同時進行音頻的錄製和播放,同時也支持音頻錄製和播放不一樣時進行。默認該分類不支持混音的,爲了支持混音,可使用AVAudioSessionCategoryOptionMixWithOthers這個option。而且用戶必須容許才能夠進行錄製。
  • AVAudioSessionCategoryMultiRoute:該分類用於同時將不一樣的音頻數據流路由到不一樣的輸出設備,能夠輸入輸出還能夠支持同時輸入和輸出。使用此分類,須要更專業的知識的支持。

2.2.2 配置音頻會話

音頻會話在應用程序的生命週期中是能夠修改的,但一般咱們只對其配置一次,就是在應用程序啓動時。那麼,配置應用程序的最佳位置就是- (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;
}
複製代碼

2.3 使用 AVAudioPlayer播放音頻

音頻播放時不少應用程序的常見需求,AV Foundation讓這一功能的實現變得很是簡單,這一點要歸功於一個名爲AVAudioPlayer的類。該類的實例提供了一種簡單地從文本內存中播放音頻的方法。ui

AVAudioPlayer構建與Core Audio中的C-based Audio Queue Services的最頂層。因此它能夠提供全部你在Audio Queue Services中所能找到的核心功能,好比播放、循環甚至音頻計量,但使用的是Objective-C接口。除非你須要從網絡中播放音頻,須要訪問原始音頻樣本或須要很是低的延時,不然AVAudioPlayer都能勝任。atom

2.3.1 建立AVAudioPlayer

有兩種方法能夠建立一個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];
}
複製代碼

2.3.2 對播放進行控制

播放實例包含了全部開發者指望的對播放行爲進行控制的方法。調用play方法能夠實現當即播放音頻的功能,pause方法能夠對播放暫停,stop方法能夠中止播放行爲。pause方法和stop方法在應用程序外面看來實現的功能都是中止當前的播放行爲。下一時間咱們調用play方法,經過pause和stop方法中止的音頻都會繼續播放。這二者最主要的區別在於調用stop方法會撤銷調用prepareToPlay時所作的設置,而調用pause方法不會。操作系統

除了前面描述的標準常規方法以外,開發者還可使用其餘一些方法,以下:

  • 修改播放器的音量: 播放器的音量獨立於系統的音量,音量或播放增益定義爲0.0(靜音)到1.0(最大音量)之間的浮點值。
  • 修改播放器的pan值: 容許使用立體聲播放聲音:播放器的pan值由一個浮點數表示。範圍從-1.0(極左)到1.0(極右)默認值爲0.0(居中)。
  • 調整播放率: 容許用戶在不改變音調的狀況下調整播放率,範圍從0.5(半速)到2.0(2倍速)
  • 經過設置 numberOfLoops屬性實現音頻無縫循環: 給這個屬性設置一個大於0的數,能夠實現播放器n次循環播放。若是屬性賦值爲-1會致使播放器無限循環。
  • 進行音頻計量: 當播放器發生時從播放器讀取音量力度的平均值和峯值。

2.4 AVAudioPlayer演練

需求:同步播放三個播放器,經過控制每一個播放器的音量等級和立體聲方面的pan值將這些音樂混合,進而控制總體播放速率。

  • AVAudioPlayerManager.h
@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

複製代碼
  • AVAudioPlayerManager.m
@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;
}
複製代碼

2.5 配置音頻會話

在上面這個例子中,咱們沒有配置音頻會話,因此咱們使用的系統默認的音頻會話的配置。

  • 操做一,切換設備的響鈴/靜音開關,在靜音狀態下,音頻輸出靜音,在響鈴狀態音頻正常輸出。
  • 操做二,鎖屏操做,音頻輸出中止,解鎖屏幕,音頻繼續播放

以上兩個操做並非咱們但願的,咱們但願切換響鈴/靜音開關繼續播放音頻而且鎖屏後繼續播放音頻,因此咱們要設置音頻會話。

  • - (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;
}
複製代碼
  • 當音頻會話設置完成後,再次運行程序,切換響鈴/鎖屏按鈕,播放的聲音不會消失了。可是鎖屏後,聲音仍然消失。
  • 設置AVAudioSessionCategoryPlayback可讓音頻在後臺進行輸出,可是前提是咱們須要設置info.plist,讓設備支持後臺播放的功能
<key>UIBackgroundModes</key>
	<array>
		<string>audio</string>
	</array>
複製代碼
  • 添加該配置後,音頻輸出就能夠在後臺完成了,鎖屏按鈕也不會使其中止。

2.6 處理中斷事件

中斷在iOS設備中常常出現,在使用設備的過程當中常常會有諸如電話呼入、鬧鈴響起等狀況。雖然iOS系統自己能夠很好地處理這些事件。不過咱們仍須要針對這些狀況作本身的處理。

  1. 在設備上運行應用程序並播放音頻
  2. 當音頻處於播放狀態時,從另一臺設備發起電話呼叫以製造中斷
  3. 掛斷電話,中止呼叫

按照上述的場景進行測試,你會發現,當中斷髮生時,播放中的音頻會慢慢消失和暫停。這個效果是自動實現的,咱們沒有作任何的處理。當另外一臺手機的電話被掛斷,會出現一些問題,播放/中止功能消失,音頻也再也不繼續播放。

2.6.1 音頻會話通知

  • 首先須要監聽中斷出現的通知,註冊AVAudioSession發送的通知AVAudioSessionInterruptionNotification。只須要註冊一次,在init方法中進行通知的註冊。
- (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;
}
複製代碼
  • 接收到通知後,處理通知
  • 從userInfo中獲取信息,獲取開始打斷和結束打斷的枚舉值,開始打斷後,中止播放。若是控制器處理一些業務邏輯,經過代理傳遞出去
  • 當打斷結束後,獲取音頻會話被從新激活,咱們繼續播放,經過代理傳遞到控制器,處理相關業務邏輯
- (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];
}
複製代碼

2.7 對線路改變的響應

在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完成的一個簡單地播放器功能。實際開發中,咱們只要注意處理咱們真正遇到的場景就能夠了。

相關文章
相關標籤/搜索