components auto-connect & track widgets, less autosave spam

importing toolkit from live interpreter now works
This commit is contained in:
tassaron 2017-07-23 01:53:54 -04:00
parent 450b944b87
commit bf0890e7c8
21 changed files with 600 additions and 616 deletions

View File

@ -2,7 +2,7 @@ from setuptools import setup
import os
__version__ = '2.0.0.rc1'
__version__ = '2.0.0.rc2'
def package_files(directory):

View File

@ -1 +1,13 @@
import sys
import os
if getattr(sys, 'frozen', False):
# frozen
wd = os.path.dirname(sys.executable)
else:
# unfrozen
wd = os.path.dirname(os.path.realpath(__file__))
# make relative imports work when using /src as a package
sys.path.insert(0, wd)

View File

@ -10,7 +10,6 @@ import sys
import time
from core import Core
from toolkit import loadDefaultSettings
class Command(QtCore.QObject):
@ -55,7 +54,6 @@ class Command(QtCore.QObject):
self.args = self.parser.parse_args()
self.settings = Core.settings
loadDefaultSettings(self)
if self.args.projpath:
projPath = self.args.projpath

View File

@ -5,8 +5,28 @@
from PyQt5 import uic, QtCore, QtWidgets
import os
from core import Core
from toolkit.common import getPresetDir
from presetmanager import getPresetDir
def commandWrapper(func):
'''Intercepts each component's command() method to check for global args'''
def decorator(self, arg):
if arg.startswith('preset='):
_, preset = arg.split('=', 1)
path = os.path.join(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)
)
self.core.openPreset(path, self.compPos, preset)
# Don't call the component's command() method
return
else:
return func(self, arg)
return decorator
class ComponentMetaclass(type(QtCore.QObject)):
@ -16,10 +36,14 @@ class ComponentMetaclass(type(QtCore.QObject)):
E.g., takes only major version from version string & decorates methods
'''
def __new__(cls, name, parents, attrs):
# print('Creating %s component' % attrs['name'])
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]
# Turn certain class methods into properties and classmethods
for key in ('error', 'properties', 'audio', 'commandHelp'):
for key in ('error', 'properties', 'audio'):
if key not in attrs:
continue
attrs[key] = property(attrs[key])
@ -29,6 +53,10 @@ class ComponentMetaclass(type(QtCore.QObject)):
continue
attrs[key] = classmethod(key)
# Do not apply these mutations to the base class
if parents[0] != QtCore.QObject:
attrs['command'] = commandWrapper(attrs['command'])
# Turn version string into a number
try:
if 'version' not in attrs:
@ -54,19 +82,24 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
name = 'Component'
# ui = 'nameOfNonDefaultUiFile'
version = '1.0.0'
# The 1st number (before dot, aka the major version) is used to determine
# The major version (before the first dot) is used to determine
# preset compatibility; the rest is ignored so it can be non-numeric.
modified = QtCore.pyqtSignal(int, dict)
# ^ Signal used to tell core program that the component state changed,
# you shouldn't need to use this directly, it is used by self.update()
def __init__(self, moduleIndex, compPos):
def __init__(self, moduleIndex, compPos, core):
super().__init__()
self.currentPreset = None
self.moduleIndex = moduleIndex
self.compPos = compPos
self.core = core
self.currentPreset = None
self._trackedWidgets = {}
self._presetNames = {}
# Stop lengthy processes in response to this variable
self.canceled = False
@ -114,28 +147,103 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
return []
def commandHelp(self):
'''Help text as string for this component's commandline arguments'''
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
def update(self):
'''Read widget values from self.page, then call super().update()'''
self.parent.drawPreview()
saveValueStore = self.savePreset()
saveValueStore['preset'] = self.currentPreset
self.modified.emit(self.compPos, saveValueStore)
def loadPreset(self, presetDict, presetName):
def widget(self, parent):
'''
Subclasses take (presetDict, presetName=None) as args.
Must use super().loadPreset(presetDict, presetName) first,
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/colour) and initialize
'''
self.parent = parent
self.settings = parent.settings
self.page = self.loadUi(self.__class__.ui)
# Connect widget signals
widgets = {
'lineEdit': self.page.findChildren(QtWidgets.QLineEdit),
'checkBox': self.page.findChildren(QtWidgets.QCheckBox),
'spinBox': self.page.findChildren(QtWidgets.QSpinBox),
'comboBox': self.page.findChildren(QtWidgets.QComboBox),
}
widgets['spinBox'].extend(
self.page.findChildren(QtWidgets.QDoubleSpinBox)
)
for widget in widgets['lineEdit']:
widget.textChanged.connect(self.update)
for widget in widgets['checkBox']:
widget.stateChanged.connect(self.update)
for widget in widgets['spinBox']:
widget.valueChanged.connect(self.update)
for widget in widgets['comboBox']:
widget.currentIndexChanged.connect(self.update)
def trackWidgets(self, trackDict, presetNames=None):
'''
Name widgets to track in update(), savePreset(), and loadPreset()
Accepts a dict with attribute names as keys and widgets as values.
Optional: a dict of attribute names to map to preset variable names
'''
self._trackedWidgets = trackDict
if type(presetNames) is dict:
self._presetNames = presetNames
def update(self):
'''
Reads all tracked widget values into instance attributes
and tells the MainWindow that the component was modified.
Call at the END of your method if you need to subclass this.
'''
for attr, widget in self._trackedWidgets.items():
if type(widget) == QtWidgets.QLineEdit:
setattr(self, attr, widget.text())
elif type(widget) == QtWidgets.QSpinBox \
or type(widget) == QtWidgets.QDoubleSpinBox:
setattr(self, attr, widget.value())
elif type(widget) == QtWidgets.QCheckBox:
setattr(self, attr, widget.isChecked())
elif type(widget) == QtWidgets.QComboBox:
setattr(self, attr, widget.currentIndex())
if not self.core.openingProject:
self.parent.drawPreview()
saveValueStore = self.savePreset()
saveValueStore['preset'] = self.currentPreset
self.modified.emit(self.compPos, saveValueStore)
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']
for attr, widget in self._trackedWidgets.items():
val = presetDict[
attr if attr not in self._presetNames
else self._presetNames[attr]
]
if type(widget) == QtWidgets.QLineEdit:
widget.setText(val)
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)
def savePreset(self):
saveValueStore = {}
for attr, widget in self._trackedWidgets.items():
saveValueStore[
attr if attr not in self._presetNames
else self._presetNames[attr]
] = getattr(self, attr)
return saveValueStore
def preFrameRender(self, **kwargs):
'''
@ -151,34 +259,27 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
for key, value in kwargs.items():
setattr(self, key, value)
def command(self, arg):
def commandHelp(self):
'''Help text as string for this component's commandline arguments'''
def command(self, arg=''):
'''
Configure a component using argument from the commandline.
Use super().command(arg) at the end of a subclass's method,
if no arguments are found in that method first
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.
'''
if arg.startswith('preset='):
_, preset = arg.split('=', 1)
path = os.path.join(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)
)
self.core.openPreset(path, self.compPos, preset)
else:
print(
self.__doc__, 'Usage:\n'
'Open a preset for this component:\n'
' "preset=Preset Name"')
print(self.commandHelp)
quit(0)
print(
self.__class__.name, 'Usage:\n'
'Open a preset for this component:\n'
' "preset=Preset Name"'
)
self.commandHelp()
quit(0)
def loadUi(self, filename):
'''Load a Qt Designer ui file to use for this component's widget'''
return uic.loadUi(os.path.join(Core.componentsPath, filename))
return uic.loadUi(os.path.join(self.core.componentsPath, filename))
def cancel(self):
'''Stop any lengthy process in response to this variable.'''
@ -191,16 +292,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
### Reference methods for creating a new component
### (Inherit from this class and define these)
def widget(self, parent):
self.parent = parent
self.settings = parent.settings
self.page = self.loadUi('example.ui')
# --- connect widget signals here ---
return self.page
def previewRender(self, previewWorker):
width = int(self.settings.value('outputWidth'))
height = int(previewWorker.core.settings.value('outputHeight'))
height = int(self.settings.value('outputHeight'))
from toolkit.frame import BlankFrame
image = BlankFrame(width, height)
return image
@ -217,7 +311,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
class BadComponentInit(Exception):
'''
General purpose exception components can raise to indicate
General purpose exception that components can raise to indicate
a Python issue with e.g., dynamic creation of instances or something.
Decorative for now, may have future use for logging.
'''

View File

@ -13,18 +13,15 @@ class Component(Component):
name = 'Color'
version = '1.0.0'
def widget(self, parent):
self.parent = parent
self.settings = parent.settings
page = self.loadUi('color.ui')
def widget(self, *args):
self.color1 = (0, 0, 0)
self.color2 = (133, 133, 133)
self.x = 0
self.y = 0
super().widget(*args)
page.lineEdit_color1.setText('%s,%s,%s' % self.color1)
page.lineEdit_color2.setText('%s,%s,%s' % self.color2)
self.page.lineEdit_color1.setText('%s,%s,%s' % self.color1)
self.page.lineEdit_color2.setText('%s,%s,%s' % self.color2)
btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.color1).name()
@ -32,68 +29,55 @@ class Component(Component):
btnStyle2 = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.color2).name()
page.pushButton_color1.setStyleSheet(btnStyle1)
page.pushButton_color2.setStyleSheet(btnStyle2)
page.pushButton_color1.clicked.connect(lambda: self.pickColor(1))
page.pushButton_color2.clicked.connect(lambda: self.pickColor(2))
self.page.pushButton_color1.setStyleSheet(btnStyle1)
self.page.pushButton_color2.setStyleSheet(btnStyle2)
self.page.pushButton_color1.clicked.connect(lambda: self.pickColor(1))
self.page.pushButton_color2.clicked.connect(lambda: self.pickColor(2))
# disable color #2 until non-default 'fill' option gets changed
page.lineEdit_color2.setDisabled(True)
page.pushButton_color2.setDisabled(True)
page.spinBox_x.valueChanged.connect(self.update)
page.spinBox_y.valueChanged.connect(self.update)
page.spinBox_width.setValue(
self.page.lineEdit_color2.setDisabled(True)
self.page.pushButton_color2.setDisabled(True)
self.page.spinBox_width.setValue(
int(self.settings.value("outputWidth")))
page.spinBox_height.setValue(
self.page.spinBox_height.setValue(
int(self.settings.value("outputHeight")))
page.lineEdit_color1.textChanged.connect(self.update)
page.lineEdit_color2.textChanged.connect(self.update)
page.spinBox_x.valueChanged.connect(self.update)
page.spinBox_y.valueChanged.connect(self.update)
page.spinBox_width.valueChanged.connect(self.update)
page.spinBox_height.valueChanged.connect(self.update)
page.checkBox_trans.stateChanged.connect(self.update)
self.fillLabels = [
'Solid',
'Linear Gradient',
'Radial Gradient',
]
for label in self.fillLabels:
page.comboBox_fill.addItem(label)
page.comboBox_fill.setCurrentIndex(0)
page.comboBox_fill.currentIndexChanged.connect(self.update)
page.comboBox_spread.currentIndexChanged.connect(self.update)
page.spinBox_radialGradient_end.valueChanged.connect(self.update)
page.spinBox_radialGradient_start.valueChanged.connect(self.update)
page.spinBox_radialGradient_spread.valueChanged.connect(self.update)
page.spinBox_linearGradient_end.valueChanged.connect(self.update)
page.spinBox_linearGradient_start.valueChanged.connect(self.update)
page.checkBox_stretch.stateChanged.connect(self.update)
self.page.comboBox_fill.addItem(label)
self.page.comboBox_fill.setCurrentIndex(0)
self.page = page
return page
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,
}, presetNames={
'sizeWidth': 'width',
'sizeHeight': 'height',
}
)
def update(self):
self.color1 = rgbFromString(self.page.lineEdit_color1.text())
self.color2 = rgbFromString(self.page.lineEdit_color2.text())
self.x = self.page.spinBox_x.value()
self.y = self.page.spinBox_y.value()
self.sizeWidth = self.page.spinBox_width.value()
self.sizeHeight = self.page.spinBox_height.value()
self.trans = self.page.checkBox_trans.isChecked()
self.spread = self.page.comboBox_spread.currentIndex()
self.RG_start = self.page.spinBox_radialGradient_start.value()
self.RG_end = self.page.spinBox_radialGradient_end.value()
self.RG_centre = self.page.spinBox_radialGradient_spread.value()
self.stretch = self.page.checkBox_stretch.isChecked()
self.LG_start = self.page.spinBox_linearGradient_start.value()
self.LG_end = self.page.spinBox_linearGradient_end.value()
self.fillType = self.page.comboBox_fill.currentIndex()
if self.fillType == 0:
fillType = self.page.comboBox_fill.currentIndex()
if fillType == 0:
self.page.lineEdit_color2.setEnabled(False)
self.page.pushButton_color2.setEnabled(False)
self.page.checkBox_trans.setEnabled(False)
@ -105,10 +89,10 @@ class Component(Component):
self.page.checkBox_trans.setEnabled(True)
self.page.checkBox_stretch.setEnabled(True)
self.page.comboBox_spread.setEnabled(True)
if self.trans:
if self.page.checkBox_trans.isChecked():
self.page.lineEdit_color2.setEnabled(False)
self.page.pushButton_color2.setEnabled(False)
self.page.fillWidget.setCurrentIndex(self.fillType)
self.page.fillWidget.setCurrentIndex(fillType)
super().update()
@ -181,25 +165,11 @@ class Component(Component):
return image.finalize()
def loadPreset(self, pr, presetName=None):
super().loadPreset(pr, presetName)
def loadPreset(self, pr, *args):
super().loadPreset(pr, *args)
self.page.comboBox_fill.setCurrentIndex(pr['fillType'])
self.page.lineEdit_color1.setText('%s,%s,%s' % pr['color1'])
self.page.lineEdit_color2.setText('%s,%s,%s' % pr['color2'])
self.page.spinBox_x.setValue(pr['x'])
self.page.spinBox_y.setValue(pr['y'])
self.page.spinBox_width.setValue(pr['width'])
self.page.spinBox_height.setValue(pr['height'])
self.page.checkBox_trans.setChecked(pr['trans'])
self.page.spinBox_radialGradient_start.setValue(pr['RG_start'])
self.page.spinBox_radialGradient_end.setValue(pr['RG_end'])
self.page.spinBox_radialGradient_spread.setValue(pr['RG_centre'])
self.page.spinBox_linearGradient_start.setValue(pr['LG_start'])
self.page.spinBox_linearGradient_end.setValue(pr['LG_end'])
self.page.checkBox_stretch.setChecked(pr['stretch'])
self.page.comboBox_spread.setCurrentIndex(pr['spread'])
btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*pr['color1']).name()
@ -209,23 +179,10 @@ class Component(Component):
self.page.pushButton_color2.setStyleSheet(btnStyle2)
def savePreset(self):
return {
'color1': self.color1,
'color2': self.color2,
'x': self.x,
'y': self.y,
'fillType': self.fillType,
'width': self.sizeWidth,
'height': self.sizeHeight,
'trans': self.trans,
'stretch': self.stretch,
'spread': self.spread,
'RG_start': self.RG_start,
'RG_end': self.RG_end,
'RG_centre': self.RG_centre,
'LG_start': self.LG_start,
'LG_end': self.LG_end,
}
saveValueStore = super().savePreset()
saveValueStore['color1'] = self.color1
saveValueStore['color2'] = self.color2
return saveValueStore
def pickColor(self, num):
RGBstring, btnStyle = pickColor()
@ -242,7 +199,7 @@ class Component(Component):
print('Specify a color:\n color=255,255,255')
def command(self, arg):
if not arg.startswith('preset=') and '=' in arg:
if '=' in arg:
key, arg = arg.split('=', 1)
if key == 'color':
self.page.lineEdit_color1.setText(arg)

