iOS 音視頻學習筆記

音頻會話

//
//  ViewController.m
//  KCAVAudioPlayer
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//  AVAudioSession 音頻會話

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
#define kMusicFile @"劉若英 - 原來你也在這里.mp3"
#define kMusicSinger @"劉若英"
#define kMusicTitle @"原來你也在這里"

@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認為是暫停狀態,1是播放狀態)

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

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self setupUI];
    
}

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

/**
 *  初始化UI
 */
-(void)setupUI{
    self.title=kMusicTitle;
    self.musicSinger.text=kMusicSinger;
}

-(NSTimer *)timer{
    if (!_timer) {
    
    // chang 這里更新頻率為0.5s而不是1s
        _timer=[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateProgress) userInfo:nil repeats:true];
    }
    return _timer;
}

/**
 *  創建播放器
 *
 *  @return 音頻播放器
 */
-(AVAudioPlayer *)audioPlayer{
    if (!_audioPlayer) {
        NSString *urlStr=[[NSBundle mainBundle]pathForResource:kMusicFile ofType:nil];
        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;
        
        // 注:重要 chang
        [_audioPlayer prepareToPlay];//加載音頻文件到緩存
        if(error){
            NSLog(@"初始化播放器過程發生錯誤,錯誤信息:%@",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];// 暫停定時器,注意不能調用invalidate方法,此方法會取消,之后無法恢復
        
    }
}

/**
 *  點擊播放/暫停按鈕
 *
 *  @param sender 播放/暫停按鈕
 */
- (IBAction)playClick:(UIButton *)sender {
    if(sender.tag){
        sender.tag=0;
        [sender setImage:[UIImage imageNamed:@"playing_btn_play_n"] forState:UIControlStateNormal];
        [sender setImage:[UIImage imageNamed:@"playing_btn_play_h"] forState:UIControlStateHighlighted];
        [self pause];
    }else{
        sender.tag=1;
        [sender setImage:[UIImage imageNamed:@"playing_btn_pause_n"] forState:UIControlStateNormal];
        [sender setImage:[UIImage imageNamed:@"playing_btn_pause_h"] forState:UIControlStateHighlighted];
        [self play];
    }
}

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

/**
 *  一旦輸出改變則執行此方法
 *
 *  @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 key, id obj, BOOL *stop) {
//        NSLog(@"%@:%@",key,obj);
//    }];
}

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

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

@end

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

AVAudioRecorder 錄音

錄音機必須知道錄音文件的格式、采樣率、通道數、每個采樣點的位數等信息,但是也并不是所有的信息都必須設置,通常只需要幾個常用設置。關于錄音設置詳見幫助文檔中的“AV Foundation Audio Settings Constants”。

//
//  ViewController.m
//  AVAudioRecorder
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. 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;//錄音聲波監控(注意這里暫時不對播放進行監控)

@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

#pragma mark - 控制器視圖方法
- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self setAudioSession];
}

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

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

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

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

/**
 *  創建播放器
 *
 *  @return 播放器
 */
-(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(@"創建播放器過程中發生錯誤,錯誤信息:%@",error.localizedDescription);
            return nil;
        }
    }
    return _audioPlayer;
}

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

/**
 *  錄音聲波狀態設置
 */
-(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];//首次使用應用時如果調用record方法會詢問用戶是否允許使用麥克風
        self.timer.fireDate=[NSDate distantPast];
    }
}

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

/**
 *  點擊恢復按鈕
 *  恢復錄音只需要再次調用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

音頻隊列服務

AVAudioPlayer只能播放本地文件,并且是一次性加載所以音頻數據,初始化AVAudioPlayer時指定的URL也只能是File URL而不能是HTTP URL。當然,將音頻文件下載到本地然后再調用AVAudioPlayer來播放也是一種播放網絡音頻的辦法,但是這種方式最大的弊端就是必須等到整個音頻播放完成才能播放,而不能使用流式播放,這往往在實際開發中是不切實際的。那么在iOS中如何播放網絡流媒體呢?就是使用AudioToolbox框架中的音頻隊列服務Audio Queue Services。

播放網絡音頻方法:

//
//  ViewController.m
//  AudioQueueServices
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//  使用FreeStreamer實現網絡音頻播放

#import "ViewController.h"
#import "FSAudioStream.h"

@interface ViewController ()

@property (nonatomic,strong) FSAudioStream *audioStream;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self.audioStream play];
}

/**
 *  取得本地文件路徑
 *
 *  @return 文件路徑
 */
