Как проверить, подключено ли звуковое устройство в скрипте AutoHotkey? - PullRequest
0 голосов
/ 05 апреля 2020

У меня есть скрипт AutoHotkey, который может переключаться между несколькими звуковыми устройствами одним нажатием клавиши.

Все работает нормально, я использую утилиту nircmd для активации устройства (Установить как устройство по умолчанию)

Run, Tools\nircmd.exe setdefaultsounddevice "%playback%", где %playback% - это фактическое имя звукового устройства.

Таким образом, мой сценарий в основном проходит через 3 устройства (гарнитура, динамики, телевизор), которые есть в Sound Panel.

Но Когда мой телевизор выключен (отключен), он все еще проходит через все 3 устройства.

enter image description here

Мне нужно иметь возможность проверить, устройство отключено в моем скрипте.

Я не смог найти ни одной команды в nircmd, которая могла бы это сделать.

Пожалуйста, дайте мне знать, если у вас есть какие-либо идеи.

Спасибо .

1 Ответ

0 голосов
/ 06 апреля 2020

Конечно, очень выполнимо, но имейте в виду, что код в этом ответе становится довольно продвинутым .
Это без использования каких-либо внешних утилит.

Так что мы заинтересованы в EnumAudioEndpoints метод интерфейса IMMDeviceEnumerator .
С помощью этого метода мы можем получить список желаемых аудиоустройств на основе некоторых критериев.

Проблема:
Как мы используем такого метода в AHK?
По DllCall. И для этого нам нужен его адрес в памяти (указатель), поскольку DllCall может вызывать функции / методы по адресу.


Итак, мы начнем с получения IMMDeviceEnumerator указатель интерфейса.
Для этого нам потребуется CLSID , а в этом случае также его IID . Я нашел их с помощью поиска Google.
Затем мы используем функцию ComObjCreate AHK (и, чтобы сделать ее еще более сложной, да, мы работаем с ComObjects )
И, как указано в документации AHK, мы действительно получаем указатель вместо объекта из функции, потому что мы указали IID.

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

Теперь, когда у нас есть указатель на интерфейс pDeviceEnumerator, нам нужен указатель на метод интерфейса EnumAudioEndpoints.
Так что это наша следующая проблема.

Во-первых, нам нужно понять, что наш требуемый метод является первым методом интерфейса.
Но , потому что интерфейс наследуется от IUnknown интерфейса первые три метода на самом деле AddRef, QueryInterface и Release.
Поэтому наш желаемый метод интерфейса - это на самом деле четвертый метод интерфейса.

Итак, мы хотим получить указатель на 4-й метод этого интерфейса.
Чтобы сделать это, мы сначала хотим получить указатель на виртуальную таблицу интерфейса . Vtable содержит указатель каждого метода. И после того, как у нас есть указатель на vtable, мы можем получить указатель на нужный нам метод из этой vtable.

Чтобы получить эти указатели, мы будем использовать функцию AHK NumGet.
Обычно vtable располагается в начале ComObject (со смещением 0), поэтому NumGet наш pDeviceEnumerator указатель на смещение 0 позволяет добраться до vtable:
vtable := NumGet(pDeviceEnumerator+0)

* Указано 1077 *, поэтому AHK не обрабатывает переменную pDeviceEnumerator как переменную ByRef, а вместо этого работает по желаемому адресу в памяти. И мы опускаем второй и третий параметры, чтобы использовать значения по умолчанию, смещение 0 (это именно то, что мы хотим), а также тип UPtr нам подходит.

Теперь, когда у нас есть адрес памяти vtable, давайте, наконец, получим указатель на метод EnumAudioEndpoints.
Теперь вспомним, как это первый (но на самом деле четвертый ) метод в vtable (смещение 3).
Итак, мы хотим чтобы получить адрес в памяти, который смещен на 3 метода от адреса памяти виртуальной таблицы.

А теперь вспомните, как виртуальная таблица содержала указатели, поэтому мы хотим go переслать размер 3 указатели в памяти. Размер указателя составляет 4 байта на 32-битной машине и 8 байтов на 64-битной машине.
Так что можно с уверенностью сказать, что в настоящее время размер указателя всегда составляет 8 байтов, когда наша программа выполняется на современном настольный компьютер. Мы также можем использовать встроенную переменную AHK A_PtrSize. Он будет содержать 4 или 8.
Визуальное представление указателей, хранящихся в виртуальной таблице:

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, а второе - небольшое упрощение, которое будет работать только для вашего конкретного случая c.

Я продемонстрирую второй способ для вас, и если вы хотите сделать правильную реализацию, вы можете сами попробовать реализовать первый способ. Если у вас возникли проблемы, вы, конечно, можете обратиться за помощью.

Итак, второй способ, упрощение. Для этого я подумал о том, чтобы перечислить неподключенные устройства, и если оно есть, вы узнаете, что в указанном вами c корпусе телевизор отключен от сети.
Если его нет , вы будете знать, что телевизор подключен.

Хорошо, так что используйте метод. Он ожидает три аргумента:

  1. dataFlow
    Для этого параметра мы указываем значение из перечисления EDataFlow. Наше желаемое значение - eRender, которое является первым членом перечисления, поэтому 0 .
  2. dwStateMask
    Для этого мы указываем желаемые побитовые флаги. Нам нужны только неподключенные устройства, поэтому у нас все будет хорошо только с флагом DEVICE_STATE_UNPLUGGED (0x00000008).
  3. **ppDevices
    Здесь мы указываем указатель на переменная, которая будет получать указатель на адрес памяти, где расположен результирующий интерфейс IMMDeviceCollection.

А теперь на DllCall ing. Способ вызова метода с помощью DllCall еще больше AHK magi c, вы даже вряд ли найдете его в документации, но он вроде как.
Методы вызываются в экземплярах, поэтому для первого параметр DllCall мы передадим указатель метода, который мы сохранили в pEnumAudioEndpoints, а для второго параметра мы хотим передать указатель объекта (экземпляр интерфейса), с которым мы работаем, который мы сохранили в pDeviceEnumerator.
После этого мы обычно передаем аргументы методу.

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

Синтаксис DllCall имеет тип Type, а затем Argument.
Сначала мы передаем указатель, Ptr.
Затем мы передаем два неотрицательных числа, с типом unsigned integer, 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)


И вот мы go, упрощенный способ все готово.
Мы успешно получили количество отключенных, но включенных воспроизведения аудио d evices.
Теперь, когда мы узнаем, что мы закончили с 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 DllCall ing.
Но хорошо, это так, когда вы не используете внешнюю утилиту, которая делает все самое интересное для вас.

Если вы решили реализовать правильное решение для обработки ваших аудиоустройств, о которых я говорил, this может быть хорошей библиотекой, которую вы можете использовать или использовать. Я сам этим не пользовался, поэтому не могу сказать, устарел ли там какой-то материал.
Он сделан самим Лексикосом.

...