1. 程式人生 > IOS開發 >iOS Audio 手把手: 錄音、播放、音訊播放控制(音量取樣檢測等),Swift5,基於 AVFoundation

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 就啞巴了。

更多分類,如圖:

0

首先要對音訊操作,做一些配置。

一般操作音訊,會用到 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 是很安靜

波形,長這樣

1

做一個張口嘴巴的動畫,就是一個簡單的音量大小視覺化,音量越大,張開嘴的幅度也越大,具體見文尾的 GitHub repo

d

// 自己建立一個結構體,計量表 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 連結

續集:

iOS Audio hand by hand: 變聲,混響,語音合成 TTS,Swift5,基於 AVAudioEngine 等