From a64ad6af92e3f2a17b256a88d8a3d67b9057b233 Mon Sep 17 00:00:00 2001 From: Andrea Alberti Date: Mon, 29 Sep 2025 03:46:20 +0200 Subject: [PATCH] Fixed bug not displaying system volume when muted --- Sources/Controllers/AppDelegate.m | 91 ++++++------ Sources/Controllers/SystemVolume.m | 231 ++++++++++++++++------------- Volume Control.sublime-project | 11 +- build_config.env | 2 +- 4 files changed, 181 insertions(+), 154 deletions(-) diff --git a/Sources/Controllers/AppDelegate.m b/Sources/Controllers/AppDelegate.m index 455d511..82b2727 100644 --- a/Sources/Controllers/AppDelegate.m +++ b/Sources/Controllers/AppDelegate.m @@ -241,7 +241,7 @@ static NSTimeInterval updateSystemVolumeInterval=0.1f; static NSString * const kHelperBundleIDSuffix = @"Helper"; - (NSString *)helperBundleID { - return [[[NSBundle mainBundle] bundleIdentifier] stringByAppendingString:kHelperBundleIDSuffix]; + return [[[NSBundle mainBundle] bundleIdentifier] stringByAppendingString:kHelperBundleIDSuffix]; } - (IBAction)terminate:(id)sender @@ -285,58 +285,58 @@ static NSString * const kHelperBundleIDSuffix = @"Helper"; - (void)updateStartAtLoginMenuItem { - BOOL enabled = [self StartAtLogin]; - NSMenuItem* menuItem = [self.statusMenu itemWithTag:START_AT_LOGIN_ID]; - [menuItem setState:enabled ? NSControlStateValueOn : NSControlStateValueOff]; + BOOL enabled = [self StartAtLogin]; + NSMenuItem* menuItem = [self.statusMenu itemWithTag:START_AT_LOGIN_ID]; + [menuItem setState:enabled ? NSControlStateValueOn : NSControlStateValueOff]; } - (void)setStartAtLogin:(BOOL)enabled savePreferences:(BOOL)savePreferences { - NSString *helperBundleID = @"io.alberti42.VolumeControlHelper"; + NSString *helperBundleID = @"io.alberti42.VolumeControlHelper"; - if (@available(macOS 13.0, *)) { - SMAppService *service = [SMAppService loginItemServiceWithIdentifier:helperBundleID]; - NSError *error = nil; + if (@available(macOS 13.0, *)) { + SMAppService *service = [SMAppService loginItemServiceWithIdentifier:helperBundleID]; + NSError *error = nil; - if (enabled) { - if (service.status != SMAppServiceStatusEnabled) { - if (![service registerAndReturnError:&error]) { - NSLog(@"[Volume Control] Error registering login item: %@", error.localizedDescription); - } - } - } else { - if (service.status != SMAppServiceStatusNotRegistered) { - if (![service unregisterAndReturnError:&error]) { - NSLog(@"[Volume Control] Error unregistering login item: %@", error.localizedDescription); - } - } - } - } else { - // Legacy fallback (macOS 12 and older) - if (!SMLoginItemSetEnabled((__bridge CFStringRef)helperBundleID, enabled)) { - NSLog(@"[Volume Control] SMLoginItemSetEnabled failed."); - } - } + if (enabled) { + if (service.status != SMAppServiceStatusEnabled) { + if (![service registerAndReturnError:&error]) { + NSLog(@"[Volume Control] Error registering login item: %@", error.localizedDescription); + } + } + } else { + if (service.status != SMAppServiceStatusNotRegistered) { + if (![service unregisterAndReturnError:&error]) { + NSLog(@"[Volume Control] Error unregistering login item: %@", error.localizedDescription); + } + } + } + } else { + // Legacy fallback (macOS 12 and older) + if (!SMLoginItemSetEnabled((__bridge CFStringRef)helperBundleID, enabled)) { + NSLog(@"[Volume Control] SMLoginItemSetEnabled failed."); + } + } - if (savePreferences) { - [preferences setBool:enabled forKey:@"StartAtLoginPreference"]; - } + if (savePreferences) { + [preferences setBool:enabled forKey:@"StartAtLoginPreference"]; + } - [self updateStartAtLoginMenuItem]; + [self updateStartAtLoginMenuItem]; } - (bool)StartAtLogin { - NSString *helperBundleID = @"io.alberti42.VolumeControlHelper"; + NSString *helperBundleID = @"io.alberti42.VolumeControlHelper"; - if (@available(macOS 13.0, *)) { - SMAppService *service = [SMAppService loginItemServiceWithIdentifier:helperBundleID]; - return (service.status == SMAppServiceStatusEnabled || - service.status == SMAppServiceStatusRequiresApproval); - } else { - return [preferences boolForKey:@"StartAtLoginPreference"]; - } + if (@available(macOS 13.0, *)) { + SMAppService *service = [SMAppService loginItemServiceWithIdentifier:helperBundleID]; + return (service.status == SMAppServiceStatusEnabled || + service.status == SMAppServiceStatusRequiresApproval); + } else { + return [preferences boolForKey:@"StartAtLoginPreference"]; + } } - (void)wasAuthorized @@ -490,8 +490,11 @@ static NSString * const kHelperBundleIDSuffix = @"Helper"; else if (runningPlayerPtr == doppler) [self setSpotifyVolume:[runningPlayerPtr currentVolume]]; - if (_LockSystemAndPlayerVolume || runningPlayerPtr == systemAudio) - [self setSystemVolume:[runningPlayerPtr currentVolume]]; + // Update system UI if system volume is affected or when locked + if (_LockSystemAndPlayerVolume || runningPlayerPtr == systemAudio) { + [self setSystemVolume:[systemAudio currentVolume]]; + } + } } @@ -785,9 +788,9 @@ static NSString * const kHelperBundleIDSuffix = @"Helper"; - (IBAction)toggleStartAtLogin:(id)sender { - BOOL currentlyEnabled = [self StartAtLogin]; - [self setStartAtLogin:!currentlyEnabled savePreferences:YES]; - [self updateStartAtLoginMenuItem]; + BOOL currentlyEnabled = [self StartAtLogin]; + [self setStartAtLogin:!currentlyEnabled savePreferences:YES]; + [self updateStartAtLoginMenuItem]; } - (void) setUseAppleCMDModifier:(bool)enabled diff --git a/Sources/Controllers/SystemVolume.m b/Sources/Controllers/SystemVolume.m index f96910c..b094735 100755 --- a/Sources/Controllers/SystemVolume.m +++ b/Sources/Controllers/SystemVolume.m @@ -17,136 +17,155 @@ -(AudioDeviceID) getDefaultOutputDevice { - AudioObjectPropertyAddress getDefaultOutputDevicePropertyAddress = { - kAudioHardwarePropertyDefaultOutputDevice, - kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyElementMain - }; - - AudioDeviceID defaultOutputDeviceID; - UInt32 volumedataSize = sizeof(defaultOutputDeviceID); - OSStatus result = AudioObjectGetPropertyData(kAudioObjectSystemObject, - &getDefaultOutputDevicePropertyAddress, - 0, NULL, - &volumedataSize, &defaultOutputDeviceID); - - if(kAudioHardwareNoError != result) - { - NSLog(@"Cannot find default output device!"); - } - - return defaultOutputDeviceID; + AudioObjectPropertyAddress getDefaultOutputDevicePropertyAddress = { + kAudioHardwarePropertyDefaultOutputDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain + }; + + AudioDeviceID defaultOutputDeviceID; + UInt32 volumedataSize = sizeof(defaultOutputDeviceID); + OSStatus result = AudioObjectGetPropertyData(kAudioObjectSystemObject, + &getDefaultOutputDevicePropertyAddress, + 0, NULL, + &volumedataSize, &defaultOutputDeviceID); + + if(kAudioHardwareNoError != result) + { + NSLog(@"Cannot find default output device!"); + } + + return defaultOutputDeviceID; } - (void)setCurrentVolume:(double)currentVolume { - AudioDeviceID defaultOutputDeviceID = [self getDefaultOutputDevice]; + AudioDeviceID defaultOutputDeviceID = [self getDefaultOutputDevice]; - AudioObjectPropertyAddress volumePropertyAddress = { - kAudioHardwareServiceDeviceProperty_VirtualMainVolume, - kAudioDevicePropertyScopeOutput, - kAudioObjectPropertyElementMain - }; + AudioObjectPropertyAddress volumePropertyAddress = { + kAudioHardwareServiceDeviceProperty_VirtualMainVolume, + kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMain + }; - AudioObjectPropertyAddress mutePropertyAddress = { - kAudioDevicePropertyMute, - kAudioDevicePropertyScopeOutput, - kAudioObjectPropertyElementMain - }; + AudioObjectPropertyAddress mutePropertyAddress = { + kAudioDevicePropertyMute, + kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMain + }; - Float32 volume = (Float32)(currentVolume / 100.); - UInt32 dataSize; + Float32 volume = (Float32)(currentVolume / 100.); + UInt32 dataSize; - if (volume == 0) { - // Mute the device - UInt32 mute = 1; - dataSize = sizeof(mute); - OSStatus result = AudioObjectSetPropertyData(defaultOutputDeviceID, - &mutePropertyAddress, - 0, NULL, - dataSize, &mute); - if (result != noErr) { - NSLog(@"Failed to mute device 0x%0x", defaultOutputDeviceID); - } - } else { - // Unmute the device - UInt32 mute = 0; - dataSize = sizeof(mute); - AudioObjectSetPropertyData(defaultOutputDeviceID, - &mutePropertyAddress, - 0, NULL, - dataSize, &mute); + if (volume == 0) { + // Mute the device + UInt32 mute = 1; + dataSize = sizeof(mute); + OSStatus result = AudioObjectSetPropertyData(defaultOutputDeviceID, + &mutePropertyAddress, + 0, NULL, + dataSize, &mute); + if (result != noErr) { + NSLog(@"Failed to mute device 0x%0x", defaultOutputDeviceID); + } + } else { + // Unmute the device + UInt32 mute = 0; + dataSize = sizeof(mute); + AudioObjectSetPropertyData(defaultOutputDeviceID, + &mutePropertyAddress, + 0, NULL, + dataSize, &mute); - // Set the volume - dataSize = sizeof(volume); - OSStatus result = AudioObjectSetPropertyData(defaultOutputDeviceID, - &volumePropertyAddress, - 0, NULL, - dataSize, &volume); - if (result != noErr) { - NSLog(@"Failed to set volume for device 0x%0x", defaultOutputDeviceID); - } - } + // Set the volume + dataSize = sizeof(volume); + OSStatus result = AudioObjectSetPropertyData(defaultOutputDeviceID, + &volumePropertyAddress, + 0, NULL, + dataSize, &volume); + if (result != noErr) { + NSLog(@"Failed to set volume for device 0x%0x", defaultOutputDeviceID); + } + } } - (bool) isMuted { - AudioDeviceID defaultOutputDeviceID = [self getDefaultOutputDevice]; - - AudioObjectPropertyAddress volumePropertyAddress = { - kAudioDevicePropertyMute, - kAudioDevicePropertyScopeOutput, - kAudioObjectPropertyElementMain - }; - - UInt32 muteVal; - UInt32 muteValSize = sizeof(muteVal); - OSStatus result = AudioObjectGetPropertyData(defaultOutputDeviceID, - &volumePropertyAddress, - 0, NULL, - &muteValSize, &muteVal); - - if (result != kAudioHardwareNoError) { - NSLog(@"No volume reported for device 0x%0x", defaultOutputDeviceID); - } - - return muteVal; -} + AudioDeviceID defaultOutputDeviceID = [self getDefaultOutputDevice]; + AudioObjectPropertyAddress volumePropertyAddress = { + kAudioDevicePropertyMute, + kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMain + }; + + UInt32 muteVal; + UInt32 muteValSize = sizeof(muteVal); + OSStatus result = AudioObjectGetPropertyData(defaultOutputDeviceID, + &volumePropertyAddress, + 0, NULL, + &muteValSize, &muteVal); + + if (result != kAudioHardwareNoError) { + NSLog(@"No volume reported for device 0x%0x", defaultOutputDeviceID); + } + + return muteVal; +} - (double) currentVolume { - AudioDeviceID defaultOutputDeviceID = [self getDefaultOutputDevice]; - - AudioObjectPropertyAddress volumePropertyAddress = { - kAudioHardwareServiceDeviceProperty_VirtualMainVolume, - kAudioDevicePropertyScopeOutput, - kAudioObjectPropertyElementMain - }; - - Float32 volume; - UInt32 volumedataSize = sizeof(volume); - OSStatus result = AudioObjectGetPropertyData(defaultOutputDeviceID, - &volumePropertyAddress, - 0, NULL, - &volumedataSize, &volume); - - if (result != kAudioHardwareNoError) { - NSLog(@"No volume reported for device 0x%0x", defaultOutputDeviceID); - } - - return ((double)volume)*100.; + AudioDeviceID defaultOutputDeviceID = [self getDefaultOutputDevice]; + + // First, check mute state + AudioObjectPropertyAddress mutePropertyAddress = { + kAudioDevicePropertyMute, + kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMain + }; + + UInt32 muteVal = 0; + UInt32 muteValSize = sizeof(muteVal); + OSStatus muteResult = AudioObjectGetPropertyData(defaultOutputDeviceID, + &mutePropertyAddress, + 0, NULL, + &muteValSize, &muteVal); + + if (muteResult == kAudioHardwareNoError && muteVal == 1) { + return 0.0; // Treat mute as 0% + } + + // Otherwise, get the real volume + AudioObjectPropertyAddress volumePropertyAddress = { + kAudioHardwareServiceDeviceProperty_VirtualMainVolume, + kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMain + }; + + Float32 volume = 0; + UInt32 volumedataSize = sizeof(volume); + OSStatus result = AudioObjectGetPropertyData(defaultOutputDeviceID, + &volumePropertyAddress, + 0, NULL, + &volumedataSize, &volume); + + if (result != kAudioHardwareNoError) { + NSLog(@"No volume reported for device 0x%0x", defaultOutputDeviceID); + } + + return ((double)volume) * 100.0; } + -(void)dealloc { } -(id)init{ - if (self = [super init]) { - [self setOldVolume:[self currentVolume]]; - } - return self; + if (self = [super init]) { + [self setOldVolume:[self currentVolume]]; + } + return self; } @end diff --git a/Volume Control.sublime-project b/Volume Control.sublime-project index 55cba21..fbb8304 100644 --- a/Volume Control.sublime-project +++ b/Volume Control.sublime-project @@ -19,17 +19,22 @@ "build_systems": [ { "name": "Build with xcodebuild for debug+x86_64", - "shell_cmd": "source ./build_config.env && xcrun xcodebuild -project 'Volume Control.xcodeproj' -scheme 'Volume Control' -configuration Debug -destination 'platform=macOS,arch=x86_64' BUILD_DIR=\"\\${BUILD_DIR}\" CONFIGURATION_BUILD_DIR=\"\\${CONFIGURATION_BUILD_DIR}\" build | xcpretty", + "shell_cmd": "source ./build_config.env && xcrun xcodebuild -project 'Volume Control.xcodeproj' -scheme 'Volume Control' -configuration Debug -destination 'platform=macOS,arch=x86_64' BUILD_DIR=\"\\${BUILD_DIR}/debug\" CONFIGURATION_BUILD_DIR=\"\\${CONFIGURATION_BUILD_DIR}/debug\" build | xcpretty", + "working_dir": "${project_path}", + }, + { + "name": "Distribute", + "shell_cmd": "source ./build_config.env && xcrun xcodebuild -project 'Volume Control.xcodeproj' -scheme 'Volume Control' -configuration Release ARCHS=\"arm64 x86_64\" ONLY_ACTIVE_ARCH=NO BUILD_DIR=\"\\${BUILD_DIR}/release\" CONFIGURATION_BUILD_DIR=\"\\${CONFIGURATION_BUILD_DIR}/release\" build | xcpretty", "working_dir": "${project_path}", }, { "name": "Clean with xcodebuild for debug+x86_64", - "shell_cmd": "source ./build_config.env && xcrun xcodebuild -project 'Volume Control.xcodeproj' -scheme 'Volume Control' -configuration Debug -destination 'platform=macOS,arch=x86_64' BUILD_DIR=\"\\${BUILD_DIR}\" CONFIGURATION_BUILD_DIR=\"\\${CONFIGURATION_BUILD_DIR}\" clean | xcpretty", + "shell_cmd": "source ./build_config.env && xcrun xcodebuild -project 'Volume Control.xcodeproj' -scheme 'Volume Control' -configuration Debug -destination 'platform=macOS,arch=x86_64' BUILD_DIR=\"\\${BUILD_DIR}/debug\" CONFIGURATION_BUILD_DIR=\"\\${CONFIGURATION_BUILD_DIR}/debug\" clean | xcpretty", "working_dir": "${project_path}", }, { "name": "Run App for debug", - "shell_cmd": "source ./build_config.env && open \"\\${CONFIGURATION_BUILD_DIR}/Volume Control.app\"", + "shell_cmd": "source ./build_config.env && open \"\\${CONFIGURATION_BUILD_DIR}/debug/Volume Control.app\"", "working_dir": "${project_path}" }, { diff --git a/build_config.env b/build_config.env index e755cc8..232df67 100755 --- a/build_config.env +++ b/build_config.env @@ -1,4 +1,4 @@ # build_config.env: define build variables once export BUILD_DIR="$(dirname "$0")/build" -export CONFIGURATION_BUILD_DIR="${BUILD_DIR}/target/debug" +export CONFIGURATION_BUILD_DIR="${BUILD_DIR}/target"