iOS开发记录iOS学习开发Learning AV Foundation

Learning AV Foundation(二)AVAudio

2019-01-19  本文已影响4人  iOS猿_员

开篇

最近在学习AV Foundation 试图把学习内容记录下来 并参考一些博客文章
本期的内容是AVAudioPlayer

音频知识基础

音频文件的生成过程是将声音信息采样量化编码产生的数字信号的过程,人耳所能听到的声音,最低的频率是从20Hz起一直到最高频率20KHZ,因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,只有采样频率高于声音信号最高频率的两倍时,才能把数字信号表示的声音还原成为原来的声音,所以音频文件的采样率一般在40~50KHZ,比如最常见的CD音质采样率44.1KHZ。 (所以一般大家都觉得CD音质是最好的.) 对声音进行采样、量化过程被称为脉冲编码调制(Pulse Code Modulation),简称PCM。PCM数据是最原始的音频数据完全无损,所以PCM数据虽然音质优秀但体积庞大,为了解决这个问题先后诞生了一系列的音频格式,这些音频格式运用不同的方法对音频数据进行压缩,其中有无损压缩(ALAC、APE、FLAC)和有损压缩(MP3、AAC、OGG、WMA)两种 来源:iOS音频播放 (一):概述 by 码农人生

我觉得程寅大牛的处理音频说的很明白
大神列出一个经典的音频播放流程(以MP3为例)

  1. 读取MP3文件
  2. 解析采样率、码率、时长等信息,分离MP3中的音频帧
  3. 对分离出来的音频帧解码得到PCM数据
  4. 对PCM数据进行音效处理(均衡器、混响器等,非必须)
  5. 把PCM数据解码成音频信号
  6. 把音频信号交给硬件播放
  7. 重复1-6步直到播放完成

在iOS系统中apple对上述的流程进行了封装并提供了不同层次的接口


这是CoreAudio的接口层次

下面对其中的中高层接口进行功能说明:

可以看到apple提供的接口类型非常丰富,可以满足各种类别类需求:

image

以上内容均转自码农人生 希望大神不要介意 如果有问题 我可立即清除

使用AVAudioPlayer之前对AudioSession简介

AVAudioSession负责管理音频会话 它是个单例 在应用程序和操作系统之间负责中间人的角色 AudioSession参考

AVAudioSession主要功能包括以下几点:

注:AVAudioSession iOS6以后使用 以前叫AudioSession

如何使用AVAudioPlayer

在我的博客里面我尽量使用code胜过千言万语
使用AVAudioPlayer之前需要在AppDelegate里面导入#import <AVFoundation/AVFoundation.h>
并且启动音频会话

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    AVAudioSession *session = [AVAudioSession sharedInstance];
    NSError *error;
    if (![session setCategory:AVAudioSessionCategoryPlayback error:&error]) {
        NSLog(@"Category Error: %@", [error localizedDescription]);
    }

    if (![session setActive:YES error:&error]) {
        NSLog(@"Activation Error: %@", [error localizedDescription]);
    }

    return YES;
}

上边已经介绍了AVAudioSession

这里面说一下[session setCategory:AVAudioSessionCategoryPlayback error:&error] 里面的AVAudioSessionCategoryPlayback

音频会话分类

这是这几种分类的列表大家可以看下

记得开启后台播放


或者在plist里面修改


下面就是创建音频播放器代码

#import "ViewController.h"
#import <Masonry/Masonry.h>
#import "THControlKnob.h"
#import "THPlayButton.h"
#import <AVFoundation/AVFoundation.h>
@interface ViewController ()
//三个控制推子
@property (weak, nonatomic) IBOutlet THOrangeControlKnob *panKnob;
@property (weak, nonatomic) IBOutlet THOrangeControlKnob *volumnKnob;
@property (weak, nonatomic) IBOutlet THGreenControlKnob *rateKnob;
@property (weak, nonatomic) IBOutlet THPlayButton *playButton;
//音乐播放器
@property (nonatomic, strong) AVAudioPlayer *musicPlayer;
@property (nonatomic, getter = isPlaying) BOOL playing; //播放状态
//无关代码
@property (weak, nonatomic) IBOutlet UILabel *LeftRightRoundDec;
@property (weak, nonatomic) IBOutlet UILabel *voiceDec;
@property (weak, nonatomic) IBOutlet UILabel *rateDec;
@property (weak, nonatomic) IBOutlet UILabel *trackDescrption;
@end

