iOS - AVAudioPlayer播放音频

AVAudioPlayer

音频播放是很多应用程序的常见需求,AVFoundation让这一功能的实现变得非常简单,这一点归功于AVAudioPlayer类。这个类的实例提供了一种简单地从文本或内存中播放音频的方法。虽然接口很简单,不过它是具有很强功能的组件,并且在Mac和iOS系统中被作为实现音频播放的最佳选择

AVAudioPlayer构建于Core Audio中的C-based Audio Queue Service的最顶层。所以它可以提供所有你在Audio Queue Service中所能找到的核心功能,比如:播放、循环甚至音频计量,但使用的是非常简单友好的接口。

除非你需要从网络流中播放音频、需要访问原始音频样本或者需要非常低的时延,否则AVAudioPlayer都能胜任

属性和方法

  • 初始化AVAudioPlayer对象
// URL的初始化方式
public init(contentsOf url: URL) throws
// Data的初始化方式
public init(data: Data) throws


/* The file type hint is a constant defined in AVMediaFormat.h whose value is a UTI for a file format. e.g. AVFileTypeAIFF. */
/* Sometimes the type of a file cannot be determined from the data, or it is actually corrupt. The file type hint tells the parser what kind of data to look for so that files which are not self identifying or possibly even corrupt can be successfully parsed. */
@available(iOS 7.0, *)
public init(contentsOf url: URL, fileTypeHint utiString: String?) throws

@available(iOS 7.0, *)
public init(data: Data, fileTypeHint utiString: String?) throws
  • 配置和控制播放
// 准备播放
open func prepareToPlay() -> Bool 

// 播放音频(异步)
open func play() -> Bool

// 在某个时间点播放
open func play(atTime time: TimeInterval) -> Bool 

// 暂停播放,但是仍然保存准备播放
open func pause()

// 停止播放,不再处于准备播放状态
open func stop() 

// 是否正在播放音频
open var isPlaying: Bool { get } 

// 代理
unowned(unsafe) open var delegate: AVAudioPlayerDelegate?

// 允许使用立体声播放声音,播放器的pan值由一个浮点数表示,范围从-1.0(极左)到1.0(极右)。默认值0.0(居中)
open var pan: Float

//播放器的音量,为0.0(静音)到1.0(最大音量)之间的浮点数
open var volume: Float

//
open func setVolume(_ volume: Float, fadeDuration duration: TimeInterval) /* fade to a new volume over a duration */

// 是否允许改变播放速率,需要的话设置为true,而且必须是在prepareToPlay方法调用之前设置
open var enableRate: Bool

// 播放速率 0.5 (半速播放) ~ 2.0(倍速播放) 1.0 是正常速度
open var rate: Float 

// 循环次数,如果要循环播放,设置为负数
open var numberOfLoops: Int

// 音频格式信息设置
open var settings: [String : Any] { get } 

// 获得或设置播放声道
open var channelAssignments: [AVAudioSessionChannelDescription]? 
  • AVAudioPlayerDelegate
public protocol AVAudioPlayerDelegate : NSObjectProtocol {
    // 当播放结束会被调用,如果播放被终止该方法不会调用
    optional public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool)

    // 音频解码发生错误
    optional public func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?)

    // iOS8.0之后使用AVAudioSession来处理音频中断,下面方法只适用于iOS8之前
    // 如果音频被中断,该方法就会被回调,比如有电话呼入
    optional public func audioPlayerBeginInterruption(_ player: AVAudioPlayer)

    // 音频中断结束
    optional public func audioPlayerEndInterruption(_ player: AVAudioPlayer, withOptions flags: Int)
}
  • 管理音频信息
// 音频的声道次数 
open var numberOfChannels: Int { get }

// 音频时长
open var duration: TimeInterval { get }

// 音频文件路径
open var url: URL? { get } 

// 音频数据
open var data: Data? { get }

// 当前播放的时间点
open var currentTime: TimeInterval

// 当前对应输出设备时间
open var deviceCurrentTime: TimeInterval { get }

// 音频格式
open var format: AVAudioFormat { get }

AVAudioPlayer 支持广泛的音频格式

  • AAC (MPEG-4 Advanced Audio Coding)

  • ALAC (Apple Lossless)

  • AMR (Adaptive Multi-rate)

  • HE-AAC (MPEG-4 High Efficiency AAC)

  • iLBC (internet Low Bit Rate Codec)

  • Linear PCM (uncompressed, linear pulse code modulation)

  • MP3 (MPEG-1 audio layer 3)

  • µ-law and a-law

  • 音频等级计量

open var isMeteringEnabled: Bool /* turns level metering on or off. default is off. */

// 更新音频测量值,注意如果要更新音频测量值必须设置meteringEnabled为YES,通过音频测量值可以即时获得音频分贝等信息
open func updateMeters() 
    
// 获得指定声道的分贝峰值,注意如果要获得分贝峰值必须在此之前调用updateMeters方法
open func peakPower(forChannel channelNumber: Int) -> Float

// 返回指定声道的平均分贝值
open func averagePower(forChannel channelNumber: Int) -> Float 

