播放和录制音频

6,157 阅读7分钟
音频会话

AVAudioSession(即音频会话)参与,音频会话是应用程序和操作系统交互的中介者,通过语义描述来调度系统音频功能。

音频会话分类

AVFoundation定义了7中分类来描述应用程序所使用的音频行为,如下表。

分类作用是否允许混音音频输入音频输出
Ambient游戏、效率应用程序允许允许
Solo Ambient游戏、效率应用程序允许
Playback音频和视频播放器可选允许
Record录音机、音频捕捉允许
Play And RecordVoIP、语音聊天可选允许允许
Audio Processing离线会话和处理
Multi-Route使用外部硬件的高级 A/V 应用程序允许允许

当为应用程序选择合适的分类时,你需要思考一些问题。不如音频播放是核心功能还是次要功能?应用程序的音频是否可以和背景声音相混合?应用程序是否需要捕捉音频输入进行录制或通过网络发送音频?确定了应用程序的核心音频后,选择合适的分类就变得比较容易了。

配置音频会话

AVAudioSession的配置在应用程序的生命周期内可以修改,但通常只会对其配置一次,一般在启动方法application(_:didFinishLaunchingWithOptions:)中进行配置,如下代码所示:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    AVAudioSession *session = [AVAudioSession sharedInstaance];
    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;

}
使用AVAudioPlayer播放音频

AVAudioPlayer是AV Foundation音频播放的首选,也可以说是整个iOS音频播放的首先,其提供了Audio Queue Service中所有核心功能,适合本地播放或对时延无敏感要求的场景。 AVAudioPlayer的创建有两类接口,一类是针对本地路径文件,如init(contentsOf: URL),另一类是针对内存Data,如init(data: Data),初始化参考代码如下所示:

NSURL *fileURL = ...;
self.player = ...
if (self.player) {
    [self.player prepareToPlay];
}

在初始化的时候建议先调用prepareToPlay(),是因为会在调用play()方法之前获取需要的音频硬件并预加载AudioQueue缓冲区,可以降低调用play()方法和听到声音输出之间的时延。如果不调用prepareToPlay()方法,在调用play()方法时也会隐式调用类似于prepareToPlay()之类的方法来激活音频

AVAudioPlayer提供了一些列关于播放的生命周期控制方法,如下所示:

  • play():正式播放音频,可重新恢复被 pause 和 stop 停止的音频
  • pause():暂停音频播放,可从 play 重新恢复,但不清除掉prepareToPlay 的内容
  • stop():停止音频播放,清除掉 prepareToPlay 的内容,可从 play 重新恢复

AVAudioPlayer也提供了一部分音频控制的属性变量,如下所示:

  • volume:修改播放器的音量,范围从0.0~1.0,单位是浮点值
  • pan:播放器的声道,范围从-1(极左)到1.0(极右),默认值是0.0(居中)
  • rate:调整播放速率,从0.5~2.0
  • numberOfLoops: 循环播放次数,n > 0,实现 n 次播放,n = -1,表示无限循环
  • isMeteringEnabled: 是否启动音频计量,即输出音频的可视化计量数据
后台播放

在播放音频时,一个很常用的场景就是让App退出前台后,依然能在后台持续不断的播放音频直至用户停止。

想要在后台也能播放音频其实也不难,只需要两个步骤:

  1. 将音频会话的会话分类设置为Playback,该会话能让音频在手机静音情况下正常播放
  2. 需要在Info.plist文件中添加一个Required background modes类型数组,添加一个item项为App plays audio or streams audio/video using AirPlay

通过以上这两步,便可以让音频播放继续在后台服务。

中断处理

音频在播放时,有时候也会受到电话呼叫或Face Time呼叫,导致音频突然被中断,等到用户拒绝或者呼叫结束时,音频又开始从暂停位置重新播放。这一系列操作的成功实现,需要依赖AVAudioSession的中断通知,通过监听中断通知,当中断开始或中断结束,系统都会告诉外界发生的变化,示例代码如下所示:

func setupNotifications() {
    let nc = NotificationCenter.default
    nc.addObserver(self,
                   selector: #selector(handleInterruption),
                   name: AVAudioSession.interruptionNotification,
                   object: AVAudioSession.sharedInstance)
}

@objc func handleInterruption(notification: Notification) {
    
}
  • 中断通知会包含一个带有重要信息的userInfo,根据这个字典来决定音频的行为,暂停还是播放
  • handleInterruption(notification:):用来集中处理中断通知

handleInterruption(notification:)中处理中断通知的代码示例:

@objc func handleInterruption(notification: Notification) {
    guard let userInfo = notification.userInfo,
        let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
        let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
            return
    }

    switch type {

    case .began:

    case .ended:
       
        guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
        let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
        if options.contains(.shouldResume) {
           
        } else {
            
        }

    default: ()
    }
}
路线更换处理

