Surely very doable, but be warned, the code in this answer gets quite advanced.
This is without using any external utilities.
So we're interested in the EnumAudioEndpoints
method of the IMMDeviceEnumerator
interface.
With this method we can list desired audio devices based on some criteria.
Problem:
How do we make use of such a method in AHK?
By DllCall
ing it. And to to that, we need its address in memory (pointer), since DllCall
is able to call functions/methods by address.
So we start off by getting the IMMDeviceEnumerator
interface's pointer.
For that we require its CLSID and in this case also its IID. I found them via a Google Search.
Then we make use of AHK's ComObjCreate
function (and to make it even more complicated, yes, we're working with ComObjects)
And as the AHK documention specifies, we do indeed get a pointer instead of an object from the function, because we specified an IID.
CLSID := "{BCDE0395-E52F-467C-8E3D-C4579291692E}"
IID := "{A95664D2-9614-4F35-A746-DE8DB63617E6}"
pDeviceEnumerator := ComObjCreate(CLSID, IID)
Now that we have the pointer to the interface pDeviceEnumerator
, we need a pointer to the EnumAudioEndpoints
method of the interface.
So that's our next problem.
Firstly we need to understand, that our desired method is the first method of the interface.
But because the interface inherits from IUnknown
the interface's first three methods are actually AddRef
, QueryInterface
, and Release
.
Therefore, our desired method of the interface is actually the fourth method of the interface.
Ok, so we want to get the pointer to 4th method of this interface.
To do this, we first want to get the pointer to the interface's virtual table. The vtable contains every method's pointer. And after we have the pointer the the vtable, we can get our desired method's pointer from that vtable.
To get these pointers, we're going to use AHK's NumGet
function.
The vtable is usually located at the beginning of the ComObject (at offset 0), so lets NumGet
our pDeviceEnumerator
pointer at offset 0 to get to the vtable:
vtable := NumGet(pDeviceEnumerator+0)
+0
is specified so AHK doesn't treat the variable pDeviceEnumerator
as a ByRef variable, and instead operates at our desired address in memory.
And we're omitting the second and third parameters to use the default values, offset 0 (which is exactly what we want) and also the UPtr type is fine for us.
Now that we have the memory address of the vtable, lets finally get the pointer of the EnumAudioEndpoints
method.
Now remember how it's the first (but actually fourth) method in the vtable (offset 3).
So we want to get the address in memory which is offset 3 methods from the vtable's memory address.
And now remember how the vtable contained pointers, so we want to go forward the size of 3 pointers in memory. The size of a pointer is 4 bytes on a 32bit machine, and 8 bytes on a 64bit machine.
So it's pretty safe to say that nowadays the size of a pointer is always 8 bytes, when our program is ran on a modern desktop computer. We can also make use of the built in AHK variable A_PtrSize
. It'll contain 4 or 8.
Visual representation of the pointers being stored in the vtable:
vtable offset (bytes)
AddRef 0
QueryInterface 8
Release 16
EnumAudioEndpoints 24
GetDefaultAudioEndpoint 32
GetDevice 40
...
So we want to NumGet
at offset 24 bytes:
pEnumAudioEndpoints := NumGet(vtable+0, 3*A_PtrSize)
(Demonstrating the usage of A_PtrSize
to make your script compatible on 32bit machines as well, but you could just as well not have that and specify 24)
Ok, phew, now we finally have the pointer to the IMMDeviceEnumerator::EnumAudioEndpoints
method, which means we can finally use it.
So the next problem is, how do we use it?
Firstly we need to decide how we want to use it. I can think of two ways we'd want to use it.
The first being list all active & plugged in devices and doing desired stuff with them and ditching the usage of nircmd altogether, and the second being a little simplification that'll work just for your specific case.
I'll demonstrate the second way for you, and if you want to make a proper implementation, you can yourself try to implement the first way. If you run into problems, you can of course ask for help.
So, the second way, the simplification. For this what I thought of, was listing the unplugged devices and if there is one, you'll know what in your specific case the TV is unplugged.
If there isn't one, you'll know that the TV is plugged in.
Ok, so onto using the method.
It expects three arguments:
dataFlow
For this parameter we specify a value from the EDataFlow
enum. Our desired value is eRender
, which is the first member of the enum, so 0.
dwStateMask
For this we specify desired bitwise flags. We want only unplugged devices, so we'll be fine with just the DEVICE_STATE_UNPLUGGED
flag (0x00000008
).
**ppDevices
Here we specify a pointer to a variable, which is going to receive the pointer to the memory address where the resulting IMMDeviceCollection
interface is located.
And now onto DllCall
ing. The way to call a method with DllCall
is even more AHK magic, you'll hardly even find it from the documentation even, but it's kind of there.
Methods are called on instances, so for the first parameter of the DllCall
we'll pass the pointer of the method, which we have stored in pEnumAudioEndpoints
, and for the second parameter we want to pass the pointer of the object (instance of interface) we're acting on, which we have stored in pDeviceEnumerator
.
After that, we normally pass arguments to the method.
DllCall(pEnumAudioEndpoints, Ptr, pDeviceEnumerator, UInt, 0, UInt, 0x00000008, PtrP, pDeviceCollection)
The syntax of DllCall
is Type followed by Argument.
First we pass a pointer, Ptr.
Then we pass two non-negative numbers, the type unsigned integer, UInt, will do fine.
Then we pass a PtrP to pass the pointer of the variable pDeviceCollection
.
You'll notice that this variable was never even declared, but that's fine, AHK is such a forgiving language so it'll automatically create the variable for us.
Ok, now the DllCall
is all done and we have a pointer to a resulting IMMDeviceCollection
interface.
You'll notice how the interface includes two methods, GetCount
and Item
.
For the simplified way I'm demonstrating for you, we're interested in the GetCount
method.
So once again, we'll get the address of that interface's vtable:
vtable := NumGet(pDeviceCollection+0)
And again, we're interested in the first (but actually fourth) method of the interface (offset 3):
pGetCount := NumGet(vtable+0, 3*A_PtrSize)
And then we can already use the method, so lets DllCall
again.
And this time the object we're acting upon is IMMDeviceCollection
, and we have its pointer stored in the pDeviceCollection
variable.
The functions expects just one argument *pcDevices
, a pointer to a variable which is going to receive the number of devices there is on our device collection.
DllCall(pGetCount, Ptr, pDeviceCollection, UIntP, DeviceCount)
And there we go, the simplified way is all done.
We successfully received the number of unplugged, but enabled, audio playback devices.
Now at the end when we know we're done with the ComObjects, we should release them (as the documentation specifies). This is necessarily not 100% required, but is definitely good practice, so lets release the ComObjects:
ObjRelease(pDeviceEnumerator)
ObjRelease(pDeviceCollection)
And here's a full example script for the simplified way:
#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"
If this seems like it isvery complex/hard, that's because it kind of is.
I'd say this is almost as complicated as AHK DllCall
ing gets.
But well, that's the way it is when you don't use an external utility that does all the cool stuff for you.
If you decide to implement the proper solution for handling your audio devices I talked about, this might be a good library you can use, or take reference from. I haven't used it myself, so can't say if some stuff there is outdated.
It's made by Lexikos himself.