iOS多媒体:音频

作者: 时光啊混蛋_97boy | 来源:发表于2020-10-27 10:34 被阅读0次

    原创:知识探索型文章
    创作不易,请珍惜,之后会持续更新,不断完善
    个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
    温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

    目录

    • 一、音效
      • 1、简介
      • 2、Demo演示
    • 二、音乐
      • 1、简介
      • 2、Demo
    • 三、音频会话
      • 1、简介
      • 2、Demo演示
    • 四、播放音乐库中的音乐
      • 1、属性和方法
      • 2、获取媒体文件列表
      • 3、Demo演示
    • 五、音频队列服务
      • 1、简介
      • 2、Demo演示
    • Demo
    • 参考文献

    一、音效

    1、简介

    在iOS中音频播放从形式上可以分为音效播放和音乐播放。前者主要指的是一些短音频播放,通常作为点缀音频,对于这类音频不需要进行进度、循环等控制。后者指的是一些较长的音频,通常是主音频,对于这些音频的播放通常需要进行精确的控制。在iOS中播放两类音频分别使用AudioToolbox.frameworkAVFoundation.framework来完成音效和音乐播放。AudioToolbox.framework是一套基于C语言的框架,使用它来播放音效其本质是将短音频注册到系统声音服务(System Sound Service)。

    System Sound Service的使用限制
    • 音频播放时间不能超过30s。
    • 数据必须是PCM或者IMA4格式。
    • 音频文件必须打包成.caf.aif.wav中的一种(注意这是官方文档的说法,实际测试发现.mp3也可以播放)。
    使用System Sound Service播放音效的步骤
    1. 调用AudioServicesCreateSystemSoundID( CFURLRef inFileURLSystemSoundID* outSystemSoundID)函数获得系统声音ID
    2. 如果需要监听播放完成操作,则使用AudioServicesAddSystemSoundCompletion( SystemSoundID inSystemSoundIDCFRunLoopRef inRunLoopCFStringRef inRunLoopModeAudioServicesSystemSoundCompletionProc inCompletionRoutinevoid* inClientData)方法注册回调函数。
    3. 调用AudioServicesPlaySystemSound(SystemSoundID inSystemSoundID) 或者AudioServicesPlayAlertSound(SystemSoundID inSystemSoundID) 方法播放音效(后者带有震动效果)。

    2、Demo演示

    a、播放音效文件
    #import <AudioToolbox/AudioToolbox.h>
    
    -(void)playSoundEffect:(NSString *)name
    {
        NSString *audioFile = [[NSBundle mainBundle] pathForResource:name ofType:nil];
        NSURL *fileUrl = [NSURL fileURLWithPath:audioFile];
        
        //1.获得系统声音ID
        SystemSoundID soundID = 0;
    
        /**
         * inFileUrl:音频文件url
         * outSystemSoundID:声音id(此函数会将音效文件加入到系统音频服务中并返回一个长整形ID)
         */
        AudioServicesCreateSystemSoundID((__bridge CFURLRef)(fileUrl), &soundID);
      
        //如果需要在播放完之后执行某些操作,可以调用如下方法注册一个播放完成回调函数
        AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, soundCompleteCallback, NULL);
        
        //2.播放音频
        AudioServicesPlaySystemSound(soundID);//播放音效
        AudioServicesPlayAlertSound(soundID);//播放音效并震动
    }
    
    b、 播放完成回调函数
    void soundCompleteCallback(SystemSoundID soundID,void * clientData)
    {
        NSLog(@"播放完成...");
    }
    
    c、调用方式
    [self playSoundEffect:@"videoRing.caf"]; // 传入音频文件名称
    

    输出结果为:

    2020-09-01 09:53:33.464405+0800 AVAudioRecorderDemo[5612:339649] 播放完成...
    
    d、遇到的问题

    这里会遇到一个问题,下面这行代码返回的path为空,而笔者已经把Sound.caf文件添加到项目中,最后在项目设置的Build Phases页面,在Copy Bundle Resources栏目下添加该Sound.caf文件就解决了。

    NSString *path = [[NSBundle mainBundle] pathForResource:@"Sound.caf" ofType:nil];  
    

    二、音乐

    1、简介

    如果播放较大的音频或者要对音频有精确的控制则System Sound Service可能就很难满足实际需求了,通常这种情况会选择使用AVFoundation.framework中的AVAudioPlayer来实现。AVAudioPlayer可以看成一个播放器,它支持多种音频格式,而且能够进行进度、音量、播放速度等控制。

    AVAudioPlayer的属性
    @property(readonly, getter=isPlaying) BOOL playing //是否正在播放,只读
    @property(readonly) NSUInteger numberOfChannels //音频声道数,只读
    @property(readonly) NSTimeInterval duration //音频时长
    @property(readonly) NSURL *url //音频文件路径,只读
    @property(readonly) NSData *data //音频数据,只读
    @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 //输出设备播放音频的时间,注意如果播放中被暂停此时间也会继续累加
    @property NSInteger numberOfLoops //循环播放次数,如果为0则不循环,如果小于0则无限循环,大于0则表示循环次数
    @property(readonly) NSDictionary *settings //音频播放设置信息,只读
    @property(getter=isMeteringEnabled) BOOL meteringEnabled //是否启用音频测量,默认为NO,一旦启用音频测量可以通过updateMeters方法更新测量值
    
    AVAudioPlayer的方法
    - (instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError //使用文件URL初始化播放器,注意这个URL不能是HTTP URL,AVAudioPlayer不支持加载网络媒体流,只能播放本地文件
    - (instancetype)initWithData:(NSData *)data error:(NSError **)outError //使用NSData初始化播放器,注意使用此方法时必须文件格式和文件后缀一致,否则出错,所以相比此方法更推荐使用上述方法 
    - (BOOL)prepareToPlay; //加载音频文件到缓冲区,注意即使在播放之前音频文件没有加载到缓冲区程序也会隐式调用此方法
    - (BOOL)play; //播放音频文件
    - (BOOL)playAtTime:(NSTimeInterval)time //在指定的时间开始播放音频
    - (void)pause; //暂停播放
    - (void)stop; //停止播放
    - (void)updateMeters; //更新音频测量值,注意如果要更新音频测量值必须设置meteringEnabled为YES,通过音频测量值可以即时获得音频分贝等信息
     (float)peakPowerForChannel:(NSUInteger)channelNumber; //获得指定声道的分贝峰值,注意如果要获得分贝峰值必须在此之前调用updateMeters方法
    - (float)averagePowerForChannel:(NSUInteger)channelNumber; //获得指定声道的分贝平均值,注意如果要获得分贝平均值必须在此之前调用updateMeters方法
    
    AVAudioPlayer的代理方法
    - (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag; //音频播放完成
    

    2、Demo

    a、功能点

    下面就使用AVAudioPlayer实现一个简单播放器,在这个播放器中实现了播放、暂停、显示播放进度功能,当然例如调节音量、设置循环模式、甚至是声波图像(通过分析音频分贝值)等功能都可以实现,这里就不再一一演示。实现步骤如下:

    1. 初始化AVAudioPlayer对象,此时通常指定本地文件路径。
    2. 设置播放器属性,例如重复次数、音量大小等。
    3. 调用play方法播放。

    运行效果如下:

    播放音乐
    2020-09-01 11:22:00.863313+0800 AVAudioRecorderDemo[6245:396604] 音乐播放完成...
    
    b、扩展和头文件

    当然由于AVAudioPlayer一次只能播放一个音频文件,所以上一曲、下一曲其实可以通过创建多个播放器对象来完成,这里暂不实现。播放进度的实现主要依靠一个定时器实时计算当前播放时长和音频总时长的比例,另外为了演示委托方法,下面的代码中也实现了播放完成委托方法,通常如果有下一曲功能的话播放完可以触发下一曲音乐播放。

    #import "MusicViewController.h"
    #import <AVFoundation/AVFoundation.h>
    #define kMusicFile @"桜道.mp3"
    #define kMusicSinger @"歌手:Jusqu'à Grand-Père"
    #define kMusicTitle @"歌曲:桜道"
    
    @interface MusicViewController ()<AVAudioPlayerDelegate>
    
    @property (nonatomic,strong) AVAudioPlayer *audioPlayer; //播放器
    @property (strong, nonatomic) UILabel *controlPanel; //控制面板
    @property (strong, nonatomic) UIProgressView *playProgress; //播放进度
    @property (strong, nonatomic) UILabel *musicSinger; //演唱者
    @property (strong, nonatomic) UIButton *playOrPause; //播放/暂停按钮(如果tag为0认为是暂停状态,1是播放状态)
    
    @property (weak ,nonatomic) NSTimer *timer; //进度更新定时器
    
    @end
    
    c、播放控制

    播放音频

    -(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方法,此方法会取消,之后无法恢复
        }
    }
    

    点击播放/暂停按钮

    - (void)playClick:(UIButton *)sender
    {
        if(sender.tag)
        {
            sender.tag = 0;
            [sender setImage:[UIImage imageNamed:@"playing_btn_play_n"] forState:UIControlStateNormal];
            [self pause];
        }
        else
        {
            sender.tag = 1;
            [sender setImage:[UIImage imageNamed:@"playing_btn_pause_n"] forState:UIControlStateNormal];
            [self play];
        }
    }
    

    更新播放进度

    -(void)updateProgress
    {
        float progress = self.audioPlayer.currentTime / self.audioPlayer.duration;
        [self.playProgress setProgress:progress animated:true];
    }
    
    d、AVAudioPlayerDelegate
    -(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
    {
        NSLog(@"音乐播放完成...");
        // 下一首
    }
    
    e、创建播放器和计时器

    创建播放器

    -(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;
            [_audioPlayer prepareToPlay]; //加载音频文件到缓存
            
            if(error)
            {
                NSLog(@"初始化播放器过程发生错误,错误信息:%@",error.localizedDescription);
                return nil;
            }
        }
        return _audioPlayer;
    }
    

    创建计时器

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

    三、音频会话

    1、简介

    事实上上面的播放器还存在一些问题,例如通常我们看到的播放器即使退出到后台也是可以播放的,而这个播放器如果退出到后台它会自动暂停。

    a、支持后台播放的条件
    AVAudioSession *audioSession = [AVAudioSession sharedInstance];
    [audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
    [audioSession setActive:YES error:nil];
    
    • 设置后台运行模式:在plist文件中添加Required background modes,并且设置item 0App plays audio or streams audio/video using AirPlay(其实可以直接通过XcodeProject Targets-Capabilities-Background Modes中设置)。
    • 设置AVAudioSession的类型为AVAudioSessionCategoryPlayback并且调用setActive:方法启动会话。
    • 为了能够让应用退到后台之后支持耳机控制,建议添加远程控制事件(这一步不是后台播放必须的)。

    前两步是后台播放所必须设置的,第三步主要用于接收远程事件,如果这一步不设置虽让也能够在后台播放,但是无法获得音频控制权(如果在使用当前应用之前使用其他播放器播放音乐的话,此时如果按耳机播放键或者控制中心的播放按钮则会播放前一个应用的音频),并且不能使用耳机进行音频控制。

    第一步操作相信大家都很容易理解,如果应用程序要允许运行到后台必须设置,正常情况下应用如果进入后台会被挂起,通过该设置可以让应用程序继续在后台运行。但是第二步使用的AVAudioSession有必要进行一下详细的说明。

    在iOS中每个应用都有一个音频会话,这个会话就通过AVAudioSession来表示。AVAudioSession同样存在于AVFoundation框架中,它是单例模式设计,通过sharedInstance进行访问。在使用Apple设备时大家会发现有些应用只要打开其他音频播放就会终止,而有些应用却可以和其他应用同时播放,在多种音频环境中如何去控制播放的方式就是通过音频会话来完成的。

    b、音频会话的几种会话模式:
    会话类型 说明 是否要求输入 是否要求输出 是否遵从静音键
    AVAudioSessionCategoryAmbient 混音播放,可以与其他音频应用同时播放
    AVAudioSessionCategorySoloAmbient 独占播放
    AVAudioSessionCategoryPlayback 后台播放,也是独占的
    AVAudioSessionCategoryRecord 录音模式,用于录音时使用
    AVAudioSessionCategoryPlayAndRecord 播放和录音,此时可以录音也可以播放
    AVAudioSessionCategoryAudioProcessing 硬件解码音频,此时不能播放和录制
    AVAudioSessionCategoryMultiRoute 多种输入输出,例如可以耳机、USB设备同时播放

    注意:是否遵循静音键表示在播放过程中如果用户通过硬件设置为静音是否能关闭声音。

    根据前面对音频会话的理解,相信大家开发出能够在后台播放的音频播放器并不难,但是注意一下,在前面的代码中也提到设置完音频会话类型之后需要调用setActive:方法将会话激活才能起作用。类似的,如果一个应用已经在播放音频,打开我们的应用之后设置了在后台播放的会话类型,此时其他应用的音频会停止而播放我们的音频,如果希望我们的程序音频播放完之后(关闭或退出到后台之后)能够继续播放其他应用的音频的话则可以调用setActive:方法关闭会话。


    2、Demo演示

    a、扩展和头文件
    #import "AudioSessionViewController.h"
    #import <AVFoundation/AVFoundation.h>
    #define kMusicFile @"桜道.mp3"
    #define kMusicSinger @"歌手:Jusqu'à Grand-Père"
    #define kMusicTitle @"歌曲:桜道"
    
    @interface AudioSessionViewController ()<AVAudioPlayerDelegate>
    
    @property (nonatomic,strong) AVAudioPlayer *audioPlayer; //播放器
    @property (strong, nonatomic) UILabel *controlPanel; //控制面板
    @property (strong, nonatomic) UIProgressView *playProgress; //播放进度
    @property (strong, nonatomic) UILabel *musicSinger; //演唱者
    @property (strong, nonatomic) UIButton *playOrPause; //播放/暂停按钮(如果tag为0认为是暂停状态,1是播放状态)
    
    @property (weak ,nonatomic) NSTimer *timer; //进度更新定时器
    
    @end
    
    b、耳机控制

    实现了拔出耳机暂停音乐播放的功能,这也是一个比较常见的功能。可以通过通知获得输出改变的通知,然后拿到通知对象后根据userInfo获得是何种改变类型,进而根据情况对音乐进行暂停操作。

    -(void)routeChange:(NSNotification *)notification
    {
        NSDictionary *dictionary = notification.userInfo;
        [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
            NSLog(@"notification userInfo,key:%@,value:%@",key,obj);
        }];
        int changeReason = [dictionary[AVAudioSessionRouteChangeReasonKey] intValue];
        
        // 等于AVAudioSessionRouteChangeReasonOldDeviceUnavailable 表示旧输出不可用
        if (changeReason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable)
        {
            AVAudioSessionRouteDescription *routeDescription = dictionary[AVAudioSessionRouteChangePreviousRouteKey];
            AVAudioSessionPortDescription *portDescription= [routeDescription.outputs firstObject];
            
            //原设备为耳机则暂停
            if ([portDescription.portType isEqualToString:@"Headphones"])
            {
                [self pause];
            }
        }
    }
    

    显示当前视图控制器时注册远程事件

    - (void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:animated];
        
        //开启远程控制
        [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
        
        //作为第一响应者
        //[self becomeFirstResponder];
    }
    

    当前控制器视图不显示时取消远程控制

    - (void)viewWillDisappear:(BOOL)animated
    {
        [super viewWillDisappear:animated];
        
        [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
        //[self resignFirstResponder];
    }
    
    c、播放控制

    播放音频

    - (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方法,此方法会取消,之后无法恢复
        }
    }
    

    点击播放/暂停按钮

    - (void)playClick:(UIButton *)sender
    {
        if(sender.tag)
        {
            sender.tag = 0;
            [sender setImage:[UIImage imageNamed:@"playing_btn_play_n"] forState:UIControlStateNormal];
            [self pause];
        }
        else
        {
            sender.tag = 1;
            [sender setImage:[UIImage imageNamed:@"playing_btn_pause_n"] forState:UIControlStateNormal];
            [self play];
        }
    }
    

    更新播放进度

    - (void)updateProgress
    {
        float progress = self.audioPlayer.currentTime / self.audioPlayer.duration;
        [self.playProgress setProgress:progress animated:true];
    }
    
    d、AVAudioPlayerDelegate
    -(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
    {
        NSLog(@"音乐播放完成...");
    
        // 根据实际情况播放完成可以将会话关闭,其他音频应用继续播放
        [[AVAudioSession sharedInstance] setActive:NO error:nil];
    }
    

    四、播放音乐库中的音乐

    1、属性和方法

    众所周知音乐是iOS的重要组成播放,无论是iPodiTouchiPhone还是iPad都可以在iTunes购买音乐或添加本地音乐到音乐库中同步到你的iOS设备。在MediaPlayer.frameowork中有一个MPMusicPlayerController用于播放音乐库中的音乐。

    属性

    播放器状态,枚举类型

    @property (nonatomic, readonly) MPMusicPlaybackState playbackState 
    MPMusicPlaybackStateStopped:停止播放 
    MPMusicPlaybackStatePlaying:正在播放
    MPMusicPlaybackStatePaused:暂停播放
    MPMusicPlaybackStateInterrupted:播放中断
    MPMusicPlaybackStateSeekingForward:向前查找
    MPMusicPlaybackStateSeekingBackward:向后查找
    

    重复模式,枚举类型

    @property (nonatomic) MPMusicRepeatMode repeatMode  
    MPMusicRepeatModeDefault:默认模式,使用用户的首选项(系统音乐程序设置)
    MPMusicRepeatModeNone:不重复
    MPMusicRepeatModeOne:单曲循环
    MPMusicRepeatModeAll:在当前列表内循环
    

    随机播放模式,枚举类型

    @property (nonatomic) MPMusicShuffleMode shuffleMode 
    MPMusicShuffleModeDefault:默认模式,使用用户首选项(系统音乐程序设置)
    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 //获取系统播放器,支持后台播放
    
    对象方法
    - (void)setQueueWithQuery:(MPMediaQuery *)query //使用媒体队列设置播放源媒体队列
    - (void)setQueueWithItemCollection:(MPMediaItemCollection *)itemCollection //使用媒体项集合设置播放源媒体队列
    - (void)skipToNextItem //下一曲
    - (void)skipToBeginning //从起始位置播放
    - (void)skipToPreviousItem //上一曲
    - (void)beginGeneratingPlaybackNotifications //开启播放通知,注意不同于其他播放器,MPMusicPlayerController要想获得通知必须首先开启,默认情况无法获得通知
    - (void)endGeneratingPlaybackNotifications //关闭播放通知
    - (void)prepareToPlay //做好播放准备(加载音频到缓冲区),在使用play方法播放时如果没有做好准备回自动调用该方法
    - (void)play //开始播放
    - (void)pause //暂停播放
    - (void)stop //停止播放
    - (void)beginSeekingForward //开始向前查找(快进)
    - (void)beginSeekingBackward //开始向后查找(快退)
    - (void)endSeeking //结束查找
    
    通知

    要想获得MPMusicPlayerController通知必须首先调用beginGeneratingPlaybackNotifications开启通知

    MPMusicPlayerControllerPlaybackStateDidChangeNotification //播放状态改变
    MPMusicPlayerControllerNowPlayingItemDidChangeNotification //当前播放音频改变
    MPMusicPlayerControllerVolumeDidChangeNotification //声音大小改变
    MPMediaPlaybackIsPreparedToPlayDidChangeNotification //准备好播放
    

    2、获取媒体文件列表

    MPMusicPlayerController有两种播放器:applicationMusicPlayersystemMusicPlayer,前者在应用退出后音乐播放会自动停止,后者在应用停止后不会退出播放状态。

    MPMusicPlayerController加载音乐不同于前面的AVAudioPlayer是通过一个文件路径来加载,而是需要一个播放队列。在MPMusicPlayerController中提供了两个方法来加载播放队列:- (void)setQueueWithQuery:(MPMediaQuery *)query- (void)setQueueWithItemCollection:(MPMediaItemCollection *)itemCollection,正是由于它的播放音频来源是一个队列,因此MPMusicPlayerController支持上一曲、下一曲等操作。

    那么接下来的问题就是如何获取MPMediaQueue或者MPMediaItemCollectionMPMediaQueue对象有一系列的类方法来获得媒体队列:

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

    有了这些方法,就可以很容易获到歌曲、播放列表、专辑媒体等媒体队列了,这样就可以通过: - (void)setQueueWithQuery:(MPMediaQuery *)query方法设置音乐来源了。又或者得到MPMediaQueue之后创建MPMediaItemCollection,使用- (void)setQueueWithItemCollection:(MPMediaItemCollection *)itemCollection设置音乐来源。

    有时候可能希望用户自己来选择要播放的音乐,这时可以使用MPMediaPickerController,它是一个视图控制器,类似于UIImagePickerController,选择完播放来源后可以在其代理方法中获得MPMediaItemCollection对象。

    无论是通过哪种方式获得MPMusicPlayerController的媒体源,可能都希望将每个媒体的信息显示出来,这时候可以通过MPMediaItem对象获得。一个MPMediaItem代表一个媒体文件,通过它可以访问媒体标题、专辑名称、专辑封面、音乐时长等等。无论是MPMediaQueue还是MPMediaItemCollection都有一个items属性,它是MPMediaItem数组,通过这个属性可以获得MPMediaItem对象。


    3、Demo演示

    a、功能点

    下面就简单看一下MPMusicPlayerController的使用,在下面的例子中简单演示了音乐的选择、播放、暂停、通知、下一曲、上一曲功能,相信有了上面的概念,代码读起来并不复杂(示例中是直接通过MPMeidaPicker进行音乐选择的,但是仍然提供了两个方法getLocalMediaQuerygetLocalMediaItemCollection来演示如何直接通过MPMediaQueue获得媒体队列或媒体集合)。在Info.plist文件中添加Privacy - Media Library Usage Description访问权限,提示语可为:访问音乐库权限。

    a、数据源

    获得媒体队列

    - (MPMediaQuery *)getLocalMediaQuery
    {
        MPMediaQuery *mediaQueue = [MPMediaQuery songsQuery];
        for (MPMediaItem *item in mediaQueue.items)
        {
            NSLog(@"item 标题:%@,albumTitle 专辑标题:%@",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 标题:%@,albumTitle 专辑标题:%@",item.title,item.albumTitle);
        }
        MPMediaItemCollection *mediaItemCollection = [[MPMediaItemCollection alloc] initWithItems:[array copy]];
        return mediaItemCollection;
    }
    
    b、MPMediaPickerControllerDelegate

    选择完成

    -(void)mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection
    {
        MPMediaItem *mediaItem = [mediaItemCollection.items firstObject];// 播放第一个音乐
        
        //注意很多音乐信息如标题、专辑、表演者、封面、时长等信息都可以通过MPMediaItem的valueForKey:方法得到,也都有对应的属性可以直接访问
        //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];
    }
    
    c、通知

    添加通知

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

    播放状态改变通知

    -(void)playbackStateChange:(NSNotification *)notification
    {
        switch (self.musicPlayer.playbackState)
        {
            case MPMusicPlaybackStatePlaying:
                NSLog(@"正在播放...");
                break;
            case MPMusicPlaybackStatePaused:
                NSLog(@"播放暂停.");
                break;
            case MPMusicPlaybackStateStopped:
                NSLog(@"播放停止.");
                break;
            default:
                break;
        }
    }
    
    d、创建媒体播发器
    -(MPMusicPlayerController *)musicPlayer
    {
        if (!_musicPlayer)
        {
            _musicPlayer = [MPMusicPlayerController systemMusicPlayer];
            // 开启通知,否则监控不到MPMusicPlayerController的通知
            [_musicPlayer beginGeneratingPlaybackNotifications];
            // 添加通知
            [self addNotification];
            
            // 如果不使用MPMediaPickerController可以使用如下方法获得音乐库媒体队列
            //[_musicPlayer setQueueWithItemCollection:[self getLocalMediaItemCollection]];
        }
        return _musicPlayer;
    }
    
    e、创建媒体选择器
    -(MPMediaPickerController *)mediaPicker
    {
        if (!_mediaPicker)
        {
            // 初始化媒体选择器,这里设置媒体类型为音乐,其实这里也可以选择视频、广播等
            //_mediaPicker = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeMusic];
            _mediaPicker = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeAny];
            // 允许多选
            _mediaPicker.allowsPickingMultipleItems = YES;
            // 显示iCloud选项
            _mediaPicker.showsCloudItems = YES;
            _mediaPicker.prompt = @"请选择要播放的音乐";
            //设置选择器代理
            _mediaPicker.delegate = self;
        }
        return _mediaPicker;
    }
    
    @end
    

    注意:模拟器和没有安装Apple music app的真机都会报错,必须在真机上调试,且安装苹果自带的音乐APP。

    The requested app extension could not be found
    

    五、音频队列服务

    1、简介

    无论是前面的录音还是音频播放均不支持网络流媒体播放,当然对于录音来说这种需求可能不大,但是对于音频播放来说有时候就很有必要了。AVAudioPlayer只能播放本地文件,并且是一次性加载所以音频数据,初始化AVAudioPlayer时指定的URL也只能是File URL而不能是HTTP URL

    当然,将音频文件下载到本地然后再调用AVAudioPlayer来播放也是一种播放网络音频的办法,但是这种方式最大的弊端就是必须等到整个音频播放完成才能播放,而不能使用流式播放,这往往在实际开发中是不切实际的。那么在iOS中如何播放网络流媒体呢?就是使用AudioToolbox框架中的音频队列服务Audio Queue Services

    使用音频队列服务完全可以做到音频播放和录制,首先看一下录音音频服务队列:

    录音音频服务队列

    一个音频服务队列Audio Queue由三部分组成:

    • 三个缓冲器Buffers:每个缓冲器都是一个存储音频数据的临时仓库。
    • 一个缓冲队列Buffer Queue:一个包含音频缓冲器的有序队列。
    • 一个回调Callback:一个自定义的队列回调函数。

    声音通过输入设备进入缓冲队列中,首先填充第一个缓冲器;当第一个缓冲器填充满之后自动填充下一个缓冲器,同时会调用回调函数,在回调函数中需要将缓冲器中的音频数据写入磁盘,同时将缓冲器放回到缓冲队列中以便重用。

    下面是Apple官方关于音频队列服务的流程示意图:

    官方关于音频队列服务的流程示意图

    类似的,看一下音频播放缓冲队列,其组成部分和录音缓冲队列类似。

    音频播放缓冲队列

    但是在音频播放缓冲队列中,回调函数调用的时机不同于音频录制缓冲队列,流程刚好相反。将音频读取到缓冲器中,一旦一个缓冲器填充满之后就放到缓冲队列中,然后继续填充其他缓冲器。当开始播放时,则从第一个缓冲器中读取音频进行播放,一旦播放完之后就会触发回调函数,开始播放下一个缓冲器中的音频,同时填充第一个缓冲器放,填充满之后再次放回到缓冲队列。

    下面是详细的流程图:

    详细的流程图

    当然,要明白音频队列服务的原理并不难,问题是如何实现这个自定义的回调函数,这其中我们有大量的工作要做,控制播放状态、处理异常中断、进行音频编码等等。由于牵扯内容过多,而且不是本文目的,如果以后有时间将另开一篇文章重点介绍,目前有很多第三方优秀框架可以直接使用,例如FreeStreamer

    使用FreeStreamer之前要做如下准备工作:

    1. 拷贝FreeStreamer中的Reachability.hReachability.mCommonastreamer两个文件夹中的内容到项目中。
    2. 添加FreeStreamer使用的类库:CFNetwork.frameworkAudioToolbox.frameworkAVFoundation.frameworklibxml2.dylibMediaPlayer.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

    2、Demo演示

    然后就可以编写代码播放网络音频了。

    #import "FreeStreamerViewController.h"
    #import "FSAudioStream.h"
    
    @interface FreeStreamerViewController ()
    
    @property (nonatomic,strong) FSAudioStream *audioStream;
    
    @end
    
    @implementation FreeStreamerViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
    
        [self.audioStream play];
    }
    
    // 取得本地文件路径
    -(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对象
    -(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
    

    其实FreeStreamer的功能很强大,不仅仅是播放本地、网络音频那么简单,它还支持播放列表、检查包内容、RSS订阅、播放中断等很多强大的功能,甚至还包含了一个音频分析器。


    Demo

    Demo在我的Github上,欢迎下载。
    Multi-MediaDemo

    参考文献

    iOS 录音的常用设置 Objective-C
    iOS 录音、音频的拼接剪切以及边录边压缩转码
    AVFoundation视频导入导出
    iOS 使用 Lame 转码 MP3 的最正确姿势
    iOS开发系列--音频播放、录音、视频播放、拍照、视频录制

    相关文章

      网友评论

        本文标题:iOS多媒体:音频

        本文链接:https://www.haomeiwen.com/subject/vrwtvktx.html