From a3a80ca3d4eb2e2cb6e9415ce7af42b3e056ba76 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Thu, 22 Jan 2026 22:34:08 +0530 Subject: [PATCH] Update Feedback for Volume Change --- DynamicIsland/Localizable.xcstrings | 11 ++++ DynamicIsland/audio-feedback.m4a | Bin 0 -> 2624 bytes .../components/Settings/SettingsView.swift | 12 ++++ .../managers/SystemChangesObserver.swift | 52 ++++++++++++++++++ DynamicIsland/models/Constants.swift | 1 + 5 files changed, 76 insertions(+) create mode 100644 DynamicIsland/audio-feedback.m4a diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index d124eac..3b4f4db 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -3382,6 +3382,10 @@ } } }, + "Audio feedback" : { + "comment" : "A section header that lists audio feedback settings.", + "isCommentAutoGenerated" : true + }, "Authorize" : { "comment" : "A button that authorizes an app extension.", "isCommentAutoGenerated" : true @@ -15194,6 +15198,10 @@ }, "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." : { @@ -16032,6 +16040,9 @@ } } } + }, + "Requires Accessibility permission so Dynamic Island can intercept the hardware volume keys." : { + }, "Reset" : { "localizations" : { diff --git a/DynamicIsland/audio-feedback.m4a b/DynamicIsland/audio-feedback.m4a new file mode 100644 index 0000000000000000000000000000000000000000..b0eb634de51cbe7dc54229f577656c3545c640a4 GIT binary patch literal 2624 zcmZQzV30{GsVwj{aa3Rc0uY^9oS&OpU}6O1GUlcvmVm?=7}y>uGB7)2NdG?o=8h8J z5HLSH7z3g}QDHQy&?CnDPy@Qq!1D_feY-8&uisTLe=1;pRYf7ctH)vY^?85yJheRk zS5!Ns_^#CUx(eC*Ox7_sEyQ( zSycQ}(cUcYM9F@Z33s1)9Gv9e{Mz&N_LNNq90|!24!`%vUEp1~p?cGmGqZ|ZGzI3J zlYgF`5}-Q2F|V(QWtRN;8vYx*rzc%6jF_v}`YR}PSFTNTrSZ?>c%|I%b+onxJy&|Ktb%7izcfRZFVo zmo9&I<>|Z!S97&qr>;!7lI>FQUD%#w#zluC&-2f_nRZnD`}aiUs@qD1EB9(2^WExP z^!|Os!p=9r=RHfxzeIe~)4hGxvh(!XirsFHu6(ujwXSpDv-`MVrflw#wX?D+dzR0? z^Fwd-wf)}L=k1VLHO-s}{pemqY9 zlQjE`r&W>%x*HbH_|oj06>qlX*}HR>?-oXEnJ2Sz*%Mi2!_XPwVee+&kDa(Ssd)Q4 z%d{)O#lH9TUatIc_S&_U>U-hb7md08U9D`MJmKV_@PE#!D>fBxnlMd-b=mQjvNR2ET8S%Yx=R)W>3+a!fWsH8B6CpOK*Cbc7dUQ zK`C^dP!Ol(k|~FrG*xy?4DtNv>At}09>(0M)N4WpRowvo2w_cq( zFCLw?i1A!r8JBV9jkvv^vx_RiYRkK~Jv#d&|7C`|iq3Y)(~G+gWX_EEZm~t@*0xph zPP<#RJ@x8yn_1`l3cRps#}<J{TxvdyCe`~Dt*+mlHdXlcTy=V%)y&<5pZRJ_khLf!V5?*$}+M=D=M3y+xZd7%UjX85o$Ia`W@cfaO3=ZdpbOs62Q& z{r?IOW?~3KU@yoWKU87=hu;AW>XW0@ea#gN$VXBVdaMXnit}4+0=@s2rG90m@Z@WgrAl z594YeUIoMtfOsxYSfscl859;E0Me@i#J@^YN)mzer`%L94P-JXA%`t6KH%XDk_3Yz znK{Lv_SGI;AOO-bxv7cZ7)Z{~)k`cW$Vt`93@Ob^1^GfGFEKY2qzfbx>>J<&WVQgC jXCPZau3%6wfC31^%*_nKFmiN+OMz9GnHj(ZC}aQtT0&&A literal 0 HcmV?d00001 diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 59d65dc..069d625 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -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 { Defaults.Toggle("Color-coded volume display", key: .useColorCodedVolumeDisplay) .disabled(colorCodingDisabled) diff --git a/DynamicIsland/managers/SystemChangesObserver.swift b/DynamicIsland/managers/SystemChangesObserver.swift index 980d9de..3491dcf 100644 --- a/DynamicIsland/managers/SystemChangesObserver.swift +++ b/DynamicIsland/managers/SystemChangesObserver.swift @@ -2,6 +2,7 @@ import Foundation import CoreGraphics import Defaults import AppKit +import AVFoundation final class SystemChangesObserver: MediaKeyInterceptorDelegate { private weak var coordinator: DynamicIslandViewCoordinator? @@ -9,6 +10,7 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate { private let brightnessController = SystemBrightnessController.shared private let keyboardBacklightController = SystemKeyboardBacklightController.shared private let mediaKeyInterceptor = MediaKeyInterceptor.shared + private let volumeFeedbackPlayer = VolumeFeedbackPlayer.shared private static let headsetIconSymbols: Set = [ "airpods", @@ -140,6 +142,7 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate { let baseStep = stepSize(for: step, base: standardVolumeStep) let delta = direction == .up ? baseStep : -baseStep volumeController.adjust(by: delta) + volumeFeedbackPlayer.playIfNeeded() } 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 + } + } + } +} + diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index f5833d5..1db0658 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -871,6 +871,7 @@ extension Defaults.Keys { static let enableBrightnessHUD = Key("enableBrightnessHUD", default: true) static let enableKeyboardBacklightHUD = Key("enableKeyboardBacklightHUD", default: true) static let systemHUDSensitivity = Key("systemHUDSensitivity", default: 5) + static let playVolumeChangeFeedback = Key("playVolumeChangeFeedback", default: false) // MARK: Custom OSD Window Feature static let enableCustomOSD = Key("enableCustomOSD", default: false)