RealAudioRecorder.swift 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import Foundation
  2. import Combine
  3. import AVFoundation
  4. /// Real audio recorder using AVAudioRecorder for production recording.
  5. /// Requests microphone permissions, configures AVAudioSession, and outputs AAC (.m4a) files.
  6. final class RealAudioRecorder: AudioRecorderProtocol {
  7. // MARK: - Published Properties
  8. @Published var isRecording: Bool = false
  9. @Published var elapsedTime: TimeInterval = 0
  10. @Published var currentAmplitude: Float = 0
  11. @Published var waveformSamples: [Float] = []
  12. @Published var outputFileURL: URL? = nil
  13. @Published var isPaused: Bool = false
  14. // MARK: - Private Properties
  15. private var audioRecorder: AVAudioRecorder?
  16. private var timer: Timer?
  17. private let timerInterval: TimeInterval = 0.05
  18. private let maxSampleCount = 200
  19. init() {
  20. NotificationCenter.default.addObserver(
  21. self,
  22. selector: #selector(handleInterruption),
  23. name: AVAudioSession.interruptionNotification,
  24. object: AVAudioSession.sharedInstance()
  25. )
  26. }
  27. // MARK: - AudioRecorderProtocol
  28. func startRecording() {
  29. guard !isRecording else { return }
  30. // Reset state
  31. elapsedTime = 0
  32. currentAmplitude = 0
  33. waveformSamples = []
  34. outputFileURL = nil
  35. // Request Permission
  36. let session = AVAudioSession.sharedInstance()
  37. session.requestRecordPermission { [weak self] granted in
  38. guard let self else { return }
  39. if granted {
  40. DispatchQueue.main.async {
  41. self.setupAndRecord()
  42. }
  43. } else {
  44. print("[RealAudioRecorder] Microphone permission denied")
  45. }
  46. }
  47. }
  48. private func setupAndRecord() {
  49. let session = AVAudioSession.sharedInstance()
  50. do {
  51. // Set up audio session category and mode for recording, ducking other apps
  52. try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth, .duckOthers])
  53. try session.setActive(true)
  54. // Create target file URL
  55. let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
  56. let filename = "recording_\(UUID().uuidString).m4a"
  57. let fileURL = documents.appendingPathComponent(filename)
  58. self.outputFileURL = fileURL
  59. // Configure AAC recorder settings
  60. let settings: [String: Any] = [
  61. AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
  62. AVSampleRateKey: 12000.0,
  63. AVNumberOfChannelsKey: 1,
  64. AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
  65. ]
  66. let recorder = try AVAudioRecorder(url: fileURL, settings: settings)
  67. recorder.isMeteringEnabled = true
  68. if recorder.prepareToRecord() {
  69. recorder.record()
  70. self.audioRecorder = recorder
  71. self.isRecording = true
  72. self.isPaused = false
  73. self.startTimer()
  74. print("[RealAudioRecorder] Recording started: \(fileURL.lastPathComponent)")
  75. } else {
  76. print("[RealAudioRecorder] Prepare to record failed")
  77. }
  78. } catch {
  79. print("[RealAudioRecorder] Setup failed: \(error.localizedDescription)")
  80. }
  81. }
  82. func stopRecording() {
  83. guard isRecording else { return }
  84. invalidateTimer()
  85. audioRecorder?.stop()
  86. audioRecorder = nil
  87. isRecording = false
  88. isPaused = false
  89. currentAmplitude = 0
  90. // Deactivate audio session
  91. try? AVAudioSession.sharedInstance().setActive(false)
  92. print("[RealAudioRecorder] Recording stopped. File saved.")
  93. }
  94. func pauseRecording() {
  95. guard isRecording else { return }
  96. audioRecorder?.pause()
  97. isPaused = true
  98. invalidateTimer()
  99. }
  100. func resumeRecording() {
  101. guard isRecording else { return }
  102. try? AVAudioSession.sharedInstance().setActive(true)
  103. audioRecorder?.record()
  104. isPaused = false
  105. startTimer()
  106. }
  107. // MARK: - Private Helpers
  108. private func startTimer() {
  109. invalidateTimer()
  110. timer = Timer.scheduledTimer(withTimeInterval: timerInterval, repeats: true) { [weak self] _ in
  111. self?.tick()
  112. }
  113. if let timer {
  114. RunLoop.current.add(timer, forMode: .common)
  115. }
  116. }
  117. private func invalidateTimer() {
  118. timer?.invalidate()
  119. timer = nil
  120. }
  121. private func tick() {
  122. guard let recorder = audioRecorder, recorder.isRecording else { return }
  123. // Update metering
  124. recorder.updateMeters()
  125. elapsedTime = recorder.currentTime
  126. // Get average decibels for channel 0
  127. let power = recorder.averagePower(forChannel: 0)
  128. // Map decibels (-50.0 dB to 0.0 dB) to 0.0 - 1.0 amplitude
  129. let minDb: Float = -50.0
  130. let level: Float
  131. if power < minDb {
  132. level = 0
  133. } else if power >= 0 {
  134. level = 1
  135. } else {
  136. level = (power - minDb) / -minDb
  137. }
  138. currentAmplitude = level
  139. waveformSamples.append(level)
  140. if waveformSamples.count > maxSampleCount {
  141. waveformSamples.removeFirst(waveformSamples.count - maxSampleCount)
  142. }
  143. }
  144. @objc private func handleInterruption(notification: Notification) {
  145. guard let userInfo = notification.userInfo,
  146. let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
  147. let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
  148. return
  149. }
  150. switch type {
  151. case .began:
  152. DispatchQueue.main.async { [weak self] in
  153. guard let self else { return }
  154. if self.isRecording && !self.isPaused {
  155. self.pauseRecording()
  156. print("[RealAudioRecorder] Audio session interrupted. Recording automatically paused.")
  157. }
  158. }
  159. case .ended:
  160. guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
  161. let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
  162. if options.contains(.shouldResume) {
  163. DispatchQueue.main.async { [weak self] in
  164. guard let self else { return }
  165. if self.isRecording && self.isPaused {
  166. self.resumeRecording()
  167. print("[RealAudioRecorder] Interruption ended. Recording automatically resumed.")
  168. }
  169. }
  170. }
  171. @unknown default:
  172. break
  173. }
  174. }
  175. deinit {
  176. NotificationCenter.default.removeObserver(self)
  177. invalidateTimer()
  178. audioRecorder?.stop()
  179. }
  180. }