LNVoiceRecorder.swift 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. //
  2. // LNVoiceRecorder.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/12/4.
  6. //
  7. import Foundation
  8. import AVFAudio
  9. enum LNIMChatVoiceRecorderState {
  10. case ready
  11. case recording
  12. case pausing
  13. }
  14. protocol LNVoiceRecorderNotify {
  15. func onRecordTaskDurationChanged(taskId: String, duration: Double, volumeRatio: Double)
  16. func onRecordTaskRecording(taskId: String)
  17. func onRecordTaskPause(taskId: String)
  18. func onRecordTaskStop(taskId: String)
  19. func onRecordTaskReachMaxDuration(taskId: String, fileUrl: URL?, duration: Double)
  20. }
  21. extension LNVoiceRecorderNotify {
  22. func onRecordTaskDurationChanged(taskId: String, duration: Double, volumeRatio: Double) { }
  23. func onRecordTaskRecording(taskId: String) { }
  24. func onRecordTaskPause(taskId: String) { }
  25. func onRecordTaskStop(taskId: String) { }
  26. func onRecordTaskReachMaxDuration(taskId: String, fileUrl: URL?, duration: Double) { }
  27. }
  28. class LNVoiceRecorder {
  29. static let shared = LNVoiceRecorder()
  30. private(set) var curState: LNIMChatVoiceRecorderState = .ready
  31. var isRecording: Bool {
  32. curState == .recording
  33. }
  34. private var recorder: AVAudioRecorder?
  35. private var durationTimer: Timer?
  36. private let timerInterval = 0.1
  37. private var curDuration: Double = 0.0
  38. private var curTaskId: String = ""
  39. private var maxDuration: Double = -1
  40. private init() { }
  41. func startRecord(_ maxDuration: Double? = nil, handler: @escaping (String?) -> Void) {
  42. guard curState == .ready,
  43. setupNewRecorder(),
  44. recorder?.record() == true
  45. else {
  46. handler(nil)
  47. return
  48. }
  49. if let maxDuration {
  50. self.maxDuration = maxDuration
  51. } else {
  52. self.maxDuration = -1
  53. }
  54. startDurationTimer()
  55. curState = .recording
  56. curTaskId = "\(curTime)"
  57. runOnMain { [weak self] in
  58. guard let self else { return }
  59. notifyTaskStart()
  60. }
  61. handler(curTaskId)
  62. }
  63. func pauseRecord() {
  64. guard curState == .recording else { return }
  65. recorder?.pause()
  66. stopDurationTimer()
  67. curState = .pausing
  68. notifyTaskPause()
  69. }
  70. func continueRecord() {
  71. guard curState == .pausing else { return }
  72. if maxDuration != -1,
  73. maxDuration <= curDuration {
  74. let result = stopRecord()
  75. notifyTaskReachMaxDuration(file: result.0, duration: result.1)
  76. return
  77. }
  78. recorder?.record()
  79. startDurationTimer(true)
  80. curState = .recording
  81. notifyTaskStart()
  82. }
  83. @discardableResult
  84. func stopRecord() -> (URL?, Double) {
  85. recorder?.stop()
  86. try? AVAudioSession.sharedInstance().setActive(false)
  87. stopDurationTimer()
  88. curState = .ready
  89. let filePath = recorder?.url
  90. notifyTaskStop()
  91. curTaskId = ""
  92. return (filePath, curDuration)
  93. }
  94. }
  95. extension LNVoiceRecorder {
  96. private func setupNewRecorder() -> Bool {
  97. recorder?.stop()
  98. try? AVAudioSession.sharedInstance().setActive(false)
  99. recorder?.deleteRecording()
  100. do {
  101. try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default)
  102. try AVAudioSession.sharedInstance().setActive(true)
  103. // 录音参数配置(M4A 格式,高质量)
  104. let recordSettings: [String: Any] = [
  105. AVFormatIDKey: Int(kAudioFormatMPEG4AAC), // 音频格式(AAC)
  106. AVSampleRateKey: 16000.0, // 采样率
  107. AVNumberOfChannelsKey: 1, // 声道数(立体声)
  108. AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue, // 音质
  109. AVLinearPCMBitDepthKey: 16
  110. ]
  111. // 创建录音器
  112. let recorder = try AVAudioRecorder(url: newVoicePath(), settings: recordSettings)
  113. recorder.isMeteringEnabled = true // 开启音量监测
  114. recorder.prepareToRecord()
  115. self.recorder = recorder
  116. curState = .ready
  117. return true
  118. } catch {
  119. Log.e("录音器初始化失败:\(error.localizedDescription)")
  120. }
  121. return false
  122. }
  123. func newVoicePath() -> URL {
  124. URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("\(UUID().uuidString)__voice_record.m4a")
  125. }
  126. }
  127. extension LNVoiceRecorder {
  128. private func startDurationTimer(_ isContinue: Bool = false) {
  129. stopDurationTimer()
  130. if !isContinue {
  131. curDuration = 0
  132. notifyTaskProgress(duration: curDuration, volumeRatio: 0)
  133. }
  134. let timer = Timer.scheduledTimer(withTimeInterval: timerInterval, repeats: true, block: { [weak self] _ in
  135. guard let self else { return }
  136. curDuration = recorder?.currentTime ?? 0
  137. var volumeRatio: Double = 0
  138. if let recorder {
  139. recorder.updateMeters()
  140. let decibel = Double(recorder.averagePower(forChannel: 0))
  141. volumeRatio = max(0.0, min(1.0, (decibel + 160.0) / 160.0))
  142. }
  143. notifyTaskProgress(duration: curDuration, volumeRatio: volumeRatio)
  144. if maxDuration != -1,
  145. maxDuration <= curDuration {
  146. let result = stopRecord()
  147. notifyTaskReachMaxDuration(file: result.0, duration: result.1)
  148. }
  149. })
  150. timer.tolerance = 0.1 // 允许轻微延迟,提升性能
  151. RunLoop.current.add(timer, forMode: .common)
  152. durationTimer = timer
  153. }
  154. private func stopDurationTimer() {
  155. durationTimer?.invalidate()
  156. durationTimer = nil
  157. }
  158. }
  159. extension LNVoiceRecorder {
  160. private func notifyTaskProgress(duration: Double, volumeRatio: Double) {
  161. let taskId = curTaskId
  162. LNEventDeliver.notifyEvent {
  163. ($0 as? LNVoiceRecorderNotify)?.onRecordTaskDurationChanged(taskId: taskId, duration: duration, volumeRatio: volumeRatio)
  164. }
  165. }
  166. private func notifyTaskStop() {
  167. let taskId = curTaskId
  168. LNEventDeliver.notifyEvent {
  169. ($0 as? LNVoiceRecorderNotify)?.onRecordTaskStop(taskId: taskId)
  170. }
  171. }
  172. private func notifyTaskStart() {
  173. let taskId = curTaskId
  174. LNEventDeliver.notifyEvent {
  175. ($0 as? LNVoiceRecorderNotify)?.onRecordTaskRecording(taskId: taskId)
  176. }
  177. }
  178. private func notifyTaskPause() {
  179. let taskId = curTaskId
  180. LNEventDeliver.notifyEvent {
  181. ($0 as? LNVoiceRecorderNotify)?.onRecordTaskPause(taskId: taskId)
  182. }
  183. }
  184. private func notifyTaskReachMaxDuration(file: URL?, duration: Double) {
  185. let taskId = curTaskId
  186. LNEventDeliver.notifyEvent {
  187. ($0 as? LNVoiceRecorderNotify)?.onRecordTaskReachMaxDuration(taskId: taskId, fileUrl: file, duration: duration)
  188. }
  189. }
  190. }