diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..440eec4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,18 @@ +name: Build binaries +on: + push: + tags: + - "v*.*.*" + workflow_dispatch: +jobs: + pyinstaller-build: + runs-on: windows-latest + steps: + - name: Create Executable + uses: sayyid5416/pyinstaller@v1 + with: + python_ver: '3.11' + spec: 'LPHK.py' + requirements: 'INSTALL/requirements.txt' + upload_exe_with_name: 'My executable' + options: --onefile, --name "LPHK", --windowed, ---icon resources\LPHK.ico, --add-data Version:., --add-data resources\:resources\ diff --git a/.gitignore b/.gitignore index f6148da..471907a 100644 --- a/.gitignore +++ b/.gitignore @@ -32,8 +32,8 @@ MANIFEST # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest -*.spec - +# *.spec +Output/ # Installer logs pip-log.txt pip-delete-this-directory.txt diff --git a/INSTALL/environment-build.yml b/INSTALL/environment-build.yml index e67d520..67567de 100644 --- a/INSTALL/environment-build.yml +++ b/INSTALL/environment-build.yml @@ -14,3 +14,4 @@ dependencies: - https://github.com/pyinstaller/pyinstaller/archive/develop.zip - py-getch - pyautogui + - python-vlc diff --git a/INSTALL/environment.yml b/INSTALL/environment.yml index 9be07a7..4e2a10c 100644 --- a/INSTALL/environment.yml +++ b/INSTALL/environment.yml @@ -13,3 +13,4 @@ dependencies: - tkcolorpicker - py-getch - pyautogui + - python-vlc diff --git a/INSTALL/requirements.txt b/INSTALL/requirements.txt index 5e28551..ce8e8b6 100644 --- a/INSTALL/requirements.txt +++ b/INSTALL/requirements.txt @@ -1,8 +1,9 @@ MouseInfo==0.1.3 -Pillow==9.3.0 +Pillow py-getch==1.0.1 PyAutoGUI==0.9.50 -pygame==2.1.2 +pygame +platformdirs PyGetWindow==0.0.8 PyMsgBox==1.0.7 pynput==1.6.8 @@ -12,6 +13,7 @@ PyScreeze==0.1.26 python-xlib==0.27 python3-xlib==0.15 PyTweening==1.0.3 -six==1.14.0 +six tkcolorpicker==2.1.3 +python-vlc -e git+https://github.com/FMMT666/launchpad.py.git@master#egg=launchpad-py diff --git a/LPHK.iss b/LPHK.iss new file mode 100644 index 0000000..623ae7e --- /dev/null +++ b/LPHK.iss @@ -0,0 +1,56 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define MyAppName "LPHK" +#define MyAppVersion "1.0.1" +#define MyAppPublisher "Clafter" +#define MyAppURL "https://github.com/clafter/LPHK" +#define MyAppExeName "LPHK.exe" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{49A18FB6-F3FE-4265-9BC5-B3F7AD939DF9} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +;AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\{#MyAppName} +; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run +; on anything but x64 and Windows 11 on Arm. +ArchitecturesAllowed=x64compatible +; "ArchitecturesInstallIn64BitMode=x64compatible" requests that the +; install be done in "64-bit mode" on x64 or Windows 11 on Arm, +; meaning it should use the native 64-bit Program Files directory and +; the 64-bit view of the registry. +ArchitecturesInstallIn64BitMode=x64compatible +DisableProgramGroupPage=yes +; Uncomment the following line to run in non administrative install mode (install for current user only.) +;PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog +OutputBaseFilename=lphk_setup +SetupIconFile=.\resources\LPHK.ico +Compression=lzma +SolidCompression=yes +WizardStyle=modern + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +Source: ".\dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + diff --git a/LPHK.py b/LPHK.py index d8ec612..895314f 100755 --- a/LPHK.py +++ b/LPHK.py @@ -38,15 +38,10 @@ def get_first_textfile_line(file_path): first_line = file_lines[0] return first_line.strip() +from platformdirs import * -USERPATH_FILE = os.path.join(PATH, "USERPATH") -if os.path.exists(USERPATH_FILE): - IS_PORTABLE = False - USER_PATH = get_first_textfile_line(USERPATH_FILE) - os.makedirs(USER_PATH, exist_ok=True) -else: - IS_PORTABLE = True - USER_PATH = PROG_PATH +IS_PORTABLE = False +USER_PATH = user_data_dir("LPHK", "io.github.clafter", ensure_exists=True) # Get program version VERSION = get_first_textfile_line(os.path.join(PATH, "VERSION")) @@ -87,7 +82,7 @@ def datetime_str(): sys.exit("[LPHK] Error loading launchpad.py") print("") -import lp_events, scripts, files, sound, window +import lp_events, scripts, files, sound, sound_vlc, window from utils import launchpad_connector lp = launchpad.Launchpad() @@ -108,6 +103,7 @@ def init(): files.init(USER_PATH) sound.init(USER_PATH) + sound_vlc.init(USER_PATH) def shutdown(): diff --git a/LPHK.spec b/LPHK.spec new file mode 100644 index 0000000..d66341c --- /dev/null +++ b/LPHK.spec @@ -0,0 +1,41 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['LPHK.py'], + pathex=[], + binaries=[], + datas=[ + ('VERSION','.'), + ('resources', 'resources') + ], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='LPHK', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/README.md b/README.md index cd15cc4..4966d72 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,19 @@ Commands follow the format: `COMMAND arg1 arg2 ...`. Scripts are just a text fil * `SOUND_STOP` * Stops all sounds currently playing. * If (argument 1) supplied, fading for (argument 1) milliseconds and then stops the sounds. +* `SOUND_VLC` + * Play a sound named (argument 1) inside the `user_sounds/` folder. + * Supports all Files compatible with VLC-Mediaplayer. + * VLC must be installed + * Only one sound volume can be played at a time. Multiple sounds with different volumes will be played at the same volume. + * If (argument 2) supplied, set volume to (argument 2). + * Range is 0 to 100 + * If (argument 3) supplied, set the start time to (argument 3) milliseconds. + * If (argument 4) supplied, set the end time to (argument 4) milliseconds. + * If (argument 5) supplied, set fadeout time to (argument 5) milliseconds +* `SOUND_VLC_STOP` + * Stops all sounds currently playing via VLC. + * If (argument 1) supplied, fading for (argument 1) milliseconds and then stops the sounds. * `WAIT_UNPRESSED` * Waits until the button the script is bound to is unpressed. (no arguments) * `WEB` diff --git a/scripts.py b/scripts.py index 8a74da4..4384572 100644 --- a/scripts.py +++ b/scripts.py @@ -1,7 +1,7 @@ import threading, webbrowser, os, subprocess from time import sleep from functools import partial -import lp_events, lp_colors, kb, sound, ms +import lp_events, lp_colors, kb, sound, sound_vlc, ms COLOR_PRIMED = 5 #red COLOR_FUNC_KEYS_PRIMED = 9 #amber @@ -10,7 +10,7 @@ import files -VALID_COMMANDS = ["@ASYNC", "@SIMPLE", "@LOAD_LAYOUT", "STRING", "DELAY", "TAP", "PRESS", "RELEASE", "WEB", "WEB_NEW", "CODE", "SOUND", "SOUND_STOP", "WAIT_UNPRESSED", "M_MOVE", "M_SET", "M_SCROLL", "M_LINE", "M_LINE_MOVE", "M_LINE_SET", "LABEL", "IF_PRESSED_GOTO_LABEL", "IF_UNPRESSED_GOTO_LABEL", "GOTO_LABEL", "REPEAT_LABEL", "IF_PRESSED_REPEAT_LABEL", "IF_UNPRESSED_REPEAT_LABEL", "M_STORE", "M_RECALL", "M_RECALL_LINE", "OPEN", "RELEASE_ALL", "RESET_REPEATS"] +VALID_COMMANDS = ["@ASYNC", "@SIMPLE", "@LOAD_LAYOUT", "STRING", "DELAY", "TAP", "PRESS", "RELEASE", "WEB", "WEB_NEW", "CODE", "SOUND", "SOUND_STOP", "SOUND_VLC", "SOUND_VLC_STOP", "WAIT_UNPRESSED", "M_MOVE", "M_SET", "M_SCROLL", "M_LINE", "M_LINE_MOVE", "M_LINE_SET", "LABEL", "IF_PRESSED_GOTO_LABEL", "IF_UNPRESSED_GOTO_LABEL", "GOTO_LABEL", "REPEAT_LABEL", "IF_PRESSED_REPEAT_LABEL", "IF_UNPRESSED_REPEAT_LABEL", "M_STORE", "M_RECALL", "M_RECALL_LINE", "OPEN", "RELEASE_ALL", "RESET_REPEATS"] ASYNC_HEADERS = ["@ASYNC", "@SIMPLE"] threads = [[None for y in range(9)] for x in range(9)] @@ -240,6 +240,31 @@ def main_logic(idx): else: print("[scripts] " + coords + " Stopping sounds") sound.stop() + elif split_line[0] == "SOUND_VLC": + if len(split_line) == 6: + print("[scripts] " + coords + " Play sound file " + split_line[1] + " at volume " + str(split_line[2]) + " from " + str(split_line[3]) + " to " + str(split_line[4])+ " with " + str(split_line[5]) + " milliseconds fadeout time") + sound_vlc.play_vlc(split_line[1], int(split_line[2]), float(split_line[3]), float(split_line[4]),float(split_line[5])) + if len(split_line) == 5: + print("[scripts] " + coords + " Play sound file " + split_line[1] + " at volume " + str(split_line[2]) + " from " + str(split_line[3]) + " to " + str(split_line[4])) + sound_vlc.play_vlc(split_line[1], int(split_line[2]), float(split_line[3]), float(split_line[4])) + if len(split_line) == 4: + print("[scripts] " + coords + " Play sound file " + split_line[1] + " at volume " + str(split_line[2]) + " from " + str(split_line[3])) + sound_vlc.play_vlc(split_line[1], int(split_line[2]), float(split_line[3])) + if len(split_line) == 3: + print("[scripts] " + coords + " Play sound file " + split_line[1] + " at volume " + str(split_line[2])) + sound_vlc.play_vlc(split_line[1], int(split_line[2])) + if len(split_line) == 2: + print("[scripts] " + coords + " Play sound file " + split_line[1]) + sound_vlc.play_vlc(split_line[1]) + elif split_line[0] == "SOUND_VLC_STOP": + if len(split_line) > 1: + delay = split_line[1] + print("[scripts] " + coords + + " Stopping sounds with " + delay + " milliseconds fadeout time") + sound_vlc.fadeout_vlc(int(delay)) + else: + print("[scripts] " + coords + " Stopping sounds") + sound_vlc.stop_vlc() elif split_line[0] == "WAIT_UNPRESSED": print("[scripts] " + coords + " Wait for script key to be unpressed") while lp_events.pressed[x][y]: diff --git a/sound_vlc.py b/sound_vlc.py new file mode 100644 index 0000000..c8d64b3 --- /dev/null +++ b/sound_vlc.py @@ -0,0 +1,110 @@ +import os +import vlc +import time +import threading + +SOUND_PATH = "/user_sounds/" +PATH = None + +# Create a new instance of the VLC player +instance = vlc.Instance() +player_test = instance.media_player_new() +instances = [] +players = [] +fading = False + + +def init(path_in): + global PATH + PATH = path_in + + +def full_name(filename): + name = PATH + SOUND_PATH + filename + if PATH.find('\\') > 0: + name = name.replace('/', '\\') + return name + + +def is_valid(filename): + final_name = full_name(filename) + try: + media = instance.media_new(final_name) + player_test.set_media(media) + return player_test.will_play() + except: + return False + + +def play_vlc(filename, volume=100, start_time=0, end_time=0, fadeout=0): + global fading + fading = False + final_name = full_name(filename) + + # Create a new player + player = instance.media_player_new() + players.append(player) + + # Play the media + media = instance.media_new(final_name) + player.set_media(media) + # Set volume using + player.audio_set_volume(volume) + vlc.libvlc_audio_set_mute(player, False) + player.play() + + if start_time > 0: + print("Setting start time to", start_time) + player.set_time(int(start_time * 1000)) + if end_time > 0 and end_time > start_time: + # Non-blocking timer to stop the player after the end time + stop_player_thread_instance = threading.Thread(target=stop_player_thread, + args=(player, end_time - start_time, fadeout)) + stop_player_thread_instance.start() + + +def stop_player_thread(player, delay, fadeout): + time.sleep(delay) + if fadeout > 0: + fadeout_vlc(fadeout, [player]) + else: + player.stop() + players.remove(player) + + +def stop_vlc(): + global fading + fading = False + for player in players: + player.stop() + players.clear() + + +def fadeout_vlc(delay, fadeout_players=None): + global fading + + global players + if fadeout_players is None: + fadeout_players = [] + for player in players: + fadeout_players.append(player) + fading = True + fadeout_vlc_thread_instance = threading.Thread(target=fadeout_vlc_thread, args=(delay, fadeout_players,)) + fadeout_vlc_thread_instance.start() + + +def fadeout_vlc_thread(delay, fadeout_players): + global fading + + delay = delay / 1000 + fadeout_start_time = time.time() + current_volumes = [playerI.audio_get_volume() for playerI in fadeout_players] + while time.time() - fadeout_start_time < delay and fading: + for i, player in enumerate(fadeout_players): + volume = int((1 - (time.time() - fadeout_start_time) / delay) * current_volumes[i]) + player.audio_set_volume(volume) + time.sleep(0.01) + fading = False + for player in fadeout_players: + player.stop() + fadeout_players.clear() diff --git a/window.py b/window.py index cea0dfc..4dcca0e 100644 --- a/window.py +++ b/window.py @@ -724,7 +724,8 @@ def make(): def close(): global root_destroyed, launchpad app.modified_layout_save_prompt() - app.disconnect_lp() + if lpcon.get_launchpad() is not None: + app.disconnect_lp() if not root_destroyed: root.destroy()