音頻

概覽

隨著移動互聯(lián)網(wǎng)的發(fā)展,如今的手機早已不是打電話、發(fā)短信那么簡單了,播放音樂、視頻、錄音、拍照等都是很常用的功能。在iOS中對于多媒體的支持是非常強大的,無論是音視頻播放、錄制,還是對麥克風、攝像頭的操作都提供了多套API。在今天的文章中將會對這些內(nèi)容進行一一介紹:

音頻

在iOS中音頻播放從形式上可以分為音效播放和音樂播放。前者主要指的是一些短音頻播放,通常作為點綴音頻,對于這類音頻不需要進行進度、循環(huán)等控制。后者指的是一些較長的音頻,通常是主音頻,對于這些音頻的播放通常需要進行精確的控制。在iOS中播放兩類音頻分別使用AudioToolbox.framework和AVFoundation.framework來完成音效和音樂播放。

音效

AudioToolbox.framework是一套基于C語言的框架,使用它來播放音效其本質(zhì)是將短音頻注冊到系統(tǒng)聲音服務(System Sound Service)。System Sound Service是一種簡單、底層的聲音播放服務,但是它本身也存在著一些限制:

  • 音頻播放時間不能超過30s
  • 數(shù)據(jù)必須是PCM或者IMA4格式
  • 音頻文件必須打包成.caf、.aif、.wav中的一種(注意這是官方文檔的說法,實際測試發(fā)現(xiàn)一些.mp3也可以播放)

使用System Sound Service 播放音效的步驟如下:

  1. 調(diào)用AudioServicesCreateSystemSoundID( CFURLRef inFileURL, SystemSoundID* outSystemSoundID)函數(shù)獲得系統(tǒng)聲音ID。
  2. 如果需要監(jiān)聽播放完成操作,則使用AudioServicesAddSystemSoundCompletion( SystemSoundID inSystemSoundID,
    CFRunLoopRef inRunLoop, CFStringRef inRunLoopMode, AudioServicesSystemSoundCompletionProc inCompletionRoutine, void* inClientData)方法注冊回調(diào)函數(shù)。
    3.調(diào)用AudioServicesPlaySystemSound(SystemSoundID inSystemSoundID) 或者AudioServicesPlayAlertSound(SystemSoundID inSystemSoundID) 方法播放音效(后者帶有震動效果)。

下面是一個簡單的示例程序:

//
//  ViewController.m
//  01-音效
//
//  Created by Andy on 2020/2/13.
//  Copyright ? 2020 李正林. All rights reserved.
//

#import "ViewController.h"
#import <AudioToolbox/AudioToolbox.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    [self playSoundEffect:@"videoRing.caf"];
}

/// 播放完成回調(diào)函數(shù)
/// @param soundID 系統(tǒng)聲音ID
/// @param clientData 回調(diào)時傳遞的數(shù)據(jù)
void soundCompleteCallback(SystemSoundID soundID, void * clientData) {
    
    NSLog(@"播放完成...");
}

/// 播放音頻文件
/// @param name 音頻文件名稱
- (void)playSoundEffect:(NSString *)name {
    
    NSString *audioFile = [[NSBundle mainBundle] pathForResource:name ofType:nil];
    NSURL *fileUrl = [NSURL fileURLWithPath:audioFile];
    // 1.獲得系統(tǒng)聲音ID
    SystemSoundID soundID = 0;
    /**
     * inFileUrl:音頻文件url
     * outSystemSoundID:聲音id(此函數(shù)會將音頻文件加入到系統(tǒng)音頻服務器中并返回一個長整型ID)
     */
    AudioServicesCreateSystemSoundID((__bridge CFURLRef)(fileUrl), &soundID);
    // 如果需要在播放完之后執(zhí)行某些操作,可以調(diào)用如下方法注冊一個播放完成回調(diào)函數(shù)
    AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, soundCompleteCallback, NULL);
    //2.播放音頻
    AudioServicesPlaySystemSound(soundID); // 播放音效
//    AudioServicesPlayAlertSound(soundID); // 播放音效并震動
}

@end

音樂

如果播放較大的音頻或者要對音頻有精確的控制則System Sound Service可能就很難滿足實際需求了,通常這種情況會選擇使用AVFoundation.framework中的AVAudioPlayer來實現(xiàn)。AVAudioPlayer可以看成一個播放器,它支持多種音頻格式,而且能夠進行進度、音量、播放速度等控制。首先簡單看一下AVAudioPlayer常用的屬性和方法:

屬性 說明
@property(readonly, getter=isPlaying) BOOL playing 是否正在播放,只讀
@property(readonly) NSUInteger numberOfChannels 音頻聲道數(shù),只讀
@property(readonly) NSTimeInterval duration 音頻時長
@property(readonly) NSURL *url 音頻文件路徑,只讀
@property(readonly) NSData *data 音頻數(shù)據(jù),只讀
@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 輸出設備播放音頻的時間,注意如果播放中被暫停此時間也會繼續(xù)累加
@property NSInteger numberOfLoops 循環(huán)播放次數(shù),如果為0則不循環(huán),如果小于0則無限循環(huán),大于0則表示循環(huán)次數(shù)
@property(readonly) NSDictionary *settings 音頻播放設置信息,只讀
@property(getter=isMeteringEnabled) BOOL meteringEnabled 是否啟用音頻測量,默認為NO,一旦啟用音頻測量可以通過updateMeters方法更新測量值
對象方法 說明
- (instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError 使用文件URL初始化播放器,注意這個URL不能是HTTP URL,AVAudioPlayer不支持加載網(wǎng)絡媒體流,只能播放本地文件
- (instancetype)initWithData:(NSData *)data error:(NSError **)outError 使用NSData初始化播放器,注意使用此方法時必須文件格式和文件后綴一致,否則出錯,所以相比此方法更推薦使用上述方法或- (instancetype)initWithData:(NSData *)data fileTypeHint:(NSString *)utiString error:(NSError **)outError方法進行初始化
- (BOOL)prepareToPlay; 加載音頻文件到緩沖區(qū),注意即使在播放之前音頻文件沒有加載到緩沖區(qū)程序也會隱式調(diào)用此方法。
- (BOOL)play; 播放音頻文件
- (BOOL)playAtTime:(NSTimeInterval)time 在指定的時間開始播放音頻
- (void)pause; 暫停播放
- (void)stop; 停止播放
- (void)updateMeters 更新音頻測量值,注意如果要更新音頻測量值必須設置meteringEnabled為YES,通過音頻測量值可以即時獲得音頻分貝等信息
- (float)peakPowerForChannel:(NSUInteger)channelNumber; 獲得指定聲道的分貝峰值,注意如果要獲得分貝峰值必須在此之前調(diào)用updateMeters方法
- (float)averagePowerForChannel:(NSUInteger)channelNumber 獲得指定聲道的分貝平均值,注意如果要獲得分貝平均值必須在此之前調(diào)用updateMeters方法
@property(nonatomic, copy) NSArray *channelAssignments 獲得或設置播放聲道
代理方法 說明
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag 音頻播放完成
- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error 音頻解碼發(fā)生錯誤

AVAudioPlayer的使用比較簡單:

1.初始化AVAudioPlayer對象,此時通常指定本地文件路徑。
2.設置播放器屬性,例如重復次數(shù)、音量大小等。
3.調(diào)用play方法播放。

下面就使用AVAudioPlayer實現(xiàn)一個簡單播放器,在這個播放器中實現(xiàn)了播放、暫停、顯示播放進度功能,當然例如調(diào)節(jié)音量、設置循環(huán)模式、甚至是聲波圖像(通過分析音頻分貝值)等功能都可以實現(xiàn),這里就不再一一演示。界面效果如下:

Simulator Screen Shot - iPhone 11 Pro Max - 2020-02-15 at 22.10.28.png

當然由于AVAudioPlayer一次只能播放一個音頻文件,所有上一曲、下一曲其實可以通過創(chuàng)建多個播放器對象來完成,這里暫不實現(xiàn)。播放進度的實現(xiàn)主要依靠一個定時器實時計算當前播放時長和音頻總時長的比例,另外為了演示委托方法,下面的代碼中也實現(xiàn)了播放完成委托方法,通常如果有下一曲功能的話播放完可以觸發(fā)下一曲音樂播放。下面是主要代碼:

//
//  ViewController.m
//  02-音樂
//
//  Created by Andy on 2020/2/13.
//  Copyright ? 2020 李正林. All rights reserved.
//

#define kMusicFile @"KGETOSKBDLQMOD02.mp3"
#define kMusicSinger @"影視劇樂隊"
#define kMusicTitle @"喜劇之王背景音樂"

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>

@interface ViewController () <AVAudioPlayerDelegate>

@property (nonatomic, strong) AVAudioPlayer * audioPlayer; //播放器
@property (weak, nonatomic) IBOutlet UILabel *controlPanel; //控制面板
@property (weak, nonatomic) IBOutlet UIProgressView *playProgress;//播放進度
@property (weak, nonatomic) IBOutlet UILabel *musicSinger; //演唱者
@property (weak, nonatomic) IBOutlet UIButton *playOrPause; //播放/暫停按鈕(如果tag為0認為是暫停狀態(tài),1是播放狀態(tài))

@property (weak ,nonatomic) NSTimer *timer;//進度更新定時器

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    [self setupUI];
}

- (void)setupUI {
    
    self.controlPanel.text = kMusicTitle;
    self.musicSinger.text = kMusicSinger;
    self.playOrPause.selected = YES;
    self.playProgress.progress = 0.0;
}

- (NSTimer *)timer {
    
    if (!_timer) {
        _timer=[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateProgress) userInfo:nil repeats:true];
    }
    return _timer;
}

/// 創(chuàng)建播放器
- (AVAudioPlayer *)audioPlayer {
    
    if (!_audioPlayer) {
        
        NSString *urlStr=[[NSBundle mainBundle]pathForResource:kMusicFile ofType:nil];
        NSURL *url=[NSURL fileURLWithPath:urlStr];
        NSError *error=nil;
        // 初始化播放器,注意這里的Url參數(shù)只能時文件路徑,不支持HTTP Url
        _audioPlayer=[[AVAudioPlayer alloc]initWithContentsOfURL:url error:&error];
        // 設置播放器屬性
        _audioPlayer.numberOfLoops = 0; // 設置為0不循環(huán)
        _audioPlayer.delegate = self;
        [_audioPlayer prepareToPlay]; // 加載音頻文件到緩存
        
        if(error) {
            
            NSLog(@"初始化播放器過程發(fā)生錯誤,錯誤信息:%@",error.localizedDescription);
            return nil;
        }
    }
    return _audioPlayer;
}

/// 播放音頻
- (void)play {
    
    if (![self.audioPlayer isPlaying]) {
        [self.audioPlayer play];
        self.timer.fireDate=[NSDate distantPast];//恢復定時器
    }
}

/// 暫停播放
- (void)pause {
    
    if ([self.audioPlayer isPlaying]) {
        [self.audioPlayer pause];
        self.timer.fireDate=[NSDate distantFuture]; // 暫停定時器,注意不能調(diào)用invalidate方法,此方法會取消,之后無法恢復
    }
}

/// 點擊播放/暫停按鈕
/// @param sender 播放/暫停按鈕
- (IBAction)playClick:(UIButton *)sender {
    
    sender.selected = !sender.selected;
    if(sender.selected) {
        sender.selected = YES;
        [self pause];
    } else {
        sender.selected = NO;
        [self play];
    }
}

/// 更新播放進度
- (void)updateProgress {
    
    float progress= self.audioPlayer.currentTime /self.audioPlayer.duration;
    [self.playProgress setProgress:progress animated:true];
}

#pragma mark - 播放器代理方法
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
    
    NSLog(@"音樂播放完成...");
    self.playOrPause.selected = YES;
}

@end

音頻會話

事實上上面的播放器還存在一些問題,例如通常我們看到的播放器即使退出到后臺也是可以播放的,而這個播放器如果退出到后臺它會自動暫停。如果要支持后臺播放需要做下面幾件事情:

1.設置后臺運行模式:在plist文件中添加Required background modes,并且設置item 0=App plays audio or streams audio/video using AirPlay(其實可以直接通過Xcode在Project Targets-Capabilities-Background Modes中設置)

WeChat35d58ef472a48c086cfc1ca8bfb2f7da.png

2.設置AVAudioSession的類型為AVAudioSessionCategoryPlayback并且調(diào)用setActive::方法啟動會話。

AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
[audioSession setActive:YES error:nil];

3.為了能夠讓應用退到后臺之后支持耳機控制,建議添加遠程控制事件(這一步不是后臺播放必須的)

前兩步是后臺播放所必須設置的,第三步主要用于接收遠程事件,這部分內(nèi)容之前的文章中有詳細介紹,如果這一步不設置雖然也能夠在后臺播放,但是無法獲得音頻控制權(quán)(如果在使用當前應用之前使用其他播放器播放音樂的話,此時如果按耳機播放鍵或者控制中心的播放按鈕則會播放前一個應用的音頻),并且不能使用耳機進行音頻控制。第一步操作相信大家都很容易理解,如果應用程序要允許運行到后臺必須設置,正常情況下應用如果進入后臺會被掛起,通過該設置可以讓應用程序繼續(xù)在后臺運行。但是第二步使用的AVAudioSession有必要進行一下詳細的說明。

在iOS中每個應用都有一個音頻會話,這個會話就通過AVAudioSession來表示。AVAudioSession同樣存在于AVFoundation框架中,它是單例模式設計,通過sharedInstance進行訪問。在使用Apple設備時大家會發(fā)現(xiàn)有些應用只要打開其他音頻播放就會終止,而有些應用卻可以和其他應用同時播放,在多種音頻環(huán)境中如何去控制播放的方式就是通過音頻會話來完成的。下面是音頻會話的幾種會話模式:

會話類型 說明 是否要求輸入 是否要求輸出 是否遵從靜音鍵
AVAudioSessionCategoryAmbient 混音播放,可以與其他音頻應用同時播放
AVAudioSessionCategorySoloAmbient 獨占播放
AVAudioSessionCategoryPlayback 后臺播放,也是獨占的
AVAudioSessionCategoryRecord 錄音模式,用于錄音時使用
AVAudioSessionCategoryPlayAndRecord 播放和錄音,此時可以錄音也可以播放
AVAudioSessionCategoryAudioProcessing 硬件解碼音頻,此時不能播放和錄制
AVAudioSessionCategoryMultiRoute 多種輸入輸出,例如可以耳機、USB設備同時播放

注意:是否遵循靜音鍵表示在播放過程中如果用戶通過硬件設置為靜音是否能關閉聲音。

根據(jù)前面對音頻會話的理解,相信大家開發(fā)出能夠在后臺播放的音頻播放器并不難,但是注意一下,在前面的代碼中也提到設置完音頻會話類型之后需要調(diào)用setActive::方法將會話激活才能起作用。類似的,如果一個應用已經(jīng)在播放音頻,打開我們的應用之后設置了在后臺播放的會話類型,此時其他應用的音頻會停止而播放我們的音頻,如果希望我們的程序音頻播放完之后(關閉或退出到后臺之后)能夠繼續(xù)播放其他應用的音頻的話則可以調(diào)用setActive::方法關閉會話。代碼如下:

//
//  ViewController.m
//  03-音頻會話
//
//  Created by Andy on 2020/2/13.
//  Copyright ? 2020 李正林. All rights reserved.
//

#define kMusicFile @"KGETOSKBDLQMOD03.mp3"
#define kMusicSinger @"陳瑞"
#define kMusicTitle @"天生一對"

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>

@interface ViewController () <AVAudioPlayerDelegate>

@property (nonatomic, strong) AVAudioPlayer * audioPlayer; //播放器
@property (weak, nonatomic) IBOutlet UILabel *controlPanel; //控制面板
@property (weak, nonatomic) IBOutlet UIProgressView *playProgress;//播放進度
@property (weak, nonatomic) IBOutlet UILabel *musicSinger; //演唱者
@property (weak, nonatomic) IBOutlet UIButton *playOrPause; //播放/暫停按鈕(如果tag為0認為是暫停狀態(tài),1是播放狀態(tài))

@property (weak ,nonatomic) NSTimer *timer;//進度更新定時器

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    [self setupUI];
}

/// 顯示當面視圖控制器時注冊遠程事件
/// @param animated 是否以動畫的形式顯示
- (void)viewWillAppear:(BOOL)animated {
    
    [super viewWillAppear:animated];
    
    // 開啟遠程控制
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
    // 作為第一響應者
//    [self becomeFirstResponder];
}