-(NSURL *)getFileUrl{
    NSString *urlStr=[[NSBundle mainBundle]pathForResource:@"劉若英 - 原來你也在這里.mp3" ofType:nil];
    NSURL *url=[NSURL fileURLWithPath:urlStr];
    return url;
}
-(NSURL *)getNetworkUrl{
    NSString *urlStr=@"http://192.168.1.102/liu.mp3";
    NSURL *url=[NSURL URLWithString:urlStr];
    return url;
}

/**
 *  創建FSAudioStream對象
 *
 *  @return FSAudioStream對象
 */
-(FSAudioStream *)audioStream{
    if (!_audioStream) {
        NSURL *url=[self getNetworkUrl];
        //創建FSAudioStream對象
        _audioStream=[[FSAudioStream alloc]initWithUrl:url];
        _audioStream.onFailure=^(FSAudioStreamError error,NSString *description){
            NSLog(@"播放過程中發生錯誤,錯誤信息:%@",description);
        };
        _audioStream.onCompletion=^(){
            NSLog(@"播放完成!");
        };
        [_audioStream setVolume:0.5];//設置聲音
    }
    return _audioStream;
}

@end

擴展--使用AVFoundation生成縮略圖

使用MPMoviePlayerController來生成縮略圖足夠簡單,但是如果僅僅是是為了生成縮略圖而不進行視頻播放的話,此刻使用MPMoviePlayerController就有點大材小用了。其實使用AVFundation框架中的AVAssetImageGenerator就可以獲取視頻縮略圖。使用AVAssetImageGenerator獲取縮略圖大致分為三個步驟:

創建AVURLAsset對象(此類主要用于獲取媒體信息,包括視頻、聲音等)。
根據AVURLAsset創建AVAssetImageGenerator對象。
使用AVAssetImageGenerator的copyCGImageAtTime::方法獲得指定時間點的截圖。

//
//  ViewController.m
//  AVAssetImageGenerator
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

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

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //獲取第13.0s的縮略圖
    [self thumbnailImageRequest:13.0];
}

#pragma mark - 私有方法
/**
 *  取得本地文件路徑
 *
 *  @return 文件路徑
 */
-(NSURL *)getFileUrl{
    NSString *urlStr=[[NSBundle mainBundle] pathForResource:@"The New Look of OS X Yosemite.mp4" ofType:nil];
    NSURL *url=[NSURL fileURLWithPath:urlStr];
    return url;
}

/**
 *  取得網絡文件路徑
 *
 *  @return 文件路徑
 */
-(NSURL *)getNetworkUrl{
    NSString *urlStr=@"http://192.168.1.161/The New Look of OS X Yosemite.mp4";
    urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSURL *url=[NSURL URLWithString:urlStr];
    return url;
}

/**
 *  截取指定時間的視頻縮略圖
 *
 *  @param timeBySecond 時間點
 */
-(void)thumbnailImageRequest:(CGFloat )timeBySecond{
    //創建URL
    NSURL *url=[self getNetworkUrl];
    //根據url創建AVURLAsset
    AVURLAsset *urlAsset=[AVURLAsset assetWithURL:url];
    //根據AVURLAsset創建AVAssetImageGenerator
    AVAssetImageGenerator *imageGenerator=[AVAssetImageGenerator assetImageGeneratorWithAsset:urlAsset];
    /*截圖
     * requestTime:縮略圖創建時間
     * actualTime:縮略圖實際生成的時間
     */
    NSError *error=nil;
    CMTime time=CMTimeMakeWithSeconds(timeBySecond, 10);//CMTime是表示電影時間信息的結構體,第一個參數表示是視頻第幾秒,第二個參數表示每秒幀數.(如果要活的某一秒的第幾幀可以使用CMTimeMake方法)
    CMTime actualTime;
    CGImageRef cgImage= [imageGenerator copyCGImageAtTime:time actualTime:&actualTime error:&error];
    if(error){
        NSLog(@"截取視頻縮略圖時發生錯誤,錯誤信息:%@",error.localizedDescription);
        return;
    }
    CMTimeShow(actualTime);
    UIImage *image=[UIImage imageWithCGImage:cgImage];//轉化為UIImage
    //保存到相冊
    UIImageWriteToSavedPhotosAlbum(image,nil, nil, nil);
    CGImageRelease(cgImage);
}

