Skip to content
Merged
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
import Foundation

/// Chromium decorates USB labels with a vendor/product suffix; AVFoundation does not.
/// Resolve IDs first and only accept unambiguous names, never a substring match.
/// The suffixes Chromium appends to a macOS audio input's name to build its label
/// (`GetDeviceLabel` in media/audio/mac/core_audio_util_mac.cc): one per CoreAudio
/// transport type, or the USB vendor:product pair. AVFoundation's `localizedName` carries
/// none of them, so "BlackHole 2ch (Virtual)" has to be matched as "BlackHole 2ch".
private let chromiumLabelSuffixes = [
#" \((Built-in|Virtual|Aggregate|Bluetooth)\)$"#,
#" \([0-9a-fA-F]{4}:[0-9a-fA-F]{4}\)$"#,
]

/// The device name a Chromium label was built from, or nil when it carries no known suffix.
/// Only one suffix is ever removed: Chromium appends exactly one.
func stripChromiumLabelSuffix(_ label: String) -> String? {
for pattern in chromiumLabelSuffixes {
guard let range = label.range(of: pattern, options: .regularExpression) else { continue }
let name = String(label[..<range.lowerBound])
return name.isEmpty ? nil : name
}
return nil
}

/// Resolve IDs first and only accept unambiguous names, never a substring match. A name
/// is tried as given, then without the suffix Chromium decorated it with; a name that
/// two inputs share resolves to neither, so the caller falls back to the default and says so.
public func resolveMicrophoneDeviceID(
deviceID: String?,
deviceName: String?,
Expand All @@ -16,13 +37,8 @@ public func resolveMicrophoneDeviceID(
let exact = devices.filter { $0.name == name }
if exact.count == 1 { return exact[0].id }
if !exact.isEmpty { return nil }
let normalized = name.replacingOccurrences(
of: #" \([0-9a-fA-F]{4}:[0-9a-fA-F]{4}\)$"#,
with: "",
options: .regularExpression
)
guard normalized != name else { return nil }
let matches = devices.filter { $0.name == normalized }
guard let undecorated = stripChromiumLabelSuffix(name) else { return nil }
let matches = devices.filter { $0.name == undecorated }
return matches.count == 1 ? matches[0].id : nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ final class MicrophoneDeviceSelectionTests: XCTestCase {
}

func testDoesNotGuessMissingOrPartialNames() {
for name in [nil, "", "Shure", "Shure MV7 (Virtual)", "Shure MV7 (14ed:1012) extra"] as [String?] {
for name in [
nil, "", "Shure", "Shure MV7 (14ed:1012) extra", "Shure MV7 (USB)", "Shure MV7 (virtual)",
"Shure MV7 (Virtual) (Virtual)", "(Virtual)",
] as [String?] {
XCTAssertNil(resolveMicrophoneDeviceID(deviceID: "browser-hash", deviceName: name, devices: devices))
}
}
Expand Down Expand Up @@ -45,8 +48,62 @@ final class MicrophoneDeviceSelectionTests: XCTestCase {
XCTAssertEqual(resolveMicrophoneCapture(supported: true, deviceID: nil, deviceName: nil, devices: devices), .device(nil))
}

/// Labels as Chromium 146 lists them on macOS 26, beside the AVCaptureDevice that
/// CoreAudio reports for the same input (uniqueID, localizedName).
private let macInputs = [
(id: "BlackHole2ch_UID", name: "BlackHole 2ch"),
(id: "BuiltInMicrophoneDevice", name: "MacBook Pro Microphone"),
(id: "AC-3E-B1-00-00-01:input", name: "AirPods Pro"),
(id: "AppleUSBAudioEngine:Shure:MV7:1:1", name: "Shure MV7"),
(id: "~:AMS2_Aggregate:0", name: "Aggregate Device"),
]

func testChromiumTransportSuffixesResolveToTheSelectedInput() {
let labels = [
"BlackHole 2ch (Virtual)": "BlackHole2ch_UID",
"MacBook Pro Microphone (Built-in)": "BuiltInMicrophoneDevice",
"AirPods Pro (Bluetooth)": "AC-3E-B1-00-00-01:input",
"Shure MV7 (14ed:1012)": "AppleUSBAudioEngine:Shure:MV7:1:1",
"Aggregate Device (Aggregate)": "~:AMS2_Aggregate:0",
]
for (label, id) in labels {
XCTAssertEqual(
resolveMicrophoneDeviceID(deviceID: "40657edc-browser-hash", deviceName: label, devices: macInputs),
id,
label
)
}
}

func testVirtualInputBesideAnotherResolvesToTheChosenOne() {
// The reason this matters: BlackHole being the default hid the miss on the test Mac.
XCTAssertEqual(
resolveMicrophoneCapture(
supported: true,
deviceID: "40657edc-browser-hash",
deviceName: "BlackHole 2ch (Virtual)",
devices: macInputs
),
.device("BlackHole2ch_UID")
)
}

func testChromiumDefaultEntryIsNotADevice() {
XCTAssertNil(resolveMicrophoneDeviceID(deviceID: "default", deviceName: "Default - BlackHole 2ch (Virtual)", devices: macInputs))
}

func testDecoratedLabelSharedByTwoInputsStaysAmbiguous() {
let twins = macInputs + [(id: "BlackHole2ch_UID_2", name: "BlackHole 2ch")]
XCTAssertNil(resolveMicrophoneDeviceID(deviceID: nil, deviceName: "BlackHole 2ch (Virtual)", devices: twins))
}

func testExactDecoratedDeviceNameIsPreserved() {
let decorated = devices + [(id: "literal", name: "Shure MV7 (14ed:1012)")]
XCTAssertEqual(resolveMicrophoneDeviceID(deviceID: nil, deviceName: "Shure MV7 (14ed:1012)", devices: decorated), "literal")
}

func testExactBluetoothNamedDeviceIsPreserved() {
let decorated = macInputs + [(id: "literal", name: "AirPods Pro (Bluetooth)")]
XCTAssertEqual(resolveMicrophoneDeviceID(deviceID: nil, deviceName: "AirPods Pro (Bluetooth)", devices: decorated), "literal")
}
}
Loading