1 音频会话
1.1 分类 category
iOS 利用音频会话(audio session)实现可管理的音频环境,音频会话提供简单实用的方法使 OS 得知应用程序应该如何与 iOS 音频环境进行交互。AVFoundation 定义了 7 种分类来描述音频行为
分类 | 作用 | 是否允许混音 | 音频输入输出模式 | 是否支持后台 | 是否遵循静音切换 | |
---|---|---|---|---|---|---|
Ambient | 游戏、效率应用程序 | 支持 | O | 不支持 | 不支持 | 遵循 |
Solo Ambient(default) | 游戏、效率应用程序 | 不支持 | O | 不支持 | 遵循 | |
Playback | 音频和视频播放器 | 可选 | O | 支持 | 不遵循 | |
Record | 录音机、音频捕捉 | 不支持 | I | 支持 | 不遵循 | |
Play and Record | VoIP、语音聊天 | 可选 | I/O | 支持 | 不遵循 | |
Audio Processing | 离线会话和处理 | F | 不能播放和录制 | 不遵循 | ||
Multi-Route | 使用外部硬件的高级 A/V 应用程序 | F | I/O | 不遵循 |
同时可以用 options 和 modes 进一步自定义开发。
1.1.1 options
options 有以下选项
- AVAudioSessionCategoryOptionMixWithOthers
支持 AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, 和 AVAudioSessionCategoryMultiRoute,AVAudioSessionCategoryAmbient 自动设置了此选项,AVAudioSessionCategoryOptionDuckOthers 和AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers 也自动设置了此选项。如果使用这个选项激活会话,应用程序的音频不会中断从其他应用程序(如音乐应用程序)的音频,否则激活会话会打断其他音频会话。
- AVAudioSessionCategoryOptionDuckOthers
支持 AVAudioSessionCategoryAmbient,AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, 和 AVAudioSessionCategoryMultiRoute。设置此选项能够在播放音频时低音量听到后台播放的其他音频。整个选项周期与会话激活周期一致。
- AVAudioSessionCategoryOptionAllowBluetooth
支持 AVAudioSessionCategoryRecord,AVAudioSessionCategoryPlayAndRecord;允许蓝牙免提设备启用。当应用使用 setPreferredInput:error: 方法选择了蓝牙无线设备作为输入时,也会自动选择相应的蓝牙设备作为输出,使用 MPVolumeView 对象将蓝牙设备作为输出时,输入也会相应改变。
- AVAudioSessionCategoryOptionDefaultToSpeaker
支持 AVAudioSessionCategoryPlayAndRecord;在没有其他的音频路径(如耳机)可以使用的情况下设置这个选项,会议音频将通过设备的内置扬声器播放。当不设置此选项,并且没有其他的音频输出可用或选择时,音频将通过接收器播放。只有 iPhone 设备都配备有一个接收器; iPad 和 iPod touch 设备,此选项没有任何效果
当你的 iPhone 接有多个外接音频设备时(耳塞,蓝牙耳机等),AudioSession 将遵循 last-in wins 的原则来选择外接设备,即声音将被导向最后接入的设备。
当没有接入任何音频设备时,一般情况下声音会默认从扬声器出来,但有一个例外的情况:在 PlayAndRecord 这个 category 下,听筒会成为默认的输出设备。如果你想要改变这个行为,可以提供 MPVolumeView 来让用户切换到扬声器,也可通过 overrideOutputAudioPort 方法来 programmingly 切换到扬声器,也可以修改 category option 为AVAudioSessionCategoryOptionDefaultToSpeaker。
- AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers
支持 AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, and AVAudioSessionCategoryMultiRoute,设置此选项能使应用程序的音频会话与其他会话混合,但是会中断使用了 AVAudioSessionModeSpokenAudio 模式的会话。其他应用的音频会在此会话启动后暂停,并在此会话关闭后重新恢复。
在用到 AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers 选项时,中断了其他应用的音频后,自己的应用音频结束播放时,若想恢复其他应用的音频,需要在关闭音频会话的时候设置AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation 选项
[session setActive:NO
withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
error:<#Your error object, or nil for testing#>];
- AVAudioSessionCategoryOptionAllowAirPlay
支持 AVAudioSessionCategoryPlayAndRecord,允许会话在 AirPlay 设备上执行。
1.1.2 mode
mode 用于定制化 audio sessions,如果将分类的 mode 设置不合理会执行默认的模式行为,如将 AVAudioSessionCategoryMultiRoute 类别设置 AVAudioSessionModeGameChat 模式。
-
AVAudioSessionModeDefault 默认音频会话模式
-
AVAudioSessionModeVoiceChat 如果应用需要执行例如 VoIP 类型的双向语音通信则选择此模式
-
AVAudioSessionModeVideoChat 如果应用正在进行在线视频会议,请指定此模式
-
AVAudioSessionModeGameChat 该模式由Game Kit 提供给使用 Game Kit 的语音聊天服务的应用程序设置
-
AVAudioSessionModeVideoRecording 如果应用正在录制电影,则选此模式
-
AVAudioSessionModeMeasurement 如果您的应用正在执行音频输入或输出的测量,请指定此模式
-
AVAudioSessionModeMoviePlayback 如果您的应用正在播放电影内容,请指定此模式
-
AVAudioSessionModeSpokenAudio 当需要持续播放语音,同时希望在其他程序播放短语音时暂停播放此应用语音,选取此模式
1.2 配置音频会话
首先获得指向 AVAudioSession 的单例指针,设置合适的分类,最后激活会话。
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]);
}
2. 播放音频
AVAudioPlayer 构建于 Core Audio 的 C-based Audio Queue Services 最顶层,局限性在于无法从网络流播放音频,不能访问原始音频样本,不能满足非常低的时延。
2.1 创建 AVAudioPlayer
可以通过 NSData 或本地音频文件的 NSURL 两种方式创建 AVAudioPlayer。
NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:@"rock" withExtension:@"mp3"];
self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileUrl error:nil];
if (self.player) {
[self.player prepareToPlay];
}
创建出 AVAudioPlayer 后建议调用 prepareToPlay 方法,这个方法会取得需要的音频硬件并预加载 Audio Queue 的缓冲区,当然如果不主动调用,执行 play 方法时也会默认调用,但是会造成轻微播放的延时。
2.2 对播放进行控制
AVAudioPlayer 的 play 可以播放音频,stop 和 pause 都可以暂停播放,但是 stop 会撤销调用 prepareToPlay 所做的设置。
- 修改播放器的音量:播放器音量独立于系统音量,音量或播放增益定义为 0.0(静音)到 1.0(最大音量)之间的浮点值
- 修改播放器的 pan 值:允许使用立体声播放声音,pan 值从 -1.0(极左)到 1.0(极右),默认值 0.0(居中)
- 调整播放率:0.5(半速)到 2.0(2 倍速)
- 设置 numberOfLoops 实现无缝循环:-1 表示无限循环(音频循环可以是未压缩的线性 PCM 音频,也可以是 AAC 之类的压缩格式音频,MP3 格式不推荐循环)
- 音频计量:当播放发生时从播放器读取音量力度的平均值和峰值
2.3 实践
2.3.1 播放音频
NSTimeInterval delayTime = [self.players[0] deviceCurrentTime] + 0.01;
for (AVAudioPlayer *player in self.players) {
[player playAtTime:delayTime];
}
self.playing = YES;
对于多个需要播放的音频,如果希望同步播放效果,则需要捕捉当前设备时间并添加一个小延时,从而具有一个从开始播放时间计算的参照时间。deviveCurrentTime 是一个独立于系统事件的音频设备的时间值,当有多于 audioPlayer 处于 play 或者 pause 状态时 deviveCurrentTime 会单调增加,没有时置位为 0。playAtTime 的参数 time 要求必须是基于 deviveCurrentTime 且大于等于 deviveCurrentTime 的时间。
2.3.2 暂停播放
for (AVAudioPlayer *player in self.players) {
[player stop];
player.currentTime = 0.0f;
}
暂停时需要将 audioPlayer 的 currentTime 值设置为 0.0,当音频正在播放时,这个值用于标识当前播放位置的偏移,不播放音频时标识重新播放音频的起始偏移。
2.3.4 修改音量、pan值、播放速率和循环
player.enableRate = YES;
player.rate = rate;
player.volume = volume;
player.pan = pan;
player.numberOfLoops = -1;
2.4 配置音频会话
如果希望应用程序播放音频时屏蔽静音切换动作,需要设置会话分类为 AVAudioSessionCategoryPlayback,但是如果希望按下锁屏后还可以播放,就需要在 plist 里加入一个 Required background modes 类型的数组,在其中添加 App plays audio or streams audio/video using AirPlay。
2.5 处理中断事件
中断事件是指电话呼入、闹钟响起、弹出 FaceTime 等,中断事件发生时系统会调用 AVAudioPlayer 的 AVAudioPlayerDelegate 类型的 delegate 的下列方法
- (void)audioPlayerBeginInterruption:(AVAudioPlayer *)player NS_DEPRECATED_IOS(2_2, 8_0);
- (void)audioPlayerEndInterruption:(AVAudioPlayer *)player withOptions:(NSUInteger)flags NS_DEPRECATED_IOS(6_0, 8_0);
中断结束调用的方法会带入一个 options 参数,如果是 AVAudioSessionInterruptionOptionShouldResume 则表明可以恢复播放音频了。
2.6 处理线路改变
在 iOS 设备上添加或移除音频输入、输出线路时会引发线路改变,最佳实践是,插入耳机时播放动作不改动,拔出耳机时应当暂停播放。
首先需要监听通知
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];
[self.delegate playbackStopped];
}
}
}
这里 AVAudioSessionPortHeadphones 只包含了有线耳机,无线蓝牙耳机需要判断 AVAudioSessionPortBluetoothA2DP 值。
3. 录制音频
AVAudioRecorder 用于负责录制音频。
3.1 创建 AVAudioRecorder
创建 AVAudioRecorder 需要以下信息
- 用于写入音频的本地文件 URL
- 用于配置录音会话键值信息的字典
- 用于捕捉错误的 NSError
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]);
}
prepareToRecord 方法执行底层 Audio Queue 初始化必要过程,并在指定位置创建文件。
3.2 通用设置参数
- 音频格式
AVFormatIDKey 键对应写入内容的音频格式,它有以下可选值
kAudioFormatLinearPCM
kAudioFormatMPEG4AAC
kAudioFormatAppleLossless
kAudioFormatAppleIMA4
kAudioFormatiLBC
kAudioFormatULaw
kAudioFormatLinearPCM 会将未压缩的音频流写入文件,文件体积大。kAudioFormatMPEG4AAC 和 kAudioFormatAppleIMA4 的压缩格式会显著缩小文件,并保证高质量音频内容。但是要注意,制定的音频格式与文件类型应该兼容,例如 wav 格式对应 kAudioFormatLinearPCM 值。
- 采样率
AVSampleRateKey 指示采样率,即对输入的模拟音频信号每一秒内的采样数。常用值 8000,16000,22050,44100。
- 通道数
AVNumberOfChannelsKey 指示定义记录音频内容的通道数,除非使用外部硬件录制,否则通常选择单声道。
- 编码位元深度
AVEncoderBitDepthHintKey 指示编码位元深度,从 8 到 32。
- 音频质量
AVEncoderAudioQualityKey 指示音频质量,可选值有 AVAudioQualityMin, AVAudioQualityLow, AVAudioQualityMedium, AVAudioQualityHigh, AVAudioQualityMax。
3.3 实践
3.3.1 配置音频会话
录音和播放应用应当使用 AVAudioSessionCategoryPlayAndRecord 分类来配置会话。
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]);
}
注意录音前需要申请麦克风权限。
3.3.2 录音控制
对录音过程的控制如下
[self.recorder record];
[self.recorder pause];
[self.recorder stop];
其中选择了 stop 录音即停止,此时 AVAudioRecorder 会调用其遵循 AVAudioRecorderDelegate 协议的代理的 - (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag
方法。
3.3.3 录音保存
在初始化 AVAudioRecorder 时指定了临时文件目录作为存储音频的位置,音频录制结束时需要保存到 Document 目录下
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);
}
这里调用了 NSFileManager 的 copyItemAtURL 方法将文件内容拷贝到 Document 目录下。
3.3.4 展示时间
记录音频时需要展示时间提示用户当前录制时间,AVAudioRecorder 的 currentTime 属性可以获知当前时间,将其格式化后即可进行展示
- (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];
}
但是需要实时展示时间的话,不能通过 KVO 来解决,只能加入到 NSTimer 中,每 0.5s 执行一次。
[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];
3.3.5 可视化音频信号
AVAudioRecorder 和 AVAudioPlayer 都有两个方法获取当前音频的平均分贝和峰值分贝数据。
- (float)averagePowerForChannel:(NSUInteger)channelNumber; /* returns average power in decibels for a given channel */
- (float)peakPowerForChannel:(NSUInteger)channelNumber; /* returns peak power in decibels for a given channel */
返回值从 -160dB(静音) 到 0dB(最大分贝)。
获取值之前要在初始化播放器或记录器时设置 meteringEnabled 为 YES。
首先需要将 -160 到 0 的分贝值转为 0 到 1 范围内,需要用到下面这个类
@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
接下来可以实时获取到分贝平均值和峰值
- (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 方法。
网友评论