IOS音视频(三)AVFoundation 播放和录音

  • 回顾一下,上一篇博客“IOS音视频(二)AVFoundation视频捕捉” 中讲解了关于AVFoundation框架对摄像头视频的捕捉能力,并用两个demo(一个OC的Demo,一个Swift的Demo)详细讲解了AVFoundation处理摄像头视频捕捉的能力,可以捕捉静态图片,也可以捕捉实时视频流,可以录制视频,还提供了接口操作闪光灯,开启手电筒模式等等功能。但是这些讲解都是基于苹果官方文档一些接口讲解的,学习了这些我们虽然知道了怎么调用苹果的接口实现相关功能,但是我们并不知道其中的原理性知识,后续的博客中将从视频采集,视频编码,视频解码等原理方面详细讲解,由于时间问题,写完一篇博客基本上要花费一天的时间,所以进度有些慢,博客中也参考了很多大神的博客,但是这些博客是我们平时收集在印象笔记中的,可能有时候忘记添加原始地址了,后续有时间会补上。

  • 感觉说了好多废话,好了,先简单介绍一下:本篇博客主要讲解AVFoundation在音频处理方面的能力。

  • 在音频方面,我们主要是指录制音频和播放音频两个重要的能力,在AVFoundation框架中,等为我们提供了相关类,很容易就实现这些功能。但是我们需要理解一下原理性的知识,便于我们开发中遇到问题就可以及时解决。

  • 在开始讲解录音和播放音频之前,有必要学习一下音频的一些理论知识,方便我们更好的理解。

  • 本篇博客的录音Demo点击这里下载:AVFoundation录音Demo swift版本, AVFoundation录音播放demo OC版本

1. 音频理论知识

1.1 声音的物理性质

  • 声音是波

声音是如何产生的呢?

  1. 声音是有物体振动而产生的。


    振动产生声音

    如图所示,当小球撞击到音叉的时候,音叉会产生振动,对周围的空气产生挤压,从而产生声音。声音是一种压力波,当演奏乐器、拍打一扇门或者敲击桌面时,它们的振动都会引起空气有节奏的振动,使周围的空气产生疏密变化,形成疏密相间的纵波(可以理解为石头落入水中激起的波纹),由此就产生了声波,这种现象会一直延续到振动消失为止。

  • 声波的三要素:
  1. 声波的三要素是频率、振幅和波形,频率代表音阶的高低,振幅代表响度,波形代表音色。
  2. 频率(过零率)越高,波长就越短。低频声响的波长则较长,所以其可以更容易地绕过障碍物,因此能量衰减就小,声音就会传得远,反之则会得到完全相反的结论。
  3. 响度其实就是能量大小的反映,用不同的力度敲击桌子,声音的大小势必也会不同。在生活中,分贝常用于描述响度的大小。声音超过一定的分贝,人类的耳朵就会受不了。

人类耳朵的听力有一个频率范围,大约是20Hz~20kHz,不过,即使是在这个频率范围内,不同的频率,听力的感觉也会不一样,业界非常著名的等响曲线,就是用来描述等响条件下声压级与声波频率关系的,人耳对3~4kHz频率范围内的声音比较敏感,而对于较低或较高频率的声音,敏感度就会有所减弱;在声压级较低时,听觉的频率特性会很不均匀;而在声压级较高时,听觉的频率特性会变得较为均匀。频率范围较宽的音乐,其声压以80~90dB为最佳,超过90dB将会损害人耳(105dB为人耳极限)。

  • 声音的传播介质

吉他是通过演奏者拨动琴弦来发出声音的,鼓是通过鼓槌敲击鼓面发出声音的,这些声音的产生都离不开振动,就连我们说话也是因为声带振动而产生声音的。既然都是振动产生的声音,那为什么吉他、鼓和人声听起来相差这么大呢?这是因为介质不同。我们的声带振动发出声音之后,经过口腔、颅腔等局部区域的反射,再经过空气传播到别人的耳朵里,这就是我们说的话被别人听到的过程,其中包括了最初的发声介质与颅腔、口腔,还有中间的传播介质等。事实上,声音的传播介质很广,它可以通过空气、液体和固体进行传播;而且介质不同,传播的速度也不同,比如,声音在空气中的传播速度为340m/s,在蒸馏水中的传播速度为1497m/s,而在铁棒中的传播速度则可以高达5200m/s;不过,声音在真空中是无法传播的。

  • 吸音和隔音原理
  1. 吸音主要是解决声音反射而产生的嘈杂感,吸音材料可以衰减入射音源的反射能量,从而达到对原有声源的保真效果,比如录音棚里面的墙壁上就会使用吸音棉材料。
  2. 隔音主要是解决声音的透射而降低主体空间内的吵闹感,隔音棉材料可以衰减入射音源的透射能量,从而达到主体空间的安静状态,比如KTV里面的墙壁上就会安装隔音棉材料。
  • 回音

当我们在高山或空旷地带高声大喊的时候,经常会听到回声(echo)。之所以会有回声是因为声音在传播过程中遇到障碍物会反弹回来,再次被我们听到。但是,若两种声音传到我们的耳朵里的时差小于80毫秒,我们就无法区分开这两种声音了,其实在日常生活中,人耳也在收集回声,只不过由于嘈杂的外界环境以及回声的分贝(衡量声音能量值大小的单位)比较低,所以我们的耳朵分辨不出这样的声音,或者说是大脑能接收到但分辨不出。

  • 共鸣

自然界中有光能、水能,生活中有机械能、电能,其实声音也可以产生能量,例如两个频率相同的物体,敲击其中一个物体时另一个物体也会振动发声。这种现象称为共鸣,共鸣证明了声音传播可以带动另一个物体振动,也就是说,声音的传播过程也是一种能量的传播过程。

1.2 数字音频

