AVFoundation开发秘籍笔记:第2章 播放和录制音频

在iOS 3.0中,苹果公司增加了音频录制功能。
AVAudioPlayer:音频播放
AVAudioRecorder:音频录制。

2.1 Mac 和ios的音频环境

Mac系统的音频环境非常自由和灵活--大部分情况都由用户控制。

iOS平台上的情况:
你在iPhone播放音乐的时候,此时有电话拨入,音乐会立即停止并处于暂停状态,此时听到的是手机呼叫的铃声。挂掉电话后,刚才的音乐声再次响起。戴上了耳机。当音乐继续播放时音频输出到了耳机里。当听完这首音乐摘下耳机后,你会发现声音自动转回内置扬声器并处于暂停状态。

iOS系统提供了一个可管理的音频环境(managed audio environment),音频会话(audiosession)。

2.2 理解音频会话

音频会话在应用程序和操作系统之间扮演着中间人的角色。它提供了一种简单实用的方法使OS得知应用程序应该如何与iOS音频环境进行交互。

所有IOS应用程序都具有音频会话,无论其是否使用。默认音频会话来自于以下一些预配置:
●激活了音频播放,但是音频录制未激活。
●当用户切换响铃/静音开关到“静音”模式时,应用程序播放的所有音频都会消失。
●当设备显示解锁屏幕时,应用程序的音频处于静音状态。
●当应用程序播放音频时,所有后台播放的音频都会处于静音状态。

2.2.1 音频会话分类

确定了应用程序的核心音频行为后,选择合适的分类。

如果开发者需要更复杂的功能,其中一些分类可以通过使用options和modes方法进一步自定义开发。

2.2.2 配置音频会话

音频会话可以修改,但通常只配置一次, 在application:didFinishLaunchingWithOptions:方法中配置。

- (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提供了与应用程序音频会话交互的接口,所以开发者需要取得指向该单例的指针。通过设置合适的分类,开发者可为音频的播放指定需要的音频会话,其中定制一些行为。最后告知该音频会话激活该配置。

2.3 使用 AVAudioPlayer播放音频

AVAudioPlayer:播放、循环甚至音频计量,但使用的是非常简单并友好的Objective-C接口。除非你需要从网络流中播放音频、需要访问原始音频样本或者需要非常低的时延,否则AVAudioPlayer都能胜任。

2.3.1 创建 AVAudioPlayer

有两种方法可以创建一个 AVAudioPlayer:

- (nullable instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError;
- (nullable instancetype)initWithData:(NSData *)data error:(NSError **)outError;

使用包含要播放音频的内存版本的NSData,或者本地音频文件的NSURL。

示例:

NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"dance" withExtension:@"mp3"];
    // Must maintain a strong reference to player
    self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:nil];
    if (self.player) {
        [self .player prepareToPlay];
    } else {
        NSLog(@"创建失败");
    }

如果返回一个有效的播放实例,建议调用prepareToPlay方法。这样音频硬件并预加载Audio Queue的缓冲区。调用prepareToPlay这个动作是可选的,当调用play方法时会隐性激活,不过在创建时准备播放器可以降低调用play方法和听到声音输出之间的延时。

2.3.2 对播放进行控制

播放实例包含了所有开发者期望的对播放行为进行控制的方法。
play:立即播放音频
pause:播放暂停
stop:停止播放
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会导致播放器无限循环。
●进行音频计量:当播放发生时从播放器读取音量力度的平均值及峰值。可将这些数据提供给VU计量器或其他可视化元件,向用户提供可视化的反馈效果。

2.4 创建 Audio Looper

Audio Looper应用 程序(如图2-1所示),同步播放三个播放器实例,通过控制每个播放器的音量等级和立体声方面的pan值将这些声音混合,进而控制整体播放率。

管理音频播放器和控制其播放行为的代码写在一个 名为THPlayerController的单独类中。

代码清单2-1 THPlayerControlier 接口

@interface THPlayerController : NSObject
@property (nonatomic, getter = isPlaying) BOOL playing;
// Global methods
- (void)play;
- (void)stop;
- (void)adjustRate:(float)rate;
// Player-specific methods
- (void)adjustPan:(float)pan forPlayerAtIndex:(NSUInteger)index;
- (void)adjustVolume:(float)volume forPlayerAtIndex:(NSUInteger)index;
@end

播放控制器定义了play、stop和adjustRate方法,一起对三个播放器实例的播放行为进行控制,还为每个播放器定义了诸如控制pan值和音量的几个方法。

代码清单2-2给出了init方法和一个私有方法,这两个方法用于创建播放器实例。

代码清单2-2 THPlayerController 初始化

#import "THPlayerController.h"
#import 

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

@implementation THPlayerController

- (id)init {
    self = [super init];
    if (self) {
        AVAudioPlayer *guitarPlayer = [self playerForFile:@"guitar"];
        AVAudioPlayer *bassPlayer = [self playerForFile:@"bass"];
        AVAudioPlayer *drumsPlayer = [self playerForFile:@"drums"];
        _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; // 循环次数
        player.enableRate = YES;
        [player prepareToPlay];
    } else {
        NSLog(@"Error creating player: %@", [error localizedDescription]);
    }
    return player;
}
@end

应用程序包含三个循环: guitar.caf、 bass.caf和drums.caf。通过在init方法中调用私有的playerForFile:方法来为每个文件创建一个播放器实例。该方法会读取文件的URL并创建一个 新的播放器实例。假设初始化过程是顺利的,应用程序会设置循环计数为-1,使得播放器无限循环播放,设置enableRate属性为YES可以对播放率进行控制,最后调用prepareToPlay启动播放。初始化就顺利完成了,下面继续研究播放方法(如代码清单2-3所示)。

