【AVFoundation】AVAudioPlayer播放音频、中断处理、线路改变处理

1,000 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

AVFoundation 是Apple iOS和OS X系统中用于处理基于时间的媒体数据的高级框架,通过开发所需的工具提供了强大的功能集,让开发者能够基于苹果平台创建当下最先进的媒体应用程序,其针对64位处理器设计,充分利用了多核硬件优势,会自动提供硬件加速操作,确保大部分设备能以最佳性能运行,是iOS开发接触音视频开发必学的框架之一

参与掘金日新计划,持续记录AVFoundation学习,Demo学习地址,这篇文章主要讲述利用AVAudioPlayer实现音频播放,以及播放时的中断、线路改变处理,其他类的相关用法可查看我的其他文章。

AVAudioSession 音频会话

  • 在学习音频之前,先理解音频会话是很有必要的,例如,当我们播放音乐时,接到电话音乐会暂停,拔掉耳机音乐暂停,插入耳机音乐继续播放,这是一个复杂的步骤,在iOS开发中,音频会话帮我们把这一切都处理了,我们只需要配置合适的音频会话即可

  • AVFoundation定义了7种分类来描述应用所使用的音频行为

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

多数情况使用以上几种分类就可以满足大部分应用所需,若需要更复杂功能,其中一些可以通过使用options和mods的方法进一步自定义开发,例如如果选择了Play And Record则需将options设置为defaultToSpeaker,否则播放音乐是从听筒不是扬声器。

  • 音频会话在App生命周期中可以根据需要配置多次,但一般在应用启动时配置
private func confitAudioSession() {
        let audioSession = AVAudioSession.sharedInstance()
        do {
        // 配置会话分类
            _ = try audioSession.setCategory(.playAndRecord)
        } catch {
            CQLog(error)
        }
        do {
        // 激活音频会话
            _ = try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
        } catch {
            CQLog(error)
        }
    }

AVAudioPlayer

  • 诞生于iOS2.2,至今已经14年,但仍然是最常用的几个类之一,音频播放时很多App的常见需求,AVAudioPlayer让这一功能的实现变的非常简单
  • AVAudioPlayer构建于CoreAudio中的C-based Audio Queue Services的最顶层,所以它可以提供所有你在Audio Queue Services中所能找到的核心功能,比如播放、循环甚至音频计量,使用的是非常友好简单的OC/Swift接口,除非你需要从网络流中播放音频、需要访问原始音频样本或者需要非常低的延迟,否则AVAudioPlayer都能胜任。

  • 创建AVAudioPlayer,可以用URL创建,也可以用NSData创建
private var audioPlayer: AVAudioPlayer!

let fileUrl: URL = Bundle.main.url(forResource: "kenengfou", withExtension: "mp3")!
let musicData: Data = try! Data(contentsOf: fileUrl)
audioPlayer = try! AVAudioPlayer(data: musicData)
audioPlayer = try! AVAudioPlayer(contentsOf: fileUrl)
  • 开始加载,不调用也会隐性调用,但会增加play和听到之间的延时
audioPlayer.prepareToPlay()
  • 播放
audioPlayer.play()
  • 暂停,play会继续播放
audioPlayer.pause()
  • 停止,play同样会继续播放,和pause的区别是,stop会撤销调用prepareToPlay时所作的设置,pause则不会
audioPlayer.stop()
  • 修改音量,独立于系统音量,可以实现很多有趣的效果,例如渐隐,0.0-1.0
audioPlayer.volume = 1
  • pan值,允许使用立体声,pan的范围-1.0(左)-1.0(右),默认0.0居中
audioPlayer.pan = 0
  • 速率,在不改变音调的情况下调整播放速率,0.5(半速)-2.0(2倍速),1.0正常速度,部分资源可能无效
audioPlayer.rate = 1.0
  • 循环次数,-1无限循环
audioPlayer.numberOfLoops = -1

音频中断处理

  • 注册中断通知
