SpeakBG.swift 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. // LoopFollow
  2. // SpeakBG.swift
  3. // Created by Jonas Björkert on 2025-05-03.
  4. import AVFoundation
  5. import CallKit
  6. import Foundation
  7. extension MainViewController {
  8. func evaluateSpeakConditions(currentValue: Int, previousValue: Int) {
  9. guard Storage.shared.speakBG.value else {
  10. return
  11. }
  12. let always = Storage.shared.speakBGAlways.value
  13. let lowThreshold = Storage.shared.speakLowBGLimit.value
  14. let fastDropDelta = Storage.shared.speakFastDropDelta.value
  15. let highThreshold = Storage.shared.speakHighBGLimit.value
  16. let speakLowBG = Storage.shared.speakLowBG.value
  17. let speakProactiveLowBG = Storage.shared.speakProactiveLowBG.value
  18. let speakHighBG = Storage.shared.speakHighBG.value
  19. // Speak always
  20. if always {
  21. speakBG(currentValue: currentValue, previousValue: previousValue)
  22. LogManager.shared.log(category: .general, message: "Speaking because 'Always' is enabled.", isDebug: true)
  23. return
  24. }
  25. // Speak if low or last value was low
  26. if speakLowBG {
  27. if currentValue <= Int(lowThreshold) || previousValue <= Int(lowThreshold) {
  28. speakBG(currentValue: currentValue, previousValue: previousValue)
  29. LogManager.shared.log(category: .general, message: "Speaking because of 'Low' condition.", isDebug: true)
  30. return
  31. }
  32. }
  33. // Speak predictive low if...
  34. // * low or last value was low
  35. // * next predictive value is low
  36. // * fast drop occurs below high
  37. if speakProactiveLowBG {
  38. let predictiveTrigger = !predictionData.isEmpty && Double(predictionData.first!.sgv) <= lowThreshold
  39. if predictiveTrigger ||
  40. currentValue <= Int(lowThreshold) || previousValue <= Int(lowThreshold) ||
  41. (currentValue <= Int(highThreshold) && (previousValue - currentValue) >= Int(fastDropDelta))
  42. {
  43. speakBG(currentValue: currentValue, previousValue: previousValue)
  44. LogManager.shared.log(category: .general, message: "Speaking because of 'Proactive Low' condition. Predictive trigger: \(predictiveTrigger)", isDebug: true)
  45. return
  46. }
  47. }
  48. // Speak if high or if last value was high
  49. if speakHighBG {
  50. if currentValue >= Int(highThreshold) || previousValue >= Int(highThreshold) {
  51. speakBG(currentValue: currentValue, previousValue: previousValue)
  52. LogManager.shared.log(category: .general, message: "Speaking because of 'High' condition.", isDebug: true)
  53. return
  54. }
  55. }
  56. LogManager.shared.log(category: .general, message: "No condition met for speaking.", isDebug: true)
  57. }
  58. struct AnnouncementTexts {
  59. var stable: String
  60. var increase: String
  61. var decrease: String
  62. var currentBGIs: String
  63. static func forLanguage(_ language: String) -> AnnouncementTexts {
  64. switch language {
  65. case "it":
  66. return AnnouncementTexts(
  67. stable: "ed è stabile",
  68. increase: "ed è salita di",
  69. decrease: "ed è scesa di",
  70. currentBGIs: "Glicemia attuale è"
  71. )
  72. case "sk":
  73. return AnnouncementTexts(
  74. stable: "a je stabilná",
  75. increase: "a stúpla o",
  76. decrease: "a klesla o",
  77. currentBGIs: "Aktuálna glykémia je"
  78. )
  79. case "sv":
  80. return AnnouncementTexts(
  81. stable: "och det är stabilt",
  82. increase: "och det har ökat med",
  83. decrease: "och det har minskat med",
  84. currentBGIs: "Blodsockret är"
  85. )
  86. case "en": fallthrough
  87. default:
  88. return AnnouncementTexts(
  89. stable: "and it is stable",
  90. increase: "and it is up",
  91. decrease: "and it is down",
  92. currentBGIs: "Glucose is"
  93. )
  94. }
  95. }
  96. }
  97. enum LanguageVoiceMapping {
  98. static let voiceLanguageMap: [String: String] = [
  99. "en": "en-US",
  100. "it": "it-IT",
  101. "sk": "sk-SK",
  102. "sv": "sv-SE",
  103. ]
  104. static func voiceLanguageCode(forAppLanguage appLanguage: String) -> String {
  105. return voiceLanguageMap[appLanguage, default: "en-US"]
  106. }
  107. }
  108. // Speaks the current blood glucose value and the change from the previous value.
  109. // Repeated calls to the function within 30 seconds are prevented.
  110. func speakBG(currentValue: Int, previousValue: Int) {
  111. let audioSession = AVAudioSession.sharedInstance()
  112. do {
  113. try audioSession.setCategory(.playback, mode: .default)
  114. try audioSession.setActive(true)
  115. } catch {
  116. LogManager.shared.log(category: .alarm, message: "speakBG, Failed to set up audio session: \(error)")
  117. }
  118. // Get the current time
  119. let currentTime = Date()
  120. // Check if speakBG was called less than 30 seconds ago. If so, prevent repeated announcements and return.
  121. // If `lastSpeechTime` is `nil` (i.e., this is the first time `speakBG` is being called), use `Date.distantPast` as the default
  122. // value to ensure that the `guard` statement passes and the announcement is made.
  123. guard currentTime.timeIntervalSince(lastSpeechTime ?? .distantPast) >= 30 else {
  124. LogManager.shared.log(category: .general, message: "Repeated calls to speakBG detected!", isDebug: true)
  125. return
  126. }
  127. // Update the last speech time
  128. lastSpeechTime = currentTime
  129. let bloodGlucoseDifference = currentValue - previousValue
  130. let preferredLanguage = Storage.shared.speakLanguage.value
  131. let voiceLanguageCode = LanguageVoiceMapping.voiceLanguageCode(forAppLanguage: preferredLanguage)
  132. let texts = AnnouncementTexts.forLanguage(preferredLanguage)
  133. let negligibleThreshold = 3
  134. let localizedCurrentValue = Localizer.toDisplayUnits(String(currentValue)).replacingOccurrences(of: ",", with: ".")
  135. let announcementText: String
  136. if abs(bloodGlucoseDifference) <= negligibleThreshold {
  137. announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(texts.stable)"
  138. } else {
  139. let directionText = bloodGlucoseDifference < 0 ? texts.decrease : texts.increase
  140. let absoluteDifference = Localizer.toDisplayUnits(String(abs(bloodGlucoseDifference))).replacingOccurrences(of: ",", with: ".")
  141. announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(directionText) \(absoluteDifference)"
  142. }
  143. let speechUtterance = AVSpeechUtterance(string: announcementText)
  144. speechUtterance.voice = AVSpeechSynthesisVoice(language: voiceLanguageCode)
  145. speechSynthesizer.speak(speechUtterance)
  146. }
  147. }