代码清单2-3 THPlayerController play方法的实现

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

要对三个播放器实例的播放进行同步,需要捕捉当前设备时间并添加一个小延时,这样就会具有一个从开始播放时间计算的参照时间。通过对每个实例调用playAtTime:方法并传递延时参照时间,遍历播放器数组并开始播放。这就保证了这些播放器在音频播放时始终保持紧密同步。下面继续学习stop方法的实现过程,如代码清单2-4所示。

代码清单2-4 THPlayerController stop方法的实现

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

stop方法很直接,如果音频正在播放,遍历播放器数组并对每一个对象调用stop方法。开发者可能还需要为设置每个播放器的currentTime属性为0.0f,这样做会让播放进度回到音频文件的原点。最终你需要更新playing状态来指明播放已经停止。

用户界面上具有一个高级播放率旋钮,可以让用户在不改变音调的前提下减慢或加快播放速度。速率旋钮调整的最小值为0.5,及减慢一半的速率;最大值为1.5,即以1.5倍速率播 放。adjustRate:方法接收传进来的数值并将其应用于每个播放器(如代码清单2-5所示)。

代码清单2-5 THPlayerController adjustRate:方法的实现

- (void)adjustRate:(float)rate {
    for (AVAudioPlayer *player in self.players) {
        player.rate = rate;
    }
}

现在我们已经实现了所有的功能,开始运行应用程序吧。我们可以对内容进行播放和停止操作,并调整播放速率,三个播放器完美实现同步。

注意:
双击界面上的旋钮可让它重新回到默认值。

控制单独音频
应用程序还支持用户对每个播放器调整音量和pan值。控件都已经配置好,只不过现在还没有实现具体功能,现在我们就为其添加具体功能。实现这些方法的代码如代码清单2-6所示。

代码清单2-6 THPlayerController 的调节音量和pan值的方法

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

代码中的“adjust"方法会接收一个浮点类型的调整值和一个用于表示所修改播放器的索引值。pan值的区间为-1.0(极左)到1.0(极右),音量的区间为0.0(静音)到1.0(最大音量)。

再次运行该应用程序,现在可以带起你的耳机试着调整旋钮感受一下了。

Audio Looper应用程序是应用AVAudioPlayer非常好的示例。

2.5 配置音频会话

进行几组测试,以确保应用程序能够良好运行。如果你还没有这样做,现在就在设备上编译并部署这个应用程序。开始播放音频并切换设备侧面的“铃音/静音”开关,可以听到音频输出在两种状态下切换。第二个测试,当音频正在播放时,点击设备上的Lock按钮,你应该听到声音逐渐消失。由于应用程序的核心功能是播放音频资源,这两个动作都是我们不希望的。

如果你回顾上一节中对AVAudioSession的讨论,会发现所有iOS应用程序都自动带有一个默认音频会话,分类名称为Solo Ambient。这个类型可以播放音频,但对于一个主要功能为音频播放的应用程序来说并不合适。由于默认分类不能提供我们期望的功能,需要对音频会话进行更明确的配置。音频会话通常会在应用程序启动时进行一次配置,所以可将配置代码写在THAppDelegate的application:didFinishlaunchingWithOptions:方法中。如代码清单2-7所示,对Audio Looper应用程序的音频会话进行了合理配置。

代码清单2-7 THAppDelegate 音频会话设置

#import "THAppDelegate.h"
#import 

@implementation THAppDelegate

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

我们首先处理AVAudioSession单例,由于播放音频是应用程序最主要的功能,需要指定AVAudioSessionCategoryPlayback分类。最后通过调用setAction:rror:方法并传递YES值来激活会话。在这个示例中,我们只是简单地将出现的错误打印出来,记得在实际的产品代码中要对错误进行适当处理。

当音频会话正确配置完毕后,重新在你的设备上部署应用程序并运行测试。你会发现切换"铃音/静音"开关不能让声音消失。这个行为看起来不错,那么下面实现在锁定设备时能够继续播放音频的功能。如果按FLock按钮,可以听见音乐持续播放,那么说明是有问题的。我们遇到了与前面相同的情况。是不是应该将分类设置成AVAudioSessionCategoryPlayback呢?答案是否定的。设置成上述分类可以让应用程序在后台播放音频,这是设备锁定时所处的状态,但是我们仍需对应用程序的Info.plist文件进行细微修改来实现这个功能。

在Info.plist文件添加一个新的Required background modes类型的数组,在其中添加名为App plays audio or streams audio/video using AirPlay的选项(如图2-2所示)。


另外,也可以右击ProjectNavigator中的Info.plist文件,在相应的XML部分编辑plist,依次选择Open As | Source Code。将下面条目添加到文件底部的标签前:

UIBackgroundModes
    
        audio
    

添加这一设置表示应用程序现在 允许在后台播放音频内容。再次编译并部署应用程序,再次播放。现在如果按下设备上的Lock按钮,可以听到音频仍然可以从后台播放出来。太棒了!

2.6 处理中断事件

中断:电话呼入、闹钟响起及弹出FaceTime请求等情况。

按照下面的步骤进行测试:
(1)在设备上运行Audio Looper应用程序并播放音频。
(2)当音频处于播放状态时,从另一台设备上向当前设备发起电话呼叫或FaceTime呼叫以制造中断。
(3)在另一台设备上按Decline按钮终止呼叫或FaceTime请求。

当中断发生时,播放中的音频会慢慢消失和暂停。这一效果是自动实现的,我们没有编写任何代码。不过当我们点击另一台设备上的Decline按钮以终止中断时,会发现一些问题。Play/Stop按钮处于不可用状态,音频播放也没有如预期般恢复。显然这不是我们所希望的效果,下面来看如何解决这些问题。

音频会话通知