导入几个第三方控件的类用于音乐播放

这上边的三个旋钮就是导入的开源库

下面创建播放器AVAudioPlayer
创建时需要一个NSURL代表要播放的文件路径 这里简单从bundle中拖了一首歌进去了

#pragma mark -
#pragma mark - 创建AVAudioPlayer与播放状态控制
/**

 创建音乐播放器

 @param fileName 文件名
 @param fileExtension 文件扩展名
 @return 播放器实例
 */
- (AVAudioPlayer *)createPlayForFile:(NSString *)fileName
                       withExtension:(NSString *)fileExtension{
    NSURL *url = [[NSBundle mainBundle] URLForResource:fileName withExtension:fileExtension];
    NSError *error = nil;
    AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
    if (audioPlayer) {
        audioPlayer.numberOfLoops = -1; //-1无限循环
        audioPlayer.enableRate = YES; //启动倍速控制
        [audioPlayer prepareToPlay];
    } else {
        NSLog(@"Error creating player: %@",[error localizedDescription]);
    }
    return audioPlayer;
}

numberOfLoops = -1; 代表本首歌 无限循环 其它常数代表循环次数
enableRate 代表是否启用倍速调节 0.5x 1.0x 2.0x 等倍速 1.0代表正常速度

这里说一下[audioPlayer prepareToPlay]
调用这个函数是为了取得需要的音频硬件并预加载Audio Queue的缓冲区. 当然也可以不调用这个方法直接调用 [audioPlayer play],但当 调用play方法时也会隐性激活,调用prepareToPlay是为了减少 创建播放器时预设加载和听到声音输出之间的延时

@implementation ViewController
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        if (self.musicPlayer == nil) {
            self.musicPlayer = [self createPlayForFile:@"384551_1438267683" withExtension:@"mp3"];
        }
        [self setupNotifications];
    }
    return self;
}
- (void)awakeFromNib{
    [super awakeFromNib];
    if (self.musicPlayer == nil) {
        self.musicPlayer = [self createPlayForFile:@"384551_1438267683" withExtension:@"mp3"];
    }
    [self setupNotifications];
}

initWithNibNameawakeFromNib时候调用一下创建播放器的代码
这个[self setupNotifications];后面说

先添加一些常见的方法封装 比如 播放、暂停、停止

- (void)play {
    if (self.musicPlayer == nil) { return; }
    if (!self.playing) {
        NSTimeInterval delayTime = [self.musicPlayer deviceCurrentTime] + 0.01;
        [self.musicPlayer playAtTime:delayTime];
        self.playing = YES;
    }

    self.trackDescrption.text = [self.musicPlayer.url absoluteString];
    [self configNowPlayingInfoCenter]; //配置后台播放的页面信息
}
- (void)stop {
    if (self.musicPlayer == nil) { return; }
    if (self.playing) {
        [self.musicPlayer stop];
        self.musicPlayer.currentTime = 0.0f;
        self.playing = NO;
    }
}
- (void)pause {
    if (self.musicPlayer == nil) { return; }
    if (self.playing) {
        [self.musicPlayer pause];
        self.playing = NO;
    }
}

这里看到[self.musicPlayer deviceCurrentTime] + 0.01 加了 -0.01的延时, 是为了以后大家做播放器的时候 有可能暂停或者歌曲切换时 有可能 向前向后做片段衔接, 也是为了使用 playAtTime去播放 指定位置的音乐用于 意外暂停或者播放上次播放的配置信息使用 这里看到我写了一个
[self configNowPlayingInfoCenter];配置后台播放的页面信息
这个主要用于播放音乐在后台时 锁屏显示的屏幕信息 请看下面代码

