iOS Audio 手把手: 錄音、播放、音頻播放控制(音量採樣檢測等),Swift5,基於 AVFoundation

錄音,就要用到麥克風了git

iOS 設備中,每個應用 app,都有一個音頻會話 Audio Session.github

app 調用音頻相關,天然會用到 iOS 的硬件功能。bash

音頻會話 Audio Session ,就是來管理音頻操做的。session

iOS 使用音頻,管理粒度很細

你以爲: 後臺播放的音樂,要不要與你 app 的音頻,混雜在一塊兒?app

Audio Session 處理音頻,經過他的分類 Audio Session Category 設置框架

默認的分類,oop

1, 容許播放,不容許錄音。post

2, 靜音按鈕開啓後,你的應用就啞吧了,播放音頻沒聲音。動畫

3, 鎖屏後,你的應用也啞吧了,播放音頻沒聲音。ui

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 等

相關文章
相關標籤/搜索