@end

AVPlayer

MPMoviePlayerController足夠強大,幾乎不用寫幾行代碼就能完成一個播放器,但是正是由于它的高度封裝使得要自定義這個播放器變得很復雜,甚至是不可能完成。例如有些時候需要自定義播放器的樣式,那么如果要使用MPMoviePlayerController就不合適了,如果要對視頻有自由的控制則可以使用AVPlayer。AVPlayer存在于AVFoundation中,它更加接近于底層,所以靈活性也更強:

AVPlayer本身并不能顯示視頻,而且它也不像MPMoviePlayerController有一個view屬性。如果AVPlayer要顯示必須創建一個播放器層AVPlayerLayer用于展示,播放器層繼承于CALayer,有了AVPlayerLayer之后添加到控制器視圖的layer中即可。要使用 AVPlayer 首先了解一下幾個常用的類:

AVAsset:主要用于獲取多媒體信息,是一個抽象類,不能直接使用。

AVURLAsset:AVAsset的子類,可以根據一個URL路徑創建一個包含媒體信息的AVURLAsset對象。

AVPlayerItem:一個媒體資源管理對象,管理者視頻的一些基本信息和狀態,一個AVPlayerItem對應著一個視頻資源。

AVPlayer 沒有播放狀態屬性,通常情況下可以通過判斷播放器的播放速度來獲得播放狀態。如果rate為0說明是停止狀態,1是則是正常播放狀態。

播放視頻時,特別是播放網絡視頻往往需要知道視頻加載情況、緩沖情況、播放情況,這些信息可以通過KVO監控AVPlayerItem的status、loadedTimeRanges屬性來獲得。當 AVPlayerItem 的 status 屬性為AVPlayerStatusReadyToPlay是說明正在播放,只有處于這個狀態時才能獲得視頻時長等信息;當loadedTimeRanges的改變時(每緩沖一部分數據就會更新此屬性)可以獲得本次緩沖加載的視頻范圍(包含起始時間、本次加載時長),這樣一來就可以實時獲得緩沖情況。然后就是依靠AVPlayer的- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block方法獲得播放進度,這個方法會在設定的時間間隔內定時更新播放進度,通過time參數通知客戶端。

存在問題
無論是MPMoviePlayerController還是AVPlayer來播放視頻都相當強大,但是它也存在著一些不可回避的問題,那就是支持的視頻編碼格式很有限:H.264、MPEG-4,擴展名(壓縮格式):.mp4、.mov、.m4v、.m2v、.3gp、.3g2等。但是無論是MPMoviePlayerController還是AVPlayer它們都支持絕大多數音頻編碼,所以大家如果純粹是為了播放音樂的話也可以考慮使用這兩個播放器。那么如何支持更多視頻編碼格式呢?目前來說主要還是依靠第三方框架,在iOS上常用的視頻編碼、解碼框架有:VLC、ffmpeg

基礎

目前我們在計算機上進行音頻播放都需要依賴于音頻文件,音頻文件的生成過程是將聲音信息采樣、量化和編碼產生的數字信號的過程,人耳所能聽到的聲音,最低的頻率是從20Hz起一直到最高頻率20KHZ,因此音頻文件格式的最大帶寬是20KHZ。根據奈奎斯特的理論,只有采樣頻率高于聲音信號最高頻率的兩倍時,才能把數字信號表示的聲音還原成為原來的聲音,所以音頻文件的采樣率一般在40~50KHZ,比如最常見的CD音質采樣率44.1KHZ。

