Re: Can an AutoHotkey script be notified of volume changing and muting/unmuting?
Posted: 11 Dec 2019, 17:13
Let's help each other out
https://www.autohotkey.com/boards/
https://www.autohotkey.com/boards/viewtopic.php?f=76&t=68846
lexikos' RegisterSyncCallback() solves that problem. This code works well without dll and another script:teadrinker wrote: ↑The problem is that IAudioEndpointVolumeCallback works in another thread.
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, ¶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*)
}
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
}
}
}
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.