/// 當前控制器視圖不顯示時取消遠程控制
/// @param animated 是否以動畫的形式消失
- (void)viewWillDisappear:(BOOL)animated {
    
    [super viewWillDisappear:animated];
    [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
//    [self resignFirstResponder];
}

- (void)setupUI {
    
    self.controlPanel.text = kMusicTitle;
    self.musicSinger.text = kMusicSinger;
    self.playOrPause.selected = YES;
    self.playProgress.progress = 0.0;
}

- (NSTimer *)timer {
    
    if (!_timer) {
        _timer=[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateProgress) userInfo:nil repeats:true];
    }
    return _timer;
}

/// 創(chuàng)建播放器
- (AVAudioPlayer *)audioPlayer {
    
    if (!_audioPlayer) {
        
        NSString *urlStr=[[NSBundle mainBundle]pathForResource:kMusicFile ofType:nil];
        NSURL *url=[NSURL fileURLWithPath:urlStr];
        NSError *error=nil;
        // 初始化播放器,注意這里的Url參數(shù)只能時文件路徑,不支持HTTP Url
        _audioPlayer=[[AVAudioPlayer alloc]initWithContentsOfURL:url error:&error];
        // 設置播放器屬性
        _audioPlayer.numberOfLoops = 0; // 設置為0不循環(huán)
        _audioPlayer.delegate = self;
        [_audioPlayer prepareToPlay]; // 加載音頻文件到緩存
        
        if(error) {
            
            NSLog(@"初始化播放器過程發(fā)生錯誤,錯誤信息:%@",error.localizedDescription);
            return nil;
        }
        
        // 設置后臺播放模式
        AVAudioSession *audioSession = [AVAudioSession sharedInstance];
        [audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
//        [audioSession setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:nil];
        [audioSession setActive:YES error:nil];
        // 添加通知,撥出耳機后暫停播放
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChange:) name:AVAudioSessionRouteChangeNotification object:nil];
    }
    return _audioPlayer;
}

/// 播放音頻
- (void)play {
    
    if (![self.audioPlayer isPlaying]) {
        [self.audioPlayer play];
        self.timer.fireDate=[NSDate distantPast];//恢復定時器
    }
}

/// 暫停播放
- (void)pause {
    
    if ([self.audioPlayer isPlaying]) {
        [self.audioPlayer pause];
        self.timer.fireDate=[NSDate distantFuture]; // 暫停定時器,注意不能調(diào)用invalidate方法,此方法會取消,之后無法恢復
    }
}

/// 點擊播放/暫停按鈕
/// @param sender 播放/暫停按鈕
- (IBAction)playClick:(UIButton *)sender {
    
    sender.selected = !sender.selected;
    if(sender.selected) {
        sender.selected = YES;
        [self pause];
    } else {
        sender.selected = NO;
        [self play];
    }
}

/// 更新播放進度
- (void)updateProgress {
    
    float progress= self.audioPlayer.currentTime /self.audioPlayer.duration;
    [self.playProgress setProgress:progress animated:true];
}

/// 一旦輸出改變則執(zhí)行此方法
/// @param notification 輸出改變通知對象
- (void)routeChange:(NSNotification *)notification {
    
    NSDictionary *dic = notification.userInfo;
    int changeReason = [dic[AVAudioSessionRouteChangeReasonKey] intValue];
    // 等于AVAudioSessionRouteChangeReasonOldDeviceUnavailable表示舊輸出不可用
    if (changeReason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
        
        AVAudioSessionRouteDescription *routeDescription = dic[AVAudioSessionRouteChangePreviousRouteKey];
        AVAudioSessionPortDescription *portDescription = [routeDescription.outputs firstObject];
        // 原設備為耳機則暫停
        if ([portDescription.portType isEqualToString:@"Headphones"]) {
            [self pause];
        }
    }
    
//    [dic enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
//
//        NSLog(@"key:%@ - obj:%@",key,obj);
//    }];
}

- (void)dealloc {
    
    [[NSNotificationCenter defaultCenter] removeObserver:self name:AVAudioSessionRouteChangeNotification object:nil];
}

#pragma mark - 播放器代理方法
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
    
    NSLog(@"音樂播放完成...");
    self.playOrPause.selected = YES;
    
    // 根據(jù)實際情況播放完成可以將會話關閉,其他音頻應用繼續(xù)播放
    [[AVAudioSession sharedInstance] setActive:NO error:nil];
}

@end

在上面的代碼中還實現(xiàn)了拔出耳機暫停音樂播放的功能,這也是一個比較常見的功能。在iOS7及以后的版本中可以通過通知獲得輸出改變的通知,然后拿到通知對象后根據(jù)userInfo獲得是何種改變類型,進而根據(jù)情況對音樂進行暫停操作。


運行效果如下:

Simulator Screen Shot - iPhone 11 Pro Max - 2020-02-15 at 22.11.25.png

擴展--播放音樂庫中的音樂

眾所周知音樂是iOS的重要組成播放,無論是iPod、iTouch、iPhone還是iPad都可以在iTunes購買音樂或添加本地音樂到音樂庫中同步到你的iOS設備。在MediaPlayer.frameowork中有一個MPMusicPlayerController用于播放音樂庫中的音樂。

下面先來看一下MPMusicPlayerController的常用屬性和方法:

屬性 說明
@property (nonatomic, readonly) MPMusicPlaybackState playbackState 播放器狀態(tài),枚舉類型:MPMusicPlaybackStateStopped:停止播放 MPMusicPlaybackStatePlaying:正在播放 MPMusicPlaybackStatePaused:暫停播放 MPMusicPlaybackStateInterrupted:播放中斷 MPMusicPlaybackStateSeekingForward:向前查找 MPMusicPlaybackStateSeekingBackward:向后查找
@property (nonatomic) MPMusicRepeatMode repeatMode 重復模式,枚舉類型: MPMusicRepeatModeDefault:默認模式,使用用戶的首選項(系統(tǒng)音樂程序設置) MPMusicRepeatModeNone:不重復 MPMusicRepeatModeOne:單曲循環(huán) MPMusicRepeatModeAll:在當前列表內(nèi)循環(huán)
@property (nonatomic) MPMusicShuffleMode shuffleMode 隨機播放模式,枚舉類型: MPMusicShuffleModeDefault:默認模式,使用用戶首選項(系統(tǒng)音樂程序設置) MPMusicShuffleModeOff:不隨機播放 MPMusicShuffleModeSongs:按歌曲隨機播放 MPMusicShuffleModeAlbums:按專輯隨機播放
@property (nonatomic, copy) MPMediaItem *nowPlayingItem 正在播放的音樂項
@property (nonatomic, readonly) NSUInteger indexOfNowPlayingItem 當前正在播放的音樂在播放隊列中的索引
@property(nonatomic, readonly) BOOL isPreparedToPlay 是否準好播放準備
@property(nonatomic) NSTimeInterval currentPlaybackTime 當前已播放時間,單位:秒
@property(nonatomic) float currentPlaybackRate 當前播放速度,是一個播放速度倍率,0表示暫停播放,1代表正常速度
類方法 說明
+ (MPMusicPlayerController *)applicationMusicPlayer; 獲取應用播放器,注意此類播放器無法在后臺播放
+ (MPMusicPlayerController *)systemMusicPlayer 獲取系統(tǒng)播放器,支持后臺播放
對象方法 說明
- (void)setQueueWithQuery:(MPMediaQuery *)query 使用媒體隊列設置播放源媒體隊列
- (void)setQueueWithItemCollection:(MPMediaItemCollection *)itemCollection 使用媒體項集合設置播放源媒體隊列
- (void)skipToNextItem 下一曲
- (void)skipToBeginning 從起始位置播放
- (void)skipToPreviousItem 上一曲
- (void)beginGeneratingPlaybackNotifications 開啟播放通知,注意不同于其他播放器,MPMusicPlayerController要想獲得通知必須首先開啟,默認情況無法獲得通知
- (void)endGeneratingPlaybackNotifications 關閉播放通知
- (void)prepareToPlay 做好播放準備(加載音頻到緩沖區(qū)),在使用play方法播放時如果沒有做好準備回自動調(diào)用該方法
- (void)play 開始播放
- (void)pause 暫停播放
- (void)stop 停止播放
- (void)beginSeekingForward 開始向前查找(快進)
- (void)beginSeekingBackward 開始向后查找(快退)
- (void)endSeeking 結(jié)束查找
通知 說明(注意:要想獲得MPMusicPlayerController通知必須首先調(diào)用beginGeneratingPlaybackNotifications開啟通知)
MPMusicPlayerControllerPlaybackStateDidChangeNotification 播放狀態(tài)改變
MPMusicPlayerControllerNowPlayingItemDidChangeNotification 當前播放音頻改變
MPMusicPlayerControllerVolumeDidChangeNotification 聲音大小改變
MPMediaPlaybackIsPreparedToPlayDidChangeNotification 準備好播放
  • MPMusicPlayerController有兩種播放器:applicationMusicPlayer和systemMusicPlayer,前者在應用退出后音樂播放會自動停止,后者在應用停止后不會退出播放狀態(tài)。
  • MPMusicPlayerController加載音樂不同于前面的AVAudioPlayer是通過一個文件路徑來加載,而是需要一個播放隊列。在MPMusicPlayerController中提供了兩個方法來加載播放隊列:- (void)setQueueWithQuery:(MPMediaQuery *)query和- (void)setQueueWithItemCollection:(MPMediaItemCollection *)itemCollection,正是由于它的播放音頻來源是一個隊列,因此MPMusicPlayerController支持上一曲、下一曲等操作。