注册中断通知:AVAudioSessionInterruptionNotification。在控制器的init方法中注册该通知,如代码清单2-8所示。

代码清单2-8注册中 断通知

- (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];
        NSNotificationCenter *nsnc = [NSNotificationCenter defaultCenter];
        [nsnc addObserver:self
                 selector:@selector(handleRouteChange:)
                     name:AVAudioSessionRouteChangeNotification
                   object:[AVAudioSession sharedInstance]];
    }
    return self;
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)handleRouteChange:(NSNotification *)notification {
    
}

推送的通知会包含一个带 有许多重要信息的userInfo字典,根据这个字典可以确定采取哪些合适的操作。

在handleInterruption:方法中,首先通过检索AVAudioSesionInterruptionTypeKey的值确定中断类型(type)。返回值是AVAudioSessionInterruptionType,这是用于表示中断开始或结束的枚举类型(如代码清单2-9所示)。

代码清单2-9确定中断类型

- (void)handleInterruption:(NSNotification *)notification {
    NSDictionary *info = notification.userInfo;
    AVAudioSessionInterruptionType type = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
    if (type == AVAudioSessionInterruptionTypeBegan) {
        // Handle AVAudioSess ionInterruptionTypeBegan
    } else {
        // Handle AVAudioSessionInter rupt ionTypeEnded
    }
}
typedef NS_ENUM(NSUInteger, AVAudioSessionRouteChangeReason)
{
    AVAudioSessionRouteChangeReasonUnknown = 0,// 原因不明;
    AVAudioSessionRouteChangeReasonNewDeviceAvailable = 1,// 有新设备可用,如耳机插入
    AVAudioSessionRouteChangeReasonOldDeviceUnavailable = 2,// 一个旧设备不可用,如耳机拔出
    AVAudioSessionRouteChangeReasonCategoryChange = 3,// 音频类别被改变,如Audio从Play back 变成Play And Record
    
    AVAudioSessionRouteChangeReasonOverride = 4,// 音频线路(route)改变,如类别是Play and Record,输出社诶已经从默认的接收器改变成为扬声器
    AVAudioSessionRouteChangeReasonWakeFromSleep = 6,// 设备从休眠中醒来
    
    AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory = 7,// 没有路径返回当前的类别,如Record雷彪当前没有输入设备
    AVAudioSessionRouteChangeReasonRouteConfigurationChange NS_ENUM_AVAILABLE_IOS(7_0) = 8// 当前输入/输出口没变,但设置修改,如一个端口的数据选择已经改变。
}

当中断出现时,需要采取的一个动作 就是正确更新Play/Stop按钮的状态及相关的标签。THPlayerController不对用户界面进行管理,所以你需要一个 方法将这些中断事件通知给视图控制器。如果你查看THPlayerController头文件的开头,会看到如代码清单2-10所示的协议。

代码清单2-10协议

@protocol THPlayerControllerDelegate 
- (void)playbackStopped;
- (void)playbackBegan;
@end

应用程序的视图控制器已经采用该协议并将其设置为委托。这提供了一种简洁的方法来更新应用程序的用户界面。基于上面的讲解,下面开始处理AVAdioesionInterruptionTypeBegan类型,如代码清单2-11所示。

代码清单2-11开始处理 中断

- (void) handleInterruption: (NSNotification *)notification {
    NSDictionary *info = notification.userInfo;
    AVAudioSessionInterruptionType type =
    [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
    
    if (type == AVAudioSessionInterruptionTypeBegan) {
        [self stop];
        if (self.delegate) {
            [self.delegate playbackStopped] ;
        }
    } else {
        // Handle interruption ended
    }
}

我们调用了stop方法,并通过调用委托函数的playbackStopped方法向委托通知中断状态。很重要的一点是当通知被接收时,音频会话已经被终止,且AVAudioPlayer实例处于暂停状态。 调用控制器的stop方法只能更新内部状态,并不能停止播放。

如果中断类型为AVAudioSessionInterruptionTypeEnded,userInfo字典会包含一个AVAudio-SessionInterruptionOptions值来表明音频会话是否已经重新激活以及是否可以再次播放(如代码清单2-12所示)。

代码清单2-12处理中断结束

- (void) handleInterruption: (NSNotification *)notification {
    NSDictionary *info = notification.userInfo;
    AVAudioSessionInterruptionType type = [info [AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
    
    if (type == AVAudioSessionInterruptionTypeBegan) {
        [self stop];
        if (self.delegate) {
            [self.delegate playbackStopped];
        }
    } else {
        AVAudioSessionInterruptionOptions options =
        [info[AVAudioSessionInterruptionOptionKey] unsignedIntegerValue] ;
        if (options == AVAudioSessionInterruptionOptionShouldResume) {
            [self play];
            if (self.delegate) {
                [self.delegate playbackBegan] ;
            }
        }
    }
}

如果上述代码中options的值为AVAudioSessionInterruptionOptionShouldResume,需要调用控制器的play方法并通过调用自身的playbackBegan方法通知委托。

再次编译和部署应用程序并重新执行之前的中断测试。现在当中断开始时,播放器的用户界面会正确地更新;当应用程序的控制权返回应用程序时,会继续播放音频并且一切正常。

2.7 对线路改变 的响应

在结束AVAudioPlayer知识讲解前,最后一个需要了解的领域就是如何确保应用程序对线路变换做出正确的响应。在iOS设备上添加或移除音频输入、输出线路时,会发生线路改变。有多重原因会导致线路的变化,比如用户插入耳机或断开USB麦克风。当这些事件发生时,音频会根据情况改变输入或输出线路,同时AVAudioSession会广播一个描述 该变化的通知给所有相关的侦听器。为遵循Apple的Human Interface Guidelines(HIG)的相关定义,应用程序应该成为这些相关侦听器中的一员。

下面对Audio Looper应用程序再做一个测试。 开始播放,并在播放期间插入耳机。音频输出线路变为耳机插孔并继续正常播放,这正是我们所期望的效果。保持音频处于播放状态, 断开耳机连接。音频线路再次回到设备的内置扬声器,我们再次听到了声音。虽然线路变化同预期一样,不过按照苹果公司的相关文档,该音频应该处于静音状态。当用户插入耳机时,隐含的意思是用户不希望外界听到具体的音频内容。这就意味着当用户断开耳机时,播放的内容可能需要继续保密,所以我们需要停止音频播放。那么应该怎么做呢?

当线路发生变化时要有通知,那么首先我们需要注册AVAudioSession发送的通知,该通知名为AVAudioSessionRouteChangeNotification.该通知包含一个userInfo字典,该字典带有相应通知发送的原因信息及前一个线路的描述,这样我们就可以确定线路变化的情况了。

收这些变化通知前,需要为这些通知进行注册。与注册中断通知的情况一样,在THPlayerController的init方法中实现注册过程(如代码清单2-13所示)。

代码清单2-13注册线路变化通知

- (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];
        NSNotificationCenter *nsnc = [NSNotificationCenter defaultCenter];
        [nsnc addObserver:self
                 selector:@selector(handleRouteChange:)
                     name:AVAudioSessionRouteChangeNotification
                   object:[AVAudioSession sharedInstance]];
    }
    return self;
}
    
- (void)handleRouteChange:(NSNotification *)notification {
}

接收到通知后要做的第一件事 是判断线路变更发生的原因。查看保存在userInfo字典中的表示原因的AVAudioSessionRouteChangeReasonKey值。这个返回值是一个用于表示变化原因的无符号整数。通过原因可以推断出不同的事件,比如有新设备接入或改变音频会话类型,不过我们需要特殊注意的是耳机断开这个事件。这个事件对应的原因为AVAudioSession,RouteChangeReasonOldDeviceUnavailable(如代码清单2-14所示)。

代码清单2-14判断通知原因

- (void)handleRouteChange:(NSNotification *)notification {
    NSDictionary *info = notification.userInfo;
    AVAudioSessionRouteChangeReason reason =
        [info[AVAudioSessionRouteChangeReasonKey] unsignedIntValue];
    if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
       
    }
}

