AudioRecorderController.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import Foundation
  2. import SCSiriWaveformView
  3. import AVKit
  4. protocol AudioRecorderControllerDelegate: class {
  5. func didFinishAudioAtPath(path: String)
  6. func didClose()
  7. }
  8. class AudioRecorderController: UIViewController, AVAudioRecorderDelegate {
  9. weak var delegate: AudioRecorderControllerDelegate?
  10. // Recording...
  11. var meterUpdateDisplayLink: CADisplayLink?
  12. var isRecordingPaused: Bool = false
  13. // maximumRecordDuration > 0 -> restrict max time period for one take
  14. var maximumRecordDuration = 0.0
  15. // Private variables
  16. var oldSessionCategory: AVAudioSession.Category?
  17. var wasIdleTimerDisabled: Bool = false
  18. var recordingFilePath: String = ""
  19. var audioRecorder: AVAudioRecorder?
  20. var normalTintColor: UIColor = UIColor.systemBlue
  21. var highlightedTintColor = UIColor.systemRed
  22. var isFirstUsage: Bool = true
  23. lazy var waveFormView: SCSiriWaveformView = {
  24. let view = SCSiriWaveformView()
  25. view.alpha = 0.0
  26. view.waveColor = .clear
  27. view.backgroundColor = .clear
  28. view.translatesAutoresizingMaskIntoConstraints = false
  29. view.primaryWaveLineWidth = 3.0
  30. view.secondaryWaveLineWidth = 1.0
  31. return view
  32. }()
  33. lazy var noRecordingPermissionView: UIImageView = {
  34. let view = UIImageView(image: UIImage(named: "microphone_access"))
  35. view.translatesAutoresizingMaskIntoConstraints = false
  36. view.alpha = 0.0
  37. view.contentMode = UIView.ContentMode.scaleAspectFit
  38. view.isAccessibilityElement = true
  39. view.accessibilityLabel = """
  40. \(String.localized("perm_required_title"))
  41. \(String.localized("perm_explain_access_to_mic_denied"))
  42. """
  43. return view
  44. }()
  45. lazy var cancelButton: UIBarButtonItem = {
  46. let button = UIBarButtonItem.init(barButtonSystemItem: UIBarButtonItem.SystemItem.cancel,
  47. target: self,
  48. action: #selector(cancelAction))
  49. return button
  50. }()
  51. lazy var doneButton: UIBarButtonItem = {
  52. let button = UIBarButtonItem.init(title: String.localized("menu_send"),
  53. style: UIBarButtonItem.Style.done,
  54. target: self,
  55. action: #selector(doneAction))
  56. return button
  57. }()
  58. lazy var cancelRecordingButton: UIBarButtonItem = {
  59. let button = UIBarButtonItem.init(barButtonSystemItem: UIBarButtonItem.SystemItem.trash,
  60. target: self,
  61. action: #selector(cancelRecordingAction))
  62. button.tintColor = UIColor.themeColor(light: .darkGray, dark: .lightGray)
  63. return button
  64. }()
  65. lazy var pauseButton: UIBarButtonItem = {
  66. let button = UIBarButtonItem.init(barButtonSystemItem: UIBarButtonItem.SystemItem.pause,
  67. target: self,
  68. action: #selector(pauseRecordingButtonAction))
  69. button.tintColor = UIColor.themeColor(light: .darkGray, dark: .lightGray)
  70. return button
  71. }()
  72. lazy var startRecordingButton: UIBarButtonItem = {
  73. let button = UIBarButtonItem.init(image: UIImage(named: "audio_record"),
  74. style: UIBarButtonItem.Style.plain,
  75. target: self,
  76. action: #selector(recordingButtonAction))
  77. button.tintColor = UIColor.themeColor(light: .darkGray, dark: .lightGray)
  78. return button
  79. }()
  80. lazy var continueRecordingButton: UIBarButtonItem = {
  81. let button = UIBarButtonItem.init(image: UIImage(named: "audio_record"),
  82. style: UIBarButtonItem.Style.plain,
  83. target: self,
  84. action: #selector(continueRecordingButtonAction))
  85. button.tintColor = UIColor.themeColor(light: .darkGray, dark: .lightGray)
  86. return button
  87. }()
  88. lazy var flexItem = {
  89. return UIBarButtonItem.init(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil)
  90. }()
  91. override func viewDidLoad() {
  92. super.viewDidLoad()
  93. self.accessibilityViewIsModal = true
  94. self.view.backgroundColor = UIColor.themeColor(light: .white, dark: .black)
  95. self.navigationController?.isToolbarHidden = false
  96. self.navigationController?.toolbar.isTranslucent = true
  97. self.navigationController?.navigationBar.isTranslucent = true
  98. self.navigationItem.title = String.localized("voice_message")
  99. self.navigationItem.leftBarButtonItem = cancelButton
  100. self.navigationItem.rightBarButtonItem = doneButton
  101. waveFormView.frame = self.view.bounds
  102. self.view.addSubview(waveFormView)
  103. self.view.addSubview(noRecordingPermissionView)
  104. waveFormView.fill(view: view)
  105. noRecordingPermissionView.fill(view: view, paddingLeading: 100, paddingTrailing: 100, paddingTop: 200, paddingBottom: 200)
  106. self.navigationController?.toolbar.tintColor = normalTintColor
  107. self.navigationController?.navigationBar.tintColor = normalTintColor
  108. self.navigationController?.navigationBar.isTranslucent = true
  109. self.navigationController?.toolbar.isTranslucent = true
  110. // Define the recorder setting
  111. let recordSettings = [AVFormatIDKey: kAudioFormatMPEG4AAC,
  112. AVSampleRateKey: 44100.0,
  113. AVNumberOfChannelsKey: 1] as [String: Any]
  114. let globallyUniqueString = ProcessInfo.processInfo.globallyUniqueString
  115. recordingFilePath = NSTemporaryDirectory().appending(globallyUniqueString).appending(".m4a")
  116. _ = try? audioRecorder = AVAudioRecorder.init(url: URL(fileURLWithPath: recordingFilePath), settings: recordSettings)
  117. audioRecorder?.delegate = self
  118. audioRecorder?.isMeteringEnabled = true
  119. }
  120. override func viewWillAppear(_ animated: Bool) {
  121. super.viewWillAppear(animated)
  122. startUpdatingMeter()
  123. wasIdleTimerDisabled = UIApplication.shared.isIdleTimerDisabled
  124. NotificationCenter.default.addObserver(self,
  125. selector: #selector(didBecomeActiveNotification),
  126. name: UIApplication.didBecomeActiveNotification,
  127. object: nil)
  128. validateMicrophoneAccess()
  129. }
  130. override func viewDidAppear(_ animated: Bool) {
  131. super.viewDidAppear(animated)
  132. if UIAccessibility.isVoiceOverRunning {
  133. UIAccessibility.post(notification: .layoutChanged, argument: self.doneButton)
  134. }
  135. }
  136. override func viewWillDisappear(_ animated: Bool) {
  137. super.viewWillDisappear(animated)
  138. NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
  139. audioRecorder?.delegate = nil
  140. audioRecorder?.stop()
  141. audioRecorder = nil
  142. stopUpdatingMeter()
  143. UIApplication.shared.isIdleTimerDisabled = wasIdleTimerDisabled
  144. }
  145. override func viewDidDisappear(_ animated: Bool) {
  146. super.viewDidDisappear(animated)
  147. delegate?.didClose()
  148. }
  149. func startUpdatingMeter() {
  150. meterUpdateDisplayLink?.invalidate()
  151. meterUpdateDisplayLink = CADisplayLink.init(target: self, selector: #selector(updateMeters))
  152. meterUpdateDisplayLink?.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
  153. }
  154. func stopUpdatingMeter() {
  155. meterUpdateDisplayLink?.invalidate()
  156. meterUpdateDisplayLink = nil
  157. }
  158. @objc func updateMeters() {
  159. if let audioRecorder = audioRecorder {
  160. if audioRecorder.isRecording || isRecordingPaused {
  161. audioRecorder.updateMeters()
  162. let normalizedValue: Float = pow(10, audioRecorder.averagePower(forChannel: 0) / 20)
  163. waveFormView.waveColor = highlightedTintColor
  164. waveFormView.update(withLevel: CGFloat(normalizedValue))
  165. self.navigationItem.title = String.timeStringForInterval(audioRecorder.currentTime)
  166. } else {
  167. waveFormView.waveColor = normalTintColor
  168. waveFormView.update(withLevel: 0)
  169. }
  170. }
  171. }
  172. @objc func recordingButtonAction() {
  173. logger.debug("start recording")
  174. self.setToolbarItems([flexItem, cancelRecordingButton, flexItem, pauseButton, flexItem], animated: true)
  175. cancelRecordingButton.isEnabled = true
  176. doneButton.isEnabled = true
  177. if FileManager.default.fileExists(atPath: recordingFilePath) {
  178. _ = try? FileManager.default.removeItem(atPath: recordingFilePath)
  179. }
  180. let session = AVAudioSession.sharedInstance()
  181. oldSessionCategory = session.category
  182. _ = try? session.setCategory(AVAudioSession.Category.record)
  183. UIApplication.shared.isIdleTimerDisabled = true
  184. audioRecorder?.prepareToRecord()
  185. isRecordingPaused = false
  186. if maximumRecordDuration <= 0 {
  187. audioRecorder?.record()
  188. } else {
  189. audioRecorder?.record(forDuration: maximumRecordDuration)
  190. }
  191. }
  192. @objc func continueRecordingButtonAction() {
  193. logger.debug("continue recording")
  194. self.setToolbarItems([flexItem, cancelRecordingButton, flexItem, pauseButton, flexItem], animated: true)
  195. isRecordingPaused = false
  196. audioRecorder?.record()
  197. }
  198. @objc func pauseRecordingButtonAction() {
  199. logger.debug("pause")
  200. isRecordingPaused = true
  201. audioRecorder?.pause()
  202. self.setToolbarItems([flexItem, cancelRecordingButton, flexItem, continueRecordingButton, flexItem], animated: true)
  203. }
  204. @objc func cancelRecordingAction() {
  205. logger.debug("cancel recording")
  206. isRecordingPaused = false
  207. cancelRecordingButton.isEnabled = false
  208. doneButton.isEnabled = false
  209. audioRecorder?.stop()
  210. _ = try? FileManager.default.removeItem(atPath: recordingFilePath)
  211. self.navigationItem.title = String.localized("voice_message")
  212. }
  213. @objc func cancelAction() {
  214. logger.debug("cancel Action")
  215. cancelRecordingAction()
  216. dismiss(animated: true, completion: nil)
  217. }
  218. @objc func doneAction() {
  219. logger.debug("done with Action")
  220. isRecordingPaused = false
  221. audioRecorder?.stop()
  222. if let delegate = self.delegate {
  223. delegate.didFinishAudioAtPath(path: recordingFilePath)
  224. }
  225. dismiss(animated: true, completion: nil)
  226. }
  227. @objc func didBecomeActiveNotification() {
  228. validateMicrophoneAccess()
  229. }
  230. func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
  231. if flag {
  232. self.setToolbarItems([flexItem, cancelRecordingButton, flexItem, startRecordingButton, flexItem], animated: true)
  233. if let oldSessionCategory = oldSessionCategory {
  234. _ = try? AVAudioSession.sharedInstance().setCategory(oldSessionCategory)
  235. UIApplication.shared.isIdleTimerDisabled = wasIdleTimerDisabled
  236. }
  237. } else {
  238. try? FileManager.default.removeItem(at: URL(fileURLWithPath: recordingFilePath))
  239. }
  240. }
  241. func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) {
  242. logger.error("audio recording failed: \(error?.localizedDescription ?? "unknown")")
  243. }
  244. func validateMicrophoneAccess() {
  245. let audioSession = AVAudioSession.sharedInstance()
  246. audioSession.requestRecordPermission({(granted: Bool) -> Void in
  247. DispatchQueue.main.async { [weak self] in
  248. if let self = self {
  249. self.noRecordingPermissionView.alpha = granted ? 0.0 : 1.0
  250. self.waveFormView.alpha = granted ? 1.0 : 0.0
  251. self.doneButton.isEnabled = granted
  252. if self.isFirstUsage {
  253. if !granted {
  254. self.setToolbarItems([self.flexItem, self.startRecordingButton, self.flexItem], animated: true)
  255. self.startRecordingButton.isEnabled = false
  256. } else {
  257. self.pauseButton.isEnabled = granted
  258. self.recordingButtonAction()
  259. }
  260. self.isFirstUsage = false
  261. } else {
  262. self.startRecordingButton.isEnabled = granted
  263. }
  264. }
  265. }
  266. })
  267. }
  268. }