版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2018.08.19 |
前言
AVFoundation
框架是ios中很重要的框架,所有與視頻音頻相關的軟硬件控制都在這個框架里面,接下來這幾篇就主要對這個框架進行介紹和講解。感興趣的可以看我上幾篇。
1. AVFoundation框架解析(一)—— 基本概覽
2. AVFoundation框架解析(二)—— 實現(xiàn)視頻預覽錄制保存到相冊
3. AVFoundation框架解析(三)—— 幾個關鍵問題之關于框架的深度概括
4. AVFoundation框架解析(四)—— 幾個關鍵問題之AVFoundation探索(一)
5. AVFoundation框架解析(五)—— 幾個關鍵問題之AVFoundation探索(二)
6. AVFoundation框架解析(六)—— 視頻音頻的合成(一)
7. AVFoundation框架解析(七)—— 視頻組合和音頻混合調試
8. AVFoundation框架解析(八)—— 優(yōu)化用戶的播放體驗
9. AVFoundation框架解析(九)—— AVFoundation的變化(一)
10. AVFoundation框架解析(十)—— AVFoundation的變化(二)
11. AVFoundation框架解析(十一)—— AVFoundation的變化(三)
12. AVFoundation框架解析(十二)—— AVFoundation的變化(四)
13. AVFoundation框架解析(十三)—— 構建基本播放應用程序
14. AVFoundation框架解析(十四)—— VAssetWriter和AVAssetReader的Timecode支持(一)
15. AVFoundation框架解析(十五)—— VAssetWriter和AVAssetReader的Timecode支持(二)
16. AVFoundation框架解析(十六)—— 一個簡單示例之播放、錄制以及混合視頻(一)
17. AVFoundation框架解析(十七)—— 一個簡單示例之播放、錄制以及混合視頻之源碼及效果展示(二)
18. AVFoundation框架解析(十八)—— AVAudioEngine之基本概覽(一)
開始
向大多數(shù)iOS開發(fā)人員提及音頻處理,他們認為很困難甚至是恐懼。這是因為,在iOS 8之前,它意味著深入探討低級Core Audio
框架的深度 - 只有少數(shù)勇敢的靈魂才能做到這一點。值得慶幸的是,隨著iOS 8和AVAudioEngine
的發(fā)布,這一切都在2014年發(fā)生了變化。本文將向您展示如何使用Apple的新的更高級別的音頻工具audio toolkit
包來制作音頻處理應用程序,而無需深入研究Core Audio
。
那就對了!您不再需要搜索模糊的基于指針的C / C ++結構和內存緩沖區(qū)來收集原始音頻數(shù)據(jù)。
在這個AVAudioEngine
教程中,您將使用AVAudioEngine
構建下一個優(yōu)秀的播客應用程序。更具體地說,您將添加由UI控制的音頻功能:播放/暫停按鈕,跳過前進/后退按鈕,進度條和播放速率選擇器。當你完成后,你會有一個很棒的應用程序。
注意:寫作本文的環(huán)境Swift 4, iOS 11, Xcode 9。
iOS Audio Framework Introduction - iOS音頻框架介紹
在進入項目之前,首先看一下iOS音頻框架的概述:
-
CoreAudio
和AudioToolbox
是低級C框架。 -
AVFoundation
是一個Objective-C / Swift框架。 -
AVAudioEngine
是AVFoundation
的一部分。
-
AVAudioEngine
是一個定義一組連接的音頻節(jié)點的類。 您將向項目添加兩個節(jié)點:AVAudioPlayerNode
和AVAudioUnitTimePitch
。
Setup Audio - 設置Audio
打開ViewController.swift
并查看內部。 在頂部,您將看到所有連接的outlets
和類變量。 actions
還連接到sb中的相應outlets
。
將以下代碼添加到setupAudio()
:
// 1
audioFileURL = Bundle.main.url(forResource: "Intro", withExtension: "mp4")
// 2
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: audioFormat)
engine.prepare()
do {
// 3
try engine.start()
} catch let error {
print(error.localizedDescription)
}
仔細看看發(fā)生了什么:
- 1)這將獲取bundle音頻文件的URL。 設置后,它將在上面變量聲明部分的
audioFileURL
的didSet
塊中實例化audioFile
。 - 2)將播放器節(jié)點附加到引擎,在連接其他節(jié)點之前必須執(zhí)行此操作。 這些節(jié)點將生成,處理或輸出音頻。 音頻引擎提供連接到播放器節(jié)點的主混音器節(jié)點。 默認情況下,主混音器連接到
engine
默認輸出節(jié)點(iOS設備揚聲器)。prepare()
預分配所需的資源。
接下來,將以下內容添加到scheduleAudioFile()
:
guard let audioFile = audioFile else { return }
skipFrame = 0
player.scheduleFile(audioFile, at: nil) { [weak self] in
self?.needsFileScheduled = true
}
這會調度播放整個audioFile
。 at:
是您希望音頻播放的未來時間(AVAudioTime)
。 設置為nil
會立即開始播放。 該文件僅調度播放一次。 再次點擊Play
按鈕不會從頭重新開始。 您需要重新調度再次播放。 播放完音頻文件后,在完成塊中設置標志needsFileScheduled
。
還有其他調度音頻用于播放:
-
scheduleBuffer(AVAudioPCMBuffer,completionHandler:AVAudioNodeCompletionHandler?= nil)
:這提供了預先加載音頻數(shù)據(jù)的緩沖區(qū)。 -
scheduleSegment(AVAudioFile,startingFrame:AVAudioFramePosition,frameCount:AVAudioFrameCount,at:AVAudioTime?,completionHandler:AVAudioNodeCompletionHandler?= nil)
:這就像scheduleFile
,除了你指定開始播放的音頻幀和播放的幀數(shù)。
然后,將以下內容添加到playTapped(_ :)
:
// 1
sender.isSelected = !sender.isSelected
// 2
if player.isPlaying {
player.pause()
} else {
if needsFileScheduled {
needsFileScheduled = false
scheduleAudioFile()
}
player.play()
}
下面細分一下:
- 1)切換按鈕的選擇狀態(tài),這會更改sb中設置的按鈕圖像。
- 2)使用
player.isPlaying
確定當前播放器正在播放。 如果是這樣,暫停它,如果不是,請播放。 您還可以檢查needsFileScheduled
并根據(jù)需要重新調度文件。
Build并運行,然后點擊playPauseButton
。 你應該聽到聲音。 但是,沒有UI反饋,你不知道文件有多長或者你現(xiàn)在播放到哪里。
Add Progress Feedback - 增加進度反饋
在viewDidLoad()
中添加如下代碼:
updater = CADisplayLink(target: self, selector: #selector(updateUI))
updater?.add(to: .current, forMode: .defaultRunLoopMode)
updater?.isPaused = true
CADisplayLink
是一個計時器對象,與顯示器的刷新率同步。 您使用方法updateUI
實例化它。 然后,將其添加到運行循環(huán)中 - 在本例中為默認運行循環(huán)default run loop
。 最后,它不需要開始運行,因此將isPaused
設置為true
。
用以下內容替換playTapped(_ :)
的實現(xiàn):
sender.isSelected = !sender.isSelected
if player.isPlaying {
disconnectVolumeTap()
updater?.isPaused = true
player.pause()
} else {
if needsFileScheduled {
needsFileScheduled = false
scheduleAudioFile()
}
connectVolumeTap()
updater?.isPaused = false
player.play()
}
這里的關鍵是當播放器暫停時使用updater.isPaused = true
暫停UI。 您將在下面的VU Meter
部分中了解connectVolumeTap()
和disconnectVolumeTap()
。
使用以下內容替換var currentFrame:AVAudioFramePosition = 0
:
var currentFrame: AVAudioFramePosition {
// 1
guard
let lastRenderTime = player.lastRenderTime,
// 2
let playerTime = player.playerTime(forNodeTime: lastRenderTime)
else {
return 0
}
// 3
return playerTime.sampleTime
}
currentFrame
返回播放器呈現(xiàn)的最后一個音頻樣本。 下面一步步的看:
- 1)
player.lastRenderTime
返回引擎啟動時間的時間。 如果引擎未運行,則lastRenderTime
返回nil。 - 2)
player.playerTime(forNodeTime :)
將lastRenderTime
轉換為相對于播放器開始時間的時間。 如果播放器沒有播放,那么playerTime
將返回nil。 - 3)
sampleTime
是音頻文件中的一些音頻采樣的時間。
現(xiàn)在進行UI更新。 將以下內容添加到updateUI()
:
// 1
currentPosition = currentFrame + skipFrame
currentPosition = max(currentPosition, 0)
currentPosition = min(currentPosition, audioLengthSamples)
// 2
progressBar.progress = Float(currentPosition) / Float(audioLengthSamples)
let time = Float(currentPosition) / audioSampleRate
countUpLabel.text = formatted(time: time)
countDownLabel.text = formatted(time: audioLengthSeconds - time)
// 3
if currentPosition >= audioLengthSamples {
player.stop()
updater?.isPaused = true
playPauseButton.isSelected = false
disconnectVolumeTap()
}
下面我們一步一步的看:
- 1)屬性
skipFrame
是添加到currentFrame
或從currentFrame
中減去的偏移量,最初設置為零。 確保currentPosition
不超出文件范圍。 - 2)將
progressBar.progress
更新為audioFile
中的currentPosition
。 通過將currentPosition
除以audioFile
的sampleRate
來計算時間。 將countUpLabel
和countDownLabel
文本更新為audioFile
中的當前時間。 - 3)如果
currentPosition
位于文件末尾,則:- 停止播放器。
- 暫停計時器。
- 重置
playPauseButton
選擇狀態(tài)。 - 斷開音量tap。
Build并運行,然后點擊playPauseButton
。 再次,您將聽到聲音,但這次progressBar
和計時器標簽提供以前缺少的狀態(tài)信息。
Implement the VU Meter - 實現(xiàn)VU Meter
現(xiàn)在是時候添加VU Meter
功能了。 這是一個UIView定位在暫停圖標的欄之間。 視圖的高度由播放音頻的平均功率決定。 這是您進行某些音頻處理的第一次機會。
您將計算1k音頻樣本緩沖區(qū)的平均功率。 確定音頻樣本緩沖器的平均功率的常用方法是計算樣本的均方根(RMS)。
平均功率是以分貝表示的一系列音頻樣本數(shù)據(jù)的平均值。 還有峰值功率,這是一系列樣本數(shù)據(jù)中的最大值。
在connectVolumeTap()
下面添加以下helper
方法:
func scaledPower(power: Float) -> Float {
// 1
guard power.isFinite else { return 0.0 }
// 2
if power < minDb {
return 0.0
} else if power >= 1.0 {
return 1.0
} else {
// 3
return (fabs(minDb) - fabs(power)) / fabs(minDb)
}
}
scaledPower(power :)
將負功率分貝值轉換為正值,以適應調整上面的volumeMeterHeight.constant
值。 這是它的作用:
- 1)
power.isFinite
檢查以確保功率是有效值 - 即,不是NaN
- 如果不是則返回0.0。 - 2)這將我們的
vuMeter
的dynamic range設置為80db。 對于低于-80.0的任何值,返回0.0。 iOS上的分貝值范圍為-160db,接近靜音,為0db,最大功率。minDb
設置為-80.0,動態(tài)范圍為80db。 您可以更改此值以查看它如何影響vuMeter。 - 3)計算0.0到1.0之間的縮放值。
現(xiàn)在,將以下內容添加到connectVolumeTap()
:
// 1
let format = engine.mainMixerNode.outputFormat(forBus: 0)
// 2
engine.mainMixerNode.installTap(onBus: 0, bufferSize: 1024, format: format) { buffer, when in
// 3
guard
let channelData = buffer.floatChannelData,
let updater = self.updater
else {
return
}
let channelDataValue = channelData.pointee
// 4
let channelDataValueArray = stride(from: 0,
to: Int(buffer.frameLength),
by: buffer.stride).map{ channelDataValue[$0] }
// 5
let rms = sqrt(channelDataValueArray.map{ $0 * $0 }.reduce(0, +) / Float(buffer.frameLength))
// 6
let avgPower = 20 * log10(rms)
// 7
let meterLevel = self.scaledPower(power: avgPower)
DispatchQueue.main.async {
self.volumeMeterHeight.constant = !updater.isPaused ?
CGFloat(min((meterLevel * self.pauseImageHeight), self.pauseImageHeight)) : 0.0
}
}
這里進行細分說明:
- 1)獲取
mainMixerNode
輸出的數(shù)據(jù)格式。 - 2)
installTap(onBus:0,bufferSize:1024,format:format)
使您可以訪問mainMixerNode
輸出總線上的音頻數(shù)據(jù)。您請求1024字節(jié)的緩沖區(qū)大小,但不保證請求的大小,特別是如果您請求的緩沖區(qū)太小或太大。 Apple的文檔沒有說明這些限制是什么。完成block接收AVAudioPCMBuffer
和AVAudioTime
作為參數(shù)。您可以檢查buffer.frameLength
以確定實際的緩沖區(qū)大小。when
提供緩沖區(qū)的捕獲時間。 - 3)
buffer.floatChannelData
為您提供了指向每個樣本數(shù)據(jù)的指針數(shù)組。channelDataValue
是UnsafeMutablePointer <Float>
的數(shù)組 - 4)從
UnsafeMutablePointer <Float>
數(shù)組轉換為Float
數(shù)組會使以后的計算更容易。為此,請使用stride(from:to:by :)
在channelDataValue
中創(chuàng)建索引數(shù)組。然后map{channelDataValue [$ 0]}
以訪問和存儲channelDataValueArray
中的數(shù)據(jù)值。 - 5)計算RMS涉及映射/縮減/除法操作。首先,映射操作對數(shù)組中的所有值進行平方,reduce操作求和。將平方和除以緩沖區(qū)大小,然后取平方根,生成緩沖區(qū)中音頻樣本數(shù)據(jù)的RMS。這應該是介于0.0和1.0之間的值,但可能存在一些邊緣情況,它是負值。
- 6)RMS轉換為分貝(Acoustic Decibel reference)。這應該是-160和0之間的值,但如果
rms
為負,則該值為NaN
。 - 7)將分貝縮放為適合您的
vuMeter
的值。
最后,將以下內容添加到disconnectVolumeTap()
:
engine.mainMixerNode.removeTap(onBus: 0)
volumeMeterHeight.constant = 0
AVAudioEngine
每個總線只允許一次點擊。 在不使用時將其刪除是一種很好的做法。
Build并運行,然后點擊playPauseButton
。 vuMeter
現(xiàn)在處于活動狀態(tài),提供音頻數(shù)據(jù)的平均功率反饋。
Implementing Skip - 實現(xiàn)Skip
是時候實現(xiàn)跳過前進和后退按鈕了。skipForwardButton
在音頻文件中向前跳10秒,skipBackwardButton
跳回10秒。
添加以下內容到seek(to:)
:
guard
let audioFile = audioFile,
let updater = updater
else {
return
}
// 1
skipFrame = currentPosition + AVAudioFramePosition(time * audioSampleRate)
skipFrame = max(skipFrame, 0)
skipFrame = min(skipFrame, audioLengthSamples)
currentPosition = skipFrame
// 2
player.stop()
if currentPosition < audioLengthSamples {
updateUI()
needsFileScheduled = false
// 3
player.scheduleSegment(audioFile,
startingFrame: skipFrame,
frameCount: AVAudioFrameCount(audioLengthSamples - skipFrame),
at: nil) { [weak self] in
self?.needsFileScheduled = true
}
// 4
if !updater.isPaused {
player.play()
}
}
這是進行詳細分解:
- 1)通過乘以
audioSampleRate
將時間(以秒為單位)轉換為幀位置,并將其添加到currentPosition
。然后,確保skipFrame
不在文件開頭之前,也不超過文件末尾。 - 2)
player.stop()
不僅停止播放,還清除所有先前調度的事件。調用updateUI()
將UI設置為新的currentPosition
值。 - 3)
player.scheduleSegment(_:startingFrame:frameCount:at :)
調度從audioFile
的skipFrame
位置開始播放。frameCount
是要播放的幀數(shù)。您想要播放到文件末尾,因此將其設置為audioLengthSamples - skipFrame
。最后,at:nil
指定立即開始播放,而不是在將來的某個時間開始播放。 - 4)如果在調用
skip
之前播放器正在播放,則調用player.play()
以恢復播放。updater.isPaused
可以方便地確定這一點,因為只有先前暫停了播放器才會生效。
Build并運行,然后點擊playPauseButton
。點擊skipBackwardButton
并使用skipForwardButton
跳過前進和后退。觀察progressBar
和計數(shù)標簽的變化。
Implementing Rate Change - 實現(xiàn)播放速率的改變
最后要實現(xiàn)的是改變播放速度。 如今,以超過1倍的速度收聽播客是一項受歡迎的功能。
在setupAudio()
中,替換以下內容:
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: audioFormat)
以及:
engine.attach(player)
engine.attach(rateEffect)
engine.connect(player, to: rateEffect, format: audioFormat)
engine.connect(rateEffect, to: engine.mainMixerNode, format: audioFormat)
這會將rateEffect
(AVAudioUnitTimePitch
節(jié)點)連接到音頻圖并將其連接起來。 此節(jié)點類型是效果節(jié)點,具體來說,它可以改變播放速率和音頻音高。
didChangeRateValue()
action處理對rateSlider
的更改。 它計算rateSliderValues
數(shù)組的索引并設置rateValue
,它設置rateEffect.rate
。 rateSlider
的值范圍為0.5x到3.0x
Build并運行,然后點擊playPauseButton
。 調整rateSlider
就可以聽一下效果聲音了。
參考文章
后記
本篇主要講述了AVAudioEngine之詳細說明和一個簡單示例,感興趣的給個贊或者關注~~~