知道有设备断开连接后,需要向userInfo字典提出请求,以获得其中用于描述前一个线路的AVAudioSessionRouteDescription。线路的描述信息整合在一个输入NSArray和一个输出NSArray中。数组中的元素都是AVAudioSessionPortDescription的实例,用于描述不同的I/O接口属性。在上述情况下,你需要从线路描述中找到第一个输出接口并判断其是否为耳机接口。如果是,则停止播放,并调用委托函数的playbackStopped方法(如代码清单2-15所示)。

代码清单2-15耳机断开时停 止播放

- (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];
            [self.delegate playbackStopped];
        }
    }
}

将最新更改编译和部署到设备上,并测试最新的行为。现在当我们断开耳机时,音频播放会如期望般停止。

当开发者创建媒体应用程序时,处理线路变化和正确对中断做出响应是常见的核心问题。为正确处理各场景而做出的一些努力会在将来用户具体使用时给应用程序带来极大帮助。

Audio Looper应用程序现在全部完成了,下面将注意力转移到AVAudioPlayer的同伴上,即AVAudioRecorder。

2.8 使用 AVAudioRecorder录制音频

2.8.1 创建AVAudioRecorder

创建AVAudioRecorder实例时需要为其提供数据的一些信息,分别是:
●用于表示音频流写入文件的本地文件URL。
●包含用于配置录音会话键值信息的NSDictionary对象。
●用于捕捉初始化阶段各种错误的NSError指针。

上述条件的具体设置代码如下所示:

NSString *tmpDir = NSTemporaryDirectory(); // output directory
NSString *filePath = [tmpDir stringByAppendingPathComponent:@"memo.caf"];
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
NSDictionary *settings = @{
                           AVFormatIDKey : @(kAudioFormatAppleIMA4),
                           AVSampleRateKey : @44100.0f,
                           AVNumberOfChannelsKey : @1,
                           AVEncoderBitDepthHintKey : @16,
                           AVEncoderAudioQualityKey : @(AVAudioQualityMedium)
                           };
NSError *error;
self.recorder = [[AVAudioRecorder alloc] initWithURL:fileURL settings:settings error:&error];
if (self.recorder) {
    [self.recorder prepareToRecord];
} else {
     // Handle error
    NSLog(@"Error: %@", [error localizedDescription]);
}

为成功创建AVRecorder实例,建议调用其prepareToRecord方法。与AVPlayer的prepareToPlay方法类似,这个方法执行底层Audio Queue初始化的必要过程。该方法还在URL参数指定的位置创建一个文件,将录制启动时的延时降到最小。

在设置字典中指定的键值信息也值得讨论一番。 开发者可以使用的完整可用键信息在中定义。大部分的键都专门定义了特有的格式,不过下面介绍的都是一些通用的音频格式。

1.音频格式

AVFormatlIDKey键定义了写入内容的音频格式,下 面的常量都是音频格式所支持的值:

kAudioFormatLinearPCM
kAudioFormatMPEG4AAC
kAudioForma tAppleLossless
kAudioFo rmatAppleIMA4
kAudi oFormatiLBC
kAudioFormatULaw

指定kAudioFormatLinearPCM会将未压缩的音频流写入到文件中。这种格式的保真度最高,不过相应的文件也最大。选择诸如AAC(kAudioFormatMPEG4AAC)或AppleIMA4(kAudioFormat-AppleIMA4)的压缩格式会显著缩小文件,还能保证高质量的音频内容。

