SpeakBG.swift 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  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 "it":
  65. return AnnouncementTexts(
  66. stable: "ed è stabile",
  67. increase: "ed è salita di",
  68. decrease: "ed è scesa di",
  69. currentBGIs: "Glicemia attuale è"
  70. )
  71. case "sk":
  72. return AnnouncementTexts(
  73. stable: "a je stabilná",
  74. increase: "a stúpla o",
  75. decrease: "a klesla o",
  76. currentBGIs: "Aktuálna glykémia je"
  77. )
  78. case "sv":
  79. return AnnouncementTexts(
  80. stable: "och det är stabilt",
  81. increase: "och det har ökat med",
  82. decrease: "och det har minskat med",
  83. currentBGIs: "Blodsockret är"
  84. )
  85. case "en": fallthrough
  86. default:
  87. return AnnouncementTexts(
  88. stable: "and it is stable",
  89. increase: "and it is up",
  90. decrease: "and it is down",
  91. currentBGIs: "Glucose is"
  92. )
  93. }
  94. }
  95. }
  96. enum LanguageVoiceMapping {
  97. static let voiceLanguageMap: [String: String] = [
  98. "en": "en-US",
  99. "it": "it-IT",
  100. "sk": "sk-SK",
  101. "sv": "sv-SE",
  102. ]
  103. static func voiceLanguageCode(forAppLanguage appLanguage: String) -> String {
  104. return voiceLanguageMap[appLanguage, default: "en-US"]
  105. }
  106. }
  107. // Speaks the current blood glucose value and the change from the previous value.
  108. func speakBG(currentValue: Int, previousValue: Int) {
  109. // 1. Check if there's a new, unspoken BG value
  110. guard let lastBG = bgData.last else {
  111. // No data, so nothing to speak.
  112. return
  113. }
  114. // Compare the timestamp of the latest BG reading with the last one we spoke.
  115. guard lastBG.date > lastSpokenBGDate else {
  116. // The latest value has already been spoken, so we do nothing.
  117. return
  118. }
  119. // 2. Now that we know we need to speak, activate the audio session.
  120. let audioSession = AVAudioSession.sharedInstance()
  121. do {
  122. try audioSession.setCategory(.playback, mode: .default, options: .duckOthers)
  123. try audioSession.setActive(true)
  124. } catch {
  125. LogManager.shared.log(category: .general, message: "speakBG, Failed to set up audio session: \(error)")
  126. return
  127. }
  128. // 3. Mark this BG value as spoken
  129. // This prevents race conditions where another call might try to speak the same value.
  130. lastSpokenBGDate = lastBG.date
  131. // 4. Generate announcement text.
  132. let bloodGlucoseDifference = currentValue - previousValue
  133. let preferredLanguage = Storage.shared.speakLanguage.value
  134. let voiceLanguageCode = LanguageVoiceMapping.voiceLanguageCode(forAppLanguage: preferredLanguage)
  135. let texts = AnnouncementTexts.forLanguage(preferredLanguage)
  136. let negligibleThreshold = 3
  137. let localizedCurrentValue = Localizer.toDisplayUnits(String(currentValue)).replacingOccurrences(of: ",", with: ".")
  138. let announcementText: String
  139. if abs(bloodGlucoseDifference) <= negligibleThreshold {
  140. announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(texts.stable)"
  141. } else {
  142. let directionText = bloodGlucoseDifference < 0 ? texts.decrease : texts.increase
  143. let absoluteDifference = Localizer.toDisplayUnits(String(abs(bloodGlucoseDifference))).replacingOccurrences(of: ",", with: ".")
  144. announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(directionText) \(absoluteDifference)"
  145. }
  146. // 5. Speak.
  147. let speechUtterance = AVSpeechUtterance(string: announcementText)
  148. speechUtterance.voice = AVSpeechSynthesisVoice(language: voiceLanguageCode)
  149. speechSynthesizer.speak(speechUtterance)
  150. }
  151. }