View File

@ -2,7 +2,6 @@ from PIL import Image, ImageDraw, ImageEnhance
from PyQt5 import QtGui, QtCore, QtWidgets
import os
from core import Core
from component import Component
from toolkit.frame import BlankFrame
@ -11,35 +10,26 @@ class Component(Component):
name = 'Image'
version = '1.0.0'
def widget(self, parent):
self.parent = parent
self.settings = parent.settings
page = self.loadUi('image.ui')
page.lineEdit_image.textChanged.connect(self.update)
page.pushButton_image.clicked.connect(self.pickImage)
page.spinBox_scale.valueChanged.connect(self.update)
page.spinBox_rotate.valueChanged.connect(self.update)
page.spinBox_color.valueChanged.connect(self.update)
page.checkBox_stretch.stateChanged.connect(self.update)
page.checkBox_mirror.stateChanged.connect(self.update)
page.spinBox_x.valueChanged.connect(self.update)
page.spinBox_y.valueChanged.connect(self.update)
self.page = page
return page
def update(self):
self.imagePath = self.page.lineEdit_image.text()
self.scale = self.page.spinBox_scale.value()
self.rotate = self.page.spinBox_rotate.value()
self.color = self.page.spinBox_color.value()
self.xPosition = self.page.spinBox_x.value()
self.yPosition = self.page.spinBox_y.value()
self.stretched = self.page.checkBox_stretch.isChecked()
self.mirror = self.page.checkBox_mirror.isChecked()
super().update()
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,
'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',
},
)
def previewRender(self, previewWorker):
width = int(self.settings.value('outputWidth'))
@ -89,41 +79,18 @@ class Component(Component):
return frame
def loadPreset(self, pr, presetName=None):
super().loadPreset(pr, presetName)
self.page.lineEdit_image.setText(pr['image'])
self.page.spinBox_scale.setValue(pr['scale'])
self.page.spinBox_color.setValue(pr['color'])
self.page.spinBox_rotate.setValue(pr['rotate'])
self.page.spinBox_x.setValue(pr['x'])
self.page.spinBox_y.setValue(pr['y'])
self.page.checkBox_stretch.setChecked(pr['stretched'])
self.page.checkBox_mirror.setChecked(pr['mirror'])
def savePreset(self):
return {
'image': self.imagePath,
'scale': self.scale,
'color': self.color,
'rotate': self.rotate,
'stretched': self.stretched,
'mirror': self.mirror,
'x': self.xPosition,
'y': self.yPosition,
}
def pickImage(self):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Image", imgDir,
"Image Files (%s)" % " ".join(Core.imageFormats))
"Image Files (%s)" % " ".join(self.core.imageFormats))
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_image.setText(filename)
self.update()
def command(self, arg):
if not arg.startswith('preset=') and '=' in arg:
if '=' in arg:
key, arg = arg.split('=', 1)
if key == 'path' and os.path.exists(arg):
try:

View File

@ -18,59 +18,46 @@ class Component(Component):
def names():
return ['Original Audio Visualization']
def widget(self, parent):
self.parent = parent
self.settings = parent.settings
def widget(self, *args):
self.visColor = (255, 255, 255)
self.scale = 20
self.y = 0
self.canceled = False
super().widget(*args)
page = self.loadUi('original.ui')
page.comboBox_visLayout.addItem("Classic")
page.comboBox_visLayout.addItem("Split")
page.comboBox_visLayout.addItem("Bottom")
page.comboBox_visLayout.addItem("Top")
page.comboBox_visLayout.setCurrentIndex(0)
page.comboBox_visLayout.currentIndexChanged.connect(self.update)
page.lineEdit_visColor.setText('%s,%s,%s' % self.visColor)
page.pushButton_visColor.clicked.connect(lambda: self.pickColor())
self.page.comboBox_visLayout.addItem("Classic")
self.page.comboBox_visLayout.addItem("Split")
self.page.comboBox_visLayout.addItem("Bottom")
self.page.comboBox_visLayout.addItem("Top")
self.page.comboBox_visLayout.setCurrentIndex(0)
self.page.lineEdit_visColor.setText('%s,%s,%s' % self.visColor)
self.page.pushButton_visColor.clicked.connect(lambda: self.pickColor())
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.visColor).name()
page.pushButton_visColor.setStyleSheet(btnStyle)
page.lineEdit_visColor.textChanged.connect(self.update)
page.spinBox_scale.valueChanged.connect(self.update)
page.spinBox_y.valueChanged.connect(self.update)
self.page.pushButton_visColor.setStyleSheet(btnStyle)
self.page = page
return page
self.trackWidgets({
'layout': self.page.comboBox_visLayout,
'scale': self.page.spinBox_scale,
'y': self.page.spinBox_y,
})
def update(self):
self.layout = self.page.comboBox_visLayout.currentIndex()
self.visColor = rgbFromString(self.page.lineEdit_visColor.text())
self.scale = self.page.spinBox_scale.value()
self.y = self.page.spinBox_y.value()
super().update()
def loadPreset(self, pr, presetName=None):
super().loadPreset(pr, presetName)
def loadPreset(self, pr, *args):
super().loadPreset(pr, *args)
self.page.lineEdit_visColor.setText('%s,%s,%s' % pr['visColor'])
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*pr['visColor']).name()
self.page.pushButton_visColor.setStyleSheet(btnStyle)
self.page.comboBox_visLayout.setCurrentIndex(pr['layout'])
self.page.spinBox_scale.setValue(pr['scale'])
self.page.spinBox_y.setValue(pr['y'])
def savePreset(self):
return {
'layout': self.layout,
'visColor': self.visColor,
'scale': self.scale,
'y': self.y,
}
saveValueStore = super().savePreset()
saveValueStore['visColor'] = self.visColor
return saveValueStore
def previewRender(self, previewWorker):
spectrum = numpy.fromfunction(
@ -206,7 +193,7 @@ class Component(Component):
return im
def command(self, arg):
if not arg.startswith('preset=') and '=' in arg:
if '=' in arg:
key, arg = arg.split('=', 1)
try:
if key == 'color':

View File

@ -10,26 +10,15 @@ class Component(Component):
name = 'Sound'
version = '1.0.0'
def widget(self, parent):
self.parent = parent
self.settings = parent.settings
page = self.loadUi('sound.ui')
page.lineEdit_sound.textChanged.connect(self.update)
page.pushButton_sound.clicked.connect(self.pickSound)
page.checkBox_chorus.stateChanged.connect(self.update)
page.spinBox_delay.valueChanged.connect(self.update)
page.spinBox_volume.valueChanged.connect(self.update)
self.page = page
return page
def update(self):
self.sound = self.page.lineEdit_sound.text()
self.delay = self.page.spinBox_delay.value()
self.volume = self.page.spinBox_volume.value()
self.chorus = self.page.checkBox_chorus.isChecked()
super().update()
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,
})
def previewRender(self, previewWorker):
width = int(self.settings.value('outputWidth'))
@ -67,7 +56,7 @@ class Component(Component):
sndDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Sound", sndDir,
"Audio Files (%s)" % " ".join(Core.audioFormats))
"Audio Files (%s)" % " ".join(self.core.audioFormats))
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_sound.setText(filename)
@ -78,30 +67,15 @@ class Component(Component):
height = int(self.settings.value('outputHeight'))
return BlankFrame(width, height)
def loadPreset(self, pr, presetName=None):
super().loadPreset(pr, presetName)
self.page.lineEdit_sound.setText(pr['sound'])
self.page.checkBox_chorus.setChecked(pr['chorus'])
self.page.spinBox_delay.setValue(pr['delay'])
self.page.spinBox_volume.setValue(pr['volume'])
def savePreset(self):
return {
'sound': self.sound,
'chorus': self.chorus,
'delay': self.delay,
'volume': self.volume,
}
def commandHelp(self):
print('Path to audio file:\n path=/filepath/to/sound.ogg')
def command(self, arg):
if not arg.startswith('preset=') and '=' in arg:
if '=' in arg:
key, arg = arg.split('=', 1)
if key == 'path':
if '*%s' % os.path.splitext(arg)[1] \
not in Core.audioFormats:
not in self.core.audioFormats:
print("Not a supported audio format")
quit(1)
self.page.lineEdit_sound.setText(arg)

