【问题标题】:How to check if sound device connected in AutoHotkey script?如何检查声音设备是否在 AutoHotkey 脚本中连接?
【发布时间】:2020-07-17 17:54:44
【问题描述】:

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

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

Run, Tools\nircmd.exe setdefaultsounddevice "%playback%",其中%playback% 是实际的声音设备名称。

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

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

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

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

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

谢谢。

【问题讨论】:

    标签: audio autohotkey device nircmd


    【解决方案1】:

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

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

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


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

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

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

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

    好的,所以我们要获取此接口的第四个方法的指针。
    为此,我们首先要获取指向接口virtual table 的指针。 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接口所在的内存地址的指针。

    现在转到DllCalling。使用DllCall 调用方法的方式更具有 AHK 的魔力,您甚至几乎无法从文档中找到它,但它就在那里。
    在实例上调用方法,因此对于DllCall 的第一个参数,我们将传递存储在pEnumAudioEndpoints 中的方法的指针,而对于第二个参数,我们要传递对象的指针(接口实例)我们正在执行的操作,我们已将其存储在pDeviceEnumerator
    之后,我们通常将参数传递给方法。

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

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


    好的,现在DllCall 已经完成,我们有一个指向结果IMMDeviceCollection 接口的指针。
    您会注意到接口如何包含两个方法,GetCountItem
    对于我为您演示的简化方式,我们对 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 一样复杂。
    但是,当您不使用为您完成所有很酷的事情的外部实用程序时,情况就是这样。

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

    【讨论】:

    • 嗯,谢谢你,这是一个很大的努力:) 看来你知道你在做什么。我已经彻底阅读了您的帖子,这确实不是最简单的实现。我实际上是在尝试找到一种无需编写任何低级 win32 API 代码即可检测设备的方法。应该有一些实用程序可以做到这一点。
    • 当然,可能有一些外部实用程序会在后台执行类似的操作。虽然不确定你为什么想要一个。除非它不想费心去学习像这个答案那样先进的东西。我当然可以理解,但也许不要自己写,然后复制粘贴别人的作品?我在帖子底部链接的库可能应该这样做。我想说这样的库可以被视为您正在寻找的这些外部实用程序之一。
    猜你喜欢
    • 2013-01-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多