《AV Foundation 开发秘籍》读书笔记(二)

第二章 音频播放和录制

1. 音频会话

音频会话分类

Category 播放类型 后台播放 静音或屏幕关闭 音频输入 音频输出 作用
AVAudioSessionCategoryAmbient 混合播放 有影响 支持 游戏背景音乐
AVAudioSessionCategorySoloAmbient(默认) 独占播放 有影响 支持 微信中播放语音
AVAudioSessionCategoryPlayback 可选 支持 支持 音频播放器
AVAudioSessionCategoryRecord 独占录音 支持 支持 微信中录制语音
AVAudioSessionCategoryPlayAndRecord 可选 支持 支持 支持 微信语音聊天
AVAudioSessionCategoryAudioProcessing —— —— —— 硬件解码音频
AVAudioSessionCategoryMultiRoute 支持 支持 多设备输入输出

上述分类所提供的几种常见行为可以满足大部分应用程序的需要,如果需要更复杂的功能,上述其中一种分类可以通过使用 options 和 modes 方法进一步自定义开发。

激活音频会话

- (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;
}

如果分类允许后台播放,则应该打开 Capabilities 中的 Background Modes 继而勾选后台播放音频选项

2. 音频播放

除非需要从网络流中播放音频、需要访问原始音频样本,或者需要非常低的时延,否则 AVAudioPlayer 都能胜任

- (void)viewDidLoad {
    [super viewDidLoad];

    NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"白洁01" withExtension:@"mp3"];
    
    // Must Maintain a strong reference to player
    self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:nil];
    
    if (self.player) {
        self.player.numberOfLoops = -1;  // 循环播放
        [self.player prepareToPlay];
    }
}

- (IBAction)play {
    
    [self.player play];
}

prepareToPlay 方法是可选的,在调用 play 方法前也会自动调用,作用是取得需要的音频硬件并预加载 Audio Queue 的缓冲区,降低调用 play 方法后的延时。

- (IBAction)pause {
    
    [self.player pause];
}

- (IBAction)stop {
    
    [self.player stop];
    self.player.currentTime = 0.0f;
}

通过 pause 和 stop 方法停止的音频都会继续播放。最主要的区别在底层处理上,调用 stop 方法会撤销调用 prepareToPlay 时所做的设置,而调用 pause 方法则不会。

// 音量 0 ~ 1
- (IBAction)voice:(UISlider *)sender {
    
    self.player.volume = sender.value;
}

// 声道 -1 ~ 1
- (IBAction)pan:(UISlider *)sender {
    
    self.player.pan = sender.value;
}

// 速率 0.5 ~ 2
- (IBAction)speed:(UISlider *)sender {
    
    self.player.rate = sender.value;
}

如果要改变速率,在初始化 AVAudioPlayer 时应做出如下设置

self.player.enableRate = YES;

3. 处理中断事件

当有电话呼入、闹钟响起的时候,播放中的音频会慢慢消失和暂停,但是终止通话后,播放、停止按钮的控件和音频的播放没有恢复。为了优化用户体验,需要监听这些事件,并作出处理:

[[NSNotificationCenter defaultCenter] 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.player pause];
        
    } else {
        
        // 中断结束,判断是否允许继续播放
        AVAudioSessionInterruptionOptions options = [info[AVAudioSessionInterruptionOptionKey] unsignedIntegerValue];
        if (options == AVAudioSessionInterruptionOptionShouldResume) {
            
            // 允许继续播放,则继续播放
            [self.player play];
        }
    }
}

4. 对线路改变的响应

