AVFoundation框架解析看这里(3)- 音频AVAudio

2020-12-23 09:59:40 浏览数 (1)

前言

AVFoundation框架是ios中很重要的框架,所有与视频音频相关的软硬件控制都在这个框架里面,接下来这几篇就主要对这个框架进行介绍和讲解。 便于读者查阅这个AVFoundation框架系列,在此提供目录直通车。 AVFoundation框架解析目录 AVFoundation框架解析目录 AVFoundation框架解析目录

本章导读

上一章节主要以媒体捕捉以起点,拍摄、保存视频,本章将以音频AVFAudio为重点,主要知识点有:

  • AVAudioSession 音频会话处理类
  • AVAudioRecorder 音频录制类
  • AVAudioPlayer 音频播放类
  • AVSpeechSynthesizer 文字转音频类

先看看官方的音频处理流程图:

音频处理流程图.png

可以发现,不同应用是共享音频硬件设备资源(麦克风,扬声器),单例AVAudioSession管理多个APP对音频硬件设备的资源使用。通过AudioSession API,可以控制App的audio相关的行为:

  • 为app选择输入输出的路由 (通过扬声器还是听筒播放)
  • 协调音频播放的app之间的关联,以及系统的声音处理
  • 处理被其他apps打断
  • 创建一个录音或者播放音乐的

AVAudioSession

单例,使用流程为:获取单例AVAudioSession ——> 设置Category和Options ——> 激活回话。在设置Category、Mode和Options,要注意不同的场景。

