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

如何在macOS中通过非默认输出设备播放麦克风音频?

在macOS上使用非默认输出设备监听麦克风音频

问题背景

需要在不修改系统默认输出设备的前提下,通过指定的非默认输出设备(比如外接耳机,系统默认是MacBook Pro扬声器)监听麦克风音频。目前已实现默认输出设备的麦克风监听,但尝试修改输出设备ID的代码无效,音频仍从默认设备输出。

现有默认输出监听代码

private func resetAudioEngine() {
    if let audioEngine = audioEngine {
        audioEngine.stop()
        self.audioEngine = nil
    }
    
    let audioEngine = AVAudioEngine()
    audioEngine.connect(audioEngine.inputNode, to: audioEngine.mainMixerNode, format: nil)
    audioEngine.connect(audioEngine.mainMixerNode, to: outputNode, format: nil)
    self.audioEngine = audioEngine
}

尝试过但无效的代码

var outputDeviceID = deviceid
if let audioSetup  = audioEngine.outputNode.audioUnit {
            AudioUnitSetProperty(audioSetup,kAudioOutputUnitProperty_CurrentDevice,kAudioUnitScope_Global,AudioUnitElement(0),
                                          &outputDeviceID, UInt32(MemoryLayout.size(ofValue: outputDeviceID)))
        }

解决方案

核心思路是直接创建绑定目标输出设备的AVAudioOutputNode,而非修改默认输出节点的属性——因为AVAudioEngine的默认outputNode与系统默认设备强绑定,修改其AudioUnit属性容易因初始化时机、权限等问题失效。

完整实现代码

import AVFoundation

class AudioMonitorManager {
    private var audioEngine: AVAudioEngine?
    
    /// 启动麦克风监听并输出到指定设备
    /// - Parameter targetDeviceID: 目标输出设备的ID
    func startMonitoringToTargetDevice(targetDeviceID: AudioDeviceID) throws {
        stopMonitoring()
        
        let audioEngine = AVAudioEngine()
        let inputNode = audioEngine.inputNode
        let mainMixer = audioEngine.mainMixerNode
        
        // 基于目标设备ID创建自定义输出节点
        let outputConfig = AVAudioDeviceIOConfiguration(
            deviceID: targetDeviceID,
            channelCount: 2,
            sampleRate: inputNode.inputFormat(forBus: 0).sampleRate,
            isInput: false
        )
        let targetOutputNode = try AVAudioOutputNode(configuration: outputConfig)
        
        // 连接音频链路:麦克风输入 → 混音器 → 目标输出节点
        audioEngine.connect(inputNode, to: mainMixer, format: nil)
        audioEngine.connect(mainMixer, to: targetOutputNode, format: nil)
        
        // 将自定义输出节点添加到引擎并启动
        audioEngine.attach(targetOutputNode)
        try audioEngine.start()
        
        self.audioEngine = audioEngine
    }
    
    /// 停止监听
    func stopMonitoring() {
        audioEngine?.stop()
        audioEngine = nil
    }
    
    /// 获取所有可用输出设备列表
    static func getAllOutputDevices() -> [(name: String, id: AudioDeviceID)] {
        var devices: [(name: String, id: AudioDeviceID)] = []
        let systemAudioObject = AudioObjectID(kAudioObjectSystemObject)
        
        // 获取所有音频设备ID
        var address = AudioObjectPropertyAddress(
            mSelector: kAudioHardwarePropertyDevices,
            mScope: kAudioObjectPropertyScopeGlobal,
            mElement: kAudioObjectPropertyElementMaster
        )
        var propertySize: UInt32 = 0
        guard AudioObjectGetPropertyDataSize(systemAudioObject, &address, 0, nil, &propertySize) == noErr else {
            return devices
        }
        
        let deviceCount = Int(propertySize) / MemoryLayout<AudioDeviceID>.stride
        var deviceIDs = Array(repeating: AudioDeviceID(), count: deviceCount)
        guard AudioObjectGetPropertyData(systemAudioObject, &address, 0, nil, &propertySize, &deviceIDs) == noErr else {
            return devices
        }
        
        // 筛选输出设备并获取名称
        for deviceID in deviceIDs {
            // 判断是否为输出设备
            var outputAddress = AudioObjectPropertyAddress(
                mSelector: kAudioDevicePropertyStreamConfiguration,
                mScope: kAudioDevicePropertyScopeOutput,
                mElement: kAudioObjectPropertyElementMaster
            )
            var outputSize: UInt32 = 0
            if AudioObjectGetPropertyDataSize(deviceID, &outputAddress, 0, nil, &outputSize) != noErr || outputSize == 0 {
                continue
            }
            
            // 获取设备名称
            var nameAddress = AudioObjectPropertyAddress(
                mSelector: kAudioDevicePropertyDeviceNameCFString,
                mScope: kAudioObjectPropertyScopeGlobal,
                mElement: kAudioObjectPropertyElementMaster
            )
            var name: CFString = "" as CFString
            var nameSize = UInt32(MemoryLayout<CFString>.size)
            if AudioObjectGetPropertyData(deviceID, &nameAddress, 0, nil, &nameSize, &name) == noErr {
                devices.append((name: name as String, id: deviceID))
            }
        }
        return devices
    }
}

关键说明

  1. 自定义输出节点:通过AVAudioOutputNode(configuration:)直接绑定目标设备ID,从根源上避免了与系统默认设备的关联问题
  2. 节点挂载:必须调用audioEngine.attach(targetOutputNode)将自定义节点添加到引擎,否则引擎无法识别并调度该节点
  3. 设备枚举:getAllOutputDevices()方法可获取所有可用输出设备的名称和ID,用于用户选择目标设备

内容的提问来源于stack exchange,提问作者shahed

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 01:55:16