NotificationCenter.default.addObserver(self, selector: #selector(self.handleInterruptionNotification(_:)), name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance())
  • 判断中断类型业务处理 从userInfo字典里,检索AVAudioSessionInterruptionTypeKey判断中断类型,在中断结束后,检索AVAudioSessionInterruptionOptionKey判断是否已经重新激活以及是否可以再次播放
/// 处理中断
    @objc private func handleInterruptionNotification(_ notification: Notification) {
        // 接收一个字典 AVAudioSessionInterruptionTypeKey确定中断类型
        let info: Dictionary = notification.userInfo!
//        CQLog(info)
        let interruptionType: AVAudioSession.InterruptionType = AVAudioSession.InterruptionType(rawValue: info[AVAudioSessionInterruptionTypeKey]! as! UInt)!
        switch interruptionType {
        case .began:
            CQLog("中断开始")
        case .ended:
            CQLog("中断结束")
            let options: AVAudioSession.InterruptionOptions = AVAudioSession.InterruptionOptions(rawValue: info[AVAudioSessionInterruptionOptionKey]! as! UInt)
            // 是否已经重新激活,是否可以再次播放
            if options == .shouldResume {
                CQLog("中断结束,已经重新激活,可以再次播放")
            } else {
                CQLog("中断结束,尚未激活,无法播放")
            }
        @unknown default:
            fatalError("未知的音频中断类型")
        }
    }

线路改变处理

  • 注册线路改变通知
NotificationCenter.default.addObserver(self, selector: #selector(self.handleRouteChangeNotification(_:)), name: AVAudioSession.routeChangeNotification, object: AVAudioSession.sharedInstance())
  • 判断线路改变类型业务处理 可以通过AVAudioSessionRouteChangeReasonKey查找userInfo判断线路变化原因,可以通过AVAudioSessionRouteChangePreviousRouteKey检索判断前一个端口类型
/// 处理线路变化
    @objc private func handleRouteChangeNotification(_ notification: Notification) {
        // 判断线路变化原因
        let info: Dictionary = notification.userInfo!
//        CQLog(info)
        let routeChangeReason: AVAudioSession.RouteChangeReason = AVAudioSession.RouteChangeReason(rawValue: info[AVAudioSessionRouteChangeReasonKey]! as! UInt)!
        switch routeChangeReason {
        case .unknown:
            CQLog("线路变化原因-未知")
        case .newDeviceAvailable:
            CQLog("线路变化原因-接入的新设备可用(例如,插入了耳机)")
        case .oldDeviceUnavailable:
            CQLog("线路变化原因-旧设备无法使用(例如,耳机被拔下)")
        case .categoryChange:
            CQLog("线路变化原因-音频类别发生了变化(例如重新设定了AVAudioSession.sharedInstance().setCategory)")
        case .override:
            CQLog("线路变化原因-这条线路已被重写(例如音频会话分类是AVAudioSessionCategoryPlayAndRecord时,改变了options)")
        case .wakeFromSleep:
            CQLog("线路变化原因-装置唤醒")
        case .noSuitableRouteForCategory:
            CQLog("线路变化原因-当没有当前类别路由时,例如音频会话分类是AVAudioSessionCategoryRecord时,却没有输入设备可用")
        case .routeConfigurationChange:
            CQLog("线路变化原因-输入/输出端口的集合没有改变,但某些方面他们的配置已经改变(例如,端口选择的数据源发生了变化)")
        @unknown default:
            fatalError("未知的音频线路变化类型")
        }
        
        // 获取前一个线路的描述
        let previousRoute: AVAudioSessionRouteDescription = info[AVAudioSessionRouteChangePreviousRouteKey] as! AVAudioSessionRouteDescription
        // 输出端口描述
        let previousRouteOutputs: [AVAudioSessionPortDescription] = previousRoute.outputs
        let previousOutput = previousRoute.outputs[0]
        // 获取输出端口类型
        let portType: AVAudioSession.Port = previousOutput.portType
        // 判断端口类型
        if portType == AVAudioSession.Port.lineIn {
            CQLog("端口类型-停靠连接器上的线电平输入")
        } else if portType == AVAudioSession.Port.builtInMic {
            CQLog("端口类型-iOS设备内置麦克风")
        } else if portType == AVAudioSession.Port.headsetMic {
            CQLog("端口类型-有线耳机上的麦克风")
        } else if portType == AVAudioSession.Port.lineOut {
            CQLog("端口类型-停靠连接器上的线电平输出")
        } else if portType == AVAudioSession.Port.headphones {
            CQLog("端口类型-耳机")
        } else if portType == AVAudioSession.Port.bluetoothA2DP {
            CQLog("端口类型-蓝牙A2DP输出")
        } else if portType == AVAudioSession.Port.builtInReceiver {
            CQLog("端口类型-打电话时贴在耳朵上的扬声器")
        } else if portType == AVAudioSession.Port.builtInSpeaker {
            CQLog("端口类型-iOS设备内置扬声器")
        } else if portType == AVAudioSession.Port.HDMI {
            CQLog("端口类型-高清多媒体接口输出")
        } else if portType == AVAudioSession.Port.airPlay {
            CQLog("端口类型-airPlay设备输出")
        } else if portType == AVAudioSession.Port.bluetoothLE {
            CQLog("端口类型-低功耗蓝牙设备输出")
        } else if portType == AVAudioSession.Port.bluetoothHFP {
            CQLog("端口类型-蓝牙免提配置文件设备上的输入或输出")
        } else if portType == AVAudioSession.Port.usbAudio {
            CQLog("端口类型-usb设备上的输入或输出")
        } else if portType == AVAudioSession.Port.carAudio {
            CQLog("端口类型-通过汽车音频输入或输出")
        } else if #available(iOS 14.0, *) {
            if portType == AVAudioSession.Port.virtual {
                CQLog("端口类型-与实际音频硬件不对应的输入或输出")
            } else if portType == AVAudioSession.Port.PCI {
                CQLog("端口类型-通过PCI(外围组件互连)总线连接的输入或输出")
            } else if portType == AVAudioSession.Port.fireWire {
                CQLog("端口类型-通过火线连接输入或输出")
            } else if portType == AVAudioSession.Port.displayPort {
                CQLog("端口类型-displayPort输入输出")
            } else if portType == AVAudioSession.Port.AVB {
                CQLog("端口类型-通过AVB(音视频桥接)连接输入或输出")
            } else if portType == AVAudioSession.Port.thunderbolt {
                CQLog("端口类型-通过雷电接口连接输入或输出")
            }
        } else {
            CQLog("端口类型-未知类型")
        }
    }