View File

@ -16,12 +16,10 @@ class Component(Component):
super().__init__(*args)
self.titleFont = QFont()
def widget(self, parent):
self.parent = parent
self.settings = parent.settings
def widget(self, *args):
super().widget(*args)
height = int(self.settings.value('outputHeight'))
width = int(self.settings.value('outputWidth'))
self.textColor = (255, 255, 255)
self.title = 'Text'
self.alignment = 1
@ -30,40 +28,35 @@ class Component(Component):
self.xPosition = width / 2 - fm.width(self.title)/2
self.yPosition = height / 2 * 1.036
page = self.loadUi('text.ui')
page.comboBox_textAlign.addItem("Left")
page.comboBox_textAlign.addItem("Middle")
page.comboBox_textAlign.addItem("Right")
self.page.comboBox_textAlign.addItem("Left")
self.page.comboBox_textAlign.addItem("Middle")
self.page.comboBox_textAlign.addItem("Right")
page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor)
page.pushButton_textColor.clicked.connect(self.pickColor)
self.page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor)
self.page.pushButton_textColor.clicked.connect(self.pickColor)
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.textColor).name()
page.pushButton_textColor.setStyleSheet(btnStyle)
self.page.pushButton_textColor.setStyleSheet(btnStyle)
page.lineEdit_title.setText(self.title)
page.comboBox_textAlign.setCurrentIndex(int(self.alignment))
page.spinBox_fontSize.setValue(int(self.fontSize))
page.spinBox_xTextAlign.setValue(int(self.xPosition))
page.spinBox_yTextAlign.setValue(int(self.yPosition))
self.page.lineEdit_title.setText(self.title)
self.page.comboBox_textAlign.setCurrentIndex(int(self.alignment))
self.page.spinBox_fontSize.setValue(int(self.fontSize))
self.page.spinBox_xTextAlign.setValue(int(self.xPosition))
self.page.spinBox_yTextAlign.setValue(int(self.yPosition))
page.fontComboBox_titleFont.currentFontChanged.connect(self.update)
page.lineEdit_title.textChanged.connect(self.update)
page.comboBox_textAlign.currentIndexChanged.connect(self.update)
page.spinBox_xTextAlign.valueChanged.connect(self.update)
page.spinBox_yTextAlign.valueChanged.connect(self.update)
page.spinBox_fontSize.valueChanged.connect(self.update)
page.lineEdit_textColor.textChanged.connect(self.update)
self.page = page
return page
self.page.fontComboBox_titleFont.currentFontChanged.connect(
self.update
)
self.trackWidgets({
'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,
})
def update(self):
self.title = self.page.lineEdit_title.text()
self.alignment = self.page.comboBox_textAlign.currentIndex()
self.titleFont = self.page.fontComboBox_titleFont.currentFont()
self.fontSize = self.page.spinBox_fontSize.value()
self.xPosition = self.page.spinBox_xTextAlign.value()
self.yPosition = self.page.spinBox_yTextAlign.value()
self.textColor = rgbFromString(
self.page.lineEdit_textColor.text())
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
@ -87,32 +80,22 @@ class Component(Component):
x = self.xPosition - offset
return x, self.yPosition
def loadPreset(self, pr, presetName=None):
super().loadPreset(pr, presetName)
def loadPreset(self, pr, *args):
super().loadPreset(pr, *args)
self.page.lineEdit_title.setText(pr['title'])
font = QFont()
font.fromString(pr['titleFont'])
self.page.fontComboBox_titleFont.setCurrentFont(font)
self.page.spinBox_fontSize.setValue(pr['fontSize'])
self.page.comboBox_textAlign.setCurrentIndex(pr['alignment'])
self.page.spinBox_xTextAlign.setValue(pr['xPosition'])
self.page.spinBox_yTextAlign.setValue(pr['yPosition'])
self.page.lineEdit_textColor.setText('%s,%s,%s' % pr['textColor'])
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*pr['textColor']).name()
self.page.pushButton_textColor.setStyleSheet(btnStyle)
def savePreset(self):
return {
'title': self.title,
'titleFont': self.titleFont.toString(),
'alignment': self.alignment,
'fontSize': self.fontSize,
'xPosition': self.xPosition,
'yPosition': self.yPosition,
'textColor': self.textColor
}
saveValueStore = super().savePreset()
saveValueStore['titleFont'] = self.titleFont.toString()
saveValueStore['textColor'] = self.textColor
return saveValueStore
def previewRender(self, previewWorker):
width = int(self.settings.value('outputWidth'))
@ -158,7 +141,7 @@ class Component(Component):
print('Set custom x, y position:\n x=500 y=500')
def command(self, arg):
if not arg.startswith('preset=') and '=' in arg:
if '=' in arg:
key, arg = arg.split('=', 1)
if key == 'color':
self.page.lineEdit_textColor.setText(arg)

View File