注意:
你所指定的音频格式一定要和URL参数定义的文件类型兼容。比如,如果录制一个名为test.wav的文件,隐含的意思就是录制的音频必须满足Waveform Audio FileFormat(WAVE)的格式要求,即低字节序、Linear PCM。为AVFormatIDKey值指定除kAudioFormatLinearPCM之外的值会导致错误。查询NSError的localizedDescription会返回下面的错误信息描述:
The operation couldn't be completed. (OSStatus error 1718449215.)
1718449215错误状态是4字节编码的整数值,'fmt?'意指定义了一种不兼容的音频格式。

2.采样率

AVSampleRateKey用于定义录音器的采样率。采样率定义了对输入的模拟音频信号每一秒内的采样数。在录制音频的质量及最终文件大小方面,采样率扮演着至关重要的角色。使用低采样率,比如8kHz,会导致粗粒度、AM广播类型的录制效果,不过文件会比较小;使用44.1kHz的采样率(CD质量的采样率)会得到非常高质量的内容,不过文件就比较大。对于使用什么采样率最好没有一个明确的定义,不过开发者应该尽量使用标准的采样率,比如8000、16000、22050或44100。最终是我们的耳朵在进行判断。

3.通道数

AVNumberOfChannelsKey用于定义记录音频内容的通道数。指定默认值1意味着使用单声道录制,设置为2意味着使用立体声录制。除非使用外部硬件进行录制,否则通常应该创建单声道录音。

4.指定格式的键

处理Linear PCM或压缩音频格式时,可以定义一些其他指定格式的键。可在Xcode帮助文档中的AV Foundation Audio Settings Constants引用中找到完整的列表。

2.8.2 控制录音过程

创建了一个可用的录音器实例后,就可以开始录音了。AVAudioRecorder包含一些方法可以支持无限时长的录制,比如在未来某一时间点开始录制或录制指定时长的内容等。开发者甚至可以暂停录制并在之后从这个停止的地方继续重启录制。如此灵活的功能非常方便,也使得AVAudioRecorder成为Mac和iOS平台上很多标准录音的首选方法。

现在我们基本了解了AVAudioRecorder,我们一起构建一个简 单的语音备忘应用程序来体会这些功能的实际用法。

2.9 创建Voice Memo座用程序

Voice Memo座用程序(如圉2-3所示)的基本功能是让用户记录一段吾音备忘录,支持在录制过程中晢停,并可以保存多条記彖以各之后播放。在实现了込些基本功能后,我們再考忠如何改善用户体验。可在Chapter2目彖中找到一个名カVoiceMemo_Starter的启动項目。


该应用程序已経預配畳好了基本的用户界面,只是尚未实现具体的功能。我們会在THRecorderController类中实现核心录制行为,但在此之前,下面先对音频会活的属性迸行配置使其支持录音。

2.9.1配置音頻会活

该应用程序的核心功能是彖制并播放珸音各忘录。我们知道不能使用默人的Solo Ambient分炎(AVAudioSessionCategorySoloAmbient),因カ亥分类不支持音頻輸入。由于我們既需要录音又需要対外播放,那么合适的分类应该是AVAudioSessionCategoryPlayAndRecord。在应用程序委托THAppDelegate中配置音頻会活,并使用上述分类,如代碼清単2-16所示。

代碣清単2-16 THAppDelegate 音頻会话没置

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    AVAudioSession *session = [AVAudioSession sharedInstance];
    NSError *error;
    if (![session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]) {
        NSLog(@"Category Error: %@", [error localizedDescription]);
    }
    if (![session setActive:YES error:&error]) {
        NSLog(@"Activation Error: %@", [error localizedDescription]);
    }
    
    return YES;
}

从iOS 7开始,在使用麦克风之前,操作系统要求应用程序必须得到用户的明确许可。当应用程序试图访问麦克风时,操作系统会为用户弹出如图2-4所示的对话框。


如果用户选择"Don’t Allow"按钮,那么所有试图录制的内容都是没有声音的。如果用户改变了想法,允许应用程序录制,需要改变一些设置,具体方法为在Setting应用程序中设置Privacy | Microphone settings。

现在已经配置好音频会话,开始实现录音功能吧。

2.9.2 实现录音功能

代码清单2-17给出了THRecorderController类的接口。

代码清单2-17 THRecorderController 接口

typedef void(^THRecordingStopCompletionHandler)(BOOL);
typedef void(^THRecordingSaveCompletionHandler)(BOOL, id);
@class THMemo;

@interface THRecorderController : NSObject
@property (nonatomic, readonly) NSString *formattedCurrentTime;
// Recorder methods
- (BOOL)record;
- (void)pause;
- (void)stopWithCompletionHandler:(THRecordingStopCompletionHandler)handler;
- (void)saveRecordingWithName:(NSString *)name
            completionHandler:(THRecordingSaveCompletionHandler)handler;
// Player methods
- (BOOL)playbackMemo:(THMemo *)memo;
@end

该类定义了核心的传输操作来控制整个录制周期,并定义了方法来管理之前录制完毕的备忘信息的播放。下面看一下该类的实现代码并开始实现这些方法,如代码清单2-18所示。

代码清单2-18 THRecorderController 类扩展

@interface THRecorderController () 
@property (strong, nonatomic) AVAudioPlayer *player;
@property (strong, nonatomic) AVAudioRecorder *recorder;
@property (strong, nonatomic) THRecordingStopCompletionHandler completionHandler;
@end

在THRecorderController.m文件中定义的类扩展采用AVAudioRecorderDelegate协议。录音器的委托协议定义了一个方法,当录制完成时会发送一个通知。所以得到录制过程完成的信息非常重要,这样你才能让用户为这段录音命名,并保存起来。下面继续研究控制器的init方法是如何实现的,如代码清单2-19所示。

