From 239ae81ae224209591b71ccd0355f73b65fae933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lluc=20Sim=C3=B3=20Margalef?= Date: Tue, 21 Nov 2023 23:34:34 +0100 Subject: [PATCH] initial commit --- .gitignore | 5 + README.md | 45 +++++ automarker.py | 317 ++++++++++++++++++++++++++++++++++++ automarker.spec | 51 ++++++ automarker.txt | 0 extension_installer_mac.sh | 36 ++++ extension_installer_win.bat | 25 +++ flag.txt | 1 + icon.ico | Bin 0 -> 5448 bytes 9 files changed, 480 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 automarker.py create mode 100644 automarker.spec create mode 100644 automarker.txt create mode 100644 extension_installer_mac.sh create mode 100644 extension_installer_win.bat create mode 100644 flag.txt create mode 100644 icon.ico diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a37ef0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build/ +dist/ +audio.mp3 +audio.wav +.venv/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6802908 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# AutoMarker + +AutoMarker is a Python-based GUI application that allows users to place markers on a Premiere Pro active sequence based on a music file's tempo. The application uses the librosa library to detect beats in the audio file and the pymiere library to interact with Adobe Premiere Pro. + +## Features + +- Detects beats in an audio file and places markers on a Premiere Pro active sequence. +- Supports audio files in formats such as .wav, .mp3, .flac, .ogg, and .aiff. +- Allows users to specify the frequency of markers and an offset for the first beat. +- Checks if Adobe Premiere Pro is running and provides feedback to the user. + +## Installation + +AutoMarker is packaged as a standalone executable using PyInstaller. This means that the user does not need to install Python or any dependencies to run the application. + +To install AutoMarker, follow these steps: + +1. Download the executable file for your operating system from the provided source. +2. Run the downloaded file to install AutoMarker. + +## Usage + +After installing AutoMarker, you can launch it from your system's application menu. The application will open in a new window. + +To use AutoMarker, follow these steps: + +1. Click the "Select audio file" button to choose an audio file. +2. If Adobe Premiere Pro is running, click the "Create markers" button to place markers on the active sequence based on the audio file's tempo. +3. You can adjust the frequency of markers and the offset for the first beat using the sliders. + +## Troubleshooting + +If you encounter any issues while using AutoMarker, ensure that Adobe Premiere Pro is installed and running. If the problem persists, please get in touch for further assistance. + +## Contributing + +We welcome contributions to AutoMarker. If you have a feature request, bug report, or want to improve the documentation, please submit an issue or pull request on our GitHub repository. + +## License + +AutoMarker is licensed under the MIT License. For more information, please refer to the LICENSE file in the repository. + +## Contact + +For any questions or concerns, please contact me personally at lluc.simo5@gmail.com \ No newline at end of file diff --git a/automarker.py b/automarker.py new file mode 100644 index 0000000..29cde80 --- /dev/null +++ b/automarker.py @@ -0,0 +1,317 @@ +import librosa +import tkinter as tk +from tkinter import filedialog +from tkinter import ttk +from threading import Thread +import pymiere +import os +import sys +import re +import json +import subprocess +from distutils.version import StrictVersion +import platform + +if platform.system().lower() == "windows": + WINDOWS_SYSTEM = True + import winreg as wr # python 3 +else: + # if not windows, assume it is a macOS + WINDOWS_SYSTEM = False + +if getattr(sys, 'frozen', False): + basedir = sys._MEIPASS +else: + basedir = os.path.dirname(__file__) + +try: + from ctypes import windll # Only exists on Windows. + + myappid = "acrilique.automarker" + windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) +except ImportError: + pass + +try: + with open(os.path.join(basedir, 'flag.txt'), 'r') as flag_file: + flag_content = flag_file.read().strip() +except FileNotFoundError: + # If the file is not found, treat it as not installed + flag_content = "not_installed" + +if flag_content == "not_installed": + # Execute the batch script + if WINDOWS_SYSTEM: + subprocess.run([os.path.join(basedir, 'extension_installer_win.bat')]) + else: + subprocess.run([os.path.join(basedir, 'extension_installer_mac.sh')]) + # Update the flag.txt file to indicate that the script has been executed, erasing old text + with open('flag.txt', 'w') as flag_file: + flag_file.write("installed") + +CREATE_NO_WINDOW = 0x08000000 +PREMIERE_PROCESS_NAME = "adobe premiere pro.exe" if WINDOWS_SYSTEM else "Adobe Premiere Pro" +CEPPANEL_PROCESS_NAME = "CEPHtmlEngine.exe" if WINDOWS_SYSTEM else "CEPHtmlEngine" + +def update_runvar(): + if(is_premiere_running()[0]): + runvar.set("Premiere is running!") + else: + runvar.set("Premiere isn't running :(") + root.after(1000, update_runvar) + +def auto_beat_marker(): + if (is_premiere_running()[0]): + thread = Thread(target=place_marks) + thread.daemon = True + thread.start() + +def place_marks(): + # Detect beats in audio file + info.set("Reading file from source...") + root.update() + data, samplerate = librosa.load(path=path.get()) + info.set("Getting beat positions...") + root.update() + tempo, beatsamples = librosa.beat.beat_track(y=data, units="time") # [list] beat location in samples + + every = everyvar.get() + offset = offsetvar.get() + if (every > 1): + # Add only every x beats + beatsamples = beatsamples[offset::every] + + info.set("Placing markers...") + root.update() + end_of_sequence = pymiere.objects.app.project.activeSequence.end + # Create markers using pymiere + for sample in beatsamples: + if sample < end_of_sequence: + pymiere.objects.app.project.activeSequence.markers.createMarker(sample) + info.set("Done!") + +def select_file(): + file_path = filedialog.askopenfilename( + initialdir=os.path.expanduser("~"), + title='Select an audio file', + filetypes=[('Audio files', ['.wav', 'mp3', '.flac', '.ogg', '.aiff']), ('All files', '.*')] + ) + path.set(file_path) + +root = tk.Tk() +root.title("AutoMarker") + +# Get the window width and height +window_width = root.winfo_reqwidth() +window_height = root.winfo_reqheight() + +# Get the screen width and height +screen_width = root.winfo_screenwidth() +screen_height = root.winfo_screenheight() + +# Calculate the position of the left and top borders of the window +position_top = int(screen_height / 2 - window_height / 2) +position_right = int(screen_width / 2 - window_width / 2) + +# Set the geometry of the window +root.geometry("+{}+{}".format(position_right, position_top)) + +style = ttk.Style() +style.theme_use(themename='xpnative') + +mainframe = tk.Frame(root) +mainframe.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E, tk.S)) +root.columnconfigure(0, weight=1) +root.rowconfigure(0, weight=1) + +runvar = tk.StringVar() +path = tk.StringVar() +info = tk.StringVar() +everyvar = tk.IntVar(value=1) +offsetvar = tk.IntVar(value=0) + +authorLabel = tk.Label(mainframe, text="by acrilique") +runvarLabel = tk.Label(mainframe, textvariable=runvar) +pathLabel = tk.Label(mainframe, textvariable=path) +infoLabel = tk.Label(mainframe, textvariable=info) +readmeButton = ttk.Button(mainframe, text="Readme", command=lambda: os.startfile(os.path.join(basedir, 'README.md'))) +selectFileButton = ttk.Button(mainframe, text="Select audio file", command=select_file, width=40) +doButton = ttk.Button(mainframe, text="Create markers", command=auto_beat_marker, width=40) +everyLabel = tk.Label(mainframe, text="Place markers every x beats") +everyScale = ttk.LabeledScale(mainframe, variable=everyvar, from_=1, to=16, compound='bottom') +offsetLabel = tk.Label(mainframe, text="Offset first beat") +offsetScale = ttk.LabeledScale(mainframe, variable=offsetvar, from_=0, to=16, compound='bottom') + +everyScale.update() +offsetScale.update() + +authorLabel.grid(column=1, row=0, sticky=(tk.W)) +readmeButton.grid(column=1, row=0, sticky=(tk.E)) +pathLabel.grid(column=1, row=1, sticky=(tk.W, tk.E)) +selectFileButton.grid(column=1, row=2, sticky=(tk.W, tk.E)) +doButton.grid(column=1, row=3, sticky=(tk.W, tk.E)) +infoLabel.grid(column=1,row=8, sticky=(tk.E)) +everyLabel.grid(column=1, row=4, sticky=(tk.W)) +everyScale.grid(column=1, row=5, sticky=(tk.W, tk.E)) +offsetLabel.grid(column=1, row=6, sticky=(tk.W)) +offsetScale.grid(column=1, row=7, sticky=(tk.W, tk.E)) + +runvarLabel.grid(column=1, row=8, sticky=(tk.W)) + +for child in mainframe.winfo_children(): + child.grid_configure(padx=5, pady=5) + +root.bind('', auto_beat_marker) + +############################################ +############################################ +# Functions to check if premiere is running +def is_premiere_running(): + """ + Is there a running instance of the Premiere Pro app on this machine ? + + :return: (bool) process is running, (int) pid + """ + return exe_is_running(PREMIERE_PROCESS_NAME) + +def start_premiere(use_bat=False): + raise SystemError("Could not guaranty premiere started") + +def exe_is_running(exe_name): + """ + List processes by name to know if one is running + + :param exe_name: (str) exact name of the process (ex : 'pycharm64.exe' for windows or 'Safari' for mac) + :return: (bool) process is running, (int) pid + """ + pids = _get_pids_from_name(exe_name) + if len(pids) == 0: + return False, None + if len(pids) > 1: + raise OSError("More than one process matching name '{}' were found running (pid: {})".format(exe_name, pids)) + return True, pids[0] + +def count_running_exe(exe_name): + """ + List processes by name to know how many are running + + :param exe_name: (str) exact name of the process (ex : 'pycharm64.exe' for windows or 'Safari' for mac) + :return: (int) Number of process with given name running + """ + return len(_get_pids_from_name(exe_name)) + +def get_last_premiere_exe(): + """ + Get the executable path on disk of the last installed Premiere Pro version + + :return: (str) path to executable + """ + get_last_premiere_exe_func = _get_last_premiere_exe_windows if WINDOWS_SYSTEM else _get_last_premiere_exe_mac + return get_last_premiere_exe_func() + +def _get_pids_from_name(process_name): + """ + Given a process name get ids of running process matching this name + + :param process_name: (str) process name (ex : 'pycharm64.exe' for windows or 'Safari' for mac) + :return: (list of int) pids + """ + if WINDOWS_SYSTEM: + # use tasklist windows command with filter by name + call = 'TASKLIST', '/FI', 'imagename eq {}'.format(process_name) + output = subprocess.check_output(call, creationflags=CREATE_NO_WINDOW) + if sys.version_info >= (3, 0): + output = output.decode(encoding="437") # encoding for windows console + # parse output lines + lines = output.strip().splitlines() + matching_lines = [l for l in lines if l.lower().startswith(process_name.lower())] + return [int(re.findall(" ([0-9]{1,6}) [a-zA-Z]", l)[0]) for l in matching_lines] + else: + # use pgrep UNIX command to filter processes by name + try: + output = subprocess.check_output(["pgrep", process_name]) + except subprocess.CalledProcessError: # pgrep seems to crash if the given name is not a running process... + return list() + # parse output lines + lines = output.strip().splitlines() + return list(map(int, lines)) + +# ----- platform specific functions ----- +def _get_last_premiere_exe_windows(): + """ + WINDOWS ONLY + Get the executable path on disk of the last installed Premiere Pro version using windows registry + + :return: (str) path to executable + """ + premiere_versions = _get_installed_softwares_info("adobe premiere pro") + if not premiere_versions: + raise OSError("Could not find an Adobe Premiere Pro version installed on this computer") + # find last installed version + last_version_num = sorted([StrictVersion(v["DisplayVersion"]) for v in premiere_versions])[-1] + last_version_info = [v for v in premiere_versions if v["DisplayVersion"] == str(last_version_num)][0] + # search actual exe path + base_path = last_version_info["InstallLocation"] + build_year = last_version_info["DisplayName"].split(" ")[-1] + wrong_paths = list() + for folder_name in ["Adobe Premiere Pro CC {}", "Adobe Premiere Pro {}", ""]: # different versions formatting + exe_path = os.path.join(base_path, folder_name.format(build_year), "Adobe Premiere Pro.exe") + if not os.path.isfile(exe_path): + wrong_paths.append(exe_path) + continue + wrong_paths = list() + break + if len(wrong_paths) != 0: + raise IOError("Could not find Premiere executable in '{}'".format(wrong_paths)) + return exe_path + +def _get_last_premiere_exe_mac(): + """ + MACOS ONLY + Get the executable path on disk of the last installed Premiere Pro version using macOS System Profiler + + :return: (str) path to executable + """ + # list all installed app to a json datastructure + output = subprocess.check_output(["system_profiler", "-json", "SPApplicationsDataType"]) + apps_data = json.loads(output)["SPApplicationsDataType"] + # filter Premiere pro installed versions + premiere_apps = [data for data in apps_data if "adobe premiere pro" in data["_name"].lower()] + if not premiere_apps: + raise OSError("Could not find an Adobe Premiere Pro version installed on this computer") + # get last app version path + premiere_apps.sort(key=lambda d: d["version"], reverse=True) + return premiere_apps[0]["path"] + +def _get_installed_softwares_info(name_filter, names=["DisplayVersion", "InstallLocation"]): + """ + WINDOWS ONLY + Looking into Uninstall key in Windows registry, we can get some infos about installed software + + :param name_filter: (str) filter software containing this name + :return: (list of dict) info of software found + """ + reg = wr.ConnectRegistry(None, wr.HKEY_LOCAL_MACHINE) + key = wr.OpenKey(reg, r"SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall") + apps_info = list() + # list all installed apps + for i in range(wr.QueryInfoKey(key)[0]): + subkey_name = wr.EnumKey(key,i) + subkey = wr.OpenKey(key, subkey_name) + try: + soft_name = wr.QueryValueEx(subkey, "DisplayName")[0] + except EnvironmentError: + continue + if name_filter.lower() not in soft_name.lower(): + continue + apps_info.append(dict({n: wr.QueryValueEx(subkey, n)[0] for n in names}, DisplayName=soft_name)) + return apps_info +########################################### +########################################### + + + +root.iconbitmap(os.path.join(basedir, "icon.ico")) +root.after(0, update_runvar) + +root.mainloop() \ No newline at end of file diff --git a/automarker.spec b/automarker.spec new file mode 100644 index 0000000..e98a17e --- /dev/null +++ b/automarker.spec @@ -0,0 +1,51 @@ +# -*- mode: python ; coding: utf-8 -*- + + +block_cipher = None + + +a = Analysis( + ['automarker.py'], + pathex=[], + binaries=[], + datas=[('icon.ico', '.'), ('extension_installer_mac.sh', '.'), ('extension_installer_win.bat', '.'), ('README.md', '.'), ('flag.txt', '.')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='AutoMarker', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon='icon.ico', +) +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='automarker', +) diff --git a/automarker.txt b/automarker.txt new file mode 100644 index 0000000..e69de29 diff --git a/extension_installer_mac.sh b/extension_installer_mac.sh new file mode 100644 index 0000000..f41fa48 --- /dev/null +++ b/extension_installer_mac.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Auto install PymiereLink extension to Premiere on mac + +# Get temp path +tempdir=$(mktemp -d) + +# Download zxp (extension) file +echo "Download .zxp file" +url="https://raw.githubusercontent.com/qmasingarbe/pymiere/master/pymiere_link.zxp" +fname_zxp=$(basename "$url") +path_zxp="$tempdir/$fname_zxp" +curl "$url" --output "$path_zxp" + +# Download ExManCmd (extension manager) +echo "Download ExManCmd" +url="https://download.macromedia.com/pub/extensionmanager/ExManCmd_mac.dmg" +fname_exman=$(basename "$url") +path_exman="$tempdir/$fname_exman" +curl "$url" --output "$path_exman" + +# Mount ExManCmd DMG +mount_path="$tempdir/ExManCmdMount" +echo "Mount ExManCmd DMG: $path_exman to $mount_path" +hdiutil attach "$path_exman" -mountpoint $mount_path + +# Install the .zxp file +exmancmd="$mount_path/Contents/MacOS/ExManCmd" +echo "Install zxp" +"$exmancmd" --install "$path_zxp" +# For debugging +# "$exmancmd" --list all + +# Clean up +echo "Unmount ExManCmd DMG" +hdiutil detach "$mount_path" +rm -rf "$tempdir" \ No newline at end of file diff --git a/extension_installer_win.bat b/extension_installer_win.bat new file mode 100644 index 0000000..fef0729 --- /dev/null +++ b/extension_installer_win.bat @@ -0,0 +1,25 @@ +@echo off +rem Auto install PymiereLink extension to Premiere on windows + +echo Downloading Adobe Extension Manager +curl "http://download.macromedia.com/pub/extensionmanager/ExManCmd_win.zip" --output %temp%\ExManCmd_win.zip +echo. + +echo Download PymiereLink extension +curl "https://raw.githubusercontent.com/qmasingarbe/pymiere/master/pymiere_link.zxp" --output %temp%\pymiere_link.zxp +echo. + +echo Unzip Extension Manager +rem require powershell +powershell Expand-Archive %temp%\ExManCmd_win.zip -DestinationPath %temp%\ExManCmd_win -Force +echo. + +echo Install Extension +call %temp%\ExManCmd_win\ExManCmd.exe /install %temp%\pymiere_link.zxp +if %ERRORLEVEL% NEQ 0 ( + echo Installation failed... +) else ( + echo. + echo Installation successful ! +) + diff --git a/flag.txt b/flag.txt new file mode 100644 index 0000000..3856a44 --- /dev/null +++ b/flag.txt @@ -0,0 +1 @@ +installed \ No newline at end of file diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..da938971186ac195e22d9dca9e4fdea856727ecc GIT binary patch literal 5448 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%0AS|W8}rJ~~=36XeF%qCnt(XmzwgKrP4=I|ju zOyja2UM91O5l;D`;Tl0cB_Vx&haN{tmClw^8IW zE_vF&XFZVb>GHBS1)4V}rm=`VFupSJR2s5lr9zOr}huA)diH@81!4ooq zG6gGc%-1)LDlDF3y9pjpnm6xr44RW{qf95n==I|%gc`fNL+`)ROpCBN8+RLB-WkIe zBwGeHRyUSsAQx=hCEdUY)GJ>}iY?m(^Q64hP_xDY8G2uA9Hpg5R+gS%8+&xZDMZiJ z_4N9=C9Y;J7vs%HX}tFgjkk%8liLFisd*i(0(8@8R_jvUktp%NSR(@k)caKC&0JBA zo@pN10hTX5dR>t1qtI|W7jN1j922P=hw5D>ikz{XcF9 Bw9fzl literal 0 HcmV?d00001