Problems with global hotkey for Tidal

Get help with using AutoHotkey (v1.1 and older) and its commands and hotkeys
roundedge
Posts: 8
Joined: 12 Jan 2020, 09:53

Problems with global hotkey for Tidal

23 May 2020, 06:01

I have been using hotkeys I have found online for start/stop on Winamp and Spotify.

I tried using these but replacing ahk_class, but I cant get it working on Tidal.

Winamp

Code: Select all

PrintScreen::
IfWinNotExist ahk_class Winamp v1.x
    return
ControlSend, ahk_parent, c  ; Pause/Unpause
return
Spotify

Code: Select all

Scrolllock::
DetectHiddenWindows On
PostMessage, 0x319,, 0xE0000,, ahk_class Chrome_WidgetWin_0
return   

I found this for Tidal but it only works when window is active

Code: Select all

f8::
DetectHiddenWindows, On
WinGet, winInfo, List, ahk_exe Tidal.exe
Loop, %winInfo%
{
thisID := winInfo%A_Index%
ControlFocus , , ahk_id %thisID%
ControlSend, , {space}, ahk_id %thisID%
}
return
[Mod edit: [code][/code] tags added]

I want it to work when it isnt in focus.
The ahk_class for Tidal is Chrome_WidgetWin_1
Meroveus
Posts: 44
Joined: 23 May 2016, 17:38

Re: Problems with global hotkey for Tidal

23 May 2020, 10:32

Does Tidal have more than one window?
If it does, then your code sends a space to all of them.
if there is only one, and always only one then

Code: Select all

f8::
	DetectHiddenWindows, On
	Tidal_ID := WinExist("ahk_exe Tidal.exe")
	If Tidal_ID {
		SetKeyDelay,-1,50
		ControlSend,ahk_parent, {space}, ahk_id %Tidal_ID%
	)
