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 等

相关文章
相关标签/搜索