Audio Input/Output Device Status & Master Volume Monitor #21

Closed
opened 2026-09-08 10:29:35 +01:00 by gronod · 1 comment
Owner

Resolved in Milestone 3. Implemented MMAudioTelemetryProvider.h/.m enumerating CoreAudio input/output devices, nominal sample rates, master volume, and mute status. Verified with unit tests. Merged into develop.

Resolved in Milestone 3. Implemented `MMAudioTelemetryProvider.h/.m` enumerating CoreAudio input/output devices, nominal sample rates, master volume, and mute status. Verified with unit tests. Merged into `develop`.
Author
Owner

Technical Implementation Plan & Code Solution

1. CoreAudio Hardware Abstraction Layer (HAL)

CoreAudio HAL interfaces directly with system audio endpoints:

  • kAudioHardwarePropertyDefaultOutputDevice
  • kAudioHardwarePropertyDefaultInputDevice
  • kAudioDevicePropertyNominalSampleRate
  • kAudioDevicePropertyVolumeScalar
  • kAudioDevicePropertyMute

2. Objective-C Provider (MMAudioProvider.m)

#import "MMAudioProvider.h"
#import <CoreAudio/CoreAudio.h>

@implementation MMAudioProvider

- (NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error {
    AudioDeviceID outputDevice = [self defaultAudioDeviceForScope:kAudioObjectPropertyScopeOutput];
    AudioDeviceID inputDevice = [self defaultAudioDeviceForScope:kAudioObjectPropertyScopeInput];

    NSString *outName = [self deviceNameForID:outputDevice];
    NSString *inName = [self deviceNameForID:inputDevice];
    Float64 sampleRate = [self sampleRateForDevice:outputDevice];
    Float32 volume = [self volumeForDevice:outputDevice];
    UInt32 isMuted = [self muteStateForDevice:outputDevice];

    return @{
        @"defaultOutput": outName ?: @"None",
        @"defaultInput": inName ?: @"None",
        @"sampleRateHz": @(sampleRate),
        @"outputVolume": @(volume),
        @"isMuted": @(isMuted != 0)
    };
}

- (AudioDeviceID)defaultAudioDeviceForScope:(AudioObjectPropertyScope)scope {
    AudioDeviceID deviceID = kAudioObjectUnknown;
    UInt32 size = sizeof(deviceID);
    AudioObjectPropertyAddress addr = {
        scope == kAudioObjectPropertyScopeOutput ? kAudioHardwarePropertyDefaultOutputDevice : kAudioHardwarePropertyDefaultInputDevice,
        kAudioObjectPropertyScopeGlobal,
        kAudioObjectPropertyElementMain
    };
    AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, 0, NULL, &size, &deviceID);
    return deviceID;
}

- (NSString *)deviceNameForID:(AudioDeviceID)dID {
    CFStringRef name = NULL;
    UInt32 size = sizeof(CFStringRef);
    AudioObjectPropertyAddress addr = {
        kAudioObjectPropertyName,
        kAudioObjectPropertyScopeGlobal,
        kAudioObjectPropertyElementMain
    };
    if (AudioObjectGetPropertyData(dID, &addr, 0, NULL, &size, &name) == noErr && name) {
        return (__bridge_transfer NSString *)name;
    }
    return @"Unknown";
}

- (Float64)sampleRateForDevice:(AudioDeviceID)dID {
    Float64 rate = 0;
    UInt32 size = sizeof(rate);
    AudioObjectPropertyAddress addr = {
        kAudioDevicePropertyNominalSampleRate,
        kAudioObjectPropertyScopeGlobal,
        kAudioObjectPropertyElementMain
    };
    AudioObjectGetPropertyData(dID, &addr, 0, NULL, &size, &rate);
    return rate;
}

- (Float32)volumeForDevice:(AudioDeviceID)dID {
    Float32 vol = 0;
    UInt32 size = sizeof(vol);
    AudioObjectPropertyAddress addr = {
        kAudioDevicePropertyVolumeScalar,
        kAudioObjectPropertyScopeOutput,
        kAudioObjectPropertyElementMain
    };
    AudioObjectGetPropertyData(dID, &addr, 0, NULL, &size, &vol);
    return vol;
}

- (UInt32)muteStateForDevice:(AudioDeviceID)dID {
    UInt32 muted = 0;
    UInt32 size = sizeof(muted);
    AudioObjectPropertyAddress addr = {
        kAudioDevicePropertyMute,
        kAudioObjectPropertyScopeOutput,
        kAudioObjectPropertyElementMain
    };
    AudioObjectGetPropertyData(dID, &addr, 0, NULL, &size, &muted);
    return muted;
}
@end

3. Swift UI Integration

@Observable
public final class AudioSnapshot {
    public var defaultOutput: String = ""
    public var defaultInput: String = ""
    public var sampleRateKHz: Double = 0.0
    public var volumePercent: Double = 0.0
    public var isMuted: Bool = false

