Can an AutoHotkey script be notified of volume changing and muting/unmuting?

Get help with using AutoHotkey (v1.1 and older) and its commands and hotkeys
teadrinker
Posts: 4309
Joined: 29 Mar 2015, 09:41
Contact:

Re: Can an AutoHotkey script be notified of volume changing and muting/unmuting?

13 Dec 2019, 23:08

teadrinker wrote: The problem is that IAudioEndpointVolumeCallback works in another thread.
lexikos' RegisterSyncCallback() solves that problem. This code works well without dll and another script:

Code: Select all

SetBatchLines, -1

Gui, +AlwaysOnTop
Gui, Font, s16
Gui, Add, Text, y13, Volume:
Gui, Add, Text, xp y+5 wp right, Muted:
Gui, Add, Text, x+3 y13 left, 100
Gui, Add, Text, xp y+5, 0
GuiControl,, Static3, % Round( VA_GetMasterVolume() )
GuiControl,, Static4, % VA_GetMasterMute()
Gui, Show

IAudioEndpointVolume := VA_GetAudioEndpointVolume("playback")
IAudioEndpointVolumeCallback := IAudioEndpointVolumeCallback_Create(IAudioEndpointVolume)
VA_IAudioEndpointVolume_RegisterControlChangeNotify(IAudioEndpointVolume, IAudioEndpointVolumeCallback)
OnExit( Func("Clear").Bind(IAudioEndpointVolume, IAudioEndpointVolumeCallback) )
Return

GuiClose() {
   ExitApp
}

SetInfo(muted, volume) {
   GuiControl,, Static3, % Round( volume*100 )
   GuiControl,, Static4, % muted
}

Clear(IAudioEndpointVolume, IAudioEndpointVolumeCallback) {
   VA_IAudioEndpointVolume_UnregisterControlChangeNotify(IAudioEndpointVolume, IAudioEndpointVolumeCallback)
   ObjRelease(IAudioEndpointVolumeCallback)
   ObjRelease(IAudioEndpointVolume)
}

IAudioEndpointVolumeCallback_Create(aev) {
   static VTBL := [ "QueryInterface"
                  , "AddRef"
                  , "Release"
                  , "OnNotify" ]
                  
        , heapSize := A_PtrSize*10
        , heapOffset := A_PtrSize*9
        
        , flags := (HEAP_GENERATE_EXCEPTIONS := 0x4) | (HEAP_NO_SERIALIZE := 0x1)
        , HEAP_ZERO_MEMORY := 0x8
   
   hHeap := DllCall("HeapCreate", "UInt", flags, "Ptr", 0, "Ptr", 0, "Ptr")
   addr := IAudioEndpointVolumeCallback := DllCall("HeapAlloc", "Ptr", hHeap, "UInt", HEAP_ZERO_MEMORY, "Ptr", heapSize, "Ptr")
   addr := NumPut(addr + A_PtrSize, addr + 0)
   for k, v in VTBL
      addr := NumPut( RegisterSyncCallback("IAudioEndpointVolumeCallback_" . v), addr + 0 )
   NumPut(hHeap, IAudioEndpointVolumeCallback + heapOffset)
   Return IAudioEndpointVolumeCallback
}

IAudioEndpointVolumeCallback_QueryInterface(this, riid, ppvObject) {
   Return 0 ; S_OK
}

IAudioEndpointVolumeCallback_AddRef(this) {
   static refOffset := A_PtrSize*8
   NumPut(refCount := NumGet(this + refOffset, "UInt") + 1, this + refOffset, "UInt")
   Return refCount
}

IAudioEndpointVolumeCallback_Release(this) {
   static refOffset := A_PtrSize*8
        , heapOffset := A_PtrSize*9
   NumPut(refCount := NumGet(this + refOffset, "UInt") - 1, this + refOffset, "UInt")
   if (refCount = 0) {
      hHeap := NumGet(this + heapOffset)
      DllCall("HeapDestroy", "Ptr", hHeap)
   }
   Return refCount
}

IAudioEndpointVolumeCallback_OnNotify(this, pNotify) {
   timer := Func("SetInfo").Bind(NumGet(pNotify + 16, "UInt"), NumGet(pNotify + 20, "Float"))
   SetTimer, % timer, -10
   Return 0
}

