AVFoundation框架解析(二十)—— AVAudioEngine之詳細說明和一個簡單示例源碼(三)

版本記錄

版本號 時間
V1.0 2018.08.19

前言

AVFoundation框架是ios中很重要的框架寨躁,所有與視頻音頻相關(guān)的軟硬件控制都在這個框架里面举户,接下來這幾篇就主要對這個框架進行介紹和講解榄鉴。感興趣的可以看我上幾篇财著。
1. AVFoundation框架解析(一)—— 基本概覽
2. AVFoundation框架解析(二)—— 實現(xiàn)視頻預(yù)覽錄制保存到相冊
3. AVFoundation框架解析(三)—— 幾個關(guān)鍵問題之關(guān)于框架的深度概括
4. AVFoundation框架解析(四)—— 幾個關(guān)鍵問題之AVFoundation探索(一)
5. AVFoundation框架解析(五)—— 幾個關(guān)鍵問題之AVFoundation探索(二)
6. AVFoundation框架解析(六)—— 視頻音頻的合成(一)
7. AVFoundation框架解析(七)—— 視頻組合和音頻混合調(diào)試
8. AVFoundation框架解析(八)—— 優(yōu)化用戶的播放體驗
9. AVFoundation框架解析(九)—— AVFoundation的變化(一)
10. AVFoundation框架解析(十)—— AVFoundation的變化(二)
11. AVFoundation框架解析(十一)—— AVFoundation的變化(三)
12. AVFoundation框架解析(十二)—— AVFoundation的變化(四)
13. AVFoundation框架解析(十三)—— 構(gòu)建基本播放應(yīng)用程序
14. AVFoundation框架解析(十四)—— VAssetWriter和AVAssetReader的Timecode支持(一)
15. AVFoundation框架解析(十五)—— VAssetWriter和AVAssetReader的Timecode支持(二)
16. AVFoundation框架解析(十六)—— 一個簡單示例之播放诵冒、錄制以及混合視頻(一)
17. AVFoundation框架解析(十七)—— 一個簡單示例之播放耕皮、錄制以及混合視頻之源碼及效果展示(二)
18. AVFoundation框架解析(十八)—— AVAudioEngine之基本概覽(一)
19. AVFoundation框架解析(十九)—— AVAudioEngine之詳細說明和一個簡單示例(二)

源碼

下面我們一起看一下源碼境蜕。

1. ViewController.swift
import UIKit
import AVFoundation

class ViewController: UIViewController {

  // MARK: Outlets
  @IBOutlet weak var playPauseButton: UIButton!
  @IBOutlet weak var skipForwardButton: UIButton!
  @IBOutlet weak var skipBackwardButton: UIButton!
  @IBOutlet weak var progressBar: UIProgressView!
  @IBOutlet weak var meterView: UIView!
  @IBOutlet weak var volumeMeterHeight: NSLayoutConstraint!
  @IBOutlet weak var rateSlider: UISlider!
  @IBOutlet weak var rateLabel: UILabel!
  @IBOutlet weak var rateLabelLeading: NSLayoutConstraint!
  @IBOutlet weak var countUpLabel: UILabel!
  @IBOutlet weak var countDownLabel: UILabel!

  // MARK: AVAudio properties
  var engine = AVAudioEngine()
  var player = AVAudioPlayerNode()
  var rateEffect = AVAudioUnitTimePitch()

  var audioFile: AVAudioFile? {
    didSet {
      if let audioFile = audioFile {
        audioLengthSamples = audioFile.length
        audioFormat = audioFile.processingFormat
        audioSampleRate = Float(audioFormat?.sampleRate ?? 44100)
        audioLengthSeconds = Float(audioLengthSamples) / audioSampleRate
      }
    }
  }
  var audioFileURL: URL? {
    didSet {
      if let audioFileURL = audioFileURL {
        audioFile = try? AVAudioFile(forReading: audioFileURL)
      }
    }
  }
  var audioBuffer: AVAudioPCMBuffer?

  // MARK: other properties
  var audioFormat: AVAudioFormat?
  var audioSampleRate: Float = 0
  var audioLengthSeconds: Float = 0
  var audioLengthSamples: AVAudioFramePosition = 0
  var needsFileScheduled = true
  let rateSliderValues: [Float] = [0.5, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0]
  var rateValue: Float = 1.0 {
    didSet {
      rateEffect.rate = rateValue
      updateRateLabel()
    }
  }
  var updater: CADisplayLink?
  var currentFrame: AVAudioFramePosition {
    guard let lastRenderTime = player.lastRenderTime,
      let playerTime = player.playerTime(forNodeTime: lastRenderTime) else {
        return 0
    }

    return playerTime.sampleTime
  }
  var seekFrame: AVAudioFramePosition = 0
  var currentPosition: AVAudioFramePosition = 0
  let pauseImageHeight: Float = 26.0
  let minDb: Float = -80.0

