mirror of
https://github.com/aidygus/LinVAM.git
synced 2024-11-23 09:18:04 +11:00
Merge pull request #3 from smirgol/master
Fixes & implementation of playing audio files. See changes.md for det…
This commit is contained in:
commit
a229683c57
3
.gitignore
vendored
3
.gitignore
vendored
@ -127,5 +127,8 @@ dmypy.json
|
|||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
|
# Ignore voicepack files
|
||||||
|
voicepacks/*
|
||||||
|
|
||||||
# End of https://www.gitignore.io/api/python
|
# End of https://www.gitignore.io/api/python
|
||||||
|
|
||||||
|
33
README.md
33
README.md
@ -1,5 +1,6 @@
|
|||||||
# LinVAM
|
# LinVAM
|
||||||
Linux Voice Activated Macro
|
Linux Voice Activated Macro
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
This project is currently a work-in-progress and is minimally functional only for english.
|
This project is currently a work-in-progress and is minimally functional only for english.
|
||||||
|
|
||||||
@ -10,6 +11,7 @@ Known bugs and planned additions
|
|||||||
- Remember last loaded profile and load on start
|
- Remember last loaded profile and load on start
|
||||||
- Log window showing spoken words the V2T recognises with ability to right click and assign voice command and actions to current profile
|
- Log window showing spoken words the V2T recognises with ability to right click and assign voice command and actions to current profile
|
||||||
- Support for joysticks and gaming devices
|
- Support for joysticks and gaming devices
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
- python3
|
- python3
|
||||||
- PyQt5
|
- PyQt5
|
||||||
@ -17,20 +19,33 @@ Known bugs and planned additions
|
|||||||
- pyaudio
|
- pyaudio
|
||||||
- pocketsphinx
|
- pocketsphinx
|
||||||
- swig3.0
|
- swig3.0
|
||||||
|
|
||||||
|
## Optional requirements
|
||||||
|
- xdotool
|
||||||
|
- ffplay (part of ffmpeg, usually already installed)
|
||||||
|
- HCS voicepacks
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
- $ pip3 install PyQt5
|
- $ pip3 install PyQt5
|
||||||
- $ pip3 install python3-xlib
|
- $ pip3 install python3-xlib
|
||||||
- $ pip3 install pyaudio
|
- $ pip3 install pyaudio
|
||||||
- $ pip3 install pocketsphinx
|
- $ pip3 install pocketsphinx
|
||||||
- $ sudo apt-get install swig3.0
|
- $ sudo apt-get install swig3.0
|
||||||
|
- $ (optional) sudo apt install xdotool
|
||||||
|
- $ (optional) sudo apt install ffmpeg
|
||||||
- $ sudo ln -s /usr/bin/swig3.0 /usr/bin/swig
|
- $ sudo ln -s /usr/bin/swig3.0 /usr/bin/swig
|
||||||
- $ git clone https://github.com/aidygus/LinVAM.git
|
- $ git clone https://github.com/aidygus/LinVAM.git
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
This script must be run with root privilege because it must hook and simulate input devices such as keyboard, mouse etc.
|
This script must be run with root privilege because it must hook and simulate input devices such as keyboard, mouse etc.
|
||||||
- $ cd LinVAM
|
- $ cd LinVAM
|
||||||
- $ xhost +
|
- $ xhost +
|
||||||
- $ sudo ./main.py
|
- $ sudo ./main.py
|
||||||
|
|
||||||
|
As an alternative, if you use the X window manager and have xdotool installed, you can run the script like this:
|
||||||
|
- $ cd LinVAM
|
||||||
|
- $ ./main.py -noroot
|
||||||
|
|
||||||
### Profiles
|
### Profiles
|
||||||
Multiple profiles are supported. To create a new profile for a specific task/game click new and the main profile editor window will be displayed
|
Multiple profiles are supported. To create a new profile for a specific task/game click new and the main profile editor window will be displayed
|
||||||
|
|
||||||
@ -46,3 +61,21 @@ You can also assign mouse movements and system commands if you require (eg openi
|
|||||||
![Main GUI](https://raw.githubusercontent.com/aidygus/LinVAM/master/.img/complex.png)
|
![Main GUI](https://raw.githubusercontent.com/aidygus/LinVAM/master/.img/complex.png)
|
||||||
### Threshold
|
### Threshold
|
||||||
As a rough guide use a value of 10 for each syllable of a word then tweak it down for better accuracy.
|
As a rough guide use a value of 10 for each syllable of a word then tweak it down for better accuracy.
|
||||||
|
|
||||||
|
### Output audio
|
||||||
|
In the Command Edit Dialog, chose 'Other' and then 'Play sound'. Pick the sound you would like to play.
|
||||||
|
For this to work you need to copy any audio file you would like to use to the folder 'voicepacks'.
|
||||||
|
You are required to create a subfolder to hold all your audio files (voicepack folder), then within that subfolder, create as many folders as you like to group your audio files (category folders).
|
||||||
|
Place the audio file into these category folders or in any subfolder within a category folder.
|
||||||
|
In theory any audio file should work, but tested only with MP3 files.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
/voicepacks/my voicepack/custom commands/hello.mp3
|
||||||
|
/voicepacks/my voicepack/other/thank you.mp3
|
||||||
|
|
||||||
|
If you own a HCS voicepack, copy the whole voicepack folder (like 'hcspack', 'hcspack-eden', ...) to the 'voicepacks' folder, so it reads like this:
|
||||||
|
/voicepacks/hcspack/...
|
||||||
|
|
||||||
|
### Improve voice recognition accuracy
|
||||||
|
Please see this resource on how to train the acoustic model of pocketsphinx to match your voice:
|
||||||
|
https://cmusphinx.github.io/wiki/tutorialadapt/
|
66
changes.md
Normal file
66
changes.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# Changelog smirgol
|
||||||
|
|
||||||
|
### Issues:
|
||||||
|
- (fixed) voice recognition autostarts, there is no way to disable it. the flag "self.m_listening" does nothing
|
||||||
|
- (fixed) when profile is changed, the new commands will not be loaded, as the voice recognition is already running - by default with the Elite Dangerous settings. You cannot use any other profile.
|
||||||
|
- (fixed) "When I say" commands need to be all lower case, otherwise it won't be recognized.
|
||||||
|
|
||||||
|
### Known/New Issues:
|
||||||
|
- When using external speakers it will pick up a playing audio sample and might trigger commands. Use headphones! :)
|
||||||
|
- On a Valve Index headset, the earphones, while being good, will feed the Index microphone causing troubles with the voice recognition.
|
||||||
|
There's probably nothing that can be done about it, maybe some sort of audio filter could be introduced to cut off low-volume sounds?
|
||||||
|
Lower voice audio for now to minimize the issue.
|
||||||
|
- There sometimes is a window error in the console when clicking on a 'cancel' button, does not seem to do harm, but should fix that if possible.
|
||||||
|
- Sometimes keypress signals are not recognized by game, this is the case with xdotool, don't know if this also happens with standard keyboard library.
|
||||||
|
It seems to solve the issue if you at least once manually press any key while the game window is focused
|
||||||
|
|
||||||
|
### Added:
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- ffplay
|
||||||
|
- xdotool (optional, won't need root privileges for keypresses)
|
||||||
|
|
||||||
|
Not sure about these, I had them installed already:
|
||||||
|
- import subprocess
|
||||||
|
- import shlex
|
||||||
|
- import signal
|
||||||
|
|
||||||
|
### TODO
|
||||||
|
- select multiple files (from same category) to play one of them randomly
|
||||||
|
|
||||||
|
### FUTURE IDEAS
|
||||||
|
- remember settings and load on start (nice to have)
|
||||||
|
- change key assignment logic to record a keypress / buttonpress combo
|
||||||
|
- will definitely break existing profiles, as we need to record keycodes and stuff
|
||||||
|
- maybe add a state machine like voice attack does
|
||||||
|
- look into reading log files from elite dangerous to trigger certain actions.
|
||||||
|
this would be a very game specific addition though...
|
||||||
|
- make connection to a specific voice pack more loose, so you can easily change the voice pack,
|
||||||
|
without re-editing all the commands. maybe even a different voice pack for certain actions,
|
||||||
|
like in voice attack (crewmen)
|
||||||
|
- reorganize main window to hold more settings and a log window for displaying actions
|
||||||
|
- look into other text2speech engines
|
||||||
|
i gave julius-speech a quick try. its pretty easy to setup and looks promising, but it
|
||||||
|
also has its problems with detecting some "exotic" words. as not being a native
|
||||||
|
english speaker, my pronounciation is not the best sometimes, which doesn't help. :)
|
||||||
|
- look into make recognized words using regex expressions
|
@ -5,6 +5,7 @@ from ui_commandeditwnd import Ui_CommandEditDialog
|
|||||||
from keyactioneditwnd import KeyActionEditWnd
|
from keyactioneditwnd import KeyActionEditWnd
|
||||||
from mouseactioneditwnd import MouseActionEditWnd
|
from mouseactioneditwnd import MouseActionEditWnd
|
||||||
from pauseactioneditwnd import PauseActionEditWnd
|
from pauseactioneditwnd import PauseActionEditWnd
|
||||||
|
from soundactioneditwnd import SoundActionEditWnd
|
||||||
import json
|
import json
|
||||||
|
|
||||||
class CommandEditWnd(QDialog):
|
class CommandEditWnd(QDialog):
|
||||||
@ -12,6 +13,7 @@ class CommandEditWnd(QDialog):
|
|||||||
super().__init__(p_parent)
|
super().__init__(p_parent)
|
||||||
self.ui = Ui_CommandEditDialog()
|
self.ui = Ui_CommandEditDialog()
|
||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
|
self.m_parent = p_parent
|
||||||
|
|
||||||
self.ui.deleteBut.clicked.connect(self.slotDelete)
|
self.ui.deleteBut.clicked.connect(self.slotDelete)
|
||||||
self.ui.ok.clicked.connect(self.slotOK)
|
self.ui.ok.clicked.connect(self.slotOK)
|
||||||
@ -27,6 +29,7 @@ class CommandEditWnd(QDialog):
|
|||||||
w_otherMenu = QMenu()
|
w_otherMenu = QMenu()
|
||||||
w_otherMenu.addAction('Stop Another Command', self.slotStopAnotherCommand)
|
w_otherMenu.addAction('Stop Another Command', self.slotStopAnotherCommand)
|
||||||
w_otherMenu.addAction('Execute Another Command', self.slotDoAnotherCommand)
|
w_otherMenu.addAction('Execute Another Command', self.slotDoAnotherCommand)
|
||||||
|
w_otherMenu.addAction('Play Sound', self.slotNewSoundEdit)
|
||||||
self.ui.otherBut.setMenu(w_otherMenu)
|
self.ui.otherBut.setMenu(w_otherMenu)
|
||||||
|
|
||||||
self.m_command = {}
|
self.m_command = {}
|
||||||
@ -73,6 +76,14 @@ class CommandEditWnd(QDialog):
|
|||||||
w_commandDoAction['command name'] = text
|
w_commandDoAction['command name'] = text
|
||||||
self.addAction(w_commandDoAction)
|
self.addAction(w_commandDoAction)
|
||||||
|
|
||||||
|
def slotDoPlaySound(self):
|
||||||
|
text, okPressed = QInputDialog.getItem(self, "Set sound to play", "Enter sound file:", list(self.m_parent.m_parent.m_sound.m_sounds), 0, False)
|
||||||
|
if okPressed and text != '':
|
||||||
|
w_commandDoAction = {}
|
||||||
|
w_commandDoAction['name'] = 'command play sound'
|
||||||
|
w_commandDoAction['command name'] = text
|
||||||
|
self.addAction(w_commandDoAction)
|
||||||
|
|
||||||
def slotNewKeyEdit(self):
|
def slotNewKeyEdit(self):
|
||||||
w_keyEditWnd = KeyActionEditWnd(None, self)
|
w_keyEditWnd = KeyActionEditWnd(None, self)
|
||||||
if w_keyEditWnd.exec() == QDialog.Accepted:
|
if w_keyEditWnd.exec() == QDialog.Accepted:
|
||||||
@ -88,6 +99,11 @@ class CommandEditWnd(QDialog):
|
|||||||
if w_pauseEditWnd.exec() == QDialog.Accepted:
|
if w_pauseEditWnd.exec() == QDialog.Accepted:
|
||||||
self.addAction(w_pauseEditWnd.m_pauseAction)
|
self.addAction(w_pauseEditWnd.m_pauseAction)
|
||||||
|
|
||||||
|
def slotNewSoundEdit(self):
|
||||||
|
w_soundEditWnd = SoundActionEditWnd(self.m_parent.m_parent.m_sound, None, self)
|
||||||
|
if w_soundEditWnd.exec() == QDialog.Accepted:
|
||||||
|
self.addAction(w_soundEditWnd.m_soundAction)
|
||||||
|
|
||||||
def slotActionUp(self):
|
def slotActionUp(self):
|
||||||
currentIndex = self.ui.actionsListWidget.currentRow()
|
currentIndex = self.ui.actionsListWidget.currentRow()
|
||||||
currentItem = self.ui.actionsListWidget.takeItem(currentIndex);
|
currentItem = self.ui.actionsListWidget.takeItem(currentIndex);
|
||||||
@ -132,6 +148,10 @@ class CommandEditWnd(QDialog):
|
|||||||
if okPressed and text != '':
|
if okPressed and text != '':
|
||||||
w_action['command name'] = text
|
w_action['command name'] = text
|
||||||
w_jsonAction = json.dumps(w_action)
|
w_jsonAction = json.dumps(w_action)
|
||||||
|
elif w_action['name'] == 'play sound':
|
||||||
|
w_soundEditWnd = SoundActionEditWnd(self.m_parent.m_parent.m_sound, w_action, self)
|
||||||
|
if w_soundEditWnd.exec() == QDialog.Accepted:
|
||||||
|
w_jsonAction = json.dumps(w_soundEditWnd.m_soundAction)
|
||||||
|
|
||||||
w_item.setText(w_jsonAction)
|
w_item.setText(w_jsonAction)
|
||||||
w_item.setData(Qt.UserRole, w_jsonAction)
|
w_item.setData(Qt.UserRole, w_jsonAction)
|
||||||
|
518
main.py
518
main.py
@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/python3
|
||||||
from PyQt5.QtCore import *
|
from PyQt5.QtCore import *
|
||||||
from PyQt5.QtGui import *
|
from PyQt5.QtGui import *
|
||||||
from PyQt5.QtWidgets import *
|
from PyQt5.QtWidgets import *
|
||||||
@ -11,252 +11,324 @@ import pickle
|
|||||||
import signal
|
import signal
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import shlex
|
||||||
|
from soundfiles import SoundFiles
|
||||||
|
|
||||||
class MainWnd(QWidget):
|
class MainWnd(QWidget):
|
||||||
def __init__(self, p_parent = None):
|
def __init__(self, p_parent = None):
|
||||||
super().__init__(p_parent)
|
super().__init__(p_parent)
|
||||||
self.ui = Ui_MainWidget()
|
|
||||||
self.ui.setupUi(self)
|
|
||||||
self.m_profileExecutor = ProfileExecutor()
|
|
||||||
|
|
||||||
self.ui.profileCbx.currentIndexChanged.connect(self.slotProfileChanged)
|
self.ui = Ui_MainWidget()
|
||||||
self.ui.addBut.clicked.connect(self.slotAddNewProfile)
|
self.ui.setupUi(self)
|
||||||
self.ui.editBut.clicked.connect(self.slotEditProfile)
|
self.handleArgs()
|
||||||
self.ui.removeBut.clicked.connect(self.slotRemoveProfile)
|
self.m_sound = SoundFiles()
|
||||||
self.ui.listeningChk.stateChanged.connect(self.slotListeningEnabled)
|
self.m_profileExecutor = ProfileExecutor(None, self)
|
||||||
self.ui.ok.clicked.connect(self.slotOK)
|
|
||||||
self.ui.cancel.clicked.connect(self.slotCancel)
|
|
||||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
||||||
|
|
||||||
if self.loadFromDatabase() > 0 :
|
if not os.geteuid() == 0 and not self.m_config['noroot'] == 1:
|
||||||
# if self.loadTestProfiles() > 0:
|
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")
|
||||||
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):
|
self.ui.profileCbx.currentIndexChanged.connect(self.slotProfileChanged)
|
||||||
w_profiles = []
|
self.ui.addBut.clicked.connect(self.slotAddNewProfile)
|
||||||
w_profileCnt = self.ui.profileCbx.count()
|
self.ui.editBut.clicked.connect(self.slotEditProfile)
|
||||||
for w_idx in range(w_profileCnt):
|
self.ui.copyBut.clicked.connect(self.slotCopyProfile)
|
||||||
w_jsonProfile = self.ui.profileCbx.itemData(w_idx)
|
self.ui.removeBut.clicked.connect(self.slotRemoveProfile)
|
||||||
if w_jsonProfile == None:
|
self.ui.listeningChk.stateChanged.connect(self.slotListeningEnabled)
|
||||||
continue
|
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)
|
||||||
|
|
||||||
w_profile = json.loads(w_jsonProfile)
|
if self.loadFromDatabase() > 0 :
|
||||||
w_profiles.append(w_profile)
|
# 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()
|
||||||
|
|
||||||
with open(self.getSettingsPath("profiles.dat"), "wb") as f:
|
|
||||||
pickle.dump(w_profiles, f, pickle.HIGHEST_PROTOCOL)
|
|
||||||
|
|
||||||
def loadFromDatabase(self):
|
def saveToDatabase(self):
|
||||||
w_profiles = []
|
w_profiles = []
|
||||||
with open(self.getSettingsPath("profiles.dat"), "rb") as f:
|
w_profileCnt = self.ui.profileCbx.count()
|
||||||
w_profiles = pickle.load(f)
|
for w_idx in range(w_profileCnt):
|
||||||
|
w_jsonProfile = self.ui.profileCbx.itemData(w_idx)
|
||||||
|
if w_jsonProfile == None:
|
||||||
|
continue
|
||||||
|
|
||||||
for w_profile in w_profiles:
|
w_profile = json.loads(w_jsonProfile)
|
||||||
self.ui.profileCbx.addItem(w_profile['name'])
|
w_profiles.append(w_profile)
|
||||||
w_jsonProfile = json.dumps(w_profile)
|
|
||||||
self.ui.profileCbx.setItemData(self.ui.profileCbx.count() - 1, w_jsonProfile)
|
|
||||||
|
|
||||||
return len(w_profiles)
|
with open(self.getSettingsPath("profiles.dat"), "wb") as f:
|
||||||
|
pickle.dump(w_profiles, f, pickle.HIGHEST_PROTOCOL)
|
||||||
|
|
||||||
def getSettingsPath(self, setting):
|
def loadFromDatabase(self):
|
||||||
home = os.path.expanduser("~") + '/.linvam/'
|
w_profiles = []
|
||||||
if not os.path.exists(home):
|
with open(self.getSettingsPath("profiles.dat"), "rb") as f:
|
||||||
os.mkdir(home)
|
w_profiles = pickle.load(f)
|
||||||
if not os.path.exists(home + setting):
|
|
||||||
shutil.copyfile(setting, home + setting)
|
|
||||||
|
|
||||||
return home + setting
|
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)
|
||||||
|
|
||||||
def loadTestProfiles(self):
|
return len(w_profiles)
|
||||||
|
|
||||||
w_carProfileDict = {
|
def getSettingsPath(self, setting):
|
||||||
"name": "car game",
|
home = os.path.expanduser("~") + '/.linvam/'
|
||||||
"commands": [
|
if not os.path.exists(home):
|
||||||
{'name': 'forward',
|
os.mkdir(home)
|
||||||
'actions': [
|
if not os.path.exists(home + setting):
|
||||||
{'name': 'key action', 'key': 'up', 'type': 1}
|
shutil.copyfile(setting, home + setting)
|
||||||
],
|
|
||||||
'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 = {
|
return home + setting
|
||||||
"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
|
def loadTestProfiles(self):
|
||||||
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
|
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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
def slotProfileChanged(self, p_idx):
|
w_airplaneProfileDict = {
|
||||||
w_jsonProfile = self.ui.profileCbx.itemData(p_idx)
|
"name": "airplane game",
|
||||||
if w_jsonProfile != None:
|
"commands": [
|
||||||
self.m_activeProfile = json.loads(w_jsonProfile)
|
{'name': 'up',
|
||||||
self.m_profileExecutor.setProfile(self.m_activeProfile)
|
'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)
|
||||||
|
|
||||||
def slotAddNewProfile(self):
|
i = 0
|
||||||
w_profileEditWnd = ProfileEditWnd(None, self)
|
for w_profile in w_profiles:
|
||||||
if w_profileEditWnd.exec() == QDialog.Accepted:
|
self.ui.profileCbx.addItem(w_profile['name'])
|
||||||
w_profile = w_profileEditWnd.m_profile
|
w_jsonProfile = json.dumps(w_profile)
|
||||||
self.m_profileExecutor.setProfile(w_profile)
|
self.ui.profileCbx.setItemData(i, w_jsonProfile)
|
||||||
self.ui.profileCbx.addItem(w_profile['name'])
|
i = i + 1
|
||||||
w_jsonProfile = json.dumps(w_profile)
|
|
||||||
self.ui.profileCbx.setItemData(self.ui.profileCbx.count()-1, w_jsonProfile)
|
|
||||||
|
|
||||||
def slotEditProfile(self):
|
return i
|
||||||
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 slotRemoveProfile(self):
|
def slotProfileChanged(self, p_idx):
|
||||||
w_curIdx = self.ui.profileCbx.currentIndex()
|
w_jsonProfile = self.ui.profileCbx.itemData(p_idx)
|
||||||
if w_curIdx >= 0:
|
if w_jsonProfile != None:
|
||||||
self.ui.profileCbx.removeItem(w_curIdx)
|
self.m_activeProfile = json.loads(w_jsonProfile)
|
||||||
|
self.m_profileExecutor.setProfile(self.m_activeProfile)
|
||||||
|
|
||||||
w_curIdx = self.ui.profileCbx.currentIndex()
|
def slotAddNewProfile(self):
|
||||||
if w_curIdx >= 0:
|
w_profileEditWnd = ProfileEditWnd(None, self)
|
||||||
w_jsonProfile = self.ui.profileCbx.itemData(w_curIdx)
|
if w_profileEditWnd.exec() == QDialog.Accepted:
|
||||||
w_profile = json.loads(w_jsonProfile)
|
w_profile = w_profileEditWnd.m_profile
|
||||||
self.m_profileExecutor.setProfile(w_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 slotListeningEnabled(self, p_enabled):
|
def slotEditProfile(self):
|
||||||
if p_enabled:
|
w_idx = self.ui.profileCbx.currentIndex()
|
||||||
self.m_profileExecutor.setEnableListening(True)
|
w_jsonProfile = self.ui.profileCbx.itemData(w_idx)
|
||||||
else:
|
w_profile = json.loads(w_jsonProfile)
|
||||||
self.m_profileExecutor.setEnableListening(False)
|
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\)"')
|
||||||
|
window_id = str(subprocess.check_output(args))
|
||||||
|
window_id = window_id.replace('b\'', '')
|
||||||
|
window_id = window_id.replace('\\n\'','')
|
||||||
|
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
|
||||||
|
|
||||||
def slotOK(self):
|
|
||||||
self.saveToDatabase()
|
|
||||||
self.m_profileExecutor.stop()
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def slotCancel(self):
|
|
||||||
self.m_profileExecutor.stop()
|
|
||||||
self.close()
|
|
||||||
exit()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
mainWnd = MainWnd()
|
mainWnd = MainWnd()
|
||||||
mainWnd.show()
|
mainWnd.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
100
mainwnd.ui
100
mainwnd.ui
@ -25,7 +25,7 @@
|
|||||||
<property name="verticalSpacing">
|
<property name="verticalSpacing">
|
||||||
<number>20</number>
|
<number>20</number>
|
||||||
</property>
|
</property>
|
||||||
<item row="1" column="0">
|
<item row="2" column="0">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
<property name="spacing">
|
<property name="spacing">
|
||||||
<number>20</number>
|
<number>20</number>
|
||||||
@ -38,7 +38,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="horizontalSpacer">
|
<spacer name="horizontalSpacer_2">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
@ -50,6 +50,57 @@
|
|||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupBox">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>200</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="title">
|
||||||
|
<string>Voice Volume</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
<widget class="QSlider" name="sliderVolume">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>10</x>
|
||||||
|
<y>20</y>
|
||||||
|
<width>181</width>
|
||||||
|
<height>21</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Change voice volume</string>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>100</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>100</number>
|
||||||
|
</property>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="ok">
|
<widget class="QPushButton" name="ok">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
@ -78,13 +129,19 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="0">
|
<item row="1" column="0">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
<property name="spacing">
|
<property name="spacing">
|
||||||
<number>20</number>
|
<number>20</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>60</width>
|
<width>60</width>
|
||||||
@ -100,7 +157,7 @@
|
|||||||
<widget class="QComboBox" name="profileCbx">
|
<widget class="QComboBox" name="profileCbx">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>250</width>
|
<width>300</width>
|
||||||
<height>0</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
@ -110,7 +167,7 @@
|
|||||||
<widget class="QPushButton" name="addBut">
|
<widget class="QPushButton" name="addBut">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>130</width>
|
<width>75</width>
|
||||||
<height>0</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
@ -123,7 +180,7 @@
|
|||||||
<widget class="QPushButton" name="editBut">
|
<widget class="QPushButton" name="editBut">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>130</width>
|
<width>75</width>
|
||||||
<height>0</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
@ -132,11 +189,27 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="copyBut">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>75</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Copy currently selected profile</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Copy</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="removeBut">
|
<widget class="QPushButton" name="removeBut">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>130</width>
|
<width>75</width>
|
||||||
<height>0</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
@ -145,19 +218,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<spacer name="horizontalSpacer_3">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
@ -13,6 +13,7 @@ class ProfileEditWnd(QDialog):
|
|||||||
self.ui = Ui_ProfileEditDialog()
|
self.ui = Ui_ProfileEditDialog()
|
||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
self.m_profile = {}
|
self.m_profile = {}
|
||||||
|
self.m_parent = p_parent
|
||||||
|
|
||||||
self.ui.cmdTable.setHorizontalHeaderLabels(('Spoken command', 'Actions'))
|
self.ui.cmdTable.setHorizontalHeaderLabels(('Spoken command', 'Actions'))
|
||||||
self.ui.cmdTable.setSelectionBehavior(QAbstractItemView.SelectRows)
|
self.ui.cmdTable.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
|
@ -4,31 +4,44 @@ import time
|
|||||||
import threading
|
import threading
|
||||||
import os, pyaudio
|
import os, pyaudio
|
||||||
import shutil
|
import shutil
|
||||||
|
import re
|
||||||
from pocketsphinx.pocketsphinx import *
|
from pocketsphinx.pocketsphinx import *
|
||||||
from sphinxbase.sphinxbase import *
|
from sphinxbase.sphinxbase import *
|
||||||
|
from soundfiles import SoundFiles
|
||||||
|
|
||||||
|
|
||||||
class ProfileExecutor(threading.Thread):
|
class ProfileExecutor(threading.Thread):
|
||||||
mouse = Controller()
|
mouse = Controller()
|
||||||
|
|
||||||
def __init__(self, p_profile = None):
|
def __init__(self, p_profile = None, p_parent = None):
|
||||||
threading.Thread.__init__(self)
|
# threading.Thread.__init__(self)
|
||||||
|
|
||||||
|
# does nothing?
|
||||||
self.setProfile(p_profile)
|
self.setProfile(p_profile)
|
||||||
self.m_stop = False
|
self.m_stop = False
|
||||||
self.m_listening = True
|
self.m_listening = False
|
||||||
self.m_cmdThreads = {}
|
self.m_cmdThreads = {}
|
||||||
|
|
||||||
self.m_config = Decoder.default_config()
|
self.m_config = Decoder.default_config()
|
||||||
self.m_config.set_string('-hmm', os.path.join('model', 'en-us/en-us'))
|
self.m_config.set_string('-hmm', os.path.join('model', 'en-us/en-us'))
|
||||||
self.m_config.set_string('-dict', os.path.join('model', 'en-us/cmudict-en-us.dict'))
|
self.m_config.set_string('-dict', os.path.join('model', 'en-us/cmudict-en-us.dict'))
|
||||||
self.m_config.set_string('-kws', 'command.list')
|
self.m_config.set_string('-kws', 'command.list')
|
||||||
|
# you usually don't want all this info stuff as a regular user. it just covers up init messages
|
||||||
|
self.m_config.set_string('-logfn', '/dev/null')
|
||||||
|
|
||||||
p = pyaudio.PyAudio()
|
self.m_pyaudio = pyaudio.PyAudio()
|
||||||
self.m_stream = p.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True)
|
self.m_stream = self.m_pyaudio.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True)
|
||||||
self.m_stream.start_stream()
|
|
||||||
|
|
||||||
# Process audio chunk by chunk. On keyword detected perform action and restart search
|
# Process audio chunk by chunk. On keyword detected perform action and restart search
|
||||||
self.m_decoder = Decoder(self.m_config)
|
self.m_decoder = Decoder(self.m_config)
|
||||||
|
|
||||||
|
self.m_thread = False
|
||||||
|
|
||||||
|
self.p_parent = p_parent
|
||||||
|
if not self.p_parent == None:
|
||||||
|
self.m_sound = self.p_parent.m_sound
|
||||||
|
|
||||||
|
|
||||||
def getSettingsPath(self, setting):
|
def getSettingsPath(self, setting):
|
||||||
home = os.path.expanduser("~") + '/.linvam/'
|
home = os.path.expanduser("~") + '/.linvam/'
|
||||||
if not os.path.exists(home):
|
if not os.path.exists(home):
|
||||||
@ -39,25 +52,46 @@ class ProfileExecutor(threading.Thread):
|
|||||||
return home + setting
|
return home + setting
|
||||||
|
|
||||||
def setProfile(self, p_profile):
|
def setProfile(self, p_profile):
|
||||||
|
#print("setProfile")
|
||||||
self.m_profile = p_profile
|
self.m_profile = p_profile
|
||||||
if self.m_profile == None:
|
if self.m_profile == None:
|
||||||
return
|
return
|
||||||
|
#print ("writing command list")
|
||||||
w_commandWordFile = open(self.getSettingsPath('command.list'), 'w')
|
w_commandWordFile = open(self.getSettingsPath('command.list'), 'w')
|
||||||
w_commands = self.m_profile['commands']
|
w_commands = self.m_profile['commands']
|
||||||
i = 0
|
i = 0
|
||||||
for w_command in w_commands:
|
for w_command in w_commands:
|
||||||
if i != 0:
|
if i != 0:
|
||||||
w_commandWordFile.write('\n')
|
w_commandWordFile.write('\n')
|
||||||
w_commandWordFile.write(w_command['name'] + ' /1e-%d/' % w_command['threshold'])
|
w_commandWordFile.write(w_command['name'].lower() + ' /1e-%d/' % w_command['threshold'])
|
||||||
i = i + 1
|
i = i + 1
|
||||||
w_commandWordFile.close()
|
w_commandWordFile.close()
|
||||||
self.m_config.set_string('-kws', self.getSettingsPath('command.list'))
|
self.m_config.set_string('-kws', self.getSettingsPath('command.list'))
|
||||||
|
# load new command list into decoder and restart it
|
||||||
|
if self.m_listening == True:
|
||||||
|
self.stop()
|
||||||
|
self.m_stream.start_stream()
|
||||||
|
# a self.m_decoder.reinit(self.config) will segfault?
|
||||||
|
self.m_decoder = Decoder(self.m_config)
|
||||||
|
self.m_stop = False
|
||||||
|
self.m_thread = threading.Thread(target=self.doListen, args=())
|
||||||
|
self.m_thread.start()
|
||||||
|
else:
|
||||||
|
self.m_decoder.reinit(self.m_config)
|
||||||
|
|
||||||
def setEnableListening(self, p_enable):
|
def setEnableListening(self, p_enable):
|
||||||
self.m_listening = p_enable
|
if self.m_listening == False and p_enable == True:
|
||||||
|
self.m_stream.start_stream()
|
||||||
|
self.m_listening = p_enable
|
||||||
|
self.m_stop = False
|
||||||
|
self.m_thread = threading.Thread(target=self.doListen, args=())
|
||||||
|
self.m_thread.start()
|
||||||
|
elif self.m_listening == True and p_enable == False:
|
||||||
|
self.stop()
|
||||||
|
|
||||||
def run(self):
|
def doListen(self):
|
||||||
|
print("Detection started")
|
||||||
|
self.m_listening = True
|
||||||
self.m_decoder.start_utt()
|
self.m_decoder.start_utt()
|
||||||
while self.m_stop != True:
|
while self.m_stop != True:
|
||||||
buf = self.m_stream.read(1024)
|
buf = self.m_stream.read(1024)
|
||||||
@ -65,8 +99,14 @@ class ProfileExecutor(threading.Thread):
|
|||||||
self.m_decoder.process_raw(buf, False, False)
|
self.m_decoder.process_raw(buf, False, False)
|
||||||
|
|
||||||
if self.m_decoder.hyp() != None:
|
if self.m_decoder.hyp() != None:
|
||||||
# print([(seg.word, seg.prob, seg.start_frame, seg.end_frame) for seg in decoder.seg()])
|
#print([(seg.word, seg.prob, seg.start_frame, seg.end_frame) for seg in self.m_decoder.seg()])
|
||||||
# print("Detected keyword, restarting search")
|
#print("Detected keyword, restarting search")
|
||||||
|
|
||||||
|
# hack :)
|
||||||
|
for seg in self.m_decoder.seg():
|
||||||
|
print("Detected: ",seg.word)
|
||||||
|
break
|
||||||
|
|
||||||
#
|
#
|
||||||
# Here you run the code you want based on keyword
|
# Here you run the code you want based on keyword
|
||||||
#
|
#
|
||||||
@ -76,9 +116,21 @@ class ProfileExecutor(threading.Thread):
|
|||||||
self.m_decoder.end_utt()
|
self.m_decoder.end_utt()
|
||||||
self.m_decoder.start_utt()
|
self.m_decoder.start_utt()
|
||||||
|
|
||||||
|
# def run(self):
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.m_stop = True
|
if self.m_listening == True:
|
||||||
threading.Thread.join(self)
|
self.m_stop = True
|
||||||
|
self.m_listening = False
|
||||||
|
self.m_decoder.end_utt()
|
||||||
|
self.m_thread.join()
|
||||||
|
self.m_stream.stop_stream()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
self.stop()
|
||||||
|
self.m_stream.close()
|
||||||
|
self.m_pyaudio.terminate()
|
||||||
|
|
||||||
|
|
||||||
def doAction(self, p_action):
|
def doAction(self, p_action):
|
||||||
# {'name': 'key action', 'key': 'left', 'type': 0}
|
# {'name': 'key action', 'key': 'left', 'type': 0}
|
||||||
@ -91,20 +143,14 @@ class ProfileExecutor(threading.Thread):
|
|||||||
if w_actionName == 'key action':
|
if w_actionName == 'key action':
|
||||||
w_key = p_action['key']
|
w_key = p_action['key']
|
||||||
w_type = p_action['type']
|
w_type = p_action['type']
|
||||||
if w_type == 1:
|
self.pressKey(w_key, w_type)
|
||||||
keyboard.press(w_key)
|
|
||||||
print("pressed key: ", w_key)
|
|
||||||
elif w_type == 0:
|
|
||||||
keyboard.release(w_key)
|
|
||||||
print("released key: ", w_key)
|
|
||||||
elif w_type == 10:
|
|
||||||
keyboard.press(w_key)
|
|
||||||
keyboard.release(w_key)
|
|
||||||
print("pressed and released key: ", w_key)
|
|
||||||
elif w_actionName == 'pause action':
|
elif w_actionName == 'pause action':
|
||||||
|
print("Sleep ", p_action['time'])
|
||||||
time.sleep(p_action['time'])
|
time.sleep(p_action['time'])
|
||||||
elif w_actionName == 'command stop action':
|
elif w_actionName == 'command stop action':
|
||||||
self.stopCommand(p_action['command name'])
|
self.stopCommand(p_action['command name'])
|
||||||
|
elif w_actionName == 'command play sound' or w_actionName == 'play sound':
|
||||||
|
self.playSound(p_action)
|
||||||
elif w_actionName == 'mouse move action':
|
elif w_actionName == 'mouse move action':
|
||||||
if p_action['absolute']:
|
if p_action['absolute']:
|
||||||
ProfileExecutor.mouse.position([p_action['x'], p_action['y']])
|
ProfileExecutor.mouse.position([p_action['x'], p_action['y']])
|
||||||
@ -167,7 +213,7 @@ class ProfileExecutor(threading.Thread):
|
|||||||
w_commands = self.m_profile['commands']
|
w_commands = self.m_profile['commands']
|
||||||
flag = False
|
flag = False
|
||||||
for w_command in w_commands:
|
for w_command in w_commands:
|
||||||
if w_command['name'] == p_cmdName:
|
if w_command['name'].lower() == p_cmdName:
|
||||||
flag = True
|
flag = True
|
||||||
break
|
break
|
||||||
if flag == False:
|
if flag == False:
|
||||||
@ -195,3 +241,66 @@ class ProfileExecutor(threading.Thread):
|
|||||||
if p_cmdName in self.m_cmdThreads.keys():
|
if p_cmdName in self.m_cmdThreads.keys():
|
||||||
self.m_cmdThreads[p_cmdName].stop()
|
self.m_cmdThreads[p_cmdName].stop()
|
||||||
del self.m_cmdThreads[p_cmdName]
|
del self.m_cmdThreads[p_cmdName]
|
||||||
|
|
||||||
|
def playSound(self, p_cmdName):
|
||||||
|
sound_file = './voicepacks/' + p_cmdName['pack'] + '/' + p_cmdName['cat'] + '/' + p_cmdName['file']
|
||||||
|
self.m_sound.play(sound_file)
|
||||||
|
|
||||||
|
|
||||||
|
def pressKey(self, w_key, w_type):
|
||||||
|
if self.p_parent.m_config['noroot'] == 1:
|
||||||
|
# xdotool has a different key mapping. translate old existing mappings of special keys
|
||||||
|
# use this to find key name: xev -event keyboard
|
||||||
|
w_key = re.sub('left ctrl', 'Control_L', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('right ctrl', 'Control_R', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('left shift', 'Shift_L', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('right shift', 'Shift_R', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('left alt', 'Alt_L', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('right alt', 'Alt_R', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('left windows', 'Super_L', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('right windows', 'Super_R', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('tab', 'Tab', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('esc', 'Escape', w_key, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
w_key = re.sub('left', 'Left', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('right', 'Right', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('up', 'Up', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('down', 'Down', w_key, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
w_key = re.sub('ins$', 'Insert', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('del$', 'Delete', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('home', 'Home', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('end', 'End', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('Page\s?up', 'Prior', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('Page\s?down', 'Next', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('return', 'Return', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('enter', 'Return', w_key, flags=re.IGNORECASE)
|
||||||
|
w_key = re.sub('backspace', 'BackSpace', w_key, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
w_key = w_key.replace('insert', 'Insert')
|
||||||
|
w_key = w_key.replace('delete', 'Delete')
|
||||||
|
|
||||||
|
window_cmd = ""
|
||||||
|
if not self.p_parent.m_config['xdowindowid'] == None:
|
||||||
|
window_cmd = " windowactivate --sync " + str(self.p_parent.m_config['xdowindowid'])
|
||||||
|
|
||||||
|
if w_type == 1:
|
||||||
|
os.system('xdotool ' + window_cmd + ' keydown ' + str(w_key) )
|
||||||
|
print("pressed key: ", w_key)
|
||||||
|
elif w_type == 0:
|
||||||
|
os.system('xdotool' + window_cmd + ' keyup ' + str(w_key))
|
||||||
|
print("released key: ", w_key)
|
||||||
|
elif w_type == 10:
|
||||||
|
os.system('xdotool' + window_cmd + ' key ' + str(w_key))
|
||||||
|
print("pressed and released key: ", w_key)
|
||||||
|
else:
|
||||||
|
if w_type == 1:
|
||||||
|
keyboard.press(w_key)
|
||||||
|
print("pressed key: ", w_key)
|
||||||
|
elif w_type == 0:
|
||||||
|
keyboard.release(w_key)
|
||||||
|
print("released key: ", w_key)
|
||||||
|
elif w_type == 10:
|
||||||
|
keyboard.press(w_key)
|
||||||
|
keyboard.release(w_key)
|
||||||
|
print("pressed and released key: ", w_key)
|
164
soundactioneditwnd.py
Normal file
164
soundactioneditwnd.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
from PyQt5.QtCore import *
|
||||||
|
from PyQt5.QtGui import *
|
||||||
|
from PyQt5.QtWidgets import *
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
from ui_soundactioneditwnd import Ui_SoundSelect
|
||||||
|
|
||||||
|
class SoundActionEditWnd(QDialog):
|
||||||
|
def __init__(self, p_sounds, p_soundAction = None, p_parent = None):
|
||||||
|
super().__init__(p_parent)
|
||||||
|
self.ui = Ui_SoundSelect()
|
||||||
|
self.ui.setupUi(self)
|
||||||
|
|
||||||
|
if p_sounds == None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.p_sounds = p_sounds
|
||||||
|
self.selectedVoicepack = False
|
||||||
|
self.selectedCategory = False
|
||||||
|
self.selectedFile = False
|
||||||
|
|
||||||
|
self.ui.buttonOkay.clicked.connect(self.slotOK)
|
||||||
|
self.ui.buttonCancel.clicked.connect(super().reject)
|
||||||
|
self.ui.buttonPlaySound.clicked.connect(self.playSound)
|
||||||
|
self.ui.buttonStopSound.clicked.connect(self.stopSound)
|
||||||
|
self.ui.buttonOkay.setEnabled(False)
|
||||||
|
|
||||||
|
# restore stuff when editing
|
||||||
|
if not p_soundAction == None:
|
||||||
|
self.selectedVoicepack = p_soundAction['pack']
|
||||||
|
self.selectedCategory = p_soundAction['cat']
|
||||||
|
self.selectedFile = p_soundAction['file']
|
||||||
|
self.ui.buttonOkay.setEnabled(True)
|
||||||
|
|
||||||
|
self.listVoicepacks_model = QStandardItemModel()
|
||||||
|
self.ui.listVoicepacks.setModel(self.listVoicepacks_model)
|
||||||
|
self.ui.listVoicepacks.clicked.connect(self.onVoicepackSelect)
|
||||||
|
|
||||||
|
self.listCategories_model = QStandardItemModel()
|
||||||
|
self.ui.listCategories.setModel(self.listCategories_model)
|
||||||
|
self.ui.listCategories.clicked.connect(self.onCategorySelect)
|
||||||
|
|
||||||
|
self.listFiles_model = QStandardItemModel()
|
||||||
|
self.ui.listFiles.setModel(self.listFiles_model)
|
||||||
|
self.ui.listFiles.clicked.connect(self.onFileSelect)
|
||||||
|
self.ui.listFiles.doubleClicked.connect(self.selectAndPlay)
|
||||||
|
|
||||||
|
s = sorted(p_sounds.m_sounds)
|
||||||
|
for v in s:
|
||||||
|
item = QStandardItem(v)
|
||||||
|
self.listVoicepacks_model.appendRow(item)
|
||||||
|
|
||||||
|
self.ui.filterCategories.textChanged.connect(self.populateCategories)
|
||||||
|
self.ui.filterFiles.textChanged.connect(self.populateFiles)
|
||||||
|
|
||||||
|
|
||||||
|
self.populateCategories(False)
|
||||||
|
self.populateFiles(False)
|
||||||
|
|
||||||
|
# when editing, select old entries
|
||||||
|
if not self.selectedVoicepack == False:
|
||||||
|
item = self.listVoicepacks_model.findItems(self.selectedVoicepack)
|
||||||
|
if len(item) > 0:
|
||||||
|
index = self.listVoicepacks_model.indexFromItem(item[0])
|
||||||
|
self.ui.listVoicepacks.setCurrentIndex(index)
|
||||||
|
|
||||||
|
if not self.selectedCategory == False:
|
||||||
|
item = self.listCategories_model.findItems(self.selectedCategory)
|
||||||
|
if len(item) > 0:
|
||||||
|
index = self.listCategories_model.indexFromItem(item[0])
|
||||||
|
self.ui.listCategories.setCurrentIndex(index)
|
||||||
|
|
||||||
|
if not self.selectedFile == False:
|
||||||
|
item = self.listFiles_model.findItems(self.selectedFile)
|
||||||
|
if len(item) > 0:
|
||||||
|
index = self.listFiles_model.indexFromItem(item[0])
|
||||||
|
self.ui.listFiles.setCurrentIndex(index)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def slotOK(self):
|
||||||
|
self.m_soundAction = {'name': 'play sound', 'pack': self.selectedVoicepack, 'cat' : self.selectedCategory, 'file' : self.selectedFile}
|
||||||
|
super().accept()
|
||||||
|
|
||||||
|
def slotCancel(self):
|
||||||
|
super().reject()
|
||||||
|
|
||||||
|
def onVoicepackSelect(self):
|
||||||
|
index = self.ui.listVoicepacks.currentIndex()
|
||||||
|
itemText = index.data()
|
||||||
|
self.selectedVoicepack = itemText
|
||||||
|
self.populateCategories()
|
||||||
|
self.ui.buttonOkay.setEnabled(False)
|
||||||
|
self.ui.buttonPlaySound.setEnabled(False)
|
||||||
|
|
||||||
|
def onCategorySelect(self):
|
||||||
|
index = self.ui.listCategories.currentIndex()
|
||||||
|
itemText = index.data()
|
||||||
|
self.selectedCategory = itemText
|
||||||
|
self.populateFiles()
|
||||||
|
self.ui.buttonOkay.setEnabled(False)
|
||||||
|
self.ui.buttonPlaySound.setEnabled(False)
|
||||||
|
|
||||||
|
|
||||||
|
def onFileSelect(self):
|
||||||
|
index = self.ui.listFiles.currentIndex()
|
||||||
|
itemText = index.data()
|
||||||
|
self.selectedFile = itemText
|
||||||
|
self.ui.buttonOkay.setEnabled(True)
|
||||||
|
self.ui.buttonPlaySound.setEnabled(True)
|
||||||
|
|
||||||
|
def selectAndPlay(self):
|
||||||
|
self.onFileSelect()
|
||||||
|
self.playSound()
|
||||||
|
|
||||||
|
def populateCategories(self, reset = True):
|
||||||
|
if self.selectedVoicepack == False:
|
||||||
|
return
|
||||||
|
|
||||||
|
if reset == True:
|
||||||
|
self.listCategories_model.removeRows( 0, self.listCategories_model.rowCount() )
|
||||||
|
self.listFiles_model.removeRows( 0, self.listFiles_model.rowCount() )
|
||||||
|
self.selectedCategory = False
|
||||||
|
self.selectedFile = False
|
||||||
|
|
||||||
|
filter_categories = self.ui.filterCategories.toPlainText()
|
||||||
|
if len(filter_categories) == 0:
|
||||||
|
filter_categories = None
|
||||||
|
|
||||||
|
s = sorted(self.p_sounds.m_sounds[self.selectedVoicepack])
|
||||||
|
for v in s:
|
||||||
|
if not filter_categories == None:
|
||||||
|
if not re.search(filter_categories, v, re.IGNORECASE):
|
||||||
|
continue
|
||||||
|
item = QStandardItem(v)
|
||||||
|
self.listCategories_model.appendRow(item)
|
||||||
|
|
||||||
|
def populateFiles(self, reset = True):
|
||||||
|
if self.selectedVoicepack == False or self.selectedCategory == False:
|
||||||
|
return
|
||||||
|
|
||||||
|
if reset == True:
|
||||||
|
self.listFiles_model.removeRows( 0, self.listFiles_model.rowCount() )
|
||||||
|
self.selectedFile = False
|
||||||
|
|
||||||
|
filter_files = self.ui.filterFiles.toPlainText()
|
||||||
|
if len(filter_files) == 0:
|
||||||
|
filter_files = None
|
||||||
|
|
||||||
|
s = sorted(self.p_sounds.m_sounds[self.selectedVoicepack][self.selectedCategory])
|
||||||
|
for v in s:
|
||||||
|
if not filter_files == None:
|
||||||
|
if not re.search(filter_files, v, re.IGNORECASE):
|
||||||
|
continue
|
||||||
|
item = QStandardItem(v)
|
||||||
|
self.listFiles_model.appendRow(item)
|
||||||
|
|
||||||
|
def playSound(self):
|
||||||
|
sound_file = './voicepacks/' + self.selectedVoicepack + '/' + self.selectedCategory + '/' + self.selectedFile
|
||||||
|
self.p_sounds.play(sound_file)
|
||||||
|
|
||||||
|
def stopSound(self):
|
||||||
|
self.p_sounds.stop()
|
241
soundactioneditwnd.ui
Normal file
241
soundactioneditwnd.ui
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>SoundSelect</class>
|
||||||
|
<widget class="QGroupBox" name="SoundSelect">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>1119</width>
|
||||||
|
<height>362</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Sound selection</string>
|
||||||
|
</property>
|
||||||
|
<property name="locale">
|
||||||
|
<locale language="English" country="UnitedStates"/>
|
||||||
|
</property>
|
||||||
|
<widget class="QLabel" name="labelVoicepacks">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>10</x>
|
||||||
|
<y>30</y>
|
||||||
|
<width>111</width>
|
||||||
|
<height>17</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="locale">
|
||||||
|
<locale language="English" country="UnitedStates"/>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>VoicePacks:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QLabel" name="labelCategories">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>160</x>
|
||||||
|
<y>30</y>
|
||||||
|
<width>111</width>
|
||||||
|
<height>17</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="locale">
|
||||||
|
<locale language="English" country="UnitedStates"/>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Categories:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QListView" name="listFiles">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>660</x>
|
||||||
|
<y>90</y>
|
||||||
|
<width>451</width>
|
||||||
|
<height>231</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="locale">
|
||||||
|
<locale language="English" country="UnitedStates"/>
|
||||||
|
</property>
|
||||||
|
<property name="editTriggers">
|
||||||
|
<set>QAbstractItemView::NoEditTriggers</set>
|
||||||
|
</property>
|
||||||
|
<property name="showDropIndicator" stdset="0">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="alternatingRowColors">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QListView" name="listCategories">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>160</x>
|
||||||
|
<y>90</y>
|
||||||
|
<width>491</width>
|
||||||
|
<height>231</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="locale">
|
||||||
|
<locale language="English" country="UnitedStates"/>
|
||||||
|
</property>
|
||||||
|
<property name="editTriggers">
|
||||||
|
<set>QAbstractItemView::NoEditTriggers</set>
|
||||||
|
</property>
|
||||||
|
<property name="showDropIndicator" stdset="0">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="alternatingRowColors">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QListView" name="listVoicepacks">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>10</x>
|
||||||
|
<y>50</y>
|
||||||
|
<width>141</width>
|
||||||
|
<height>271</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="locale">
|
||||||
|
<locale language="English" country="UnitedStates"/>
|
||||||
|
</property>
|
||||||
|
<property name="editTriggers">
|
||||||
|
<set>QAbstractItemView::NoEditTriggers</set>
|
||||||
|
</property>
|
||||||
|
<property name="showDropIndicator" stdset="0">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="alternatingRowColors">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QLabel" name="labelFiles">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>660</x>
|
||||||
|
<y>30</y>
|
||||||
|
<width>111</width>
|
||||||
|
<height>17</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="locale">
|
||||||
|
<locale language="English" country="UnitedStates"/>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Voice files:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QPushButton" name="buttonOkay">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>940</x>
|
||||||
|
<y>330</y>
|
||||||
|
<width>80</width>
|
||||||
|
<height>25</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="locale">
|
||||||
|
<locale language="English" country="UnitedStates"/>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Ok</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QPushButton" name="buttonCancel">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>1030</x>
|
||||||
|
<y>330</y>
|
||||||
|
<width>80</width>
|
||||||
|
<height>25</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="locale">
|
||||||
|
<locale language="English" country="UnitedStates"/>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Cancel</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QPlainTextEdit" name="filterCategories">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>160</x>
|
||||||
|
<y>50</y>
|
||||||
|
<width>491</width>
|
||||||
|
<height>31</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Filter categories</string>
|
||||||
|
</property>
|
||||||
|
<property name="statusTip">
|
||||||
|
<string>Filter</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QPlainTextEdit" name="filterFiles">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>660</x>
|
||||||
|
<y>50</y>
|
||||||
|
<width>451</width>
|
||||||
|
<height>31</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Filter voice files</string>
|
||||||
|
</property>
|
||||||
|
<property name="statusTip">
|
||||||
|
<string>Filter</string>
|
||||||
|
</property>
|
||||||
|
<property name="plainText">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QPushButton" name="buttonPlaySound">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>410</x>
|
||||||
|
<y>330</y>
|
||||||
|
<width>80</width>
|
||||||
|
<height>25</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Play Sound</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QPushButton" name="buttonStopSound">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>500</x>
|
||||||
|
<y>330</y>
|
||||||
|
<width>80</width>
|
||||||
|
<height>25</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Stop Sound</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
87
soundfiles.py
Normal file
87
soundfiles.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from os import path
|
||||||
|
import subprocess
|
||||||
|
import shlex
|
||||||
|
import signal
|
||||||
|
|
||||||
|
# I've tried a couple of libs that are capable of playing mp3:
|
||||||
|
# pysound - offers no way to stop sounds. can't play files with whitespace in path
|
||||||
|
# pygame - has problems with certain mp3 files from voice packs
|
||||||
|
# mpg123 - does not offer volume control. need to be threaded as it's a system binary
|
||||||
|
#
|
||||||
|
# ffplay - need to be threaded as it's a system binary. i picked this one as it uses ffmpeg which
|
||||||
|
# is an excellent tool and should be installed on any system already
|
||||||
|
# I needed some process stuff to be able to stop an already playing sound (kill ffplay subprocess)
|
||||||
|
|
||||||
|
|
||||||
|
class SoundFiles():
|
||||||
|
def __init__(self):
|
||||||
|
print("SoundFiles: init")
|
||||||
|
self.m_sounds = {}
|
||||||
|
self.scanSoundFiles()
|
||||||
|
self.thread_play = False
|
||||||
|
self.volume = 100
|
||||||
|
|
||||||
|
def scanSoundFiles(self):
|
||||||
|
print("SoundFiles: scanning")
|
||||||
|
if not path.exists('./voicepacks'):
|
||||||
|
print("No folder 'voicepacks' found. Please create one and copy all your voicepacks in there.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk("./voicepacks"):
|
||||||
|
for file in files:
|
||||||
|
if file.endswith(".mp3"):
|
||||||
|
# we expect a path like this:
|
||||||
|
# voicepacks/VOICEPACKNAME/COMMANDGROUP/(FURTHER_OPTIONAL_FOLDERS/)FILE
|
||||||
|
path_parts = root.split('/')
|
||||||
|
|
||||||
|
if len(path_parts) < 4:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not path_parts[2] in self.m_sounds:
|
||||||
|
self.m_sounds[path_parts[2]] = {}
|
||||||
|
|
||||||
|
category = path_parts[3]
|
||||||
|
# there might be subfolders, so we have more than just 4 split results...
|
||||||
|
# for the ease of my mind, we concat the voicepack subfolders to 1 category name
|
||||||
|
# like voicepacks/hcspack/Characters/Astra/blah.mp3 will become:
|
||||||
|
#
|
||||||
|
# voicepack = hcspack
|
||||||
|
# category = Characters/Astra
|
||||||
|
# file = blah.mp4
|
||||||
|
|
||||||
|
if len(path_parts) > 4:
|
||||||
|
for i in range(4, len(path_parts)):
|
||||||
|
category = category + '/' + path_parts[i]
|
||||||
|
|
||||||
|
if not category in self.m_sounds[path_parts[2]]:
|
||||||
|
self.m_sounds[path_parts[2]][category] = []
|
||||||
|
|
||||||
|
self.m_sounds[path_parts[2]][category].append(file)
|
||||||
|
|
||||||
|
def play(self, sound_file):
|
||||||
|
if not os.path.isfile(sound_file):
|
||||||
|
print("ERROR - Sound file not found: ", sound_file)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
# construct shell command. use shlex to split it up into valid args for Popen.
|
||||||
|
cmd = "ffplay -nodisp -autoexit -loglevel quiet -volume " + str(self.volume) + " \"" + sound_file + "\"";
|
||||||
|
args = shlex.split(cmd)
|
||||||
|
self.thread_play = subprocess.Popen(args)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if not self.thread_play == False:
|
||||||
|
# that aint no nice, but it's the only way i got the subprocess reliably killed.
|
||||||
|
# self.thread_play.terminate() or kill() should do the trick, but it won't
|
||||||
|
try:
|
||||||
|
os.kill(self.thread_play.pid, signal.SIGKILL)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setVolume(self, volume):
|
||||||
|
self.volume = volume;
|
@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
# Form implementation generated from reading ui file 'mainwnd.ui'
|
# Form implementation generated from reading ui file 'mainwnd.ui'
|
||||||
#
|
#
|
||||||
# Created by: PyQt5 UI code generator 5.12.1
|
# Created by: PyQt5 UI code generator 5.14.2
|
||||||
#
|
#
|
||||||
# WARNING! All changes made in this file will be lost!
|
# WARNING! All changes made in this file will be lost!
|
||||||
|
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
|
||||||
@ -28,6 +29,19 @@ class Ui_MainWidget(object):
|
|||||||
self.horizontalLayout_2.addWidget(self.listeningChk)
|
self.horizontalLayout_2.addWidget(self.listeningChk)
|
||||||
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
self.horizontalLayout_2.addItem(spacerItem)
|
self.horizontalLayout_2.addItem(spacerItem)
|
||||||
|
self.groupBox = QtWidgets.QGroupBox(MainWidget)
|
||||||
|
self.groupBox.setMinimumSize(QtCore.QSize(200, 42))
|
||||||
|
self.groupBox.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
|
self.groupBox.setObjectName("groupBox")
|
||||||
|
self.sliderVolume = QtWidgets.QSlider(self.groupBox)
|
||||||
|
self.sliderVolume.setGeometry(QtCore.QRect(10, 20, 181, 21))
|
||||||
|
self.sliderVolume.setMaximum(100)
|
||||||
|
self.sliderVolume.setProperty("value", 100)
|
||||||
|
self.sliderVolume.setOrientation(QtCore.Qt.Horizontal)
|
||||||
|
self.sliderVolume.setObjectName("sliderVolume")
|
||||||
|
self.horizontalLayout_2.addWidget(self.groupBox)
|
||||||
|
spacerItem1 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout_2.addItem(spacerItem1)
|
||||||
self.ok = QtWidgets.QPushButton(MainWidget)
|
self.ok = QtWidgets.QPushButton(MainWidget)
|
||||||
self.ok.setMinimumSize(QtCore.QSize(130, 0))
|
self.ok.setMinimumSize(QtCore.QSize(130, 0))
|
||||||
self.ok.setObjectName("ok")
|
self.ok.setObjectName("ok")
|
||||||
@ -36,33 +50,40 @@ class Ui_MainWidget(object):
|
|||||||
self.cancel.setMinimumSize(QtCore.QSize(130, 0))
|
self.cancel.setMinimumSize(QtCore.QSize(130, 0))
|
||||||
self.cancel.setObjectName("cancel")
|
self.cancel.setObjectName("cancel")
|
||||||
self.horizontalLayout_2.addWidget(self.cancel)
|
self.horizontalLayout_2.addWidget(self.cancel)
|
||||||
self.gridLayout_2.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
|
self.gridLayout_2.addLayout(self.horizontalLayout_2, 2, 0, 1, 1)
|
||||||
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||||
self.horizontalLayout.setSpacing(20)
|
self.horizontalLayout.setSpacing(20)
|
||||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||||
self.label = QtWidgets.QLabel(MainWidget)
|
self.label = QtWidgets.QLabel(MainWidget)
|
||||||
|
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
|
||||||
|
sizePolicy.setHorizontalStretch(0)
|
||||||
|
sizePolicy.setVerticalStretch(0)
|
||||||
|
sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
|
||||||
|
self.label.setSizePolicy(sizePolicy)
|
||||||
self.label.setMinimumSize(QtCore.QSize(60, 0))
|
self.label.setMinimumSize(QtCore.QSize(60, 0))
|
||||||
self.label.setObjectName("label")
|
self.label.setObjectName("label")
|
||||||
self.horizontalLayout.addWidget(self.label)
|
self.horizontalLayout.addWidget(self.label)
|
||||||
self.profileCbx = QtWidgets.QComboBox(MainWidget)
|
self.profileCbx = QtWidgets.QComboBox(MainWidget)
|
||||||
self.profileCbx.setMinimumSize(QtCore.QSize(250, 0))
|
self.profileCbx.setMinimumSize(QtCore.QSize(300, 0))
|
||||||
self.profileCbx.setObjectName("profileCbx")
|
self.profileCbx.setObjectName("profileCbx")
|
||||||
self.horizontalLayout.addWidget(self.profileCbx)
|
self.horizontalLayout.addWidget(self.profileCbx)
|
||||||
self.addBut = QtWidgets.QPushButton(MainWidget)
|
self.addBut = QtWidgets.QPushButton(MainWidget)
|
||||||
self.addBut.setMinimumSize(QtCore.QSize(130, 0))
|
self.addBut.setMinimumSize(QtCore.QSize(75, 0))
|
||||||
self.addBut.setObjectName("addBut")
|
self.addBut.setObjectName("addBut")
|
||||||
self.horizontalLayout.addWidget(self.addBut)
|
self.horizontalLayout.addWidget(self.addBut)
|
||||||
self.editBut = QtWidgets.QPushButton(MainWidget)
|
self.editBut = QtWidgets.QPushButton(MainWidget)
|
||||||
self.editBut.setMinimumSize(QtCore.QSize(130, 0))
|
self.editBut.setMinimumSize(QtCore.QSize(75, 0))
|
||||||
self.editBut.setObjectName("editBut")
|
self.editBut.setObjectName("editBut")
|
||||||
self.horizontalLayout.addWidget(self.editBut)
|
self.horizontalLayout.addWidget(self.editBut)
|
||||||
|
self.copyBut = QtWidgets.QPushButton(MainWidget)
|
||||||
|
self.copyBut.setMinimumSize(QtCore.QSize(75, 0))
|
||||||
|
self.copyBut.setObjectName("copyBut")
|
||||||
|
self.horizontalLayout.addWidget(self.copyBut)
|
||||||
self.removeBut = QtWidgets.QPushButton(MainWidget)
|
self.removeBut = QtWidgets.QPushButton(MainWidget)
|
||||||
self.removeBut.setMinimumSize(QtCore.QSize(130, 0))
|
self.removeBut.setMinimumSize(QtCore.QSize(75, 0))
|
||||||
self.removeBut.setObjectName("removeBut")
|
self.removeBut.setObjectName("removeBut")
|
||||||
self.horizontalLayout.addWidget(self.removeBut)
|
self.horizontalLayout.addWidget(self.removeBut)
|
||||||
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
self.gridLayout_2.addLayout(self.horizontalLayout, 1, 0, 1, 1)
|
||||||
self.horizontalLayout.addItem(spacerItem1)
|
|
||||||
self.gridLayout_2.addLayout(self.horizontalLayout, 0, 0, 1, 1)
|
|
||||||
|
|
||||||
self.retranslateUi(MainWidget)
|
self.retranslateUi(MainWidget)
|
||||||
QtCore.QMetaObject.connectSlotsByName(MainWidget)
|
QtCore.QMetaObject.connectSlotsByName(MainWidget)
|
||||||
@ -71,21 +92,13 @@ class Ui_MainWidget(object):
|
|||||||
_translate = QtCore.QCoreApplication.translate
|
_translate = QtCore.QCoreApplication.translate
|
||||||
MainWidget.setWindowTitle(_translate("MainWidget", "LinVAM"))
|
MainWidget.setWindowTitle(_translate("MainWidget", "LinVAM"))
|
||||||
self.listeningChk.setText(_translate("MainWidget", "Enable Listening"))
|
self.listeningChk.setText(_translate("MainWidget", "Enable Listening"))
|
||||||
|
self.groupBox.setTitle(_translate("MainWidget", "Voice Volume"))
|
||||||
|
self.sliderVolume.setToolTip(_translate("MainWidget", "Change voice volume"))
|
||||||
self.ok.setText(_translate("MainWidget", "OK"))
|
self.ok.setText(_translate("MainWidget", "OK"))
|
||||||
self.cancel.setText(_translate("MainWidget", "Cancel"))
|
self.cancel.setText(_translate("MainWidget", "Cancel"))
|
||||||
self.label.setText(_translate("MainWidget", "Profile:"))
|
self.label.setText(_translate("MainWidget", "Profile:"))
|
||||||
self.addBut.setText(_translate("MainWidget", "Add"))
|
self.addBut.setText(_translate("MainWidget", "Add"))
|
||||||
self.editBut.setText(_translate("MainWidget", "Edit"))
|
self.editBut.setText(_translate("MainWidget", "Edit"))
|
||||||
|
self.copyBut.setToolTip(_translate("MainWidget", "Copy currently selected profile"))
|
||||||
|
self.copyBut.setText(_translate("MainWidget", "Copy"))
|
||||||
self.removeBut.setText(_translate("MainWidget", "Remove"))
|
self.removeBut.setText(_translate("MainWidget", "Remove"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import sys
|
|
||||||
app = QtWidgets.QApplication(sys.argv)
|
|
||||||
MainWidget = QtWidgets.QWidget()
|
|
||||||
ui = Ui_MainWidget()
|
|
||||||
ui.setupUi(MainWidget)
|
|
||||||
MainWidget.show()
|
|
||||||
sys.exit(app.exec_())
|
|
||||||
|
93
ui_soundactioneditwnd.py
Normal file
93
ui_soundactioneditwnd.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Form implementation generated from reading ui file 'soundactioneditwnd.ui'
|
||||||
|
#
|
||||||
|
# Created by: PyQt5 UI code generator 5.14.2
|
||||||
|
#
|
||||||
|
# WARNING! All changes made in this file will be lost!
|
||||||
|
|
||||||
|
|
||||||
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
|
||||||
|
class Ui_SoundSelect(object):
|
||||||
|
def setupUi(self, SoundSelect):
|
||||||
|
SoundSelect.setObjectName("SoundSelect")
|
||||||
|
SoundSelect.resize(1119, 362)
|
||||||
|
SoundSelect.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||||
|
self.labelVoicepacks = QtWidgets.QLabel(SoundSelect)
|
||||||
|
self.labelVoicepacks.setGeometry(QtCore.QRect(10, 30, 111, 17))
|
||||||
|
self.labelVoicepacks.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||||
|
self.labelVoicepacks.setObjectName("labelVoicepacks")
|
||||||
|
self.labelCategories = QtWidgets.QLabel(SoundSelect)
|
||||||
|
self.labelCategories.setGeometry(QtCore.QRect(160, 30, 111, 17))
|
||||||
|
self.labelCategories.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||||
|
self.labelCategories.setObjectName("labelCategories")
|
||||||
|
self.listFiles = QtWidgets.QListView(SoundSelect)
|
||||||
|
self.listFiles.setGeometry(QtCore.QRect(660, 90, 451, 231))
|
||||||
|
self.listFiles.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||||
|
self.listFiles.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||||
|
self.listFiles.setProperty("showDropIndicator", False)
|
||||||
|
self.listFiles.setAlternatingRowColors(True)
|
||||||
|
self.listFiles.setObjectName("listFiles")
|
||||||
|
self.listCategories = QtWidgets.QListView(SoundSelect)
|
||||||
|
self.listCategories.setEnabled(True)
|
||||||
|
self.listCategories.setGeometry(QtCore.QRect(160, 90, 491, 231))
|
||||||
|
self.listCategories.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||||
|
self.listCategories.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||||
|
self.listCategories.setProperty("showDropIndicator", False)
|
||||||
|
self.listCategories.setAlternatingRowColors(True)
|
||||||
|
self.listCategories.setObjectName("listCategories")
|
||||||
|
self.listVoicepacks = QtWidgets.QListView(SoundSelect)
|
||||||
|
self.listVoicepacks.setGeometry(QtCore.QRect(10, 50, 141, 271))
|
||||||
|
self.listVoicepacks.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||||
|
self.listVoicepacks.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||||
|
self.listVoicepacks.setProperty("showDropIndicator", False)
|
||||||
|
self.listVoicepacks.setAlternatingRowColors(True)
|
||||||
|
self.listVoicepacks.setObjectName("listVoicepacks")
|
||||||
|
self.labelFiles = QtWidgets.QLabel(SoundSelect)
|
||||||
|
self.labelFiles.setGeometry(QtCore.QRect(660, 30, 111, 17))
|
||||||
|
self.labelFiles.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||||
|
self.labelFiles.setObjectName("labelFiles")
|
||||||
|
self.buttonOkay = QtWidgets.QPushButton(SoundSelect)
|
||||||
|
self.buttonOkay.setEnabled(True)
|
||||||
|
self.buttonOkay.setGeometry(QtCore.QRect(940, 330, 80, 25))
|
||||||
|
self.buttonOkay.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||||
|
self.buttonOkay.setObjectName("buttonOkay")
|
||||||
|
self.buttonCancel = QtWidgets.QPushButton(SoundSelect)
|
||||||
|
self.buttonCancel.setGeometry(QtCore.QRect(1030, 330, 80, 25))
|
||||||
|
self.buttonCancel.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||||
|
self.buttonCancel.setObjectName("buttonCancel")
|
||||||
|
self.filterCategories = QtWidgets.QPlainTextEdit(SoundSelect)
|
||||||
|
self.filterCategories.setGeometry(QtCore.QRect(160, 50, 491, 31))
|
||||||
|
self.filterCategories.setObjectName("filterCategories")
|
||||||
|
self.filterFiles = QtWidgets.QPlainTextEdit(SoundSelect)
|
||||||
|
self.filterFiles.setGeometry(QtCore.QRect(660, 50, 451, 31))
|
||||||
|
self.filterFiles.setPlainText("")
|
||||||
|
self.filterFiles.setObjectName("filterFiles")
|
||||||
|
self.buttonPlaySound = QtWidgets.QPushButton(SoundSelect)
|
||||||
|
self.buttonPlaySound.setGeometry(QtCore.QRect(410, 330, 80, 25))
|
||||||
|
self.buttonPlaySound.setToolTip("")
|
||||||
|
self.buttonPlaySound.setObjectName("buttonPlaySound")
|
||||||
|
self.buttonStopSound = QtWidgets.QPushButton(SoundSelect)
|
||||||
|
self.buttonStopSound.setGeometry(QtCore.QRect(500, 330, 80, 25))
|
||||||
|
self.buttonStopSound.setToolTip("")
|
||||||
|
self.buttonStopSound.setObjectName("buttonStopSound")
|
||||||
|
|
||||||
|
self.retranslateUi(SoundSelect)
|
||||||
|
QtCore.QMetaObject.connectSlotsByName(SoundSelect)
|
||||||
|
|
||||||
|
def retranslateUi(self, SoundSelect):
|
||||||
|
_translate = QtCore.QCoreApplication.translate
|
||||||
|
SoundSelect.setWindowTitle(_translate("SoundSelect", "Sound selection"))
|
||||||
|
self.labelVoicepacks.setText(_translate("SoundSelect", "VoicePacks:"))
|
||||||
|
self.labelCategories.setText(_translate("SoundSelect", "Categories:"))
|
||||||
|
self.labelFiles.setText(_translate("SoundSelect", "Voice files:"))
|
||||||
|
self.buttonOkay.setText(_translate("SoundSelect", "Ok"))
|
||||||
|
self.buttonCancel.setText(_translate("SoundSelect", "Cancel"))
|
||||||
|
self.filterCategories.setToolTip(_translate("SoundSelect", "Filter categories"))
|
||||||
|
self.filterCategories.setStatusTip(_translate("SoundSelect", "Filter"))
|
||||||
|
self.filterFiles.setToolTip(_translate("SoundSelect", "Filter voice files"))
|
||||||
|
self.filterFiles.setStatusTip(_translate("SoundSelect", "Filter"))
|
||||||
|
self.buttonPlaySound.setText(_translate("SoundSelect", "Play Sound"))
|
||||||
|
self.buttonStopSound.setText(_translate("SoundSelect", "Stop Sound"))
|
Loading…
Reference in New Issue
Block a user