iOS开发-IM语音录制

这里主要介绍一下语音录制功能的实现,主要使用的录音器AVAudioRecorder,这里涉及到麦克风权限,这个是从以前项目爬出来的,下面给出删减过后的文件内容


import UIKit
import AVFoundation

class kjSwiftExHelper: NSObject {
    
    class func kjKeywindow() -> UIWindow? {
        var window: UIWindow?
        if #available(iOS 13.0, *) {
            for winScene in ((UIApplication.shared.connectedScenes as? Set<UIWindowScene>)!) {
                if winScene.activationState == .foregroundActive {
                    window = winScene.windows.first
                    break
                }
            }
        } else {
            window = UIApplication.shared.keyWindow
        }
        return window
    }
    
    class func kjWindows() -> [UIWindow] {
        var windows = Array<UIWindow>()
        
        if #available(iOS 13.0, *) {
            for winScene in ((UIApplication.shared.connectedScenes as? Set<UIWindowScene>)!) {
                if winScene.activationState == .foregroundActive {
                    windows = winScene.windows
                    break
                }
            }
        } else {
            windows = UIApplication.shared.windows
        }
        
        return windows
    }
    
    /// 获取当前正在展示的控制器
    class func kjDisplayViewController() -> UIViewController? {
        
        func kj_topViewController(v: UIViewController?) -> UIViewController? {
            if v is UINavigationController {
                return kj_topViewController(v: (v as! UINavigationController).topViewController)
            } else if v is UITabBarController {
                return kj_topViewController(v: (v as! UITabBarController).selectedViewController)
            }
            return v
        }
        let window = kjKeywindow()
        var ctrl: UIViewController? = kj_topViewController(v: window?.rootViewController)
        while let presentVC = ctrl?.presentedViewController {
            ctrl = kj_topViewController(v: presentVC)
        }
        return ctrl
    }
    
    /// 麦克风权限 true-麦克风可用,false-麦克风不可用
    class func kjVerifyMicrophoneAuth() -> Bool {
        let micStatus = AVCaptureDevice.authorizationStatus(for: AVMediaType.audio)
        guard micStatus == AVAuthorizationStatus.authorized else {
            //麦克风不可用
            if micStatus == AVAuthorizationStatus.notDetermined {
                //未提示授权
                AVAudioSession.sharedInstance().requestRecordPermission { (allowed) in
                    //开始授权
                }
            } else {
                //提示去设置开启麦克风权限
                let alert = UIAlertController(title: "无法使用麦克风",
                                              message: "需要开启麦克风权限才能进行语音答题和TA聊天,请在iPhone设置中开启",
                                              preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "开启",
                                              style: .default,
                                              handler: { (action) in
                                                // 去设置页开启麦克风权限
                                                if let url = URL(string: UIApplication.openSettingsURLString) {
                                                    UIApplication.shared.open(url, options: [:], completionHandler: nil)
                                                }
                }))
                alert.addAction(UIAlertAction(title: "取消",
                                              style: .cancel,
                                              handler: { (action) in
                                                
                }))
                
                if let viewController = kjSwiftExHelper.kjDisplayViewController() {
                    alert.show(viewController, sender: nil)
                }
            }
            return false
        }
        //麦克风可用
        return true
    }
}

下面直接给出录音器管理类的所有内容,注释都有,感觉没必要在这里写那么清楚


import UIKit
import AVFoundation


// MARK: - 协议
protocol KJAudioRecorderDelegate: AnyObject {
    
    /// 语音录制成功
    /// - Parameters:
    ///   - kjUrl: 语音文件的本地路径
    ///   - kjDur: 语音文件的时长
    func kjAudioRecorderSucceed(kjUrl: URL, kjDur: TimeInterval) -> Void
    
    
    /// 语音录制失败
    /// - Parameter kjError: 失败的原因
    func kjAudioRecorderFailure(kjError: NSError) -> Void
    
    
    /// 录音时长变化
    /// - Parameter kjDur: 时长
    func kjAudioFileDurationChanged(kjDur: TimeInterval) -> Void
    
    
    /// 音量变化
    /// - Parameter kjVol: 音量
    func kjAudioVolumeChanged(kjVol: Double) -> Void
}

// 可选实现
extension KJAudioRecorderDelegate {
    /// 录音时长变化
    /// - Parameter kjDur: 时长
    func kjVoiceFileDurationChanged(kjDur: TimeInterval) -> Void {}
    
    
    /// 音量变化
    /// - Parameter kjVol: 音量
    func kjVoiceVolumeChanged(kjVol: Double) -> Void {}
}


// MARK: - 枚举 错误码

