版本記錄
| 版本號 | 時間 |
|---|---|
| V1.0 | 2021.05.18 星期二 |
前言
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之詳細說明和一個簡單示例(二)
20. AVFoundation框架解析(二十)—— AVAudioEngine之詳細說明和一個簡單示例源碼(三)
21. AVFoundation框架解析(二十一)—— 一個簡單的視頻流預(yù)覽和播放示例之解析(一)
22. AVFoundation框架解析(二十二)—— 一個簡單的視頻流預(yù)覽和播放示例之源碼(二)
23. AVFoundation框架解析(二十三) —— 向視頻層添加疊加層和動畫(一)
24. AVFoundation框架解析(二十四) —— 向視頻層添加疊加層和動畫(二)
25. AVFoundation框架解析(二十五) —— 播放、錄制和合并視頻簡單示例(一)
26. AVFoundation框架解析(二十六) —— 播放、錄制和合并視頻簡單示例(二)
27. AVFoundation框架解析(二十七) —— 基于AVAudioEngine的簡單使用示例(一)
源碼
1. Swift
首先看下工程組織結(jié)構(gòu)

下面就是正文了
1. AppMain.swift
import SwiftUI
@main
struct AppMain: App {
var body: some Scene {
WindowGroup {
PlayerView()
}
}
}
2. Color+Additions.swift
import SwiftUI
extension Color {
static let rwGreen = Color("rw-green")
static let groupedBackground = Color(.systemGroupedBackground)
}
3. Image+Additions.swift
import SwiftUI
extension Image {
static let artwork = Image("artwork")
static let play = Image(systemName: "play.fill")
static let pause = Image(systemName: "pause.fill")
static let forward = Image(systemName: "goforward.10")
static let backward = Image(systemName: "gobackward.10")
}
4. PlaybackValue.swift
import Foundation
struct PlaybackValue: Identifiable {
let value: Double
let label: String
var id: String {
return "\(label)-\(value)"
}
}
5. PlayerTime.swift
import Foundation
struct PlayerTime {
let elapsedText: String
let remainingText: String
static let zero: PlayerTime = .init(elapsedTime: 0, remainingTime: 0)
init(elapsedTime: Double, remainingTime: Double) {
elapsedText = PlayerTime.formatted(time: elapsedTime)
remainingText = PlayerTime.formatted(time: remainingTime)
}
private static func formatted(time: Double) -> String {
var seconds = Int(ceil(time))
var hours = 0
var mins = 0
if seconds > TimeConstant.secsPerHour {
hours = seconds / TimeConstant.secsPerHour
seconds -= hours * TimeConstant.secsPerHour
}
if seconds > TimeConstant.secsPerMin {
mins = seconds / TimeConstant.secsPerMin
seconds -= mins * TimeConstant.secsPerMin
}
var formattedString = ""
if hours > 0 {
formattedString = "\(String(format: "%02d", hours)):"
}
formattedString += "\(String(format: "%02d", mins)):\(String(format: "%02d", seconds))"
return formattedString
}
}
6. PlayerViewModel.swift
import SwiftUI
import AVFoundation
// swiftlint:disable:next type_body_length
class PlayerViewModel: NSObject, ObservableObject {
// MARK: Public properties
var isPlaying = false {
willSet {
withAnimation {
objectWillChange.send()
}
}
}
var isPlayerReady = false {
willSet {
objectWillChange.send()
}
}
var playbackRateIndex: Int = 1 {
willSet {
objectWillChange.send()
}
didSet {
updateForRateSelection()
}
}
var playbackPitchIndex: Int = 1 {
willSet {
objectWillChange.send()
}
didSet {
updateForPitchSelection()
}
}
var playerProgress: Double = 0 {
willSet {
objectWillChange.send()
}
}
var playerTime: PlayerTime = .zero {
willSet {
objectWillChange.send()
}
}
var meterLevel: Float = 0 {
willSet {
objectWillChange.send()
}
}
let allPlaybackRates: [PlaybackValue] = [
.init(value: 0.5, label: "0.5x"),
.init(value: 1, label: "1x"),
.init(value: 1.25, label: "1.25x"),
.init(value: 2, label: "2x")
]
let allPlaybackPitches: [PlaybackValue] = [
.init(value: -0.5, label: "-?"),
.init(value: 0, label: "0"),
.init(value: 0.5, label: "+?")
]
// MARK: Private properties
private let engine = AVAudioEngine()
private let player = AVAudioPlayerNode()
private let timeEffect = AVAudioUnitTimePitch()
private var displayLink: CADisplayLink?
private var needsFileScheduled = true
private var audioFile: AVAudioFile?
private var audioSampleRate: Double = 0
private var audioLengthSeconds: Double = 0
private var seekFrame: AVAudioFramePosition = 0
private var currentPosition: AVAudioFramePosition = 0
private var audioLengthSamples: AVAudioFramePosition = 0
private var currentFrame: AVAudioFramePosition {
guard
let lastRenderTime = player.lastRenderTime,
let playerTime = player.playerTime(forNodeTime: lastRenderTime)
else {
return 0
}
return playerTime.sampleTime
}
// MARK: - Public
override init() {
super.init()
setupAudio()
setupDisplayLink()
}
func playOrPause() {
isPlaying.toggle()
if player.isPlaying {
displayLink?.isPaused = true
disconnectVolumeTap()
player.pause()
} else {
displayLink?.isPaused = false
connectVolumeTap()
if needsFileScheduled {
scheduleAudioFile()
}
player.play()
}
}
func skip(forwards: Bool) {
let timeToSeek: Double
if forwards {
timeToSeek = 10
} else {
timeToSeek = -10
}
seek(to: timeToSeek)
}
// MARK: - Private
private func setupAudio() {
guard let fileURL = Bundle.main.url(forResource: "Intro", withExtension: "mp3") else {
return
}
do {
let file = try AVAudioFile(forReading: fileURL)
let format = file.processingFormat
audioLengthSamples = file.length
audioSampleRate = format.sampleRate
audioLengthSeconds = Double(audioLengthSamples) / audioSampleRate
audioFile = file
configureEngine(with: format)
} catch {
print("Error reading the audio file: \(error.localizedDescription)")
}
}
private func configureEngine(with format: AVAudioFormat) {
engine.attach(player)
engine.attach(timeEffect)
engine.connect(
player,
to: timeEffect,
format: format)
engine.connect(
timeEffect,
to: engine.mainMixerNode,
format: format)
engine.prepare()
do {
try engine.start()
scheduleAudioFile()
isPlayerReady = true
} catch {
print("Error starting the player: \(error.localizedDescription)")
}
}
private func scheduleAudioFile() {
guard
let file = audioFile,
needsFileScheduled
else {
return
}
needsFileScheduled = false
seekFrame = 0
player.scheduleFile(file, at: nil) {
self.needsFileScheduled = true
}
}
// MARK: Audio adjustments
private func seek(to time: Double) {
guard let audioFile = audioFile else {
return
}
let offset = AVAudioFramePosition(time * audioSampleRate)
seekFrame = currentPosition + offset
seekFrame = max(seekFrame, 0)
seekFrame = min(seekFrame, audioLengthSamples)
currentPosition = seekFrame
let wasPlaying = player.isPlaying
player.stop()
if currentPosition < audioLengthSamples {
updateDisplay()
needsFileScheduled = false
let frameCount = AVAudioFrameCount(audioLengthSamples - seekFrame)
player.scheduleSegment(
audioFile,
startingFrame: seekFrame,
frameCount: frameCount,
at: nil
) {
self.needsFileScheduled = true
}
if wasPlaying {
player.play()
}
}
}
private func updateForRateSelection() {
let selectedRate = allPlaybackRates[playbackRateIndex]
timeEffect.rate = Float(selectedRate.value)
}
private func updateForPitchSelection() {
let selectedPitch = allPlaybackPitches[playbackPitchIndex]
// 1 octave = 1200 cents
timeEffect.pitch = 1200 * Float(selectedPitch.value)
}
// MARK: Audio metering
private func scaledPower(power: Float) -> Float {
guard power.isFinite else {
return 0.0
}
let minDb: Float = -80
if power < minDb {
return 0.0
} else if power >= 1.0 {
return 1.0
} else {
return (abs(minDb) - abs(power)) / abs(minDb)
}
}
private func connectVolumeTap() {
let format = engine.mainMixerNode.outputFormat(forBus: 0)
engine.mainMixerNode.installTap(
onBus: 0,
bufferSize: 1024,
format: format
) { buffer, _ in
guard let channelData = buffer.floatChannelData 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 {
return $0 * $0
}
.reduce(0, +) / Float(buffer.frameLength))
let avgPower = 20 * log10(rms)
let meterLevel = self.scaledPower(power: avgPower)
DispatchQueue.main.async {
self.meterLevel = self.isPlaying ? meterLevel : 0
}
}
}
private func disconnectVolumeTap() {
engine.mainMixerNode.removeTap(onBus: 0)
meterLevel = 0
}
// MARK: Display updates
private func setupDisplayLink() {
displayLink = CADisplayLink(
target: self,
selector: #selector(updateDisplay))
displayLink?.add(to: .current, forMode: .default)
displayLink?.isPaused = true
}
@objc private func updateDisplay() {
currentPosition = currentFrame + seekFrame
currentPosition = max(currentPosition, 0)
currentPosition = min(currentPosition, audioLengthSamples)
if currentPosition >= audioLengthSamples {
player.stop()
seekFrame = 0
currentPosition = 0
isPlaying = false
displayLink?.isPaused = true
disconnectVolumeTap()
}
playerProgress = Double(currentPosition) / Double(audioLengthSamples)
let time = Double(currentPosition) / audioSampleRate
playerTime = PlayerTime(
elapsedTime: time,
remainingTime: audioLengthSeconds - time)
}
}
7. TimeConstant.swift
import Foundation
enum TimeConstant {
static let secsPerMin = 60
static let secsPerHour = TimeConstant.secsPerMin * 60
}
8. PlayerView.swift
import SwiftUI
struct PlayerView: View {
@StateObject var viewModel = PlayerViewModel()
var body: some View {
VStack {
Image.artwork
.resizable()
.aspectRatio(
nil,
contentMode: .fit)
.padding()
.layoutPriority(1)
controlsView
.padding(.bottom)
}
}
private var controlsView: some View {
VStack {
ProgressView(value: viewModel.playerProgress)
.progressViewStyle(
LinearProgressViewStyle(tint: .rwGreen))
.padding(.bottom, 8)
HStack {
Text(viewModel.playerTime.elapsedText)
Spacer()
Text(viewModel.playerTime.remainingText)
}
.font(.system(size: 14, weight: .semibold))
Spacer()
audioControlButtons
.disabled(!viewModel.isPlayerReady)
.padding(.bottom)
Spacer()
adjustmentControlsView
}
.padding(.horizontal)
}
private var adjustmentControlsView: some View {
VStack {
HStack {
Text("Playback speed")
.font(.system(size: 16, weight: .bold))
Spacer()
}
Picker("Select a rate", selection: $viewModel.playbackRateIndex) {
ForEach(0..<viewModel.allPlaybackRates.count) {
Text(viewModel.allPlaybackRates[$0].label)
}
}
.pickerStyle(SegmentedPickerStyle())
.disabled(!viewModel.isPlayerReady)
.padding(.bottom, 20)
HStack {
Text("Pitch adjustment")
.font(.system(size: 16, weight: .bold))
Spacer()
}
Picker("Select a pitch", selection: $viewModel.playbackPitchIndex) {
ForEach(0..<viewModel.allPlaybackPitches.count) {
Text(viewModel.allPlaybackPitches[$0].label)
}
}
.pickerStyle(SegmentedPickerStyle())
.disabled(!viewModel.isPlayerReady)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 5)
.fill(Color.groupedBackground))
}
private var audioControlButtons: some View {
HStack(spacing: 20) {
Spacer()
Button {
viewModel.skip(forwards: false)
} label: {
Image.backward
}
.font(.system(size: 32))
Spacer()
Button {
viewModel.playOrPause()
} label: {
ZStack {
Color.rwGreen
.frame(
width: 10,
height: 35 * CGFloat(viewModel.meterLevel))
.opacity(0.5)
viewModel.isPlaying ? Image.pause : Image.play
}
}
.frame(width: 40)
.font(.system(size: 45))
Spacer()
Button {
viewModel.skip(forwards: true)
} label: {
Image.forward
}
.font(.system(size: 32))
Spacer()
}
.foregroundColor(.primary)
.padding(.vertical, 20)
.frame(height: 58)
}
}
后記
本篇主要講述了基于
AVAudioEngine的簡單使用示例,感興趣的給個贊或者關(guān)注~~~
