Deutsch | English | Español | français | 日本語 | 한국어 | Português | Русский | 中文
一个用c++编写的简单控制台程序,能内录特定进程(或排除指定进程)发出的声音,也支持传统的全局混音录制,完全借助原生 Windows API实现,无需安装任何虚拟声卡。
日常使用电脑时,录制系统内部的声音是一个再常见不过的需求,但真正做起来却往往不尽人意。我们常常想要保存某一段音频,却总被突如其来的通知声、无关的语音消息或是背景音乐干扰,很难得到一份干净纯粹的录音。
比如说,你正在参加一堂重要的在线课程,希望将其完整录制下来便于后续复习重听。课程内容翔实,讲师声音清晰。但同时,一个紧急的团队语音会议拉你加入,组内正在进行着嘈杂的讨论。如果此时使用系统自带或常规的录音软件,就会不加区分地将课程讲师的声音、团队成员的讨论声、以及时不时叮咚作响的各类软件通知音,全部混合在一起。最终你得到的,是一份几乎无法使用的混乱录音。那么能否像指定一个窗口进行截图那样,只“录制”特定软件的声音?
又比如使用读屏软件的朋友,想录制一期播客。这时候需要依赖读屏语音浏览文档、执行操作,但这些提示音绝不能出现在最终要分享的作品里。或者,想要特定程序的声音被录进去,奇他的都不需要,比如录制教程的同时要录制读屏声音。更棘手的是,许多现代笔记本为了简化音频驱动,甚至直接取消了“立体声混音”(Stereo Mix)这个功能,使得最基础的“系统内录”都变得遥不可及,给我们的创作过程带来极大不便。
对于开发者而言,类似的痛点同样存在。假设需要开发一款电脑K歌应用,需要同时录制用户的干声(麦克风输入的外界声音)和来自播放器程序的伴奏音乐。在传统音频核心实现下,几乎很难做到,常规方法只能录制系统混音,即所有声音混合后的输出,无法单独分离出伴奏播放器(如QQ音乐、网易云)的纯净音频流。开发者往往不得不依赖虚拟音频路由、进程注入拦截等复杂方案,既增加了用户的使用门槛,又引入了不稳定因素。
这就好比说是某个屏幕截图软件,它只能截取整个屏幕内容,却无法单独选中某个窗口区域。传统的系统内录就相当于说是“全屏截图”,一股脑地将所有正在播放的声音混合成一个整体,用户无法从中单独分离出特定进程的音频。而我们所实现的进程级音频捕获,则相当于实现了“窗口截图”式的精准操作,能指定某个应用程序,单独抓取其发出的声音流,从而真正实现音频层面的精准隔离。
以上所有场景,都指向了一个核心问题:进程级音频流的精确隔离捕获。
过去,最常见的一个办法就是使用虚拟音频设备(如 VB-CABLE、Voicemeeter 等)解决这类问题。虚拟音频驱动软件通过在电脑上创建虚拟的输入输出通道(vioc),可以让用户手动“串联”不同应用的音频流,确实能在一定程度上实现声音的分离。但这种方式通常需要用户理解相对复杂的音频路由概念,手动进行设置,并且增加的软件层也可能引入一些难以排查的延迟或稳定性问题。
转折大约出现在 Windows10 2004((build 19041))之后。Microsoft首次在底层音频架构 WASAPI 中开放了 进程级音频环回捕获 的原生支持。这也就是说,开发者终于可以直接请求操作系统捕获特定进程的音频流,不再需要依赖第三方工具“绕路”实现。
本项目即是基于这一系统能力构建的一个轻量级命令行程序。旨在为用户提供一条新思路,利用 Windows 原生音频会话接口实现快速、精准地捕获特定应用程序的音频输出。
更重要的是,该功能还有一个极具价值的应用方向:为 AI 语音识别提供高质量输入。诸如 OpenAI Whisper 等语音转文字模型对输入音频的信噪比非常敏感。借助本项目演示的方案,通过进程排除或包含模式隔离音频流,用户可以轻松滤除奇他应用发出的背景音乐、无关人声和系统通知等噪声干扰,为识别引擎输送更为纯净的音频流,从而显著提升实时字幕、会议纪要等应用的准确性与可靠性。这一切都无需额外硬件或复杂的软件配置,全部在系统层面原生完成,性能极佳。
本程序是一个简单的命令行内录工具,其核心功能围绕不同层级的音频环回捕获展开:
- 全局回环录制:支持捕获系统当前默认音频输出设备上的所有声音,效果等同于传统的“立体声混音”功能,不依赖硬件设备支持。
- 进程包含录制:支持以进程 ID (PID) 为目标,仅录制该特定进程及其所有子进程所发出的声音。
- 进程排除录制:同样支持以进程 ID (PID) 为目标,录制除该特定进程及其子进程之外的所有系统声音。
- 标准格式输出:所有捕获的音频均保存成原始的 PCM
.wav
文件(格式:44.1kHz 采样率, 16位深度, 立体声双声道)。 - 原生轻量:程序直接调用 Windows 系统原生 API,无需安装任何第三方驱动或依赖库,本身为绿色单文件,开销很低。
- 异步低耗:基于异步事件模型构建,在录制过程中对 CPU 的占用非常低,不会对系统性能造成干扰。
在使用“进程包含”(mode 1)或“进程排除”(mode 2)模式前,需要先获知目标程序的进程 ID (PID)。这是一个动态的数字,每次程序启动时都可能不同。您可以通过以下步骤在 Windows 任务管理器中找到:
- 按快捷键
Ctrl + Shift + Esc
,打开任务管理器。 - 如果任务管理器显示为简化视图,点击左下角的“详细信息”展开。
- 切换到“详细信息”选项卡。
- 在“名称”一列中,找到想要录制或排除的应用程序的进程名,例如
chrome.exe
(谷歌浏览器),msedge.exe
(Edge浏览器),wmplayer.exe
(Windows Media Player) 等。 - 在其旁边,“PID”一列中显示的数字,就是您需要的进程 ID。
本程序通过以下三个命令行参数进行控制:
--pid <PID>
: 指定目标进程的 ID。此参数在模式1
和2
中为必需参数。--mode <MODE>
: 设置录制模式。此参数必需。0
: 全局环回模式1
: 进程包含模式2
: 进程排除模式
--path <FILEPATH>
: 指定输出的.wav
文件的完整路径和文件名。此参数必需。
模式一:全局录制 (录制系统所有声音)
- 场景:您希望录制当前电脑播放的所有声音,并将其保存到
D:\
盘,命名为system_audio.wav
。 - 命令:
ProcessAudioRecorder.exe --mode 0 --path D:\system_audio.wav
模式二:进程包含模式 (仅录制指定程序的声音)
- 场景:您正在用 Chrome 浏览器观看一个在线视频,并希望只录制浏览器的声音。通过任务管理器查到
chrome.exe
的 PID 是8888
。您希望将录音保存到D:\
盘,命名为chrome_audio.wav
。 - 命令:
ProcessAudioRecorder.exe --pid 8888 --mode 1 --path D:\chrome_audio.wav
模式三:进程排除模式 (录制除指定程序外的所有声音)
- 场景:您正在参加一个重要的在线会议,同时后台的音乐软件(假设其 PID 为
9999
)在播放音乐。您希望只录制会议以及奇他进程的声音,而完全排除音乐声。 - 命令:
ProcessAudioRecorder.exe --pid 9999 --mode 2 --path D:\meeting_audio.wav
当命令执行成功后,程序将开始录音,您将在控制台窗口看到类似于如下实时状态信息:
● Recording [00:00:15] - Press Ctrl+C to stop
这会动态显示当前的已录制时长。
需要停止录制时,请确保输入焦点停留于控制台窗口,然后按快捷键 Ctrl + C
。程序会立刻响应,并优雅停止音频捕获工作,将所有缓冲数据写入文件,修复 WAV 文件头信息,然后自动退出。文件将完整地保存在您指定的路径。
本程序的核心代码都集中在 CLoopbackCapture
类中。在编写过程中,主要围绕几个关键问题进行设计。下面,分享一下这些思考过程和具体的技术实现细节。
最初的一个想法是,能否在一个简单的循环里完成所有事情:检查是否有音频数据,有就抓取,然后直接写入文件。但这立刻被证明了是行不通的。
问题在于,音频捕获是一个对时间极其敏感的任务。Windows 音频引擎会以固定的时间间隔(例如每10毫秒)准备好一批新的音频数据。程序必须在这个极短的时间窗口内响应,并将数据从缓冲区中取走。任何微小的延迟,比如几十毫秒,都可能导致下个数据包到来时,缓冲区还未被清空,从而发生数据覆盖,也就是常说的“爆音”/“丢帧”。
而文件写入操作,本质上是一个 I/O 操作。它的完成时间受很多不可控因素影响:硬盘是机械的还是固态的、当前系统 I/O 是否繁忙、是否有杀毒软件在扫描等。它的耗时可能从几毫秒到几百毫秒不等,非常不稳定。
如果将这两个任务放在同一个线程里,就等于让一个需要百米冲刺的运动员,背上一个重量不定的沙袋。结果可想而知。
因此,架构上的第一个关键决策就是必须将这两个任务彻底分开。本程序采用了一种常见的任务队列分工思路:
-
设立一个“生产者”线程:这个角色由 Windows Media Foundation (WMF) 的回调线程担任。它的职责被设计得极其单一和纯粹:一旦被系统唤醒,就以最快的速度通过
m_AudioCaptureClient->GetBuffer()
从音频引擎的共享内存中抓取数据,然后立即将数据块(一个std::vector<BYTE>
)移动到内存中的一个公共队列m_AudioQueue
里。完成这个动作后,它的任务就结束了,立刻“让位”,等待下一次唤醒。整个过程都在内存中进行,速度飞快。 -
设立一个“消费者”线程:这是在
OnStartCapture
函数中手动创建的一个标准 C++ 线程 (std::thread
),它执行WriterThreadProc
函数。这个线程的节奏可以很“从容”。它唯一的工作就是检查m_AudioQueue
队列里是否有“待处理”的数据。如果有,就取出来,然后不慌不忙地调用WriteFile
将其写入磁盘。即使这次写入耗时较长,也完全不会阻塞到正在高速运行的“生产者”。
这两者之间通过 m_AudioQueue
这个队列和 m_QueueMutex
互斥锁进行数据交换和信号同步,实现了完美的时空解耦,从根本上保证了音频捕获的流畅和数据的完整性。
实现进程隔离的魔法,并非来源于什么复杂的实时音频包分析技术,而是得益于找到了一个正确的系统 API 实现。对于全局录制,我们使用的是 WASAPI
传统的 IMMDevice::Activate
来获取默认音频设备的 IAudioClient
接口。但要实现进程隔离,就必须用到 ActivateAudioInterfaceAsync
这个函数。
这个函数的核心在于它的第三个参数,一个 PROPVARIANT
结构。调用者需要在这里传入一个 AUDIOCLIENT_ACTIVATION_PARAMS
结构体。其中的两个字段是关键:
ActivationType
必须被设置为AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK
。ProcessLoopbackParams.TargetProcessId
则被设置为我们想要捕获的目标进程 PID。
当我们带着这样的参数调用 ActivateAudioInterfaceAsync
时,我们实际上是在向 Windows 音频子系统(AudioSrv.dll
)发出一个特殊的请求。系统在收到这个请求后,不会返回给我们一个代表物理设备(如扬声器)的音频端点,而是在内核层为我们动态构建一个虚拟的、只与目标进程的音频流相关联的捕获端点。
后续我们从这个端点通过 IAudioCaptureClient
抓取到的所有数据,都天然是经过系统“过滤”后的纯净音频流。这种由操作系统层面提供的原生支持,其效率和稳定性远非应用层的任何方案所能比拟的。实话讲,当我第一次成功调用并捕获到纯净的进程音频时,确实有种找到了“秘密通道”的兴奋感。
在确定了数据获取的方式后,下一个问题是:我们该如何知道“何时”去获取数据?
一个最朴素的想法是启动一个循环,在循环里不断调用 IAudioCaptureClient::GetNextPacketSize()
检查有没有新数据,中间再加个短暂的 Sleep
防止 CPU 占用过高。这种“忙等待”或轮询的方式,虽然简单,但弊端很明显:Sleep
的时长难以把握,设长了会增加延迟,设短了又浪费 CPU 资源。
幸运的是,WASAPI 提供了更为优雅的事件驱动模型。我们在 ActivateCompleted
和 ActivateAudioInterfaceGlobal
中进行了如下设置:
- 首先,通过
m_AudioClient->SetEventHandle(m_SampleReadyEvent.get())
,我们将一个自己创建的内核事件句柄 (m_SampleReadyEvent
) 告知给了音频客户端。这样,每当音频引擎准备好一批新数据时,它就会自动去触发(Set)这个事件。 - 接着,我们利用 Media Foundation 的
MFPutWaitingWorkItem
函数。这个函数像一个智能的调度器,我们告诉它:“请帮我监视m_SampleReadyEvent
这个事件。一旦它被触发,就请从你的线程池里派一个线程,去执行我的OnSampleReady
回调函数。” - 最关键的一步是,在
OnSampleReady
函数处理完数据后,只要程序还处于捕获状态,我们就会在函数的末尾再次调用MFPutWaitingWorkItem
,相当于重新为下一次事件提交了一个“监听续订”请求。
这就构成了一个完美的闭环。在没有音频数据的大部分时间,程序没有任何活动的捕获线程,相关的 CPU 占用几乎为零。只有当硬件中断通知 CPU 有新数据、音频引擎处理完并触发事件后,代码才会被唤醒执行。这是一种很高效的异步模式。
和 Windows 底层 API 打交道,尤其是 Media Foundation 这样的核心 API,就免不了要和 COM打交道。这就表示需要实现大量的回调接口,比如 IActivateAudioInterfaceCompletionHandler
和 IMFAsyncCallback
。教科书式的代码,是让你对每一个接口都需要一个独立的 class
,并完整实现 IUnknown
的三个方法:QueryInterface
, AddRef
, Release
。项目中至少需要三个这样的回调对象,如果都这么写,会产生大量功能相同、仅仅是名字不同的样板代码。
我个人非常讨厌写这类重复的东西,我想你也是。为了让主逻辑 CLoopbackCapture
类更清爽,我借鉴了一些常见的 C++ 工程技巧,在 common.h
中定义了 METHODASYNCCALLBACK
这个宏。
它不是简单的文本替换。当我们在 CLoopbackCapture
类中使用 METHODASYNCCALLBACK(CLoopbackCapture, SampleReady, OnSampleReady)
时,预处理器会展开并在 CLoopbackCapture
内部定义一个名为 CallbackSampleReady
的嵌套类。这个嵌套类继承自 IMFAsyncCallback
并实现了它的所有方法。
这里的关键技巧在于 offsetof
宏。在嵌套类的构造函数和方法中,我们可以通过 this
指针拿到当前嵌套类对象的地址。由于它是在 CLoopbackCapture
内部声明的成员变量,所以它的地址与外层 CLoopbackCapture
对象的地址之间有一个固定的、可在编译期计算出来的偏移量。通过 (Parent*)((BYTE*)this - offsetof(Parent, m_x##AsyncCallback))
这行代码,我们就能安全地反向计算出父对象的指针,从而将回调的 Invoke
调用完美地委托给父类的 OnSampleReady
成员函数。
这套操作下来,我们就在主类里用一行宏,代替了几十行重复的 COM 实现代码,并且保证了类型安全和高性能。
在消费者线程 WriterThreadProc
中,当发现音频队列 m_AudioQueue
为空时,则需要等待。如果用一个 while(m_AudioQueue.empty()) {}
这样的循环来等待,无疑会把一个 CPU 核心跑到100%。
这里我们使用了 C++11 引入的 std::condition_variable
来实现高效的等待。代码中的 m_QueueCV.wait(lock, ...)
会让消费者线程进入休眠状态,并原子性地释放掉它持有的互斥锁 lock
,允许生产者线程进入临界区。
值得一提的是 wait
函数的第二个参数,那个 lambda 表达式:[this] { return !m_AudioQueue.empty() || !m_bIsCapturing; }
。这不仅仅是一个唤醒条件。熟悉多线程编程的朋友可能知道,条件变量存在“虚假唤醒”(spurious wakeups)的可能,即线程有时会在没有被 notify
的情况下意外唤醒。如果 wait
函数没有这个 lambda 谓词,线程被虚假唤醒后会直接往下执行,此时队列可能仍是空的,从而引发错误。而有了这个 lambda,线程每次被唤醒(无论是正常还是虚假唤醒),都会重新检查这个条件。只有当条件为真(队列不为空,或者捕获已停止)时,wait
函数才会返回。这是一个保证并发代码正确性的关键细节。
最后,想谈谈代码的健壮性。在用 C++ 编写 Windows 程序时,最头疼的问题之一就是资源管理。COM 接口指针需要 Release
,内核句柄(HANDLE
)需要 CloseHandle
。在复杂的函数中,如果有多条 return
路径,或者代码可能抛出异常,忘记在某个分支释放资源是家常便饭,这也是很多程序长时间运行后变得不稳定的根源。
为了彻底解决这个问题,本项目全面采用了微软官方的 Windows Implementation Library (WIL)。这是一个纯头文件的库,它提供了一系列遵循 RAII (Resource Acquisition Is Initialization) 理念的智能封装类。
在代码中,会看到大量的类似于:
wil::com_ptr_nothrow<IAudioClient> m_AudioClient;
:代替裸的IAudioClient*
指针。当m_AudioClient
对象被销毁时,它的析构函数会自动调用Release()
。wil::unique_hfile m_hFile;
:代替HANDLE
。当m_hFile
对象离开作用域时,它的析构函数会自动调用CloseHandle()
。wil::unique_event_nothrow m_SampleReadyEvent;
:同理,用于管理事件句柄。
全面使用 WIL 后,我们就不再需要在代码中手动编写任何资源释放的语句。C++ 的作用域和析构机制保证了无论函数如何退出,所有被管理的资源都将被正确、及时地释放。这让代码逻辑变得异常清晰,也从根本上杜绝了资源泄漏的可能性,让我们可以更专注于实现核心功能本身。坦白说,这极大地提升了编写底层系统代码的幸福感。
本项目使用 Visual Studio 2022 构建,并依赖标准的 C++ 工具链和 Windows SDK。
- IDE: Visual Studio 2022
- 工作负载: 需安装 "使用 C++ 的桌面开发" (Desktop development with C++) 工作负载。
- Windows SDK: 版本 10.0.19041.0 或更高版本。
-
克隆仓库
git clone https://github.com/CingZeoi/AudioLoopbackRecorder.git
-
打开解决方案 使用 Visual Studio 2022 打开仓库根目录下的
ProcessAudioRecorder.sln
。 -
还原 NuGet 包 项目依赖
Microsoft.Windows.ImplementationLibrary
(WIL),通过 NuGet 进行管理。在解决方案资源管理器中,右键点击解决方案ProcessAudioRecorder
,选择 "还原 NuGet 程序包" (Restore NuGet Packages)。Visual Studio 会自动下载并配置所需的依赖项。 -
生成项目
- 在 Visual Studio 顶部的工具栏中,将解决方案配置选择为 "Release"。
- 将解决方案平台选择为 "x64"。
- 从主菜单中选择 "生成" (Build) -> "生成解决方案" (Build Solution),或直接按快捷键
F7
。
本项目最初的目的,是验证 Windows 新音频接口在解决特定场景下的可行性,因此当前的实现更像是一个功能性的“原型演示程序”,旨在提供一条新的实现思路,而非一个面面俱到的成熟产品。由于个人时间和精力所限,很多可以深入挖掘的方向都只是浅尝辄止。
进程级音频捕获这个需求确实相对小众,但它恰恰能解决一些特定人群和场景下的“刚需”痛点。本程序将核心的捕获流程打通并稳定下来,但它依然是块有待雕琢的璞玉。如果你对这个方向感兴趣,以下是一些我认为很有价值,但尚未着手去做的改进思路:
-
输出格式的扩展 当前程序仅支持输出未经压缩的 WAV 文件。这是最直接、最能保证音质的选择,但也导致了文件体积较大。一个显而易见的改进是引入音频编码库。例如,集成
lame
库来支持输出 MP3 格式,或者集成libopus
库来支持更现代、更高压缩率的 Opus 格式。这无疑会大大提升程序的实用性。 -
交互方式的优化 命令行界面对于开发者而言是高效的,但对于普通用户来说,每次都需要打开任务管理器查找 PID 并输入命令,操作上确实不够便捷。如果能为其开发一个简单的图形界面,将会极大地降低使用门槛。比如,一个能列出当前所有可播放音频进程的列表,用户只需点击一下就能开始或停止录制,这将是体验上的巨大飞跃。
-
核心逻辑的模块化封装 目前,所有的捕获逻辑都封装在
CLoopbackCapture
这个 C++ 类中。它的设计初衷就是为了高内聚,使其具备良好的独立性。理论上,我们可以将这个类稍作重构,编译成一个动态链接库(DLL),并为其导出一套稳定的 C 语言风格的 API 接口。如果能实现这一点,ProcessAudioRecorder
将不再仅仅是一个工具,而是一个可被复用的“音频捕获引擎”。届时,无论是 Python、C#、Rust 还是其它任何支持 FFI 的语言,都可以轻松地集成进程音频捕获的能力,或许能催生出一些更有趣的应用。
本程序在这方面只迈出了第一步。我把它开源出来,希望能为遇到类似问题的朋友提供一个可行的思路和参考实现。如果你有新的想法,或者在某个方向上有更深入的理解,非常欢迎 Issue 讨论,或者直接 Pull Request。期待和你一起,让它变得更完善。
首先,感谢微软的工程师们,为 Windows 平台提供了如此强大且设计精良的进程级音频捕获接口,它是一切实现的基础。
同时,也要感谢 Windows Implementation Library (WIL) 库的开发团队,这个优秀的库让使用 C++ 进行现代、安全的 Windows 系统编程变得更加高效和愉悦。
开发探索过程中,我也从网络上众多开发者分享的关于 WASAPI 和 Media Foundation 的技术文章、文档和社区讨论中获益良多,在此一并表示感谢。
Copyright (c) 2025 CingZeoi(Cirong.Zhang). All rights reserved.
本项目采用 MIT 许可协议 授权。
特此授予任何获得本软件及其相关文档文件(以下简称“软件”)副本的人士免费的许可,可以无限制地处理该软件,包括但不限于使用、复制、修改、合并、出版、分发、再许可和/或销售软件的副本,并允许获得软件的人士在下述条件下这样做:
上述版权声明和本许可声明应包含在软件的所有副本或重要部分中。
本软件的初衷是作为一个技术工具,用于解决合法的、经过授权的音频处理与录制需求。
禁止将本软件用于任何违反当地、国家或国际法律的用途。 包括但不限于,在未经所有相关方明确同意的情况下,擅自录制、截取或收集私密音频对话或数据。音频录制在许多司法管辖区都受到严格的隐私法规管辖。
本软件的使用者应自行承担全部责任,确保其使用行为符合所有适用的隐私保护和数据安全法律。作者不为任何滥用本软件的行为或因此类滥用而产生的任何法律后果承担任何责任。请在尊重他人隐私和合法权益的前提下,负责任地使用本工具。
本软件按“原样”提供,不作任何明示或暗示的保证,包括但不限于对适销性、特定用途适用性和非侵权性的保证。在任何情况下,作者或版权持有人均不对任何索赔、损害或其它责任承担任何责任,无论是在合同、侵权或其他行为中,无论是由于软件本身、使用软件或与软件相关的其他交易所引起的。
在开发过程中,以下来自微软官方的文档提供了关键性的指导和参考:
-
- 文档详细介绍了使用 WASAPI 捕获音频流的基础流程和核心概念。
-
- 该文档阐述了如何进行音频回环录制,是实现“立体声混音”功能的理论基础。
张赐荣 ,视障人士,信息无障碍推动者。
多年来,深耕于Web/PC/移动端可访问性的研究与实践工作,对跨平台无障碍解决方案拥有深刻的独特理论和丰富的实战经验。
精通视障用户软件体验设计,致力于用专业的能力改善、提升产品可及性体验。