Update Feedback for Volume Change

This commit is contained in:
Hariharan Mudaliar 2026-01-22 22:34:08 +05:30
commit a3a80ca3d4
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
5 changed files with 76 additions and 0 deletions

View file

@ -3382,6 +3382,10 @@
} }
} }
}, },
"Audio feedback" : {
"comment" : "A section header that lists audio feedback settings.",
"isCommentAutoGenerated" : true
},
"Authorize" : { "Authorize" : {
"comment" : "A button that authorizes an app extension.", "comment" : "A button that authorizes an app extension.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -15194,6 +15198,10 @@
}, },
"Pinned Items" : { "Pinned Items" : {
},
"Plays the supplied feedback clip whenever you press the hardware volume keys." : {
"comment" : "A help text describing the audio feedback feature.",
"isCommentAutoGenerated" : true
}, },
"Please configure your API key for the selected AI provider in model settings." : { "Please configure your API key for the selected AI provider in model settings." : {
@ -16032,6 +16040,9 @@
} }
} }
} }
},
"Requires Accessibility permission so Dynamic Island can intercept the hardware volume keys." : {
}, },
"Reset" : { "Reset" : {
"localizations" : { "localizations" : {

Binary file not shown.

View file

@ -1859,6 +1859,18 @@ struct HUD: View {
} }
} }
Section {
Defaults.Toggle("Play feedback when volume is changed", key: .playVolumeChangeFeedback)
.settingsHighlight(id: highlightID("Play feedback when volume is changed"))
.help("Plays the supplied feedback clip whenever you press the hardware volume keys.")
} header: {
Text("Audio feedback")
} footer: {
Text("Requires Accessibility permission so Dynamic Island can intercept the hardware volume keys.")
.foregroundStyle(.secondary)
.font(.caption)
}
Section { Section {
Defaults.Toggle("Color-coded volume display", key: .useColorCodedVolumeDisplay) Defaults.Toggle("Color-coded volume display", key: .useColorCodedVolumeDisplay)
.disabled(colorCodingDisabled) .disabled(colorCodingDisabled)

View file

@ -2,6 +2,7 @@ import Foundation
import CoreGraphics import CoreGraphics
import Defaults import Defaults
import AppKit import AppKit
import AVFoundation
final class SystemChangesObserver: MediaKeyInterceptorDelegate { final class SystemChangesObserver: MediaKeyInterceptorDelegate {
private weak var coordinator: DynamicIslandViewCoordinator? private weak var coordinator: DynamicIslandViewCoordinator?
@ -9,6 +10,7 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
private let brightnessController = SystemBrightnessController.shared private let brightnessController = SystemBrightnessController.shared
private let keyboardBacklightController = SystemKeyboardBacklightController.shared private let keyboardBacklightController = SystemKeyboardBacklightController.shared
private let mediaKeyInterceptor = MediaKeyInterceptor.shared private let mediaKeyInterceptor = MediaKeyInterceptor.shared
private let volumeFeedbackPlayer = VolumeFeedbackPlayer.shared
private static let headsetIconSymbols: Set<String> = [ private static let headsetIconSymbols: Set<String> = [
"airpods", "airpods",
@ -140,6 +142,7 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
let baseStep = stepSize(for: step, base: standardVolumeStep) let baseStep = stepSize(for: step, base: standardVolumeStep)
let delta = direction == .up ? baseStep : -baseStep let delta = direction == .up ? baseStep : -baseStep
volumeController.adjust(by: delta) volumeController.adjust(by: delta)
volumeFeedbackPlayer.playIfNeeded()
} }
func mediaKeyInterceptorDidToggleMute(_ interceptor: MediaKeyInterceptor) { func mediaKeyInterceptorDidToggleMute(_ interceptor: MediaKeyInterceptor) {
@ -325,4 +328,53 @@ private extension SystemChangesObserver {
} }
} }
private final class VolumeFeedbackPlayer {
static let shared = VolumeFeedbackPlayer()
private let queue = DispatchQueue(label: "com.dynamicisland.volume-feedback-player")
private var player: AVAudioPlayer?
private var lastPlayDate: Date = .distantPast
private let minimumInterval: TimeInterval = 0.08
private var didLogMissingAsset = false
private init() {}
func playIfNeeded() {
guard Defaults[.playVolumeChangeFeedback] else { return }
queue.async { [weak self] in
guard let self else { return }
let now = Date()
guard now.timeIntervalSince(self.lastPlayDate) >= self.minimumInterval else { return }
self.lastPlayDate = now
self.preparePlayerIfNeeded()
guard let player = self.player else { return }
player.currentTime = 0
player.play()
}
}
private func preparePlayerIfNeeded() {
if player != nil { return }
guard let url = Bundle.main.url(forResource: "audio-feedback", withExtension: "m4a") else {
if !didLogMissingAsset {
NSLog("⚠️ audio-feedback.m4a is missing from the app bundle; volume feedback disabled.")
didLogMissingAsset = true
}
return
}
do {
let audioPlayer = try AVAudioPlayer(contentsOf: url)
audioPlayer.volume = 1
audioPlayer.prepareToPlay()
player = audioPlayer
} catch {
if !didLogMissingAsset {
NSLog("⚠️ Failed to initialize volume feedback player: \(error.localizedDescription)")
didLogMissingAsset = true
}
}
}
}

View file

@ -871,6 +871,7 @@ extension Defaults.Keys {
static let enableBrightnessHUD = Key<Bool>("enableBrightnessHUD", default: true) static let enableBrightnessHUD = Key<Bool>("enableBrightnessHUD", default: true)
static let enableKeyboardBacklightHUD = Key<Bool>("enableKeyboardBacklightHUD", default: true) static let enableKeyboardBacklightHUD = Key<Bool>("enableKeyboardBacklightHUD", default: true)
static let systemHUDSensitivity = Key<Int>("systemHUDSensitivity", default: 5) static let systemHUDSensitivity = Key<Int>("systemHUDSensitivity", default: 5)
static let playVolumeChangeFeedback = Key<Bool>("playVolumeChangeFeedback", default: false)
// MARK: Custom OSD Window Feature // MARK: Custom OSD Window Feature
static let enableCustomOSD = Key<Bool>("enableCustomOSD", default: false) static let enableCustomOSD = Key<Bool>("enableCustomOSD", default: false)