AV Foundation使用AVAudioPlayer播放音频

2.1 iOS的音频环境

当你在iPhone上点开一首歌曲,音频在内置扬声器中播放出来,此时有电话拨入,音乐会当即中止并处于暂停状态,此时听到的是手机呼叫的铃音。若是此时你挂掉电话,刚才的音乐声再次响起,当你插上耳机,音乐播放时音频输出到了耳机里。当听完这首音乐摘下耳机后,你会发现声音自动转回内置扬声器并处于暂停状态。objective-c

iOS系统提供了一个可管理的音频环境(managed audio environment),能够带给全部iOS用户很是好的用户体验,这一过程具体是如何实现的呢?这里会用到音频会话(audio session)。网络

2.2 理解音频会话

音频会话在应用程序和操做系统之间扮演着中间人的角色。它提供了一种简单实用的方法使OS得知应用程序应该如何与iOS音频环境进行交互。你不须要了解与音频硬件交互的细节,只须要对应用程序的行为进行语义上的描述便可。这一点使得你能够指明应用程序的通常音频行为,并能够把对该行为的管理委托给音频会话,这样OS系统就能够对用户使用音频的体验进行适当的管理。session

全部iOS应用程序都具备音频会话,不管其是否使用。默认音频会话来自于如下一些预配置:app

  • 支持音频播放,不支持音频录制
  • 在iOS中,当用户切换响铃/静音开关到“静音”模式时,应用程序正在被播放的全部音频都会消失
  • 在iOS中,当设备锁屏时,应用程序的音频将处于静音状态
  • 当应用程序播放音频时,全部其余后台播放音频,例如音乐的应用程序都会被静音。

默认音频会话提供了许多实用功能,可是在大多数状况下,你须要自定义音频会话来适配你本身应用程序的需求。oop

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的类。该类的实例提供了一种简单地从文本内存中播放音频的方法。ui

AVAudioPlayer构建与Core Audio中的C-based Audio Queue Services的最顶层。因此它能够提供全部你在Audio Queue Services中所能找到的核心功能,好比播放、循环甚至音频计量,但使用的是Objective-C接口。除非你须要从网络中播放音频,须要访问原始音频样本或须要很是低的延时,不然AVAudioPlayer都能胜任。atom

2.3.1 建立AVAudioPlayer

有两种方法能够建立一个AVAudioPlayer,使用包含播放音频的内存版本的NSData或本地音频文件的NSURL。spa

@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系统自己能够很好地处理这些事件。不过咱们仍须要针对这些状况作本身的处理。

  1. 在设备上运行应用程序并播放音频
  2. 当音频处于播放状态时,从另一台设备发起电话呼叫以制造中断
  3. 挂断电话,中止呼叫

按照上述的场景进行测试,你会发现,当中断发生时,播放中的音频会慢慢消失和暂停。这个效果是自动实现的,咱们没有作任何的处理。当另外一台手机的电话被挂断,会出现一些问题,播放/中止功能消失,音频也再也不继续播放。

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完成的一个简单地播放器功能。实际开发中,咱们只要注意处理咱们真正遇到的场景就能够了。

相关文章
相关标签/搜索