win10开发虚拟麦克风,应用程序与其通信注入音频数据的处理
在Windows上实现虚拟麦克风驱动的过程中,用户态应用程序需与驱动通信,以注入语音数据。记录了开发过程遇到的问题。
最近有个需求,windows上实现虚拟声卡驱动,要包括虚拟扬声器和虚拟麦克风。windows提供了几个不同的开发框架(微软提供了示例代码),说明如下:
1. msvad
优点:支持win7
缺点:进行win7的hck测试时,失败用例较多,不知道怎么修改
说明:hck是win7提供的测试套件,win7能用且获得签名的驱动,正常win10也能用
2. AVStream\avshws
优点:支持win7, hck通过机率大
缺点:开发难度大,示例代码是camera,不知道怎么改成声卡
3. sysvad(simple)
微软示例代码中sysvad目录下有多个子目录,其中有个simpleaudiosimple子目录仅实现了最简单的扬声器和麦克风,符合我的需求
优点:开发难度效,示例代码能通过hlk用例,获取证书签名
缺点:不支持win7
说明:hlk是win10提供的测试套件
==============================================================
进入正题:
1. 选定了sysvad(simple)后,经过简单的适配修改,驱动可在打开了测试模式的win10云桌面中进行安装。
2. 驱动示例代码中生成了一段正弦波的pcm数据,模拟麦克风硬件的输入。
3. 我的场景是云桌面中安装一个虚拟的麦克风设备,然后将客户机真实的麦克风语音数据发送到云桌面,云桌面中的应用程序将接收的语音数据注入到虚拟麦克风设备中。
至此我需要修改驱动代码,使得应用程序可以与驱动通信,向其中注入语音数据。要解决这个问题,想到的思路是为设备对象创建一个用户态可以访问symbolicLink, 用户态程序调CreateFile打开这个设备符号,拿到设备句柄,然后调 DeviceIoControl 与设备通信,向其传输数据。
真正实施时遇到了一些错误,在此做个整理与大家分享。
概括:sysvad(simple) 使用了微软的PortCls框架,其内部会创建一个设备对象,为该对象创建symbolicLink的思路是不对的。应该是调 IoCreateDevice 创建一个设备对象,该对象用于与应用程序通信,便于应用程序注入音频数据,PortCls框架内部创建的设备对象只用于处理音频相关的业务。
------------------------------------------------------------------------------------------------------------------
1. 最开始使用NT的接口创建symboliclink,但用户态打开文件失败,GetLastError()返回2。
说明:NT创建接口的关键字: IoCreateDevice ,IoCreateSymbolicLink
2. 怀疑不能用NT的接口,于是换用 wdm的接口, 还是一样的现象
说明:wdm创建接口的关键字: IoRegisterDeviceInterface,IoSetDeviceInterfaceState
3. 网上找答案,有个帖子提到,驱动需要挂IRP_MJ_CREATE 和 IRP_MJ_CLOSE 例程,例程中完成irp即可, 代码如下:
DriverEntry
(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPathName
)
{
...
DriverObject->MajorFunction[IRP_MJ_CREATE] = SimpleAudioSampleCreate;
...
}
NTSTATUS
SimpleAudioSampleCreate(
_In_ PDEVICE_OBJECT DeviceObject,
_In_ PIRP Irp
)
{
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
按上面修改后,CreateFile打开文件成功,但打开系统的“声音设置”界面,看不到虚拟声卡对应的扬声器和麦克风设备。
为什么会这样呢?
示例代码,在AddDevice函数中有调用
ntStatus =
PcAddAdapterDevice
(
DriverObject,
PhysicalDeviceObject,
PCPFNSTARTDEVICE(StartDevice),
maxObjects,
sizeof(DEVICE_CONTEXT) + PORT_CLASS_DEVICE_EXTENSION_SIZE
);
框架函数PcAddAdapterDevice内部会设置: DriverObject->MajorFunction[IRP_MJ_CREATE] = 框架内部的例程。
当应用程序 CreateFile时,会发送irp给PortCls框架内部的例程,这个流程不认识这个irp,返回相应错误,则表现为CreateFile返回2;
当我们自己挂载了IRP_MJ_CREATE例程且直接返回,会使得PortCls框架内部对应的例程没有被调用,少了一些操作,导致“声音设置”界面看不到虚拟声卡对应的扬声器和麦克风设备。
4. 网上说应该自己调 IoCreateDevice 和 IoCreateSymbolicLink 创建一个控制设备并在用户空间暴露符号连接。即一个驱动绑定两个设备对象,PortCls框架创建的设备对象处理与音频相关的业务,自己创建的设备对象,用于与私有的应用程序通信,注入音频数据。
此外 IRP_MJ_CREATE 例程需按如下修改:
NTSTATUS
SimpleAudioSampleCreate(
_In_ PDEVICE_OBJECT DeviceObject,
_In_ PIRP Irp
)
{
if (g_CtrlDeviceObj == DeviceObject) // g_CtrlDeviceObj 是调IoCreateDevice 返回的控制设备对象的指针
{
// 来自于对控制设备的create操作,irp返回success,使得CreateFile可以返回成功
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
else
{
// 来自于对音频设备的create操作,交给PortCls框架处理
return PcDispatchIrp(DeviceObject, Irp);
}
}
背后的逻辑:
自己调IoCreateDevice 创建的控制设备对象(用DeviceObject_ctrl表示)中会有如下关系:
DeviceObject_ctrl->DriverObject = DriverObject;
PortCls框架中创建的音频设备对象(用DeviceObject_portcls_audio表示)也有如下的关系:
DeviceObject_portcls_audio->DriverObject = DriverObject;
系统处理音频相关的请求时,会打开对应的interface(wdm创建的symbolicLink称为interface)与驱动通信,会发送请求给内核中的 DeviceObject_portcls_audio 对象,
当是Create请求时,则会进入IRP_MJ_CREATE例程,即SimpleAudioSampleCreate函数,此时第一个入参为DeviceObject_portcls_audio ,其值不等于 g_CtrlDeviceObj,则会将irp发给PortCls框架处理;
同理:应用程序打开 DeviceObject_ctrl 对应的symbolicLink时,也会进入 SimpleAudioSampleCreate 函数,此时第一个入参为DeviceObject_ctrl,其值等于 g_CtrlDeviceObj,则会将irp立即完成,使得应用程序成功打开设备文件。
补充:驱动中也有变量,用于指向关联的设备对象
DriverObject->DeviceObject
DeviceObject指向的是一个链表,DeviceObject_ctrl 和 DeviceObject_portcls_audio 都挂载在该链表中
出现了新的问题:
应用程序打开 DeviceObject_ctrl 对应的symbolicLink时,CreateFile文件返回失败,GetLastError()的值为433: ERROR_INVALID_ACCESS
5.继续问AI,得到的反馈是不能在AddDevice函数中创建控制设备对象,而应在DriverEntry中创建
//修改前的代码
DriverEntry
(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPathName
)
{
...
ntStatus = PcInitializeAdapterDriver(DriverObject,
RegistryPathName,
(PDRIVER_ADD_DEVICE)AddDevice);
}
NTSTATUS AddDevice
(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PDEVICE_OBJECT PhysicalDeviceObject
)
{
PAGED_CODE();
NTSTATUS ntStatus;
ULONG maxObjects;
DPF(D_TERSE, ("[AddDevice]"));
maxObjects = g_MaxMiniports;
// Tell the class driver to add the device.
//
ntStatus =
PcAddAdapterDevice
(
DriverObject,
PhysicalDeviceObject,
PCPFNSTARTDEVICE(StartDevice),
maxObjects,
sizeof(DEVICE_CONTEXT) + PORT_CLASS_DEVICE_EXTENSION_SIZE
);
if (!NT_SUCCESS(ntStatus)) {
DbgPrint("[hsraudio:AddDevice] AddDevice PcAddAdapterDevice fail\n");
return ntStatus;
}
PDEVICE_OBJECT fdo = DriverObject->DeviceObject;
PDEVICE_CONTEXT deviceCtx =
(PDEVICE_CONTEXT)((PUCHAR)fdo->DeviceExtension + PORT_CLASS_DEVICE_EXTENSION_SIZE);
deviceCtx->pRingBuffer = NULL;
// <== 开始是在这个位置调IoCreateDevice 和 IoCreateSymbolicLink 创建控制设备对应的设备对象以及在用户空间暴露符号连接的
return ntStatus;
} // AddDevice
=======================================================================
// 修改后的代码:
DriverEntry
(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPathName
)
{
UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\HsrVdiAudio");
UNICODE_STRING symName = RTL_CONSTANT_STRING(L"\\DosDevices\\HsrVdiAudio");
...
ntStatus = PcInitializeAdapterDriver(DriverObject,
RegistryPathName,
(PDRIVER_ADD_DEVICE)AddDevice);
...
// 改到 DriverEntry中创建了
// create control device and symbolicLink
ntStatus = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &g_CtrlDeviceObj);
IF_FAILED_ACTION_JUMP(
ntStatus,
DPF(D_ERROR, ("[hsraudio:DriverEntry] IoCreateDevice fail 0x%x", ntStatus)),
Done);
ntStatus = IoCreateSymbolicLink(&symName, &devName);
IF_FAILED_ACTION_JUMP(
ntStatus,
DPF(D_ERROR, ("[hsraudio:DriverEntry] IoCreateSymbolicLink fail 0x%x", ntStatus)),
Done);
}
IoCreateDevice ,IoCreateSymbolicLink 移到DriverEntry函数后,问题解决,系统“声音设置”中可以看到虚拟声卡对应的扬声器和麦克风设备;应用程序中也可以正常打开设备文件了。
附上应用程序相关打开代码:
#include <windows.h>
int main()
{
// 1. 打开设备接口
HANDLE hDev = CreateFileW(
L"\\\\.\\HsrVdiAudio",
GENERIC_WRITE, // 仅写权限
0,
NULL, // 默认安全
OPEN_EXISTING, // 必须已存在
FILE_ATTRIBUTE_NORMAL, // 普通文件属性
NULL
);
if (hDev == INVALID_HANDLE_VALUE) {
std::cerr << "打开设备失败,错误码3:" << GetLastError() << std::endl;
return 1;
}
...
}
为什么会这样呢,AI给出的解释如下:
AddDevice 是 Pnp 框架调用驱动处理“真实或虚拟硬件”的入口,此处应调用 PcAddAdapterDevice()、PcRegisterSubdevice(),创建 PortCls 所需的内部设备对象;
这里再创建额外设备(如你那样的控制设备)就变成了多余或错误注册,且此设备未参与 Plug-and-Play,行为异常,系统可能无法正确分发 IRP
看不懂,只能先这样理解了。
6. 其它注意点
DriverEntry
(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPathName
)
{
...
DriverObject->MajorFunction[IRP_MJ_PNP] = PnpHandler;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = SimpleAudioSampleDeviceControl;
DriverObject->MajorFunction[IRP_MJ_CREATE] = SimpleAudioSampleCreate;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = SimpleAudioSampleClose;
...
}
如前所述,驱动中挂载的例程是公共的,不同设备对象会进入相同的例程函数,函数中需要通过入参来判断是哪个设备对象产生的调用,进行不同的处理,举例说明:
PnpHandler 不需要做判断,因为控制设备对象不受PNP管理;
SimpleAudioSampleCreate 需要进行判断,上面已经提供代码。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)