aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md103
-rw-r--r--setup.py58
-rw-r--r--src/__init__.py27
-rw-r--r--src/__main__.py37
-rw-r--r--src/command.py152
-rw-r--r--src/component.py657
-rw-r--r--src/components/color.py136
-rw-r--r--src/components/image.py71
-rw-r--r--src/components/life.py276
-rw-r--r--src/components/life.ui2
-rw-r--r--src/components/original.py189
-rw-r--r--src/components/sound.py54
-rw-r--r--src/components/spectrum.py300
-rw-r--r--src/components/text.py148
-rw-r--r--src/components/video.py178
-rw-r--r--src/components/waveform.py170
-rw-r--r--src/core.py420
-rw-r--r--src/gui/actions.py69
-rw-r--r--src/gui/mainwindow.py648
-rw-r--r--src/gui/presetmanager.py157
-rw-r--r--src/gui/preview_thread.py50
-rw-r--r--src/gui/preview_win.py45
-rw-r--r--src/tests/__init__.py14
-rw-r--r--src/tests/test_commandline_export.py17
-rw-r--r--src/tests/test_commandline_parser.py11
-rw-r--r--src/tests/test_core_init.py16
-rw-r--r--src/toolkit/common.py87
-rw-r--r--src/toolkit/ffmpeg.py455
-rw-r--r--src/toolkit/frame.py69
-rw-r--r--src/video_thread.py182
30 files changed, 2558 insertions, 2240 deletions
diff --git a/README.md b/README.md
index ed9f928..791c4eb 100644
--- a/README.md
+++ b/README.md
@@ -1,89 +1,102 @@
# Audio Visualizer Python
+
**We need a good name that is not as generic as "audio-visualizer-python"!**
This is a little GUI tool which creates an audio visualization video from an input audio file. Different components can be added and layered to change the resulting video and add images, videos, gradients, text, etc. Encoding options can be changed with a variety of different output containers.
The program works on **Linux**, **macOS**, and **Windows**. If you encounter problems running it or have other bug reports or features that you wish to see implemented, please fork the project and submit a pull request and/or file an [issue](https://github.com/djfun/audio-visualizer-python/issues) on this project.
-
# Screenshots & Videos
+
[<img title="AVP running on Windows" alt="Screenshot of program on Windows" src="screenshot.png" width="707">](/screenshot.png?raw=true)
## A video created by this app
-* **[YouTube: A day in spring](https://www.youtube.com/watch?v=-M3jR1NuJHM)** 🎥
+
+- **[YouTube: A day in spring](https://www.youtube.com/watch?v=-M3jR1NuJHM)** 🎥
## Video demonstration of the app features
-* [YouTube: Audio Visualizer Python v2.0.0 demonstration](https://www.youtube.com/watch?v=EVt2ckQs1Yg) 🎥
+- [YouTube: Audio Visualizer Python v2.0.0 demonstration](https://www.youtube.com/watch?v=EVt2ckQs1Yg) 🎥
# Installation
+
## Installation on Ubuntu 24.04
-* Install dependencies: `sudo apt install ffmpeg pipx python3-pyqt5`
-* Download this repo and run `pipx install .` in this directory
-* Run the program with `avp` from terminal
+- Install system dependencies: `sudo apt install ffmpeg`
+- Make a virtual environment: `python -m venv env`
+- Activate it: `source env/bin/activate`
+- Install Python dependencies: `pip install pyqt6 pillow numpy`
+- Install this program: `pip install .` in this directory
+- Run the program with `avp` from terminal
## Installation on Windows
-* Install Python from the Windows Store
-* Add Python to your system PATH (it should ask during the installation process)
-* Download this repo (extract from zip if needed)
-* Download and install [FFmpeg](https://www.ffmpeg.org/download.html). Use the GPL-licensed static builds.
-* Add FFmpeg to the system PATH as well (program will then work anywhere)
- * Alternatively, copy ffmpeg.exe into the folder that you want to run the program within
-* Open command prompt, `cd` into the repo directory, and run: `pip install .`
-* Now run `avp` or `python -m avp` from a command prompt window to start the app
+
+- Install Python from the Windows Store
+- Add Python to your system PATH (it should ask during the installation process)
+- Download this repo (extract from zip if needed)
+- Download and install [FFmpeg](https://www.ffmpeg.org/download.html). Use the GPL-licensed static builds.
+- Add FFmpeg to the system PATH as well (program will then work anywhere)
+ - Alternatively, copy ffmpeg.exe into the folder that you want to run the program within
+- Open command prompt, `cd` into the repo directory, and run: `pip install pyqt6 pillow numpy .`
+- Now run `avp` or `python -m avp` from a command prompt window to start the app
## Installation on macOS
-* We need help writing instructions for macOS, but the program should work in theory.
+
+- We need help writing instructions for macOS, but the program should work in theory.
# [Keyboard Shortcuts](https://github.com/djfun/audio-visualizer-python/wiki/Keyboard-Shortcuts)
-| Key Combo | Effect |
-| ------------------------- | -------------------------------------------------- |
-| Ctrl+S | Save Current Project |
-| Ctrl+A | Save Project As... |
-| Ctrl+O | Open Project |
-| Ctrl+N | New Project (prompts to save current project) |
-| Ctrl+Z | Undo |
-| Ctrl+Shift+Z _or_ Ctrl+Y | Redo |
-| Ctrl+T _or_ Insert | Add Component |
-| Ctrl+R _or_ Delete | Remove Component |
-| Ctrl+Space | Focus Component List |
-| Ctrl+Shift+S | Save Component Preset |
-| Ctrl+Shift+C | Remove Preset from Component |
-| Ctrl+Up | Move Selected Component Up |
-| Ctrl+Down | Move Selected Component Down |
-| Ctrl+Home | Move Selected Component to Top |
-| Ctrl+End | Move Selected Component to Bottom |
-| Ctrl+Shift+U | Open Undo History |
-| Ctrl+Shift+F | Show FFmpeg Command |
+| Key Combo | Effect |
+| ------------------------ | --------------------------------------------- |
+| Ctrl+S | Save Current Project |
+| Ctrl+A | Save Project As... |
+| Ctrl+O | Open Project |
+| Ctrl+N | New Project (prompts to save current project) |
+| Ctrl+Z | Undo |
+| Ctrl+Shift+Z _or_ Ctrl+Y | Redo |
+| Ctrl+T _or_ Insert | Add Component |
+| Ctrl+R _or_ Delete | Remove Component |
+| Ctrl+Space | Focus Component List |
+| Ctrl+Shift+S | Save Component Preset |
+| Ctrl+Shift+C | Remove Preset from Component |
+| Ctrl+Up | Move Selected Component Up |
+| Ctrl+Down | Move Selected Component Down |
+| Ctrl+Home | Move Selected Component to Top |
+| Ctrl+End | Move Selected Component to Bottom |
+| Ctrl+Shift+U | Open Undo History |
+| Ctrl+Shift+F | Show FFmpeg Command |
# Commandline Mode
+
Projects can be created with the GUI then loaded from the commandline for easy automation of video production. Some components have commandline options for extra customization, and you can save "presets" with settings to load if the commandline option doesn't exist.
## Example/test command
-* Create a video with a grey "classic visualizer", background image, and text:
- * `avp -c 0 image path=src/tests/data/test.jpg -c 1 classic color=180,180,180 -c 2 text "title=Episode 371" -i src/tests/data/test.ogg -o output.mp4`
-* [See more about commandline mode in the wiki!](https://github.com/djfun/audio-visualizer-python/wiki/Commandline-Mode)
+- Create a video with a grey "classic visualizer", background image, and text:
+ - `avp -c 0 image path=src/tests/data/test.jpg -c 1 classic color=180,180,180 -c 2 text "title=Episode 371" -i src/tests/data/test.ogg -o output.mp4`
+- [See more about commandline mode in the wiki!](https://github.com/djfun/audio-visualizer-python/wiki/Commandline-Mode)
# Developer Information
+
## Known Working Versions of Dependencies
-* Python 3.10
-* FFmpeg 4.4.1
-* PyQt5 (Qt v5.15.3)
-* Pillow 9.1.0
-* NumPy 1.22.3
+
+- Python 3.13
+- FFmpeg 8.0.1
+- PyQt6 v6.10.2 (Qt v6.10.1)
+- Pillow 12.1.0
+- NumPy 2.4.1
## Getting Faster Export Times
-* [Pillow-SIMD](https://github.com/uploadcare/pillow-simd) may be used as a drop-in replacement for Pillow if you desire faster video export times, but it must be compiled from source. For help installing dependencies to compile Pillow-SIMD, see the [Pillow installation guide](https://pillow.readthedocs.io/en/stable/installation.html). Then add the `-SIMD` suffix into `setup.py` and install as usual.
+
+- [Pillow-SIMD](https://github.com/uploadcare/pillow-simd) may be used as a drop-in replacement for Pillow if you desire faster video export times, but it must be compiled from source. For help installing dependencies to compile Pillow-SIMD, see the [Pillow installation guide](https://pillow.readthedocs.io/en/stable/installation.html).
## Developing a New Component
-* Information for developing a component is in our wiki: [How a Component Works](https://github.com/djfun/audio-visualizer-python/wiki/How-a-Component-Works)
-* File an issue on GitHub if you need help fitting your visualizer into our component system; we would be happy to collaborate
+- Information for developing a component is in our wiki: [How a Component Works](https://github.com/djfun/audio-visualizer-python/wiki/How-a-Component-Works)
+- File an issue on GitHub if you need help fitting your visualizer into our component system; we would be happy to collaborate
# License
+
Source code of audio-visualizer-python is licensed under the MIT license.
Some dependencies of this application are under the GPL license. When packaged with these dependencies, audio-visualizer-python may also be under the terms of this GPL license.
diff --git a/setup.py b/setup.py
index 89c51e2..b62dbad 100644
--- a/setup.py
+++ b/setup.py
@@ -15,49 +15,53 @@ def getTextFromFile(filename, fallback):
return output
-PACKAGE_NAME = 'avp'
-SOURCE_DIRECTORY = 'src'
-SOURCE_PACKAGE_REGEX = re.compile(rf'^{SOURCE_DIRECTORY}')
-PACKAGE_DESCRIPTION = 'Create audio visualization videos from a GUI or commandline'
+PACKAGE_NAME = "avp"
+SOURCE_DIRECTORY = "src"
+SOURCE_PACKAGE_REGEX = re.compile(rf"^{SOURCE_DIRECTORY}")
+PACKAGE_DESCRIPTION = "Create audio visualization videos from a GUI or commandline"
avp = import_module(SOURCE_DIRECTORY)
-source_packages = find_packages(include=[SOURCE_DIRECTORY, f'{SOURCE_DIRECTORY}.*'])
-proj_packages = [SOURCE_PACKAGE_REGEX.sub(PACKAGE_NAME, name) for name in source_packages]
+source_packages = find_packages(include=[SOURCE_DIRECTORY, f"{SOURCE_DIRECTORY}.*"])
+proj_packages = [
+ SOURCE_PACKAGE_REGEX.sub(PACKAGE_NAME, name) for name in source_packages
+]
setup(
- name='audio_visualizer_python',
+ name="audio_visualizer_python",
version=avp.__version__,
- url='https://github.com/djfun/audio-visualizer-python',
- license='MIT',
+ url="https://github.com/djfun/audio-visualizer-python",
+ license="MIT",
description=PACKAGE_DESCRIPTION,
- author=getTextFromFile('AUTHORS', 'djfun, tassaron'),
- long_description=getTextFromFile('README.md', PACKAGE_DESCRIPTION),
+ author=getTextFromFile("AUTHORS", "djfun, tassaron"),
+ long_description=getTextFromFile("README.md", PACKAGE_DESCRIPTION),
classifiers=[
- 'Development Status :: 4 - Beta',
- 'License :: OSI Approved :: MIT License',
- 'Programming Language :: Python :: 3 :: Only',
- 'Intended Audience :: End Users/Desktop',
- 'Topic :: Multimedia :: Video :: Non-Linear Editor',
+ "Development Status :: 4 - Beta",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python :: 3 :: Only",
+ "Intended Audience :: End Users/Desktop",
+ "Topic :: Multimedia :: Video :: Non-Linear Editor",
],
keywords=[
- 'visualizer', 'visualization', 'commandline video',
- 'video editor', 'ffmpeg', 'podcast'
+ "visualizer",
+ "visualization",
+ "commandline video",
+ "video editor",
+ "ffmpeg",
+ "podcast",
],
packages=proj_packages,
package_dir={PACKAGE_NAME: SOURCE_DIRECTORY},
include_package_data=True,
install_requires=[
- 'Pillow==9.1.1',
- 'PyQt5',
- 'numpy',
- 'pytest',
- 'pytest-qt',
+ "Pillow",
+ "PyQt6",
+ "numpy",
+ "pytest",
+ "pytest-qt",
],
entry_points={
- 'console_scripts': [
- f'avp = {PACKAGE_NAME}.__main__:main'
- ],
- }
+ "console_scripts": [f"avp = {PACKAGE_NAME}.__main__:main"],
+ },
)
diff --git a/src/__init__.py b/src/__init__.py
index 2080de5..ee9bebb 100644
--- a/src/__init__.py
+++ b/src/__init__.py
@@ -3,20 +3,21 @@ import os
import logging
-__version__ = '2.0.0'
+__version__ = "2.1.0"
class Logger(logging.getLoggerClass()):
- '''
- Custom Logger class to handle custom VERBOSE log level.
- Levels used in this program are as follows:
- VERBOSE Annoyingly frequent debug messages (e.g, in loops)
- DEBUG Ordinary debug information
- INFO Expected events that are expensive or irreversible
- WARNING A non-fatal error or suspicious behaviour
- ERROR Any error that would interrupt the user
- CRITICAL Things that really shouldn't happen at all
- '''
+ """
+ Custom Logger class to handle custom VERBOSE log level.
+ Levels used in this program are as follows:
+ VERBOSE Annoyingly frequent debug messages (e.g, in loops)
+ DEBUG Ordinary debug information
+ INFO Expected events that are expensive or irreversible
+ WARNING A non-fatal error or suspicious behaviour
+ ERROR Any error that would interrupt the user
+ CRITICAL Things that really shouldn't happen at all
+ """
+
def __init__(self, name, level=logging.NOTSET):
super().__init__(name, level)
logging.addLevelName(5, "VERBOSE")
@@ -24,11 +25,13 @@ class Logger(logging.getLoggerClass()):
def verbose(self, msg, *args, **kwargs):
if self.isEnabledFor(5):
self._log(5, msg, args, **kwargs)
+
+
logging.setLoggerClass(Logger)
logging.VERBOSE = 5
-if getattr(sys, 'frozen', False):
+if getattr(sys, "frozen", False):
# frozen
wd = os.path.dirname(sys.executable)
else:
diff --git a/src/__main__.py b/src/__main__.py
index 284fd2c..db48788 100644
--- a/src/__main__.py
+++ b/src/__main__.py
@@ -1,30 +1,30 @@
-from PyQt5.QtWidgets import QApplication
+from PyQt6.QtWidgets import QApplication
import sys
import logging
import re
import string
-log = logging.getLogger('AVP.Main')
+log = logging.getLogger("AVP.Main")
def main() -> int:
"""Returns an exit code (0 for success)"""
proj = None
- mode = 'GUI'
+ mode = "GUI"
# Determine whether we're in GUI or commandline mode
if len(sys.argv) > 2:
- mode = 'commandline'
+ mode = "commandline"
elif len(sys.argv) == 2:
- if sys.argv[1].startswith('-'):
- mode = 'commandline'
+ if sys.argv[1].startswith("-"):
+ mode = "commandline"
else:
# remove unsafe punctuation characters such as \/?*&^%$#
- if sys.argv[1].endswith('.avp'):
+ if sys.argv[1].endswith(".avp"):
# remove file extension
sys.argv[1] = sys.argv[1][:-4]
- sys.argv[1] = re.sub(f'[{re.escape(string.punctuation)}]', '', sys.argv[1])
+ sys.argv[1] = re.sub(f"[{re.escape(string.punctuation)}]", "", sys.argv[1])
# opening a project file with gui
proj = sys.argv[1]
@@ -32,8 +32,16 @@ def main() -> int:
app = QApplication(sys.argv)
app.setApplicationName("audio-visualizer")
+ screen = app.primaryScreen()
+ if screen is None:
+ dpi = None
+ log.error("Could not detect DPI")
+ else:
+ dpi = screen.physicalDotsPerInchX()
+ log.info("Detected screen DPI: %s", dpi)
+
# Launch program
- if mode == 'commandline':
+ if mode == "commandline":
from .command import Command
main = Command()
@@ -42,14 +50,15 @@ def main() -> int:
# Both branches here may occur in one execution:
# Commandline parsing could change mode back to GUI
- if mode == 'GUI':
+ if mode == "GUI":
from .gui.mainwindow import MainWindow
- mainWindow = MainWindow(proj)
+ mainWindow = MainWindow(proj, dpi)
log.debug("Finished creating MainWindow")
mainWindow.raise_()
- return app.exec_()
+ return app.exec()
+
-if __name__ == '__main__':
- sys.exit(main()) \ No newline at end of file
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/src/command.py b/src/command.py
index 53801fa..783ac26 100644
--- a/src/command.py
+++ b/src/command.py
@@ -1,9 +1,10 @@
-'''
- When using commandline mode, this module's object handles interpreting
- the arguments and giving them to Core, which tracks the main program state.
- Then it immediately exports a video.
-'''
-from PyQt5 import QtCore
+"""
+When using commandline mode, this module's object handles interpreting
+the arguments and giving them to Core, which tracks the main program state.
+Then it immediately exports a video.
+"""
+
+from PyQt6 import QtCore
import argparse
import os
import sys
@@ -16,12 +17,12 @@ import logging
from . import core
-log = logging.getLogger('AVP.Commandline')
+log = logging.getLogger("AVP.Commandline")
class Command(QtCore.QObject):
"""
- This replaces the GUI MainWindow when in commandline mode.
+ This replaces the GUI MainWindow when in commandline mode.
"""
createVideo = QtCore.pyqtSignal()
@@ -29,7 +30,7 @@ class Command(QtCore.QObject):
def __init__(self):
super().__init__()
self.core = core.Core()
- core.Core.mode = 'commandline'
+ core.Core.mode = "commandline"
self.dataDir = self.core.dataDir
self.canceled = False
self.settings = core.Core.settings
@@ -39,52 +40,58 @@ class Command(QtCore.QObject):
def parseArgs(self):
parser = argparse.ArgumentParser(
- prog='avp' if os.path.basename(sys.argv[0]) == "__main__.py" else None,
- description='Create a visualization for an audio file',
- epilog='EXAMPLE COMMAND: avp myvideotemplate '
- '-i ~/Music/song.mp3 -o ~/video.mp4 '
- '-c 0 image path=~/Pictures/thisWeeksPicture.jpg '
- '-c 1 video "preset=My Logo" -c 2 vis layout=classic'
+ prog="avp" if os.path.basename(sys.argv[0]) == "__main__.py" else None,
+ description="Create a visualization for an audio file",
+ epilog="EXAMPLE COMMAND: avp myvideotemplate "
+ "-i ~/Music/song.mp3 -o ~/video.mp4 "
+ "-c 0 image path=~/Pictures/thisWeeksPicture.jpg "
+ '-c 1 video "preset=My Logo" -c 2 vis layout=classic',
)
-
# input/output automatic-export commands
+ parser.add_argument("-i", "--input", metavar="SOUND", help="input audio file")
parser.add_argument(
- '-i', '--input', metavar='SOUND',
- help='input audio file'
+ "-o", "--output", metavar="OUTPUT", help="output video file"
)
parser.add_argument(
- '-o', '--output', metavar='OUTPUT',
- help='output video file'
- )
- parser.add_argument(
- '--export-project', action='store_true',
- help='use input and output files from project file if -i or -o is missing'
+ "--export-project",
+ action="store_true",
+ help="use input and output files from project file if -i or -o is missing",
)
# mutually exclusive debug options
debugCommands = parser.add_mutually_exclusive_group()
debugCommands.add_argument(
- '--test', action='store_true',
- help='run tests and create a report full of debugging info'
+ "--test",
+ action="store_true",
+ help="run tests and create a report full of debugging info",
)
debugCommands.add_argument(
- '--debug', action='store_true',
- help='create bigger logfiles while program is running'
+ "--debug",
+ action="store_true",
+ help="create bigger logfiles while program is running",
)
# project/GUI options
parser.add_argument(
- 'projpath', metavar='path-to-project',
- help='open a project file (.avp)', nargs='?')
+ "projpath",
+ metavar="path-to-project",
+ help="open a project file (.avp)",
+ nargs="?",
+ )
parser.add_argument(
- '-c', '--comp', metavar=('LAYER', 'ARG'),
- help='first arg must be component NAME to insert at LAYER.'
+ "-c",
+ "--comp",
+ metavar=("LAYER", "ARG"),
+ help="first arg must be component NAME to insert at LAYER."
'"help" for information about possible args for a component.',
- nargs='*', action='append')
+ nargs="*",
+ action="append",
+ )
parser.add_argument(
- '--no-preview', action='store_true',
- help='disable live preview during export'
+ "--no-preview",
+ action="store_true",
+ help="disable live preview during export",
)
args = parser.parse_args()
@@ -101,15 +108,11 @@ class Command(QtCore.QObject):
if args.projpath:
projPath = args.projpath
if not os.path.dirname(projPath):
- projPath = os.path.join(
- self.settings.value("projectDir"),
- projPath
- )
- if not projPath.endswith('.avp'):
- projPath += '.avp'
+ projPath = os.path.join(self.settings.value("projectDir"), projPath)
+ if not projPath.endswith(".avp"):
+ projPath += ".avp"
self.core.openProject(self, projPath)
- self.core.selectedComponents = list(
- reversed(self.core.selectedComponents))
+ self.core.selectedComponents = list(reversed(self.core.selectedComponents))
self.core.componentListChanged()
if args.comp:
@@ -120,14 +123,17 @@ class Command(QtCore.QObject):
try:
pos = int(pos)
except ValueError:
- print(pos, 'is not a layer number.')
+ print(pos, "is not a layer number.")
quit(1)
realName = self.parseCompName(name)
if not realName:
- print(name, 'is not a valid component name.')
+ print(name, "is not a valid component name.")
quit(1)
modI = self.core.moduleIndexFor(realName)
i = self.core.insertComponent(pos, modI, self)
+ if i is None:
+ print(name, "could not be initialized.")
+ quit(1)
for arg in compargs:
self.core.selectedComponents[i].command(arg)
@@ -135,15 +141,12 @@ class Command(QtCore.QObject):
errcode, data = self.core.parseAvFile(projPath)
input_ = None
output = None
- for key, value in data['WindowFields']:
- if 'outputFile' in key:
+ for key, value in data["WindowFields"]:
+ if "outputFile" in key:
output = value
if output and not os.path.dirname(value):
- output = os.path.join(
- os.path.expanduser('~'),
- output
- )
- if 'audioFile' in key:
+ output = os.path.join(os.path.expanduser("~"), output)
+ if "audioFile" in key:
input_ = value
# use input/output from project file, overwritten by -i and -o
@@ -153,7 +156,7 @@ class Command(QtCore.QObject):
self.createAudioVisualization(
input_ if not args.input else args.input,
- output if not args.output else args.output
+ output if not args.output else args.output,
)
return "commandline"
@@ -165,11 +168,11 @@ class Command(QtCore.QObject):
core.Core.previewEnabled = False
elif (
- args.projpath is None and
- 'help' not in sys.argv and
- '--debug' not in sys.argv and
- '--test' not in sys.argv
- ):
+ args.projpath is None
+ and "help" not in sys.argv
+ and "--debug" not in sys.argv
+ and "--test" not in sys.argv
+ ):
parser.print_help()
quit(1)
@@ -180,12 +183,9 @@ class Command(QtCore.QObject):
print("No components selected. Adding a default visualizer.")
time.sleep(1)
self.core.insertComponent(0, 0, self)
- self.core.selectedComponents = list(
- reversed(self.core.selectedComponents))
+ self.core.selectedComponents = list(reversed(self.core.selectedComponents))
self.core.componentListChanged()
- self.worker = self.core.newVideoWorker(
- self, input, output
- )
+ self.worker = self.core.newVideoWorker(self, input, output)
# quit(0) after video is created
self.worker.videoCreated.connect(self.videoCreated)
self.lastProgressUpdate = time.time()
@@ -199,16 +199,18 @@ class Command(QtCore.QObject):
@QtCore.pyqtSlot(str)
def progressBarSetText(self, value):
- if 'Export ' in value:
+ if "Export " in value:
# Don't duplicate completion/failure messages
return
- if not value.startswith('Exporting') \
- and time.time() - self.lastProgressUpdate >= 0.05:
+ if (
+ not value.startswith("Exporting")
+ and time.time() - self.lastProgressUpdate >= 0.05
+ ):
# Show most messages very often
print(value)
elif time.time() - self.lastProgressUpdate >= 2.0:
# Give user time to read ffmpeg's output during the export
- print('##### %s' % value)
+ print("##### %s" % value)
else:
return
self.lastProgressUpdate = time.time()
@@ -221,9 +223,9 @@ class Command(QtCore.QObject):
quit(code)
def showMessage(self, **kwargs):
- print(kwargs['msg'])
- if 'detail' in kwargs:
- print(kwargs['detail'])
+ print(kwargs["msg"])
+ if "detail" in kwargs:
+ print(kwargs["detail"])
@QtCore.pyqtSlot(str, str)
def videoThreadError(self, msg, detail):
@@ -235,7 +237,7 @@ class Command(QtCore.QObject):
pass
def parseCompName(self, name):
- '''Deduces a proper component name out of a commandline arg'''
+ """Deduces a proper component name out of a commandline arg"""
if name.title() in self.core.compNames:
return name.title()
@@ -244,9 +246,7 @@ class Command(QtCore.QObject):
return compName
compFileNames = [
- os.path.splitext(
- os.path.basename(mod.__file__)
- )[0]
+ os.path.splitext(os.path.basename(mod.__file__))[0]
for mod in self.core.modules
]
for i, compFileName in enumerate(compFileNames):
@@ -258,15 +258,17 @@ class Command(QtCore.QObject):
def runTests(self):
from . import tests
+
test_report = os.path.join(core.Core.logDir, "test_report.log")
tests.run(test_report)
# Choose a numbered location to put the output file
logNumber = 0
+
def getFilename():
"""Get a numbered filename for the final test report"""
nonlocal logNumber
- name = os.path.join(os.path.expanduser('~'), "avp_test_report")
+ name = os.path.join(os.path.expanduser("~"), "avp_test_report")
while True:
possibleName = f"{name}{logNumber:0>2}.txt"
if os.path.exists(possibleName) and logNumber < 100:
diff --git a/src/component.py b/src/component.py
index 1e10f66..01d4e44 100644
--- a/src/component.py
+++ b/src/component.py
@@ -1,9 +1,10 @@
-'''
- Base classes for components to import. Read comments for some documentation
- on making a valid component.
-'''
-from PyQt5 import uic, QtCore, QtWidgets
-from PyQt5.QtGui import QColor
+"""
+Base classes for components to import. Read comments for some documentation
+on making a valid component.
+"""
+
+from PyQt6 import uic, QtCore, QtWidgets
+from PyQt6.QtGui import QColor, QUndoCommand
import os
import sys
import math
@@ -13,18 +14,22 @@ from copy import copy
from .toolkit.frame import BlankFrame
from .toolkit import (
- getWidgetValue, setWidgetValue, connectWidget, rgbFromString, blockSignals
+ getWidgetValue,
+ setWidgetValue,
+ connectWidget,
+ rgbFromString,
+ blockSignals,
)
-log = logging.getLogger('AVP.ComponentHandler')
+log = logging.getLogger("AVP.ComponentHandler")
class ComponentMetaclass(type(QtCore.QObject)):
- '''
- Checks the validity of each Component class and mutates some attrs.
- E.g., takes only major version from version string & decorates methods
- '''
+ """
+ Checks the validity of each Component class and mutates some attrs.
+ E.g., takes only major version from version string & decorates methods
+ """
def initializationWrapper(func):
def initializationWrapper(self, *args, **kwargs):
@@ -32,51 +37,55 @@ class ComponentMetaclass(type(QtCore.QObject)):
return func(self, *args, **kwargs)
except Exception:
try:
- raise ComponentError(self, 'initialization process')
+ raise ComponentError(self, "initialization process")
except ComponentError:
return
+
return initializationWrapper
def renderWrapper(func):
def renderWrapper(self, *args, **kwargs):
try:
log.verbose(
- '### %s #%s renders a preview frame ###',
- self.__class__.name, str(self.compPos),
+ "### %s #%s renders a preview frame ###",
+ self.__class__.name,
+ str(self.compPos),
)
return func(self, *args, **kwargs)
except Exception as e:
try:
- if e.__class__.__name__.startswith('Component'):
+ if e.__class__.__name__.startswith("Component"):
raise
else:
- raise ComponentError(self, 'renderer')
+ raise ComponentError(self, "renderer")
except ComponentError:
return BlankFrame()
+
return renderWrapper
def commandWrapper(func):
- '''Intercepts the command() method to check for global args'''
+ """Intercepts the command() method to check for global args"""
+
def commandWrapper(self, arg):
- if arg.startswith('preset='):
- _, preset = arg.split('=', 1)
+ if arg.startswith("preset="):
+ _, preset = arg.split("=", 1)
path = os.path.join(self.core.getPresetDir(self), preset)
if not os.path.exists(path):
print('Couldn\'t locate preset "%s"' % preset)
quit(1)
else:
- print('Opening "%s" preset on layer %s' % (
- preset, self.compPos)
- )
+ print('Opening "%s" preset on layer %s' % (preset, self.compPos))
self.core.openPreset(path, self.compPos, preset)
# Don't call the component's command() method
return
else:
return func(self, arg)
+
return commandWrapper
def propertiesWrapper(func):
- '''Intercepts the usual properties if the properties are locked.'''
+ """Intercepts the usual properties if the properties are locked."""
+
def propertiesWrapper(self):
if self._lockedProperties is not None:
return self._lockedProperties
@@ -85,22 +94,26 @@ class ComponentMetaclass(type(QtCore.QObject)):
return func(self)
except Exception:
try:
- raise ComponentError(self, 'properties')
+ raise ComponentError(self, "properties")
except ComponentError:
return []
+
return propertiesWrapper
def errorWrapper(func):
- '''Intercepts the usual error message if it is locked.'''
+ """Intercepts the usual error message if it is locked."""
+
def errorWrapper(self):
if self._lockedError is not None:
return self._lockedError
else:
return func(self)
+
return errorWrapper
def loadPresetWrapper(func):
- '''Wraps loadPreset to handle the self.openingPreset boolean'''
+ """Wraps loadPreset to handle the self.openingPreset boolean"""
+
class openingPreset:
def __init__(self, comp):
self.comp = comp
@@ -117,17 +130,19 @@ class ComponentMetaclass(type(QtCore.QObject)):
return func(self, *args)
except Exception:
try:
- raise ComponentError(self, 'preset loader')
+ raise ComponentError(self, "preset loader")
except ComponentError:
return
+
return presetWrapper
def updateWrapper(func):
- '''
- Calls _preUpdate before every subclass update().
- Afterwards, for non-user updates, calls _autoUpdate().
- For undoable updates triggered by the user, calls _userUpdate()
- '''
+ """
+ Calls _preUpdate before every subclass update().
+ Afterwards, for non-user updates, calls _autoUpdate().
+ For undoable updates triggered by the user, calls _userUpdate()
+ """
+
class wrap:
def __init__(self, comp, auto):
self.comp = comp
@@ -137,28 +152,33 @@ class ComponentMetaclass(type(QtCore.QObject)):
self.comp._preUpdate()
def __exit__(self, *args):
- if self.auto or self.comp.openingPreset \
- or not hasattr(self.comp.parent, 'undoStack'):
- log.verbose('Automatic update')
+ if (
+ self.auto
+ or self.comp.openingPreset
+ or not hasattr(self.comp.parent, "undoStack")
+ ):
+ log.verbose("Automatic update")
self.comp._autoUpdate()
else:
- log.verbose('User update')
+ log.verbose("User update")
self.comp._userUpdate()
def updateWrapper(self, **kwargs):
- auto = kwargs['auto'] if 'auto' in kwargs else False
+ auto = kwargs["auto"] if "auto" in kwargs else False
with wrap(self, auto):
try:
return func(self)
except Exception:
try:
- raise ComponentError(self, 'update method')
+ raise ComponentError(self, "update method")
except ComponentError:
return
+
return updateWrapper
def widgetWrapper(func):
- '''Connects all widgets to update method after the subclass's method'''
+ """Connects all widgets to update method after the subclass's method"""
+
class wrap:
def __init__(self, comp):
self.comp = comp
@@ -169,92 +189,99 @@ class ComponentMetaclass(type(QtCore.QObject)):
def __exit__(self, *args):
for widgetList in self.comp._allWidgets.values():
for widget in widgetList:
- log.verbose('Connecting %s', str(
- widget.__class__.__name__))
+ log.verbose("Connecting %s", str(widget.__class__.__name__))
connectWidget(widget, self.comp.update)
def widgetWrapper(self, *args, **kwargs):
- auto = kwargs['auto'] if 'auto' in kwargs else False
+ auto = kwargs["auto"] if "auto" in kwargs else False
with wrap(self):
try:
return func(self, *args, **kwargs)
except Exception:
try:
- raise ComponentError(self, 'widget creation')
+ raise ComponentError(self, "widget creation")
except ComponentError:
return
+
return widgetWrapper
def __new__(cls, name, parents, attrs):
- if 'ui' not in attrs:
+ if "ui" not in attrs:
# Use module name as ui filename by default
- attrs['ui'] = '%s.ui' % os.path.splitext(
- attrs['__module__'].split('.')[-1]
- )[0]
+ attrs["ui"] = (
+ "%s.ui" % os.path.splitext(attrs["__module__"].split(".")[-1])[0]
+ )
decorate = (
- 'names', # Class methods
- 'error', 'audio', 'properties', # Properties
- 'preFrameRender', 'previewRender',
- 'loadPreset', 'command',
- 'update', 'widget',
+ "names", # Class methods
+ "error",
+ "audio",
+ "properties", # Properties
+ "preFrameRender",
+ "previewRender",
+ "loadPreset",
+ "command",
+ "update",
+ "widget",
)
# Auto-decorate methods
for key in decorate:
if key not in attrs:
continue
- if key in ('names'):
+ if key in ("names"):
attrs[key] = classmethod(attrs[key])
- elif key in ('audio'):
+ elif key in ("audio"):
attrs[key] = property(attrs[key])
- elif key == 'command':
+ elif key == "command":
attrs[key] = cls.commandWrapper(attrs[key])
- elif key == 'previewRender':
+ elif key == "previewRender":
attrs[key] = cls.renderWrapper(attrs[key])
- elif key == 'preFrameRender':
+ elif key == "preFrameRender":
attrs[key] = cls.initializationWrapper(attrs[key])
- elif key == 'properties':
+ elif key == "properties":
attrs[key] = cls.propertiesWrapper(attrs[key])
- elif key == 'error':
+ elif key == "error":
attrs[key] = cls.errorWrapper(attrs[key])
- elif key == 'loadPreset':
+ elif key == "loadPreset":
attrs[key] = cls.loadPresetWrapper(attrs[key])
- elif key == 'update':
+ elif key == "update":
attrs[key] = cls.updateWrapper(attrs[key])
- elif key == 'widget' and parents[0] != QtCore.QObject:
+ elif key == "widget" and parents[0] != QtCore.QObject:
attrs[key] = cls.widgetWrapper(attrs[key])
# Turn version string into a number
try:
- if 'version' not in attrs:
+ if "version" not in attrs:
log.error(
- 'No version attribute in %s. Defaulting to 1',
- attrs['name'])
- attrs['version'] = 1
+ "No version attribute in %s. Defaulting to 1",
+ attrs["name"],
+ )
+ attrs["version"] = 1
else:
- attrs['version'] = int(attrs['version'].split('.')[0])
+ attrs["version"] = int(attrs["version"].split(".")[0])
except ValueError:
log.critical(
- '%s component has an invalid version string:\n%s',
- attrs['name'], str(attrs['version'])
+ "%s component has an invalid version string:\n%s",
+ attrs["name"],
+ str(attrs["version"]),
)
except KeyError:
- log.critical('%s component has no version string.', attrs['name'])
+ log.critical("%s component has no version string.", attrs["name"])
else:
return super().__new__(cls, name, parents, attrs)
quit(1)
class Component(QtCore.QObject, metaclass=ComponentMetaclass):
- '''
- The base class for components to inherit.
- '''
+ """
+ The base class for components to inherit.
+ """
- name = 'Component'
+ name = "Component"
# ui = 'name_Of_Non_Default_Ui_File'
- version = '1.0.0'
+ version = "1.0.0"
# The major version (before the first dot) is used to determine
# preset compatibility; the rest is ignored so it can be non-numeric.
@@ -297,19 +324,19 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def __repr__(self):
import pprint
+
try:
preset = self.savePreset()
except Exception as e:
- preset = '%s occurred while saving preset' % str(e)
-
- return (
- 'Component(module %s, pos %s) (%s)\n'
- 'Name: %s v%s\nPreset: %s' % (
- self.moduleIndex, self.compPos,
- object.__repr__(self),
- self.__class__.name, str(self.__class__.version),
- pprint.pformat(preset)
- )
+ preset = "%s occurred while saving preset" % str(e)
+
+ return "Component(module %s, pos %s) (%s)\n" "Name: %s v%s\nPreset: %s" % (
+ self.moduleIndex,
+ self.compPos,
+ object.__repr__(self),
+ self.__class__.name,
+ str(self.__class__.version),
+ pprint.pformat(preset),
)
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
@@ -321,17 +348,17 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
return image
def preFrameRender(self, **kwargs):
- '''
- Must call super() when subclassing
- Triggered only before a video is exported (video_thread.py)
- self.audioFile = filepath to the main input audio file
- self.completeAudioArray = a list of audio samples
- self.sampleSize = number of audio samples per video frame
- self.progressBarUpdate = signal to set progress bar number
- self.progressBarSetText = signal to set progress bar text
- Use the latter two signals to update the MainWindow if needed
- for a long initialization procedure (i.e., for a visualizer)
- '''
+ """
+ Must call super() when subclassing
+ Triggered only before a video is exported (video_thread.py)
+ self.audioFile = filepath to the main input audio file
+ self.completeAudioArray = a list of audio samples
+ self.sampleSize = number of audio samples per video frame
+ self.progressBarUpdate = signal to set progress bar number
+ self.progressBarSetText = signal to set progress bar text
+ Use the latter two signals to update the MainWindow if needed
+ for a long initialization procedure (i.e., for a visualizer)
+ """
for key, value in kwargs.items():
setattr(self, key, value)
@@ -348,92 +375,94 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
def properties(self):
- '''
- Return a list of properties to signify if your component is
- non-animated ('static'), returns sound ('audio'), or has
- encountered an error in configuration ('error').
- '''
+ """
+ Return a list of properties to signify if your component is
+ non-animated ('static'), returns sound ('audio'), or has
+ encountered an error in configuration ('error').
+ """
return []
def error(self):
- '''
- Return a string containing an error message, or None for a default.
- Or tuple of two strings for a message with details.
- Alternatively use lockError(msgString) within properties()
- to skip this method entirely.
- '''
+ """
+ Return a string containing an error message, or None for a default.
+ Or tuple of two strings for a message with details.
+ Alternatively use lockError(msgString) within properties()
+ to skip this method entirely.
+ """
return
def audio(self):
- '''
- Return audio to mix into master as a tuple with two elements:
- The first element can be:
- - A string (path to audio file),
- - Or an object that returns audio data through a pipe
- The second element must be a dictionary of ffmpeg filters/options
- to apply to the input stream. See the filter docs for ideas:
- https://ffmpeg.org/ffmpeg-filters.html
- '''
+ """
+ Return audio to mix into master as a tuple with two elements:
+ The first element can be:
+ - A string (path to audio file),
+ - Or an object that returns audio data through a pipe
+ The second element must be a dictionary of ffmpeg filters/options
+ to apply to the input stream. See the filter docs for ideas:
+ https://ffmpeg.org/ffmpeg-filters.html
+ """
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# Idle Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
def widget(self, parent):
- '''
- Call super().widget(*args) to create the component widget
- which also auto-connects any common widgets (e.g., checkBoxes)
- to self.update(). Then in a subclass connect special actions
- (e.g., pushButtons to select a file) and initialize
- '''
+ """
+ Call super().widget(*args) to create the component widget
+ which also auto-connects any common widgets (e.g., checkBoxes)
+ to self.update(). Then in a subclass connect special actions
+ (e.g., pushButtons to select a file) and initialize
+ """
self.parent = parent
self.settings = parent.settings
log.verbose(
- 'Creating UI for %s #%s\'s widget',
- self.__class__.name, self.compPos
+ "Creating UI for %s #%s's widget",
+ self.__class__.name,
+ self.compPos,
)
self.page = self.loadUi(self.__class__.ui)
# Find all normal widgets which will be connected after subclass method
self._allWidgets = {
- 'lineEdit': self.page.findChildren(QtWidgets.QLineEdit),
- 'checkBox': self.page.findChildren(QtWidgets.QCheckBox),
- 'spinBox': self.page.findChildren(QtWidgets.QSpinBox),
- 'comboBox': self.page.findChildren(QtWidgets.QComboBox),
+ "lineEdit": self.page.findChildren(QtWidgets.QLineEdit),
+ "checkBox": self.page.findChildren(QtWidgets.QCheckBox),
+ "spinBox": self.page.findChildren(QtWidgets.QSpinBox),
+ "comboBox": self.page.findChildren(QtWidgets.QComboBox),
}
- self._allWidgets['spinBox'].extend(
+ self._allWidgets["spinBox"].extend(
self.page.findChildren(QtWidgets.QDoubleSpinBox)
)
def update(self):
- '''
- Starting point for a component update. A subclass should override
- this method, and the base class will then magically insert a call
- to either _autoUpdate() or _userUpdate() at the end.
- '''
+ """
+ Starting point for a component update. A subclass should override
+ this method, and the base class will then magically insert a call
+ to either _autoUpdate() or _userUpdate() at the end.
+ """
def loadPreset(self, presetDict, presetName=None):
- '''
- Subclasses should take (presetDict, *args) as args.
- Must use super().loadPreset(presetDict, *args) first,
- then update self.page widgets using the preset dict.
- '''
- self.currentPreset = presetName \
- if presetName is not None else presetDict['preset']
+ """
+ Subclasses should take (presetDict, *args) as args.
+ Must use super().loadPreset(presetDict, *args) first,
+ then update self.page widgets using the preset dict.
+ """
+ self.currentPreset = (
+ presetName if presetName is not None else presetDict["preset"]
+ )
for attr, widget in self._trackedWidgets.items():
- key = attr if attr not in self._presetNames \
- else self._presetNames[attr]
+ key = attr if attr not in self._presetNames else self._presetNames[attr]
try:
val = presetDict[key]
except KeyError as e:
log.info(
- '%s missing value %s. Outdated preset?',
- self.currentPreset, str(e)
+ "%s missing value %s. Outdated preset?",
+ self.currentPreset,
+ str(e),
)
val = getattr(self, key)
if attr in self._colorWidgets:
- widget.setText('%s,%s,%s' % val)
+ widget.setText("%s,%s,%s" % val)
btnStyle = (
"QPushButton { background-color : %s; outline: none; }"
% QColor(*val).name()
@@ -450,8 +479,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
saveValueStore = {}
for attr, widget in self._trackedWidgets.items():
presetAttrName = (
- attr if attr not in self._presetNames
- else self._presetNames[attr]
+ attr if attr not in self._presetNames else self._presetNames[attr]
)
if attr in self._relativeWidgets:
try:
@@ -465,19 +493,18 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
return saveValueStore
def commandHelp(self):
- '''Help text as string for this component's commandline arguments'''
-
- def command(self, arg=''):
- '''
- Configure a component using an arg from the commandline. This is
- never called if global args like 'preset=' are found in the arg.
- So simply check for any non-global args in your component and
- call super().command() at the end to get a Help message.
- '''
+ """Help text as string for this component's commandline arguments"""
+
+ def command(self, arg=""):
+ """
+ Configure a component using an arg from the commandline. This is
+ never called if global args like 'preset=' are found in the arg.
+ So simply check for any non-global args in your component and
+ call super().command() at the end to get a Help message.
+ """
print(
- self.__class__.name, 'Usage:\n'
- 'Open a preset for this component:\n'
- ' "preset=Preset Name"'
+ self.__class__.name,
+ "Usage:\n" "Open a preset for this component:\n" ' "preset=Preset Name"',
)
self.commandHelp()
quit(0)
@@ -486,19 +513,21 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
# "Private" Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
def _preUpdate(self):
- '''Happens before subclass update()'''
+ """Happens before subclass update()"""
for attr in self._relativeWidgets:
self.updateRelativeWidget(attr)
def _userUpdate(self):
- '''Happens after subclass update() for an undoable update by user.'''
+ """Happens after subclass update() for an undoable update by user."""
oldWidgetVals = {
- attr: copy(getattr(self, attr))
- for attr in self._trackedWidgets
+ attr: copy(getattr(self, attr)) for attr in self._trackedWidgets
}
newWidgetVals = {
- attr: getWidgetValue(widget)
- if attr not in self._colorWidgets else rgbFromString(widget.text())
+ attr: (
+ getWidgetValue(widget)
+ if attr not in self._colorWidgets
+ else rgbFromString(widget.text())
+ )
for attr, widget in self._trackedWidgets.items()
}
modifiedWidgets = {
@@ -511,7 +540,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self.parent.undoStack.push(action)
def _autoUpdate(self):
- '''Happens after subclass update() for an internal component update.'''
+ """Happens after subclass update() for an internal component update."""
newWidgetVals = {
attr: getWidgetValue(widget)
for attr, widget in self._trackedWidgets.items()
@@ -520,10 +549,10 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self._sendUpdateSignal()
def setAttrs(self, attrDict):
- '''
- Sets attrs (linked to trackedWidgets) in this component to
- the values in the attrDict. Mutates certain widget values if needed
- '''
+ """
+ Sets attrs (linked to trackedWidgets) in this component to
+ the values in the attrDict. Mutates certain widget values if needed
+ """
for attr, val in attrDict.items():
if attr in self._colorWidgets:
# Color Widgets must have a tuple & have a button to update
@@ -533,110 +562,111 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
rgbTuple = rgbFromString(val)
btnStyle = (
"QPushButton { background-color : %s; outline: none; }"
- % QColor(*rgbTuple).name())
+ % QColor(*rgbTuple).name()
+ )
self._colorWidgets[attr].setStyleSheet(btnStyle)
setattr(self, attr, rgbTuple)
else:
# Normal tracked widget
setattr(self, attr, val)
- log.verbose('Setting %s self.%s to %s' % (
- self.__class__.name, attr, val))
+ log.verbose("Setting %s self.%s to %s" % (self.__class__.name, attr, val))
def setWidgetValues(self, attrDict):
- '''
- Sets widgets defined by keys in trackedWidgets in this preset to
- the values in the attrDict.
- '''
- affectedWidgets = [
- self._trackedWidgets[attr] for attr in attrDict
- ]
+ """
+ Sets widgets defined by keys in trackedWidgets in this preset to
+ the values in the attrDict.
+ """
+ affectedWidgets = [self._trackedWidgets[attr] for attr in attrDict]
with blockSignals(affectedWidgets):
for attr, val in attrDict.items():
widget = self._trackedWidgets[attr]
if attr in self._colorWidgets:
- val = '%s,%s,%s' % val
+ val = "%s,%s,%s" % val
setWidgetValue(widget, val)
def _sendUpdateSignal(self):
if not self.core.openingProject:
self.parent.drawPreview()
saveValueStore = self.savePreset()
- saveValueStore['preset'] = self.currentPreset
+ saveValueStore["preset"] = self.currentPreset
self.modified.emit(self.compPos, saveValueStore)
def trackWidgets(self, trackDict, **kwargs):
- '''
- Name widgets to track in update(), savePreset(), loadPreset(), and
- command(). Requires a dict of attr names as keys, widgets as values
-
- Optional args:
- 'presetNames': preset variable names to replace attr names
- 'commandArgs': arg keywords that differ from attr names
- 'colorWidgets': identify attr as RGB tuple & update button CSS
- 'relativeWidgets': change value proportionally to resolution
-
- NOTE: Any kwarg key set to None will selectively disable tracking.
- '''
+ """
+ Name widgets to track in update(), savePreset(), loadPreset(), and
+ command(). Requires a dict of attr names as keys, widgets as values
+
+ Optional args:
+ 'presetNames': preset variable names to replace attr names
+ 'commandArgs': arg keywords that differ from attr names
+ 'colorWidgets': identify attr as RGB tuple & update button CSS
+ 'relativeWidgets': change value proportionally to resolution
+
+ NOTE: Any kwarg key set to None will selectively disable tracking.
+ """
self._trackedWidgets = trackDict
for kwarg in kwargs:
try:
if kwarg in (
- 'presetNames',
- 'commandArgs',
- 'colorWidgets',
- 'relativeWidgets',
- ):
- setattr(self, '_{}'.format(kwarg), kwargs[kwarg])
+ "presetNames",
+ "commandArgs",
+ "colorWidgets",
+ "relativeWidgets",
+ ):
+ setattr(self, "_{}".format(kwarg), kwargs[kwarg])
else:
- raise ComponentError(
- self, 'Nonsensical keywords to trackWidgets.')
+ raise ComponentError(self, "Nonsensical keywords to trackWidgets.")
except ComponentError:
continue
- if kwarg == 'colorWidgets':
+ if kwarg == "colorWidgets":
+
def makeColorFunc(attr):
def pickColor_():
self.mergeUndo = False
self.pickColor(
self._trackedWidgets[attr],
- self._colorWidgets[attr]
+ self._colorWidgets[attr],
)
self.mergeUndo = True
+
return pickColor_
- self._colorFuncs = {
- attr: makeColorFunc(attr) for attr in kwargs[kwarg]
- }
+
+ self._colorFuncs = {attr: makeColorFunc(attr) for attr in kwargs[kwarg]}
for attr, func in self._colorFuncs.items():
self._colorWidgets[attr].clicked.connect(func)
self._colorWidgets[attr].setStyleSheet(
- "QPushButton {"
- "background-color : #FFFFFF; outline: none; }"
+ "QPushButton {" "background-color : #FFFFFF; outline: none; }"
)
- if kwarg == 'relativeWidgets':
+ if kwarg == "relativeWidgets":
# store maximum values of spinBoxes to be scaled appropriately
for attr in kwargs[kwarg]:
- self._relativeMaximums[attr] = \
- self._trackedWidgets[attr].maximum()
+ self._relativeMaximums[attr] = self._trackedWidgets[attr].maximum()
self.updateRelativeWidgetMaximum(attr)
- setattr(
- self, attr, getWidgetValue(self._trackedWidgets[attr])
- )
+ setattr(self, attr, getWidgetValue(self._trackedWidgets[attr]))
self._preUpdate()
self._autoUpdate()
def pickColor(self, textWidget, button):
- '''Use color picker to get color input from the user.'''
+ """Use color picker to get color input from the user."""
dialog = QtWidgets.QColorDialog()
- dialog.setOption(QtWidgets.QColorDialog.ShowAlphaChannel, True)
+ # TODO alpha channel is not actually shown in most color picker widgets?
+ dialog.setOption(
+ QtWidgets.QColorDialog.ColorDialogOption.ShowAlphaChannel, True
+ )
color = dialog.getColor()
if color.isValid():
- RGBstring = '%s,%s,%s' % (
- str(color.red()), str(color.green()), str(color.blue()))
- btnStyle = "QPushButton{background-color: %s; outline: none;}" \
- % color.name()
+ RGBstring = "%s,%s,%s" % (
+ str(color.red()),
+ str(color.green()),
+ str(color.blue()),
+ )
+ btnStyle = (
+ "QPushButton{background-color: %s; outline: none;}" % color.name()
+ )
textWidget.setText(RGBstring)
button.setStyleSheet(btnStyle)
@@ -659,25 +689,25 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self._lockedSize = None
def loadUi(self, filename):
- '''Load a Qt Designer ui file to use for this component's widget'''
+ """Load a Qt Designer ui file to use for this component's widget"""
return uic.loadUi(os.path.join(self.core.componentsPath, filename))
@property
def width(self):
if self._lockedSize is None:
- return int(self.settings.value('outputWidth'))
+ return int(self.settings.value("outputWidth"))
else:
return self._lockedSize[0]
@property
def height(self):
if self._lockedSize is None:
- return int(self.settings.value('outputHeight'))
+ return int(self.settings.value("outputHeight"))
else:
return self._lockedSize[1]
def cancel(self):
- '''Stop any lengthy process in response to this variable.'''
+ """Stop any lengthy process in response to this variable."""
self.canceled = True
def reset(self):
@@ -688,22 +718,22 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def relativeWidgetAxis(func):
def relativeWidgetAxis(self, attr, *args, **kwargs):
hasVerticalWords = (
- lambda attr:
- 'height' in attr.lower() or
- 'ypos' in attr.lower() or
- attr == 'y'
+ lambda attr: "height" in attr.lower()
+ or "ypos" in attr.lower()
+ or attr == "y"
)
- if 'axis' not in kwargs:
+ if "axis" not in kwargs:
axis = self.width
if hasVerticalWords(attr):
axis = self.height
- kwargs['axis'] = axis
- if 'axis' in kwargs and type(kwargs['axis']) is tuple:
- axis = kwargs['axis'][0]
+ kwargs["axis"] = axis
+ if "axis" in kwargs and type(kwargs["axis"]) is tuple:
+ axis = kwargs["axis"][0]
if hasVerticalWords(attr):
- axis = kwargs['axis'][1]
- kwargs['axis'] = axis
+ axis = kwargs["axis"][1]
+ kwargs["axis"] = axis
return func(self, attr, *args, **kwargs)
+
return relativeWidgetAxis
@relativeWidgetAxis
@@ -712,14 +742,20 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
val = self._relativeValues[attr]
if val > 50.0:
log.warning(
- '%s #%s attempted to set %s to dangerously high number %s',
- self.__class__.name, self.compPos, attr, val
+ "%s #%s attempted to set %s to dangerously high number %s",
+ self.__class__.name,
+ self.compPos,
+ attr,
+ val,
)
val = 50.0
- result = math.ceil(kwargs['axis'] * val)
+ result = math.ceil(kwargs["axis"] * val)
log.verbose(
- 'Converting %s: f%s to px%s using axis %s',
- attr, val, result, kwargs['axis']
+ "Converting %s: f%s to px%s using axis %s",
+ attr,
+ val,
+ result,
+ kwargs["axis"],
)
return result
@@ -727,65 +763,63 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def floatValForAttr(self, attr, val=None, **kwargs):
if val is None:
val = self._trackedWidgets[attr].value()
- return val / kwargs['axis']
+ return val / kwargs["axis"]
def setRelativeWidget(self, attr, floatVal):
- '''Set a relative widget using a float'''
+ """Set a relative widget using a float"""
pixelVal = self.pixelValForAttr(attr, floatVal)
with blockSignals(self._trackedWidgets[attr]):
self._trackedWidgets[attr].setValue(pixelVal)
self.update(auto=True)
def getOldAttr(self, attr):
- '''
- Returns previous state of this attr. Used to determine whether
- a relative widget must be updated. Required because undoing/redoing
- can make determining the 'previous' value tricky.
- '''
+ """
+ Returns previous state of this attr. Used to determine whether
+ a relative widget must be updated. Required because undoing/redoing
+ can make determining the 'previous' value tricky.
+ """
if self.oldAttrs is not None:
return self.oldAttrs[attr]
else:
try:
return getattr(self, attr)
except AttributeError:
- log.error('Using visible values instead of oldAttrs')
+ log.error("Using visible values instead of oldAttrs")
return self._trackedWidgets[attr].value()
def updateRelativeWidget(self, attr):
- '''Called by _preUpdate() for each relativeWidget before each update'''
+ """Called by _preUpdate() for each relativeWidget before each update"""
oldUserValue = self.getOldAttr(attr)
newUserValue = self._trackedWidgets[attr].value()
newRelativeVal = self.floatValForAttr(attr, newUserValue)
if attr in self._relativeValues:
oldRelativeVal = self._relativeValues[attr]
- if oldUserValue == newUserValue \
- and oldRelativeVal != newRelativeVal:
+ if oldUserValue == newUserValue and oldRelativeVal != newRelativeVal:
# Float changed without pixel value changing, which
# means the pixel value needs to be updated
log.debug(
- 'Updating %s #%s\'s relative widget: %s',
- self.__class__.name, self.compPos, attr)
+ "Updating %s #%s's relative widget: %s",
+ self.__class__.name,
+ self.compPos,
+ attr,
+ )
with blockSignals(self._trackedWidgets[attr]):
self.updateRelativeWidgetMaximum(attr)
pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
self._trackedWidgets[attr].setValue(pixelVal)
- if attr not in self._relativeValues \
- or oldUserValue != newUserValue:
+ if attr not in self._relativeValues or oldUserValue != newUserValue:
self._relativeValues[attr] = newRelativeVal
def updateRelativeWidgetMaximum(self, attr):
- maxRes = int(self.core.resolutions[0].split('x')[0])
- newMaximumValue = self.width * (
- self._relativeMaximums[attr] /
- maxRes
- )
+ maxRes = int(self.core.resolutions[0].split("x")[0])
+ newMaximumValue = self.width * (self._relativeMaximums[attr] / maxRes)
self._trackedWidgets[attr].setMaximum(int(newMaximumValue))
class ComponentError(RuntimeError):
- '''Gives the MainWindow a traceback to display, and cancels the export.'''
+ """Gives the MainWindow a traceback to display, and cancels the export."""
prevErrors = []
lastTime = time.time()
@@ -794,42 +828,46 @@ class ComponentError(RuntimeError):
if msg is None and sys.exc_info()[0] is not None:
msg = str(sys.exc_info()[1])
else:
- msg = 'Unknown error.'
- log.error("ComponentError by %s's %s: %s" % (
- caller.name, name, msg))
+ msg = "Unknown error."
+ log.error("ComponentError by %s's %s: %s" % (caller.name, name, msg))
# Don't create multiple windows for quickly repeated messages
if len(ComponentError.prevErrors) > 1:
ComponentError.prevErrors.pop()
ComponentError.prevErrors.insert(0, name)
curTime = time.time()
- if name in ComponentError.prevErrors[1:] \
- and curTime - ComponentError.lastTime < 1.0:
+ if (
+ name in ComponentError.prevErrors[1:]
+ and curTime - ComponentError.lastTime < 1.0
+ ):
return
ComponentError.lastTime = time.time()
from .toolkit import formatTraceback
+
if sys.exc_info()[0] is not None:
- string = (
- "%s component (#%s): %s encountered %s %s: %s" % (
- caller.__class__.name,
- str(caller.compPos),
- name,
- 'an' if any([
- sys.exc_info()[0].__name__.startswith(vowel)
- for vowel in ('A', 'I', 'U', 'O', 'E')
- ]) else 'a',
- sys.exc_info()[0].__name__,
- str(sys.exc_info()[1])
- )
+ string = "%s component (#%s): %s encountered %s %s: %s" % (
+ caller.__class__.name,
+ str(caller.compPos),
+ name,
+ (
+ "an"
+ if any(
+ [
+ sys.exc_info()[0].__name__.startswith(vowel)
+ for vowel in ("A", "I", "U", "O", "E")
+ ]
+ )
+ else "a"
+ ),
+ sys.exc_info()[0].__name__,
+ str(sys.exc_info()[1]),
)
detail = formatTraceback(sys.exc_info()[2])
else:
string = name
detail = "Attributes:\n%s" % (
- "\n".join(
- [m for m in dir(caller) if not m.startswith('_')]
- )
+ "\n".join([m for m in dir(caller) if not m.startswith("_")])
)
super().__init__(string)
@@ -837,28 +875,29 @@ class ComponentError(RuntimeError):
caller._error.emit(string, detail)
-class ComponentUpdate(QtWidgets.QUndoCommand):
- '''Command object for making a component action undoable'''
+class ComponentUpdate(QUndoCommand):
+ """Command object for making a component action undoable"""
+
def __init__(self, parent, oldWidgetVals, modifiedVals):
- super().__init__(
- 'change %s component #%s' % (
- parent.name, parent.compPos
- )
- )
+ super().__init__("change %s component #%s" % (parent.name, parent.compPos))
self.undone = False
self.res = (int(parent.width), int(parent.height))
self.parent = parent
self.oldWidgetVals = {
- attr: copy(val)
- if attr not in self.parent._relativeWidgets
- else self.parent.floatValForAttr(attr, val, axis=self.res)
+ attr: (
+ copy(val)
+ if attr not in self.parent._relativeWidgets
+ else self.parent.floatValForAttr(attr, val, axis=self.res)
+ )
for attr, val in oldWidgetVals.items()
if attr in modifiedVals
}
self.modifiedVals = {
- attr: val
- if attr not in self.parent._relativeWidgets
- else self.parent.floatValForAttr(attr, val, axis=self.res)
+ attr: (
+ val
+ if attr not in self.parent._relativeWidgets
+ else self.parent.floatValForAttr(attr, val, axis=self.res)
+ )
for attr, val in modifiedVals.items()
}
@@ -877,12 +916,13 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
self.modifiedVals[attr] = val
else:
log.warning(
- '%s component settings changed at once. (%s)',
- len(self.modifiedVals), repr(self.modifiedVals)
+ "%s component settings changed at once. (%s)",
+ len(self.modifiedVals),
+ repr(self.modifiedVals),
)
def id(self):
- '''If 2 consecutive updates have same id, Qt will call mergeWith()'''
+ """If 2 consecutive updates have same id, Qt will call mergeWith()"""
return self.id_
def mergeWith(self, other):
@@ -890,20 +930,23 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
return True
def setWidgetValues(self, attrDict):
- '''
- Mask the component's usual method to handle our
- relative widgets in case the resolution has changed.
- '''
+ """
+ Mask the component's usual method to handle our
+ relative widgets in case the resolution has changed.
+ """
newAttrDict = {
- attr: val if attr not in self.parent._relativeWidgets
- else self.parent.pixelValForAttr(attr, val)
+ attr: (
+ val
+ if attr not in self.parent._relativeWidgets
+ else self.parent.pixelValForAttr(attr, val)
+ )
for attr, val in attrDict.items()
}
self.parent.setWidgetValues(newAttrDict)
def redo(self):
if self.undone:
- log.info('Redoing component update')
+ log.info("Redoing component update")
self.parent.oldAttrs = self.relativeWidgetValsAfterUndo
self.setWidgetValues(self.modifiedVals)
self.parent.update(auto=True)
@@ -916,7 +959,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
self.parent._sendUpdateSignal()
def undo(self):
- log.info('Undoing component update')
+ log.info("Undoing component update")
self.undone = True
self.parent.oldAttrs = self.relativeWidgetValsAfterRedo
self.setWidgetValues(self.oldWidgetVals)
diff --git a/src/components/color.py b/src/components/color.py
index 8d0edd2..1f32c23 100644
--- a/src/components/color.py
+++ b/src/components/color.py
@@ -1,16 +1,16 @@
-from PyQt5 import QtGui
+from PyQt6 import QtGui
import logging
from ..component import Component
from ..toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor
-log = logging.getLogger('AVP.Components.Color')
+log = logging.getLogger("AVP.Components.Color")
class Component(Component):
- name = 'Color'
- version = '1.0.0'
+ name = "Color"
+ version = "1.0.0"
def widget(self, *args):
self.x = 0
@@ -20,48 +20,56 @@ class Component(Component):
# disable color #2 until non-default 'fill' option gets changed
self.page.lineEdit_color2.setDisabled(True)
self.page.pushButton_color2.setDisabled(True)
- self.page.spinBox_width.setValue(
- int(self.settings.value("outputWidth")))
- self.page.spinBox_height.setValue(
- int(self.settings.value("outputHeight")))
+ self.page.spinBox_width.setValue(int(self.settings.value("outputWidth")))
+ self.page.spinBox_height.setValue(int(self.settings.value("outputHeight")))
self.fillLabels = [
- 'Solid',
- 'Linear Gradient',
- 'Radial Gradient',
+ "Solid",
+ "Linear Gradient",
+ "Radial Gradient",
]
for label in self.fillLabels:
self.page.comboBox_fill.addItem(label)
self.page.comboBox_fill.setCurrentIndex(0)
- self.trackWidgets({
- 'x': self.page.spinBox_x,
- 'y': self.page.spinBox_y,
- 'sizeWidth': self.page.spinBox_width,
- 'sizeHeight': self.page.spinBox_height,
- 'trans': self.page.checkBox_trans,
- 'spread': self.page.comboBox_spread,
- 'stretch': self.page.checkBox_stretch,
- 'RG_start': self.page.spinBox_radialGradient_start,
- 'LG_start': self.page.spinBox_linearGradient_start,
- 'RG_end': self.page.spinBox_radialGradient_end,
- 'LG_end': self.page.spinBox_linearGradient_end,
- 'RG_centre': self.page.spinBox_radialGradient_spread,
- 'fillType': self.page.comboBox_fill,
- 'color1': self.page.lineEdit_color1,
- 'color2': self.page.lineEdit_color2,
- }, presetNames={
- 'sizeWidth': 'width',
- 'sizeHeight': 'height',
- }, colorWidgets={
- 'color1': self.page.pushButton_color1,
- 'color2': self.page.pushButton_color2,
- }, relativeWidgets=[
- 'x', 'y',
- 'sizeWidth', 'sizeHeight',
- 'LG_start', 'LG_end',
- 'RG_start', 'RG_end', 'RG_centre',
- ])
+ self.trackWidgets(
+ {
+ "x": self.page.spinBox_x,
+ "y": self.page.spinBox_y,
+ "sizeWidth": self.page.spinBox_width,
+ "sizeHeight": self.page.spinBox_height,
+ "trans": self.page.checkBox_trans,
+ "spread": self.page.comboBox_spread,
+ "stretch": self.page.checkBox_stretch,
+ "RG_start": self.page.spinBox_radialGradient_start,
+ "LG_start": self.page.spinBox_linearGradient_start,
+ "RG_end": self.page.spinBox_radialGradient_end,
+ "LG_end": self.page.spinBox_linearGradient_end,
+ "RG_centre": self.page.spinBox_radialGradient_spread,
+ "fillType": self.page.comboBox_fill,
+ "color1": self.page.lineEdit_color1,
+ "color2": self.page.lineEdit_color2,
+ },
+ presetNames={
+ "sizeWidth": "width",
+ "sizeHeight": "height",
+ },
+ colorWidgets={
+ "color1": self.page.pushButton_color1,
+ "color2": self.page.pushButton_color2,
+ },
+ relativeWidgets=[
+ "x",
+ "y",
+ "sizeWidth",
+ "sizeHeight",
+ "LG_start",
+ "LG_end",
+ "RG_start",
+ "RG_end",
+ "RG_centre",
+ ],
+ )
def update(self):
fillType = self.page.comboBox_fill.currentIndex()
@@ -86,7 +94,7 @@ class Component(Component):
return self.drawFrame(self.width, self.height)
def properties(self):
- return ['static']
+ return ["static"]
def frameRender(self, frameNo):
log.debug("Color component is drawing frame #%s", frameNo)
@@ -96,8 +104,12 @@ class Component(Component):
r, g, b = self.color1
shapeSize = (self.sizeWidth, self.sizeHeight)
# in default state, skip all this logic and return a plain fill
- if self.fillType == 0 and shapeSize == (width, height) \
- and self.x == 0 and self.y == 0:
+ if (
+ self.fillType == 0
+ and shapeSize == (width, height)
+ and self.x == 0
+ and self.y == 0
+ ):
return FloodFrame(width, height, (r, g, b, 255))
# Return a solid image at x, y
@@ -120,19 +132,26 @@ class Component(Component):
if self.fillType == 1: # Linear Gradient
brush = QtGui.QLinearGradient(
- self.LG_start,
- self.LG_start,
- self.LG_end+width/3,
- self.LG_end)
+ float(self.LG_start),
+ float(self.LG_start),
+ float(self.LG_end + width / 3),
+ float(self.LG_end),
+ )
elif self.fillType == 2: # Radial Gradient
brush = QtGui.QRadialGradient(
- self.RG_start,
- self.RG_end,
- w, h,
- self.RG_centre)
-
- brush.setSpread(self.spread)
+ float(self.RG_start),
+ float(self.RG_end),
+ float(w),
+ float(h),
+ float(self.RG_centre),
+ )
+ spread = QtGui.QGradient.Spread.PadSpread
+ if self.spread == 1:
+ spread = QtGui.QGradient.Spread.ReflectSpread
+ elif self.spread == 2:
+ spread = QtGui.QGradient.Spread.RepeatSpread
+ brush.setSpread(spread)
brush.setColorAt(0.0, PaintColor(*self.color1))
if self.trans:
brush.setColorAt(1.0, PaintColor(0, 0, 0, 0))
@@ -141,20 +160,17 @@ class Component(Component):
else:
brush.setColorAt(1.0, PaintColor(*self.color2))
image.setBrush(brush)
- image.drawRect(
- self.x, self.y,
- self.sizeWidth, self.sizeHeight
- )
+ image.drawRect(self.x, self.y, self.sizeWidth, self.sizeHeight)
return image.finalize()
def commandHelp(self):
- print('Specify a color:\n color=255,255,255')
+ print("Specify a color:\n color=255,255,255")
def command(self, arg):
- if '=' in arg:
- key, arg = arg.split('=', 1)
- if key == 'color':
+ if "=" in arg:
+ key, arg = arg.split("=", 1)
+ if key == "color":
self.page.lineEdit_color1.setText(arg)
return
super().command(arg)
diff --git a/src/components/image.py b/src/components/image.py
index 42f9564..2393611 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -1,5 +1,5 @@
from PIL import Image, ImageDraw, ImageEnhance
-from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt6 import QtGui, QtCore, QtWidgets
import os
from ..component import Component
@@ -7,37 +7,39 @@ from ..toolkit.frame import BlankFrame
class Component(Component):
- name = 'Image'
- version = '1.0.1'
+ name = "Image"
+ version = "1.0.1"
def widget(self, *args):
super().widget(*args)
self.page.pushButton_image.clicked.connect(self.pickImage)
- self.trackWidgets({
- 'imagePath': self.page.lineEdit_image,
- 'scale': self.page.spinBox_scale,
- 'stretchScale': self.page.spinBox_scale_stretch,
- 'rotate': self.page.spinBox_rotate,
- 'color': self.page.spinBox_color,
- 'xPosition': self.page.spinBox_x,
- 'yPosition': self.page.spinBox_y,
- 'stretched': self.page.checkBox_stretch,
- 'mirror': self.page.checkBox_mirror,
- }, presetNames={
- 'imagePath': 'image',
- 'xPosition': 'x',
- 'yPosition': 'y',
- }, relativeWidgets=[
- 'xPosition', 'yPosition', 'scale'
- ])
+ self.trackWidgets(
+ {
+ "imagePath": self.page.lineEdit_image,
+ "scale": self.page.spinBox_scale,
+ "stretchScale": self.page.spinBox_scale_stretch,
+ "rotate": self.page.spinBox_rotate,
+ "color": self.page.spinBox_color,
+ "xPosition": self.page.spinBox_x,
+ "yPosition": self.page.spinBox_y,
+ "stretched": self.page.checkBox_stretch,
+ "mirror": self.page.checkBox_mirror,
+ },
+ presetNames={
+ "imagePath": "image",
+ "xPosition": "x",
+ "yPosition": "y",
+ },
+ relativeWidgets=["xPosition", "yPosition", "scale"],
+ )
def previewRender(self):
return self.drawFrame(self.width, self.height)
def properties(self):
- props = ['static']
+ props = ["static"]
if not os.path.exists(self.imagePath):
- props.append('error')
+ props.append("error")
return props
def error(self):
@@ -57,17 +59,15 @@ class Component(Component):
# Modify image's appearance
if self.color != 100:
- image = ImageEnhance.Color(image).enhance(
- float(self.color / 100)
- )
+ image = ImageEnhance.Color(image).enhance(float(self.color / 100))
if self.mirror:
- image = image.transpose(Image.FLIP_LEFT_RIGHT)
+ image = image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
if self.stretched and image.size != (width, height):
- image = image.resize((width, height), Image.ANTIALIAS)
+ image = image.resize((width, height), Image.Resampling.LANCZOS)
if scale != 100:
newHeight = int((image.height / 100) * scale)
newWidth = int((image.width / 100) * scale)
- image = image.resize((newWidth, newHeight), Image.ANTIALIAS)
+ image = image.resize((newWidth, newHeight), Image.Resampling.LANCZOS)
# Paste image at correct position
frame.paste(image, box=(self.xPosition, self.yPosition))
@@ -79,8 +79,11 @@ class Component(Component):
def pickImage(self):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
- self.page, "Choose Image", imgDir,
- "Image Files (%s)" % " ".join(self.core.imageFormats))
+ self.page,
+ "Choose Image",
+ imgDir,
+ "Image Files (%s)" % " ".join(self.core.imageFormats),
+ )
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.mergeUndo = False
@@ -88,9 +91,9 @@ class Component(Component):
self.mergeUndo = True
def command(self, arg):
- if '=' in arg:
- key, arg = arg.split('=', 1)
- if key == 'path' and os.path.exists(arg):
+ if "=" in arg:
+ key, arg = arg.split("=", 1)
+ if key == "path" and os.path.exists(arg):
try:
Image.open(arg)
self.page.lineEdit_image.setText(arg)
@@ -102,7 +105,7 @@ class Component(Component):
super().command(arg)
def commandHelp(self):
- print('Load an image:\n path=/filepath/to/image.png')
+ print("Load an image:\n path=/filepath/to/image.png")
def savePreset(self):
# Maintain the illusion that the scale spinbox is one widget
diff --git a/src/components/life.py b/src/components/life.py
index e19ed36..b3c2c58 100644
--- a/src/components/life.py
+++ b/src/components/life.py
@@ -1,16 +1,21 @@
-from PyQt5 import QtGui, QtCore, QtWidgets
-from PyQt5.QtWidgets import QUndoCommand
+from PyQt6 import QtGui, QtCore, QtWidgets
+from PyQt6.QtGui import QUndoCommand
from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter
import os
import math
+import logging
+
from ..component import Component
from ..toolkit.frame import BlankFrame, scale
+log = logging.getLogger("AVP.Component.Life")
+
+
class Component(Component):
- name = 'Conway\'s Game of Life'
- version = '1.0.0'
+ name = "Conway's Game of Life"
+ version = "1.0.0"
def widget(self, *args):
super().widget(*args)
@@ -18,34 +23,50 @@ class Component(Component):
self.updateGridSize()
# The initial grid: a "Queen Bee Shuttle"
# https://conwaylife.com/wiki/Queen_bee_shuttle
- self.startingGrid = set([
- (3, 7), (3, 8),
- (4, 7), (4, 8),
- (8, 7),
- (9, 6), (9, 8),
- (10, 5), (10, 9),
- (11, 6), (11, 7), (11, 8),
- (12, 4), (12, 5), (12, 9), (12, 10),
- (23, 6), (23, 7),
- (24, 6), (24, 7)
- ])
+ self.startingGrid = set(
+ [
+ (3, 7),
+ (3, 8),
+ (4, 7),
+ (4, 8),
+ (8, 7),
+ (9, 6),
+ (9, 8),
+ (10, 5),
+ (10, 9),
+ (11, 6),
+ (11, 7),
+ (11, 8),
+ (12, 4),
+ (12, 5),
+ (12, 9),
+ (12, 10),
+ (23, 6),
+ (23, 7),
+ (24, 6),
+ (24, 7),
+ ]
+ )
# Amount of 'bleed' (off-canvas coordinates) on each side of the grid
self.bleedSize = 40
self.page.pushButton_pickImage.clicked.connect(self.pickImage)
- self.trackWidgets({
- 'tickRate': self.page.spinBox_tickRate,
- 'scale': self.page.spinBox_scale,
- 'color': self.page.lineEdit_color,
- 'shapeType': self.page.comboBox_shapeType,
- 'shadow': self.page.checkBox_shadow,
- 'customImg': self.page.checkBox_customImg,
- 'showGrid': self.page.checkBox_showGrid,
- 'image': self.page.lineEdit_image,
- }, colorWidgets={
- 'color': self.page.pushButton_color,
- })
+ self.trackWidgets(
+ {
+ "tickRate": self.page.spinBox_tickRate,
+ "scale": self.page.spinBox_scale,
+ "color": self.page.lineEdit_color,
+ "shapeType": self.page.comboBox_shapeType,
+ "shadow": self.page.checkBox_shadow,
+ "customImg": self.page.checkBox_customImg,
+ "showGrid": self.page.checkBox_showGrid,
+ "image": self.page.lineEdit_image,
+ },
+ colorWidgets={
+ "color": self.page.pushButton_color,
+ },
+ )
self.shiftButtons = (
self.page.toolButton_up,
self.page.toolButton_down,
@@ -56,7 +77,9 @@ class Component(Component):
def shiftFunc(i):
def shift():
self.shiftGrid(i)
+
return shift
+
shiftFuncs = [shiftFunc(i) for i in range(len(self.shiftButtons))]
for i, widget in enumerate(self.shiftButtons):
widget.clicked.connect(shiftFuncs[i])
@@ -66,8 +89,11 @@ class Component(Component):
def pickImage(self):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
- self.page, "Choose Image", imgDir,
- "Image Files (%s)" % " ".join(self.core.imageFormats))
+ self.page,
+ "Choose Image",
+ imgDir,
+ "Image Files (%s)" % " ".join(self.core.imageFormats),
+ )
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.mergeUndo = False
@@ -98,20 +124,20 @@ class Component(Component):
self.page.label_image.setVisible(False)
self.page.lineEdit_image.setVisible(False)
self.page.pushButton_pickImage.setVisible(False)
- enabled = (len(self.startingGrid) > 0)
+ enabled = len(self.startingGrid) > 0
for widget in self.shiftButtons:
widget.setEnabled(enabled)
def previewClickEvent(self, pos, size, button):
pos = (
math.ceil((pos[0] / size[0]) * self.gridWidth) - 1,
- math.ceil((pos[1] / size[1]) * self.gridHeight) - 1
+ math.ceil((pos[1] / size[1]) * self.gridHeight) - 1,
)
action = ClickGrid(self, pos, button)
self.parent.undoStack.push(action)
def updateGridSize(self):
- w, h = self.core.resolutions[-1].split('x')
+ w, h = self.core.resolutions[-1].split("x")
self.gridWidth = int(int(w) / self.scale)
self.gridHeight = int(int(h) / self.scale)
self.pxWidth = math.ceil(self.width / self.gridWidth)
@@ -125,10 +151,8 @@ class Component(Component):
self.tickGrids = {0: self.startingGrid}
def properties(self):
- if self.customImg and (
- not self.image or not os.path.exists(self.image)
- ):
- return ['error']
+ if self.customImg and (not self.image or not os.path.exists(self.image)):
+ return ["error"]
return []
def error(self):
@@ -162,42 +186,47 @@ class Component(Component):
drawer = ImageDraw.Draw(frame)
rect = (
(drawPtX, drawPtY),
- (drawPtX + self.pxWidth, drawPtY + self.pxHeight)
+ (drawPtX + self.pxWidth, drawPtY + self.pxHeight),
)
shape = self.page.comboBox_shapeType.currentText().lower()
# Rectangle
- if shape == 'rectangle':
+ if shape == "rectangle":
drawer.rectangle(rect, fill=self.color)
# Elliptical
- elif shape == 'elliptical':
+ elif shape == "elliptical":
drawer.ellipse(rect, fill=self.color)
tenthX, tenthY = scale(10, self.pxWidth, self.pxHeight, int)
smallerShape = (
- (drawPtX + tenthX + int(tenthX / 4),
- drawPtY + tenthY + int(tenthY / 2)),
- (drawPtX + self.pxWidth - tenthX - int(tenthX / 4),
- drawPtY + self.pxHeight - (tenthY + int(tenthY / 2)))
+ (
+ drawPtX + tenthX + int(tenthX / 4),
+ drawPtY + tenthY + int(tenthY / 2),
+ ),
+ (
+ drawPtX + self.pxWidth - tenthX - int(tenthX / 4),
+ drawPtY + self.pxHeight - (tenthY + int(tenthY / 2)),
+ ),
)
outlineShape = (
- (drawPtX + int(tenthX / 4),
- drawPtY + int(tenthY / 2)),
- (drawPtX + self.pxWidth - int(tenthX / 4),
- drawPtY + self.pxHeight - int(tenthY / 2))
+ (drawPtX + int(tenthX / 4), drawPtY + int(tenthY / 2)),
+ (
+ drawPtX + self.pxWidth - int(tenthX / 4),
+ drawPtY + self.pxHeight - int(tenthY / 2),
+ ),
)
# Circle
- if shape == 'circle':
+ if shape == "circle":
drawer.ellipse(outlineShape, fill=self.color)
drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
# Lilypad
- elif shape == 'lilypad':
+ elif shape == "lilypad":
drawer.pieslice(smallerShape, 290, 250, fill=self.color)
# Pie
- elif shape == 'pie':
+ elif shape == "pie":
drawer.pieslice(outlineShape, 35, 320, fill=self.color)
hX, hY = scale(50, self.pxWidth, self.pxHeight, int) # halfline
@@ -205,12 +234,15 @@ class Component(Component):
qX, qY = scale(20, self.pxWidth, self.pxHeight, int) # quarterline
# Path
- if shape == 'path':
+ if shape == "path":
drawer.ellipse(rect, fill=self.color)
rects = {
direction: False
for direction in (
- 'up', 'down', 'left', 'right',
+ "up",
+ "down",
+ "left",
+ "right",
)
}
for cell in self.nearbyCoords(x, y):
@@ -218,60 +250,59 @@ class Component(Component):
continue
if cell[0] == x:
if cell[1] < y:
- rects['up'] = True
+ rects["up"] = True
if cell[1] > y:
- rects['down'] = True
+ rects["down"] = True
if cell[1] == y:
if cell[0] < x:
- rects['left'] = True
+ rects["left"] = True
if cell[0] > x:
- rects['right'] = True
+ rects["right"] = True
for direction, rect in rects.items():
if rect:
- if direction == 'up':
+ if direction == "up":
sect = (
(drawPtX, drawPtY),
- (drawPtX + self.pxWidth, drawPtY + hY)
+ (drawPtX + self.pxWidth, drawPtY + hY),
)
- elif direction == 'down':
+ elif direction == "down":
sect = (
(drawPtX, drawPtY + hY),
- (drawPtX + self.pxWidth,
- drawPtY + self.pxHeight)
+ (
+ drawPtX + self.pxWidth,
+ drawPtY + self.pxHeight,
+ ),
)
- elif direction == 'left':
+ elif direction == "left":
sect = (
(drawPtX, drawPtY),
- (drawPtX + hX,
- drawPtY + self.pxHeight)
+ (drawPtX + hX, drawPtY + self.pxHeight),
)
- elif direction == 'right':
+ elif direction == "right":
sect = (
(drawPtX + hX, drawPtY),
- (drawPtX + self.pxWidth,
- drawPtY + self.pxHeight)
+ (
+ drawPtX + self.pxWidth,
+ drawPtY + self.pxHeight,
+ ),
)
drawer.rectangle(sect, fill=self.color)
# Duck
- elif shape == 'duck':
+ elif shape == "duck":
duckHead = (
(drawPtX + qX, drawPtY + qY),
- (drawPtX + int(qX * 3), drawPtY + int(tY * 2))
+ (drawPtX + int(qX * 3), drawPtY + int(tY * 2)),
)
duckBeak = (
(drawPtX + hX, drawPtY + qY),
- (drawPtX + self.pxWidth + qX,
- drawPtY + int(qY * 3))
- )
- duckWing = (
- (drawPtX, drawPtY + hY),
- rect[1]
+ (drawPtX + self.pxWidth + qX, drawPtY + int(qY * 3)),
)
+ duckWing = ((drawPtX, drawPtY + hY), rect[1])
duckBody = (
(drawPtX + int(qX / 4), drawPtY + int(qY * 3)),
- (drawPtX + int(tX * 2), drawPtY + self.pxHeight)
+ (drawPtX + int(tX * 2), drawPtY + self.pxHeight),
)
drawer.ellipse(duckBody, fill=self.color)
drawer.ellipse(duckHead, fill=self.color)
@@ -279,11 +310,16 @@ class Component(Component):
drawer.pieslice(duckBeak, 145, 200, fill=self.color)
# Peace
- elif shape == 'peace':
- line = ((
- drawPtX + hX - int(tenthX / 2), drawPtY + int(tenthY / 2)),
- (drawPtX + hX + int(tenthX / 2),
- drawPtY + self.pxHeight - int(tenthY / 2))
+ elif shape == "peace":
+ line = (
+ (
+ drawPtX + hX - int(tenthX / 2),
+ drawPtY + int(tenthY / 2),
+ ),
+ (
+ drawPtX + hX + int(tenthX / 2),
+ drawPtY + self.pxHeight - int(tenthY / 2),
+ ),
)
drawer.ellipse(outlineShape, fill=self.color)
drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
@@ -291,21 +327,15 @@ class Component(Component):
def slantLine(difference):
return (
- (drawPtX + difference), (drawPtY + self.pxHeight - qY)
+ (drawPtX + difference),
+ (drawPtY + self.pxHeight - qY),
), (
- (drawPtX + hX), (drawPtY + hY)
+ (drawPtX + hX),
+ (drawPtY + hY),
)
- drawer.line(
- slantLine(qX),
- fill=self.color,
- width=tenthX
- )
- drawer.line(
- slantLine(self.pxWidth - qX),
- fill=self.color,
- width=tenthX
- )
+ drawer.line(slantLine(qX), fill=self.color, width=tenthX)
+ drawer.line(slantLine(self.pxWidth - qX), fill=self.color, width=tenthX)
for x, y in grid:
drawPtX = x * self.pxWidth
@@ -331,44 +361,38 @@ class Component(Component):
w, h = scale(0.05, self.width, self.height, int)
for x in range(self.pxWidth, self.width, self.pxWidth):
drawer.rectangle(
- ((x, 0),
- (x + w, self.height)),
+ ((x, 0), (x + w, self.height)),
fill=self.color,
)
for y in range(self.pxHeight, self.height, self.pxHeight):
drawer.rectangle(
- ((0, y),
- (self.width, y + h)),
+ ((0, y), (self.width, y + h)),
fill=self.color,
)
return frame
def gridForTick(self, tick):
- '''
+ """
Given a tick number over 0, returns a new grid (a set of tuples).
This must compute the previous ticks' grids if not already computed
- '''
+ """
if tick - 1 not in self.tickGrids:
self.tickGrids[tick - 1] = self.gridForTick(tick - 1)
-
+
lastGrid = self.tickGrids[tick - 1]
def neighbours(x, y):
- return {
- cell for cell in self.nearbyCoords(x, y)
- if cell in lastGrid
- }
+ return {cell for cell in self.nearbyCoords(x, y) if cell in lastGrid}
newGrid = set()
# Copy cells from the previous grid if they have 2 or 3 neighbouring cells
# and if they are within the grid or its bleed area (off-canvas area)
for x, y in lastGrid:
if (
- -self.bleedSize > x > self.gridWidth + self.bleedSize
- or
- -self.bleedSize > y > self.gridHeight + self.bleedSize
- ):
+ -self.bleedSize > x > self.gridWidth + self.bleedSize
+ or -self.bleedSize > y > self.gridHeight + self.bleedSize
+ ):
continue
surrounding = len(neighbours(x, y))
if surrounding == 2 or surrounding == 3:
@@ -376,7 +400,8 @@ class Component(Component):
# Find positions around living cells which must be checked for reproduction
potentialNewCells = {
- coordTup for origin in lastGrid
+ coordTup
+ for origin in lastGrid
for coordTup in list(self.nearbyCoords(*origin))
}
# Check for reproduction
@@ -392,11 +417,11 @@ class Component(Component):
def savePreset(self):
pr = super().savePreset()
- pr['GRID'] = sorted(self.startingGrid)
+ pr["GRID"] = sorted(self.startingGrid)
return pr
def loadPreset(self, pr, *args):
- self.startingGrid = set(pr['GRID'])
+ self.startingGrid = set(pr["GRID"])
if self.startingGrid:
for widget in self.shiftButtons:
widget.setEnabled(True)
@@ -414,15 +439,17 @@ class Component(Component):
class ClickGrid(QUndoCommand):
- def __init__(self, comp, pos, id_):
- super().__init__(
- "click %s component #%s" % (comp.name, comp.compPos))
+ def __init__(self, comp, pos, button):
+ super().__init__("click %s component #%s" % (comp.name, comp.compPos))
self.comp = comp
self.pos = [pos]
- self.id_ = id_
+ if button == QtCore.Qt.MouseButton.RightButton:
+ self.button = 2
+ else:
+ self.button = 1
def id(self):
- return self.id_
+ return self.button
def mergeWith(self, other):
self.pos.extend(other.pos)
@@ -439,21 +466,21 @@ class ClickGrid(QUndoCommand):
self.comp.update(auto=True)
def redo(self):
- if self.id_ == 1: # Left-click
+ if self.button == 1: # Left-click
self.add()
- elif self.id_ == 2: # Right-click
+ elif self.button == 2: # Right-click
self.remove()
def undo(self):
- if self.id_ == 1: # Left-click
+ if self.button == 1: # Left-click
self.remove()
- elif self.id_ == 2: # Right-click
+ elif self.button == 2: # Right-click
self.add()
+
class ShiftGrid(QUndoCommand):
def __init__(self, comp, direction):
- super().__init__(
- "change %s component #%s" % (comp.name, comp.compPos))
+ super().__init__("change %s component #%s" % (comp.name, comp.compPos))
self.comp = comp
self.direction = direction
self.distance = 1
@@ -466,10 +493,7 @@ class ShiftGrid(QUndoCommand):
return True
def newGrid(self, Xchange, Ychange):
- return {
- (x + Xchange, y + Ychange)
- for x, y in self.comp.startingGrid
- }
+ return {(x + Xchange, y + Ychange) for x, y in self.comp.startingGrid}
def redo(self):
if self.direction == 0:
diff --git a/src/components/life.ui b/src/components/life.ui
index 3d6ad5a..30cf9d0 100644
--- a/src/components/life.ui
+++ b/src/components/life.ui
@@ -372,7 +372,7 @@ p, li { white-space: pre-wrap; }
&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;- A cell with more than 3 neighbours will die from overpopulation.&lt;/p&gt;
&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;- An empty space surrounded by 3 live cells will cause reproduction.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
- <property name="tabStopWidth">
+ <property name="tabStopDistance">
<number>80</number>
</property>
<property name="textInteractionFlags">
diff --git a/src/components/original.py b/src/components/original.py
index 289e982..fad797b 100644
--- a/src/components/original.py
+++ b/src/components/original.py
@@ -1,9 +1,5 @@
import numpy
from PIL import Image, ImageDraw
-from PyQt5 import QtGui, QtCore, QtWidgets
-from PyQt5.QtGui import QColor
-import os
-import time
from copy import copy
from ..component import Component
@@ -11,14 +7,14 @@ from ..toolkit.frame import BlankFrame
class Component(Component):
- name = 'Classic Visualizer'
- version = '1.0.0'
+ name = "Classic Visualizer"
+ version = "1.0.0"
def names(*args):
- return ['Original Audio Visualization']
+ return ["Original Audio Visualization"]
def properties(self):
- return ['pcm']
+ return ["pcm"]
def widget(self, *args):
self.scale = 20
@@ -31,23 +27,30 @@ class Component(Component):
self.page.comboBox_visLayout.addItem("Top")
self.page.comboBox_visLayout.setCurrentIndex(0)
- self.page.lineEdit_visColor.setText('255,255,255')
-
- self.trackWidgets({
- 'visColor': self.page.lineEdit_visColor,
- 'layout': self.page.comboBox_visLayout,
- 'scale': self.page.spinBox_scale,
- 'y': self.page.spinBox_y,
- 'smooth': self.page.spinBox_smooth,
- }, colorWidgets={
- 'visColor': self.page.pushButton_visColor,
- }, relativeWidgets=[
- 'y',
- ])
+ self.page.lineEdit_visColor.setText("255,255,255")
+
+ self.trackWidgets(
+ {
+ "visColor": self.page.lineEdit_visColor,
+ "layout": self.page.comboBox_visLayout,
+ "scale": self.page.spinBox_scale,
+ "y": self.page.spinBox_y,
+ "smooth": self.page.spinBox_smooth,
+ },
+ colorWidgets={
+ "visColor": self.page.pushButton_visColor,
+ },
+ relativeWidgets=[
+ "y",
+ ],
+ )
def previewRender(self):
spectrum = numpy.fromfunction(
- lambda x: float(self.scale)/2500*(x-128)**2, (255,), dtype="int16")
+ lambda x: float(self.scale) / 2500 * (x - 128) ** 2,
+ (255,),
+ dtype="int16",
+ )
return self.drawBars(
self.width, self.height, spectrum, self.visColor, self.layout
)
@@ -63,41 +66,53 @@ class Component(Component):
if self.canceled:
break
self.lastSpectrum = self.transformData(
- i, self.completeAudioArray, self.sampleSize,
- self.smoothConstantDown, self.smoothConstantUp,
- self.lastSpectrum)
+ i,
+ self.completeAudioArray,
+ self.sampleSize,
+ self.smoothConstantDown,
+ self.smoothConstantUp,
+ self.lastSpectrum,
+ )
self.spectrumArray[i] = copy(self.lastSpectrum)
- progress = int(100*(i/len(self.completeAudioArray)))
+ progress = int(100 * (i / len(self.completeAudioArray)))
if progress >= 100:
progress = 100
- pStr = "Analyzing audio: "+str(progress)+'%'
+ pStr = "Analyzing audio: " + str(progress) + "%"
self.progressBarSetText.emit(pStr)
self.progressBarUpdate.emit(int(progress))
def frameRender(self, frameNo):
arrayNo = frameNo * self.sampleSize
return self.drawBars(
- self.width, self.height,
+ self.width,
+ self.height,
self.spectrumArray[arrayNo],
- self.visColor, self.layout)
+ self.visColor,
+ self.layout,
+ )
def transformData(
- self, i, completeAudioArray, sampleSize,
- smoothConstantDown, smoothConstantUp, lastSpectrum):
+ self,
+ i,
+ completeAudioArray,
+ sampleSize,
+ smoothConstantDown,
+ smoothConstantUp,
+ lastSpectrum,
+ ):
if len(completeAudioArray) < (i + sampleSize):
sampleSize = len(completeAudioArray) - i
window = numpy.hanning(sampleSize)
- data = completeAudioArray[i:i+sampleSize][::1] * window
+ data = completeAudioArray[i : i + sampleSize][::1] * window
paddedSampleSize = 2048
- paddedData = numpy.pad(
- data, (0, paddedSampleSize - sampleSize), 'constant')
+ paddedData = numpy.pad(data, (0, paddedSampleSize - sampleSize), "constant")
spectrum = numpy.fft.fft(paddedData)
sample_rate = 44100
- frequencies = numpy.fft.fftfreq(len(spectrum), 1./sample_rate)
+ frequencies = numpy.fft.fftfreq(len(spectrum), 1.0 / sample_rate)
- y = abs(spectrum[0:int(paddedSampleSize/2) - 1])
+ y = abs(spectrum[0 : int(paddedSampleSize / 2) - 1])
# filter the noise away
# y[y<80] = 0
@@ -106,22 +121,26 @@ class Component(Component):
y[numpy.isinf(y)] = 0
if lastSpectrum is not None:
- lastSpectrum[y < lastSpectrum] = \
- y[y < lastSpectrum] * smoothConstantDown + \
- lastSpectrum[y < lastSpectrum] * (1 - smoothConstantDown)
-
- lastSpectrum[y >= lastSpectrum] = \
- y[y >= lastSpectrum] * smoothConstantUp + \
- lastSpectrum[y >= lastSpectrum] * (1 - smoothConstantUp)
+ lastSpectrum[y < lastSpectrum] = y[
+ y < lastSpectrum
+ ] * smoothConstantDown + lastSpectrum[y < lastSpectrum] * (
+ 1 - smoothConstantDown
+ )
+
+ lastSpectrum[y >= lastSpectrum] = y[
+ y >= lastSpectrum
+ ] * smoothConstantUp + lastSpectrum[y >= lastSpectrum] * (
+ 1 - smoothConstantUp
+ )
else:
lastSpectrum = y
- x = frequencies[0:int(paddedSampleSize/2) - 1]
+ x = frequencies[0 : int(paddedSampleSize / 2) - 1]
return lastSpectrum
def drawBars(self, width, height, spectrum, color, layout):
- vH = height-height/8
+ vH = height - height / 8
bF = width / 64
bH = bF / 2
bQ = bF / 4
@@ -133,72 +152,92 @@ class Component(Component):
bP = height / 1200
for j in range(0, 63):
- draw.rectangle((
- bH + j * bF, vH+bQ, bH + j * bF + bF, vH + bQ -
- spectrum[j * 4] * bP - bH), fill=color2)
-
- draw.rectangle((
- bH + bQ + j * bF, vH, bH + bQ + j * bF + bH, vH -
- spectrum[j * 4] * bP), fill=color)
-
- imBottom = imTop.transpose(Image.FLIP_TOP_BOTTOM)
+ x0 = bH + j * bF
+ y0 = vH + bQ
+ y1 = vH + bQ - spectrum[j * 4] * bP - bH
+ x1 = bH + j * bF + bF
+ draw.rectangle(
+ (
+ x0,
+ y0 if y0 < y1 else y1,
+ x1 if x1 > x0 else x0,
+ y1 if y0 < y1 else y0,
+ ),
+ fill=color2,
+ )
+
+ x0 = bH + bQ + j * bF
+ y0 = vH
+ x1 = bH + bQ + j * bF + bH
+ y1 = vH - spectrum[j * 4] * bP
+ draw.rectangle(
+ (
+ x0,
+ y0 if y0 < y1 else y1,
+ x1 if x1 > x0 else x0,
+ y1 if y0 < y1 else y0,
+ ),
+ fill=color,
+ )
+
+ imBottom = imTop.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
im = BlankFrame(width, height)
if layout == 0: # Classic
- y = self.y - int(height/100*43)
+ y = self.y - int(height / 100 * 43)
im.paste(imTop, (0, y), mask=imTop)
- y = self.y + int(height/100*43)
+ y = self.y + int(height / 100 * 43)
im.paste(imBottom, (0, y), mask=imBottom)
if layout == 1: # Split
- y = self.y + int(height/100*10)
+ y = self.y + int(height / 100 * 10)
im.paste(imTop, (0, y), mask=imTop)
- y = self.y - int(height/100*10)
+ y = self.y - int(height / 100 * 10)
im.paste(imBottom, (0, y), mask=imBottom)
if layout == 2: # Bottom
- y = self.y + int(height/100*10)
+ y = self.y + int(height / 100 * 10)
im.paste(imTop, (0, y), mask=imTop)
if layout == 3: # Top
- y = self.y - int(height/100*10)
+ y = self.y - int(height / 100 * 10)
im.paste(imBottom, (0, y), mask=imBottom)
return im
def command(self, arg):
- if '=' in arg:
- key, arg = arg.split('=', 1)
+ if "=" in arg:
+ key, arg = arg.split("=", 1)
try:
- if key == 'color':
+ if key == "color":
self.page.lineEdit_visColor.setText(arg)
return
- elif key == 'layout':
- if arg == 'classic':
+ elif key == "layout":
+ if arg == "classic":
self.page.comboBox_visLayout.setCurrentIndex(0)
- elif arg == 'split':
+ elif arg == "split":
self.page.comboBox_visLayout.setCurrentIndex(1)
- elif arg == 'bottom':
+ elif arg == "bottom":
self.page.comboBox_visLayout.setCurrentIndex(2)
- elif arg == 'top':
+ elif arg == "top":
self.page.comboBox_visLayout.setCurrentIndex(3)
return
- elif key == 'scale':
+ elif key == "scale":
arg = int(arg)
self.page.spinBox_scale.setValue(arg)
return
- elif key == 'y':
+ elif key == "y":
arg = int(arg)
self.page.spinBox_y.setValue(arg)
return
except ValueError:
- print('You must enter a number.')
+ print("You must enter a number.")
quit(1)
super().command(arg)
def commandHelp(self):
- print('Give a layout name:\n layout=[classic/split/bottom/top]')
- print('Specify a color:\n color=255,255,255')
- print('Visualizer scale (20 is default):\n scale=number')
- print('Y position:\n y=number')
+ print("Give a layout name:\n layout=[classic/split/bottom/top]")
+ print("Specify a color:\n color=255,255,255")
+ print("Visualizer scale (20 is default):\n scale=number")
+ print("Y position:\n y=number")
diff --git a/src/components/sound.py b/src/components/sound.py
index 118ea23..2df8e38 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -1,4 +1,4 @@
-from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt6 import QtGui, QtCore, QtWidgets
import os
from ..component import Component
@@ -6,25 +6,28 @@ from ..toolkit.frame import BlankFrame
class Component(Component):
- name = 'Sound'
- version = '1.0.0'
+ name = "Sound"
+ version = "1.0.0"
def widget(self, *args):
super().widget(*args)
self.page.pushButton_sound.clicked.connect(self.pickSound)
- self.trackWidgets({
- 'sound': self.page.lineEdit_sound,
- 'chorus': self.page.checkBox_chorus,
- 'delay': self.page.spinBox_delay,
- 'volume': self.page.spinBox_volume,
- }, commandArgs={
- 'sound': None,
- })
+ self.trackWidgets(
+ {
+ "sound": self.page.lineEdit_sound,
+ "chorus": self.page.checkBox_chorus,
+ "delay": self.page.spinBox_delay,
+ "volume": self.page.spinBox_volume,
+ },
+ commandArgs={
+ "sound": None,
+ },
+ )
def properties(self):
- props = ['static', 'audio']
+ props = ["static", "audio"]
if not os.path.exists(self.sound):
- props.append('error')
+ props.append("error")
return props
def error(self):
@@ -36,20 +39,22 @@ class Component(Component):
def audio(self):
params = {}
if self.delay != 0.0:
- params['adelay'] = '=%s' % str(int(self.delay * 1000.00))
+ params["adelay"] = "=%s" % str(int(self.delay * 1000.00))
if self.chorus:
- params['chorus'] = \
- '=0.5:0.9:50|60|40:0.4|0.32|0.3:0.25|0.4|0.3:2|2.3|1.3'
+ params["chorus"] = "=0.5:0.9:50|60|40:0.4|0.32|0.3:0.25|0.4|0.3:2|2.3|1.3"
if self.volume != 1.0:
- params['volume'] = '=%s:replaygain_noclip=0' % str(self.volume)
+ params["volume"] = "=%s:replaygain_noclip=0" % str(self.volume)
return (self.sound, params)
def pickSound(self):
sndDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
- self.page, "Choose Sound", sndDir,
- "Audio Files (%s)" % " ".join(self.core.audioFormats))
+ self.page,
+ "Choose Sound",
+ sndDir,
+ "Audio Files (%s)" % " ".join(self.core.audioFormats),
+ )
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.mergeUndo = False
@@ -57,14 +62,13 @@ class Component(Component):
self.mergeUndo = True
def commandHelp(self):
- print('Path to audio file:\n path=/filepath/to/sound.ogg')
+ print("Path to audio file:\n path=/filepath/to/sound.ogg")
def command(self, arg):
- if '=' in arg:
- key, arg = arg.split('=', 1)
- if key == 'path':
- if '*%s' % os.path.splitext(arg)[1] \
- not in self.core.audioFormats:
+ if "=" in arg:
+ key, arg = arg.split("=", 1)
+ if key == "path":
+ if "*%s" % os.path.splitext(arg)[1] not in self.core.audioFormats:
print("Not a supported audio format")
quit(1)
self.page.lineEdit_sound.setText(arg)
diff --git a/src/components/spectrum.py b/src/components/spectrum.py
index 30d5426..062ebc7 100644
--- a/src/components/spectrum.py
+++ b/src/components/spectrum.py
@@ -1,5 +1,5 @@
from PIL import Image
-from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt6 import QtGui, QtCore, QtWidgets
import os
import math
import subprocess
@@ -10,16 +10,20 @@ from ..component import Component
from ..toolkit.frame import BlankFrame, scale
from ..toolkit import checkOutput, connectWidget
from ..toolkit.ffmpeg import (
- openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound
+ openPipe,
+ closePipe,
+ getAudioDuration,
+ FfmpegVideo,
+ exampleSound,
)
-log = logging.getLogger('AVP.Components.Spectrum')
+log = logging.getLogger("AVP.Components.Spectrum")
class Component(Component):
- name = 'Spectrum'
- version = '1.0.1'
+ name = "Spectrum"
+ version = "1.0.1"
def widget(self, *args):
self.previewFrame = None
@@ -30,34 +34,36 @@ class Component(Component):
self.previewSize = (214, 120)
self.previewPipe = None
- if hasattr(self.parent, 'lineEdit_audioFile'):
+ if hasattr(self.parent, "lineEdit_audioFile"):
# update preview when audio file changes (if genericPreview is off)
- self.parent.lineEdit_audioFile.textChanged.connect(
- self.update
- )
+ self.parent.lineEdit_audioFile.textChanged.connect(self.update)
- self.trackWidgets({
- 'filterType': self.page.comboBox_filterType,
- 'window': self.page.comboBox_window,
- 'mode': self.page.comboBox_mode,
- 'amplitude': self.page.comboBox_amplitude0,
- 'amplitude1': self.page.comboBox_amplitude1,
- 'amplitude2': self.page.comboBox_amplitude2,
- 'display': self.page.comboBox_display,
- 'zoom': self.page.spinBox_zoom,
- 'tc': self.page.spinBox_tc,
- 'x': self.page.spinBox_x,
- 'y': self.page.spinBox_y,
- 'mirror': self.page.checkBox_mirror,
- 'draw': self.page.checkBox_draw,
- 'scale': self.page.spinBox_scale,
- 'color': self.page.comboBox_color,
- 'compress': self.page.checkBox_compress,
- 'mono': self.page.checkBox_mono,
- 'hue': self.page.spinBox_hue,
- }, relativeWidgets=[
- 'x', 'y',
- ])
+ self.trackWidgets(
+ {
+ "filterType": self.page.comboBox_filterType,
+ "window": self.page.comboBox_window,
+ "mode": self.page.comboBox_mode,
+ "amplitude": self.page.comboBox_amplitude0,
+ "amplitude1": self.page.comboBox_amplitude1,
+ "amplitude2": self.page.comboBox_amplitude2,
+ "display": self.page.comboBox_display,
+ "zoom": self.page.spinBox_zoom,
+ "tc": self.page.spinBox_tc,
+ "x": self.page.spinBox_x,
+ "y": self.page.spinBox_y,
+ "mirror": self.page.checkBox_mirror,
+ "draw": self.page.checkBox_draw,
+ "scale": self.page.spinBox_scale,
+ "color": self.page.comboBox_color,
+ "compress": self.page.checkBox_compress,
+ "mono": self.page.checkBox_mono,
+ "hue": self.page.spinBox_hue,
+ },
+ relativeWidgets=[
+ "x",
+ "y",
+ ],
+ )
for widget in self._trackedWidgets.values():
connectWidget(widget, lambda: self.changed())
@@ -78,18 +84,18 @@ class Component(Component):
def previewRender(self):
changedSize = self.updateChunksize()
- if not changedSize \
- and not self.changedOptions \
- and self.previewFrame is not None:
- log.debug(
- 'Spectrum #%s is reusing old preview frame' % self.compPos)
+ if (
+ not changedSize
+ and not self.changedOptions
+ and self.previewFrame is not None
+ ):
+ log.debug("Spectrum #%s is reusing old preview frame" % self.compPos)
return self.previewFrame
frame = self.getPreviewFrame()
self.changedOptions = False
if not frame:
- log.warning(
- 'Spectrum #%s failed to create a preview frame' % self.compPos)
+ log.warning("Spectrum #%s failed to create a preview frame" % self.compPos)
self.previewFrame = None
return BlankFrame(self.width, self.height)
else:
@@ -105,10 +111,12 @@ class Component(Component):
self.video = FfmpegVideo(
inputPath=self.audioFile,
filter_=self.makeFfmpegFilter(),
- width=w, height=h,
+ width=w,
+ height=h,
chunkSize=self.chunkSize,
frameRate=int(self.settings.value("outputFrameRate")),
- parent=self.parent, component=self,
+ parent=self.parent,
+ component=self,
)
def frameRender(self, frameNo):
@@ -133,38 +141,55 @@ class Component(Component):
command = [
self.core.FFMPEG_BIN,
- '-thread_queue_size', '512',
- '-r', str(self.settings.value("outputFrameRate")),
- '-ss', "{0:.3f}".format(startPt),
- '-i',
- self.core.junkStream
- if genericPreview else inputFile,
- '-f', 'image2pipe',
- '-pix_fmt', 'rgba',
+ "-thread_queue_size",
+ "512",
+ "-r",
+ str(self.settings.value("outputFrameRate")),
+ "-ss",
+ "{0:.3f}".format(startPt),
+ "-i",
+ self.core.junkStream if genericPreview else inputFile,
+ "-f",
+ "image2pipe",
+ "-pix_fmt",
+ "rgba",
]
command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt))
- command.extend([
- '-an',
- '-s:v', '%sx%s' % scale(self.scale, self.width, self.height, str),
- '-codec:v', 'rawvideo', '-',
- '-frames:v', '1',
- ])
+ command.extend(
+ [
+ "-an",
+ "-s:v",
+ "%sx%s" % scale(self.scale, self.width, self.height, str),
+ "-codec:v",
+ "rawvideo",
+ "-",
+ "-frames:v",
+ "1",
+ ]
+ )
if self.core.logEnabled:
logFilename = os.path.join(
- self.core.logDir, 'preview_%s.log' % str(self.compPos))
- log.debug('Creating FFmpeg process (log at %s)' % logFilename)
- with open(logFilename, 'w') as logf:
- logf.write(" ".join(command) + '\n\n')
- with open(logFilename, 'a') as logf:
+ self.core.logDir, "preview_%s.log" % str(self.compPos)
+ )
+ log.debug("Creating FFmpeg process (log at %s)" % logFilename)
+ with open(logFilename, "w") as logf:
+ logf.write(" ".join(command) + "\n\n")
+ with open(logFilename, "a") as logf:
self.previewPipe = openPipe(
- command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
- stderr=logf, bufsize=10**8
+ command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=logf,
+ bufsize=10**8,
)
else:
self.previewPipe = openPipe(
- command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
- stderr=subprocess.DEVNULL, bufsize=10**8
+ command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ bufsize=10**8,
)
byteFrame = self.previewPipe.stdout.read(self.chunkSize)
closePipe(self.previewPipe)
@@ -173,132 +198,151 @@ class Component(Component):
return frame
def makeFfmpegFilter(self, preview=False, startPt=0):
- '''Makes final FFmpeg filter command'''
+ """Makes final FFmpeg filter command"""
def getFilterComplexCommand():
- '''Inner function that creates the final, complex part of the filter command'''
+ """Inner function that creates the final, complex part of the filter command"""
nonlocal self
genericPreview = self.settings.value("pref_genericPreview")
def getFilterComplexCommandForType():
- '''Determine portion of filter command that changes depending on selected type'''
+ """Determine portion of filter command that changes depending on selected type"""
nonlocal self
if preview:
w, h = self.previewSize
else:
w, h = (self.width, self.height)
color = self.page.comboBox_color.currentText().lower()
-
+
if self.filterType == 0: # Spectrum
if self.amplitude == 0:
- amplitude = 'sqrt'
+ amplitude = "sqrt"
elif self.amplitude == 1:
- amplitude = 'cbrt'
+ amplitude = "cbrt"
elif self.amplitude == 2:
- amplitude = '4thrt'
+ amplitude = "4thrt"
elif self.amplitude == 3:
- amplitude = '5thrt'
+ amplitude = "5thrt"
elif self.amplitude == 4:
- amplitude = 'lin'
+ amplitude = "lin"
elif self.amplitude == 5:
- amplitude = 'log'
+ amplitude = "log"
filter_ = (
- f'showspectrum=s={w}x{h}:'
- 'slide=scroll:'
- f'win_func={self.page.comboBox_window.currentText()}:'
- f'color={color}:'
- f'scale={amplitude},'
- 'colorkey=color=black:'
- 'similarity=0.1:blend=0.5'
+ f"showspectrum=s={w}x{h}:"
+ "slide=scroll:"
+ f"win_func={self.page.comboBox_window.currentText()}:"
+ f"color={color}:"
+ f"scale={amplitude},"
+ "colorkey=color=black:"
+ "similarity=0.1:blend=0.5"
)
elif self.filterType == 1: # Histogram
if self.amplitude1 == 0:
- amplitude = 'log'
+ amplitude = "log"
elif self.amplitude1 == 1:
- amplitude = 'lin'
+ amplitude = "lin"
if self.display == 0:
- display = 'log'
+ display = "log"
elif self.display == 1:
- display = 'sqrt'
+ display = "sqrt"
elif self.display == 2:
- display = 'cbrt'
+ display = "cbrt"
elif self.display == 3:
- display = 'lin'
+ display = "lin"
elif self.display == 4:
- display = 'rlog'
+ display = "rlog"
filter_ = (
f'ahistogram=r={str(self.settings.value("outputFrameRate"))}:'
- f's={w}x{h}:'
- 'dmode=separate:'
- f'ascale={amplitude}:'
- f'scale={display}'
+ f"s={w}x{h}:"
+ "dmode=separate:"
+ f"ascale={amplitude}:"
+ f"scale={display}"
)
elif self.filterType == 2: # Vector Scope
if self.amplitude2 == 0:
- amplitude = 'log'
+ amplitude = "log"
elif self.amplitude2 == 1:
- amplitude = 'sqrt'
+ amplitude = "sqrt"
elif self.amplitude2 == 2:
- amplitude = 'cbrt'
+ amplitude = "cbrt"
elif self.amplitude2 == 3:
- amplitude = 'lin'
+ amplitude = "lin"
m = self.page.comboBox_mode.currentText()
filter_ = (
- f'avectorscope=s={w}x{h}:'
+ f"avectorscope=s={w}x{h}:"
f'draw={"line" if self.draw else "dot"}:'
- f'm={m}:'
- f'scale={amplitude}:'
- f'zoom={str(self.zoom)}'
+ f"m={m}:"
+ f"scale={amplitude}:"
+ f"zoom={str(self.zoom)}"
)
elif self.filterType == 3: # Musical Scale
filter_ = (
f'showcqt=r={str(self.settings.value("outputFrameRate"))}:'
- f's={w}x{h}:'
- 'count=30:'
- 'text=0:'
- f'tc={str(self.tc)},'
- 'colorkey=color=black:'
- 'similarity=0.1:blend=0.5'
+ f"s={w}x{h}:"
+ "count=30:"
+ "text=0:"
+ f"tc={str(self.tc)},"
+ "colorkey=color=black:"
+ "similarity=0.1:blend=0.5"
)
elif self.filterType == 4: # Phase
filter_ = (
f'aphasemeter=r={str(self.settings.value("outputFrameRate"))}:'
- f's={w}x{h}:'
- 'video=1 [atrash][vtmp1]; '
- '[atrash] anullsink; '
- '[vtmp1] colorkey=color=black:'
- 'similarity=0.1:blend=0.5, '
- 'crop=in_w/8:in_h:(in_w/8)*7:0 '
+ f"s={w}x{h}:"
+ "video=1 [atrash][vtmp1]; "
+ "[atrash] anullsink; "
+ "[vtmp1] colorkey=color=black:"
+ "similarity=0.1:blend=0.5, "
+ "crop=in_w/8:in_h:(in_w/8)*7:0 "
)
return filter_
-
if self.filterType < 2:
- exampleSnd = exampleSound('freq')
+ exampleSnd = exampleSound("freq")
elif self.filterType == 2 or self.filterType == 4:
- exampleSnd = exampleSound('stereo')
+ exampleSnd = exampleSound("stereo")
elif self.filterType == 3:
- exampleSnd = exampleSound('white')
- compression = 'compand=gain=4,' if self.compress else ''
- aformat = 'aformat=channel_layouts=mono,' if self.mono and self.filterType not in (2, 4) else ''
+ exampleSnd = exampleSound("white")
+ compression = "compand=gain=4," if self.compress else ""
+ aformat = (
+ "aformat=channel_layouts=mono,"
+ if self.mono and self.filterType not in (2, 4)
+ else ""
+ )
filter_ = getFilterComplexCommandForType()
- hflip = 'hflip, ' if self.mirror else ''
- trim = 'trim=start=%s:end=%s, ' % ("{0:.3f}".format(startPt + 12), "{0:.3f}".format(startPt + 12.5)) if preview else ''
- scale_ = 'scale=%sx%s' % scale(self.scale, self.width, self.height, str)
- hue = ', hue=h=%s:s=10' % str(self.hue) if self.hue > 0 and self.filterType != 3 else ''
- convolution = ', convolution=-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2' if self.filterType == 3 else ''
-
+ hflip = "hflip, " if self.mirror else ""
+ trim = (
+ "trim=start=%s:end=%s, "
+ % (
+ "{0:.3f}".format(startPt + 12),
+ "{0:.3f}".format(startPt + 12.5),
+ )
+ if preview
+ else ""
+ )
+ scale_ = "scale=%sx%s" % scale(self.scale, self.width, self.height, str)
+ hue = (
+ ", hue=h=%s:s=10" % str(self.hue)
+ if self.hue > 0 and self.filterType != 3
+ else ""
+ )
+ convolution = (
+ ", convolution=-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2"
+ if self.filterType == 3
+ else ""
+ )
+
return (
f"{exampleSnd if preview and genericPreview else '[0:a] '}"
f"{compression}{aformat}{filter_} [v1]; "
f"[v1] {hflip}{trim}{scale_}{hue}{convolution} [v]"
)
-
return [
- '-filter_complex',
+ "-filter_complex",
getFilterComplexCommand(),
- '-map', '[v]',
+ "-map",
+ "[v]",
]
def updateChunksize(self):
@@ -311,9 +355,9 @@ class Component(Component):
def finalizeFrame(self, imageData):
try:
image = Image.frombytes(
- 'RGBA',
+ "RGBA",
scale(self.scale, self.width, self.height, int),
- imageData
+ imageData,
)
self._image = image
except ValueError:
diff --git a/src/components/text.py b/src/components/text.py
index 3238d2a..40c981a 100644
--- a/src/components/text.py
+++ b/src/components/text.py
@@ -1,22 +1,22 @@
from PIL import ImageEnhance, ImageFilter, ImageChops
-from PyQt5.QtGui import QColor, QFont
-from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt6.QtGui import QColor, QFont
+from PyQt6 import QtGui, QtCore, QtWidgets
import os
import logging
from ..component import Component
from ..toolkit.frame import FramePainter, PaintColor
-log = logging.getLogger('AVP.Components.Text')
+log = logging.getLogger("AVP.Components.Text")
class Component(Component):
- name = 'Title Text'
- version = '1.0.1'
+ name = "Title Text"
+ version = "1.0.1"
def widget(self, *args):
super().widget(*args)
- self.title = 'Text'
+ self.title = "Text"
self.alignment = 1
self.titleFont = QFont()
self.fontSize = self.height / 13.5
@@ -29,33 +29,44 @@ class Component(Component):
self.page.lineEdit_title.setText(self.title)
self.page.pushButton_center.clicked.connect(self.centerXY)
- self.page.fontComboBox_titleFont.currentFontChanged.connect(self._sendUpdateSignal)
+ self.page.fontComboBox_titleFont.currentFontChanged.connect(
+ self._sendUpdateSignal
+ )
# The QFontComboBox must be connected directly to the Qt Signal
# which triggers the preview to update.
# This unfortunately makes changing the font into a non-undoable action.
# Must be something broken in the conversion to a ComponentAction
- self.trackWidgets({
- 'textColor': self.page.lineEdit_textColor,
- 'title': self.page.lineEdit_title,
- 'alignment': self.page.comboBox_textAlign,
- 'fontSize': self.page.spinBox_fontSize,
- 'xPosition': self.page.spinBox_xTextAlign,
- 'yPosition': self.page.spinBox_yTextAlign,
- 'fontStyle': self.page.comboBox_fontStyle,
- 'stroke': self.page.spinBox_stroke,
- 'strokeColor': self.page.lineEdit_strokeColor,
- 'shadow': self.page.checkBox_shadow,
- 'shadX': self.page.spinBox_shadX,
- 'shadY': self.page.spinBox_shadY,
- 'shadBlur': self.page.spinBox_shadBlur,
- }, colorWidgets={
- 'textColor': self.page.pushButton_textColor,
- 'strokeColor': self.page.pushButton_strokeColor,
- }, relativeWidgets=[
- 'xPosition', 'yPosition', 'fontSize',
- 'stroke', 'shadX', 'shadY', 'shadBlur'
- ])
+ self.trackWidgets(
+ {
+ "textColor": self.page.lineEdit_textColor,
+ "title": self.page.lineEdit_title,
+ "alignment": self.page.comboBox_textAlign,
+ "fontSize": self.page.spinBox_fontSize,
+ "xPosition": self.page.spinBox_xTextAlign,
+ "yPosition": self.page.spinBox_yTextAlign,
+ "fontStyle": self.page.comboBox_fontStyle,
+ "stroke": self.page.spinBox_stroke,
+ "strokeColor": self.page.lineEdit_strokeColor,
+ "shadow": self.page.checkBox_shadow,
+ "shadX": self.page.spinBox_shadX,
+ "shadY": self.page.spinBox_shadY,
+ "shadBlur": self.page.spinBox_shadBlur,
+ },
+ colorWidgets={
+ "textColor": self.page.pushButton_textColor,
+ "strokeColor": self.page.pushButton_strokeColor,
+ },
+ relativeWidgets=[
+ "xPosition",
+ "yPosition",
+ "fontSize",
+ "stroke",
+ "shadX",
+ "shadY",
+ "shadBlur",
+ ],
+ )
self.centerXY()
def update(self):
@@ -74,20 +85,23 @@ class Component(Component):
self.page.spinBox_shadBlur.setHidden(True)
def centerXY(self):
- self.setRelativeWidget('xPosition', 0.5)
- self.setRelativeWidget('yPosition', 0.521)
+ self.setRelativeWidget("xPosition", 0.5)
+ self.setRelativeWidget("yPosition", 0.521)
def getXY(self):
- '''Returns true x, y after considering alignment settings'''
+ """Returns true x, y after considering alignment settings"""
fm = QtGui.QFontMetrics(self.titleFont)
- x = self.pixelValForAttr('xPosition')
+ text_width = fm.boundingRect(self.title).width()
+ x = self.pixelValForAttr("xPosition")
- if self.alignment == 1: # Middle
- offset = int(fm.width(self.title)/2)
- x -= offset
- if self.alignment == 2: # Right
- offset = fm.width(self.title)
- x -= offset
+ if self.alignment == 1: # Middle
+ offset = int(text_width / 2)
+ elif self.alignment == 2: # Right
+ offset = text_width
+ else:
+ raise ValueError(f"Alignment value {self.alignment} unknown")
+
+ x -= offset
return x, self.yPosition
@@ -95,21 +109,21 @@ class Component(Component):
super().loadPreset(pr, *args)
font = QFont()
- font.fromString(pr['titleFont'])
+ font.fromString(pr["titleFont"])
self.page.fontComboBox_titleFont.setCurrentFont(font)
def savePreset(self):
saveValueStore = super().savePreset()
- saveValueStore['titleFont'] = self.titleFont.toString()
+ saveValueStore["titleFont"] = self.titleFont.toString()
return saveValueStore
def previewRender(self):
return self.addText(self.width, self.height)
def properties(self):
- props = ['static']
+ props = ["static"]
if not self.title:
- props.append('error')
+ props.append("error")
return props
def error(self):
@@ -121,26 +135,26 @@ class Component(Component):
def addText(self, width, height):
font = self.titleFont
font.setPixelSize(self.fontSize)
- font.setStyle(QFont.StyleNormal)
- font.setWeight(QFont.Normal)
- font.setCapitalization(QFont.MixedCase)
+ font.setStyle(QFont.Style.StyleNormal)
+ font.setWeight(QFont.Weight.Normal)
+ font.setCapitalization(QFont.Capitalization.MixedCase)
if self.fontStyle == 1:
- font.setWeight(QFont.DemiBold)
+ font.setWeight(QFont.Weight.DemiBold)
if self.fontStyle == 2:
- font.setWeight(QFont.Bold)
+ font.setWeight(QFont.Weight.Bold)
elif self.fontStyle == 3:
- font.setStyle(QFont.StyleItalic)
+ font.setStyle(QFont.Style.StyleItalic)
elif self.fontStyle == 4:
- font.setWeight(QFont.Bold)
- font.setStyle(QFont.StyleItalic)
+ font.setWeight(QFont.Weight.Bold)
+ font.setStyle(QFont.Style.StyleItalic)
elif self.fontStyle == 5:
- font.setStyle(QFont.StyleOblique)
+ font.setStyle(QFont.Style.StyleOblique)
elif self.fontStyle == 6:
- font.setCapitalization(QFont.SmallCaps)
+ font.setCapitalization(QFont.Capitalization.SmallCaps)
image = FramePainter(width, height)
x, y = self.getXY()
- log.debug('Text position translates to %s, %s', x, y)
+ log.debug("Text position translates to %s, %s", x, y)
if self.stroke > 0:
outliner = QtGui.QPainterPathStroker()
outliner.setWidth(self.stroke)
@@ -149,16 +163,16 @@ class Component(Component):
# PathStroker ignores smallcaps so we need this weird hack
path.addText(x, y, font, self.title[0])
fm = QtGui.QFontMetrics(font)
- newX = x + fm.width(self.title[0])
+ newX = x + fm.boundingRect(self.title[0]).width()
strokeFont = self.page.fontComboBox_titleFont.currentFont()
- strokeFont.setCapitalization(QFont.SmallCaps)
+ strokeFont.setCapitalization(QFont.Capitalization.SmallCaps)
strokeFont.setPixelSize(int((self.fontSize / 7) * 5))
- strokeFont.setLetterSpacing(QFont.PercentageSpacing, 139)
+ strokeFont.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 139)
path.addText(newX, y, strokeFont, self.title[1:])
else:
path.addText(x, y, font, self.title)
path = outliner.createStroke(path)
- image.setPen(QtCore.Qt.NoPen)
+ image.setPen(QtCore.Qt.PenStyle.NoPen)
image.setBrush(PaintColor(*self.strokeColor))
image.drawPath(path)
@@ -178,27 +192,27 @@ class Component(Component):
return frame
def commandHelp(self):
- print('Enter a string to use as centred white text:')
+ print("Enter a string to use as centred white text:")
print(' "title=User Error"')
- print('Specify a text color:\n color=255,255,255')
- print('Set custom x, y position:\n x=500 y=500')
+ print("Specify a text color:\n color=255,255,255")
+ print("Set custom x, y position:\n x=500 y=500")
def command(self, arg):
- if '=' in arg:
- key, arg = arg.split('=', 1)
- if key == 'color':
+ if "=" in arg:
+ key, arg = arg.split("=", 1)
+ if key == "color":
self.page.lineEdit_textColor.setText(arg)
return
- elif key == 'size':
+ elif key == "size":
self.page.spinBox_fontSize.setValue(int(arg))
return
- elif key == 'x':
+ elif key == "x":
self.page.spinBox_xTextAlign.setValue(int(arg))
return
- elif key == 'y':
+ elif key == "y":
self.page.spinBox_yTextAlign.setValue(int(arg))
return
- elif key == 'title':
+ elif key == "title":
self.page.lineEdit_title.setText(arg)
return
super().command(arg)
diff --git a/src/components/video.py b/src/components/video.py
index 60ca800..65a05af 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -1,5 +1,5 @@
from PIL import Image
-from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt6 import QtGui, QtCore, QtWidgets
import os
import math
import subprocess
@@ -11,15 +11,15 @@ from ..toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo
from ..toolkit import checkOutput
-log = logging.getLogger('AVP.Components.Video')
+log = logging.getLogger("AVP.Components.Video")
class Component(Component):
- name = 'Video'
- version = '1.0.0'
+ name = "Video"
+ version = "1.0.0"
def widget(self, *args):
- self.videoPath = ''
+ self.videoPath = ""
self.badAudio = False
self.x = 0
self.y = 0
@@ -27,23 +27,28 @@ class Component(Component):
super().widget(*args)
self._image = BlankFrame(self.width, self.height)
self.page.pushButton_video.clicked.connect(self.pickVideo)
- self.trackWidgets({
- 'videoPath': self.page.lineEdit_video,
- 'loopVideo': self.page.checkBox_loop,
- 'useAudio': self.page.checkBox_useAudio,
- 'distort': self.page.checkBox_distort,
- 'scale': self.page.spinBox_scale,
- 'volume': self.page.spinBox_volume,
- 'xPosition': self.page.spinBox_x,
- 'yPosition': self.page.spinBox_y,
- }, presetNames={
- 'videoPath': 'video',
- 'loopVideo': 'loop',
- 'xPosition': 'x',
- 'yPosition': 'y',
- }, relativeWidgets=[
- 'xPosition', 'yPosition',
- ])
+ self.trackWidgets(
+ {
+ "videoPath": self.page.lineEdit_video,
+ "loopVideo": self.page.checkBox_loop,
+ "useAudio": self.page.checkBox_useAudio,
+ "distort": self.page.checkBox_distort,
+ "scale": self.page.spinBox_scale,
+ "volume": self.page.spinBox_volume,
+ "xPosition": self.page.spinBox_x,
+ "yPosition": self.page.spinBox_y,
+ },
+ presetNames={
+ "videoPath": "video",
+ "loopVideo": "loop",
+ "xPosition": "x",
+ "yPosition": "y",
+ },
+ relativeWidgets=[
+ "xPosition",
+ "yPosition",
+ ],
+ )
def update(self):
if self.page.checkBox_useAudio.isChecked():
@@ -64,7 +69,7 @@ class Component(Component):
def properties(self):
props = []
outputFile = None
- if hasattr(self.parent, 'lineEdit_outputFile'):
+ if hasattr(self.parent, "lineEdit_outputFile"):
# check only happens in GUI mode
outputFile = self.parent.lineEdit_outputFile.text()
@@ -72,34 +77,42 @@ class Component(Component):
self.lockError("There is no video selected.")
elif not os.path.exists(self.videoPath):
self.lockError("The video selected does not exist!")
- elif outputFile and os.path.realpath(self.videoPath) == os.path.realpath(outputFile):
+ elif outputFile and os.path.realpath(self.videoPath) == os.path.realpath(
+ outputFile
+ ):
self.lockError("Input and output paths match.")
if self.useAudio:
- props.append('audio')
- if not testAudioStream(self.videoPath) \
- and self.error() is None:
- self.lockError(
- "Could not identify an audio stream in this video.")
+ props.append("audio")
+ if not testAudioStream(self.videoPath) and self.error() is None:
+ self.lockError("Could not identify an audio stream in this video.")
return props
def audio(self):
params = {}
if self.volume != 1.0:
- params['volume'] = '=%s:replaygain_noclip=0' % str(self.volume)
+ params["volume"] = "=%s:replaygain_noclip=0" % str(self.volume)
return (self.videoPath, params)
def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs)
self.updateChunksize()
- self.video = FfmpegVideo(
- inputPath=self.videoPath, filter_=self.makeFfmpegFilter(),
- width=self.width, height=self.height, chunkSize=self.chunkSize,
- frameRate=int(self.settings.value("outputFrameRate")),
- parent=self.parent, loopVideo=self.loopVideo,
- component=self
- ) if os.path.exists(self.videoPath) else None
+ self.video = (
+ FfmpegVideo(
+ inputPath=self.videoPath,
+ filter_=self.makeFfmpegFilter(),
+ width=self.width,
+ height=self.height,
+ chunkSize=self.chunkSize,
+ frameRate=int(self.settings.value("outputFrameRate")),
+ parent=self.parent,
+ loopVideo=self.loopVideo,
+ component=self,
+ )
+ if os.path.exists(self.videoPath)
+ else None
+ )
def frameRender(self, frameNo):
if FfmpegVideo.threadError is not None:
@@ -112,8 +125,10 @@ class Component(Component):
def pickVideo(self):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
- self.page, "Choose Video",
- imgDir, "Video Files (%s)" % " ".join(self.core.videoFormats)
+ self.page,
+ "Choose Video",
+ imgDir,
+ "Video Files (%s)" % " ".join(self.core.videoFormats),
)
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
@@ -127,33 +142,50 @@ class Component(Component):
command = [
self.core.FFMPEG_BIN,
- '-thread_queue_size', '512',
- '-i', self.videoPath,
- '-f', 'image2pipe',
- '-pix_fmt', 'rgba',
+ "-thread_queue_size",
+ "512",
+ "-i",
+ self.videoPath,
+ "-f",
+ "image2pipe",
+ "-pix_fmt",
+ "rgba",
]
command.extend(self.makeFfmpegFilter())
- command.extend([
- '-codec:v', 'rawvideo', '-',
- '-ss', '90',
- '-frames:v', '1',
- ])
+ command.extend(
+ [
+ "-codec:v",
+ "rawvideo",
+ "-",
+ "-ss",
+ "90",
+ "-frames:v",
+ "1",
+ ]
+ )
if self.core.logEnabled:
logFilename = os.path.join(
- self.core.logDir, 'preview_%s.log' % str(self.compPos))
- log.debug('Creating ffmpeg process (log at %s)' % logFilename)
- with open(logFilename, 'w') as logf:
- logf.write(" ".join(command) + '\n\n')
- with open(logFilename, 'a') as logf:
+ self.core.logDir, "preview_%s.log" % str(self.compPos)
+ )
+ log.debug("Creating ffmpeg process (log at %s)" % logFilename)
+ with open(logFilename, "w") as logf:
+ logf.write(" ".join(command) + "\n\n")
+ with open(logFilename, "a") as logf:
pipe = openPipe(
- command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
- stderr=logf, bufsize=10**8
+ command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=logf,
+ bufsize=10**8,
)
else:
pipe = openPipe(
- command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
- stderr=subprocess.DEVNULL, bufsize=10**8
+ command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ bufsize=10**8,
)
byteFrame = pipe.stdout.read(self.chunkSize)
@@ -164,9 +196,8 @@ class Component(Component):
def makeFfmpegFilter(self):
return [
- '-filter_complex',
- '[0:v] scale=%s:%s' % scale(
- self.scale, self.width, self.height, str),
+ "-filter_complex",
+ "[0:v] scale=%s:%s" % scale(self.scale, self.width, self.height, str),
]
def updateChunksize(self):
@@ -177,10 +208,10 @@ class Component(Component):
self.chunkSize = 4 * width * height
def command(self, arg):
- if '=' in arg:
- key, arg = arg.split('=', 1)
- if key == 'path' and os.path.exists(arg):
- if '*%s' % os.path.splitext(arg)[1] in self.core.videoFormats:
+ if "=" in arg:
+ key, arg = arg.split("=", 1)
+ if key == "path" and os.path.exists(arg):
+ if "*%s" % os.path.splitext(arg)[1] in self.core.videoFormats:
self.page.lineEdit_video.setText(arg)
self.page.spinBox_scale.setValue(100)
self.page.checkBox_loop.setChecked(True)
@@ -188,7 +219,7 @@ class Component(Component):
else:
print("Not a supported video format")
quit(1)
- elif arg == 'audio':
+ elif arg == "audio":
if not self.page.lineEdit_video.text():
print("'audio' option must follow a video selection")
quit(1)
@@ -197,28 +228,25 @@ class Component(Component):
super().command(arg)
def commandHelp(self):
- print('Load a video:\n path=/filepath/to/video.mp4')
- print('Using audio:\n path=/filepath/to/video.mp4 audio')
+ print("Load a video:\n path=/filepath/to/video.mp4")
+ print("Using audio:\n path=/filepath/to/video.mp4 audio")
def finalizeFrame(self, imageData):
try:
if self.distort:
- image = Image.frombytes(
- 'RGBA',
- (self.width, self.height),
- imageData)
+ image = Image.frombytes("RGBA", (self.width, self.height), imageData)
else:
image = Image.frombytes(
- 'RGBA',
+ "RGBA",
scale(self.scale, self.width, self.height, int),
- imageData)
+ imageData,
+ )
self._image = image
except ValueError:
# use last good frame
image = self._image
- if self.scale != 100 \
- or self.xPosition != 0 or self.yPosition != 0:
+ if self.scale != 100 or self.xPosition != 0 or self.yPosition != 0:
frame = BlankFrame(self.width, self.height)
frame.paste(image, box=(self.xPosition, self.yPosition))
else:
diff --git a/src/components/waveform.py b/src/components/waveform.py
index eef6de0..7dc0b99 100644
--- a/src/components/waveform.py
+++ b/src/components/waveform.py
@@ -1,6 +1,6 @@
from PIL import Image
-from PyQt5 import QtGui, QtCore, QtWidgets
-from PyQt5.QtGui import QColor
+from PyQt6 import QtGui, QtCore, QtWidgets
+from PyQt6.QtGui import QColor
import os
import math
import subprocess
@@ -10,44 +10,51 @@ from ..component import Component
from ..toolkit.frame import BlankFrame, scale
from ..toolkit import checkOutput
from ..toolkit.ffmpeg import (
- openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound
+ openPipe,
+ closePipe,
+ getAudioDuration,
+ FfmpegVideo,
+ exampleSound,
)
-log = logging.getLogger('AVP.Components.Waveform')
+log = logging.getLogger("AVP.Components.Waveform")
class Component(Component):
- name = 'Waveform'
- version = '1.0.0'
+ name = "Waveform"
+ version = "1.0.0"
def widget(self, *args):
super().widget(*args)
self._image = BlankFrame(self.width, self.height)
- self.page.lineEdit_color.setText('255,255,255')
-
- if hasattr(self.parent, 'lineEdit_audioFile'):
- self.parent.lineEdit_audioFile.textChanged.connect(
- self.update
- )
-
- self.trackWidgets({
- 'color': self.page.lineEdit_color,
- 'mode': self.page.comboBox_mode,
- 'amplitude': self.page.comboBox_amplitude,
- 'x': self.page.spinBox_x,
- 'y': self.page.spinBox_y,
- 'mirror': self.page.checkBox_mirror,
- 'scale': self.page.spinBox_scale,
- 'opacity': self.page.spinBox_opacity,
- 'compress': self.page.checkBox_compress,
- 'mono': self.page.checkBox_mono,
- }, colorWidgets={
- 'color': self.page.pushButton_color,
- }, relativeWidgets=[
- 'x', 'y',
- ])
+ self.page.lineEdit_color.setText("255,255,255")
+
+ if hasattr(self.parent, "lineEdit_audioFile"):
+ self.parent.lineEdit_audioFile.textChanged.connect(self.update)
+
+ self.trackWidgets(
+ {
+ "color": self.page.lineEdit_color,
+ "mode": self.page.comboBox_mode,
+ "amplitude": self.page.comboBox_amplitude,
+ "x": self.page.spinBox_x,
+ "y": self.page.spinBox_y,
+ "mirror": self.page.checkBox_mirror,
+ "scale": self.page.spinBox_scale,
+ "opacity": self.page.spinBox_opacity,
+ "compress": self.page.checkBox_compress,
+ "mono": self.page.checkBox_mono,
+ },
+ colorWidgets={
+ "color": self.page.pushButton_color,
+ },
+ relativeWidgets=[
+ "x",
+ "y",
+ ],
+ )
def previewRender(self):
self.updateChunksize()
@@ -64,10 +71,13 @@ class Component(Component):
self.video = FfmpegVideo(
inputPath=self.audioFile,
filter_=self.makeFfmpegFilter(),
- width=w, height=h,
+ width=w,
+ height=h,
chunkSize=self.chunkSize,
frameRate=int(self.settings.value("outputFrameRate")),
- parent=self.parent, component=self, debug=True,
+ parent=self.parent,
+ component=self,
+ debug=True,
)
def frameRender(self, frameNo):
@@ -94,37 +104,54 @@ class Component(Component):
command = [
self.core.FFMPEG_BIN,
- '-thread_queue_size', '512',
- '-r', str(self.settings.value("outputFrameRate")),
- '-ss', "{0:.3f}".format(startPt),
- '-i',
- self.core.junkStream
- if genericPreview else inputFile,
- '-f', 'image2pipe',
- '-pix_fmt', 'rgba',
+ "-thread_queue_size",
+ "512",
+ "-r",
+ str(self.settings.value("outputFrameRate")),
+ "-ss",
+ "{0:.3f}".format(startPt),
+ "-i",
+ self.core.junkStream if genericPreview else inputFile,
+ "-f",
+ "image2pipe",
+ "-pix_fmt",
+ "rgba",
]
command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt))
- command.extend([
- '-an',
- '-s:v', '%sx%s' % scale(self.scale, self.width, self.height, str),
- '-codec:v', 'rawvideo', '-',
- '-frames:v', '1',
- ])
+ command.extend(
+ [
+ "-an",
+ "-s:v",
+ "%sx%s" % scale(self.scale, self.width, self.height, str),
+ "-codec:v",
+ "rawvideo",
+ "-",
+ "-frames:v",
+ "1",
+ ]
+ )
if self.core.logEnabled:
logFilename = os.path.join(
- self.core.logDir, 'preview_%s.log' % str(self.compPos))
- log.debug('Creating ffmpeg log at %s', logFilename)
- with open(logFilename, 'w') as logf:
- logf.write(" ".join(command) + '\n\n')
- with open(logFilename, 'a') as logf:
+ self.core.logDir, "preview_%s.log" % str(self.compPos)
+ )
+ log.debug("Creating ffmpeg log at %s", logFilename)
+ with open(logFilename, "w") as logf:
+ logf.write(" ".join(command) + "\n\n")
+ with open(logFilename, "a") as logf:
pipe = openPipe(
- command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
- stderr=logf, bufsize=10**8
+ command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=logf,
+ bufsize=10**8,
)
else:
pipe = openPipe(
- command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
- stderr=subprocess.DEVNULL, bufsize=10**8
+ command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ bufsize=10**8,
)
byteFrame = pipe.stdout.read(self.chunkSize)
closePipe(pipe)
@@ -135,35 +162,35 @@ class Component(Component):
def makeFfmpegFilter(self, preview=False, startPt=0):
w, h = scale(self.scale, self.width, self.height, str)
if self.amplitude == 0:
- amplitude = 'lin'
+ amplitude = "lin"
elif self.amplitude == 1:
- amplitude = 'log'
+ amplitude = "log"
elif self.amplitude == 2:
- amplitude = 'sqrt'
+ amplitude = "sqrt"
elif self.amplitude == 3:
- amplitude = 'cbrt'
+ amplitude = "cbrt"
hexcolor = QColor(*self.color).name()
opacity = "{0:.1f}".format(self.opacity / 100)
genericPreview = self.settings.value("pref_genericPreview")
if self.mode < 3:
filter_ = (
- 'showwaves='
+ "showwaves="
f'r={str(self.settings.value("outputFrameRate"))}:'
f's={self.settings.value("outputWidth")}x{self.settings.value("outputHeight")}:'
f'mode={self.page.comboBox_mode.currentText().lower() if self.mode != 3 else "p2p"}:'
- f'colors={hexcolor}@{opacity}:scale={amplitude}'
+ f"colors={hexcolor}@{opacity}:scale={amplitude}"
)
elif self.mode > 2:
filter_ = (
f'showfreqs=s={str(self.settings.value("outputWidth"))}x{str(self.settings.value("outputHeight"))}:'
f'mode={"line" if self.mode == 4 else "bar"}:'
- f'colors={hexcolor}@{opacity}'
+ f"colors={hexcolor}@{opacity}"
f":ascale={amplitude}:fscale={'log' if self.mono else 'lin'}"
)
baselineHeight = int(self.height * (4 / 1080))
return [
- '-filter_complex',
+ "-filter_complex",
f"{exampleSound('wave', extra='') if preview and genericPreview else '[0:a] '}"
f"{'compand=gain=4,' if self.compress else ''}"
f"{'aformat=channel_layouts=mono,' if self.mono and self.mode < 3 else ''}"
@@ -171,12 +198,14 @@ class Component(Component):
f"{', drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=%s:color=%s@%s' % (baselineHeight, hexcolor, opacity) if self.mode < 2 else ''}"
f"{', hflip' if self.mirror else''}"
" [v1]; "
- '[v1] scale=%s:%s%s [v]' % (
- w, h,
- ', trim=duration=%s' % "{0:.3f}".format(startPt + 3)
- if preview else '',
+ "[v1] scale=%s:%s%s [v]"
+ % (
+ w,
+ h,
+ ", trim=duration=%s" % "{0:.3f}".format(startPt + 3) if preview else "",
),
- '-map', '[v]',
+ "-map",
+ "[v]",
]
def updateChunksize(self):
@@ -186,15 +215,14 @@ class Component(Component):
def finalizeFrame(self, imageData):
try:
image = Image.frombytes(
- 'RGBA',
+ "RGBA",
scale(self.scale, self.width, self.height, int),
- imageData
+ imageData,
)
self._image = image
except ValueError:
image = self._image
- if self.scale != 100 \
- or self.x != 0 or self.y != 0:
+ if self.scale != 100 or self.x != 0 or self.y != 0:
frame = BlankFrame(self.width, self.height)
frame.paste(image, box=(self.x, self.y))
else:
diff --git a/src/core.py b/src/core.py
index 1ad4a67..df6ff63 100644
--- a/src/core.py
+++ b/src/core.py
@@ -1,8 +1,9 @@
-'''
- Home to the Core class which tracks program state. Used by GUI & commandline
- to create a list of components and create a video thread to export.
-'''
-from PyQt5 import QtCore, QtGui, uic
+"""
+Home to the Core class which tracks program state. Used by GUI & commandline
+to create a list of components and create a video thread to export.
+"""
+
+from PyQt6 import QtCore, QtGui, uic
import sys
import os
import json
@@ -12,20 +13,20 @@ import logging
from . import toolkit
-log = logging.getLogger('AVP.Core')
+log = logging.getLogger("AVP.Core")
STDOUT_LOGLVL = logging.WARNING
FILE_LIBLOGLVL = logging.WARNING
FILE_LOGLVL = logging.INFO
class Core:
- '''
- MainWindow and Command module both use an instance of this class
- to store the core program state. This object tracks the components,
- talks to the components, handles opening/creating project files
- and presets, and creates the video thread to export.
- This class also stores constants as class variables.
- '''
+ """
+ MainWindow and Command module both use an instance of this class
+ to store the core program state. This object tracks the components,
+ talks to the components, handles opening/creating project files
+ and presets, and creates the video thread to export.
+ This class also stores constants as class variables.
+ """
def __init__(self):
self.importComponents()
@@ -34,9 +35,7 @@ class Core:
self.openingProject = False
def __repr__(self):
- return "\n=~=~=~=\n".join(
- [repr(comp) for comp in self.selectedComponents]
- )
+ return "\n=~=~=~=\n".join([repr(comp) for comp in self.selectedComponents])
def importComponents(self):
def findComponents():
@@ -44,11 +43,12 @@ class Core:
name, ext = os.path.splitext(f)
if name.startswith("__"):
continue
- elif ext == '.py':
+ elif ext == ".py":
yield name
- log.debug('Importing component modules')
+
+ log.debug("Importing component modules")
self.modules = [
- import_module('.components.%s' % name, __package__)
+ import_module(".components.%s" % name, __package__)
for name in findComponents()
]
# store canonical module names and indexes
@@ -62,7 +62,7 @@ class Core:
# store alternative names for modules
self.altCompNames = []
for i, mod in enumerate(self.modules):
- if hasattr(mod.Component, 'names'):
+ if hasattr(mod.Component, "names"):
for name in mod.Component.names():
self.altCompNames.append((name, i))
@@ -71,10 +71,10 @@ class Core:
component.compPos = i
def insertComponent(self, compPos, component, loader):
- '''
- Creates a new component using these args:
- (compPos, component obj or moduleIndex, MWindow/Command/Core obj)
- '''
+ """
+ Creates a new component using these args:
+ (compPos, component obj or moduleIndex, MWindow/Command/Core obj)
+ """
if compPos < 0 or compPos > len(self.selectedComponents):
compPos = len(self.selectedComponents)
if len(self.selectedComponents) > 50:
@@ -82,25 +82,16 @@ class Core:
if type(component) is int:
# create component using module index in self.modules
moduleIndex = int(component)
- log.debug(
- 'Creating new component from module #%s', str(moduleIndex))
- component = self.modules[moduleIndex].Component(
- moduleIndex, compPos, self
- )
+ log.debug("Creating new component from module #%s", str(moduleIndex))
+ component = self.modules[moduleIndex].Component(moduleIndex, compPos, self)
component.widget(loader)
else:
moduleIndex = -1
- log.debug(
- 'Inserting previously-created %s component', component.name)
+ log.debug("Inserting previously-created %s component", component.name)
- component._error.connect(
- loader.videoThreadError
- )
- self.selectedComponents.insert(
- compPos,
- component
- )
- if hasattr(loader, 'insertComponent'):
+ component._error.connect(loader.videoThreadError)
+ self.selectedComponents.insert(compPos, component)
+ if hasattr(loader, "insertComponent"):
loader.insertComponent(compPos)
self.componentListChanged()
@@ -123,9 +114,7 @@ class Core:
self.componentListChanged()
def updateComponent(self, i):
- log.debug(
- 'Auto-updating %s #%s',
- self.selectedComponents[i], str(i))
+ log.debug("Auto-updating %s #%s", self.selectedComponents[i], str(i))
self.selectedComponents[i].update(auto=True)
def moduleIndexFor(self, compName):
@@ -141,63 +130,59 @@ class Core:
self.selectedComponents[compIndex].currentPreset = None
def openPreset(self, filepath, compIndex, presetName):
- '''Applies a preset to a specific component'''
+ """Applies a preset to a specific component"""
saveValueStore = self.getPreset(filepath)
if not saveValueStore:
return False
comp = self.selectedComponents[compIndex]
- comp.loadPreset(
- saveValueStore,
- presetName
- )
+ comp.loadPreset(saveValueStore, presetName)
self.savedPresets[presetName] = dict(saveValueStore)
return True
def getPreset(self, filepath):
- '''Returns the preset dict stored at this filepath'''
+ """Returns the preset dict stored at this filepath"""
if not os.path.exists(filepath):
return False
- with open(filepath, 'r') as f:
+ with open(filepath, "r") as f:
for line in f:
saveValueStore = toolkit.presetFromString(line.strip())
break
return saveValueStore
def getPresetDir(self, comp):
- '''Get the preset subdir for a particular version of a component'''
+ """Get the preset subdir for a particular version of a component"""
return os.path.join(Core.presetDir, comp.name, str(comp.version))
def openProject(self, loader, filepath):
- ''' loader is the object calling this method which must have
+ """loader is the object calling this method which must have
its own showMessage(**kwargs) method for displaying errors.
- '''
+ """
if not os.path.exists(filepath):
- loader.showMessage(msg='Project file not found.')
+ loader.showMessage(msg="Project file not found.")
return
errcode, data = self.parseAvFile(filepath)
if errcode == 0:
self.openingProject = True
try:
- if hasattr(loader, 'window'):
- for widget, value in data['WindowFields']:
- widget = eval('loader.%s' % widget)
+ if hasattr(loader, "window"):
+ for widget, value in data["WindowFields"]:
+ widget = eval("loader.%s" % widget)
with toolkit.blockSignals(widget):
toolkit.setWidgetValue(widget, value)
- for key, value in data['Settings']:
+ for key, value in data["Settings"]:
Core.settings.setValue(key, value)
- for tup in data['Components']:
+ for tup in data["Components"]:
name, vers, preset = tup
clearThis = False
modified = False
# add loaded named presets to savedPresets dict
- if 'preset' in preset and preset['preset'] is not None:
- nam = preset['preset']
- filepath2 = os.path.join(
- Core.presetDir, name, str(vers), nam)
+ if "preset" in preset and preset["preset"] is not None:
+ nam = preset["preset"]
+ filepath2 = os.path.join(Core.presetDir, name, str(vers), nam)
origSaveValueStore = self.getPreset(filepath2)
if origSaveValueStore:
self.savedPresets[nam] = dict(origSaveValueStore)
@@ -207,33 +192,31 @@ class Core:
clearThis = True
# create the actual component object & get its index
- i = self.insertComponent(
- -1,
- self.moduleIndexFor(name),
- loader
- )
+ i = self.insertComponent(-1, self.moduleIndexFor(name), loader)
+ if i is None:
+ loader.showMessage(
+ msg=f"Component '{name}' didn't initialize correctly and had to be removed."
+ )
+ continue
if i == -1:
loader.showMessage(msg="Too many components!")
break
try:
- if 'preset' in preset and preset['preset'] is not None:
- self.selectedComponents[i].loadPreset(
- preset
- )
+ if "preset" in preset and preset["preset"] is not None:
+ self.selectedComponents[i].loadPreset(preset)
else:
self.selectedComponents[i].loadPreset(
- preset,
- preset['preset']
+ preset, preset["preset"]
)
except KeyError as e:
- log.warning('%s missing value: %s' % (
- self.selectedComponents[i], e)
+ log.warning(
+ "%s missing value: %s" % (self.selectedComponents[i], e)
)
if clearThis:
self.clearPreset(i)
- if hasattr(loader, 'updateComponentTitle'):
+ if hasattr(loader, "updateComponentTitle"):
loader.updateComponentTitle(i, modified)
self.openingProject = False
return True
@@ -243,56 +226,57 @@ class Core:
if errcode == 1:
typ, value, tb = data
- if typ.__name__ == 'KeyError':
+ if typ.__name__ == "KeyError":
# probably just an old version, still loadable
- log.warning('Project file missing value: %s' % value)
+ log.warning("Project file missing value: %s" % value)
return
- if hasattr(loader, 'createNewProject'):
+ if hasattr(loader, "createNewProject"):
loader.createNewProject(prompt=False)
- msg = '%s: %s\n\n' % (typ.__name__, value)
+ msg = "%s: %s\n\n" % (typ.__name__, value)
msg += toolkit.formatTraceback(tb)
loader.showMessage(
msg="Project file '%s' is corrupted." % filepath,
showCancel=False,
- icon='Warning',
- detail=msg)
+ icon="Warning",
+ detail=msg,
+ )
self.openingProject = False
return False
def parseAvFile(self, filepath):
- '''
- Parses an avp (project) or avl (preset package) file.
- Returns dictionary with section names as the keys, each one
- contains a list of tuples: (compName, version, compPresetDict)
- '''
- log.debug('Parsing av file: %s', filepath)
- validSections = (
- 'Components',
- 'Settings',
- 'WindowFields'
- )
+ """
+ Parses an avp (project) or avl (preset package) file.
+ Returns dictionary with section names as the keys, each one
+ contains a list of tuples: (compName, version, compPresetDict)
+ """
+ log.debug("Parsing av file: %s", filepath)
+ validSections = ("Components", "Settings", "WindowFields")
data = {sect: [] for sect in validSections}
try:
- with open(filepath, 'r') as f:
+ with open(filepath, "r") as f:
+
def parseLine(line):
- '''Decides if a file line is a section header'''
+ """Decides if a file line is a section header"""
line = line.strip()
- newSection = ''
+ newSection = ""
- if line.startswith('[') and line.endswith(']') \
- and line[1:-1] in validSections:
+ if (
+ line.startswith("[")
+ and line.endswith("]")
+ and line[1:-1] in validSections
+ ):
newSection = line[1:-1]
return line, newSection
- section = ''
+ section = ""
i = 0
for line in f:
line, newSection = parseLine(line)
if newSection:
section = str(newSection)
continue
- if line and section == 'Components':
+ if line and section == "Components":
if i == 0:
lastCompName = str(line)
i += 1
@@ -301,14 +285,12 @@ class Core:
i += 1
elif i == 2:
lastCompPreset = toolkit.presetFromString(line)
- data[section].append((
- lastCompName,
- lastCompVers,
- lastCompPreset
- ))
+ data[section].append(
+ (lastCompName, lastCompVers, lastCompPreset)
+ )
i = 0
elif line and section:
- key, value = line.split('=', 1)
+ key, value = line.split("=", 1)
data[section].append((key, value.strip()))
return 0, data
@@ -319,51 +301,40 @@ class Core:
errcode, data = self.parseAvFile(filepath)
returnList = []
if errcode == 0:
- name, vers, preset = data['Components'][0]
- presetName = preset['preset'] \
- if preset['preset'] else os.path.basename(filepath)[:-4]
- newPath = os.path.join(
- Core.presetDir,
- name,
- vers,
- presetName
+ name, vers, preset = data["Components"][0]
+ presetName = (
+ preset["preset"]
+ if preset["preset"]
+ else os.path.basename(filepath)[:-4]
)
+ newPath = os.path.join(Core.presetDir, name, vers, presetName)
if os.path.exists(newPath):
return False, newPath
- preset['preset'] = presetName
- self.createPresetFile(
- name, vers, presetName, preset
- )
+ preset["preset"] = presetName
+ self.createPresetFile(name, vers, presetName, preset)
return True, presetName
elif errcode == 1:
# TODO: an error message
- return False, ''
+ return False, ""
def exportPreset(self, exportPath, compName, vers, origName):
- internalPath = os.path.join(
- Core.presetDir, compName, str(vers), origName
- )
+ internalPath = os.path.join(Core.presetDir, compName, str(vers), origName)
if not os.path.exists(internalPath):
return
if os.path.exists(exportPath):
os.remove(exportPath)
- with open(internalPath, 'r') as f:
+ with open(internalPath, "r") as f:
internalData = [line for line in f]
try:
saveValueStore = toolkit.presetFromString(internalData[0].strip())
- self.createPresetFile(
- compName, vers,
- origName, saveValueStore,
- exportPath
- )
+ self.createPresetFile(compName, vers, origName, saveValueStore, exportPath)
return True
except Exception:
return False
- def createPresetFile(
- self, compName, vers, presetName, saveValueStore, filepath=''):
- '''Create a preset file (.avl) at filepath using args.
- Or if filepath is empty, create an internal preset using args'''
+ def createPresetFile(self, compName, vers, presetName, saveValueStore, filepath=""):
+ """Create a preset file (.avl) at filepath using args.
+ Or if filepath is empty, create an internal preset using args"""
if not filepath:
dirname = os.path.join(Core.presetDir, compName, str(vers))
if not os.path.exists(dirname):
@@ -371,54 +342,55 @@ class Core:
filepath = os.path.join(dirname, presetName)
internal = True
else:
- if not filepath.endswith('.avl'):
- filepath += '.avl'
+ if not filepath.endswith(".avl"):
+ filepath += ".avl"
internal = False
- with open(filepath, 'w') as f:
+ with open(filepath, "w") as f:
if not internal:
- f.write('[Components]\n')
- f.write('%s\n' % compName)
- f.write('%s\n' % str(vers))
+ f.write("[Components]\n")
+ f.write("%s\n" % compName)
+ f.write("%s\n" % str(vers))
f.write(toolkit.presetToString(saveValueStore))
def createProjectFile(self, filepath, window=None):
- '''Create a project file (.avp) using the current program state'''
- log.info('Creating %s', filepath)
+ """Create a project file (.avp) using the current program state"""
+ log.info("Creating %s", filepath)
settingsKeys = [
- 'componentDir',
- 'inputDir',
- 'outputDir',
- 'presetDir',
- 'projectDir',
+ "componentDir",
+ "inputDir",
+ "outputDir",
+ "presetDir",
+ "projectDir",
]
try:
if not filepath.endswith(".avp"):
- filepath += '.avp'
+ filepath += ".avp"
if os.path.exists(filepath):
os.remove(filepath)
- with open(filepath, 'w') as f:
- f.write('[Components]\n')
+ with open(filepath, "w") as f:
+ f.write("[Components]\n")
for comp in self.selectedComponents:
saveValueStore = comp.savePreset()
- saveValueStore['preset'] = comp.currentPreset
- f.write('%s\n' % str(comp))
- f.write('%s\n' % str(comp.version))
- f.write('%s\n' % toolkit.presetToString(saveValueStore))
+ saveValueStore["preset"] = comp.currentPreset
+ f.write("%s\n" % str(comp))
+ f.write("%s\n" % str(comp.version))
+ f.write("%s\n" % toolkit.presetToString(saveValueStore))
- f.write('\n[Settings]\n')
+ f.write("\n[Settings]\n")
for key in Core.settings.allKeys():
if key in settingsKeys:
- f.write('%s=%s\n' % (key, Core.settings.value(key)))
+ f.write("%s=%s\n" % (key, Core.settings.value(key)))
if window:
- f.write('\n[WindowFields]\n')
+ f.write("\n[WindowFields]\n")
f.write(
- 'lineEdit_audioFile=%s\n'
- 'lineEdit_outputFile=%s\n' % (
+ "lineEdit_audioFile=%s\n"
+ "lineEdit_outputFile=%s\n"
+ % (
window.lineEdit_audioFile.text(),
- window.lineEdit_outputFile.text()
+ window.lineEdit_outputFile.text(),
)
)
return True
@@ -426,8 +398,9 @@ class Core:
return False
def newVideoWorker(self, loader, audioFile, outputPath):
- '''loader is MainWindow or Command object which must own the thread'''
+ """loader is MainWindow or Command object which must own the thread"""
from . import video_thread
+
self.videoThread = QtCore.QThread(loader)
videoWorker = video_thread.Worker(
loader, audioFile, outputPath, self.selectedComponents
@@ -450,18 +423,18 @@ class Core:
@classmethod
def storeSettings(cls):
- '''Store settings/paths to directories as class variables'''
+ """Store settings/paths to directories as class variables"""
from .__init__ import wd
from .toolkit.ffmpeg import findFfmpeg
cls.wd = wd
dataDir = QtCore.QStandardPaths.writableLocation(
- QtCore.QStandardPaths.AppConfigLocation
+ QtCore.QStandardPaths.StandardLocation.AppConfigLocation
)
# Windows: C:/Users/<USER>/AppData/Local/audio-visualizer
# macOS: ~/Library/Preferences/audio-visualizer
# Linux: ~/.config/audio-visualizer
- with open(os.path.join(wd, 'encoder-options.json')) as json_file:
+ with open(os.path.join(wd, "encoder-options.json")) as json_file:
encoderOptions = json.load(json_file)
# Locate FFmpeg
@@ -470,53 +443,60 @@ class Core:
print("Could not find FFmpeg")
settings = {
- 'canceled': False,
- 'FFMPEG_BIN': ffmpegBin,
- 'dataDir': dataDir,
- 'settings': QtCore.QSettings(
- os.path.join(dataDir, 'settings.ini'),
- QtCore.QSettings.IniFormat),
- 'presetDir': os.path.join(dataDir, 'presets'),
- 'componentsPath': os.path.join(wd, 'components'),
- 'junkStream': os.path.join(wd, 'gui', 'background.png'),
- 'encoderOptions': encoderOptions,
- 'resolutions': [
- '1920x1080',
- '1280x720',
- '854x480',
+ "canceled": False,
+ "FFMPEG_BIN": ffmpegBin,
+ "dataDir": dataDir,
+ "settings": QtCore.QSettings(
+ os.path.join(dataDir, "settings.ini"),
+ QtCore.QSettings.Format.IniFormat,
+ ),
+ "presetDir": os.path.join(dataDir, "presets"),
+ "componentsPath": os.path.join(wd, "components"),
+ "junkStream": os.path.join(wd, "gui", "background.png"),
+ "encoderOptions": encoderOptions,
+ "resolutions": [
+ "1920x1080",
+ "1280x720",
+ "854x480",
],
- 'logDir': os.path.join(dataDir, 'log'),
- 'logEnabled': False,
- 'previewEnabled': True,
+ "logDir": os.path.join(dataDir, "log"),
+ "logEnabled": False,
+ "previewEnabled": True,
}
- settings['videoFormats'] = toolkit.appendUppercase([
- '*.mp4',
- '*.mov',
- '*.mkv',
- '*.avi',
- '*.webm',
- '*.flv',
- ])
- settings['audioFormats'] = toolkit.appendUppercase([
- '*.mp3',
- '*.wav',
- '*.ogg',
- '*.fla',
- '*.flac',
- '*.aac',
- ])
- settings['imageFormats'] = toolkit.appendUppercase([
- '*.png',
- '*.jpg',
- '*.tif',
- '*.tiff',
- '*.gif',
- '*.bmp',
- '*.ico',
- '*.xbm',
- '*.xpm',
- ])
+ settings["videoFormats"] = toolkit.appendUppercase(
+ [
+ "*.mp4",
+ "*.mov",
+ "*.mkv",
+ "*.avi",
+ "*.webm",
+ "*.flv",
+ ]
+ )
+ settings["audioFormats"] = toolkit.appendUppercase(
+ [
+ "*.mp3",
+ "*.wav",
+ "*.ogg",
+ "*.fla",
+ "*.flac",
+ "*.aac",
+ ]
+ )
+ settings["imageFormats"] = toolkit.appendUppercase(
+ [
+ "*.png",
+ "*.jpg",
+ "*.tif",
+ "*.tiff",
+ "*.gif",
+ "*.bmp",
+ "*.ico",
+ "*.xbm",
+ "*.xpm",
+ ]
+ )
# Register all settings as class variables
for classvar, val in settings.items():
@@ -526,7 +506,10 @@ class Core:
if not os.path.exists(cls.dataDir):
os.makedirs(cls.dataDir)
for neededDirectory in (
- cls.presetDir, cls.logDir, cls.settings.value("projectDir")):
+ cls.presetDir,
+ cls.logDir,
+ cls.settings.value("projectDir"),
+ ):
if not os.path.exists(neededDirectory):
os.mkdir(neededDirectory)
cls.makeLogger(deleteOldLogs=True)
@@ -546,7 +529,7 @@ class Core:
"outputPreset": "medium",
"outputFormat": "mp4",
"outputContainer": "MP4",
- "projectDir": os.path.join(cls.dataDir, 'projects'),
+ "projectDir": os.path.join(cls.dataDir, "projects"),
"pref_insertCompAtTop": True,
"pref_genericPreview": True,
"pref_undoLimit": 10,
@@ -559,15 +542,15 @@ class Core:
# Allow manual editing of prefs. (Surprisingly necessary as Qt seems to
# store True as 'true' but interprets a manually-added 'true' as str.)
for key in cls.settings.allKeys():
- if not key.startswith('pref_'):
+ if not key.startswith("pref_"):
continue
val = cls.settings.value(key)
try:
val = int(val)
except ValueError:
- if val == 'true':
+ if val == "true":
val = True
- elif val == 'false':
+ elif val == "false":
val = False
cls.settings.setValue(key, val)
@@ -576,18 +559,16 @@ class Core:
# send critical log messages to stdout
logStream = logging.StreamHandler()
logStream.setLevel(STDOUT_LOGLVL)
- streamFormatter = logging.Formatter(
- '<%(name)s> %(levelname)s: %(message)s'
- )
+ streamFormatter = logging.Formatter("<%(name)s> %(levelname)s: %(message)s")
logStream.setFormatter(streamFormatter)
- log = logging.getLogger('AVP')
+ log = logging.getLogger("AVP")
log.addHandler(logStream)
if FILE_LOGLVL is not None:
# write log files as well!
Core.logEnabled = True
- logFilename = os.path.join(Core.logDir, 'avp_debug.log')
- libLogFilename = os.path.join(Core.logDir, 'global_debug.log')
+ logFilename = os.path.join(Core.logDir, "avp_debug.log")
+ libLogFilename = os.path.join(Core.logDir, "global_debug.log")
if deleteOldLogs:
for log_ in (logFilename, libLogFilename):
@@ -599,8 +580,8 @@ class Core:
libLogFile = logging.FileHandler(libLogFilename, delay=True)
libLogFile.setLevel(FILE_LIBLOGLVL)
fileFormatter = logging.Formatter(
- '[%(asctime)s] %(threadName)-10.10s %(name)-23.23s %(levelname)s: '
- '%(message)s'
+ "[%(asctime)s] %(threadName)-10.10s %(name)-23.23s %(levelname)s: "
+ "%(message)s"
)
logFile.setFormatter(fileFormatter)
libLogFile.setFormatter(fileFormatter)
@@ -611,5 +592,6 @@ class Core:
# lowest level must be explicitly set on the root Logger
libLog.setLevel(0)
+
# always store settings in class variables even if a Core object is not created
Core.storeSettings()
diff --git a/src/gui/actions.py b/src/gui/actions.py
index afb980a..654b2a0 100644
--- a/src/gui/actions.py
+++ b/src/gui/actions.py
@@ -1,53 +1,61 @@
-'''
- QCommand classes for every undoable user action performed in the MainWindow
-'''
-from PyQt5.QtWidgets import QUndoCommand
+"""
+QCommand classes for every undoable user action performed in the MainWindow
+"""
+
+from PyQt6.QtGui import QUndoCommand
import os
+import logging
from copy import copy
from ..core import Core
+log = logging.getLogger("AVP.Gui.Actions")
+
+
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# COMPONENT ACTIONS
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+
class AddComponent(QUndoCommand):
def __init__(self, parent, compI, moduleI):
super().__init__(
- "create new %s component" %
- parent.core.modules[moduleI].Component.name
+ "create new %s component" % parent.core.modules[moduleI].Component.name
)
self.parent = parent
self.moduleI = moduleI
self.compI = compI
self.comp = None
+ self.valid = True
def redo(self):
if self.comp is None:
- self.parent.core.insertComponent(
- self.compI, self.moduleI, self.parent)
+ i = self.parent.core.insertComponent(self.compI, self.moduleI, self.parent)
+ if i != self.compI:
+ self.valid = False
+ if i is not None:
+ log.error(
+ f"Expected new component index to be {self.compI} but received {i}"
+ )
else:
# inserting previously-created component
- self.parent.core.insertComponent(
- self.compI, self.comp, self.parent)
+ self.parent.core.insertComponent(self.compI, self.comp, self.parent)
def undo(self):
+ if not self.valid:
+ return
self.comp = self.parent.core.selectedComponents[self.compI]
self.parent._removeComponent(self.compI)
class RemoveComponent(QUndoCommand):
def __init__(self, parent, selectedRows):
- super().__init__('remove component')
+ super().__init__("remove component")
self.parent = parent
componentList = self.parent.listWidget_componentList
- self.selectedRows = [
- componentList.row(selected) for selected in selectedRows
- ]
- self.components = [
- parent.core.selectedComponents[i] for i in self.selectedRows
- ]
+ self.selectedRows = [componentList.row(selected) for selected in selectedRows]
+ self.components = [parent.core.selectedComponents[i] for i in self.selectedRows]
def redo(self):
self.parent._removeComponent(self.selectedRows[0])
@@ -55,9 +63,7 @@ class RemoveComponent(QUndoCommand):
def undo(self):
componentList = self.parent.listWidget_componentList
for index, comp in zip(self.selectedRows, self.components):
- self.parent.core.insertComponent(
- index, comp, self.parent
- )
+ self.parent.core.insertComponent(index, comp, self.parent)
self.parent.drawPreview()
@@ -70,7 +76,7 @@ class MoveComponent(QUndoCommand):
self.id_ = ord(tag[0])
def id(self):
- '''If 2 consecutive updates have same id, Qt will call mergeWith()'''
+ """If 2 consecutive updates have same id, Qt will call mergeWith()"""
return self.id_
def mergeWith(self, other):
@@ -105,6 +111,7 @@ class MoveComponent(QUndoCommand):
# PRESET ACTIONS
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+
class ClearPreset(QUndoCommand):
def __init__(self, parent, compI):
super().__init__("clear preset")
@@ -112,7 +119,7 @@ class ClearPreset(QUndoCommand):
self.compI = compI
self.component = self.parent.core.selectedComponents[compI]
self.store = self.component.savePreset()
- self.store['preset'] = self.component.currentPreset
+ self.store["preset"] = self.component.currentPreset
def redo(self):
self.parent.core.clearPreset(self.compI)
@@ -132,20 +139,19 @@ class OpenPreset(QUndoCommand):
comp = self.parent.core.selectedComponents[compI]
self.store = comp.savePreset()
- self.store['preset'] = copy(comp.currentPreset)
+ self.store["preset"] = copy(comp.currentPreset)
def redo(self):
self.parent._openPreset(self.presetName, self.compI)
def undo(self):
- self.parent.core.selectedComponents[self.compI].loadPreset(
- self.store)
+ self.parent.core.selectedComponents[self.compI].loadPreset(self.store)
self.parent.parent.updateComponentTitle(self.compI, self.store)
class RenamePreset(QUndoCommand):
def __init__(self, parent, path, oldName, newName):
- super().__init__('rename preset')
+ super().__init__("rename preset")
self.parent = parent
self.path = path
self.oldName = oldName
@@ -162,14 +168,13 @@ class DeletePreset(QUndoCommand):
def __init__(self, parent, compName, vers, presetFile):
self.parent = parent
self.preset = (compName, vers, presetFile)
- self.path = os.path.join(
- Core.presetDir, compName, str(vers), presetFile
- )
+ self.path = os.path.join(Core.presetDir, compName, str(vers), presetFile)
self.store = self.parent.core.getPreset(self.path)
- self.presetName = self.store['preset']
- super().__init__('delete %s preset (%s)' % (self.presetName, compName))
+ self.presetName = self.store["preset"]
+ super().__init__("delete %s preset (%s)" % (self.presetName, compName))
self.loadedPresets = [
- i for i, comp in enumerate(self.parent.core.selectedComponents)
+ i
+ for i, comp in enumerate(self.parent.core.selectedComponents)
if self.presetName == str(comp.currentPreset)
]
diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py
index 159dc02..b0a564b 100644
--- a/src/gui/mainwindow.py
+++ b/src/gui/mainwindow.py
@@ -1,11 +1,13 @@
-'''
- When using GUI mode, this module's object (the main window) takes
- user input to construct a program state (stored in the Core object).
- This shows a preview of the video being created and allows for saving
- projects and exporting the video at a later time.
-'''
-from PyQt5 import QtCore, QtWidgets, uic
-import PyQt5.QtWidgets as QtWidgets
+"""
+When using GUI mode, this module's object (the main window) takes
+user input to construct a program state (stored in the Core object).
+This shows a preview of the video being created and allows for saving
+projects and exporting the video at a later time.
+"""
+
+from PyQt6 import QtCore, QtWidgets, uic
+import PyQt6.QtWidgets as QtWidgets
+from PyQt6.QtGui import QUndoStack, QShortcut
from PIL import Image
from queue import Queue
import sys
@@ -21,46 +23,59 @@ from .preview_win import PreviewWindow
from .presetmanager import PresetManager
from .actions import *
from ..toolkit import (
- disableWhenEncoding, disableWhenOpeningProject, checkOutput, blockSignals
+ disableWhenEncoding,
+ disableWhenOpeningProject,
+ checkOutput,
+ blockSignals,
)
-appName = 'Audio Visualizer'
-log = logging.getLogger('AVP.Gui.MainWindow')
+appName = "Audio Visualizer"
+log = logging.getLogger("AVP.Gui.MainWindow")
+
+
+class MyQUndoStack(QUndoStack):
+ # FIXME move this class
+ @property
+ def encoding(self):
+ return self.parent().encoding
+
+ @disableWhenEncoding
+ def undo(self, *args, **kwargs):
+ super().undo(*args, **kwargs)
+
+ @disableWhenEncoding
+ def redo(self, *args, **kwargs):
+ super().redo(*args, **kwargs)
class MainWindow(QtWidgets.QMainWindow):
- '''
- The MainWindow wraps many Core methods in order to update the GUI
- accordingly. E.g., instead of self.core.openProject(), it will use
- self.openProject() and update the window titlebar within the wrapper.
+ """
+ The MainWindow wraps many Core methods in order to update the GUI
+ accordingly. E.g., instead of self.core.openProject(), it will use
+ self.openProject() and update the window titlebar within the wrapper.
- MainWindow manages the autosave feature, although Core has the
- primary functions for opening and creating project files.
- '''
+ MainWindow manages the autosave feature, although Core has the
+ primary functions for opening and creating project files.
+ """
createVideo = QtCore.pyqtSignal()
newTask = QtCore.pyqtSignal(list) # for the preview window
processTask = QtCore.pyqtSignal()
- def __init__(self, project):
+ def __init__(self, project, dpi):
super().__init__()
- log.debug(
- 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId())))
+ log.debug("Main thread id: {}".format(int(QtCore.QThread.currentThreadId())))
uic.loadUi(os.path.join(Core.wd, "gui", "mainwindow.ui"), self)
- desk = QtWidgets.QDesktopWidget()
- dpi = desk.physicalDpiX()
- log.info("Detected screen DPI: %s", dpi)
-
- self.resize(
- int(self.width() *
- (dpi / 144)),
- int(self.height() *
- (dpi / 144))
- )
+
+ if dpi:
+ self.resize(
+ int(self.width() * (dpi / 144)),
+ int(self.height() * (dpi / 144)),
+ )
self.core = Core()
- Core.mode = 'GUI'
+ Core.mode = "GUI"
# widgets of component settings
self.pages = []
self.lastAutosave = time.time()
@@ -72,15 +87,13 @@ class MainWindow(QtWidgets.QMainWindow):
# Find settings created by Core object
self.dataDir = Core.dataDir
self.presetDir = Core.presetDir
- self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
+ self.autosavePath = os.path.join(self.dataDir, "autosave.avp")
self.settings = Core.settings
# Create stack of undoable user actions
- self.undoStack = QtWidgets.QUndoStack(self)
+ self.undoStack = MyQUndoStack(self)
undoLimit = self.settings.value("pref_undoLimit")
self.undoStack.setUndoLimit(undoLimit)
- self.undoStack.undo = disableWhenEncoding(self.undoStack.undo)
- self.undoStack.redo = disableWhenEncoding(self.undoStack.redo)
# Create Undo Dialog - A standard QUndoView on a standard QDialog
self.undoDialog = QtWidgets.QDialog(self)
@@ -94,18 +107,17 @@ class MainWindow(QtWidgets.QMainWindow):
self.presetManager = PresetManager(self)
# Create the preview window and its thread, queues, and timers
- log.debug('Creating preview window')
- self.previewWindow = PreviewWindow(self, os.path.join(
- Core.wd, 'gui', "background.png"))
+ log.debug("Creating preview window")
+ self.previewWindow = PreviewWindow(
+ self, os.path.join(Core.wd, "gui", "background.png")
+ )
self.verticalLayout_previewWrapper.addWidget(self.previewWindow)
- log.debug('Starting preview thread')
+ log.debug("Starting preview thread")
self.previewQueue = Queue()
self.previewThread = QtCore.QThread(self)
self.previewWorker = preview_thread.Worker(
- self.core,
- self.settings,
- self.previewQueue
+ self.core, self.settings, self.previewQueue
)
self.previewWorker.moveToThread(self.previewThread)
self.newTask.connect(self.previewWorker.createPreviewImage)
@@ -113,12 +125,12 @@ class MainWindow(QtWidgets.QMainWindow):
self.previewWorker.error.connect(self.previewWindow.threadError)
self.previewWorker.imageCreated.connect(self.showPreviewImage)
self.previewThread.start()
- self.previewThread.finished.connect(lambda: log.info('Preview thread finished.'))
+ self.previewThread.finished.connect(
+ lambda: log.info("Preview thread finished.")
+ )
timeout = 500
- log.debug(
- 'Preview timer set to trigger when idle for %sms' % str(timeout)
- )
+ log.debug("Preview timer set to trigger when idle for %sms" % str(timeout))
self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self.processTask.emit)
self.timer.start(timeout)
@@ -128,7 +140,7 @@ class MainWindow(QtWidgets.QMainWindow):
# Undo Feature
def toggleUndoButtonEnabled(*_):
- """ Enable/disable undo button depending on whether UndoStack contains Actions """
+ """Enable/disable undo button depending on whether UndoStack contains Actions"""
try:
undoButton.setEnabled(self.undoStack.count())
except RuntimeError:
@@ -138,50 +150,41 @@ class MainWindow(QtWidgets.QMainWindow):
style = self.pushButton_undo.style()
undoButton = self.pushButton_undo
undoButton.setIcon(
- style.standardIcon(QtWidgets.QStyle.SP_FileDialogBack)
+ style.standardIcon(QtWidgets.QStyle.StandardPixmap.SP_FileDialogBack)
)
undoButton.clicked.connect(self.undoStack.undo)
undoButton.setEnabled(False)
self.undoStack.cleanChanged.connect(toggleUndoButtonEnabled)
self.undoMenu = QtWidgets.QMenu()
- self.undoMenu.addAction(
- self.undoStack.createUndoAction(self)
- )
- self.undoMenu.addAction(
- self.undoStack.createRedoAction(self)
- )
- action = self.undoMenu.addAction('Show History...')
- action.triggered.connect(
- lambda _: self.showUndoStack()
- )
+ self.undoMenu.addAction(self.undoStack.createUndoAction(self))
+ self.undoMenu.addAction(self.undoStack.createRedoAction(self))
+ action = self.undoMenu.addAction("Show History...")
+ action.triggered.connect(lambda _: self.showUndoStack())
undoButton.setMenu(self.undoMenu)
# end of Undo Feature
style = self.pushButton_listMoveUp.style()
self.pushButton_listMoveUp.setIcon(
- style.standardIcon(QtWidgets.QStyle.SP_ArrowUp)
+ style.standardIcon(QtWidgets.QStyle.StandardPixmap.SP_ArrowUp)
)
style = self.pushButton_listMoveDown.style()
self.pushButton_listMoveDown.setIcon(
- style.standardIcon(QtWidgets.QStyle.SP_ArrowDown)
+ style.standardIcon(QtWidgets.QStyle.StandardPixmap.SP_ArrowDown)
)
style = self.pushButton_removeComponent.style()
self.pushButton_removeComponent.setIcon(
- style.standardIcon(QtWidgets.QStyle.SP_DialogDiscardButton)
+ style.standardIcon(QtWidgets.QStyle.StandardPixmap.SP_DialogDiscardButton)
)
- if sys.platform == 'darwin':
- log.debug(
- 'Darwin detected: showing progress label below progress bar')
+ if sys.platform == "darwin":
+ log.debug("Darwin detected: showing progress label below progress bar")
self.progressBar_createVideo.setTextVisible(False)
else:
self.progressLabel.setHidden(True)
- self.toolButton_selectAudioFile.clicked.connect(
- self.openInputFileDialog)
+ self.toolButton_selectAudioFile.clicked.connect(self.openInputFileDialog)
- self.toolButton_selectOutputFile.clicked.connect(
- self.openOutputFileDialog)
+ self.toolButton_selectOutputFile.clicked.connect(self.openOutputFileDialog)
def changedField():
self.autosave()
@@ -192,43 +195,36 @@ class MainWindow(QtWidgets.QMainWindow):
self.progressBar_createVideo.setValue(0)
- self.pushButton_createVideo.clicked.connect(
- self.createAudioVisualization)
+ self.pushButton_createVideo.clicked.connect(self.createAudioVisualization)
self.pushButton_Cancel.clicked.connect(self.stopVideo)
- for i, container in enumerate(Core.encoderOptions['containers']):
- self.comboBox_videoContainer.addItem(container['name'])
- if container['name'] == self.settings.value('outputContainer'):
+ for i, container in enumerate(Core.encoderOptions["containers"]):
+ self.comboBox_videoContainer.addItem(container["name"])
+ if container["name"] == self.settings.value("outputContainer"):
selectedContainer = i
self.comboBox_videoContainer.setCurrentIndex(selectedContainer)
- self.comboBox_videoContainer.currentIndexChanged.connect(
- self.updateCodecs
- )
+ self.comboBox_videoContainer.currentIndexChanged.connect(self.updateCodecs)
self.updateCodecs()
for i in range(self.comboBox_videoCodec.count()):
codec = self.comboBox_videoCodec.itemText(i)
- if codec == self.settings.value('outputVideoCodec'):
+ if codec == self.settings.value("outputVideoCodec"):
self.comboBox_videoCodec.setCurrentIndex(i)
for i in range(self.comboBox_audioCodec.count()):
codec = self.comboBox_audioCodec.itemText(i)
- if codec == self.settings.value('outputAudioCodec'):
+ if codec == self.settings.value("outputAudioCodec"):
self.comboBox_audioCodec.setCurrentIndex(i)
- self.comboBox_videoCodec.currentIndexChanged.connect(
- self.updateCodecSettings
- )
+ self.comboBox_videoCodec.currentIndexChanged.connect(self.updateCodecSettings)
- self.comboBox_audioCodec.currentIndexChanged.connect(
- self.updateCodecSettings
- )
+ self.comboBox_audioCodec.currentIndexChanged.connect(self.updateCodecSettings)
- vBitrate = int(self.settings.value('outputVideoBitrate'))
- aBitrate = int(self.settings.value('outputAudioBitrate'))
+ vBitrate = int(self.settings.value("outputVideoBitrate"))
+ aBitrate = int(self.settings.value("outputAudioBitrate"))
self.spinBox_vBitrate.setValue(vBitrate)
self.spinBox_aBitrate.setValue(aBitrate)
@@ -239,30 +235,27 @@ class MainWindow(QtWidgets.QMainWindow):
self.compMenu = QtWidgets.QMenu()
for i, comp in enumerate(self.core.modules):
action = self.compMenu.addAction(comp.Component.name)
- action.triggered.connect(
- lambda _, item=i: self.addComponent(0, item)
- )
+ action.triggered.connect(lambda _, item=i: self.addComponent(0, item))
self.pushButton_addComponent.setMenu(self.compMenu)
componentList.dropEvent = self.dragComponent
- componentList.itemSelectionChanged.connect(
- self.changeComponentWidget
- )
+ componentList.itemSelectionChanged.connect(self.changeComponentWidget)
componentList.itemSelectionChanged.connect(
self.presetManager.clearPresetListSelection
)
- self.pushButton_removeComponent.clicked.connect(
- lambda: self.removeComponent()
- )
+ self.pushButton_removeComponent.clicked.connect(lambda: self.removeComponent())
- componentList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
- componentList.customContextMenuRequested.connect(
- self.componentContextMenu
+ componentList.setContextMenuPolicy(
+ QtCore.Qt.ContextMenuPolicy.CustomContextMenu
)
+ componentList.customContextMenuRequested.connect(self.componentContextMenu)
- currentRes = str(self.settings.value('outputWidth'))+'x' + \
- str(self.settings.value('outputHeight'))
+ currentRes = (
+ str(self.settings.value("outputWidth"))
+ + "x"
+ + str(self.settings.value("outputHeight"))
+ )
for i, res in enumerate(Core.resolutions):
self.comboBox_resolution.addItem(res)
if res == currentRes:
@@ -272,24 +265,14 @@ class MainWindow(QtWidgets.QMainWindow):
self.updateResolution
)
- self.pushButton_listMoveUp.clicked.connect(
- lambda: self.moveComponent(-1)
- )
- self.pushButton_listMoveDown.clicked.connect(
- lambda: self.moveComponent(1)
- )
+ self.pushButton_listMoveUp.clicked.connect(lambda: self.moveComponent(-1))
+ self.pushButton_listMoveDown.clicked.connect(lambda: self.moveComponent(1))
# Configure the Projects Menu
self.projectMenu = QtWidgets.QMenu()
- self.menuButton_newProject = self.projectMenu.addAction(
- "New Project"
- )
- self.menuButton_newProject.triggered.connect(
- lambda: self.createNewProject()
- )
- self.menuButton_openProject = self.projectMenu.addAction(
- "Open Project"
- )
+ self.menuButton_newProject = self.projectMenu.addAction("New Project")
+ self.menuButton_newProject.triggered.connect(lambda: self.createNewProject())
+ self.menuButton_openProject = self.projectMenu.addAction("Open Project")
self.menuButton_openProject.triggered.connect(
lambda: self.openOpenProjectDialog()
)
@@ -303,22 +286,18 @@ class MainWindow(QtWidgets.QMainWindow):
self.pushButton_projects.setMenu(self.projectMenu)
# Configure the Presets Button
- self.pushButton_presets.clicked.connect(
- self.openPresetManager
- )
+ self.pushButton_presets.clicked.connect(self.openPresetManager)
self.updateWindowTitle()
- log.debug('Showing main window')
+ log.debug("Showing main window")
self.show()
if project and project != self.autosavePath:
- if not project.endswith('.avp'):
- project += '.avp'
+ if not project.endswith(".avp"):
+ project += ".avp"
# open a project from the commandline
if not os.path.dirname(project):
- project = os.path.join(
- self.settings.value("projectDir"), project
- )
+ project = os.path.join(self.settings.value("projectDir"), project)
self.currentProject = project
self.settings.setValue("currentProject", project)
if os.path.exists(self.autosavePath):
@@ -335,7 +314,8 @@ class MainWindow(QtWidgets.QMainWindow):
ch = self.showMessage(
msg="Restore unsaved changes in project '%s'?"
% os.path.basename(self.currentProject)[:-4],
- showCancel=True)
+ showCancel=True,
+ )
if ch:
self.saveProjectChanges()
else:
@@ -352,16 +332,16 @@ class MainWindow(QtWidgets.QMainWindow):
msg="FFmpeg could not be found. This is a critical error. "
"Install FFmpeg, or download it and place the program executable "
"in the same folder as this program.",
- icon='Critical'
+ icon="Critical",
)
else:
if not self.settings.value("ffmpegMsgShown"):
try:
with open(os.devnull, "w") as f:
ffmpegVers = checkOutput(
- [self.core.FFMPEG_BIN, '-version'], stderr=f
+ [self.core.FFMPEG_BIN, "-version"], stderr=f
)
- goodVersion = str(ffmpegVers).split()[2].startswith('4')
+ goodVersion = str(ffmpegVers).split()[2].startswith("4")
except Exception:
goodVersion = False
else:
@@ -375,70 +355,61 @@ class MainWindow(QtWidgets.QMainWindow):
self.settings.setValue("ffmpegMsgShown", True)
# Hotkeys for projects
- QtWidgets.QShortcut("Ctrl+S", self, self.saveCurrentProject)
- QtWidgets.QShortcut("Ctrl+A", self, self.openSaveProjectDialog)
- QtWidgets.QShortcut("Ctrl+O", self, self.openOpenProjectDialog)
- QtWidgets.QShortcut("Ctrl+N", self, self.createNewProject)
+
+ QShortcut("Ctrl+S", self, self.saveCurrentProject)
+ QShortcut("Ctrl+A", self, self.openSaveProjectDialog)
+ QShortcut("Ctrl+O", self, self.openOpenProjectDialog)
+ QShortcut("Ctrl+N", self, self.createNewProject)
# Hotkeys for undo/redo
- QtWidgets.QShortcut("Ctrl+Z", self, self.undoStack.undo)
- QtWidgets.QShortcut("Ctrl+Y", self, self.undoStack.redo)
- QtWidgets.QShortcut("Ctrl+Shift+Z", self, self.undoStack.redo)
+ QShortcut("Ctrl+Z", self, self.undoStack.undo)
+ QShortcut("Ctrl+Y", self, self.undoStack.redo)
+ QShortcut("Ctrl+Shift+Z", self, self.undoStack.redo)
# Hotkeys for component list
- for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert):
- QtWidgets.QShortcut(
- inskey, self,
- activated=lambda: self.pushButton_addComponent.click()
- )
- for delkey in ("Ctrl+R", QtCore.Qt.Key_Delete):
- QtWidgets.QShortcut(
- delkey, self.listWidget_componentList,
- self.removeComponent
+ for inskey in ("Ctrl+T", QtCore.Qt.Key.Key_Insert):
+ QShortcut(
+ inskey,
+ self,
+ activated=lambda: self.pushButton_addComponent.click(),
)
- QtWidgets.QShortcut(
- "Ctrl+Space", self,
- activated=lambda: self.listWidget_componentList.setFocus()
- )
- QtWidgets.QShortcut(
- "Ctrl+Shift+S", self,
- self.presetManager.openSavePresetDialog
- )
- QtWidgets.QShortcut(
- "Ctrl+Shift+C", self, self.presetManager.clearPreset
+ for delkey in ("Ctrl+R", QtCore.Qt.Key.Key_Delete):
+ QShortcut(delkey, self.listWidget_componentList, self.removeComponent)
+ QShortcut(
+ "Ctrl+Space",
+ self,
+ activated=lambda: self.listWidget_componentList.setFocus(),
)
+ QShortcut("Ctrl+Shift+S", self, self.presetManager.openSavePresetDialog)
+ QShortcut("Ctrl+Shift+C", self, self.presetManager.clearPreset)
- QtWidgets.QShortcut(
- "Ctrl+Up", self.listWidget_componentList,
- activated=lambda: self.moveComponent(-1)
+ QShortcut(
+ "Ctrl+Up",
+ self.listWidget_componentList,
+ activated=lambda: self.moveComponent(-1),
)
- QtWidgets.QShortcut(
- "Ctrl+Down", self.listWidget_componentList,
- activated=lambda: self.moveComponent(1)
+ QShortcut(
+ "Ctrl+Down",
+ self.listWidget_componentList,
+ activated=lambda: self.moveComponent(1),
)
- QtWidgets.QShortcut(
- "Ctrl+Home", self.listWidget_componentList,
- activated=lambda: self.moveComponent('top')
+ QShortcut(
+ "Ctrl+Home",
+ self.listWidget_componentList,
+ activated=lambda: self.moveComponent("top"),
)
- QtWidgets.QShortcut(
- "Ctrl+End", self.listWidget_componentList,
- activated=lambda: self.moveComponent('bottom')
+ QShortcut(
+ "Ctrl+End",
+ self.listWidget_componentList,
+ activated=lambda: self.moveComponent("bottom"),
)
- QtWidgets.QShortcut(
- "Ctrl+Shift+F", self, self.showFfmpegCommand
- )
- QtWidgets.QShortcut(
- "Ctrl+Shift+U", self, self.showUndoStack
- )
+ QShortcut("Ctrl+Shift+F", self, self.showFfmpegCommand)
+ QShortcut("Ctrl+Shift+U", self, self.showUndoStack)
if log.isEnabledFor(logging.DEBUG):
- QtWidgets.QShortcut(
- "Ctrl+Alt+Shift+R", self, self.drawPreview
- )
- QtWidgets.QShortcut(
- "Ctrl+Alt+Shift+A", self, lambda: log.debug(repr(self))
- )
+ QShortcut("Ctrl+Alt+Shift+R", self, self.drawPreview)
+ QShortcut("Ctrl+Alt+Shift+A", self, lambda: log.debug(repr(self)))
# Close MainWindow when receiving Ctrl+C from terminal
signal.signal(signal.SIGINT, lambda *args: self.close())
@@ -450,18 +421,27 @@ class MainWindow(QtWidgets.QMainWindow):
def __repr__(self):
return (
- '%s\n'
- '\n%s\n'
- '#####\n'
- 'Preview thread is %s\n' % (
+ "%s\n"
+ "\n%s\n"
+ "#####\n"
+ "Preview thread is %s\n"
+ % (
super().__repr__(),
- "core not initialized" if not hasattr(self, "core") else repr(self.core),
- 'live' if hasattr(self, "previewThread") and self.previewThread.isRunning() else 'dead',
+ (
+ "core not initialized"
+ if not hasattr(self, "core")
+ else repr(self.core)
+ ),
+ (
+ "live"
+ if hasattr(self, "previewThread") and self.previewThread.isRunning()
+ else "dead"
+ ),
)
)
def closeEvent(self, event):
- log.info('Ending the preview thread')
+ log.info("Ending the preview thread")
self.timer.stop()
self.previewThread.quit()
self.previewThread.wait()
@@ -473,11 +453,11 @@ class MainWindow(QtWidgets.QMainWindow):
windowTitle = appName
try:
if self.currentProject:
- windowTitle += ' - %s' % \
- os.path.splitext(
- os.path.basename(self.currentProject))[0]
+ windowTitle += (
+ " - %s" % os.path.splitext(os.path.basename(self.currentProject))[0]
+ )
if self.autosaveExists(identical=False):
- windowTitle += '*'
+ windowTitle += "*"
except AttributeError:
pass
log.verbose(f'Window title is "{windowTitle}"')
@@ -485,38 +465,38 @@ class MainWindow(QtWidgets.QMainWindow):
@QtCore.pyqtSlot(int, dict)
def updateComponentTitle(self, pos, presetStore=False):
- '''
- Sets component title to modified or unmodified when given boolean.
- If given a preset dict, compares it against the component to
- determine if it is modified.
- A component with no preset is always unmodified.
- '''
+ """
+ Sets component title to modified or unmodified when given boolean.
+ If given a preset dict, compares it against the component to
+ determine if it is modified.
+ A component with no preset is always unmodified.
+ """
if type(presetStore) is dict:
- name = presetStore['preset']
+ name = presetStore["preset"]
if name is None or name not in self.core.savedPresets:
modified = False
else:
- modified = (presetStore != self.core.savedPresets[name])
+ modified = presetStore != self.core.savedPresets[name]
modified = bool(presetStore)
if pos < 0:
- pos = len(self.core.selectedComponents)-1
+ pos = len(self.core.selectedComponents) - 1
name = self.core.selectedComponents[pos].name
title = str(name)
if self.core.selectedComponents[pos].currentPreset:
- title += ' - %s' % self.core.selectedComponents[pos].currentPreset
+ title += " - %s" % self.core.selectedComponents[pos].currentPreset
if modified:
- title += '*'
+ title += "*"
if type(presetStore) is bool:
log.debug(
- 'Forcing %s #%s\'s modified status to %s: %s',
- name, pos, modified, title
+ "Forcing %s #%s's modified status to %s: %s",
+ name,
+ pos,
+ modified,
+ title,
)
else:
- log.debug(
- 'Setting %s #%s\'s title: %s',
- name, pos, title
- )
+ log.debug("Setting %s #%s's title: %s", name, pos, title)
self.listWidget_componentList.item(pos).setText(title)
def updateCodecs(self):
@@ -525,20 +505,20 @@ class MainWindow(QtWidgets.QMainWindow):
aCodecWidget = self.comboBox_audioCodec
index = containerWidget.currentIndex()
name = containerWidget.itemText(index)
- self.settings.setValue('outputContainer', name)
+ self.settings.setValue("outputContainer", name)
vCodecWidget.clear()
aCodecWidget.clear()
- for container in Core.encoderOptions['containers']:
- if container['name'] == name:
- for vCodec in container['video-codecs']:
+ for container in Core.encoderOptions["containers"]:
+ if container["name"] == name:
+ for vCodec in container["video-codecs"]:
vCodecWidget.addItem(vCodec)
- for aCodec in container['audio-codecs']:
+ for aCodec in container["audio-codecs"]:
aCodecWidget.addItem(aCodec)
def updateCodecSettings(self):
- '''Updates settings.ini to match encoder option widgets'''
+ """Updates settings.ini to match encoder option widgets"""
vCodecWidget = self.comboBox_videoCodec
vBitrateWidget = self.spinBox_vBitrate
aBitrateWidget = self.spinBox_aBitrate
@@ -549,10 +529,10 @@ class MainWindow(QtWidgets.QMainWindow):
currentAudioCodec = aCodecWidget.currentIndex()
currentAudioCodec = aCodecWidget.itemText(currentAudioCodec)
currentAudioBitrate = aBitrateWidget.value()
- self.settings.setValue('outputVideoCodec', currentVideoCodec)
- self.settings.setValue('outputAudioCodec', currentAudioCodec)
- self.settings.setValue('outputVideoBitrate', currentVideoBitrate)
- self.settings.setValue('outputAudioBitrate', currentAudioBitrate)
+ self.settings.setValue("outputVideoCodec", currentVideoCodec)
+ self.settings.setValue("outputAudioCodec", currentAudioCodec)
+ self.settings.setValue("outputVideoBitrate", currentVideoBitrate)
+ self.settings.setValue("outputAudioBitrate", currentAudioBitrate)
@disableWhenOpeningProject
def autosave(self, force=False):
@@ -567,54 +547,54 @@ class MainWindow(QtWidgets.QMainWindow):
# curve up to 5 seconds cooldown and maintains that for 30 secs
# if a component is continuously updated
timeDiff = self.lastAutosave - self.autosaveTimes.pop()
- if not force and timeDiff >= 1.0 \
- and timeDiff <= 10.0:
+ if not force and timeDiff >= 1.0 and timeDiff <= 10.0:
if self.autosaveCooldown / 4.0 < 0.5:
self.autosaveCooldown += 1.0
- self.autosaveCooldown = (
- 5.0 * (self.autosaveCooldown / 5.0)
- ) + (self.autosaveCooldown / 5.0) * 2
+ self.autosaveCooldown = (5.0 * (self.autosaveCooldown / 5.0)) + (
+ self.autosaveCooldown / 5.0
+ ) * 2
elif force or timeDiff >= self.autosaveCooldown * 5:
self.autosaveCooldown = 0.2
self.autosaveTimes.insert(0, self.lastAutosave)
else:
- log.debug('Autosave rejected by cooldown')
+ log.debug("Autosave rejected by cooldown")
def autosaveExists(self, identical=True):
- '''Determines if creating the autosave should be blocked.'''
+ """Determines if creating the autosave should be blocked."""
try:
- if self.currentProject and os.path.exists(self.autosavePath) \
- and filecmp.cmp(
- self.autosavePath, self.currentProject) == identical:
+ if (
+ self.currentProject
+ and os.path.exists(self.autosavePath)
+ and filecmp.cmp(self.autosavePath, self.currentProject) == identical
+ ):
log.debug(
- 'Autosave found %s to be identical'
- % 'not' if not identical else ''
+ "Autosave found %s to be identical" % "not" if not identical else ""
)
return True
except FileNotFoundError:
- log.error(
- 'Project file couldn\'t be located: %s', self.currentProject)
+ log.error("Project file couldn't be located: %s", self.currentProject)
return identical
return False
def saveProjectChanges(self):
- '''Overwrites project file with autosave file'''
+ """Overwrites project file with autosave file"""
try:
os.remove(self.currentProject)
os.rename(self.autosavePath, self.currentProject)
return True
except (FileNotFoundError, IsADirectoryError) as e:
- self.showMessage(
- msg='Project file couldn\'t be saved.',
- detail=str(e))
+ self.showMessage(msg="Project file couldn't be saved.", detail=str(e))
return False
def openInputFileDialog(self):
inputDir = self.settings.value("inputDir", os.path.expanduser("~"))
fileName, _ = QtWidgets.QFileDialog.getOpenFileName(
- self, "Open Audio File",
- inputDir, "Audio Files (%s)" % " ".join(Core.audioFormats))
+ self,
+ "Open Audio File",
+ inputDir,
+ "Audio Files (%s)" % " ".join(Core.audioFormats),
+ )
if fileName:
self.settings.setValue("inputDir", os.path.dirname(fileName))
@@ -624,17 +604,18 @@ class MainWindow(QtWidgets.QMainWindow):
outputDir = self.settings.value("outputDir", os.path.expanduser("~"))
fileName, _ = QtWidgets.QFileDialog.getSaveFileName(
- self, "Set Output Video File",
+ self,
+ "Set Output Video File",
outputDir,
- "Video Files (%s);; All Files (*)" % " ".join(
- Core.videoFormats))
+ "Video Files (%s);; All Files (*)" % " ".join(Core.videoFormats),
+ )
if fileName:
self.settings.setValue("outputDir", os.path.dirname(fileName))
self.lineEdit_outputFile.setText(fileName)
def stopVideo(self):
- log.info('Export cancelled')
+ log.info("Export cancelled")
self.videoWorker.cancel()
self.canceled = True
@@ -645,14 +626,13 @@ class MainWindow(QtWidgets.QMainWindow):
if audioFile and outputPath and self.core.selectedComponents:
if not os.path.dirname(outputPath):
- outputPath = os.path.join(
- os.path.expanduser("~"), outputPath)
+ outputPath = os.path.join(os.path.expanduser("~"), outputPath)
if outputPath and os.path.isdir(outputPath):
self.showMessage(
- msg='Chosen filename matches a directory, which '
- 'cannot be overwritten. Please choose a different '
- 'filename or move the directory.',
- icon='Warning',
+ msg="Chosen filename matches a directory, which "
+ "cannot be overwritten. Please choose a different "
+ "filename or move the directory.",
+ icon="Warning",
)
return
else:
@@ -661,19 +641,14 @@ class MainWindow(QtWidgets.QMainWindow):
msg="You must select an audio file and output filename."
)
elif not self.core.selectedComponents:
- self.showMessage(
- msg="Not enough components."
- )
+ self.showMessage(msg="Not enough components.")
return
self.canceled = False
self.progressBarUpdated(-1)
- self.videoWorker = self.core.newVideoWorker(
- self, audioFile, outputPath
- )
+ self.videoWorker = self.core.newVideoWorker(self, audioFile, outputPath)
self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated)
- self.videoWorker.progressBarSetText.connect(
- self.progressBarSetText)
+ self.videoWorker.progressBarSetText.connect(self.progressBarSetText)
self.videoWorker.imageCreated.connect(self.showPreviewImage)
self.videoWorker.encoding.connect(self.changeEncodingStatus)
self.createVideo.emit()
@@ -683,14 +658,14 @@ class MainWindow(QtWidgets.QMainWindow):
try:
self.stopVideo()
except AttributeError as e:
- if 'videoWorker' not in str(e):
+ if "videoWorker" not in str(e):
raise
self.showMessage(
msg=msg,
detail=detail,
- icon='Critical',
+ icon="Critical",
)
- log.info('%s', repr(self))
+ log.info("%s", repr(self))
def changeEncodingStatus(self, status):
self.encoding = status
@@ -718,7 +693,7 @@ class MainWindow(QtWidgets.QMainWindow):
# Close undo history dialog if open
self.undoDialog.close()
# Show label under progress bar on macOS
- if sys.platform == 'darwin':
+ if sys.platform == "darwin":
self.progressLabel.setHidden(False)
else:
self.pushButton_createVideo.setEnabled(True)
@@ -749,33 +724,33 @@ class MainWindow(QtWidgets.QMainWindow):
@QtCore.pyqtSlot(str)
def progressBarSetText(self, value):
- if sys.platform == 'darwin':
+ if sys.platform == "darwin":
self.progressLabel.setText(value)
else:
self.progressBar_createVideo.setFormat(value)
def updateResolution(self):
resIndex = int(self.comboBox_resolution.currentIndex())
- res = Core.resolutions[resIndex].split('x')
+ res = Core.resolutions[resIndex].split("x")
changed = res[0] != self.settings.value("outputWidth")
- self.settings.setValue('outputWidth', res[0])
- self.settings.setValue('outputHeight', res[1])
+ self.settings.setValue("outputWidth", res[0])
+ self.settings.setValue("outputHeight", res[1])
if changed:
for i in range(len(self.core.selectedComponents)):
self.core.updateComponent(i)
def drawPreview(self, force=False, **kwargs):
- '''Use autosave keyword arg to force saving or not saving if needed'''
+ """Use autosave keyword arg to force saving or not saving if needed"""
self.newTask.emit(self.core.selectedComponents)
# self.processTask.emit()
- if force or 'autosave' in kwargs:
- if force or kwargs['autosave']:
+ if force or "autosave" in kwargs:
+ if force or kwargs["autosave"]:
self.autosave(True)
else:
self.autosave()
self.updateWindowTitle()
- @QtCore.pyqtSlot('QImage')
+ @QtCore.pyqtSlot("QImage")
def showPreviewImage(self, image):
self.previewWindow.changePixmap(image)
@@ -786,36 +761,35 @@ class MainWindow(QtWidgets.QMainWindow):
def showFfmpegCommand(self):
from textwrap import wrap
from ..toolkit.ffmpeg import createFfmpegCommand
+
command = createFfmpegCommand(
self.lineEdit_audioFile.text(),
self.lineEdit_outputFile.text(),
- self.core.selectedComponents
+ self.core.selectedComponents,
)
command = " ".join(command)
log.info(f"FFmpeg command: {command}")
lines = wrap(command, 49)
- self.showMessage(
- msg=f"Current FFmpeg command:\n\n{' '.join(lines)}"
- )
+ self.showMessage(msg=f"Current FFmpeg command:\n\n{' '.join(lines)}")
def addComponent(self, compPos, moduleIndex):
- '''Creates an undoable action that adds a new component.'''
+ """Creates an undoable action that adds a new component."""
action = AddComponent(self, compPos, moduleIndex)
self.undoStack.push(action)
def insertComponent(self, index):
- '''Triggered by Core to finish initializing a new component.'''
+ """Triggered by Core to finish initializing a new component."""
+ if not hasattr(self.core.selectedComponents[index], "page"):
+ log.error("Component failed to initialize")
+ return
componentList = self.listWidget_componentList
stackedWidget = self.stackedWidget
- componentList.insertItem(
- index,
- self.core.selectedComponents[index].name)
+ componentList.insertItem(index, self.core.selectedComponents[index].name)
componentList.setCurrentRow(index)
# connect to signal that adds an asterisk when modified
- self.core.selectedComponents[index].modified.connect(
- self.updateComponentTitle)
+ self.core.selectedComponents[index].modified.connect(self.updateComponentTitle)
self.pages.insert(index, self.core.selectedComponents[index].page)
stackedWidget.insertWidget(index, self.pages[index])
@@ -842,15 +816,15 @@ class MainWindow(QtWidgets.QMainWindow):
@disableWhenEncoding
def moveComponent(self, change):
- '''Moves a component relatively from its current position'''
+ """Moves a component relatively from its current position"""
componentList = self.listWidget_componentList
tag = change
- if change == 'top':
+ if change == "top":
change = -componentList.currentRow()
- elif change == 'bottom':
- change = len(componentList)-componentList.currentRow()-1
+ elif change == "bottom":
+ change = len(componentList) - componentList.currentRow() - 1
else:
- tag = 'down' if change == 1 else 'up'
+ tag = "down" if change == 1 else "up"
row = componentList.currentRow()
newRow = row + change
@@ -859,38 +833,39 @@ class MainWindow(QtWidgets.QMainWindow):
self.undoStack.push(action)
def getComponentListMousePos(self, position):
- '''
+ """
Given a QPos, returns the component index under the mouse cursor
or -1 if no component is there.
- '''
+ """
componentList = self.listWidget_componentList
+ if hasattr(position, "toPointF"):
+ position = position.toPointF()
+ position = position.toPoint()
+
modelIndexes = [
- componentList.model().index(i)
- for i in range(componentList.count())
- ]
- rects = [
- componentList.visualRect(modelIndex)
- for modelIndex in modelIndexes
+ componentList.model().index(i) for i in range(componentList.count())
]
+ rects = [componentList.visualRect(modelIndex) for modelIndex in modelIndexes]
mousePos = [rect.contains(position) for rect in rects]
if not any(mousePos):
# Not clicking a component
mousePos = -1
else:
mousePos = mousePos.index(True)
- log.debug('Click component list row %s' % mousePos)
+ log.debug("Click component list row %s" % mousePos)
return mousePos
@disableWhenEncoding
def dragComponent(self, event):
- '''Used as Qt drop event for the component listwidget'''
+ """Used as Qt drop event for the component listwidget"""
componentList = self.listWidget_componentList
- mousePos = self.getComponentListMousePos(event.pos())
+ mousePos = self.getComponentListMousePos(event.position())
+
if mousePos > -1:
change = (componentList.currentRow() - mousePos) * -1
else:
- change = (componentList.count() - componentList.currentRow() - 1)
+ change = componentList.count() - componentList.currentRow() - 1
self.moveComponent(change)
def changeComponentWidget(self):
@@ -900,30 +875,27 @@ class MainWindow(QtWidgets.QMainWindow):
self.stackedWidget.setCurrentIndex(index)
def openPresetManager(self):
- '''Preset manager for importing, exporting, renaming, deleting'''
+ """Preset manager for importing, exporting, renaming, deleting"""
self.presetManager.show_()
def clear(self):
- '''Get a blank slate'''
+ """Get a blank slate"""
self.core.clearComponents()
self.listWidget_componentList.clear()
for widget in self.pages:
self.stackedWidget.removeWidget(widget)
self.pages = []
- for field in (
- self.lineEdit_audioFile,
- self.lineEdit_outputFile
- ):
+ for field in (self.lineEdit_audioFile, self.lineEdit_outputFile):
with blockSignals(field):
- field.setText('')
+ field.setText("")
self.progressBarUpdated(0)
- self.progressBarSetText('')
+ self.progressBarSetText("")
self.undoStack.clear()
@disableWhenEncoding
def createNewProject(self, prompt=True):
if prompt:
- self.openSaveChangesDialog('starting a new project')
+ self.openSaveChangesDialog("starting a new project")
self.clear()
self.currentProject = None
@@ -946,11 +918,10 @@ class MainWindow(QtWidgets.QMainWindow):
if self.autosaveExists(identical=False):
ch = self.showMessage(
msg="You have unsaved changes in project '%s'. "
- "Save before %s?" % (
- os.path.basename(self.currentProject)[:-4],
- phrase
- ),
- showCancel=True)
+ "Save before %s?"
+ % (os.path.basename(self.currentProject)[:-4], phrase),
+ showCancel=True,
+ )
if ch:
success = self.saveProjectChanges()
@@ -959,13 +930,15 @@ class MainWindow(QtWidgets.QMainWindow):
def openSaveProjectDialog(self):
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
- self, "Create Project File",
+ self,
+ "Create Project File",
self.settings.value("projectDir"),
- "Project Files (*.avp)")
+ "Project Files (*.avp)",
+ )
if not filename:
return
if not filename.endswith(".avp"):
- filename += '.avp'
+ filename += ".avp"
self.settings.setValue("projectDir", os.path.dirname(filename))
self.settings.setValue("currentProject", filename)
self.currentProject = filename
@@ -975,20 +948,25 @@ class MainWindow(QtWidgets.QMainWindow):
@disableWhenEncoding
def openOpenProjectDialog(self):
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
- self, "Open Project File",
+ self,
+ "Open Project File",
self.settings.value("projectDir"),
- "Project Files (*.avp)")
+ "Project Files (*.avp)",
+ )
self.openProject(filename)
def openProject(self, filepath, prompt=True):
- if not filepath or not os.path.exists(filepath) \
- or not filepath.endswith('.avp'):
+ if (
+ not filepath
+ or not os.path.exists(filepath)
+ or not filepath.endswith(".avp")
+ ):
return
self.clear()
# ask to save any changes that are about to get deleted
if prompt:
- self.openSaveChangesDialog('opening another project')
+ self.openSaveChangesDialog("opening another project")
self.currentProject = filepath
self.settings.setValue("currentProject", filepath)
@@ -999,29 +977,32 @@ class MainWindow(QtWidgets.QMainWindow):
self.updateWindowTitle()
def showMessage(self, **kwargs):
- parent = kwargs['parent'] if 'parent' in kwargs else self
+ parent = kwargs["parent"] if "parent" in kwargs else self
msg = QtWidgets.QMessageBox(parent)
msg.setWindowTitle(appName)
msg.setModal(True)
- msg.setText(kwargs['msg'])
+ msg.setText(kwargs["msg"])
msg.setIcon(
- eval('QtWidgets.QMessageBox.%s' % kwargs['icon'])
- if 'icon' in kwargs else QtWidgets.QMessageBox.Information
+ eval("QtWidgets.QMessageBox.Icon.%s" % kwargs["icon"])
+ if "icon" in kwargs
+ else QtWidgets.QMessageBox.Icon.Information
)
- msg.setDetailedText(kwargs['detail'] if 'detail' in kwargs else None)
- if 'showCancel'in kwargs and kwargs['showCancel']:
+ msg.setDetailedText(kwargs["detail"] if "detail" in kwargs else None)
+ if "showCancel" in kwargs and kwargs["showCancel"]:
msg.setStandardButtons(
- QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
+ QtWidgets.QMessageBox.StandardButton.Ok
+ | QtWidgets.QMessageBox.StandardButton.Cancel
+ )
else:
- msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
- ch = msg.exec_()
+ msg.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok)
+ ch = msg.exec()
if ch == 1024:
return True
return False
@disableWhenEncoding
def componentContextMenu(self, QPos):
- '''Appears when right-clicking the component list'''
+ """Appears when right-clicking the component list"""
componentList = self.listWidget_componentList
self.menu = QtWidgets.QMenu()
parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0))
@@ -1031,9 +1012,7 @@ class MainWindow(QtWidgets.QMainWindow):
# Show preset menu if clicking a component
self.presetManager.findPresets()
menuItem = self.menu.addAction("Save Preset")
- menuItem.triggered.connect(
- self.presetManager.openSavePresetDialog
- )
+ menuItem.triggered.connect(self.presetManager.openSavePresetDialog)
# submenu for opening presets
try:
@@ -1046,17 +1025,16 @@ class MainWindow(QtWidgets.QMainWindow):
for version, presetName in presets:
menuItem = self.presetSubmenu.addAction(presetName)
menuItem.triggered.connect(
- lambda _, presetName=presetName:
- self.presetManager.openPreset(presetName)
+ lambda _, presetName=presetName: self.presetManager.openPreset(
+ presetName
+ )
)
except KeyError:
pass
if self.core.selectedComponents[index].currentPreset:
menuItem = self.menu.addAction("Clear Preset")
- menuItem.triggered.connect(
- self.presetManager.clearPreset
- )
+ menuItem.triggered.connect(self.presetManager.clearPreset)
self.menu.addSeparator()
# "Add Component" submenu
diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py
index 9cf95b4..11a9d9b 100644
--- a/src/gui/presetmanager.py
+++ b/src/gui/presetmanager.py
@@ -1,8 +1,9 @@
-'''
- Preset manager object handles all interactions with presets, including
- the context menu accessed from MainWindow.
-'''
-from PyQt5 import QtCore, QtWidgets, uic
+"""
+Preset manager object handles all interactions with presets, including
+the context menu accessed from MainWindow.
+"""
+
+from PyQt6 import QtCore, QtWidgets, uic
import string
import os
import logging
@@ -12,53 +13,43 @@ from ..core import Core
from .actions import *
-log = logging.getLogger('AVP.Gui.PresetManager')
+log = logging.getLogger("AVP.Gui.PresetManager")
class PresetManager(QtWidgets.QDialog):
def __init__(self, parent):
super().__init__()
- uic.loadUi(
- os.path.join(Core.wd, 'gui', 'presetmanager.ui'), self)
+ uic.loadUi(os.path.join(Core.wd, "gui", "presetmanager.ui"), self)
self.parent = parent
self.core = parent.core
self.settings = parent.settings
self.presetDir = parent.presetDir
- if not self.settings.value('presetDir'):
+ if not self.settings.value("presetDir"):
self.settings.setValue(
- "presetDir",
- os.path.join(parent.dataDir, 'projects'))
+ "presetDir", os.path.join(parent.dataDir, "projects")
+ )
self.findPresets()
# window
- self.lastFilter = '*'
+ self.lastFilter = "*"
self.presetRows = [] # list of (comp, vers, name) tuples
- self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
+
+ # FIXME
+ # self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
# connect button signals
- self.pushButton_delete.clicked.connect(
- self.openDeletePresetDialog
- )
- self.pushButton_rename.clicked.connect(
- self.openRenamePresetDialog
- )
- self.pushButton_import.clicked.connect(
- self.openImportDialog
- )
- self.pushButton_export.clicked.connect(
- self.openExportDialog
- )
- self.pushButton_close.clicked.connect(
- self.close
- )
+ self.pushButton_delete.clicked.connect(self.openDeletePresetDialog)
+ self.pushButton_rename.clicked.connect(self.openRenamePresetDialog)
+ self.pushButton_import.clicked.connect(self.openImportDialog)
+ self.pushButton_export.clicked.connect(self.openExportDialog)
+ self.pushButton_close.clicked.connect(self.close)
# create filter box and preset list
self.drawFilterList()
self.comboBox_filter.currentIndexChanged.connect(
lambda: self.drawPresetList(
- self.comboBox_filter.currentText(),
- self.lineEdit_search.text()
+ self.comboBox_filter.currentText(), self.lineEdit_search.text()
)
)
@@ -69,17 +60,16 @@ class PresetManager(QtWidgets.QDialog):
self.lineEdit_search.setCompleter(completer)
self.lineEdit_search.textChanged.connect(
lambda: self.drawPresetList(
- self.comboBox_filter.currentText(),
- self.lineEdit_search.text()
+ self.comboBox_filter.currentText(), self.lineEdit_search.text()
)
)
- self.drawPresetList('*')
+ self.drawPresetList("*")
def show_(self):
- '''Open a new preset manager window from the mainwindow'''
+ """Open a new preset manager window from the mainwindow"""
self.findPresets()
self.drawFilterList()
- self.drawPresetList('*')
+ self.drawPresetList("*")
self.show()
def findPresets(self):
@@ -100,14 +90,12 @@ class PresetManager(QtWidgets.QDialog):
continue
self.presets = {
compName: [
- (vers, preset)
- for name, vers, preset in parseList
- if name == compName
+ (vers, preset) for name, vers, preset in parseList if name == compName
]
for compName, _, __ in parseList
}
- def drawPresetList(self, compFilter=None, presetFilter=''):
+ def drawPresetList(self, compFilter=None, presetFilter=""):
self.listWidget_presets.clear()
if compFilter:
self.lastFilter = str(compFilter)
@@ -116,13 +104,11 @@ class PresetManager(QtWidgets.QDialog):
self.presetRows = []
presetNames = []
for component, presets in self.presets.items():
- if compFilter != '*' and component != compFilter:
+ if compFilter != "*" and component != compFilter:
continue
for vers, preset in presets:
if not presetFilter or presetFilter in preset:
- self.listWidget_presets.addItem(
- '%s: %s' % (component, preset)
- )
+ self.listWidget_presets.addItem("%s: %s" % (component, preset))
self.presetRows.append((component, vers, preset))
if preset not in presetNames:
presetNames.append(preset)
@@ -130,18 +116,18 @@ class PresetManager(QtWidgets.QDialog):
def drawFilterList(self):
self.comboBox_filter.clear()
- self.comboBox_filter.addItem('*')
+ self.comboBox_filter.addItem("*")
for component in self.presets:
self.comboBox_filter.addItem(component)
def clearPreset(self, compI=None):
- '''Functions on mainwindow level from the context menu'''
+ """Functions on mainwindow level from the context menu"""
compI = self.parent.listWidget_componentList.currentRow()
action = ClearPreset(self.parent, compI)
self.parent.undoStack.push(action)
def openSavePresetDialog(self):
- '''Functions on mainwindow level from the context menu'''
+ """Functions on mainwindow level from the context menu"""
selectedComponents = self.core.selectedComponents
componentList = self.parent.listWidget_componentList
@@ -152,10 +138,10 @@ class PresetManager(QtWidgets.QDialog):
currentPreset = selectedComponents[index].currentPreset
newName, OK = QtWidgets.QInputDialog.getText(
self.parent,
- 'Audio Visualizer',
- 'New Preset Name:',
- QtWidgets.QLineEdit.Normal,
- currentPreset
+ "Audio Visualizer",
+ "New Preset Name:",
+ QtWidgets.QLineEdit.EchoMode.Normal,
+ currentPreset,
)
if OK:
if badName(newName):
@@ -164,21 +150,23 @@ class PresetManager(QtWidgets.QDialog):
if newName:
if index != -1:
selectedComponents[index].currentPreset = newName
- saveValueStore = \
- selectedComponents[index].savePreset()
- saveValueStore['preset'] = newName
+ saveValueStore = selectedComponents[index].savePreset()
+ saveValueStore["preset"] = newName
componentName = str(selectedComponents[index]).strip()
vers = selectedComponents[index].version
self.createNewPreset(
- componentName, vers, newName,
- saveValueStore, window=self.parent)
+ componentName,
+ vers,
+ newName,
+ saveValueStore,
+ window=self.parent,
+ )
self.findPresets()
self.drawPresetList()
self.openPreset(newName, index)
break
- def createNewPreset(
- self, compName, vers, filename, saveValueStore, **kwargs):
+ def createNewPreset(self, compName, vers, filename, saveValueStore, **kwargs):
path = os.path.join(self.presetDir, compName, str(vers), filename)
if self.presetExists(path, **kwargs):
return
@@ -188,11 +176,11 @@ class PresetManager(QtWidgets.QDialog):
if os.path.exists(path):
window = kwargs.get("window", self)
ch = self.parent.showMessage(
- msg="%s already exists! Overwrite it?" %
- os.path.basename(path),
+ msg="%s already exists! Overwrite it?" % os.path.basename(path),
showCancel=True,
- icon='Warning',
- parent=window)
+ icon="Warning",
+ parent=window,
+ )
if not ch:
# user clicked cancel
return True
@@ -225,10 +213,10 @@ class PresetManager(QtWidgets.QDialog):
return
comp, vers, name = self.presetRows[row]
ch = self.parent.showMessage(
- msg='Really delete %s?' % name,
+ msg="Really delete %s?" % name,
showCancel=True,
- icon='Warning',
- parent=self
+ icon="Warning",
+ parent=self,
)
if not ch:
return
@@ -240,9 +228,9 @@ class PresetManager(QtWidgets.QDialog):
def warnMessage(self, window=None):
self.parent.showMessage(
- msg='Preset names must contain only letters, '
- 'numbers, and spaces.',
- parent=window if window else self)
+ msg="Preset names must contain only letters, " "numbers, and spaces.",
+ parent=window if window else self,
+ )
def getPresetRow(self):
row = self.listWidget_presets.currentRow()
@@ -262,14 +250,14 @@ class PresetManager(QtWidgets.QDialog):
rowTuple = (
self.core.selectedComponents[compIndex].name,
self.core.selectedComponents[compIndex].version,
- preset
+ preset,
)
for i, tup in enumerate(self.presetRows):
if rowTuple == tup:
index = i
break
else:
- return -1
+ return -1
return index
def openRenamePresetDialog(self):
@@ -281,10 +269,10 @@ class PresetManager(QtWidgets.QDialog):
while True:
newName, OK = QtWidgets.QInputDialog.getText(
self,
- 'Preset Manager',
- 'Rename Preset:',
- QtWidgets.QLineEdit.Normal,
- self.presetRows[index][2]
+ "Preset Manager",
+ "Rename Preset:",
+ QtWidgets.QLineEdit.EchoMode.Normal,
+ self.presetRows[index][2],
)
if OK:
if badName(newName):
@@ -292,8 +280,7 @@ class PresetManager(QtWidgets.QDialog):
continue
if newName:
comp, vers, oldName = self.presetRows[index]
- path = os.path.join(
- self.presetDir, comp, str(vers))
+ path = os.path.join(self.presetDir, comp, str(vers))
newPath = os.path.join(path, newName)
if self.presetExists(newPath):
return
@@ -311,20 +298,21 @@ class PresetManager(QtWidgets.QDialog):
self.drawPresetList()
path = os.path.dirname(newPath)
for i, comp in enumerate(self.core.selectedComponents):
- if self.core.getPresetDir(comp) == path \
- and comp.currentPreset == oldName:
+ if self.core.getPresetDir(comp) == path and comp.currentPreset == oldName:
self.core.openPreset(newPath, i, newName)
self.parent.updateComponentTitle(i, False)
self.parent.drawPreview()
def openImportDialog(self):
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
- self, "Import Preset File",
+ self,
+ "Import Preset File",
self.settings.value("presetDir"),
- "Preset Files (*.avl)")
+ "Preset Files (*.avl)",
+ )
if filename:
# get installed path & ask user to overwrite if needed
- path = ''
+ path = ""
while True:
if path:
if self.presetExists(path):
@@ -345,15 +333,16 @@ class PresetManager(QtWidgets.QDialog):
if index == -1:
return
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
- self, "Export Preset",
+ self,
+ "Export Preset",
self.settings.value("presetDir"),
- "Preset Files (*.avl)")
+ "Preset Files (*.avl)",
+ )
if filename:
comp, vers, name = self.presetRows[index]
if not self.core.exportPreset(filename, comp, vers, name):
self.parent.showMessage(
- msg='Couldn\'t export %s.' % filename,
- parent=self
+ msg="Couldn't export %s." % filename, parent=self
)
self.settings.setValue("presetDir", os.path.dirname(filename))
diff --git a/src/gui/preview_thread.py b/src/gui/preview_thread.py
index 3943a5c..1d78516 100644
--- a/src/gui/preview_thread.py
+++ b/src/gui/preview_thread.py
@@ -1,9 +1,10 @@
-'''
- Thread that runs to create QImages for MainWindow's preview label.
- Processes a queue of component lists.
-'''
-from PyQt5 import QtCore, QtGui, uic
-from PyQt5.QtCore import pyqtSignal, pyqtSlot
+"""
+Thread that runs to create QImages for MainWindow's preview label.
+Processes a queue of component lists.
+"""
+
+from PyQt6 import QtCore, QtGui, uic
+from PyQt6.QtCore import pyqtSignal, pyqtSlot
from PIL import Image
from PIL.ImageQt import ImageQt
from queue import Queue, Empty
@@ -26,8 +27,8 @@ class Worker(QtCore.QObject):
super().__init__()
self.core = core
self.settings = settings
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
+ width = int(self.settings.value("outputWidth"))
+ height = int(self.settings.value("outputHeight"))
self.queue = queue
self.background = Checkerboard(width, height)
@@ -35,10 +36,10 @@ class Worker(QtCore.QObject):
@pyqtSlot(list)
def createPreviewImage(self, components):
dic = {
- "components": components,
+ "components": components,
}
self.queue.put(dic)
- log.debug('Preview thread id: {}'.format(int(QtCore.QThread.currentThreadId())))
+ log.debug("Preview thread id: {}".format(int(QtCore.QThread.currentThreadId())))
@pyqtSlot()
def process(self):
@@ -49,31 +50,34 @@ class Worker(QtCore.QObject):
self.queue.get(block=False)
except Empty:
continue
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
- if self.background.width != width \
- or self.background.height != height:
+ width = int(self.settings.value("outputWidth"))
+ height = int(self.settings.value("outputHeight"))
+ if self.background.width != width or self.background.height != height:
self.background = Checkerboard(width, height)
frame = self.background.copy()
- log.info('Creating new preview frame')
+ log.info("Creating new preview frame")
components = nextPreviewInformation["components"]
for component in reversed(components):
try:
component.lockSize(width, height)
newFrame = component.previewRender()
component.unlockSize()
- frame = Image.alpha_composite(
- frame, newFrame
- )
+ frame = Image.alpha_composite(frame, newFrame)
except ValueError as e:
- errMsg = "Bad frame returned by %s's preview renderer. " \
- "%s. New frame size was %s*%s; should be %s*%s." % (
- str(component), str(e).capitalize(),
- newFrame.width, newFrame.height,
- width, height
+ errMsg = (
+ "Bad frame returned by %s's preview renderer. "
+ "%s. New frame size was %s*%s; should be %s*%s."
+ % (
+ str(component),
+ str(e).capitalize(),
+ newFrame.width,
+ newFrame.height,
+ width,
+ height,
)
+ )
log.critical(errMsg)
self.error.emit(errMsg)
break
diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py
index d910456..f52f8a3 100644
--- a/src/gui/preview_win.py
+++ b/src/gui/preview_win.py
@@ -1,18 +1,20 @@
-from PyQt5 import QtCore, QtGui, QtWidgets
+from PyQt6 import QtCore, QtGui, QtWidgets
import logging
-log = logging.getLogger('AVP.Gui.PreviewWindow')
+log = logging.getLogger("AVP.Gui.PreviewWindow")
class PreviewWindow(QtWidgets.QLabel):
- '''
- Paints the preview QLabel in MainWindow and maintains the aspect ratio
- when the window is resized.
- '''
+ """
+ Paints the preview QLabel in MainWindow and maintains the aspect ratio
+ when the window is resized.
+ """
+
def __init__(self, parent, img):
super().__init__()
self.parent = parent
- self.setFrameStyle(QtWidgets.QFrame.StyledPanel)
+ # FIXME
+ # self.setFrameStyle(QtWidgets.QFrame.StyledPanel)
self.pixmap = QtGui.QPixmap(img)
def paintEvent(self, event):
@@ -21,12 +23,13 @@ class PreviewWindow(QtWidgets.QLabel):
point = QtCore.QPoint(0, 0)
scaledPix = self.pixmap.scaled(
size,
- QtCore.Qt.KeepAspectRatio,
- transformMode=QtCore.Qt.SmoothTransformation)
+ QtCore.Qt.AspectRatioMode.KeepAspectRatio,
+ transformMode=QtCore.Qt.TransformationMode.SmoothTransformation,
+ )
# start painting the label from left upper corner
- point.setX(int((size.width() - scaledPix.width())/2))
- point.setY(int((size.height() - scaledPix.height())/2))
+ point.setX(int((size.width() - scaledPix.width()) / 2))
+ point.setY(int((size.height() - scaledPix.height()) / 2))
painter.drawPixmap(point, scaledPix)
def changePixmap(self, img):
@@ -40,22 +43,16 @@ class PreviewWindow(QtWidgets.QLabel):
i = self.parent.listWidget_componentList.currentRow()
if i >= 0:
component = self.parent.core.selectedComponents[i]
- if not hasattr(component, 'previewClickEvent'):
+ if not hasattr(component, "previewClickEvent"):
return
- pos = (event.x(), event.y())
+ qpoint = event.position().toPoint()
+ pos = (qpoint.x(), qpoint.y())
size = (self.width(), self.height())
butt = event.button()
- log.info('Click event for #%s: %s button %s' % (
- i, pos, butt))
- component.previewClickEvent(
- pos, size, butt
- )
+ log.info("Click event for #%s: %s button %s" % (i, pos, butt))
+ component.previewClickEvent(pos, size, butt)
@QtCore.pyqtSlot(str)
def threadError(self, msg):
- self.parent.showMessage(
- msg=msg,
- icon='Critical',
- parent=self
- )
- log.info('%', repr(self.parent))
+ self.parent.showMessage(msg=msg, icon="Critical", parent=self)
+ log.info("%", repr(self.parent))
diff --git a/src/tests/__init__.py b/src/tests/__init__.py
index 345bd96..e2d83e7 100644
--- a/src/tests/__init__.py
+++ b/src/tests/__init__.py
@@ -5,20 +5,22 @@ from ..core import Core
def getTestDataPath(filename):
- return os.path.join(Core.wd, 'tests', 'data', filename)
+ return os.path.join(Core.wd, "tests", "data", filename)
def run(logFile):
"""Run Pytest, which then imports and runs all tests in this module."""
- os.environ["PYTEST_QT_API"] = "pyqt5"
+ os.environ["PYTEST_QT_API"] = "PyQt6"
with open(logFile, "w") as f:
# temporarily redirect stdout to a text file so we capture pytest's output
sys.stdout = f
try:
- val = pytest.main([
- os.path.dirname(__file__),
- "-s", # disable pytest's internal capturing of stdout etc.
- ])
+ val = pytest.main(
+ [
+ os.path.dirname(__file__),
+ "-s", # disable pytest's internal capturing of stdout etc.
+ ]
+ )
finally:
sys.stdout = sys.__stdout__
diff --git a/src/tests/test_commandline_export.py b/src/tests/test_commandline_export.py
index 7f3530f..6126da7 100644
--- a/src/tests/test_commandline_export.py
+++ b/src/tests/test_commandline_export.py
@@ -7,11 +7,20 @@ from pytestqt import qtbot
def test_commandline_classic_export(qtbot):
- '''Run Qt event loop and create a video in the system /tmp or /temp'''
+ """Run Qt event loop and create a video in the system /tmp or /temp"""
soundFile = getTestDataPath("test.ogg")
outputDir = tempfile.mkdtemp(prefix="avp-test-")
outputFilename = os.path.join(outputDir, "output.mp4")
- sys.argv = ['', '-c', '0', 'classic', '-i', soundFile, '-o', outputFilename]
+ sys.argv = [
+ "",
+ "-c",
+ "0",
+ "classic",
+ "-i",
+ soundFile,
+ "-o",
+ outputFilename,
+ ]
command = Command()
command.quit = lambda _: None
@@ -19,10 +28,10 @@ def test_commandline_classic_export(qtbot):
# Command object now has a video_thread Worker which is exporting the video
with qtbot.waitSignal(command.worker.videoCreated, timeout=10000):
- '''
+ """
Wait until videoCreated is emitted by the video_thread Worker
or until 10 second timeout has passed
- '''
+ """
print(f"Test Video created at {outputFilename}")
assert os.path.exists(outputFilename)
diff --git a/src/tests/test_commandline_parser.py b/src/tests/test_commandline_parser.py
index 0813e00..5d1232b 100644
--- a/src/tests/test_commandline_parser.py
+++ b/src/tests/test_commandline_parser.py
@@ -5,28 +5,28 @@ from ..command import Command
def test_commandline_help():
command = Command()
- sys.argv = ['', '--help']
+ sys.argv = ["", "--help"]
with pytest.raises(SystemExit):
command.parseArgs()
def test_commandline_help_if_bad_args():
command = Command()
- sys.argv = ['', '--junk']
+ sys.argv = ["", "--junk"]
with pytest.raises(SystemExit):
command.parseArgs()
def test_commandline_launches_gui_if_debug():
command = Command()
- sys.argv = ['', '--debug']
+ sys.argv = ["", "--debug"]
mode = command.parseArgs()
assert mode == "GUI"
def test_commandline_launches_gui_if_debug_with_project():
command = Command()
- sys.argv = ['', 'test', '--debug']
+ sys.argv = ["", "test", "--debug"]
mode = command.parseArgs()
assert mode == "GUI"
@@ -34,11 +34,12 @@ def test_commandline_launches_gui_if_debug_with_project():
def test_commandline_tries_to_export():
command = Command()
didCallFunction = False
+
def captureFunction(*args):
nonlocal didCallFunction
didCallFunction = True
- sys.argv = ['', '-c', '0', 'classic', '-i', '_', '-o', '_']
+ sys.argv = ["", "-c", "0", "classic", "-i", "_", "-o", "_"]
command.createAudioVisualization = captureFunction
command.parseArgs()
assert didCallFunction
diff --git a/src/tests/test_core_init.py b/src/tests/test_core_init.py
index 438f7fe..950dc13 100644
--- a/src/tests/test_core_init.py
+++ b/src/tests/test_core_init.py
@@ -4,15 +4,15 @@ from ..core import Core
def test_component_names():
core = Core()
assert core.compNames == [
- 'Classic Visualizer',
- 'Color',
+ "Classic Visualizer",
+ "Color",
"Conway's Game of Life",
- 'Image',
- 'Sound',
- 'Spectrum',
- 'Title Text',
- 'Video',
- 'Waveform',
+ "Image",
+ "Sound",
+ "Spectrum",
+ "Title Text",
+ "Video",
+ "Waveform",
]
diff --git a/src/toolkit/common.py b/src/toolkit/common.py
index 2e800eb..e35aba2 100644
--- a/src/toolkit/common.py
+++ b/src/toolkit/common.py
@@ -1,7 +1,8 @@
-'''
- Common functions
-'''
-from PyQt5 import QtWidgets
+"""
+Common functions
+"""
+
+from PyQt6 import QtWidgets
import string
import os
import sys
@@ -11,43 +12,38 @@ from copy import copy
from collections import OrderedDict
-log = logging.getLogger('AVP.Toolkit.Common')
+log = logging.getLogger("AVP.Toolkit.Common")
class blockSignals:
- '''
- Context manager to temporarily block list of QtWidgets from updating,
- and guarantee restoring the previous state afterwards.
- '''
+ """
+ Context manager to temporarily block list of QtWidgets from updating,
+ and guarantee restoring the previous state afterwards.
+ """
+
def __init__(self, widgets):
if type(widgets) is dict:
self.widgets = concatDictVals(widgets)
else:
- self.widgets = (
- widgets if hasattr(widgets, '__iter__')
- else [widgets]
- )
+ self.widgets = widgets if hasattr(widgets, "__iter__") else [widgets]
def __enter__(self):
log.verbose(
- 'Blocking signals for %s',
- ", ".join([
- str(w.__class__.__name__) for w in self.widgets
- ])
+ "Blocking signals for %s",
+ ", ".join([str(w.__class__.__name__) for w in self.widgets]),
)
self.oldStates = [w.signalsBlocked() for w in self.widgets]
for w in self.widgets:
w.blockSignals(True)
def __exit__(self, *args):
- log.verbose(
- 'Resetting blockSignals to %s', str(bool(sum(self.oldStates))))
+ log.verbose("Resetting blockSignals to %s", str(bool(sum(self.oldStates))))
for w, state in zip(self.widgets, self.oldStates):
w.blockSignals(state)
def concatDictVals(d):
- '''Concatenates all values in given dict into one list.'''
+ """Concatenates all values in given dict into one list."""
key, value = d.popitem()
d[key] = value
final = copy(value)
@@ -60,22 +56,22 @@ def concatDictVals(d):
def badName(name):
- '''Returns whether a name contains non-alphanumeric chars'''
+ """Returns whether a name contains non-alphanumeric chars"""
return any([letter in string.punctuation for letter in name])
def alphabetizeDict(dictionary):
- '''Alphabetizes a dict into OrderedDict '''
+ """Alphabetizes a dict into OrderedDict"""
return OrderedDict(sorted(dictionary.items(), key=lambda t: t[0]))
def presetToString(dictionary):
- '''Returns string repr of a preset'''
+ """Returns string repr of a preset"""
return repr(alphabetizeDict(dictionary))
def presetFromString(string):
- '''Turns a string repr of OrderedDict into a regular dict'''
+ """Turns a string repr of OrderedDict into a regular dict"""
return dict(eval(string))
@@ -86,19 +82,21 @@ def appendUppercase(lst):
def pipeWrapper(func):
- '''A decorator to insert proper kwargs into Popen objects.'''
+ """A decorator to insert proper kwargs into Popen objects."""
+
def pipeWrapper(commandList, **kwargs):
- if sys.platform == 'win32':
+ if sys.platform == "win32":
# Stop CMD window from appearing on Windows
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
- kwargs['startupinfo'] = startupinfo
+ kwargs["startupinfo"] = startupinfo
- if 'bufsize' not in kwargs:
- kwargs['bufsize'] = 10**8
- if 'stdin' not in kwargs:
- kwargs['stdin'] = subprocess.DEVNULL
+ if "bufsize" not in kwargs:
+ kwargs["bufsize"] = 10**8
+ if "stdin" not in kwargs:
+ kwargs["stdin"] = subprocess.DEVNULL
return func(commandList, **kwargs)
+
return pipeWrapper
@@ -113,6 +111,7 @@ def disableWhenEncoding(func):
return
else:
return func(self, *args, **kwargs)
+
return decorator
@@ -122,13 +121,14 @@ def disableWhenOpeningProject(func):
return
else:
return func(self, *args, **kwargs)
+
return decorator
def rgbFromString(string):
- '''Turns an RGB string like "255, 255, 255" into a tuple'''
+ """Turns an RGB string like "255, 255, 255" into a tuple"""
try:
- tup = tuple([int(i) for i in string.split(',')])
+ tup = tuple([int(i) for i in string.split(",")])
if len(tup) != 3:
raise ValueError
for i in tup:
@@ -141,42 +141,42 @@ def rgbFromString(string):
def formatTraceback(tb=None):
import traceback
+
if tb is None:
import sys
+
tb = sys.exc_info()[2]
- return 'Traceback:\n%s' % "\n".join(traceback.format_tb(tb))
+ return "Traceback:\n%s" % "\n".join(traceback.format_tb(tb))
def connectWidget(widget, func):
if type(widget) == QtWidgets.QLineEdit:
widget.textChanged.connect(func)
- elif type(widget) == QtWidgets.QSpinBox \
- or type(widget) == QtWidgets.QDoubleSpinBox:
+ elif type(widget) == QtWidgets.QSpinBox or type(widget) == QtWidgets.QDoubleSpinBox:
widget.valueChanged.connect(func)
elif type(widget) == QtWidgets.QCheckBox:
widget.stateChanged.connect(func)
elif type(widget) == QtWidgets.QComboBox:
widget.currentIndexChanged.connect(func)
else:
- log.warning('Failed to connect %s ', str(widget.__class__.__name__))
+ log.warning("Failed to connect %s ", str(widget.__class__.__name__))
return False
return True
def setWidgetValue(widget, val):
- '''Generic setValue method for use with any typical QtWidget'''
- log.verbose('Setting %s to %s' % (str(widget.__class__.__name__), val))
+ """Generic setValue method for use with any typical QtWidget"""
+ log.verbose("Setting %s to %s" % (str(widget.__class__.__name__), val))
if type(widget) == QtWidgets.QLineEdit:
widget.setText(val)
- elif type(widget) == QtWidgets.QSpinBox \
- or type(widget) == QtWidgets.QDoubleSpinBox:
+ elif type(widget) == QtWidgets.QSpinBox or type(widget) == QtWidgets.QDoubleSpinBox:
widget.setValue(val)
elif type(widget) == QtWidgets.QCheckBox:
widget.setChecked(val)
elif type(widget) == QtWidgets.QComboBox:
widget.setCurrentIndex(val)
else:
- log.warning('Failed to set %s ', str(widget.__class__.__name__))
+ log.warning("Failed to set %s ", str(widget.__class__.__name__))
return False
return True
@@ -184,8 +184,7 @@ def setWidgetValue(widget, val):
def getWidgetValue(widget):
if type(widget) == QtWidgets.QLineEdit:
return widget.text()
- elif type(widget) == QtWidgets.QSpinBox \
- or type(widget) == QtWidgets.QDoubleSpinBox:
+ elif type(widget) == QtWidgets.QSpinBox or type(widget) == QtWidgets.QDoubleSpinBox:
return widget.value()
elif type(widget) == QtWidgets.QCheckBox:
return widget.isChecked()
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index ff06469..5aedff3 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -1,6 +1,7 @@
-'''
- Tools for using ffmpeg
-'''
+"""
+Tools for using ffmpeg
+"""
+
import numpy
import sys
import os
@@ -14,67 +15,74 @@ from .. import core
from .common import checkOutput, pipeWrapper
-log = logging.getLogger('AVP.Toolkit.Ffmpeg')
+log = logging.getLogger("AVP.Toolkit.Ffmpeg")
class FfmpegVideo:
- '''Opens a pipe to ffmpeg and stores a buffer of raw video frames.'''
+ """Opens a pipe to ffmpeg and stores a buffer of raw video frames."""
# error from the thread used to fill the buffer
threadError = None
def __init__(self, **kwargs):
mandatoryArgs = [
- 'inputPath',
- 'filter_',
- 'width',
- 'height',
- 'frameRate', # frames per second
- 'chunkSize', # number of bytes in one frame
- 'parent', # mainwindow object
- 'component', # component object
+ "inputPath",
+ "filter_",
+ "width",
+ "height",
+ "frameRate", # frames per second
+ "chunkSize", # number of bytes in one frame
+ "parent", # mainwindow object
+ "component", # component object
]
for arg in mandatoryArgs:
setattr(self, arg, kwargs[arg])
self.frameNo = -1
- self.currentFrame = 'None'
+ self.currentFrame = "None"
self.map_ = None
- if 'loopVideo' in kwargs and kwargs['loopVideo']:
- self.loopValue = '-1'
+ if "loopVideo" in kwargs and kwargs["loopVideo"]:
+ self.loopValue = "-1"
else:
- self.loopValue = '0'
- if 'filter_' in kwargs:
- if kwargs['filter_'][0] != '-filter_complex':
- kwargs['filter_'].insert(0, '-filter_complex')
+ self.loopValue = "0"
+ if "filter_" in kwargs:
+ if kwargs["filter_"][0] != "-filter_complex":
+ kwargs["filter_"].insert(0, "-filter_complex")
else:
- kwargs['filter_'] = None
+ kwargs["filter_"] = None
self.command = [
core.Core.FFMPEG_BIN,
- '-thread_queue_size', '512',
- '-r', str(self.frameRate),
- '-stream_loop', str(self.loopValue),
- '-i', self.inputPath,
- '-f', 'image2pipe',
- '-pix_fmt', 'rgba',
+ "-thread_queue_size",
+ "512",
+ "-r",
+ str(self.frameRate),
+ "-stream_loop",
+ str(self.loopValue),
+ "-i",
+ self.inputPath,
+ "-f",
+ "image2pipe",
+ "-pix_fmt",
+ "rgba",
]
- if type(kwargs['filter_']) is list:
- self.command.extend(
- kwargs['filter_']
- )
- self.command.extend([
- '-codec:v', 'rawvideo', '-',
- ])
+ if type(kwargs["filter_"]) is list:
+ self.command.extend(kwargs["filter_"])
+ self.command.extend(
+ [
+ "-codec:v",
+ "rawvideo",
+ "-",
+ ]
+ )
self.frameBuffer = PriorityQueue()
self.frameBuffer.maxsize = self.frameRate
self.finishedFrames = {}
self.thread = threading.Thread(
- target=self.fillBuffer,
- name='FFmpeg Frame-Fetcher'
+ target=self.fillBuffer, name="FFmpeg Frame-Fetcher"
)
self.thread.daemon = True
self.thread.start()
@@ -91,22 +99,29 @@ class FfmpegVideo:
def fillBuffer(self):
from ..component import ComponentError
+
if core.Core.logEnabled:
logFilename = os.path.join(
- core.Core.logDir, 'render_%s.log' % str(self.component.compPos)
+ core.Core.logDir, "render_%s.log" % str(self.component.compPos)
)
- log.debug('Creating ffmpeg process (log at %s)', logFilename)
- with open(logFilename, 'w') as logf:
- logf.write(" ".join(self.command) + '\n\n')
- with open(logFilename, 'a') as logf:
+ log.debug("Creating ffmpeg process (log at %s)", logFilename)
+ with open(logFilename, "w") as logf:
+ logf.write(" ".join(self.command) + "\n\n")
+ with open(logFilename, "a") as logf:
self.pipe = openPipe(
- self.command, stdin=subprocess.DEVNULL,
- stdout=subprocess.PIPE, stderr=logf, bufsize=10**8
+ self.command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=logf,
+ bufsize=10**8,
)
else:
self.pipe = openPipe(
- self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
- stderr=subprocess.DEVNULL, bufsize=10**8
+ self.command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ bufsize=10**8,
)
while True:
@@ -117,12 +132,13 @@ class FfmpegVideo:
# If we run out of frames, use the last good frame and loop.
try:
if len(self.currentFrame) == 0:
- self.frameBuffer.put((self.frameNo-1, self.lastFrame))
+ self.frameBuffer.put((self.frameNo - 1, self.lastFrame))
continue
except AttributeError:
FfmpegVideo.threadError = ComponentError(
- self.component, 'video',
- "Video seemed playable but wasn't."
+ self.component,
+ "video",
+ "Video seemed playable but wasn't.",
)
break
@@ -130,11 +146,12 @@ class FfmpegVideo:
self.currentFrame = self.pipe.stdout.read(self.chunkSize)
except ValueError as e:
if str(e) == "PyMemoryView_FromBuffer(): info->buf must not be NULL":
- log.debug("Ignored 'info->buf must not be NULL' error from FFmpeg pipe")
+ log.debug(
+ "Ignored 'info->buf must not be NULL' error from FFmpeg pipe"
+ )
return
else:
- FfmpegVideo.threadError = ComponentError(
- self.component, 'video')
+ FfmpegVideo.threadError = ComponentError(self.component, "video")
if len(self.currentFrame) != 0:
self.frameBuffer.put((self.frameNo, self.currentFrame))
@@ -153,19 +170,17 @@ def closePipe(pipe):
def findFfmpeg():
if sys.platform == "win32":
- bin = 'ffmpeg.exe'
+ bin = "ffmpeg.exe"
else:
- bin = 'ffmpeg'
+ bin = "ffmpeg"
- if getattr(sys, 'frozen', False):
+ if getattr(sys, "frozen", False):
# The application is frozen
bin = os.path.join(core.Core.wd, bin)
with open(os.devnull, "w") as f:
try:
- checkOutput(
- [bin, '-version'], stderr=f
- )
+ checkOutput([bin, "-version"], stderr=f)
except (subprocess.CalledProcessError, FileNotFoundError):
bin = ""
@@ -173,9 +188,9 @@ def findFfmpeg():
def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
- '''
- Constructs the major ffmpeg command used to export the video
- '''
+ """
+ Constructs the major ffmpeg command used to export the video
+ """
if duration == -1:
duration = getAudioDuration(inputFile)
safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters
@@ -183,31 +198,33 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
Core = core.Core
# Test if user has libfdk_aac
- encoders = checkOutput(
- "%s -encoders -hide_banner" % Core.FFMPEG_BIN, shell=True
- )
+ encoders = checkOutput("%s -encoders -hide_banner" % Core.FFMPEG_BIN, shell=True)
encoders = encoders.decode("utf-8")
- acodec = Core.settings.value('outputAudioCodec')
+ acodec = Core.settings.value("outputAudioCodec")
options = Core.encoderOptions
- containerName = Core.settings.value('outputContainer')
- vcodec = Core.settings.value('outputVideoCodec')
- vbitrate = str(Core.settings.value('outputVideoBitrate'))+'k'
- acodec = Core.settings.value('outputAudioCodec')
- abitrate = str(Core.settings.value('outputAudioBitrate'))+'k'
-
- for cont in options['containers']:
- if cont['name'] == containerName:
- container = cont['container']
+ containerName = Core.settings.value("outputContainer")
+ vcodec = Core.settings.value("outputVideoCodec")
+ vbitrate = str(Core.settings.value("outputVideoBitrate")) + "k"
+ acodec = Core.settings.value("outputAudioCodec")
+ abitrate = str(Core.settings.value("outputAudioBitrate")) + "k"
+
+ for cont in options["containers"]:
+ if cont["name"] == containerName:
+ container = cont["container"]
break
- vencoders = options['video-codecs'][vcodec]
- aencoders = options['audio-codecs'][acodec]
+ vencoders = options["video-codecs"][vcodec]
+ aencoders = options["audio-codecs"][acodec]
def error():
nonlocal encoders, encoder
- log.critical("Selected encoder (%s) is not supported by Ffmpeg. The supported encoders are: %s", encoder, encoders)
+ log.critical(
+ "Selected encoder (%s) is not supported by Ffmpeg. The supported encoders are: %s",
+ encoder,
+ encoders,
+ )
return []
for encoder in vencoders:
@@ -226,57 +243,75 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
ffmpegCommand = [
Core.FFMPEG_BIN,
- '-thread_queue_size', '512',
- '-y', # overwrite the output file if it already exists.
-
+ "-thread_queue_size",
+ "512",
+ "-y", # overwrite the output file if it already exists.
# INPUT VIDEO
- '-f', 'rawvideo',
- '-vcodec', 'rawvideo',
- '-s', f'{Core.settings.value("outputWidth")}x{Core.settings.value("outputHeight")}',
- '-pix_fmt', 'rgba',
- '-r', str(Core.settings.value('outputFrameRate')),
- '-t', duration,
- '-an', # the video input has no sound
- '-i', '-', # the video input comes from a pipe
-
+ "-f",
+ "rawvideo",
+ "-vcodec",
+ "rawvideo",
+ "-s",
+ f'{Core.settings.value("outputWidth")}x{Core.settings.value("outputHeight")}',
+ "-pix_fmt",
+ "rgba",
+ "-r",
+ str(Core.settings.value("outputFrameRate")),
+ "-t",
+ duration,
+ "-an", # the video input has no sound
+ "-i",
+ "-", # the video input comes from a pipe
# INPUT SOUND
- '-t', duration,
- '-i', inputFile
+ "-t",
+ duration,
+ "-i",
+ inputFile,
]
- extraAudio = [
- comp.audio for comp in components
- if 'audio' in comp.properties()
- ]
+ extraAudio = [comp.audio for comp in components if "audio" in comp.properties()]
segment = createAudioFilterCommand(extraAudio, safeDuration)
ffmpegCommand.extend(segment)
# Map audio from the filters or the single audio input, and map video from the pipe
- ffmpegCommand.extend([
- '-map', '0:v',
- '-map', '[a]' if segment else '1:a',
- ])
-
- ffmpegCommand.extend([
- # OUTPUT
- '-vcodec', vencoder,
- '-acodec', aencoder,
- '-b:v', vbitrate,
- '-b:a', abitrate,
- '-pix_fmt', Core.settings.value('outputVideoFormat'),
- '-preset', Core.settings.value('outputPreset'),
- '-f', container
- ])
-
- if acodec == 'aac':
- ffmpegCommand.append('-strict')
- ffmpegCommand.append('-2')
+ ffmpegCommand.extend(
+ [
+ "-map",
+ "0:v",
+ "-map",
+ "[a]" if segment else "1:a",
+ ]
+ )
+
+ ffmpegCommand.extend(
+ [
+ # OUTPUT
+ "-vcodec",
+ vencoder,
+ "-acodec",
+ aencoder,
+ "-b:v",
+ vbitrate,
+ "-b:a",
+ abitrate,
+ "-pix_fmt",
+ Core.settings.value("outputVideoFormat"),
+ "-preset",
+ Core.settings.value("outputPreset"),
+ "-f",
+ container,
+ ]
+ )
+
+ if acodec == "aac":
+ ffmpegCommand.append("-strict")
+ ffmpegCommand.append("-2")
ffmpegCommand.append(outputFile)
return ffmpegCommand
def createAudioFilterCommand(extraAudio, duration):
- '''Add extra inputs and any needed filters to the main ffmpeg command.'''
+ """Add extra inputs and any needed filters to the main ffmpeg command."""
# NOTE: Global filters are currently hard-coded here for debugging use
globalFilters = 0 # increase to add global filters
@@ -288,21 +323,23 @@ def createAudioFilterCommand(extraAudio, duration):
extraFilters = {}
for streamNo, params in enumerate(reversed(extraAudio)):
extraInputFile, params = params
- ffmpegCommand.extend([
- '-t', duration,
- # Tell ffmpeg about shorter clips (seemingly not needed)
- # streamDuration = getAudioDuration(extraInputFile)
- # if streamDuration and streamDuration > float(safeDuration)
- # else "{0:.3f}".format(streamDuration),
- '-i', extraInputFile
- ])
+ ffmpegCommand.extend(
+ [
+ "-t",
+ duration,
+ # Tell ffmpeg about shorter clips (seemingly not needed)
+ # streamDuration = getAudioDuration(extraInputFile)
+ # if streamDuration and streamDuration > float(safeDuration)
+ # else "{0:.3f}".format(streamDuration),
+ "-i",
+ extraInputFile,
+ ]
+ )
# Construct dataset of extra filters we'll need to add later
for ffmpegFilter in params:
if streamNo + 2 not in extraFilters:
extraFilters[streamNo + 2] = []
- extraFilters[streamNo + 2].append((
- ffmpegFilter, params[ffmpegFilter]
- ))
+ extraFilters[streamNo + 2].append((ffmpegFilter, params[ffmpegFilter]))
# Start creating avfilters! Popen-style, so don't use semicolons;
extraFilterCommand = []
@@ -318,63 +355,73 @@ def createAudioFilterCommand(extraAudio, duration):
extraFilters[streamNo + 1] = []
# Also filter the primary audio track
extraFilters[1] = []
- tmpInputs = {
- streamNo: globalFilters - 1
- for streamNo in extraFilters
- }
+ tmpInputs = {streamNo: globalFilters - 1 for streamNo in extraFilters}
# Add the global filters!
# NOTE: list length must = globalFilters, currently hardcoded
if tmpInputs:
- extraFilterCommand.extend([
- '[%s:a] ashowinfo [%stmp0]' % (
- str(streamNo),
- str(streamNo)
- )
- for streamNo in tmpInputs
- ])
+ extraFilterCommand.extend(
+ [
+ "[%s:a] ashowinfo [%stmp0]" % (str(streamNo), str(streamNo))
+ for streamNo in tmpInputs
+ ]
+ )
# Now add the per-stream filters!
for streamNo, paramList in extraFilters.items():
for param in paramList:
- source = '[%s:a]' % str(streamNo) \
- if tmpInputs[streamNo] == -1 else \
- '[%stmp%s]' % (
- str(streamNo), str(tmpInputs[streamNo])
- )
+ source = (
+ "[%s:a]" % str(streamNo)
+ if tmpInputs[streamNo] == -1
+ else "[%stmp%s]" % (str(streamNo), str(tmpInputs[streamNo]))
+ )
tmpInputs[streamNo] = tmpInputs[streamNo] + 1
extraFilterCommand.append(
- '%s %s%s [%stmp%s]' % (
- source, param[0], param[1], str(streamNo),
- str(tmpInputs[streamNo])
+ "%s %s%s [%stmp%s]"
+ % (
+ source,
+ param[0],
+ param[1],
+ str(streamNo),
+ str(tmpInputs[streamNo]),
)
)
# Join all the filters together and combine into 1 stream
- extraFilterCommand = "; ".join(extraFilterCommand) + '; ' \
- if tmpInputs else ''
- ffmpegCommand.extend([
- '-filter_complex',
- extraFilterCommand +
- '%s amix=inputs=%s:duration=first [a]'
- % (
- "".join([
- '[%stmp%s]' % (str(i), tmpInputs[i])
- if i in extraFilters else '[%s:a]' % str(i)
- for i in range(1, len(extraAudio) + 2)
- ]),
- str(len(extraAudio) + 1)
- ),
- ])
+ extraFilterCommand = "; ".join(extraFilterCommand) + "; " if tmpInputs else ""
+ ffmpegCommand.extend(
+ [
+ "-filter_complex",
+ extraFilterCommand
+ + "%s amix=inputs=%s:duration=first [a]"
+ % (
+ "".join(
+ [
+ (
+ "[%stmp%s]" % (str(i), tmpInputs[i])
+ if i in extraFilters
+ else "[%s:a]" % str(i)
+ )
+ for i in range(1, len(extraAudio) + 2)
+ ]
+ ),
+ str(len(extraAudio) + 1),
+ ),
+ ]
+ )
return ffmpegCommand
def testAudioStream(filename):
- '''Test if an audio stream definitely exists'''
+ """Test if an audio stream definitely exists"""
audioTestCommand = [
core.Core.FFMPEG_BIN,
- '-i', filename,
- '-vn', '-f', 'null', '-'
+ "-i",
+ filename,
+ "-vn",
+ "-f",
+ "null",
+ "-",
]
try:
checkOutput(audioTestCommand, stderr=subprocess.DEVNULL)
@@ -385,8 +432,8 @@ def testAudioStream(filename):
def getAudioDuration(filename):
- '''Try to get duration of audio file as float, or False if not possible'''
- command = [core.Core.FFMPEG_BIN, '-i', filename]
+ """Try to get duration of audio file as float, or False if not possible"""
+ command = [core.Core.FFMPEG_BIN, "-i", filename]
try:
fileInfo = checkOutput(command, stderr=subprocess.STDOUT)
@@ -397,17 +444,17 @@ def getAudioDuration(filename):
return False
try:
- info = fileInfo.decode("utf-8").split('\n')
+ info = fileInfo.decode("utf-8").split("\n")
except UnicodeDecodeError as e:
- log.error('Unicode error:', str(e))
+ log.error("Unicode error:", str(e))
return False
for line in info:
- if 'Duration' in line:
- d = line.split(',')[0]
- d = d.split(' ')[3]
- d = d.split(':')
- duration = float(d[0])*3600 + float(d[1])*60 + float(d[2])
+ if "Duration" in line:
+ d = line.split(",")[0]
+ d = d.split(" ")[3]
+ d = d.split(":")
+ duration = float(d[0]) * 3600 + float(d[1]) * 60 + float(d[2])
break
else:
# String not found in output
@@ -416,10 +463,10 @@ def getAudioDuration(filename):
def readAudioFile(filename, videoWorker):
- '''
- Creates the completeAudioArray given to components
- and used to draw the classic visualizer.
- '''
+ """
+ Creates the completeAudioArray given to components
+ and used to draw the classic visualizer.
+ """
duration = getAudioDuration(filename)
if not duration:
log.error(f"Audio file {filename} doesn't exist or unreadable.")
@@ -427,15 +474,23 @@ def readAudioFile(filename, videoWorker):
command = [
core.Core.FFMPEG_BIN,
- '-i', filename,
- '-f', 's16le',
- '-acodec', 'pcm_s16le',
- '-ar', '44100', # ouput will have 44100 Hz
- '-ac', '1', # mono (set to '2' for stereo)
- '-']
+ "-i",
+ filename,
+ "-f",
+ "s16le",
+ "-acodec",
+ "pcm_s16le",
+ "-ar",
+ "44100", # ouput will have 44100 Hz
+ "-ac",
+ "1", # mono (set to '2' for stereo)
+ "-",
+ ]
in_pipe = openPipe(
command,
- stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ bufsize=10**8,
)
completeAudioArray = numpy.empty(0, dtype="int16")
@@ -447,18 +502,18 @@ def readAudioFile(filename, videoWorker):
return
# read 2 seconds of audio
progress += 4
- raw_audio = in_pipe.stdout.read(88200*4)
+ raw_audio = in_pipe.stdout.read(88200 * 4)
if len(raw_audio) == 0:
break
- audio_array = numpy.fromstring(raw_audio, dtype="int16")
+ audio_array = numpy.frombuffer(raw_audio, dtype="int16")
completeAudioArray = numpy.append(completeAudioArray, audio_array)
- percent = int(100*(progress/duration))
+ percent = int(100 * (progress / duration))
if percent >= 100:
percent = 100
if lastPercent != percent:
- string = 'Loading audio file: '+str(percent)+'%'
+ string = "Loading audio file: " + str(percent) + "%"
videoWorker.progressBarSetText.emit(string)
videoWorker.progressBarUpdate.emit(percent)
@@ -468,25 +523,23 @@ def readAudioFile(filename, videoWorker):
in_pipe.wait()
# add 0s the end
- completeAudioArrayCopy = numpy.zeros(
- len(completeAudioArray) + 44100, dtype="int16")
- completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray
+ completeAudioArrayCopy = numpy.zeros(len(completeAudioArray) + 44100, dtype="int16")
+ completeAudioArrayCopy[: len(completeAudioArray)] = completeAudioArray
completeAudioArray = completeAudioArrayCopy
return (completeAudioArray, duration)
-def exampleSound(
- style='white', extra='apulsator=offset_l=0.35:offset_r=0.67'):
- '''Help generate an example sound for use in creating a preview'''
+def exampleSound(style="white", extra="apulsator=offset_l=0.35:offset_r=0.67"):
+ """Help generate an example sound for use in creating a preview"""
- if style == 'white':
- src = '-2+random(0)'
- elif style == 'freq':
- src = 'sin(1000*t*PI*t)'
- elif style == 'wave':
- src = 'sin(random(0)*2*PI*t)*tan(random(0)*2*PI*t)'
- elif style == 'stereo':
- src = '0.1*sin(2*PI*(360-2.5/2)*t) | 0.1*sin(2*PI*(360+2.5/2)*t)'
+ if style == "white":
+ src = "-2+random(0)"
+ elif style == "freq":
+ src = "sin(1000*t*PI*t)"
+ elif style == "wave":
+ src = "sin(random(0)*2*PI*t)*tan(random(0)*2*PI*t)"
+ elif style == "stereo":
+ src = "0.1*sin(2*PI*(360-2.5/2)*t) | 0.1*sin(2*PI*(360+2.5/2)*t)"
- return "aevalsrc='%s', %s%s" % (src, extra, ', ' if extra else '')
+ return "aevalsrc='%s', %s%s" % (src, extra, ", " if extra else "")
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index 520bd43..94537a6 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -1,25 +1,27 @@
-'''
- Common tools for drawing compatible frames in a Component's frameRender()
-'''
-from PyQt5 import QtGui
+"""
+Common tools for drawing compatible frames in a Component's frameRender()
+"""
+
+from PyQt6 import QtGui
from PIL import Image
from PIL.ImageQt import ImageQt
+from PyQt6 import QtCore
import sys
import os
import math
import logging
-
from .. import core
-log = logging.getLogger('AVP.Toolkit.Frame')
+log = logging.getLogger("AVP.Toolkit.Frame")
class FramePainter(QtGui.QPainter):
- '''
- A QPainter for a blank frame, which can be converted into a
- Pillow image with finalize()
- '''
+ """
+ A QPainter for a blank frame, which can be converted into a
+ Pillow image with finalize()
+ """
+
def __init__(self, width, height):
image = BlankFrame(width, height)
log.debug("Creating QImage from PIL image object")
@@ -34,21 +36,33 @@ class FramePainter(QtGui.QPainter):
def finalize(self):
log.verbose("Finalizing FramePainter")
+ buffer = QtCore.QBuffer()
+ buffer.open(QtCore.QBuffer.OpenModeFlag.ReadWrite)
+ self.image.save(buffer, "PNG")
+ import io
+
+ frame = Image.open(io.BytesIO(buffer.data()))
+ buffer.close()
+ self.end()
+ return frame
imBytes = self.image.bits().asstring(self.image.byteCount())
- frame = Image.frombytes(
- 'RGBA', (self.image.width(), self.image.height()), imBytes
+ frame = Image.frombytes(
+ "RGBA", (self.image.width(), self.image.height()), imBytes
)
self.end()
return frame
class PaintColor(QtGui.QColor):
- '''Reverse the painter colour if the hardware stores RGB values backward'''
+ """
+ Subclass of QtGui.QColor with an added scale() method
+ Previously this class reversed the painter colour to solve
+ hardware issues related to endianness,
+ but Qt appears to deal with this itself nowadays
+ """
+
def __init__(self, r, g, b, a=255):
- if sys.byteorder == 'big':
- super().__init__(r, g, b, a)
- else:
- super().__init__(b, g, r, a)
+ super().__init__(r, g, b, a)
def scale(scalePercent, width, height, returntype=None):
@@ -63,7 +77,8 @@ def scale(scalePercent, width, height, returntype=None):
def defaultSize(framefunc):
- '''Makes width/height arguments optional'''
+ """Makes width/height arguments optional"""
+
def decorator(*args):
if len(args) < 2:
newArgs = list(args)
@@ -75,6 +90,7 @@ def defaultSize(framefunc):
newArgs.insert(0, width)
args = tuple(newArgs)
return framefunc(*args)
+
return decorator
@@ -84,21 +100,18 @@ def FloodFrame(width, height, RgbaTuple):
@defaultSize
def BlankFrame(width, height):
- '''The base frame used by each component to start drawing.'''
+ """The base frame used by each component to start drawing."""
return FloodFrame(width, height, (0, 0, 0, 0))
@defaultSize
def Checkerboard(width, height):
- '''
- A checkerboard to represent transparency to the user.
- TODO: Would be cool to generate this image with numpy instead.
- '''
- log.debug('Creating new %s*%s checkerboard' % (width, height))
+ """
+ A checkerboard to represent transparency to the user.
+ """
+ # TODO: Would be cool to generate this image with numpy instead.
+ log.debug("Creating new %s*%s checkerboard" % (width, height))
image = FloodFrame(1920, 1080, (0, 0, 0, 0))
- image.paste(Image.open(
- os.path.join(core.Core.wd, 'gui', "background.png")),
- (0, 0)
- )
+ image.paste(Image.open(os.path.join(core.Core.wd, "gui", "background.png")), (0, 0))
image = image.resize((width, height))
return image
diff --git a/src/video_thread.py b/src/video_thread.py
index 70e4d5d..5d72409 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -1,4 +1,4 @@
-'''
+"""
Worker thread created to export a video. It has a slot to begin export using
an input file, output path, and component list.
@@ -6,9 +6,10 @@ Signals are emitted to update MainWindow's progress bar, detail text, and previe
A Command object takes the place of MainWindow while in commandline mode.
Export can be cancelled with cancel()
-'''
-from PyQt5 import QtCore, QtGui
-from PyQt5.QtCore import pyqtSignal, pyqtSlot
+"""
+
+from PyQt6 import QtCore, QtGui
+from PyQt6.QtCore import pyqtSignal, pyqtSlot
from PIL import Image
from PIL.ImageQt import ImageQt
import numpy
@@ -22,8 +23,10 @@ import logging
from .component import ComponentError
from .toolkit.frame import Checkerboard
from .toolkit.ffmpeg import (
- openPipe, readAudioFile,
- getAudioDuration, createFfmpegCommand
+ openPipe,
+ readAudioFile,
+ getAudioDuration,
+ createFfmpegCommand,
)
@@ -32,7 +35,7 @@ log = logging.getLogger("AVP.VideoThread")
class Worker(QtCore.QObject):
- imageCreated = pyqtSignal('QImage')
+ imageCreated = pyqtSignal("QImage")
videoCreated = pyqtSignal()
progressBarUpdate = pyqtSignal(int)
progressBarSetText = pyqtSignal(str)
@@ -61,31 +64,34 @@ class Worker(QtCore.QObject):
self.inputFile, self.outputFile, self.components, duration
)
except sp.CalledProcessError as e:
- #FIXME video_thread should own this error signal, not components
- self.components[0]._error.emit("Ffmpeg could not be found. Is it installed?", str(e))
+ # FIXME video_thread should own this error signal, not components
+ self.components[0]._error.emit(
+ "Ffmpeg could not be found. Is it installed?", str(e)
+ )
self.error = True
return
-
+
if not ffmpegCommand:
- #FIXME video_thread should own this error signal, not components
- self.components[0]._error.emit("The FFmpeg command could not be generated.", "")
- log.critical("Cancelling render process due to failure while generating the ffmpeg command.")
+ # FIXME video_thread should own this error signal, not components
+ self.components[0]._error.emit(
+ "The FFmpeg command could not be generated.", ""
+ )
+ log.critical(
+ "Cancelling render process due to failure while generating the ffmpeg command."
+ )
self.failExport()
return
return ffmpegCommand
def determineAudioLength(self):
- '''
+ """
Returns audio length which determines length of final video, or False if failure occurs
- '''
- if any([
- True if 'pcm' in comp.properties() else False
- for comp in self.components
- ]):
+ """
+ if any(
+ [True if "pcm" in comp.properties() else False for comp in self.components]
+ ):
self.progressBarSetText.emit("Loading audio file...")
- audioFileTraits = readAudioFile(
- self.inputFile, self
- )
+ audioFileTraits = readAudioFile(self.inputFile, self)
if audioFileTraits is None:
self.cancelExport()
return False
@@ -95,25 +101,27 @@ class Worker(QtCore.QObject):
duration = getAudioDuration(self.inputFile)
self.completeAudioArray = []
self.audioArrayLen = int(
- ((duration * self.hertz) +
- self.hertz) - self.sampleSize)
+ ((duration * self.hertz) + self.hertz) - self.sampleSize
+ )
return duration
def preFrameRender(self):
- '''
+ """
Initializes components that need to pre-compute stuff.
Also prerenders "static" components like text and merges them if possible
- '''
+ """
self.staticComponents = {}
# Call preFrameRender on each component
canceledByComponent = False
- initText = ", ".join([
- "%s) %s" % (num, str(component))
- for num, component in enumerate(reversed(self.components))
- ])
- print('Loaded Components:', initText)
- log.info('Calling preFrameRender for %s', initText)
+ initText = ", ".join(
+ [
+ "%s) %s" % (num, str(component))
+ for num, component in enumerate(reversed(self.components))
+ ]
+ )
+ print("Loaded Components:", initText)
+ log.info("Calling preFrameRender for %s", initText)
for compNo, comp in enumerate(reversed(self.components)):
try:
comp.preFrameRender(
@@ -122,80 +130,85 @@ class Worker(QtCore.QObject):
audioArrayLen=self.audioArrayLen,
sampleSize=self.sampleSize,
progressBarUpdate=self.progressBarUpdate,
- progressBarSetText=self.progressBarSetText
+ progressBarSetText=self.progressBarSetText,
)
except ComponentError:
log.warning(
- '#%s %s encountered an error in its preFrameRender method',
+ "#%s %s encountered an error in its preFrameRender method",
compNo,
- comp
+ comp,
)
compProps = comp.properties()
- if 'error' in compProps or comp._lockedError is not None:
+ if "error" in compProps or comp._lockedError is not None:
self.cancel()
self.canceled = True
canceledByComponent = True
- compError = comp.error() \
- if type(comp.error()) is tuple else (comp.error(), '')
+ compError = (
+ comp.error() if type(comp.error()) is tuple else (comp.error(), "")
+ )
errMsg = (
- "Component #%s (%s) encountered an error!" % (
- str(compNo), comp.name
- )
- if comp.error() is None else
- 'Export cancelled by component #%s (%s): %s' % (
- str(compNo),
- comp.name,
- compError[0]
- )
+ "Component #%s (%s) encountered an error!"
+ % (str(compNo), comp.name)
+ if comp.error() is None
+ else "Export cancelled by component #%s (%s): %s"
+ % (str(compNo), comp.name, compError[0])
)
log.error(errMsg)
comp._error.emit(errMsg, compError[1])
break
- if 'static' in compProps:
- log.info('Saving static frame from #%s %s', compNo, comp)
- self.staticComponents[compNo] = \
- comp.frameRender(0).copy()
+ if "static" in compProps:
+ log.info("Saving static frame from #%s %s", compNo, comp)
+ self.staticComponents[compNo] = comp.frameRender(0).copy()
# Check if any errors occured
log.debug("Checking if a component wishes to cancel the export...")
if self.canceled:
if canceledByComponent:
log.error(
- 'Export cancelled by component #%s (%s): %s',
+ "Export cancelled by component #%s (%s): %s",
compNo,
comp.name,
- 'No message.' if comp.error() is None else (
- comp.error() if type(comp.error()) is str
- else comp.error()[0]
- )
+ (
+ "No message."
+ if comp.error() is None
+ else (
+ comp.error()
+ if type(comp.error()) is str
+ else comp.error()[0]
+ )
+ ),
)
self.cancelExport()
-
+
# Merge static frames that can be merged to reduce workload
def mergeConsecutiveStaticComponentFrames(self):
log.info("Merging consecutive static component frames")
for compNo in range(len(self.components)):
- if compNo not in self.staticComponents \
- or compNo + 1 not in self.staticComponents:
+ if (
+ compNo not in self.staticComponents
+ or compNo + 1 not in self.staticComponents
+ ):
continue
self.staticComponents[compNo + 1] = Image.alpha_composite(
self.staticComponents.pop(compNo),
- self.staticComponents[compNo + 1]
+ self.staticComponents[compNo + 1],
)
self.staticComponents[compNo] = None
+
mergeConsecutiveStaticComponentFrames(self)
def frameRender(self, audioI):
- '''
+ """
Renders a frame composited together from the frames returned by each component
audioI is a multiple of self.sampleSize, which can be divided to determine frameNo
- '''
+ """
+
def err():
self.closePipe()
self.cancelExport()
self.error = True
- msg = 'A call to renderFrame in the video thread failed critically.'
+ msg = "A call to renderFrame in the video thread failed critically."
log.critical(msg)
comp._error.emit(msg, str(e))
@@ -222,18 +235,16 @@ class Worker(QtCore.QObject):
if frame is None: # bottom-most layer
frame = comp.frameRender(bgI)
else:
- frame = Image.alpha_composite(
- frame, comp.frameRender(bgI)
- )
+ frame = Image.alpha_composite(frame, comp.frameRender(bgI))
except Exception as e:
err()
return frame
def showPreview(self, frame):
- '''
+ """
Receives a final frame that will be piped to FFmpeg,
adds it to the MainWindow for the live preview
- '''
+ """
# We must store a reference to this QImage
# or else Qt will garbage-collect it on the C++ side
self.latestPreview = ImageQt(frame)
@@ -241,7 +252,7 @@ class Worker(QtCore.QObject):
@pyqtSlot()
def createVideo(self):
- '''
+ """
1. Numpy is set to ignore division errors during this method
2. Determine length of final video
3. Call preFrameRender on each component
@@ -250,15 +261,14 @@ class Worker(QtCore.QObject):
6. Iterate over the audio data array and call frameRender on the components to get frames
7. Close the out_pipe
8. Call postFrameRender on each component
- '''
+ """
log.debug("Video worker received signal to createVideo")
- log.debug(
- 'Video thread id: {}'.format(int(QtCore.QThread.currentThreadId())))
- numpy.seterr(divide='ignore')
+ log.debug("Video thread id: {}".format(int(QtCore.QThread.currentThreadId())))
+ numpy.seterr(divide="ignore")
self.encoding.emit(True)
self.extraAudio = []
- self.width = int(self.settings.value('outputWidth'))
- self.height = int(self.settings.value('outputHeight'))
+ self.width = int(self.settings.value("outputWidth"))
+ self.height = int(self.settings.value("outputHeight"))
# Set core.Core.canceled to False and call .reset() on each component
self.reset()
@@ -284,16 +294,18 @@ class Worker(QtCore.QObject):
if not ffmpegCommand:
return
cmd = " ".join(ffmpegCommand)
- print('###### FFMPEG COMMAND ######\n%s' % cmd)
- print('############################')
+ print("###### FFMPEG COMMAND ######\n%s" % cmd)
+ print("############################")
log.info(cmd)
# Open pipe to FFmpeg
- log.info('Opening pipe to FFmpeg')
+ log.info("Opening pipe to FFmpeg")
try:
self.out_pipe = openPipe(
ffmpegCommand,
- stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
+ stdin=sp.PIPE,
+ stdout=sys.stdout,
+ stderr=sys.stdout,
)
except sp.CalledProcessError:
log.critical("Out_Pipe to FFmpeg couldn't be created!", exc_info=True)
@@ -334,7 +346,7 @@ class Worker(QtCore.QObject):
# Finished creating the video!
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
- numpy.seterr(all='print')
+ numpy.seterr(all="print")
self.closePipe()
@@ -348,14 +360,14 @@ class Worker(QtCore.QObject):
except Exception:
pass
self.progressBarUpdate.emit(0)
- self.progressBarSetText.emit('Export Canceled')
+ self.progressBarSetText.emit("Export Canceled")
else:
if self.error:
self.failExport()
else:
print("Export Complete")
self.progressBarUpdate.emit(100)
- self.progressBarSetText.emit('Export Complete')
+ self.progressBarSetText.emit("Export Complete")
self.error = False
self.canceled = False
@@ -366,21 +378,21 @@ class Worker(QtCore.QObject):
try:
self.out_pipe.stdin.close()
except (BrokenPipeError, OSError):
- log.debug('Broken pipe to FFmpeg!')
+ log.debug("Broken pipe to FFmpeg!")
if self.out_pipe.stderr is not None:
log.error(self.out_pipe.stderr.read())
self.out_pipe.stderr.close()
self.error = True
self.out_pipe.wait()
- def cancelExport(self, message='Export Canceled'):
+ def cancelExport(self, message="Export Canceled"):
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit(message)
self.encoding.emit(False)
self.videoCreated.emit()
def failExport(self):
- self.cancelExport('Export Failed')
+ self.cancelExport("Export Failed")
def updateProgress(self, pStr, pVal):
self.progressBarValue.emit(pVal)