音频会话

音频会话在应用程序和操作系统之间扮演着中间人的角色。它提供了一种简单实用的方法使得iOS得知应用程序应该如何与iOS音频环境进行交互。

所有iOS应用程序都具有音频会话,无论其是否实用。默认音频会话来自于一些预配置:

  • 激活了音频播放,但是音频录制未激活
  • 当用户切换响铃/静音开关到“静音”模式时,应用程序播放的所有音频都会消失
  • 当设备显示解锁屏幕时,应用程序的音频处于静音状态
  • 当应用程序播放音频时,所有后台播放的音频都会处于静音状态
音频会话分类

AV Foundation定义了7种分类来描述应用程序所使用的音频行为。如下表

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

音频会话在应用程序的声明周期中是可以修改的,但通常我们只对其配置一次,就是在应用程序启动时。配置音频会话的最佳位置就是应用程序委托的application:didFinishLaunchingWithOptions:方法

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let session = AVAudioSession.sharedInstance()
        if #available(iOS 10.0, *) {
            do {
                try session.setCategory(.playback, mode: .default)
                try session.setActive(true)
            } catch {
                print(error)
            }
        } else {
            session.perform(NSSelectorFromString("setCategory:error:"), with: AVAudioSession.Category.playback)
        }
        return true
    }

AVAudioSession提供了与应用程序音频会话交互的接口,所以需要取得指向该单例的指针。通过设置合适的分类,可为音频的播放指定需要的音频会话,其中定制一些行为。最后告知该音频会话激活该配置

使用AVAudioPlayer播放音频

使用storyboard搭建一个简单的UI页面,支持显示歌曲名称、当前播放时间、播放、暂停、停止、调整音量等功能,如下

屏幕快照 2018-12-05 下午4.36.00.png
class AudioPlayerController: UIViewController {

    @IBOutlet weak var trackTitle: UILabel! // 歌曲名称
    @IBOutlet weak var playedTime: UILabel! // 播放时间
    @IBOutlet weak var volumeSlider: UISlider! // 音量调整

    var audioPlayer = AVAudioPlayer()
    var isPlaying = false // 自定义变量判断是否属于播放
    var timer: Timer?

    override func viewDidLoad() {
        super.viewDidLoad()
        trackTitle.text = "Future Islands - Tin Man"
        setupAudioPlayer()
    }

    func setupAudioPlayer() {
        let path = Bundle.main.path(forResource: "Future Islands - Tin Man", ofType: "mp3")!
        let url = URL(fileURLWithPath: path)
        do {
            audioPlayer =  try AVAudioPlayer(contentsOf: url)
            audioPlayer.prepareToPlay()
            audioPlayer.delegate = self
        } catch {
            print(" can't load file")
        }
    }
}

播放暂停功能

 @IBAction func playOrPauseMusic(_ sender: Any) {
        if isPlaying { // 检查当前播放状态,处于播放状态则停止播放,否则播放
            audioPlayer.pause()
            isPlaying = false
        } else {
            audioPlayer.play()
            isPlaying = true
            // 创建定时器更新播放时间
            timer = Timer.scheduledTimer(timeInterval: 1.0,
                                         target: self,
                                         selector: #selector(updateTime),
                                         userInfo: nil,
                                         repeats: true)
        }
    }

停止功能

 @IBAction func stopMusic(_ sender: Any) {
        stopTimer() // 停止定时器
        audioPlayer.stop()  // 停止播放
        audioPlayer.currentTime = 0      // 回到初始状态
        isPlaying = false
    }

音量调整

@IBAction func volumeChanged(_ sender: Any) {
        audioPlayer.volume = volumeSlider.value
 }

定时更新播放时间

 @objc func updateTime() {
        if !isPlaying { return } 

        let currentTime = Int(audioPlayer.currentTime)
        let minutes = currentTime / 60
        let seconds = currentTime - minutes * 60

        playedTime.text = String(format: "%02d:%02d", minutes, seconds)
  }

  func stopTimer() {
      timer?.invalidate()
      timer = nil
  }

代理处理播放完成停止通知

extension AudioPlayerController: AVAudioPlayerDelegate {
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        stopTimer()
    }
}

到这里我们已经可以播放音频了,但是还存在缺陷,比如想要后台播放音频,或者音频播放受到打扰等情况没有处理。

后台播放

想要实现后台播放,仅仅是在info.plist文件中添加键值App plays audio or streams audio/video using AirPlay,该值表示应用程序允许在后台播放音频内容。设置好之后当程序按下设备的Lock按钮或退出程序,仍然可以听到音频的播放

屏幕快照 2018-10-10 下午2.19.12.png
UIBackgroundModes
    
        audio
    

音频会话中断通知处理

我们要确保应用程序可以正确地处理中断事件。中断在iOS设备中经常出现,在使用设备的过程中经常会有诸如电话呼入、闹钟响起及弹出FaceTime请求等情况。虽然iOS系统本身可以很好地处理这些事件,不过我们还是要确保“我们自己”针对这些情况的处理也足够完美,如果不处理,可能由于情况中断之后,play/stop按钮处于不可用状态,音频播放也没有如预期般恢复,这显然不是我们所期望的。

  • 音频会话通知

