| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181 |
- // LoopFollow
- // SpeakBG.swift
- import AVFoundation
- import CallKit
- import Foundation
- extension MainViewController {
- func evaluateSpeakConditions(currentValue: Int, previousValue: Int) {
- guard Storage.shared.speakBG.value else {
- return
- }
- let always = Storage.shared.speakBGAlways.value
- let lowThreshold = Storage.shared.speakLowBGLimit.value
- let fastDropDelta = Storage.shared.speakFastDropDelta.value
- let highThreshold = Storage.shared.speakHighBGLimit.value
- let speakLowBG = Storage.shared.speakLowBG.value
- let speakProactiveLowBG = Storage.shared.speakProactiveLowBG.value
- let speakHighBG = Storage.shared.speakHighBG.value
- // Speak always
- if always {
- speakBG(currentValue: currentValue, previousValue: previousValue)
- LogManager.shared.log(category: .general, message: "Speaking because 'Always' is enabled.", isDebug: true)
- return
- }
- // Speak if low or last value was low
- if speakLowBG {
- if currentValue <= Int(lowThreshold) || previousValue <= Int(lowThreshold) {
- speakBG(currentValue: currentValue, previousValue: previousValue)
- LogManager.shared.log(category: .general, message: "Speaking because of 'Low' condition.", isDebug: true)
- return
- }
- }
- // Speak predictive low if...
- // * low or last value was low
- // * next predictive value is low
- // * fast drop occurs below high
- if speakProactiveLowBG {
- let predictiveTrigger = !predictionData.isEmpty && Double(predictionData.first!.sgv) <= lowThreshold
- if predictiveTrigger ||
- currentValue <= Int(lowThreshold) || previousValue <= Int(lowThreshold) ||
- (currentValue <= Int(highThreshold) && (previousValue - currentValue) >= Int(fastDropDelta))
- {
- speakBG(currentValue: currentValue, previousValue: previousValue)
- LogManager.shared.log(category: .general, message: "Speaking because of 'Proactive Low' condition. Predictive trigger: \(predictiveTrigger)", isDebug: true)
- return
- }
- }
- // Speak if high or if last value was high
- if speakHighBG {
- if currentValue >= Int(highThreshold) || previousValue >= Int(highThreshold) {
- speakBG(currentValue: currentValue, previousValue: previousValue)
- LogManager.shared.log(category: .general, message: "Speaking because of 'High' condition.", isDebug: true)
- return
- }
- }
- LogManager.shared.log(category: .general, message: "No condition met for speaking.", isDebug: true)
- }
- struct AnnouncementTexts {
- var stable: String
- var increase: String
- var decrease: String
- var currentBGIs: String
- static func forLanguage(_ language: String) -> AnnouncementTexts {
- switch language {
- case "fr":
- return AnnouncementTexts(
- stable: "et c'est stable",
- increase: "et c'est monté de",
- decrease: "et c'est descendu de",
- currentBGIs: "La glycémie est"
- )
- case "it":
- return AnnouncementTexts(
- stable: "ed è stabile",
- increase: "ed è salita di",
- decrease: "ed è scesa di",
- currentBGIs: "Glicemia attuale è"
- )
- case "sk":
- return AnnouncementTexts(
- stable: "a je stabilná",
- increase: "a stúpla o",
- decrease: "a klesla o",
- currentBGIs: "Aktuálna glykémia je"
- )
- case "sv":
- return AnnouncementTexts(
- stable: "och det är stabilt",
- increase: "och det har ökat med",
- decrease: "och det har minskat med",
- currentBGIs: "Blodsockret är"
- )
- case "en": fallthrough
- default:
- return AnnouncementTexts(
- stable: "and it is stable",
- increase: "and it is up",
- decrease: "and it is down",
- currentBGIs: "Glucose is"
- )
- }
- }
- }
- enum LanguageVoiceMapping {
- static let voiceLanguageMap: [String: String] = [
- "en": "en-US",
- "fr": "fr-FR",
- "it": "it-IT",
- "sk": "sk-SK",
- "sv": "sv-SE",
- ]
- static func voiceLanguageCode(forAppLanguage appLanguage: String) -> String {
- return voiceLanguageMap[appLanguage, default: "en-US"]
- }
- }
- // Speaks the current blood glucose value and the change from the previous value.
- func speakBG(currentValue: Int, previousValue: Int) {
- // 1. Check if there's a new, unspoken BG value
- guard let lastBG = bgData.last else {
- // No data, so nothing to speak.
- return
- }
- // Compare the timestamp of the latest BG reading with the last one we spoke.
- guard lastBG.date > lastSpokenBGDate else {
- // The latest value has already been spoken, so we do nothing.
- return
- }
- // 2. Now that we know we need to speak, activate the audio session.
- let audioSession = AVAudioSession.sharedInstance()
- do {
- try audioSession.setCategory(.playback, mode: .default, options: .duckOthers)
- try audioSession.setActive(true)
- } catch {
- LogManager.shared.log(category: .general, message: "speakBG, Failed to set up audio session: \(error)")
- return
- }
- // 3. Mark this BG value as spoken
- // This prevents race conditions where another call might try to speak the same value.
- lastSpokenBGDate = lastBG.date
- // 4. Generate announcement text.
- let bloodGlucoseDifference = currentValue - previousValue
- let preferredLanguage = Storage.shared.speakLanguage.value
- let voiceLanguageCode = LanguageVoiceMapping.voiceLanguageCode(forAppLanguage: preferredLanguage)
- let texts = AnnouncementTexts.forLanguage(preferredLanguage)
- let negligibleThreshold = 3
- let localizedCurrentValue = Localizer.toDisplayUnits(String(currentValue)).replacingOccurrences(of: ",", with: ".")
- let announcementText: String
- if abs(bloodGlucoseDifference) <= negligibleThreshold {
- announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(texts.stable)"
- } else {
- let directionText = bloodGlucoseDifference < 0 ? texts.decrease : texts.increase
- let absoluteDifference = Localizer.toDisplayUnits(String(abs(bloodGlucoseDifference))).replacingOccurrences(of: ",", with: ".")
- announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(directionText) \(absoluteDifference)"
- }
- // 5. Speak.
- let speechUtterance = AVSpeechUtterance(string: announcementText)
- speechUtterance.voice = AVSpeechSynthesisVoice(language: voiceLanguageCode)
- speechSynthesizer.speak(speechUtterance)
- }
- }
|