You need to enable JavaScript to run this app. Mac使用BlackHole 2ch采集系统音频时虚拟设备无声音输出排查
优惠活动
大模型
产品
解决方案
定价
更多

Mac使用BlackHole 2ch采集系统音频时虚拟设备无声音输出排查

macOS 平台BlackHole虚拟音频驱动无实体设备声音回放问题

问题背景

在macOS系统上使用开源虚拟音频驱动BlackHole采集系统输出音频时,遇到如下异常:

  • BlackHole驱动会创建对应的虚拟音频设备,例如支持2通道的BlackHole 2ch
  • 将该虚拟设备设置为系统音频输出设备后,可正常通过BlackHole同步创建的对应虚拟输入设备捕获系统输出的音频流
  • 但此时虚拟输出链路无声音回放,无法从扬声器、耳机等实体音频设备听到任何声音

排查现状

  • 已知需要在核心IO处理函数BlackHole_DoIOOperation()中补充相关实现逻辑,但暂未明确优化方向
  • 界面参考素材:
    • BlackHole创建的虚拟输出设备截图
    • BlackHole创建的虚拟输入设备截图
  • 最初怀疑问题出在一行缓冲区清零代码,移除该行注释后问题仍未解决,对应代码行如下:
    // memset(ioMainBuffer, 0, inIOBufferFrameSize * NUMBER_OF_CHANNELS * sizeof(Float32));

核心函数源代码

static OSStatus BlackHole_DoIOOperation(AudioServerPlugInDriverRef          inDriver,
                                        AudioObjectID                       inDeviceObjectID,
                                        AudioObjectID                       inStreamObjectID,
                                        UInt32                              inClientID,
                                        UInt32                              inOperationID,
                                        UInt32                              inIOBufferFrameSize,
                                        const AudioServerPlugInIOCycleInfo* inIOCycleInfo,
                                        void*                               ioMainBuffer,
                                        void*                               ioSecondaryBuffer)
{
    //  该函数用于执行指定的音频IO操作,对当前设备而言,仅需为ReadInput操作清空对应缓冲区
    DebugMsg("BlackHole DoIOOperation()");
    #pragma unused(inClientID, inIOCycleInfo, ioSecondaryBuffer)
    
    //  声明局部变量
    OSStatus theAnswer = 0;
    
    //  参数合法性校验
    FailWithAction(inDriver != gAudioServerPlugInDriverRef, theAnswer = kAudioHardwareBadObjectError, Done, "BlackHole_DoIOOperation: 无效的驱动引用");
    FailWithAction(inDeviceObjectID != kObjectID_Device, theAnswer = kAudioHardwareBadObjectError, Done, "BlackHole_DoIOOperation: 无效的设备ID");
    FailWithAction((inStreamObjectID != kObjectID_Stream_Input) && (inStreamObjectID != kObjectID_Stream_Output), theAnswer = kAudioHardwareBadObjectError, Done, "BlackHole_DoIOOperation: 无效的流ID");
    
    //  获取IO操作互斥锁
    pthread_mutex_lock(&gDevice_IOMutex);

    //  音频流从BlackHole虚拟设备流向应用的输入读取逻辑
    if(inOperationID == kAudioServerPlugInIOOperationReadInput)
    {

        Float32* buffer = (Float32*)ioMainBuffer;
        UInt64 mSampleTime = inIOCycleInfo->mInputTime.mSampleTime;

        for (UInt32 frame = 0; frame < inIOBufferFrameSize; frame++)
        {
            for (int channel = 0; channel < NUMBER_OF_CHANNELS; channel++)
            {
                //  全局静音状态下不执行数据写入
                if (!gMute_Output_Master_Value)
                {
                    //  将环形缓冲区数据写入主IO缓冲区
                    buffer[frame*NUMBER_OF_CHANNELS+channel] = ringBuffer[((mSampleTime+frame)%kDevice_RingBufferSize)*NUMBER_OF_CHANNELS+channel];
                }
                else
                {
                    buffer[frame*NUMBER_OF_CHANNELS+channel] = 0;
                }
                
                //  8192个采样点后清空对应环形缓冲区数据
                ringBuffer[((mSampleTime+frame-8192)%kDevice_RingBufferSize)*NUMBER_OF_CHANNELS+channel] = 0;

            }
        }
    }
    
    //  音频流从应用写入BlackHole虚拟设备的输出混音逻辑
    if(inOperationID == kAudioServerPlugInIOOperationWriteMix)
    {
        DebugMsg("output...");
        Float32* buffer = (Float32*) ioMainBuffer;
        UInt64 mSampleTime = inIOCycleInfo->mOutputTime.mSampleTime;
        for (UInt32 frame = 0; frame < inIOBufferFrameSize; frame++)
        {
            for (int channel = 0; channel < NUMBER_OF_CHANNELS; channel++)
            {
                //  全局静音状态下不执行数据写入
                if (!gMute_Output_Master_Value)
                {
                    //  将音量缩放后的音频数据写入内部环形缓冲区
                    DebugMsg("channel: %d", channel);
                    ringBuffer[((mSampleTime + frame) % kDevice_RingBufferSize) * NUMBER_OF_CHANNELS + channel] += buffer[frame * NUMBER_OF_CHANNELS + channel] * gVolume_Output_Master_Value;
                }
                else
                {
                    buffer[frame * NUMBER_OF_CHANNELS + channel] = 0;
                }
                
                //  8192个采样点后清空对应环形缓冲区数据
                ringBuffer[((mSampleTime + frame - 8192) % kDevice_RingBufferSize) * NUMBER_OF_CHANNELS + channel] = 0;
            }
        }
        theAnswer = kAudioHardwareBadObjectError;
        
        //  已注释的主IO缓冲区清零逻辑
        // memset(ioMainBuffer, 0, inIOBufferFrameSize * NUMBER_OF_CHANNELS * sizeof(Float32));
    }
    
    pthread_mutex_unlock(&gDevice_IOMutex);

Done:
    return theAnswer;
}

根因定位

  1. 操作返回值错误:在WriteMix混音处理分支末尾,代码强制将返回值设为kAudioHardwareBadObjectError,音频服务器收到错误返回后会终止当前IO周期的后续流程,直接阻断实体设备的播放动作。
  2. 虚拟链路断点:BlackHole原生设计仅实现“系统音频→虚拟输入捕获”的单向链路,WriteMix处理完成后未保留混音缓冲区的音频透传通道,所有送达虚拟设备的系统输出音频被直接截断,无有效路径送达扬声器、耳机等实体硬件。
  3. 缓冲区改写逻辑副作用:当前代码在静音分支内手动将ioMainBuffer内的音频值置0,且未保留原始混音数据,进一步导致实体输出链路无有效音频可播放。

优化方向

  • 修正WriteMix分支返回值:删除theAnswer = kAudioHardwareBadObjectError;代码行,IO操作成功时返回0(noErr),保证音频服务器能正常执行后续播放流程。
  • 实现音频透传能力:WriteMix分支完成内部环形缓冲区的音频写入后,不对ioMainBuffer内的原始混音数据做清零、置0等修改,配合macOS自带的多输出设备聚合功能,将BlackHole与实体扬声器/耳机绑定为聚合输出设备,即可同时实现“捕获系统音频+实体设备实时播放”的需求。
  • 调整静音控制逻辑:全局静音功能仅需在写入内部环形缓冲区时将对应采样值置0即可,不要修改ioMainBuffer内的原始混音数据,避免打断透传链路。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.11.06 06:06:04