版本記錄
版本號 | 時間 |
---|---|
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)注~~~