播放和录制音频(AV Foundation开发秘籍)

主要通过学习AVAudioPlayer和AVAudioRecorder类来实现应用程序中添加音频播放及音频录制的功能。
AV Foundation定义了7中分类来描述应用程序所使用的音频行为

AVAudioSessionCategory 7大类别

分类 作用 是否允许混音 音频输入 音频输出 说明
Ambient 游戏、效率应用程序 ✔️ ✔️ 混音播放
SoloAmbient(默认) 游戏、效率应用程序 ✔️ 独占播放
Playback 音频和视频播放器 可选 ✔️ 后台播放,也是独占的
Record 录音机、音频捕捉 ✔️ 录音模式,用于录音时使用
Play and Record VoIP 语音聊天 可选 ✔️ ✔️ 播放和录音,此时可以录音也可以播放
Audio Processing 离线会话处理 硬件解码音频,此时不能播放和录制
Multi-Route 使用外部硬件的高级A/V程序 ✔️ ✔️ 多种输入输出,例如可以耳机、USB设备同时播放

AVAudioSessionCategoryOptions 类别的子选项

Options 使用类别 说明
MixWithOthers PlayAndRecord, Playback, MultiRoute 与其他可以混音
DuckOthers Ambient, PlayAndRecord, Playback,MultiRoute 压低其他声音
AllowBluetooth Record,PlayAndRecord 支持蓝牙耳机
DefaultToSpeaker PlayAndRecord 默认使用免提
InterruptSpokenAudioAndMixWithOthers(9.0) PlayAndRecord,Playback, MultiRoute 当其他app音频暂停是本app的音频是否继续
AllowBluetoothA2DP PlayAndRecord 支持蓝牙A2DP耳机
AllowAirPlay PlayAndRecord 支持AirPlay

AVAudioSessionMode

模式 适用的类别 场景
Default 所有类别 默认的模式
VoiceChat PlayAndRecord VoIP
GameChat PlayAndRecord 游戏录制,由GKVoiceChat自动设置,无需手动调用
VideoRecording PlayAndRecord,Record 录制视频时
MoviePlayback Playback 视频播放
Measurement PlayAndRecord,Record,Playback 最小系统
VideoChat PlayAndRecord 视频通话
配置音频会话
    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]);
    }
/* returns singleton instance */
+ (AVAudioSession *)sharedInstance;

/* Set the session active or inactive. 
*/
- (BOOL)setActive:(BOOL)active error:(NSError **)outError;
- (BOOL)setActive:(BOOL)active withOptions:(AVAudioSessionSetActiveOptions)options error:(NSError **)outError;

/* Asynchronously activate the session. 异步激活会话,handler是回调
 */
- (void)activateWithOptions:(AVAudioSessionActivationOptions)options completionHandler:(void (^)(BOOL activated, NSError * _Nullable error))handler;
// 获取设备可用的SessionCategory
@property (readonly) NSArray<AVAudioSessionCategory> *availableCategories;

/* 设置session的category */
- (BOOL)setCategory:(AVAudioSessionCategory)category error:(NSError **)outError ;
/* set session category with options */
- (BOOL)setCategory:(AVAudioSessionCategory)category withOptions:(AVAudioSessionCategoryOptions)options error:(NSError **)outError;
/* set session category and mode with options */
- (BOOL)setCategory:(AVAudioSessionCategory)category mode:(AVAudioSessionMode)mode options:(AVAudioSessionCategoryOptions)options error:(NSError **)outError ;
- (BOOL)setCategory:(AVAudioSessionCategory)category mode:(AVAudioSessionMode)mode routeSharingPolicy:(AVAudioSessionRouteSharingPolicy)policy options:(AVAudioSessionCategoryOptions)options error:(NSError **)outError;
@property (readonly) AVAudioSessionCategory category;
@property (readonly) AVAudioSessionCategoryOptions categoryOptions;
@property (readonly) AVAudioSessionRouteSharingPolicy routeSharingPolicy;
@property (readonly) NSArray<AVAudioSessionMode> *availableModes;
- (BOOL)setMode:(AVAudioSessionMode)mode error:(NSError **)outError; /* set session mode */
@property (readonly) AVAudioSessionMode mode; /* get session mode */
/* 返回权限*/
@property (readonly) AVAudioSessionRecordPermission recordPermission;

