AlarmSound.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  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. setSoundFile(.builtin(str))
  49. }
  50. static func setSoundFile(_ file: SoundFile) {
  51. if let resolved = resolveURL(for: file) {
  52. soundURL = resolved
  53. return
  54. }
  55. LogManager.shared.log(
  56. category: .alarm,
  57. message: "AlarmSound - missing sound for \(file.id); falling back to \(SoundFile.fallback.id)"
  58. )
  59. if let fallback = resolveURL(for: .fallback) {
  60. soundURL = fallback
  61. }
  62. }
  63. private static func resolveURL(for file: SoundFile) -> URL? {
  64. switch file {
  65. case let .builtin(name):
  66. return Bundle.main.url(forResource: name, withExtension: "caf")
  67. case let .custom(uuid):
  68. return CustomSoundStore.shared.url(for: uuid)
  69. }
  70. }
  71. /*
  72. * Sets the volume of the alarm back to the volume before it has been muted.
  73. */
  74. static func unmuteVolume() {
  75. audioPlayer?.volume = 1.0
  76. muted = false
  77. }
  78. static func stop() {
  79. Observable.shared.alarmSoundPlaying.value = false
  80. repeatDelay = 0
  81. audioPlayer?.stop()
  82. audioPlayer = nil
  83. restoreSystemOutputVolume()
  84. }
  85. static func playTest() {
  86. guard !isPlaying else {
  87. return
  88. }
  89. do {
  90. audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
  91. audioPlayer!.delegate = audioPlayerDelegate
  92. activateAudioSessionWithFallback()
  93. audioPlayer?.numberOfLoops = 0
  94. if !audioPlayer!.prepareToPlay() {
  95. LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play")
  96. }
  97. if audioPlayer!.play() {
  98. if !isPlaying {
  99. LogManager.shared.log(category: .alarm, message: "AlarmSound - not playing after calling play")
  100. LogManager.shared.log(category: .alarm, message: "AlarmSound - rate value: \(audioPlayer!.rate)")
  101. }
  102. } else {
  103. LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play")
  104. }
  105. } catch {
  106. LogManager.shared.log(category: .alarm, message: "AlarmSound - unable to play sound; error: \(error)")
  107. }
  108. }
  109. static func play(repeating: Bool, delay: Int = 0) {
  110. guard !isPlaying else {
  111. return
  112. }
  113. // If repeating with delay, we'll handle it manually via the delegate
  114. // Only set repeatDelay if both repeating and delay > 0
  115. repeatDelay = (repeating && delay > 0) ? TimeInterval(delay) : 0
  116. do {
  117. audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
  118. audioPlayer!.delegate = audioPlayerDelegate
  119. activateAudioSessionWithFallback()
  120. // Only use numberOfLoops if we're not using delay-based repeating
  121. // When repeatDelay > 0, we play once and then use the delegate to schedule the next play with delay
  122. audioPlayer!.numberOfLoops = (repeating && repeatDelay == 0) ? -1 : 0
  123. // Store existing volume
  124. if systemOutputVolumeBeforeOverride == nil {
  125. systemOutputVolumeBeforeOverride = AVAudioSession.sharedInstance().outputVolume
  126. }
  127. if !audioPlayer!.prepareToPlay() {
  128. LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play")
  129. }
  130. // First sound plays immediately - delay only applies between repeated sounds
  131. if audioPlayer!.play() {
  132. if !isPlaying {
  133. LogManager.shared.log(category: .alarm, message: "AlarmSound - not playing after calling play (rate \(audioPlayer!.rate))")
  134. } else {
  135. Observable.shared.alarmSoundPlaying.value = true
  136. if repeatDelay > 0 {
  137. LogManager.shared.log(category: .alarm, message: "AlarmSound - first sound playing immediately, delay (\(repeatDelay)s) will apply between repeats")
  138. }
  139. }
  140. } else {
  141. LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play")
  142. }
  143. if Storage.shared.alarmConfiguration.value.overrideSystemOutputVolume {
  144. MPVolumeView.setVolume(Storage.shared.alarmConfiguration.value.forcedOutputVolume)
  145. }
  146. } catch {
  147. LogManager.shared.log(category: .alarm, message: "AlarmSound - unable to play sound; error: \(error)")
  148. }
  149. }
  150. fileprivate static func playNextWithDelay() {
  151. guard repeatDelay > 0 else {
  152. return
  153. }
  154. DispatchQueue.main.asyncAfter(deadline: .now() + repeatDelay) {
  155. // Check if we should still be playing (user might have stopped it)
  156. guard repeatDelay > 0 else {
  157. return
  158. }
  159. // Clean up the previous player
  160. audioPlayer?.stop()
  161. audioPlayer = nil
  162. do {
  163. audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
  164. audioPlayer!.delegate = audioPlayerDelegate
  165. activateAudioSessionWithFallback()
  166. audioPlayer!.numberOfLoops = 0
  167. if !audioPlayer!.prepareToPlay() {
  168. LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play (delayed repeat)")
  169. }
  170. if audioPlayer!.play() {
  171. Observable.shared.alarmSoundPlaying.value = true
  172. } else {
  173. LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play (delayed repeat)")
  174. }
  175. } catch {
  176. LogManager.shared.log(category: .alarm, message: "AlarmSound - unable to play sound (delayed repeat); error: \(error)")
  177. }
  178. }
  179. }
  180. static func playTerminated() {
  181. guard !isPlaying else {
  182. return
  183. }
  184. do {
  185. audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
  186. audioPlayer!.delegate = audioPlayerDelegate
  187. activateAudioSessionWithFallback()
  188. // Play endless loops
  189. audioPlayer!.numberOfLoops = 2
  190. // Store existing volume
  191. if systemOutputVolumeBeforeOverride == nil {
  192. systemOutputVolumeBeforeOverride = AVAudioSession.sharedInstance().outputVolume
  193. }
  194. if !audioPlayer!.prepareToPlay() {
  195. LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - audio player failed preparing to play")
  196. }
  197. if audioPlayer!.play() {
  198. if !isPlaying {
  199. LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - not playing after calling play")
  200. LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - rate value: \(audioPlayer!.rate)")
  201. }
  202. } else {
  203. LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - audio player failed to play")
  204. }
  205. MPVolumeView.setVolume(1.0)
  206. } catch {
  207. LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - unable to play sound; error: \(error)")
  208. }
  209. }
  210. fileprivate static func restoreSystemOutputVolume() {
  211. guard Storage.shared.alarmConfiguration.value.overrideSystemOutputVolume else {
  212. return
  213. }
  214. // cancel any volume change observations
  215. // self.volumeChangeDetector.isActive = false
  216. // restore system output volume with its value before overriding it
  217. if let volumeBeforeOverride = systemOutputVolumeBeforeOverride {
  218. MPVolumeView.setVolume(volumeBeforeOverride)
  219. }
  220. systemOutputVolumeBeforeOverride = nil
  221. }
  222. // Background activation of a non-mixable .playback session is denied by iOS
  223. // (cannotInterruptOthers, 560557684) unless the app is already actively playing
  224. // audio. In foreground, or with Silent Tune holding a mixable session alive,
  225. // options: [] succeeds and lets the alarm dominate other audio. For
  226. // Bluetooth-heartbeat users with no Silent Tune we skip [] (it would always
  227. // be denied) and ladder through mixable options so activation is still
  228. // permitted from background. Each attempt is logged so we can see in the
  229. // field which fallback (if any) the user landed on.
  230. fileprivate static func activateAudioSessionWithFallback() {
  231. let isBackgroundWithoutSilentTune = UIApplication.shared.applicationState == .background
  232. && Storage.shared.backgroundRefreshType.value != .silentTune
  233. let dominate: (label: String, options: AVAudioSession.CategoryOptions) = ("[]", [])
  234. let duck: (label: String, options: AVAudioSession.CategoryOptions) = (".duckOthers", .duckOthers)
  235. let mix: (label: String, options: AVAudioSession.CategoryOptions) = (".mixWithOthers", .mixWithOthers)
  236. let candidates = isBackgroundWithoutSilentTune ? [duck, mix] : [dominate, duck, mix]
  237. for candidate in candidates {
  238. do {
  239. try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: candidate.options)
  240. try AVAudioSession.sharedInstance().setActive(true)
  241. LogManager.shared.log(category: .alarm, message: "AlarmSound - audio session active (options: \(candidate.label))")
  242. return
  243. } catch {
  244. let nsError = error as NSError
  245. LogManager.shared.log(category: .alarm, message: "AlarmSound - audio session activation failed (options: \(candidate.label)) [code \(nsError.code)]: \(error.localizedDescription)")
  246. }
  247. }
  248. LogManager.shared.log(category: .alarm, message: "AlarmSound - all audio session option fallbacks exhausted")
  249. }
  250. }
  251. class AudioPlayerDelegate: NSObject, AVAudioPlayerDelegate {
  252. /* audioPlayerDidFinishPlaying:successfully: is called when a sound has finished playing. This method is NOT called if the player is stopped due to an interruption. */
  253. func audioPlayerDidFinishPlaying(_: AVAudioPlayer, successfully flag: Bool) {
  254. LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerDidFinishPlaying (\(flag))", isDebug: true)
  255. // If we're repeating with delay, schedule the next play
  256. if AlarmSound.repeatDelay > 0 {
  257. AlarmSound.playNextWithDelay()
  258. } else {
  259. Observable.shared.alarmSoundPlaying.value = false
  260. }
  261. }
  262. /* if an error occurs while decoding it will be reported to the delegate. */
  263. func audioPlayerDecodeErrorDidOccur(_: AVAudioPlayer, error: Error?) {
  264. if let error = error {
  265. LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerDecodeErrorDidOccur: \(error)")
  266. } else {
  267. LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerDecodeErrorDidOccur")
  268. }
  269. }
  270. /* AVAudioPlayer INTERRUPTION NOTIFICATIONS ARE DEPRECATED - Use AVAudioSession instead. */
  271. /* audioPlayerBeginInterruption: is called when the audio session has been interrupted while the player was playing. The player will have been paused. */
  272. func audioPlayerBeginInterruption(_: AVAudioPlayer) {
  273. LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerBeginInterruption")
  274. Observable.shared.alarmSoundPlaying.value = false
  275. }
  276. /* audioPlayerEndInterruption:withOptions: is called when the audio session interruption has ended and this player had been interrupted while playing. */
  277. /* Currently the only flag is AVAudioSessionInterruptionFlags_ShouldResume. */
  278. func audioPlayerEndInterruption(_: AVAudioPlayer, withOptions flags: Int) {
  279. LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerEndInterruption withOptions: \(flags)")
  280. Observable.shared.alarmSoundPlaying.value = false
  281. }
  282. }
  283. // Helper function inserted by Swift 4.2 migrator.
  284. private func convertFromAVAudioSessionCategory(_ input: AVAudioSession.Category) -> String {
  285. return input.rawValue
  286. }
  287. extension MPVolumeView {
  288. static func setVolume(_ volume: Float) {
  289. // Need to use the MPVolumeView in order to change volume, but don't care about UI set so frame to .zero
  290. let volumeView = MPVolumeView(frame: .zero)
  291. // Search for the slider
  292. let slider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider
  293. // Update the slider value with the desired volume.
  294. DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) {
  295. slider?.value = volume
  296. }
  297. // Optional - Remove the HUD
  298. let activeWindow = UIApplication.shared.connectedScenes
  299. .compactMap { $0 as? UIWindowScene }
  300. .first { $0.activationState == .foregroundActive }?
  301. .windows.first(where: \.isKeyWindow)
  302. if let window = activeWindow {
  303. volumeView.alpha = 0.000001
  304. window.addSubview(volumeView)
  305. }
  306. }
  307. }