  enum TimeConstant {
    static let secsPerMin = 60
    static let secsPerHour = TimeConstant.secsPerMin * 60
  }

  // MARK: - ViewController lifecycle
  //
  override func viewDidLoad() {
    super.viewDidLoad()

    setupRateSlider()
    countUpLabel.text = formatted(time: 0)
    countDownLabel.text = formatted(time: audioLengthSeconds)
    setupAudio()

    updater = CADisplayLink(target: self, selector: #selector(updateUI))
    updater?.add(to: .current, forMode: .defaultRunLoopMode)
    updater?.isPaused = true
  }

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    updateRateLabel()
  }
}

// MARK: - Actions
//
extension ViewController {
  @IBAction func didChangeRateValue(_ sender: UISlider) {
    let index = round(sender.value)
    rateSlider.setValue(Float(index), animated: false)
    rateValue = rateSliderValues[Int(index)]
  }

  @IBAction func playTapped(_ sender: UIButton) {
    sender.isSelected = !sender.isSelected

    if currentPosition >= audioLengthSamples {
      updateUI()
    }

    if player.isPlaying {
      disconnectVolumeTap()
      updater?.isPaused = true
      player.pause()
    } else {
      updater?.isPaused = false
      connectVolumeTap()
      if needsFileScheduled {
        needsFileScheduled = false
        scheduleAudioFile()
      }
      player.play()
    }
  }

  @IBAction func plus10Tapped(_ sender: UIButton) {
    guard let _ = player.engine else { return }
    seek(to: 10.0)
  }

  @IBAction func minus10Tapped(_ sender: UIButton) {
    guard let _ = player.engine else { return }
    needsFileScheduled = false
    seek(to: -10.0)
  }

  @objc func updateUI() {
    currentPosition = currentFrame + seekFrame
    currentPosition = max(currentPosition, 0)
    currentPosition = min(currentPosition, audioLengthSamples)

    progressBar.progress = Float(currentPosition) / Float(audioLengthSamples)
    let time = Float(currentPosition) / audioSampleRate
    countUpLabel.text = formatted(time: time)
    countDownLabel.text = formatted(time: audioLengthSeconds - time)

    if currentPosition >= audioLengthSamples {
      player.stop()
      updater?.isPaused = true
      playPauseButton.isSelected = false
      disconnectVolumeTap()
    }
  }
}

// MARK: - Display related
//
extension ViewController {
  func setupRateSlider() {
    let numSteps = rateSliderValues.count-1
    rateSlider.minimumValue = 0
    rateSlider.maximumValue = Float(numSteps)
    rateSlider.isContinuous = true
    rateSlider.setValue(1.0, animated: false)
    rateValue = 1.0
    updateRateLabel()
  }

  func updateRateLabel() {
    rateLabel.text = "\(rateValue)x"
    let trackRect = rateSlider.trackRect(forBounds: rateSlider.bounds)
    let thumbRect = rateSlider.thumbRect(forBounds: rateSlider.bounds , trackRect: trackRect, value: rateSlider.value)
    let x = thumbRect.origin.x + thumbRect.width/2 - rateLabel.frame.width/2
    rateLabelLeading.constant = x
  }

  func formatted(time: Float) -> String {
    var secs = Int(ceil(time))
    var hours = 0
    var mins = 0

    if secs > TimeConstant.secsPerHour {
      hours = secs / TimeConstant.secsPerHour
      secs -= hours * TimeConstant.secsPerHour
    }

    if secs > TimeConstant.secsPerMin {
      mins = secs / TimeConstant.secsPerMin
      secs -= mins * TimeConstant.secsPerMin
    }

    var formattedString = ""
    if hours > 0 {
      formattedString = "\(String(format: "%02d", hours)):"
    }
    formattedString += "\(String(format: "%02d", mins)):\(String(format: "%02d", secs))"
    return formattedString
  }
}

// MARK: - Audio
//
extension ViewController {
  func setupAudio() {
    audioFileURL  = Bundle.main.url(forResource: "Intro", withExtension: "mp4")

    engine.attach(player)
    engine.attach(rateEffect)
    engine.connect(player, to: rateEffect, format: audioFormat)
    engine.connect(rateEffect, to: engine.mainMixerNode, format: audioFormat)

    engine.prepare()

    do {
      try engine.start()
    } catch let error {
      print(error.localizedDescription)
    }
  }