@ -9,6 +9,7 @@ from queue import PriorityQueue
from core import Core
from component import Component, BadComponentInit
from toolkit.frame import BlankFrame
from toolkit.ffmpeg import testAudioStream
from toolkit import openPipe, checkOutput
@ -16,7 +17,7 @@ class Video:
'''Video Component Frame-Fetcher'''
def __init__(self, **kwargs):
mandatoryArgs = [
'ffmpeg', # path to ffmpeg, usually Core.FFMPEG_BIN
'ffmpeg', # path to ffmpeg, usually self.core.FFMPEG_BIN
'videoPath',
'width',
'height',
@ -110,47 +111,40 @@ class Component(Component):
name = 'Video'
version = '1.0.0'
def widget(self, parent):
self.parent = parent
self.settings = parent.settings
page = self.loadUi('video.ui')
def widget(self, *args):
self.videoPath = ''
self.badVideo = False
self.badAudio = False
self.x = 0
self.y = 0
self.loopVideo = False
page.lineEdit_video.textChanged.connect(self.update)
page.pushButton_video.clicked.connect(self.pickVideo)
page.checkBox_loop.stateChanged.connect(self.update)
page.checkBox_distort.stateChanged.connect(self.update)
page.checkBox_useAudio.stateChanged.connect(self.update)
page.spinBox_scale.valueChanged.connect(self.update)
page.spinBox_volume.valueChanged.connect(self.update)
page.spinBox_x.valueChanged.connect(self.update)
page.spinBox_y.valueChanged.connect(self.update)
self.page = page
return page
super().widget(*args)
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',
}
)
def update(self):
self.videoPath = self.page.lineEdit_video.text()
self.loopVideo = self.page.checkBox_loop.isChecked()
self.useAudio = self.page.checkBox_useAudio.isChecked()
self.distort = self.page.checkBox_distort.isChecked()
self.scale = self.page.spinBox_scale.value()
self.volume = self.page.spinBox_volume.value()
self.xPosition = self.page.spinBox_x.value()
self.yPosition = self.page.spinBox_y.value()
if self.useAudio:
if self.page.checkBox_useAudio.isChecked():
self.page.label_volume.setEnabled(True)
self.page.spinBox_volume.setEnabled(True)
else:
self.page.label_volume.setEnabled(False)
self.page.spinBox_volume.setEnabled(False)
super().update()
def previewRender(self, previewWorker):
@ -188,18 +182,7 @@ class Component(Component):
return "The video selected is corrupt!"
def testAudioStream(self):
# test if an audio stream really exists
audioTestCommand = [
Core.FFMPEG_BIN,
'-i', self.videoPath,
'-vn', '-f', 'null', '-'
]
try:
checkOutput(audioTestCommand, stderr=subprocess.DEVNULL)
except subprocess.CalledProcessError:
self.badAudio = True
else:
self.badAudio = False
self.badAudio = testAudioStream(self.videoPath)
def audio(self):
params = {}
@ -214,7 +197,7 @@ class Component(Component):
self.blankFrame_ = BlankFrame(width, height)
self.updateChunksize(width, height)
self.video = Video(
ffmpeg=Core.FFMPEG_BIN, videoPath=self.videoPath,
ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath,
width=width, height=height, chunkSize=self.chunkSize,
frameRate=int(self.settings.value("outputFrameRate")),
parent=self.parent, loopVideo=self.loopVideo,
@ -227,34 +210,11 @@ class Component(Component):
else:
return self.blankFrame_
def loadPreset(self, pr, presetName=None):
super().loadPreset(pr, presetName)
self.page.lineEdit_video.setText(pr['video'])
self.page.checkBox_loop.setChecked(pr['loop'])
self.page.checkBox_useAudio.setChecked(pr['useAudio'])
self.page.checkBox_distort.setChecked(pr['distort'])
self.page.spinBox_scale.setValue(pr['scale'])
self.page.spinBox_volume.setValue(pr['volume'])
self.page.spinBox_x.setValue(pr['x'])
self.page.spinBox_y.setValue(pr['y'])
def savePreset(self):
return {
'video': self.videoPath,
'loop': self.loopVideo,
'useAudio': self.useAudio,
'distort': self.distort,
'scale': self.scale,
'volume': self.volume,
'x': self.xPosition,
'y': self.yPosition,
}
def pickVideo(self):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Video",
imgDir, "Video Files (%s)" % " ".join(Core.videoFormats)
imgDir, "Video Files (%s)" % " ".join(self.core.videoFormats)
)
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
@ -266,7 +226,7 @@ class Component(Component):
return
command = [
self.parent.core.FFMPEG_BIN,
self.core.FFMPEG_BIN,
'-thread_queue_size', '512',
'-i', self.videoPath,
'-f', 'image2pipe',
@ -294,10 +254,10 @@ class Component(Component):
self.chunkSize = 4*width*height
def command(self, arg):
if not arg.startswith('preset=') and '=' in 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 Core.videoFormats:
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)

View File

@ -1,5 +1,6 @@
'''
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
import sys
@ -8,7 +9,6 @@ import json
from importlib import import_module
import toolkit
from toolkit.ffmpeg import findFfmpeg
import video_thread
@ -16,82 +16,21 @@ 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 and handles opening/creating project files
and presets. The class also stores constants as class variables.
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.
'''
@classmethod
def storeSettings(cls):
'''Store settings/paths to directories as class variables.'''
if getattr(sys, 'frozen', False):
# frozen
wd = os.path.dirname(sys.executable)
else:
wd = os.path.dirname(os.path.realpath(__file__))
dataDir = QtCore.QStandardPaths.writableLocation(
QtCore.QStandardPaths.AppConfigLocation
)
with open(os.path.join(wd, 'encoder-options.json')) as json_file:
encoderOptions = json.load(json_file)
settings = {
'wd': wd,
'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'),
'encoderOptions': encoderOptions,
'FFMPEG_BIN': findFfmpeg(),
'canceled': False,
}
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():
setattr(cls, classvar, val)
# Make settings accessible to the toolkit package
toolkit.init(settings)
def __init__(self):
Core.storeSettings()
self.findComponents()
self.selectedComponents = []
self.savedPresets = {} # copies of presets to detect modification
self.openingProject = False
def findComponents(self):
'''Imports all the component modules'''
def findComponents():
for f in sorted(os.listdir(Core.componentsPath)):
for f in os.listdir(Core.componentsPath):
name, ext = os.path.splitext(f)
if name.startswith("__"):
continue
@ -104,8 +43,13 @@ class Core:
# store canonical module names and indexes
self.moduleIndexes = [i for i in range(len(self.modules))]
self.compNames = [mod.Component.name for mod in self.modules]
self.altCompNames = []
# alphabetize modules by Component name
sortedModules = sorted(zip(self.compNames, self.modules))
self.compNames = [y[0] for y in sortedModules]
self.modules = [y[1] for y in sortedModules]
# store alternative names for modules
self.altCompNames = []
for i, mod in enumerate(self.modules):
if hasattr(mod.Component, 'names'):
for name in mod.Component.names():
@ -116,14 +60,17 @@ class Core:
component.compPos = i
def insertComponent(self, compPos, moduleIndex, loader):
'''Creates a new component'''
'''
Creates a new component using these args:
(compPos, moduleIndex in self.modules, MWindow/Command/Core obj)
'''
if compPos < 0 or compPos > len(self.selectedComponents):
compPos = len(self.selectedComponents)
if len(self.selectedComponents) > 50:
return None
component = self.modules[moduleIndex].Component(
moduleIndex, compPos
moduleIndex, compPos, self
)
self.selectedComponents.insert(
compPos,
@ -206,6 +153,7 @@ class Core:
errcode, data = self.parseAvFile(filepath)
if errcode == 0:
self.openingProject = True
try:
if hasattr(loader, 'window'):
for widget, value in data['WindowFields']:
@ -239,7 +187,8 @@ class Core:
i = self.insertComponent(
-1,
self.moduleIndexFor(name),
loader)
loader
)
if i is None:
loader.showMessage(msg="Too many components!")
break
@ -284,6 +233,7 @@ class Core:
showCancel=False,
icon='Warning',
detail=msg)
self.openingProject = False
def parseAvFile(self, filepath):
'''Parses an avp (project) or avl (preset package) file.
@ -467,8 +417,106 @@ class Core:
def cancel(self):
Core.canceled = True
toolkit.cancel()
def reset(self):
Core.canceled = False
toolkit.reset()
@classmethod
def storeSettings(cls):
'''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
)
with open(os.path.join(wd, 'encoder-options.json')) as json_file:
encoderOptions = json.load(json_file)
settings = {
'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'),
'encoderOptions': encoderOptions,
'resolutions': [
'1920x1080',
'1280x720',
'854x480',
],
'windowHasFocus': False,
'FFMPEG_BIN': findFfmpeg(),
'canceled': False,
}
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():
setattr(cls, classvar, val)
cls.loadDefaultSettings()
@classmethod
def loadDefaultSettings(cls):
defaultSettings = {
"outputWidth": 1280,
"outputHeight": 720,
"outputFrameRate": 30,
"outputAudioCodec": "AAC",
"outputAudioBitrate": "192",
"outputVideoCodec": "H264",
"outputVideoBitrate": "2500",
"outputVideoFormat": "yuv420p",
"outputPreset": "medium",
"outputFormat": "mp4",
"outputContainer": "MP4",
"projectDir": os.path.join(cls.dataDir, 'projects'),
"pref_insertCompAtTop": True,
}
for parm, value in defaultSettings.items():
if cls.settings.value(parm) is None:
cls.settings.setValue(parm, value)
# 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_'):
continue
val = cls.settings.value(key)
if val in ('true', 'false'):
cls.settings.setValue(key, True if val == 'true' else False)
# always store settings in class variables even if a Core object is not created
Core.storeSettings()

