Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 79 additions & 31 deletions packages/host/Sources/SoundBridgeHost/Devices/DeviceDiscovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,21 @@ class DeviceDiscovery {
print("[DeviceEnum] Validation note: \(reason)")
}

// Check if device supports hardware volume control
let fixedVolume = !deviceHasVolumeControl(deviceID)
print("[DeviceEnum] Volume control: \(fixedVolume ? "Fixed (no hardware volume)" : "Adjustable (hardware volume supported)")")

if !fixedVolume {
print("[DeviceEnum] ✗ SKIP: Writable Core Audio volume control already available")
continue
}

if validation.valid {
print("[DeviceEnum] ✓ ACCEPTED: Adding to device list")
} else {
logger.warning("ACCEPTED WITH WARNING: Device may not work properly")
}

// Check if device supports hardware volume control
let fixedVolume = !deviceHasVolumeControl(deviceID)
print("[DeviceEnum] Volume control: \(fixedVolume ? "Fixed (no hardware volume)" : "Adjustable (hardware volume supported)")")

devices.append(PhysicalDevice(
id: deviceID,
name: name,
Expand Down Expand Up @@ -261,42 +266,63 @@ class DeviceDiscovery {
return AudioObjectGetPropertyDataSize(deviceID, &streamAddress, 0, nil, &streamSize) == noErr && streamSize > 0
}

/// Check if device has active output channels (not just streams that report existence)
private func deviceHasActiveChannels(_ deviceID: AudioDeviceID) -> Bool {
/// Return the total number of output channels reported by Core Audio.
private func getOutputChannelCount(_ deviceID: AudioDeviceID) -> Int? {
var configAddress = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyStreamConfiguration,
mScope: kAudioDevicePropertyScopeOutput,
mElement: kAudioObjectPropertyElementMain
)

var dataSize: UInt32 = 0
guard AudioObjectGetPropertyDataSize(deviceID, &configAddress, 0, nil, &dataSize) == noErr else {
return false
guard AudioObjectGetPropertyDataSize(
deviceID,
&configAddress,
0,
nil,
&dataSize
) == noErr,
dataSize > 0
else {
return nil
}

let bufferListPointer = UnsafeMutablePointer<AudioBufferList>.allocate(capacity: Int(dataSize))
defer { bufferListPointer.deallocate() }
let rawPointer = UnsafeMutableRawPointer.allocate(
byteCount: Int(dataSize),
alignment: MemoryLayout<AudioBufferList>.alignment
)
defer { rawPointer.deallocate() }

let bufferListPointer = rawPointer.bindMemory(
to: AudioBufferList.self,
capacity: 1
)

guard AudioObjectGetPropertyData(deviceID, &configAddress, 0, nil, &dataSize, bufferListPointer) == noErr else {
return false
guard AudioObjectGetPropertyData(
deviceID,
&configAddress,
0,
nil,
&dataSize,
bufferListPointer
) == noErr else {
return nil
}

let bufferList = bufferListPointer.pointee
let bufferCount = Int(bufferList.mNumberBuffers)
let buffers = UnsafeMutableAudioBufferListPointer(bufferListPointer)

if bufferCount == 0 {
return false
return buffers.reduce(0) {
$0 + Int($1.mNumberChannels)
}
}

// Check if at least one buffer has channels
let buffers = UnsafeMutableAudioBufferListPointer(bufferListPointer)
for buffer in buffers {
if buffer.mNumberChannels > 0 {
return true
}
/// Check if the device reports at least one active output channel.
private func deviceHasActiveChannels(_ deviceID: AudioDeviceID) -> Bool {
guard let channelCount = getOutputChannelCount(deviceID) else {
return false
}

return false
return channelCount > 0
}

/// Check jack connection status for HDMI/DisplayPort devices
Expand Down Expand Up @@ -429,23 +455,45 @@ class DeviceDiscovery {
return (true, nil)
}

/// Check if device supports hardware volume control via kAudioDevicePropertyVolumeScalar.
/// Checks master (element 0) and channel-level (elements 1, 2) on output scope.
/// Returns true if at least one element supports volume control.
/// Check whether Core Audio exposes a writable output volume control.
///
/// Checks the main element plus every output channel reported by the device.
/// A volume property must both exist and be settable to count as usable.
private func deviceHasVolumeControl(_ deviceID: AudioDeviceID) -> Bool {
let elements: [UInt32] = [
kAudioObjectPropertyElementMain, // element 0 (master)
1, // element 1 (left channel)
2 // element 2 (right channel)
guard let channelCount = getOutputChannelCount(deviceID) else {
return false
}

var elements: [UInt32] = [
kAudioObjectPropertyElementMain,
]

if channelCount > 0 {
elements.append(
contentsOf: (1 ... channelCount).map { UInt32($0) }
)
}

for element in elements {
var address = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyVolumeScalar,
mScope: kAudioDevicePropertyScopeOutput,
mElement: element
)
if AudioObjectHasProperty(deviceID, &address) {

guard AudioObjectHasProperty(deviceID, &address) else {
continue
}

var isSettable = DarwinBoolean(false)

let status = AudioObjectIsPropertySettable(
deviceID,
&address,
&isSettable
)

if status == noErr && isSettable.boolValue {
return true
}
}
Expand Down