在准备为出现的中断事件采取动作前,首先需要得到中断出现的通知,注册应用程序的AVAudioSession发送的通知AVAudioSessionRouteChangeNotification

func setupNotification() {
     // 处理中断
     NotificationCenter.default.addObserver(self,
                         selector: #selector(handleInterruption(_:)),
                         name: AVAudioSession.interruptionNotification,
                         object: AVAudioSession.sharedInstance())
}

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

首先判断是否是AVAudioSessionInterruptionTypeBegan,如果是表示已经中断,所以调用了stop 方法停止播放

注意一点:当通知被接收时,音频会话已经被终止,且AVAudioPlayer实例处于暂停状态。调用控制器的stop方法只能更新内部状态,并不能停止播放

如果中断类型为AVAudioSessionInterruptionTypeEndeduserInfo字典会包含一个AVAudioSessionInterruptionOptions值来表明音频会话是否已经重新激活以及是否可以再次播放。所以上面代码获取options值,并判断是否为AVAudioSessionInterruptionOptionShouldResume,如果是需要调用控制器的play方法

  • 对线路改变的响应

最后一个需要了解的就是如何确保应用程序对线路变换作出正确的响应。在iOS设备上添加或移除音频输入、输出线路时,会发生线路改变。

有多种原因会导致线路的变化,比如用户插入耳机或断开USB麦克风。当这些事件发生时,音频会根据情况改变输入或输出线路,同时AVAudioSession会广播一个描述该变化的通知给所有相关的侦听器,应用程序应该成为这些相关侦听器的一员。

应用程序可以做一个测试。开始播放,并在播放器期间插入耳机。音频输出线路变为耳机插孔并继续正常播放,这正是我们所期望的效果。保持音频处于播放状态,断开耳机连接。音频线路再次回到设备的内置扬声器,我们再次听到声音。

虽然线路变化同预期一样,不过按照苹果公司的相关文档,该音频应该处于静音状态。当用户插入耳机时,隐含的意思是用户不希望外界听到具体的音频内容。这就意味着当用户断开耳机时,播放的内容可能需要继续保密,所以我们需要停止音频播放,那么应该怎么做呢?

当线路发生变化时要有通知,那么首先我们需要注册AVAudioSession发送的通知,该通知名为AVAudioSessionRouteChangeNotification。该通知包含一个userInfo字典,该字典带有相应通知发送的原因信息及前一个线路的描述,这样我们就可以确定线路变化的情况了。

收这些变化通知之前,需要为这些通知进行注册,与注册中断通知的情况一样,在setupNotification方法中进行添加通知,并实现通知方法

 func setupNotification() {
        // 处理中断
        NotificationCenter.default
            .addObserver(self,
                         selector: #selector(handleInterruption(_:)),
                         name: AVAudioSession.interruptionNotification,
                         object: AVAudioSession.sharedInstance())

        // 处理线路变换
        NotificationCenter.default
            .addObserver(self,
                         selector: #selector(handleRouteChange(_:)),
                         name: AVAudioSession.routeChangeNotification,
                         object: AVAudioSession.sharedInstance())
    }

 @objc func handleRouteChange(_ notification: Notification) {
        if let info = (notification as NSNotification).userInfo as? [String: Any],
            let key = info[AVAudioSessionRouteChangeReasonKey] as? UInt {
            let reason = AVAudioSession.RouteChangeReason(rawValue: key)
            if reason == .oldDeviceUnavailable {
                let previousRoute = info[AVAudioSessionRouteChangePreviousRouteKey] as! AVAudioSessionRouteDescription
                let previousOutput = previousRoute.outputs.first!
                if convertFromAVAudioSessionPort(previousOutput.portType) == convertFromAVAudioSessionPort(AVAudioSession.Port.headphones) {
                    audioPlayer.stop()
                }
            }
        }
    }

 fileprivate func convertFromAVAudioSessionPort(_ input: AVAudioSession.Port) -> String {
     return input.rawValue
 }

收到通知后要做的第一件事是判断线路变更发生的原因。

这要看保存在userInfo字典中表示原因的AVAudioSessionRouteChangeReasonKey。这个返回值是一个用于表示变化原因的无符号整数。通过原因可以推断出不同的事件,比如有新设备接入或改变音频会话类型,不过我们需要特殊注意的是耳机断开事件。这个事件的原因是AVAudioSessionRouteChangeReasonOldDeviceUnavailable

有设备断开连接后,需要向userInfo字典提出请求,以获得其中用于描述前一个线路的AVAudioSessionRouteDescription。线路的描述信息整合在一个输入NSArray和一个输出的NSArray中。数组中元素都是AVAudioSessionPortDescription的实例,用于描述不同的I/O接口属性。

在上述情况下,需要从线路描述中找到第一个输出接口并判断是否为耳机接口。如果是,则停止播放

参考

AVAudioPlayer
《AVFoundation开发秘笈》

你可能感兴趣的:(iOS - AVAudioPlayer播放音频)