iOS Audio 手把手: 录音、播放、音频播放控制(音量采样检测等),Swift5,基于 AVFoundation

7,731 阅读6分钟

录音,就要用到麦克风了

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 等