import os import sys import json from time import time import threading import soundfile as sf import sounddevice as sd from pynput.keyboard import Listener from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5.QtCore import QFile, QTextStream from PyQt5.QtWidgets import QApplication import ui_preferences import ui_hotkeys import ui_sundpood import ui_overlay import themes import keys ###! UI !### class OverlayUi(QtWidgets.QMainWindow, ui_overlay.Ui_MainWindow): def __init__(self, parent=None): super().__init__(parent) self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint) self.setGeometry(QtCore.QRect(0, 0, 250, 20)) self.setupUi(self) def keyPressEvent(self, e): if e.key() == QtCore.Qt.Key_F1: self.hide() win.show() class PreferencesUi(QtWidgets.QMainWindow, ui_preferences.Ui_MainWindow): def __init__(self, parent=None): super().__init__(parent) self.setWindowFlags(QtCore.Qt.FramelessWindowHint) self.setupUi(self) def mousePressEvent(self, event): self.offset = event.pos() def mouseMoveEvent(self, event): x = event.globalX() y = event.globalY() x_w = self.offset.x() y_w = self.offset.y() self.move(x-x_w, y-y_w) class HotkeysUi(QtWidgets.QMainWindow, ui_hotkeys.Ui_MainWindow): def __init__(self, parent=None): super().__init__(parent) self.setWindowFlags(QtCore.Qt.FramelessWindowHint) self.setupUi(self) def mousePressEvent(self, event): self.offset = event.pos() def mouseMoveEvent(self, event): x = event.globalX() y = event.globalY() x_w = self.offset.x() y_w = self.offset.y() self.move(x-x_w, y-y_w) class MainUi(QtWidgets.QMainWindow, ui_sundpood.Ui_MainWindow): def __init__(self, parent=None): super().__init__(parent) self.setWindowFlags(QtCore.Qt.FramelessWindowHint) self.setupUi(self) def mousePressEvent(self, event): self.offset = event.pos() def mouseMoveEvent(self, event): x = event.globalX() y = event.globalY() x_w = self.offset.x() y_w = self.offset.y() self.move(x-x_w, y-y_w) def keyPressEvent(self, e): if e.key() == QtCore.Qt.Key_F1: pref.close() self.hide() over.show() def closeEvent(self, event): hotk.close() pref.close() ###! JSON !### def jsonread(file): ''' Чтение JSON файла file file - имя JSON файла, может содержать полный или относительный путь ''' with open(file, "r", encoding='utf-8') as read_file: data = json.load(read_file) return data def jsonwrite(file, data): ''' Запись в JSON файл file данные data file - имя JSON файла, может содержать полный или относительный путь data - данные, может быть словарем/списком/кортежем/строкой/числом ''' with open(file, 'w', encoding='utf-8') as write_file: write_file.write(json.dumps(data)) ###! FUNCTIONS !### def toggle_stylesheet(path): ''' Toggle the stylesheet to use the desired path in the Qt resource system (prefixed by `:/`) or generically (a path to a file on system). :path: A full path to a resource or file on system ''' # get the QApplication instance, or crash if not set app = QApplication.instance() if app is None: raise RuntimeError("No Qt Application found.") path = os.path.join('themes', path) file = QFile(path) file.open(QFile.ReadOnly | QFile.Text) stream = QTextStream(file) theme = stream.readAll() pref.setStyleSheet(theme) win.setStyleSheet(theme) hotk.setStyleSheet(theme) save() def find_device(): ''' Ищем устройство device и возвращаем его индекс ''' list_ = list(sd.query_devices()) # Список устройств вывода звука device = 'VoiceMeeter Input' # Имя искомого устройства msg = QtWidgets.QMessageBox() # Окно ошибки (Устройство не найдено) msg.setIcon(QtWidgets.QMessageBox.Critical) msg.setText("You don't install VoiceMeeter") msg.setInformativeText('install VoiceMetter from "redist" folder or download it from \nvb-audio.com/Voicemeeter') msg.setWindowTitle('Error') for i in list_: if device in i['name']: return i['name'] msg.exec_() exit() def sound_convert(dir_, format_): ''' Конвертация файла name в папке path в формат format_ по средством ffmpeg.exe path - папка с файлом, может содержать полный или относительный путь name - название файла, может только название файла format_ - формат в который нужно конвертировать ''' if os.path.exists('ffmpeg.exe'): path, name = os.path.split(dir_) old_name = name if os.path.splitext(dir_)[1] in ['.mp3', '.m4a']: name = os.path.splitext(name)[0] + format_ os.system(f'ffmpeg.exe -i "{os.path.join(path, old_name)}" "play.wav" -hide_banner -y') return "play.wav" else: return dir_ def get_files(dir_, config): ''' Сбор всех аудифайлов dir_ - Путь к папке со звуками config - Имя файла настроек ''' msg = QtWidgets.QMessageBox() # Окно ошибки (Не найдены файлы в папке 'sound') msg.setIcon(QtWidgets.QMessageBox.Critical) msg.setText("You don't have any sounds in 'sound' folder") msg.setInformativeText('download sound in .wav / .mp3 / .m4a format') msg.setWindowTitle('Error') ffmsg = QtWidgets.QMessageBox() # Окно ошибки (Не найден ffmpeg.exe) ffmsg.setIcon(QtWidgets.QMessageBox.Critical) ffmsg.setText("You don't have ffmpeg.exe in the program root folder") ffmsg.setInformativeText('download ffmpeg from ffmpeg.org for converting sound files') ffmsg.setWindowTitle('Error') if os.path.exists(dir_): if len(os.listdir(dir_)) == 0: msg.exec_() sounds = [] # Все аудио файлы sounds_list = [f'{dir_}\\'] # Начальная категория if not os.path.exists('ffmpeg.exe'): ffmsg.exec_() exit() for i in os.listdir(dir_): if os.path.isfile(os.path.join(dir_, i)): name = os.path.join(dir_, i) if name == None: sounds_list.append(i) elif os.path.splitext(name)[1] in ['.mp3', '.m4a', '.wav']: sounds_list.append(name) else: sounds_list_cat = [os.path.join(dir_, i)] for x in os.listdir(os.path.join(dir_, i)): if os.path.isfile(os.path.join(dir_, i, x)): if name == None: sounds_list_cat.append(x) elif os.path.splitext(x)[1] in ['.mp3', '.m4a', '.wav']: sounds_list_cat.append(x) sounds.append(sounds_list_cat) sounds.append(sounds_list) if os.path.exists(config): hotkeys = jsonread(config)['hotkeys'] KEYS_CMD = jsonread(config)['KEYS_CMD'] sounds_list = { 'hotkeys':hotkeys, 'sounds':sounds, 'Theme':'None', 'KEYS_CMD':KEYS_CMD} else: sounds_list = { 'hotkeys':{}, 'sounds':sounds, 'Theme':'None', 'KEYS_CMD':{ 'select_move_up' :' ',# вверх 'select_move_down' :' ',# вниз 'select_move_left' :' ',# влево 'select_move_right' :' ',# вправо 'play_sound' :' ',# Играть 'stop_sound' :' ',# Остановить }} jsonwrite(config, sounds_list) else: sounds_list = { 'hotkeys':{}, 'sounds':'', 'Theme':'None', 'KEYS_CMD':{ 'select_move_up' :' ',# вверх 'select_move_down' :' ',# вниз 'select_move_left' :' ',# влево 'select_move_right' :' ',# вправо 'play_sound' :' ',# Играть 'stop_sound' :' ',# Остановить }} jsonwrite(config, sounds_list) os.mkdir(dir_) msg.exec_() def play_sound(*argv): if False in argv: try: sound = os.path.join(win.soundList.item(0).text(), win.soundList.currentItem().text()) except AttributeError: return False else: sound = argv[0] try: sound = sound_convert(sound, '.wav') data, fs = sf.read(sound, dtype='float32') sd.play(data, fs) #os.remove(sound) except RuntimeError: pass def cat_select(cat): win.soundList.clear() for i in menu: if cat == i[0]: win.soundList.addItems(i) def hotkey_remap(): ''' Переназначение хоткея btn - индекс кнопки хоткея в списке HOTKEYS ''' def check(key): key = str(key).replace("'",'') try: sound = os.path.join(win.soundList.item(0).text(), win.soundList.currentItem().text()) except AttributeError: win.hkset.setEnabled(True) return False if key not in keys.forbidden: hotkeys.update({key:sound}) elif key == 'Key.backspace': del hotkeys[find_key(hotkeys, sound)] save() hotk.hotkeyList.clear() for i in sound_get_dict['sounds']: for x in i: x = os.path.join(i[0], x) if x in hotkeys.values(): hotk.hotkeyList.addItem(f'{find_key(hotkeys, x)}\t:{x}') win.hkset.setEnabled(True) return False win.hkset.setEnabled(False) hotkey_remap_Listener = Listener( on_release=check) hotkey_remap_Listener.start() def hotkey_delete(): key = hotk.hotkeyList.currentItem().text().split(':')[0].replace('\t', '') hotkeys.pop(key) save() hotk.hotkeyList.clear() for i in sound_get_dict['sounds']: for x in i: x = os.path.join(i[0], x) if x in hotkeys.values(): hotk.hotkeyList.addItem(f'{find_key(hotkeys, x)}\t:{x}') def pref_remap(btn, func_): ''' Переназначение клавиши в окно Preference btn - PyQt5 кнопка func_ - строковое значени функции из словаря KEYS_CMD ''' def check(key): ''' Проверка кнопки btn на не участие в списке запрещенных клавиш keys.forbidden если это клавиша 'Key.backspace' то стираем значение key - pynput код клавиши ''' key = str(key).replace("'",'') if key not in keys.forbidden: KEYS_CMD.update({find_key(COMMAND_DICT, func_) : key}) btn.setText(keys.dict_[key]) elif key == 'Key.backspace': KEYS_CMD.update({find_key(COMMAND_DICT, func_) : ' '}) btn.setText(' ') for i in PREF_BTN: i.setEnabled(True) save() return False for i in PREF_BTN: i.setEnabled(False) hotkey_remap_Listener = Listener( on_release=check) hotkey_remap_Listener.start() def find_key(dict, val): ''' Поиск ключа в словаре dict по значению val dict - словарь val - значение ''' return next((key for key, value in dict.items() if value == val), None) def save(): ''' Сохранение настроек оверлея и глобальных хоткеев ''' sounds = jsonread(config)['sounds']# Все аудиофайлы KEYS_JSON = {} # Настроенные клавиши for i in KEYS_CMD.keys(): KEYS_JSON.setdefault(COMMAND_DICT[i], KEYS_CMD[i]) theme = pref.themesList.currentItem().text() sounds_list = {'sounds':sounds, 'hotkeys':hotkeys, 'Theme':theme, 'KEYS_CMD':KEYS_JSON} jsonwrite(config, sounds_list) ###! CONTROL !### def select_move(mode): ''' Перемешение по оверлейному меню mode - кортеж из двух цифр (смещение по категории, смещение по списку) ''' select[0] += mode[0] # Категории select[1] += mode[1] # Файлы в категории if select[0] > len(menu)-1 or select[0] < -len(menu)+1: select[0] = 0 if select[1] > len(menu[select[0]])-1 or select[1] < -len(menu[select[0]])+1: select[1] = 0 if mode[0] != 0: select[1] = 0 over.label.setText(menu[select[0]][select[1]]) win.select_label.setText(menu[select[0]][select[1]]) def key_check(key): key = str(key).replace("'",'') # Преобразование кода в строку key_n = '' # Переведенное значение клавиши try: key_n = keys.dict_[key] except KeyError: pass if key in hotkeys.keys(): play_sound(hotkeys[key]) elif key in KEYS_CMD.values(): find_key(KEYS_CMD, key)() def main(): key_check_Listener = Listener( on_release=key_check) key_check_Listener.start() win.exit_button.clicked.connect(win.close) win.min_button.clicked.connect(win.showMinimized) pref.exit_button.clicked.connect(pref.close) pref.min_button.clicked.connect(pref.showMinimized) hotk.exit_button.clicked.connect(hotk.close) hotk.min_button.clicked.connect(hotk.showMinimized) win.hkset.clicked.connect(hotkey_remap) win.pref_button.clicked.connect(pref.show) win.hotkeys_button.clicked.connect(hotk.show) win.catList.currentTextChanged.connect(cat_select) win.stop_button.clicked.connect(lambda: sd.stop()) win.play_button.clicked.connect(play_sound) pref.play_sound.clicked.connect( lambda: pref_remap(pref.play_sound, 'play_sound')) pref.stop_sound.clicked.connect( lambda: pref_remap(pref.stop_sound, 'stop_sound')) pref.select_move_up.clicked.connect( lambda: pref_remap(pref.select_move_up, 'select_move_up')) pref.select_move_down.clicked.connect( lambda: pref_remap(pref.select_move_down, 'select_move_down')) pref.select_move_left.clicked.connect( lambda: pref_remap(pref.select_move_left, 'select_move_left')) pref.select_move_right.clicked.connect( lambda: pref_remap(pref.select_move_right, 'select_move_right')) pref.themesList.currentTextChanged.connect(toggle_stylesheet) hotk.delete_button.clicked.connect(hotkey_delete) win.show() if __name__ == '__main__': ### Создание окна ### app = QApplication([]) # Приложение over = OverlayUi() # Окно оверлея pref = PreferencesUi() # Окно настроек hotk = HotkeysUi() # Окно хоткеев win = MainUi() # Основное окно PREF_BTN = [ # Список кнопок настроек клавиш в pref pref.select_move_up, pref.select_move_down, pref.select_move_left, pref.select_move_right, pref.play_sound, pref.stop_sound,] ### Поиск устроства ввода ### sd.default.device = find_device() # Установка устройства вывода по умолчанию ### Глобальные переменные ### dir_ = 'sound' config = 'settings.json' get_files(dir_, config) # Сбор всех аудиофайлов sound_get_dict = jsonread(config) # Загрузка данных hotkeys = sound_get_dict['hotkeys'] # Загрузка словаря хоткеев theme = sound_get_dict['Theme'] # Загрузка темы menu = sound_get_dict['sounds'] # Загрузка оверлейного меню select = [0, 0] # Установка курсора оверлея в нулевую позицию if theme != 'None': toggle_stylesheet(theme) pref.themesList.addItems(os.listdir('themes')) for i in sound_get_dict['sounds']: win.catList.addItem(i[0]) for x in i: x = os.path.join(i[0], x) if x in hotkeys.values(): hotk.hotkeyList.addItem(f'{find_key(hotkeys, x)}\t: {x}') COMMAND_DICT = { # Словарь функций к строковому значению lambda: select_move((0, -1)):'select_move_up', # вверх lambda: select_move((0, 1)) :'select_move_down', # вниз lambda: select_move((-1, 0)):'select_move_left', # влево lambda: select_move((1, 0)) :'select_move_right', # вправо lambda: play_sound(os.path.join(menu[select[0]][0], menu[select[0]][select[1]])):'play_sound', # Играть lambda: sd.stop() :'stop_sound', # Остановить } KEYS_JSON = sound_get_dict['KEYS_CMD']# Загрузка настроенных клавиш KEYS_CMD = COMMAND_DICT.copy() # Настроенные клавиши ### Установка настроенных клавиш ### for i in KEYS_CMD.keys(): KEYS_CMD.update({i:KEYS_JSON[COMMAND_DICT[i]]}) KEYS_JSON = None combo = 0 for i in KEYS_CMD.values(): PREF_BTN[combo].setText(keys.dict_[i]) combo += 1 main() sys.exit(app.exec())