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 0000000..da93897 Binary files /dev/null and b/icon.ico differ