Bläddra i källkod

Add option to have delay between alarm sounds

codebymini 7 månader sedan
förälder
incheckning
60be233cf7

+ 77 - 1
LoopFollow/Alarm/Alarm.swift

@@ -90,9 +90,83 @@ struct Alarm: Identifiable, Codable, Equatable {
     /// When the sound should repeat
     var repeatSoundOption: RepeatSoundOption = .always
 
+    /// Delay in seconds between repeated alarm sounds (0 = no delay, only applies when repeating)
+    var soundDelay: Int = 0
+
     /// When is the alarm active
     var activeOption: ActiveOption = .always
 
+    // MARK: - Codable (Custom implementation for backward compatibility)
+
+    enum CodingKeys: String, CodingKey {
+        case id, type, name, isEnabled, snoozedUntil
+        case aboveBG, belowBG, threshold, predictiveMinutes, delta
+        case persistentMinutes, monitoringWindow, soundFile
+        case snoozeDuration, playSoundOption, repeatSoundOption
+        case soundDelay, activeOption
+        case missedBolusPrebolusWindow, missedBolusIgnoreSmallBolusUnits
+        case missedBolusIgnoreUnderGrams, missedBolusIgnoreUnderBG
+        case bolusCountThreshold, bolusWindowMinutes
+    }
+
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+
+        id = try container.decode(UUID.self, forKey: .id)
+        type = try container.decode(AlarmType.self, forKey: .type)
+        name = try container.decode(String.self, forKey: .name)
+        isEnabled = try container.decodeIfPresent(Bool.self, forKey: .isEnabled) ?? true
+        snoozedUntil = try container.decodeIfPresent(Date.self, forKey: .snoozedUntil)
+        aboveBG = try container.decodeIfPresent(Double.self, forKey: .aboveBG)
+        belowBG = try container.decodeIfPresent(Double.self, forKey: .belowBG)
+        threshold = try container.decodeIfPresent(Double.self, forKey: .threshold)
+        predictiveMinutes = try container.decodeIfPresent(Int.self, forKey: .predictiveMinutes)
+        delta = try container.decodeIfPresent(Double.self, forKey: .delta)
+        persistentMinutes = try container.decodeIfPresent(Int.self, forKey: .persistentMinutes)
+        monitoringWindow = try container.decodeIfPresent(Int.self, forKey: .monitoringWindow)
+        soundFile = try container.decode(SoundFile.self, forKey: .soundFile)
+        snoozeDuration = try container.decodeIfPresent(Int.self, forKey: .snoozeDuration) ?? 5
+        playSoundOption = try container.decodeIfPresent(PlaySoundOption.self, forKey: .playSoundOption) ?? .always
+        repeatSoundOption = try container.decodeIfPresent(RepeatSoundOption.self, forKey: .repeatSoundOption) ?? .always
+        // Handle backward compatibility: default to 0 if soundDelay is missing
+        soundDelay = try container.decodeIfPresent(Int.self, forKey: .soundDelay) ?? 0
+        activeOption = try container.decodeIfPresent(ActiveOption.self, forKey: .activeOption) ?? .always
+        missedBolusPrebolusWindow = try container.decodeIfPresent(Int.self, forKey: .missedBolusPrebolusWindow)
+        missedBolusIgnoreSmallBolusUnits = try container.decodeIfPresent(Double.self, forKey: .missedBolusIgnoreSmallBolusUnits)
+        missedBolusIgnoreUnderGrams = try container.decodeIfPresent(Double.self, forKey: .missedBolusIgnoreUnderGrams)
+        missedBolusIgnoreUnderBG = try container.decodeIfPresent(Double.self, forKey: .missedBolusIgnoreUnderBG)
+        bolusCountThreshold = try container.decodeIfPresent(Int.self, forKey: .bolusCountThreshold)
+        bolusWindowMinutes = try container.decodeIfPresent(Int.self, forKey: .bolusWindowMinutes)
+    }
+
+    func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encode(id, forKey: .id)
+        try container.encode(type, forKey: .type)
+        try container.encode(name, forKey: .name)
+        try container.encode(isEnabled, forKey: .isEnabled)
+        try container.encodeIfPresent(snoozedUntil, forKey: .snoozedUntil)
+        try container.encodeIfPresent(aboveBG, forKey: .aboveBG)
+        try container.encodeIfPresent(belowBG, forKey: .belowBG)
+        try container.encodeIfPresent(threshold, forKey: .threshold)
+        try container.encodeIfPresent(predictiveMinutes, forKey: .predictiveMinutes)
+        try container.encodeIfPresent(delta, forKey: .delta)
+        try container.encodeIfPresent(persistentMinutes, forKey: .persistentMinutes)
+        try container.encodeIfPresent(monitoringWindow, forKey: .monitoringWindow)
+        try container.encode(soundFile, forKey: .soundFile)
+        try container.encode(snoozeDuration, forKey: .snoozeDuration)
+        try container.encode(playSoundOption, forKey: .playSoundOption)
+        try container.encode(repeatSoundOption, forKey: .repeatSoundOption)
+        try container.encode(soundDelay, forKey: .soundDelay)
+        try container.encode(activeOption, forKey: .activeOption)
+        try container.encodeIfPresent(missedBolusPrebolusWindow, forKey: .missedBolusPrebolusWindow)
+        try container.encodeIfPresent(missedBolusIgnoreSmallBolusUnits, forKey: .missedBolusIgnoreSmallBolusUnits)
+        try container.encodeIfPresent(missedBolusIgnoreUnderGrams, forKey: .missedBolusIgnoreUnderGrams)
+        try container.encodeIfPresent(missedBolusIgnoreUnderBG, forKey: .missedBolusIgnoreUnderBG)
+        try container.encodeIfPresent(bolusCountThreshold, forKey: .bolusCountThreshold)
+        try container.encodeIfPresent(bolusWindowMinutes, forKey: .bolusWindowMinutes)
+    }
+
     // ─────────────────────────────────────────────────────────────
     //  Missed‑Bolus‑specific settings
     // ─────────────────────────────────────────────────────────────
