Set Audio Playback Device for Process Topic is solved

Get help with using AutoHotkey (v1.1 and older) and its commands and hotkeys
User avatar
Masonjar13
Posts: 1555
Joined: 20 Jul 2014, 10:16
Location: Не Россия
Contact:

Set Audio Playback Device for Process

09 Jun 2017, 19:55

Pretty much as it sounds. I have multiple playback devices and want certain processes to use certain devices. I don't see anything in the VA library concerning this, but I could have overlooked it. I don't typically work with audio, so I'm unsure of where to start with this; what api's should I be looking at?

Example software: Audio Router
It's open source and may directly help, but I personally don't know where to start. The reason I don't want to use this software is because it's required to be running (active routing?).
OS: Windows 10 Pro | Editor: Notepad++
My Personal Function Library | Old Build - New Build
qwerty12
Posts: 468
Joined: 04 Mar 2016, 04:33
Contact:

Re: Set Audio Playback Device for Process

10 Jun 2017, 09:02

I was going to ask what's wrong with leaving the program in the background, but I see it's constantly using CPU time (probably to update its sound meters)...

What Audio Router and CheVolume (a paid program that does the same thing [and on that note if you have an ASUS motherboard, check out Sound Studio III]) both do is they load a DLL inside the process that hooks the same audio interfaces VA wraps. So if a program directly/indirectly calls IMMDeviceEnumerator::GetDefaultAudioEndpoint, the DLL - again, running inside of the process - will intercept the call and return the IMMDevice pointing to the playback device you actually want the sound to come out of. Audio Router hooks many more of these methods, just take a look inside the audio-router folder. (I'm not really sure how it works, TBH.)

I think this might actually be possible to do with AutoHotkey_H.dll and MinHook: use MinHook to hijack CoCreateInstance and then patch the vtable of the appropriate COM objects it spits out. Use the Audio Router code to determine what's actually needed. Some things you'd need to look out for:
  • How do you tell the injected AutoHotkey instance what device you want the sound outputted to? Registry/pipes/window messages etc.? Mind, HotKeyIt's InjectAhkDll does let you communicate with the injected AutoHotkey instance so this might not be such a concern.
  • Both CheVolume and Audio Router take steps to ensure that their DLLs perform operations in a thread-safe manner. Again, I don't think this is impossible in AutoHotkey_H, but this stuff isn't my forte.
Alternatively, you could just terminate the Audio Router GUI process once you've set up your redirects. I'm not sure what effect this has on the stability of the injected DLL's code, but I think redirection usually stops because the Audio Router GUI tells its injected instances to stop redirecting and unload itself from the process. By killing the GUI, you've deprived it of its chance to do that.
User avatar
Masonjar13
Posts: 1555
Joined: 20 Jul 2014, 10:16
Location: Не Россия
Contact:

Re: Set Audio Playback Device for Process

17 Jun 2017, 07:05

I presume you mean Sonic Studio III? I do in fact have an ASUS mobo, but I do not have Sonic Studio, nor am I completely sure how to obtain it. Sonic Studio in general seems to only be available via a mobo specific RealTek driver package for Windows 10, which I'm not using.

While I'm no stranger to DLL's, injection has always escaped me; InjectAhkDll always crashed the process for me.
qwerty12 wrote:Alternatively, you could just terminate the Audio Router GUI process once you've set up your redirects. I'm not sure what effect this has on the stability of the injected DLL's code, but I think redirection usually stops because the Audio Router GUI tells its injected instances to stop redirecting and unload itself from the process. By killing the GUI, you've deprived it of its chance to do that.
Are you insinuating I force-kill the GUI, or the process? Audio Router does kill the rerouting on the GUI close method. I'm not entirely sure how to destroy a GUI of a process, though.

For safety + my lack of direct device manipulation knowledge, I've decided to write a wrapper for Audio Router. I'm going to play around with it a bit more and post back with anything I can scrape up.
OS: Windows 10 Pro | Editor: Notepad++
My Personal Function Library | Old Build - New Build
qwerty12
Posts: 468
Joined: 04 Mar 2016, 04:33
Contact:

Re: Set Audio Playback Device for Process

17 Jun 2017, 09:08

Masonjar13 wrote:I presume you mean Sonic Studio III?
Oops, yes.
While I'm no stranger to DLL's, injection has always escaped me; InjectAhkDll always crashed the process for me.
From past posts, I've had success using InjectAhkDll to load a script inside Explorer.exe, Notepad and even AutoHotkey itself. Assuming it was AutoHotkey_H you were injecting, maybe an anti-virus program was interfering.
Are you insinuating I force-kill the GUI, or the process? Audio Router does kill the rerouting on the GUI close method. I'm not entirely sure how to destroy a GUI of a process, though.
Force kill the GUI. Just terminate Audio Router.exe in Task Manager or using taskkill etc.
User avatar
Masonjar13
Posts: 1555
Joined: 20 Jul 2014, 10:16
Location: Не Россия
Contact:

Re: Set Audio Playback Device for Process

17 Jun 2017, 10:28

Alright, so I've got it working, but only with the second method, which I'm not sure is foolproof..

Code: Select all

class audioRouter {
    winId:="Audio Router ahk_class Audio Router ahk_exe Audio Router.exe"
    winRoutId:="Route Audio ahk_class #32770 ahk_exe Audio Router.exe"
    path:=""
    
    __new(path){
        detectHiddenWindows,on
        detectHiddenText,on
        setControlDelay -1
        this.path:=path
    }
    
    __delete(){
        this.killIt()
    }
    
    setProcessToDevice(procName,deviceName:="Default Audio Device",method:=1){
        this.runIt()
        if(method=1)
            this._method1(procName)        
        else if(method=2)
            this._method2(procName)
        this._selectDeviceRouteWindow(deviceName)
        this.killIt()
    }

    getDeviceList(procName){
        this.runIt()
        this._method2(procName)
        deviceList:=[]
        loop {
            control,choose,% a_index,ComboBox1,% this.winRoutId
            if(errorlevel)
                break
            controlGetText,t,ComboBox1,% this.winRoutId
            deviceList.push(t)
        }
        this.killIt()
        return deviceList
    }
    
    ; internal
    _method1(procName){
        winMenuSelectItem,% "ahk_id " . this.hwnd,% "",File,Switch View
        control,chooseString,% procName,SysListView321,% "ahk_id " . this.hwnd ; doesn't work
    }
    
    _method2(procName){
        loop {
            controlGetText,cT,% "Static" . a_index,% "ahk_id " . this.hwnd
            if(errorlevel)
                break
            if(inStr(cT,procName)){
                controlClick,% "Button" . a_index+1,% "ahk_id " . this.hwnd
                controlSend,% "Button" . a_index+1,{down 3}{enter},% "ahk_id " . this.hwnd
                break
            }
        }
    }
    
    _selectDeviceRouteWindow(deviceName){
        winWait,% this.winRoutId
        control,chooseString,% deviceName,ComboBox1,% this.winRoutId
        controlClick,Button1,% this.winRoutId,,,,NA
        sleep 1000
    }
    
    runIt(){
        run,% this.path,,hide,hpid
        winWait,% this.winId
        winGet,hhwnd,id,% this.winId
        this.pid:=hpid,this.hwnd:=hhwnd
        winMove,% "ahk_id " . hhwnd,,a_screenWidth*2,a_screenHeight*2
    }
    
    killIt(){
        process,close,% this.pid
        this.pid:="",this.hwnd:=""
    }
}
It does pop up a window, inevitably, but that's not terribly important. I'd much rather use the first method, due to the not-totally-stable nature of the second method.

Code: Select all

#singleInstance force
#persistent
#include *i audioRouter Wrapper.ahk
rout:=new audioRouter("") ; path to "Audio Router.exe"
deviceList:=rout.getDeviceList("Firefox")
for i,a in deviceList
    devStr.=a . "`n"
msgbox,,Audio Device List,% devStr
;rout.setProcessToDevice("Firefox",,2)
return

esc::exitApp
Any idea how to get the first method working?

Edit:
qwerty12 wrote: From past posts, I've had success using InjectAhkDll to load a script inside Explorer.exe, Notepad and even AutoHotkey itself. Assuming it was AutoHotkey_H you were injecting, maybe an anti-virus program was interfering.
It's been quite a while, I can't really remember what I was doing.. :eh: I'll look into it again perhaps, I could certainly make use of it for a number of things.
OS: Windows 10 Pro | Editor: Notepad++
My Personal Function Library | Old Build - New Build
qwerty12
Posts: 468
Joined: 04 Mar 2016, 04:33
Contact:

Re: Set Audio Playback Device for Process

17 Jun 2017, 11:08

Masonjar13 wrote:Any idea how to get the first method working?
GUI stuff really isn't my thing, but it seems Audio Router uses a list view for that page, which Control doesn't mention anywhere so I assume it doesn't work there.

You could try running your script as admin and using something like ControlGet to get the value of the selected item in the first column and then use ControlSend (for lack of a better idea) to advance onto the next option until you hit the right process name. https://autohotkey.com/boards/viewtopic.php?t=3513 might also work, I don't know.
User avatar
Masonjar13
Posts: 1555
Joined: 20 Jul 2014, 10:16
Location: Не Россия
Contact:

Re: Set Audio Playback Device for Process

18 Jun 2017, 08:44

The link unfortunately won't get me any handle for them, and I'd still have to loop through them, so I went with the basic method. Either way though, the only way to activate the item is to double-click it. Meaning, I'll need to get the position of the ListView item. I came up with using LVM_GETITEMPOSITION, but it doesn't seem to be working. I believe I have the wparam wrong, but I'm not sure what exactly it wants as an index..

I could also go with taking the item height * position#, but I'm not sure how to get the height besides LVM_GETITEMRECT, which has the same wparam, so not terribly helpful.

It could also be that the message name is incorrect? I got it from here.
qwerty12 wrote:GUI stuff really isn't my thing, but it seems Audio Router uses a list view for that page, which Control doesn't mention anywhere so I assume it doesn't work there.
I was reading "ListBox" as "ListView," which is how I came up with that. My mistake

Code: Select all

class audioRouter {
    winId:="Audio Router ahk_class Audio Router ahk_exe Audio Router.exe"
    winRoutId:="Route Audio ahk_class #32770 ahk_exe Audio Router.exe"
    path:=""
    
    __new(path){
        detectHiddenWindows,on
        detectHiddenText,on
        setControlDelay -1
        this.path:=path
    }
    
    __delete(){
        this.killIt()
    }
    
    setProcessToDevice(procName,deviceName:="Default Audio Device",method:=1){
        this.runIt()
        if(method=1)
            this._method1(procName)        
        else if(method=2)
            this._method2(procName)
        this._selectDeviceRouteWindow(deviceName)
        this.killIt()
    }

    getDeviceList(procName){
        this.runIt()
        this._method2(procName)
        deviceList:=[]
        loop {
            control,choose,% a_index,ComboBox1,% this.winRoutId
            if(errorlevel)
                break
            controlGetText,t,ComboBox1,% this.winRoutId
            deviceList.push(t)
        }
        this.killIt()
        return deviceList
    }
    
    ; internal
    _method1(procName){
        lvc:="SysListView321"
        lvmIndex:=0
        winMenuSelectItem,% "ahk_id " . this.hwnd,% "",File,Switch View
        while(!lvh)
            controlGet,lvh,Hwnd,,% lvc,% this.winId
        controlFocus,% lvc,% this.winId
        loop {
            controlSend,,{down},% "ahk_id " . lvh
            controlGet,lvT,list,focused,,% "ahk_id " . lvh
            lvmIndex++
            msgbox % procName "`n" lvT "`n" inStr(lvT,procName)
        }until lvT&&inStr(lvT,procName)

        pos:=this.LVM_GETITEMPOSITION(lvmIndex,lvh)
        msgbox % pos.x "`n" pos.y
        postMessage,0x203,0,pos.x&0xFFFF | pos.y<<16,,% "ahk_id " . lvh ; WM_LBUTTONDBLCLCK 
        ;control,chooseString,% procName,SysListView321,% "ahk_id " . this.hwnd ; doesn't work
        
    }
    
    _method2(procName){
        loop {
            controlGetText,cT,% "Static" . a_index,% "ahk_id " . this.hwnd
            if(errorlevel)
                break
            if(inStr(cT,procName)){
                controlClick,% "Button" . a_index+1,% "ahk_id " . this.hwnd
                controlSend,% "Button" . a_index+1,{down 3}{enter},% "ahk_id " . this.hwnd
                break
            }
        }
    }
    
    _selectDeviceRouteWindow(deviceName){
        winWait,% this.winRoutId
        control,chooseString,% deviceName,ComboBox1,% this.winRoutId
        controlClick,Button1,% this.winRoutId,,,,NA
        sleep 1000
    }
    
    runIt(debug:=0){
        run,% this.path,,% debug?"":"hide",hpid
        winWait,% this.winId
        winGet,hhwnd,id,% this.winId
        this.pid:=hpid,this.hwnd:=hhwnd
        if(!debug)
            winMove,% "ahk_id " . hhwnd,,a_screenWidth*2,a_screenHeight*2
    }
    
    killIt(){
        process,close,% this.pid
        this.pid:="",this.hwnd:=""
    }
    
    ; other
    LVM_GETITEMPOSITION(itemIndex,hwnd){
        LVM_GETITEMPOSITION:=0x1000+16
        varSetCapacity(point,8,0)
        sendMessage,% LVM_GETITEMPOSITION,% itemIndex,&point,,% "ahk_id " . hwnd
        return {x: numGet(point,0,"UInt"),y: numGet(point,4,"UInt")}
    }
}

Code: Select all

#singleInstance force
#persistent
#include *i audioRouter Wrapper.ahk

rout:=new audioRouter("Audio Router.exe")
rout.runIt(1)
rout._method1("Firefox")
return
OS: Windows 10 Pro | Editor: Notepad++
My Personal Function Library | Old Build - New Build
User avatar
Masonjar13
Posts: 1555
Joined: 20 Jul 2014, 10:16
Location: Не Россия
Contact:

Re: Set Audio Playback Device for Process

18 Jun 2017, 09:14

Oh yes, and if anyone could come up with a better method of getting the device list, synonymous with Audio Router, that would be appreciated!
OS: Windows 10 Pro | Editor: Notepad++
My Personal Function Library | Old Build - New Build
qwerty12
Posts: 468
Joined: 04 Mar 2016, 04:33
Contact:

Re: Set Audio Playback Device for Process

18 Jun 2017, 09:23

Masonjar13 wrote:I came up with using LVM_GETITEMPOSITION, but it doesn't seem to be working. I believe I have the wparam wrong, but I'm not sure what exactly it wants as an index..
LVM_GETITEMPOSITION expects a pointer to a POINT structure in the process' address space itself. Try something like this:

Code: Select all

#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.

full_command_line := DllCall("GetCommandLine", "str")

if not (A_IsAdmin or RegExMatch(full_command_line, " /restart(?!\S)"))
{
    try
    {
        if A_IsCompiled
            Run *RunAs "%A_ScriptFullPath%" /restart
        else
            Run *RunAs "%A_AhkPath%" /restart "%A_ScriptFullPath%"
    }
    ExitApp
}

try {
	itemIdx := 0

	if (!(hwnd := WinExist("Audio Router ahk_class Audio Router ahk_exe Audio Router.exe")))
		throw
	ControlGet, SysListView321, Hwnd,, SysListView321, ahk_id %hwnd%
	WinGet PID, PID, ahk_id %hwnd%
	
	if (!(hProcess := DllCall("OpenProcess", "UInt", PROCESS_QUERY_INFORMATION := 0x0400 | PROCESS_VM_OPERATION := 0x0008 | PROCESS_VM_READ := 0x0010 | PROCESS_VM_WRITE := 0x0020, "Int", False, "UInt", PID, "Ptr"))) ; open handle to process 
		throw

	if (!(remotePoint := DllCall("VirtualAllocEx", "Ptr", hProcess, "Ptr", 0, "Ptr", 8, "UInt", MEM_COMMIT := 0x00001000, "UInt", PAGE_READWRITE := 0x04, "Ptr"))) ; allocate memory inside process
		throw

	if (!DllCall("SendMessage", "Ptr", SysListView321, "UInt", LVM_GETITEMPOSITION := 0x1000+16, "Ptr", itemIdx, "Ptr", remotePoint, "Ptr")) ; because I find AHK's built-in clunkier
		throw

	VarSetCapacity(point, 8)
	if (!DllCall("ReadProcessMemory", "Ptr", hProcess, "Ptr", remotePoint, "Ptr", &point, "Ptr", 8, "Ptr*", br) || br != 8) ; read point structure out from process and save into local copy
		throw
	MsgBox % "x: " . NumGet(point, 0, "Int") . ", y: " . NumGet(point, 4, "Int")
} finally {
	if (hProcess) {
		if (remotePoint)
			DllCall("VirtualFreeEx", "Ptr", hProcess, "Ptr", remotePoint, "Ptr", 0, "UInt", MEM_RELEASE := 0x8000) ; free allocated memory in remote process

		DllCall("CloseHandle", "Ptr", hProcess) ; close handle to remote process
	}
}
User avatar
Masonjar13
Posts: 1555
Joined: 20 Jul 2014, 10:16
Location: Не Россия
Contact:

Re: Set Audio Playback Device for Process  Topic is solved

18 Jun 2017, 11:30

That makes SO much more sense! I've adapted your code into the object and it's all working as expected. Thank you for all your help, qwerty12! It's not quite as nice as doing it natively, but it's within my own ability (with help), anyway.

Code: Select all

/*
Methods:
    setProcessToDevice(ProcessName,DeviceName:="Default Audio Device")
      This sets the specified process to be used with the specified
      audio playback device.
    
    getDeviceList()
      Returns an array object containing all available devices
      from Audio Router.

Returned error codes:
    setProcessToDevice()
      1 = Waiting for Audio Router window timed out.
        derivative of runIt()
      2 = Process not found in list.
        derivative of _method1()
      3 = Waiting for Route Audio window timed out.
        derivative of _selectDeviceRouteWindow()
    
    getDeviceList()
      1 = Waiting for Audio Router window timed out.
        derivative of runIt()
      2 = Process not found in list.
        derivative of _method1()
      3 = Waiting for Route Audio window timed out.
*/
class audioRouter {
    winId:="Audio Router ahk_class Audio Router ahk_exe " . (selfProc:="Audio Router.exe")
    winRoutId:="Route Audio ahk_class #32770 ahk_exe Audio Router.exe"
    path:=""
    
    __new(path){
        if(!a_isAdmin){
            msgbox,,Error,This object requires administrative privileges.
            return
        }
        if(!fileExist(path)){
            msgbox,,Error,Path doesn't exist.
            return
        }
        detectHiddenWindows,on
        detectHiddenText,on
        setControlDelay -1
        this.path:=path
        return this
    }
    
    __delete(){
        this.killIt()
    }
    
    setProcessToDevice(procName,deviceName:="Default Audio Device"){
        if(this.runIt())
            return 1
        if(!this._method1(procName))
            if(this._selectDeviceRouteWindow(deviceName))
                return 3
        else
            return 2
        
        this.killIt()
    }

    getDeviceList(){
        if(this.runIt())
            return 1
        if(this._method1(this.selfProc))
            return 2
        deviceList:=[]
        winWait,% this.winRoutId,,10
        if(errorlevel)
            return 3
        loop {
            control,choose,% a_index,ComboBox1,% this.winRoutId
            if(errorlevel)
                break
            controlGetText,t,ComboBox1,% this.winRoutId
            deviceList.push(t)
        }
        this.killIt()
        return deviceList
    }
    
    ; internal
    _method1(procName){
        lvc:="SysListView321"
        lvmIndex:=0
        winMenuSelectItem,% "ahk_id " . this.hwnd,% "",File,Switch View
        while(!lvh)
            controlGet,lvh,Hwnd,,% lvc,% this.winId
        controlFocus,% lvc,% this.winId
        while(!lvT||!inStr(lvT,procName)){
            lvtL:=lvT
            controlSend,,{down},% "ahk_id " . lvh
            controlGet,lvT,list,focused,,% "ahk_id " . lvh
            if(lvT=lvtL)
                return 1
            lvmIndex++
        }

        pos:=this.LVM_GETITEMPOSITION(lvmIndex,lvh)
        postMessage,0x203,0,pos.x&0xFFFF | pos.y<<16,,% "ahk_id " . lvh ; WM_LBUTTONDBLCLCK
    }
/*    
    _method2(procName){
        loop {
            controlGetText,cT,% "Static" . a_index,% "ahk_id " . this.hwnd
            if(errorlevel)
                break
            if(inStr(cT,procName)){
                controlClick,% "Button" . a_index+1,% "ahk_id " . this.hwnd
                controlSend,% "Button" . a_index+1,{down 3}{enter},% "ahk_id " . this.hwnd
                break
            }
        }
    }
*/    
    _selectDeviceRouteWindow(deviceName){
        winWait,% this.winRoutId,,10
        if(errorlevel)
            return 1
        control,chooseString,% deviceName,ComboBox1,% this.winRoutId
        controlClick,Button1,% this.winRoutId,,,,NA
        sleep 1000
    }
    
    runIt(debug:=0){
        run,% this.path,,% debug?"":"hide",hpid
        winWait,% this.winId,,10
        if(errorlevel)
            return 1
        winGet,hhwnd,id,% this.winId
        this.pid:=hpid,this.hwnd:=hhwnd
        if(!debug)
            winMove,% "ahk_id " . hhwnd,,a_screenWidth*2,a_screenHeight*2
    }
    
    killIt(){
        process,close,% this.pid
        this.pid:="",this.hwnd:=""
    }
    
    ; other
    LVM_GETITEMPOSITION(itemIdx,hwnd){ ;credit to qwerty12 - https://autohotkey.com/boards/viewtopic.php?p=154570#p154570
        if (!(hProcess := DllCall("OpenProcess", "UInt", PROCESS_QUERY_INFORMATION := 0x0400 | PROCESS_VM_OPERATION := 0x0008 | PROCESS_VM_READ := 0x0010 | PROCESS_VM_WRITE := 0x0020, "Int", False, "UInt", this.pid, "Ptr"))) ; open handle to process 
            throw
        if (!(remotePoint := DllCall("VirtualAllocEx", "Ptr", hProcess, "Ptr", 0, "Ptr", 8, "UInt", MEM_COMMIT := 0x00001000, "UInt", PAGE_READWRITE := 0x04, "Ptr"))) ; allocate memory inside process
            throw
        if (!DllCall("SendMessage", "Ptr", hwnd, "UInt", LVM_GETITEMPOSITION := 0x1000+16, "Ptr", itemIdx, "Ptr", remotePoint, "Ptr")) ; because I find AHK's built-in clunkier
            throw
        varSetCapacity(point,8,0)
        if (!DllCall("ReadProcessMemory", "Ptr", hProcess, "Ptr", remotePoint, "Ptr", &point, "Ptr", 8, "Ptr*", br) || br != 8) ; read point structure out from process and save into local copy
            throw
;        sendMessage,% LVM_GETITEMPOSITION,% itemIndex,&point,,% "ahk_id " . hwnd ; ,% "ahk_id " . hwnd
        itemPos:={x: numGet(point,0,"Int"),y: numGet(point,4,"Int")}
        if (hProcess) {
            if (remotePoint)
                DllCall("VirtualFreeEx", "Ptr", hProcess, "Ptr", remotePoint, "Ptr", 0, "UInt", MEM_RELEASE := 0x8000) ; free allocated memory in remote process
            DllCall("CloseHandle", "Ptr", hProcess) ; close handle to remote process
        }
        return itemPos
    }
}
OS: Windows 10 Pro | Editor: Notepad++
My Personal Function Library | Old Build - New Build
qwerty12
Posts: 468
Joined: 04 Mar 2016, 04:33
Contact:

Re: Set Audio Playback Device for Process

18 Jun 2017, 12:07

No problem, Masonjar13!

For device names, using Lexikos' VA library might work (the only additional output device I get on this basic laptop of mine is for the headphones, so I can't verify if the order will match on proper desktop systems with all sorts of audio jacks):

