From e92e9d79f95ad67e83074ef318278c3486601eac Mon Sep 17 00:00:00 2001
From: DH4
Date: Fri, 23 Jun 2017 17:38:05 -0500
Subject: QT5 Conversion + Directory Structure
---
src/command.py | 126 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 126 insertions(+)
create mode 100644 src/command.py
(limited to 'src/command.py')
diff --git a/src/command.py b/src/command.py
new file mode 100644
index 0000000..1a1e810
--- /dev/null
+++ b/src/command.py
@@ -0,0 +1,126 @@
+from PyQt4 import QtCore
+from PyQt4.QtCore import QSettings
+import argparse
+import os
+import sys
+
+import core
+import video_thread
+from main import LoadDefaultSettings
+
+
+class Command(QtCore.QObject):
+
+ videoTask = QtCore.pyqtSignal(str, str, list)
+
+ def __init__(self):
+ QtCore.QObject.__init__(self)
+ self.core = core.Core()
+ self.dataDir = self.core.dataDir
+ self.canceled = False
+
+ self.parser = argparse.ArgumentParser(
+ description='Create a visualization for an audio file',
+ epilog='EXAMPLE COMMAND: main.py myvideotemplate.avp '
+ '-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')
+ self.parser.add_argument(
+ '-i', '--input', metavar='SOUND',
+ help='input audio file')
+ self.parser.add_argument(
+ '-o', '--output', metavar='OUTPUT',
+ help='output video file')
+
+ # optional arguments
+ self.parser.add_argument(
+ 'projpath', metavar='path-to-project',
+ help='open a project file (.avp)', nargs='?')
+ self.parser.add_argument(
+ '-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')
+
+ self.args = self.parser.parse_args()
+ self.settings = QSettings(
+ os.path.join(self.dataDir, 'settings.ini'), QSettings.IniFormat)
+ LoadDefaultSettings(self)
+
+ if self.args.projpath:
+ self.core.openProject(self, self.args.projpath)
+ self.core.selectedComponents = list(
+ reversed(self.core.selectedComponents))
+ self.core.componentListChanged()
+
+ if self.args.comp:
+ for comp in self.args.comp:
+ pos = comp[0]
+ name = comp[1]
+ args = comp[2:]
+ try:
+ pos = int(pos)
+ except ValueError:
+ print(pos, 'is not a layer number.')
+ quit(1)
+ realName = self.parseCompName(name)
+ if not realName:
+ print(name, 'is not a valid component name.')
+ quit(1)
+ modI = self.core.moduleIndexFor(realName)
+ i = self.core.insertComponent(pos, modI, self)
+ for arg in args:
+ self.core.selectedComponents[i].command(arg)
+
+ if self.args.input and self.args.output:
+ self.createAudioVisualisation()
+ elif 'help' not in sys.argv:
+ self.parser.print_help()
+ quit(1)
+
+ def createAudioVisualisation(self):
+ self.videoThread = QtCore.QThread(self)
+ self.videoWorker = video_thread.Worker(self)
+ self.videoWorker.moveToThread(self.videoThread)
+ self.videoWorker.videoCreated.connect(self.videoCreated)
+
+ self.videoThread.start()
+ self.videoTask.emit(
+ self.args.input,
+ self.args.output,
+ list(reversed(self.core.selectedComponents))
+ )
+
+ def videoCreated(self):
+ self.videoThread.quit()
+ self.videoThread.wait()
+ quit(0)
+
+ def showMessage(self, **kwargs):
+ print(kwargs['msg'])
+ if 'detail' in kwargs:
+ print(kwargs['detail'])
+
+ def drawPreview(self, *args):
+ pass
+
+ def parseCompName(self, name):
+ '''Deduces a proper component name out of a commandline arg'''
+
+ if name.title() in self.core.compNames:
+ return name.title()
+ for compName in self.core.compNames:
+ if name.capitalize() in compName:
+ return compName
+
+ compFileNames = [ \
+ os.path.splitext(os.path.basename(
+ mod.__file__))[0] \
+ for mod in self.core.modules \
+ ]
+ for i, compFileName in enumerate(compFileNames):
+ if name.lower() in compFileName:
+ return self.core.compNames[i]
+ return
+
+ return None
--
cgit v1.2.3
From 680214f5180a12f2250d8e266df9375ce99b9f80 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Fri, 23 Jun 2017 23:00:24 -0400
Subject: qt5 fixes
also pep8 compliance
---
src/command.py | 15 +++----
src/components/__base__.py | 14 +++----
src/components/color.py | 15 ++++---
src/components/image.py | 2 +-
src/components/video.py | 14 ++++---
src/core.py | 28 +++++++------
src/main.py | 6 +--
src/mainwindow.py | 100 ++++++++++++++++++++++++++-------------------
src/presetmanager.py | 55 +++++++++++++------------
src/video_thread.py | 11 ++---
10 files changed, 143 insertions(+), 117 deletions(-)
(limited to 'src/command.py')
diff --git a/src/command.py b/src/command.py
index 1a1e810..2f71f31 100644
--- a/src/command.py
+++ b/src/command.py
@@ -22,9 +22,9 @@ class Command(QtCore.QObject):
self.parser = argparse.ArgumentParser(
description='Create a visualization for an audio file',
epilog='EXAMPLE COMMAND: main.py myvideotemplate.avp '
- '-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')
+ '-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')
self.parser.add_argument(
'-i', '--input', metavar='SOUND',
help='input audio file')
@@ -113,10 +113,11 @@ class Command(QtCore.QObject):
if name.capitalize() in compName:
return compName
- compFileNames = [ \
- os.path.splitext(os.path.basename(
- mod.__file__))[0] \
- for mod in self.core.modules \
+ compFileNames = [
+ os.path.splitext(
+ os.path.basename(mod.__file__)
+ )[0]
+ for mod in self.core.modules
]
for i, compFileName in enumerate(compFileNames):
if name.lower() in compFileName:
diff --git a/src/components/__base__.py b/src/components/__base__.py
index a4677b1..a24af40 100644
--- a/src/components/__base__.py
+++ b/src/components/__base__.py
@@ -39,7 +39,7 @@ class Component(QtCore.QObject):
then update self.page widgets using the preset dict.
'''
self.currentPreset = presetName \
- if presetName != None else presetDict['preset']
+ if presetName is not None else presetDict['preset']
def preFrameRender(self, **kwargs):
'''Triggered only before a video is exported (video_thread.py)
@@ -66,8 +66,8 @@ class Component(QtCore.QObject):
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)
else:
print(
@@ -88,8 +88,8 @@ class Component(QtCore.QObject):
and return this as an RGB string and QPushButton stylesheet.
In a subclass apply stylesheet to any color selection widgets
'''
- dialog = QtGui.QColorDialog()
- dialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True)
+ dialog = QtWidgets.QColorDialog()
+ dialog.setOption(QtWidgets.QColorDialog.ShowAlphaChannel, True)
color = dialog.getColor()
if color.isValid():
RGBstring = '%s,%s,%s' % (
@@ -142,10 +142,10 @@ class Component(QtCore.QObject):
return image
'''
+
class BadComponentInit(Exception):
def __init__(self, arg, name):
- string = \
-'''################################
+ string = '''################################
Mandatory argument "%s" not specified
in %s instance initialization
###################################'''
diff --git a/src/components/color.py b/src/components/color.py
index 8f9a1d1..2e3902a 100644
--- a/src/components/color.py
+++ b/src/components/color.py
@@ -53,7 +53,7 @@ class Component(__base__.Component):
page.spinBox_height.valueChanged.connect(self.update)
page.checkBox_trans.stateChanged.connect(self.update)
- self.fillLabels = [ \
+ self.fillLabels = [
'Solid',
'Linear Gradient',
'Radial Gradient',
@@ -126,8 +126,8 @@ class Component(__base__.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 Image.new("RGBA", (width, height), (r, g, b, 255))
frame = self.blankFrame(width, height)
@@ -143,9 +143,11 @@ class Component(__base__.Component):
image = ImageQt(frame)
painter = QtGui.QPainter(image)
if self.stretch:
- w = width; h = height
+ w = width
+ h = height
else:
- w = self.sizeWidth; h = self.sizeWidth
+ w = self.sizeWidth
+ h = self.sizeWidth
if self.fillType == 1: # Linear Gradient
brush = QtGui.QLinearGradient(
@@ -170,7 +172,8 @@ class Component(__base__.Component):
else:
brush.setColorAt(1.0, QColor(*self.color2))
painter.setBrush(brush)
- painter.drawRect(self.x, self.y,
+ painter.drawRect(
+ self.x, self.y,
self.sizeWidth, self.sizeHeight)
painter.end()
imBytes = image.bits().asstring(image.numBytes())
diff --git a/src/components/image.py b/src/components/image.py
index 8ca88d3..3517af6 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -85,7 +85,7 @@ class Component(__base__.Component):
def pickImage(self):
imgDir = self.settings.value("backgroundDir", os.path.expanduser("~"))
- filename = QtGui.QFileDialog.getOpenFileName(
+ filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Image", imgDir,
"Image Files (%s)" % " ".join(self.imageFormats))
if filename:
diff --git a/src/components/video.py b/src/components/video.py
index 58ce7a3..0090426 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -41,8 +41,8 @@ class Video:
'-i', self.videoPath,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',
- '-filter:v', 'scale=%s:%s' %
- scale(self.scale, self.width, self.height, str),
+ '-filter:v', 'scale=%s:%s' % scale(
+ self.scale, self.width, self.height, str),
'-vcodec', 'rawvideo', '-',
]
@@ -180,7 +180,7 @@ class Component(__base__.Component):
def pickVideo(self):
imgDir = self.settings.value("backgroundDir", os.path.expanduser("~"))
- filename = QtGui.QFileDialog.getOpenFileName(
+ filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Video",
imgDir, "Video Files (%s)" % " ".join(self.videoFormats)
)
@@ -199,8 +199,8 @@ class Component(__base__.Component):
'-i', self.videoPath,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',
- '-filter:v', 'scale=%s:%s' %
- scale(self.scale, width, height, str),
+ '-filter:v', 'scale=%s:%s' % scale(
+ self.scale, width, height, str),
'-vcodec', 'rawvideo', '-',
'-ss', '90',
'-vframes', '1',
@@ -238,6 +238,7 @@ class Component(__base__.Component):
def commandHelp(self):
print('Load a video:\n path=/filepath/to/video.mp4')
+
def scale(scale, width, height, returntype=None):
width = (float(width) / 100.0) * float(scale)
height = (float(height) / 100.0) * float(scale)
@@ -248,6 +249,7 @@ def scale(scale, width, height, returntype=None):
else:
return (width, height)
+
def finalizeFrame(self, imageData, width, height):
if self.distort:
try:
@@ -265,7 +267,7 @@ def finalizeFrame(self, imageData, width, height):
imageData)
if self.scale != 100 \
- or self.xPosition != 0 or self.yPosition != 0:
+ or self.xPosition != 0 or self.yPosition != 0:
frame = self.blankFrame(width, height)
frame.paste(image, box=(self.xPosition, self.yPosition))
else:
diff --git a/src/core.py b/src/core.py
index bb5d351..670a3c5 100644
--- a/src/core.py
+++ b/src/core.py
@@ -179,7 +179,7 @@ class Core():
clearThis = False
# add loaded named presets to savedPresets dict
- if 'preset' in preset and preset['preset'] != None:
+ if 'preset' in preset and preset['preset'] is not None:
nam = preset['preset']
filepath2 = os.path.join(
self.presetDir, name, str(vers), nam)
@@ -195,12 +195,12 @@ class Core():
-1,
self.moduleIndexFor(name),
loader)
- if i == None:
+ if i is None:
loader.showMessage(msg="Too many components!")
break
try:
- if 'preset' in preset and preset['preset'] != None:
+ if 'preset' in preset and preset['preset'] is not None:
self.selectedComponents[i].loadPreset(
preset
)
@@ -210,8 +210,8 @@ class Core():
preset['preset']
)
except KeyError as e:
- print('%s missing value %s' %
- (self.selectedComponents[i], e))
+ print('%s missing value %s' % (
+ self.selectedComponents[i], e))
if clearThis:
self.clearPreset(i)
@@ -221,7 +221,6 @@ class Core():
errcode = 1
data = sys.exc_info()
-
if errcode == 1:
typ, value, _ = data
if typ.__name__ == KeyError:
@@ -274,11 +273,11 @@ class Core():
i += 1
elif i == 2:
lastCompPreset = Core.presetFromString(line)
- data[section].append(
- (lastCompName,
+ data[section].append((
+ lastCompName,
lastCompVers,
- lastCompPreset)
- )
+ lastCompPreset
+ ))
i = 0
return 0, data
except:
@@ -309,7 +308,9 @@ class Core():
return False, ''
def exportPreset(self, exportPath, compName, vers, origName):
- internalPath = os.path.join(self.presetDir, compName, str(vers), origName)
+ internalPath = os.path.join(
+ self.presetDir, compName, str(vers), origName
+ )
if not os.path.exists(internalPath):
return
if os.path.exists(exportPath):
@@ -328,7 +329,7 @@ class Core():
return False
def createPresetFile(
- self, compName, vers, presetName, saveValueStore, filepath=''):
+ 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:
@@ -463,7 +464,8 @@ class Core():
@staticmethod
def presetToString(dictionary):
'''Alphabetizes a dict into OrderedDict & returns string repr'''
- return repr(OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])))
+ return repr(
+ OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])))
@staticmethod
def presetFromString(string):
diff --git a/src/main.py b/src/main.py
index 4bf26db..58fdb46 100644
--- a/src/main.py
+++ b/src/main.py
@@ -30,7 +30,6 @@ def LoadDefaultSettings(self):
}
for parm, value in default.items():
- #print(parm, self.settings.value(parm))
if self.settings.value(parm) is None:
self.settings.setValue(parm, value)
@@ -51,7 +50,7 @@ if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
app.setApplicationName("audio-visualizer")
- app.setOrganizationName("audio-visualizer")
+ # app.setOrganizationName("audio-visualizer")
if mode == 'cmd':
from command import *
@@ -76,7 +75,8 @@ if __name__ == "__main__":
dpi = desc.physicalDpiX()
topMargin = 0 if (dpi == 96) else int(10 * (dpi / 96))
- window.resize(window.width() * (dpi / 96), window.height() * (dpi / 96))
+ window.resize(
+ window.width() * (dpi / 96), window.height() * (dpi / 96))
# window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0)
main = MainWindow(window, proj)
diff --git a/src/mainwindow.py b/src/mainwindow.py
index a52a0f4..7a9e397 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -116,7 +116,6 @@ class MainWindow(QtWidgets.QMainWindow):
codec = window.comboBox_videoCodec.itemText(i)
if codec == self.settings.value('outputVideoCodec'):
window.comboBox_videoCodec.setCurrentIndex(i)
- #print(codec)
for i in range(window.comboBox_audioCodec.count()):
codec = window.comboBox_audioCodec.itemText(i)
@@ -146,10 +145,11 @@ class MainWindow(QtWidgets.QMainWindow):
# Make component buttons
self.compMenu = QMenu()
+ self.compActions = []
for i, comp in enumerate(self.core.modules):
action = self.compMenu.addAction(comp.Component.__doc__)
action.triggered.connect(
- lambda item=i: self.core.insertComponent(0, item, self))
+ lambda _, item=i: self.core.insertComponent(0, item, self))
self.window.pushButton_addComponent.setMenu(self.compMenu)
@@ -160,9 +160,10 @@ class MainWindow(QtWidgets.QMainWindow):
self.window.pushButton_removeComponent.clicked.connect(
lambda _: self.removeComponent())
- componentList.setContextMenuPolicy(
- QtCore.Qt.CustomContextMenu)
- componentList.customContextMenuRequested.connect(self.componentContextMenu)
+ componentList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ componentList.customContextMenuRequested.connect(
+ self.componentContextMenu
+ )
currentRes = str(self.settings.value('outputWidth'))+'x' + \
str(self.settings.value('outputHeight'))
@@ -245,19 +246,30 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog)
QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject)
- QtWidgets.QShortcut("Ctrl+T", self.window, activated=lambda:
- self.window.pushButton_addComponent.click())
- QtWidgets.QShortcut("Ctrl+Space", self.window, activated=lambda:
- self.window.listWidget_componentList.setFocus())
- QtWidgets.QShortcut("Ctrl+Shift+S", self.window,
- self.presetManager.openSavePresetDialog)
- QtWidgets.QShortcut("Ctrl+Shift+C", self.window,
- self.presetManager.clearPreset)
-
- QtWidgets.QShortcut("Ctrl+Up", self.window,
- activated=lambda: self.moveComponent(-1))
- QtWidgets.QShortcut("Ctrl+Down", self.window,
- activated=lambda: self.moveComponent(1))
+ QtWidgets.QShortcut(
+ "Ctrl+T", self.window,
+ activated=lambda: self.window.pushButton_addComponent.click()
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+Space", self.window,
+ activated=lambda: self.window.listWidget_componentList.setFocus()
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+Shift+S", self.window,
+ self.presetManager.openSavePresetDialog
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+Shift+C", self.window, self.presetManager.clearPreset
+ )
+
+ QtWidgets.QShortcut(
+ "Ctrl+Up", self.window,
+ activated=lambda: self.moveComponent(-1)
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+Down", self.window,
+ activated=lambda: self.moveComponent(1)
+ )
QtWidgets.QShortcut("Ctrl+Home", self.window, self.moveComponentTop)
QtWidgets.QShortcut("Ctrl+End", self.window, self.moveComponentBottom)
QtWidgets.QShortcut("Ctrl+r", self.window, self.removeComponent)
@@ -280,7 +292,7 @@ class MainWindow(QtWidgets.QMainWindow):
def updateComponentTitle(self, pos, presetStore=False):
if type(presetStore) == dict:
name = presetStore['preset']
- if name == None or name not in self.core.savedPresets:
+ if name is None or name not in self.core.savedPresets:
modified = False
else:
modified = (presetStore != self.core.savedPresets[name])
@@ -362,21 +374,22 @@ class MainWindow(QtWidgets.QMainWindow):
def openInputFileDialog(self):
inputDir = self.settings.value("inputDir", os.path.expanduser("~"))
- fileName = QtGui.QFileDialog.getOpenFileName(
+ fileName, _ = QtWidgets.QFileDialog.getOpenFileName(
self.window, "Open Audio File",
inputDir, "Audio Files (%s)" % " ".join(self.core.audioFormats))
- if not fileName == "":
+ if fileName:
self.settings.setValue("inputDir", os.path.dirname(fileName))
self.window.lineEdit_audioFile.setText(fileName)
def openOutputFileDialog(self):
outputDir = self.settings.value("outputDir", os.path.expanduser("~"))
- fileName = QtGui.QFileDialog.getSaveFileName(
+ fileName, _ = QtWidgets.QFileDialog.getSaveFileName(
self.window, "Set Output Video File",
outputDir,
- "Video Files (%s);; All Files (*)" % " ".join(self.core.videoFormats))
+ "Video Files (%s);; All Files (*)" % " ".join(
+ self.core.videoFormats))
if not fileName == "":
self.settings.setValue("outputDir", os.path.dirname(fileName))
@@ -547,13 +560,13 @@ class MainWindow(QtWidgets.QMainWindow):
'''Drop event for the component listwidget'''
componentList = self.window.listWidget_componentList
- modelIndexes = [ \
- componentList.model().index(i) \
- for i in range(componentList.count()) \
+ modelIndexes = [
+ componentList.model().index(i)
+ for i in range(componentList.count())
]
- rects = [ \
- componentList.visualRect(modelIndex) \
- for modelIndex in modelIndexes \
+ rects = [
+ componentList.visualRect(modelIndex)
+ for modelIndex in modelIndexes
]
rowPos = [rect.contains(event.pos()) for rect in rects]
@@ -602,9 +615,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),
+ "Save before %s?" % (
+ os.path.basename(self.currentProject)[:-4],
+ phrase
+ ),
showCancel=True)
if ch:
success = self.saveProjectChanges()
@@ -613,7 +627,7 @@ class MainWindow(QtWidgets.QMainWindow):
os.remove(self.autosavePath)
def openSaveProjectDialog(self):
- filename = QtGui.QFileDialog.getSaveFileName(
+ filename, _ = QtWidgets.QFileDialog.getSaveFileName(
self.window, "Create Project File",
self.settings.value("projectDir"),
"Project Files (*.avp)")
@@ -628,7 +642,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.core.createProjectFile(filename)
def openOpenProjectDialog(self):
- filename = QtGui.QFileDialog.getOpenFileName(
+ filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.window, "Open Project File",
self.settings.value("projectDir"),
"Project Files (*.avp)")
@@ -657,17 +671,19 @@ class MainWindow(QtWidgets.QMainWindow):
def showMessage(self, **kwargs):
parent = kwargs['parent'] if 'parent' in kwargs else self.window
- msg = QtGui.QMessageBox(parent)
+ msg = QtWidgets.QMessageBox(parent)
msg.setModal(True)
msg.setText(kwargs['msg'])
msg.setIcon(
- kwargs['icon'] if 'icon' in kwargs else QtGui.QMessageBox.Information)
+ kwargs['icon']
+ if 'icon' in kwargs else QtWidgets.QMessageBox.Information
+ )
msg.setDetailedText(kwargs['detail'] if 'detail' in kwargs else None)
if 'showCancel'in kwargs and kwargs['showCancel']:
msg.setStandardButtons(
- QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel)
+ QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
else:
- msg.setStandardButtons(QtGui.QMessageBox.Ok)
+ msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
ch = msg.exec_()
if ch == 1024:
return True
@@ -687,7 +703,7 @@ class MainWindow(QtWidgets.QMainWindow):
return
self.presetManager.findPresets()
- self.menu = QtGui.QMenu()
+ self.menu = QMenu()
menuItem = self.menu.addAction("Save Preset")
menuItem.triggered.connect(
self.presetManager.openSavePresetDialog
@@ -695,8 +711,10 @@ class MainWindow(QtWidgets.QMainWindow):
# submenu for opening presets
try:
- presets = self.presetManager.presets[str(self.core.selectedComponents[index])]
- self.submenu = QtGui.QMenu("Open Preset")
+ presets = self.presetManager.presets[
+ str(self.core.selectedComponents[index])
+ ]
+ self.submenu = QMenu("Open Preset")
self.menu.addMenu(self.submenu)
for version, presetName in presets:
diff --git a/src/presetmanager.py b/src/presetmanager.py
index ec3f5cd..97f6e0e 100644
--- a/src/presetmanager.py
+++ b/src/presetmanager.py
@@ -1,4 +1,4 @@
-from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt5 import QtCore, QtWidgets
import string
import os
@@ -21,13 +21,15 @@ class PresetManager(QtWidgets.QDialog):
# window
self.lastFilter = '*'
- self.presetRows = [] # list of (comp, vers, name) tuples
+ self.presetRows = [] # list of (comp, vers, name) tuples
self.window = window
self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
# connect button signals
- self.window.pushButton_delete.clicked.connect(self.openDeletePresetDialog)
- self.window.pushButton_rename.clicked.connect(self.openRenamePresetDialog)
+ self.window.pushButton_delete.clicked.connect(
+ self.openDeletePresetDialog)
+ self.window.pushButton_rename.clicked.connect(
+ self.openRenamePresetDialog)
self.window.pushButton_import.clicked.connect(self.openImportDialog)
self.window.pushButton_export.clicked.connect(self.openExportDialog)
self.window.pushButton_close.clicked.connect(self.window.close)
@@ -36,7 +38,8 @@ class PresetManager(QtWidgets.QDialog):
self.drawFilterList()
self.window.comboBox_filter.currentIndexChanged.connect(
lambda: self.drawPresetList(
- self.window.comboBox_filter.currentText(), self.window.lineEdit_search.text()
+ self.window.comboBox_filter.currentText(),
+ self.window.lineEdit_search.text()
)
)
@@ -47,7 +50,8 @@ class PresetManager(QtWidgets.QDialog):
self.window.lineEdit_search.setCompleter(completer)
self.window.lineEdit_search.textChanged.connect(
lambda: self.drawPresetList(
- self.window.comboBox_filter.currentText(), self.window.lineEdit_search.text()
+ self.window.comboBox_filter.currentText(),
+ self.window.lineEdit_search.text()
)
)
self.drawPresetList('*')
@@ -72,16 +76,14 @@ class PresetManager(QtWidgets.QDialog):
parseList.append((compName, int(compVers), preset))
except ValueError:
continue
- self.presets =\
- {
- compName : \
- [
- (vers, preset) \
- for name, vers, preset in parseList \
- if name == compName \
- ] \
- for compName, _, __ in parseList \
- }
+ self.presets = {
+ compName: [
+ (vers, preset)
+ for name, vers, preset in parseList
+ if name == compName
+ ]
+ for compName, _, __ in parseList
+ }
def drawPresetList(self, compFilter=None, presetFilter=''):
self.window.listWidget_presets.clear()
@@ -96,7 +98,8 @@ class PresetManager(QtWidgets.QDialog):
continue
for vers, preset in presets:
if not presetFilter or presetFilter in preset:
- self.window.listWidget_presets.addItem('%s: %s' % (component, preset))
+ self.window.listWidget_presets.addItem(
+ '%s: %s' % (component, preset))
self.presetRows.append((component, vers, preset))
if preset not in presetNames:
presetNames.append(preset)
@@ -124,11 +127,11 @@ class PresetManager(QtWidgets.QDialog):
while True:
index = componentList.currentRow()
currentPreset = selectedComponents[index].currentPreset
- newName, OK = QtGui.QInputDialog.getText(
+ newName, OK = QtWidgets.QInputDialog.getText(
self.parent.window,
'Audio Visualizer',
'New Preset Name:',
- QtGui.QLineEdit.Normal,
+ QtWidgets.QLineEdit.Normal,
currentPreset
)
if OK:
@@ -149,7 +152,7 @@ class PresetManager(QtWidgets.QDialog):
break
def createNewPreset(
- self, compName, vers, filename, saveValueStore, **kwargs):
+ self, compName, vers, filename, saveValueStore, **kwargs):
path = os.path.join(self.presetDir, compName, str(vers), filename)
if self.presetExists(path, **kwargs):
return
@@ -163,7 +166,7 @@ class PresetManager(QtWidgets.QDialog):
msg="%s already exists! Overwrite it?" %
os.path.basename(path),
showCancel=True,
- icon=QtGui.QMessageBox.Warning,
+ icon=QtWidgets.QMessageBox.Warning,
parent=window)
if not ch:
# user clicked cancel
@@ -196,7 +199,7 @@ class PresetManager(QtWidgets.QDialog):
ch = self.parent.showMessage(
msg='Really delete %s?' % name,
showCancel=True,
- icon=QtGui.QMessageBox.Warning,
+ icon=QtWidgets.QMessageBox.Warning,
parent=self.window
)
if not ch:
@@ -223,11 +226,11 @@ class PresetManager(QtWidgets.QDialog):
while True:
index = presetList.currentRow()
- newName, OK = QtGui.QInputDialog.getText(
+ newName, OK = QtWidgets.QInputDialog.getText(
self.window,
'Preset Manager',
'Rename Preset:',
- QtGui.QLineEdit.Normal,
+ QtWidgets.QLineEdit.Normal,
self.presetRows[index][2]
)
if OK:
@@ -250,7 +253,7 @@ class PresetManager(QtWidgets.QDialog):
break
def openImportDialog(self):
- filename = QtGui.QFileDialog.getOpenFileName(
+ filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.window, "Import Preset File",
self.settings.value("presetDir"),
"Preset Files (*.avl)")
@@ -275,7 +278,7 @@ class PresetManager(QtWidgets.QDialog):
def openExportDialog(self):
if not self.window.listWidget_presets.selectedItems():
return
- filename = QtGui.QFileDialog.getSaveFileName(
+ filename, _ = QtWidgets.QFileDialog.getSaveFileName(
self.window, "Export Preset",
self.settings.value("presetDir"),
"Preset Files (*.avl)")
diff --git a/src/video_thread.py b/src/video_thread.py
index 5ea6d21..b45381c 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -121,15 +121,12 @@ class Worker(QtCore.QObject):
vencoders = options['video-codecs'][vcodec]
aencoders = options['audio-codecs'][acodec]
- #print(encoders)
for encoder in vencoders:
- #print(encoder)
if encoder in encoders:
vencoder = encoder
break
for encoder in aencoders:
- #print(encoder)
if encoder in encoders:
aencoder = encoder
break
@@ -167,10 +164,10 @@ class Worker(QtCore.QObject):
numpy.seterr(divide='ignore')
# Call preFrameRender on all components
- print('Loaded Components:', ", ".join(
- ["%s) %s" % (num, str(component)) \
- for num, component in enumerate(reversed(self.components))
- ]))
+ print('Loaded Components:', ", ".join([
+ "%s) %s" % (num, str(component))
+ for num, component in enumerate(reversed(self.components))
+ ]))
self.staticComponents = {}
numComps = len(self.components)
for compNo, comp in enumerate(self.components):
--
cgit v1.2.3
From 675a06dd4c10babb3ef2553f6c7cdd92b5f5ef0a Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 25 Jun 2017 14:27:56 -0400
Subject: project files save settings & out/in fields
---
src/command.py | 6 ++---
src/components/image.py | 4 +--
src/components/video.py | 8 +++---
src/core.py | 71 ++++++++++++++++++++++++++++++++++++++++++-------
src/mainwindow.py | 30 +++++++++++++++------
src/presetmanager.py | 4 +--
6 files changed, 94 insertions(+), 29 deletions(-)
(limited to 'src/command.py')
diff --git a/src/command.py b/src/command.py
index 2f71f31..b400773 100644
--- a/src/command.py
+++ b/src/command.py
@@ -1,5 +1,4 @@
-from PyQt4 import QtCore
-from PyQt4.QtCore import QSettings
+from PyQt5 import QtCore
import argparse
import os
import sys
@@ -43,8 +42,7 @@ class Command(QtCore.QObject):
nargs='*', action='append')
self.args = self.parser.parse_args()
- self.settings = QSettings(
- os.path.join(self.dataDir, 'settings.ini'), QSettings.IniFormat)
+ self.settings = self.core.settings
LoadDefaultSettings(self)
if self.args.projpath:
diff --git a/src/components/image.py b/src/components/image.py
index 143ae59..4bb33b1 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -83,12 +83,12 @@ class Component(__base__.Component):
}
def pickImage(self):
- imgDir = self.settings.value("backgroundDir", os.path.expanduser("~"))
+ imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Image", imgDir,
"Image Files (%s)" % " ".join(self.imageFormats))
if filename:
- self.settings.setValue("backgroundDir", os.path.dirname(filename))
+ self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_image.setText(filename)
self.update()
diff --git a/src/components/video.py b/src/components/video.py
index 44f88a5..d37dd99 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -88,8 +88,8 @@ class Video:
self.parent.showMessage(
msg='%s couldn\'t be loaded. '
'This is a fatal error.' % os.path.basename(
- self.videoPath
- ),
+ self.videoPath
+ ),
detail=str(e)
)
self.parent.stopVideo()
@@ -188,13 +188,13 @@ class Component(__base__.Component):
}
def pickVideo(self):
- imgDir = self.settings.value("backgroundDir", os.path.expanduser("~"))
+ imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Video",
imgDir, "Video Files (%s)" % " ".join(self.videoFormats)
)
if filename:
- self.settings.setValue("backgroundDir", os.path.dirname(filename))
+ self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_video.setText(filename)
self.update()
diff --git a/src/core.py b/src/core.py
index fdba1c4..d7e8219 100644
--- a/src/core.py
+++ b/src/core.py
@@ -30,6 +30,10 @@ class Core():
# unfrozen
self.wd = os.path.dirname(os.path.realpath(__file__))
self.componentsPath = os.path.join(self.wd, 'components')
+ self.settings = QtCore.QSettings(
+ os.path.join(self.dataDir, 'settings.ini'),
+ QtCore.QSettings.IniFormat
+ )
self.loadEncoderOptions()
self.videoFormats = Core.appendUppercase([
@@ -169,13 +173,23 @@ class Core():
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:
try:
- for i, tup in enumerate(data['Components']):
+ if hasattr(loader, 'window'):
+ for pair in data['WindowFields']:
+ widget, value = pair.split('=', 1)
+ widget = eval('loader.window.%s' % widget)
+ widget.setText(value.strip())
+
+ for pair in data['Settings']:
+ key, value = pair.split('=', 1)
+ self.settings.setValue(key, value.strip())
+
+ for tup in data['Components']:
name, vers, preset = tup
clearThis = False
modified = False
@@ -213,7 +227,7 @@ class Core():
preset['preset']
)
except KeyError as e:
- print('%s missing value %s' % (
+ print('%s missing value: %s' % (
self.selectedComponents[i], e)
)
@@ -221,23 +235,26 @@ class Core():
self.clearPreset(i)
if hasattr(loader, 'updateComponentTitle'):
loader.updateComponentTitle(i, modified)
+
except:
errcode = 1
data = sys.exc_info()
if errcode == 1:
- typ, value, _ = data
- if typ.__name__ == KeyError:
+ typ, value, tb = data
+ if typ.__name__ == 'KeyError':
# probably just an old version, still loadable
print('file missing value: %s' % value)
return
if hasattr(loader, 'createNewProject'):
loader.createNewProject()
- msg = '%s: %s' % (typ.__name__, value)
+ import traceback
+ msg = '%s: %s\n\nTraceback:\n' % (typ.__name__, value)
+ msg += "\n".join(traceback.format_tb(tb))
loader.showMessage(
msg="Project file '%s' is corrupted." % filepath,
showCancel=False,
- icon=QtGui.QMessageBox.Warning,
+ icon='Warning',
detail=msg)
def parseAvFile(self, filepath):
@@ -250,7 +267,11 @@ class Core():
with open(filepath, 'r') as f:
def parseLine(line):
'''Decides if a file line is a section header'''
- validSections = ('Components')
+ validSections = (
+ 'Components',
+ 'Settings',
+ 'WindowFields'
+ )
line = line.strip()
newSection = ''
@@ -283,6 +304,8 @@ class Core():
lastCompPreset
))
i = 0
+ elif line and section:
+ data[section].append(line)
return 0, data
except:
return 1, sys.exc_info()
@@ -354,8 +377,22 @@ class Core():
f.write('%s\n' % str(vers))
f.write(Core.presetToString(saveValueStore))
- def createProjectFile(self, filepath):
+ def createProjectFile(self, filepath, window=None):
'''Create a project file (.avp) using the current program state'''
+ forbiddenSettingsKeys = [
+ 'currentProject',
+ 'outputAudioBitrate',
+ 'outputAudioCodec',
+ 'outputContainer',
+ 'outputFormat',
+ 'outputFrameRate',
+ 'outputHeight',
+ 'outputPreset',
+ 'outputVideoBitrate',
+ 'outputVideoCodec',
+ 'outputVideoFormat',
+ 'outputWidth',
+ ]
try:
if not filepath.endswith(".avp"):
filepath += '.avp'
@@ -363,12 +400,28 @@ class Core():
os.remove(filepath)
with open(filepath, 'w') as f:
print('creating %s' % filepath)
+
f.write('[Components]\n')
for comp in self.selectedComponents:
saveValueStore = comp.savePreset()
f.write('%s\n' % str(comp))
f.write('%s\n' % str(comp.version()))
f.write('%s\n' % Core.presetToString(saveValueStore))
+
+ f.write('[Settings]\n')
+ for key in self.settings.allKeys():
+ if key not in forbiddenSettingsKeys:
+ f.write('%s=%s\n' % (key, self.settings.value(key)))
+
+ if window:
+ f.write('[WindowFields]\n')
+ f.write(
+ 'lineEdit_audioFile=%s\n'
+ 'lineEdit_outputFile=%s\n' % (
+ window.lineEdit_audioFile.text(),
+ window.lineEdit_outputFile.text()
+ )
+ )
return True
except:
return False
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 76c2b62..e4e4f38 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -1,6 +1,5 @@
from queue import Queue
from PyQt5 import QtCore, QtGui, uic, QtWidgets
-from PyQt5.QtCore import QSettings, Qt
from PyQt5.QtWidgets import QMenu, QShortcut
import sys
import os
@@ -27,7 +26,9 @@ class PreviewWindow(QtWidgets.QLabel):
painter = QtGui.QPainter(self)
point = QtCore.QPoint(0, 0)
scaledPix = self.pixmap.scaled(
- size, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)
+ size,
+ QtCore.Qt.KeepAspectRatio,
+ transformMode=QtCore.Qt.SmoothTransformation)
# start painting the label from left upper corner
point.setX((size.width() - scaledPix.width())/2)
@@ -59,8 +60,7 @@ class MainWindow(QtWidgets.QMainWindow):
# Create data directory, load/create settings
self.dataDir = self.core.dataDir
self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
- self.settings = QSettings(
- os.path.join(self.dataDir, 'settings.ini'), QSettings.IniFormat)
+ self.settings = self.core.settings
LoadDefaultSettings(self)
self.presetManager = PresetManager(
uic.loadUi(
@@ -94,6 +94,13 @@ class MainWindow(QtWidgets.QMainWindow):
window.toolButton_selectOutputFile.clicked.connect(
self.openOutputFileDialog)
+ def changedField():
+ self.autosave()
+ self.updateWindowTitle()
+
+ window.lineEdit_audioFile.textChanged.connect(changedField)
+ window.lineEdit_outputFile.textChanged.connect(changedField)
+
window.progressBar_createVideo.setValue(0)
window.pushButton_createVideo.clicked.connect(
@@ -359,7 +366,7 @@ class MainWindow(QtWidgets.QMainWindow):
if os.path.exists(self.autosavePath):
os.remove(self.autosavePath)
elif force or time.time() - self.lastAutosave >= 0.1:
- self.core.createProjectFile(self.autosavePath)
+ self.core.createProjectFile(self.autosavePath, self.window)
self.lastAutosave = time.time()
def autosaveExists(self, identical=True):
@@ -625,6 +632,13 @@ class MainWindow(QtWidgets.QMainWindow):
for widget in self.pages:
self.window.stackedWidget.removeWidget(widget)
self.pages = []
+ for field in (
+ self.window.lineEdit_audioFile,
+ self.window.lineEdit_outputFile
+ ):
+ field.blockSignals(True)
+ field.setText('')
+ field.blockSignals(False)
@disableWhenEncoding
def createNewProject(self):
@@ -637,7 +651,7 @@ class MainWindow(QtWidgets.QMainWindow):
def saveCurrentProject(self):
if self.currentProject:
- self.core.createProjectFile(self.currentProject)
+ self.core.createProjectFile(self.currentProject, self.window)
self.updateWindowTitle()
else:
self.openSaveProjectDialog()
@@ -670,7 +684,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.settings.setValue("projectDir", os.path.dirname(filename))
self.settings.setValue("currentProject", filename)
self.currentProject = filename
- self.core.createProjectFile(filename)
+ self.core.createProjectFile(filename, self.window)
self.updateWindowTitle()
@disableWhenEncoding
@@ -707,7 +721,7 @@ class MainWindow(QtWidgets.QMainWindow):
msg.setModal(True)
msg.setText(kwargs['msg'])
msg.setIcon(
- kwargs['icon']
+ eval('QtWidgets.QMessageBox.%s' % kwargs['icon'])
if 'icon' in kwargs else QtWidgets.QMessageBox.Information
)
msg.setDetailedText(kwargs['detail'] if 'detail' in kwargs else None)
diff --git a/src/presetmanager.py b/src/presetmanager.py
index 069bf62..3ab49ef 100644
--- a/src/presetmanager.py
+++ b/src/presetmanager.py
@@ -176,7 +176,7 @@ class PresetManager(QtWidgets.QDialog):
msg="%s already exists! Overwrite it?" %
os.path.basename(path),
showCancel=True,
- icon=QtWidgets.QMessageBox.Warning,
+ icon='Warning',
parent=window)
if not ch:
# user clicked cancel
@@ -209,7 +209,7 @@ class PresetManager(QtWidgets.QDialog):
ch = self.parent.showMessage(
msg='Really delete %s?' % name,
showCancel=True,
- icon=QtWidgets.QMessageBox.Warning,
+ icon='Warning',
parent=self.window
)
if not ch:
--
cgit v1.2.3
From 6a1a5cd6eb931f5f9316f89c680ca318f845a746 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 25 Jun 2017 15:31:42 -0400
Subject: --export commandline option
overrides -i and -o to use saved fields from a project file
---
src/command.py | 48 +++++++++++++++++++++++++++++++++++++++---------
src/components/video.py | 3 ++-
src/core.py | 2 ++
src/mainwindow.py | 7 +++++--
src/preview_thread.py | 3 ++-
5 files changed, 50 insertions(+), 13 deletions(-)
(limited to 'src/command.py')
diff --git a/src/command.py b/src/command.py
index b400773..09b54ac 100644
--- a/src/command.py
+++ b/src/command.py
@@ -23,13 +23,20 @@ class Command(QtCore.QObject):
epilog='EXAMPLE COMMAND: main.py myvideotemplate.avp '
'-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')
+ '-c 1 video "preset=My Logo" -c 2 vis layout=classic'
+ )
self.parser.add_argument(
'-i', '--input', metavar='SOUND',
- help='input audio file')
+ help='input audio file'
+ )
self.parser.add_argument(
'-o', '--output', metavar='OUTPUT',
- help='output video file')
+ help='output video file'
+ )
+ self.parser.add_argument(
+ '-e', '--export', action='store_true',
+ help='use input and output files from project file'
+ )
# optional arguments
self.parser.add_argument(
@@ -46,7 +53,15 @@ class Command(QtCore.QObject):
LoadDefaultSettings(self)
if self.args.projpath:
- self.core.openProject(self, self.args.projpath)
+ projPath = self.args.projpath
+ if not os.path.dirname(projPath):
+ 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.componentListChanged()
@@ -70,13 +85,28 @@ class Command(QtCore.QObject):
for arg in args:
self.core.selectedComponents[i].command(arg)
- if self.args.input and self.args.output:
- self.createAudioVisualisation()
+ if self.args.export and self.args.projpath:
+ errcode, data = self.core.parseAvFile(projPath)
+ for line in data['WindowFields']:
+ if 'outputFile' in line:
+ output = line.split('=', 1)[1]
+ if not os.path.dirname(output):
+ output = os.path.join(
+ os.path.expanduser('~'),
+ output
+ )
+ if 'audioFile' in line:
+ input = line.split('=', 1)[1]
+ self.createAudioVisualisation(input, output)
+
+ elif self.args.input and self.args.output:
+ self.createAudioVisualisation(self.args.input, self.args.output)
+
elif 'help' not in sys.argv:
self.parser.print_help()
quit(1)
- def createAudioVisualisation(self):
+ def createAudioVisualisation(self, input, output):
self.videoThread = QtCore.QThread(self)
self.videoWorker = video_thread.Worker(self)
self.videoWorker.moveToThread(self.videoThread)
@@ -84,8 +114,8 @@ class Command(QtCore.QObject):
self.videoThread.start()
self.videoTask.emit(
- self.args.input,
- self.args.output,
+ input,
+ output,
list(reversed(self.core.selectedComponents))
)
diff --git a/src/components/video.py b/src/components/video.py
index d37dd99..02bb44b 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -90,7 +90,8 @@ class Video:
'This is a fatal error.' % os.path.basename(
self.videoPath
),
- detail=str(e)
+ detail=str(e),
+ icon='Warning'
)
self.parent.stopVideo()
break
diff --git a/src/core.py b/src/core.py
index d7e8219..a435c2c 100644
--- a/src/core.py
+++ b/src/core.py
@@ -183,7 +183,9 @@ class Core():
for pair in data['WindowFields']:
widget, value = pair.split('=', 1)
widget = eval('loader.window.%s' % widget)
+ widget.blockSignals(True)
widget.setText(value.strip())
+ widget.blockSignals(False)
for pair in data['Settings']:
key, value = pair.split('=', 1)
diff --git a/src/mainwindow.py b/src/mainwindow.py
index e4e4f38..203992b 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -229,7 +229,9 @@ class MainWindow(QtWidgets.QMainWindow):
project += '.avp'
# open a project from the commandline
if not os.path.dirname(project):
- project = os.path.join(os.path.expanduser('~'), project)
+ project = os.path.join(
+ self.settings.value("projectDir"), project
+ )
self.currentProject = project
self.settings.setValue("currentProject", project)
if os.path.exists(self.autosavePath):
@@ -433,7 +435,8 @@ class MainWindow(QtWidgets.QMainWindow):
self.showMessage(
msg='Chosen filename matches a directory, which '
'cannot be overwritten. Please choose a different '
- 'filename or move the directory.'
+ 'filename or move the directory.',
+ icon='Warning',
)
return
else:
diff --git a/src/preview_thread.py b/src/preview_thread.py
index 769656b..e58f04e 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -58,7 +58,8 @@ class Worker(QtCore.QObject):
msg="Bad frame returned by %s's previewRender method. "
"This is a fatal error." %
str(component),
- detail=str(e)
+ detail=str(e),
+ icon='Warning'
)
quit(1)
--
cgit v1.2.3
From 2c82a65d1b79b898b2bc27fc5b1e0362fc160c46 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 25 Jun 2017 15:50:31 -0400
Subject: needs more tuples
---
src/command.py | 12 ++++++------
src/core.py | 27 +++++++++++++--------------
2 files changed, 19 insertions(+), 20 deletions(-)
(limited to 'src/command.py')
diff --git a/src/command.py b/src/command.py
index 09b54ac..3eea1b6 100644
--- a/src/command.py
+++ b/src/command.py
@@ -87,16 +87,16 @@ class Command(QtCore.QObject):
if self.args.export and self.args.projpath:
errcode, data = self.core.parseAvFile(projPath)
- for line in data['WindowFields']:
- if 'outputFile' in line:
- output = line.split('=', 1)[1]
- if not os.path.dirname(output):
+ for key, value in data['WindowFields']:
+ if 'outputFile' in key:
+ output = value
+ if not os.path.dirname(value):
output = os.path.join(
os.path.expanduser('~'),
output
)
- if 'audioFile' in line:
- input = line.split('=', 1)[1]
+ if 'audioFile' in key:
+ input = value
self.createAudioVisualisation(input, output)
elif self.args.input and self.args.output:
diff --git a/src/core.py b/src/core.py
index 341aa01..2994a24 100644
--- a/src/core.py
+++ b/src/core.py
@@ -180,16 +180,14 @@ class Core():
if errcode == 0:
try:
if hasattr(loader, 'window'):
- for pair in data['WindowFields']:
- widget, value = pair.split('=', 1)
+ for widget, value in data['WindowFields']:
widget = eval('loader.window.%s' % widget)
widget.blockSignals(True)
- widget.setText(value.strip())
+ widget.setText(value)
widget.blockSignals(False)
- for pair in data['Settings']:
- key, value = pair.split('=', 1)
- self.settings.setValue(key, value.strip())
+ for key, value in data['Settings']:
+ self.settings.setValue(key, value)
for tup in data['Components']:
name, vers, preset = tup
@@ -264,16 +262,16 @@ class Core():
Returns dictionary with section names as the keys, each one
contains a list of tuples: (compName, version, compPresetDict)
'''
- data = {}
+ validSections = (
+ 'Components',
+ 'Settings',
+ 'WindowFields'
+ )
+ data = {sect: [] for sect in validSections}
try:
with open(filepath, 'r') as f:
def parseLine(line):
'''Decides if a file line is a section header'''
- validSections = (
- 'Components',
- 'Settings',
- 'WindowFields'
- )
line = line.strip()
newSection = ''
@@ -289,7 +287,6 @@ class Core():
line, newSection = parseLine(line)
if newSection:
section = str(newSection)
- data[section] = []
continue
if line and section == 'Components':
if i == 0:
@@ -307,7 +304,9 @@ class Core():
))
i = 0
elif line and section:
- data[section].append(line)
+ key, value = line.split('=', 1)
+ data[section].append((key, value.strip()))
+
return 0, data
except:
return 1, sys.exc_info()
--
cgit v1.2.3
From ba0409829de62b745d6f87749572a416061a42b4 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Tue, 4 Jul 2017 19:52:52 -0400
Subject: moved functions into toolkit, fixed CMD appearing on Windows
---
src/command.py | 2 +-
src/components/video.py | 5 +--
src/core.py | 65 +++++++++++++-----------------------
src/core.pyc | Bin 0 -> 15050 bytes
src/main.py | 35 --------------------
src/mainwindow.py | 4 +--
src/presetmanager.py | 5 +--
src/toolkit.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++++
src/video_thread.py | 3 +-
9 files changed, 119 insertions(+), 85 deletions(-)
create mode 100644 src/core.pyc
create mode 100644 src/toolkit.py
(limited to 'src/command.py')
diff --git a/src/command.py b/src/command.py
index 3eea1b6..ee0e48d 100644
--- a/src/command.py
+++ b/src/command.py
@@ -5,7 +5,7 @@ import sys
import core
import video_thread
-from main import LoadDefaultSettings
+from toolkit import LoadDefaultSettings
class Command(QtCore.QObject):
diff --git a/src/components/video.py b/src/components/video.py
index 175cf29..19a9106 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -8,6 +8,7 @@ from queue import PriorityQueue
from component import Component, BadComponentInit
from frame import BlankFrame
+from toolkit import openPipe
class Video:
@@ -72,7 +73,7 @@ class Video:
self.frameBuffer.task_done()
def fillBuffer(self):
- pipe = subprocess.Popen(
+ pipe = openPipe(
self.command, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, bufsize=10**8
)
@@ -217,7 +218,7 @@ class Component(Component):
'-ss', '90',
'-vframes', '1',
]
- pipe = subprocess.Popen(
+ pipe = openPipe(
command, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, bufsize=10**8
)
diff --git a/src/core.py b/src/core.py
index 3fa67db..9ea9666 100644
--- a/src/core.py
+++ b/src/core.py
@@ -1,21 +1,24 @@
+'''
+ Home to the Core class which tracks the program state
+'''
import sys
import os
from PyQt5 import QtCore, QtGui, uic
-from os.path import expanduser
import subprocess as sp
import numpy
-from PIL import Image
-from shutil import rmtree
-import time
-from collections import OrderedDict
import json
from importlib import import_module
from PyQt5.QtCore import QStandardPaths
-import string
+import toolkit
-class Core():
+class Core:
+ '''
+ MainWindow and Command module both use an instance of this class
+ to store the program state. This object tracks the components,
+ opens projects and presets, and stores settings/paths to data.
+ '''
def __init__(self):
self.dataDir = QStandardPaths.writableLocation(
QStandardPaths.AppConfigLocation
@@ -34,7 +37,7 @@ class Core():
)
self.loadEncoderOptions()
- self.videoFormats = Core.appendUppercase([
+ self.videoFormats = toolkit.appendUppercase([
'*.mp4',
'*.mov',
'*.mkv',
@@ -42,7 +45,7 @@ class Core():
'*.webm',
'*.flv',
])
- self.audioFormats = Core.appendUppercase([
+ self.audioFormats = toolkit.appendUppercase([
'*.mp3',
'*.wav',
'*.ogg',
@@ -50,7 +53,7 @@ class Core():
'*.flac',
'*.aac',
])
- self.imageFormats = Core.appendUppercase([
+ self.imageFormats = toolkit.appendUppercase([
'*.png',
'*.jpg',
'*.tif',
@@ -175,7 +178,7 @@ class Core():
return False
with open(filepath, 'r') as f:
for line in f:
- saveValueStore = Core.presetFromString(line.strip())
+ saveValueStore = toolkit.presetFromString(line.strip())
break
return saveValueStore
@@ -307,7 +310,7 @@ class Core():
lastCompVers = str(line)
i += 1
elif i == 2:
- lastCompPreset = Core.presetFromString(line)
+ lastCompPreset = toolkit.presetFromString(line)
data[section].append((
lastCompName,
lastCompVers,
@@ -357,7 +360,7 @@ class Core():
with open(internalPath, 'r') as f:
internalData = [line for line in f]
try:
- saveValueStore = Core.presetFromString(internalData[0].strip())
+ saveValueStore = toolkit.presetFromString(internalData[0].strip())
self.createPresetFile(
compName, vers,
origName, saveValueStore,
@@ -387,7 +390,7 @@ class Core():
f.write('[Components]\n')
f.write('%s\n' % compName)
f.write('%s\n' % str(vers))
- f.write(Core.presetToString(saveValueStore))
+ f.write(toolkit.presetToString(saveValueStore))
def createProjectFile(self, filepath, window=None):
'''Create a project file (.avp) using the current program state'''
@@ -411,7 +414,7 @@ class Core():
saveValueStore = comp.savePreset()
f.write('%s\n' % str(comp))
f.write('%s\n' % str(comp.version()))
- f.write('%s\n' % Core.presetToString(saveValueStore))
+ f.write('%s\n' % toolkit.presetToString(saveValueStore))
f.write('\n[Settings]\n')
for key in self.settings.allKeys():
@@ -450,7 +453,9 @@ class Core():
else:
try:
with open(os.devnull, "w") as f:
- sp.check_call(['ffmpeg', '-version'], stdout=f, stderr=f)
+ sp.check_call(
+ ['ffmpeg', '-version'], stdout=f, stderr=f
+ )
return "ffmpeg"
except:
return "avconv"
@@ -459,10 +464,9 @@ class Core():
command = [self.FFMPEG_BIN, '-i', filename]
try:
- fileInfo = sp.check_output(command, stderr=sp.STDOUT, shell=False)
+ fileInfo = toolkit.checkOutput(command, stderr=sp.STDOUT)
except sp.CalledProcessError as ex:
fileInfo = ex.output
- pass
info = fileInfo.decode("utf-8").split('\n')
for line in info:
@@ -480,7 +484,7 @@ class Core():
'-ar', '44100', # ouput will have 44100 Hz
'-ac', '1', # mono (set to '2' for stereo)
'-']
- in_pipe = sp.Popen(
+ in_pipe = toolkit.openPipe(
command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8)
completeAudioArray = numpy.empty(0, dtype="int16")
@@ -525,26 +529,3 @@ class Core():
def reset(self):
self.canceled = False
-
- @staticmethod
- def badName(name):
- '''Returns whether a name contains non-alphanumeric chars'''
- return any([letter in string.punctuation for letter in name])
-
- @staticmethod
- def presetToString(dictionary):
- '''Alphabetizes a dict into OrderedDict & returns string repr'''
- return repr(
- OrderedDict(sorted(dictionary.items(), key=lambda t: t[0]))
- )
-
- @staticmethod
- def presetFromString(string):
- '''Turns a string repr of OrderedDict into a regular dict'''
- return dict(eval(string))
-
- @staticmethod
- def appendUppercase(lst):
- for form, i in zip(lst, range(len(lst))):
- lst.append(form.upper())
- return lst
diff --git a/src/core.pyc b/src/core.pyc
new file mode 100644
index 0000000..ce68831
Binary files /dev/null and b/src/core.pyc differ
diff --git a/src/main.py b/src/main.py
index bae9adf..b0ece29 100644
--- a/src/main.py
+++ b/src/main.py
@@ -7,41 +7,6 @@ import preview_thread
import video_thread
-def disableWhenEncoding(func):
- def decorator(*args, **kwargs):
- if args[0].encoding:
- return
- else:
- return func(*args, **kwargs)
- return decorator
-
-
-def LoadDefaultSettings(self):
- 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)
-
if __name__ == "__main__":
mode = 'gui'
if len(sys.argv) > 2:
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 5068108..e8a3221 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -1,6 +1,6 @@
-from queue import Queue
from PyQt5 import QtCore, QtGui, uic, QtWidgets
from PyQt5.QtWidgets import QMenu, QShortcut
+from queue import Queue
import sys
import os
import signal
@@ -11,7 +11,7 @@ import core
import preview_thread
import video_thread
from presetmanager import PresetManager
-from main import LoadDefaultSettings, disableWhenEncoding
+from toolkit import LoadDefaultSettings, disableWhenEncoding
class PreviewWindow(QtWidgets.QLabel):
diff --git a/src/presetmanager.py b/src/presetmanager.py
index 68679ec..805b93e 100644
--- a/src/presetmanager.py
+++ b/src/presetmanager.py
@@ -3,6 +3,7 @@ import string
import os
import core
+import toolkit
class PresetManager(QtWidgets.QDialog):
@@ -147,7 +148,7 @@ class PresetManager(QtWidgets.QDialog):
currentPreset
)
if OK:
- if core.Core.badName(newName):
+ if toolkit.badName(newName):
self.warnMessage(self.parent.window)
continue
if newName:
@@ -252,7 +253,7 @@ class PresetManager(QtWidgets.QDialog):
self.presetRows[index][2]
)
if OK:
- if core.Core.badName(newName):
+ if toolkit.badName(newName):
self.warnMessage()
continue
if newName:
diff --git a/src/toolkit.py b/src/toolkit.py
new file mode 100644
index 0000000..8dce645
--- /dev/null
+++ b/src/toolkit.py
@@ -0,0 +1,85 @@
+'''
+ Common functions
+'''
+import string
+import os
+import sys
+import subprocess
+from collections import OrderedDict
+
+
+def badName(name):
+ '''Returns whether a name contains non-alphanumeric chars'''
+ return any([letter in string.punctuation for letter in name])
+
+
+def presetToString(dictionary):
+ '''Alphabetizes a dict into OrderedDict & returns string repr'''
+ return repr(
+ OrderedDict(sorted(dictionary.items(), key=lambda t: t[0]))
+ )
+
+
+def presetFromString(string):
+ '''Turns a string repr of OrderedDict into a regular dict'''
+ return dict(eval(string))
+
+
+def appendUppercase(lst):
+ for form, i in zip(lst, range(len(lst))):
+ lst.append(form.upper())
+ return lst
+
+
+def checkOutput(commandList, **kwargs):
+ _subprocess(subprocess.check_output)
+
+
+def openPipe(commandList, **kwargs):
+ _subprocess(subprocess.Popen)
+
+
+def _subprocess(func, commandList, **kwargs):
+ if not sys.platform == 'win32':
+ # Stop CMD window from appearing on Windows
+ # http://code.activestate.com/recipes/409002/
+ startupinfo = subprocess.STARTUPINFO()
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+ kwargs['startupinfo'] = startupinfo
+ return func(commandList, shell=False, **kwargs)
+
+
+def disableWhenEncoding(func):
+ def decorator(*args, **kwargs):
+ if args[0].encoding:
+ return
+ else:
+ return func(*args, **kwargs)
+ return decorator
+
+
+def LoadDefaultSettings(self):
+ 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)
diff --git a/src/video_thread.py b/src/video_thread.py
index 9b0bf56..aed4d60 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -13,6 +13,7 @@ from copy import copy
import signal
import core
+from toolkit import openPipe
class Worker(QtCore.QObject):
@@ -191,7 +192,7 @@ class Worker(QtCore.QObject):
self.progressBarUpdate.emit(100)
# Create ffmpeg pipe and queues for frames
- self.out_pipe = sp.Popen(
+ self.out_pipe = openPipe(
ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout)
self.compositeQueue = Queue()
self.compositeQueue.maxsize = 20
--
cgit v1.2.3
From 9986b1c829caa12bcea120bb37ebb57ab5e0e874 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 6 Jul 2017 12:40:03 -0400
Subject: image options to mirror & saturate colours
and some friendly docstrings
---
freeze.py | 1 +
src/command.py | 5 ++++
src/components/image.py | 21 +++++++++++++++-
src/components/image.ui | 64 ++++++++++++++++++++++++++++++++++++++++++++++++-
src/core.py | 2 +-
src/mainwindow.py | 9 ++++++-
src/presetmanager.py | 4 ++++
src/preview_thread.py | 18 ++++++++++----
src/video_thread.py | 7 ++++++
9 files changed, 122 insertions(+), 9 deletions(-)
(limited to 'src/command.py')
diff --git a/freeze.py b/freeze.py
index a81f325..3266f45 100644
--- a/freeze.py
+++ b/freeze.py
@@ -33,6 +33,7 @@ buildOptions = dict(
"PIL.Image",
"PIL.ImageQt",
"PIL.ImageDraw",
+ "PIL.ImageEnhance",
],
include_files=deps,
)
diff --git a/src/command.py b/src/command.py
index ee0e48d..be194d8 100644
--- a/src/command.py
+++ b/src/command.py
@@ -1,3 +1,8 @@
+'''
+ 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
import argparse
import os
diff --git a/src/components/image.py b/src/components/image.py
index 4ccfc80..c9da137 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -1,4 +1,4 @@
-from PIL import Image, ImageDraw
+from PIL import Image, ImageDraw, ImageEnhance
from PyQt5 import QtGui, QtCore, QtWidgets
import os
@@ -20,7 +20,9 @@ class Component(Component):
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)
@@ -31,9 +33,11 @@ class Component(Component):
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()
self.parent.drawPreview()
super().update()
@@ -56,33 +60,48 @@ class Component(Component):
frame = BlankFrame(width, height)
if self.imagePath and os.path.exists(self.imagePath):
image = Image.open(self.imagePath)
+
+ # Modify image's appearance
+ if self.color != 100:
+ image = ImageEnhance.Color(image).enhance(
+ float(self.color / 100)
+ )
+ if self.mirror:
+ image = image.transpose(Image.FLIP_LEFT_RIGHT)
if self.stretched and image.size != (width, height):
image = image.resize((width, height), Image.ANTIALIAS)
if self.scale != 100:
newHeight = int((image.height / 100) * self.scale)
newWidth = int((image.width / 100) * self.scale)
image = image.resize((newWidth, newHeight), Image.ANTIALIAS)
+
+ # Paste image at correct position
frame.paste(image, box=(self.xPosition, self.yPosition))
if self.rotate != 0:
frame = frame.rotate(self.rotate)
+
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 {
'preset': self.currentPreset,
'image': self.imagePath,
'scale': self.scale,
+ 'color': self.color,
'rotate': self.rotate,
'stretched': self.stretched,
+ 'mirror': self.mirror,
'x': self.xPosition,
'y': self.yPosition,
}
diff --git a/src/components/image.ui b/src/components/image.ui
index 33488f8..e549ed0 100644
--- a/src/components/image.ui
+++ b/src/components/image.ui
@@ -208,10 +208,17 @@
+ -
+
+
+ Mirror
+
+
+
-
- Rotation
+ Rotate
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
@@ -290,6 +297,61 @@
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Color
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ QAbstractSpinBox::UpDownArrows
+
+
+ %
+
+
+ 0
+
+
+ 999
+
+
+ 1
+
+
+ 100
+
+
+
+
+
-
diff --git a/src/core.py b/src/core.py
index 9ea9666..5623039 100644
--- a/src/core.py
+++ b/src/core.py
@@ -1,5 +1,5 @@
'''
- Home to the Core class which tracks the program state
+ Home to the Core class which tracks program state. Used by GUI & commandline
'''
import sys
import os
diff --git a/src/mainwindow.py b/src/mainwindow.py
index e8a3221..1c6bbc4 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -1,3 +1,9 @@
+'''
+ 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, QtGui, uic, QtWidgets
from PyQt5.QtWidgets import QMenu, QShortcut
from queue import Queue
@@ -79,6 +85,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.previewWorker = preview_thread.Worker(self, self.previewQueue)
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)
@@ -296,11 +303,11 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QShortcut("Ctrl+End", self.window, self.moveComponentBottom)
QtWidgets.QShortcut("Ctrl+r", self.window, self.removeComponent)
+ @QtCore.pyqtSlot()
def cleanUp(self):
self.timer.stop()
self.previewThread.quit()
self.previewThread.wait()
- self.autosave()
def updateWindowTitle(self):
appName = 'Audio Visualizer'
diff --git a/src/presetmanager.py b/src/presetmanager.py
index 805b93e..40aa73f 100644
--- a/src/presetmanager.py
+++ b/src/presetmanager.py
@@ -1,3 +1,7 @@
+'''
+ Preset manager object handles all interactions with presets, including
+ the context menu accessed from MainWindow.
+'''
from PyQt5 import QtCore, QtWidgets
import string
import os
diff --git a/src/preview_thread.py b/src/preview_thread.py
index e58f04e..afb5e50 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -1,3 +1,7 @@
+'''
+ 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
from PIL import Image
@@ -11,6 +15,7 @@ from copy import copy
class Worker(QtCore.QObject):
imageCreated = pyqtSignal(['QImage'])
+ error = pyqtSignal()
def __init__(self, parent=None, queue=None):
QtCore.QObject.__init__(self)
@@ -59,12 +64,15 @@ class Worker(QtCore.QObject):
"This is a fatal error." %
str(component),
detail=str(e),
- icon='Warning'
+ icon='Warning',
+ parent=None # mainwindow is in a different thread
)
- quit(1)
-
- self._image = ImageQt(frame)
- self.imageCreated.emit(QtGui.QImage(self._image))
+ from frame import BlankFrame
+ self.imageCreated.emit(ImageQt(BlankFrame))
+ self.error.emit()
+ break
+ else:
+ self.imageCreated.emit(ImageQt(frame))
except Empty:
True
diff --git a/src/video_thread.py b/src/video_thread.py
index aed4d60..d35a37a 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -1,3 +1,10 @@
+'''
+ Thread created to export a video. It has a slot to begin export using
+ an input file, output path, and component list. During export multiple
+ threads are created to render the video as quickly as possible. Signals
+ are emitted to update MainWindow's progress bar, detail text, and preview.
+ Export can be cancelled with cancel() + reset()
+'''
from PyQt5 import QtCore, QtGui, uic
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PIL import Image, ImageDraw, ImageFont
--
cgit v1.2.3
From 62ab09e3f36dcaf6c1a4680dc6c4d048fb2e165c Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sat, 15 Jul 2017 01:00:03 -0400
Subject: Video comp verifies audio streams, videoThread moved into Core
off-by-1 bug fixed in exporting, & use fewer threads for fewer CPUs
---
src/command.py | 22 ++++++++--------------
src/components/video.py | 25 ++++++++++++++++++++-----
src/core.py | 16 ++++++++++++++++
src/mainwindow.py | 22 ++++++----------------
src/video_thread.py | 35 +++++++++++++++++++++++------------
5 files changed, 73 insertions(+), 47 deletions(-)
(limited to 'src/command.py')
diff --git a/src/command.py b/src/command.py
index be194d8..41618f8 100644
--- a/src/command.py
+++ b/src/command.py
@@ -9,13 +9,12 @@ import os
import sys
import core
-import video_thread
from toolkit import LoadDefaultSettings
class Command(QtCore.QObject):
- videoTask = QtCore.pyqtSignal(str, str, list)
+ createVideo = QtCore.pyqtSignal()
def __init__(self):
QtCore.QObject.__init__(self)
@@ -112,21 +111,16 @@ class Command(QtCore.QObject):
quit(1)
def createAudioVisualisation(self, input, output):
- self.videoThread = QtCore.QThread(self)
- self.videoWorker = video_thread.Worker(self)
- self.videoWorker.moveToThread(self.videoThread)
- self.videoWorker.videoCreated.connect(self.videoCreated)
-
- self.videoThread.start()
- self.videoTask.emit(
- input,
- output,
- 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.videoCreated.connect(self.videoCreated)
+ self.createVideo.emit()
def videoCreated(self):
- self.videoThread.quit()
- self.videoThread.wait()
quit(0)
def showMessage(self, **kwargs):
diff --git a/src/components/video.py b/src/components/video.py
index 8aa1420..b3b6a59 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -8,7 +8,7 @@ from queue import PriorityQueue
from component import Component, BadComponentInit
from frame import BlankFrame
-from toolkit import openPipe
+from toolkit import openPipe, checkOutput
class Video:
@@ -155,14 +155,29 @@ class Component(Component):
def properties(self):
props = []
- if self.useAudio:
- props.append('audio')
if not self.videoPath or self.badVideo \
or not os.path.exists(self.videoPath):
- props.append('error')
+ return ['error']
+
+ if self.useAudio:
+ props.append('audio')
+ # test if an audio stream really exists
+ audioTestCommand = [
+ self.core.FFMPEG_BIN,
+ '-i', self.videoPath,
+ '-vn', '-f', 'null', '-'
+ ]
+ try:
+ checkOutput(audioTestCommand, stderr=subprocess.DEVNULL)
+ except subprocess.CalledProcessError:
+ self.badAudio = True
+ return ['error']
+
return props
def error(self):
+ if hasattr(self, 'badAudio'):
+ return "Could not identify an audio stream in this video."
if not self.videoPath:
return "There is no video selected."
if not os.path.exists(self.videoPath):
@@ -180,7 +195,7 @@ class Component(Component):
self.blankFrame_ = BlankFrame(width, height)
self.updateChunksize(width, height)
self.video = Video(
- ffmpeg=self.parent.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,
diff --git a/src/core.py b/src/core.py
index 2500fa6..55bf261 100644
--- a/src/core.py
+++ b/src/core.py
@@ -12,6 +12,7 @@ from PyQt5.QtCore import QStandardPaths
import toolkit
from frame import Frame
+import video_thread
class Core:
@@ -633,6 +634,21 @@ class Core:
return completeAudioArray
+ def newVideoWorker(self, loader, audioFile, outputPath):
+ self.videoThread = QtCore.QThread(loader)
+ videoWorker = video_thread.Worker(
+ loader, audioFile, outputPath, self.selectedComponents
+ )
+ videoWorker.moveToThread(self.videoThread)
+ videoWorker.videoCreated.connect(self.videoCreated)
+
+ self.videoThread.start()
+ return videoWorker
+
+ def videoCreated(self):
+ self.videoThread.quit()
+ self.videoThread.wait()
+
def cancel(self):
self.canceled = True
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 771b6b8..76ed179 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -16,7 +16,6 @@ import time
import core
import preview_thread
-import video_thread
from presetmanager import PresetManager
from toolkit import LoadDefaultSettings, disableWhenEncoding, checkOutput
@@ -49,9 +48,9 @@ class PreviewWindow(QtWidgets.QLabel):
class MainWindow(QtWidgets.QMainWindow):
- newTask = QtCore.pyqtSignal(list)
+ createVideo = QtCore.pyqtSignal()
+ newTask = QtCore.pyqtSignal(list) # for the preview window
processTask = QtCore.pyqtSignal()
- videoTask = QtCore.pyqtSignal(str, str, list)
def __init__(self, window, project):
QtWidgets.QMainWindow.__init__(self)
@@ -497,20 +496,15 @@ class MainWindow(QtWidgets.QMainWindow):
self.canceled = False
self.progressBarUpdated(-1)
- self.videoThread = QtCore.QThread(self)
- self.videoWorker = video_thread.Worker(self)
- self.videoWorker.moveToThread(self.videoThread)
- self.videoWorker.videoCreated.connect(self.videoCreated)
+ self.videoWorker = self.core.newVideoWorker(
+ self, audioFile, outputPath
+ )
self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated)
self.videoWorker.progressBarSetText.connect(
self.progressBarSetText)
self.videoWorker.imageCreated.connect(self.showPreviewImage)
self.videoWorker.encoding.connect(self.changeEncodingStatus)
- self.videoThread.start()
- self.videoTask.emit(
- audioFile,
- outputPath,
- self.core.selectedComponents)
+ self.createVideo.emit()
def changeEncodingStatus(self, status):
self.encoding = status
@@ -569,10 +563,6 @@ class MainWindow(QtWidgets.QMainWindow):
else:
self.window.progressBar_createVideo.setFormat(value)
- def videoCreated(self):
- self.videoThread.quit()
- self.videoThread.wait()
-
def updateResolution(self):
resIndex = int(self.window.comboBox_resolution.currentIndex())
res = self.resolutions[resIndex].split('x')
diff --git a/src/video_thread.py b/src/video_thread.py
index b0562db..5295a3b 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -19,7 +19,7 @@ import time
import signal
import core
-from toolkit import openPipe, checkOutput
+from toolkit import openPipe
from frame import Checkerboard
@@ -31,13 +31,19 @@ class Worker(QtCore.QObject):
progressBarSetText = pyqtSignal(str)
encoding = pyqtSignal(bool)
- def __init__(self, parent=None):
+
+ def __init__(self, parent, inputFile, outputFile, components):
QtCore.QObject.__init__(self)
self.core = parent.core
self.settings = parent.core.settings
self.modules = parent.core.modules
+ parent.createVideo.connect(self.createVideo)
+
self.parent = parent
- parent.videoTask.connect(self.createVideo)
+ self.components = components
+ self.outputFile = outputFile
+ self.inputFile = inputFile
+
self.sampleSize = 1470 # 44100 / 30 = 1470
self.canceled = False
self.error = False
@@ -55,7 +61,7 @@ class Worker(QtCore.QObject):
bgI = int(audioI / self.sampleSize)
frame = None
for compNo, comp in reversed(list(enumerate(self.components))):
- layerNo = len(self.components) - compNo
+ layerNo = len(self.components) - compNo - 1
if layerNo in self.staticComponents:
if self.staticComponents[layerNo] is None:
# this layer was merged into a following layer
@@ -106,12 +112,10 @@ class Worker(QtCore.QObject):
self.previewQueue.task_done()
- @pyqtSlot(str, str, list)
- def createVideo(self, inputFile, outputFile, components):
+ @pyqtSlot()
+ def createVideo(self):
numpy.seterr(divide='ignore')
self.encoding.emit(True)
- self.components = components
- self.outputFile = outputFile
self.extraAudio = []
self.width = int(self.settings.value('outputWidth'))
self.height = int(self.settings.value('outputHeight'))
@@ -131,7 +135,7 @@ class Worker(QtCore.QObject):
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
self.progressBarSetText.emit("Loading audio file...")
- self.completeAudioArray = self.core.readAudioFile(inputFile, self)
+ self.completeAudioArray = self.core.readAudioFile(self.inputFile, self)
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit("Starting components...")
@@ -189,7 +193,9 @@ class Worker(QtCore.QObject):
)
self.staticComponents[compNo] = None
- ffmpegCommand = self.core.createFfmpegCommand(inputFile, outputFile)
+ ffmpegCommand = self.core.createFfmpegCommand(
+ self.inputFile, self.outputFile
+ )
print('###### FFMPEG COMMAND ######\n%s' % " ".join(ffmpegCommand))
print('############################')
self.out_pipe = openPipe(
@@ -200,9 +206,14 @@ class Worker(QtCore.QObject):
# START CREATING THE VIDEO
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
- # Make three renderNodes in new threads to create the frames
+ # Make 2 or 3 renderNodes in new threads to create the frames
self.renderThreads = []
- for i in range(3):
+ try:
+ numCpus = len(os.sched_getaffinity(0))
+ except:
+ numCpus = os.cpu_count()
+
+ for i in range(2 if numCpus <= 2 else 3):
self.renderThreads.append(
Thread(target=self.renderNode, name="Render Thread"))
self.renderThreads[i].daemon = True
--
cgit v1.2.3
From bcb8f27c2e4434d2296dcd66bf279b76ee0d0a4f Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sat, 15 Jul 2017 13:13:53 -0400
Subject: use -t on inputs so ffmpeg knows when to stop filters
+ better feedback in cmd mode
---
src/command.py | 20 ++++++++++++++++++++
src/components/sound.py | 5 +++++
src/components/video.py | 38 ++++++++++++++++++++++++++------------
src/core.py | 8 ++++++--
src/main.py | 11 ++++++-----
src/video_thread.py | 8 ++++----
6 files changed, 67 insertions(+), 23 deletions(-)
(limited to 'src/command.py')
diff --git a/src/command.py b/src/command.py
index 41618f8..84d798d 100644
--- a/src/command.py
+++ b/src/command.py
@@ -7,6 +7,7 @@ from PyQt5 import QtCore
import argparse
import os
import sys
+import time
import core
from toolkit import LoadDefaultSettings
@@ -118,8 +119,27 @@ class Command(QtCore.QObject):
self, input, output
)
self.worker.videoCreated.connect(self.videoCreated)
+ self.lastProgressUpdate = time.time()
+ self.worker.progressBarSetText.connect(self.progressBarSetText)
self.createVideo.emit()
+ @QtCore.pyqtSlot(str)
+ def progressBarSetText(self, value):
+ if 'Export ' in value:
+ # Don't duplicate completion/failure messages
+ return
+ 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)
+ else:
+ return
+ self.lastProgressUpdate = time.time()
+
+ @QtCore.pyqtSlot()
def videoCreated(self):
quit(0)
diff --git a/src/components/sound.py b/src/components/sound.py
index fedc32b..4a5714b 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -79,6 +79,11 @@ class Component(Component):
if not arg.startswith('preset=') and '=' 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)
return
+
super().command(arg)
diff --git a/src/components/video.py b/src/components/video.py
index b3b6a59..0b93293 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -116,6 +116,7 @@ class Component(Component):
page = self.loadUi('video.ui')
self.videoPath = ''
self.badVideo = False
+ self.badAudio = False
self.x = 0
self.y = 0
self.loopVideo = False
@@ -161,22 +162,14 @@ class Component(Component):
if self.useAudio:
props.append('audio')
- # test if an audio stream really exists
- audioTestCommand = [
- self.core.FFMPEG_BIN,
- '-i', self.videoPath,
- '-vn', '-f', 'null', '-'
- ]
- try:
- checkOutput(audioTestCommand, stderr=subprocess.DEVNULL)
- except subprocess.CalledProcessError:
- self.badAudio = True
+ self.testAudioStream()
+ if self.badAudio:
return ['error']
return props
def error(self):
- if hasattr(self, 'badAudio'):
+ if self.badAudio:
return "Could not identify an audio stream in this video."
if not self.videoPath:
return "There is no video selected."
@@ -185,6 +178,20 @@ class Component(Component):
if self.badVideo:
return "The video selected is corrupt!"
+ def testAudioStream(self):
+ # test if an audio stream really exists
+ audioTestCommand = [
+ self.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
+
def audio(self):
return (self.videoPath, {'map': '-v'})
@@ -277,7 +284,7 @@ class Component(Component):
if not arg.startswith('preset=') and '=' in arg:
key, arg = arg.split('=', 1)
if key == 'path' and os.path.exists(arg):
- if os.path.splitext(arg)[1] in self.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)
@@ -285,10 +292,17 @@ class Component(Component):
else:
print("Not a supported video format")
quit(1)
+ elif arg == 'audio':
+ if not self.page.lineEdit_video.text():
+ print("'audio' option must follow a video selection")
+ quit(1)
+ self.page.checkBox_useAudio.setChecked(True)
+ return
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')
def scale(scale, width, height, returntype=None):
diff --git a/src/core.py b/src/core.py
index 55bf261..4c12209 100644
--- a/src/core.py
+++ b/src/core.py
@@ -464,10 +464,11 @@ class Core:
except sp.CalledProcessError:
return "avconv"
- def createFfmpegCommand(self, inputFile, outputFile):
+ def createFfmpegCommand(self, inputFile, outputFile, duration):
'''
Constructs the major ffmpeg command used to export the video
'''
+ duration = str(duration)
# Test if user has libfdk_aac
encoders = toolkit.checkOutput(
@@ -516,10 +517,12 @@ class Core:
),
'-pix_fmt', 'rgba',
'-r', self.settings.value('outputFrameRate'),
+ '-t', duration,
'-i', '-', # the video input comes from a pipe
'-an', # the video input has no sound
# INPUT SOUND
+ '-t', duration,
'-i', inputFile
]
@@ -532,6 +535,7 @@ class Core:
for streamNo, params in enumerate(extraAudio):
extraInputFile, params = params
ffmpegCommand.extend([
+ '-t', duration,
'-i', extraInputFile
])
if 'map' in params and params['map'] == '-v':
@@ -632,7 +636,7 @@ class Core:
completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray
completeAudioArray = completeAudioArrayCopy
- return completeAudioArray
+ return (completeAudioArray, duration)
def newVideoWorker(self, loader, audioFile, outputPath):
self.videoThread = QtCore.QThread(loader)
diff --git a/src/main.py b/src/main.py
index b0ece29..2216d2a 100644
--- a/src/main.py
+++ b/src/main.py
@@ -8,13 +8,13 @@ import video_thread
if __name__ == "__main__":
- mode = 'gui'
+ mode = 'GUI'
if len(sys.argv) > 2:
- mode = 'cmd'
+ mode = 'commandline'
elif len(sys.argv) == 2:
if sys.argv[1].startswith('-'):
- mode = 'cmd'
+ mode = 'commandline'
else:
# opening a project file with gui
proj = sys.argv[1]
@@ -22,16 +22,17 @@ if __name__ == "__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")
- if mode == 'cmd':
+ if mode == 'commandline':
from command import *
main = Command()
- elif mode == 'gui':
+ elif mode == 'GUI':
from mainwindow import *
import atexit
import signal
diff --git a/src/video_thread.py b/src/video_thread.py
index 5295a3b..674765a 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -31,7 +31,6 @@ class Worker(QtCore.QObject):
progressBarSetText = pyqtSignal(str)
encoding = pyqtSignal(bool)
-
def __init__(self, parent, inputFile, outputFile, components):
QtCore.QObject.__init__(self)
self.core = parent.core
@@ -135,7 +134,9 @@ class Worker(QtCore.QObject):
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
self.progressBarSetText.emit("Loading audio file...")
- self.completeAudioArray = self.core.readAudioFile(self.inputFile, self)
+ self.completeAudioArray, duration = self.core.readAudioFile(
+ self.inputFile, self
+ )
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit("Starting components...")
@@ -144,7 +145,6 @@ class Worker(QtCore.QObject):
for num, component in enumerate(reversed(self.components))
]))
self.staticComponents = {}
- numComps = len(self.components)
for compNo, comp in enumerate(reversed(self.components)):
comp.preFrameRender(
worker=self,
@@ -194,7 +194,7 @@ class Worker(QtCore.QObject):
self.staticComponents[compNo] = None
ffmpegCommand = self.core.createFfmpegCommand(
- self.inputFile, self.outputFile
+ self.inputFile, self.outputFile, duration
)
print('###### FFMPEG COMMAND ######\n%s' % " ".join(ffmpegCommand))
print('############################')
--
cgit v1.2.3
From f454814867443ceeeca2a3a2c2a676947184503c Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 20 Jul 2017 20:31:38 -0400
Subject: ffmpeg functions moved to toolkit, component format simplified
component methods are auto-decorated & settings are now class variables
---
freeze.py | 7 +-
setup.py | 15 +-
src/command.py | 10 +-
src/component.py | 167 +++++++++++++-------
src/components/color.py | 8 +-
src/components/image.py | 11 +-
src/components/original.py | 11 +-
src/components/sound.py | 14 +-
src/components/text.py | 8 +-
src/components/video.py | 23 ++-
src/core.py | 379 ++++++++-------------------------------------
src/mainwindow.py | 81 ++++++----
src/presetmanager.py | 20 +--
src/preview_thread.py | 4 +-
src/toolkit/common.py | 12 +-
src/toolkit/core.py | 18 +++
src/toolkit/ffmpeg.py | 284 +++++++++++++++++++++++++++++++++
src/toolkit/frame.py | 6 +-
src/video_thread.py | 45 ++++--
19 files changed, 628 insertions(+), 495 deletions(-)
create mode 100644 src/toolkit/core.py
create mode 100644 src/toolkit/ffmpeg.py
(limited to 'src/command.py')
diff --git a/freeze.py b/freeze.py
index c9b7918..3281cad 100644
--- a/freeze.py
+++ b/freeze.py
@@ -2,8 +2,8 @@ from cx_Freeze import setup, Executable
import sys
import os
-# Dependencies are automatically detected, but it might need
-# fine tuning.
+from setup import VERSION
+
deps = [os.path.join('src', p) for p in os.listdir('src') if p]
deps.append('ffmpeg.exe' if sys.platform == 'win32' else 'ffmpeg')
@@ -39,7 +39,6 @@ buildOptions = dict(
include_files=deps,
)
-
base = 'Win32GUI' if sys.platform == 'win32' else None
executables = [
@@ -53,7 +52,7 @@ executables = [
setup(
name='audio-visualizer-python',
- version='2.0',
+ version=VERSION,
description='GUI tool to render visualization videos of audio files',
options=dict(build_exe=buildOptions),
executables=executables
diff --git a/setup.py b/setup.py
index 6ef688a..5abb976 100644
--- a/setup.py
+++ b/setup.py
@@ -2,6 +2,9 @@ from setuptools import setup
import os
+VERSION = '2.0.0.rc1'
+
+
def package_files(directory):
paths = []
for (path, directories, filenames) in os.walk(directory):
@@ -12,7 +15,7 @@ def package_files(directory):
setup(
name='audio_visualizer_python',
- version='2.0.0rc1',
+ version=VERSION,
url='https://github.com/djfun/audio-visualizer-python/tree/feature-newgui',
license='MIT',
description='Create audio visualization videos from a GUI or commandline',
@@ -20,8 +23,7 @@ setup(
"them as Projects to continue editing later. Different components can "
"be added and layered to add visualizers, images, videos, gradients, "
"text, etc. Use Projects created in the GUI with commandline mode to "
- "automate your video production workflow without learning any complex "
- "syntax.",
+ "automate your video production workflow without any complex syntax.",
classifiers=[
'Development Status :: 4 - Beta',
'License :: OSI Approved :: MIT License',
@@ -29,10 +31,13 @@ setup(
'Intended Audience :: End Users/Desktop',
'Topic :: Multimedia :: Video :: Non-Linear Editor',
],
- keywords=['visualizer', 'visualization', 'commandline video',
- 'video editor', 'ffmpeg', 'podcast'],
+ keywords=[
+ 'visualizer', 'visualization', 'commandline video',
+ 'video editor', 'ffmpeg', 'podcast'
+ ],
packages=[
'avpython',
+ 'avpython.toolkit',
'avpython.components'
],
package_dir={'avpython': 'src'},
diff --git a/src/command.py b/src/command.py
index 84d798d..046a1bf 100644
--- a/src/command.py
+++ b/src/command.py
@@ -9,8 +9,8 @@ import os
import sys
import time
-import core
-from toolkit import LoadDefaultSettings
+from core import Core
+from toolkit import loadDefaultSettings
class Command(QtCore.QObject):
@@ -19,7 +19,7 @@ class Command(QtCore.QObject):
def __init__(self):
QtCore.QObject.__init__(self)
- self.core = core.Core()
+ self.core = Core()
self.dataDir = self.core.dataDir
self.canceled = False
@@ -54,8 +54,8 @@ class Command(QtCore.QObject):
nargs='*', action='append')
self.args = self.parser.parse_args()
- self.settings = self.core.settings
- LoadDefaultSettings(self)
+ self.settings = Core.settings
+ loadDefaultSettings(self)
if self.args.projpath:
projPath = self.args.projpath
diff --git a/src/component.py b/src/component.py
index 7842bd6..92cc65c 100644
--- a/src/component.py
+++ b/src/component.py
@@ -1,33 +1,87 @@
'''
- Base classes for components to import.
+ Base classes for components to import. Read comments for some documentation
+ on making a valid component.
'''
from PyQt5 import uic, QtCore, QtWidgets
import os
+from core import Core
+from toolkit.common import getPresetDir
-class Component(QtCore.QObject):
+
+class ComponentMetaclass(type(QtCore.QObject)):
+ '''
+ Checks the validity of each Component class imported, and
+ mutates some attributes for easier use by the core program.
+ E.g., takes only major version from version string & decorates methods
+ '''
+ def __new__(cls, name, parents, attrs):
+ # print('Creating %s component' % attrs['name'])
+
+ # Turn certain class methods into properties and classmethods
+ for key in ('error', 'properties', 'audio', 'commandHelp'):
+ if key not in attrs:
+ continue
+ attrs[key] = property(attrs[key])
+
+ for key in ('names'):
+ if key not in attrs:
+ continue
+ attrs[key] = classmethod(key)
+
+ # Turn version string into a number
+ try:
+ if 'version' not in attrs:
+ print(
+ 'No version attribute in %s. Defaulting to 1' %
+ attrs['name'])
+ attrs['version'] = 1
+ else:
+ attrs['version'] = int(attrs['version'].split('.')[0])
+ except ValueError:
+ print('%s component has an invalid version string:\n%s' % (
+ attrs['name'], str(attrs['version'])))
+ except KeyError:
+ print('%s component has no version string.' % attrs['name'])
+ else:
+ return super().__new__(cls, name, parents, attrs)
+ quit(1)
+
+
+class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
- A class for components to inherit. Read comments for documentation
- on making a valid component. All subclasses must implement this signal:
- modified = QtCore.pyqtSignal(int, bool)
+ The base class for components to inherit.
'''
- def __init__(self, moduleIndex, compPos, core):
+ name = 'Component'
+ version = '1.0.0'
+ # The 1st number (before dot, aka the major version) 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):
super().__init__()
self.currentPreset = None
- self.canceled = False
self.moduleIndex = moduleIndex
self.compPos = compPos
- self.core = core
+
+ # Stop lengthy processes in response to this variable
+ self.canceled = False
def __str__(self):
- return self.__doc__
+ return self.__class__.name
- def version(self):
- '''
- Change this number to identify new versions of a component
- '''
- return 1
+ def __repr__(self):
+ return '%s\n%s\n%s' % (
+ self.__class__.name, str(self.__class__.version), self.savePreset()
+ )
+
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # Properties
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
def properties(self):
'''
@@ -43,19 +97,32 @@ class Component(QtCore.QObject):
'''
return
- def cancel(self):
+ def audio(self):
'''
- Stop any lengthy process in response to this variable
+ 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
'''
- self.canceled = True
- def reset(self):
- self.canceled = False
-
- def update(self):
+ def names():
'''
- Read your widget values from self.page, then call super().update()
+ Alternative names for renaming a component between project files.
'''
+ 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
@@ -92,7 +159,7 @@ class Component(QtCore.QObject):
'''
if arg.startswith('preset='):
_, preset = arg.split('=', 1)
- path = os.path.join(self.core.getPresetDir(self), preset)
+ path = os.path.join(getPresetDir(self), preset)
if not os.path.exists(path):
print('Couldn\'t locate preset "%s"' % preset)
quit(1)
@@ -106,14 +173,19 @@ class Component(QtCore.QObject):
self.__doc__, 'Usage:\n'
'Open a preset for this component:\n'
' "preset=Preset Name"')
- self.commandHelp()
+ print(self.commandHelp)
quit(0)
- def commandHelp(self):
- '''Print help text for this Component's commandline arguments'''
-
def loadUi(self, filename):
- return uic.loadUi(os.path.join(self.core.componentsPath, filename))
+ '''Load a Qt Designer ui file to use for this component's widget'''
+ return uic.loadUi(os.path.join(Core.componentsPath, filename))
+
+ def cancel(self):
+ '''Stop any lengthy process in response to this variable.'''
+ self.canceled = True
+
+ def reset(self):
+ self.canceled = False
'''
### Reference methods for creating a new component
@@ -121,47 +193,34 @@ class Component(QtCore.QObject):
def widget(self, parent):
self.parent = parent
- page = self.loadUi('example.ui')
+ self.settings = parent.settings
+ self.page = self.loadUi('example.ui')
# --- connect widget signals here ---
- self.page = page
- return page
+ return self.page
def previewRender(self, previewWorker):
- width = int(previewWorker.core.settings.value('outputWidth'))
+ width = int(self.settings.value('outputWidth'))
height = int(previewWorker.core.settings.value('outputHeight'))
- from frame import BlankFrame
+ from toolkit.frame import BlankFrame
image = BlankFrame(width, height)
return image
def frameRender(self, layerNo, frameNo):
audioArrayIndex = frameNo * self.sampleSize
- width = int(self.worker.core.settings.value('outputWidth'))
- height = int(self.worker.core.settings.value('outputHeight'))
- from frame import BlankFrame
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
+ from toolkit.frame import BlankFrame
image = BlankFrame(width, height)
return image
-
- 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
- \'''
-
- @classmethod
- def names(cls):
- \'''
- Alternative names for renaming a component between project files.
- \'''
- return []
'''
class BadComponentInit(Exception):
+ '''
+ General purpose exception 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.
+ '''
def __init__(self, arg, name):
string = '''################################
Mandatory argument "%s" not specified
diff --git a/src/components/color.py b/src/components/color.py
index 8d2526d..03371e7 100644
--- a/src/components/color.py
+++ b/src/components/color.py
@@ -10,13 +10,12 @@ from toolkit import rgbFromString, pickColor
class Component(Component):
- '''Color'''
-
- modified = QtCore.pyqtSignal(int, dict)
+ name = 'Color'
+ version = '1.0.0'
def widget(self, parent):
self.parent = parent
- self.settings = self.parent.core.settings
+ self.settings = parent.settings
page = self.loadUi('color.ui')
self.color1 = (0, 0, 0)
@@ -211,7 +210,6 @@ class Component(Component):
def savePreset(self):
return {
- 'preset': self.currentPreset,
'color1': self.color1,
'color2': self.color2,
'x': self.x,
diff --git a/src/components/image.py b/src/components/image.py
index 7f3f610..591e03e 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -2,18 +2,18 @@ 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
class Component(Component):
- '''Image'''
-
- modified = QtCore.pyqtSignal(int, dict)
+ name = 'Image'
+ version = '1.0.0'
def widget(self, parent):
self.parent = parent
- self.settings = self.parent.core.settings
+ self.settings = parent.settings
page = self.loadUi('image.ui')
page.lineEdit_image.textChanged.connect(self.update)
@@ -102,7 +102,6 @@ class Component(Component):
def savePreset(self):
return {
- 'preset': self.currentPreset,
'image': self.imagePath,
'scale': self.scale,
'color': self.color,
@@ -117,7 +116,7 @@ class Component(Component):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Image", imgDir,
- "Image Files (%s)" % " ".join(self.core.imageFormats))
+ "Image Files (%s)" % " ".join(Core.imageFormats))
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_image.setText(filename)
diff --git a/src/components/original.py b/src/components/original.py
index 586204a..ae40df3 100644
--- a/src/components/original.py
+++ b/src/components/original.py
@@ -12,17 +12,15 @@ from toolkit import rgbFromString, pickColor
class Component(Component):
- '''Classic Visualizer'''
+ name = 'Classic Visualizer'
+ version = '1.0.0'
- modified = QtCore.pyqtSignal(int, dict)
-
- @classmethod
- def names(cls):
+ def names():
return ['Original Audio Visualization']
def widget(self, parent):
self.parent = parent
- self.settings = self.parent.core.settings
+ self.settings = parent.settings
self.visColor = (255, 255, 255)
self.scale = 20
self.y = 0
@@ -68,7 +66,6 @@ class Component(Component):
def savePreset(self):
return {
- 'preset': self.currentPreset,
'layout': self.layout,
'visColor': self.visColor,
'scale': self.scale,
diff --git a/src/components/sound.py b/src/components/sound.py
index 5b06405..677a22f 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -1,14 +1,14 @@
from PyQt5 import QtGui, QtCore, QtWidgets
import os
+from core import Core
from component import Component
from toolkit.frame import BlankFrame
class Component(Component):
- '''Sound'''
-
- modified = QtCore.pyqtSignal(int, dict)
+ name = 'Sound'
+ version = '1.0.0'
def widget(self, parent):
self.parent = parent
@@ -32,8 +32,8 @@ class Component(Component):
super().update()
def previewRender(self, previewWorker):
- width = int(previewWorker.core.settings.value('outputWidth'))
- height = int(previewWorker.core.settings.value('outputHeight'))
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
return BlankFrame(width, height)
def preFrameRender(self, **kwargs):
@@ -67,7 +67,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(self.core.audioFormats))
+ "Audio Files (%s)" % " ".join(Core.audioFormats))
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_sound.setText(filename)
@@ -101,7 +101,7 @@ class Component(Component):
key, arg = arg.split('=', 1)
if key == 'path':
if '*%s' % os.path.splitext(arg)[1] \
- not in self.core.audioFormats:
+ not in Core.audioFormats:
print("Not a supported audio format")
quit(1)
self.page.lineEdit_sound.setText(arg)
diff --git a/src/components/text.py b/src/components/text.py
index fc3ef5f..d511f22 100644
--- a/src/components/text.py
+++ b/src/components/text.py
@@ -9,9 +9,8 @@ from toolkit import rgbFromString, pickColor
class Component(Component):
- '''Title Text'''
-
- modified = QtCore.pyqtSignal(int, dict)
+ name = 'Title Text'
+ version = '1.0.0'
def __init__(self, *args):
super().__init__(*args)
@@ -19,7 +18,7 @@ class Component(Component):
def widget(self, parent):
self.parent = parent
- self.settings = self.parent.core.settings
+ self.settings = parent.settings
height = int(self.settings.value('outputHeight'))
width = int(self.settings.value('outputWidth'))
@@ -106,7 +105,6 @@ class Component(Component):
def savePreset(self):
return {
- 'preset': self.currentPreset,
'title': self.title,
'titleFont': self.titleFont.toString(),
'alignment': self.alignment,
diff --git a/src/components/video.py b/src/components/video.py
index a9f334e..b35c2e5 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -6,6 +6,7 @@ import subprocess
import threading
from queue import PriorityQueue
+from core import Core
from component import Component, BadComponentInit
from toolkit.frame import BlankFrame
from toolkit import openPipe, checkOutput
@@ -106,9 +107,8 @@ class Video:
class Component(Component):
- '''Video'''
-
- modified = QtCore.pyqtSignal(int, dict)
+ name = 'Video'
+ version = '1.0.0'
def widget(self, parent):
self.parent = parent
@@ -154,8 +154,8 @@ class Component(Component):
super().update()
def previewRender(self, previewWorker):
- width = int(previewWorker.core.settings.value('outputWidth'))
- height = int(previewWorker.core.settings.value('outputHeight'))
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
self.updateChunksize(width, height)
frame = self.getPreviewFrame(width, height)
if not frame:
@@ -190,7 +190,7 @@ class Component(Component):
def testAudioStream(self):
# test if an audio stream really exists
audioTestCommand = [
- self.core.FFMPEG_BIN,
+ Core.FFMPEG_BIN,
'-i', self.videoPath,
'-vn', '-f', 'null', '-'
]
@@ -209,12 +209,12 @@ class Component(Component):
def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs)
- width = int(self.worker.core.settings.value('outputWidth'))
- height = int(self.worker.core.settings.value('outputHeight'))
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
self.blankFrame_ = BlankFrame(width, height)
self.updateChunksize(width, height)
self.video = Video(
- ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath,
+ ffmpeg=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,
@@ -240,7 +240,6 @@ class Component(Component):
def savePreset(self):
return {
- 'preset': self.currentPreset,
'video': self.videoPath,
'loop': self.loopVideo,
'useAudio': self.useAudio,
@@ -255,7 +254,7 @@ class Component(Component):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Video",
- imgDir, "Video Files (%s)" % " ".join(self.core.videoFormats)
+ imgDir, "Video Files (%s)" % " ".join(Core.videoFormats)
)
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
@@ -298,7 +297,7 @@ class Component(Component):
if not arg.startswith('preset=') and '=' 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 '*%s' % os.path.splitext(arg)[1] in Core.videoFormats:
self.page.lineEdit_video.setText(arg)
self.page.spinBox_scale.setValue(100)
self.page.checkBox_loop.setChecked(True)
diff --git a/src/core.py b/src/core.py
index 07c1f71..dd2ef18 100644
--- a/src/core.py
+++ b/src/core.py
@@ -1,46 +1,56 @@
'''
Home to the Core class which tracks program state. Used by GUI & commandline
'''
+from PyQt5 import QtCore, QtGui, uic
import sys
import os
-from PyQt5 import QtCore, QtGui, uic
-import subprocess as sp
-import numpy
import json
from importlib import import_module
-from PyQt5.QtCore import QStandardPaths
import toolkit
-from toolkit.frame import Frame
+from toolkit.ffmpeg import findFfmpeg
import video_thread
class Core:
'''
MainWindow and Command module both use an instance of this class
- to store the program state. This object tracks the components,
- opens projects and presets, and stores settings/paths to data.
+ to store the main program state. This object tracks the components
+ as an instance, has methods for managing the components and for
+ opening/creating project files and presets.
'''
- def __init__(self):
- Frame.core = self
- self.dataDir = QStandardPaths.writableLocation(
- QStandardPaths.AppConfigLocation
- )
- self.presetDir = os.path.join(self.dataDir, 'presets')
+
+ @classmethod
+ def storeSettings(cls):
+ '''
+ Stores settings/paths to directories as class variables
+ '''
if getattr(sys, 'frozen', False):
# frozen
- self.wd = os.path.dirname(sys.executable)
+ wd = os.path.dirname(sys.executable)
else:
- # unfrozen
- self.wd = os.path.dirname(os.path.realpath(__file__))
- self.componentsPath = os.path.join(self.wd, 'components')
- self.settings = QtCore.QSettings(
- os.path.join(self.dataDir, 'settings.ini'),
- QtCore.QSettings.IniFormat
- )
+ wd = os.path.dirname(os.path.realpath(__file__))
- self.loadEncoderOptions()
- self.videoFormats = toolkit.appendUppercase([
+ 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',
@@ -48,7 +58,7 @@ class Core:
'*.webm',
'*.flv',
])
- self.audioFormats = toolkit.appendUppercase([
+ settings['audioFormats'] = toolkit.appendUppercase([
'*.mp3',
'*.wav',
'*.ogg',
@@ -56,7 +66,7 @@ class Core:
'*.flac',
'*.aac',
])
- self.imageFormats = toolkit.appendUppercase([
+ settings['imageFormats'] = toolkit.appendUppercase([
'*.png',
'*.jpg',
'*.tif',
@@ -68,15 +78,22 @@ class Core:
'*.xpm',
])
- self.FFMPEG_BIN = self.findFfmpeg()
+ # 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 = []
- # copies of named presets to detect modification
- self.savedPresets = {}
+ self.savedPresets = {} # copies of presets to detect modification
def findComponents(self):
def findComponents():
- for f in sorted(os.listdir(self.componentsPath)):
+ for f in sorted(os.listdir(Core.componentsPath)):
name, ext = os.path.splitext(f)
if name.startswith("__"):
continue
@@ -88,7 +105,7 @@ class Core:
]
# store canonical module names and indexes
self.moduleIndexes = [i for i in range(len(self.modules))]
- self.compNames = [mod.Component.__doc__ for mod in self.modules]
+ self.compNames = [mod.Component.name for mod in self.modules]
self.altCompNames = []
# store alternative names for modules
for i, mod in enumerate(self.modules):
@@ -108,7 +125,7 @@ class Core:
return None
component = self.modules[moduleIndex].Component(
- moduleIndex, compPos, self
+ moduleIndex, compPos
)
self.selectedComponents.insert(
compPos,
@@ -171,10 +188,6 @@ class Core:
self.savedPresets[presetName] = dict(saveValueStore)
return True
- def getPresetDir(self, comp):
- return os.path.join(
- self.presetDir, str(comp), str(comp.version()))
-
def getPreset(self, filepath):
'''Returns the preset dict stored at this filepath'''
if not os.path.exists(filepath):
@@ -204,7 +217,7 @@ class Core:
widget.blockSignals(False)
for key, value in data['Settings']:
- self.settings.setValue(key, value)
+ Core.settings.setValue(key, value)
for tup in data['Components']:
name, vers, preset = tup
@@ -215,7 +228,7 @@ class Core:
if 'preset' in preset and preset['preset'] is not None:
nam = preset['preset']
filepath2 = os.path.join(
- self.presetDir, name, str(vers), nam)
+ Core.presetDir, name, str(vers), nam)
origSaveValueStore = self.getPreset(filepath2)
if origSaveValueStore:
self.savedPresets[nam] = dict(origSaveValueStore)
@@ -336,7 +349,7 @@ class Core:
presetName = preset['preset'] \
if preset['preset'] else os.path.basename(filepath)[:-4]
newPath = os.path.join(
- self.presetDir,
+ Core.presetDir,
name,
vers,
presetName
@@ -354,7 +367,7 @@ class Core:
def exportPreset(self, exportPath, compName, vers, origName):
internalPath = os.path.join(
- self.presetDir, compName, str(vers), origName
+ Core.presetDir, compName, str(vers), origName
)
if not os.path.exists(internalPath):
return
@@ -378,7 +391,7 @@ class Core:
'''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(self.presetDir, compName, str(vers))
+ dirname = os.path.join(Core.presetDir, compName, str(vers))
if not os.path.exists(dirname):
os.makedirs(dirname)
filepath = os.path.join(dirname, presetName)
@@ -417,13 +430,13 @@ class Core:
saveValueStore = comp.savePreset()
saveValueStore['preset'] = comp.currentPreset
f.write('%s\n' % str(comp))
- f.write('%s\n' % str(comp.version()))
+ f.write('%s\n' % str(comp.version))
f.write('%s\n' % toolkit.presetToString(saveValueStore))
f.write('\n[Settings]\n')
- for key in self.settings.allKeys():
+ for key in Core.settings.allKeys():
if key in settingsKeys:
- f.write('%s=%s\n' % (key, self.settings.value(key)))
+ f.write('%s=%s\n' % (key, Core.settings.value(key)))
if window:
f.write('\n[WindowFields]\n')
@@ -438,280 +451,8 @@ class Core:
except:
return False
- def loadEncoderOptions(self):
- file_path = os.path.join(self.wd, 'encoder-options.json')
- with open(file_path) as json_file:
- self.encoder_options = json.load(json_file)
-
- def findFfmpeg(self):
- if getattr(sys, 'frozen', False):
- # The application is frozen
- if sys.platform == "win32":
- return os.path.join(self.wd, 'ffmpeg.exe')
- else:
- return os.path.join(self.wd, 'ffmpeg')
-
- else:
- if sys.platform == "win32":
- return "ffmpeg"
- else:
- try:
- with open(os.devnull, "w") as f:
- toolkit.checkOutput(
- ['ffmpeg', '-version'], stderr=f
- )
- return "ffmpeg"
- except sp.CalledProcessError:
- return "avconv"
-
- def createFfmpegCommand(self, inputFile, outputFile, duration):
- '''
- Constructs the major ffmpeg command used to export the video
- '''
- safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters
- duration = "{0:.3f}".format(duration + 0.1) # used by input sources
-
- # Test if user has libfdk_aac
- encoders = toolkit.checkOutput(
- "%s -encoders -hide_banner" % self.FFMPEG_BIN, shell=True
- )
- encoders = encoders.decode("utf-8")
-
- acodec = self.settings.value('outputAudioCodec')
-
- options = self.encoder_options
- containerName = self.settings.value('outputContainer')
- vcodec = self.settings.value('outputVideoCodec')
- vbitrate = str(self.settings.value('outputVideoBitrate'))+'k'
- acodec = self.settings.value('outputAudioCodec')
- abitrate = str(self.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]
-
- for encoder in vencoders:
- if encoder in encoders:
- vencoder = encoder
- break
-
- for encoder in aencoders:
- if encoder in encoders:
- aencoder = encoder
- break
-
- ffmpegCommand = [
- self.FFMPEG_BIN,
- '-thread_queue_size', '512',
- '-y', # overwrite the output file if it already exists.
-
- # INPUT VIDEO
- '-f', 'rawvideo',
- '-vcodec', 'rawvideo',
- '-s', '%sx%s' % (
- self.settings.value('outputWidth'),
- self.settings.value('outputHeight'),
- ),
- '-pix_fmt', 'rgba',
- '-r', self.settings.value('outputFrameRate'),
- '-t', duration,
- '-i', '-', # the video input comes from a pipe
- '-an', # the video input has no sound
-
- # INPUT SOUND
- '-t', duration,
- '-i', inputFile
- ]
-
- # Add extra audio inputs and any needed avfilters
- # NOTE: Global filters are currently hard-coded here for debugging use
- globalFilters = 0 # increase to add global filters
- extraAudio = [
- comp.audio() for comp in self.selectedComponents
- if 'audio' in comp.properties()
- ]
- if extraAudio or globalFilters > 0:
- # Add -i options for extra input files
- extraFilters = {}
- for streamNo, params in enumerate(reversed(extraAudio)):
- extraInputFile, params = params
- ffmpegCommand.extend([
- '-t', safeDuration,
- # Tell ffmpeg about shorter clips (seemingly not needed)
- # streamDuration = self.getAudioDuration(extraInputFile)
- # if 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]
- ))
-
- # Start creating avfilters! Popen-style, so don't use semicolons;
- extraFilterCommand = []
-
- if globalFilters <= 0:
- # Dictionary of last-used tmp labels for a given stream number
- tmpInputs = {streamNo: -1 for streamNo in extraFilters}
- else:
- # Insert blank entries for global filters into extraFilters
- # so the per-stream filters know what input to source later
- for streamNo in range(len(extraAudio), 0, -1):
- if streamNo + 1 not in extraFilters:
- extraFilters[streamNo + 1] = []
- # Also filter the primary audio track
- extraFilters[1] = []
- 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
- ])
-
- # 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])
- )
- tmpInputs[streamNo] = tmpInputs[streamNo] + 1
- extraFilterCommand.append(
- '%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)
- ),
- ])
-
- # Only map audio from the filters, and video from the pipe
- ffmpegCommand.extend([
- '-map', '0:v',
- '-map', '[a]',
- ])
-
- ffmpegCommand.extend([
- # OUTPUT
- '-vcodec', vencoder,
- '-acodec', aencoder,
- '-b:v', vbitrate,
- '-b:a', abitrate,
- '-pix_fmt', self.settings.value('outputVideoFormat'),
- '-preset', self.settings.value('outputPreset'),
- '-f', container
- ])
-
- if acodec == 'aac':
- ffmpegCommand.append('-strict')
- ffmpegCommand.append('-2')
-
- ffmpegCommand.append(outputFile)
- return ffmpegCommand
-
- def getAudioDuration(self, filename):
- command = [self.FFMPEG_BIN, '-i', filename]
-
- try:
- fileInfo = toolkit.checkOutput(command, stderr=sp.STDOUT)
- except sp.CalledProcessError as ex:
- fileInfo = ex.output
-
- info = fileInfo.decode("utf-8").split('\n')
- 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])
- return duration
-
- def readAudioFile(self, filename, parent):
- duration = self.getAudioDuration(filename)
-
- command = [
- self.FFMPEG_BIN,
- '-i', filename,
- '-f', 's16le',
- '-acodec', 'pcm_s16le',
- '-ar', '44100', # ouput will have 44100 Hz
- '-ac', '1', # mono (set to '2' for stereo)
- '-']
- in_pipe = toolkit.openPipe(
- command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8
- )
-
- completeAudioArray = numpy.empty(0, dtype="int16")
-
- progress = 0
- lastPercent = None
- while True:
- if self.canceled:
- break
- # read 2 seconds of audio
- progress += 4
- raw_audio = in_pipe.stdout.read(88200*4)
- if len(raw_audio) == 0:
- break
- audio_array = numpy.fromstring(raw_audio, dtype="int16")
- completeAudioArray = numpy.append(completeAudioArray, audio_array)
-
- percent = int(100*(progress/duration))
- if percent >= 100:
- percent = 100
-
- if lastPercent != percent:
- string = 'Loading audio file: '+str(percent)+'%'
- parent.progressBarSetText.emit(string)
- parent.progressBarUpdate.emit(percent)
-
- lastPercent = percent
-
- in_pipe.kill()
- in_pipe.wait()
-
- # add 0s the end
- completeAudioArrayCopy = numpy.zeros(
- len(completeAudioArray) + 44100, dtype="int16")
- completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray
- completeAudioArray = completeAudioArrayCopy
-
- return (completeAudioArray, duration)
-
def newVideoWorker(self, loader, audioFile, outputPath):
+ '''loader is MainWindow or Command object which must own the thread'''
self.videoThread = QtCore.QThread(loader)
videoWorker = video_thread.Worker(
loader, audioFile, outputPath, self.selectedComponents
@@ -727,7 +468,9 @@ class Core:
self.videoThread.wait()
def cancel(self):
- self.canceled = True
+ Core.canceled = True
+ toolkit.cancel()
def reset(self):
- self.canceled = False
+ Core.canceled = False
+ toolkit.reset()
diff --git a/src/mainwindow.py b/src/mainwindow.py
index ca8e697..9944d1a 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -14,13 +14,17 @@ import signal
import filecmp
import time
-import core
+from core import Core
import preview_thread
from presetmanager import PresetManager
-from toolkit import LoadDefaultSettings, disableWhenEncoding, checkOutput
+from toolkit import loadDefaultSettings, disableWhenEncoding, checkOutput
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
@@ -47,6 +51,14 @@ class PreviewWindow(QtWidgets.QLabel):
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.
+
+ 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
@@ -57,25 +69,26 @@ class MainWindow(QtWidgets.QMainWindow):
# print('main thread id: {}'.format(QtCore.QThread.currentThreadId()))
self.window = window
- self.core = core.Core()
+ self.core = Core()
self.pages = [] # widgets of component settings
self.lastAutosave = time.time()
self.encoding = False
# Create data directory, load/create settings
- self.dataDir = self.core.dataDir
+ self.dataDir = Core.dataDir
+ self.presetDir = Core.presetDir
self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
- self.settings = self.core.settings
- LoadDefaultSettings(self)
+ self.settings = Core.settings
+ loadDefaultSettings(self)
self.presetManager = PresetManager(
uic.loadUi(
- os.path.join(self.core.wd, 'presetmanager.ui')), self)
+ os.path.join(Core.wd, 'presetmanager.ui')), self)
if not os.path.exists(self.dataDir):
os.makedirs(self.dataDir)
for neededDirectory in (
- self.core.presetDir, self.settings.value("projectDir")):
+ self.presetDir, self.settings.value("projectDir")):
if not os.path.exists(neededDirectory):
os.mkdir(neededDirectory)
@@ -120,7 +133,7 @@ class MainWindow(QtWidgets.QMainWindow):
window.pushButton_Cancel.clicked.connect(self.stopVideo)
- for i, container in enumerate(self.core.encoder_options['containers']):
+ for i, container in enumerate(Core.encoderOptions['containers']):
window.comboBox_videoContainer.addItem(container['name'])
if container['name'] == self.settings.value('outputContainer'):
selectedContainer = i
@@ -160,14 +173,14 @@ class MainWindow(QtWidgets.QMainWindow):
window.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings)
self.previewWindow = PreviewWindow(self, os.path.join(
- self.core.wd, "background.png"))
+ Core.wd, "background.png"))
window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
# Make component buttons
self.compMenu = QMenu()
self.compActions = []
for i, comp in enumerate(self.core.modules):
- action = self.compMenu.addAction(comp.Component.__doc__)
+ action = self.compMenu.addAction(comp.Component.name)
action.triggered.connect(
lambda _, item=i: self.core.insertComponent(0, item, self)
)
@@ -336,8 +349,14 @@ class MainWindow(QtWidgets.QMainWindow):
"Ctrl+Down", self.window,
activated=lambda: self.moveComponent(1)
)
- QtWidgets.QShortcut("Ctrl+Home", self.window, self.moveComponentTop)
- QtWidgets.QShortcut("Ctrl+End", self.window, self.moveComponentBottom)
+ QtWidgets.QShortcut(
+ "Ctrl+Home", self.window,
+ activated=lambda: self.moveComponent('top')
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+End", self.window,
+ activated=lambda: self.moveComponent('bottom')
+ )
QtWidgets.QShortcut("Ctrl+r", self.window, self.removeComponent)
@QtCore.pyqtSlot()
@@ -389,7 +408,7 @@ class MainWindow(QtWidgets.QMainWindow):
vCodecWidget.clear()
aCodecWidget.clear()
- for container in self.core.encoder_options['containers']:
+ for container in Core.encoderOptions['containers']:
if container['name'] == name:
for vCodec in container['video-codecs']:
vCodecWidget.addItem(vCodec)
@@ -397,6 +416,7 @@ class MainWindow(QtWidgets.QMainWindow):
aCodecWidget.addItem(aCodec)
def updateCodecSettings(self):
+ '''Updates settings.ini to match encoder option widgets'''
vCodecWidget = self.window.comboBox_videoCodec
vBitrateWidget = self.window.spinBox_vBitrate
aBitrateWidget = self.window.spinBox_aBitrate
@@ -416,11 +436,12 @@ class MainWindow(QtWidgets.QMainWindow):
if not self.currentProject:
if os.path.exists(self.autosavePath):
os.remove(self.autosavePath)
- elif force or time.time() - self.lastAutosave >= 0.1:
+ elif force or time.time() - self.lastAutosave >= 0.2:
self.core.createProjectFile(self.autosavePath, self.window)
self.lastAutosave = time.time()
def autosaveExists(self, identical=True):
+ '''Determines if creating the autosave should be blocked.'''
try:
if self.currentProject and os.path.exists(self.autosavePath) \
and filecmp.cmp(
@@ -432,6 +453,7 @@ class MainWindow(QtWidgets.QMainWindow):
return False
def saveProjectChanges(self):
+ '''Overwrites project file with autosave file'''
try:
os.remove(self.currentProject)
os.rename(self.autosavePath, self.currentProject)
@@ -447,7 +469,7 @@ class MainWindow(QtWidgets.QMainWindow):
fileName, _ = QtWidgets.QFileDialog.getOpenFileName(
self.window, "Open Audio File",
- inputDir, "Audio Files (%s)" % " ".join(self.core.audioFormats))
+ inputDir, "Audio Files (%s)" % " ".join(Core.audioFormats))
if fileName:
self.settings.setValue("inputDir", os.path.dirname(fileName))
@@ -460,7 +482,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.window, "Set Output Video File",
outputDir,
"Video Files (%s);; All Files (*)" % " ".join(
- self.core.videoFormats))
+ Core.videoFormats))
if fileName:
self.settings.setValue("outputDir", os.path.dirname(fileName))
@@ -587,10 +609,11 @@ class MainWindow(QtWidgets.QMainWindow):
def showFfmpegCommand(self):
from textwrap import wrap
- command = self.core.createFfmpegCommand(
+ from toolkit.ffmpeg import createFfmpegCommand
+ command = createFfmpegCommand(
self.window.lineEdit_audioFile.text(),
self.window.lineEdit_outputFile.text(),
- self.core.getAudioDuration(self.window.lineEdit_audioFile.text())
+ self.core.selectedComponents
)
lines = wrap(" ".join(command), 49)
self.showMessage(
@@ -603,7 +626,7 @@ class MainWindow(QtWidgets.QMainWindow):
componentList.insertItem(
index,
- self.core.selectedComponents[index].__doc__)
+ self.core.selectedComponents[index].name)
componentList.setCurrentRow(index)
# connect to signal that adds an asterisk when modified
@@ -632,6 +655,10 @@ class MainWindow(QtWidgets.QMainWindow):
def moveComponent(self, change):
'''Moves a component relatively from its current position'''
componentList = self.window.listWidget_componentList
+ if change == 'top':
+ change = -componentList.currentRow()
+ elif change == 'bottom':
+ change = len(componentList)-componentList.currentRow()-1
stackedWidget = self.window.stackedWidget
row = componentList.currentRow()
@@ -650,21 +677,9 @@ class MainWindow(QtWidgets.QMainWindow):
stackedWidget.setCurrentIndex(newRow)
self.drawPreview()
- @disableWhenEncoding
- def moveComponentTop(self):
- componentList = self.window.listWidget_componentList
- row = -componentList.currentRow()
- self.moveComponent(row)
-
- @disableWhenEncoding
- def moveComponentBottom(self):
- componentList = self.window.listWidget_componentList
- row = len(componentList)-componentList.currentRow()-1
- self.moveComponent(row)
-
@disableWhenEncoding
def dragComponent(self, event):
- '''Drop event for the component listwidget'''
+ '''Used as Qt drop event for the component listwidget'''
componentList = self.window.listWidget_componentList
modelIndexes = [
diff --git a/src/presetmanager.py b/src/presetmanager.py
index 6e003a1..825fdee 100644
--- a/src/presetmanager.py
+++ b/src/presetmanager.py
@@ -15,11 +15,11 @@ class PresetManager(QtWidgets.QDialog):
self.parent = parent
self.core = parent.core
self.settings = parent.settings
- self.presetDir = self.core.presetDir
+ self.presetDir = parent.presetDir
if not self.settings.value('presetDir'):
self.settings.setValue(
"presetDir",
- os.path.join(self.core.dataDir, 'projects'))
+ os.path.join(parent.dataDir, 'projects'))
self.findPresets()
@@ -161,7 +161,7 @@ class PresetManager(QtWidgets.QDialog):
selectedComponents[index].savePreset()
saveValueStore['preset'] = newName
componentName = str(selectedComponents[index]).strip()
- vers = selectedComponents[index].version()
+ vers = selectedComponents[index].version
self.createNewPreset(
componentName, vers, newName,
saveValueStore, window=self.parent.window)
@@ -195,13 +195,13 @@ class PresetManager(QtWidgets.QDialog):
def openPreset(self, presetName, compPos=None):
componentList = self.parent.window.listWidget_componentList
- selectedComponents = self.parent.core.selectedComponents
+ selectedComponents = self.core.selectedComponents
index = compPos if compPos is not None else componentList.currentRow()
if index == -1:
return
componentName = str(selectedComponents[index]).strip()
- version = selectedComponents[index].version()
+ version = selectedComponents[index].version
dirname = os.path.join(self.presetDir, componentName, str(version))
filepath = os.path.join(dirname, presetName)
self.core.openPreset(filepath, index, presetName)
@@ -243,6 +243,7 @@ class PresetManager(QtWidgets.QDialog):
parent=window if window else self.window)
def openRenamePresetDialog(self):
+ # TODO: maintain consistency by changing this to call createNewPreset()
presetList = self.window.listWidget_presets
if presetList.currentRow() == -1:
return
@@ -273,11 +274,12 @@ class PresetManager(QtWidgets.QDialog):
os.rename(oldPath, newPath)
self.findPresets()
self.drawPresetList()
-
for i, comp in enumerate(self.core.selectedComponents):
- if comp.currentPreset == oldName:
- comp.currentPreset = newName
- self.parent.updateComponentTitle(i, True)
+ if toolkit.getPresetDir(comp) == path \
+ and comp.currentPreset == oldName:
+ self.core.openPreset(newPath, i, newName)
+ self.parent.updateComponentTitle(i, False)
+ self.parent.drawPreview()
break
def openImportDialog(self):
diff --git a/src/preview_thread.py b/src/preview_thread.py
index c28e048..3fc73b3 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -22,8 +22,8 @@ class Worker(QtCore.QObject):
parent.newTask.connect(self.createPreviewImage)
parent.processTask.connect(self.process)
self.parent = parent
- self.core = self.parent.core
- self.settings = self.parent.core.settings
+ self.core = parent.core
+ self.settings = parent.settings
self.queue = queue
width = int(self.settings.value('outputWidth'))
diff --git a/src/toolkit/common.py b/src/toolkit/common.py
index e3a1649..763d582 100644
--- a/src/toolkit/common.py
+++ b/src/toolkit/common.py
@@ -8,6 +8,13 @@ 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'''
@@ -103,8 +110,9 @@ def rgbFromString(string):
return (255, 255, 255)
-def LoadDefaultSettings(self):
- ''' Runs once at each program start-up. Fills in default settings
+def loadDefaultSettings(self):
+ '''
+ Runs once at each program start-up. Fills in default settings
for any settings not found in settings.ini
'''
self.resolutions = [
diff --git a/src/toolkit/core.py b/src/toolkit/core.py
new file mode 100644
index 0000000..a96a684
--- /dev/null
+++ b/src/toolkit/core.py
@@ -0,0 +1,18 @@
+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
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
new file mode 100644
index 0000000..89d4e9d
--- /dev/null
+++ b/src/toolkit/ffmpeg.py
@@ -0,0 +1,284 @@
+'''
+ Tools for using ffmpeg
+'''
+import numpy
+import sys
+import os
+import subprocess as sp
+
+from toolkit.common import Core, 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')
+ else:
+ return os.path.join(Core.wd, 'ffmpeg')
+
+ else:
+ if sys.platform == "win32":
+ return "ffmpeg"
+ else:
+ try:
+ with open(os.devnull, "w") as f:
+ checkOutput(
+ ['ffmpeg', '-version'], stderr=f
+ )
+ return "ffmpeg"
+ except sp.CalledProcessError:
+ return "avconv"
+
+
+def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
+ '''
+ 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
+ duration = "{0:.3f}".format(duration + 0.1) # used by input sources
+
+ # Test if user has libfdk_aac
+ encoders = checkOutput(
+ "%s -encoders -hide_banner" % Core.FFMPEG_BIN, shell=True
+ )
+ encoders = encoders.decode("utf-8")
+
+ 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']
+ break
+
+ vencoders = options['video-codecs'][vcodec]
+ aencoders = options['audio-codecs'][acodec]
+
+ for encoder in vencoders:
+ if encoder in encoders:
+ vencoder = encoder
+ break
+
+ for encoder in aencoders:
+ if encoder in encoders:
+ aencoder = encoder
+ break
+
+ ffmpegCommand = [
+ Core.FFMPEG_BIN,
+ '-thread_queue_size', '512',
+ '-y', # overwrite the output file if it already exists.
+
+ # INPUT VIDEO
+ '-f', 'rawvideo',
+ '-vcodec', 'rawvideo',
+ '-s', '%sx%s' % (
+ Core.settings.value('outputWidth'),
+ Core.settings.value('outputHeight'),
+ ),
+ '-pix_fmt', 'rgba',
+ '-r', Core.settings.value('outputFrameRate'),
+ '-t', duration,
+ '-i', '-', # the video input comes from a pipe
+ '-an', # the video input has no sound
+
+ # INPUT SOUND
+ '-t', duration,
+ '-i', inputFile
+ ]
+
+ # Add extra audio inputs and any needed avfilters
+ # NOTE: Global filters are currently hard-coded here for debugging use
+ globalFilters = 0 # increase to add global filters
+ extraAudio = [
+ comp.audio for comp in components
+ if 'audio' in comp.properties
+ ]
+ if extraAudio or globalFilters > 0:
+ # Add -i options for extra input files
+ extraFilters = {}
+ for streamNo, params in enumerate(reversed(extraAudio)):
+ extraInputFile, params = params
+ ffmpegCommand.extend([
+ '-t', safeDuration,
+ # Tell ffmpeg about shorter clips (seemingly not needed)
+ # streamDuration = getAudioDuration(extraInputFile)
+ # if 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]
+ ))
+
+ # Start creating avfilters! Popen-style, so don't use semicolons;
+ extraFilterCommand = []
+
+ if globalFilters <= 0:
+ # Dictionary of last-used tmp labels for a given stream number
+ tmpInputs = {streamNo: -1 for streamNo in extraFilters}
+ else:
+ # Insert blank entries for global filters into extraFilters
+ # so the per-stream filters know what input to source later
+ for streamNo in range(len(extraAudio), 0, -1):
+ if streamNo + 1 not in extraFilters:
+ extraFilters[streamNo + 1] = []
+ # Also filter the primary audio track
+ extraFilters[1] = []
+ 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
+ ])
+
+ # 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])
+ )
+ tmpInputs[streamNo] = tmpInputs[streamNo] + 1
+ extraFilterCommand.append(
+ '%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)
+ ),
+ ])
+
+ # Only map audio from the filters, and video from the pipe
+ ffmpegCommand.extend([
+ '-map', '0:v',
+ '-map', '[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 getAudioDuration(filename):
+ command = [Core.FFMPEG_BIN, '-i', filename]
+
+ try:
+ fileInfo = checkOutput(command, stderr=sp.STDOUT)
+ except sp.CalledProcessError as ex:
+ fileInfo = ex.output
+
+ info = fileInfo.decode("utf-8").split('\n')
+ 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])
+ return duration
+
+
+def readAudioFile(filename, parent):
+ duration = getAudioDuration(filename)
+
+ command = [
+ 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)
+ '-']
+ in_pipe = openPipe(
+ command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8
+ )
+
+ completeAudioArray = numpy.empty(0, dtype="int16")
+
+ progress = 0
+ lastPercent = None
+ while True:
+ if Core.canceled:
+ return
+ # read 2 seconds of audio
+ progress += 4
+ raw_audio = in_pipe.stdout.read(88200*4)
+ if len(raw_audio) == 0:
+ break
+ audio_array = numpy.fromstring(raw_audio, dtype="int16")
+ completeAudioArray = numpy.append(completeAudioArray, audio_array)
+
+ percent = int(100*(progress/duration))
+ if percent >= 100:
+ percent = 100
+
+ if lastPercent != percent:
+ string = 'Loading audio file: '+str(percent)+'%'
+ parent.progressBarSetText.emit(string)
+ parent.progressBarUpdate.emit(percent)
+
+ lastPercent = percent
+
+ in_pipe.kill()
+ in_pipe.wait()
+
+ # add 0s the end
+ completeAudioArrayCopy = numpy.zeros(
+ len(completeAudioArray) + 44100, dtype="int16")
+ completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray
+ completeAudioArray = completeAudioArrayCopy
+
+ return (completeAudioArray, duration)
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index cddb611..83fd59e 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -7,9 +7,7 @@ from PIL.ImageQt import ImageQt
import sys
import os
-
-class Frame:
- '''Controller class for all frames.'''
+from toolkit.common import Core
class FramePainter(QtGui.QPainter):
@@ -59,7 +57,7 @@ def Checkerboard(width, height):
'''
image = FloodFrame(1920, 1080, (0, 0, 0, 0))
image.paste(Image.open(
- os.path.join(Frame.core.wd, "background.png")),
+ os.path.join(Core.wd, "background.png")),
(0, 0)
)
image = image.resize((width, height))
diff --git a/src/video_thread.py b/src/video_thread.py
index 1f2eaf5..8517b92 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -5,9 +5,9 @@
are emitted to update MainWindow's progress bar, detail text, and preview.
Export can be cancelled with cancel()
'''
-from PyQt5 import QtCore, QtGui, uic
+from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import pyqtSignal, pyqtSlot
-from PIL import Image, ImageDraw, ImageFont
+from PIL import Image
from PIL.ImageQt import ImageQt
import numpy
import subprocess as sp
@@ -19,6 +19,7 @@ import time
import signal
from toolkit import openPipe
+from toolkit.ffmpeg import readAudioFile, createFfmpegCommand
from toolkit.frame import Checkerboard
@@ -33,7 +34,7 @@ class Worker(QtCore.QObject):
def __init__(self, parent, inputFile, outputFile, components):
QtCore.QObject.__init__(self)
self.core = parent.core
- self.settings = parent.core.settings
+ self.settings = parent.settings
self.modules = parent.core.modules
parent.createVideo.connect(self.createVideo)
@@ -133,12 +134,17 @@ class Worker(QtCore.QObject):
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
self.progressBarSetText.emit("Loading audio file...")
- self.completeAudioArray, duration = self.core.readAudioFile(
+ audioFileTraits = readAudioFile(
self.inputFile, self
)
+ if audioFileTraits is None:
+ self.cancelExport()
+ return
+ self.completeAudioArray, duration = audioFileTraits
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit("Starting components...")
+ canceledByComponent = False
print('Loaded Components:', ", ".join([
"%s) %s" % (num, str(component))
for num, component in enumerate(reversed(self.components))
@@ -153,14 +159,15 @@ class Worker(QtCore.QObject):
progressBarSetText=self.progressBarSetText
)
- if 'error' in comp.properties():
+ if 'error' in comp.properties:
self.cancel()
self.canceled = True
+ canceledByComponent = True
errMsg = "Component #%s encountered an error!" % compNo \
- if comp.error() is None else 'Component #%s (%s): %s' % (
+ if comp.error is None else 'Component #%s (%s): %s' % (
str(compNo),
str(comp),
- comp.error()
+ comp.error
)
self.parent.showMessage(
msg=errMsg,
@@ -168,17 +175,16 @@ class Worker(QtCore.QObject):
parent=None # MainWindow is in a different thread
)
break
- if 'static' in comp.properties():
+ if 'static' in comp.properties:
self.staticComponents[compNo] = \
comp.frameRender(compNo, 0).copy()
if self.canceled:
- print('Export cancelled by component #%s (%s): %s' % (
- compNo, str(comp), comp.error()
- ))
- self.progressBarSetText.emit('Export Canceled')
- self.encoding.emit(False)
- self.videoCreated.emit()
+ if canceledByComponent:
+ print('Export cancelled by component #%s (%s): %s' % (
+ compNo, str(comp), comp.error
+ ))
+ self.cancelExport()
return
# Merge consecutive static component frames together
@@ -192,8 +198,8 @@ class Worker(QtCore.QObject):
)
self.staticComponents[compNo] = None
- ffmpegCommand = self.core.createFfmpegCommand(
- self.inputFile, self.outputFile, duration
+ ffmpegCommand = createFfmpegCommand(
+ self.inputFile, self.outputFile, self.components, duration
)
print('###### FFMPEG COMMAND ######\n%s' % " ".join(ffmpegCommand))
print('############################')
@@ -280,7 +286,6 @@ class Worker(QtCore.QObject):
pass
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit('Export Canceled')
-
else:
if self.error:
print("Export Failed")
@@ -297,6 +302,12 @@ class Worker(QtCore.QObject):
self.encoding.emit(False)
self.videoCreated.emit()
+ def cancelExport(self):
+ self.progressBarUpdate.emit(0)
+ self.progressBarSetText.emit('Export Canceled')
+ self.encoding.emit(False)
+ self.videoCreated.emit()
+
def updateProgress(self, pStr, pVal):
self.progressBarValue.emit(pVal)
self.progressBarSetText.emit(pStr)
--
cgit v1.2.3
From bf0890e7c87c730b8970c1a20c5b6a9a1a55d203 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 23 Jul 2017 01:53:54 -0400
Subject: components auto-connect & track widgets, less autosave spam
importing toolkit from live interpreter now works
---
setup.py | 2 +-
src/__init__.py | 12 +++
src/command.py | 2 -
src/component.py | 196 +++++++++++++++++++++++++++++++++------------
src/components/color.py | 137 +++++++++++--------------------
src/components/image.py | 77 +++++-------------
src/components/original.py | 59 ++++++--------
src/components/sound.py | 50 +++---------
src/components/text.py | 81 ++++++++-----------
src/components/video.py | 98 +++++++----------------
src/core.py | 196 ++++++++++++++++++++++++++++-----------------
src/main.py | 23 ++----
src/mainwindow.py | 125 +++++++++++++++++++----------
src/mainwindow.ui | 3 +
src/presetmanager.py | 15 ++--
src/preview_thread.py | 17 ++--
src/toolkit/common.py | 56 +++----------
src/toolkit/core.py | 18 -----
src/toolkit/ffmpeg.py | 46 ++++++++---
src/toolkit/frame.py | 4 +-
src/video_thread.py | 7 +-
21 files changed, 604 insertions(+), 620 deletions(-)
delete mode 100644 src/toolkit/core.py
(limited to 'src/command.py')
diff --git a/setup.py b/setup.py
index a2d8495..d4f226b 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup
import os
-__version__ = '2.0.0.rc1'
+__version__ = '2.0.0.rc2'
def package_files(directory):
diff --git a/src/__init__.py b/src/__init__.py
index 8b13789..2f4cffa 100644
--- a/src/__init__.py
+++ b/src/__init__.py
@@ -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)
diff --git a/src/command.py b/src/command.py
index 046a1bf..ca186e5 100644
--- a/src/command.py
+++ b/src/command.py
@@ -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
diff --git a/src/component.py b/src/component.py
index 92cc65c..bec2df5 100644
--- a/src/component.py
+++ b/src/component.py
@@ -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 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/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 loadPreset(self, presetDict, presetName):
+ def update(self):
'''
- Subclasses take (presetDict, presetName=None) as args.
- Must use super().loadPreset(presetDict, presetName) first,
+ 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.
'''
diff --git a/src/components/color.py b/src/components/color.py
index 03371e7..8257ed9 100644
--- a/src/components/color.py
+++ b/src/components/color.py
@@ -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 = page
- return page
+ 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,
+ }, 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)
diff --git a/src/components/image.py b/src/components/image.py
index 591e03e..a705904 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -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:
diff --git a/src/components/original.py b/src/components/original.py
index ae40df3..2bda878 100644
--- a/src/components/original.py
+++ b/src/components/original.py
@@ -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
-
- 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())
+ super().widget(*args)
+
+ 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':
diff --git a/src/components/sound.py b/src/components/sound.py
index 677a22f..dd3cbab 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -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)
diff --git a/src/components/text.py b/src/components/text.py
index d511f22..1d64617 100644
--- a/src/components/text.py
+++ b/src/components/text.py
@@ -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)
-
- 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))
-
- 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.pushButton_textColor.setStyleSheet(btnStyle)
+
+ 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))
+
+ 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)
diff --git a/src/components/video.py b/src/components/video.py
index 8758b12..677e3ee 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -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)
diff --git a/src/core.py b/src/core.py
index f6cf5eb..eb6398b 100644
--- a/src/core.py
+++ b/src/core.py
@@ -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()
diff --git a/src/main.py b/src/main.py
index 6a9a25e..977da3b 100644
--- a/src/main.py
+++ b/src/main.py
@@ -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)
+def main():
+ 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()
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 2d598ae..f333513 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -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
diff --git a/src/mainwindow.ui b/src/mainwindow.ui
index b491323..b43d375 100644
--- a/src/mainwindow.ui
+++ b/src/mainwindow.ui
@@ -22,6 +22,9 @@
0
+
+ Qt::StrongFocus
+
MainWindow
diff --git a/src/presetmanager.py b/src/presetmanager.py
index 64e2203..643e180 100644
--- a/src/presetmanager.py
+++ b/src/presetmanager.py
@@ -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))
diff --git a/src/preview_thread.py b/src/preview_thread.py
index 3fc73b3..9917e4b 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -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)
diff --git a/src/toolkit/common.py b/src/toolkit/common.py
index 763d582..5fe601f 100644
--- a/src/toolkit/common.py
+++ b/src/toolkit/common.py
@@ -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)
diff --git a/src/toolkit/core.py b/src/toolkit/core.py
deleted file mode 100644
index a96a684..0000000
--- a/src/toolkit/core.py
+++ /dev/null
@@ -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
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index cc59a6c..30dc0b3 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -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
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index 83fd59e..ca2a054 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -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))
diff --git a/src/video_thread.py b/src/video_thread.py
index 8517b92..7fe3e02 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -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()
--
cgit v1.2.3
From d92fc6373fd070f0ea303e9795eb7648d5cd9e90 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 23 Jul 2017 22:55:41 -0400
Subject: ComponentError exception wraps previewRender
probably where errors are likeliest to be found
---
src/command.py | 6 +++
src/component.py | 119 +++++++++++++++++++++++++++---------------------
src/components/video.py | 6 +--
src/core.py | 3 ++
src/mainwindow.py | 8 ++--
src/toolkit/frame.py | 18 ++++++++
src/video_thread.py | 4 +-
7 files changed, 104 insertions(+), 60 deletions(-)
(limited to 'src/command.py')
diff --git a/src/command.py b/src/command.py
index ca186e5..74ca821 100644
--- a/src/command.py
+++ b/src/command.py
@@ -146,6 +146,12 @@ class Command(QtCore.QObject):
if 'detail' in kwargs:
print(kwargs['detail'])
+ @QtCore.pyqtSlot(str, str)
+ def videoThreadError(self, msg, detail):
+ print(msg)
+ print(detail)
+ quit(1)
+
def drawPreview(self, *args):
pass
diff --git a/src/component.py b/src/component.py
index 8b5f1b8..41cb5eb 100644
--- a/src/component.py
+++ b/src/component.py
@@ -6,54 +6,64 @@ from PyQt5 import uic, QtCore, QtWidgets
import os
-def commandWrapper(func):
- '''Intercepts each component's command() method to check for global args'''
- def decorator(self, arg):
- if arg.startswith('preset='):
- from presetmanager import getPresetDir
- _, 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
-
-
-def propertiesWrapper(func):
- '''Intercepts the usual properties if the properties are locked.'''
- def decorator(self):
- if self._lockedProperties is not None:
- return self._lockedProperties
- else:
- return func(self)
- return decorator
-
-
-def errorWrapper(func):
- '''Intercepts the usual error message if it is locked.'''
- def decorator(self):
- if self._lockedError is not None:
- return self._lockedError
- else:
- return func(self)
- return decorator
-
-
class ComponentMetaclass(type(QtCore.QObject)):
'''
Checks the validity of each Component class imported, and
mutates some attributes for easier use by the core program.
E.g., takes only major version from version string & decorates methods
'''
+
+ def renderWrapper(func):
+ def decorator(self, *args, **kwargs):
+ try:
+ return func(self, *args, **kwargs)
+ except:
+ from toolkit.frame import BlankFrame
+ try:
+ raise ComponentError(self, 'renderer', immediate=True)
+ except ComponentError:
+ return BlankFrame()
+ return decorator
+
+ def commandWrapper(func):
+ '''Intercepts each component's command() method to check for global args'''
+ def decorator(self, arg):
+ if arg.startswith('preset='):
+ from presetmanager import getPresetDir
+ _, 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
+
+ def propertiesWrapper(func):
+ '''Intercepts the usual properties if the properties are locked.'''
+ def decorator(self):
+ if self._lockedProperties is not None:
+ return self._lockedProperties
+ else:
+ return func(self)
+ return decorator
+
+ def errorWrapper(func):
+ '''Intercepts the usual error message if it is locked.'''
+ def decorator(self):
+ if self._lockedError is not None:
+ return self._lockedError
+ else:
+ return func(self)
+ return decorator
+
def __new__(cls, name, parents, attrs):
if 'ui' not in attrs:
# Use module name as ui filename by default
@@ -62,7 +72,11 @@ class ComponentMetaclass(type(QtCore.QObject)):
)[0]
# if parents[0] == QtCore.QObject: else:
- decorate = ('names', 'error', 'audio', 'command', 'properties')
+ decorate = (
+ 'names', # Class methods
+ 'error', 'audio', 'properties', # Properties
+ 'previewRender', 'command',
+ )
# Auto-decorate methods
for key in decorate:
@@ -76,13 +90,16 @@ class ComponentMetaclass(type(QtCore.QObject)):
attrs[key] = property(attrs[key])
if key == 'command':
- attrs[key] = commandWrapper(attrs[key])
+ attrs[key] = cls.commandWrapper(attrs[key])
+
+ if key == 'previewRender':
+ attrs[key] = cls.renderWrapper(attrs[key])
if key == 'properties':
- attrs[key] = propertiesWrapper(attrs[key])
+ attrs[key] = cls.propertiesWrapper(attrs[key])
if key == 'error':
- attrs[key] = errorWrapper(attrs[key])
+ attrs[key] = cls.errorWrapper(attrs[key])
# Turn version string into a number
try:
@@ -223,11 +240,11 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
if kwarg in ('presetNames', 'commandArgs'):
setattr(self, '_%s' % kwarg, kwargs[kwarg])
else:
- raise BadComponentInit(
+ raise ComponentError(
self,
'Nonsensical keywords to trackWidgets.',
immediate=True)
- except BadComponentInit:
+ except ComponentError:
continue
def update(self):
@@ -366,7 +383,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
-class BadComponentInit(AttributeError):
+class ComponentError(RuntimeError):
'''
Indicates a Python error in constructing a component.
Raising this locks the component into an error state,
@@ -397,9 +414,7 @@ class BadComponentInit(AttributeError):
)
if immediate:
- caller.parent.showMessage(
- msg=string, detail=detail, icon='Warning'
- )
+ caller._error.emit(string, detail)
else:
caller.lockProperties(['error'])
caller.lockError((string, detail))
diff --git a/src/components/video.py b/src/components/video.py
index d3696d4..383531e 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -7,7 +7,7 @@ import threading
from queue import PriorityQueue
from core import Core
-from component import Component, BadComponentInit
+from component import Component, ComponentError
from toolkit.frame import BlankFrame
from toolkit.ffmpeg import testAudioStream
from toolkit import openPipe, checkOutput
@@ -195,14 +195,14 @@ class Component(Component):
self.updateChunksize(width, height)
try:
self.video = Video(
- ffmpeg=self.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,
component=self, scale=self.scale
) if os.path.exists(self.videoPath) else None
except KeyError:
- raise BadComponentInit(self, 'Frame Fetcher initialization')
+ raise ComponentError(self, 'Frame Fetcher initialization')
def frameRender(self, layerNo, frameNo):
if self.video:
diff --git a/src/core.py b/src/core.py
index 2f9c36c..4c08c04 100644
--- a/src/core.py
+++ b/src/core.py
@@ -76,6 +76,9 @@ class Core:
component
)
self.componentListChanged()
+ self.selectedComponents[compPos]._error.connect(
+ loader.videoThreadError
+ )
# init component's widget for loading/saving presets
self.selectedComponents[compPos].widget(loader)
diff --git a/src/mainwindow.py b/src/mainwindow.py
index a32c1b4..03b8dde 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -578,7 +578,11 @@ class MainWindow(QtWidgets.QMainWindow):
detail=detail,
icon='Warning',
)
- self.stopVideo()
+ try:
+ self.stopVideo()
+ except AttributeError as e:
+ if 'videoWorker' not in str(e):
+ raise
def changeEncodingStatus(self, status):
self.encoding = status
@@ -684,8 +688,6 @@ class MainWindow(QtWidgets.QMainWindow):
# connect to signal that adds an asterisk when modified
self.core.selectedComponents[index].modified.connect(
self.updateComponentTitle)
- self.core.selectedComponents[index]._error.connect(
- self.videoThreadError)
self.pages.insert(index, self.core.selectedComponents[index].page)
stackedWidget.insertWidget(index, self.pages[index])
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index ca2a054..b66e037 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -41,15 +41,33 @@ class PaintColor(QtGui.QColor):
super().__init__(b, g, r, a)
+def defaultSize(framefunc):
+ '''Makes width/height arguments optional'''
+ def decorator(*args):
+ if len(args) < 2:
+ newArgs = list(args)
+ if len(args) == 0 or len(args) == 1:
+ height = int(core.Core.settings.value("outputHeight"))
+ newArgs.append(height)
+ if len(args) == 0:
+ width = int(core.Core.settings.value("outputWidth"))
+ newArgs.insert(0, width)
+ args = tuple(newArgs)
+ return framefunc(*args)
+ return decorator
+
+
def FloodFrame(width, height, RgbaTuple):
return Image.new("RGBA", (width, height), RgbaTuple)
+@defaultSize
def BlankFrame(width, height):
'''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.
diff --git a/src/video_thread.py b/src/video_thread.py
index 68eae4f..dd957e5 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -18,7 +18,7 @@ from threading import Thread, Event
import time
import signal
-from component import BadComponentInit
+from component import ComponentError
from toolkit import openPipe
from toolkit.ffmpeg import readAudioFile, createFfmpegCommand
from toolkit.frame import Checkerboard
@@ -160,7 +160,7 @@ class Worker(QtCore.QObject):
progressBarUpdate=self.progressBarUpdate,
progressBarSetText=self.progressBarSetText
)
- except BadComponentInit:
+ except ComponentError:
pass
if 'error' in comp.properties():
--
cgit v1.2.3
From 6fc0398602c42a3d219ec92163c480c1833ab0c2 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 27 Jul 2017 18:43:02 -0400
Subject: quit if project doesn't exist when exporting from commandline
---
src/command.py | 4 +++-
src/core.py | 6 ++++--
2 files changed, 7 insertions(+), 3 deletions(-)
(limited to 'src/command.py')
diff --git a/src/command.py b/src/command.py
index 74ca821..18f7408 100644
--- a/src/command.py
+++ b/src/command.py
@@ -64,7 +64,9 @@ class Command(QtCore.QObject):
)
if not projPath.endswith('.avp'):
projPath += '.avp'
- self.core.openProject(self, projPath)
+ success = self.core.openProject(self, projPath)
+ if not success:
+ quit(1)
self.core.selectedComponents = list(
reversed(self.core.selectedComponents))
self.core.componentListChanged()
diff --git a/src/core.py b/src/core.py
index b371d64..1c29774 100644
--- a/src/core.py
+++ b/src/core.py
@@ -214,7 +214,8 @@ class Core:
self.clearPreset(i)
if hasattr(loader, 'updateComponentTitle'):
loader.updateComponentTitle(i, modified)
-
+ self.openingProject = False
+ return True
except Exception:
errcode = 1
data = sys.exc_info()
@@ -234,7 +235,8 @@ class Core:
showCancel=False,
icon='Warning',
detail=msg)
- self.openingProject = False
+ self.openingProject = False
+ return False
def parseAvFile(self, filepath):
'''
--
cgit v1.2.3
From 733c005eeaf5d3ff15e0f60d320f5c03472bad60 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Mon, 14 Aug 2017 18:41:45 -0400
Subject: undoable removeComponent action
---
src/command.py | 1 +
src/component.py | 3 +--
src/core.py | 36 ++++++++++++++++++++++++------------
src/gui/actions.py | 37 +++++++++++++++++++++++++++++++++++++
src/gui/mainwindow.py | 28 +++++++++++++++-------------
src/gui/presetmanager.py | 7 +------
src/main.py | 4 ++--
7 files changed, 81 insertions(+), 35 deletions(-)
create mode 100644 src/gui/actions.py
(limited to 'src/command.py')
diff --git a/src/command.py b/src/command.py
index 18f7408..4116c5a 100644
--- a/src/command.py
+++ b/src/command.py
@@ -19,6 +19,7 @@ class Command(QtCore.QObject):
def __init__(self):
QtCore.QObject.__init__(self)
self.core = Core()
+ Core.mode = 'commandline'
self.dataDir = self.core.dataDir
self.canceled = False
diff --git a/src/component.py b/src/component.py
index cf3085c..0e5144c 100644
--- a/src/component.py
+++ b/src/component.py
@@ -59,9 +59,8 @@ class ComponentMetaclass(type(QtCore.QObject)):
'''Intercepts the command() method to check for global args'''
def commandWrapper(self, arg):
if arg.startswith('preset='):
- from presetmanager import getPresetDir
_, preset = arg.split('=', 1)
- path = os.path.join(getPresetDir(self), preset)
+ path = os.path.join(self.core.getPresetDir(self), preset)
if not os.path.exists(path):
print('Couldn\'t locate preset "%s"' % preset)
quit(1)
diff --git a/src/core.py b/src/core.py
index 4dfb210..20b9c1d 100644
--- a/src/core.py
+++ b/src/core.py
@@ -64,31 +64,39 @@ class Core:
for i, component in enumerate(self.selectedComponents):
component.compPos = i
- def insertComponent(self, compPos, moduleIndex, loader):
+ def insertComponent(self, compPos, component, loader):
'''
Creates a new component using these args:
- (compPos, moduleIndex in self.modules, MWindow/Command/Core obj)
+ (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:
return None
- log.debug('Inserting Component from module #%s' % moduleIndex)
- component = self.modules[moduleIndex].Component(
- moduleIndex, compPos, self
+ if type(component) is int:
+ # create component using module index in self.modules
+ moduleIndex = int(component)
+ log.debug('Creating new component from module #%s' % moduleIndex)
+ component = self.modules[moduleIndex].Component(
+ moduleIndex, compPos, self
+ )
+ # init component's widget for loading/saving presets
+ component.widget(loader)
+ else:
+ moduleIndex = -1
+ log.debug(
+ 'Inserting previously-created %s component' % component.name)
+
+ component._error.connect(
+ loader.videoThreadError
)
self.selectedComponents.insert(
compPos,
component
)
self.componentListChanged()
- self.selectedComponents[compPos]._error.connect(
- loader.videoThreadError
- )
-
- # init component's widget for loading/saving presets
- self.selectedComponents[compPos].widget(loader)
- self.updateComponent(compPos)
+ if moduleIndex > -1:
+ self.updateComponent(compPos)
if hasattr(loader, 'insertComponent'):
loader.insertComponent(compPos)
@@ -156,6 +164,10 @@ class Core:
break
return saveValueStore
+ def getPresetDir(self, comp):
+ '''Get the preset subdir for a particular version of a component'''
+ return os.path.join(Core.presetDir, str(comp), str(comp.version))
+
def openProject(self, loader, filepath):
''' loader is the object calling this method which must have
its own showMessage(**kwargs) method for displaying errors.
diff --git a/src/gui/actions.py b/src/gui/actions.py
new file mode 100644
index 0000000..5cf64e1
--- /dev/null
+++ b/src/gui/actions.py
@@ -0,0 +1,37 @@
+'''
+ QCommand classes for every undoable user action performed in the MainWindow
+'''
+from PyQt5.QtWidgets import QUndoCommand
+
+
+class RemoveComponent(QUndoCommand):
+ def __init__(self, parent, selectedRows):
+ super().__init__('Remove component')
+ self.parent = parent
+ componentList = self.parent.window.listWidget_componentList
+ self.selectedRows = [
+ componentList.row(selected) for selected in selectedRows
+ ]
+ self.components = [
+ parent.core.selectedComponents[i] for i in self.selectedRows
+ ]
+
+ def redo(self):
+ stackedWidget = self.parent.window.stackedWidget
+ componentList = self.parent.window.listWidget_componentList
+ for index in self.selectedRows:
+ stackedWidget.removeWidget(self.parent.pages[index])
+ componentList.takeItem(index)
+ self.parent.core.removeComponent(index)
+ self.parent.pages.pop(index)
+ self.parent.changeComponentWidget()
+ self.parent.drawPreview()
+
+ def undo(self):
+ componentList = self.parent.window.listWidget_componentList
+ for index, comp in zip(self.selectedRows, self.components):
+ self.parent.core.insertComponent(
+ index, comp, self.parent
+ )
+ self.parent.drawPreview()
+
diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py
index af6e190..2edb750 100644
--- a/src/gui/mainwindow.py
+++ b/src/gui/mainwindow.py
@@ -16,9 +16,10 @@ import time
import logging
from core import Core
-import preview_thread
-from preview_win import PreviewWindow
-from presetmanager import PresetManager
+import gui.preview_thread as preview_thread
+from gui.preview_win import PreviewWindow
+from gui.presetmanager import PresetManager
+from gui.actions import *
from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput
@@ -43,9 +44,12 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QMainWindow.__init__(self)
self.window = window
self.core = Core()
+ Core.mode = 'GUI'
log.debug(
'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId())))
+ self.undoStack = QtWidgets.QUndoStack(self)
+
# widgets of component settings
self.pages = []
self.lastAutosave = time.time()
@@ -62,7 +66,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.presetManager = PresetManager(
uic.loadUi(
- os.path.join(Core.wd, 'presetmanager.ui')), self)
+ os.path.join(Core.wd, 'gui', 'presetmanager.ui')), self)
# Create the preview window and its thread, queues, and timers
log.debug('Creating preview window')
@@ -298,6 +302,9 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog)
QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog)
QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject)
+ QtWidgets.QShortcut("Ctrl+Z", self.window, self.undoStack.undo)
+ QtWidgets.QShortcut("Ctrl+Y", self.window, self.undoStack.redo)
+ QtWidgets.QShortcut("Ctrl+Shift+Z", self.window, self.undoStack.redo)
# Hotkeys for component list
for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert):
@@ -685,15 +692,10 @@ class MainWindow(QtWidgets.QMainWindow):
def removeComponent(self):
componentList = self.window.listWidget_componentList
-
- for selected in componentList.selectedItems():
- index = componentList.row(selected)
- self.window.stackedWidget.removeWidget(self.pages[index])
- componentList.takeItem(index)
- self.core.removeComponent(index)
- self.pages.pop(index)
- self.changeComponentWidget()
- self.drawPreview()
+ selected = componentList.selectedItems()
+ if selected:
+ action = RemoveComponent(self, selected)
+ self.undoStack.push(action)
@disableWhenEncoding
def moveComponent(self, change):
diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py
index b1eeb34..1cc0887 100644
--- a/src/gui/presetmanager.py
+++ b/src/gui/presetmanager.py
@@ -302,7 +302,7 @@ class PresetManager(QtWidgets.QDialog):
self.findPresets()
self.drawPresetList()
for i, comp in enumerate(self.core.selectedComponents):
- if getPresetDir(comp) == path \
+ if self.core.getPresetDir(comp) == path \
and comp.currentPreset == oldName:
self.core.openPreset(newPath, i, newName)
self.parent.updateComponentTitle(i, False)
@@ -351,8 +351,3 @@ 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))
diff --git a/src/main.py b/src/main.py
index 3a6fbe7..c1278da 100644
--- a/src/main.py
+++ b/src/main.py
@@ -35,11 +35,11 @@ def main():
log.debug("Finished creating command object")
elif mode == 'GUI':
- from mainwindow import MainWindow
+ from gui.mainwindow import MainWindow
import atexit
import signal
- window = uic.loadUi(os.path.join(wd, "mainwindow.ui"))
+ window = uic.loadUi(os.path.join(wd, "gui", "mainwindow.ui"))
# window.adjustSize()
desc = QtWidgets.QDesktopWidget()
dpi = desc.physicalDpiX()
--
cgit v1.2.3
From 8411857030d92e448d5c64682f396e677161afbe Mon Sep 17 00:00:00 2001
From: tassaron
Date: Mon, 28 Aug 2017 18:54:54 -0400
Subject: ctrl-c ends commandline mode properly
---
setup.py | 2 +-
src/command.py | 9 +++++++++
src/components/spectrum.py | 3 ++-
src/core.py | 10 ++++------
src/toolkit/frame.py | 1 +
src/video_thread.py | 11 ++++++++---
6 files changed, 25 insertions(+), 11 deletions(-)
(limited to 'src/command.py')
diff --git a/setup.py b/setup.py
index dd546e2..cdf4c4a 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup
import os
-__version__ = '2.0.0.rc4'
+__version__ = '2.0.0rc5'
def package_files(directory):
diff --git a/src/command.py b/src/command.py
index 4116c5a..cd3c6c3 100644
--- a/src/command.py
+++ b/src/command.py
@@ -8,6 +8,7 @@ import argparse
import os
import sys
import time
+import signal
from core import Core
@@ -91,6 +92,9 @@ class Command(QtCore.QObject):
for arg in args:
self.core.selectedComponents[i].command(arg)
+ # ctrl-c stops the export thread
+ signal.signal(signal.SIGINT, self.stopVideo)
+
if self.args.export and self.args.projpath:
errcode, data = self.core.parseAvFile(projPath)
for key, value in data['WindowFields']:
@@ -124,6 +128,11 @@ class Command(QtCore.QObject):
self.worker.progressBarSetText.connect(self.progressBarSetText)
self.createVideo.emit()
+ def stopVideo(self, *args):
+ self.worker.error = True
+ self.worker.cancelExport()
+ self.worker.cancel()
+
@QtCore.pyqtSlot(str)
def progressBarSetText(self, value):
if 'Export ' in value:
diff --git a/src/components/spectrum.py b/src/components/spectrum.py
index 77cb086..6675f5b 100644
--- a/src/components/spectrum.py
+++ b/src/components/spectrum.py
@@ -98,7 +98,8 @@ class Component(Component):
def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs)
- self.previewPipe.wait()
+ if self.previewPipe is not None:
+ self.previewPipe.wait()
self.updateChunksize()
w, h = scale(self.scale, self.width, self.height, str)
self.video = FfmpegVideo(
diff --git a/src/core.py b/src/core.py
index 1a90296..d7445c9 100644
--- a/src/core.py
+++ b/src/core.py
@@ -13,8 +13,8 @@ import toolkit
log = logging.getLogger('AVP.Core')
-STDOUT_LOGLVL = logging.INFO
-FILE_LOGLVL = logging.VERBOSE
+STDOUT_LOGLVL = logging.WARNING
+FILE_LOGLVL = None
class Core:
@@ -77,8 +77,7 @@ class Core:
if compPos < 0 or compPos > len(self.selectedComponents):
compPos = len(self.selectedComponents)
if len(self.selectedComponents) > 50:
- return None
-
+ return -1
if type(component) is int:
# create component using module index in self.modules
moduleIndex = int(component)
@@ -188,7 +187,6 @@ class Core:
for key, value in data['Settings']:
Core.settings.setValue(key, value)
-
for tup in data['Components']:
name, vers, preset = tup
clearThis = False
@@ -213,7 +211,7 @@ class Core:
self.moduleIndexFor(name),
loader
)
- if i is None:
+ if i == -1:
loader.showMessage(msg="Too many components!")
break
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index aefb55f..0e200b5 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -32,6 +32,7 @@ class FramePainter(QtGui.QPainter):
super().setPen(penStyle)
def finalize(self):
+ log.verbose("Finalizing FramePainter")
imBytes = self.image.bits().asstring(self.image.byteCount())
frame = Image.frombytes(
'RGBA', (self.image.width(), self.image.height()), imBytes
diff --git a/src/video_thread.py b/src/video_thread.py
index 823ac73..91ebe93 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -252,9 +252,14 @@ class Worker(QtCore.QObject):
print('############################')
log.info('Opening pipe to ffmpeg')
log.info(cmd)
- self.out_pipe = openPipe(
- ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
- )
+ try:
+ self.out_pipe = openPipe(
+ ffmpegCommand,
+ stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
+ )
+ except sp.CalledProcessError:
+ log.critical('Ffmpeg pipe couldn\'t be created!')
+ raise
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# START CREATING THE VIDEO
--
cgit v1.2.3
From a42ea1cd69fcf3f6c1b2ff79871cd00f24b95118 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Fri, 22 Apr 2022 17:10:35 -0400
Subject: add commandline option for tests. add first tests
---
.gitignore | 10 ++++++----
src/command.py | 39 ++++++++++++++++++++++++++++++++-------
src/tests/__init__.py | 32 ++++++++++++++++++++++++++++++++
src/tests/data/test.jpg | Bin 0 -> 48766 bytes
src/tests/data/test.ogg | Bin 0 -> 30043 bytes
src/tests/data/test.png | Bin 0 -> 220 bytes
src/tests/test_core_init.py | 19 +++++++++++++++++++
src/tests/test_export_classic.py | 5 +++++
8 files changed, 94 insertions(+), 11 deletions(-)
create mode 100644 src/tests/__init__.py
create mode 100644 src/tests/data/test.jpg
create mode 100644 src/tests/data/test.ogg
create mode 100644 src/tests/data/test.png
create mode 100644 src/tests/test_core_init.py
create mode 100644 src/tests/test_export_classic.py
(limited to 'src/command.py')
diff --git a/.gitignore b/.gitignore
index 380168f..1595776 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,11 @@
__pycache__
*.py[cod]
-build/*
-dist/*
-env/*
-.vscode/*
+*.egg-info
+.pytest_cache
+build/
+dist/
+env/
+.vscode/
*.mkv
*.mp4
*.wav
diff --git a/src/command.py b/src/command.py
index cd3c6c3..49026c6 100644
--- a/src/command.py
+++ b/src/command.py
@@ -9,21 +9,33 @@ import os
import sys
import time
import signal
+import logging
-from core import Core
+from . import core
+
+
+log = logging.getLogger('AVP.Commandline')
class Command(QtCore.QObject):
+ """
+ This replaces the GUI MainWindow when in commandline mode.
+ """
createVideo = QtCore.pyqtSignal()
def __init__(self):
QtCore.QObject.__init__(self)
- self.core = Core()
- Core.mode = 'commandline'
+ self.core = core.Core()
+ core.Core.mode = 'commandline'
self.dataDir = self.core.dataDir
self.canceled = False
+ self.settings = core.Core.settings
+
+ # ctrl-c stops the export thread
+ signal.signal(signal.SIGINT, self.stopVideo)
+ def parseArgs(self):
self.parser = argparse.ArgumentParser(
description='Create a visualization for an audio file',
epilog='EXAMPLE COMMAND: main.py myvideotemplate.avp '
@@ -31,6 +43,10 @@ class Command(QtCore.QObject):
'-c 0 image path=~/Pictures/thisWeeksPicture.jpg '
'-c 1 video "preset=My Logo" -c 2 vis layout=classic'
)
+ self.parser.add_argument(
+ '-t', '--test', action='store_true',
+ help='run tests and generate a logfile to report a bug'
+ )
self.parser.add_argument(
'-i', '--input', metavar='SOUND',
help='input audio file'
@@ -55,7 +71,10 @@ class Command(QtCore.QObject):
nargs='*', action='append')
self.args = self.parser.parse_args()
- self.settings = Core.settings
+
+ if self.args.test:
+ self.runTests()
+ quit(0)
if self.args.projpath:
projPath = self.args.projpath
@@ -92,9 +111,6 @@ class Command(QtCore.QObject):
for arg in args:
self.core.selectedComponents[i].command(arg)
- # ctrl-c stops the export thread
- signal.signal(signal.SIGINT, self.stopVideo)
-
if self.args.export and self.args.projpath:
errcode, data = self.core.parseAvFile(projPath)
for key, value in data['WindowFields']:
@@ -188,3 +204,12 @@ class Command(QtCore.QObject):
return
return None
+
+ def runTests(self):
+ core.FILE_LOGLVL = logging.DEBUG
+ from . import tests
+ test_report = os.path.join(core.Core.logDir, "test_report.log")
+ tests.run(test_report)
+ with open(test_report, "r") as f:
+ output = f.readlines()
+ print("".join(output))
diff --git a/src/tests/__init__.py b/src/tests/__init__.py
new file mode 100644
index 0000000..f2b2ff1
--- /dev/null
+++ b/src/tests/__init__.py
@@ -0,0 +1,32 @@
+import pytest
+import os
+import sys
+from ..core import Core
+from ..command import Command
+
+
+@pytest.fixture
+def core():
+ return Core()
+
+
+@pytest.fixture
+def command():
+ """Like a MainWindow for commandline mode, this owns the Core"""
+ return Command()
+
+
+def run(logFile):
+ """Run Pytest, which then imports and runs all tests in this module."""
+ 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.
+ ])
+ finally:
+ sys.stdout = sys.__stdout__
+
+ return val
diff --git a/src/tests/data/test.jpg b/src/tests/data/test.jpg
new file mode 100644
index 0000000..86266d9
Binary files /dev/null and b/src/tests/data/test.jpg differ
diff --git a/src/tests/data/test.ogg b/src/tests/data/test.ogg
new file mode 100644
index 0000000..46af76c
Binary files /dev/null and b/src/tests/data/test.ogg differ
diff --git a/src/tests/data/test.png b/src/tests/data/test.png
new file mode 100644
index 0000000..f1ffd4a
Binary files /dev/null and b/src/tests/data/test.png differ
diff --git a/src/tests/test_core_init.py b/src/tests/test_core_init.py
new file mode 100644
index 0000000..696533a
--- /dev/null
+++ b/src/tests/test_core_init.py
@@ -0,0 +1,19 @@
+from .__init__ import core
+
+
+def test_component_names(core):
+ assert core.compNames == [
+ 'Classic Visualizer',
+ 'Color',
+ "Conway's Game of Life",
+ 'Image',
+ 'Sound',
+ 'Spectrum',
+ 'Title Text',
+ 'Video',
+ 'Waveform',
+ ]
+
+
+def test_moduleindex(core):
+ assert core.moduleIndexFor("Classic Visualizer") == 0
diff --git a/src/tests/test_export_classic.py b/src/tests/test_export_classic.py
new file mode 100644
index 0000000..a6d3e8c
--- /dev/null
+++ b/src/tests/test_export_classic.py
@@ -0,0 +1,5 @@
+from .__init__ import command
+
+
+def test_export_classic_visualizer_default(command):
+ assert command
--
cgit v1.2.3
From 0d59d29945d1538d4de073d92aa6a287d7907829 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Mon, 25 Apr 2022 13:51:08 -0400
Subject: add --debug option and rename -e so it's more explicit
---
src/command.py | 25 ++++++++++++++++++-------
1 file changed, 18 insertions(+), 7 deletions(-)
(limited to 'src/command.py')
diff --git a/src/command.py b/src/command.py
index 49026c6..0aab0f7 100644
--- a/src/command.py
+++ b/src/command.py
@@ -43,10 +43,6 @@ class Command(QtCore.QObject):
'-c 0 image path=~/Pictures/thisWeeksPicture.jpg '
'-c 1 video "preset=My Logo" -c 2 vis layout=classic'
)
- self.parser.add_argument(
- '-t', '--test', action='store_true',
- help='run tests and generate a logfile to report a bug'
- )
self.parser.add_argument(
'-i', '--input', metavar='SOUND',
help='input audio file'
@@ -56,8 +52,16 @@ class Command(QtCore.QObject):
help='output video file'
)
self.parser.add_argument(
- '-e', '--export', action='store_true',
- help='use input and output files from project file'
+ '--export-project', action='store_true',
+ help='ignore -i and -o, use input and output from project file'
+ )
+ self.parser.add_argument(
+ '--test', action='store_true',
+ help='run tests, generate logfiles, then exit'
+ )
+ self.parser.add_argument(
+ '--debug', action='store_true',
+ help='create bigger logfiles while program is running'
)
# optional arguments
@@ -72,6 +76,11 @@ class Command(QtCore.QObject):
self.args = self.parser.parse_args()
+ if self.args.debug:
+ core.FILE_LOGLVL = logging.DEBUG
+ core.STDOUT_LOGLVL = logging.DEBUG
+ core.Core.makeLogger()
+
if self.args.test:
self.runTests()
quit(0)
@@ -111,7 +120,7 @@ class Command(QtCore.QObject):
for arg in args:
self.core.selectedComponents[i].command(arg)
- if self.args.export and self.args.projpath:
+ if self.args.export_project and self.args.projpath:
errcode, data = self.core.parseAvFile(projPath)
for key, value in data['WindowFields']:
if 'outputFile' in key:
@@ -139,6 +148,7 @@ class Command(QtCore.QObject):
self.worker = self.core.newVideoWorker(
self, input, output
)
+ # quit(0) after video is created
self.worker.videoCreated.connect(self.videoCreated)
self.lastProgressUpdate = time.time()
self.worker.progressBarSetText.connect(self.progressBarSetText)
@@ -207,6 +217,7 @@ class Command(QtCore.QObject):
def runTests(self):
core.FILE_LOGLVL = logging.DEBUG
+ core.Core.makeLogger()
from . import tests
test_report = os.path.join(core.Core.logDir, "test_report.log")
tests.run(test_report)
--
cgit v1.2.3
From 17b4cba6d1a5f24b4de3b53f79b93dd409e28ccd Mon Sep 17 00:00:00 2001
From: tassaron
Date: Tue, 26 Apr 2022 13:10:29 -0400
Subject: tests for commandline argument parsing
---
src/command.py | 6 +++++-
src/main.py | 12 +++++------
src/tests/__init__.py | 4 ++++
src/tests/test_commandline_parser.py | 39 ++++++++++++++++++++++++++++++++++++
src/tests/test_export_classic.py | 5 -----
5 files changed, 54 insertions(+), 12 deletions(-)
create mode 100644 src/tests/test_commandline_parser.py
delete mode 100644 src/tests/test_export_classic.py
(limited to 'src/command.py')
diff --git a/src/command.py b/src/command.py
index 0aab0f7..db72de7 100644
--- a/src/command.py
+++ b/src/command.py
@@ -133,14 +133,18 @@ class Command(QtCore.QObject):
if 'audioFile' in key:
input = value
self.createAudioVisualisation(input, output)
+ return "commandline"
elif self.args.input and self.args.output:
self.createAudioVisualisation(self.args.input, self.args.output)
+ return "commandline"
- elif 'help' not in sys.argv:
+ elif 'help' not in sys.argv and self.args.projpath is None and '--debug' not in sys.argv:
self.parser.print_help()
quit(1)
+ return "GUI"
+
def createAudioVisualisation(self, input, output):
self.core.selectedComponents = list(
reversed(self.core.selectedComponents))
diff --git a/src/main.py b/src/main.py
index 5fabda3..39fa997 100644
--- a/src/main.py
+++ b/src/main.py
@@ -30,25 +30,25 @@ def main():
from .command import Command
main = Command()
- main.parseArgs()
+ mode = main.parseArgs()
log.debug("Finished creating command object")
- elif mode == 'GUI':
+ # Both branches here may occur in one execution:
+ # Commandline parsing could change mode back to GUI
+ if mode == 'GUI':
from .gui.mainwindow import MainWindow
window = uic.loadUi(os.path.join(wd, "gui", "mainwindow.ui"))
- # window.adjustSize()
desc = QtWidgets.QDesktopWidget()
dpi = desc.physicalDpiX()
-
- topMargin = 0 if (dpi == 96) else int(10 * (dpi / 96))
+ log.info("Detected screen DPI: %s", dpi)
+
window.resize(
int(window.width() *
(dpi / 96)),
int(window.height() *
(dpi / 96))
)
- # window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0)
main = MainWindow(window, proj)
log.debug("Finished creating main window")
diff --git a/src/tests/__init__.py b/src/tests/__init__.py
index f2b2ff1..062dca7 100644
--- a/src/tests/__init__.py
+++ b/src/tests/__init__.py
@@ -16,6 +16,10 @@ def command():
return Command()
+def getTestData(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."""
with open(logFile, "w") as f:
diff --git a/src/tests/test_commandline_parser.py b/src/tests/test_commandline_parser.py
new file mode 100644
index 0000000..d672441
--- /dev/null
+++ b/src/tests/test_commandline_parser.py
@@ -0,0 +1,39 @@
+import sys
+import pytest
+from .__init__ import command
+
+
+def test_commandline_help(command):
+ sys.argv = ['', '--help']
+ with pytest.raises(SystemExit):
+ command.parseArgs()
+
+
+def test_commandline_help_if_bad_args(command):
+ sys.argv = ['', '--junk']
+ with pytest.raises(SystemExit):
+ command.parseArgs()
+
+
+def test_commandline_launches_gui_if_debug(command):
+ sys.argv = ['', '--debug']
+ mode = command.parseArgs()
+ assert mode == "GUI"
+
+
+def test_commandline_launches_gui_if_debug_with_project(command):
+ sys.argv = ['', 'test', '--debug']
+ mode = command.parseArgs()
+ assert mode == "GUI"
+
+
+def test_commandline_export_creates_audio_visualization(command):
+ didCallFunction = False
+ def captureFunction(*args):
+ nonlocal didCallFunction
+ didCallFunction = True
+
+ sys.argv = ['', '-c', '0', 'classic', '-i', '_', '-o', '_']
+ command.createAudioVisualisation = captureFunction
+ command.parseArgs()
+ assert didCallFunction
diff --git a/src/tests/test_export_classic.py b/src/tests/test_export_classic.py
deleted file mode 100644
index a6d3e8c..0000000
--- a/src/tests/test_export_classic.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from .__init__ import command
-
-
-def test_export_classic_visualizer_default(command):
- assert command
--
cgit v1.2.3
From 4e6159725227de952f0c578595423aa51d9a0b34 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 28 Apr 2022 19:26:09 -0400
Subject: change call to superclass __init__ to super()
---
src/command.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'src/command.py')
diff --git a/src/command.py b/src/command.py
index db72de7..267117e 100644
--- a/src/command.py
+++ b/src/command.py
@@ -25,7 +25,7 @@ class Command(QtCore.QObject):
createVideo = QtCore.pyqtSignal()
def __init__(self):
- QtCore.QObject.__init__(self)
+ super()
self.core = core.Core()
core.Core.mode = 'commandline'
self.dataDir = self.core.dataDir
--
cgit v1.2.3
From 069edd9086ad7a99c78c5637af23d50a633396cf Mon Sep 17 00:00:00 2001
From: tassaron
Date: Fri, 29 Apr 2022 12:58:26 -0400
Subject: use super().__init__ in the modern python3 style
---
src/command.py | 2 +-
src/gui/preview_thread.py | 4 ++--
src/gui/preview_win.py | 2 +-
src/video_thread.py | 4 ++--
4 files changed, 6 insertions(+), 6 deletions(-)
(limited to 'src/command.py')
diff --git a/src/command.py b/src/command.py
index 267117e..cc13684 100644
--- a/src/command.py
+++ b/src/command.py
@@ -25,7 +25,7 @@ class Command(QtCore.QObject):
createVideo = QtCore.pyqtSignal()
def __init__(self):
- super()
+ super().__init__()
self.core = core.Core()
core.Core.mode = 'commandline'
self.dataDir = self.core.dataDir
diff --git a/src/gui/preview_thread.py b/src/gui/preview_thread.py
index 7829476..614b584 100644
--- a/src/gui/preview_thread.py
+++ b/src/gui/preview_thread.py
@@ -23,10 +23,10 @@ class Worker(QtCore.QObject):
error = pyqtSignal(str)
def __init__(self, parent=None, queue=None):
- QtCore.QObject.__init__(self)
+ super().__init__()
parent.newTask.connect(self.createPreviewImage)
parent.processTask.connect(self.process)
- self.parent = parent
+ #self.parent = parent
self.core = parent.core
self.settings = parent.settings
self.queue = queue
diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py
index 27e0a59..426ff66 100644
--- a/src/gui/preview_win.py
+++ b/src/gui/preview_win.py
@@ -10,7 +10,7 @@ class PreviewWindow(QtWidgets.QLabel):
when the window is resized.
'''
def __init__(self, parent, img):
- super(PreviewWindow, self).__init__()
+ super().__init__()
self.parent = parent
self.setFrameStyle(QtWidgets.QFrame.StyledPanel)
self.pixmap = QtGui.QPixmap(img)
diff --git a/src/video_thread.py b/src/video_thread.py
index 2fe264a..4a28261 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -39,13 +39,13 @@ class Worker(QtCore.QObject):
encoding = pyqtSignal(bool)
def __init__(self, parent, inputFile, outputFile, components):
- QtCore.QObject.__init__(self)
+ super().__init__()
self.core = parent.core
self.settings = parent.settings
self.modules = parent.core.modules
parent.createVideo.connect(self.createVideo)
- self.parent = parent
+ #self.parent = parent
self.components = components
self.outputFile = outputFile
self.inputFile = inputFile
--
cgit v1.2.3
From 597da503cadbb1008e0cf15c37f570ad4d27c105 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sat, 30 Apr 2022 22:53:42 -0400
Subject: create test report in home folder after `--test`
---
src/command.py | 35 ++++++++++++++++++++++++++++++++++-
1 file changed, 34 insertions(+), 1 deletion(-)
(limited to 'src/command.py')
diff --git a/src/command.py b/src/command.py
index cc13684..bf7941a 100644
--- a/src/command.py
+++ b/src/command.py
@@ -9,6 +9,7 @@ import os
import sys
import time
import signal
+import shutil
import logging
from . import core
@@ -225,6 +226,38 @@ class Command(QtCore.QObject):
from . import tests
test_report = os.path.join(core.Core.logDir, "test_report.log")
tests.run(test_report)
+
+ # Print test report into terminal
with open(test_report, "r") as f:
output = f.readlines()
- print("".join(output))
+ test_output = "".join(output)
+ print(test_output)
+
+ # 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")
+ while True:
+ possibleName = f"{name}{logNumber:0>2}.txt"
+ if os.path.exists(possibleName) and logNumber < 100:
+ logNumber += 1
+ continue
+ break
+ return possibleName
+
+ # Copy latest debug log to chosen test report location
+ filename = getFilename()
+ if logNumber == 100:
+ print("Test Report could not be created.")
+ return
+ try:
+ shutil.copy(os.path.join(core.Core.logDir, "avp_debug.log"), filename)
+ except FileNotFoundError:
+ print("No debug log found.")
+ # Append actual test report to debug log
+ with open(filename, "a") as f:
+ f.write(f"{'='*59} debug log ends {'='*59}\n")
+ f.write(test_output)
+ print(f"Test Report created at {filename}")
--
cgit v1.2.3