View File

@ -2,22 +2,17 @@ from PyQt5 import uic, QtWidgets
import sys
import os
from __init__ import wd
def main():
if getattr(sys, 'frozen', False):
# frozen
wd = os.path.dirname(sys.executable)
else:
# unfrozen
wd = os.path.dirname(os.path.realpath(__file__))
# make local imports work everywhere
sys.path.insert(0, wd)
app = QtWidgets.QApplication(sys.argv)
app.setApplicationName("audio-visualizer")
# Determine mode
mode = 'GUI'
if len(sys.argv) > 2:
mode = 'commandline'
elif len(sys.argv) == 2:
if sys.argv[1].startswith('-'):
mode = 'commandline'
@ -28,11 +23,7 @@ def main():
# normal gui launch
proj = None
print('Starting Audio Visualizer in %s mode' % mode)
app = QtWidgets.QApplication(sys.argv)
app.setApplicationName("audio-visualizer")
# app.setOrganizationName("audio-visualizer")
# Launch program
if mode == 'commandline':
from command import Command
@ -61,9 +52,7 @@ def main():
signal.signal(signal.SIGINT, main.cleanUp)
atexit.register(main.cleanUp)
# applicable to both modes
sys.exit(app.exec_())
if __name__ == "__main__":
main()

View File

