You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 01:20:47