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系统本身可以很好地处理这些事件。不过我们仍需要针对这些情况做自己的处理。

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



这篇关于AV Foundation使用AVAudioPlayer播放音频的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程