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; }
根因定位
- 操作返回值错误:在WriteMix混音处理分支末尾,代码强制将返回值设为
kAudioHardwareBadObjectError,音频服务器收到错误返回后会终止当前IO周期的后续流程,直接阻断实体设备的播放动作。 - 虚拟链路断点:BlackHole原生设计仅实现“系统音频→虚拟输入捕获”的单向链路,WriteMix处理完成后未保留混音缓冲区的音频透传通道,所有送达虚拟设备的系统输出音频被直接截断,无有效路径送达扬声器、耳机等实体硬件。
- 缓冲区改写逻辑副作用:当前代码在静音分支内手动将ioMainBuffer内的音频值置0,且未保留原始混音数据,进一步导致实体输出链路无有效音频可播放。
优化方向
- 修正WriteMix分支返回值:删除
theAnswer = kAudioHardwareBadObjectError;代码行,IO操作成功时返回0(noErr),保证音频服务器能正常执行后续播放流程。 - 实现音频透传能力:WriteMix分支完成内部环形缓冲区的音频写入后,不对ioMainBuffer内的原始混音数据做清零、置0等修改,配合macOS自带的多输出设备聚合功能,将BlackHole与实体扬声器/耳机绑定为聚合输出设备,即可同时实现“捕获系统音频+实体设备实时播放”的需求。
- 调整静音控制逻辑:全局静音功能仅需在写入内部环形缓冲区时将对应采样值置0即可,不要修改ioMainBuffer内的原始混音数据,避免打断透传链路。
内容的提问来源于stack exchange,提问作者Will Tang
相关产品推荐
相关产品推荐