Code: Select all

audioDeviceNames := ["Default Audio Device"]
i := 1
while ((device := VA_GetDevice("playback:" . i))) {
	audioDeviceNames.Push(VA_GetDeviceName(device))
	ObjRelease(device)
	i += 1
}

;for _, name in audioDeviceNames
;	MsgBox %name%
User avatar
Masonjar13
Posts: 1555
Joined: 20 Jul 2014, 10:16
Location: Не Россия
Contact:

Re: Set Audio Playback Device for Process

18 Jun 2017, 13:24

VA is what I was thinking, too, but I wasn't sure exactly how to do it. I've tested it on my desktop and it matches the list in Audio Router identically. Worst case is that someone attempts to use one that doesn't show up in Audio Router and it feeds back a safe error. It's significantly faster anyways, and doesn't pop up a window. The fewer windows it has to pop up the better ;)

Added that in, and fixed a few other problems.

Code: Select all

#include <VA>

/*
Methods:
    setProcessToDevice(ProcessName,DeviceName:="Default Audio Device")
      This sets the specified process to be used with the specified
      audio playback device.
    
    getDeviceList()
      Returns an array object containing all available playback
      devices.

Returned error codes:
    setProcessToDevice()
      Returns string defining the error.
*/

class audioRouter {
    winId:="Audio Router ahk_class Audio Router ahk_exe Audio Router.exe"
    winRoutId:="Route Audio ahk_class #32770 ahk_exe Audio Router.exe"
    path:=""
    
