ios 音频开发

最近接触到新项目里的音频业务,根据这几天的整理,总结一点内容,方便记录。后续不断更新。。。

在iOS程序中,音频播放随处可见,有的声音只有1秒,有的声音好几分钟 。iOS支持的音频格式AAC、ALAC、IMA4、linear、MP3。

AVAudioPlayer

AVAudioPlayer类用于回放音频数据。是一个易于使用的类,它提供了大量的功能。使用该类可以实现音频的载入,播放,暂停,停止。需要加入AVFoundation.framework框架,在使用的类中引入<AVFoundation/AVFoundation.h>。

  • AVAudioPlayer只能播放本地音乐文件

可以通过音频的NSData或者本地音频文件的url,来创建一个AVAudioPlayer实例,如加载本地的music.mp3的音频文件:

NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:@"music" withExtension:@"mp3"];
self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileUrl error:nil];

if (self.player) {
    [self.player prepareToPlay];
}

加载音频文件后,可以调用prepareToPlay方法,这样可以提前获取需要的硬件支持,并加载音频到缓冲区。在调用play方法时,减少开始播放的延迟。

当调用play方法后,开始播放音乐:

[self.player play];

可以调用pause或stop来暂停播放,这里的stop方法的效果也只是暂停播放,不同之处是stop会撤销prepareToPlay方法所做的准备。

[self.player stop];

另外,我们可以进行更多的操作:

单独设置音乐的音量(默认1.0,可设置范围为0.0至1.0,两个极端为静音、系统音量):

self.player.volume = 0.5;

修改左右声道的平衡(默认0.0,可设置范围为-1.0至1.0,两个极端分别为只有左声道、只有右声道):

self.player.pan = -1;

设置播放速度(默认1.0,可设置范围为0.5至2.0,两个极端分别为一半速度、两倍速度):

self.player.rate = 0.5;

设置循环播放(默认1,若设置值大于0,则为相应的循环次数,设置为-1可以实现无限循环):

self.player.numberOfLoops = -1;

AVPlayer

支持播放本地、分步下载、或在线流媒体音视频,不仅可以播放音频,配合AVPlayerLayer类可实现视频播放。另外支持播放进度监听。

  • AVPLayer:可以用来播放在线及本地音视频
  • AVAudioSession:音频会话,主要用来管理音频设置与硬件交互
    使用时需要导入

1.AVPlayer需要通过AVPlayerItem来关联需要播放的媒体。

  #import <AVFoundation/AVFoundation.h>
AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:urlStr]];
AVPlayer *player = [[AVPlayer alloc] initWithPlayerItem:item];
  • 2.在准备播放前,通过KVO添加播放状态改变监听

[self.player.currentItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];

处理KVO回调事件:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"status"]) {
        switch (self.player.status) {
            case AVPlayerStatusUnknown:
            {
                NSLog(@"未知转态");
            }
                break;
            case AVPlayerStatusReadyToPlay:
            {
                NSLog(@"准备播放");
            }
                break;
            case AVPlayerStatusFailed:
            {
                NSLog(@"加载失败");
            }
                break;

            default:
                break;
        }

    }
}
  • 3.KVO监听音乐缓冲状态:
[self.player.currentItem addObserver:self
                          forKeyPath:@"loadedTimeRanges"
                             options:NSKeyValueObservingOptionNew
                             context:nil];



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

{
    //...

    if ([keyPath isEqualToString:@"loadedTimeRanges"]) {

        NSArray * timeRanges = self.player.currentItem.loadedTimeRanges;
        //本次缓冲的时间范围
        CMTimeRange timeRange = [timeRanges.firstObject CMTimeRangeValue];
        //缓冲总长度
        NSTimeInterval totalLoadTime = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration);
        //音乐的总时间
        NSTimeInterval duration = CMTimeGetSeconds(self.player.currentItem.duration);
        //计算缓冲百分比例
        NSTimeInterval scale = totalLoadTime/duration;
        //更新缓冲进度条
        //        self.loadTimeProgress.progress = scale;
    }
}
  • 4.开始播放后,通过KVO添加播放结束事件监听
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(playFinished:)
                                             name:AVPlayerItemDidPlayToEndTimeNotification
                                           object:_player.currentItem];
  • 5.开始播放时,通过AVPlayer的方法监听播放进度,并更新进度条(定期监听的方法):
__weak typeof(self) weakSelf = self;
[self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
    //当前播放的时间
    float current = CMTimeGetSeconds(time);
    //总时间
    float total = CMTimeGetSeconds(item.duration);
    if (current) {
        float progress = current / total;
        //更新播放进度条
        weakSelf.playSlider.value = progress;
    }
}];
  • 6.用户拖动进度条,修改播放进度
- (void)playSliderValueChange:(UISlider *)sender
{
    //根据值计算时间
    float time = sender.value * CMTimeGetSeconds(self.player.currentItem.duration);
    //跳转到当前指定时间
    [self.player seekToTime:CMTimeMake(time, 1)];
}