/*
    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, &params)
        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*)
}
User avatar
JoeWinograd
Posts: 2178
Joined: 10 Feb 2014, 20:00
Location: U.S. Central Time Zone

Re: Can an AutoHotkey script be notified of volume changing and muting/unmuting?

14 Dec 2019, 10:25

Hi teadrinker,
Thank you for your continuing efforts on this...very much appreciated! I'll give the new code a spin this weekend and will let you know how it goes. Regards, Joe
User avatar
JoeWinograd
Posts: 2178
Joined: 10 Feb 2014, 20:00
Location: U.S. Central Time Zone

Re: Can an AutoHotkey script be notified of volume changing and muting/unmuting?

18 Dec 2019, 12:09

Hi teadrinker,
A quick note to let you know that your new code is working perfectly here on a W10/64-bit system with 1.1.32.00/U64...no crashes...no freezes. Next task is to integrate your code into mine. Will let you know how that goes. Thanks for your ongoing help, Joe
User avatar
JoeWinograd
Posts: 2178
Joined: 10 Feb 2014, 20:00
Location: U.S. Central Time Zone

Re: Can an AutoHotkey script be notified of volume changing and muting/unmuting?

19 Dec 2019, 15:03

Hi teadrinker,
Following up on my last post, I integrated your new code into my horizontal volume slider script...tested on W7 and W10...works great! :bravo:

A question for you. I'm not very knowledgeable with call-backs or DLLs, both of which you use heavily. I'm wondering if anything in your code survives termination of the script. In other words, after an ExitApp, is anything that you deployed via a call-back or DLL (or anything else) still active in Windows? If so, what would I have to do before an ExitApp to close/delete/remove any remnants of the script? Thanks, Joe
teadrinker
Posts: 4309
Joined: 29 Mar 2015, 09:41
Contact:

Re: Can an AutoHotkey script be notified of volume changing and muting/unmuting?

19 Dec 2019, 15:45

No, after the process is closed, all its resources are relised.
User avatar
JoeWinograd
Posts: 2178
Joined: 10 Feb 2014, 20:00
Location: U.S. Central Time Zone

Re: Can an AutoHotkey script be notified of volume changing and muting/unmuting?

19 Dec 2019, 16:09

> after the process is closed, all its resources are released

Great news! Excellent!
User avatar
mprost
Posts: 8
Joined: 24 Dec 2018, 02:50

Re: Can an AutoHotkey script be notified of volume changing and muting/unmuting?

24 Mar 2024, 11:20

BTW, in case somebody is interested in something like this but for AHK v2, I found this script posted by u/plankoe in this reddit thread:

Code: Select all

#Requires AutoHotkey v2.0

#Include Audio.ahk ; https://github.com/thqby/ahk2_lib/blob/master/Audio/Audio.ahk

Persistent()

OnVolumeChange(myFunction)
; to remove the callback, call OnVolumeChange, pass the same function as before, with 0 in the second parameter:
; OnVolumeChange(myFunction, 0)

myFunction(data) {
	muted := data.bMuted
	volume := Round(data.masterVolume * 100)

	MsgBox("Hi. The current volume is: " volume ", and mute is " (muted? "on" : "off"))
}

OnVolumeChange(function, AddRemove := 1) {
	static callbacks := Map(), init := 0

	if callbacks.Has(function) && AddRemove = 0 {
		callbacks.Delete(function)
		return
	}

	callbacks.Set(function, 1)

	if !init {
		static AEV, vt
		init := 1

		de := IMMDeviceEnumerator()
		IMMD := de.GetDefaultAudioEndpoint()
		AEV := IMMD.Activate(IAudioEndpointVolume)

		; create IAudioEndpointVolumeCallback
		paramCounts := "3112"
		vt := Buffer((StrLen(paramCounts) + 1) * A_PtrSize)
		p := NumPut("ptr", vt.ptr + A_PtrSize, vt)
		loop parse paramCounts
			p := NumPut("ptr", CallbackCreate(AudioEndpointVolumeCallback.Bind(A_Index - 1),, A_LoopField), p)

		AEV.RegisterControlChangeNotify(vt)
		OnExit(UnRegister, -1)
		UnRegister(*) {
			AEV.UnregisterControlChangeNotify(vt)
		}
	}

	AudioEndpointVolumeCallback(index, pInterface, params*) {
		switch index {
		case 0:
			static IID_IUnknown                     := "{00000000-0000-0000-C000-000000000046}"
				, IID_IAudioEndpointVolumeCallback := "{657804fa-d6ad-4496-8a60-352752af4f89}"

			VarSetStrCapacity(&iid, 38)
			DllCall("ole32\StringFromGUID2", "Ptr", params[1], "Str", iid, "Int", 39)
			if (iid = IID_IAudioEndpointVolumeCallback || iid = IID_IUnknown)
			{
				NumPut("ptr", pInterface, params[2])
				return 0 ; S_OK
			}
			NumPut("ptr", 0, params[2])
			return 0x80004002 ; E_NOINTERFACE
		case 3: ; OnNotify
			data := params[1]
			bMuted := NumGet(data, 16, "int")
			masterVolume := NumGet(data, 20, "float")
			for cb in callbacks
				cb.Call({bMuted : bMuted, masterVolume : masterVolume})
		default:
			return 0x80004001 ; E_NOTIMPL
		}
	}
}
His/her notes:
You could use SetTimer and call SoundGetVolume repeatedly to check if the volume changes, but I don't like constantly checking using a loop.

To detect when the volume changes without SetTimer, you need the Core Audio API. The library Audio.ahk by thqby makes it easier to use the Core Audio API in AutoHotkey. The script below needs Audio.ahk to function.

To use this script, call OnVolumeChange, and pass a function to it. In this example, the function is myFunction. When the user changes the master volume, the callback function (myFunction) is called with one parameter. This parameter contains an object (defined as data in myFunction) that contains the properties:

bMuted : true if master volume is muted. false if not muted.

masterVolume : a float between 0.0 and 1.0. 0.0 is mute, 1.0 is max volume.

Return to “Ask for Help (v1)”

Who is online

Users browsing this forum: CaseyMoon, imstupidpleshelp, mmflume, Rohwedder, ShatterCoder and 180 guests