1.2.1 采样、量化和编码

  • 为了将模拟信号数字化,需要3个过程分别是采样、量化和编码。

  • 首先要对模拟信号进行采样,所谓采样就是在时间轴上对信号进行数字化。根据奈奎斯特定理(也称为采样定理),按比声音最高频率高2倍以上的频率对声音进行采样(也称为AD转换)。

  • 对于高质量的音频信号,其频率范围(人耳能够听到的频率范围)是20Hz~20kHz,所以采样频率一般为44.1kHz,这样就可以保证采样声音达到20kHz也能被数字化,从而使得经过数字化处理之后,人耳听到的声音质量不会被降低。而所谓的44.1kHz就是代表1秒会采样44100次。

  • 那么,具体的每个采样又该如何表示呢?

  • 这就是量化

量化是指在幅度轴上对信号进行数字化,比如用16比特的二进制信号来表示声音的一个采样,而16比特(一个short)所表示的范围是[-32768,32767],共有65536个可能取值,因此最终模拟的音频信号在幅度上也分为了65536层。如下图所示:


量化采样过程
  • 既然每一个量化都是一个采样,那么这么多的采样该如何进行存储呢?

  • 这就需要-编码。所谓编码,就是按照一定的格式记录采样和量化后的数字数据,比如顺序存储或压缩存储,等等。

  • 这里面涉及了很多种格式,通常所说的音频的裸数据格式就是脉冲编码调制(Pulse Code Modulation,PCM)数据。描述一段PCM数据一般需要以下几个概念:量化格式(sampleFormat)、采样率(sampleRate)、声道数(channel)。以CD的音质为例:量化格式(有的地方描述为位深度)为16比特(2字节),采样率为44100,声道数为2,这些信息就描述了CD的音质。

  • 而对于声音格式,还有一个概念用来描述它的大小,称为数据比特率,即1秒时间内的比特数目,它用于衡量音频数据单位时间内的容量大小。

  • 而对于CD音质的数据,比特率为多少呢?

计算如下:
44100 * 16 * 2 = […]

  • 那么在1分钟里,这类CD音质的数据需要占据多大的存储空间呢?
  1. 计算如下:
    1378.125 * 60 / 8 / 1024 = 10.09MB
  2. 如果sampleFormat更加精确(比如用4字节来描述一个采样),或者sampleRate更加密集(比如48kHz的采样率),那么所占的存储空间就会更大,同时能够描述的声音细节就会越精确。存储的这段二进制数据即表示将模拟信号转换为数字信号了,以后就可以对这段二进制数据进行存储、播放、复制,或者进行其他任何操作。
  • 肯定有小伙伴有疑问,麦克风是如何采集声音的呢?

麦克风里面有一层碳膜,非常薄而且十分敏感。前面介绍过,声音其实是一种纵波,会压缩空气也会压缩这层碳膜,碳膜在受到挤压时也会发出振动,在碳膜的下方就是一个电极,碳膜在振动的时候会接触电极,接触时间的长短和频率与声波的振动幅度和频率有关,这样就完成了声音信号到电信号的转换。之后再经过放大电路处理,就可以实施后面的采样量化处理了。


麦克风内部视图
  • 那么什么是分贝呢?

分贝是用来表示声音强度的单位。日常生活中听到的声音,若以声压值来表示,由于其变化范围非常大,可以达到六个数量级以上,同时由于我们的耳朵对声音信号强弱刺激的反应不是线性的,而是呈对数比例关系,所以引入分贝的概念来表达声学量值。所谓分贝是指两个相同的物理量(例如,A1和A0)之比取以10为底的对数并乘以10(或20),即:N= 10 * lg(A1 / A0)
分贝符号为“dB”,它是无量纲的。式中A0是基准量(或参考量),A1是被量度量。

1.2.2 音频编码

  • 前面提到了CD音质的数据采样格式,曾计算出每分钟需要的存储空间约为10.1MB,如果仅仅是将其存放在存储设备(光盘、硬盘)中,可能是可以接受的,但是若要在网络中实时在线传播的话,那么这个数据量可能就太大了,所以必须对其进行压缩编码
  • 压缩编码的基本指标之一就是压缩比,压缩比通常小于1(否则就没有必要去做压缩,因为压缩就是要减小数据容量)。
  • 压缩算法包括有损压缩无损压缩
  1. 无损压缩是指解压后的数据可以完全复原。在常用的压缩格式中,用得较多的是有损压缩,
  2. 有损压缩是指解压后的数据不能完全复原,会丢失一部分信息,压缩比越小,丢失的信息就越多,信号还原后的失真就会越大。根据不同的应用场景(包括存储设备、传输网络环境、播放设备等),可以选用不同的压缩编码算法,如PCM、WAV、AAC、MP3、Ogg等。
  • 压缩编码的原理

压缩编码的原理:实际上是压缩掉冗余信号,冗余信号是指不能被人耳感知到的信号,包含人耳听觉范围之外的音频信号以及被掩蔽掉的音频信号等。人耳听觉范围之外的音频信号在前面已经提到过,所以在此不再赘述。而被掩蔽掉的音频信号则主要是因为人耳的掩蔽效应,主要表现为频域掩蔽效应与时域掩蔽效应,无论是在时域还是频域上,被掩蔽掉的声音信号都被认为是冗余信息,不进行编码处理。

  • 压缩编码有哪些格式呢?