@ -17,7 +17,7 @@ import time
from core import Core
import preview_thread
from presetmanager import PresetManager
from toolkit import loadDefaultSettings, disableWhenEncoding, checkOutput
from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput
class PreviewWindow(QtWidgets.QLabel):
@ -25,6 +25,7 @@ class PreviewWindow(QtWidgets.QLabel):
Paints the preview QLabel and maintains the aspect ratio when the
window is resized.
'''
def __init__(self, parent, img):
super(PreviewWindow, self).__init__()
self.parent = parent
@ -49,6 +50,14 @@ class PreviewWindow(QtWidgets.QLabel):
self.pixmap = QtGui.QPixmap(img)
self.repaint()
@QtCore.pyqtSlot(str)
def threadError(self, msg):
self.parent.showMessage(
msg=msg,
icon='Warning',
parent=self
)
class MainWindow(QtWidgets.QMainWindow):
'''
@ -66,13 +75,16 @@ class MainWindow(QtWidgets.QMainWindow):
def __init__(self, window, project):
QtWidgets.QMainWindow.__init__(self)
# print('main thread id: {}'.format(QtCore.QThread.currentThreadId()))
self.window = window
self.core = Core()
self.pages = [] # widgets of component settings
# widgets of component settings
self.pages = []
self.lastAutosave = time.time()
# list of previous five autosave times, used to reduce update spam
self.autosaveTimes = []
self.autosaveCooldown = 0.2
self.encoding = False
# Create data directory, load/create settings
@ -80,7 +92,6 @@ class MainWindow(QtWidgets.QMainWindow):
self.presetDir = Core.presetDir
self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
self.settings = Core.settings
loadDefaultSettings(self)
self.presetManager = PresetManager(
uic.loadUi(
os.path.join(Core.wd, 'presetmanager.ui')), self)
@ -92,13 +103,17 @@ class MainWindow(QtWidgets.QMainWindow):
if not os.path.exists(neededDirectory):
os.mkdir(neededDirectory)
# Make queues/timers for the preview thread
# Create the preview window and its thread, queues, and timers
self.previewWindow = PreviewWindow(self, os.path.join(
Core.wd, "background.png"))
window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
self.previewQueue = Queue()
self.previewThread = QtCore.QThread(self)
self.previewWorker = preview_thread.Worker(self, self.previewQueue)
self.previewWorker.error.connect(self.previewWindow.threadError)
self.previewWorker.moveToThread(self.previewThread)
self.previewWorker.imageCreated.connect(self.showPreviewImage)
self.previewWorker.error.connect(self.cleanUp)
self.previewThread.start()
self.timer = QtCore.QTimer(self)
@ -106,6 +121,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.timer.start(500)
# Begin decorating the window and connecting events
self.window.installEventFilter(self)
componentList = self.window.listWidget_componentList
if sys.platform == 'darwin':
@ -168,14 +184,9 @@ class MainWindow(QtWidgets.QMainWindow):
window.spinBox_vBitrate.setValue(vBitrate)
window.spinBox_aBitrate.setValue(aBitrate)
window.spinBox_vBitrate.valueChanged.connect(self.updateCodecSettings)
window.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings)
self.previewWindow = PreviewWindow(self, os.path.join(
Core.wd, "background.png"))
window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
# Make component buttons
self.compMenu = QMenu()
for i, comp in enumerate(self.core.modules):
@ -204,7 +215,7 @@ class MainWindow(QtWidgets.QMainWindow):
currentRes = str(self.settings.value('outputWidth'))+'x' + \
str(self.settings.value('outputHeight'))
for i, res in enumerate(self.resolutions):
for i, res in enumerate(Core.resolutions):
window.comboBox_resolution.addItem(res)
if res == currentRes:
currentRes = i
@ -375,6 +386,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.previewThread.quit()
self.previewThread.wait()
@disableWhenOpeningProject
def updateWindowTitle(self):
appName = 'Audio Visualizer'
try:
@ -442,13 +454,29 @@ class MainWindow(QtWidgets.QMainWindow):
self.settings.setValue('outputVideoBitrate', currentVideoBitrate)
self.settings.setValue('outputAudioBitrate', currentAudioBitrate)
@disableWhenOpeningProject
def autosave(self, force=False):
if not self.currentProject:
if os.path.exists(self.autosavePath):
os.remove(self.autosavePath)
elif force or time.time() - self.lastAutosave >= 0.2:
elif force or time.time() - self.lastAutosave >= self.autosaveCooldown:
self.core.createProjectFile(self.autosavePath, self.window)
self.lastAutosave = time.time()
if len(self.autosaveTimes) >= 5:
# Do some math to reduce autosave spam. This gives a smooth
# 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 self.autosaveCooldown / 4.0 < 0.5:
self.autosaveCooldown += 1.0
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)
def autosaveExists(self, identical=True):
'''Determines if creating the autosave should be blocked.'''
@ -602,15 +630,20 @@ class MainWindow(QtWidgets.QMainWindow):
def updateResolution(self):
resIndex = int(self.window.comboBox_resolution.currentIndex())
res = self.resolutions[resIndex].split('x')
res = Core.resolutions[resIndex].split('x')
self.settings.setValue('outputWidth', res[0])
self.settings.setValue('outputHeight', res[1])
self.drawPreview()
def drawPreview(self, force=False):
def drawPreview(self, force=False, **kwargs):
'''Use autosave keyword arg to force saving or not saving if needed'''
self.newTask.emit(self.core.selectedComponents)
# self.processTask.emit()
self.autosave(force)
if force or 'autosave' in kwargs:
if force or kwargs['autosave']:
self.autosave(True)
else:
self.autosave()
self.updateWindowTitle()
@QtCore.pyqtSlot(QtGui.QImage)
@ -685,9 +718,13 @@ class MainWindow(QtWidgets.QMainWindow):
stackedWidget.insertWidget(newRow, page)
componentList.setCurrentRow(newRow)
stackedWidget.setCurrentIndex(newRow)
self.drawPreview()
self.drawPreview(True)
def getComponentListRects(self):
def getComponentListMousePos(self, position):
'''
Given a QPos, returns the component index under the mouse cursor
or -1 if no component is there.
'''
componentList = self.window.listWidget_componentList
modelIndexes = [
@ -698,20 +735,23 @@ class MainWindow(QtWidgets.QMainWindow):
componentList.visualRect(modelIndex)
for modelIndex in modelIndexes
]
return rects
mousePos = [rect.contains(position) for rect in rects]
if not any(mousePos):
# Not clicking a component
mousePos = -1
else:
mousePos = mousePos.index(True)
return mousePos
@disableWhenEncoding
def dragComponent(self, event):
'''Used as Qt drop event for the component listwidget'''
componentList = self.window.listWidget_componentList
rects = self.getComponentListRects()
rowPos = [rect.contains(event.pos()) for rect in rects]
if not any(rowPos):
return
i = rowPos.index(True)
change = (componentList.currentRow() - i) * -1
mousePos = self.getComponentListMousePos(event.pos())
if mousePos > -1:
change = (componentList.currentRow() - mousePos) * -1
else:
change = (componentList.count() - componentList.currentRow() -1)
self.moveComponent(change)
def changeComponentWidget(self):
@ -814,9 +854,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.settings.setValue("projectDir", os.path.dirname(filepath))
# actually load the project using core method
self.core.openProject(self, filepath)
if self.window.listWidget_componentList.count() == 0:
self.drawPreview()
self.autosave(True)
self.drawPreview(autosave=False)
self.updateWindowTitle()
def showMessage(self, **kwargs):
@ -843,20 +881,11 @@ class MainWindow(QtWidgets.QMainWindow):
def componentContextMenu(self, QPos):
'''Appears when right-clicking the component list'''
componentList = self.window.listWidget_componentList
index = componentList.currentRow()
self.menu = QMenu()
parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0))
rects = self.getComponentListRects()
rowPos = [rect.contains(QPos) for rect in rects]
if not any(rowPos):
# Insert components at the top if clicking nothing
rowPos = 0
else:
rowPos = rowPos.index(True)
if index == rowPos:
index = self.getComponentListMousePos(QPos)
if index > -1:
# Show preset menu if clicking a component
self.presetManager.findPresets()
menuItem = self.menu.addAction("Save Preset")
@ -891,13 +920,23 @@ class MainWindow(QtWidgets.QMainWindow):
# "Add Component" submenu
self.submenu = QMenu("Add")
self.menu.addMenu(self.submenu)
insertCompAtTop = self.settings.value("pref_insertCompAtTop")
for i, comp in enumerate(self.core.modules):
menuItem = self.submenu.addAction(comp.Component.name)
menuItem.triggered.connect(
lambda _, item=i: self.core.insertComponent(
rowPos, item, self
0 if insertCompAtTop else index, item, self
)
)
)
self.menu.move(parentPosition + QPos)
self.menu.show()
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.WindowActivate \
or event.type() == QtCore.QEvent.FocusIn:
Core.windowHasFocus = True
elif event.type()== QtCore.QEvent.WindowDeactivate \
or event.type() == QtCore.QEvent.FocusOut:
Core.windowHasFocus = False
return False

View File

@ -22,6 +22,9 @@
<height>0</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>

View File

@ -6,7 +6,8 @@ from PyQt5 import QtCore, QtWidgets
import string
import os
import toolkit
from toolkit import badName
from core import Core
class PresetManager(QtWidgets.QDialog):
@ -151,7 +152,7 @@ class PresetManager(QtWidgets.QDialog):
currentPreset
)
if OK:
if toolkit.badName(newName):
if badName(newName):
self.warnMessage(self.parent.window)
continue
if newName:
@ -236,7 +237,6 @@ class PresetManager(QtWidgets.QDialog):
os.remove(filepath)
def warnMessage(self, window=None):
print(window)
self.parent.showMessage(
msg='Preset names must contain only letters, '
'numbers, and spaces.',
@ -272,7 +272,7 @@ class PresetManager(QtWidgets.QDialog):
self.presetRows[index][2]
)
if OK:
if toolkit.badName(newName):
if badName(newName):
self.warnMessage()
continue
if newName:
@ -289,7 +289,7 @@ class PresetManager(QtWidgets.QDialog):
self.findPresets()
self.drawPresetList()
for i, comp in enumerate(self.core.selectedComponents):
if toolkit.getPresetDir(comp) == path \
if getPresetDir(comp) == path \
and comp.currentPreset == oldName:
self.core.openPreset(newPath, i, newName)
self.parent.updateComponentTitle(i, False)
@ -338,3 +338,8 @@ class PresetManager(QtWidgets.QDialog):
def clearPresetListSelection(self):
self.window.listWidget_presets.setCurrentRow(-1)
def getPresetDir(comp):
'''Get the preset subdir for a particular version of a component'''
return os.path.join(Core.presetDir, str(comp), str(comp.version))

View File