@@ -180,7 +254,9 @@ struct Alarm: Identifiable, Codable, Equatable {
 
         if playSound {
             AlarmSound.setSoundFile(str: soundFile.rawValue)
-            AlarmSound.play(repeating: shouldRepeat)
+            // Only use delay if repeating is enabled, otherwise delay doesn't make sense
+            let delay = shouldRepeat ? soundDelay : 0
+            AlarmSound.play(repeating: shouldRepeat, delay: delay)
         }
     }
 

+ 18 - 0
LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift

@@ -40,6 +40,24 @@ struct AlarmAudioSection: View {
                     allowed: RepeatSoundOption.allowed(for: alarm.activeOption)
                 )
             }
+
+            Stepper(
+                value: $alarm.soundDelay,
+                in: 0 ... 60,
+                step: 5
+            ) {
+                HStack {
+                    Text("Delay Between Sounds")
+                    Spacer()
+                    if alarm.soundDelay == 0 {
+                        Text("Off")
+                            .foregroundColor(.secondary)
+                    } else {
+                        Text("\(alarm.soundDelay) sec")
+                            .foregroundColor(.secondary)
+                    }
+                }
+            }
         }.onChange(of: alarm.activeOption) { newActive in
             let playAllowed = PlaySoundOption.allowed(for: newActive)
             if !playAllowed.contains(alarm.playSoundOption) {

+ 61 - 3
LoopFollow/Controllers/AlarmSound.swift

@@ -33,6 +33,8 @@ class AlarmSound {
     fileprivate static var alarmPlayingForTimer = Timer()
     fileprivate static let alarmPlayingForInterval = 290
 
+    fileprivate static var repeatDelay: TimeInterval = 0
+
     fileprivate func startAlarmPlayingForTimer(time: TimeInterval) {
         AlarmSound.alarmPlayingForTimer = Timer.scheduledTimer(timeInterval: time,
                                                                target: self,
@@ -70,6 +72,7 @@ class AlarmSound {
     static func stop() {
         Observable.shared.alarmSoundPlaying.value = false
 
+        repeatDelay = 0
         audioPlayer?.stop()
         audioPlayer = nil
 
@@ -108,13 +111,17 @@ class AlarmSound {
         }
     }
 
-    static func play(repeating: Bool) {
+    static func play(repeating: Bool, delay: Int = 0) {
         guard !isPlaying else {
             return
         }
 
         enableAudio()
 
+        // If repeating with delay, we'll handle it manually via the delegate
+        // Only set repeatDelay if both repeating and delay > 0
+        repeatDelay = (repeating && delay > 0) ? TimeInterval(delay) : 0
+
         do {
             audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
             audioPlayer!.delegate = audioPlayerDelegate
@@ -122,7 +129,9 @@ class AlarmSound {
             try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category(rawValue: convertFromAVAudioSessionCategory(AVAudioSession.Category.playback)))
             try AVAudioSession.sharedInstance().setActive(true)
 
-            audioPlayer!.numberOfLoops = repeating ? -1 : 0
+            // Only use numberOfLoops if we're not using delay-based repeating
+            // When repeatDelay > 0, we play once and then use the delegate to schedule the next play with delay
+            audioPlayer!.numberOfLoops = (repeating && repeatDelay == 0) ? -1 : 0
 
             // Store existing volume
             if systemOutputVolumeBeforeOverride == nil {
@@ -133,12 +142,16 @@ class AlarmSound {
                 LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play")
             }
 
+            // First sound plays immediately - delay only applies between repeated sounds
             if audioPlayer!.play() {
                 if !isPlaying {
                     LogManager.shared.log(category: .alarm, message: "AlarmSound - not playing after calling play")
                     LogManager.shared.log(category: .alarm, message: "AlarmSound - rate value: \(audioPlayer!.rate)")
                 } else {
                     Observable.shared.alarmSoundPlaying.value = true
+                    if repeatDelay > 0 {
+                        LogManager.shared.log(category: .alarm, message: "AlarmSound - first sound playing immediately, delay (\(repeatDelay)s) will apply between repeats")
+                    }
                 }
             } else {
                 LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play")
@@ -152,6 +165,45 @@ class AlarmSound {
         }
     }
 
+    fileprivate static func playNextWithDelay() {
+        guard repeatDelay > 0 else {
+            return
+        }
+
+        DispatchQueue.main.asyncAfter(deadline: .now() + repeatDelay) {
+            // Check if we should still be playing (user might have stopped it)
+            guard repeatDelay > 0 else {
+                return
+            }
+
+            // Clean up the previous player
+            audioPlayer?.stop()
+            audioPlayer = nil
+
+            do {
+                audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
+                audioPlayer!.delegate = audioPlayerDelegate
+
+                try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
+                try AVAudioSession.sharedInstance().setActive(true)
+
+                audioPlayer!.numberOfLoops = 0
+
+                if !audioPlayer!.prepareToPlay() {
+                    LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play (delayed repeat)")
+                }
+
+                if audioPlayer!.play() {
+                    Observable.shared.alarmSoundPlaying.value = true
+                } else {
+                    LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play (delayed repeat)")
+                }
+            } catch {
+                LogManager.shared.log(category: .alarm, message: "AlarmSound - unable to play sound (delayed repeat); error: \(error)")
+            }
+        }
+    }
+
     static func playTerminated() {
         guard !isPlaying else {
             return
@@ -222,7 +274,13 @@ class AudioPlayerDelegate: NSObject, AVAudioPlayerDelegate {
     /* audioPlayerDidFinishPlaying:successfully: is called when a sound has finished playing. This method is NOT called if the player is stopped due to an interruption. */
     func audioPlayerDidFinishPlaying(_: AVAudioPlayer, successfully flag: Bool) {
         LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerDidFinishPlaying (\(flag))", isDebug: true)
-        Observable.shared.alarmSoundPlaying.value = false
+
+        // If we're repeating with delay, schedule the next play
+        if AlarmSound.repeatDelay > 0 {
+            AlarmSound.playNextWithDelay()
+        } else {
+            Observable.shared.alarmSoundPlaying.value = false
+        }
     }
 
     /* if an error occurs while decoding it will be reported to the delegate. */