那么接下來的問題就是如何獲取MPMediaQueue或者MPMediaItemCollection?MPMediaQueue對象有一系列的類方法來獲得媒體隊列:

+ (MPMediaQuery *)albumsQuery;
+ (MPMediaQuery *)artistsQuery;
+ (MPMediaQuery *)songsQuery;
+ (MPMediaQuery *)playlistsQuery;
+ (MPMediaQuery *)podcastsQuery;
+ (MPMediaQuery *)audiobooksQuery;
+ (MPMediaQuery *)compilationsQuery;
+ (MPMediaQuery *)composersQuery;
+ (MPMediaQuery *)genresQuery;

有了這些方法,就可以很容易獲到歌曲、播放列表、專輯媒體等媒體隊列了,這樣就可以通過:- (void)setQueueWithQuery:(MPMediaQuery *)query方法設置音樂來源了。又或者得到MPMediaQueue之后創(chuàng)建MPMediaItemCollection,使用- (void)setQueueWithItemCollection:(MPMediaItemCollection *)itemCollection設置音樂來源。

有時候可能希望用戶自己來選擇要播放的音樂,這時可以使用MPMediaPickerController,它是一個視圖控制器,類似于UIImagePickerController,選擇完播放來源后可以在其代理方法中獲得MPMediaItemCollection對象。

無論是通過哪種方式獲得MPMusicPlayerController的媒體源,可能都希望將每個媒體的信息顯示出來,這時候可以通過MPMediaItem對象獲得。一個MPMediaItem代表一個媒體文件,通過它可以訪問媒體標題、專輯名稱、專輯封面、音樂時長等等。無論是MPMediaQueue還是MPMediaItemCollection都有一個items屬性,它是MPMediaItem數(shù)組,通過這個屬性可以獲得MPMediaItem對象。

下面就簡單看一下MPMusicPlayerController的使用,在下面的例子中簡單演示了音樂的選擇、播放、暫停、通知、下一曲、上一曲功能,相信有了上面的概念,代碼讀起來并不復雜(示例中是直接通過MPMeidaPicker進行音樂選擇的,但是仍然提供了兩個方法getLocalMediaQuery和getLocalMediaItemCollection來演示如何直接通過MPMediaQueue獲得媒體隊列或媒體集合):

//
//  ViewController.m
//  04-擴展--播放音樂庫中的音樂
//
//  Created by Andy on 2020/2/15.
//  Copyright ? 2020 李正林. All rights reserved.
//

#import "ViewController.h"
#import <MediaPlayer/MediaPlayer.h>

@interface ViewController () <MPMediaPickerControllerDelegate>

@property (nonatomic,strong) MPMediaPickerController *mediaPicker;//媒體選擇控制器
@property (nonatomic,strong) MPMusicPlayerController *musicPlayer; //音樂播放器

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}

-(void)dealloc {
    
    [self.musicPlayer endGeneratingPlaybackNotifications];
}

/// 獲得音樂播放器
- (MPMusicPlayerController *)musicPlayer {
    
    if (!_musicPlayer) {
        _musicPlayer = [MPMusicPlayerController systemMusicPlayer];
        [_musicPlayer beginGeneratingPlaybackNotifications];//開啟通知,否則監(jiān)控不到MPMusicPlayerController的通知
        [self addNotification];//添加通知
        // 如果不使用MPMediaPickerController可以使用如下方法獲得音樂庫媒體隊列
        // [_musicPlayer setQueueWithItemCollection:[self getLocalMediaItemCollection]];
    }
    return _musicPlayer;
}

/// 創(chuàng)建媒體選擇器
- (MPMediaPickerController *)mediaPicker {
    
    if (!_mediaPicker) {
        // 初始化媒體選擇器,這里設置媒體類型為音樂,其實這里也可以選擇視頻、廣播等
//        _mediaPicker = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeMusic];
        _mediaPicker = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeAny];
        _mediaPicker.allowsPickingMultipleItems = YES; // 允許多選
//        _mediaPicker.showsCloudItems = YES; // 顯示icloud選項
        _mediaPicker.prompt = @"請選擇要播放的音樂";
        _mediaPicker.delegate = self; // 設置選擇器代理
    }
    return _mediaPicker;
}

/// 取得媒體隊列
- (MPMediaQuery *)getLocalMediaQuery {
    
    MPMediaQuery *mediaQueue = [MPMediaQuery songsQuery];
    for (MPMediaItem *item in mediaQueue.items) {
        NSLog(@"標題:%@,%@",item.title,item.albumTitle);
    }
    return mediaQueue;
}

/// 取得媒體集合
- (MPMediaItemCollection *)getLocalMediaItemCollection {
    
    MPMediaQuery *mediaQueue = [MPMediaQuery songsQuery];
    NSMutableArray *array = [NSMutableArray array];
    for (MPMediaItem *item in mediaQueue.items) {
        
        [array addObject:item];
        NSLog(@"標題:%@,%@",item.title,item.albumTitle);
    }
    MPMediaItemCollection *mediaItemCollection = [[MPMediaItemCollection alloc]initWithItems:[array copy]];
    
    return mediaItemCollection;
}

#pragma mark - MPMediaPickerController代理方法
//選擇完成
- (void)mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection {
    
    MPMediaItem *mediaItem=[mediaItemCollection.items firstObject];//第一個播放音樂
    //注意很多音樂信息如標題、專輯、表演者、封面、時長等信息都可以通過MPMediaItem的valueForKey:方法得到,但是從iOS7開始都有對應的屬性可以直接訪問
//    NSString *title = [mediaItem valueForKey:MPMediaItemPropertyAlbumTitle];
//    NSString *artist = [mediaItem valueForKey:MPMediaItemPropertyAlbumArtist];
//    MPMediaItemArtwork *artwork = [mediaItem valueForKey:MPMediaItemPropertyArtwork];
    //UIImage *image = [artwork imageWithSize:CGSizeMake(100, 100)]; // 專輯圖片
    NSLog(@"標題:%@,表演者:%@,專輯:%@",mediaItem.title ,mediaItem.artist,mediaItem.albumTitle);
    [self.musicPlayer setQueueWithItemCollection:mediaItemCollection];
    [self dismissViewControllerAnimated:YES completion:nil];
}

// 取消選擇
- (void)mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker {
    [self dismissViewControllerAnimated:YES completion:nil];
}

#pragma mark - 通知

/// 添加通知
- (void)addNotification {
    
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter addObserver:self selector:@selector(playbackStateChange:) name:MPMusicPlayerControllerPlaybackStateDidChangeNotification object:self.musicPlayer];
}

/// 播放狀態(tài)改變通知
/// @param notification 通知對象
- (void)playbackStateChange:(NSNotification *)notification {
    
    switch (self.musicPlayer.playbackState) {
            
        case MPMusicPlaybackStatePlaying:
            NSLog(@"正在播放...");
            break;
            
        case MPMusicPlaybackStatePaused:
            NSLog(@"播放暫停.");
            break;
            
        case MPMusicPlaybackStateStopped:
            NSLog(@"播放停止.");
            break;
            
        default:
            break;
    }
}

#pragma mark - UI事件
- (IBAction)selectClick:(UIButton *)sender {
    
    [self presentViewController:self.mediaPicker animated:YES completion:nil];
}

- (IBAction)playClick:(UIButton *)sender {
    
    [self.musicPlayer play];
}

