aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authortassaron2026-01-11 14:29:58 -0500
committertassaron2026-01-11 14:29:58 -0500
commit669756b391d26661cf2e2a97a304e73343ef6655 (patch)
tree9cf2d4858c209bdab9f44d5c7f95c2a30b37f7a6 /src
parent9d45f7f1a986aaa5d3c084c7ae747442b94a61b1 (diff)
update to Qt 6 and Pillow 12
and yeah, I accidentally ran black on the codebase. I don't want to spend more free time fixing that. All of these changes are simple renames or removals, nothing too major.
Diffstat (limited to 'src')
-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
28 files changed, 2469 insertions, 2168 deletions
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)