如何在macOS的Swift中强制音频输出至默认内置扬声器?
macOS SwiftUI 强制音频路由至内置扬声器的可靠方案
问题背景
在macOS SwiftUI应用中使用AVAudioPlayer播放音频时,需要强制将声音输出到内置扬声器,尝试过以下方案但未解决问题:
AVRoutePickerView:仅支持iOS和Mac Catalyst,不兼容原生macOS- 固定设备ID的
AVAudioEngine方案:设备ID不固定,无法通用 - 禁用蓝牙:非最优方案,未测试
当前播放代码:
func playTheSound() { let url = Bundle.main.url(forResource: "Blow", withExtension: "mp3") player = try! AVAudioPlayer(contentsOf: url!) player?.play() print("Sound was played") }
可靠解决方案
1. 动态获取内置扬声器的AudioDeviceID
通过Core Audio API枚举所有音频输出设备,筛选出内置扬声器,以下是工具函数:
import AVFoundation import CoreAudio func getBuiltInSpeakerDeviceID() -> AudioDeviceID? { var address = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDevices, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMaster ) var propertySize: UInt32 = 0 guard AudioObjectGetPropertyDataSize(AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &propertySize) == noErr else { return nil } let deviceCount = Int(propertySize) / MemoryLayout<AudioDeviceID>.stride var devices = [AudioDeviceID](repeating: 0, count: deviceCount) guard AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &propertySize, &devices) == noErr else { return nil } for deviceID in devices { // 判断是否为输出设备 var isOutputDevice: UInt32 = 0 var outputSize = UInt32(MemoryLayout<UInt32>.size) let outputAddress = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyDeviceHasOutput, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMaster ) guard AudioObjectGetPropertyData(deviceID, &outputAddress, 0, nil, &outputSize, &isOutputDevice) == noErr, isOutputDevice == 1 else { continue } // 获取设备名称 var nameSize: UInt32 = 0 let nameAddress = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyDeviceNameCFString, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMaster ) guard AudioObjectGetPropertyDataSize(deviceID, &nameAddress, 0, nil, &nameSize) == noErr else { continue } var name = CFStringCreateMutable(nil, 0) guard AudioObjectGetPropertyData(deviceID, &nameAddress, 0, nil, &nameSize, &name) == noErr else { continue } let deviceName = name as String // 适配多语言环境下的设备名称 if deviceName.contains("Built-in Speaker") || deviceName.contains("内置扬声器") { return deviceID } } return nil }
2. 使用AVAudioEngine播放并强制路由到内置扬声器
AVAudioPlayer无法直接指定输出设备,推荐使用AVAudioEngine精确控制音频路由:
import AVFoundation class AudioPlayerManager: ObservableObject { private let engine = AVAudioEngine() private var playerNode = AVAudioPlayerNode() func playBuiltInSpeakerSound() { guard let soundURL = Bundle.main.url(forResource: "Blow", withExtension: "mp3"), let builtInDeviceID = getBuiltInSpeakerDeviceID() else { print("无法找到内置扬声器或音频文件") return } // 重置引擎状态 engine.stop() engine.reset() engine.attach(playerNode) guard let file = try? AVAudioFile(forReading: soundURL) else { print("加载音频文件失败") return } engine.connect(playerNode, to: engine.outputNode, format: file.processingFormat) // 设置输出设备为内置扬声器 let outputUnit = engine.outputNode.audioUnit! var deviceID = builtInDeviceID let status = AudioUnitSetProperty( outputUnit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, 0, &deviceID, UInt32(MemoryLayout<AudioDeviceID>.size) ) guard status == noErr else { print("设置输出设备失败:\(status)") return } do { try engine.start() playerNode.scheduleFile(file, at: nil) { self.playerNode.stop() self.engine.stop() } playerNode.play() print("通过内置扬声器播放音频") } catch { print("引擎启动失败:\(error.localizedDescription)") } } }
3. 关键注意事项
- 多语言适配:不同语言环境下内置扬声器的名称可能不同,需根据应用支持的语言调整名称匹配逻辑
- 权限配置:macOS 10.15+需在
Info.plist中添加NSMicrophoneUsageDescription(部分Core Audio操作依赖该权限) - 无全局影响:该方案仅针对当前应用的音频流设置路由,不会修改系统默认输出设备,比禁用蓝牙更友好
内容的提问来源于stack exchange,提问作者Mansidak
相关产品推荐
相关产品推荐

