iOS一点通

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

2020-01-31  本文已影响0人  孔雨露

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 采样、量化和编码

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


量化采样过程

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

  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 音频编码

  1. 无损压缩是指解压后的数据可以完全复原。在常用的压缩格式中,用得较多的是有损压缩,
  2. 有损压缩是指解压后的数据不能完全复原,会丢失一部分信息,压缩比越小,丢失的信息就越多,信号还原后的失真就会越大。根据不同的应用场景(包括存储设备、传输网络环境、播放设备等),可以选用不同的压缩编码算法,如PCM、WAV、AAC、MP3、Ogg等。

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

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

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

1.3 音频编解码

2. 播放音频

2.1 AVAudioPlayer简介

  1. 播放任何持续时间的声音
  2. 播放来自文件或内存缓冲区的声音
  3. 循环播放
  4. 同时播放多个声音,每个音频播放器一个声音,精确同步
  5. 控制相对播放级别、立体声定位和播放速度
  6. 查找声音文件中的特定点,该点支持快进和快退等应用程序特性.
  7. 获取可用于回放级别测量的数据.

(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

2.2 AVAudioPlayer实现音频播放

2.2.1 创建 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,当音频正在播放时,这个值用于标识当前播放位置的偏移,不播放音频时标识重新播放音频的起始偏移。

2.2.4 修改音量、播放速率

player.enableRate = YES;
player.rate = rate;
player.volume = volume;
player.pan = pan;
player.numberOfLoops = -1;

2.2.5 配置音频会话

2.2.6 处理中断事件

- (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 处理线路改变

        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 播放本地音频

#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
#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 播放远程音频

@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()
    }
} 

3. 录制音频

3.1 AVAudioRecorder 简介

class AVAudioRecorder : NSObject
  1. 持续录音,直到用户停止
  2. 指定的持续时间的录音
  3. 暂停并继续录音
  4. 获取可用于提供电平测量的输入声级数据
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

3.2 AVAudioSession 简介

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

  1. 激活了音频播放,但是音频录制未激活。
  2. 当用户切换响铃/静音开发到静音模式是,应用程序播放的所有音频都会消失。
  3. 当设备显示解锁屏幕时,所有后台播放的音频都会处于静音状态。
  4. 当应用程序播放音频时,所有后台播放的音频都会处于静音状态。
class AVAudioSession : NSObject
  1. 它支持音频回放,但不允许音频录制(tvOS不支持音频录制)。
  2. 在iOS系统中,将铃声/静音开关设置为静音模式,应用程序播放的任何音频都会被静音。
  3. 在iOS系统中,锁定设备会使应用程序的音频静音。
  4. 当应用程序播放音频时,它会静音任何其他背景音频。

3.2.1 音频会话模式

类别 来电静音/锁屏静音 中断非混合应用程序的音频 允许音频输入(录制)和输出(回放) 作用
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 录音时配置音频会话模式

    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 对象初始化

  1. 用于写入音频的本地文件 URL
  2. 用于配置录音会话键值信息的字典
  3. 用于捕捉错误的 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]);
        }

3.2.1.4 录音文件保存

3.2.2 录音完整代码

@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)!
}

4. 可视化音频信号

- (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实例。

5. 异常处理

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

上一篇下一篇

猜你喜欢

热点阅读