代码清单2-19 THRecorderController init方法

- (id)init {
    self = [super init];
    if (self) {
        NSString *tmpDir = NSTemporaryDirectory();
        NSString *filePath = [tmpDir stringByAppendingPathComponent:@"memo.caf"];
        NSURL *fileURL = [NSURL fileURLWithPath:filePath];
        NSDictionary *settings = @{
                                   AVFormatIDKey : @(kAudioFormatAppleIMA4),
                                   AVSampleRateKey : @44100.0f,
                                   AVNumberOfChannelsKey : @1,
                                   AVEncoderBitDepthHintKey : @16,
                                   AVEncoderAudioQualityKey : @(AVAudioQualityMedium)
                                   };
        NSError *error;
        self.recorder = [[AVAudioRecorder alloc] initWithURL:fileURL settings:settings error:&error];
        if (self.recorder) {
            self.recorder.delegate = self;
            [self.recorder prepareToRecord];
        } else {
            NSLog(@"Error: %@", [error localizedDescription]);
        }
    }
    return self;
}

在init方法中我们对录音器进行了配置,我们记录到tmp目录中一个名为memo.caf的文件。在录制音频的过程中,Core Audio Format(CAF )通常是最好的容器格式,因为它和内容无关并可以保存Core Audio支持的任何音频格式。你需要定义录音设置,以便使用AppleIMA4作为音频格式,采样率为44.1kHz,位深为16位,单声道录制。这些设置考虑了质量和文件大小的平衡。

录音器实例就这样创建完成了,下面继续看一下代码清单2-20中所示的多种传输方法。

代码清单2-20 THRecorderController 传输方法

- (BOOL)record {
    return [self.recorder record];
}

- (void)pause {
    [self.recorder pause];
}

- (void)stopWithCompletionHandler:(THRecordingStopCompletionHandler)handler {
    self.completionHandler = handler;
    [self.recorder stop];
}

- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)success {
    if (self.completionHandler) {
        self.completionHandler(success);
    }
}

这些方法的实现很直接,因为我们将其委托给录音器实例来完成这些工作。不过stopWithCompletionHandler:需要做一下解释。当用户点击Stop按钮时,会调用该方法并传递一个完成块。我们保存对这个块的引用并调用录音器实例的stop方法,录音器实例启动结束音频录制的过程。当音频录制完毕后,录音器调用其委托方法,此时需要执行完成块来告知发起方,以便其采取适当的操作。这种情况下,视图控制器会弹出一个警告框给用户,让用户为录制的内容命名并保存。下面看一下代码清单2-21所示的保存操作。

代码清单2-21 THRecorderController保存方法

- (void)saveRecordingWithName:(NSString *)name completionHandler:(THRecordingSaveCompletionHandler)handler {

    NSTimeInterval timestamp = [NSDate timeIntervalSinceReferenceDate];
    NSString *filename = [NSString stringWithFormat:@"%@-%f.m4a", name, timestamp];

    NSString *docsDir = [self documentsDirectory];
    NSString *destPath = [docsDir stringByAppendingPathComponent:filename];

    NSURL *srcURL = self.recorder.url;
    NSURL *destURL = [NSURL fileURLWithPath:destPath];

    NSError *error;
    BOOL success = [[NSFileManager defaultManager] copyItemAtURL:srcURL toURL:destURL error:&error];
    if (success) {
        handler(YES, [THMemo memoWithTitle:name url:destURL]);
        [self.recorder prepareToRecord];
    } else {
        handler(NO, error);
    }
}

- (NSString *)documentsDirectory {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    return [paths objectAtIndex:0];
}

当用户停止录音,进入对语音备忘命名阶段时,视图控制器会调用saveRecordingWithName:completionHandler:方法。该方法通过唯一文件名将tmp目录中的录制文件复制到Documents目录。如果复制操作成功,会调用完成块,传递回一个新的包含名字和录制音频URL的THMemo实例。之后视图控制器代码使用THMemo实例来创建录制好的语音备忘文件列表。

现在可以编译并运行该应用程序,观察实际操作中这些核心功能的实现效果。可以录制、暂停和继续录制、停止录制并保存到列表中。下面看一下最后还有哪些功能没有实现。

虽然现在能够成功录制音频和创建结果列表,但是还不能回过来播放这些内容。我们之前定义了一个playbackMemo:方法,但尚未实现该功能,现在就来实现吧。实现播放功能会用到前面的AVAudioPlayer(如代码清单2-22所示)。

代码清单2-22 THRecorderController 播放方法

- (BOOL)playbackMemo:(THMemo *)memo {
    [self.player stop];
    self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:memo.url error:nil];
    if (self.player) {
        [self.player play];
        return YES;
    }
    return NO;
}

首先停止现有的播放器(如果存在播放器的话)。我们通过保存在THMemo中的URL创建一个新的AVAudioPlayer实例,调用播放器的play方法。本例忽略所有初始化过程中出现的错误,但在你实际编写代码时,一定要注意应用程序的可靠性。

播放功能做好后,最后一个尚未完成的功能就是展示播放时间。用户界面上有一个标签用于展示时间,不过现在这个标签的内容始终显示00:00:00,还是无法令人信服的功能。AVAudioRecorder具有的currentTime属性可以简单地构建一个用户界面,为用户提供时间反馈信息。该属性返回一个NSTimeInterval,用于指示从录制开始到现在的时间,以秒来计算。NSTimelnterval不适合在用户界面上展示,不过我们可以通过一些处理让其能够美观地展示。下面看一下formattedCurrentTime方法的实现代码(如代码清单2-23所示)。

代码清单2-23 THRecorderController formattedCurrentTime方法