- (IBAction)puaseClick:(UIButton *)sender {
    
    [self.musicPlayer pause];
}

- (IBAction)stopClick:(UIButton *)sender {
    
    [self.musicPlayer stop];
}

- (IBAction)nextClick:(UIButton *)sender {
    
    [self.musicPlayer skipToNextItem];
}

- (IBAction)prevClick:(UIButton *)sender {
    
    [self.musicPlayer skipToPreviousItem];
}


@end

運行效果如下:


Simulator Screen Shot - iPhone 11 Pro Max - 2020-02-15 at 19.16.04.png

錄音

除了上面說的,在AVFoundation框架中還要一個AVAudioRecorder類專門處理錄音操作,它同樣支持多種音頻格式。與AVAudioPlayer類似,你完全可以將它看成是一個錄音機控制類,下面是常用的屬性和方法:

屬性 說明
@property(readonly, getter=isRecording) BOOL recording; 是否正在錄音,只讀
@property(readonly) NSURL *url 錄音文件地址,只讀
@property(readonly) NSDictionary *settings 錄音文件設置,只讀
@property(readonly) NSTimeInterval currentTime 錄音時長,只讀,注意僅僅在錄音狀態(tài)可用
@property(readonly) NSTimeInterval deviceCurrentTime 輸入設置的時間長度,只讀,注意此屬性一直可訪問
@property(getter=isMeteringEnabled) BOOL meteringEnabled; 是否啟用錄音測量,如果啟用錄音測量可以獲得錄音分貝等數(shù)據(jù)信息
@property(nonatomic, copy) NSArray *channelAssignments 當前錄音的通道
對象方法 說明
- (instancetype)initWithURL:(NSURL *)url settings:(NSDictionary *)settings error:(NSError **)outError 錄音機對象初始化方法,注意其中的url必須是本地文件url,settings是錄音格式、編碼等設置
- (BOOL)prepareToRecord 準備錄音,主要用于創(chuàng)建緩沖區(qū),如果不手動調(diào)用,在調(diào)用record錄音時也會自動調(diào)用
- (BOOL)record 開始錄音
- (BOOL)recordAtTime:(NSTimeInterval)time 在指定的時間開始錄音,一般用于錄音暫停再恢復錄音
- (BOOL)recordForDuration:(NSTimeInterval) duration 按指定的時長開始錄音
- (BOOL)recordAtTime:(NSTimeInterval)time forDuration:(NSTimeInterval) duration 在指定的時間開始錄音,并指定錄音時長
- (void)pause; 暫停錄音
- (void)stop; 停止錄音
- (BOOL)deleteRecording; 刪除錄音,注意要刪除錄音此時錄音機必須處于停止狀態(tài)
- (void)updateMeters; 更新測量數(shù)據(jù),注意只有meteringEnabled為YES此方法才可用
- (float)peakPowerForChannel:(NSUInteger)channelNumber; 指定通道的測量峰值,注意只有調(diào)用完updateMeters才有值
- (float)averagePowerForChannel:(NSUInteger)channelNumber 指定通道的測量平均值,注意只有調(diào)用完updateMeters才有值
代理方法 說明
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag 完成錄音
- (void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder *)recorder error:(NSError *)error 錄音編碼發(fā)生錯誤

AVAudioRecorder很多屬性和方法跟AVAudioPlayer都是類似的,但是它的創(chuàng)建有所不同,在創(chuàng)建錄音機時除了指定路徑外還必須指定錄音設置信息,因為錄音機必須知道錄音文件的格式、采樣率、通道數(shù)、每個采樣點的位數(shù)等信息,但是也并不是所有的信息都必須設置,通常只需要幾個常用設置。關于錄音設置詳見幫助文檔中的“AV Foundation Audio Settings Constants”。

下面就使用AVAudioRecorder創(chuàng)建一個錄音機,實現(xiàn)了錄音、暫停、停止、播放等功能,實現(xiàn)效果大致如下:


Simulator Screen Shot - iPhone 11 Pro Max - 2020-02-15 at 21.51.07.png

在這個示例中將實行一個完整的錄音控制,包括錄音、暫停、恢復、停止,同時還會實時展示用戶錄音的聲音波動,當用戶點擊完停止按鈕還會自動播放錄音文件。程序的構(gòu)建主要分為以下幾步:

  1. 設置音頻會話類型為AVAudioSessionCategoryPlayAndRecord,因為程序中牽扯到錄音和播放操作。
  2. 創(chuàng)建錄音機AVAudioRecorder,指定錄音保存的路徑并且設置錄音屬性,注意對于一般的錄音文件要求的采樣率、位數(shù)并不高,需要適當設置以保證錄音文件的大小和效果。
  3. 設置錄音機代理以便在錄音完成后播放錄音,打開錄音測量保證能夠?qū)崟r獲得錄音時的聲音強度。(注意聲音強度范圍-160到0,0代表最大輸入)
  4. 創(chuàng)建音頻播放器AVAudioPlayer,用于在錄音完成之后播放錄音。
  5. 創(chuàng)建一個定時器以便實時刷新錄音測量值并更新錄音強度到UIProgressView中顯示。
  6. 添加錄音、暫停、恢復、停止操作,需要注意錄音的恢復操作其實是有音頻會話管理的,恢復時只要再次調(diào)用record方法即可,無需手動管理恢復時間等。

下面是主要代碼:

//
//  ViewController.m
//  05-錄音
//
//  Created by Andy on 2020/2/15.
//  Copyright ? 2020 李正林. All rights reserved.
//

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>

#define kRecordAudioFile @"myRecord.caf"

@interface ViewController () <AVAudioRecorderDelegate>

@property (nonatomic,strong) AVAudioRecorder *audioRecorder; // 音頻錄音機
@property (nonatomic,strong) AVAudioPlayer *audioPlayer; // 音頻播放器,用于播放錄音文件
@property (nonatomic,strong) NSTimer *timer; // 錄音聲波監(jiān)控(注意這里暫時不對播放進行監(jiān)控)

@property (weak, nonatomic) IBOutlet UIButton *record; // 開始錄音
@property (weak, nonatomic) IBOutlet UIButton *pause; // 暫停錄音
@property (weak, nonatomic) IBOutlet UIButton *resume; // 恢復錄音
@property (weak, nonatomic) IBOutlet UIButton *stop; // 停止錄音
@property (weak, nonatomic) IBOutlet UIProgressView *audioPower; // 音頻波動

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    [self setAudioSession];
}

#pragma mark - 私有方法