主要有:WAV编码, MP3编码, AAC编码, Ogg编码。

  • WAV编码:
  1. PCM(脉冲编码调制)是Pulse Code Modulation的缩写。前面已经介绍过PCM大致的工作流程,而WAV编码的一种实现(有多种实现方式,但是都不会进行压缩操作)就是在PCM数据格式的前面加上44字节,分别用来描述PCM的采样率、声道数、数据格式等信息。
  2. 特点:音质非常好,大量软件都支持。
  3. 适用场合:多媒体开发的中间文件、保存音乐和音效素材。
  • MP3编码:
  1. MP3具有不错的压缩比,使用LAME编码(MP3编码格式的一种实现)的中高码率的MP3文件,听感上非常接近源WAV文件,当然在不同的应用场景下,应该调整合适的参数以达到最好的效果。
  2. 特点:音质在128Kbit/s以上表现还不错,压缩比比较高,大量软件和硬件都支持,兼容性好。
  3. 适用场合:高比特率下对兼容性有要求的音乐欣赏。
  • AAC编码
  1. AAC是新一代的音频有损压缩技术,它通过一些附加的编码技术(比如PS、SBR等),衍生出了LC-AACHE-AACHE-AAC v2三种主要的编码格式。LC-AAC是比较传统的AAC,相对而言,其主要应用于中高码率场景的编码(≥80Kbit/s);HE-AAC(相当于AAC+SBR)主要应用于中低码率场景的编码(≤80Kbit/s);而新近推出的HE-AAC v2(相当于AAC+SBR+PS)主要应用于低码率场景的编码(≤48Kbit/s)。事实上大部分编码器都设置为≤48Kbit/s自动启用PS技术,而>48Kbit/s则不加PS,相当于普通的HE-AAC。
  2. 特点:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码。
  3. 适用场合:128Kbit/s以下的音频编码,多用于视频中音频轨的编码。
  • Ogg编码:
  1. Ogg是一种非常有潜力的编码,在各种码率下都有比较优秀的表现,尤其是在中低码率场景下。Ogg除了音质好之外,还是完全免费的,这为Ogg获得更多的支持打好了基础。Ogg有着非常出色的算法,可以用更小的码率达到更好的音质,128Kbit/s的Ogg比192Kbit/s甚至更高码率的MP3还要出色。但目前因为还没有媒体服务软件的支持,因此基于Ogg的数字广播还无法实现。Ogg目前受支持的情况还不够好,无论是软件上的还是硬件上的支持,都无法和MP3相提并论。
  2. 特点:可以用比MP3更小的码率实现比MP3更好的音质,高中低码率下均有良好的表现,兼容性不够好,流媒体特性不支持。
  3. 适用场合:语音聊天的音频消息场景。

1.3 音频编解码

  • 只要是Core Audio框架支持的音频编解码, AVFoundation框架都可以支持, 这意味着 AVFoundation能够支持大量不同格式的资源。然而在不用线性PCM音频的情况下,更多的只能使用AAC。

  • 高级音频编码AAC是H.264标准相应的音频处理方式,目前已经成为音频流和下载的音频资源中最主流的编码方式。这种格式比MP3格式有着显著的提升,可以在低比特率的前提下提供更高质量的音频,是在Web上发布和传播的音频格式中最为理想的。此外,AAC没有来自证书和许可方面的限制,这一限制曾经在MP3格式上饱受诟病。

  • AVFoundationCore Audio框架都提供对MP3数据解码支持,但是不支持对其进行编码。

  • AVFoundation框架最开始是一个仅针对音频的框架,该框架的前身在IOS2.2版本中引入,只包含一个出来音频播放的类;在iOS 3.0中,苹果公司增加了音频录制功能。虽然这些类都是目前该框架中最古老的,但他们任然是最常用的几个类。

  • 上面讲解了这么多音频理论知识,接下来我们将从 AVFoundation的两个基础类AVAudioPlayerAVAudioRecorder来讲解音频播放和音频录制功能。

2. 播放音频

2.1 AVAudioPlayer简介

  • IOS中能播放音频的类有很多,这里我们主要讲解用AVAudioPlayer类来实现音频播放功能。
  • 这里先来简单介绍一下AVAudioPlayer
  • AVAudioPlayer类实战IOS2.2就开始支持的。它是一种提供从文件或存储器中播放音频数据的音频播放器。除非您正在播放从网络流中捕获的音频或需要非常低的I/O延迟,否则请将该类用于音频播放。
  • AVAudioPlayer继承自NSObject
  • AVAudioPlayer提供了以下功能:
  1. 播放任何持续时间的声音
  2. 播放来自文件或内存缓冲区的声音
  3. 循环播放
  4. 同时播放多个声音,每个音频播放器一个声音,精确同步
  5. 控制相对播放级别、立体声定位和播放速度
  6. 查找声音文件中的特定点,该点支持快进和快退等应用程序特性.
  7. 获取可用于回放级别测量的数据.
  • AVAudioPlayer类允许你在iOS和macOS中播放任何音频格式的声音。你可以实现一个委托来处理中断(比如iOS上的来电),并在声音播放完毕时更新用户界面。委派方法在AVAudioPlayerDelegate中描述。该类使用Objective-C声明的属性特性来管理关于声音的信息,比如声音时间轴中的播放点,以及访问播放选项,比如音量和循环。
  • 要在iOS上为回放配置一个适当的音频会话,请参见AVAudioSessionAVAudioSessionDelegate
  • AVAudioPlayerDelegate主要提供了两个代理回调方法:

(1)当音频播放完成时,会调用下面的回调方法:

optional func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, 
                             successfully flag: Bool)

(2)当音频播放器在播放过程中遇到解码错误时会调用下面这个回调方法:

optional func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, 
                                       error: Error?)
  • 要播放、暂停或停止音频播放器,调用它的一个回放控制方法可以使用下面这些接口:


//异步播放声音。
func play() -> Bool

//以异步方式播放声音,从音频输出设备时间轴中的指定点开始播放。
func play(atTime: TimeInterval) -> Bool


//暂停播放;声音准备好从它停止的地方恢复播放。
func pause()

//停止播放并撤消播放所需的设置。
func stop()

//通过预加载音频播放器的缓冲区来准备播放。
func prepareToPlay() -> Bool

//淡入到一个新的卷在一个特定的持续时间。
func setVolume(Float, fadeDuration: TimeInterval)

//一个布尔值,指示音频播放器是否正在播放(真)或不(假)。
var isPlaying: Bool

//音频播放器的播放音量,线性范围从0.0到1.0。
var volume: Float

//音频播放器的立体声平移位置。
var pan: Float

//音频播放器的播放速率。
var rate: Float

//一个布尔值,用于指定是否为音频播放器启用播放速率调整。
var enableRate: Bool

//一个声音返回到开始的次数,到达结束时,重复播放。
var numberOfLoops: Int

//音频播放器的委托对象。
var delegate: AVAudioPlayerDelegate?

//一种协议,它允许一个委托响应音频中断和音频解码错误,并完成声音的回放。
protocol AVAudioPlayerDelegate