enum KJVoiceRecorderErrorCode: Int {
    case kjPathError = 0        // 路径错误
    case kjRecorderError = 1    // 录音器初始化失败
    case kjMicAuthError = 2     // 麦克风权限
    case kjInterruption = 3     // 被中断
    case kjEncodeError = 4
}


// MARK: - 录音器

class kjAudioRecorder: NSObject {

    /// 录音器
    fileprivate var kjRecorder: AVAudioRecorder?
    
    /// 代理
    fileprivate weak var kjRecorderDelegate: KJAudioRecorderDelegate?
    
    /// 最大时长 默认为0-表示不限时长
    fileprivate var kjMaxDur: TimeInterval = 0
    
    /// 定时器
    fileprivate var kjTimer: Timer?
    
    /// 录音器配置
    fileprivate lazy var kjConfigure: Dictionary<String, Any> = {
        return [
            //音频格式
            AVFormatIDKey: kAudioFormatMPEG4AAC,
            //采样率
            AVSampleRateKey: 44100,
            //声道
            AVNumberOfChannelsKey: 2,
            //采样位数
            AVLinearPCMBitDepthKey: 32,
            //比特采样率
            AVEncoderBitRateKey: 128000
        ]
    }()
    
    deinit {
        kjTimer?.invalidate()
        removeNotification()
    }
    
    // MARK: - 外部调用
    
    /// 外部初始化
    /// - Parameters:
    ///   - maxDuration: 可录制最大时间
    ///   - delegate: 代理
    init(maxDuration: TimeInterval = 0, delegate: KJAudioRecorderDelegate) {
        super.init()
        kjRecorderDelegate = delegate
        kjMaxDur = maxDuration
        kjTimer = Timer(timeInterval: 0.2, repeats: true, block: { [weak self] (timer) in
            self?.getAudioDurationAndVolume()
        })
        if kjTimer != nil {
            RunLoop.current.add(kjTimer!, forMode: RunLoop.Mode.common)
            kjTimer?.fireDate = Date.distantFuture
        }
    }
    
    /// 录音器状态
    func isRecording() -> Bool {
        return kjRecorder?.isRecording ?? false
    }
    
    /// 开始录音
    func begin() {
        // 麦克风权限
        guard kjSwiftExHelper.kjVerifyMicrophoneAuth() == true else {
            // 麦克风不可用
            kjRecorderDelegate?.kjAudioRecorderFailure(kjError: kjHandleError(code: .kjMicAuthError))
            return
        }
        // 文件路径
        guard let fileUrl = kjAudioFilePath() else {
            kjRecorderDelegate?.kjAudioRecorderFailure(kjError: kjHandleError(code: .kjPathError))
            return
        }
        // 初始化录音器
        do {
            kjRecorder = try AVAudioRecorder(url: fileUrl, settings: kjConfigure)
        } catch {
            print(error)
        }
        guard kjRecorder != nil else {
            kjRecorderDelegate?.kjAudioRecorderFailure(kjError: kjHandleError(code: .kjRecorderError))
            return
        }
        // 设置会话
        handleAudioSession(isOpen: true)
        // 协议
        kjRecorder?.delegate = self
        // 开启音量检测
        kjRecorder?.isMeteringEnabled = true
        // 准备录音
        guard kjRecorder?.prepareToRecord() == true else {
            kjRecorderDelegate?.kjAudioRecorderFailure(kjError: kjHandleError(code: .kjRecorderError))
            handleAudioSession(isOpen: false)
            return
        }
        if start() == false {
            // 录音器开启失败
            handleAudioSession(isOpen: false)
        }
    }
    
    /// 重启录音
    func resume() -> Void {
        if start() == false {
            // 录音器开启失败
            handleAudioSession(isOpen: false)
        }
    }
    
    /// 暂停录音
    func pause() {
        if kjRecorder?.isRecording == true {
            kjTimer?.fireDate = Date.distantFuture
            kjRecorder?.pause()
        }
    }
    
    /// 停止录音
    func stop() {
        if kjRecorder?.isRecording == true {
            kjRecorder?.stop()
            handleAudioSession(isOpen: false)
        }
    }
    
    /// 取消录制 (会删除已录制的文件)
    func cancel() {
        pause()
        if kjRecorder?.deleteRecording() == false {
            // 取消录制失败
            if let file = kjRecorder?.url {
                //手动删除文件
                if FileManager.default.fileExists(atPath: file.path) == true {
                    try? FileManager.default.removeItem(at: file)
                }
                stop()
            }
        }
        handleAudioSession(isOpen: false)
    }
    
    // MARK: - 私有
    
    fileprivate func start() -> Bool {
        if kjRecorder != nil && kjRecorder?.isRecording == false {
            if (kjMaxDur > 0 ? kjRecorder?.record(forDuration: kjMaxDur) : kjRecorder?.record()) == false {
                // 录音器开启成功
                kjTimer?.fireDate = Date.distantPast
                return true
            }
        }
        return false
    }
    
