fisofo

Looking to capture IMMNotificationClient::OnDeviceStateChanged events

4 posts in this topic

As mentioned at the end of >this thread, I'm looking to code a way to capture OnDeviceStateChanged events. I've read through the msdn info, as well as an example they have, but it's frankly over my head. Tracexx's code utilizes some of this stuff, but not this particular method.

The purpose is for triggering code when an audio endpoint is added/removed/changed from the system. GUIRegisterMsg($WM_DEVICECHANGE, 'WM_DEVICECHANGE') can be used for some of these when they are USB related, but not when a bluetooth speaker (for example) connects or disconnects.

Share this post


Link to post
Share on other sites



Great, you made new thread.

Ok so first you have to know what object is. Object is set of methods and it's represented by simple pointer. There is no mumbo-jumbo about that. If you want to go deeper it can be said that object is pointer to pointer pointing to set of pointers. Each of pointers in that set is new function pointer. In case of object with thre methods/functions:  

--> Function1 Pointer
                                /
|OBJECT| ---> OBJECT Pointer --+  --> Function2 Pointer
                                \
                                  --> Function3 Pointer

So how to make object in AutoIt? One of my goals as AutoIt developer was to add so-called event or sink or callback objects. Even though all the code was done and all that's left was to commit the addition, my role as developer was over due to ... I will stop now before I say something that would offend some people.

But, don't worry. Considering AutoIt has callback functions these objects could be build using them. The maing thing to account for is packing the set of functions inside object.

You know by now that there is thing called Interface definition. This is description of the object of that interface. In AutoIt that's done by "Interface description string". If you go through the IPolicyConfig script code you will see $tagIMMDeviceEnumerator or $tagIPolicyConfig, or... Those are interface desc. strings and they describe every function (method) object exposes. Each of lines is one function and inside the object they are represented by pointer. So if I would create each function with DllCallbackRegister() and use that pointer in place of method I would create object.

