iOS 录音及播放(含蓝牙)- Swift版

因项目需要,经常接触蓝牙音频相关的操作,如录音、播放、音频播放控制等,但截止目前,项目的工程都是以 OC 来实现,体验了 Swift 便利,自已用 Swift 写了个实现录音、播放(含蓝牙)的 Demo。

一、前言

1.1 音频主要参数

  • 采样率(samplerate): 模拟信号数字化的过程,每秒钟采集的数据量,采样频率越高,数据量越大,音质越好
  • 声道数(channels): 即单声道或双声道 (iPhone 无法直接采集双声道,但可以模拟,即复制一份采集到的单声道数据.安卓部分机型可以)
  • 位宽: 每个采样点的大小,位数越多,表示越精细,音质越好,一般是 8 bit 或 16 bit
  • 数据格式: iOS 端设备采集的原始数据为线性 PCM 类型音频数据
  • 其他: 还可以设置采样值的精度,每个数据包有几帧数据,每帧数据占多少字节等等

1.2 Audio Session

APP 调用音频相关,自然会用到 iOS 的硬件功能,iOS 设备中,每一个应用 APP,都有一个音频会话 Audio Session ,就是来管理音频操作的,基于 AVFoundation。

iOS 使用音频,管理粒度很细,Audio Session 处理音频,通过它的分类 Audio Session Category 设置默认的分类,

  • 允许播放,不允许录音
  • 静音按钮开启后,应用就哑巴了,播放音频没声音
  • 锁屏后,应用也哑巴了,播放音频没声音
  • 如果后台有别的 APP 播放音频,你 APP 要开始播放音频的时候,别的 APP 就哑巴了
  • 设置混播或单播模式

二、手机录音(MIC)及播放

2.1 申请权限

右击 Info.plist , 选择以源码方式打开,如下图

在列表中根据自己需要选择添加对应的 Key-Value,常用的有:

 <key>NSCalendarsUsageDescription</key>
 <string>为了更好的体验,请允许访问日历</string>
 <key>NSCameraUsageDescription</key>
 <string>为了更好的体验,请允许访问您的相机</string>
 <key>NSContactsUsageDescription</key>
 <string>为了更好的体验,请允许访问您的联系人</string>
 <key>NSLocationAlwaysUsageDescription</key>
 <string>为了更好的体验,请允许app后台获取位置</string>
 <key>NSLocationWhenInUseUsageDescription</key>
 <string>为了更好的体验,请允许使用时获取位置</string>
 <key>NSMicrophoneUsageDescription</key>
 <string>为了更好的体验,请允许访问您的麦克风</string>
 <key>NSPhotoLibraryUsageDescription</key>
 <string>为了更好的体验,请允许访问您的相册</string>
 <key>NSRemindersUsageDescription</key>
 <string>为了更好的体验,请允许访问备忘录</string>

本例中因用到 MIC 权限,所以只需添加 MIC 权限即可以。

2.2 初始化 Audio Session

设置 Audio Session 的分类,AVAudioSession.CategoryOptions.defaultToSpeaker , 允许我们的 APP 调用内置的麦克风来录音,又可以播放音频。
这里要做录音及播放功能,就把分类的选项也改了。

    let session = AVAudioSession.sharedInstance()
    //设置session类型
    do {
        try session.setCategory(AVAudioSessionCategoryPlayAndRecord)
    } catch let err{
        print("设置类型失败:\(err.localizedDescription)")
    }
    //设置session动作
    do {
        try session.setActive(true)
    } catch let err {
        print("初始化动作失败:\(err.localizedDescription)")
    }

2.3 录音初始化

录音初始化包括:设置录音采样的描述信息,实例化 audioRecorder

    var soundRecorder : AVAudioRecorder!
    let file_path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first?.appending("/record.wav")

    //录音设置,注意,后面需要转换成NSNumber,如果不转换,你会发现,无法录制音频文件,猜测是因为底层还是用OC写的原因
    let recordSetting: [String: Any] = [AVSampleRateKey: NSNumber(value: 16000),//采样率
                         AVFormatIDKey: NSNumber(value: kAudioFormatLinearPCM),//音频格式
                         AVLinearPCMBitDepthKey: NSNumber(value: 16),//采样位数
                         AVNumberOfChannelsKey: NSNumber(value: 1),//通道数
                         AVEncoderAudioQualityKey: NSNumber(value: AVAudioQuality.min.rawValue)//录音质量
    ];

    do {
        let audioFilename = URL(fileURLWithPath: file_path!)
        soundRecorder = try AVAudioRecorder(url: audioFilename, settings: recordSetting )
        soundRecorder.delegate = self
        soundRecorder.prepareToRecord()
    } catch {
        print(error)
    }

2.4 开始录音及停止录音

开始录音及停止录音涉及两个接口

    //开始录音
    soundRecorder.record()
    //停止录音
    soundRecorder.stop()

绑定按键操作

    if recordBTN.titleLabel?.text == "Record" {
        soundRecorder.record()
        recordBTN.setTitle("Stop", for: .normal)
        playBTN.isEnabled = false
    } else {
        soundRecorder.stop()
        recordBTN.setTitle("Record", for: .normal)
        playBTN.isEnabled = false
    }

2.5 播放

录音好了,开始播放,首先进行初始化

    var soundPlayer : AVAudioPlayer!

    func setupPlayer() {
        let audioFilename = URL(fileURLWithPath: file_path!)
        do {
            soundPlayer = try AVAudioPlayer(contentsOf: audioFilename)
            soundPlayer.delegate = self
            soundPlayer.prepareToPlay()
            soundPlayer.volume = 1.0
        } catch {
            print(error)
        }
    }

开始播放

    if playBTN.titleLabel?.text == "Play" {
        playBTN.setTitle("Stop", for: .normal)
        recordBTN.isEnabled = false
        setupPlayer()
        soundPlayer.play()
    } else {
        soundPlayer.stop()
        playBTN.setTitle("Play", for: .normal)
        recordBTN.isEnabled = false
    }

三、蓝牙录音及播放

蓝牙录音及播放实际上与手机本机操作差不多,只需要设置下 Audio Session。

手机本机设置下 Audio Session :

    do {
        try session.setCategory(AVAudioSessionCategoryPlayAndRecord)
    } catch let err{
        print("设置类型失败:\(err.localizedDescription)")
    }

蓝牙录音的 Audio Session 设置:

    do {
        try session.setCategory(AVAudioSessionCategoryPlayAndRecord,with: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP])
    } catch let err{
        print("设置类型失败:\(err.localizedDescription)")
    }

其他的不需要特殊处理。


附加操作

采样音量大小计量

AVAudioPlayer 有音频的计量功能,播放音频的时候,音频计量可以检测到,波形的平均能级等信息
AVAudioPlayer 的方法 averagePower(forChannel:),会返回当前的分贝值,取值范围是 -160 ~ 0 db, 0 是很吵, -160 是很安静
波形,长这样

控制播放音量大小

音量的取值范围是 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
    }