Linux Voice Activated Macro
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

335 lines
10 KiB

Fixes & implementation of playing audio files. See changes.md for details. - hacked profileexcutor to reload command list when changing it. it did not do that, so changing the profile had no effect at all. - hacked profileexecutor to somewhat make use of "Enable listening". It was an option without functionality at all until now. - created new directory 'voicepacks', copy HCS voicepacks here - new command to play a sound - new gui to select sounds from voicepacks - created playsound class that reads voicepack files and plays audio files with ffplay - added alternate keypress handling using xdotool, which won't require root privileges. might not work for any key existing in profiles, as some need to be remapped. added some remappings to profileexecutor.py pressKey() - added basic command line argument reading to set some configs. right now there: -noroot - will enable xdotool usage for keypresses -xdowindowid <windowid> - will send keypresses to this window only, makes it more relyable when window is not focused for any reason - added auto-detection of Elite Dangerous client window id if -noroot is used and no -xdowindowid is supplied. for this to work, start this script AFTER the client is already running. - monkey-wrenched a volume slider to the main window - added confirmation dialog for removing a profile (!) - copy existing profile. added a copy button to the main menu - properly shutdown audio recording stream - updated gitignore to ignore audio files
2 years ago
#!/usr/bin/python3
3 years ago
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from ui_mainwnd import Ui_MainWidget
from profileeditwnd import ProfileEditWnd
import json
from profileexecutor import ProfileExecutor
import sys
import pickle
import signal
import os
import shutil
Fixes & implementation of playing audio files. See changes.md for details. - hacked profileexcutor to reload command list when changing it. it did not do that, so changing the profile had no effect at all. - hacked profileexecutor to somewhat make use of "Enable listening". It was an option without functionality at all until now. - created new directory 'voicepacks', copy HCS voicepacks here - new command to play a sound - new gui to select sounds from voicepacks - created playsound class that reads voicepack files and plays audio files with ffplay - added alternate keypress handling using xdotool, which won't require root privileges. might not work for any key existing in profiles, as some need to be remapped. added some remappings to profileexecutor.py pressKey() - added basic command line argument reading to set some configs. right now there: -noroot - will enable xdotool usage for keypresses -xdowindowid <windowid> - will send keypresses to this window only, makes it more relyable when window is not focused for any reason - added auto-detection of Elite Dangerous client window id if -noroot is used and no -xdowindowid is supplied. for this to work, start this script AFTER the client is already running. - monkey-wrenched a volume slider to the main window - added confirmation dialog for removing a profile (!) - copy existing profile. added a copy button to the main menu - properly shutdown audio recording stream - updated gitignore to ignore audio files
2 years ago
import subprocess
import shlex
from soundfiles import SoundFiles
3 years ago
class MainWnd(QWidget):
Fixes & implementation of playing audio files. See changes.md for details. - hacked profileexcutor to reload command list when changing it. it did not do that, so changing the profile had no effect at all. - hacked profileexecutor to somewhat make use of "Enable listening". It was an option without functionality at all until now. - created new directory 'voicepacks', copy HCS voicepacks here - new command to play a sound - new gui to select sounds from voicepacks - created playsound class that reads voicepack files and plays audio files with ffplay - added alternate keypress handling using xdotool, which won't require root privileges. might not work for any key existing in profiles, as some need to be remapped. added some remappings to profileexecutor.py pressKey() - added basic command line argument reading to set some configs. right now there: -noroot - will enable xdotool usage for keypresses -xdowindowid <windowid> - will send keypresses to this window only, makes it more relyable when window is not focused for any reason - added auto-detection of Elite Dangerous client window id if -noroot is used and no -xdowindowid is supplied. for this to work, start this script AFTER the client is already running. - monkey-wrenched a volume slider to the main window - added confirmation dialog for removing a profile (!) - copy existing profile. added a copy button to the main menu - properly shutdown audio recording stream - updated gitignore to ignore audio files
2 years ago
def __init__(self, p_parent = None):
super().__init__(p_parent)
self.ui = Ui_MainWidget()
self.ui.setupUi(self)
self.handleArgs()
self.m_sound = SoundFiles()
self.m_profileExecutor = ProfileExecutor(None, self)
if not os.geteuid() == 0 and not self.m_config['noroot'] == 1:
print("\033[93m\nWARNING: no root privileges, unable to send key strokes to the system.\nConsider running this as root.\nFor that you might need to install some python modules for the root user. \n\033[0m")
self.ui.profileCbx.currentIndexChanged.connect(self.slotProfileChanged)
self.ui.addBut.clicked.connect(self.slotAddNewProfile)
self.ui.editBut.clicked.connect(self.slotEditProfile)
self.ui.copyBut.clicked.connect(self.slotCopyProfile)
self.ui.removeBut.clicked.connect(self.slotRemoveProfile)
self.ui.listeningChk.stateChanged.connect(self.slotListeningEnabled)
self.ui.ok.clicked.connect(self.slotOK)
self.ui.cancel.clicked.connect(self.slotCancel)
self.ui.sliderVolume.valueChanged.connect(lambda: self.m_sound.setVolume(self.ui.sliderVolume.value()))
signal.signal(signal.SIGINT, signal.SIG_DFL)
if self.loadFromDatabase() > 0 :
# if self.loadTestProfiles() > 0:
w_jsonProfile = self.ui.profileCbx.itemData(0)
if w_jsonProfile != None:
self.m_activeProfile = json.loads(w_jsonProfile)
self.m_profileExecutor.setProfile(self.m_activeProfile)
#self.m_profileExecutor.start()
def saveToDatabase(self):
w_profiles = []
w_profileCnt = self.ui.profileCbx.count()
for w_idx in range(w_profileCnt):
w_jsonProfile = self.ui.profileCbx.itemData(w_idx)
if w_jsonProfile == None:
continue
w_profile = json.loads(w_jsonProfile)
w_profiles.append(w_profile)
with open(self.getSettingsPath("profiles.dat"), "wb") as f:
pickle.dump(w_profiles, f, pickle.HIGHEST_PROTOCOL)
def loadFromDatabase(self):
w_profiles = []
with open(self.getSettingsPath("profiles.dat"), "rb") as f:
w_profiles = pickle.load(f)
for w_profile in w_profiles:
self.ui.profileCbx.addItem(w_profile['name'])
w_jsonProfile = json.dumps(w_profile)
self.ui.profileCbx.setItemData(self.ui.profileCbx.count() - 1, w_jsonProfile)
return len(w_profiles)
def getSettingsPath(self, setting):
home = os.path.expanduser("~") + '/.linvam/'
if not os.path.exists(home):
os.mkdir(home)
if not os.path.exists(home + setting):
shutil.copyfile(setting, home + setting)
return home + setting
def loadTestProfiles(self):
w_carProfileDict = {
"name": "car game",
"commands": [
{'name': 'forward',
'actions': [
{'name': 'key action', 'key': 'up', 'type': 1}
],
'repeat': 1,
'async': False,
'threshold': 12
},
{'name': 'back',
'actions': [
{'name': 'key action', 'key': 'down', 'type': 1}
],
'repeat': 1,
'async': False,
'threshold': 3
},
{'name': 'left',
'actions': [{'name': 'key action', 'key': 'right', 'type': 0},
{'name': 'key action', 'key': 'left', 'type': 1},
],
'repeat': 1,
'async': False,
'threshold': 10
},
{'name': 'right',
'actions': [{'name': 'key action', 'key': 'left', 'type': 0},
{'name': 'key action', 'key': 'right', 'type': 1},
],
'repeat': 1,
'async': False,
'threshold': 3
},
{'name': 'stop',
'actions': [
{'name': 'key action', 'key': 'left', 'type': 0},
{'name': 'key action', 'key': 'right', 'type': 0},
{'name': 'key action', 'key': 'up', 'type': 0},
{'name': 'key action', 'key': 'down', 'type': 0}
],
'repeat': 1,
'async': False,
'threshold': 8
}
]
}
w_airplaneProfileDict = {
"name": "airplane game",
"commands": [
{'name': 'up',
'actions': [
{'name': 'command stop action', 'command name': 'down'},
{'name': 'mouse move action', 'x': 0, 'y': -5, 'absolute': False},
{'name': 'pause action', 'time': 0.01}
],
'repeat': -1,
'async': True,
'threshold': 3
},
{'name': 'left',
'actions': [
{'name': 'command stop action', 'command name': 'right'},
{'name': 'mouse move action', 'x': -5, 'y': 0, 'absolute': False},
{'name': 'pause action', 'time': 0.005}
],
'repeat': -1,
'async': True,
'threshold': 3
},
{'name': 'right',
'actions': [
{'name': 'command stop action', 'command name': 'left'},
{'name': 'mouse move action', 'x': 5, 'y': 0, 'absolute': False},
{'name': 'pause action', 'time': 0.005}
],
'repeat': -1,
'async': True,
'threshold': 3
},
{'name': 'down',
'actions': [
{'name': 'command stop action', 'command name': 'up'},
{'name': 'mouse move action', 'x': 0, 'y': 5, 'absolute': False},
{'name': 'pause action', 'time': 0.005}
],
'repeat': -1,
'async': True,
'threshold': 3
},
{'name': 'shoot',
'actions': [
{'name': 'mouse click action', 'button': 'left', 'type': 1},
{'name': 'pause action', 'time': 0.03}
],
'repeat': 1,
'async': False,
'threshold': 3
},
{'name': 'stop',
'actions': [
{'name': 'command stop action', 'command name': 'up'},
{'name': 'command stop action', 'command name': 'left'},
{'name': 'command stop action', 'command name': 'right'},
{'name': 'command stop action', 'command name': 'down'},
{'name': 'mouse click action', 'button': 'left', 'type': 0}
],
'repeat': 1,
'async': False,
'threshold': 3
}
]
}
w_profiles = []
w_profiles.append(w_carProfileDict)
w_profiles.append(w_airplaneProfileDict)
i = 0
for w_profile in w_profiles:
self.ui.profileCbx.addItem(w_profile['name'])
w_jsonProfile = json.dumps(w_profile)
self.ui.profileCbx.setItemData(i, w_jsonProfile)
i = i + 1
return i
def slotProfileChanged(self, p_idx):
w_jsonProfile = self.ui.profileCbx.itemData(p_idx)
if w_jsonProfile != None:
self.m_activeProfile = json.loads(w_jsonProfile)
self.m_profileExecutor.setProfile(self.m_activeProfile)
def slotAddNewProfile(self):
w_profileEditWnd = ProfileEditWnd(None, self)
if w_profileEditWnd.exec() == QDialog.Accepted:
w_profile = w_profileEditWnd.m_profile
self.m_profileExecutor.setProfile(w_profile)
self.ui.profileCbx.addItem(w_profile['name'])
w_jsonProfile = json.dumps(w_profile)
self.ui.profileCbx.setItemData(self.ui.profileCbx.count()-1, w_jsonProfile)
def slotEditProfile(self):
w_idx = self.ui.profileCbx.currentIndex()
w_jsonProfile = self.ui.profileCbx.itemData(w_idx)
w_profile = json.loads(w_jsonProfile)
w_profileEditWnd = ProfileEditWnd(w_profile, self)
if w_profileEditWnd.exec() == QDialog.Accepted:
w_profile = w_profileEditWnd.m_profile
self.m_profileExecutor.setProfile(w_profile)
self.ui.profileCbx.setItemText(w_idx, w_profile['name'])
w_jsonProfile = json.dumps(w_profile)
self.ui.profileCbx.setItemData(w_idx, w_jsonProfile)
def slotCopyProfile(self):
text, okPressed = QInputDialog.getText(self, "Copy profile", "Enter new profile name:", QLineEdit.Normal, "")
if okPressed and text != '':
# todo: check if name not already in use
w_idx = self.ui.profileCbx.currentIndex()
w_jsonProfile = self.ui.profileCbx.itemData(w_idx)
w_profile = json.loads(w_jsonProfile)
w_profile_copy = w_profile
w_profile_copy['name'] = text
self.ui.profileCbx.addItem(w_profile_copy['name'])
w_jsonProfile = json.dumps(w_profile_copy)
self.ui.profileCbx.setItemData(self.ui.profileCbx.count()-1, w_jsonProfile)
def slotRemoveProfile(self):
buttonReply = QMessageBox.question(self, 'Remove Profile', "Do you really want to delete this profile?", QMessageBox.No | QMessageBox.Yes, QMessageBox.No)
if buttonReply == QMessageBox.No:
return
w_curIdx = self.ui.profileCbx.currentIndex()
if w_curIdx >= 0:
self.ui.profileCbx.removeItem(w_curIdx)
w_curIdx = self.ui.profileCbx.currentIndex()
if w_curIdx >= 0:
w_jsonProfile = self.ui.profileCbx.itemData(w_curIdx)
w_profile = json.loads(w_jsonProfile)
self.m_profileExecutor.setProfile(w_profile)
def slotListeningEnabled(self, p_enabled):
if p_enabled:
self.m_profileExecutor.setEnableListening(True)
else:
self.m_profileExecutor.setEnableListening(False)
def slotOK(self):
self.saveToDatabase()
self.m_profileExecutor.shutdown()
self.close()
def slotCancel(self):
self.m_profileExecutor.shutdown()
self.close()
exit()
def handleArgs(self):
self.m_config = {
'noroot' : 0,
'xdowindowid' : None
}
if len(sys.argv) == 1:
return
for i in range(1,len(sys.argv)):
if sys.argv[i] == '-noroot':
self.m_config['noroot'] = 1
elif sys.argv[i] == '-xdowindowid' and (i+1 < len(sys.argv)):
self.m_config['xdowindowid'] = sys.argv[i+1]
i = i+1
# try to help: if -noroot is supplied, but no xdowindowid, try to determine the id
# Elite Dangerous only
try:
args = shlex.split('xdotool search --name "\(CLIENT\)"')
Fixes &amp; implementation of playing audio files. See changes.md for details. - hacked profileexcutor to reload command list when changing it. it did not do that, so changing the profile had no effect at all. - hacked profileexecutor to somewhat make use of &#34;Enable listening&#34;. It was an option without functionality at all until now. - created new directory &#39;voicepacks&#39;, copy HCS voicepacks here - new command to play a sound - new gui to select sounds from voicepacks - created playsound class that reads voicepack files and plays audio files with ffplay - added alternate keypress handling using xdotool, which won&#39;t require root privileges. might not work for any key existing in profiles, as some need to be remapped. added some remappings to profileexecutor.py pressKey() - added basic command line argument reading to set some configs. right now there: -noroot - will enable xdotool usage for keypresses -xdowindowid &lt;windowid&gt; - will send keypresses to this window only, makes it more relyable when window is not focused for any reason - added auto-detection of Elite Dangerous client window id if -noroot is used and no -xdowindowid is supplied. for this to work, start this script AFTER the client is already running. - monkey-wrenched a volume slider to the main window - added confirmation dialog for removing a profile (!) - copy existing profile. added a copy button to the main menu - properly shutdown audio recording stream - updated gitignore to ignore audio files
2 years ago
window_id = str(subprocess.check_output(args))
window_id = window_id.replace('b\'', '')
window_id = window_id.replace('\\n\'','')
Fixes &amp; implementation of playing audio files. See changes.md for details. - hacked profileexcutor to reload command list when changing it. it did not do that, so changing the profile had no effect at all. - hacked profileexecutor to somewhat make use of &#34;Enable listening&#34;. It was an option without functionality at all until now. - created new directory &#39;voicepacks&#39;, copy HCS voicepacks here - new command to play a sound - new gui to select sounds from voicepacks - created playsound class that reads voicepack files and plays audio files with ffplay - added alternate keypress handling using xdotool, which won&#39;t require root privileges. might not work for any key existing in profiles, as some need to be remapped. added some remappings to profileexecutor.py pressKey() - added basic command line argument reading to set some configs. right now there: -noroot - will enable xdotool usage for keypresses -xdowindowid &lt;windowid&gt; - will send keypresses to this window only, makes it more relyable when window is not focused for any reason - added auto-detection of Elite Dangerous client window id if -noroot is used and no -xdowindowid is supplied. for this to work, start this script AFTER the client is already running. - monkey-wrenched a volume slider to the main window - added confirmation dialog for removing a profile (!) - copy existing profile. added a copy button to the main menu - properly shutdown audio recording stream - updated gitignore to ignore audio files
2 years ago
except subprocess.CalledProcessError:
window_id = None
pass
if not window_id == None:
# check if it's really the Client we want
try:
window_name = subprocess.check_output(['xdotool', 'getwindowname', str(window_id)])
except subprocess.CalledProcessError:
window_name = None
pass
if not window_name == None:
if not str(window_name).find('Elite - Dangerous') == -1:
print("Window ID: ", str(window_id), ", window name: ", window_name)
print("Auto-detected window id of ED Client: ", window_id)
self.m_config['xdowindowid'] = window_id
3 years ago
if __name__ == "__main__":
Fixes &amp; implementation of playing audio files. See changes.md for details. - hacked profileexcutor to reload command list when changing it. it did not do that, so changing the profile had no effect at all. - hacked profileexecutor to somewhat make use of &#34;Enable listening&#34;. It was an option without functionality at all until now. - created new directory &#39;voicepacks&#39;, copy HCS voicepacks here - new command to play a sound - new gui to select sounds from voicepacks - created playsound class that reads voicepack files and plays audio files with ffplay - added alternate keypress handling using xdotool, which won&#39;t require root privileges. might not work for any key existing in profiles, as some need to be remapped. added some remappings to profileexecutor.py pressKey() - added basic command line argument reading to set some configs. right now there: -noroot - will enable xdotool usage for keypresses -xdowindowid &lt;windowid&gt; - will send keypresses to this window only, makes it more relyable when window is not focused for any reason - added auto-detection of Elite Dangerous client window id if -noroot is used and no -xdowindowid is supplied. for this to work, start this script AFTER the client is already running. - monkey-wrenched a volume slider to the main window - added confirmation dialog for removing a profile (!) - copy existing profile. added a copy button to the main menu - properly shutdown audio recording stream - updated gitignore to ignore audio files
2 years ago
app = QApplication(sys.argv)
mainWnd = MainWnd()
mainWnd.show()
sys.exit(app.exec_())