AV Foundation使用AVAudioPlayer播放音频
2020/4/24 23:22:54
本文主要是介绍AV Foundation使用AVAudioPlayer播放音频,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
2.1 iOS的音频环境
当你在iPhone上点开一首歌曲,音频在内置扬声器中播放出来,此时有电话拨入,音乐会立即停止并处于暂停状态,此时听到的是手机呼叫的铃音。如果此时你挂掉电话,刚才的音乐声再次响起,当你插上耳机,音乐播放时音频输出到了耳机里。当听完这首音乐摘下耳机后,你会发现声音自动转回内置扬声器并处于暂停状态。
iOS系统提供了一个可管理的音频环境(managed audio environment),可以带给所有iOS用户非常好的用户体验,这一过程具体是如何实现的呢?这里会用到音频会话(audio session)。
2.2 理解音频会话
音频会话在应用程序和操作系统之间扮演着中间人的角色。它提供了一种简单实用的方法使OS得知应用程序应该如何与iOS音频环境进行交互。你不需要了解与音频硬件交互的细节,只需要对应用程序的行为进行语义上的描述即可。这一点使得你可以指明应用程序的一般音频行为,并可以把对该行为的管理委托给音频会话,这样OS系统就可以对用户使用音频的体验进行适当的管理。
所有iOS应用程序都具有音频会话,无论其是否使用。默认音频会话来自于以下一些预配置:
- 支持音频播放,不支持音频录制
- 在iOS中,当用户切换响铃/静音开关到“静音”模式时,应用程序正在被播放的所有音频都会消失
- 在iOS中,当设备锁屏时,应用程序的音频将处于静音状态
- 当应用程序播放音频时,所有其他后台播放音频,例如音乐的应用程序都会被静音。
默认音频会话提供了许多实用功能,但是在大多数情况下,你需要自定义音频会话来适配你自己应用程序的需求。
2.2.1 音频会话的分类
- AVAudioSessionCategoryAmbient:支持混音,锁屏和响铃/静音开关会使音频静音,只允许输出(播放)音频。
- AVAudioSessionCategorySoloAmbient:默认设置,不支持混音,锁屏和响铃/静音开关会使音频静音,只允许输出(播放)音频。
- AVAudioSessionCategoryPlayback:默认不支持混音,如果想要支持混音,可以使用AVAudioSessionCategoryOptionMixWithOthers这个option。锁屏和响铃/静音开关不会使音频静音,为了支持应用程序转到后台可以继续在后台播放音频,可以在info.plist文件中添加UIBackgroundModes的key和audio的值。
- AVAudioSessionCategoryRecord:只要该会话处于激活状态,会使系统中所有输出静音。为了支持应用程序转到后台可以继续在后台录制音频,需要在info.plist文件中添加UIBackgroundModes的key和audio的值。并且用户必须允许,才可以进行录制。
- AVAudioSessionCategoryPlayAndRecord:这个分类可以同时用来播放和录制音频。锁屏和响铃/静音开关不会使音频静音,要在应用程序转到后台可以继续播放音频需要在info.plist文件中添加UIBackgroundModes的key和audio的值。该分类支持同时进行音频的录制和播放,同时也支持音频录制和播放不同时进行。默认该分类不支持混音的,为了支持混音,可以使用AVAudioSessionCategoryOptionMixWithOthers这个option。并且用户必须允许才可以进行录制。
- AVAudioSessionCategoryMultiRoute:该分类用于同时将不同的音频数据流路由到不同的输出设备,可以输入输出还可以支持同时输入和输出。使用此分类,需要更专业的知识的支持。
2.2.2 配置音频会话
音频会话在应用程序的生命周期中是可以修改的,但通常我们只对其配置一次,就是在应用程序启动时。那么,配置应用程序的最佳位置就是- (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; } 复制代码
2.3 使用 AVAudioPlayer播放音频
音频播放时很多应用程序的常见需求,AV Foundation让这一功能的实现变得非常简单,这一点要归功于一个名为AVAudioPlayer的类。该类的实例提供了一种简单地从文本或内存中播放音频的方法。
AVAudioPlayer构建与Core Audio中的C-based Audio Queue Services的最顶层。所以它可以提供所有你在Audio Queue Services中所能找到的核心功能,比如播放、循环甚至音频计量,但使用的是Objective-C接口。除非你需要从网络中播放音频,需要访问原始音频样本或需要非常低的延时,否则AVAudioPlayer都能胜任。
2.3.1 创建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]; } 复制代码
2.3.2 对播放进行控制
播放实例包含了所有开发者期望的对播放行为进行控制的方法。调用play方法可以实现立即播放音频的功能,pause方法可以对播放暂停,stop方法可以停止播放行为。pause方法和stop方法在应用程序外面看来实现的功能都是停止当前的播放行为。下一时间我们调用play方法,通过pause和stop方法停止的音频都会继续播放。这两者最主要的区别在于调用stop方法会撤销调用prepareToPlay时所做的设置,而调用pause方法不会。
除了前面描述的标准常规方法之外,开发者还可以使用其他一些方法,如下:
- 修改播放器的音量: 播放器的音量独立于系统的音量,音量或播放增益定义为0.0(静音)到1.0(最大音量)之间的浮点值。
- 修改播放器的pan值: 允许使用立体声播放声音:播放器的pan值由一个浮点数表示。范围从-1.0(极左)到1.0(极右)默认值为0.0(居中)。
- 调整播放率: 允许用户在不改变音调的情况下调整播放率,范围从0.5(半速)到2.0(2倍速)
- 通过设置 numberOfLoops属性实现音频无缝循环: 给这个属性设置一个大于0的数,可以实现播放器n次循环播放。如果属性赋值为-1会导致播放器无限循环。
- 进行音频计量: 当播放器发生时从播放器读取音量力度的平均值和峰值。
2.4 AVAudioPlayer演练
需求:同步播放三个播放器,通过控制每个播放器的音量等级和立体声方面的pan值将这些音乐混合,进而控制整体播放速率。
- AVAudioPlayerManager.h
@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 复制代码
- AVAudioPlayerManager.m
@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; } 复制代码
2.5 配置音频会话
在上面这个例子中,我们没有配置音频会话,所以我们使用的系统默认的音频会话的配置。
- 操作一,切换设备的响铃/静音开关,在静音状态下,音频输出静音,在响铃状态音频正常输出。
- 操作二,锁屏操作,音频输出停止,解锁屏幕,音频继续播放
以上两个操作并不是我们希望的,我们希望切换响铃/静音开关继续播放音频并且锁屏后继续播放音频,所以我们要设置音频会话。
- 在
- (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; } 复制代码
- 当音频会话设置完成后,再次运行程序,切换响铃/锁屏按钮,播放的声音不会消失了。但是锁屏后,声音仍然消失。
- 设置AVAudioSessionCategoryPlayback可以让音频在后台进行输出,但是前提是我们需要设置info.plist,让设备支持后台播放的功能
<key>UIBackgroundModes</key> <array> <string>audio</string> </array> 复制代码
- 添加该配置后,音频输出就可以在后台完成了,锁屏按钮也不会使其停止。
2.6 处理中断事件
中断在iOS设备中经常出现,在使用设备的过程中经常会有诸如电话呼入、闹铃响起等情况。虽然iOS系统本身可以很好地处理这些事件。不过我们仍需要针对这些情况做自己的处理。
- 在设备上运行应用程序并播放音频
- 当音频处于播放状态时,从另外一台设备发起电话呼叫以制造中断
- 挂断电话,停止呼叫
按照上述的场景进行测试,你会发现,当中断发生时,播放中的音频会慢慢消失和暂停。这个效果是自动实现的,我们没有做任何的处理。当另一台手机的电话被挂断,会出现一些问题,播放/停止功能消失,音频也不再继续播放。
2.6.1 音频会话通知
- 首先需要监听中断出现的通知,注册AVAudioSession发送的通知AVAudioSessionInterruptionNotification。只需要注册一次,在init方法中进行通知的注册。
- (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; } 复制代码
- 接收到通知后,处理通知
- 从userInfo中获取信息,获取开始打断和结束打断的枚举值,开始打断后,停止播放。如果控制器处理一些业务逻辑,通过代理传递出去
- 当打断结束后,获取音频会话被重新激活,我们继续播放,通过代理传递到控制器,处理相关业务逻辑
- (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]; } 复制代码
2.7 对线路改变的响应
在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完成的一个简单地播放器功能。实际开发中,我们只要注意处理我们真正遇到的场景就可以了。
这篇关于AV Foundation使用AVAudioPlayer播放音频的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值