    __new(path){
        if(!a_isAdmin){
            msgbox,,Error,This object requires administrative privileges.
            return
        }
        if(!fileExist(path)){
            msgbox,,Error,Path doesn't exist.
            return
        }
        detectHiddenWindows,on
        detectHiddenText,on
        setControlDelay -1
        this.path:=path
        return this
    }
    
    __delete(){
        this.killIt()
    }
    
    setProcessToDevice(procName,deviceName:="Default Audio Device"){
        if(this.runIt())
            return "Waiting for Audio Router window timed out."
        if(this._method1(procName)){
            this.killIt()
            return "Process not found in list."
        }
        if(this._selectDeviceRouteWindow(deviceName)){
            this.killIt()
            return "Waiting for Route Audio window timed out."
        }
        this.killIt()
    }

    getDeviceList(){ ;credit to qwerty12 - https://autohotkey.com/boards/viewtopic.php?p=154595#p154595
        audioDevices:=["Default Audio Device"],i:=1
        while(device:=VA_GetDevice("playback:" . i++))
            audioDevices.push(VA_GetDeviceName(device)),ObjRelease(device)
        return audioDevices
    }
    
    ; internal
    _method1(procName){
        lvc:="SysListView321"
        lvmIndex:=0
        winMenuSelectItem,% "ahk_id " . this.hwnd,% "",File,Switch View
        while(!lvh)
            controlGet,lvh,Hwnd,,% lvc,% this.winId
        controlGet,lvx,list,count,,% "ahk_id " . lvh
        controlFocus,,% "ahk_id " . lvh
        while(!lvT||!inStr(lvT,procName)){
            controlSend,,{down},% "ahk_id " . lvh
            controlGet,lvT,list,focused,,% "ahk_id " . lvh
            if(++lvmIndex>lvx)
                return 1
        }

        pos:=this.LVM_GETITEMPOSITION(lvmIndex,lvh)
        postMessage,0x203,0,pos.x&0xFFFF | pos.y<<16,,% "ahk_id " . lvh ; WM_LBUTTONDBLCLCK
    }
    
