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/2021 —
BIG 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.