/// 設置音頻會話
- (void)setAudioSession {
    
    AVAudioSession *audioSession=[AVAudioSession sharedInstance];
    // 設置為播放和錄音狀態(tài),以便可以在錄制完之后播放錄音
    [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
    [audioSession setActive:YES error:nil];
}

/// 取得錄音文件保存路徑
- (NSURL *)getSavePath {
    
    NSString *urlStr = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    urlStr = [urlStr stringByAppendingPathComponent:kRecordAudioFile];
    NSLog(@"file path:%@",urlStr);
    NSURL *url=[NSURL fileURLWithPath:urlStr];
    return url;
}

/// 取得錄音文件設置
- (NSDictionary *)getAudioSetting {
    
    NSMutableDictionary *dicM=[NSMutableDictionary dictionary];
    // 設置錄音格式
    [dicM setObject:@(kAudioFormatLinearPCM) forKey:AVFormatIDKey];
    // 設置錄音采樣率,8000是電話采樣率,對于一般錄音已經(jīng)夠了
    [dicM setObject:@(8000) forKey:AVSampleRateKey];
    // 設置通道,這里采用單聲道
    [dicM setObject:@(1) forKey:AVNumberOfChannelsKey];
    // 每個采樣點位數(shù),分為8、16、24、32
    [dicM setObject:@(8) forKey:AVLinearPCMBitDepthKey];
    // 是否使用浮點數(shù)采樣
    [dicM setObject:@(YES) forKey:AVLinearPCMIsFloatKey];
    // ....其他設置等
    return dicM;
}

/// 獲得錄音機對象
- (AVAudioRecorder *)audioRecorder {
    
    if (!_audioRecorder) {
        // 創(chuàng)建錄音文件保存路徑
        NSURL *url = [self getSavePath];
        // 創(chuàng)建錄音格式設置
        NSDictionary *setting = [self getAudioSetting];
        // 創(chuàng)建錄音機
        NSError *error = nil;
        _audioRecorder = [[AVAudioRecorder alloc] initWithURL:url settings:setting error:&error];
        _audioRecorder.delegate = self;
        _audioRecorder.meteringEnabled = YES; // 如果要監(jiān)控聲波則必須設置為YES
        if (error) {
            NSLog(@"創(chuàng)建錄音機對象時發(fā)生錯誤,錯誤信息:%@",error.localizedDescription);
            return nil;
        }
    }
    return _audioRecorder;
}

/// 創(chuàng)建播放器
- (AVAudioPlayer *)audioPlayer {
    
    if (!_audioPlayer) {
        NSURL *url = [self getSavePath];
        NSError *error = nil;
        _audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
        _audioPlayer.numberOfLoops = 0;
        [_audioPlayer prepareToPlay];
        if (error) {
            NSLog(@"創(chuàng)建播放器過程中發(fā)生錯誤,錯誤信息:%@",error.localizedDescription);
            return nil;
        }
    }
    return _audioPlayer;
}

/// 錄音聲波監(jiān)控定時器
- (NSTimer *)timer {
    
    if (!_timer) {
        _timer = [NSTimer scheduledTimerWithTimeInterval:0.1f target:self selector:@selector(audioPowerChange) userInfo:nil repeats:YES];
    }
    return _timer;
}

/// 錄音聲波狀態(tài)設置
- (void)audioPowerChange {
    
    [self.audioRecorder updateMeters]; // 更新測量值
    float power = [self.audioRecorder averagePowerForChannel:0]; // 取得第一個通道的音頻,注意音頻強度范圍時-160到0
    CGFloat progress = (1.0/160.0) * (power+160.0);
    [self.audioPower setProgress:progress];
}

#pragma mark - UI事件

/// 點擊錄音按鈕
/// @param sender 錄音按鈕
- (IBAction)recordClick:(UIButton *)sender {
    
    if (![self.audioRecorder isRecording]) {
        [self.audioRecorder record]; // 首次使用應用時如果調(diào)用record方法會詢問用戶是否允許使用麥克風
        self.timer.fireDate = [NSDate distantPast];
    }
}

/// 點擊暫定按鈕
/// @param sender  暫停按鈕
- (IBAction)pauseClick:(UIButton *)sender {
    
    if ([self.audioRecorder isRecording]) {
        [self.audioRecorder pause];
        self.timer.fireDate = [NSDate distantFuture];
    }
}

/// 點擊恢復按鈕  - 恢復錄音只需要再次調(diào)用record,AVAudioSession會幫助你記錄上次錄音位置并追加錄音
/// @param sender 恢復按鈕
- (IBAction)resumeClick:(UIButton *)sender {
    
    [self recordClick:sender];
}

/// 點擊停止按鈕
/// @param sender 停止按鈕
- (IBAction)stopClick:(UIButton *)sender {
    
    [self.audioRecorder stop];
    self.timer.fireDate = [NSDate distantFuture];
    self.audioPower.progress = 0.0;
}

#pragma mark - 錄音機代理方法

/// 錄音完成,錄音完成后播放錄音
/// @param recorder 錄音機對象
/// @param flag 是否成功
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag {
    if (![self.audioPlayer isPlaying]) {
        [self.audioPlayer play];
    }
    NSLog(@"錄音完成!");
}

@end

音頻隊列服務

大家應該已經(jīng)注意到了,無論是前面的錄音還是音頻播放均不支持網(wǎng)絡流媒體播放,當然對于錄音來說這種需求可能不大,但是對于音頻播放來說有時候就很有必要了。AVAudioPlayer只能播放本地文件,并且是一次性加載所以音頻數(shù)據(jù),初始化AVAudioPlayer時指定的URL也只能是File URL而不能是HTTP URL。當然,將音頻文件下載到本地然后再調(diào)用AVAudioPlayer來播放也是一種播放網(wǎng)絡音頻的辦法,但是這種方式最大的弊端就是必須等到整個音頻播放完成才能播放,而不能使用流式播放,這往往在實際開發(fā)中是不切實際的。那么在iOS中如何播放網(wǎng)絡流媒體呢?就是使用AudioToolbox框架中的音頻隊列服務Audio Queue Services。

使用音頻隊列服務完全可以做到音頻播放和錄制,首先看一下錄音音頻服務隊列:

一個音頻服務隊列Audio Queue有三部分組成:

三個緩沖器Buffers:每個緩沖器都是一個存儲音頻數(shù)據(jù)的臨時倉庫。

一個緩沖隊列Buffer Queue:一個包含音頻緩沖器的有序隊列。

一個回調(diào)Callback:一個自定義的隊列回調(diào)函數(shù)。

聲音通過輸入設備進入緩沖隊列中,首先填充第一個緩沖器;當?shù)谝粋€緩沖器填充滿之后自動填充下一個緩沖器,同時會調(diào)用回調(diào)函數(shù);在回調(diào)函數(shù)中需要將緩沖器中的音頻數(shù)據(jù)寫入磁盤,同時將緩沖器放回到緩沖隊列中以便重用。下面是Apple官方關于音頻隊列服務的流程示意圖:

類似的,看一下音頻播放緩沖隊列,其組成部分和錄音緩沖隊列類似。

但是在音頻播放緩沖隊列中,回調(diào)函數(shù)調(diào)用的時機不同于音頻錄制緩沖隊列,流程剛好相反。將音頻讀取到緩沖器中,一旦一個緩沖器填充滿之后就放到緩沖隊列中,然后繼續(xù)填充其他緩沖器;當開始播放時,則從第一個緩沖器中讀取音頻進行播放;一旦播放完之后就會觸發(fā)回調(diào)函數(shù),開始播放下一個緩沖器中的音頻,同時填充第一個緩沖器放;填充滿之后再次放回到緩沖隊列。下面是詳細的流程:

當然,要明白音頻隊列服務的原理并不難,問題是如何實現(xiàn)這個自定義的回調(diào)函數(shù),這其中我們有大量的工作要做,控制播放狀態(tài)、處理異常中斷、進行音頻編碼等等。由于牽扯內(nèi)容過多,而且不是本文目的,如果以后有時間將另開一篇文章重點介紹,目前有很多第三方優(yōu)秀框架可以直接使用,例如AudioStreamer、FreeStreamer。由于前者當前只有非ARC版本,所以下面不妨使用FreeStreamer來簡單演示在線音頻播放的過程,當然在使用之前要做如下準備工作:

  1. 拷貝FreeStreamer中的Reachability.h、Reachability.m和Common、astreamer兩個文件夾中的內(nèi)容到項目中。
  2. 添加FreeStreamer使用的類庫:CFNetwork.framework、AudioToolbox.framework、AVFoundation.framework
    、libxml2.dylib、MediaPlayer.framework。
  3. 如果引用libxml2.dylib編譯不通過,需要在Xcode的Targets-Build Settings-Header Build Path中添加$(SDKROOT)/usr/include/libxml2。
  4. 將FreeStreamer中的FreeStreamerMobile-Prefix.pch文件添加到項目中并將Targets-Build Settings-Precompile Prefix Header設置為YES,在Targets-Build Settings-Prefix Header設置為$(SRCROOT)/項目名稱/FreeStreamerMobile-Prefix.pch(因為Xcode6默認沒有pch文件)

然后就可以編寫代碼播放網(wǎng)絡音頻了:

//
//  FreeStreamerManager.h
//  06-音頻隊列服務
//
//  Created by Andy on 2020/2/16.
//  Copyright ? 2020 李正林. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <FSAudioStream.h>

@protocol FreeStreamerPlayFinishedManagerDelegate <NSObject>

- (void)freeStreamerManagerPlayFinished;

@end

NS_ASSUME_NONNULL_BEGIN

@interface FreeStreamerManager : NSObject

@property (nonatomic, weak) id<FreeStreamerPlayFinishedManagerDelegate> delegate;

@property (nonatomic, strong) FSAudioStream * audioStream;

/**
 播放,可以播放網(wǎng)絡音頻資源或本地fileurl音頻資源

 @param url 網(wǎng)絡或本地URL資源
 */
- (void)freeStreamerManagerPlayWithUrl:(NSURL *)url;

/**
 暫停
 */
- (void)freeStreamerManagerPause;

/**
 恢復播放
 */
- (void)freeStreamerManagerResume;

/**
 停止
 */
- (void)freeStreamerManagerStop;

/**
 上一首

 @param prevUrl URL音頻文件資源
 */
- (void)freeStreamerManagerPrevWithUrl:(NSURL *)prevUrl;