typedef void (^PermissionBlock)(BOOL granted);
- (void)requestRecordPermission:(PermissionBlock)response;
- (BOOL)overrideOutputAudioPort:(AVAudioSessionPortOverride)portOverride error:(NSError **)outError;
@property (readonly, getter=isOtherAudioPlaying) BOOL otherAudioPlaying;
@property (readonly) BOOL secondaryAudioShouldBeSilencedHint;
@property (readonly) AVAudioSessionRouteDescription *currentRoute;
- (BOOL)setPreferredInput:(nullable AVAudioSessionPortDescription *)inPort error:(NSError **)outError API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos, macos);
@property (readonly, nullable) AVAudioSessionPortDescription *preferredInput API_AVAILABLE(ios(7.0), tvos(9.0)) API_UNAVAILABLE(watchos, macos); /* Get the preferred input port.  Will be nil if no preference has been set */

@property (readonly, nullable) NSArray<AVAudioSessionPortDescription *> *availableInputs ;

创建AVAudioPlayer

AVAudioPlayer构建与CoreAudio中的C-based Audio Queue Services的最顶层。它可以提供播放,循环,音频计量等简单友好的Objective-C接口。

- (AVAudioPlayer *)playerForFile:(NSString *)name {

    NSURL *fileURL = [[NSBundle mainBundle] URLForResource:name
                                             withExtension:@"caf"];

    NSError *error;
    AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL
                                                                   error:&error];
    if (player) {
        player.numberOfLoops = -1; // 无限循环
        player.enableRate = YES;
        [player prepareToPlay];  //相当于预加载Audio Queue的缓冲区
    } else {
        NSLog(@"Error creating player: %@", [error localizedDescription]);
    }
    return player;
}

播放控制

  • 修改播放器的音量
  • 修改播放器的pan值 允许使用立体声播放声音,-1.0 到 1.0 之间
  • 调整播放率rate 0.5表示半速,1表示正常,2表示2倍速播放
  • numberOfLoops 实现无缝循环 -1表示无限循环,n > 0表示循环n次
THPlayerController.h
@interface THPlayerController : NSObject
@property (nonatomic, getter = isPlaying) BOOL playing; //是否正在播放
// 播放
- (void)play;
//停止
- (void)stop;
//设置速率
- (void)adjustRate:(float)rate;
// 设置pan值
- (void)adjustPan:(float)pan forPlayerAtIndex:(NSUInteger)index;
//设置音量
- (void)adjustVolume:(float)volume forPlayerAtIndex:(NSUInteger)index;

@end
#import "THPlayerController.h"
#import <AVFoundation/AVFoundation.h>

@interface THPlayerController () <AVAudioPlayerDelegate>
@property (strong, nonatomic) NSArray *players;
@end

@implementation THPlayerController

#pragma mark - Initialization

- (id)init {
    self = [super init];
    if (self) {
        AVAudioPlayer *guitarPlayer = [self playerForFile:@"guitar"];
        AVAudioPlayer *bassPlayer = [self playerForFile:@"bass"];
        AVAudioPlayer *drumsPlayer = [self playerForFile:@"drums"];

        guitarPlayer.delegate = self;

        _players = @[guitarPlayer, bassPlayer, drumsPlayer];
    }
    return self;
}