//设置锁屏状态,显示的歌曲信息
-(void)configNowPlayingInfoCenter{
    if (NSClassFromString(@"MPNowPlayingInfoCenter")) {
        NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];

        //歌曲名称
        [dict setObject:@"歌曲名称" forKey:MPMediaItemPropertyTitle];

        //演唱者
        [dict setObject:@"演唱者" forKey:MPMediaItemPropertyArtist];

        //专辑名
        [dict setObject:@"专辑名" forKey:MPMediaItemPropertyAlbumTitle];

        //专辑缩略图
        UIImage *image = [UIImage imageNamed:@"sunyazhou"];
        MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithImage:image];
        [dict setObject:artwork forKey:MPMediaItemPropertyArtwork];

        //音乐剩余时长
        [dict setObject:@20 forKey:MPMediaItemPropertyPlaybackDuration];

        //音乐当前播放时间 在计时器中修改
       // [dict setObject:[NSNumber numberWithDouble:100.0] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];

        //设置锁屏状态下屏幕显示播放音乐信息
        [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
    }
}

如果需要在计时器中不断刷新锁屏状态下的播放进度条请写如下代码

//计时器修改进度
- (void)changeProgress:(NSTimer *)sender{
    if(self.player){
        //当前播放时间
        NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:[[MPNowPlayingInfoCenter defaultCenter] nowPlayingInfo]];
        [dict setObject:[NSNumber numberWithDouble:self.player.currentTime] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime]; //音乐当前已经过时间
        [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
    }
}

下面我们来介绍一下
[self setupNotifications];注册监听 音频意外中断和耳机拔出时要暂停音乐播放
实现代码如下

/**
 播放的通知处理
 */
- (void)setupNotifications {
    NSNotificationCenter *nsnc = [NSNotificationCenter defaultCenter];

    //添加意外中断音频播放的通知
    [nsnc addObserver:self
             selector:@selector(handleInterruption:)
                 name:AVAudioSessionInterruptionNotification
               object:[AVAudioSession sharedInstance]];

    //添加线路变化通知
    [nsnc addObserver:self
             selector:@selector(hanldeRouteChange:)
                 name:AVAudioSessionRouteChangeNotification
               object:[AVAudioSession sharedInstance]];
}

注:记得在delloc里面[[NSNotificationCenter defaultCenter] removeObserver:self]

意外中断音频发生的场景 例如 听歌过程中来电话或者 按住home键使用siri

下面是具体方法实现

/**
 音频意外打断处理
 @param notification 通知信息
 */
- (void)handleInterruption:(NSNotification *)notification {
    NSDictionary *info = notification.userInfo;
    AVAudioSessionInterruptionType type = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
    if (type == AVAudioSessionInterruptionTypeBegan) {
        //Handle AVAudioSessionInterruptionTypeBegan
        [self pause];
    } else {
        //Handle AVAudioSessionInterruptionTypeEnded
        AVAudioSessionInterruptionOptions options = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
        NSError *error = nil;
        //激活音频会话 允许外接音响
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
                                         withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:nil];
        [[AVAudioSession sharedInstance] setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
        if (options == AVAudioSessionInterruptionOptionShouldResume) {
            [self play];
        } else {
            [self play];
        }

        self.playButton.selected = YES;

        if (error) {
            NSLog(@"AVAudioSessionInterruptionOptionShouldResume失败:%@",[error localizedDescription]);
        }
    }
}

先说handleInterruption意外情况下中断比如我按住home键使用siri
我会收到意外打断的通知当 type == AVAudioSessionInterruptionTypeBegan时 我们停止音乐播放或者暂停.
当type != AVAudioSessionInterruptionTypeBegan的时候一定是AVAudioSessionInterruptionTypeEnded这个时候notification.userInfo里面包含一个AVAudioSessionInterruptionOptions值来表明音频会话是否已经重新激活以及是否可以再次播放

注:这个地方遇到个坑 当意外中断时候有时音频会话会很不灵敏 后来发现这种情况下需要重新激活会话 如下代码:

[[AVAudioSession sharedInstance] setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];

这里AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation是为了通知其它应用会话被我激活了 很多播放器开发者很不讲究 每次从来不用这个方法导致每次别人播放完音频 自己都收不到音频重新播放的信息 建议大家以和为贵, 写良心代码.

因为我外接的小米蓝牙音响发现还是不好使 最后又补上了AVAudioSessionCategoryOptionAllowBluetooth这个

激活音频会话 允许外接音响

[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:nil];

就好使了

下面说一下耳机插拔或者USB麦克风断开 Apple有个什么Human Interface Guidelines(HIG)相关定义 意思是说当硬件耳机拔出时建议 暂停播放音乐或者麦克风断开时。就是处于静音状态。是为了保密播放内容不被外界听到,不管苹果啥规定 我们都得照办 否则就得被拒。