/**
 下一首

 @param nextUrl URL音頻文件資源
 */
- (void)freeStreamerManagerNextWithUrl:(NSURL *)nextUrl;

/// 上一首 或下一首
/// @param prevOrNextUrl URL音頻文件資源
- (void)freeStreamerManagerPrevOrNextWithUrl:(NSURL *)prevOrNextUrl;

@end

NS_ASSUME_NONNULL_END

//
//  FreeStreamerManager.m
//  06-音頻隊列服務
//
//  Created by Andy on 2020/2/16.
//  Copyright ? 2020 李正林. All rights reserved.
//

#import "FreeStreamerManager.h"

@interface FreeStreamerManager ()

@property (nonatomic, strong) NSTimer * audioTimer;

@end

@implementation FreeStreamerManager

- (void)freeStreamerManagerPlayWithUrl:(NSURL *)url {
    
    if (!_audioStream) {
        _audioStream = [[FSAudioStream alloc] init];
        // 播放失敗的回調(diào)
        _audioStream.onFailure = ^(FSAudioStreamError error, NSString *errorDescription) {
            NSLog(@"播放過程中發(fā)生錯誤,錯誤信息:%@",errorDescription);
        };
        // 播放完成的回調(diào)
        __weak typeof(self) weakSelf = self;
        _audioStream.onCompletion=^(){
            NSLog(@"播放完成!");
            if ([weakSelf.delegate respondsToSelector:@selector(freeStreamerManagerPlayFinished)]) {
                
                [weakSelf.delegate freeStreamerManagerPlayFinished];
            }
        };
        // 設置音量
        [_audioStream setVolume:0.5];
        // 使用音頻鏈接URL播放音頻
        [_audioStream playFromURL:url];
    }
    // 不進行檢測格式 <開啟檢測之后,有些網(wǎng)絡音頻鏈接無法播放>
    _audioStream.strictContentTypeChecking = NO;
    _audioStream.defaultContentType = @"audio/mpeg";
    
    if (!self.audioStream.isPlaying) {
        [self.audioStream play];
    }
}

- (void)freeStreamerManagerPause {
    
    if (self.audioStream.isPlaying) {
        [self.audioStream pause];
    }
}

- (void)freeStreamerManagerResume {
    
    if (!self.audioStream.isPlaying) {
        [self.audioStream pause];
    }
}

- (void)freeStreamerManagerStop {
    
    [self.audioStream stop];
}

- (void)freeStreamerManagerPrevWithUrl:(NSURL *)prevUrl {
    
    [self.audioStream playFromURL:prevUrl];
}

- (void)freeStreamerManagerNextWithUrl:(NSURL *)nextUrl {
    
    [self.audioStream playFromURL:nextUrl];
}

- (void)freeStreamerManagerPrevOrNextWithUrl:(NSURL *)prevOrNextUrl {
    
    [self.audioStream playFromURL:prevOrNextUrl];
}

@end


//
//  ViewController.m
//  06-音頻隊列服務 - AudioQueueServices
//
//  Created by Andy on 2020/2/15.
//  Copyright ? 2020 李正林. All rights reserved.
// 使用FreeStreamer實現(xiàn)網(wǎng)絡音頻播放


#import "ViewController.h"
#import "FreeStreamerManager.h"

@interface ViewController () <FreeStreamerPlayFinishedManagerDelegate>

/// 播放序號
@property (weak, nonatomic) IBOutlet UILabel *musicIndexLabel;

/// 背景圖
@property (weak, nonatomic) IBOutlet UIImageView *backgroundIconImgView;

/// FreeStreamerManager
@property (nonatomic, strong) FreeStreamerManager * streamerManager;

/// 進度
@property (weak, nonatomic) IBOutlet UISlider *progress;

/// 音量
@property (weak, nonatomic) IBOutlet UISlider *valumSlider;

/// 當前播放時間
@property (weak, nonatomic) IBOutlet UILabel *currentTimelabel;

/// 總時長
@property (weak, nonatomic) IBOutlet UILabel *totalTimeLabel;

@property (nonatomic, assign) NSInteger totalTime;

@property (nonatomic, strong) NSTimer * audioTimer;

/// 當前播放序號
@property (nonatomic, assign) NSInteger currentIndex;

/// 總列表序號
@property (nonatomic, assign) NSInteger totalIndex;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    [self setData];
}

- (void)setData {
    
    self.currentIndex = 1;
    self.totalIndex = 51;
    
    self.musicIndexLabel.text = [NSString stringWithFormat:@"%ld / %ld", self.currentIndex, self.totalIndex];
}

- (NSURL *)getFileUrl {
    
    NSString *urlString = [[NSBundle mainBundle] pathForResource:@"KGETOSKBDLQMOD01.mp3" ofType:nil];
    NSURL *url = [NSURL fileURLWithPath:urlString];
    return url;
}

- (NSURL *)getNetworkUrl {
    
    NSString *urlString = @"http://up.mcyt.net/down/45957.mp3";
    NSURL *url = [NSURL URLWithString:urlString];
    return url;
}

- (FreeStreamerManager *)streamerManager{
    if (!_streamerManager) {
        _streamerManager = [[FreeStreamerManager alloc] init];
        _streamerManager.delegate = self;
    }
    return _streamerManager;
}


- (IBAction)play:(id)sender {
    
    if (!_streamerManager) {
        _streamerManager = [[FreeStreamerManager alloc] init];
    }
    [_streamerManager freeStreamerManagerPlayWithUrl:[self getMusicUrlWithCurrentIndex:self.currentIndex]];
    [self addTimer];
}

- (IBAction)pause:(id)sender {
    
    [_streamerManager freeStreamerManagerPause];
}

- (IBAction)resume:(id)sender {
    
    [_streamerManager freeStreamerManagerResume];
}

- (IBAction)stop:(id)sender {
    
    [_streamerManager freeStreamerManagerStop];
    [self removeTimer];
}

// 上一曲
- (IBAction)prev:(id)sender {
    
    [self.streamerManager freeStreamerManagerStop];
    self.currentIndex--;
    
}

// 下一曲
- (IBAction)next:(id)sender {
    
    [self.streamerManager freeStreamerManagerStop];
    self.currentIndex++;
    
}

- (void)setCurrentIndex:(NSInteger)currentIndex {
    
    _currentIndex = currentIndex;
    
    if (_currentIndex == 0) {
        _currentIndex = self.totalIndex;
    } else if (_currentIndex == self.totalIndex + 1) {
        _currentIndex = 1;
    }
    
    self.musicIndexLabel.text = [NSString stringWithFormat:@"%ld / %ld", self.currentIndex, self.totalIndex];
    
    [self.streamerManager freeStreamerManagerPrevOrNextWithUrl:[self getMusicUrlWithCurrentIndex:_currentIndex]];
    
    self.backgroundIconImgView.image = [UIImage imageNamed:[self getMusicBackgroundImageViewFileNameWithCurrentIndex:_currentIndex]];
}

- (NSURL *)getMusicUrlWithCurrentIndex:(NSInteger)currentIndex {
    
    NSString *musicName = [NSString stringWithFormat:@"%@%02ld.mp3", @"KGETOSKBDLQMOD", currentIndex];
    
    NSString *urlString = [[NSBundle mainBundle] pathForResource:musicName ofType:nil];
    NSURL *url = [NSURL fileURLWithPath:urlString];
    return url;
}

- (NSString *)getMusicBackgroundImageViewFileNameWithCurrentIndex:(NSInteger)currentIndex {
    
    NSString *backgroundImageViewName = [NSString stringWithFormat:@"0C55353F-%ld", currentIndex];
    return backgroundImageViewName;
}

// 快進 / 快退
// 使用[self.audioStream seekToPosition:position]進行播放進度的切換
//根據(jù)不同的狀態(tài)給UISlider添加不同的addTarget方法:

- (IBAction)changeValueForProgress:(UISlider *)sender {
    
    [self.progress addTarget:self action:@selector(progressChangeAction:) forControlEvents:(UIControlEventValueChanged)];
//    [self.progress addTarget:self action:@selector(progressTouchBeginAction:) forControlEvents:(UIControlEventTouchDown)];
//    [self.progress addTarget:self action:@selector(progressTouchEndAction:) forControlEvents:(UIControlEventTouchUpInside)];
}

- (IBAction)changeValueForProgressTouchDown:(UISlider *)sender {
    
    [self.progress addTarget:self action:@selector(progressTouchBeginAction:) forControlEvents:(UIControlEventTouchDown)];
}