- (AVAudioPlayer *)playerForFile:(NSString *)name {

    NSURL *fileURL = [[NSBundle mainBundle] URLForResource:name
                                             withExtension:@"caf"];

    NSError *error;
    AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL
                                                                   error:&error];
    if (player) {
        player.numberOfLoops = -1; // loop indefinitely
        player.enableRate = YES;
        [player prepareToPlay];
    } else {
        NSLog(@"Error creating player: %@", [error localizedDescription]);
    }

    return player;
}


#pragma mark - Global playback control methods

- (void)play {
    if (!self.playing) {
        NSTimeInterval delayTime = [self.players[0] deviceCurrentTime] + 0.01;
        for (AVAudioPlayer *player in self.players) {
            [player playAtTime:delayTime];
        }
        self.playing = YES;
    }
}

- (void)stop {
    if (self.playing) {
        for (AVAudioPlayer *player in self.players) {
            [player stop];
            player.currentTime = 0.0f; //player.currentTime回到原点
        }
        self.playing = NO;
    }
}

- (void)adjustRate:(float)rate {
    for (AVAudioPlayer *player in self.players) {
        player.rate = rate;
    }
}
- (void)adjustPan:(float)pan forPlayerAtIndex:(NSUInteger)index {
    if ([self isValidIndex:index]) {
        AVAudioPlayer *player = self.players[index];
        player.pan = pan;
    }
}
- (void)adjustVolume:(float)volume forPlayerAtIndex:(NSUInteger)index {
    if ([self isValidIndex:index]) {
        AVAudioPlayer *player = self.players[index];
        player.volume = volume;
    }
}
- (BOOL)isValidIndex:(NSUInteger)index {
    return index == 0 || index < self.players.count;
}
@end

处理中断事件

音频播放过程中,遇到电话呼入、闹钟等事件,会中断音频的播放
设置监听中断事件

初始化AVAudioPlayer之后设置中断事件的监听
NSNotificationCenter *nsnc = [NSNotificationCenter defaultCenter];
[nsnc addObserver:self selector:@selector(handleInterruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]];
//相应回调
-(void)handleInterruption:(NSNotification *)notification{
    NSDictionary *info = notification.userInfo;
    AVAudioSessionInterruptionType type = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
    if (type == AVAudioSessionInterruptionTypeBegan) {  //中断开始
        [self stop];  
    }else{
        AVAudioSessionInterruptionOptions options = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
        if (options == AVAudioSessionInterruptionOptionShouldResume) {  //恢复
            [self play];
            }
        }
    }
}
对线路改变的响应

线路是指输出线路从扬声器模式变为耳机,或者耳机变为扬声器模式。

线路改变原因的枚举
AVAudioSessionRouteChangeReasonUnknown = 0,
AVAudioSessionRouteChangeReasonNewDeviceAvailable = 1,
AVAudioSessionRouteChangeReasonOldDeviceUnavailable = 2,
AVAudioSessionRouteChangeReasonCategoryChange = 3,
AVAudioSessionRouteChangeReasonOverride = 4,
AVAudioSessionRouteChangeReasonWakeFromSleep = 6,
AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory = 7,
AVAudioSessionRouteChangeReasonRouteConfigurationChange = 8 
音频输入
AVAudioSessionPortUSBAudio,
 AVAudioSessionPortHeadsetMic, 
AVAudioSessionPortBuiltInMic.  
音频输出
AVAudioSessionPortUSBAudio,  usb接口
AVAudioSessionPortLineOut, 
AVAudioSessionPortHeadphones,  耳机输出
AVAudioSessionPortHDMI, 
AVAudioSessionPortBuiltInSpeaker.   内置扬声器
NSNotificationCenter *nsnc = [NSNotificationCenter defaultCenter];
[nsnc addObserver:self
         selector:@selector(handleRouteChange:)
             name:AVAudioSessionRouteChangeNotification
           object:[AVAudioSession sharedInstance]];