    _selectDeviceRouteWindow(deviceName){
        winWait,% this.winRoutId,,10
        if(errorlevel)
            return 1
        control,chooseString,% deviceName,ComboBox1,% this.winRoutId
        controlClick,Button1,% this.winRoutId,,,,NA
        sleep 1000
    }
    
    runIt(debug:=0){
        run,% this.path,,% debug?"":"hide",hpid
        winWait,% this.winId,,10
        if(errorlevel)
            return 1
        winGet,hhwnd,id,% this.winId
        this.pid:=hpid,this.hwnd:=hhwnd
        if(!debug){
            sysGet,width,78
            sysget,height,79
            winMove,% "ahk_id " . hhwnd,,width,height
        }
    }
    
    killIt(){
        process,close,% this.pid
        this.pid:="",this.hwnd:=""
    }
    
    ; other
    LVM_GETITEMPOSITION(itemIdx,hwnd){ ;credit to qwerty12 - https://autohotkey.com/boards/viewtopic.php?p=154570#p154570
        if (!(hProcess := DllCall("OpenProcess", "UInt", PROCESS_QUERY_INFORMATION := 0x0400 | PROCESS_VM_OPERATION := 0x0008 | PROCESS_VM_READ := 0x0010 | PROCESS_VM_WRITE := 0x0020, "Int", False, "UInt", this.pid, "Ptr"))) ; open handle to process 
            throw
        if (!(remotePoint := DllCall("VirtualAllocEx", "Ptr", hProcess, "Ptr", 0, "Ptr", 8, "UInt", MEM_COMMIT := 0x00001000, "UInt", PAGE_READWRITE := 0x04, "Ptr"))) ; allocate memory inside process
            throw
        if (!DllCall("SendMessage", "Ptr", hwnd, "UInt", LVM_GETITEMPOSITION := 0x1000+16, "Ptr", itemIdx, "Ptr", remotePoint, "Ptr")) ; because I find AHK's built-in clunkier
            throw
        varSetCapacity(point,8,0)
        if (!DllCall("ReadProcessMemory", "Ptr", hProcess, "Ptr", remotePoint, "Ptr", &point, "Ptr", 8, "Ptr*", br) || br != 8) ; read point structure out from process and save into local copy
            throw
;        sendMessage,% LVM_GETITEMPOSITION,% itemIndex,&point,,% "ahk_id " . hwnd ; ,% "ahk_id " . hwnd
        itemPos:={x: numGet(point,0,"Int"),y: numGet(point,4,"Int")}
        if (hProcess) {
            if (remotePoint)
                DllCall("VirtualFreeEx", "Ptr", hProcess, "Ptr", remotePoint, "Ptr", 0, "UInt", MEM_RELEASE := 0x8000) ; free allocated memory in remote process
            DllCall("CloseHandle", "Ptr", hProcess) ; close handle to remote process
        }
        return itemPos
    }
    
/*  deprecated

    getDeviceList(){
        if(this.runIt())
            return 1
        if(this._method1(this.selfProc))
            return 2
        deviceList:=[]
        winWait,% this.winRoutId,,10
        if(errorlevel)
            return 3
        loop {
            control,choose,% a_index,ComboBox1,% this.winRoutId
            if(errorlevel)
                break
            controlGetText,t,ComboBox1,% this.winRoutId
            deviceList.push(t)
        }
        this.killIt()
        return deviceList
    }
    
    _method2(procName){
        loop {
            controlGetText,cT,% "Static" . a_index,% "ahk_id " . this.hwnd
            if(errorlevel)
                break
            if(inStr(cT,procName)){
                controlClick,% "Button" . a_index+1,% "ahk_id " . this.hwnd
                controlSend,% "Button" . a_index+1,{down 3}{enter},% "ahk_id " . this.hwnd
                break
            }
        }
    }
*/
}
OS: Windows 10 Pro | Editor: Notepad++
My Personal Function Library | Old Build - New Build
komrad
Posts: 2
Joined: 12 Nov 2020, 18:49