- (IBAction)changeValueForProgressTouchUpInside:(UISlider *)sender {
    
    [self.progress addTarget:self action:@selector(progressTouchEndAction:) forControlEvents:(UIControlEventTouchUpInside)];
}

- (void)addTimer {
    
    self.audioTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(playProgressAction) userInfo:nil repeats:YES];
    [self.audioTimer fire];
    NSLog(@"addTimer");
}

- (void)removeTimer {
    
    [self.audioTimer invalidate];
    self.audioTimer = nil;
    NSLog(@"removeTimer");
}

// 進度正在改變
- (void)progressChangeAction:(UISlider *)slider {
    float value = slider.value;
    // 進度 * 總時間 獲取當前時間
    float current = value * _totalTime;
    // 當前分鐘數(shù)
    double minutesElapsed =floor(fmod(current/60.0,60.0));
    // 當前秒數(shù)
    double secondsElapsed =floor(fmod(current,60.0));
    // 格式化當前時間
    NSString *currentTime = [NSString stringWithFormat:@"%02.0f:%02.0f", minutesElapsed, secondsElapsed];
    // 改變顯示當前時間的標簽文字
    self.currentTimelabel.text = currentTime;
}
// 開始改變進度
- (void)progressTouchBeginAction:(UISlider *)sender {
    NSLog(@"開始觸摸");
    [self removeTimer];
    // 暫停
    [self pause:nil];
}
// 結(jié)束改變進度
- (void)progressTouchEndAction:(UISlider *)sender {
    NSLog(@"結(jié)束觸摸");
    [self addTimer];
    // 播放
    [self play:nil];
    // 獲取進度 0 ~ 1
    float value = sender.value == 0 ? 0.001 : sender.value;
    // 創(chuàng)建播放進度對象
    FSStreamPosition position;
    // 賦值
    position.position = value;
    // 跳轉(zhuǎn)進度
    [self.streamerManager.audioStream seekToPosition:position];
}

- (void)playProgressAction {
    
    FSStreamPosition cur = self.streamerManager.audioStream.currentTimePlayed;
    float playbackTime = cur.playbackTimeInSeconds/1;
    double minutesElapsed = floor(fmod(playbackTime/60.0,60.0));
    double secondsElapsed = floor(fmod(playbackTime,60.0));
    NSString *currentTime = [NSString stringWithFormat:@"%02.0f:%02.0f", minutesElapsed, secondsElapsed];
    NSLog(@"當前播放時間:%f", playbackTime);//播放進度
    NSLog(@"格式化當前播放時間:%@", currentTime);
    // 獲取視頻的總時長
    float totalTime = playbackTime / cur.position;
    // 記錄音頻總時間
    _totalTime = totalTime;
    NSLog(@"總時間:%f", totalTime);
    if ([[NSString stringWithFormat:@"%f",totalTime] isEqualToString:@"nan"]) {
        NSLog(@"格式化總時間:00:00");
    }else{
        double minutesElapsed1 =floor(fmod(totalTime/60.0,60.0));
        double secondsElapsed1 =floor(fmod(totalTime,60.0));
        NSString *total = [NSString stringWithFormat:@"/ %02.0f:%02.0f",minutesElapsed1, secondsElapsed1];
        NSLog(@"格式化總時間:%@", total);
        // 改變當前播放時間和音頻總時間的顯示
        self.currentTimelabel.text = currentTime;
        self.totalTimeLabel.text = total;
    }
    float  prebuffer = (float)self.streamerManager.audioStream.prebufferedByteCount;
    float contentlength = (float)self.streamerManager.audioStream.contentLength;
    if (contentlength>0) {
        NSLog(@"緩存進度:%f", prebuffer / contentlength);
        // 改變播放進度
        self.progress.value = cur.position;
    }
}

- (IBAction)valumChangeAction:(UISlider *)sender {
    
    [self.valumSlider addTarget:self action:@selector(valumSliderChangeAction:) forControlEvents:(UIControlEventValueChanged)];
}

- (void)valumSliderChangeAction:(UISlider *)slider {
    
    self.streamerManager.audioStream.volume = slider.value;
}




#pragma mark- 接收遠程控制信息

// 配置第一響應者
// 讓播放控制類成為第一響應者,后臺的控制在該類中響應:
- (void)viewWillAppear:(BOOL)animated {
    
    [super viewWillAppear:animated];
    
    //以及設置app支持接受遠程控制事件代碼。設置app支持接受遠程控制事件,
    
    //其實就是在dock中可以顯示應用程序圖標,同時點擊該圖片時,打開app。
    
    //或者鎖屏時,雙擊home鍵,屏幕上方出現(xiàn)應用程序播放控制按鈕。
    
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
    
    [self becomeFirstResponder]; //成為FristResponder
}

- (void)viewWillDisappear:(BOOL)animated {
    
    [super viewWillDisappear:animated];
    
    [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
    
    [self resignFirstResponder];
}

// 接收遠程控制信息
// 實現(xiàn)遠程控制接收事件,進行區(qū)分事件的類別,響應不同的操作:
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
    if (event.type == UIEventTypeRemoteControl) {
        switch (event.subtype) {
                // 播放
            case UIEventSubtypeRemoteControlPlay:
            {
                [self play:nil];
            }
                break;
                // 暫停
            case UIEventSubtypeRemoteControlPause:
            {
                [self pause:nil];
            }
                break;
                // 停止播放
            case UIEventSubtypeRemoteControlStop:
            {
                [self.streamerManager.audioStream stop];
            }
                break;
                // 播放下一曲按鈕
            case UIEventSubtypeRemoteControlNextTrack:
            {
                [self next:nil];
            }
                break;
                // 播放上一曲按鈕
            case UIEventSubtypeRemoteControlPreviousTrack:
            {
                [self prev:nil];
            }
                break;
            case UIEventSubtypeRemoteControlTogglePlayPause:
            {
                if (self.streamerManager.audioStream.isPlaying) {
                    [self pause:nil];
                } else {
                    [self play:nil];
                }
            }
                break;
            default:
                break;
        }
    }
}

#pragma mark-  FreeStreamerPlayFinishedManagerDelegate
- (void)freeStreamerManagerPlayFinished {
    
    NSLog(@"---FreeStreamerPlayFinishedManagerDelegate---play finished-----");
    self.currentIndex++;
}

/**
// 修改鎖屏界面音頻信息
// 當前音頻開始播放及時修改信息。
// 改變鎖屏歌曲信息
- (void)setLockScreenNowPlayingInfo {
    
    //更新鎖屏時的歌曲信息
    if (NSClassFromString(@"MPNowPlayingInfoCenter")) {
        
        NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
        // 歌曲名
        [dict setObject:@"體面" forKey:MPMediaItemPropertyTitle];
        // 演唱者
        [dict setObject:@"于文文" forKey:MPMediaItemPropertyArtist];
        // 專輯名
        [dict setObject:@"專輯《體面》" forKey:MPMediaItemPropertyAlbumTitle];
        
        //專輯縮略圖
        UIImage *newImage = [UIImage imageNamed:@"音樂"];
        [dict setObject:[[MPMediaItemArtwork alloc] initWithImage:newImage] forKey:MPMediaItemPropertyArtwork];
        
        //設置鎖屏狀態(tài)下屏幕顯示播放音樂信息
        [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
        
    }
    
}
 */


// 修改鎖屏界面音頻的播放進度
/**
 定時器修改進度
 
 @param duration 總時間
 @param current 當前時間
 */
//- (void)changeLockProgress:(NSInteger)duration current:(NSInteger)current {
//    if(self.audioStream.isPlaying) {
//
//        //當前播放時間
//        NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:[[MPNowPlayingInfoCenter defaultCenter] nowPlayingInfo]];
//        // 歌曲總時長
//        [dict setObject:@(duration) forKey:MPMediaItemPropertyPlaybackDuration];
//        // 當前播放時間
//        [dict setObject:@(current) forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
//        [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
//
//    }
//}


@end


運行效果如下:


Simulator Screen Shot - iPhone 11 Pro Max - 2020-02-16 at 19.17.04.png

其實FreeStreamer的功能很強大,不僅僅是播放本地、網(wǎng)絡音頻那么簡單,它還支持播放列表、檢查包內(nèi)容、RSS訂閱、播放中斷等很多強大的功能,甚至還包含了一個音頻分析器,有興趣的朋友可以訪問官網(wǎng)查看詳細用法。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容