在使用音乐软件时,经常会在有一个场景,比如从扬声器切换成耳机,或从耳机切换成扬声器等。有时候将耳机切换成扬声器时还继续播放用户的音频内容,是一件十分危险的事,这是因为可能用户目前听到的音频是十分隐私的内容。

正因为有这种需求场景,AVAudioSession提供了线路更换的通知,当手机设备上的线路(如扬声器切换成耳机)发生更改时,会触发AVAudioSession.routeChangeNotification通知给开发者,开发者需遵循《iOS用户体验规范》对音频实现播放或暂停等功能。

监听路线更换的通知示例代码如下所示:

func setupNotifications() {
    let nc = NotificationCenter.default
    nc.addObserver(self,
                   selector: #selector(handleRouteChange),
                   name: AVAudioSession.routeChangeNotification,
                   object: nil)
}

@objc func handleRouteChange(notification: Notification) {
}
  • 当输出音频或者输出设备发生变化时,都会发出该通知
  • notification 包含一个 userInfo 字典,可以获取通知发送的原因及前一个线路的描述

handleRouteChange(notification:) 处理代码示例:

@objc func handleRouteChange(notification: Notification) {
    // 获取线路是否发生变化以及变化的原因
    guard let userInfo = notification.userInfo,
        let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
        let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
            return
    }
    
    // 判断变化原因
    switch reason {

    case .newDeviceAvailable: //找到新设备
        let session = AVAudioSession.sharedInstance()
        headphonesConnected = hasHeadphones(in: session.currentRoute)
    
    case .oldDeviceUnavailable://老设备断开
        // 获取线路描述信息
        if let previousRoute =
            userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
            headphonesConnected = hasHeadphones(in: previousRoute)
        }
    
    default: ()
    }
}

func hasHeadphones(in routeDescription: AVAudioSessionRouteDescription) -> Bool {
    // 找到第一个输出口,判断是否是耳机入口
    return !routeDescription.outputs.filter({$0.portType == .headphones}).isEmpty
}
使用AVAudioRecorder录制音频

AVAudioRecorder是AV Foundation上用来作音频录制的接口之一,是对Audio Queue Services的高级封装,使用AVAudioRecorder录制也不复杂。

AVAudioRecorder创建过程十分简单,主要有两步:

  1. 生成一个URL,附加给AVAudioRecorder作为音频流写入地址
  2. 生成一个字典settings,用来规范音频流的格式,同样附加给AVAudioRecorder

AVAudioRecorder创建过程的示例代码:

do {
    self.recorder = try AVAudioRecorder(url: fileURL, settings: setting)
    self.recorder.delegate = self
    self.recorder.isMeteringEnabled = true
    self.recorder.prepareToRecord()
} catch {
    fatalError(error.localizedDescription)
}
  • prepareToRecord()方法的作用是初始化录制需要的资源,包括创建文件等,将录制启动的延时降到最小
  • setting中的键值信息包含音频格式、采样率等
  • URL文件路径的后缀要和音频格式对应上,否则会存在问题

setting是用来规范音频流的录制格式,常见的键值有:

  • AVFormatIDKey:音频格式
  • AVSampleRateKey:采样率
  • AVNumberOfChannelsKey:通道数
  • AVEncoderBitDepthHintKey:量化位数
  • AVEncoderAudioQualityKey:音质

使用AVAudioRecorder录制音频时,需要将音频会话的会话分类设置成playAndRecord,创建AVAudioRecorder,实现AVAudioRecordeDelegate协议,AVAudioRecordeDelegate的内容很简单,主要是录制完成和录制出错的回调,其他方法基本上已经废弃。

文字转语音

AV Foundation 提供了一个语音合成框架,用于管理语音和语音合成,其中最常用的一个功能就是文字转语音,即AVSpeechSynthesisVoice

要让App带上文字转语音的功能,只需要两步即可:

  1. 创建一个AVSpeechUtterance对象,并附加上内容字符串,语音参数,如声音、速率等
  2. 将AVSpeechUtterance对象附加到即AVSpeechSynthesisVoice实例上即可,有AVSpeechSynthesisVoice实例控制语音的生命周期

代码示例如下:

// 创建AVSpeechUtterance实例,并附加字符内容
let utterance = AVSpeechUtterance(string: "The quick brown fox jumped over the lazy dog.")
utterance.rate = 0.57   // 速率
utterance.pitchMultiplier = 0.8  
utterance.postUtteranceDelay = 0.2
utterance.volume = 0.8  // 音量

let voice = AVSpeechSynthesisVoice(language: "en-GB")
utterance.voice = voice

let synthesizer = AVSpeechSynthesizer()
synthesizer.speak(utterance)

AVSpeechUtterance实例也有相应的一个Delegate,即AVSpeechSynthesizerDelegate,主要是对文字语音过程中的生命周期进行回调,有时间读者们可以自己看看相关的API。