    /// 处理 AudioSession
    ///
    /// - Parameter isOpen: 是否开启 true - 开启(让其他有声音的应用都停止), false - 关闭(让其他正在播放的应用能继续播放)
    fileprivate func handleAudioSession(isOpen: Bool) {
        let session = AVAudioSession.sharedInstance()
        if isOpen {
            // 开启定时器
            kjTimer?.fireDate = Date.distantPast
            try? session.setCategory(AVAudioSession.Category.record)
            try? session.setActive(true, options: AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation)
        } else {
            // 停止定时器
            kjTimer?.fireDate = Date.distantFuture
            try? session.setActive(false, options: AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation)
        }
    }
    
    /// 获取当前音量和已录制的时长
    fileprivate func getAudioDurationAndVolume() {
        if kjRecorder != nil && kjRecorder?.isRecording == true {
            //音量
            kjRecorder?.updateMeters()
            let voi = pow(10.0, 0.05 * kjRecorder!.peakPower(forChannel: 0))
            kjRecorderDelegate?.kjAudioVolumeChanged(kjVol: Double(voi))
            //时长
            kjRecorderDelegate?.kjAudioFileDurationChanged(kjDur: kjRecorder!.currentTime)
        }
    }
    
    
    /// 组装错误信息
    /// - Parameter code: 错误码
    /// - Returns: NSError
    fileprivate func kjHandleError(code: KJVoiceRecorderErrorCode) -> NSError {
        var errorInfo = ""
        switch code {
        case .kjPathError:
            errorInfo = "音频录音获取失败"
            break
        case .kjRecorderError:
            errorInfo = "录音器初始化失败"
            break
        case .kjMicAuthError:
            errorInfo = "没有麦克风权限"
            break
        case .kjInterruption:
            errorInfo = "录音被(电话、闹铃等)中断"
            break
        case .kjEncodeError:
            errorInfo = "录音编码失败"
            break
        }
        return NSError(domain: "JOIMIMAudioRec",
                       code: code.rawValue,
                       userInfo: [NSDebugDescriptionErrorKey: errorInfo])
    }
    
    /// 音频录制保存的路径
    ///
    /// - Returns: 路径
    fileprivate func kjAudioFilePath() -> URL? {
        if let dir = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last {
            let file = dir + "/audio"
            // 创建文件夹
            if FileManager.default.fileExists(atPath: file) == false {
                try? FileManager.default.createDirectory(atPath: file, withIntermediateDirectories: true, attributes: nil)
            }
            return URL(fileURLWithPath: file + "/" + UUID().uuidString + ".aac")
        }
        return nil
    }
    
    // MARK: - 通知
    
    /// 添加被中断的通知
    fileprivate func addInterruptionNotification() {
        NotificationCenter.default.addObserver(self, selector: #selector(interruption(ntf:)), name: AVAudioSession.interruptionNotification, object: nil)
    }
    
    /// 移除通知
    fileprivate func removeNotification() {
        NotificationCenter.default.removeObserver(self)
    }
    
    /// 处理被中断
    @objc fileprivate func interruption(ntf: Notification) {
        if ntf.userInfo != nil {
            if let interuptType = ntf.userInfo![AVAudioSessionInterruptionTypeKey] as? UInt {
                if interuptType == AVAudioSession.SilenceSecondaryAudioHintType.begin.rawValue {
                    kjRecorderDelegate?.kjAudioRecorderFailure(kjError: kjHandleError(code: .kjInterruption))
                }
            }
        }
        cancel()
    }
    
}


// MARK: - AVAudioRecorderDelegate
extension kjAudioRecorder: AVAudioRecorderDelegate {
    /// 录音完成的回调
    func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
        handleAudioSession(isOpen: false)
        if flag {
            // 获取录音时长
            let asset = AVURLAsset.init(url: recorder.url)
            kjRecorderDelegate?.kjAudioRecorderSucceed(kjUrl: recorder.url, kjDur: CMTimeGetSeconds(asset.duration))
        } else {
            cancel()
        }
    }
    
    /// 编码错误的回调
    func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) {
        handleAudioSession(isOpen: false)
        kjRecorderDelegate?.kjAudioRecorderFailure(kjError: kjHandleError(code: .kjEncodeError))
    }
}

这里只提供了开始录制begin()、重启录制resume ()、暂停录制pause()、停止录制stop()、取消录制cancel(),语音格式是.aac,只是一个简单的语音录制功能,如果需要扩展,请自行复制下来后修改,这里已经放出所有的代码,希望能帮助到你,感谢你的阅读。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339