//音频播放器的设置字典,包含与播放器相关的声音信息。
var settings: [String : Any]
  • 此外AVAudioPlayer提供了管理关于声音的信息的接口:

//声音中与音频播放器相关联的音频通道的数量。
var numberOfChannels: Int

//与音频播放器相关联的AVAudioSessionChannelDescription对象的数组
var channelAssignments: [AVAudioSessionChannelDescription]?

//与音频播放器相关联的声音的总持续时间(以秒为单位).
var duration: TimeInterval

//播放点,以秒为单位,在与音频播放器关联的声音的时间轴内。
var currentTime: TimeInterval

//音频输出设备的时间值,以秒为单位。
var deviceCurrentTime: TimeInterval

//与音频播放器关联的声音的URL。
var url: URL?

//包含与音频播放器相关联的声音的数据对象。
var data: Data?

//当前音频播放器的UID。
var currentDevice: String?

//缓冲区中音频的格式。
var format: AVAudioFormat
  • 此外AVAudioPlayer还提供了使用音频电平测量的接口:

//一个布尔值,用于指定音频播放器的音频电平测量开/关状态。
var isMeteringEnabled: Bool

//返回给定频道的平均功率(以分贝为单位)。
func averagePower(forChannel: Int) -> Float

//返回给定频道的峰值功率,以分贝表示所播放的声音。
func peakPower(forChannel: Int) -> Float

//返回刷新音频播放器所有频道的平均和峰值功率值。
func updateMeters()

//格式标识符。
let AVFormatIDKey: String

//采样率,用赫兹表示,表示为NSNumber浮点值。一般为8000,和16K
let AVSampleRateKey: String

//用NSNumber整数值表示的通道数。
let AVNumberOfChannelsKey: String

2.2 AVAudioPlayer实现音频播放

  • AVAudioPlayer 构建于 Core Audio 的 C-based Audio Queue Services 最顶层,局限性在于无法从网络流播放音频,不能访问原始音频样本,不能满足非常低的时延。

2.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.2 对播放进行控制

AVAudioPlayer 的 play 可以播放音频,stop 和 pause 都可以暂停播放,但是 stop 会撤销调用 prepareToPlay 所做的设置。从上面介绍的AVAudioPlayer属性可以知道如何设置。具体设置 如下:

  1. 修改播放器的音量:播放器音量独立于系统音量,音量或播放增益定义为 0.0(静音)到 1.0(最大音量)之间的浮点值
  2. 修改播放器的 pan 值:允许使用立体声播放声音,pan 值从 -1.0(极左)到 1.0(极右),默认值 0.0(居中)
  3. 调整播放率:0.5(半速)到 2.0(2 倍速)
  4. 设置 numberOfLoops 实现无缝循环:-1 表示无限循环(音频循环可以是未压缩的线性 PCM 音频,也可以是 AAC 之类的压缩格式音频,MP3 格式不推荐循环)
  5. 音频计量:当播放发生时从播放器读取音量力度的平均值和峰值

2.2.3 播放/停止音频

  • 播放音频
        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 的时间。

  • 停止播放音频
        for (AVAudioPlayer *player in self.players) {
            [player stop];
            player.currentTime = 0.0f;
        }

暂停时需要将 audioPlayer 的 currentTime 值设置为 0.0,当音频正在播放时,这个值用于标识当前播放位置的偏移,不播放音频时标识重新播放音频的起始偏移。

  • 调用play方法可以实现立即播放音频的功能,pause方法可以对播放暂停,那么可想而知stop方法可以停止播放行为。有趣的是,pausestop方法在应用程序外面看来实现的功能都是停止当前播放行为。下一时间里我们调用play方法,通过pausestop方法停止的音频都会继续播放。
  • 这两者最主要的区别在底层处理上。调用stop方法撤销调用prepareToPlay时所做的设置,而调用pause方法则不会。

2.2.4 修改音量、播放速率

  • 我们可以简单通过设置AVAudioPlayer对象的属性来修改音量,播放速率,是否循环播放等,如下:
player.enableRate = YES;
player.rate = rate;
player.volume = volume;
player.pan = pan;
player.numberOfLoops = -1;
  • 修改播放器的音量:播放器的音量独立于系统音量,我们可以通过对播放器音量的处理实现很多有趣的效果,比如声音渐隐效果。音量或播放增益定义为0.0(静音)到 1.0 (最大音量)之间的浮点值。
  • 修改播放器的pan值: 允许使用立体声播放声音,播放器的pan值是有一个浮点数表示,范围从-1.0(极左)到1.0(极右)。默认值为0.0(居中)
  • 调整播放率: IOS5版本中加入了一个强大功能,即允许用户在不改变音调的情况下调整播放率,范围从0.5(半速)到2.0(2倍速)。如果正记录一首复杂的音乐或语音,放慢速度会有很大的帮助;当我们想快速浏览一份政府常规会议内容是,加速播放就很有帮助。
  • 通过设置 numberOfLoops 属性实现音频无缝循环 : 给这个属性设置一个大于0 的数,可以实现播放器n次循环播放。相反,为该属性赋值-1会导致播放器无限循环。

2.2.5 配置音频会话

  • 由于音频会话是所有应用公用的,所有一般在程序启动时设置,是通过AVAudioSession单例来设置的。

  • 如果希望应用程序播放音频时屏蔽静音切换动作,需要设置会话分类为 AVAudioSessionCategoryPlayback,但是如果希望按下锁屏后还可以播放,就需要在 plist 里加入一个 Required background modes 类型的数组,在其中添加 App plays audio or streams audio/video using AirPlay。

  • 后面讲解录音时还会详细讲解配置音频会话。

2.2.6 处理中断事件

  • 中断事件是指电话呼入、闹钟响起、弹出 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 则表明可以恢复播放音频了。

在准备为出现的中断时间采取动作前,首先要得到中断出现的通知,注册应用程序的AVAudioSession发送的通知AVAudioSessionInterruptionNofication.