后台播放

AVAudioSession中配置选项:

  • AVAudioSessionCategory
1.AVAudioSessionCategoryAmbient
当前App的播放声音可以和其他app播放的声音共存,当锁屏或按静音时停止。

2.AVAudioSessionCategorySoloAmbient
只能播放当前App的声音,其他app的声音会停止,当锁屏或按静音时停止。

3.AVAudioSessionCategoryPlayback
只能播放当前App的声音,其他app的声音会停止,当锁屏或按静音时不会停止。

4.AVAudioSessionCategoryRecord
只能用于录音,其他app的声音会停止,当锁屏或按静音时不会停止

5.AVAudioSessionCategoryPlayAndRecord
在录音的同时播放其他声音,当锁屏或按静音时不会停止
可用于听筒播放,比如微信语音消息听筒播放

6.AVAudioSessionCategoryAudioProcessing
使用硬件解码器处理音频,该音频会话使用期间,不能播放或录音

7.AVAudioSessionCategoryMultiRoute
多种音频输入输出,例如可以耳机、USB设备同时播放等
  • AVAudioSessionCategoryOptions
1.AVAudioSessionModeDefault
默认的模式,适用于所有的场景,可用于场景还原

2.AVAudioSessionModeVoiceChat
适用类别 :
AVAudioSessionCategoryPlayAndRecord 
应用场景VoIP

3.AVAudioSessionModeGameChat
适用类别:
AVAudioSessionCategoryPlayAndRecord 
应用场景游戏录制,由GKVoiceChat自动设置,无需手动调用

4.AVAudioSessionModeVideoRecording
适用类别:
AVAudioSessionCategoryPlayAndRecord
AVAudioSessionCategoryRecord 
应用场景视频录制

5.AVAudioSessionModeMoviePlayback
适用类别:
AVAudioSessionCategoryPlayBack
应用场景视频播放

6.AVAudioSessionModeVideoChat
适用类别:
AVAudioSessionCategoryPlayAndRecord
应用场景视频通话

7.AVAudioSessionModeMeasurement
适用类别:
AVAudioSessionCategoryPlayAndRecord
AVAudioSessionCategoryRecord
AVAudioSessionCategoryPlayback
AVAudioSessionModeSpokenAudio
  • AVAudioSessionMode
1.AVAudioSessionCategoryOptionMixWithOthers
适用于:
AVAudioSessionCategoryPlayAndRecord
AVAudioSessionCategoryPlayback
AVAudioSessionCategoryMultiRoute 
用于可以和其他app进行混音

2.AVAudioSessionCategoryOptionDuckOthers
适用于:
AVAudioSessionCategoryAmbient
AVAudioSessionCategoryPlayAndRecord
AVAudioSessionCategoryPlayback
AVAudioSessionCategoryMultiRoute
用于压低其他声音播放的音量,使期音量变小

3.AVAudioSessionCategoryOptionAllowBluetooth
适用于:
AVAudioSessionCategoryRecord and AVAudioSessionCategoryPlayAndRecord
用于是否支持蓝牙设备耳机等

4.AVAudioSessionCategoryOptionDefaultToSpeaker
适用于:
AVAudioSessionCategoryPlayAndRecord
用于将声音从Speaker播放,外放,即免提

5.AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers
适用于:
AVAudioSessionCategoryPlayAndRecord
AVAudioSessionCategoryPlayback
AVAudioSessionCategoryMultiRoute

6.AVAudioSessionCategoryOptionAllowBluetoothA2DP
适用于:
AVAudioSessionCategoryPlayAndRecord
蓝牙和a2dp

7.AVAudioSessionCategoryOptionAllowAirPlay
适用于:
AVAudioSessionCategoryPlayAndRecord
airplay

系统中断响应

上面说的这些Category啊、Option啊以及Mode都是对自己作为播放主体时的表现,但是假设,现在正在播放着,突然来电话了、闹钟响了或者你在后台放歌但是用户启动其他App用上面的方法影响的时候,我们的App该如何表现呢?最常用的场景当然是先暂停,待恢复的时候再继续。那我们的App要如何感知到这个终端以及何时恢复呢?

AVAudioSession提供了多种Notifications来进行此类状况的通知。其中将来电话、闹铃响等都归结为一般性的中断,用
AVAudioSessionInterruptionNotification来通知。其回调回来的userInfo主要包含两个键:

AVAudioSessionInterruptionTypeKey: 取值为AVAudioSessionInterruptionTypeBegan表示中断开始,我们应该暂停播放和采集

取值为AVAudioSessionInterruptionTypeEnded表示中断结束,我们可以继续播放和采集。

AVAudioSessionInterruptionOptionKey: 当前只有一种值AVAudioSessionInterruptionOptionShouldResume表示此时也应该恢复继续播放和采集。
而将其他App占据AudioSession的时候用AVAudioSessionSilenceSecondaryAudioHintNotification来进行通知。其回调回来的userInfo键为:

AVAudioSessionSilenceSecondaryAudioHintTypeKey
可能包含的值:
AVAudioSessionSilenceSecondaryAudioHintTypeBegin: 表示其他App开始占据Session
AVAudioSessionSilenceSecondaryAudioHintTypeEnd: 表示其他App开始释放Session

首先在AppDelegate.m的- (BOOL)application:didFinishLaunchingWithOptions:方法中添加代码:

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

然后在Info.plist文件中添加:

<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>

锁屏信息

  • 在每次准备播放下一首时,更新锁屏信息:
MPNowPlayingInfoCenter *infoCenter = [MPNowPlayingInfoCenter defaultCenter];
    MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithImage:[UIImage imageNamed:@"封面图片"]];
    infoCenter.nowPlayingInfo = @{
                                  MPMediaItemPropertyTitle :@"歌曲名",
                                  MPMediaItemPropertyArtist :@"歌手名",
                                  MPMediaItemPropertyPlaybackDuration :歌曲时间长度,
                                  MPNowPlayingInfoPropertyElapsedPlaybackTime : @(已播放时间长度),
                                  MPMediaItemPropertyArtwork : artwork
                                  };

通过耳机、锁屏界面控制

  • 在需要处理远程控制事件的具体控制器或其它类中调用下面这个方法
 #import <MediaPlayer/MPRemoteCommandCenter.h>
 #import <MediaPlayer/MPRemoteCommand.h>
- (void)remoteControlEventHandler
{
    // 直接使用sharedCommandCenter来获取MPRemoteCommandCenter的shared实例
    MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];

// 启用播放命令 (锁屏界面和上拉快捷功能菜单处的播放按钮触发的命令)
commandCenter.playCommand.enabled = YES;
// 为播放命令添加响应事件, 在点击后触发
[commandCenter.playCommand addTarget:self action:@selector(playAction:)];

// 播放, 暂停, 上下曲的命令默认都是启用状态, 即enabled默认为YES
// 为暂停, 上一曲, 下一曲分别添加对应的响应事件
[commandCenter.pauseCommand addTarget:self action:@selector(pauseAction:)];
[commandCenter.previousTrackCommand addTarget:self action:@selector(previousTrackAction:)];
[commandCenter.nextTrackCommand addTarget:self action:@selector(nextTrackAction:)];

// 启用耳机的播放/暂停命令 (耳机上的播放按钮触发的命令)
commandCenter.togglePlayPauseCommand.enabled = YES;
// 为耳机的按钮操作添加相关的响应事件
[commandCenter.togglePlayPauseCommand addTarget:self action:@selector(playOrPauseAction:)];
}

-(void)playAction:(id)obj{
    [[HYPlayerTool sharePlayerTool] play];
}
-(void)pauseAction:(id)obj{
    [[HYPlayerTool sharePlayerTool] pause];
}
-(void)nextTrackAction:(id)obj{
    [[HYPlayerTool sharePlayerTool] playNext];
}
-(void)previousTrackAction:(id)obj{
    [[HYPlayerTool sharePlayerTool] playPre];
}
-(void)playOrPauseAction:(id)obj{
    if ([[HYPlayerTool sharePlayerTool] isPlaying]) {
        [[HYPlayerTool sharePlayerTool] pause];
    }else{
        [[HYPlayerTool sharePlayerTool] play];
    }
}

AVQueuePlayer

AVPlayer只支持单个媒体资源的播放,我们可以使用AVPlayer的子类AVQueuePlayer实现列表播放。在AVPlayer的基础上,增加以下方法:

//通过给定的AVPlayerItem数组创建一个AVQueuePlayer实例
+ (instancetype)queuePlayerWithItems:(NSArray<AVPlayerItem *> *)items;

//通过给定的AVPlayerItem数组初始化AVQueuePlayer实例
- (AVQueuePlayer *)initWithItems:(NSArray<AVPlayerItem *> *)items;

//获得当前的播放队列数组
- (NSArray<AVPlayerItem *> *)items;

//停止播放当前音乐,并播放队列中的下一首
- (void)advanceToNextItem;

//往播放队列中插入新的AVPlayerItem
- (void)insertItem:(AVPlayerItem *)item afterItem:(nullable AVPlayerItem *)afterItem;

//从播放队列中移除指定AVPlayerItem
- (void)removeItem:(AVPlayerItem *)item;

//清空播放队列
- (void)removeAllItems;
  • 官方API中没找到播放上一首的方法,所以其实直接用AVPlayer做列表播放也是可以的,自己维护一个播放列表数组,监听用户点击上一首和下一首按钮,并监听播放结束事件,调用 AVPlayer 实例的replaceCurrentItemWithPlayerItem:方法传入播放列表中的上一首或下一首就行。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容