Re: Set Audio Playback Device for Process

13 Nov 2020, 14:00

Hey Masonjar13 & qwerty12

Your code looks like exactly what I'm looking for! I tried to make it work but had some trouble.
Could you help me get it to run?

I want to set the playback device for, let's say, vlc.exe I wrote the following:

Code: Select all


#include %A_ScriptDir%\audio.ahk  ; your script from the last post, ( I have VA included as well) 

myAudio := new audioRouter
myAudio.setProcessToDevice("vlc.exe")
...and sadly, nothing happens. At first, the window from Audio Router would not show at all, the process would start but the window would not show at all. I managed to fix that, but from here I am lost...

I know this is an older post, but if you are still using this script, please let me know how to make it run! I'd love to use it too.
User avatar
Masonjar13
Posts: 1555
Joined: 20 Jul 2014, 10:16
Location: Не Россия
Contact:

Re: Set Audio Playback Device for Process

13 Nov 2020, 14:50

@komrad, the whole point of this is to hide the window, so of course you wouldn't see it. If you want a window, this class is not for you.

In Windows 10, they added native functionality. Open up explorer and go to ms-settings:apps-volume, you can change everything in there. This is what I use now, as it's significantly more stable and usable than Audio Router. I haven't found an API for it as of now though, so I haven't been able to automate it.
OS: Windows 10 Pro | Editor: Notepad++
My Personal Function Library | Old Build - New Build
komrad
Posts: 2
Joined: 12 Nov 2020, 18:49

Re: Set Audio Playback Device for Process

13 Nov 2020, 15:17

@Masonjar13

hey! Thanks for the quick reply.

I actually don't want the window, it's just that I kept getting the error ""Waiting for Audio Router window timed out." So naturally, I thought a window should appear, at least for a split second. My bad.

I'll take a look at "ms-settings:apps-volume". Thank you for the suggestion.

Also, do you know, is there an issue with WinMenuSelectItem? I wanted to try and make a small hotkey that would just switch to the different list view in Audio Router, and for some reason, "WinMenuSelectItem ahk_exe Audio Router.exe,,File, Switch View" does not seem to work.
I mean, fair enough, in the docs they mentioned it might not work for every menu.

Thanks again!

Return to “Ask for Help (v1)”

Who is online

Users browsing this forum: No registered users and 78 guests