第二章 音频播放和录制
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