AV Foundation音频录制和播放

1,230 阅读10分钟

概述

本篇文章同大家分享AV Foundation中音频的部分功能,主要有:

  1. 数字音频
  2. 音频播放和录制
  3. 文字转语音

数字音频

声音是由物体振动产生的波,通过介质(如空气)传播并能被人或动物听觉器官感知的波动现象,其本质是物体振动导致介质(如空气)振动,使周围介质(空气)产生疏密变化,形成了疏密相间的纵波。

声音有三个重要的特性,分别是音调、响度、音色。音调是指声音的高低,有频率决定,频率越高音调越高(频率单位是Hz,赫兹),人耳听觉范围是20Hz~20000Hz,20Hz以下成为次声波,20000Hz以上成为超声波;响度是人主观上感觉声音的大小(俗称音量),由振幅和人离声源的距离决定,振幅越大响度越大,人和声源的距离越小,响度越大;音色又称音频,波形决定了声音的音色。

声音的波形如下图所示:

  • 横坐标表示频率
  • 纵坐标表示振幅

数字音频是一种利用数字化手段对声音进行录制、存放、编辑、压缩或播放的技术,因为声音可以通过傅里叶变换,分解成不同频率不同强度的正弦波叠加,为模拟信号转换成电信号,用0、1形式存储在计算机上提供了可能。

数字音频涉及到两个重要的变量,一个是采样的频率,指周期内采集数据转成电信号存储,周期越短,频率越高,声音还原越真实,常用的采样频率有20.5kHz、44.1kHz、48kHz;另外一个是量化位数,即采样后数据存储的最大位数,计算机存储的数据是有限的,对采集后的数据不可能无限的精确,需要舍与得,常用的量化位数有8位、16位、32位.

下图是某段数字音频的采集数据图:

对声音进行数字化,如果不作任何压缩保留原始数据,这样做需要很大的空间,比如一个44.1kHz、16位LPCM音频文件每分钟可能要占用10MB内存空间。因此业界对数字音频压缩推出了许多的音频标准格式,以下介绍常用的几种:

  • WAV:微软开发的音频格式,支持音频压缩,但被经常用来存放未经压缩的无损音频
  • MP3:常用的音频文件压缩技术,用来大幅度降低音频的数量
  • AAC:目前最火的格式之一,相对于MP3,音质更加,文件更小,理想情况下可压缩至原文件的十八分之一
  • APE:无损压缩,可将文件压缩为原来的一半
  • FLAC:无所压缩

音频播放和录制

iOS 在音频这块有许多的框架,有高级框架,如AVKit、AV Foundation,有底层框架,如Core Audio、CoreMedia。AV Foundation正是对这些底层框架进行封装,抽象成高级接口,从而方便了开发者,iOS整个音视频处理框架如下图所示:

音频会话

在使用AV Foundation处理音频时,需要有一个核心对象AVAudioSession(即音频会话)参与,音频会话是应用程序和操作系统交互的中介者,通过语义描述来调度系统音频功能。

音频会话使用时需要配置会话属性,即AVAudioSession.Category(音频会话分类),不同的分类,所拥有的系统权限也不一样,如下表所示:

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

AVAudioSession实例在应用程序中是一个单例对象,开发者无法直接通过构造器初始化一个AVAudioSession实例,而需要通过其单例方法sharedInstance()返回,AVAudioSession的配置在应用程序的生命周期内可以修改,但通常只会对其配置一次,一般在启动方法application(_:didFinishLaunchingWithOptions:)中进行配置,如下代码所示:

do {
    try AVAudioSession.sharedInstance().setCategory(.playAndRecord)
    try AVAudioSession.sharedInstance().setActive(true, options: [])
} catch {
    ....
}

AVAudioPlayer播放音频

AVAudioPlayer是AV Foundation音频播放的首选,也可以说是整个iOS音频播放的首先,其提供了Audio Queue Service中所有核心功能,适合本地播放或对时延无敏感要求的场景。

AVAudioPlayer的创建有两类接口,一类是针对本地路径文件,如init(contentsOf: URL),另一类是针对内存Data,如init(data: Data),初始化参考代码如下所示:

NSURL *fileURL = ...;
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。

总结

本篇文章主要是分享了以下几个内容:

  • 数字音频的本质
  • AVAudioPlayer播放音频,其中涉及到音频会话的分类、后台播放、播放中断、路线更换等处理方式
  • AVAudioRecorder录制音频
  • AV Foundation的文字转语音

这便是AV Foundation在音频处理上的入门内容,通过本片文章,希望可以帮助大家能够快速理解数字音频以及iOS上如何处理数字音频,文章所涉及的内容也有相应的代码供大家参考(源码传送门)。若文章中有什么错误或描述不当的内容,欢迎指正。

参考文献

声音(百度百科)

Responding to Audio Session Interruptions

AVSpeechSynthesizerDelegate

Responding to Audio Session Route Changes

AVFAudio