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

Get help with using AutoHotkey and its commands and hotkeys
User avatar
JoeWinograd
Posts: 1353
Joined: 10 Feb 2014, 20:00

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

13 Oct 2019, 12:12

Hi Folks,

I wrote a script that allows volume changing and muting/unmuting via its system tray icon and hotkeys. The script reflects the volume and mute/unmute changes via its tray tip and tray icon. However, if the volume or mute status is changed in another way, such as the standard speaker icon in the system tray or some other app, my script doesn't know that, so its tray tip and tray icon will not reflect the current status.

I was thinking that there might be an OnMessage for it, but I didn't see anything in the List of Windows Messages about audio/sound/volume.

I could Sleep or SetTimer to check it periodically, but I'd rather not do that. There must be a notification mechanism in Windows, because the standard speaker icon in the system tray instantly reflects the volume level and mute status when my script changes them. Anyone know how an AutoHotkey script can get those volume and mute notifications? Thanks much, Joe
User avatar
JoeWinograd
Posts: 1353
Joined: 10 Feb 2014, 20:00

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

13 Oct 2019, 16:22

Thanks for the link, boiler...the OnNotify method looks promising...if I can figure out how to translate it into AHK. :) if anyone reading this thread has already done that, I'll be very grateful for the code. Regards, Joe
lexikos
Posts: 6668
Joined: 30 Sep 2013, 04:07
GitHub: Lexikos

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

13 Oct 2019, 16:48

VA.ahk wraps IAudioEndpointVolume and the interfaces required to get it. You would need to use IAudioEndpointVolume::RegisterControlChangeNotify (VA_IAudioEndpointVolume_RegisterControlChangeNotify) to register a callback interface. The callback interface is not something you wrap; you must implement it by building the vtable and filling it with callback function pointers at the appropriate positions. You should refer to the SDK headers for ordering (keyword: IAudioEndpointVolumeCallbackVtbl), since the documentation sorts alphabetically.

Note that some native COM notifications call the callback interface from a worker thread; i.e. not AutoHotkey's main thread. This would make the script unstable. Judging by comments in Using IMMNotificationClient in a more safe manner and given that it is a related API, IAudioEndpointVolume's notifications are probably like that, and will not be safe to catch without some intermediary, like machine code (calling SendMessage to notify the script) or a DLL.

Otherwise, for one example of implementing an interface, see class ActiveScriptSite (which implements two interfaces in one object, so is not the best example). Creating COM Object Event Handler has an example and some explanation (but keep in mind the bug noted at the end of the topic). There are probably more examples.
User avatar
JoeWinograd
Posts: 1353
Joined: 10 Feb 2014, 20:00

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

13 Oct 2019, 17:44

Thanks, lexikos. I haven't read all 13 pages at the VA.ahk thread yet, so the answer might be in there, but do you know off the top of your head if it works as-is in W7/8.1/10, or would it need changes?
gregster
Posts: 3410
Joined: 30 Sep 2013, 06:48

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

13 Oct 2019, 19:17

