| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227 |
- //
- // LNVoiceRecorder.swift
- // Lanu
- //
- // Created by OneeChan on 2025/12/4.
- //
- import Foundation
- import AVFAudio
- enum LNIMChatVoiceRecorderState {
- case ready
- case recording
- case pausing
- }
- protocol LNVoiceRecorderNotify {
- func onRecordTaskDurationChanged(taskId: String, duration: Double, volumeRatio: Double)
- func onRecordTaskRecording(taskId: String)
- func onRecordTaskPause(taskId: String)
- func onRecordTaskStop(taskId: String)
- func onRecordTaskReachMaxDuration(taskId: String, fileUrl: URL?, duration: Double)
- }
- extension LNVoiceRecorderNotify {
- func onRecordTaskDurationChanged(taskId: String, duration: Double, volumeRatio: Double) { }
- func onRecordTaskRecording(taskId: String) { }
- func onRecordTaskPause(taskId: String) { }
- func onRecordTaskStop(taskId: String) { }
- func onRecordTaskReachMaxDuration(taskId: String, fileUrl: URL?, duration: Double) { }
- }
- class LNVoiceRecorder {
- static let shared = LNVoiceRecorder()
-
- private(set) var curState: LNIMChatVoiceRecorderState = .ready
- var isRecording: Bool {
- curState == .recording
- }
- private var recorder: AVAudioRecorder?
-
- private var durationTimer: Timer?
- private let timerInterval = 0.1
- private var curDuration: Double = 0.0
- private var curTaskId: String = ""
- private var maxDuration: Double = -1
-
- private init() { }
-
- func startRecord(_ maxDuration: Double? = nil, handler: @escaping (String?) -> Void) {
- guard curState == .ready,
- setupNewRecorder(),
- recorder?.record() == true
- else {
- handler(nil)
- return
- }
- if let maxDuration {
- self.maxDuration = maxDuration
- } else {
- self.maxDuration = -1
- }
-
- startDurationTimer()
- curState = .recording
- curTaskId = "\(curTime)"
-
- runOnMain { [weak self] in
- guard let self else { return }
- notifyTaskStart()
- }
- handler(curTaskId)
- }
-
- func pauseRecord() {
- guard curState == .recording else { return }
- recorder?.pause()
- stopDurationTimer()
- curState = .pausing
-
- notifyTaskPause()
- }
-
- func continueRecord() {
- guard curState == .pausing else { return }
- if maxDuration != -1,
- maxDuration <= curDuration {
- let result = stopRecord()
- notifyTaskReachMaxDuration(file: result.0, duration: result.1)
- return
- }
-
- recorder?.record()
- startDurationTimer(true)
- curState = .recording
-
- notifyTaskStart()
- }
-
- @discardableResult
- func stopRecord() -> (URL?, Double) {
- recorder?.stop()
- try? AVAudioSession.sharedInstance().setActive(false)
- stopDurationTimer()
- curState = .ready
-
- let filePath = recorder?.url
- notifyTaskStop()
-
- curTaskId = ""
-
- return (filePath, curDuration)
- }
- }
- extension LNVoiceRecorder {
- private func setupNewRecorder() -> Bool {
- recorder?.stop()
- try? AVAudioSession.sharedInstance().setActive(false)
- recorder?.deleteRecording()
-
- do {
- try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default)
- try AVAudioSession.sharedInstance().setActive(true)
-
- // 录音参数配置(M4A 格式,高质量)
- let recordSettings: [String: Any] = [
- AVFormatIDKey: Int(kAudioFormatMPEG4AAC), // 音频格式(AAC)
- AVSampleRateKey: 16000.0, // 采样率
- AVNumberOfChannelsKey: 1, // 声道数(立体声)
- AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue, // 音质
- AVLinearPCMBitDepthKey: 16
- ]
-
- // 创建录音器
- let recorder = try AVAudioRecorder(url: newVoicePath(), settings: recordSettings)
- recorder.isMeteringEnabled = true // 开启音量监测
- recorder.prepareToRecord()
-
- self.recorder = recorder
- curState = .ready
- return true
- } catch {
- Log.e("录音器初始化失败:\(error.localizedDescription)")
- }
- return false
- }
-
- func newVoicePath() -> URL {
- URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("\(UUID().uuidString)__voice_record.m4a")
- }
- }
- extension LNVoiceRecorder {
- private func startDurationTimer(_ isContinue: Bool = false) {
- stopDurationTimer()
- if !isContinue {
- curDuration = 0
- notifyTaskProgress(duration: curDuration, volumeRatio: 0)
- }
- let timer = Timer.scheduledTimer(withTimeInterval: timerInterval, repeats: true, block: { [weak self] _ in
- guard let self else { return }
- curDuration = recorder?.currentTime ?? 0
-
- var volumeRatio: Double = 0
- if let recorder {
- recorder.updateMeters()
- let decibel = Double(recorder.averagePower(forChannel: 0))
- volumeRatio = max(0.0, min(1.0, (decibel + 160.0) / 160.0))
- }
- notifyTaskProgress(duration: curDuration, volumeRatio: volumeRatio)
-
- if maxDuration != -1,
- maxDuration <= curDuration {
- let result = stopRecord()
- notifyTaskReachMaxDuration(file: result.0, duration: result.1)
- }
- })
- timer.tolerance = 0.1 // 允许轻微延迟,提升性能
- RunLoop.current.add(timer, forMode: .common)
- durationTimer = timer
- }
-
- private func stopDurationTimer() {
- durationTimer?.invalidate()
- durationTimer = nil
- }
- }
- extension LNVoiceRecorder {
- private func notifyTaskProgress(duration: Double, volumeRatio: Double) {
- let taskId = curTaskId
- LNEventDeliver.notifyEvent {
- ($0 as? LNVoiceRecorderNotify)?.onRecordTaskDurationChanged(taskId: taskId, duration: duration, volumeRatio: volumeRatio)
- }
- }
-
- private func notifyTaskStop() {
- let taskId = curTaskId
- LNEventDeliver.notifyEvent {
- ($0 as? LNVoiceRecorderNotify)?.onRecordTaskStop(taskId: taskId)
- }
- }
-
- private func notifyTaskStart() {
- let taskId = curTaskId
- LNEventDeliver.notifyEvent {
- ($0 as? LNVoiceRecorderNotify)?.onRecordTaskRecording(taskId: taskId)
- }
- }
-
- private func notifyTaskPause() {
- let taskId = curTaskId
- LNEventDeliver.notifyEvent {
- ($0 as? LNVoiceRecorderNotify)?.onRecordTaskPause(taskId: taskId)
- }
- }
-
- private func notifyTaskReachMaxDuration(file: URL?, duration: Double) {
- let taskId = curTaskId
- LNEventDeliver.notifyEvent {
- ($0 as? LNVoiceRecorderNotify)?.onRecordTaskReachMaxDuration(taskId: taskId, fileUrl: file, duration: duration)
- }
- }
- }
|