Skip to content

Commit 958907f

Browse files
committed
Alternative version of volume change detection
1 parent fa4d3b9 commit 958907f

File tree

1 file changed

+63
-84
lines changed

1 file changed

+63
-84
lines changed

LoopFollow/Controllers/VolumeButtonHandler.swift

Lines changed: 63 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ class VolumeButtonHandler: NSObject {
1717
private let volumeButtonPressTimeWindow: TimeInterval = 0.3
1818
private let volumeButtonCooldown: TimeInterval = 0.5
1919

20+
// KVO observer for system volume
21+
private var volumeObserver: NSKeyValueObservation?
22+
2023
private var lastVolume: Float = 0.0
2124
private var isMonitoring = false
22-
private var volumeMonitoringTimer: Timer?
2325
private var alarmStartTime: Date?
2426
private var lastVolumeButtonPressTime: Date?
2527

@@ -46,45 +48,6 @@ class VolumeButtonHandler: NSObject {
4648
.store(in: &cancellables)
4749
}
4850

49-
private func checkVolumeChange() {
50-
let currentVolume = AVAudioSession.sharedInstance().outputVolume
51-
let volumeDifference = abs(currentVolume - lastVolume)
52-
let now = Date()
53-
54-
if volumeDifference > volumeButtonPressThreshold {
55-
if let startTime = alarmStartTime {
56-
let timeSinceAlarmStart = now.timeIntervalSince(startTime)
57-
58-
// Ignore volume changes from alarm system
59-
if timeSinceAlarmStart < 2.0, currentVolume > lastVolume {
60-
if volumeDifference <= 0.15, timeSinceAlarmStart < 1.5 {
61-
lastVolume = currentVolume
62-
return
63-
}
64-
}
65-
}
66-
67-
recordVolumeChange(currentVolume: currentVolume, timestamp: now)
68-
69-
if lastVolume > 0, let startTime = alarmStartTime {
70-
let timeSinceAlarmStart = now.timeIntervalSince(startTime)
71-
72-
if timeSinceAlarmStart > volumeButtonActivationDelay {
73-
if let lastPress = lastVolumeButtonPressTime {
74-
let timeSinceLastPress = now.timeIntervalSince(lastPress)
75-
if timeSinceLastPress < volumeButtonCooldown { return }
76-
}
77-
78-
if isLikelyVolumeButtonPress(volumeDifference: volumeDifference, timestamp: now) {
79-
snoozeActiveAlarm()
80-
}
81-
}
82-
}
83-
}
84-
85-
lastVolume = currentVolume
86-
}
87-
8851
private func recordVolumeChange(currentVolume: Float, timestamp: Date) {
8952
recentVolumeChanges.append((volume: currentVolume, timestamp: timestamp))
9053

@@ -99,7 +62,6 @@ class VolumeButtonHandler: NSObject {
9962
volumeChangePattern.removeFirst()
10063
}
10164
}
102-
10365
lastSignificantVolumeChange = timestamp
10466
}
10567