對聲音進行采樣、量化過程被稱為脈沖編碼調制(Pulse Code Modulation),簡稱PCM。PCM數據是最原始的音頻數據完全無損,所以PCM數據雖然音質優秀但體積龐大,為了解決這個問題先后誕生了一系列的音頻格式,這些音頻格式運用不同的方法對音頻數據進行壓縮,其中有無損壓縮(ALAC、APE、FLAC)和有損壓縮(MP3、AAC、OGG、WMA)兩種。

MP3格式中的碼率(BitRate)代表了MP3數據的壓縮質量,現在常用的碼率有128kbit/s、160kbit/s、320kbit/s等等,這個值越高聲音質量也就越高。MP3編碼方式常用的有兩種固定碼率(Constant bitrate,CBR)和可變碼率(Variable bitrate,VBR)。
MP3格式中的數據通常由兩部分組成,一部分為ID3用來存儲歌名、演唱者、專輯、音軌數等信息,另一部分為音頻數據。音頻數據部分以幀(frame)為單位存儲,每個音頻都有自己的幀頭,如圖所示就是一個MP3文件幀結構圖(圖片同樣來自互聯網)。MP3中的每一個幀都有自己的幀頭,其中存儲了采樣率等解碼必須的信息,所以每一個幀都可以獨立于文件存在和播放,這個特性加上高壓縮比使得MP3文件成為了音頻流播放的主流格式。幀頭之后存儲著音頻數據,這些音頻數據是若干個PCM數據幀經過壓縮算法壓縮得到的,對CBR的MP3數據來說每個幀中包含的PCM數據幀是固定的,而VBR是可變的。

iOS音頻播放概述

了解了基礎概念之后我們就可以列出一個經典的音頻播放流程(以MP3為例):

  1. 讀取MP3文件
  2. 解析采樣率、碼率、時長等信息,分離MP3中的音頻幀
  3. 對分離出來的音頻幀解碼得到PCM數據
  4. 對PCM數據進行音效處理(均衡器、混響器等,非必須)
  5. 把PCM數據解碼成音頻信號
  6. 把音頻信號交給硬件播放
  7. 重復1-6步直到播放完成

CoreAudio的接口層次

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:用于游戲音頻播放

使用場景

  • 如果你只是想實現音頻的播放,沒有其他需求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進行音頻播放的流程.png

音頻播放的實現級別:

(1) 離線播放:這里并不是指應用不聯網,而是指播放本地音頻文件,包括先下完完成音頻文件再進行播放的情況,這種使用AVFoundation里的AVAudioPlayer可以滿足
(2) 在線播放:使用AVFoundation的AVPlayer可以滿足
(3) 在線播放同時存儲文件:使用
AudioFileStreamer + AudioQueue 可以滿足
(4) 在線播放且帶有音效處理:使用
AudioFileStreamer + AudioQueue + 音效模塊(系統自帶或者
自行開發)來滿足

功能需求

通常音樂播放并展示到界面上需要我們實現的功能如下:
1、(核心)播放器通過一個網絡鏈接播放音樂
2、(基本)播放器的常用操作:暫停、播放、上一首、下一首等等
3、(基本)監聽該音樂的播放進度、獲取音樂的總時間、當前播放時間
4、(基本)監聽改播放器狀態:
?????(1)媒體加載狀態
?????(2)數據緩沖狀態
?????(3)播放完畢狀態
5、(可選)Remote Control控制音樂的播放
6、(可選)Now Playing Center展示正在播放的音樂

監聽改播放器狀態

[songItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {

    if ([keyPath isEqualToString:@"status"]) {
        switch (self.player.status) {            
            case AVPlayerStatusUnknown:                
                BASE_INFO_FUN(@"KVO:未知狀態,此時不能播放");                
                break;            
            case AVPlayerStatusReadyToPlay:                
                self.status = SUPlayStatusReadyToPlay;                    
                BASE_INFO_FUN(@"KVO:準備完畢,可以播放");                
                break;
            case AVPlayerStatusFailed:
                BASE_INFO_FUN(@"KVO:加載失敗,網絡或者服務器出現問題");
                break;            
            default:                
                break;        
        }
    }
}

播放完后移除觀察者:[songItem removeObserver:self forKeyPath:@"status"];

Remote Control控制音樂的播放

Remote Control可以讓你在不打開APP的情況下控制其播放,最常見的出現于鎖屏界面、從屏幕底部上拉和耳機線控三種,可以達到增強用戶體驗的作用。

我們在AppDelegate里去設置Remote Control:
(1)聲明接收Remote Control事件

[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];

(2)重寫方法,成為第一響應者

- (BOOL)canBecomeFirstResponder {    
    return YES;
}

(3)對事件進行處理

- (void)remoteControlReceivedWithEvent:(UIEvent *)event {       
    switch (event.subtype)    {        
        case UIEventSubtypeRemoteControlPlay:
            [self.player startPlay];
            BASE_INFO_FUN(@“remote_播放");
            break;        
        case UIEventSubtypeRemoteControlPause:            
            [self.player pausePlay];
            BASE_INFO_FUN(@"remote_暫停");
            break;        
        case UIEventSubtypeRemoteControlNextTrack:
            [self.player playNextSong];
            BASE_INFO_FUN(@"remote_下一首");
            break;        
        case UIEventSubtypeRemoteControlTogglePlayPause:            
            self.player.isPlaying ? [self.player pausePlay] : [self.player startPlay];           
            BASE_INFO_FUN(@“remote_耳機的播放/暫停");
            break;        
        default:            
            break;    }
}

Now Playing Center

Now Playing Center可以在鎖屏界面展示音樂的信息,也達到增強用戶體驗的作用。

- (void)configNowPlayingCenter {    
    BASE_INFO_FUN(@"配置NowPlayingCenter");
    NSMutableDictionary * info = [NSMutableDictionary dictionary];
    // 音樂的標題
    [info setObject:_player.currentSong.title forKey:MPMediaItemPropertyTitle];
     // 音樂的藝術家
    [info setObject:_player.currentSong.artist forKey:MPMediaItemPropertyArtist];
     // 音樂的播放時間
    [info setObject:@(self.player.playTime.intValue) forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
     // 音樂的播放速度
    [info setObject:@(1) forKey:MPNowPlayingInfoPropertyPlaybackRate];
     // 音樂的總時間
    [info setObject:@(self.player.playDuration.intValue) forKey:MPMediaItemPropertyPlaybackDuration];
     // 音樂的封面
    MPMediaItemArtwork * artwork = [[MPMediaItemArtwork alloc] initWithImage:_player.coverImg];
    [info setObject:artwork forKey:MPMediaItemPropertyArtwork];
     // 完成設置
    [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:info];
}

Now Playing Center并不需要每一秒都去刷新(設置),它是根據你設置的PlaybackRate來計算進度條展示的進度,比如你PlaybackRate傳1,那就是1秒刷新一次進度顯示,當然暫停播放的時候它也會自動暫停。

那什么時候設置Now Playing Center比較合適呢?對于播放網絡音樂來說,需要刷新的有幾個時間點:當前播放的歌曲變化時(如切換到下一首)、當前歌曲信息變化時(如從Unknown到ReadyToPlay)、當前歌曲拖動進度時。

如果有讀者是使用百度音樂聽歌的話,會發現其帶有鎖屏歌詞,其實它是采用“將歌詞和封面合成新的圖片設置為Now Playing Center的封面 + 歌詞躍進時刷新Now Playing Center”來實現的。

幾種技術優缺點對比

參考:

  1. iOS開發系列--音頻播放、錄音、視頻播放、拍照、視頻錄制
  2. http://msching.github.io/blog/2014/07/07/audio-in-ios/
  3. iOS音頻篇:使用AVPlayer播放網絡音樂
  4. iOS音頻篇:使用AVPlayer播放網絡音樂
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容