- (NSString *)formattedCurrentTime {
    NSUInteger time = (NSUInteger)self.recorder.currentTime;
    NSInteger hours = (time / 3600);
    NSInteger minutes = (time / 60) % 60;
    NSInteger seconds = time % 60;

    NSString *format = @"%02i:%02i:%02i";
    return [NSString stringWithFormat:format, hours, minutes, seconds];
}

首先我们针对录音器对象读取currentTime,返回值是一个双精度类型的NSTimeInterval。由于我们不需要双精度这么精确的数据,需要将时间保存为一个NSUInteger。之后计算当前时间有效的小时、分钟和秒值,并创建一个HH:mm:ss格式的NSString。

你可能很好奇如何使用这个时间。该方法会返回一个用于呈现的完美格式字符串,但是它只是一个时间点。那么如何随着时间的推移实时更新展示呢?首先想到对currentTime属性使用Key-Value Observing(KVO)。虽然你可能经常在AV Foundation中 使用KVO,不过在AVAudioRecorder(和AVAudioPlayer)的代码中是不可以使用的,因为currentTime属性是不可见的。你可以改用NSTimer并按照一定的时间间隔轮询值。代码清单2-24给出了应用程序的MainViewController类中的代码实现,设置了一个新的NSTimer实例并每半秒一次。

代码清单2-24 MainViewController 时间轮询

