iOS Audio 手把手: 錄音、播放、音訊播放控制(音量取樣檢測等),Swift5,基於 AVFoundation
錄音,就要用到麥克風了
iOS 裝置中,每一個應用 app,都有一個音訊會話 Audio Session.
app 呼叫音訊相關,自然會用到 iOS 的硬體功能。
音訊會話 Audio Session ,就是來管理音訊操作的。
iOS 使用音訊,管理粒度很細
你覺得: 後臺播放的音樂,要不要與你 app 的音訊,混雜在一起?
Audio Session 處理音訊,通過他的分類 Audio Session Category 設定
預設的分類,
1, 允許播放,不允許錄音。
2, 靜音按鈕開啟後,你的應用就啞巴了,播放音訊沒聲音。
3, 鎖屏後,你的應用也啞巴了,播放音訊沒聲音。
4, 如果後臺有別的 app 播放音訊,你 app 要開始播放音訊的時候,別的 app 就啞巴了。
更多分類,如圖:
首先要對音訊操作,做一些配置。
一般操作音訊,會用到 AVFoundation
框架,先引入 import AVFoundation
設定 Audio Session 的分類,AVAudioSession.CategoryOptions.defaultToSpeaker
,允許我們的 app,呼叫內建的麥克風來錄音,又可以播放音訊。
這裡要做錄音功能,就把分類的選項也改了。
分類的預設選項是,音訊播放的是收聽者,即上面的喇叭口,場景一般是你把手機拿到耳朵邊,打電話。
現在把音訊播放路徑,指向說話的人,即麥克風,下面的喇叭口。
// 這是一個全域性變數,記錄麥克風許可權的
var appHasMicAccess = true
// ...
// 先獲取一個 AVAudioSession 的例項
let session = AVAudioSession.sharedInstance()
do {
// 在這裡,設定分類
try session.setCategory(AVAudioSession.Category.playAndRecord,options: AVAudioSession.CategoryOptions.defaultToSpeaker)
try session.setActive(true )
// 檢查 app 有沒有許可權,使用該裝置麥克風
session.requestRecordPermission({ (isGranted: Bool) in
if isGranted {
// 你的 app 想要錄製音訊,使用者必須授予麥克風許可權
appHasMicAccess = true
}
else{
appHasMicAccess = false
}
})
} catch let error as NSError {
print("AVAudioSession configuration error: \(error.localizedDescription)")
}
複製程式碼
進入錄音,
// 這是一個列舉變數,用來手動追蹤錄音的狀態
var audioStatus: AudioStatus = AudioStatus.Stopped
var audioRecorder: AVAudioRecorder!
func setupRecorder() {
// getURLforMemo,這個方法,拿到一個可以保存錄音檔案的,臨時路徑
// getURLforMemo , 具體見下面的 GitHub 連結
let fileURL = getURLforMemo()
// 設定錄音取樣的描述資訊
/*
線性脈衝編碼調製,非壓縮的資料格式
取樣頻率, 44.1 千赫茲的,CD 級別的效果
單聲道,就錄製一個單音
*/
let recordSettings = [
AVFormatIDKey: Int(kAudioFormatLinearPCM),AVSampleRateKey: 44100.0,AVNumberOfChannelsKey: 1,AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
] as [String : Any]
do {
// 例項化 audioRecorder
audioRecorder = try AVAudioRecorder(url: fileURL,settings: recordSettings)
audioRecorder.delegate = self
audioRecorder.prepareToRecord()
} catch {
print("Error creating audio Recorder.")
}
}
// 開始錄音
func record() {
startUpdateLoop()
// 追蹤,記錄下當前 app 的錄音狀態
audioStatus = .recording
// 這一行,就是開始錄音了
audioRecorder.record()
}
// 停止錄音
func stopRecording() {
recordButton.setBackgroundImage(UIImage(named: "button-record"),for: UIControl.State.normal )
audioStatus = .stopped
audioRecorder.stop()
stopUpdateLoop()
}
複製程式碼
錄音結束,通過代理 AVAudioRecorderDelegate ,更新狀態
func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder,successfully flag: Bool) {
audioStatus = .stopped
// 因為這個場景,錄製完了, 必須手動點選,
// 所以不需要在這裡更新 UI
}
複製程式碼
錄音好了,做播放
播放錄音
var audioPlayer: AVAudioPlayer!
// 開始播放
func play() {
// getURLforMemo,這個方法,拿到一個可以保存錄音檔案的,臨時路徑
// getURLforMemo , 具體見下面的 GitHub 連結
let fileURL = getURLforMemo()
do {
// 例項化 audioPlayer
audioPlayer = try AVAudioPlayer(contentsOf: fileURL)
audioPlayer.delegate = self
// 檢查音訊檔案不為空,才播放音訊檔案
if audioPlayer.duration > 0.0 {
setPlayButtonOn(flag: true)
audioPlayer.play()
audioStatus = .Playing
startUpdateLoop()
}
} catch {
print("Error loading audio Player")
}
}
// 停止播放
func stopPlayback() {
setPlayButtonOn(flag: false)
audioStatus = .stopped
audioPlayer.stop()
stopUpdateLoop()
}
複製程式碼
播放結束,通過代理 AVAudioPlayerDelegate ,更新 UI
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer,successfully flag: Bool) {
// 因為只有在這裡,我們才知道,播放完了的時機
setPlayButtonOn(flag: false)
audioStatus = .stopped
stopUpdateLoop()
}
複製程式碼
顯示錄音/ 播放進展的 UI
要顯示顯示錄音/ 播放的進展,就要用到計時器了,
因為錄音/ 播放,每時每刻,都在變化。
計時器三步走:
開啟計時器,
var soundTimer: CFTimeInterval = 0.0
var updateTimer: CADisplayLink!
func startUpdateLoop(){
if updateTimer != nil{
updateTimer.invalidate()
}
// 計時器是非常輕量級的物件,使用前,先銷燬
updateTimer = CADisplayLink(target: self,selector: #selector(ViewController.updateLoop))
updateTimer.preferredFramesPerSecond = 1
updateTimer.add(to: RunLoop.current,forMode: RunLoop.Mode.common)
}
複製程式碼
定時,做事情
@objc func updateLoop(){
if audioStatus == .recording{
// 錄音狀態,定時重新整理
if CFAbsoluteTimeGetCurrent() - soundTimer > 0.5 {
timeLabel.text = formattedCurrentTime(UInt(audioRecorder.currentTime))
soundTimer = CFAbsoluteTimeGetCurrent()
}
}
else if audioStatus == .playing{
// 播放狀態,定時重新整理
if CFAbsoluteTimeGetCurrent() - soundTimer > 0.5 {
timeLabel.text = formattedCurrentTime(UInt(audioPlayer.currentTime))
soundTimer = CFAbsoluteTimeGetCurrent()
}
}
}
複製程式碼
銷燬計時器
需要停止的時候,就呼叫這個方法,例如: 播放完成的代理方法中,再一次點選播放按鈕...
func stopUpdateLoop(){
updateTimer.invalidate()
updateTimer = nil
// formattedCurrentTime,這個方法,時間轉文字,具體見文尾的 GitHub 連結
timeLabel.text = formattedCurrentTime(UInt(0))
}
複製程式碼
取樣音量大小計量
AVAudioPlayer 有音訊的計量功能,播放音訊的時候,音訊計量可以檢測到,波形的平均能級等資訊
AVAudioPlayer 的方法 averagePower(forChannel:)
,會返回當前的分貝值,取值範圍是 -160 ~ 0 db, 0 是很吵, -160 是很安靜
波形,長這樣
做一個張口嘴巴的動畫,就是一個簡單的音量大小視覺化,音量越大,張開嘴的幅度也越大,具體見文尾的 GitHub repo
// 自己建立一個結構體,計量表 MeterTable
// 音訊計量返回的浮點數的範圍 -160 ~ 0,先做分貝轉振幅,轉換為 0 ~ 1 之間
// 張口嘴巴的動畫的圖片有 5 張,分為 5 個級別,上面的取值範圍,就要劃分為對應的五個層級,
// MeterTable 就要把採集的聲音,對映到對應的圖片
let meterTable = MeterTable(tableSize: 100)
// ...
// 播放前,先要啟用音量分貝值檢測功能
audioPlayer.isMeteringEnabled = true
// ...
// 將採集到的音量大小,對映為圖片編號
// 更新狀態的方法,一定要用到計時器。
// 該方法,要在計時器方法中使用到,具體見文尾的 github repo
func meterLevelsToFrame() -> Int{
guard let player = audioPlayer else {
return 1
}
player.updateMeters()
// 之前設定了,播放器是單聲道
let avgPower = player.averagePower(forChannel: 0)
let linearLevel = meterTable.valueForPower(power: avgPower)
// 繼續處理資料,轉換出一個能級,具體見文尾的 GitHub repo
let powerPercentage = Int(round(linearLevel * 100))
// 目前總共有 5 張圖片
let totalFrames = 5
// 根據音量大小,決定呈現哪一張
// 圖片命名是 01~05,所以要 + 1
let frame = ( powerPercentage / totalFrames ) + 1
return min(frame,totalFrames)
}
複製程式碼
音訊播放控制: 包含音量大小控制、左右聲道切換、播放迴圈、播放速率控制等等
控制播放音量大小
音量的取值範圍是 0 ~ 1, 0 是靜音,1 是最大
func toSetVolumn(value: Float){
guard let player = audioPlayer else {
return
}
// 蘋果都封裝好了,設定 audioPlayer 的 volume
player.volume = value
}
複製程式碼
設定左右聲道
取值範圍是 -1 到 1,
-1 是全左,1 是全右,0是均衡聲道
func toSetPan(value: Float) {
guard let player = audioPlayer else {
return
}
// 蘋果都封裝好了,設定 audioPlayer 的 pan
player.pan = value
}
複製程式碼
設定播放迴圈
迴圈的取值範圍是 -1 到 Int.max,
numberOfLoops 取值 0 到 Int.max,則會多播放那個取值的次數
func toSetLoopPlayback(loop: Bool) {
guard let player = audioPlayer else {
return
}
// 蘋果都封裝好了,設定 audioPlayer 的 numberOfLoops
if loop == true{
// numberOfLoops 為 -1,無限迴圈,直到 audioPlayer 停止
player.numberOfLoops = -1
}
else{
// numberOfLoops 為 0,僅播放一次,不迴圈
player.numberOfLoops = 0
}
}
複製程式碼
設定播放速率
audioPlayer 的播放速率範圍是,0.5 ~ 2.0
0.5 是半速播放,1.0 是正常播放,2.0 是倍速播放
// 播放前,要點亮 audioPlayer 的播放速率控制,為可用
audioPlayer.enableRate = true
// ...
func toSetRate(value: Float) {
guard let player = audioPlayer else {
return
}
// 蘋果都封裝好了,設定 audioPlayer 的 rate
player.rate = value
}
複製程式碼
github 連結
續集: