版本記錄
版本號 | 時間 |
---|---|
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之詳細說明和一個簡單示例泼舱,感興趣的給個贊或者關注~~~