//线路的改变  比如耳机断开是停止播放
- (void)handleRouteChange:(NSNotification *)notification {
    NSDictionary *info = notification.userInfo;
    AVAudioSessionRouteChangeReason reason =
        [info[AVAudioSessionRouteChangeReasonKey] unsignedIntValue];
//旧设备断开链接
    if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
        AVAudioSessionRouteDescription *previousRoute =
            info[AVAudioSessionRouteChangePreviousRouteKey];
//端口描述        
AVAudioSessionPortDescription *previousOutput = previousRoute.outputs[0];
        NSString *portType = previousOutput.portType;
        if ([portType isEqualToString:AVAudioSessionPortHeadphones]) {
            [self stop];
        }
    }
}
音频录制AVAudioRecorder

创建AVAudioRecorder实例时需要为其提供数据的一些信息

- (nullable instancetype)initWithURL:(NSURL *)url settings:(NSDictionary<NSString *, id> *)settings error:(NSError **)outError;
  • 音频流写入文件的本地url地址
  • 配置录音会话的设置信息
    在 #import <AVFAudio/AVAudioSettings.h> 文件里有音频的设置键信息。
AVFormatIDKey     音频格式
AVSampleRateKey;    采样率         
AVNumberOfChannelsKey;      通道数             

格式:kAudioFormatLinearPCM会将未压缩的音频流写入文件中。 这中格式保真度最高,kAudioFormatMPEG4AAC的压缩格式会显著缩小文件,还能保证高质量的音频内容。
采样率:对输入的音频信号每一秒内的采样数。一般标准的采样率为8000、16000、22050和44100赫兹。
通道数:定义记录音频内容的通道数。默认值为1意味着使用单声道录制,设置为2采用立体声录制。

-(NSURL *)getSavePath{
    NSString *urlStr = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
    urlStr = [urlStr stringByAppendingPathComponent:kRecordAudioFile];
    NSLog(@"file path:%@",urlStr);
    NSURL *url = [NSURL fileURLWithPath:urlStr];
    return url;
}

//录音参数设置
-(NSDictionary *)getAudioSetting{
    NSMutableDictionary *dicM = @{}.mutableCopy;
    [dicM setObject:@(kAudioFormatLinearPCM) forKey:AVFormatIDKey];
    [dicM setObject:@(44100) forKey:AVSampleRateKey];
    [dicM setObject:@(1) forKey:AVNumberOfChannelsKey];
    [dicM setObject:@(16) forKey:AVEncoderBitDepthHintKey];
    [dicM setObject:@(AVAudioQualityMedium) forKey:AVEncoderAudioQualityKey];
    return dicM;
}
//录音器
-(AVAudioRecorder *)audioRecorder{
    if (!_audioRecorder) {
        //保存路径
        NSURL *url = [self getSavePath];
        NSDictionary *setting = [self getAudioSetting];
        NSError *error;
        _audioRecorder = [[AVAudioRecorder alloc] initWithURL:url settings:setting error:&error];
        _audioRecorder.delegate = self;
        _audioRecorder.meteringEnabled = YES;
        [_audioRecorder prepareToRecord];
    }
    return _audioRecorder;
}
//开始录制
- (BOOL)record {
    return [self.audioRecorder record];
}
//中断
- (void)pause {
    [self.audioRecorder pause];
}
// 停止
- (void)stop {
    [self.audioRecorder stop];
}
//实现录制完成代理
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)success {
   
}
//实现代理方法 录制中断
- (void)audioRecorderBeginInterruption:(AVAudioRecorder *)recorder {

}
记录分贝等级
- (float)peakPowerForChannel:(NSUInteger)channelNumber; /* returns peak power in decibels for a given channel */
- (float)averagePowerForChannel:(NSUInteger)channelNumber; /* returns average power in decibels for a given channel */

返回用于表示分贝等级的浮点值,最大分贝的0dB到最小分贝的-160dB。 在调用这两个方法之前需要设置_audioRecorder.meteringEnabled = YES才能使用。

AV Foundation开发秘籍源码

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容