  func scheduleAudioFile() {
    guard let audioFile = audioFile else { return }

    seekFrame = 0
    player.scheduleFile(audioFile, at: nil) { [weak self] in
      self?.needsFileScheduled = true
    }
  }

  func connectVolumeTap() {
    let format = engine.mainMixerNode.outputFormat(forBus: 0)
    engine.mainMixerNode.installTap(onBus: 0, bufferSize: 1024, format: format) { buffer, when in

      guard let channelData = buffer.floatChannelData,
        let updater = self.updater else {
          return
      }

      let channelDataValue = channelData.pointee
      let channelDataValueArray = stride(from: 0,
                                         to: Int(buffer.frameLength),
                                         by: buffer.stride).map{ channelDataValue[$0] }
      let rms = sqrt(channelDataValueArray.map{ $0 * $0 }.reduce(0, +) / Float(buffer.frameLength))
      let avgPower = 20 * log10(rms)
      let meterLevel = self.scaledPower(power: avgPower)

      DispatchQueue.main.async {
        self.volumeMeterHeight.constant = !updater.isPaused ? CGFloat(min((meterLevel * self.pauseImageHeight),
                                                                          self.pauseImageHeight)) : 0.0
      }
    }
  }

  func scaledPower(power: Float) -> Float {
    guard power.isFinite else { return 0.0 }

    if power < minDb {
      return 0.0
    } else if power >= 1.0 {
      return 1.0
    } else {
      return (fabs(minDb) - fabs(power)) / fabs(minDb)
    }
  }

  func disconnectVolumeTap() {
    engine.mainMixerNode.removeTap(onBus: 0)
    volumeMeterHeight.constant = 0
  }

  func seek(to time: Float) {
    guard let audioFile = audioFile,
      let updater = updater else {
      return
    }

    seekFrame = currentPosition + AVAudioFramePosition(time * audioSampleRate)
    seekFrame = max(seekFrame, 0)
    seekFrame = min(seekFrame, audioLengthSamples)
    currentPosition = seekFrame

    player.stop()

    if currentPosition < audioLengthSamples {
      updateUI()
      needsFileScheduled = false

      player.scheduleSegment(audioFile, startingFrame: seekFrame, frameCount: AVAudioFrameCount(audioLengthSamples - seekFrame), at: nil) { [weak self] in
        self?.needsFileScheduled = true
      }

      if !updater.isPaused {
        player.play()
      }
    }
  }

}
2. sb文件

下面看一下sb文件。


效果展示

下面看一下實現(xiàn)效果凌停。

可以看見粱年,實現(xiàn)了快進、倒退罚拟、倍率調(diào)整等台诗。

后記

本篇主要講述了AVAudioEngine之詳細說明和一個簡單示例源碼和效果展示完箩,感興趣的給個贊或者關(guān)注~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市拉队,隨后出現(xiàn)的幾起案子弊知,更是在濱河造成了極大的恐慌,老刑警劉巖粱快,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秩彤,死亡現(xiàn)場離奇詭異,居然都是意外死亡事哭,警方通過查閱死者的電腦和手機漫雷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來慷蠕,“玉大人珊拼,你說我怎么就攤上這事×骺唬” “怎么了?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵仅胞,是天一觀的道長每辟。 經(jīng)常有香客問我,道長干旧,這世上最難降的妖魔是什么渠欺? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮椎眯,結(jié)果婚禮上挠将,老公的妹妹穿的比我還像新娘。我一直安慰自己编整,他們只是感情好舔稀,可當我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著掌测,像睡著了一般内贮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上汞斧,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天夜郁,我揣著相機與錄音,去河邊找鬼粘勒。 笑死竞端,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的庙睡。 我是一名探鬼主播事富,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼技俐,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了赵颅?” 一聲冷哼從身側(cè)響起虽另,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎饺谬,沒想到半個月后捂刺,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡募寨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年族展,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拔鹰。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡仪缸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出列肢,到底是詐尸還是另有隱情恰画,我是刑警寧澤,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布瓷马,位于F島的核電站拴还,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏欧聘。R本人自食惡果不足惜片林,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望怀骤。 院中可真熱鬧费封,春花似錦、人聲如沸蒋伦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凉敲。三九已至衣盾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間爷抓,已是汗流浹背势决。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蓝撇,地道東北人果复。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像渤昌,于是被迫代替她去往敵國和親虽抄。 傳聞我的和親對象是個殘疾皇子走搁,可洞房花燭夜當晚...
    茶點故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內(nèi)容