override init() {
        super.init()

        let nc = NotificationCenter.default

        nc.addObserver(self, selector: #selector(handleInterruption(_:)), name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance())
        nc.addObserver(self, selector: #selector(handleRouteChange(_:)), name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance())
    }

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

@objc func handleInterruption(_ notification: Notification) {
        if let info = (notification as NSNotification).userInfo {
            let type = info[AVAudioSessionInterruptionTypeKey] as! AVAudioSession.InterruptionType
            if type == .began {
                stop()
                delegate?.playbackStopped()
            } else {
                let options = info[AVAudioSessionInterruptionOptionKey] as! AVAudioSession.InterruptionOptions
                if options == .shouldResume {
                    play()
                    delegate?.playbackBegan()
                }
            }
        }
    }

在handleInterrupation方法中,首先通过检索AVAudioSessionInterrupationTypeKey的值确定中断类型(type),我们调用stop方法,并通过调用委托函数playbackStopped方法向委托通知中断状态。很重要的一点是当通知被接收是,音频会话已经被终止,且AVAudioPlayer实例处于暂停状态。调用控制启动stop方法只能更新内部状态,并不能停止播放。

2.2.7 处理线路改变

  • 在 iOS 设备上添加或移除音频输入、输出线路时会引发线路改变,有多重原因会导致线路的变化,比如用户插入耳机或者短裤USB麦克风。当这些事件发生时,音频会根据这些情况改变输入或者输出线路,同时AVFoundation会广播一个描述该变化的通知给所有相关侦听器。
  • 最佳实践是,插入耳机时播放动作不改动,拔出耳机时应当暂停播放。
  • 要处理线路改变,我们只能通过系统通知来实现。
  • 首先需要监听通知,代码如下:
        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];
        }
    }
}

接收到通知后要做的第一件事情是判断线路变更发生的原因。查看保存userinfo字典中的表示原因的AVAudioSessionRouteChangeReasonKey值。这个返回值是一个用于表示变化原因的无符号整数。通过原因可以推断出不同的事件。比如有新设备接入或者改变音频会话类型,不过我们需要特殊注意的是耳机短裤这个事件,这个事件的对应原因为:AVAudioSessionRouteChangeReasonOldDeviceUnavailable

知道有设备断开连接后,需要向userinfo字典提出请求,以获得其中用于描述前一个线路的AVAudioSessionPortDescription。线路的描述信息是整合在一个熟人NSArray和一个输出NSArray中。在上述情况下,你需要从线路描述中找出第一个输出接口并判断其是否为耳机接口。如果是,则停止播放,并调用委托函数的playbackStopeed方法。

这里 AVAudioSessionPortHeadphones 只包含了有线耳机,无线蓝牙耳机需要判断 AVAudioSessionPortBluetoothA2DP 值。

2.2.8 音频播放处理

2.2.8.1 播放本地音频

  • 我们可以使用AVAudioPlayer播放本地音乐,而播放远程音频需要使用AVPlayer
#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>

@interface ViewController ()
@property (nonatomic,strong)AVAudioPlayer *player;
@end

@implementation ViewController

-(AVAudioPlayer *)player{
    if (_player == nil) {
        //1.音乐资源
        NSURL *url = [[NSBundle mainBundle]URLForResource:@"235319.mp3" withExtension:nil];
        //2.创建AVAudioPlayer对象
        _player = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:nil];
        //3.准备播放(缓冲,提高播放的流畅性)
        [_player prepareToPlay];
    }
    return _player;
}
//播放(异步播放)
- (IBAction)play {
    [self.player play];
}
//暂停音乐,暂停后再开始从暂停的地方开始
- (IBAction)pause {
    [self.player pause];
}
//停止音乐,停止后再开始从头开始
- (IBAction)stop {
    [self.player stop];
    //这里要置空
    self.player = nil;
}  
@end
  • 而我们播放本地的短音频(“短音频”是指通常在程序中的播放时长为1~2秒)一般直接使用AudioServicesCreateSystemSoundID(url, &_soundID);即可,这样代价最小。
#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>

@interface ViewController ()
@property (nonatomic,assign)SystemSoundID soundID;
@end

@implementation ViewController

-(SystemSoundID)soundID{
    if (_soundID == 0) {
        //生成soundID
        CFURLRef url = (__bridge CFURLRef)[[NSBundle mainBundle]URLForResource:@"buyao.wav" withExtension:nil];
        AudioServicesCreateSystemSoundID(url, &_soundID);
    }
    return _soundID;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //播放音效
    AudioServicesPlaySystemSound(self.soundID);//不带震动效果
    //AudioServicesPlayAlertSound(<#SystemSoundID inSystemSoundID#>)//带震动效果
}

@end

2.2.8.2 播放远程音频

  • 使用AVPlayer既可以播放本地音乐也可以播放远程(网络上的)音乐

  • 播放音频流的OC代码如下:

@interface ViewController ()
@property (nonatomic,strong)AVPlayer *player;
@end

@implementation ViewController

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //播放音乐
    [self.player play];
}

#pragma mark - 懒加载
-(AVPlayer *)player{
    if (_player == nil) {
            
    //想要播放远程音乐,只要把url换成网络音乐就可以了
    //NSURL *url = [NSURL URLWithString:@"http://cc.stream.qqmusic.qq.com/C100003j8IiV1X8Oaw.m4a?fromtag=52"];

    //1.本地的音乐资源
    NSURL *url = [[NSBundle mainBundle]URLForResource:@"235319.mp3" withExtension:nil];

    //2.这种方法设置的url不可以动态的切换
    _player = [AVPlayer playerWithURL:url];

    //2.0创建一个playerItem,可以通过改变playerItem来进行切歌
    //AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:url];
    //2.1这种方法可以动态的换掉url
    //_player = [AVPlayer playerWithPlayerItem:playerItem];
    
    //AVPlayerItem *nextItem = [AVPlayerItem playerItemWithURL:nil];
    //通过replaceCurrentItemWithPlayerItem:方法来换掉url,进行切歌
    //[self.player replaceCurrentItemWithPlayerItem:nextItem];
    
    }
    return _player;
}
@end
  • 播放音频流的Swift代码如下:
//初始化音频播放,返回音频时长
//播放器相关
var playerItem:AVPlayerItem!
var audioPlayer:AVPlayer!

var audioUrl:String = "" {
    didSet{
        self.setupPlayerItem()
    }
} // 音频url

func initPlay() {
    //初始化播放器
    audioPlayer = AVPlayer()
    //监听音频播放结束
    NotificationCenter.default.addObserver(self, selector: #selector(playItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: AudioRecordManager.shared().playerItem)
    
}

//设置资源
private func setupPlayerItem() {
    guard let url = URL(string: audioUrl) else {
        return
    }
    self.playerItem = AVPlayerItem(url: url)
    self.audioPlayer.replaceCurrentItem(with: playerItem)
}

//获取音频时长
func getDuration() -> Float64 {
    if AudioRecordManager.shared().playerItem == nil {
        return 0.0
    }
    let duration : CMTime = playerItem!.asset.duration
    let seconds : Float64 = CMTimeGetSeconds(duration)
    return seconds
}
func getCurrentTime() -> Float64 {
    if AudioRecordManager.shared().playerItem == nil {
        return 0.0
    }
    let duration : CMTime = playerItem!.currentTime()
    let seconds : Float64 = CMTimeGetSeconds(duration)
    return seconds
}

//播放结束
var audioPlayEndBlock:(()->())?
func playItemDidReachEnd(notifacation:NSNotification) {
    audioPlayer?.seek(to: kCMTimeZero)
    if let block = audioPlayEndBlock {
        block()
    }
}

//播放
func playAudio() {
    if audioPlayer != nil {
        audioPlayer?.play()
    }
}

//暂停
var audioStopBlock:(()->())?
func stopAudio() {
    if audioPlayer != nil {
        audioPlayer?.pause()
        if let block = audioStopBlock {
            block()
        }
    }
}

//销毁
func destroyPlayer() {
    if AudioRecordManager.shared().playerItem != nil {
        AudioRecordManager.shared().audioPlayer?.pause()
        AudioRecordManager.shared().playerItem?.cancelPendingSeeks()
        AudioRecordManager.shared().playerItem?.asset.cancelLoading()
    }
} 

3. 录制音频

3.1 AVAudioRecorder 简介

  • AVAudioRecorder 是AVFoundation框架提供的在应用程序中提供音频录制功能的类。它也是直接继承NSObject在 IOS3.0才提供支持。
class AVAudioRecorder : NSObject
  1. 持续录音,直到用户停止
  2. 指定的持续时间的录音
  3. 暂停并继续录音
  4. 获取可用于提供电平测量的输入声级数据
  • 在iOS系统中,录制的音频来自用户内置麦克风或耳机麦克风连接的设备。在macOS中,音频来自系统的默认音频输入设备,由用户在系统首选项中设置。

  • 您可以为音频记录器实现一个委托对象,以响应音频中断和音频解码错误,并完成录制。

  • 要配置录音,包括诸如位深度、比特率和采样率转换质量等选项,请配置音频记录器的设置字典。使用设置中描述的设置键。

var settings: [String : Any] { get }
  • 只有在显式地调用prepareToRecord()方法或通过开始录制隐式地调用它之后,录音机设置才有效。音频设置键在音频设置和格式中描述。
  • 管理有关录音的信息Setting字典主要有下面这些key:

//指示录音机是否正在录音的布尔值。
var isRecording: Bool

//与录音机关联的音频文件的URL。
var url: URL

//与记录器相关联的AVAudioSessionChannelDescription对象的数组。
var channelAssignments: [AVAudioSessionChannelDescription]?

//时间,以秒为单位,从录音开始算起。
var currentTime: TimeInterval

//音频记录器所在的主机设备的时间(以秒为单位)。
var deviceCurrentTime: TimeInterval

//缓冲区中音频的格式。
var format: AVAudioFormat

3.2 AVAudioSession 简介

音频会话在应用程序和操作系统之间扮演者中间人的角色。它提供了一种简单实用的方法是OS得知应用程序应该如何与IOS音频环境进行交互。你不需要了解与音频硬件交互的细节,只需要对应用程序的行为语义上的描述即可。这一点使得你可以指明应用程序的一般音频行为,并可以把对该行为的管理委托给音频会话,这样OS系统就可以对用户使用音频的体验进行最适当的管理。

  • 所有IOS应用程序都具有音频会话,无论其是否使用。默认音频会话来自于以下一些预配置:
  1. 激活了音频播放,但是音频录制未激活。
  2. 当用户切换响铃/静音开发到静音模式是,应用程序播放的所有音频都会消失。
  3. 当设备显示解锁屏幕时,所有后台播放的音频都会处于静音状态。
  4. 当应用程序播放音频时,所有后台播放的音频都会处于静音状态。
  • 要实现录音,我们需要配置音频会话,因为系统默认是Solo Ambient模式,要录音需要设置为Play and Record模式。
  • 若要配置适当的录音会话,请参考AVAudioSession和AVAudioSessionDelegate。
  • 要实现录音功能,我们有必要来理解下AVAudioSession类,AVAudioSession类也是直接继承NSObject
class AVAudioSession : NSObject
  • 音频会话充当应用程序和操作系统之间的中介,进而充当底层音频硬件之间的中介。您使用一个音频会话来与操作系统通信应用程序音频的一般性质,而不详细说明特定的行为或与音频硬件所需的交互。您将这些细节的管理委托给音频会话,以确保操作系统能够最好地管理用户的音频体验。
  • 所有iOS、tvOS和watchOS应用程序都有一个默认的音频会话,并预先配置了以下行为:
  1. 它支持音频回放,但不允许音频录制(tvOS不支持音频录制)。
  2. 在iOS系统中,将铃声/静音开关设置为静音模式,应用程序播放的任何音频都会被静音。
  3. 在iOS系统中,锁定设备会使应用程序的音频静音。
  4. 当应用程序播放音频时,它会静音任何其他背景音频。

3.2.1 音频会话模式

  • 虽然默认的音频会话提供了有用的行为,但它通常不提供媒体应用程序需要的音频行为。要更改默认行为,需要配置应用程序的音频会话类别。
  • 你可以使用七种可能的类别(参见音频会话类别和模式),但是回放是回放应用程序最常用的一种。这个类别表明音频播放是你的应用程序的核心功能。当你指定这个类别时,你的应用程序的音频将继续与铃声/静音开关设置为静音模式(只有iOS)。使用这个类别,你也可以播放背景音频,如果你在图片背景模式中使用音频,AirPlay和图片。有关更多信息,请参见启用背景音频
  • 音频会话7种类别行为如下:
类别 来电静音/锁屏静音 中断非混合应用程序的音频 允许音频输入(录制)和输出(回放) 作用
AVAudioSessionCategoryAmbient Yes No Output only 游戏,效率应用程序
AVAudioSessionCategorySoloAmbient (默认) Yes Yes Output only 游戏,效率应用程序
AVAudioSessionCategoryPlayback No Yes by default; no by using override switch Output only 音频和视频播放器
AVAudioSessionCategoryRecord No (锁屏后继续录音) Yes Input only 录音机,音频捕捉
AVAudioSessionCategoryPlayAndRecord No Yes by default; no by using override switch Input and output VoIP,语音聊天
AVAudioSessionCategoryMultiRoute No Yes Input and output 使用外部的高级A/V应用程序

注意:当铃声/静音开关设置为静音并锁定屏幕时,为了让你的应用程序继续播放音频,请确保UIBackgroundModes音频键已添加到你的应用程序的信息中。plist文件。这个要求是除了你使用正确的类别。

  • 模式及相关类别:
模式标识符 兼容的类别 作用
AVAudioSessionModeDefault All 默认音频会话模式
AVAudioSessionModeMoviePlayback AVAudioSessionCategoryPlayback 如果您的应用正在播放电影内容,请指定此模式
AVAudioSessionModeVideoRecording AVAudioSessionCategoryPlayAndRecord,AVAudioSessionCategoryRecord 如果应用正在录制电影,则选此模式
AVAudioSessionModeVoiceChat AVAudioSessionCategoryPlayAndRecord 如果应用需要执行例如 VoIP 类型的双向语音通信则选择此模式
AVAudioSessionModeGameChat AVAudioSessionCategoryPlayAndRecord 该模式由Game Kit 提供给使用 Game Kit 的语音聊天服务的应用程序设置
AVAudioSessionModeVideoChat AVAudioSessionCategoryPlayAndRecord 如果应用正在进行在线视频会议,请指定此模式
AVAudioSessionModeSpokenAudio AVAudioSessionCategoryPlayback 当需要持续播放语音,同时希望在其他程序播放短语音时暂停播放此应用语音,选取此模式
AVAudioSessionModeMeasurement AVAudioSessionCategoryPlayAndRecord,AVAudioSessionCategoryRecord,AVAudioSessionCategoryPlayback 如果您的应用正在执行音频输入或输出的测量,请指定此模式

3.2.2 配置音频会话

3.2 AVAudioRecorder 实现录音功能

3.2.1 录音功能细节

3.2.1.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.2.1.2 录音时通用设置参数配置

  • 音频格式
  1. AVFormatIDKey 键对应写入内容的音频格式,它有以下可选值:
    kAudioFormatLinearPCM
    kAudioFormatMPEG4AAC
    kAudioFormatAppleLossless
    kAudioFormatAppleIMA4
    kAudioFormatiLBC
    kAudioFormatULaw
  2. kAudioFormatLinearPCM 会将未压缩的音频流写入文件,文件体积大。kAudioFormatMPEG4AAC 和 kAudioFormatAppleIMA4 的压缩格式会显著缩小文件,并保证高质量音频内容。但是要注意,制定的音频格式与文件类型应该兼容,例如 wav 格式对应 kAudioFormatLinearPCM 值。
  • 采样率

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

  • 通道数

AVNumberOfChannelsKey 指示定义记录音频内容的通道数,指定默认值1意味着使用单声道录制,设置2意味着使用立体声录制。除非使用外部硬件录制,否则通常选择单声道(也就是AVNumberOfChannelsKey=1)。

  • 编码位元深度

AVEncoderBitDepthHintKey 指示编码位元深度,从 8 到 32。

  • 音频质量

AVEncoderAudioQualityKey 指示音频质量,可选值有:
AVAudioQualityMin,
AVAudioQualityLow,
AVAudioQualityMedium,
AVAudioQualityHigh,
AVAudioQualityMax。

3.2.1.3 AVAudioRecorder 对象初始化

  • 创建 AVAudioRecorder 需要以下信息:
  1. 用于写入音频的本地文件 URL
  2. 用于配置录音会话键值信息的字典
  3. 用于捕捉错误的 NSError
  • 初始化时,需要调用prepareToRecord 方法执行底层 Audio Queue 初始化必要过程,并在指定位置创建文件。
  • 初始化代码如下:
        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]);
        }
  • 上面代码我们记录到tmp目录中的一个名为memo.cat的文件,在录制音频过程中,.caf (Core Audio Format)格式通常是最好的容器格式,因为它和内容无关并且可以保持Core Audio支持的任何音频格式。

  • 此外我们需要定义录音设置,以便适应Apple IMA4作为音频格式,采样率44.1kHz,位深度16位,单声道录制。这些设置考虑了质量和文件大小的平衡。

