AlarmSound.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. // LoopFollow
  2. // AlarmSound.swift
  3. import AVFoundation
  4. import Foundation
  5. import MediaPlayer
  6. import UIKit
  7. /*
  8. * Class that handles the playing and the volume of the alarm sound.
  9. */
  10. class AlarmSound {
  11. static var isPlaying: Bool {
  12. return audioPlayer?.isPlaying == true
  13. }
  14. static var isMuted: Bool {
  15. return muted
  16. }
  17. static var whichAlarm: String = "none"
  18. static var soundFile = "Indeed"
  19. static var isTesting: Bool = false
  20. fileprivate static var systemOutputVolumeBeforeOverride: Float?
  21. fileprivate static var soundURL = Bundle.main.url(forResource: "Indeed", withExtension: "caf")!
  22. fileprivate static var audioPlayer: AVAudioPlayer?
  23. fileprivate static let audioPlayerDelegate = AudioPlayerDelegate()
  24. fileprivate static var muted = false
  25. fileprivate static var alarmPlayingForTimer = Timer()
  26. fileprivate static let alarmPlayingForInterval = 290
  27. fileprivate static var repeatDelay: TimeInterval = 0
  28. fileprivate func startAlarmPlayingForTimer(time: TimeInterval) {
  29. AlarmSound.alarmPlayingForTimer = Timer.scheduledTimer(timeInterval: time,
  30. target: self,
  31. selector: #selector(AlarmSound.alarmPlayingForTimerDidEnd(_:)),
  32. userInfo: nil,
  33. repeats: true)
  34. }
  35. @objc func alarmPlayingForTimerDidEnd(_: Timer) {
  36. if !AlarmSound.isPlaying { return }
  37. AlarmSound.stop()
  38. }
  39. /*
  40. * Sets the audio volume to 0.
  41. */
  42. static func muteVolume() {
  43. audioPlayer?.volume = 0
  44. muted = true
  45. restoreSystemOutputVolume()
  46. }
  47. static func setSoundFile(str: String) {
  48. soundURL = Bundle.main.url(forResource: str, withExtension: "caf")!
  49. }
  50. /*
  51. * Sets the volume of the alarm back to the volume before it has been muted.
  52. */
  53. static func unmuteVolume() {
  54. audioPlayer?.volume = 1.0
  55. muted = false
  56. }
  57. static func stop() {
  58. Observable.shared.alarmSoundPlaying.value = false
  59. repeatDelay = 0
  60. audioPlayer?.stop()
  61. audioPlayer = nil
  62. restoreSystemOutputVolume()
  63. }
  64. static func playTest() {
  65. guard !isPlaying else {
  66. return
  67. }
  68. do {
  69. audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
  70. audioPlayer!.delegate = audioPlayerDelegate
  71. try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
  72. try AVAudioSession.sharedInstance().setActive(true)
  73. audioPlayer?.numberOfLoops = 0
  74. if !audioPlayer!.prepareToPlay() {
  75. LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play")
  76. }
  77. if audioPlayer!.play() {
  78. if !isPlaying {
  79. LogManager.shared.log(category: .alarm, message: "AlarmSound - not playing after calling play")
  80. LogManager.shared.log(category: .alarm, message: "AlarmSound - rate value: \(audioPlayer!.rate)")
  81. }
  82. } else {
  83. LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play")
  84. }
  85. } catch {
  86. LogManager.shared.log(category: .alarm, message: "AlarmSound - unable to play sound; error: \(error)")
  87. }
  88. }
  89. static func play(repeating: Bool, delay: Int = 0) {
  90. guard !isPlaying else {
  91. return
  92. }
  93. enableAudio()
  94. // If repeating with delay, we'll handle it manually via the delegate
  95. // Only set repeatDelay if both repeating and delay > 0
  96. repeatDelay = (repeating && delay > 0) ? TimeInterval(delay) : 0
  97. do {
  98. audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
  99. audioPlayer!.delegate = audioPlayerDelegate
  100. try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
  101. try AVAudioSession.sharedInstance().setActive(true)
  102. // Only use numberOfLoops if we're not using delay-based repeating
  103. // When repeatDelay > 0, we play once and then use the delegate to schedule the next play with delay
  104. audioPlayer!.numberOfLoops = (repeating && repeatDelay == 0) ? -1 : 0
  105. // Store existing volume
  106. if systemOutputVolumeBeforeOverride == nil {
  107. systemOutputVolumeBeforeOverride = AVAudioSession.sharedInstance().outputVolume
  108. }
  109. if !audioPlayer!.prepareToPlay() {
  110. LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play")
  111. }
  112. // First sound plays immediately - delay only applies between repeated sounds
  113. if audioPlayer!.play() {
  114. if !isPlaying {
  115. LogManager.shared.log(category: .alarm, message: "AlarmSound - not playing after calling play")
  116. LogManager.shared.log(category: .alarm, message: "AlarmSound - rate value: \(audioPlayer!.rate)")
  117. } else {
  118. Observable.shared.alarmSoundPlaying.value = true
  119. if repeatDelay > 0 {
  120. LogManager.shared.log(category: .alarm, message: "AlarmSound - first sound playing immediately, delay (\(repeatDelay)s) will apply between repeats")
  121. }
  122. }
  123. } else {
  124. LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play")
  125. }
  126. if Storage.shared.alarmConfiguration.value.overrideSystemOutputVolume {
  127. MPVolumeView.setVolume(Storage.shared.alarmConfiguration.value.forcedOutputVolume)
  128. }
  129. } catch {
  130. LogManager.shared.log(category: .alarm, message: "AlarmSound - unable to play sound; error: \(error)")
  131. }
  132. }
  133. fileprivate static func playNextWithDelay() {
  134. guard repeatDelay > 0 else {
  135. return
  136. }
  137. DispatchQueue.main.asyncAfter(deadline: .now() + repeatDelay) {
  138. // Check if we should still be playing (user might have stopped it)
  139. guard repeatDelay > 0 else {
  140. return
  141. }
  142. // Clean up the previous player
  143. audioPlayer?.stop()
  144. audioPlayer = nil
  145. do {
  146. audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
  147. audioPlayer!.delegate = audioPlayerDelegate
  148. try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
  149. try AVAudioSession.sharedInstance().setActive(true)
  150. audioPlayer!.numberOfLoops = 0
  151. if !audioPlayer!.prepareToPlay() {
  152. LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play (delayed repeat)")
  153. }
  154. if audioPlayer!.play() {
  155. Observable.shared.alarmSoundPlaying.value = true
  156. } else {
  157. LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play (delayed repeat)")
  158. }
  159. } catch {
  160. LogManager.shared.log(category: .alarm, message: "AlarmSound - unable to play sound (delayed repeat); error: \(error)")
  161. }
  162. }
  163. }
  164. static func playTerminated() {
  165. guard !isPlaying else {
  166. return
  167. }
  168. do {
  169. audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
  170. audioPlayer!.delegate = audioPlayerDelegate
  171. try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
  172. try AVAudioSession.sharedInstance().setActive(true)
  173. // Play endless loops
  174. audioPlayer!.numberOfLoops = 2
  175. // Store existing volume
  176. if systemOutputVolumeBeforeOverride == nil {
  177. systemOutputVolumeBeforeOverride = AVAudioSession.sharedInstance().outputVolume
  178. }
  179. if !audioPlayer!.prepareToPlay() {
  180. LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - audio player failed preparing to play")
  181. }
  182. if audioPlayer!.play() {
  183. if !isPlaying {
  184. LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - not playing after calling play")
  185. LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - rate value: \(audioPlayer!.rate)")
  186. }
  187. } else {
  188. LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - audio player failed to play")
  189. }
  190. MPVolumeView.setVolume(1.0)
  191. } catch {
  192. LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - unable to play sound; error: \(error)")
  193. }
  194. }
  195. fileprivate static func restoreSystemOutputVolume() {
  196. guard Storage.shared.alarmConfiguration.value.overrideSystemOutputVolume else {
  197. return
  198. }
  199. // cancel any volume change observations
  200. // self.volumeChangeDetector.isActive = false
  201. // restore system output volume with its value before overriding it
  202. if let volumeBeforeOverride = systemOutputVolumeBeforeOverride {
  203. MPVolumeView.setVolume(volumeBeforeOverride)
  204. }
  205. systemOutputVolumeBeforeOverride = nil
  206. }
  207. fileprivate static func enableAudio() {
  208. do {
  209. try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
  210. try AVAudioSession.sharedInstance().setActive(true)
  211. LogManager.shared.log(category: .alarm, message: "Audio session configured for alarm playback")
  212. } catch {
  213. LogManager.shared.log(category: .alarm, message: "Enable audio error: \(error)")
  214. }
  215. }
  216. }
  217. class AudioPlayerDelegate: NSObject, AVAudioPlayerDelegate {
  218. /* audioPlayerDidFinishPlaying:successfully: is called when a sound has finished playing. This method is NOT called if the player is stopped due to an interruption. */
  219. func audioPlayerDidFinishPlaying(_: AVAudioPlayer, successfully flag: Bool) {
  220. LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerDidFinishPlaying (\(flag))", isDebug: true)
  221. // If we're repeating with delay, schedule the next play
  222. if AlarmSound.repeatDelay > 0 {
  223. AlarmSound.playNextWithDelay()
  224. } else {
  225. Observable.shared.alarmSoundPlaying.value = false
  226. }
  227. }
  228. /* if an error occurs while decoding it will be reported to the delegate. */
  229. func audioPlayerDecodeErrorDidOccur(_: AVAudioPlayer, error: Error?) {
  230. if let error = error {
  231. LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerDecodeErrorDidOccur: \(error)")
  232. } else {
  233. LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerDecodeErrorDidOccur")
  234. }
  235. }
  236. /* AVAudioPlayer INTERRUPTION NOTIFICATIONS ARE DEPRECATED - Use AVAudioSession instead. */
  237. /* audioPlayerBeginInterruption: is called when the audio session has been interrupted while the player was playing. The player will have been paused. */
  238. func audioPlayerBeginInterruption(_: AVAudioPlayer) {
  239. LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerBeginInterruption")
  240. Observable.shared.alarmSoundPlaying.value = false
  241. }
  242. /* audioPlayerEndInterruption:withOptions: is called when the audio session interruption has ended and this player had been interrupted while playing. */
  243. /* Currently the only flag is AVAudioSessionInterruptionFlags_ShouldResume. */
  244. func audioPlayerEndInterruption(_: AVAudioPlayer, withOptions flags: Int) {
  245. LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerEndInterruption withOptions: \(flags)")
  246. Observable.shared.alarmSoundPlaying.value = false
  247. }
  248. }
  249. // Helper function inserted by Swift 4.2 migrator.
  250. private func convertFromAVAudioSessionCategory(_ input: AVAudioSession.Category) -> String {
  251. return input.rawValue
  252. }
  253. extension MPVolumeView {
  254. static func setVolume(_ volume: Float) {
  255. // Need to use the MPVolumeView in order to change volume, but don't care about UI set so frame to .zero
  256. let volumeView = MPVolumeView(frame: .zero)
  257. // Search for the slider
  258. let slider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider
  259. // Update the slider value with the desired volume.
  260. DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) {
  261. slider?.value = volume
  262. }
  263. // Optional - Remove the HUD
  264. if let app = UIApplication.shared.delegate as? AppDelegate, let window = app.window {
  265. volumeView.alpha = 0.000001
  266. window.addSubview(volumeView)
  267. }
  268. }
  269. }