;--------------------------------------------------------------------------------------------------------------- ; CHANGELOG: ; ; Dec 16 2021: Initial release by Glisense ltd ; ; Current script is designed for use together with Keyboard Extension® (https://keyboardextension.com) ; ; For the latest version, description and licensing terms go to https://keyboardextension.com/plugins/85fce5a1 ; ;--------------------------------------------------------------------------------------------------------------- #NoEnv ; Avoid checking empty variables to see if they are environment variables ; #NoTrayIcon ; Hide tray icon SetBatchLines, -1 if (A_Args[2] = "local test") { DetectHiddenWindows, On HostWindowHandle := HostEngineThreadId := A_Args[1] timer := Func("IfParentExist").Bind(HostWindowHandle) SetTimer, % timer, 2000 } MMC := new MasterMuteControl("SendInfo") MSG := new Messaging("SetMute") MSG.SendParam("CurrentMute", MMC.GetMute()) Return SendInfo(currentMute) { global MSG MSG.SendParam("CurrentMute", currentMute) ; Outgoing param } SetMute(param, value) { ; Incoming param global MMC if (param = "NewMute") MMC.SetMute(!!value) } class MasterMuteControl { __New(OnVolumeChange) { this.AudioEndpointVolume := IAudioEndpointVolume.Create() ; create an instance of class IAudioEndpointVolume UserFunc := IsObject(OnVolumeChange) ? OnVolumeChange : Func(OnVolumeChange) this.AudioEndpointVolumeCallback := new IAudioEndpointVolumeCallback(UserFunc) ; create a CallBack interface that monitors volume and mute changes this.AudioEndpointVolume.RegisterControlChangeNotify( this.AudioEndpointVolumeCallback.ptr ) ; connect the CallBack interface } __Delete() { this.AudioEndpointVolume.UnregisterControlChangeNotify( this.AudioEndpointVolumeCallback.ptr ) this.AudioEndpointVolumeCallback := "" this.AudioEndpointVolume := "" } SetMute(mute) { ; mute — 0 or 1 if (this.GetMute() != !!mute) this.AudioEndpointVolume.SetMute(!!mute) } GetMute() { Return this.AudioEndpointVolume.GetMute() } } class IAudioEndpointVolume extends _InterfaceBase { Create() { static IID_IAudioEndpointVolume := "{5CDF2C82-841E-4546-9722-0CF74078229A}" , eRender := 0, eConsole := 0, CLSCTX_ALL := 0x00000007 MMDeviceEnumerator := IMMDeviceEnumerator.Create() MMDevice := MMDeviceEnumerator.GetDefaultAudioEndpoint(eRender, eConsole) pIAudioEndpointVolume := MMDevice.Activate( CLSIDFromString(IID_IAudioEndpointVolume, _), CLSCTX_ALL, 0 ) Return new IAudioEndpointVolume(pIAudioEndpointVolume) } RegisterControlChangeNotify(pAudioEndpointVolumeCallback) { hr := DllCall(this.VTable(3), "Ptr", this.ptr, "Ptr", pAudioEndpointVolumeCallback) this.IsError(A_ThisFunc, hr) } UnregisterControlChangeNotify(pAudioEndpointVolumeCallback) { hr := DllCall(this.VTable(4), "Ptr", this.ptr, "Ptr", pAudioEndpointVolumeCallback) this.IsError(A_ThisFunc, hr) } SetMute(bMute, pguidEventContext := 0) { hr := DllCall(this.VTable(14), "Ptr", this.ptr, "UInt", bMute, "Ptr", pguidEventContext) this.IsError(A_ThisFunc, hr) } GetMute() { hr := DllCall(this.VTable(15), "Ptr", this.ptr, "UIntP", bMute) this.IsError(A_ThisFunc, hr) Return bMute } } class IMMDeviceEnumerator extends _InterfaceBase { Create() { static CLSID_MMDeviceEnumerator := "{BCDE0395-E52F-467C-8E3D-C4579291692E}" , IID_IMMDeviceEnumerator := "{A95664D2-9614-4F35-A746-DE8DB63617E6}" Return new IMMDeviceEnumerator( ComObjCreate(CLSID_MMDeviceEnumerator, IID_IMMDeviceEnumerator) ) } GetDefaultAudioEndpoint(dataFlow, role) { hr := DllCall(this.VTable(4), "Ptr", this.ptr, "Int", dataFlow, "Int", role, "PtrP", pIMMDevice) this.IsError(A_ThisFunc, hr) Return new IMMDevice(pIMMDevice) } } class IMMDevice extends _InterfaceBase { Activate(riid, dwClsCtx, pActivationParams := 0) { hr := DllCall(this.VTable(3), "Ptr", this.ptr, "Ptr", riid, "UInt", dwClsCtx, "Ptr", pActivationParams, "PtrP", pInterface) this.IsError(A_ThisFunc, hr) Return pInterface } } class _InterfaceBase { __New(ptr) { this.ptr := ptr } __Delete() { ObjRelease(this.ptr) } VTable(idx) { Return NumGet(NumGet(this.ptr + 0) + A_PtrSize*idx) } IsError(method, result, exc := true) { if (result = 0) Return 0 error := StrReplace(method, ".", "::") . " failed.`nResult: " . ( result = "" ? "No result" : SysError(Format("{:#x}", result & 0xFFFFFFFF)) ) . "`nErrorLevel: " . ErrorLevel if !exc Return error throw error } } class IAudioEndpointVolumeCallback { __New(UserFunc) { this.UserFunc := IsObject(UserFunc) ? UserFunc : Func(UserFunc) this._CreateVTable() } _CreateVTable() { static Methods := [ {name: "QueryInterface", paramCount: 3} , {name: "AddRef" , paramCount: 1} , {name: "Release" , paramCount: 1} , {name: "OnNotify" , paramCount: 2} ] this.SetCapacity("vtable", A_PtrSize*(Methods.Count() + 1)) pVtable := this.GetAddress("vtable") this.SetCapacity("IUnknown", A_PtrSize) this.ptr := this.GetAddress("IUnknown") NumPut(pVtable, this.ptr) this.Callbacks := [] for k, v in Methods { Callback := RegisterSyncCallback("IAudioEndpointVolumeCallback_" . v.name,, v.paramCount) NumPut( Callback, pVtable + A_PtrSize*(k - 1) ) this.Callbacks.Push(Callback) } NumPut( RegisterCallback(this.UserFunc, "Fast", 1), pVtable + A_PtrSize*(Methods.Count()) ) } __Delete() { for k, v in this.Callbacks DllCall("GlobalFree", "Ptr", v, "Ptr") DllCall("GlobalFree", "Ptr", NumGet(this.GetAddress("vtable") + A_PtrSize*4), "Ptr") this.SetCapacity("vtable", 0) this.Delete("vtable") } } IAudioEndpointVolumeCallback_QueryInterface(this, riid, ppvObject) { static IID_IUnknown := "{00000000-0000-0000-C000-000000000046}" , IID_IAudioEndpointVolumeCallback := "{657804FA-D6AD-4496-8A60-352752AF4F89}" , E_NOINTERFACE := 0x80004002, S_OK := 0, _, __ , p1 := CLSIDFromString(IID_IUnknown , _) , p2 := CLSIDFromString(IID_IAudioEndpointVolumeCallback, __) if !( DllCall("Ole32\IsEqualGUID", "Ptr", riid, "Ptr", p1) || DllCall("Ole32\IsEqualGUID", "Ptr", riid, "Ptr", p2) ) { ; if riid doesn't match IID_IUnknown nor IID_IAudioEndpointVolumeCallback NumPut(0, ppvObject + 0) Return E_NOINTERFACE } else { NumPut(this, ppvObject + 0) DllCall(NumGet(NumGet(ppvObject + 0) + A_PtrSize), "Ptr", ppvObject) Return S_OK } } IAudioEndpointVolumeCallback_AddRef(this) { Return 1 } IAudioEndpointVolumeCallback_Release(this) { Return 1 } IAudioEndpointVolumeCallback_OnNotify(this, pNotify) { newMute := NumGet(pNotify + 16, "UInt") addrUserFunc := NumGet(NumGet(this + 0) + A_PtrSize*4) timer := Func("DllCall").Bind(addrUserFunc, "UInt", newMute) SetTimer, % timer, -10 Return 0 } CLSIDFromString(IID, ByRef CLSID) { VarSetCapacity(CLSID, 16, 0) if res := DllCall("ole32\CLSIDFromString", "WStr", IID, "Ptr", &CLSID, "UInt") throw Exception("CLSIDFromString failed. Error: " . Format("{:#x}", res)) Return &CLSID } SysError(ErrorNum := "") { static flags := (FORMAT_MESSAGE_ALLOCATE_BUFFER := 0x100) | (FORMAT_MESSAGE_FROM_SYSTEM := 0x1000) (ErrorNum = "" && ErrorNum := A_LastError) DllCall("FormatMessage", "UInt", flags, "UInt", 0, "UInt", ErrorNum, "UInt", 0, "PtrP", pBuff, "UInt", 512, "Str", "") str := StrGet(pBuff), DllCall("LocalFree", "Ptr", pBuff) Return str? str : ErrorNum } /* RegisterSyncCallback A replacement for RegisterCallback for use with APIs that will call the callback on the wrong thread. Synchronizes with the script's main thread via a window message. This version tries to emulate RegisterCallback as much as possible without using RegisterCallback, so shares most of its limitations, and some enhancements that could be made are not. Other differences from v1 RegisterCallback: - Variadic mode can't be emulated exactly, so is not supported. - A_EventInfo can't be set in v1, so is not supported. - Fast mode is not supported (the option is ignored). - ByRef parameters are allowed (but ByRef is ignored). - Throws instead of returning "" on failure. */ RegisterSyncCallback(FunctionName, Options:="", ParamCount:="") { if !(fn := Func(FunctionName)) || fn.IsBuiltIn throw Exception("Bad function", -1, FunctionName) if (ParamCount == "") ParamCount := fn.MinParams if (ParamCount > fn.MaxParams && !fn.IsVariadic || ParamCount+0 < fn.MinParams) throw Exception("Bad param count", -1, ParamCount) static sHwnd := 0, sMsg, sSendMessageW if !sHwnd { Gui RegisterSyncCallback: +Parent%A_ScriptHwnd% +hwndsHwnd OnMessage(sMsg := 0x8000, Func("RegisterSyncCallback_Msg")) sSendMessageW := DllCall("GetProcAddress", "ptr", DllCall("GetModuleHandle", "str", "user32.dll", "ptr"), "astr", "SendMessageW", "ptr") } if !(pcb := DllCall("GlobalAlloc", "uint", 0, "ptr", 96, "ptr")) throw DllCall("VirtualProtect", "ptr", pcb, "ptr", 96, "uint", 0x40, "uint*", 0) p := pcb if (A_PtrSize = 8) { /* 48 89 4c 24 08 ; mov [rsp+8], rcx 48 89 54 24 10 ; mov [rsp+16], rdx 4c 89 44 24 18 ; mov [rsp+24], r8 4c'89 4c 24 20 ; mov [rsp+32], r9 48 83 ec 28' ; sub rsp, 40 4c 8d 44 24 30 ; lea r8, [rsp+48] (arg 3, ¶ms) 49 b9 .. ; mov r9, .. (arg 4, operand to follow) */ p := NumPut(0x54894808244c8948, p+0) p := NumPut(0x4c182444894c1024, p+0) p := NumPut(0x28ec834820244c89, p+0) p := NumPut( 0xb9493024448d4c, p+0) - 1 lParamPtr := p, p += 8 p := NumPut(0xba, p+0, "char") ; mov edx, nmsg p := NumPut(sMsg, p+0, "int") p := NumPut(0xb9, p+0, "char") ; mov ecx, hwnd p := NumPut(sHwnd, p+0, "int") p := NumPut(0xb848, p+0, "short") ; mov rax, SendMessageW p := NumPut(sSendMessageW, p+0) /* ff d0 ; call rax 48 83 c4 28 ; add rsp, 40 c3 ; ret */ p := NumPut(0x00c328c48348d0ff, p+0) } else ;(A_PtrSize = 4) { p := NumPut(0x68, p+0, "char") ; push ... (lParam data) lParamPtr := p, p += 4 p := NumPut(0x0824448d, p+0, "int") ; lea eax, [esp+8] p := NumPut(0x50, p+0, "char") ; push eax p := NumPut(0x68, p+0, "char") ; push nmsg p := NumPut(sMsg, p+0, "int") p := NumPut(0x68, p+0, "char") ; push hwnd p := NumPut(sHwnd, p+0, "int") p := NumPut(0xb8, p+0, "char") ; mov eax, &SendMessageW p := NumPut(sSendMessageW, p+0, "int") p := NumPut(0xd0ff, p+0, "short") ; call eax p := NumPut(0xc2, p+0, "char") ; ret argsize p := NumPut((InStr(Options, "C") ? 0 : ParamCount*4), p+0, "short") } NumPut(p, lParamPtr+0) ; To be passed as lParam. p := NumPut(&fn, p+0) p := NumPut(ParamCount, p+0, "int") return pcb } RegisterSyncCallback_Msg(wParam, lParam) { if (A_Gui != "RegisterSyncCallback") return fn := Object(NumGet(lParam + 0)) paramCount := NumGet(lParam + A_PtrSize, "int") params := [] Loop % paramCount params.Push(NumGet(wParam + A_PtrSize * (A_Index-1))) return %fn%(params*) } class Messaging { /* The Messaging class implements messaging between Keyboard Extension® for Windows (KEW) and the script. When the instance of the class Messaging is created, a Func Object or a function name can optionally be passed to the constructor of the class. This object or function is required to process incoming messages from KEW. In order to send messages to KEW, SendParam(param, value) method is used, where param is the name of the outgoing parameter value is its corresponding value UserOnMessageFunc — a custom Func Object or a function name that will be called when messages are received from KEW It accepts two mandatory parameters: param - the name of the incoming parameter value - the value of the incoming parameter */ __New(UserOnMessageFunc := "") { if UserOnMessageFunc { UserFunc := IsObject(UserOnMessageFunc) ? UserOnMessageFunc : Func(UserOnMessageFunc) this.OnMessageReceive := new this.Receiving(UserFunc) } } __Delete() { if this.OnMessageReceive { this.OnMessageReceive.Clear() this.OnMessageReceive := "" } } SendParam(param, value) { ; these gobal variables get values ​​when the plugin starts, they are required to send messages to KEW global HostWindowHandle, HostEngineThreadId while !(HostWindowHandle && HostEngineThreadId) Sleep, 10 this._SendData(HostWindowHandle, HostEngineThreadId, param . ":" . value) } _SendData(hWnd, EngineThreadId, StringData) { VarSetCapacity(message, size := StrPut(StringData, "UTF-16")*2, 0) StrPut(StringData, &message, "UTF-16") VarSetCapacity(COPYDATASTRUCT, A_PtrSize*3, 0) NumPut(EngineThreadId, COPYDATASTRUCT, 0, "UInt") NumPut(size, COPYDATASTRUCT, A_PtrSize, "UInt") NumPut(&message, COPYDATASTRUCT, A_PtrSize*2) DllCall("SendMessage", Ptr, hWnd, UInt, WM_COPYDATA := 0x4A, Ptr, 0, Ptr, ©DATASTRUCT) } class Receiving { WM_COPYDATA := 0x4A __New(UserFunc) { this.dataArr := [] this.UserFunc := UserFunc this.timer := ObjBindMethod(this, "MessageProcessing") this.OnMsg := ObjBindMethod(this, "CopyDataRead") OnMessage(this.WM_COPYDATA, this.OnMsg) } CopyDataRead(wp, lp) { data := StrGet(NumGet(lp + A_PtrSize*2), "UTF-16") this.dataArr.Push(data) timer := this.timer SetTimer, % timer, -10 Return true } MessageProcessing() { while this.dataArr[1] != "" { data := this.dataArr.RemoveAt(1) RegExMatch(data, "^(.*?):(.*)", m) param := m1, value := m2 this.UserFunc.Call(param, value) } } Clear() { OnMessage(this.WM_COPYDATA, this.OnMsg, 0) this.OnMsg := "" this.timer := "" } } } IfParentExist(hwnd) { if !WinExist("ahk_id " . hwnd) { ExitApp } }