0

我有 AutoHotkey 脚本,可以通过一次按键在多个声音设备之间切换。

一切正常,我正在使用 nircmd 实用程序激活设备(设置为默认设备)

Run, Tools\nircmd.exe setdefaultsounddevice "%playback%"%playback%实际的声音设备名称在哪里。

所以我的脚本基本上循环通过声音面板中的 3 个设备(耳机、扬声器、电视)。

但是,当我的电视关闭(断开连接)时,它仍会循环播放所有 3 台设备。

在此处输入图像描述

我需要的是能够检查设备是否在我的脚本中断开连接。

我在 nircmd 中找不到任何可以做到这一点的命令。

如果您有任何想法,请告诉我。

谢谢。

4

1 回答 1

1

当然非常可行,但请注意,此答案中的代码非常先进
这没有使用任何外部实用程序。

所以我们对接口的EnumAudioEndpoints方法IMMDeviceEnumerator感兴趣。
使用这种方法,我们可以根据某些标准列出所需的音频设备。

问题:
我们如何在 AHK 中使用这种方法?
通过DllCalling 它。为此,我们需要它在内存中的地址(指针),因为DllCall它能够通过地址调用函数/方法。


所以我们从获取IMMDeviceEnumerator接口的指针开始。
为此,我们需要它的CLSID,在这种情况下还需要它的IID。我通过谷歌搜索找到了它们。
然后我们使用 AHK 的ComObjCreate函数(为了使它更复杂,是的,我们正在使用ComObjects
并且正如 AHK 文档所指定的,我们确实从函数中获得了一个指针而不是一个对象,因为我们指定了一个独立身份证。

CLSID := "{BCDE0395-E52F-467C-8E3D-C4579291692E}"
IID := "{A95664D2-9614-4F35-A746-DE8DB63617E6}"
pDeviceEnumerator := ComObjCreate(CLSID, IID)

现在我们有了指向接口的指针pDeviceEnumerator,我们需要一个指向EnumAudioEndpoints接口方法的指针。
所以这是我们的下一个问题。

首先我们需要明白,我们想要的方法是接口的第一个方法。
但是因为接口继承自IUnknown接口的前三个方法实际上是AddRef,QueryInterfaceRelease.
所以,我们想要的接口方法其实就是接口的第四种方法。

好的,所以我们要获取指向该接口的第四个方法的指针。
为此,我们首先要获取指向接口的虚拟表的指针。vtable 包含每个方法的指针。在我们有了 vtable 的指针之后,我们可以从那个 vtable 中获取我们想要的方法的指针。

为了获得这些指针,我们将使用 AHK 的NumGet函数。
vtable 通常位于 ComObject 的开头(在偏移量 0 处),因此让NumGet我们pDeviceEnumerator在偏移量 0 处的指针指向 vtable:
vtable := NumGet(pDeviceEnumerator+0)

+0指定,因此 AHK 不会将该变量pDeviceEnumerator视为 ByRef 变量,而是在内存中我们想要的地址处操作。我们省略了第二个和第三个参数以使用默认值,偏移量 0(这正是我们想要的),而且 UPtr 类型对我们来说也很好。

现在我们有了 vtable 的内存地址,让我们最终获得EnumAudioEndpoints方法的指针。
现在记住它是 vtable(偏移量 3)中的第一个(但实际上是第四个)方法。
所以我们想得到内存中的地址,它是从 vtable 的内存地址偏移 3 个方法。

现在记住 vtable 是如何包含指针的,所以我们想要在内存中向前移动3 个指针的大小。指针的大小在 32 位机器上为 4 个字节,在 64 位机器上为 8 个字节。
所以可以肯定地说,现在指针的大小总是 8 字节,当我们的程序在现代台式计算机上运行时。我们也可以使用内置的 AHK 变量A_PtrSize。它将包含 4 或 8 个。
存储在 vtable 中的指针的可视化表示:

vtable                      offset (bytes)
AddRef                      0
QueryInterface              8
Release                     16
EnumAudioEndpoints          24
GetDefaultAudioEndpoint     32
GetDevice                   40
...

所以我们希望NumGet偏移 24 个字节:(
pEnumAudioEndpoints := NumGet(vtable+0, 3*A_PtrSize)
演示使用A_PtrSize使您的脚本在 32 位机器上也兼容,但您也可以不使用它并指定 24)


好的,呼,现在我们终于有了指向IMMDeviceEnumerator::EnumAudioEndpoints方法的指针,这意味着我们终于可以使用它了。

那么接下来的问题是,我们如何使用它呢?
首先,我们需要决定如何使用它。我可以想到两种我们想要使用它的方式。
第一个是列出所有活动和插入的设备并使用它们做想要的事情并完全放弃 nircmd 的使用,第二个是只适用于您的特定情况的一点简化。

我将为您演示第二种方式,如果您想正确实现,您可以自己尝试实现第一种方式。如果遇到问题,当然可以寻求帮助。

所以,第二种方式,简化。为此,我想到的是列出未插入的设备,如果有,您将知道在您的特定情况下电视已被拔出。
如果没有,您将知道电视已插入。

好的,所以继续使用该方法。它需要三个参数:

  1. dataFlow
    对于这个参数,我们从EDataFlow枚举中指定一个值。我们想要的值是eRender,它是枚举的第一个成员,所以0
  2. dwStateMask
    为此,我们指定所需的按位标志。我们只需要未插入的设备,因此只需使用DEVICE_STATE_UNPLUGGED标志 ( 0x00000008) 就可以了。
  3. **ppDevices
    这里我们指定一个指向变量的指针,该变量将接收指向结果IMMDeviceCollection接口所在内存地址的指针。

现在DllCall开始。调用方法的方式DllCall更具有 AHK 的魔力,您甚至几乎无法从文档中找到它,但它就在那里。
方法是在实例上调用的,所以对于第一个参数,DllCall我们将传递我们存储在的方法的指针,pEnumAudioEndpoints对于第二个参数,我们要传递对象的指针(接口的实例),我们'重新作用,我们存储在pDeviceEnumerator.
之后,我们通常将参数传递给该方法。

DllCall(pEnumAudioEndpoints, Ptr, pDeviceEnumerator, UInt, 0, UInt, 0x00000008, PtrP, pDeviceCollection)

的语法DllCall是类型后跟参数。
首先我们传递一个指针,Ptr。
然后我们传递两个非负数,无符号整数类型 UInt 就可以了。
然后我们传递一个PtrP来传递变量的指针pDeviceCollection
你会注意到这个变量甚至从未被声明过,但没关系,AHK 是一种非常宽容的语言,所以它会自动为我们创建变量。


好的,现在DllCall一切都完成了,我们有一个指向结果IMMDeviceCollection接口的指针。
您会注意到该接口如何包含两个方法,GetCount并且Item.
对于我为您演示的简化方法,我们对该GetCount方法感兴趣。
所以再一次,我们将获得该接口的 vtable 的地址:
vtable := NumGet(pDeviceCollection+0)
再一次,我们对接口的第一个(但实际上是第四个)方法(偏移量 3)感兴趣:
pGetCount := NumGet(vtable+0, 3*A_PtrSize)

然后我们已经可以使用该方法了,让我们DllCall再来一次。
这次我们作用的对象是IMMDeviceCollection,我们将它的指针存储在pDeviceCollection变量中。

这些函数只需要一个参数*pcDevices,一个指向变量的指针,该变量将接收我们设备集合中的设备数量。
DllCall(pGetCount, Ptr, pDeviceCollection, UIntP, DeviceCount)


到这里,简化的方式就完成了。
我们成功收到了未插入但已启用的音频播放设备的数量。
现在,当我们知道我们已经完成了 ComObjects 时,我们应该释放它们(如文档指定的那样)。这不一定是 100% 必需的,但绝对是一个好习惯,所以让我们释放 ComObjects:

ObjRelease(pDeviceEnumerator)
ObjRelease(pDeviceCollection)

这是简化方式的完整示例脚本:

#NoEnv ;unquoted types in DllCall don't hinder performance
CLSID := "{BCDE0395-E52F-467C-8E3D-C4579291692E}"
IID := "{A95664D2-9614-4F35-A746-DE8DB63617E6}"
pDeviceEnumerator := ComObjCreate(CLSID, IID)

vtable := NumGet(pDeviceEnumerator+0)
pEnumAudioEndpoints := NumGet(vtable+0, 3*A_PtrSize)
DllCall(pEnumAudioEndpoints, Ptr, pDeviceEnumerator, UInt, 0, UInt, 0x00000008, PtrP, pDeviceCollection)

vtable := NumGet(pDeviceCollection+0)
pGetCount := NumGet(vtable+0, 3*A_PtrSize)
DllCall(pGetCount, Ptr, pDeviceCollection, UIntP, DeviceCount)

ObjRelease(pDeviceEnumerator)
ObjRelease(pDeviceCollection)

if (DeviceCount = 0)
    MsgBox, % "No unplugged, but enabled, devices found`nI'll assume my TV is plugged in and I have three audio devices enabled"
else if (DeviceCount = 1)
    MsgBox, % "One unplugged, but enabled, device found`nI'll assume my TV is unplugged and I have only two audio devices enabled"
else
    MsgBox, % "There are " DeviceCount "unplugged audio devices"

如果这看起来非常复杂/困难,那是因为它有点复杂。
我想说这几乎和 AHK DllCalling 一样复杂。
但是,当您不使用为您完成所有很酷的事情的外部实用程序时,情况就是这样。

如果您决定实施正确的解决方案来处理我谈到的音频设备,可能是一个很好的库,您可以使用或参考。我自己没有使用过,所以不能说那里的某些东西是否已过时。
它是由 Lexikos 自己制作的。

于 2020-04-06T02:18:18.623 回答