Interface strings by definition does not include first three methods of every COM interface (we work with that). This was done to lift the load from users so AutoIt internally add first three IUnknown methods for COM objects. So I need to create callbacks for them too to have proper v-table order (google if you aren't familiar with the term).

So, to get object from interface script I can write this function:

Func ObjectFromDtag($sFunctionPrefix, $tagInterface, ByRef $tInterface)
    Local Const $tagIUnknown = "QueryInterface hresult(ptr;ptr*);" & _
            "AddRef dword();" & _
            "Release dword();"
    ; Adding IUnknown methods
    $tagInterface = $tagIUnknown & $tagInterface
    Local Const $PTR_SIZE = DllStructGetSize(DllStructCreate("ptr"))
    Local $aMethods = StringSplit(StringReplace(StringReplace(StringReplace(StringReplace(GetMethods($tagInterface), "object", "idispatch"), "variant*", "ptr"), "hresult", "long"), "bstr", "ptr"), @LF, 3)
    Local $iUbound = UBound($aMethods)
    Local $sMethod, $aSplit, $sNamePart, $aTagPart, $sTagPart, $sRet, $sParams
    ; Allocation. Read http://msdn.microsoft.com/en-us/library/ms810466.aspx to see why like this (object + methods):
    $tInterface = DllStructCreate("ptr[" & $iUbound + 1 & "]")
    If @error Then Return SetError(1, 0, 0)
    For $i = 0 To $iUbound - 1
        $aSplit = StringSplit($aMethods[$i], "|", 2)
        If UBound($aSplit) <> 2 Then ReDim $aSplit[2]
        $sNamePart = $aSplit[0]
        $sTagPart = $aSplit[1]
        $sMethod = $sFunctionPrefix & $sNamePart
        $aTagPart = StringSplit($sTagPart, ";", 2)
        $sRet = $aTagPart[0]
        $sParams = StringReplace($sTagPart, $sRet, "", 1)
        $sParams = "ptr" & $sParams
        DllStructSetData($tInterface, 1, DllCallbackGetPtr(DllCallbackRegister($sMethod, $sRet, $sParams)), $i + 2) ; Freeing is left to AutoIt.
    Next
    DllStructSetData($tInterface, 1, DllStructGetPtr($tInterface) + $PTR_SIZE) ; Interface method pointers are actually pointer size away
    Return ObjCreateInterface(DllStructGetPtr($tInterface), "", $tagInterface, False) ; and first pointer is object pointer that's wrapped
EndFunc

... So now all I need to do is use Interface description string and I have my own custom object whose pointer I get by simply calling default method (I will show you).

An example could be:

#include <WinApi.au3>

Global Const $tagMyInterface = "FirstMethod hresult(wstr);" & _
        "SecondMethod hresult(int;wstr);"
;====================================================================
; Define methods of your object. Every method starts with e.g. _MyObject_ because you will pass that to ObjectFromDtag
Func _MyObject_QueryInterface($pSelf, $pRIID, $pObj)
    ConsoleWrite("_MyNotificationHandler_QueryInterface called" & @CRLF)
    Local $tStruct = DllStructCreate("ptr", $pObj)
    Switch _WinAPI_StringFromGUID($pRIID)
        Case $sIID_IUnknown; , $sIID_Whatewer_you_want
            DllStructSetData($tStruct, 1, $pSelf)
            ConsoleWrite(" <IUnknown or Whatewer_you_want>" & @CRLF)
            Return 0 ; S_OK
    EndSwitch
    ConsoleWrite(@CRLF)
    Return 0x80004002 ; E_NOINTERFACE
EndFunc

Func _MyObject_AddRef($pSelf)
    #forceref $pSelf
    ConsoleWrite("_MyNotificationHandler_AddRef called" & @CRLF)
    Return 0
EndFunc

Func _MyObject_Release($pSelf)
    #forceref $pSelf
    ConsoleWrite("_MyNotificationHandler_Release called" & @CRLF)
    Return 0
EndFunc

Func _MyObject_FirstMethod($pSelf, $sParam)
    #forceref $pSelf
    ConsoleWrite("_MyObject_FirstMethod called. $sParam = " & $sParam & @CRLF)
    Return 0 ; S_OK
EndFunc

Func _MyObject_SecondMethod($pSelf, $iParam, $sParam)
    #forceref $pSelf
    ConsoleWrite("_MyObject_FirstMethod called. $iParam = " & $iParam & ", $sParam = " & $sParam & @CRLF)
    Return 0 ; S_OK
EndFunc
;====================================================================


; So just create your object
Global $tMyObject
Global $oMyObject = ObjectFromDtag("_MyObject_", $tagMyInterface, $tMyObject)

; Is object get?
ConsoleWrite("!!! IsObj($oMyObject) = " & IsObj($oMyObject) & @CRLF)

; Try calling some method
$oMyObject.FirstMethod("Test string")

; Try another
$oMyObject.SecondMethod(2345, "Another string")

; Get object pointer:
ConsoleWrite("+>>> Object pointer = " & $oMyObject() & @CRLF)

; The End


Func ObjectFromDtag($sFunctionPrefix, $tagInterface, ByRef $tInterface)
    Local Const $tagIUnknown = "QueryInterface hresult(ptr;ptr*);" & _
            "AddRef dword();" & _
            "Release dword();"
    ; Adding IUnknown methods
    $tagInterface = $tagIUnknown & $tagInterface
    Local Const $PTR_SIZE = DllStructGetSize(DllStructCreate("ptr"))
    ; Below line really simple even though it looks super complex. It's just written weird to fit one line, not to steal your eyes
    Local $aMethods = StringSplit(StringReplace(StringReplace(StringReplace(StringReplace(StringTrimRight(StringReplace(StringRegExpReplace($tagInterface, "\h*(\w+)\h*(\w+\*?)\h*(\((.*?)\))\h*(;|;*\z)", "$1\|$2;$4" & @LF), ";" & @LF, @LF), 1), "object", "idispatch"), "variant*", "ptr"), "hresult", "long"), "bstr", "ptr"), @LF, 3)
    Local $iUbound = UBound($aMethods)
    Local $sMethod, $aSplit, $sNamePart, $aTagPart, $sTagPart, $sRet, $sParams
    ; Allocation. Read http://msdn.microsoft.com/en-us/library/ms810466.aspx to see why like this (object + methods):
    $tInterface = DllStructCreate("ptr[" & $iUbound + 1 & "]")
    If @error Then Return SetError(1, 0, 0)
    For $i = 0 To $iUbound - 1
        $aSplit = StringSplit($aMethods[$i], "|", 2)
        If UBound($aSplit) <> 2 Then ReDim $aSplit[2]
        $sNamePart = $aSplit[0]
        $sTagPart = $aSplit[1]
        $sMethod = $sFunctionPrefix & $sNamePart
        $aTagPart = StringSplit($sTagPart, ";", 2)
        $sRet = $aTagPart[0]
        $sParams = StringReplace($sTagPart, $sRet, "", 1)
        $sParams = "ptr" & $sParams
        DllStructSetData($tInterface, 1, DllCallbackGetPtr(DllCallbackRegister($sMethod, $sRet, $sParams)), $i + 2) ; Freeing is left to AutoIt.
    Next
    DllStructSetData($tInterface, 1, DllStructGetPtr($tInterface) + $PTR_SIZE) ; Interface method pointers are actually pointer size away
    Return ObjCreateInterface(DllStructGetPtr($tInterface), "", $tagInterface, False) ; and first pointer is object pointer that's wrapped
EndFunc

IMMNotificationClient is:

Global Const $sIID_IMMNotificationClient = "{7991EEC9-7E89-4D85-8390-6C703CEC60C0}"
Global Const $tagIMMNotificationClient = "OnDeviceStateChanged hresult(wstr;dword);" & _
        "OnDeviceAdded hresult(wstr);" & _
        "OnDeviceRemoved hresult(wstr);" & _
        "OnDefaultDeviceChanged hresult(dword;dword;wstr);" & _
        "OnPropertyValueChanged hresult(wstr;int64);" ; last param type is improvisation because AutoIt lacks proper type

... Now try and ask again if there would be problems.


♡♡♡

.

eMyvnE

Share this post


Link to post
Share on other sites

trancexx, thank you for taking the time to dig into this in-depth with me! Having said that, much of what you posted is over my head, and will probably take me a long time to really get a grasp of it... I've read through it a few times, but I'm having difficulty understanding it; it's a level or three above my current grasp of programming/AutoIt.

I can *sort of* see what you are getting at, in that you are showing me how to access the methods of the object, and I can see how you've done that with your original $tagIMMDeviceEnumerator and with your example code here, but there are bits and pieces all over that I don't get or am not sure how to translate to what I want to do, and I don't even know where to start asking questions.

I figured out that I need to do a IMMDeviceEnumerator::RegisterEndpointNotificationCallback first, but I'm afraid that I'll need to table this for now until I can take more time to learn this stuff, or until someone else codes it and I can learn from that... it probably would have been easier back in college when I was actually more fluent in programming languages, but it's been too long!

If you feel like whipping the code together, that would be great, but I won't ask or expect you to do that after you've already done so much. Thanks for trying to teach me though!

Share this post


Link to post
Share on other sites

Hate to be that guy, but I'm resurrecting this post because SetDefaultEndpoint is no longer working after the latest Win10 update. I was going to just PM you Trancexx, but looks like I can't; mind taking a look?

 

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!


Register a new account

Sign in

Already have an account? Sign in here.


Sign In Now