代码语言:javascript复制
        let session = AVAudioSession.sharedInstance()
        //设置session类型
        do {
            try session.setCategory(<#T##category: AVAudioSession.Category##AVAudioSession.Category#>, mode: <#T##AVAudioSession.Mode#>, options: <#T##AVAudioSession.CategoryOptions#>)
        } catch let err {
            print("设置类型失败:(err.localizedDescription)")
        }
        //设置session动作
        do {
            try session.setActive(true)
        } catch let err {
            print("初始化动作失败:(err.localizedDescription)")
        }
AVAudioSession Category

对于Category,目前有七种枚举类型,其中每种Category都对应是否支持下面几种能力:

  • Silenced by the Silent switch:是否会响应手机静音键开关
  • Supports audio input:是否支持音频录制
  • Supports audio output:是否支持音频播放
  • Interrupts non-mixable apps audio:是否打断不支持混音播放的APP
  • 是否支持后台播放

可以用一张图表来直观感受每种category具体的能力集:

AVAudioSession.Category.png

即:

  • ambient 用于非以语音为主的应用,只支持音频播放。使用这个category的应用会随着静音键和屏幕关闭而静音,并且不会打断其他应用的音频播放。该Category无法在后台播放声音。
  • soloAmbient 系统默认使用的 Category,类似于AVAudioSessionCategoryAmbient,不同之处在于它会中止其它应用播放声音。
  • playback 用于以语音为主的应用,只支持音频播放。使用这个category的应用不会随着静音键和屏幕关闭而静音。可在后台播放声音。

需要注意一下,选择支持在静音键切到静音状态以及锁屏键切到锁屏状态下仍然可以播放音频 Category 时,必须在应用中开启支持后台音频功能UIBackgroundModes。

  • record 用于需要录音的应用,设置该category后,除了来电铃声,闹钟或日历提醒之外的其它系统声音都不会被播放。该Category只提供单纯录音功能,不支持播放。
  • playAndRecord 用于既需要播放声音又需要录音的应用,语音聊天应用(如微信)应该使用这个category。该Category提供录音和播放功能。如果你的应用需要用到iPhone上的听筒,该category是你唯一的选择,在该Category下声音的默认出口为听筒(在没有外接设备的情况下)。音频的输入和输出不需要同步进行,也可以同步进行。
  • audioProcessing 只支持本地音频编解码处理。不支持播放和录制。
  • multiRoute 支持音频播放和录制。允许多条音频流的同步输入和输出。(比如USB连接外部扬声器输出音频,蓝牙耳机同时播放另一路音频这种特殊需求)

注意:并不是一个应用只能使用一个category,程序应该根据实际需要来切换设置不同的category,举个例子,录音的时候,需要设置为AVAudioSessionCategoryRecord,当录音结束时,应根据程序需要更改category为AVAudioSessionCategoryAmbient,AVAudioSessionCategorySoloAmbient或AVAudioSessionCategoryPlayback中的一种。

AVAudioSession Mode

Category定义了七种主场景,实际开发需求中有时候需要对Category进行微调整,即Mode和Options。

同样的,可以用一张图表来直观感受每种Mode具体的能力集:

AVAudioSession Mode.png

  • default 默认模式,与所有的 Category 兼容,在具有多于一个内置麦克风的设备上,使用主麦克风。
  • voiceChat 适用于VoIP 类型的应用,例如网络电话。只能是 AVAudioSessionCategoryPlayAndRecord Category下。在这个模式系统会自动配置AVAudioSessionCategoryOptionAllowBluetooth 这个选项。系统会自动选择最佳的内置麦克风组合支持语音聊天。
  • gameChat 适用于游戏类应用。使用 GKVoiceChat 对象的应用会自动设置这个模式和 AVAudioSessionCategoryPlayAndRecord Category。实际参数和AVAudioSessionModeVideoChat一致
  • videoRecording 适用于使用摄像头采集视频的应用。只能是 AVAudioSessionCategoryPlayAndRecord 和 AVAudioSessionCategoryRecord 这两个 Category下。这个模式搭配 AVCaptureSession API 结合来用可以更好地控制音视频的输入输出路径。(例如,设置 automaticallyConfiguresApplicationAudioSession 属性,系统会自动选择最佳输出路径。
  • measurement 最小化系统。只用于 AVAudioSessionCategoryPlayAndRecord、AVAudioSessionCategoryRecord、AVAudioSessionCategoryPlayback 这几种 Category。
  • moviePlayback 适用于播放视频的应用。只用于 AVAudioSessionCategoryPlayback 这个Category。
  • videoChat 用于视频聊天类型应用,只能是 AVAudioSessionCategoryPlayAndRecord Category下。适在这个模式系统会自动配置 AVAudioSessionCategoryOptionAllowBluetooth 和 AVAudioSessionCategoryOptionDefaultToSpeaker 选项。系统会自动选择最佳的内置麦克风组合支持视频聊天。
  • spokenAudio
  • voicePrompt
AVAudioSession Options
代码语言:javascript复制
public struct CategoryOptions : OptionSet {

        public init(rawValue: UInt)

        public static var mixWithOthers: AVAudioSession.CategoryOptions { get }

        public static var duckOthers: AVAudioSession.CategoryOptions { get }

        public static var allowBluetooth: AVAudioSession.CategoryOptions { get }

        public static var defaultToSpeaker: AVAudioSession.CategoryOptions { get }

        @available(iOS 9.0, *)
        public static var interruptSpokenAudioAndMixWithOthers: AVAudioSession.CategoryOptions { get }

        @available(iOS 10.0, *)
        public static var allowBluetoothA2DP: AVAudioSession.CategoryOptions { get }

        @available(iOS 10.0, *)
        public static var allowAirPlay: AVAudioSession.CategoryOptions { get }
    }

我们还可以使用options去微调Category行为,如下表:

AVAudioSession Options.png

AVAudioRecorder

讲完AVAudioSession,我们再来看看AVAudioRecorder。AVAudioSession负责管理系统音频硬件,当我们准备录音时,配置AVAudioSession上下文,用AVAudioRecorder来实现音频录制。

  • 属性
代码语言:javascript复制
    open var isRecording: Bool { get }                       /* 是否在录音 */

    open var url: URL { get }                                       /* 录音文件保存地址 */

    open var settings: [String : Any] { get }              /* 音频设置 */    

    open var format: AVAudioFormat { get }            /* 音频设置 */    

    weak open var delegate: AVAudioRecorderDelegate?

    open var currentTime: TimeInterval { get }   

    open var deviceCurrentTime: TimeInterval { get }

    open var isMeteringEnabled: Bool        
  • 初始化方法 在录制音频时,我们要设置好音频的保存路径和音频质量,音频质量我们可以用String : Any类型来设定,你也可以用简化后的AVAudioFormat类型。这个过程,你需要了解音频结构,采样率、音频格式、采样位数、通道数、录音质量等。
代码语言:javascript复制
    public init(url: URL, settings: [String : Any]) throws

    public init(url: URL, format: AVAudioFormat) throws
  • AVAudioSettings AVAudioSettings包含了所有的AVFormat的关键字,你可以对应设置,如下:
代码语言:javascript复制
public let AVFormatIDKey: String                               /* 音频格式 */
public let AVSampleRateKey: String                          /* 采样率 */
public let AVNumberOfChannelsKey: String             /* 通道数 */

/* linear PCM keys */
public let AVLinearPCMBitDepthKey: String             /* 采样位数, one of: 8, 16, 24, 32 */
public let AVLinearPCMIsBigEndianKey: String        /* 大端还是小端  内存的组织方式 */
public let AVLinearPCMIsFloatKey: String                 /* 采样信号是整数还是浮点数*/
public let AVLinearPCMIsNonInterleaved: String      /* 是否允许音频交叉他的值 */

/* audio file type key */
public let AVAudioFileTypeKey: String                        /* 音频文件类型 */

/* encoder property keys */
public let AVEncoderAudioQualityKey: String                              /* 音频编码质量 */
public let AVEncoderAudioQualityForVBRKey: String                /* 动态比特率编码时候的音质值 */
public let AVEncoderBitRateKey: String                                       /* 解码率*/
public let AVEncoderBitRatePerChannelKey: String                   /* 声道采样率 */
public let AVEncoderBitRateStrategyKey: String                       /* 编码时比特率策略 */
public let AVEncoderBitDepthHintKey: String                             /* 位深度 */

/* sample rate converter property keys */
public let AVSampleRateConverterAlgorithmKey: String           /* 采样率转换器的算法 */
public let AVSampleRateConverterAudioQualityKey: String      /* 采样率转换器的音质值 */

/* channel layout */
public let AVChannelLayoutKey: String                                        /* 通道布局值 */

/* property values */
/* values for AVEncoderBitRateStrategyKey */
public let AVAudioBitRateStrategy_Constant: String                              /* 常数 */         
public let AVAudioBitRateStrategy_LongTermAverage: String              /* 平均数 */
public let AVAudioBitRateStrategy_VariableConstrained: String           /* 有限制的 */
public let AVAudioBitRateStrategy_Variable: String                                 /* 可变的 */

/* values for AVSampleRateConverterAlgorithmKey */
public let AVSampleRateConverterAlgorithm_Normal: String                   /* 普通 */
public let AVSampleRateConverterAlgorithm_Mastering: String               /* 母带处理 */
public let AVSampleRateConverterAlgorithm_MinimumPhase: String     /*  */
  • 录制状态
代码语言:javascript复制
   open func prepareToRecord() -> Bool 

    open func record() -> Bool

    open func record(atTime time: TimeInterval) -> Bool 

    open func record(forDuration duration: TimeInterval) -> Bool 

    open func record(atTime time: TimeInterval, forDuration duration: TimeInterval) -> Bool 

    open func pause()

    open func stop()

    open func deleteRecording() -> Bool

AVAudioPlayer

音频播放技术有多种,例如System Sound Services、AVAudioPlayer等,本文以AVAudioPlayer为音频播放技术展开说明。

  • 属性
代码语言:javascript复制
    open var isPlaying: Bool { get } 

    open var numberOfChannels: Int { get }

    open var duration: TimeInterval { get } /* the duration of the sound. */

    weak open var delegate: AVAudioPlayerDelegate?

    open var url: URL? { get }

    open var data: Data? { get }

    open var pan: Float

    open var volume: Float

    open func setVolume(_ volume: Float, fadeDuration duration: TimeInterval)

    open var enableRate: Bool 

    open var rate: Float 

    open var currentTime: TimeInterval

    open var deviceCurrentTime: TimeInterval { get }

    open var numberOfLoops: Int

    open var settings: [String : Any] { get }
    
    open var format: AVAudioFormat { get }

    open var isMeteringEnabled: Bool 

    open var channelAssignments: [AVAudioSessionChannelDescription]? 
  • 初始化方法提供了两种,基于URL和基于Data:
代码语言:javascript复制
    public init(contentsOf url: URL, fileTypeHint utiString: String?) throws

    public init(data: Data, fileTypeHint utiString: String?) throws
  • 播放状态
代码语言:javascript复制
    open func prepareToPlay() -> Bool

    open func play() -> Bool 

    open func play(atTime time: TimeInterval) -> Bool

    open func pause()

    open func stop()

音频中断处理

其他APP或者电话会中断我们的APP音频,所以相应的我们要做出处理。

我们可以通过监听AVAudioSessionInterruptionNotification这个key获取音频中断事件回调回来Userinfo键值。

AVAudioSessionInterruptionTypeKey:

取值AVAudioSessionInterruptionTypeBegan表示中断开始

取值AVAudioSessionInterruptionTypeEnded表示中断结束

中断开始:我们需要做的是保存好播放状态,上下文,更新用户界面等

中断结束:我们要做的是恢复好状态和上下文,更新用户界面,根据需求准备好之后选择是否激活我们session。

选择不同的音频播放技术,处理中断方式也有差别,具体如下:

  • System Sound Services:使用 System Sound Services 播发音频,系统会自动处理,不受APP控制,当中断发生时,音频播放会静音,当中断结束后,音频播放会恢复。
  • AV Foundation framework:AVAudioPlayer 类和 AVAudioRecorder 类提供了中断开始和结束的 Delegate 回调方法来处理中断。中断发生,系统会自动停止播放,需要做的是记录播放时间等状态,更新用户界面,等中断结束后,再次调用播放方法,系统会自动激活session。
  • Audio Queue Services, I/O audio unit:使用aduio unit这些技术需要处理中断,需要做的是记录播放或者录制的位置,中断结束后自己恢复audio session。
  • OpenAL:使用 OpenAL 播放时,同样需要自己监听中断。管理 OpenAL上下文,用户中断结束后恢复audio session。

AVSpeechSynthesizer

AVSpeechSynthesizer属于AVFAudio的一份子,整体上比较简单。它可以很方便的在iOS应用中实现”将文本转换成语音”的功能,设计到AVSpeechSynthesisVoice、AVSpeechUtterance以及AVSpeechSynthesizer等重要概念,这块暂时涉及的比较少,不做过多讲解,提供一段代码供参考:

代码语言:javascript复制
        let synthesizer = AVSpeechSynthesizer()
        let voice = AVSpeechSynthesisVoice(language: "zh-CN")
        let utterance = AVSpeechUtterance(string: "小主,您可以开始录制音频了!")
        utterance.rate = 1.0
        utterance.voice = voice
        utterance.pitchMultiplier = 0.8
        utterance.postUtteranceDelay = 0.1
        synthesizer.speak(utterance)

你可以通过AVSpeechSynthesisVoice.speechVoices()获取设备支持的语言列表。




音频录制Demo

代码语言:javascript复制
import UIKit
import AVFoundation


typealias AVFAudioRecordBlock = (_ audioUrl: URL) -> ()



/*
 不同应用共享音频硬件设备(麦克风,扬声器)
 单例AVAudioSession管理多个APP对音频硬件设备的资源使用。
 
 用单例AVFAudioRecorder来处理应用的音频处理
 */
class AVFAudioRecorder: NSObject {
    static let shared = AVFAudioRecorder()
    private override init() {}
    
    // 录音
    private var recorder: AVAudioRecorder? = nil
    private var recorderFilePath: String? = nil
    private var timer: Timer? = nil
    private var timeLength: Int = 0
    private var recordBlock: AVFAudioRecordBlock?
    

    deinit {
        timer?.invalidate()
        timer = nil
    }
    

    public func record(setting: [String: Any]? = nil, block: @escaping AVFAudioRecordBlock) {
        RRXCAuthorizeManager.authorization(.microphone, completion: { (substate) in
            if substate == .authorized {
                self.recordBlock = block
                
                let session = AVAudioSession.sharedInstance()
                do {
                    try session.setCategory(AVAudioSession.Category.playAndRecord)
                } catch let err{
                    print("设置类型失败:(err.localizedDescription)")
                }
                do {
                    try session.setActive(true)
                } catch let err {
                    print("初始化动作失败:(err.localizedDescription)")
                }
                
                self.timeLength = 0

                //默认设置
                let _recordSetting = [AVSampleRateKey: NSNumber(value: 16000),
                    AVFormatIDKey: NSNumber(value: kAudioFormatLinearPCM),
                    AVLinearPCMBitDepthKey: NSNumber(value: 16),
                    AVNumberOfChannelsKey: NSNumber(value: 1),
                    AVEncoderAudioQualityKey: NSNumber(value: AVAudioQuality.min.rawValue)
                ]
                let recordSetting: [String: Any] = setting ?? _recordSetting
                
                do {
                    self.recorderFilePath = self.getFilePath()
                    if let filePath = self.recorderFilePath {
                        let url = URL(fileURLWithPath: filePath)
                        self.recorder = try AVAudioRecorder(url: url, settings: recordSetting)
                        self.recorder?.isMeteringEnabled = true
                        self.recorder?.prepareToRecord()
                        self.recorder?.record()

                        self.timer = Timer.every(Double(1.0).seconds) { (timer: Timer) in
                            self.timeLength  = 1
                        }
                    }
                } catch let err {
                    print("录音失败:(err.localizedDescription)")
                }
            }
        })
    }

    public func stopRecord() {
        timer?.invalidate()
        timer = nil

        if let recorder = recorder {
            recorder.stop()
            self.recorder = nil
        }
        
        if let _recordBlock = recordBlock, let filePath = recorderFilePath {
            _recordBlock(URL(fileURLWithPath: filePath))
            
            recordBlock = nil
        }
    }

    public func cancelRecord() {
        stopRecord()
        
        //清空回调
        recordBlock = nil
        
        //删除录音
        if let filePath = recorderFilePath {
            try? FileManager.default.removeItem(at: URL(fileURLWithPath: filePath))
        }
    }
    
    fileprivate func getFilePath() -> String! {
        return NSHomeDirectory()   "/Library/Caches/(Date().timeIntervalSince1970).wav"
    }
}

音频播放Demo

代码语言:javascript复制
import UIKit
import AVFoundation

typealias PlayerDidFinishPlayingBlock = () -> ()

class AVFAudioPalyer: NSObject, AVAudioPlayerDelegate {
    
    private var player: AVAudioPlayer? = nil
    private var playBlock: PlayerDidFinishPlayingBlock?
    
    static let shared = AVFAudioPalyer()
    private override init() {}
    
    func play(_ url: URL?,_ block: PlayerDidFinishPlayingBlock?) {
        UIDevice.current.isProximityMonitoringEnabled = true

        do {
            try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
        } catch let err {
            print("设置扬声器失败:(err.localizedDescription)")
        }

        do {
            playBlock = block

            if let _url = url {
                player = try AVAudioPlayer(contentsOf: _url)
                player?.delegate = self
                player?.prepareToPlay()
                player?.play()
            }
        } catch let err {
            UIDevice.current.isProximityMonitoringEnabled = false
            print("播放失败:(err.localizedDescription)")
        }
    }

    //结束播放
    func stopPlay() {
        UIDevice.current.isProximityMonitoringEnabled = false
        if let player = player {
            player.stop()
            playBlock = nil
            self.player?.delegate = nil
            self.player = nil
        }
    }
    

    
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        UIDevice.current.isProximityMonitoringEnabled = false
        if let _playBlock = playBlock {
            _playBlock()
        }
    }

}

如果喜欢,请帮忙点赞。支持转载,转载请附原文链接。

0 人点赞