当你在iPhone上点开一首歌曲,音频在内置扬声器中播放出来,此时有电话拨入,音乐会立即停止并处于暂停状态,此时听到的是手机呼叫的铃音。如果此时你挂掉电话,刚才的音乐声再次响起,当你插上耳机,音乐播放时音频输出到了耳机里。当听完这首音乐摘下耳机后,你会发现声音自动转回内置扬声器并处于暂停状态。
iOS系统提供了一个可管理的音频环境(managed audio environment),可以带给所有iOS用户非常好的用户体验,这一过程具体是如何实现的呢?这里会用到音频会话(audio session)。
音频会话在应用程序和操作系统之间扮演着中间人的角色。它提供了一种简单实用的方法使OS得知应用程序应该如何与iOS音频环境进行交互。你不需要了解与音频硬件交互的细节,只需要对应用程序的行为进行语义上的描述即可。这一点使得你可以指明应用程序的一般音频行为,并可以把对该行为的管理委托给音频会话,这样OS系统就可以对用户使用音频的体验进行适当的管理。
所有iOS应用程序都具有音频会话,无论其是否使用。默认音频会话来自于以下一些预配置:
默认音频会话提供了许多实用功能,但是在大多数情况下,你需要自定义音频会话来适配你自己应用程序的需求。
音频会话在应用程序的生命周期中是可以修改的,但通常我们只对其配置一次,就是在应用程序启动时。那么,配置应用程序的最佳位置就是- (BOOL)application:didFinishLaunchingWithOptions:
方法。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 回去音频会话单例 AVAudioSession *session = [AVAudioSession sharedInstance]; // 设置音频会话分类 if (![session setCategory:AVAudioSessionCategoryPlayback error:nil]) { NSLog(@"设置音频会话失败"); } // 激活音频会话 if (![session setActive:YES error:nil]) { NSLog(@"激活音频会话失败"); } return YES; } 复制代码
音频播放时很多应用程序的常见需求,AV Foundation让这一功能的实现变得非常简单,这一点要归功于一个名为AVAudioPlayer的类。该类的实例提供了一种简单地从文本或内存中播放音频的方法。
AVAudioPlayer构建与Core Audio中的C-based Audio Queue Services的最顶层。所以它可以提供所有你在Audio Queue Services中所能找到的核心功能,比如播放、循环甚至音频计量,但使用的是Objective-C接口。除非你需要从网络中播放音频,需要访问原始音频样本或需要非常低的延时,否则AVAudioPlayer都能胜任。
有两种方法可以创建一个AVAudioPlayer,使用包含播放音频的内存版本的NSData或本地音频文件的NSURL。
@interface ViewController () @property (nonatomic, strong) AVAudioPlayer *player; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 从bundle中获取资源的NSURL实例 NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"tqsh.mp3" withExtension:nil]; // 根据URL创建一个AVAudioPlayer实例 self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:nil]; if (self.player) { // 建议开发者,先调用这个方法 // 调用此方法将预加载缓冲区并获取音频硬件,这样做可以将调用play方法和听到输出声音之间的延时降低到最小 [self.player prepareToPlay]; } } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self.player play]; } 复制代码
播放实例包含了所有开发者期望的对播放行为进行控制的方法。调用play方法可以实现立即播放音频的功能,pause方法可以对播放暂停,stop方法可以停止播放行为。pause方法和stop方法在应用程序外面看来实现的功能都是停止当前的播放行为。下一时间我们调用play方法,通过pause和stop方法停止的音频都会继续播放。这两者最主要的区别在于调用stop方法会撤销调用prepareToPlay时所做的设置,而调用pause方法不会。
除了前面描述的标准常规方法之外,开发者还可以使用其他一些方法,如下:
需求:同步播放三个播放器,通过控制每个播放器的音量等级和立体声方面的pan值将这些音乐混合,进而控制整体播放速率。
@interface AVAudioPlayerManager : NSObject @property (nonatomic, assign, readonly, getter=isPlaying) BOOL playing; - (void)play; - (void)stop; - (void)adjustRate:(CGFloat)rate; - (void)adjustPan:(CGFloat)pan forPlayerAtIndex:(NSInteger)index; - (void)adjustVolume:(CGFloat)volume forPlayerAtIndex:(NSInteger)index; @end 复制代码
@interface AVAudioPlayerManager () @property (nonatomic, assign) BOOL playing; @property (nonatomic, strong) NSArray *players; @end @implementation AVAudioPlayerManager - (instancetype)init { if (self = [super init]) { AVAudioPlayer *guitarPlayer = [self createPlayerWithFileName:@"guitar"]; AVAudioPlayer *bassPlayer = [self createPlayerWithFileName:@"bass"]; AVAudioPlayer *drumsPlayer = [self createPlayerWithFileName:@"drums"]; _players = @[guitarPlayer, bassPlayer, drumsPlayer]; } return self; } - (AVAudioPlayer *)createPlayerWithFileName:(NSString *)fileName { NSURL *fileURL = [[NSBundle mainBundle] URLForResource:fileName withExtension:@"caf"]; AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:nil]; if (player) { player.enableRate = YES; player.numberOfLoops = -1; [player prepareToPlay]; } else { NSLog(@"创建player失败"); } return player; } - (void)play { if (!self.isPlaying) { NSTimeInterval delayTime = [self.players[0] deviceCurrentTime] + 0.01; for (AVAudioPlayer *player in self.players) { [player playAtTime:delayTime]; } self.playing = YES; } } - (void)stop { if (self.isPlaying) { for (AVAudioPlayer *player in self.players) { [player stop]; player.currentTime = 0.0; } self.playing = NO; } } - (void)adjustPan:(CGFloat)pan forPlayerAtIndex:(NSInteger)index { if ([self isValidIndex:index]) { AVAudioPlayer *player = self.players[index]; player.pan = pan; } } - (void)adjustVolume:(CGFloat)volume forPlayerAtIndex:(NSInteger)index { if ([self isValidIndex:index]) { AVAudioPlayer *player = self.players[index]; player.volume = volume; } } - (void)adjustRate:(CGFloat)rate { for (AVAudioPlayer *player in self.players) { player.rate = rate; } } - (BOOL)isValidIndex:(NSInteger)index { return index == 0 || index < self.players.count; } 复制代码
在上面这个例子中,我们没有配置音频会话,所以我们使用的系统默认的音频会话的配置。
以上两个操作并不是我们希望的,我们希望切换响铃/静音开关继续播放音频并且锁屏后继续播放音频,所以我们要设置音频会话。
- (BOOL)application:didFinishLaunchingWithOptions:
对音频会话进行配置,因为我们的主要功能就是播放所以设置AVAudioSessionCategoryPlayback分类。- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { AVAudioSession *audioSession = [AVAudioSession sharedInstance]; if (![audioSession setCategory:AVAudioSessionCategoryPlayback error:nil]) { NSLog(@"设置音频会话分类失败"); } if (![audioSession setActive:YES error:nil]) { NSLog(@"音频会话激活失败"); } return YES; } 复制代码
<key>UIBackgroundModes</key> <array> <string>audio</string> </array> 复制代码
中断在iOS设备中经常出现,在使用设备的过程中经常会有诸如电话呼入、闹铃响起等情况。虽然iOS系统本身可以很好地处理这些事件。不过我们仍需要针对这些情况做自己的处理。
按照上述的场景进行测试,你会发现,当中断发生时,播放中的音频会慢慢消失和暂停。这个效果是自动实现的,我们没有做任何的处理。当另一台手机的电话被挂断,会出现一些问题,播放/停止功能消失,音频也不再继续播放。
- (instancetype)init { if (self = [super init]) { AVAudioPlayer *guitarPlayer = [self createPlayerWithFileName:@"guitar"]; AVAudioPlayer *bassPlayer = [self createPlayerWithFileName:@"bass"]; AVAudioPlayer *drumsPlayer = [self createPlayerWithFileName:@"drums"]; _players = @[guitarPlayer, bassPlayer, drumsPlayer]; // 注册音频会话中断通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterruption:) name:AVAudioSessionInterruptionNotification object:nil]; } return self; } 复制代码
- (void)handleInterruption:(NSNotification *)notification { NSDictionary *info = notification.userInfo; NSLog(@"%@", info); // 获取音频会话打断类型 AVAudioSessionInterruptionType type = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue]; if (type == AVAudioSessionInterruptionTypeBegan) { NSLog(@"开始打断"); [self stop]; // 中断停止 交给代理处理相关逻辑 if (self.delegate && [self.delegate respondsToSelector:@selector(audioPlayerManagerPlaybackStopped:)]) { [self.delegate audioPlayerManagerPlaybackStopped:self]; } } else { NSLog(@"结束打断"); AVAudioSessionInterruptionOptions options = [info[AVAudioSessionInterruptionOptionKey] unsignedIntegerValue]; if (options == AVAudioSessionInterruptionOptionShouldResume) { // 音频会话重新激活 [self play]; // 重新激活 交给代理 处理相关逻辑 if (self.delegate && [self.delegate respondsToSelector:@selector(audioPlayerManagerPlaybackBegan:)]) { [self.delegate audioPlayerManagerPlaybackBegan:self]; } } } } 复制代码
@protocol AVAudioPlayerManagerDelegate <NSObject> @optional /// 中断 -> 停止播放 - (void)audioPlayerManagerPlaybackStopped:(AVAudioPlayerManager *)manager; /// 结束中断安 -> 开始播放 - (void)audioPlayerManagerPlaybackBegan:(AVAudioPlayerManager *)manager; @end 复制代码
- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } 复制代码
在iOS设备上添加或移除音频输入、输出线路时,会发生线路改变。比如用户插入和拔出耳机。当这些事件发生时,音频会根据情况改变输入或输出线路,同时AVAudioSession会广播一个描述该变化的通知给所有相关的监听器。
对我们的例子进行一个测试,开始播放,并在播放期间插入耳机。音频的输出线路变为耳机并继续正常播放,这是我们所期望的结果。保持音频的播放状态,断开耳机的连接。音频线路再次回到设备的内置扬声器,我们再次听到了声音。虽然线路变化同预期一样,但是有一个问题,用户插上耳机可能是为了保持隐私性,耳机断开连接有可能需要继续保密,所以我们需要耳机断开连接时候,音乐要停止播放。
当线路发生变化时要有通知,我们需要注册AVAudioSession发送的通知,在init方法中。该通知为AVAdudioSessionRouteChangeNotification。
- (instancetype)init { if (self = [super init]) { AVAudioPlayer *guitarPlayer = [self createPlayerWithFileName:@"guitar"]; AVAudioPlayer *bassPlayer = [self createPlayerWithFileName:@"bass"]; AVAudioPlayer *drumsPlayer = [self createPlayerWithFileName:@"drums"]; _players = @[guitarPlayer, bassPlayer, drumsPlayer]; // 注册音频会话中断通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterruption:) name:AVAudioSessionInterruptionNotification object:nil]; // 注册线路变化通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleRouteChange:) name:AVAudioSessionRouteChangeNotification object:nil]; } return self; } 复制代码
- (void)handleRouteChange:(NSNotification *)notification { NSDictionary *userInfo = notification.userInfo; AVAudioSessionRouteChangeReason reason = [userInfo[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue]; if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { // 线路回到手机端 AVAudioSessionRouteDescription *route = userInfo[AVAudioSessionRouteChangePreviousRouteKey]; AVAudioSessionPortDescription *output = route.outputs.firstObject; AVAudioSessionPort portType = output.portType; // 耳机 或 蓝牙音频设备 if ([portType isEqualToString:AVAudioSessionPortHeadphones] || [portType isEqualToString:AVAudioSessionPortBluetoothA2DP]) { [self stop]; if (self.delegate && [self.delegate respondsToSelector:@selector(audioPlayerManagerPlaybackStopped:)]) { [self.delegate audioPlayerManagerPlaybackStopped:self]; } } } } 复制代码
现在,当我们断开耳机,音频播放也会停止。以上就是使用AVAudioPlayer完成的一个简单地播放器功能。实际开发中,我们只要注意处理我们真正遇到的场景就可以了。