- (void)startTimer {
    [self.timer invalidate];
    self.timer = [NSTimer timerWithTimeInterval:0.5
                                         target:self
                                       selector:@selector(updateTimeDisplay)
                                       userInfo:nil
                                        repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

- (void)updateTimeDisplay {
    self.timeLabel.text = self.controller.formattedCurrentTime;
}

运行应用程序并开始录制一段新的音频。 时间现在可以更新了,展示当前录制的时间。如果点击Pause按钮,计时会暂停,再次点击Record后会从上次的停止时间开始继续计时。这是一次非常好的用户体验,告诉用户录制正在进行,并使用户知道已经录制了多长时间。虽然展示时间可以表示录音正在进行,但没有告诉用户是否真的有声音在被录制。如果能将捕捉到的声音信号以可视化方式呈现出来就更好了。幸运的是,我们可以通过Metering(声音计量)来实现这个功能。

2.10 使用 Audio Metering

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

两个类使用的方法都是averagePowerForChannel:和peakPowerForChannel:。两个方法都会返回一“个用于表示声音分贝(dB)等级的浮点值。这个值的范围从表示最大分贝的0Db(full scale)到表示最小分贝或静音的-160dB。

在可以读取这些值之前,首先要通过设置录音器的meteringEnabled属性为YES才可以支持对音频进行测量。这就使得录音器可以对捕捉到的音频样本进行分贝计算。每当需要读取值时,首先需要调用updateMeters方法才能获取最新的值。

在Voice Memo应用程序中,我们需要在AVAudioRecorder实例上进行音频数据的测量,并通过结果数据为用户展示一个简单的可视化效果图。通过averagePowerForChannel:和peakPowerForChannel:两个方法提供的读数,可以得到一个表示分贝等级的浮点值,这是一个描述音量等级的对数单位。示例应用程序具有一个基于Quartz级别的计量视图,用于展示录制过程中的平均分贝和峰值分贝,不过在使用这个视图前,分贝值需要先从对数形式的-160到0范围转换为线性的0到1形式。每次请求这个计量数据,都需要做上述转换,不过更好的解决方案是只计算一次这些变化,之后按照需要进行查找。在示例应用程序中,我们找到一个名为THMeterTable(如代码清单2-25所示)的类。这是一个简化类,是基于Apple的C++的MeterTable类的Objective-C端口,在很多示例项目中都会用到这个类。

代码清单2-25 THMeterTable 的实现

#import "THMeterTable.h"

#define MIN_DB -60.0f
#define TABLE_SIZE 300

@implementation THMeterTable {
    float _scaleFactor;
    NSMutableArray *_meterTable;
}

- (id)init {
    self = [super init];
    if (self) {
        float dbResolution = MIN_DB / (TABLE_SIZE - 1);

        _meterTable = [NSMutableArray arrayWithCapacity:TABLE_SIZE];
        _scaleFactor = 1.0f / dbResolution;

        float minAmp = dbToAmp(MIN_DB);
        float ampRange = 1.0 - minAmp;
        float invAmpRange = 1.0 / ampRange;

        for (int i = 0; i < TABLE_SIZE; i++) {
            float decibels = i * dbResolution;
            float amp = dbToAmp(decibels);
            float adjAmp = (amp - minAmp) * invAmpRange;
            _meterTable[i] = @(adjAmp);
        }
    }
    return self;
}

float dbToAmp(float dB) {
    return powf(10.0f, 0.05f * dB);
}

- (float)valueForPower:(float)power {
    if (power < MIN_DB) {
        return 0.0f;
    } else if (power >= 0.0f) {
        return 1.0f;
    } else {
        int index = (int) (power * _scaleFactor);
        return [_meterTable[index] floatValue];
    }
}

@end

该类创建了一个内部数组,用于保存从计算前的分贝数到使用一定级别分贝解析之后的转换结果。这里使用的解析率为-0.2dB。解析等级可以通过修改MIN_DB和TABLE_SIZE值进行调整。

每个分贝值都通过调用dbToAmp函数转换为线性范围内的值,使其处于范围0(-60dB)到1之间,之后得到一条由这些范围内的值构成的平滑曲线,开平方计算并保存到内部查找表格中。这些值在之后需要时都可以通过调用valueForPower:方法来获取。

下面回到THRecorderController类,对其进行一些修改。 添加一个新的属性来保存计量表格,在init方法中创建它的一个新实例(如代码清单2-26所示)。

代码清单2-26 THRecorderController计量表格设置

@interface THRecorderController () 

@property (strong, nonatomic) AVAudioPlayer *player;
@property (strong, nonatomic) AVAudioRecorder *recorder;
@property (strong, nonatomic) THRecordingStopCompletionHandler completionHandler;
@property (strong, nonatomic) THMeterTable *meterTable;

@end

@implementation THRecorderController

- (id)init {
    self = [super init];
    if (self) {
        NSString *tmpDir = NSTemporaryDirectory();
        NSString *filePath = [tmpDir stringByAppendingPathComponent:@"memo.caf"];
        NSURL *fileURL = [NSURL fileURLWithPath:filePath];

        NSDictionary *settings = @{
                                   AVFormatIDKey : @(kAudioFormatAppleIMA4),
                                   AVSampleRateKey : @44100.0f,
                                   AVNumberOfChannelsKey : @1,
                                   AVEncoderBitDepthHintKey : @16,
                                   AVEncoderAudioQualityKey : @(AVAudioQualityMedium)
                                   };

        NSError *error;
        self.recorder = [[AVAudioRecorder alloc] initWithURL:fileURL settings:settings error:&error];
        if (self.recorder) {
            self.recorder.delegate = self;
            self.recorder.meteringEnabled = YES;
            [self.recorder prepareToRecord];
        } else {
            NSLog(@"Error: %@", [error localizedDescription]);
        }

        _meterTable = [[THMeterTable alloc] init];
    }

    return self;
}

现在添加一个新方法用于返回级别大小。代码清单2-27给出了实现代码。记得在类的头部添加声明。

代码清单2-27 THRecorderController levels方法

- (THLevelPair *)levels {
    [self.recorder updateMeters];
    float avgPower = [self.recorder averagePowerForChannel:0];
    float peakPower = [self.recorder peakPowerForChannel:0];
    float linearLevel = [self.meterTable valueForPower:avgPower];
    float linearPeak = [self.meterTable valueForPower:peakPower];
    return [THLevelPair levelsWithLevel:linearLevel peakLevel:linearPeak];
}

该方法首先调用录音器的updateMeters方法。该方法一定要正好在读取当前等级值之前调用,以保证读取的级别是最新的。之后向通道0请求平均值和峰值数据。通道都是0索引的,由于我们使用单声道录制,只需要询问第一个 声道即可。之后在计量表格中查询线性声音强度值并最终创建一个新的THLevelPair实例。这个类在示例项目中已经存在,它只是用来保存 返回的平均值和峰值数据。

读取音频强度值与请求当前时间类似,当需要最新值时都需要轮询录音器。与请求当前时间时一样,客户端代码可能使用NSTimer。不过由于你希望频繁更新用于展示的计量值以保持动画效果比较平滑,所以可能改用CADisplayLink作为解决方案。CADisplayLink与NSTimer类似,不过它可以与显示刷新率自动同步。如果打开MainViewController.m文件,可看到如代码清单2-28所示的方法。

代码清单2-28 THMainViewController 计量方法

- (void)startMeterTimer {
    [self.levelTimer invalidate];
    self.levelTimer = [CADisplayLink displayLinkWithTarget:self
                                                  selector:@selector(updateMeter)];
    self.levelTimer.frameInterval = 5;
    [self.levelTimer addToRunLoop:[NSRunLoop currentRunLoop]
                          forMode:NSRunLoopCommonModes];
}

- (void)stopMeterTimer {
    [self.levelTimer invalidate];
    self.levelTimer = nil;
    [self.levelMeterView resetLevelMeter];
}

- (void)updateMeter {
    THLevelPair *levels = [self.controller levels];
    self.levelMeterView.level = levels.level;
    self.levelMeterView.peakLevel = levels.peakLevel;
    [self.levelMeterView setNeedsDisplay];
}

当调用startLevelTimer方法时,会创建一个新的CADisplayLink实例, 定期调用updateLevel-Meter方法。默认情况下,调用的时间间隔与刷新率是同步的,但在本例中,设置frameInterval属性的值为4,也就是时间间隔为刷新率的1/4,这足以满足我们的需要了。

updateLevelMeter方法通过查询THRecorderController得到强度信息,将这些信息传递到levelMeterView,然后调用setNeedsDisplay重绘视图。

再次编译并运行应用程序,并开始录制一段新的内容。音量强度值会根据说话者声音的变化而变化,并不断更新展示。应用程序为用户提供了可视化的反馈效果来让他们知道语音备忘录正在被其正录制。

关于声音计量展示需要注意的一点是, 这么做会增加开销。启用计量功能会导致一些额外计算,会影响设备的耗电量。此外,示例中的应用程序使用的是基于Quartz的计量。Quartz是一个卓越的框架,但会占用CPU资源,所以不断地计算音量数据会产生一定的开销。由于该应用程序旨在记录短的语音备忘信息,通常内容不会比说一句“去超市买牛奶”或“下周五与Charlie会面”更长,所以占用CPU资源并不是很大的问题。不过如果要录制长时间的音频内容,可能需要考虑禁用音频计量功能,或者选择一种更高效的绘制方法,比如使用OpenGL ES。

2.11 小结

本章我们见识了AV Foundation的音频类所能提供的强大功能。AVAudioSession作为应用程序和更大的iOS音频环境的中间环节,可通过使用分类在语义上定义应用程序的行为,并且提供工具来观察中断和线路变化。AVAudioPlayer和AVAudioRecorder提供了一种简单但功能强大的接口,用于处理音频的播放和录制。这两个类都构建于Core Audio框架之上,但为在应用程序中实现音频录制和播放提供了一种更便捷的方法。

你可能感兴趣的:(AVFoundation开发秘籍笔记:第2章 播放和录制音频)