3.2.1.4 录音文件保存

3.2.2 录音完整代码

  • OC 录音代码如下:
@interface ViewController ()
@property (nonatomic,strong) AVAudioRecorder *recorder;
@end

@implementation ViewController
 //懒加载
 -(AVAudioRecorder *)recorder{
      if (_recorder == nil) {
          //1.创建沙盒路径
          NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
          //2.拼接音频文件
          NSString *filePath = [path stringByAppendingPathComponent:@"123.caf"];
          //3.转换成url  file://
          NSURL *url = [NSURL fileURLWithPath:filePath];
          //4.设置录音的参数
          NSDictionary *settings = @{
                                     /**录音的质量,一般给LOW就可以了
                                      typedef NS_ENUM(NSInteger, AVAudioQuality) {
                                      AVAudioQualityMin    = 0,
                                      AVAudioQualityLow    = 0x20,
                                      AVAudioQualityMedium = 0x40,
                                      AVAudioQualityHigh   = 0x60,
                                      AVAudioQualityMax    = 0x7F
                                      };*/
                                     AVEncoderAudioQualityKey : [NSNumber numberWithInteger:AVAudioQualityLow],
                                     AVEncoderBitRateKey : [NSNumber numberWithInteger:16],
                                     AVSampleRateKey : [NSNumber numberWithFloat:8000],
                                     AVNumberOfChannelsKey : [NSNumber numberWithInteger:2]
                                     };
          NSLog(@"%@",url);
          //第一个参数就是你要把录音保存到哪的url
          //第二个参数是一些录音的参数
          //第三个参数是错误信息
          self.recorder = [[AVAudioRecorder alloc]initWithURL:url settings:settings error:nil];
      }
      return _recorder;
  }
  //开始录音
  - (IBAction)start:(id)sender {
      [self.recorder record];
  }
  //停止录音
  - (IBAction)stop:(id)sender {
      [self.recorder stop];
  }