播放音频期间插入耳机,音频输出线路变成耳机插孔并继续播放。断开耳机连接,音频线路再次回到设备的内置扬声器播放。虽然线路变化和预期一样,不过按照苹果官方文档,认为该音频应该处于静音状态。

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleRouteChange:) name:AVAudioSessionRouteChangeNotification object:[AVAudioSession sharedInstance]];
- (void)handleRouteChange:(NSNotification *)notification
{
    NSDictionary *info = notification.userInfo;
    
    AVAudioSessionRouteChangeReason reason = [info[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
    if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
        
        AVAudioSessionRouteDescription *previousRoute  = info[AVAudioSessionRouteChangePreviousRouteKey];
        AVAudioSessionPortDescription  *previousOutput = previousRoute.outputs[0];
        if ([previousOutput.portType isEqualToString:AVAudioSessionPortHeadphones]) {
            
            // 停止播放音乐
            [self.player stop];
        }
    }
}

5. 音频录制

一般情况存在录音功能,必然会有播放功能,所以不能使用默认的录制音频会话,应该使用既可以录制又能播放的 AVAudioSessionCategoryPlayAndRecord

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSURL *url = [NSURL fileURLWithPath:[@"Users/mayan/Desktop" stringByAppendingPathComponent:@"voice.caf"]];
    NSDictionary *settings = @{
                               AVFormatIDKey            : @(kAudioFormatAppleIMA4),
                               AVSampleRateKey          : @22050.0f,
                               AVNumberOfChannelsKey    : @1,
                               };
    self.recorder = [[AVAudioRecorder alloc] initWithURL:url settings:settings error:nil];
    
    if (self.recorder) {
        self.recorder.delegate = self;
        [self.recorder prepareToRecord];
    }
}

- (IBAction)record {

    [self.recorder record];
}

- (IBAction)recordFinish {

    [self.recorder stop];
}

// 音频录制完成调用
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag
{
    if (flag) {
        
        // 一般把录制好的音频复制或者剪切到目的文件夹下
        NSURL *srcURL = self.recorder.url;
        NSURL *destURL = [NSURL URLWithString:@"目的文件路径/音频文件名称.caf"];
        
        [[NSFileManager defaultManager] copyItemAtURL:srcURL toURL:destURL error:nil];
    }
}

在录制音频过程中,Core Audio Format(CAF)通常是最好的容器格式,因为它和内容无关可以保存 Core Audio 支持的任何音频格式。在设置字典中指定的键值信息也值得讨论一番:

音频格式

AVFormatIDKey 定义了写入内容的音频格式,下面是常用格式:

  • kAudioFormatLinearPCM:将未压缩的音频流写入到文件中。保真度最高,文件也最大;
  • kAudioFormatMPEG4AAC(AAC) 或 kAudioFormat-AppleIMA4(Apple IMA4):文件显著缩小,还能保证高质量音频

采样率

AVSampleRateKey 定义了录音器的采样率,采样率定义了对输入的模拟音频信号每一秒的采样数。采样率越高,越能得到高质量的内容,不过文件相对越大。标准的采样率:8000、16000、22050、44100

通道数

AVNumberOfChannelsKey 定义记录音频内容的通道数。默认值 1 是单声道录制,2 是立体声录制。除非使用外部硬件录制,否则应该创建单声道录音。

6. 音频测量

AVAudioPlayer 和 AVAudioRecorder 中最实用的功能就是对音频进行测量。Audio Metering 可以读取音频的平均分贝和峰值分贝数据,并使用这些数据以可视化方式将声音大小呈现给用户。

首先在初始化 AVAudioPlayer 或 AVAudioRecorder 时应做出如下设置

self.player.meteringEnabled = YES;
self.recorder.meteringEnabled = YES;

点击音频播放或者音频录制,开始测量

- (void)startMeterTimer
{
    [self.link invalidate];
    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateMeter)];
    self.link.frameInterval = 4;  // 时间间隔为刷新率的 1/4
    [self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)stopMeterTimer
{
    [self.link invalidate];
    self.link = nil;
}

下面分别是音频播放情况下测量、音频录制情况下测量

- (void)updateMeter
{
    [self.player updateMeters];  // 刷新
    
    CGFloat num1 = [self.player averagePowerForChannel:0];
    CGFloat num2 = [self.player peakPowerForChannel:0];
    
    NSLog(@"平均分贝:%f, 峰值分贝:%f", num1, num2);
}
- (void)updateMeter
{
    [self.recorder updateMeters];  // 刷新
    
    CGFloat num1 = [self.recorder averagePowerForChannel:0];
    CGFloat num2 = [self.recorder peakPowerForChannel:0];
    
    NSLog(@"平均分贝:%f, 峰值分贝:%f", num1, num2);
}

上面方法都会返回用于表示声音分贝(dB)等级的浮点值,这个值的范围是 -160dB ~ 0dB

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

推荐阅读更多精彩内容