return
I have never tried sending keystrokes to hidden windows, so I don't know what would happen
Note that DetectHiddenWindows (https://www.autohotkey.com/docs/commands/DetectHiddenWindows.htm)
Isn't needed to find minimized windows

Setting a key delay and using ahk_parent helped me send hotkeys to a different program.
Experiment with the numbers
See https://www.autohotkey.com/docs/commands/ControlSend.htm
https://www.autohotkey.com/docs/commands/SetKeyDelay.htm

I avoid using 'this' as part of a variable name, since if you ever write classes, 'this' is used to reference class members and your code can get real confusing real fast.
roundedge
Posts: 8
Joined: 12 Jan 2020, 09:53

Re: Problems with global hotkey for Tidal

24 May 2020, 15:59

There is only one window. I havent made any of the code I posted earlier.

I found a way to solve it, but I want to find a solution where the window can stay minimized in the background and not pop up.

Code: Select all

Scrolllock::
   WinActivate, ahk_class Chrome_WidgetWin_1
   Send {space}
   WinMinimize, ahk_class Chrome_WidgetWin_1
return
Inserio
Posts: 5
Joined: 29 Jul 2018, 14:49

Re: Problems with global hotkey for Tidal

09 Mar 2021, 22:47

I ended up using Acc.ahk to accomplish this.

I've changed the logic of this to search for the now playing panel dynamically, instead of relying on hardcoded paths that appeared to break after a version update. It's also a bit more optimized, both by using a custom breadth first search, since the media buttons are near the top of the tree, and by storing the value of nowPlayingPanel in a static variable that only gets re-assigned when it doesn't exist or fails to find the object.
You can uncomment the commented line in TidalMediaButton and comment the line below it if you'd rather use the current hardcoded value (as of this edit), but it's not any noticeably faster and the speed only matters on the first use, as mentioned above. Again, this still requires opening Inspect.exe, as mentioned at the end, but I'm still trying to figure out a way around that.

Made a quick update to make the stored value the now playing panel, since that allows all of the keys to be able to be used instead of just the first one pressed.

4/13/2021 — Made a change to also cache each button in an array so it's only searched the first time that command is done or when its parent is not found. This ended up fixing an intermittent issue I had when using Play_Pause, since the Play button turns into the Pause button while music is playing and vice-versa; therefore performing its default action will work as intended.

4/15/2021BIG NEWS, I figured out how to get it work without using Inspect.exe. It has to do with SPI_SCREENREADER if you're curious. In any case, I've updated the function to set screenreader on for the duration of the search, then revert to its previous value before returning. I had to set the search to a while loop in order to deal with the case of Tidal just being opened. Fortunately, since those values are stored between calls, subsequent uses will be very quick.

4/16/2021 — Improved the logic of the function to only set the screen reader state on when it's necessary to do so. Also enclosed that section in a try/finally in order to ensure the value gets set back regardless of errors.

After heavily adapting it to get the objects to return the properties as they're supposed to, my ultimate function looks like this.

Code: Select all

TidalMediaButton(command) {
	static nowPlayingPanel := ""
	static getPlayerButtons := Func("GetNowPlayingScreen")
	static getButton := Func("GetAccButton")
	static playerButtons := []
	static SPIF_SENDCHANGE := 0x02 ; Let running applications know of the change
	static SPI_GETSCREENREADER := 0x0046
	static SPI_SETSCREENREADER := 0x0047
	switch (command) {
		case "Play_Pause":
			func_value := ["Pause","Play"]
		case "Prev":
			func_value := "Previous"
		case "Play":
			func_value := "Play"
		case "Pause":
			func_value := "Pause"
		case "Next":
			func_value := "Next"
		case "Stop":
			func_value := "Pause"
		case "Shuffle":
			func_value := "Shuffle"
		case "Block":
			func_value := "Block"
		Default:
			func_value := ""
	}
	If (!nowPlayingPanel || !Acc_Parent(nowPlayingPanel) || !playerButtons[command] || !Acc_Parent(playerButtons[command])) {
		nowPlayingPanel := Acc_BreadthFirstSearch(Acc_ObjectFromWindow(TidalID), getPlayerButtons, , true).Pop()
		playerButtons[command] := Acc_BreadthFirstSearch(nowPlayingPanel, getButton, func_value, true).Pop()
	}
	If (!playerButtons[command] || !Acc_Parent(playerButtons[command])) {
		VarSetCapacity(scn, A_PtrSize), NumPut(0, scn, 0, "Ptr") ; Store current screenreader state
		Try {
			DllCall("SystemParametersInfo", "UInt", SPI_GETSCREENREADER, "UInt", 0, "Ptr", &scn, "UInt", 0) ; Get current screenreader state. Remove this line to always revert to off
			DllCall("SystemParametersInfo", "UInt", SPI_SETSCREENREADER, "UInt", 1, "Ptr", 0, "UInt", SPIF_SENDCHANGE) ; Set screenreader on
			While (!playerButtons[command] || !Acc_Parent(playerButtons[command])) {
				; Need to wait for the tree to enumerate the first time the panel is found
				nowPlayingPanel := Acc_BreadthFirstSearch(Acc_ObjectFromWindow(TidalID), getPlayerButtons, , true).Pop()
				playerButtons[command] := Acc_BreadthFirstSearch(nowPlayingPanel, getButton, func_value, true).Pop()
			}
		} Finally {
			DllCall("SystemParametersInfo", "UInt", SPI_SETSCREENREADER, "UInt", NumGet(scn, "Ptr"), "Ptr", 0, "UInt", SPIF_SENDCHANGE) ; Revert screenreader mode to previous value
		}
	}
	If (playerButtons[command]) {
		Acc_DoDefaultAction(playerButtons[command])
	}
	DllCall("SystemParametersInfo", "UInt", SPI_SETSCREENREADER, "UInt", NumGet(scn, "Ptr"), "Ptr", 0, "UInt", SPIF_SENDCHANGE) ; Revert screenreader mode to prior value
}
GetNowPlayingScreen(oAcc) {
	If (InStr(Acc_Name(oAcc), "toggle now playing screen")) {
		Return true
	}
}
GetAccButton(oAcc, names) {
	If (Trim(Acc_Role(oAcc)) = "push button"){
		If (!names.Length()) {
			If (Trim(Acc_Name(oAcc)) == names)
				Return true
		} Else {
			Loop % names.Length()
			{
				If (Trim(Acc_Name(oAcc)) == names[A_Index])
					Return true
			}
		}
	}
}
I added a few of my own functions to Acc.ahk, which are used above, so I'll include them below:

Code: Select all

IsArray(obj) {
	try return !!obj.MaxIndex()
}
Acc_RecurseChildren(oAcc, childArray*) {
    Loop % childArray.Length()
    {
        oAcc := Acc_Child(oAcc, childArray[A_Index])
    }
    Return oAcc
}

Acc_BreadthFirstSearch(oAcc, fCondition, values:=0, stopOnFirstMatch:=false) {
    results := []
    If !IsFunc(fCondition)
        return
    If (!IsArray(oAcc)) {
        If (fCondition.call(oAcc, values)) {
            results.Push(oAcc)
            If (stopOnFirstMatch)
                return results
        }
        items := Acc_Children(oAcc)
        If (items.Length() > 0) {
            For i,item in items
            {
                If (fCondition.call(item, values)) {
                    results.Push(item)
                    If (stopOnFirstMatch)
                        return results
                }
            }
            return Acc_BreadthFirstSearch(items, fCondition, values, stopOnFirstMatch)
        }
    } Else {
        For i,item in oAcc
        {
            For j,recurse in Acc_BreadthFirstSearch(item, fCondition, values, stopOnFirstMatch)
            {
                results.Push(recurse)
                If (stopOnFirstMatch)
                    return results
            }
        }
    }
    return results
}

Acc_Name(oAcc, ChildId=0) {
    try {
        return oAcc.accName(ChildId)
    } catch
        return ""
}
Acc_DoDefaultAction(oAcc, ChildId=0) {
    try {
        return oAcc.accDoDefaultAction(ChildId)
    } catch
        return ""
}
As a side note, I used AccExplorer 2.0 in order to determine the numbers to use for Acc_RecurseChildren

EDIT:
It appears that after exiting and reopening the Tidal (or rebooting), I have to first run Inspect.exe in order for it to be able to find the push buttons required.
I don't remember where I first got Inspect.exe, but its File description is Active Accessibility Object Inspector (32-bit UNICODE Release), File version is 7.0.0.0, Product name is Microsoft Active Accessibility, and it has the MD5 hash of 4cd26901b59d5613037c56e8fc369d65.
Simply running it and attempting to launch the script once appears to be all that's necessary, however. After that, you can close Inspect.exe and this Ahk script will continue to function until Tidal is closed. If I find a workaround for this I'll update this post.

Return to “Ask for Help (v1)”

Who is online

Users browsing this forum: bobstoner289, peter_ahk, Spawnova and 355 guests