@end
  • Swift版本录音代码如下:
var recorder: AVAudioRecorder?
var player: AVAudioPlayer?
let file_path = PATH_OF_CACHE.appending("/record.wav")
var mp3file_path = PATH_OF_CACHE.appending("/audio.mp3")

private static var _sharedInstance: AudioRecordManager?
private override init() { } // 私有化init方法

/// 单例
///
/// - Returns: 单例对象
class func shared() -> AudioRecordManager {
    guard let instance = _sharedInstance else {
        _sharedInstance = AudioRecordManager()
        return _sharedInstance!
    }
    return instance
}

/// 销毁单例
class func destroy() {
    _sharedInstance = nil
}

//开始录音
func beginRecord() {
    let session = AVAudioSession.sharedInstance()
    //设置session类型
    do {
        try session.setCategory(AVAudioSessionCategoryPlayAndRecord)
    } catch let err{
        Dprint("设置类型失败:\(err.localizedDescription)")
    }
    //设置session动作
    do {
        try session.setActive(true)
    } catch let err {
        Dprint("初始化动作失败:\(err.localizedDescription)")
    }
    //录音设置,注意,后面需要转换成NSNumber,如果不转换,你会发现,无法录制音频文件,我猜测是因为底层还是用OC写的原因
    let recordSetting: [String: Any] = [AVSampleRateKey: NSNumber(value: 44100.0),//采样率
        AVFormatIDKey: NSNumber(value: kAudioFormatLinearPCM),//音频格式
        AVLinearPCMBitDepthKey: NSNumber(value: 16),//采样位数
        AVNumberOfChannelsKey: NSNumber(value: 2),//通道数
        AVEncoderAudioQualityKey: NSNumber(value: AVAudioQuality.min.rawValue)//录音质量
    ];
    //开始录音
    do {
        let url = URL(fileURLWithPath: file_path)
        recorder = try AVAudioRecorder(url: url, settings: recordSetting)
        recorder!.prepareToRecord()
        recorder!.record()
        Dprint("开始录音")
    } catch let err {
        Dprint("录音失败:\(err.localizedDescription)")
    }
}

var stopRecordBlock:((_ audioPath:String,_ audioFormat:String)->())?
//结束录音
func stopRecord() {
    let session = AVAudioSession.sharedInstance()
    //设置session类型
    do {
        try session.setCategory(AVAudioSessionCategoryPlayback)
    } catch let err{
        Dprint("设置类型失败:\(err.localizedDescription)")
    }
    //设置session动作
    do {
        try session.setActive(true)
    } catch let err {
        Dprint("初始化动作失败:\(err.localizedDescription)")
    }
    
    if let recorder = self.recorder {
        if recorder.isRecording {
            Dprint("正在录音,马上结束它,文件保存到了:\(file_path)")
            let manager = FileManager.default
            if manager.fileExists(atPath: mp3file_path) {
                do {
                    try manager.removeItem(atPath: mp3file_path)
                } catch let err {
                    Dprint(err)
                }
            }
            AudioWrapper.audioPCMtoMP3(file_path, andPath: mp3file_path)
            Dprint("正在录音,马上结束它,文件保存到了:\(mp3file_path)")
            if let block = stopRecordBlock {
                block("/audio.mp3","mp3")
            }
        }else {
            Dprint("没有录音,但是依然结束它")
        }
        recorder.stop()
        self.recorder = nil
    }else {
        Dprint("没有初始化")
    }
}

//取消录制
func cancelRecord() {
    if let recorder = self.recorder {
        if recorder.isRecording {
            recorder.stop()
            self.recorder = nil
        }
    }
}

///初始化
func initLocalPlay() {
    do {
        Dprint(mp3file_path)
        player = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: mp3file_path))
        player?.delegate = self
        Dprint("歌曲长度:\(player!.duration)")
    } catch let err {
        Dprint("播放失败:\(err.localizedDescription)")
    }
}

//播放本地音频文件
func play() {
    player?.play()
}
//暂停本地音频
func stop() {
    player?.pause()

}
var localPlayFinishBlock:(()->())?
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
    if let block = AudioRecordManager.shared().localPlayFinishBlock {
        block()
    }
}
//进度条相关
func progress()->Double{
    
    return (player?.currentTime)!/(player?.duration)!
}

4. 可视化音频信号

  • AVAudioRecorder 和 AVAudioPlayer 中最强大和最实用的功能就是对音频进行测量。Audio Metering 可以让开发者读取音频的平均分贝和峰值分贝塑胶,并实用这些数据以可视化方式将声音的大小呈现给最终用户。
  • 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

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

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

  • 接下来可以实时获取到分贝平均值和峰值:
- (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来更新。
  • CADisplayLinkNSTimer类似,不过它可以与显示刷新率自动同步。

5. 异常处理

参考书籍:《AV Foundation开发秘籍》,《音视频开发进阶指南基于Android与iOS平台的实践》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345