简介:这是基于微软虚拟音频设备(MSVAD)示例改造的虚拟声卡驱动学习源码包,面向 Windows 驱动开发者和音频编程入门者,帮助理解虚拟音频设备从注册、启动到数据流转的完整工作路径。包内共 91 个文件,以 42 个头文件与 22 个源文件为核心,另有 7 个编译脚本、7 个构建配置、8 个说明文档及 1 个安装配置文件,能够完整还原驱动框架下的编译链接流程与设备安装环节;头文件声明接口与数据结构,源文件实现音频逻辑,脚本则展示项目编译方式。代码按多种音频场景划分模块,分别演示多声道样本管理、数字版权保护、音频捕获、格式扩展和音频渲染等实现,覆盖实时音频流处理、接口封装、设备模拟等关键逻辑;同时涉及 Windows 音频 API 的调用方式、多线程回调节奏以及信号处理滤波等内容。压缩包仅 132KB,结构紧凑、轻量易读,配合说明文档与内联注释可快速定位关键函数。已有 829 人学习下载,适合想深入 Windows 音频驱动开发与虚拟声卡原理的读者作为练习素材。
1. virtual audio 虚拟声卡:先把系统的声音管道拆开再重组
很多人第一次接触 virtual audio 虚拟声卡,是想把某个程序的声音“送回”另一个程序,比如把游戏声音当作麦克风输入到会议软件里。结果装完驱动才发现,默认配置下根本没声音,要么爆音,要么声卡设备根本不出现在列表里。虚拟声卡并不是简单“内录”,它的本质是把音频管线拆成两个虚拟端点——播放端和捕获端,让系统里的任意两个程序通过这条虚拟管道互相连通。这篇文章从内核音频架构讲到路由矩阵、延迟参数和踩坑点,适合想把声音路由玩明白的开发者、直播调音师和音频测试工程师。
2. 虚拟声卡的内核逻辑:环回设备、采样率与缓冲区的三角关系
2.1 虚拟设备到底在哪一层工作:从内核音频栈往下看
传统声卡链路是:应用 → 音频服务 → 声卡驱动 → 物理硬件 → 扬声器。虚拟声卡在音频服务和驱动之间插入了一个软件定义的端点。它没有物理 DAC,只有一块内存缓冲区。应用把 PCM 数据写入播放端,驱动层原样把它转发到捕获端,这样另一端程序就能像读取麦克风一样读到这段声音。
以 Windows 为例,虚拟声卡往往注册成一个 KS(内核流)或 WDM 音频设备。打开设备管理器就能看到多出一个“音频输入和输出”条目,里面包含两个逻辑端点。系统音频服务会认为这是一个真实的声卡,任何应用都可以把它选为默认播放或默认录音设备。
Linux 下的实现路径不一样,多数发行版默认走 PulseAudio 或 PipeWire 的音源节点抽象层,虚拟环回设备以音源节点(node)的方式注册,通过模块调度实现数据回流。
我这几年在几个项目里都遇到过一个现象:只要后面接的环节多了,延迟就从 10ms 涨到 80ms 以上,声音听起来像有回声。根源往往不是驱动本身,而是链路里某一层悄悄做了格式转换,比如把 48000Hz 转成 44100Hz,转换过程引入了额外延迟。
2.2 关键配置参数:采样率、缓冲块、共享模式
动手配置前,先把三个参数理解透,否则后面会遇到一堆玄学问题。
第一是采样率。所有端点的采样率必须保持一致,或明确走转换通道。我一般建议首选 48000Hz。因为绝大多数流媒体、游戏引擎和语音软件的原生采样率就是 48000Hz,统一后绕开 SRC(采样率转换),转换会引入高频衰减和额外 CPU 开销。如果你只是做基础通话录音,44100Hz 也能用,但注意别在两端混用不同采样率。
第二是缓冲区大小(buffer size)。它决定驱动一次向应用投递多少帧音频数据。256 帧在 48000Hz 下约 5.33ms 延迟,512 帧约 10.67ms。缓冲区越小延迟越低,但 CPU 一旦抖动就会爆音。稳定性和低延迟之间要找一个平衡点,不是越低越好。
第三是共享模式。Windows 音频服务默认允许多个应用同时播放,系统会做混音。虚拟声卡驱动通常也提供一个“独占模式”开关。当某个应用以独占方式打开设备时,其他应用就无法访问该设备,这既是功能也是坑。
Windows 下可以用 PowerShell 快速查看当前设备的采样率支持情况。这是一个我常用的检查脚本:
$devices = Get-PnpDevice -Class 'AudioEndpoint' -Status 'OK' | Select-Object FriendlyName, InstanceId, Status | Format-Table -AutoSize Write-Output $devices # 若想查看某个端点支持的采样率,需要打开系统声音控制面板, # 找到对应设备 -> 属性 -> 高级这段脚本的作用是枚举当前系统里所有可用的音频端点,并把设备友好名称和实例 ID 列出来。实际运维中,我在配置虚拟声卡之前先跑一遍它,确认设备是否已经被系统识别,如果识别不到,后面一切设置都无从谈起。注意,Get-PnpDevice的-Class 'AudioEndpoint'只对 Windows 10 以上版本有效,系统版本不同返回结果会有细微差别。
至于缓冲区大小,这类参数一般不在脚本里改,而是去虚拟声卡驱动的控制面板里调整。控制面板一般会提供 128、256、512、1024 四档,按你的用途选:
| 使用场景 | 推荐缓冲区 | 预期延迟 | 风险 |
|---|---|---|---|
| 直播监听 | 128~256 | 2.7~5.3ms | 易爆音 |
| 普通路由 | 256~512 | 5.3~10.7ms | 稳定 |
| 长时间录制 | 512~1024 | 10.7~21ms | 稳定有余 |
3. 把路由矩阵搭出来:两个虚拟设备打通一个完整环路的压测
3.1 路由矩阵的组成:虚拟播放、虚拟录音、桥接节点
单一虚拟设备只能完成一对一的回环。实际场景里,比如我想同时把游戏声音、系统提示音、语音分别送到三个不同目标,就需要多个虚拟设备组合成矩阵。
路由矩阵至少包含三类节点:虚拟播放节点,作为某个应用的输出目标;虚拟录音节点,作为另一应用的输入来源;桥接节点,负责把播放端的数据复制到录音端。这个“复制”就是虚拟声卡驱动的核心工作,和物理声卡不同,它不需要真正的回采,只需要内存拷贝。
在 Linux 下,PipeWire 提供 loopback 模块来创建这种桥接。下面是一个我自己常用的配置文件:
# ~/.config/pipewire/pipewire.conf.d/virtual-loopback.conf context.modules = [ { name = libpipewire-module-loopback args = { node.name = "virtual_speaker" capture.props = { node.description = "虚拟回路播放端" media.class = "Audio/Sink" } playback.props = { node.description = "虚拟回路录音端" node.target = "" } } } ]这个配置声明了一条虚拟回路:播放端接收应用写入的数据,录音端把同一份数据处理成可用于采集的音频源。其中node.name是内部名称,供其他模块引用;node.description是显示名,会出现在音量控制界面里;capture.props中的media.class字段决定节点暴露给系统的角色。
不同发行版的 PipeWire 配置路径可能不一样,我应该说明一下:多数桌面发行版读取~/.config/pipewire/pipewire.conf.d/下的配置,但如果你用的是最小化系统,可能需要在/etc/pipewire/pipewire.conf.d/下配置。配置加载后,跑pactl list sources和pactl list sinks,能分别看到“虚拟回路录音端”和“虚拟回路播放端”两个节点。
3.2 动手搭链路:让两个程序通过路由矩阵通话
现在做一个最容易验证的实验:把某播放器的声音,通过虚拟声卡转给某录音应用。
第一步,把播放器的输出设备指定为“虚拟回路播放端”。大多数播放器在设置里都能直接选设备。第二步,把录音应用的输入设备指定为“虚拟回路录音端”。第三步,播放一段已知内容,录音应用里应该能看到输入电平跳动。
如果没声音,先看设备选择是否生效。有些程序会缓存设备列表,修改后不立即生效,重启应用往往能解决。
不过我习惯写一个统一的脚本,用来快速切换所有音频应用的后端设备。借助 PulseAudio 的wpctl或pactl,可以批处理:
#!/usr/bin/env bash # set-route.sh 将默认回环设备切换到虚拟声卡路由状态 SINK_ID=$(wpctl inspect @DEFAULT_AUDIO_SINK@ | grep -i "id:" | awk '{print $2}') SOURCE_ID=$(wpctl inspect @DEFAULT_AUDIO_SOURCE@ | grep -i "id:" | awk '{print $2}') wpctl set-default $SINK_ID wpctl set-default $SOURCE_ID echo "默认输出设备与输入设备已切换到虚拟声卡回路"这段脚本本身只是把系统默认输入输出重新指向当前系统记录的设备。关键点在于:脚本里的“默认设备”实际上就是你刚配置的虚拟端点。wpctl inspect用来查询设备 ID,wpctl set-default用来切换默认设备。如果 PipeWire 没启动或未安装 wpctl,脚本会卡在第一步,可以用pactl info看一下当前服务状态。
这套路由矩阵相当于在系统里修了一条双向二车道,任何程序只要愿意走这条路,都能变成对方的输入源。注意,这里只是建立了通路,采样率不一致的问题会在链路里造成音调漂移,这是下一章要重点避开的坑。
4. 避坑排查:真实使用中最常见的五个翻车现场
4.1 现象:爆音噼啪、声音卡顿乱跳
风扇一转,声音就开始咔咔响。原因绝大多数是缓冲区太小或 CPU 波动导致音频缓冲欠载,驱动没有足够数据可播,只能重复或插入空白。
解决方式是先把缓冲区调大一档,比如 256 帧调到 512 帧,观察一两分钟。如果还爆音,就排查后台是否有大型编译任务或杀毒扫描。我还有一个经验:别把虚拟声卡设备放在移动硬盘盒上,这类设备极容易因 USB 总线休眠而掉链接。就近放在主板直出端口上,能省很多事。
4.2 现象:反回输出来回录,形成尖锐的回声
某个应用打开了“监听此设备”功能,同时又把虚拟声卡作为录音源,数据被无限次循环拷贝,越积越多。
解决方式是检查所有相关应用里是否有监听(listen to this device)选项,一律关闭。虚拟声卡驱动自己不产生数据,它只是传递数据,所有“奇妙声音”都来自应用层的循环。
4.3 现象:录出来的声音音调变低,或播放速度明显不对
这种坑最容易出现在同时打开采样率不同的程序时。比如播放器 44100Hz,录音程序 48000Hz,驱动自动转换但算法质量差,导致音调偏移。
解决方式是强制把虚拟声卡设备采样率固定为 48000Hz,并检查所有相关应用,尽可能统一。在 Windows 上,可以打开声音控制面板,把设备属性里的默认采样率设为 48000Hz,同时关闭“启用音频增强”选项,它能干扰原始数据。
4.4 现象:驱动装好了,但设备管理器里看不到新设备
原因可能是旧驱动残留导致注册表冲突,或者安装时没有管理员权限,设备驱动服务没有被正确写入。
解决方式是重装驱动。注意先卸载干净,再重启,再安装。如果还不行,打开设备管理器,点击“操作 - 添加过时硬件”,手动指向驱动文件夹。装上后如果还是挂着感叹号,去系统日志找加载失败的具体原因——经常是“未签名驱动”在安全启动模式下被拦截。可以在固件设置里暂时关闭安全启动验证签名,或者用测试签名模式加载。
4.5 现象:延迟越来越大,半小时后声音和画面不同步
这可能不是驱动老化,而是驱动内存缓冲区泄漏。长时间挂机后,缓冲区占用持续增长,导致数据链路延迟变大。
解决方法是先重启应用进程,不行就重新插拔虚拟声卡设备,在设备管理器里禁用再启用。如果问题反复出现,建议给程序加定期重启机制。我在直播场景里就让推流软件每 4 小时自动重连一次音频设备,再没出现过长时间挂机延迟漂移。
5. 验证链路是否真正连通:环路测试与频谱校验
5.1 环路闭环测试:播放一套标准测试音,录回来比对
安装完虚拟声卡,第一步要做的不是直接录语音,而是做一次环路自检。生成一段已知频率和弦的信号,让它走虚拟声卡跑一圈,录回来检查一致性。
下面我用 Python 生成一个 1kHz 标准正弦波,再用录音库同时采集输入,最后计算信号相关性:
import numpy as np import sounddevice as sd import time SAMPLE_RATE = 48000 DURATION = 3 FREQ = 1000 t = np.arange(int(SAMPLE_RATE * DURATION)) / SAMPLE_RATE signal = 0.5 * np.sin(2 * np.pi * FREQ * t) # 播放的同时录音:播放端和录音端都指向虚拟声卡设备 recorded = sd.playrec(signal, samplerate=SAMPLE_RATE, channels=1, dtype='float32', input_channels=1) sd.wait() recorded = recorded.flatten() start_idx = int(0.5 * SAMPLE_RATE) corr = np.correlate(signal[start_idx:], recorded[start_idx:], mode='valid') peak = np.argmax(corr) / SAMPLE_RATE print(f"环路信号延迟约 {peak * 1000:.2f} ms")这里最关键的一步是sd.playrec,它要求录音和放音指向同一个虚拟设备才能做到同步起跑。corr计算的是输出信号和录音信号的互相关峰值位置,峰值出现的位置就是环回链路的总延迟。如果这个值超过 50ms,说明缓冲区设置得太保守。
5.2 频谱维度验证采样率转换质量:看谱峰、底噪、偏置
延迟没问题后,再用 FFT 看频率成分。虚拟声卡如果做砸了,频谱上通常会看到基频旁边多出不该有的次峰。
我通常录 5 秒测试音,然后做频谱分析:
import numpy as np import sounddevice as sd SAMPLE_RATE = 48000 DURATION = 5 rec = sd.rec(int(SAMPLE_RATE * DURATION), samplerate=SAMPLE_RATE, channels=1, dtype='float32') sd.wait() window = np.hanning(len(rec)) spectrum = np.fft.rfft(rec.flatten() * window) freqs = np.fft.rfftfreq(len(rec), 1/SAMPLE_RATE) amplitude = np.abs(spectrum) peak_freq = freqs[np.argmax(amplitude)] print(f"峰值频率: {peak_freq:.1f} Hz")运行后峰值频率应当在 1000Hz 附近。如果峰值落在 997Hz 或 1003Hz 附近,说明链路里确实发生了采样率漂移,需要回到驱动配置里统一采样率。如果 2000Hz、3000Hz 位置出现明显谱线,说明驱动实现有谐波失真,这时需要留意转换质量。
6. 再往前走一步:按应用定制的路由方案与自动切换习惯
虚拟声卡真正的价值不在手动切换,而在于把路由逻辑固化成脚本,让日常操作从“点十几个下拉菜单”变成一键完成。
我维护了一个“工作路由”方案:把直播伴侣的音频输出端接到虚拟声卡 A,把监听耳机的输入端接到虚拟声卡 B,然后让两条链路通过桥接节点连起来。整个方案固化成一个route-work.sh,一键完成切换:
#!/usr/bin/env bash # 切到工作路由 wpctl set-default $(wpctl inspect @DEFAULT_AUDIO_SINK@ | grep id) wpctl set-default $(wpctl inspect @DEFAULT_AUDIO_SOURCE@ | grep id)这个方案解决了以前最让人头疼的问题:切路由时要走进每个应用的声音设置里去改设备,来回要改七八个地方。现在只改默认设备就行,应用在下次启动时自动跟随系统默认值。
从那以后,我每次装完虚拟声卡驱动都强制走一遍环路延迟测试,跑一次频谱校验,才敢把新环境交给用户用。这套检查救回来不少项目。希望在配置 virtual audio 虚拟声卡时,你先顺着链路理一遍原理,再按这个顺序配置和验证,少走那些我说过的弯路。希望这些经验能帮到你。
本文还有配套的精品资源,点击获取