@@ -129,75 +91,92 @@ class VolumeButtonHandler: NSObject {
12991

13092
private func alarmStarted() {
13193
guard Storage.shared.alarmConfiguration.value.enableVolumeButtonSnooze else { return }
94+
LogManager.shared.log(category: .volumeButtonSnooze, message: "Alarm start detected, setting up volume observer.")
13295

133-
LogManager.shared.log(category: .volumeButtonSnooze, message: "Alarm start detected")
13496
alarmStartTime = Date()
135-
13697
recentVolumeChanges.removeAll()
13798
lastSignificantVolumeChange = nil
13899
volumeChangePattern.removeAll()
139100

140101
startMonitoring()
141102
}
142103

143-
func startMonitoring(retryCount: Int = 0) {
104+
private func alarmStopped() {
105+
LogManager.shared.log(category: .volumeButtonSnooze, message: "Alarm stop detected")
106+
107+
alarmStartTime = nil
108+
stopMonitoring()
109+
110+
recentVolumeChanges.removeAll()
111+
lastSignificantVolumeChange = nil
112+
volumeChangePattern.removeAll()
113+
}
114+
115+
func startMonitoring() {
144116
guard !isMonitoring else { return }
145117

146-
let audioSession = AVAudioSession.sharedInstance()
147-
let currentVolume = audioSession.outputVolume
118+
isMonitoring = true
148119

149-
if currentVolume > 0 {
150-
LogManager.shared.log(category: .volumeButtonSnooze, message: "Successfully started volume monitoring.")
151-
lastVolume = currentVolume
152-
isMonitoring = true
153-
startVolumeMonitoringTimer()
154-
return
155-
}
120+
volumeObserver = AVAudioSession.sharedInstance().observe(\.outputVolume, options: [.new]) { [weak self] session, _ in
121+
guard let self = self, let alarmStartTime = self.alarmStartTime else { return }
156122

157-
// Failure case: Volume is still 0. Let's retry if we can.
158-
let maxRetries = 5
159-
if retryCount < maxRetries {
160-
LogManager.shared.log(category: .volumeButtonSnooze, message: "Did not get a valid volume, retrying... (Attempt \(retryCount + 1)/\(maxRetries))")
161-
let delay = 0.2 // 200ms delay between retries
162-
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
163-
self?.startMonitoring(retryCount: retryCount + 1)
123+
let currentVolume = session.outputVolume
124+
let now = Date()
125+
126+
// On the first observation, capture the initial volume when the audio session
127+
// becomes active. This solves the race condition. We then return to avoid
128+
// treating this initial setup as a user-initiated button press.
129+
if self.lastVolume == 0.0, currentVolume > 0.0 {
130+
LogManager.shared.log(category: .volumeButtonSnooze, message: "Observer received initial valid volume: \(currentVolume)")
131+
self.lastVolume = currentVolume
132+
return
164133
}
165-
} else {
166-
// We've exhausted all retries, log the final failure.
167-
LogManager.shared.log(category: .volumeButtonSnooze, message: "Did not get a valid volume after \(maxRetries) retries, not monitoring.")
168-
}
169-
}
170134

171-
private func startVolumeMonitoringTimer() {
172-
guard volumeMonitoringTimer == nil else { return }
135+
guard self.lastVolume > 0.0 else { return }
173136

174-
let timer = Timer(timeInterval: 0.1, repeats: true) { [weak self] _ in
175-
self?.checkVolumeChange()
176-
}
137+
let volumeDifference = abs(currentVolume - self.lastVolume)
177138

178-
volumeMonitoringTimer = timer
139+
if volumeDifference > self.volumeButtonPressThreshold {
140+
let timeSinceAlarmStart = now.timeIntervalSince(alarmStartTime)
179141

180-
RunLoop.main.add(timer, forMode: .common)
181-
}
182-
183-
private func alarmStopped() {
184-
LogManager.shared.log(category: .volumeButtonSnooze, message: "Alarm stop detected")
142+
// Ignore volume changes from the alarm system's own ramp-up.
143+
if timeSinceAlarmStart < 2.0, currentVolume > self.lastVolume {
144+
if volumeDifference <= 0.15, timeSinceAlarmStart < 1.5 {
145+
self.lastVolume = currentVolume
146+
return
147+
}
148+
}
185149

186-
alarmStartTime = nil
150+
self.recordVolumeChange(currentVolume: currentVolume, timestamp: now)
187151

188-
stopMonitoring()
152+
if timeSinceAlarmStart > self.volumeButtonActivationDelay {
153+
if let lastPress = self.lastVolumeButtonPressTime {
154+
let timeSinceLastPress = now.timeIntervalSince(lastPress)
155+
if timeSinceLastPress < self.volumeButtonCooldown {
156+
self.lastVolume = currentVolume
157+
return
158+
}
159+
}
189160

190-
recentVolumeChanges.removeAll()
191-
lastSignificantVolumeChange = nil
192-
volumeChangePattern.removeAll()
161+
if self.isLikelyVolumeButtonPress(volumeDifference: volumeDifference, timestamp: now) {
162+
self.snoozeActiveAlarm()
163+
}
164+
}
165+
}
166+
self.lastVolume = currentVolume
167+
}
193168
}
194169

195170
func stopMonitoring() {
196171
guard isMonitoring else { return }
197172

198-
isMonitoring = false
173+
LogManager.shared.log(category: .volumeButtonSnooze, message: "Invalidating volume observer.")
174+
175+
// Invalidate the observer to stop receiving notifications and prevent memory leaks.
176+
volumeObserver?.invalidate()
177+
volumeObserver = nil
199178

200-
volumeMonitoringTimer?.invalidate()
201-
volumeMonitoringTimer = nil
179+
isMonitoring = false
180+
lastVolume = 0.0 // Reset for the next alarm.
202181
}
203182
}

0 commit comments

Comments
 (0)