- (void)hanldeRouteChange:(NSNotification *)notification {
    NSDictionary *info = notification.userInfo;
    AVAudioSessionRouteChangeReason reason = [info[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
    //老设备不可用
    if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
        AVAudioSessionRouteDescription *previousRoute = info[AVAudioSessionRouteChangePreviousRouteKey];
        AVAudioSessionPortDescription *previousOutput = previousRoute.outputs[0];
        NSString *portType = previousOutput.portType;
        if ([portType isEqualToString:AVAudioSessionPortHeadphones]) {
            [self stop];
            self.playButton.selected = NO;
        }
    }
}

这需要用AVAudioSessionRouteChangeReasonKey取出线路切换的原因AVAudioSessionRouteChangeReason 原因有这么多

typedef NS_ENUM(NSUInteger, AVAudioSessionRouteChangeReason)
{
    AVAudioSessionRouteChangeReasonUnknown = 0,
    AVAudioSessionRouteChangeReasonNewDeviceAvailable = 1,
    AVAudioSessionRouteChangeReasonOldDeviceUnavailable = 2,
    AVAudioSessionRouteChangeReasonCategoryChange = 3,
    AVAudioSessionRouteChangeReasonOverride = 4,
    AVAudioSessionRouteChangeReasonWakeFromSleep = 6,
    AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory = 7,
    AVAudioSessionRouteChangeReasonRouteConfigurationChange NS_ENUM_AVAILABLE_IOS(7_0) = 8
} NS_AVAILABLE_IOS(6_0);

我们需要这个AVAudioSessionRouteChangeReasonOldDeviceUnavailable 判断是否是旧设备
通过AVAudioSessionRouteChangePreviousRouteKey拿出

AVAudioSessionRouteDescription描述信息
previousRoute 在通过
previousRoute.outputs[0]拿出AVAudioSessionPortDescription

拿出NSString *portType = previousOutput.portType

如果[portType isEqualToString:AVAudioSessionPortHeadphones]

如果是耳机AVAudioSessionPortHeadphones则暂停播放

以上就是中断和线路切换的一些代码逻辑

下面我介绍一些好玩的

前面说的一些后台设置信息显示的内容就是上图所示 在锁屏的时候显示

但是大家一定很奇怪的是怎么实现接收 锁屏状态下 点击 上一曲 暂停/播放 下一曲等操作

需要在AppDelegate里面写上

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    AVAudioSession *session = [AVAudioSession sharedInstance];
    NSError *error;
    if (![session setCategory:AVAudioSessionCategoryPlayback error:&error]) {
        NSLog(@"Category Error: %@", [error localizedDescription]);
    }

    if (![session setActive:YES error:&error]) {
        NSLog(@"Activation Error: %@", [error localizedDescription]);
    }

    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
    [self becomeFirstResponder];
    return YES;
}

[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
行代码 以及调用自己为 [self becomeFirstResponder];第一响应者 这样写是为了应用响应音频播放 后台切换或者中断的时候更灵敏.

- (BOOL)canBecomeFirstResponder {
    return YES;
}

然后 写上如下代码 处理锁屏状态下 点击 上一曲 暂停/播放 下一曲等操作

- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
    if (event.type == UIEventTypeRemoteControl) {
        switch (event.subtype) {
            case UIEventSubtypeRemoteControlPlay:
                NSLog(@"暂停播放");
                break;
            case UIEventSubtypeRemoteControlPause:

                NSLog(@"继续播放");
                break;
            case UIEventSubtypeRemoteControlNextTrack:
                NSLog(@"下一曲");
                break;
            case UIEventSubtypeRemoteControlPreviousTrack:
                NSLog(@"上一曲");
                break;
            default:
                break;
        }
    }
}

剩余逻辑大家自己填充吧我就不介绍了.

好了AVAudioPlayer就到这吧!有啥疑问大家可以评论留言都能看到或者指正我的错误。我会及时改正.

全文完

文章最终的Demo获取:加iOS高级技术交流群:624212887,获取Demo,以及更多iOS学习资料

文章来源于网络,如有侵权请联系小编删除

上一篇下一篇

猜你喜欢

热点阅读