回顾一下,上一篇博客“IOS音视频(二)AVFoundation视频捕捉” 中讲解了关于AVFoundation框架对摄像头视频的捕捉能力,并用两个demo(一个OC的Demo,一个Swift的Demo)详细讲解了AVFoundation处理摄像头视频捕捉的能力,可以捕捉静态图片,也可以捕捉实时视频流,可以录制视频,还提供了接口操作闪光灯,开启手电筒模式等等功能。但是这些讲解都是基于苹果官方文档一些接口讲解的,学习了这些我们虽然知道了怎么调用苹果的接口实现相关功能,但是我们并不知道其中的原理性知识,后续的博客中将从视频采集,视频编码,视频解码等原理方面详细讲解,由于时间问题,写完一篇博客基本上要花费一天的时间,所以进度有些慢,博客中也参考了很多大神的博客,但是这些博客是我们平时收集在印象笔记中的,可能有时候忘记添加原始地址了,后续有时间会补上。
感觉说了好多废话,好了,先简单介绍一下:本篇博客主要讲解AVFoundation在音频处理方面的能力。
在音频方面,我们主要是指录制音频和播放音频两个重要的能力,在AVFoundation框架中,等为我们提供了相关类,很容易就实现这些功能。但是我们需要理解一下原理性的知识,便于我们开发中遇到问题就可以及时解决。
在开始讲解录音和播放音频之前,有必要学习一下音频的一些理论知识,方便我们更好的理解。
本篇博客的录音Demo点击这里下载:AVFoundation录音Demo swift版本, AVFoundation录音播放demo OC版本
声音是如何产生的呢?
- 声波的三要素是频率、振幅和波形,频率代表音阶的高低,振幅代表响度,波形代表音色。
- 频率(过零率)越高,波长就越短。低频声响的波长则较长,所以其可以更容易地绕过障碍物,因此能量衰减就小,声音就会传得远,反之则会得到完全相反的结论。
- 响度其实就是能量大小的反映,用不同的力度敲击桌子,声音的大小势必也会不同。在生活中,分贝常用于描述响度的大小。声音超过一定的分贝,人类的耳朵就会受不了。
人类耳朵的听力有一个频率范围,大约是
20Hz~20kHz
,不过,即使是在这个频率范围内,不同的频率,听力的感觉也会不一样,业界非常著名的等响曲线,就是用来描述等响条件下声压级与声波频率关系的,人耳对3~4kHz
频率范围内的声音比较敏感,而对于较低或较高频率的声音,敏感度就会有所减弱;在声压级较低时,听觉的频率特性会很不均匀;而在声压级较高时,听觉的频率特性会变得较为均匀。频率范围较宽的音乐,其声压以80~90dB为最佳,超过90dB将会损害人耳(105dB为人耳极限)。
吉他是通过演奏者拨动琴弦来发出声音的,鼓是通过鼓槌敲击鼓面发出声音的,这些声音的产生都离不开振动,就连我们说话也是因为声带振动而产生声音的。既然都是振动产生的声音,那为什么吉他、鼓和人声听起来相差这么大呢?这是因为介质不同。我们的声带振动发出声音之后,经过口腔、颅腔等局部区域的反射,再经过空气传播到别人的耳朵里,这就是我们说的话被别人听到的过程,其中包括了最初的发声介质与颅腔、口腔,还有中间的传播介质等。事实上,声音的传播介质很广,它可以通过空气、液体和固体进行传播;而且介质不同,传播的速度也不同,比如,声音在空气中的传播速度为340m/s,在蒸馏水中的传播速度为1497m/s,而在铁棒中的传播速度则可以高达5200m/s;不过,声音在真空中是无法传播的。
- 吸音主要是解决声音反射而产生的嘈杂感,吸音材料可以衰减入射音源的反射能量,从而达到对原有声源的保真效果,比如录音棚里面的墙壁上就会使用吸音棉材料。
- 隔音主要是解决声音的透射而降低主体空间内的吵闹感,隔音棉材料可以衰减入射音源的透射能量,从而达到主体空间的安静状态,比如KTV里面的墙壁上就会安装隔音棉材料。
当我们在高山或空旷地带高声大喊的时候,经常会听到回声(echo)。之所以会有回声是因为声音在传播过程中遇到障碍物会反弹回来,再次被我们听到。但是,若两种声音传到我们的耳朵里的时差小于80毫秒,我们就无法区分开这两种声音了,其实在日常生活中,人耳也在收集回声,只不过由于嘈杂的外界环境以及回声的分贝(衡量声音能量值大小的单位)比较低,所以我们的耳朵分辨不出这样的声音,或者说是大脑能接收到但分辨不出。
自然界中有光能、水能,生活中有机械能、电能,其实声音也可以产生能量,例如两个频率相同的物体,敲击其中一个物体时另一个物体也会振动发声。这种现象称为共鸣,共鸣证明了声音传播可以带动另一个物体振动,也就是说,声音的传播过程也是一种能量的传播过程。
为了将模拟信号数字化,需要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 = […]
- 计算如下:
1378.125 * 60 / 8 / 1024 = 10.09MB- 如果sampleFormat更加精确(比如用4字节来描述一个采样),或者sampleRate更加密集(比如48kHz的采样率),那么所占的存储空间就会更大,同时能够描述的声音细节就会越精确。存储的这段二进制数据即表示将模拟信号转换为数字信号了,以后就可以对这段二进制数据进行存储、播放、复制,或者进行其他任何操作。
麦克风里面有一层碳膜,非常薄而且十分敏感。前面介绍过,声音其实是一种纵波,会压缩空气也会压缩这层碳膜,碳膜在受到挤压时也会发出振动,在碳膜的下方就是一个电极,碳膜在振动的时候会接触电极,接触时间的长短和频率与声波的振动幅度和频率有关,这样就完成了声音信号到电信号的转换。之后再经过放大电路处理,就可以实施后面的采样量化处理了。
分贝是用来表示声音强度的单位。日常生活中听到的声音,若以声压值来表示,由于其变化范围非常大,可以达到六个数量级以上,同时由于我们的耳朵对声音信号强弱刺激的反应不是线性的,而是呈对数比例关系,所以引入分贝的概念来表达声学量值。所谓分贝是指两个相同的物理量(例如,A1和A0)之比取以10为底的对数并乘以10(或20),即:
N= 10 * lg(A1 / A0)
分贝符号为“dB”,它是无量纲的。式中A0是基准量(或参考量),A1是被量度量。
有损压缩
和无损压缩
。
- 无损压缩是指解压后的数据可以完全复原。在常用的压缩格式中,用得较多的是有损压缩,
- 有损压缩是指解压后的数据不能完全复原,会丢失一部分信息,压缩比越小,丢失的信息就越多,信号还原后的失真就会越大。根据不同的应用场景(包括存储设备、传输网络环境、播放设备等),可以选用不同的压缩编码算法,如PCM、WAV、AAC、MP3、Ogg等。
压缩编码的原理:实际上是压缩掉冗余信号,冗余信号是指不能被人耳感知到的信号,包含人耳听觉范围之外的音频信号以及被掩蔽掉的音频信号等。人耳听觉范围之外的音频信号在前面已经提到过,所以在此不再赘述。而被掩蔽掉的音频信号则主要是因为人耳的掩蔽效应,主要表现为频域掩蔽效应与时域掩蔽效应,无论是在时域还是频域上,被掩蔽掉的声音信号都被认为是冗余信息,不进行编码处理。
主要有:WAV编码, MP3编码, AAC编码, Ogg编码。
PCM
(脉冲编码调制)是Pulse Code Modulation的缩写。前面已经介绍过PCM大致的工作流程,而WAV编码的一种实现(有多种实现方式,但是都不会进行压缩操作)就是在PCM数据格式的前面加上44字节,分别用来描述PCM
的采样率、声道数、数据格式等信息。- 特点:音质非常好,大量软件都支持。
- 适用场合:多媒体开发的中间文件、保存音乐和音效素材。
MP3
具有不错的压缩比,使用LAME
编码(MP3
编码格式的一种实现)的中高码率的MP3
文件,听感上非常接近源WAV文件,当然在不同的应用场景下,应该调整合适的参数以达到最好的效果。- 特点:音质在128Kbit/s以上表现还不错,压缩比比较高,大量软件和硬件都支持,兼容性好。
- 适用场合:高比特率下对兼容性有要求的音乐欣赏。
AAC
是新一代的音频有损压缩技术,它通过一些附加的编码技术(比如PS、SBR等),衍生出了LC-AAC
、HE-AAC
、HE-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。- 特点:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码。
- 适用场合:128Kbit/s以下的音频编码,多用于视频中音频轨的编码。
Ogg
是一种非常有潜力的编码,在各种码率下都有比较优秀的表现,尤其是在中低码率场景下。Ogg
除了音质好之外,还是完全免费的,这为Ogg
获得更多的支持打好了基础。Ogg
有着非常出色的算法,可以用更小的码率达到更好的音质,128Kbit/s的Ogg比192Kbit/s甚至更高码率的MP3还要出色。但目前因为还没有媒体服务软件的支持,因此基于Ogg的数字广播还无法实现。Ogg
目前受支持的情况还不够好,无论是软件上的还是硬件上的支持,都无法和MP3相提并论。- 特点:可以用比MP3更小的码率实现比MP3更好的音质,高中低码率下均有良好的表现,兼容性不够好,流媒体特性不支持。
- 适用场合:语音聊天的音频消息场景。
只要是Core Audio框架支持的音频编解码, AVFoundation框架都可以支持, 这意味着 AVFoundation能够支持大量不同格式的资源。然而在不用线性PCM音频的情况下,更多的只能使用AAC。
高级音频编码AAC是H.264标准相应的音频处理方式,目前已经成为音频流和下载的音频资源中最主流的编码方式。这种格式比MP3格式有着显著的提升,可以在低比特率的前提下提供更高质量的音频,是在Web上发布和传播的音频格式中最为理想的。此外,AAC没有来自证书和许可方面的限制,这一限制曾经在MP3格式上饱受诟病。
AVFoundation和Core Audio框架都提供对MP3数据解码支持,但是不支持对其进行编码。
AVFoundation框架最开始是一个仅针对音频的框架,该框架的前身在IOS2.2版本中引入,只包含一个出来音频播放的类;在iOS 3.0中,苹果公司增加了音频录制功能。虽然这些类都是目前该框架中最古老的,但他们任然是最常用的几个类。
上面讲解了这么多音频理论知识,接下来我们将从 AVFoundation的两个基础类AVAudioPlayer和AVAudioRecorder来讲解音频播放和音频录制功能。
- 播放任何持续时间的声音
- 播放来自文件或内存缓冲区的声音
- 循环播放
- 同时播放多个声音,每个音频播放器一个声音,精确同步
- 控制相对播放级别、立体声定位和播放速度
- 查找声音文件中的特定点,该点支持快进和快退等应用程序特性.
- 获取可用于回放级别测量的数据.
(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]
//声音中与音频播放器相关联的音频通道的数量。
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
//一个布尔值,用于指定音频播放器的音频电平测量开/关状态。
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
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 方法时也会默认调用,但是会造成轻微播放的延时。
AVAudioPlayer 的 play 可以播放音频,stop 和 pause 都可以暂停播放,但是 stop 会撤销调用 prepareToPlay 所做的设置。从上面介绍的AVAudioPlayer属性可以知道如何设置。具体设置 如下:
- 修改播放器的音量:播放器音量独立于系统音量,音量或播放增益定义为 0.0(静音)到 1.0(最大音量)之间的浮点值
- 修改播放器的 pan 值:允许使用立体声播放声音,pan 值从 -1.0(极左)到 1.0(极右),默认值 0.0(居中)
- 调整播放率:0.5(半速)到 2.0(2 倍速)
- 设置 numberOfLoops 实现无缝循环:-1 表示无限循环(音频循环可以是未压缩的线性 PCM 音频,也可以是 AAC 之类的压缩格式音频,MP3 格式不推荐循环)
- 音频计量:当播放发生时从播放器读取音量力度的平均值和峰值
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
方法可以停止播放行为。有趣的是,pause
和stop
方法在应用程序外面看来实现的功能都是停止当前播放行为。下一时间里我们调用play
方法,通过pause
和stop
方法停止的音频都会继续播放。prepareToPlay
时所做的设置,而调用pause方法则不会。player.enableRate = YES;
player.rate = rate;
player.volume = volume;
player.pan = pan;
player.numberOfLoops = -1;
// Access the shared, singleton audio session instance
let session = AVAudioSession.sharedInstance()
do {
// Configure the audio session for movie playback
try session.setCategory(AVAudioSessionCategoryPlayback,
mode: AVAudioSessionModeMoviePlayback,
options: [])
} catch let error as NSError {
print("Failed to set the audio session category and mode: \(error.localizedDescription)")
}
AVAudioSession
单例,由于播放音频时应用程序最主要的功能,需要指定AVAudioSessionCategoryPlayback
分类。最后通过调用setAction:error:方法并传递Yes值来激活会话。此外还可以使用Open As|Source Code的方式打开编辑plist文件,将下面的内容添加到标签前:
<key> UIBackgroundModeskey>
<array>
<string>audiostring>
array>
添加这一设置表示应用程序现在允许后台播放音频内容,这样锁屏后,可以继续播放音频。
- (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方法只能更新内部状态,并不能停止播放。
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 值。
#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
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
使用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
//初始化音频播放,返回音频时长
//播放器相关
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()
}
}
class AVAudioRecorder : NSObject
- 持续录音,直到用户停止
- 指定的持续时间的录音
- 暂停并继续录音
- 获取可用于提供电平测量的输入声级数据
在iOS系统中,录制的音频来自用户内置麦克风或耳机麦克风连接的设备。在macOS中,音频来自系统的默认音频输入设备,由用户在系统首选项中设置。
您可以为音频记录器实现一个委托对象,以响应音频中断和音频解码错误,并完成录制。
要配置录音,包括诸如位深度、比特率和采样率转换质量等选项,请配置音频记录器的设置字典。使用设置中描述的设置键。
var settings: [String : Any] { get }
//指示录音机是否正在录音的布尔值。
var isRecording: Bool
//与录音机关联的音频文件的URL。
var url: URL
//与记录器相关联的AVAudioSessionChannelDescription对象的数组。
var channelAssignments: [AVAudioSessionChannelDescription]?
//时间,以秒为单位,从录音开始算起。
var currentTime: TimeInterval
//音频记录器所在的主机设备的时间(以秒为单位)。
var deviceCurrentTime: TimeInterval
//缓冲区中音频的格式。
var format: AVAudioFormat
音频会话在应用程序和操作系统之间扮演者中间人的角色。它提供了一种简单实用的方法是OS得知应用程序应该如何与IOS音频环境进行交互。你不需要了解与音频硬件交互的细节,只需要对应用程序的行为语义上的描述即可。这一点使得你可以指明应用程序的一般音频行为,并可以把对该行为的管理委托给音频会话,这样OS系统就可以对用户使用音频的体验进行最适当的管理。
- 激活了音频播放,但是音频录制未激活。
- 当用户切换响铃/静音开发到静音模式是,应用程序播放的所有音频都会消失。
- 当设备显示解锁屏幕时,所有后台播放的音频都会处于静音状态。
- 当应用程序播放音频时,所有后台播放的音频都会处于静音状态。
class AVAudioSession : NSObject
- 它支持音频回放,但不允许音频录制(tvOS不支持音频录制)。
- 在iOS系统中,将铃声/静音开关设置为静音模式,应用程序播放的任何音频都会被静音。
- 在iOS系统中,锁定设备会使应用程序的音频静音。
- 当应用程序播放音频时,它会静音任何其他背景音频。
类别 | 来电静音/锁屏静音 | 中断非混合应用程序的音频 | 允许音频输入(录制)和输出(回放) | 作用 |
---|---|---|---|---|
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 | 如果您的应用正在执行音频输入或输出的测量,请指定此模式 |
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]);
}
- AVFormatIDKey 键对应写入内容的音频格式,它有以下可选值:
kAudioFormatLinearPCM
kAudioFormatMPEG4AAC
kAudioFormatAppleLossless
kAudioFormatAppleIMA4
kAudioFormatiLBC
kAudioFormatULaw- 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。
- 用于写入音频的本地文件 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]);
}
上面代码我们记录到tmp目录中的一个名为memo.cat的文件,在录制音频过程中,.caf (Core Audio Format)格式通常是最好的容器格式,因为它和内容无关并且可以保持Core Audio支持的任何音频格式。
此外我们需要定义录音设置,以便适应Apple IMA4作为音频格式,采样率44.1kHz,位深度16位,单声道录制。这些设置考虑了质量和文件大小的平衡。
@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
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)!
}
- (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 */
@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
来更新。CADisplayLink
和NSTimer
类似,不过它可以与显示刷新率自动同步。参考书籍:《AV Foundation开发秘籍》,《音视频开发进阶指南基于Android与iOS平台的实践》