AVAudioSession

音频会话(AVAudioSession) 用来管理多个 APP 对音频硬件设备资源(麦克风,扬声器)的使用。
配置音频会话
// 设置后台播放功能,并调用 setActive 将会话激活才能起作用
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
[session setActive:YES error:nil];
音频会话类型
AVAudioSessionCategoryAmbient混间播放,应用会随着静音键和屏幕关闭而静音。并且不会中止其它应用播放声音,可以和其它自带应用如 iPod,safari 等同时播放声音。注意:该 Category 无法在后台播放声音。AVAudioSessionCategorySoloAmbient独占播放,应用会随着静音键和屏幕关闭而静音。不可以与其他声音混合播放,会中断其他声音。AVAudioSessionCategoryPlayback后台独占播放,当手机设置为静音或进入后台时会继续播放,如果让使声音在后台继续播放时,必须在 plist 文件里面添加 UIBackgroundModes 属性。默认情况下,使用这一类别意味着你的应用程序的音频是不可混合激活的,你的会话将中断任何其他非混合的音频会话。AVAudioSessionCategoryRecord录音模式,用于需要录音的应用,设置该 Category 后,除了来电铃声,闹钟或日历提醒之外的其它系统声音都不会被播放。该 Category 只提供单纯录音功能。如果让使声音在后台继续播放时,必须在 plist 文件里面添加 UIBackgroundModes 属性。AVAudioSessionCategoryPlayAndRecord播放和录音模式,允许播放时同时录制音频,比如 VoIP (Voice over Internet Protocol) 应用,通话时既播放音频也录制音频。除此之外同 AVAudioSessionCategoryPlayback 类似。支持 AirPlay 。但是如果设置 AVAudioSessionModeVoiceChat 属性, AirPlay 是不可用的。AVAudioSessionCategoryMultiRoute多线路模式,支持音频播放和录制。允许多条音频流的同步输入和输出。比如:USB 和耳机同时音频输出。
音频会话模式
| 模式 | 兼容的 Category | 场景 |
|---|---|---|
| AVAudioSessionModeDefault | All | 默认模式 |
| AVAudioSessionModeVoiceChat | AVAudioSessionCategoryPlayAndRecord | VoIP |
| AVAudioSessionModeVideoChat | AVAudioSessionCategoryPlayAndRecord | 视频通话 |
| AVAudioSessionModeGameChat | AVAudioSessionCategoryPlayAndRecord | 游戏录制,GKVoiceChat自动设置 |
| AVAudioSessionModeVideoRecording | AVAudioSessionCategoryPlayAndRecord AVAudioSessionCategoryRecord | 录制视频 |
| AVAudioSessionModeMoviePlayback | AVAudioSessionCategoryPlayback | 视频播放 |
| AVAudioSessionModeMeasurement | AVAudioSessionCategoryPlayAndRecord AVAudioSessionCategoryRecord AVAudioSessionCategoryPlayback | 最小系统 |
下面逐一介绍下每个Mode:
AVAudioSessionModeDefault,默认模式,与所有的 Category 兼容AVAudioSessionModeVoiceChat,适用于 VoIP 类型的应用。只能是AVAudioSessionCategoryPlayAndRecordCategory下。在这个模式系统会自动配置AVAudioSessionCategoryOptionAllowBluetooth这个选项。系统会自动选择最佳的内置麦克风组合支持语音聊天。AVAudioSessionModeVideoChat,用于视频聊天类型应用,只能是AVAudioSessionCategoryPlayAndRecordCategory下。适在这个模式系统会自动配置AVAudioSessionCategoryOptionAllowBluetooth和AVAudioSessionCategoryOptionDefaultToSpeaker选项。系统会自动选择最佳的内置麦克风组合支持视频聊天。- AVAudioSessionModeGameChat,适用于游戏类应用。使用
GKVoiceChat对象的应用会自动设置这个模式和AVAudioSessionCategoryPlayAndRecordCategory。实际参数和AVAudioSessionModeVideoChat一致。 AVAudioSessionModeVideoRecording,适用于使用摄像头采集视频的应用。只能是AVAudioSessionCategoryPlayAndRecord和AVAudioSessionCategoryRecord这两个 Category下。这个模式搭配AVCaptureSession API结合来用可以更好地控制音视频的输入输出路径。(例如,设置automaticallyConfiguresApplicationAudioSession属性,系统会自动选择最佳输出路径。AVAudioSessionModeMoviePlayback,适用于播放视频的应用。只用于AVAudioSessionCategoryPlayback这个Category。AVAudioSessionModeMeasurement,最小化系统。只用于AVAudioSessionCategoryPlayAndRecord、AVAudioSessionCategoryRecord、AVAudioSessionCategoryPlayback这几种 Category。
音频会话选项
我们还可以使用options去微调Category行为,如下表:
| Option | Option功能说明 | 兼容的 Category |
|---|---|---|
| AVAudioSessionCategoryOptionMixWithOthers | 支持和其他APP音频 mix | AVAudioSessionCategoryPlayAndRecord AVAudioSessionCategoryPlayback AVAudioSessionCategoryMultiRoute |
| AVAudioSessionCategoryOptionDuckOthers | 系统智能调低其他APP音频音量 | AVAudioSessionCategoryPlayAndRecord AVAudioSessionCategoryPlayback AVAudioSessionCategoryMultiRoute |
| AVAudioSessionCategoryOptionAllowBluetooth | 支持蓝牙音频输入 | AVAudioSessionCategoryRecord AVAudioSessionCategoryPlayAndRecord |
| AVAudioSessionCategoryOptionDefaultToSpeaker | 设置默认输出音频到扬声器 | AVAudioSessionCategoryPlayAndRecord |
调优我们的 Category
通过 Category 和合适的 Mode 和 Options 的搭配我们可以调优出我们的效果,下面举两个应用场景:用过高德地图的都知道,在后台播放 QQ 音乐的时候,如果导航语音出来,QQ 音乐不会停止,而是被智能压低和混音,等导航语音播报完后,QQ 音乐正常播放,这里我们需要后台播放音乐,所以 Category 使用AVAudioSessionCategoryPlayback,需要混音和智能压低其他 APP 音量,所以 Options 选用 AVAudioSessionCategoryOptionMixWithOthers 和 AVAudioSessionCategoryOptionDuckOthers。
后台播放
选择 Target 的 Capabilities,在 Background Modes 中选中 Audio, AirPlay, and Picture in Picture。或在 info.plist里面配置:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
音频中断处理
其他 App 或者电话会中断我们的 App 音频,所以相应的我们要做出处理。可以通过监听 AVAudioSessionInterruptionNotification 这个 key 获取音频中断事件。
回调回来 Userinfo 有键值:
AVAudioSessionInterruptionTypeKey取值AVAudioSessionInterruptionTypeBegan表示中断开始 取值AVAudioSessionInterruptionTypeEnded表示中断结束 中断开始:我们需要做的是保存好播放状态,上下文,更新用户界面等 中断结束:我们要做的是恢复好状态和上下文,更新用户界面,根据需求准备好之后选择是否激活 session。
选择不同的音频播放技术,处理中断方式也有差别,具体如下:
System Sound Services:使用System Sound Services播放音频,系统会自动处理,不受 App 控制,当中断发生时,音频播放会静音,当中断结束后,音频播放会恢复。AV Foundation framework:AVAudioPlayer类和AVAudioRecorder类提供了中断开始和结束的delegate回调方法来处理中断。中断发生,系统会自动停止播放,需要做的是记录播放时间等状态,更新用户界面,等中断结束后,再次调用播放方法,系统会自动激活 session。Audio Queue Services,I/O audio unit:使用aduio unit这些技术需要处理中断,需要做的是记录播放或者录制的位置,中断结束后自己恢复 session。OpenAL:使用OpenAL播放时,同样需要自己监听中断。管理OpenAL上下文,用户中断结束后恢复 session。
AVAudioPlayer
AVAudioPlayer 属于 AVFundation.framework 的类,是一个提供音频播放相关功能的播放器类。除非你要从网络流中播放音频、需要访问原始音频样本或者需要非常低的时延,否则AVAudioPlayer都能胜任。
AVAudioPlayer 构建于 Core Audio 中的 C-based Audio Queue Services 的最顶层。所以他可以提供所有在 Audio Queue Services 中所能找到的核心功能,比如播放、循环、甚至音频计量,但是使用的是非常友好的 OC 接口。
AVAudioPlayer 每次播放都需要将上一个 player 释放掉,然后重新创建 player 来进行播放。
AVAudioPlayer 支持以下音频格式:
ACCAMR(Adaptive multi-Rate,一种语音格式)ALAC(Apple lossless Audio Codec)iLBC(internet Low Bitrate Codec,一种语音格式)IMA4(IMA/ADPCM)linearPCM(uncompressed)u-law和a-lawMP3(MPEG-Laudio Layer 3)
AVAudioPlayer 主要属性:
playing // 播放器是否正在播放 获取通过isPlaying
numberOfChannels // 该音频的声道次数,只读
duration // 该音频播放时长
url // 音频文件路径, 只读
data // 音频数据,只读
pan NS_AVAILABLE(10_7, 4_0) // 立体声设置 设为 -1.0 则左边播放,设为 0.0 则中央播放,设为 1.0 则右边播放
enableRate NS_AVAILABLE(10_8, 5_0); // 是否允许改变播放速率
rate NS_AVAILABLE(10_8, 5_0); // 播放速率 0.5 (半速播放) ~ 2.0(倍速播放) 注1.0 是正常速度
currentTime // 该音频的播放点
deviceCurrentTime // 输出设备播放音频的时间,注意如果播放中被暂停此时间也会继续累加
numberOfLoops // 循环次数,如果要单曲循环,设置为负数
volume // 播放器的音频增益,值:0.0~1.0
channelAssignments // 获得或设置播放声道
AVAudioPlayer 初始化:
// 利用文件初始化
- (instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError * _Nullable *)outError;
// 利用音频数据来初始化
- (instancetype)initWithData:(NSData *)data error:(NSError * _Nullable *)outError;
// 利用文件初始化
- (instancetype)initWithContentsOfURL:(NSURL *)url fileTypeHint:(NSString *)utiString error:(NSError * _Nullable *)outError;
// 利用音频数据来初始化
- (instancetype)initWithData:(NSData *)data fileTypeHint:(NSString *)utiString error:(NSError * _Nullable *)outError;
AVAudioPlayer 主要方法:
// 准备播放,取得音频文件并预加载 Audio Queue 缓存区,降低调用play方法和听到声音输出之间的延时,在使用play方法时如果没有做好准备会自动调用该方法
- (BOOL)prepareToPlay;
// 异步播放
- (BOOL)play;
// 在某个时间点播放
- (BOOL)playAtTime:(NSTimeInterval)time NS_AVAILABLE(10_7, 4_0);
// 暂停播放,但仍然可以播放
- (void)pause;
// 停止播放,不再准备播放了
- (void)stop;
// 更新音频测量值,注意如果要更新音频测量值必须设置meteringEnabled为YES,通过音频测量值可以即时获得音频分贝等信息
- (void)updateMeters;
// 返回给定通道的分贝峰值功率
- (float)peakPowerForChannel:(NSUInteger)channelNumber;
// 获得指定声道的分贝峰值,注意如果要获得分贝峰值必须在此之前调用updateMeters方法
- (float)averagePowerForChannel:(NSUInteger)channelNumber;
AVAudioPlayer 代理方法:
// 音频播放完成
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag;
// 音频解码发生错误
- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error;
AVPlayer
AVPlayer 支持播放本地、分步下载、或在线流媒体音视频,不仅可以播放音频,配合 AVPlayerLayer 类可实现视频播放。另外支持播放进度监听。
AVPlayer 只支持单个媒体资源播放,需要配合 AVPlayerItem 关联来进行媒体播放。
AVPlayer 初始化:
// 类方法,从url加载音频
+ (instancetype)playerWithURL:(NSURL *)URL
// 类方法,配合AVPlayerItem使用
+ (instancetype)playerWithPlayerItem:(nullable AVPlayerItem *)item
// 实例方法,从url加载音频
- (instancetype)initWithURL:(NSURL *)URL
// 实例方法,配合AVPlayerItem使用
- (instancetype)initWithPlayerItem:(nullable AVPlayerItem *)item
AVPlayer 添加监听:
【播放状态】
// 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;
}
}
}
【缓冲状态】
// KVO监听音乐缓冲状态
[self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
// 处理KVO回调事件
- (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;
NSLog(@"---%f,---%f,---%f",totalLoadTime,duration,scale);
//更新缓冲进度条
// self.loadProgress.progress = scale;
}
}
【播放结束事件的监听】
//播放结束事件的监听
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playFinied:) name:AVPlayerItemDidPlayToEndTimeNotification
object:self.player.currentItem];
//播放结束回调
- (void)playFinied:(AVPlayerItem *)item {
NSLog(@"播放结束");
self.slider.value = 0;
}
【监听播放进度】
// 开始播放时,通过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.slider.value = progress;
}
}];
【拖动进度条改变播放进度】
// 拖动进度条改变播放进度
- (void)playValueChange:(UISlider *)sender {
// 计算时间
float time = sender.value * CMTimeGetSeconds(self.player.currentItem.duration);
// 跳到当前指定时间
[self.player seekToTime:CMTimeMake(time, 1)];
}
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
- (BOOL)canInsertItem:(AVPlayerItem *)item afterItem:(nullable AVPlayerItem *)afterItem;
// 往播放队列中插入新的 AVPlayerItem
- (void)insertItem:(AVPlayerItem *)item afterItem:(nullable AVPlayerItem *)afterItem;
// 移除指定的 AVPlayerItem
- (void)removeItem:(AVPlayerItem *)item;
// 移除所有播放队列中的AVPlayerItem
- (void)removeAllItems;
官方 API 中没找到播放上一首的方法,所以其实直接用 AVPlayer 做列表播放也是可以的,自己维护一个播放列表数组,监听用户点击上一首和下一首按钮,并监听播放结束事件,调用 AVPlayer 实例的 replaceCurrentItemWithPlayerItem: 方法传入播放列表中的上一首或下一首就行。
锁屏信息
- 在播放控制界面接受远程控制
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:YES];
// 开始接受远程控制
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
[self resignFirstResponder];
}
- (void)viewWillDisappear:(BOOL)animated {
// 接触远程控制
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
[self becomeFirstResponder];
}
// 重写父类成为响应者方法
- (BOOL)canBecomeFirstResponder {
return YES;
}
- 对远程控制事件做出相应的操作
//重写父类方法,接受外部事件的处理
- (void) remoteControlReceivedWithEvent: (UIEvent *) receivedEvent {
NSLog(@"remote");
if (receivedEvent.type == UIEventTypeRemoteControl) {
switch (receivedEvent.subtype) { // 得到事件类型
case UIEventSubtypeRemoteControlTogglePlayPause: // 暂停 ios6
[self.player pause]; // 调用你所在项目的暂停按钮的响应方法 下面的也是如此
break;
case UIEventSubtypeRemoteControlPreviousTrack: // 上一首
[self lastMusic:nil];
break;
case UIEventSubtypeRemoteControlNextTrack: // 下一首
[self nextMusic:nil];
break;
case UIEventSubtypeRemoteControlPlay: //播放
[self playMusic:nil];
break;
case UIEventSubtypeRemoteControlPause: // 暂停 ios7
[self playMusic:nil];
break;
default:
break;
}
}
}
- 设置锁屏主题
//锁屏信息
NSMutableDictionary *songInfo = [ [NSMutableDictionary alloc] init];
//锁屏图片
UIImage *img = [UIImage imageNamed:@"iPhoneX"];
if (img) {
MPMediaItemArtwork *albumArt = [[MPMediaItemArtwork alloc]initWithImage:img];
[songInfo setObject: albumArt forKey:MPMediaItemPropertyArtwork ];
}
//锁屏标题
NSString *title = @"music";
[songInfo setObject:[NSNumber numberWithFloat:100] forKey:MPMediaItemPropertyPlaybackDuration];
[songInfo setObject:title forKey:MPMediaItemPropertyTitle];
[songInfo setObject:title forKey:MPMediaItemPropertyAlbumTitle];
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:songInfo ];
通过耳机、锁屏界面控制
// 直接使用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:)];
如何做输出改变监听(拔出耳机音乐暂停播放)
iOS 6.0 后还可以监听输出改变通知。通俗来说,就是拔出耳机,音乐播放暂停。
// 添加观察者
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChange:) name:AVAudioSessionRouteChangeNotification object:nil];
// 通知方法
- (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"]) {
// 这边必须回调到主线程
dispatch_async(dispatch_get_main_queue(), ^{
self.playOrPause.selected = NO;
});
[self pauseMusic];
}
}
}