SpeakBG.swift 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. // LoopFollow
  2. // SpeakBG.swift
  3. import AVFoundation
  4. import CallKit
  5. import Foundation
  6. extension MainViewController {
  7. func evaluateSpeakConditions(currentValue: Int, previousValue: Int) {
  8. guard Storage.shared.speakBG.value else {
  9. return
  10. }
  11. let always = Storage.shared.speakBGAlways.value
  12. let lowThreshold = Storage.shared.speakLowBGLimit.value
  13. let fastDropDelta = Storage.shared.speakFastDropDelta.value
  14. let highThreshold = Storage.shared.speakHighBGLimit.value
  15. let speakLowBG = Storage.shared.speakLowBG.value
  16. let speakProactiveLowBG = Storage.shared.speakProactiveLowBG.value
  17. let speakHighBG = Storage.shared.speakHighBG.value
  18. // Speak always
  19. if always {
  20. speakBG(currentValue: currentValue, previousValue: previousValue)
  21. LogManager.shared.log(category: .general, message: "Speaking because 'Always' is enabled.", isDebug: true)
  22. return
  23. }
  24. // Speak if low or last value was low
  25. if speakLowBG {
  26. if currentValue <= Int(lowThreshold) || previousValue <= Int(lowThreshold) {
  27. speakBG(currentValue: currentValue, previousValue: previousValue)
  28. LogManager.shared.log(category: .general, message: "Speaking because of 'Low' condition.", isDebug: true)
  29. return
  30. }
  31. }
  32. // Speak predictive low if...
  33. // * low or last value was low
  34. // * next predictive value is low
  35. // * fast drop occurs below high
  36. if speakProactiveLowBG {
  37. let predictiveTrigger = !predictionData.isEmpty && Double(predictionData.first!.sgv) <= lowThreshold
  38. if predictiveTrigger ||
  39. currentValue <= Int(lowThreshold) || previousValue <= Int(lowThreshold) ||
  40. (currentValue <= Int(highThreshold) && (previousValue - currentValue) >= Int(fastDropDelta))
  41. {
  42. speakBG(currentValue: currentValue, previousValue: previousValue)
  43. LogManager.shared.log(category: .general, message: "Speaking because of 'Proactive Low' condition. Predictive trigger: \(predictiveTrigger)", isDebug: true)
  44. return
  45. }
  46. }
  47. // Speak if high or if last value was high
  48. if speakHighBG {
  49. if currentValue >= Int(highThreshold) || previousValue >= Int(highThreshold) {
  50. speakBG(currentValue: currentValue, previousValue: previousValue)
  51. LogManager.shared.log(category: .general, message: "Speaking because of 'High' condition.", isDebug: true)
  52. return
  53. }
  54. }
  55. LogManager.shared.log(category: .general, message: "No condition met for speaking.", isDebug: true)
  56. }
  57. struct AnnouncementTexts {
  58. var stable: String
  59. var increase: String
  60. var decrease: String
  61. var currentBGIs: String
  62. static func forLanguage(_ language: String) -> AnnouncementTexts {
  63. switch language {
  64. case "fr":
  65. return AnnouncementTexts(
  66. stable: "et c'est stable",
  67. increase: "et c'est monté de",
  68. decrease: "et c'est descendu de",
  69. currentBGIs: "La glycémie est"
  70. )
  71. case "it":
  72. return AnnouncementTexts(
  73. stable: "ed è stabile",
  74. increase: "ed è salita di",
  75. decrease: "ed è scesa di",
  76. currentBGIs: "Glicemia attuale è"
  77. )
  78. case "sk":
  79. return AnnouncementTexts(
  80. stable: "a je stabilná",
  81. increase: "a stúpla o",
  82. decrease: "a klesla o",
  83. currentBGIs: "Aktuálna glykémia je"
  84. )
  85. case "sv":
  86. return AnnouncementTexts(
  87. stable: "och det är stabilt",
  88. increase: "och det har ökat med",
  89. decrease: "och det har minskat med",
  90. currentBGIs: "Blodsockret är"
  91. )
  92. case "en": fallthrough
  93. default:
  94. return AnnouncementTexts(
  95. stable: "and it is stable",
  96. increase: "and it is up",
  97. decrease: "and it is down",
  98. currentBGIs: "Glucose is"
  99. )
  100. }
  101. }
  102. }
  103. enum LanguageVoiceMapping {
  104. static let voiceLanguageMap: [String: String] = [
  105. "en": "en-US",
  106. "fr": "fr-FR",
  107. "it": "it-IT",
  108. "sk": "sk-SK",
  109. "sv": "sv-SE",
  110. ]
  111. static func voiceLanguageCode(forAppLanguage appLanguage: String) -> String {
  112. return voiceLanguageMap[appLanguage, default: "en-US"]
  113. }
  114. }
  115. // Speaks the current blood glucose value and the change from the previous value.
  116. func speakBG(currentValue: Int, previousValue: Int) {
  117. // 1. Check if there's a new, unspoken BG value
  118. guard let lastBG = bgData.last else {
  119. // No data, so nothing to speak.
  120. return
  121. }
  122. // Compare the timestamp of the latest BG reading with the last one we spoke.
  123. guard lastBG.date > lastSpokenBGDate else {
  124. // The latest value has already been spoken, so we do nothing.
  125. return
  126. }
  127. // 2. Now that we know we need to speak, activate the audio session.
  128. let audioSession = AVAudioSession.sharedInstance()
  129. do {
  130. try audioSession.setCategory(.playback, mode: .default, options: .duckOthers)
  131. try audioSession.setActive(true)
  132. } catch {
  133. LogManager.shared.log(category: .general, message: "speakBG, Failed to set up audio session: \(error)")
  134. return
  135. }
  136. // 3. Mark this BG value as spoken
  137. // This prevents race conditions where another call might try to speak the same value.
  138. lastSpokenBGDate = lastBG.date
  139. // 4. Generate announcement text.
  140. let bloodGlucoseDifference = currentValue - previousValue
  141. let preferredLanguage = Storage.shared.speakLanguage.value
  142. let voiceLanguageCode = LanguageVoiceMapping.voiceLanguageCode(forAppLanguage: preferredLanguage)
  143. let texts = AnnouncementTexts.forLanguage(preferredLanguage)
  144. let negligibleThreshold = 3
  145. let localizedCurrentValue = Localizer.toDisplayUnits(String(currentValue)).replacingOccurrences(of: ",", with: ".")
  146. let announcementText: String
  147. if abs(bloodGlucoseDifference) <= negligibleThreshold {
  148. announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(texts.stable)"
  149. } else {
  150. let directionText = bloodGlucoseDifference < 0 ? texts.decrease : texts.increase
  151. let absoluteDifference = Localizer.toDisplayUnits(String(abs(bloodGlucoseDifference))).replacingOccurrences(of: ",", with: ".")
  152. announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(directionText) \(absoluteDifference)"
  153. }
  154. // 5. Speak.
  155. let speechUtterance = AVSpeechUtterance(string: announcementText)
  156. speechUtterance.voice = AVSpeechSynthesisVoice(language: voiceLanguageCode)
  157. speechSynthesizer.speak(speechUtterance)
  158. }
  159. }