@ -10,12 +10,13 @@ from queue import Queue, Empty
import os
from toolkit.frame import Checkerboard
from toolkit import disableWhenOpeningProject
class Worker(QtCore.QObject):
imageCreated = pyqtSignal(QtGui.QImage)
error = pyqtSignal()
error = pyqtSignal(str)
def __init__(self, parent=None, queue=None):
QtCore.QObject.__init__(self)
@ -30,6 +31,7 @@ class Worker(QtCore.QObject):
height = int(self.settings.value('outputHeight'))
self.background = Checkerboard(width, height)
@disableWhenOpeningProject
@pyqtSlot(list)
def createPreviewImage(self, components):
dic = {
@ -48,7 +50,6 @@ class Worker(QtCore.QObject):
self.queue.get(block=False)
except Empty:
continue
if self.background.width != width \
or self.background.height != height:
self.background = Checkerboard(width, height)
@ -65,20 +66,12 @@ class Worker(QtCore.QObject):
except ValueError as e:
errMsg = "Bad frame returned by %s's preview renderer. " \
"%s. New frame size was %s*%s; should be %s*%s. " \
"This is a fatal error." % (
"%s. New frame size was %s*%s; should be %s*%s." % (
str(component), str(e).capitalize(),
newFrame.width, newFrame.height,
width, height
)
print(errMsg)
self.parent.showMessage(
msg=errMsg,
detail=str(e),
icon='Warning',
parent=None # MainWindow is in a different thread
)
self.error.emit()
self.error.emit(errMsg)
break
except RuntimeError as e:
print(e)

View File

@ -8,13 +8,6 @@ import sys
import subprocess
from collections import OrderedDict
from toolkit.core import *
def getPresetDir(comp):
'''Get the preset subdirectory for a particular version of a component'''
return os.path.join(Core.presetDir, str(comp), str(comp.version))
def badName(name):
'''Returns whether a name contains non-alphanumeric chars'''
@ -66,14 +59,20 @@ def openPipe(commandList, **kwargs):
def disableWhenEncoding(func):
''' Blocks calls to a function while the video is being exported
in MainWindow.
'''
def decorator(*args, **kwargs):
if args[0].encoding:
def decorator(self, *args, **kwargs):
if self.encoding:
return
else:
return func(*args, **kwargs)
return func(self, *args, **kwargs)
return decorator
def disableWhenOpeningProject(func):
def decorator(self, *args, **kwargs):
if self.core.openingProject:
return
else:
return func(self, *args, **kwargs)
return decorator
@ -108,34 +107,3 @@ def rgbFromString(string):
return tup
except:
return (255, 255, 255)
def loadDefaultSettings(self):
'''
Runs once at each program start-up. Fills in default settings
for any settings not found in settings.ini
'''
self.resolutions = [
'1920x1080',
'1280x720',
'854x480'
]
default = {
"outputWidth": 1280,
"outputHeight": 720,
"outputFrameRate": 30,
"outputAudioCodec": "AAC",
"outputAudioBitrate": "192",
"outputVideoCodec": "H264",
"outputVideoBitrate": "2500",
"outputVideoFormat": "yuv420p",
"outputPreset": "medium",
"outputFormat": "mp4",
"outputContainer": "MP4",
"projectDir": os.path.join(self.dataDir, 'projects'),
}
for parm, value in default.items():
if self.settings.value(parm) is None:
self.settings.setValue(parm, value)

View File

@ -1,18 +0,0 @@
class Core:
'''A very complicated class for tracking settings'''
def init(settings):
global Core
for classvar, val in settings.items():
setattr(Core, classvar, val)
def cancel():
global Core
Core.canceled = True
def reset():
global Core
Core.canceled = False

View File

@ -4,18 +4,19 @@
import numpy
import sys
import os
import subprocess as sp
import subprocess
from toolkit.common import Core, checkOutput, openPipe
import core
from toolkit.common import checkOutput, openPipe
def findFfmpeg():
if getattr(sys, 'frozen', False):
# The application is frozen
if sys.platform == "win32":
return os.path.join(Core.wd, 'ffmpeg.exe')
return os.path.join(core.Core.wd, 'ffmpeg.exe')
else:
return os.path.join(Core.wd, 'ffmpeg')
return os.path.join(core.Core.wd, 'ffmpeg')
else:
if sys.platform == "win32":
@ -27,7 +28,7 @@ def findFfmpeg():
['ffmpeg', '-version'], stderr=f
)
return "ffmpeg"
except sp.CalledProcessError:
except subprocess.CalledProcessError:
return "avconv"
@ -37,9 +38,9 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
'''
if duration == -1:
duration = getAudioDuration(inputFile)
safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters
duration = "{0:.3f}".format(duration + 0.1) # used by input sources
Core = core.Core
# Test if user has libfdk_aac
encoders = checkOutput(
@ -213,12 +214,28 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
return ffmpegCommand
def testAudioStream(filename):
'''Test if an audio stream definitely exists'''
audioTestCommand = [
core.Core.FFMPEG_BIN,
'-i', filename,
'-vn', '-f', 'null', '-'
]
try:
checkOutput(audioTestCommand, stderr=subprocess.DEVNULL)
except subprocess.CalledProcessError:
return True
else:
return False
def getAudioDuration(filename):
command = [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=sp.STDOUT)
except sp.CalledProcessError as ex:
fileInfo = checkOutput(command, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as ex:
fileInfo = ex.output
info = fileInfo.decode("utf-8").split('\n')
@ -236,13 +253,17 @@ def getAudioDuration(filename):
def readAudioFile(filename, parent):
'''
Creates the completeAudioArray given to components
and used to draw the classic visualizer.
'''
duration = getAudioDuration(filename)
if not duration:
print('Audio file doesn\'t exist or unreadable.')
return
command = [
Core.FFMPEG_BIN,
core.Core.FFMPEG_BIN,
'-i', filename,
'-f', 's16le',
'-acodec', 'pcm_s16le',
@ -250,7 +271,8 @@ def readAudioFile(filename, parent):
'-ac', '1', # mono (set to '2' for stereo)
'-']
in_pipe = openPipe(
command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8
command,
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8
)
completeAudioArray = numpy.empty(0, dtype="int16")
@ -258,7 +280,7 @@ def readAudioFile(filename, parent):
progress = 0
lastPercent = None
while True:
if Core.canceled:
if core.Core.canceled:
return
# read 2 seconds of audio
progress += 4

View File

@ -7,7 +7,7 @@ from PIL.ImageQt import ImageQt
import sys
import os
from toolkit.common import Core
import core
class FramePainter(QtGui.QPainter):
@ -57,7 +57,7 @@ def Checkerboard(width, height):
'''
image = FloodFrame(1920, 1080, (0, 0, 0, 0))
image.paste(Image.open(
os.path.join(Core.wd, "background.png")),
os.path.join(core.Core.wd, "background.png")),
(0, 0)
)
image = image.resize((width, height))

View File

@ -18,6 +18,7 @@ from threading import Thread, Event
import time
import signal
import core
from toolkit import openPipe
from toolkit.ffmpeg import readAudioFile, createFfmpegCommand
from toolkit.frame import Checkerboard
@ -104,7 +105,8 @@ class Worker(QtCore.QObject):
while not self.stopped:
audioI, frame = self.previewQueue.get()
if time.time() - self.lastPreview >= 0.06 or audioI == 0:
if core.Core.windowHasFocus \
and time.time() - self.lastPreview >= 0.06 or audioI == 0:
image = Image.alpha_composite(background.copy(), frame)
self.imageCreated.emit(QtGui.QImage(ImageQt(image)))
self.lastPreview = time.time()
@ -231,7 +233,8 @@ class Worker(QtCore.QObject):
self.lastPreview = 0.0
self.previewDispatch = Thread(
target=self.previewDispatch, name="Render Dispatch Thread")
target=self.previewDispatch, name="Render Dispatch Thread"
)
self.previewDispatch.daemon = True
self.previewDispatch.start()