    public func update(from dict: [String: Any]) {
        self.defaultOutput = dict["defaultOutput"] as? String ?? "None"
        self.defaultInput = dict["defaultInput"] as? String ?? "None"
        self.sampleRateKHz = (dict["sampleRateHz"] as? Double ?? 0.0) / 1000.0
        self.volumePercent = (dict["outputVolume"] as? Double ?? 0.0) * 100.0
        self.isMuted = dict["isMuted"] as? Bool ?? false
    }
}

4. Zero-Latency Property Observers

AudioObjectAddPropertyListener can be wired up to listen for volume wheel / headphone plug transitions, updating the UI instantaneously without polling.

## Technical Implementation Plan & Code Solution ### 1. CoreAudio Hardware Abstraction Layer (HAL) CoreAudio HAL interfaces directly with system audio endpoints: - `kAudioHardwarePropertyDefaultOutputDevice` - `kAudioHardwarePropertyDefaultInputDevice` - `kAudioDevicePropertyNominalSampleRate` - `kAudioDevicePropertyVolumeScalar` - `kAudioDevicePropertyMute` --- ### 2. Objective-C Provider (`MMAudioProvider.m`) ```objc #import "MMAudioProvider.h" #import <CoreAudio/CoreAudio.h> @implementation MMAudioProvider - (NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error { AudioDeviceID outputDevice = [self defaultAudioDeviceForScope:kAudioObjectPropertyScopeOutput]; AudioDeviceID inputDevice = [self defaultAudioDeviceForScope:kAudioObjectPropertyScopeInput]; NSString *outName = [self deviceNameForID:outputDevice]; NSString *inName = [self deviceNameForID:inputDevice]; Float64 sampleRate = [self sampleRateForDevice:outputDevice]; Float32 volume = [self volumeForDevice:outputDevice]; UInt32 isMuted = [self muteStateForDevice:outputDevice]; return @{ @"defaultOutput": outName ?: @"None", @"defaultInput": inName ?: @"None", @"sampleRateHz": @(sampleRate), @"outputVolume": @(volume), @"isMuted": @(isMuted != 0) }; } - (AudioDeviceID)defaultAudioDeviceForScope:(AudioObjectPropertyScope)scope { AudioDeviceID deviceID = kAudioObjectUnknown; UInt32 size = sizeof(deviceID); AudioObjectPropertyAddress addr = { scope == kAudioObjectPropertyScopeOutput ? kAudioHardwarePropertyDefaultOutputDevice : kAudioHardwarePropertyDefaultInputDevice, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain }; AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, 0, NULL, &size, &deviceID); return deviceID; } - (NSString *)deviceNameForID:(AudioDeviceID)dID { CFStringRef name = NULL; UInt32 size = sizeof(CFStringRef); AudioObjectPropertyAddress addr = { kAudioObjectPropertyName, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain }; if (AudioObjectGetPropertyData(dID, &addr, 0, NULL, &size, &name) == noErr && name) { return (__bridge_transfer NSString *)name; } return @"Unknown"; } - (Float64)sampleRateForDevice:(AudioDeviceID)dID { Float64 rate = 0; UInt32 size = sizeof(rate); AudioObjectPropertyAddress addr = { kAudioDevicePropertyNominalSampleRate, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain }; AudioObjectGetPropertyData(dID, &addr, 0, NULL, &size, &rate); return rate; } - (Float32)volumeForDevice:(AudioDeviceID)dID { Float32 vol = 0; UInt32 size = sizeof(vol); AudioObjectPropertyAddress addr = { kAudioDevicePropertyVolumeScalar, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain }; AudioObjectGetPropertyData(dID, &addr, 0, NULL, &size, &vol); return vol; } - (UInt32)muteStateForDevice:(AudioDeviceID)dID { UInt32 muted = 0; UInt32 size = sizeof(muted); AudioObjectPropertyAddress addr = { kAudioDevicePropertyMute, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain }; AudioObjectGetPropertyData(dID, &addr, 0, NULL, &size, &muted); return muted; } @end ``` --- ### 3. Swift UI Integration ```swift @Observable public final class AudioSnapshot { public var defaultOutput: String = "" public var defaultInput: String = "" public var sampleRateKHz: Double = 0.0 public var volumePercent: Double = 0.0 public var isMuted: Bool = false public func update(from dict: [String: Any]) { self.defaultOutput = dict["defaultOutput"] as? String ?? "None" self.defaultInput = dict["defaultInput"] as? String ?? "None" self.sampleRateKHz = (dict["sampleRateHz"] as? Double ?? 0.0) / 1000.0 self.volumePercent = (dict["outputVolume"] as? Double ?? 0.0) * 100.0 self.isMuted = dict["isMuted"] as? Bool ?? false } } ``` --- ### 4. Zero-Latency Property Observers `AudioObjectAddPropertyListener` can be wired up to listen for volume wheel / headphone plug transitions, updating the UI instantaneously without polling.
gronod added this to the M3: Advanced Kernel, Process & Peripheral Telemetry milestone 2026-09-08 10:42:43 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: gronod/MacMonitor#21