Re latest Windows versions: I haven't used these specific endpoints, but I just used VA.ahk successfully (for much simpler tasks) a few days ago on Win 10 with latest AHK (both 64 bit).
(But I probably won't be of much help for this topic here ;) , sorry )
User avatar
JoeWinograd
Posts: 1353
Joined: 10 Feb 2014, 20:00

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

13 Oct 2019, 19:41

Thanks, gregster, good to know that least some aspects of VA.ahk worked for you on W10. Regards, Joe
teadrinker
Posts: 1079
Joined: 29 Mar 2015, 09:41
Contact:

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

04 Dec 2019, 15:02

Tried to implement, it seems to work:

Code: Select all

; #Include VA.ahk
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

SetInfo(info) {
   GuiControl,, Static3, % Round( info[2]*100 )
   GuiControl,, Static4, % info[1]
}

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

GuiClose() {
   ExitApp
}

IAudioEndpointVolumeCallback_Create(aev) {
   static VTBL := [ RegisterCallback("IAudioEndpointVolumeCallback_QueryInterface")
                  , RegisterCallback("IAudioEndpointVolumeCallback_AddRef")
                  , RegisterCallback("IAudioEndpointVolumeCallback_Release")
                  , RegisterCallback("IAudioEndpointVolumeCallback_OnNotify", "F") ]
                  
        , 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(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) {
   static info := [], timer := Func("SetInfo").Bind(info)
   Critical
   info[1] := NumGet(pNotify + 16, "UInt")
   info[2] := NumGet(pNotify + 20, "Float")
   SetTimer, % timer, -10
   Return 0
}
User avatar
JoeWinograd
Posts: 1353
Joined: 10 Feb 2014, 20:00

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

04 Dec 2019, 15:42

Hi teadrinker,
Spectacular! I tested it on W7/64-bit, W8.1/64-bit, W10/32-bit, and W10/64-bit. Worked perfectly on all systems! I can't thank you enough for this...fantastic! Regards, Joe
User avatar
JoeWinograd
Posts: 1353
Joined: 10 Feb 2014, 20:00

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

04 Dec 2019, 16:07

Hi teadrinker,

Hasn't failed here yet, but I tested it only briefly on all systems. I plan to integrate it into a horizontal-volume-slider script that I wrote for physically-impaired users...base script is here:
https://www.autohotkey.com/boards/viewtopic.php?p=235750#p235750

Once I get your code integrated into mine, I'll be able to test it more thoroughly. Will let you know if it sticks any during real usage. Thanks again! Regards, Joe
teadrinker
Posts: 1079
Joined: 29 Mar 2015, 09:41
Contact:

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

04 Dec 2019, 19:56

For me it works without freezes on Win 7 and 10 with AutoHotkey.dll like this:

Code: Select all

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

; specify the path to AutoHotkey.dll
dllpath := A_ScriptDir . "\AutoHotkeyDll\" . (A_PtrSize = 8 ? "x64w" : "Win32w") . "\AutoHotkey.dll"
if !FileExist(dllpath) {
   MsgBox, AutoHotkey.dll not found
   ExitApp
}
if GetModuleBitness(dllpath) != A_PtrSize*8 {
   MsgBox, AutoHotkey.dll has incorrect bit depth
   ExitApp
}

DllCall("LoadLibrary", "Str", dllpath)
DllCall(dllpath . "\ahktextdll", "Str", GetScript(), "Str", "", "CDecl")
OnMessage(0x4A, "WM_COPYDATA_READ")
Return

GuiClose() {
   ExitApp
}

SetInfo(data) {
   info := StrSplit(data[1], "|")
   GuiControl,, Static3, % Round( info[2]*100 )
   GuiControl,, Static4, % info[1]
}

WM_COPYDATA_READ(wp, lp) {
   static info := [], timer := Func("SetInfo").Bind(info)
   info[1] := StrGet(NumGet(lp + A_PtrSize*2), "UTF-16")
   SetTimer, % timer, -10
   Return true
}

GetModuleBitness(filePath)  {
   static MZ := 0x5A4D, type := {0x8664: 64, 0x14C: 32}
   if !oFile := FileOpen(filePath, "r", "cp0")
      throw Exception("Can't open file")
   Loop 1  {
      if !(oFile.ReadUShort() = MZ || res := 0)
         break
      oFile.Pos := 0x3C
      PEoffset := oFile.ReadUInt()
      oFile.Pos := PEoffset + 4
      PE := oFile.ReadUShort()
      res := type[PE]
   }
   oFile.Close()
   Return res
}

GetScript() {
   script =
   (
      #NoTrayIcon
      #Persistent
      SetBatchLines, -1
      Critical
      
      IAudioEndpointVolume := VA_GetAudioEndpointVolume("playback")
      IAudioEndpointVolumeCallback := IAudioEndpointVolumeCallback_Create(IAudioEndpointVolume)
      VA_IAudioEndpointVolume_RegisterControlChangeNotify(IAudioEndpointVolume, IAudioEndpointVolumeCallback)
      OnExit( Func("Clear").Bind(IAudioEndpointVolume, IAudioEndpointVolumeCallback) )
      Return

      Clear(IAudioEndpointVolume, IAudioEndpointVolumeCallback) {
         VA_IAudioEndpointVolume_UnregisterControlChangeNotify(IAudioEndpointVolume, 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( RegisterCallback("IAudioEndpointVolumeCallback_" . v, "F"), 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) {
         SendString( NumGet(pNotify + 16, "UInt") . "|" . NumGet(pNotify + 20, "Float"), %A_ScriptHwnd% )
         Return 0
      }
      
      SendString(string, hWnd)  {
         VarSetCapacity(message, size := StrPut(string, "UTF-16")*2, 0)
         StrPut(string, &message, "UTF-16")
         
         VarSetCapacity(COPYDATASTRUCT, A_PtrSize*3)
         NumPut(size, COPYDATASTRUCT, A_PtrSize, "UInt")
         NumPut(&message, COPYDATASTRUCT, A_PtrSize*2)
         
         DllCall("SendMessage", "Ptr", hWnd, "UInt", WM_COPYDATA := 0x4A, "Ptr", 0, "Ptr", &COPYDATASTRUCT)
      }
   )
   Return script
}
Last edited by teadrinker on 04 Dec 2019, 23:35, edited 3 times in total.
User avatar
JoeWinograd
Posts: 1353
Joined: 10 Feb 2014, 20:00

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

04 Dec 2019, 20:12

Hi teadrinker,
I prefer to use a script that does not need AutoHotkey.dll. Any chance of getting the prior script to work without freezing? Thanks, Joe
teadrinker
Posts: 1079
Joined: 29 Mar 2015, 09:41
Contact:

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

04 Dec 2019, 20:22

The problem is that IAudioEndpointVolumeCallback works in another thread. If this thread intersects with the script's main thread, this may cause crash or freeze. You could using another script instead of AutoHotkey.dll according to the same principle.
User avatar
JoeWinograd
Posts: 1353
Joined: 10 Feb 2014, 20:00

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

06 Dec 2019, 18:14

teadrinker wrote:If this thread intersects with the script's main thread, this may cause crash or freeze.
Thanks for explaining that. I integrated your previous code (not the AutoHotkey.dll version) into my horizontal-volume-slider script and I see what you mean about crashes and freezes...I've gotten both, although infrequently. I'd like to experiment your AutoHotkey.dll version...where can I get the AutoHotkey.dll that you use?
teadrinker wrote:You could using another script instead of AutoHotkey.dll according to the same principle.
I'd like to experiment with that, too, but I don't understand exactly what you mean. Please explain a little more. Thanks very much!

Regards, Joe
teadrinker
Posts: 1079
Joined: 29 Mar 2015, 09:41
Contact:

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

06 Dec 2019, 18:47

JoeWinograd wrote: where can I get the AutoHotkey.dll that you use?
You could get it (or them for different bitness) from here.
JoeWinograd wrote: I don't understand exactly what you mean. Please explain a little more.
I meant this:

Code: Select all

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

PID := ExecScript( GetScript() )
OnExit( Func("CloseChildScript").Bind(PID) )
OnMessage(0x4A, "WM_COPYDATA_READ")
Return

CloseChildScript(PID) {
   static WM_COMMAND := 0x111, ID_FILE_TERMINATESCRIPT := 65405
   DetectHiddenWindows, On
   PostMessage, WM_COMMAND, ID_FILE_TERMINATESCRIPT,,, ahk_pid %PID%
}

GuiClose() {
   ExitApp
}

SetInfo(data) {
   info := StrSplit(data[1], "|")
   GuiControl,, Static3, % Round( info[2]*100 )
   GuiControl,, Static4, % info[1]
}

WM_COPYDATA_READ(wp, lp) {
   static info := [], timer := Func("SetInfo").Bind(info)
   info[1] := StrGet(NumGet(lp + A_PtrSize*2), "UTF-16")
   SetTimer, % timer, -10
   Return true
}

ExecScript(script, exePath := "") {
   (!exePath && exePath := A_AhkPath)
   shell := ComObjCreate("WScript.Shell")
   exec := shell.Exec(exePath . " *")
   exec.StdIn.Write(script)
   exec.StdIn.Close()
   return exec.ProcessID
}

GetScript() {
   script =
   (
      #NoTrayIcon
      #Persistent
      SetBatchLines, -1
      Critical
      
      IAudioEndpointVolume := VA_GetAudioEndpointVolume("playback")
      IAudioEndpointVolumeCallback := IAudioEndpointVolumeCallback_Create(IAudioEndpointVolume)
      VA_IAudioEndpointVolume_RegisterControlChangeNotify(IAudioEndpointVolume, IAudioEndpointVolumeCallback)
      OnExit( Func("Clear").Bind(IAudioEndpointVolume, IAudioEndpointVolumeCallback) )
      Return

      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( RegisterCallback("IAudioEndpointVolumeCallback_" . v, "F"), 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) {
         SendString( NumGet(pNotify + 16, "UInt") . "|" . NumGet(pNotify + 20, "Float"), %A_ScriptHwnd% )
         Return 0
      }
      
      SendString(string, hWnd)  {
         VarSetCapacity(message, size := StrPut(string, "UTF-16")*2, 0)
         StrPut(string, &message, "UTF-16")
         
         VarSetCapacity(COPYDATASTRUCT, A_PtrSize*3)
         NumPut(size, COPYDATASTRUCT, A_PtrSize, "UInt")
         NumPut(&message, COPYDATASTRUCT, A_PtrSize*2)
         
         DllCall("SendMessage", "Ptr", hWnd, "UInt", WM_COPYDATA := 0x4A, "Ptr", 0, "Ptr", &COPYDATASTRUCT)
      }
   )
   Return script
}
User avatar
JoeWinograd
Posts: 1353
Joined: 10 Feb 2014, 20:00

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

06 Dec 2019, 19:19

I'm getting "Call to nonexistent function" for IAudioEndpointVolume := VA_GetAudioEndpointVolume("playback") even though the #Include VA.ahk file is there (and it works fine in the prior script). I even tried removing the #Include and putting the code in the script...same result. Here's the error dialog:

call to nonexistent function.png
call to nonexistent function.png (37.51 KiB) Viewed 238 times

Note that it says the error is on line 6, although the call is on line 56 in your script. Thanks for any insight on this. Regards, Joe
teadrinker
Posts: 1079
Joined: 29 Mar 2015, 09:41
Contact:

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

06 Dec 2019, 19:26

Add #Include VA.ahk into child script's code.

Code: Select all

   script =
   (
      #Include VA.ahk
      #NoTrayIcon
      #Persistent
      SetBatchLines, -1
      ...
User avatar
JoeWinograd
Posts: 1353
Joined: 10 Feb 2014, 20:00

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

06 Dec 2019, 21:05

Ah, thanks for that...a quick test shows that it solves the nonexistent-function-call problem. Will test it thoroughly during the weekend on W7 and W10, and let you know how it goes. Thanks very much for your help. Regards, Joe
User avatar
JoeWinograd
Posts: 1353
Joined: 10 Feb 2014, 20:00

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

11 Dec 2019, 17:12

Hi teadrinker,
Sorry for the delay in getting back to you. I tried it on W7 and am getting more frequent crashes with it than with your first code, although the crashes occur only when my horizontal slider is on the screen while the volume is changed elsewhere, such as via the system tray speaker icon. Other than that, it works very well. My tray icon, a green (unmuted) or red (muted) music note and my tray tip (with the volume level) change immediately when the mute or volume are changed elsewhere. Thanks for your code! Regards, Joe

Return to “Ask For Help”

Who is online

Users browsing this forum: Bad husband, dave444344, Flipeador, Groot, spencer and 219 guests