aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/background.pngbin0 -> 45367 bytes
-rw-r--r--src/command.py126
-rw-r--r--src/components/__base__.py153
-rw-r--r--src/components/__init__.py1
-rw-r--r--src/components/color.py246
-rw-r--r--src/components/color.ui660
-rw-r--r--src/components/image.py111
-rw-r--r--src/components/image.ui259
-rw-r--r--src/components/original.py204
-rw-r--r--src/components/original.ui108
-rw-r--r--src/components/text.py176
-rw-r--r--src/components/text.ui316
-rw-r--r--src/components/video.py273
-rw-r--r--src/components/video.ui266
-rw-r--r--src/core.py477
-rw-r--r--src/encoder-options.json130
-rw-r--r--src/main.py88
-rw-r--r--src/mainwindow.py718
-rw-r--r--src/mainwindow.ui809
-rw-r--r--src/presetmanager.py290
-rw-r--r--src/presetmanager.ui150
-rw-r--r--src/preview_thread.py59
-rw-r--r--src/video_thread.py309
23 files changed, 5929 insertions, 0 deletions
diff --git a/src/background.png b/src/background.png
new file mode 100644
index 0000000..fb58593
--- /dev/null
+++ b/src/background.png
Binary files differ
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
diff --git a/src/components/__base__.py b/src/components/__base__.py
new file mode 100644
index 0000000..a4677b1
--- /dev/null
+++ b/src/components/__base__.py
@@ -0,0 +1,153 @@
+from PyQt5 import QtGui, QtCore, QtWidgets
+from PIL import Image
+import os
+
+
+class Component(QtCore.QObject):
+ '''A base class for components to inherit from'''
+
+ # modified = QtCore.pyqtSignal(int, bool)
+
+ def __init__(self, moduleIndex, compPos, core):
+ super().__init__()
+ self.currentPreset = None
+ self.moduleIndex = moduleIndex
+ self.compPos = compPos
+ self.core = core
+
+ def __str__(self):
+ return self.__doc__
+
+ def version(self):
+ # change this number to identify new versions of a component
+ return 1
+
+ def cancel(self):
+ # please stop any lengthy process in response to this variable
+ self.canceled = True
+
+ def reset(self):
+ self.canceled = False
+
+ def update(self):
+ self.modified.emit(self.compPos, self.savePreset())
+ # read your widget values, then call super().update()
+
+ def loadPreset(self, presetDict, presetName):
+ '''Subclasses take (presetDict, presetName=None) as args.
+ Must use super().loadPreset(presetDict, presetName) first,
+ then update self.page widgets using the preset dict.
+ '''
+ self.currentPreset = presetName \
+ if presetName != None else presetDict['preset']
+
+ def preFrameRender(self, **kwargs):
+ '''Triggered only before a video is exported (video_thread.py)
+ self.worker = the video thread worker
+ self.completeAudioArray = a list of audio samples
+ self.sampleSize = number of audio samples per video frame
+ self.progressBarUpdate = signal to set progress bar number
+ self.progressBarSetText = signal to set progress bar text
+ Use the latter two signals to update the MainProgram if needed
+ for a long initialization procedure (i.e., for a visualizer)
+ '''
+ for var, value in kwargs.items():
+ exec('self.%s = value' % var)
+
+ 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
+ '''
+ if arg.startswith('preset='):
+ _, preset = arg.split('=', 1)
+ path = os.path.join(self.core.getPresetDir(self), preset)
+ if not os.path.exists(path):
+ print('Couldn\'t locate preset "%s"' % preset)
+ quit(1)
+ else:
+ print('Opening "%s" preset on layer %s' % \
+ (preset, self.compPos))
+ self.core.openPreset(path, self.compPos, preset)
+ else:
+ print(
+ self.__doc__, 'Usage:\n'
+ 'Open a preset for this component:\n'
+ ' "preset=Preset Name"')
+ self.commandHelp()
+ quit(0)
+
+ def commandHelp(self):
+ '''Print help text for this Component's commandline arguments'''
+
+ def blankFrame(self, width, height):
+ return Image.new("RGBA", (width, height), (0, 0, 0, 0))
+
+ def pickColor(self):
+ '''Use color picker to get color input from the user,
+ 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)
+ color = dialog.getColor()
+ if color.isValid():
+ RGBstring = '%s,%s,%s' % (
+ str(color.red()), str(color.green()), str(color.blue()))
+ btnStyle = "QPushButton{background-color: %s; outline: none;}" \
+ % color.name()
+ return RGBstring, btnStyle
+ else:
+ return None, None
+
+ def RGBFromString(self, string):
+ ''' Turns an RGB string like "255, 255, 255" into a tuple '''
+ try:
+ tup = tuple([int(i) for i in string.split(',')])
+ if len(tup) != 3:
+ raise ValueError
+ for i in tup:
+ if i > 255 or i < 0:
+ raise ValueError
+ return tup
+ except:
+ return (255, 255, 255)
+
+ '''
+ ### Reference methods for creating a new component
+ ### (Inherit from this class and define these)
+
+ def widget(self, parent):
+ self.parent = parent
+ page = uic.loadUi(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), 'example.ui'))
+ # --- connect widget signals here ---
+ self.page = page
+ return page
+
+ def update(self):
+ super().update()
+ self.parent.drawPreview()
+
+ def previewRender(self, previewWorker):
+ width = int(previewWorker.core.settings.value('outputWidth'))
+ height = int(previewWorker.core.settings.value('outputHeight'))
+ image = Image.new("RGBA", (width, height), (0,0,0,0))
+ return image
+
+ def frameRender(self, moduleNo, frameNo):
+ width = int(self.worker.core.settings.value('outputWidth'))
+ height = int(self.worker.core.settings.value('outputHeight'))
+ image = Image.new("RGBA", (width, height), (0,0,0,0))
+ return image
+ '''
+
+class BadComponentInit(Exception):
+ def __init__(self, arg, name):
+ string = \
+'''################################
+Mandatory argument "%s" not specified
+ in %s instance initialization
+###################################'''
+ print(string % (arg, name))
+ quit()
diff --git a/src/components/__init__.py b/src/components/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/components/__init__.py
@@ -0,0 +1 @@
+
diff --git a/src/components/color.py b/src/components/color.py
new file mode 100644
index 0000000..8f9a1d1
--- /dev/null
+++ b/src/components/color.py
@@ -0,0 +1,246 @@
+from PIL import Image, ImageDraw
+from PyQt5 import uic, QtGui, QtCore
+from PyQt5.QtGui import QColor
+from PIL.ImageQt import ImageQt
+import os
+from . import __base__
+
+
+class Component(__base__.Component):
+ '''Color'''
+
+ modified = QtCore.pyqtSignal(int, dict)
+
+ def widget(self, parent):
+ self.parent = parent
+ page = uic.loadUi(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), 'color.ui'))
+
+ self.color1 = (0, 0, 0)
+ self.color2 = (133, 133, 133)
+ self.x = 0
+ self.y = 0
+
+ page.lineEdit_color1.setText('%s,%s,%s' % self.color1)
+ page.lineEdit_color2.setText('%s,%s,%s' % self.color2)
+
+ btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \
+ % QColor(*self.color1).name()
+
+ 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))
+
+ # 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(
+ int(parent.settings.value("outputWidth")))
+ page.spinBox_height.setValue(
+ int(parent.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
+
+ def update(self):
+ self.color1 = self.RGBFromString(self.page.lineEdit_color1.text())
+ self.color2 = self.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:
+ self.page.lineEdit_color2.setEnabled(False)
+ self.page.pushButton_color2.setEnabled(False)
+ self.page.checkBox_trans.setEnabled(False)
+ self.page.checkBox_stretch.setEnabled(False)
+ self.page.comboBox_spread.setEnabled(False)
+ else:
+ self.page.lineEdit_color2.setEnabled(True)
+ self.page.pushButton_color2.setEnabled(True)
+ self.page.checkBox_trans.setEnabled(True)
+ self.page.checkBox_stretch.setEnabled(True)
+ self.page.comboBox_spread.setEnabled(True)
+ self.page.fillWidget.setCurrentIndex(self.fillType)
+
+ self.parent.drawPreview()
+ super().update()
+
+ def previewRender(self, previewWorker):
+ width = int(previewWorker.core.settings.value('outputWidth'))
+ height = int(previewWorker.core.settings.value('outputHeight'))
+ return self.drawFrame(width, height)
+
+ def preFrameRender(self, **kwargs):
+ super().preFrameRender(**kwargs)
+ return ['static']
+
+ def frameRender(self, moduleNo, arrayNo, frameNo):
+ width = int(self.worker.core.settings.value('outputWidth'))
+ height = int(self.worker.core.settings.value('outputHeight'))
+ return self.drawFrame(width, height)
+
+ def drawFrame(self, width, height):
+ 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:
+ return Image.new("RGBA", (width, height), (r, g, b, 255))
+
+ frame = self.blankFrame(width, height)
+
+ # Return a solid image at x, y
+ if self.fillType == 0:
+ image = Image.new("RGBA", shapeSize, (r, g, b, 255))
+ frame.paste(image, box=(self.x, self.y))
+ return frame
+
+ # Now fills that require using Qt...
+ elif self.fillType > 0:
+ image = ImageQt(frame)
+ painter = QtGui.QPainter(image)
+ if self.stretch:
+ w = width; h = height
+ else:
+ w = self.sizeWidth; h = self.sizeWidth
+
+ if self.fillType == 1: # Linear Gradient
+ brush = QtGui.QLinearGradient(
+ self.LG_start,
+ self.LG_start,
+ self.LG_start+width/3,
+ self.LG_end)
+
+ elif self.fillType == 2: # Radial Gradient
+ brush = QtGui.QRadialGradient(
+ self.RG_start,
+ self.RG_end,
+ w, h,
+ self.RG_centre)
+
+ brush.setSpread(self.spread)
+ brush.setColorAt(0.0, QColor(*self.color1))
+ if self.trans:
+ brush.setColorAt(1.0, QColor(0, 0, 0, 0))
+ elif self.fillType == 1 and self.stretch:
+ brush.setColorAt(0.2, QColor(*self.color2))
+ else:
+ brush.setColorAt(1.0, QColor(*self.color2))
+ painter.setBrush(brush)
+ painter.drawRect(self.x, self.y,
+ self.sizeWidth, self.sizeHeight)
+ painter.end()
+ imBytes = image.bits().asstring(image.numBytes())
+ return Image.frombytes('RGBA', (width, height), imBytes)
+
+ def loadPreset(self, pr, presetName=None):
+ super().loadPreset(pr, presetName)
+
+ 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()
+ btnStyle2 = "QPushButton { background-color : %s; outline: none; }" \
+ % QColor(*pr['color2']).name()
+ self.page.pushButton_color1.setStyleSheet(btnStyle1)
+ self.page.pushButton_color2.setStyleSheet(btnStyle2)
+
+ def savePreset(self):
+ return {
+ 'preset': self.currentPreset,
+ '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,
+ }
+
+ def pickColor(self, num):
+ RGBstring, btnStyle = super().pickColor()
+ if not RGBstring:
+ return
+ if num == 1:
+ self.page.lineEdit_color1.setText(RGBstring)
+ self.page.pushButton_color1.setStyleSheet(btnStyle)
+ else:
+ self.page.lineEdit_color2.setText(RGBstring)
+ self.page.pushButton_color2.setStyleSheet(btnStyle)
+
+ def commandHelp(self):
+ print('Specify a color:\n color=255,255,255')
+
+ def command(self, arg):
+ if not arg.startswith('preset=') and '=' in arg:
+ key, arg = arg.split('=', 1)
+ if key == 'color':
+ self.page.lineEdit_color1.setText(arg)
+ return
+ super().command(arg)
diff --git a/src/components/color.ui b/src/components/color.ui
new file mode 100644
index 0000000..a9dacea
--- /dev/null
+++ b/src/components/color.ui
@@ -0,0 +1,660 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>586</width>
+ <height>197</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <item>
+ <widget class="QLabel" name="label_textColor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>31</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Color #1</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_color1">
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="MaximumSize" stdset="0">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_color1">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>1</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maxLength">
+ <number>12</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_9">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_textColor_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>31</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Color #2</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_color2">
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="MaximumSize" stdset="0">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_color2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>1</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maxLength">
+ <number>12</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_7">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_xTitleAlign_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Width</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_width">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>0</number>
+ </property>
+ <property name="maximum">
+ <number>999999999</number>
+ </property>
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_yTitleAlign_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Height</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_height">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="maximum">
+ <number>999999999</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_7">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_xTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>X</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_x">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_yTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Y</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_y">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_9">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_textLayout">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Fill </string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_fill">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="currentIndex">
+ <number>-1</number>
+ </property>
+ <property name="sizeAdjustPolicy">
+ <enum>QComboBox::AdjustToContentsOnFirstShow</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_trans">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Transparent</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_stretch">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Stretch</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_spread">
+ <item>
+ <property name="text">
+ <string>Pad</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Reflect</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Repeat</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Minimum</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QStackedWidget" name="fillWidget">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="lineWidth">
+ <number>0</number>
+ </property>
+ <property name="currentIndex">
+ <number>2</number>
+ </property>
+ <widget class="QWidget" name="blank"/>
+ <widget class="QWidget" name="linearGradient">
+ <widget class="QWidget" name="horizontalLayoutWidget">
+ <property name="geometry">
+ <rect>
+ <x>-1</x>
+ <y>0</y>
+ <width>561</width>
+ <height>31</height>
+ </rect>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLabel" name="label_xTitleAlign_4">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Start</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_linearGradient_start">
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="singleStep">
+ <number>10</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>End</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_linearGradient_end">
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="singleStep">
+ <number>10</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <widget class="QWidget" name="radialGradient">
+ <widget class="QWidget" name="horizontalLayoutWidget_3">
+ <property name="geometry">
+ <rect>
+ <x>-1</x>
+ <y>-1</y>
+ <width>561</width>
+ <height>31</height>
+ </rect>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QLabel" name="label_xTitleAlign_6">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Start</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_radialGradient_start">
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="singleStep">
+ <number>10</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_3">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>End</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_radialGradient_end">
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="singleStep">
+ <number>10</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_4">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Centre</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_radialGradient_spread">
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::PlusMinus</enum>
+ </property>
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="value">
+ <number>3</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/components/image.py b/src/components/image.py
new file mode 100644
index 0000000..8ca88d3
--- /dev/null
+++ b/src/components/image.py
@@ -0,0 +1,111 @@
+from PIL import Image, ImageDraw
+from PyQt5 import uic, QtGui, QtCore, QtWidgets
+import os
+from . import __base__
+
+
+class Component(__base__.Component):
+ '''Image'''
+
+ modified = QtCore.pyqtSignal(int, dict)
+
+ def widget(self, parent):
+ self.parent = parent
+ self.settings = parent.settings
+ page = uic.loadUi(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), 'image.ui'))
+ self.imagePath = ''
+ self.x = 0
+ self.y = 0
+
+ page.lineEdit_image.textChanged.connect(self.update)
+ page.pushButton_image.clicked.connect(self.pickImage)
+ page.spinBox_scale.valueChanged.connect(self.update)
+ page.checkBox_stretch.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.xPosition = self.page.spinBox_x.value()
+ self.yPosition = self.page.spinBox_y.value()
+ self.stretched = self.page.checkBox_stretch.isChecked()
+ self.parent.drawPreview()
+ super().update()
+
+ def previewRender(self, previewWorker):
+ self.imageFormats = previewWorker.core.imageFormats
+ width = int(previewWorker.core.settings.value('outputWidth'))
+ height = int(previewWorker.core.settings.value('outputHeight'))
+ return self.drawFrame(width, height)
+
+ def preFrameRender(self, **kwargs):
+ super().preFrameRender(**kwargs)
+ return ['static']
+
+ def frameRender(self, moduleNo, arrayNo, frameNo):
+ width = int(self.worker.core.settings.value('outputWidth'))
+ height = int(self.worker.core.settings.value('outputHeight'))
+ return self.drawFrame(width, height)
+
+ def drawFrame(self, width, height):
+ frame = self.blankFrame(width, height)
+ if self.imagePath and os.path.exists(self.imagePath):
+ image = Image.open(self.imagePath)
+ 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)
+ frame.paste(image, box=(self.xPosition, self.yPosition))
+ 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_x.setValue(pr['x'])
+ self.page.spinBox_y.setValue(pr['y'])
+ self.page.checkBox_stretch.setChecked(pr['stretched'])
+
+ def savePreset(self):
+ return {
+ 'preset': self.currentPreset,
+ 'image': self.imagePath,
+ 'scale': self.scale,
+ 'stretched': self.stretched,
+ 'x': self.xPosition,
+ 'y': self.yPosition,
+ }
+
+ def pickImage(self):
+ imgDir = self.settings.value("backgroundDir", os.path.expanduser("~"))
+ filename = QtGui.QFileDialog.getOpenFileName(
+ self.page, "Choose Image", imgDir,
+ "Image Files (%s)" % " ".join(self.imageFormats))
+ if filename:
+ self.settings.setValue("backgroundDir", os.path.dirname(filename))
+ self.page.lineEdit_image.setText(filename)
+ self.update()
+
+ def command(self, arg):
+ if not arg.startswith('preset=') and '=' in arg:
+ key, arg = arg.split('=', 1)
+ if key == 'path' and os.path.exists(arg):
+ try:
+ Image.open(arg)
+ self.page.lineEdit_image.setText(arg)
+ self.page.checkBox_stretch.setChecked(True)
+ return
+ except OSError as e:
+ print("Not a supported image format")
+ quit(1)
+ super().command(arg)
+
+ def commandHelp(self):
+ print('Load an image:\n path=/filepath/to/image.png')
diff --git a/src/components/image.ui b/src/components/image.ui
new file mode 100644
index 0000000..6df03a5
--- /dev/null
+++ b/src/components/image.ui
@@ -0,0 +1,259 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>586</width>
+ <height>197</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <item>
+ <widget class="QLabel" name="label_textColor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>31</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Image</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_image">
+ <property name="minimumSize">
+ <size>
+ <width>1</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_image">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>1</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>...</string>
+ </property>
+ <property name="MaximumSize" stdset="0">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_9">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_xTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>X</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_x">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_yTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Y</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_y">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>-1000</number>
+ </property>
+ <property name="maximum">
+ <number>1000</number>
+ </property>
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_9">
+ <item>
+ <widget class="QCheckBox" name="checkBox_stretch">
+ <property name="text">
+ <string>Stretch</string>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_10">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Scale</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_scale">
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::UpDownArrows</enum>
+ </property>
+ <property name="suffix">
+ <string>%</string>
+ </property>
+ <property name="minimum">
+ <number>10</number>
+ </property>
+ <property name="maximum">
+ <number>400</number>
+ </property>
+ <property name="value">
+ <number>100</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/components/original.py b/src/components/original.py
new file mode 100644
index 0000000..61f463d
--- /dev/null
+++ b/src/components/original.py
@@ -0,0 +1,204 @@
+import numpy
+from PIL import Image, ImageDraw
+from PyQt5 import uic, QtGui, QtCore
+from PyQt5.QtGui import QColor
+import os
+from . import __base__
+import time
+from copy import copy
+
+
+class Component(__base__.Component):
+ '''Original Audio Visualization'''
+
+ modified = QtCore.pyqtSignal(int, dict)
+
+ def widget(self, parent):
+ self.parent = parent
+ self.visColor = (255, 255, 255)
+
+ page = uic.loadUi(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), 'original.ui'))
+ page.comboBox_visLayout.addItem("Classic")
+ page.comboBox_visLayout.addItem("Split")
+ page.comboBox_visLayout.addItem("Bottom")
+ 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())
+ btnStyle = "QPushButton { background-color : %s; outline: none; }" \
+ % QColor(*self.visColor).name()
+ page.pushButton_visColor.setStyleSheet(btnStyle)
+ page.lineEdit_visColor.textChanged.connect(self.update)
+ self.page = page
+ self.canceled = False
+ return page
+
+ def update(self):
+ self.layout = self.page.comboBox_visLayout.currentIndex()
+ self.visColor = self.RGBFromString(self.page.lineEdit_visColor.text())
+ self.parent.drawPreview()
+ super().update()
+
+ def loadPreset(self, pr, presetName=None):
+ super().loadPreset(pr, presetName)
+
+ 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'])
+
+ def savePreset(self):
+ return {
+ 'preset': self.currentPreset,
+ 'layout': self.layout,
+ 'visColor': self.visColor,
+ }
+
+ def previewRender(self, previewWorker):
+ spectrum = numpy.fromfunction(
+ lambda x: 0.008*(x-128)**2, (255,), dtype="int16")
+ width = int(previewWorker.core.settings.value('outputWidth'))
+ height = int(previewWorker.core.settings.value('outputHeight'))
+ return self.drawBars(
+ width, height, spectrum, self.visColor, self.layout)
+
+ def preFrameRender(self, **kwargs):
+ super().preFrameRender(**kwargs)
+ self.smoothConstantDown = 0.08
+ self.smoothConstantUp = 0.8
+ self.lastSpectrum = None
+ self.spectrumArray = {}
+ self.width = int(self.worker.core.settings.value('outputWidth'))
+ self.height = int(self.worker.core.settings.value('outputHeight'))
+
+ for i in range(0, len(self.completeAudioArray), self.sampleSize):
+ if self.canceled:
+ break
+ self.lastSpectrum = self.transformData(
+ i, self.completeAudioArray, self.sampleSize,
+ self.smoothConstantDown, self.smoothConstantUp,
+ self.lastSpectrum)
+ self.spectrumArray[i] = copy(self.lastSpectrum)
+
+ progress = int(100*(i/len(self.completeAudioArray)))
+ if progress >= 100:
+ progress = 100
+ pStr = "Analyzing audio: "+str(progress)+'%'
+ self.progressBarSetText.emit(pStr)
+ self.progressBarUpdate.emit(int(progress))
+
+ def frameRender(self, moduleNo, arrayNo, frameNo):
+ return self.drawBars(
+ self.width, self.height,
+ self.spectrumArray[arrayNo],
+ self.visColor, self.layout)
+
+ def pickColor(self):
+ RGBstring, btnStyle = super().pickColor()
+ if not RGBstring:
+ return
+ self.page.lineEdit_visColor.setText(RGBstring)
+ self.page.pushButton_visColor.setStyleSheet(btnStyle)
+
+ def transformData(
+ self, i, completeAudioArray, sampleSize,
+ smoothConstantDown, smoothConstantUp, lastSpectrum):
+ if len(completeAudioArray) < (i + sampleSize):
+ sampleSize = len(completeAudioArray) - i
+
+ window = numpy.hanning(sampleSize)
+ data = completeAudioArray[i:i+sampleSize][::1] * window
+ paddedSampleSize = 2048
+ paddedData = numpy.pad(
+ data, (0, paddedSampleSize - sampleSize), 'constant')
+ spectrum = numpy.fft.fft(paddedData)
+ sample_rate = 44100
+ frequencies = numpy.fft.fftfreq(len(spectrum), 1./sample_rate)
+
+ y = abs(spectrum[0:int(paddedSampleSize/2) - 1])
+
+ # filter the noise away
+ # y[y<80] = 0
+
+ y = 20 * numpy.log10(y)
+ y[numpy.isinf(y)] = 0
+
+ if lastSpectrum is not None:
+ lastSpectrum[y < lastSpectrum] = \
+ y[y < lastSpectrum] * smoothConstantDown + \
+ lastSpectrum[y < lastSpectrum] * (1 - smoothConstantDown)
+
+ lastSpectrum[y >= lastSpectrum] = \
+ y[y >= lastSpectrum] * smoothConstantUp + \
+ lastSpectrum[y >= lastSpectrum] * (1 - smoothConstantUp)
+ else:
+ lastSpectrum = y
+
+ x = frequencies[0:int(paddedSampleSize/2) - 1]
+
+ return lastSpectrum
+
+ def drawBars(self, width, height, spectrum, color, layout):
+ vH = height-height/8
+ bF = width / 64
+ bH = bF / 2
+ bQ = bF / 4
+ imTop = self.blankFrame(width, height)
+ draw = ImageDraw.Draw(imTop)
+ r, g, b = color
+ color2 = (r, g, b, 125)
+
+ bP = height / 1200
+
+ for j in range(0, 63):
+ draw.rectangle((
+ bH + j * bF, vH+bQ, bH + j * bF + bF, vH + bQ -
+ spectrum[j * 4] * bP - bH), fill=color2)
+
+ draw.rectangle((
+ bH + bQ + j * bF, vH, bH + bQ + j * bF + bH, vH -
+ spectrum[j * 4] * bP), fill=color)
+
+ imBottom = imTop.transpose(Image.FLIP_TOP_BOTTOM)
+
+ im = self.blankFrame(width, height)
+
+ if layout == 0:
+ y = 0 - int(height/100*43)
+ im.paste(imTop, (0, y), mask=imTop)
+ y = 0 + int(height/100*43)
+ im.paste(imBottom, (0, y), mask=imBottom)
+
+ if layout == 1:
+ y = 0 + int(height/100*10)
+ im.paste(imTop, (0, y), mask=imTop)
+ y = 0 - int(height/100*10)
+ im.paste(imBottom, (0, y), mask=imBottom)
+
+ if layout == 2:
+ y = 0 + int(height/100*10)
+ im.paste(imTop, (0, y), mask=imTop)
+
+ return im
+
+ def command(self, arg):
+ if not arg.startswith('preset=') and '=' in arg:
+ key, arg = arg.split('=', 1)
+ if key == 'color':
+ self.page.lineEdit_visColor.setText(arg)
+ return
+ elif key == 'layout':
+ if arg == 'classic':
+ self.page.comboBox_visLayout.setCurrentIndex(0)
+ elif arg == 'split':
+ self.page.comboBox_visLayout.setCurrentIndex(1)
+ elif arg == 'bottom':
+ self.page.comboBox_visLayout.setCurrentIndex(2)
+ return
+ super().command(arg)
+
+ def commandHelp(self):
+ print('Give a layout name:\n layout=[classic/split/bottom]')
+ print('Specify a color:\n color=255,255,255')
diff --git a/src/components/original.ui b/src/components/original.ui
new file mode 100644
index 0000000..5808653
--- /dev/null
+++ b/src/components/original.ui
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>633</width>
+ <height>178</height>
+ </rect>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>180</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_9">
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_visLayout">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Visualizer Layout</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_visLayout"/>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_5">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_visColor">
+ <property name="text">
+ <string>Visualizer Color</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_visColor">
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="MaximumSize" stdset="0">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_visColor"/>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/components/text.py b/src/components/text.py
new file mode 100644
index 0000000..0f599ed
--- /dev/null
+++ b/src/components/text.py
@@ -0,0 +1,176 @@
+from PIL import Image, ImageDraw
+from PyQt5.QtGui import QPainter, QColor, QFont
+from PyQt5 import uic, QtGui, QtCore
+from PIL.ImageQt import ImageQt
+import os
+import io
+from . import __base__
+
+
+class Component(__base__.Component):
+ '''Title Text'''
+
+ modified = QtCore.pyqtSignal(int, dict)
+
+ def __init__(self, *args):
+ super().__init__(*args)
+ self.titleFont = QFont()
+
+ def widget(self, parent):
+ height = int(parent.settings.value('outputHeight'))
+ width = int(parent.settings.value('outputWidth'))
+
+ self.parent = parent
+ self.textColor = (255, 255, 255)
+ self.title = 'Text'
+ self.alignment = 1
+ self.fontSize = height / 13.5
+ fm = QtGui.QFontMetrics(self.titleFont)
+ self.xPosition = width / 2 - fm.width(self.title)/2
+ self.yPosition = height / 2 * 1.036
+
+ page = uic.loadUi(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), 'text.ui'))
+ page.comboBox_textAlign.addItem("Left")
+ page.comboBox_textAlign.addItem("Middle")
+ page.comboBox_textAlign.addItem("Right")
+
+ page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor)
+ 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
+
+ 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 = self.RGBFromString(
+ self.page.lineEdit_textColor.text())
+ self.parent.drawPreview()
+ super().update()
+
+ def getXY(self):
+ '''Returns true x, y after considering alignment settings'''
+ fm = QtGui.QFontMetrics(self.titleFont)
+ if self.alignment == 0: # Left
+ x = self.xPosition
+
+ if self.alignment == 1: # Middle
+ offset = fm.width(self.title)/2
+ x = self.xPosition - offset
+
+ if self.alignment == 2: # Right
+ offset = fm.width(self.title)
+ x = self.xPosition - offset
+ return x, self.yPosition
+
+ def loadPreset(self, pr, presetName=None):
+ super().loadPreset(pr, presetName)
+
+ 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 {
+ 'preset': self.currentPreset,
+ 'title': self.title,
+ 'titleFont': self.titleFont.toString(),
+ 'alignment': self.alignment,
+ 'fontSize': self.fontSize,
+ 'xPosition': self.xPosition,
+ 'yPosition': self.yPosition,
+ 'textColor': self.textColor
+ }
+
+ def previewRender(self, previewWorker):
+ width = int(previewWorker.core.settings.value('outputWidth'))
+ height = int(previewWorker.core.settings.value('outputHeight'))
+ return self.addText(width, height)
+
+ def preFrameRender(self, **kwargs):
+ super().preFrameRender(**kwargs)
+ return ['static']
+
+ def frameRender(self, moduleNo, arrayNo, frameNo):
+ width = int(self.worker.core.settings.value('outputWidth'))
+ height = int(self.worker.core.settings.value('outputHeight'))
+ return self.addText(width, height)
+
+ def addText(self, width, height):
+ x, y = self.getXY()
+ im = self.blankFrame(width, height)
+ image = ImageQt(im)
+
+ painter = QPainter(image)
+ self.titleFont.setPixelSize(self.fontSize)
+ painter.setFont(self.titleFont)
+ painter.setPen(QColor(*self.textColor))
+ painter.drawText(x, y, self.title)
+ painter.end()
+
+ imBytes = image.bits().asstring(image.numBytes())
+
+ return Image.frombytes('RGBA', (width, height), imBytes)
+
+ def pickColor(self):
+ RGBstring, btnStyle = super().pickColor()
+ if not RGBstring:
+ return
+ self.page.lineEdit_textColor.setText(RGBstring)
+ self.page.pushButton_textColor.setStyleSheet(btnStyle)
+
+ def commandHelp(self):
+ print('Enter a string to use as centred white text:')
+ print(' "title=User Error"')
+ print('Specify a text color:\n color=255,255,255')
+ print('Set custom x, y position:\n x=500 y=500')
+
+ def command(self, arg):
+ if not arg.startswith('preset=') and '=' in arg:
+ key, arg = arg.split('=', 1)
+ if key == 'color':
+ self.page.lineEdit_textColor.setText(arg)
+ return
+ elif key == 'size':
+ self.page.spinBox_fontSize.setValue(int(arg))
+ return
+ elif key == 'x':
+ self.page.spinBox_xTextAlign.setValue(int(arg))
+ return
+ elif key == 'y':
+ self.page.spinBox_yTextAlign.setValue(int(arg))
+ return
+ elif key == 'title':
+ self.page.lineEdit_title.setText(arg)
+ return
+ super().command(arg)
diff --git a/src/components/text.ui b/src/components/text.ui
new file mode 100644
index 0000000..05e7f8e
--- /dev/null
+++ b/src/components/text.ui
@@ -0,0 +1,316 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>586</width>
+ <height>197</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Font</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QFontComboBox" name="fontComboBox_titleFont">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_8">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_fontSize">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Font Size</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_fontSize">
+ <property name="maximum">
+ <number>500</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_12">
+ <item>
+ <widget class="QLabel" name="label_textLayout">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Text Layout</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_textAlign"/>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_textColor">
+ <property name="text">
+ <string>Text Color</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_textColor">
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="MaximumSize" stdset="0">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_textColor"/>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_7">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_title">
+ <property name="text">
+ <string>Title</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_title">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Testing New GUI</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_6">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_xTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>X</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_xTextAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>0</number>
+ </property>
+ <property name="maximum">
+ <number>999999999</number>
+ </property>
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_7">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_yTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Y</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_yTextAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="maximum">
+ <number>999999999</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/components/video.py b/src/components/video.py
new file mode 100644
index 0000000..58ce7a3
--- /dev/null
+++ b/src/components/video.py
@@ -0,0 +1,273 @@
+from PIL import Image, ImageDraw
+from PyQt5 import uic, QtGui, QtCore
+import os
+import subprocess
+import threading
+from queue import PriorityQueue
+from . import __base__
+
+
+class Video:
+ '''Video Component Frame-Fetcher'''
+ def __init__(self, **kwargs):
+ mandatoryArgs = [
+ 'ffmpeg', # path to ffmpeg, usually core.FFMPEG_BIN
+ 'videoPath',
+ 'width',
+ 'height',
+ 'scale', # percentage scale
+ 'frameRate', # frames per second
+ 'chunkSize', # number of bytes in one frame
+ 'parent', # mainwindow object
+ 'component', # component object
+ ]
+ for arg in mandatoryArgs:
+ try:
+ exec('self.%s = kwargs[arg]' % arg)
+ except KeyError:
+ raise __base__.BadComponentInit(arg, self.__doc__)
+
+ self.frameNo = -1
+ self.currentFrame = 'None'
+ if 'loopVideo' in kwargs and kwargs['loopVideo']:
+ self.loopValue = '-1'
+ else:
+ self.loopValue = '0'
+ self.command = [
+ self.ffmpeg,
+ '-thread_queue_size', '512',
+ '-r', str(self.frameRate),
+ '-stream_loop', self.loopValue,
+ '-i', self.videoPath,
+ '-f', 'image2pipe',
+ '-pix_fmt', 'rgba',
+ '-filter:v', 'scale=%s:%s' %
+ scale(self.scale, self.width, self.height, str),
+ '-vcodec', 'rawvideo', '-',
+ ]
+
+ self.frameBuffer = PriorityQueue()
+ self.frameBuffer.maxsize = self.frameRate
+ self.finishedFrames = {}
+
+ self.thread = threading.Thread(
+ target=self.fillBuffer,
+ name=self.__doc__
+ )
+ self.thread.daemon = True
+ self.thread.start()
+
+ def frame(self, num):
+ while True:
+ if num in self.finishedFrames:
+ image = self.finishedFrames.pop(num)
+ return finalizeFrame(
+ self.component, image, self.width, self.height)
+
+ i, image = self.frameBuffer.get()
+ self.finishedFrames[i] = image
+ self.frameBuffer.task_done()
+
+ def fillBuffer(self):
+ pipe = subprocess.Popen(
+ self.command, stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL, bufsize=10**8
+ )
+ while True:
+ if self.parent.canceled:
+ break
+ self.frameNo += 1
+
+ # If we run out of frames, use the last good frame and loop.
+ if len(self.currentFrame) == 0:
+ self.frameBuffer.put((self.frameNo-1, self.lastFrame))
+ continue
+
+ self.currentFrame = pipe.stdout.read(self.chunkSize)
+ if len(self.currentFrame) != 0:
+ self.frameBuffer.put((self.frameNo, self.currentFrame))
+ self.lastFrame = self.currentFrame
+
+
+class Component(__base__.Component):
+ '''Video'''
+
+ modified = QtCore.pyqtSignal(int, dict)
+
+ def widget(self, parent):
+ self.parent = parent
+ self.settings = parent.settings
+ page = uic.loadUi(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)),
+ 'video.ui'
+ ))
+ self.videoPath = ''
+ 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.spinBox_scale.valueChanged.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.videoPath = self.page.lineEdit_video.text()
+ self.loopVideo = self.page.checkBox_loop.isChecked()
+ self.distort = self.page.checkBox_distort.isChecked()
+ self.scale = self.page.spinBox_scale.value()
+ self.xPosition = self.page.spinBox_x.value()
+ self.yPosition = self.page.spinBox_y.value()
+ self.parent.drawPreview()
+ super().update()
+
+ def previewRender(self, previewWorker):
+ self.videoFormats = previewWorker.core.videoFormats
+ width = int(previewWorker.core.settings.value('outputWidth'))
+ height = int(previewWorker.core.settings.value('outputHeight'))
+ self.updateChunksize(width, height)
+ frame = self.getPreviewFrame(width, height)
+ if not frame:
+ return self.blankFrame(width, height)
+ else:
+ return frame
+
+ def preFrameRender(self, **kwargs):
+ super().preFrameRender(**kwargs)
+ width = int(self.worker.core.settings.value('outputWidth'))
+ height = int(self.worker.core.settings.value('outputHeight'))
+ self.blankFrame_ = self.blankFrame(width, height)
+ self.updateChunksize(width, height)
+ self.video = Video(
+ ffmpeg=self.parent.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
+
+ def frameRender(self, moduleNo, arrayNo, frameNo):
+ if self.video:
+ return self.video.frame(frameNo)
+ 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_distort.setChecked(pr['distort'])
+ self.page.spinBox_scale.setValue(pr['scale'])
+ self.page.spinBox_x.setValue(pr['x'])
+ self.page.spinBox_y.setValue(pr['y'])
+
+ def savePreset(self):
+ return {
+ 'preset': self.currentPreset,
+ 'video': self.videoPath,
+ 'loop': self.loopVideo,
+ 'distort': self.distort,
+ 'scale': self.scale,
+ 'x': self.xPosition,
+ 'y': self.yPosition,
+ }
+
+ def pickVideo(self):
+ imgDir = self.settings.value("backgroundDir", os.path.expanduser("~"))
+ filename = QtGui.QFileDialog.getOpenFileName(
+ self.page, "Choose Video",
+ imgDir, "Video Files (%s)" % " ".join(self.videoFormats)
+ )
+ if filename:
+ self.settings.setValue("backgroundDir", os.path.dirname(filename))
+ self.page.lineEdit_video.setText(filename)
+ self.update()
+
+ def getPreviewFrame(self, width, height):
+ if not self.videoPath or not os.path.exists(self.videoPath):
+ return
+
+ command = [
+ self.parent.core.FFMPEG_BIN,
+ '-thread_queue_size', '512',
+ '-i', self.videoPath,
+ '-f', 'image2pipe',
+ '-pix_fmt', 'rgba',
+ '-filter:v', 'scale=%s:%s' %
+ scale(self.scale, width, height, str),
+ '-vcodec', 'rawvideo', '-',
+ '-ss', '90',
+ '-vframes', '1',
+ ]
+ pipe = subprocess.Popen(
+ command, stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL, bufsize=10**8
+ )
+ byteFrame = pipe.stdout.read(self.chunkSize)
+ frame = finalizeFrame(self, byteFrame, width, height)
+ pipe.stdout.close()
+ pipe.kill()
+
+ return frame
+
+ def updateChunksize(self, width, height):
+ if self.scale != 100 and not self.distort:
+ width, height = scale(self.scale, width, height, int)
+ self.chunkSize = 4*width*height
+
+ def command(self, arg):
+ 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:
+ self.page.lineEdit_video.setText(arg)
+ self.page.spinBox_scale.setValue(100)
+ self.page.checkBox_loop.setChecked(True)
+ return
+ else:
+ print("Not a supported video format")
+ quit(1)
+ super().command(arg)
+
+ 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)
+ if returntype == str:
+ return (str(int(width)), str(int(height)))
+ elif returntype == int:
+ return (int(width), int(height))
+ else:
+ return (width, height)
+
+def finalizeFrame(self, imageData, width, height):
+ if self.distort:
+ try:
+ image = Image.frombytes(
+ 'RGBA',
+ (width, height),
+ imageData)
+ except ValueError:
+ print('#### ignored invalid data caused by distortion ####')
+ image = self.blankFrame(width, height)
+ else:
+ image = Image.frombytes(
+ 'RGBA',
+ scale(self.scale, width, height, int),
+ imageData)
+
+ if self.scale != 100 \
+ or self.xPosition != 0 or self.yPosition != 0:
+ frame = self.blankFrame(width, height)
+ frame.paste(image, box=(self.xPosition, self.yPosition))
+ else:
+ frame = image
+ return frame
diff --git a/src/components/video.ui b/src/components/video.ui
new file mode 100644
index 0000000..f05e8a5
--- /dev/null
+++ b/src/components/video.ui
@@ -0,0 +1,266 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>586</width>
+ <height>197</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <item>
+ <widget class="QLabel" name="label_textColor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>31</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Video</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_video">
+ <property name="minimumSize">
+ <size>
+ <width>1</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_video">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>1</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>...</string>
+ </property>
+ <property name="MaximumSize" stdset="0">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_9">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_xTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>X</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_x">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_yTitleAlign">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Y</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_y">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="minimum">
+ <number>-10000</number>
+ </property>
+ <property name="maximum">
+ <number>10000</number>
+ </property>
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_9">
+ <item>
+ <widget class="QCheckBox" name="checkBox_loop">
+ <property name="text">
+ <string>Loop</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_10">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_distort">
+ <property name="text">
+ <string>Distort by scale</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Scale</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_scale">
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::UpDownArrows</enum>
+ </property>
+ <property name="suffix">
+ <string>%</string>
+ </property>
+ <property name="minimum">
+ <number>10</number>
+ </property>
+ <property name="maximum">
+ <number>400</number>
+ </property>
+ <property name="value">
+ <number>100</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QWidget" name="widget" native="true"/>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/core.py b/src/core.py
new file mode 100644
index 0000000..bb5d351
--- /dev/null
+++ b/src/core.py
@@ -0,0 +1,477 @@
+import sys
+import io
+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
+
+
+class Core():
+
+ def __init__(self):
+ self.FFMPEG_BIN = self.findFfmpeg()
+ self.dataDir = QStandardPaths.writableLocation(
+ QStandardPaths.AppConfigLocation
+ )
+ self.presetDir = os.path.join(self.dataDir, 'presets')
+ if getattr(sys, 'frozen', False):
+ # frozen
+ self.wd = os.path.dirname(sys.executable)
+ else:
+ # unfrozen
+ self.wd = os.path.dirname(os.path.realpath(__file__))
+
+ self.loadEncoderOptions()
+ self.videoFormats = Core.appendUppercase([
+ '*.mp4',
+ '*.mov',
+ '*.mkv',
+ '*.avi',
+ '*.webm',
+ '*.flv',
+ ])
+ self.audioFormats = Core.appendUppercase([
+ '*.mp3',
+ '*.wav',
+ '*.ogg',
+ '*.fla',
+ '*.flac',
+ '*.aac',
+ ])
+ self.imageFormats = Core.appendUppercase([
+ '*.png',
+ '*.jpg',
+ '*.tif',
+ '*.tiff',
+ '*.gif',
+ '*.bmp',
+ '*.ico',
+ '*.xbm',
+ '*.xpm',
+ ])
+
+ self.findComponents()
+ self.selectedComponents = []
+ # copies of named presets to detect modification
+ self.savedPresets = {}
+
+ def findComponents(self):
+ def findComponents():
+ srcPath = os.path.join(self.wd, 'components')
+ if os.path.exists(srcPath):
+ for f in sorted(os.listdir(srcPath)):
+ name, ext = os.path.splitext(f)
+ if name.startswith("__"):
+ continue
+ elif ext == '.py':
+ yield name
+ self.modules = [
+ import_module('components.%s' % name)
+ for name in findComponents()
+ ]
+ self.moduleIndexes = [i for i in range(len(self.modules))]
+ self.compNames = [mod.Component.__doc__ for mod in self.modules]
+
+ def componentListChanged(self):
+ for i, component in enumerate(self.selectedComponents):
+ component.compPos = i
+
+ def insertComponent(self, compPos, moduleIndex, loader):
+ '''Creates a new component'''
+ 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, self)
+ self.selectedComponents.insert(
+ compPos,
+ component)
+ self.componentListChanged()
+
+ # init component's widget for loading/saving presets
+ self.selectedComponents[compPos].widget(loader)
+ self.updateComponent(compPos)
+
+ if hasattr(loader, 'insertComponent'):
+ loader.insertComponent(compPos)
+ return compPos
+
+ def moveComponent(self, startI, endI):
+ comp = self.selectedComponents.pop(startI)
+ self.selectedComponents.insert(endI, comp)
+
+ self.componentListChanged()
+ return endI
+
+ def removeComponent(self, i):
+ self.selectedComponents.pop(i)
+ self.componentListChanged()
+
+ def clearComponents(self):
+ self.selectedComponents = list()
+ self.componentListChanged()
+
+ def updateComponent(self, i):
+ # print('updating %s' % self.selectedComponents[i])
+ self.selectedComponents[i].update()
+
+ def moduleIndexFor(self, compName):
+ index = self.compNames.index(compName)
+ return self.moduleIndexes[index]
+
+ def clearPreset(self, compIndex):
+ self.selectedComponents[compIndex].currentPreset = None
+
+ def openPreset(self, filepath, compIndex, presetName):
+ '''Applies a preset to a specific component'''
+ saveValueStore = self.getPreset(filepath)
+ if not saveValueStore:
+ return False
+ try:
+ self.selectedComponents[compIndex].loadPreset(
+ saveValueStore,
+ presetName
+ )
+ except KeyError as e:
+ print('preset missing value: %s' % e)
+
+ 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):
+ return False
+ with open(filepath, 'r') as f:
+ for line in f:
+ saveValueStore = Core.presetFromString(line.strip())
+ break
+ return saveValueStore
+
+ def openProject(self, loader, filepath):
+ ''' loader is the object calling this method which must have
+ its own showMessage(**kwargs) method for displaying errors.
+ '''
+ if not os.path.exists(filepath):
+ loader.showMessage(msg='Project file not found')
+ return
+
+ errcode, data = self.parseAvFile(filepath)
+ if errcode == 0:
+ try:
+ for i, tup in enumerate(data['Components']):
+ name, vers, preset = tup
+ clearThis = False
+
+ # add loaded named presets to savedPresets dict
+ if 'preset' in preset and preset['preset'] != None:
+ nam = preset['preset']
+ filepath2 = os.path.join(
+ self.presetDir, name, str(vers), nam)
+ origSaveValueStore = self.getPreset(filepath2)
+ if origSaveValueStore:
+ self.savedPresets[nam] = dict(origSaveValueStore)
+ else:
+ # saved preset was renamed or deleted
+ clearThis = True
+
+ # create the actual component object & get its index
+ i = self.insertComponent(
+ -1,
+ self.moduleIndexFor(name),
+ loader)
+ if i == None:
+ loader.showMessage(msg="Too many components!")
+ break
+
+ try:
+ if 'preset' in preset and preset['preset'] != None:
+ self.selectedComponents[i].loadPreset(
+ preset
+ )
+ else:
+ self.selectedComponents[i].loadPreset(
+ preset,
+ preset['preset']
+ )
+ except KeyError as e:
+ print('%s missing value %s' %
+ (self.selectedComponents[i], e))
+
+ if clearThis:
+ self.clearPreset(i)
+ if hasattr(loader, 'updateComponentTitle'):
+ loader.updateComponentTitle(i)
+ except:
+ errcode = 1
+ data = sys.exc_info()
+
+
+ if errcode == 1:
+ typ, value, _ = 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)
+ loader.showMessage(
+ msg="Project file '%s' is corrupted." % filepath,
+ showCancel=False,
+ icon=QtGui.QMessageBox.Warning,
+ detail=msg)
+
+ def parseAvFile(self, filepath):
+ '''Parses an avp (project) or avl (preset package) file.
+ Returns dictionary with section names as the keys, each one
+ contains a list of tuples: (compName, version, compPresetDict)
+ '''
+ data = {}
+ try:
+ with open(filepath, 'r') as f:
+ def parseLine(line):
+ '''Decides if a file line is a section header'''
+ validSections = ('Components')
+ line = line.strip()
+ newSection = ''
+
+ if line.startswith('[') and line.endswith(']') \
+ and line[1:-1] in validSections:
+ newSection = line[1:-1]
+
+ return line, newSection
+
+ section = ''
+ i = 0
+ for line in f:
+ line, newSection = parseLine(line)
+ if newSection:
+ section = str(newSection)
+ data[section] = []
+ continue
+ if line and section == 'Components':
+ if i == 0:
+ lastCompName = str(line)
+ i += 1
+ elif i == 1:
+ lastCompVers = str(line)
+ i += 1
+ elif i == 2:
+ lastCompPreset = Core.presetFromString(line)
+ data[section].append(
+ (lastCompName,
+ lastCompVers,
+ lastCompPreset)
+ )
+ i = 0
+ return 0, data
+ except:
+ return 1, sys.exc_info()
+
+ def importPreset(self, filepath):
+ errcode, data = self.parseAvFile(filepath)
+ returnList = []
+ if errcode == 0:
+ name, vers, preset = data['Components'][0]
+ presetName = preset['preset'] \
+ if preset['preset'] else os.path.basename(filepath)[:-4]
+ newPath = os.path.join(
+ self.presetDir,
+ name,
+ vers,
+ presetName
+ )
+ if os.path.exists(newPath):
+ return False, newPath
+ preset['preset'] = presetName
+ self.createPresetFile(
+ name, vers, presetName, preset
+ )
+ return True, presetName
+ elif errcode == 1:
+ # TODO: an error message
+ return False, ''
+
+ def exportPreset(self, exportPath, compName, vers, origName):
+ internalPath = os.path.join(self.presetDir, compName, str(vers), origName)
+ if not os.path.exists(internalPath):
+ return
+ if os.path.exists(exportPath):
+ os.remove(exportPath)
+ with open(internalPath, 'r') as f:
+ internalData = [line for line in f]
+ try:
+ saveValueStore = Core.presetFromString(internalData[0].strip())
+ self.createPresetFile(
+ compName, vers,
+ origName, saveValueStore,
+ exportPath
+ )
+ return True
+ except:
+ return False
+
+ def createPresetFile(
+ self, compName, vers, presetName, saveValueStore, filepath=''):
+ '''Create a preset file (.avl) at filepath using args.
+ Or if filepath is empty, create an internal preset using args'''
+ if not filepath:
+ dirname = os.path.join(self.presetDir, compName, str(vers))
+ if not os.path.exists(dirname):
+ os.makedirs(dirname)
+ filepath = os.path.join(dirname, presetName)
+ internal = True
+ else:
+ if not filepath.endswith('.avl'):
+ filepath += '.avl'
+ internal = False
+
+ with open(filepath, 'w') as f:
+ if not internal:
+ f.write('[Components]\n')
+ f.write('%s\n' % compName)
+ f.write('%s\n' % str(vers))
+ f.write(Core.presetToString(saveValueStore))
+
+ def createProjectFile(self, filepath):
+ '''Create a project file (.avp) using the current program state'''
+ try:
+ if not filepath.endswith(".avp"):
+ filepath += '.avp'
+ if os.path.exists(filepath):
+ 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))
+ return True
+ 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 sys.platform == "win32":
+ return "ffmpeg.exe"
+ else:
+ try:
+ with open(os.devnull, "w") as f:
+ sp.check_call(['ffmpeg', '-version'], stdout=f, stderr=f)
+ return "ffmpeg"
+ except:
+ return "avconv"
+
+ def readAudioFile(self, filename, parent):
+ command = [self.FFMPEG_BIN, '-i', filename]
+
+ try:
+ fileInfo = sp.check_output(command, stderr=sp.STDOUT, shell=False)
+ except sp.CalledProcessError as ex:
+ fileInfo = ex.output
+ pass
+
+ 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])
+
+ 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 = sp.Popen(
+ 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 = 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
+
+ def cancel(self):
+ self.canceled = True
+
+ 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/encoder-options.json b/src/encoder-options.json
new file mode 100644
index 0000000..78bc940
--- /dev/null
+++ b/src/encoder-options.json
@@ -0,0 +1,130 @@
+{
+ "containers":[
+ {
+ "name": "MP4",
+ "container": "mp4",
+ "default-vcodec": "H264",
+ "default-acodec": "AAC",
+ "video-codecs": [
+ "H264",
+ "H264 (nvenc)",
+ "MPEG4"
+ ],
+ "audio-codecs": [
+ "AAC",
+ "AC3",
+ "MP3"
+ ]
+ },
+ {
+ "name": "MOV",
+ "container": "mov",
+ "default-vcodec": "H264",
+ "default-acodec": "AAC",
+ "video-codecs": [
+ "H264",
+ "H264 (nvenc)",
+ "MPEG4",
+ "XVID"
+ ],
+ "audio-codecs": [
+ "AAC",
+ "AC3",
+ "MP3",
+ "PCM s16 LE"
+ ]
+ },
+ {
+ "name": "MKV",
+ "container": "matroska",
+ "default-vcodec": "H264",
+ "default-acodec": "AAC",
+ "video-codecs": [
+ "H264",
+ "H264 (nvenc)",
+ "MPEG4",
+ "MPEG2",
+ "DV",
+ "WMV"
+ ],
+ "audio-codecs": [
+ "AAC",
+ "AC3",
+ "MP3",
+ "PCM s16 LE",
+ "WMA"
+ ]
+ },
+ {
+ "name": "AVI",
+ "container": "avi",
+ "default-vcodec": "H264",
+ "default-acodec": "AAC",
+ "video-codecs": [
+ "H264",
+ "H264 (nvenc)",
+ "MPEG4",
+ "MPEG2",
+ "DV",
+ "WMV"
+ ],
+ "audio-codecs": [
+ "AAC",
+ "AC3",
+ "MP3",
+ "PCM s16 LE",
+ "WMA"
+ ]
+ },
+ {
+ "name": "WEBM",
+ "container": "webm",
+ "default-vcodec": "VP9",
+ "default-acodec": "Vorbis",
+ "video-codecs": [
+ "VP9",
+ "VP8"
+ ],
+ "audio-codecs": [
+ "Vorbis"
+ ]
+ },
+ {
+ "name": "FLV",
+ "container": "flv",
+ "default-vcodec": "FLV",
+ "default-acodec": "Vorbis",
+ "video-codecs": [
+ "Sorenson (flv)",
+ "H264",
+ "H264 (nvenc)",
+ "MPEG4"
+ ],
+ "audio-codecs": [
+ "MP3",
+ "PCM s16 LE",
+ "Vorbis"
+ ]
+ }
+ ],
+ "video-codecs":{
+ "H264": ["libx264"],
+ "H264 (nvenc)": ["h264_nvenc", "nvenc_h264"],
+ "MPEG4": ["mpeg4"],
+ "VP9": ["libvpx-vp9"],
+ "VP8": ["libvpx"],
+ "XVID": ["libxvid"],
+ "Sorenson (flv)": ["flv"],
+ "MPEG2": ["mp2video"],
+ "DV": ["dvvideo"],
+ "WMV": ["wmv2"]
+ },
+ "audio-codecs": {
+ "AAC": ["libfdk_aac", "aac"],
+ "AC3": ["ac3"],
+ "MP3": ["libmp3lame"],
+ "PCM s16 LE": ["pcm_s16le"],
+ "WMA": ["wmav2"],
+ "Vorbis": ["libvorbis"]
+ }
+} \ No newline at end of file
diff --git a/src/main.py b/src/main.py
new file mode 100644
index 0000000..4bf26db
--- /dev/null
+++ b/src/main.py
@@ -0,0 +1,88 @@
+from PyQt5 import QtGui, uic, QtWidgets
+import sys
+import os
+
+import core
+import preview_thread
+import video_thread
+
+
+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():
+ #print(parm, self.settings.value(parm))
+ if self.settings.value(parm) is None:
+ self.settings.setValue(parm, value)
+
+if __name__ == "__main__":
+ mode = 'gui'
+ if len(sys.argv) > 2:
+ mode = 'cmd'
+
+ elif len(sys.argv) == 2:
+ if sys.argv[1].startswith('-'):
+ mode = 'cmd'
+ else:
+ # opening a project file with gui
+ proj = sys.argv[1]
+ else:
+ # normal gui launch
+ proj = None
+
+ app = QtWidgets.QApplication(sys.argv)
+ app.setApplicationName("audio-visualizer")
+ app.setOrganizationName("audio-visualizer")
+
+ if mode == 'cmd':
+ from command import *
+
+ main = Command()
+
+ elif mode == 'gui':
+ from mainwindow import *
+ import atexit
+ import signal
+
+ if getattr(sys, 'frozen', False):
+ # frozen
+ wd = os.path.dirname(sys.executable)
+ else:
+ # unfrozen
+ wd = os.path.dirname(os.path.realpath(__file__))
+
+ window = uic.loadUi(os.path.join(wd, "mainwindow.ui"))
+ # window.adjustSize()
+ desc = QtWidgets.QDesktopWidget()
+ dpi = desc.physicalDpiX()
+
+ topMargin = 0 if (dpi == 96) else int(10 * (dpi / 96))
+ window.resize(window.width() * (dpi / 96), window.height() * (dpi / 96))
+ # window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0)
+
+ main = MainWindow(window, proj)
+
+ signal.signal(signal.SIGINT, main.cleanUp)
+ atexit.register(main.cleanUp)
+
+ # applicable to both modes
+ sys.exit(app.exec_())
diff --git a/src/mainwindow.py b/src/mainwindow.py
new file mode 100644
index 0000000..a52a0f4
--- /dev/null
+++ b/src/mainwindow.py
@@ -0,0 +1,718 @@
+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
+import signal
+import filecmp
+import time
+
+import core
+import preview_thread
+import video_thread
+from presetmanager import PresetManager
+from main import LoadDefaultSettings
+
+
+class PreviewWindow(QtWidgets.QLabel):
+ def __init__(self, parent, img):
+ super(PreviewWindow, self).__init__()
+ self.parent = parent
+ self.setFrameStyle(QtWidgets.QFrame.StyledPanel)
+ self.pixmap = QtGui.QPixmap(img)
+
+ def paintEvent(self, event):
+ size = self.size()
+ painter = QtGui.QPainter(self)
+ point = QtCore.QPoint(0, 0)
+ scaledPix = self.pixmap.scaled(
+ size, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)
+
+ # start painting the label from left upper corner
+ point.setX((size.width() - scaledPix.width())/2)
+ point.setY((size.height() - scaledPix.height())/2)
+ painter.drawPixmap(point, scaledPix)
+
+ def changePixmap(self, img):
+ self.pixmap = QtGui.QPixmap(img)
+ self.repaint()
+
+
+class MainWindow(QtWidgets.QMainWindow):
+
+ newTask = QtCore.pyqtSignal(list)
+ processTask = QtCore.pyqtSignal()
+ videoTask = QtCore.pyqtSignal(str, str, list)
+
+ def __init__(self, window, project):
+ QtWidgets.QMainWindow.__init__(self)
+
+ # print('main thread id: {}'.format(QtCore.QThread.currentThreadId()))
+ self.window = window
+ self.core = core.Core()
+
+ self.pages = [] # widgets of component settings
+ self.lastAutosave = time.time()
+
+ # 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)
+ LoadDefaultSettings(self)
+ self.presetManager = PresetManager(
+ uic.loadUi(
+ os.path.join(self.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")):
+ if not os.path.exists(neededDirectory):
+ os.mkdir(neededDirectory)
+
+ # Make queues/timers for the preview thread
+ self.previewQueue = Queue()
+ self.previewThread = QtCore.QThread(self)
+ self.previewWorker = preview_thread.Worker(self, self.previewQueue)
+ self.previewWorker.moveToThread(self.previewThread)
+ self.previewWorker.imageCreated.connect(self.showPreviewImage)
+ self.previewThread.start()
+
+ self.timer = QtCore.QTimer(self)
+ self.timer.timeout.connect(self.processTask.emit)
+ self.timer.start(500)
+
+ # Begin decorating the window and connecting events
+ componentList = self.window.listWidget_componentList
+
+ window.toolButton_selectAudioFile.clicked.connect(
+ self.openInputFileDialog)
+
+ window.toolButton_selectOutputFile.clicked.connect(
+ self.openOutputFileDialog)
+
+ window.progressBar_createVideo.setValue(0)
+
+ window.pushButton_createVideo.clicked.connect(
+ self.createAudioVisualisation)
+
+ window.pushButton_Cancel.clicked.connect(self.stopVideo)
+
+ for i, container in enumerate(self.core.encoder_options['containers']):
+ window.comboBox_videoContainer.addItem(container['name'])
+ if container['name'] == self.settings.value('outputContainer'):
+ selectedContainer = i
+
+ window.comboBox_videoContainer.setCurrentIndex(selectedContainer)
+ window.comboBox_videoContainer.currentIndexChanged.connect(
+ self.updateCodecs
+ )
+
+ self.updateCodecs()
+
+ for i in range(window.comboBox_videoCodec.count()):
+ 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)
+ if codec == self.settings.value('outputAudioCodec'):
+ window.comboBox_audioCodec.setCurrentIndex(i)
+
+ window.comboBox_videoCodec.currentIndexChanged.connect(
+ self.updateCodecSettings
+ )
+
+ window.comboBox_audioCodec.currentIndexChanged.connect(
+ self.updateCodecSettings
+ )
+
+ vBitrate = int(self.settings.value('outputVideoBitrate'))
+ aBitrate = int(self.settings.value('outputAudioBitrate'))
+
+ 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(
+ self.core.wd, "background.png"))
+ window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
+
+ # Make component buttons
+ self.compMenu = QMenu()
+ 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))
+
+ self.window.pushButton_addComponent.setMenu(self.compMenu)
+
+ componentList.dropEvent = self.dragComponent
+ componentList.itemSelectionChanged.connect(
+ self.changeComponentWidget)
+
+ self.window.pushButton_removeComponent.clicked.connect(
+ lambda _: self.removeComponent())
+
+ componentList.setContextMenuPolicy(
+ QtCore.Qt.CustomContextMenu)
+ componentList.customContextMenuRequested.connect(self.componentContextMenu)
+
+ currentRes = str(self.settings.value('outputWidth'))+'x' + \
+ str(self.settings.value('outputHeight'))
+ for i, res in enumerate(self.resolutions):
+ window.comboBox_resolution.addItem(res)
+ if res == currentRes:
+ currentRes = i
+ window.comboBox_resolution.setCurrentIndex(currentRes)
+ window.comboBox_resolution.currentIndexChanged.connect(
+ self.updateResolution)
+
+ self.window.pushButton_listMoveUp.clicked.connect(
+ lambda: self.moveComponent(-1)
+ )
+ self.window.pushButton_listMoveDown.clicked.connect(
+ lambda: self.moveComponent(1)
+ )
+
+ # Configure the Projects Menu
+ self.projectMenu = QMenu()
+ self.window.menuButton_newProject = self.projectMenu.addAction(
+ "New Project")
+ self.window.menuButton_newProject.triggered.connect(
+ self.createNewProject)
+
+ self.window.menuButton_openProject = self.projectMenu.addAction(
+ "Open Project")
+ self.window.menuButton_openProject.triggered.connect(
+ self.openOpenProjectDialog)
+
+ action = self.projectMenu.addAction("Save Project")
+ action.triggered.connect(self.saveCurrentProject)
+
+ action = self.projectMenu.addAction("Save Project As")
+ action.triggered.connect(self.openSaveProjectDialog)
+
+ self.window.pushButton_projects.setMenu(self.projectMenu)
+
+ # Configure the Presets Button
+ self.window.pushButton_presets.clicked.connect(
+ self.openPresetManager
+ )
+
+ window.show()
+
+ if project and project != self.autosavePath:
+ if not project.endswith('.avp'):
+ project += '.avp'
+ # open a project from the commandline
+ if not os.path.dirname(project):
+ project = os.path.join(os.path.expanduser('~'), project)
+ self.currentProject = project
+ self.settings.setValue("currentProject", project)
+ if os.path.exists(self.autosavePath):
+ os.remove(self.autosavePath)
+ else:
+ # open the last currentProject from settings
+ self.currentProject = self.settings.value("currentProject")
+
+ # delete autosave if it's identical to this project
+ if self.autosaveExists(identical=True):
+ os.remove(self.autosavePath)
+
+ if self.currentProject and os.path.exists(self.autosavePath):
+ ch = self.showMessage(
+ msg="Restore unsaved changes in project '%s'?"
+ % os.path.basename(self.currentProject)[:-4],
+ showCancel=True)
+ if ch:
+ self.saveProjectChanges()
+ else:
+ os.remove(self.autosavePath)
+
+ self.openProject(self.currentProject, prompt=False)
+ self.drawPreview(True)
+
+ # Setup Hotkeys
+ QtWidgets.QShortcut("Ctrl+S", self.window, self.saveCurrentProject)
+ 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+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)
+
+ def cleanUp(self):
+ self.timer.stop()
+ self.previewThread.quit()
+ self.previewThread.wait()
+ self.autosave()
+
+ def updateWindowTitle(self):
+ appName = 'Audio Visualizer'
+ if self.currentProject:
+ appName += ' - %s' % \
+ os.path.splitext(
+ os.path.basename(self.currentProject))[0]
+ self.window.setWindowTitle(appName)
+
+ @QtCore.pyqtSlot(int, dict)
+ def updateComponentTitle(self, pos, presetStore=False):
+ if type(presetStore) == dict:
+ name = presetStore['preset']
+ if name == None or name not in self.core.savedPresets:
+ modified = False
+ else:
+ modified = (presetStore != self.core.savedPresets[name])
+ else:
+ modified = bool(presetStore)
+ if pos < 0:
+ pos = len(self.core.selectedComponents)-1
+ title = str(self.core.selectedComponents[pos])
+ if self.core.selectedComponents[pos].currentPreset:
+ title += ' - %s' % self.core.selectedComponents[pos].currentPreset
+ if modified:
+ title += '*'
+ self.window.listWidget_componentList.item(pos).setText(title)
+
+ def updateCodecs(self):
+ containerWidget = self.window.comboBox_videoContainer
+ vCodecWidget = self.window.comboBox_videoCodec
+ aCodecWidget = self.window.comboBox_audioCodec
+ index = containerWidget.currentIndex()
+ name = containerWidget.itemText(index)
+ self.settings.setValue('outputContainer', name)
+
+ vCodecWidget.clear()
+ aCodecWidget.clear()
+
+ for container in self.core.encoder_options['containers']:
+ if container['name'] == name:
+ for vCodec in container['video-codecs']:
+ vCodecWidget.addItem(vCodec)
+ for aCodec in container['audio-codecs']:
+ aCodecWidget.addItem(aCodec)
+
+ def updateCodecSettings(self):
+ vCodecWidget = self.window.comboBox_videoCodec
+ vBitrateWidget = self.window.spinBox_vBitrate
+ aBitrateWidget = self.window.spinBox_aBitrate
+ aCodecWidget = self.window.comboBox_audioCodec
+ currentVideoCodec = vCodecWidget.currentIndex()
+ currentVideoCodec = vCodecWidget.itemText(currentVideoCodec)
+ currentVideoBitrate = vBitrateWidget.value()
+ currentAudioCodec = aCodecWidget.currentIndex()
+ currentAudioCodec = aCodecWidget.itemText(currentAudioCodec)
+ currentAudioBitrate = aBitrateWidget.value()
+ self.settings.setValue('outputVideoCodec', currentVideoCodec)
+ self.settings.setValue('outputAudioCodec', currentAudioCodec)
+ self.settings.setValue('outputVideoBitrate', currentVideoBitrate)
+ self.settings.setValue('outputAudioBitrate', currentAudioBitrate)
+
+ 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 >= 2.0:
+ self.core.createProjectFile(self.autosavePath)
+ self.lastAutosave = time.time()
+
+ def autosaveExists(self, identical=True):
+ try:
+ if self.currentProject and os.path.exists(self.autosavePath) \
+ and filecmp.cmp(
+ self.autosavePath, self.currentProject) == identical:
+ return True
+ except FileNotFoundError:
+ print('project file couldn\'t be located:', self.currentProject)
+ return identical
+ return False
+
+ def saveProjectChanges(self):
+ try:
+ os.remove(self.currentProject)
+ os.rename(self.autosavePath, self.currentProject)
+ return True
+ except (FileNotFoundError, IsADirectoryError) as e:
+ self.showMessage(
+ msg='Project file couldn\'t be saved.',
+ detail=str(e))
+ return False
+
+ def openInputFileDialog(self):
+ inputDir = self.settings.value("inputDir", os.path.expanduser("~"))
+
+ fileName = QtGui.QFileDialog.getOpenFileName(
+ self.window, "Open Audio File",
+ inputDir, "Audio Files (%s)" % " ".join(self.core.audioFormats))
+
+ if not 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(
+ self.window, "Set Output Video File",
+ outputDir,
+ "Video Files (%s);; All Files (*)" % " ".join(self.core.videoFormats))
+
+ if not fileName == "":
+ self.settings.setValue("outputDir", os.path.dirname(fileName))
+ self.window.lineEdit_outputFile.setText(fileName)
+
+ def stopVideo(self):
+ print('stop')
+ self.videoWorker.cancel()
+ self.canceled = True
+
+ def createAudioVisualisation(self):
+ # create output video if mandatory settings are filled in
+ if self.window.lineEdit_audioFile.text() and \
+ self.window.lineEdit_outputFile.text():
+ 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.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()
+ outputPath = self.window.lineEdit_outputFile.text()
+ if not os.path.dirname(outputPath):
+ outputPath = os.path.join(
+ os.path.expanduser("~"), outputPath)
+ self.videoTask.emit(
+ self.window.lineEdit_audioFile.text(),
+ outputPath,
+ self.core.selectedComponents)
+ else:
+ self.showMessage(
+ msg="You must select an audio file and output filename.")
+
+ def changeEncodingStatus(self, status):
+ if status:
+ self.window.pushButton_createVideo.setEnabled(False)
+ self.window.pushButton_Cancel.setEnabled(True)
+ self.window.comboBox_resolution.setEnabled(False)
+ self.window.stackedWidget.setEnabled(False)
+ self.window.tab_encoderSettings.setEnabled(False)
+ self.window.label_audioFile.setEnabled(False)
+ self.window.toolButton_selectAudioFile.setEnabled(False)
+ self.window.label_outputFile.setEnabled(False)
+ self.window.toolButton_selectOutputFile.setEnabled(False)
+ self.window.lineEdit_audioFile.setEnabled(False)
+ self.window.lineEdit_outputFile.setEnabled(False)
+ self.window.pushButton_addComponent.setEnabled(False)
+ self.window.pushButton_removeComponent.setEnabled(False)
+ self.window.pushButton_listMoveDown.setEnabled(False)
+ self.window.pushButton_listMoveUp.setEnabled(False)
+ self.window.listWidget_componentList.setEnabled(False)
+ self.window.menuButton_newProject.setEnabled(False)
+ self.window.menuButton_openProject.setEnabled(False)
+ else:
+ self.window.pushButton_createVideo.setEnabled(True)
+ self.window.pushButton_Cancel.setEnabled(False)
+ self.window.comboBox_resolution.setEnabled(True)
+ self.window.stackedWidget.setEnabled(True)
+ self.window.tab_encoderSettings.setEnabled(True)
+ self.window.label_audioFile.setEnabled(True)
+ self.window.toolButton_selectAudioFile.setEnabled(True)
+ self.window.lineEdit_audioFile.setEnabled(True)
+ self.window.label_outputFile.setEnabled(True)
+ self.window.toolButton_selectOutputFile.setEnabled(True)
+ self.window.lineEdit_outputFile.setEnabled(True)
+ self.window.pushButton_addComponent.setEnabled(True)
+ self.window.pushButton_removeComponent.setEnabled(True)
+ self.window.pushButton_listMoveDown.setEnabled(True)
+ self.window.pushButton_listMoveUp.setEnabled(True)
+ self.window.listWidget_componentList.setEnabled(True)
+ self.window.menuButton_newProject.setEnabled(True)
+ self.window.menuButton_openProject.setEnabled(True)
+ self.drawPreview(True)
+
+ def progressBarUpdated(self, value):
+ self.window.progressBar_createVideo.setValue(value)
+
+ def progressBarSetText(self, value):
+ 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')
+ self.settings.setValue('outputWidth', res[0])
+ self.settings.setValue('outputHeight', res[1])
+ self.drawPreview()
+
+ def drawPreview(self, force=False):
+ self.newTask.emit(self.core.selectedComponents)
+ # self.processTask.emit()
+ self.autosave(force)
+
+ def showPreviewImage(self, image):
+ self.previewWindow.changePixmap(image)
+
+ def insertComponent(self, index):
+ componentList = self.window.listWidget_componentList
+ stackedWidget = self.window.stackedWidget
+
+ componentList.insertItem(
+ index,
+ self.core.selectedComponents[index].__doc__)
+ componentList.setCurrentRow(index)
+
+ # connect to signal that adds an asterisk when modified
+ self.core.selectedComponents[index].modified.connect(
+ self.updateComponentTitle)
+
+ self.pages.insert(index, self.core.selectedComponents[index].page)
+ stackedWidget.insertWidget(index, self.pages[index])
+ stackedWidget.setCurrentIndex(index)
+
+ return index
+
+ 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()
+
+ def moveComponent(self, change):
+ '''Moves a component relatively from its current position'''
+ componentList = self.window.listWidget_componentList
+ stackedWidget = self.window.stackedWidget
+
+ row = componentList.currentRow()
+ newRow = row + change
+ if newRow > -1 and newRow < componentList.count():
+ self.core.moveComponent(row, newRow)
+
+ # update widgets
+ page = self.pages.pop(row)
+ self.pages.insert(newRow, page)
+ item = componentList.takeItem(row)
+ newItem = componentList.insertItem(newRow, item)
+ widget = stackedWidget.removeWidget(page)
+ stackedWidget.insertWidget(newRow, page)
+ componentList.setCurrentRow(newRow)
+ stackedWidget.setCurrentIndex(newRow)
+ self.drawPreview()
+
+ def moveComponentTop(self):
+ componentList = self.window.listWidget_componentList
+ row = -componentList.currentRow()
+ self.moveComponent(row)
+
+ def moveComponentBottom(self):
+ componentList = self.window.listWidget_componentList
+ row = len(componentList)-componentList.currentRow()-1
+ self.moveComponent(row)
+
+ def dragComponent(self, event):
+ '''Drop event for the component listwidget'''
+ componentList = self.window.listWidget_componentList
+
+ modelIndexes = [ \
+ componentList.model().index(i) \
+ for i in range(componentList.count()) \
+ ]
+ rects = [ \
+ componentList.visualRect(modelIndex) \
+ for modelIndex in modelIndexes \
+ ]
+
+ rowPos = [rect.contains(event.pos()) for rect in rects]
+ if not any(rowPos):
+ return
+
+ i = rowPos.index(True)
+ change = (componentList.currentRow() - i) * -1
+ self.moveComponent(change)
+
+ def changeComponentWidget(self):
+ selected = self.window.listWidget_componentList.selectedItems()
+ if selected:
+ index = self.window.listWidget_componentList.row(selected[0])
+ self.window.stackedWidget.setCurrentIndex(index)
+
+ def openPresetManager(self):
+ '''Preset manager for importing, exporting, renaming, deleting'''
+ self.presetManager.show()
+
+ def clear(self):
+ '''Get a blank slate'''
+ self.core.clearComponents()
+ self.window.listWidget_componentList.clear()
+ for widget in self.pages:
+ self.window.stackedWidget.removeWidget(widget)
+ self.pages = []
+
+ def createNewProject(self):
+ self.openSaveChangesDialog('starting a new project')
+
+ self.clear()
+ self.currentProject = None
+ self.settings.setValue("currentProject", None)
+ self.drawPreview(True)
+ self.updateWindowTitle()
+
+ def saveCurrentProject(self):
+ if self.currentProject:
+ self.core.createProjectFile(self.currentProject)
+ else:
+ self.openSaveProjectDialog()
+
+ def openSaveChangesDialog(self, phrase):
+ success = True
+ if self.autosaveExists(identical=False):
+ ch = self.showMessage(
+ msg="You have unsaved changes in project '%s'. "
+ "Save before %s?" % \
+ (os.path.basename(self.currentProject)[:-4],
+ phrase),
+ showCancel=True)
+ if ch:
+ success = self.saveProjectChanges()
+
+ if success and os.path.exists(self.autosavePath):
+ os.remove(self.autosavePath)
+
+ def openSaveProjectDialog(self):
+ filename = QtGui.QFileDialog.getSaveFileName(
+ self.window, "Create Project File",
+ self.settings.value("projectDir"),
+ "Project Files (*.avp)")
+ if not filename:
+ return
+ if not filename.endswith(".avp"):
+ filename += '.avp'
+ self.settings.setValue("projectDir", os.path.dirname(filename))
+ self.settings.setValue("currentProject", filename)
+ self.currentProject = filename
+ self.updateWindowTitle()
+ self.core.createProjectFile(filename)
+
+ def openOpenProjectDialog(self):
+ filename = QtGui.QFileDialog.getOpenFileName(
+ self.window, "Open Project File",
+ self.settings.value("projectDir"),
+ "Project Files (*.avp)")
+ self.openProject(filename)
+
+ def openProject(self, filepath, prompt=True):
+ if not filepath or not os.path.exists(filepath) \
+ or not filepath.endswith('.avp'):
+ self.updateWindowTitle()
+ return
+
+ self.clear()
+ # ask to save any changes that are about to get deleted
+ if prompt:
+ self.openSaveChangesDialog('opening another project')
+
+ self.currentProject = filepath
+ self.updateWindowTitle()
+ self.settings.setValue("currentProject", filepath)
+ 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)
+
+ def showMessage(self, **kwargs):
+ parent = kwargs['parent'] if 'parent' in kwargs else self.window
+ msg = QtGui.QMessageBox(parent)
+ msg.setModal(True)
+ msg.setText(kwargs['msg'])
+ msg.setIcon(
+ kwargs['icon'] if 'icon' in kwargs else QtGui.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)
+ else:
+ msg.setStandardButtons(QtGui.QMessageBox.Ok)
+ ch = msg.exec_()
+ if ch == 1024:
+ return True
+ return False
+
+ def componentContextMenu(self, QPos):
+ '''Appears when right-clicking a component in the list'''
+ componentList = self.window.listWidget_componentList
+ if not componentList.selectedItems():
+ return
+
+ # don't show menu if clicking empty space
+ parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0))
+ index = componentList.currentRow()
+ modelIndex = componentList.model().index(index)
+ if not componentList.visualRect(modelIndex).contains(QPos):
+ return
+
+ self.presetManager.findPresets()
+ self.menu = QtGui.QMenu()
+ menuItem = self.menu.addAction("Save Preset")
+ menuItem.triggered.connect(
+ self.presetManager.openSavePresetDialog
+ )
+
+ # submenu for opening presets
+ try:
+ presets = self.presetManager.presets[str(self.core.selectedComponents[index])]
+ self.submenu = QtGui.QMenu("Open Preset")
+ self.menu.addMenu(self.submenu)
+
+ for version, presetName in presets:
+ menuItem = self.submenu.addAction(presetName)
+ menuItem.triggered.connect(
+ lambda _, presetName=presetName:
+ self.presetManager.openPreset(presetName)
+ )
+ except KeyError:
+ pass
+
+ if self.core.selectedComponents[index].currentPreset:
+ menuItem = self.menu.addAction("Clear Preset")
+ menuItem.triggered.connect(
+ self.presetManager.clearPreset
+ )
+
+ self.menu.move(parentPosition + QPos)
+ self.menu.show()
diff --git a/src/mainwindow.ui b/src/mainwindow.ui
new file mode 100644
index 0000000..4a12fd5
--- /dev/null
+++ b/src/mainwindow.ui
@@ -0,0 +1,809 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>1008</width>
+ <height>575</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>MainWindow</string>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>9</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::MinimumExpanding</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>360</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_previewWrapper">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetDefaultConstraint</enum>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <spacer name="horizontalSpacer_previewSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::MinimumExpanding</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>420</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMinimumSize</enum>
+ </property>
+ <property name="leftMargin">
+ <number>3</number>
+ </property>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMinimumSize</enum>
+ </property>
+ <property name="leftMargin">
+ <number>3</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_16">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMinimumSize</enum>
+ </property>
+ <item>
+ <spacer name="horizontalSpacer_6">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>140</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_projects">
+ <property name="text">
+ <string>Projects</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_presets">
+ <property name="text">
+ <string>Presets</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Minimum</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>2</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QListWidget" name="listWidget_componentList">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="acceptDrops">
+ <bool>true</bool>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <property name="lineWidth">
+ <number>1</number>
+ </property>
+ <property name="tabKeyNavigation">
+ <bool>true</bool>
+ </property>
+ <property name="dragEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="dragDropOverwriteMode">
+ <bool>false</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::InternalMove</enum>
+ </property>
+ <property name="defaultDropAction">
+ <enum>Qt::MoveAction</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_14">
+ <item>
+ <widget class="QPushButton" name="pushButton_addComponent">
+ <property name="text">
+ <string>Add</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_removeComponent">
+ <property name="text">
+ <string>Remove</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_listMoveUp">
+ <property name="text">
+ <string>Up</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_listMoveDown">
+ <property name="text">
+ <string>Down</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_7">
+ <property name="leftMargin">
+ <number>4</number>
+ </property>
+ <property name="rightMargin">
+ <number>2</number>
+ </property>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetFixedSize</enum>
+ </property>
+ <property name="topMargin">
+ <number>4</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>500</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>180</height>
+ </size>
+ </property>
+ <property name="tabPosition">
+ <enum>QTabWidget::North</enum>
+ </property>
+ <property name="tabShape">
+ <enum>QTabWidget::Rounded</enum>
+ </property>
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab_exportVideo">
+ <attribute name="title">
+ <string>Export Video</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_10">
+ <property name="margin">
+ <number>10</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_audioFile">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>85</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>80</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>80</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Audio File</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_audioFile">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>28</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>28</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="toolButton_selectAudioFile">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>28</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>28</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_11">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_6">
+ <item>
+ <widget class="QLabel" name="label_outputFile">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>85</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Output File</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_outputFile">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>28</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>28</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="toolButton_selectOutputFile">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>28</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>28</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <property name="margin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QProgressBar" name="progressBar_createVideo">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="value">
+ <number>24</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Minimum</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>10</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_createVideo">
+ <property name="text">
+ <string>Create Video</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_Cancel">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Cancel</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ <zorder></zorder>
+ <zorder></zorder>
+ </widget>
+ <widget class="QWidget" name="tab_encoderSettings">
+ <attribute name="title">
+ <string>Encoder Settings</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_9">
+ <property name="margin">
+ <number>10</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_13">
+ <item>
+ <widget class="QLabel" name="label_videoFormat">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>85</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Container</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_videoContainer">
+ <property name="minimumSize">
+ <size>
+ <width>150</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_5">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Minimum</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>5</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_videoPreset">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Resolution</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_resolution">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_10">
+ <item>
+ <widget class="QLabel" name="label_videoCodec">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>85</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Video Codec</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_videoCodec">
+ <property name="minimumSize">
+ <size>
+ <width>150</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>5</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_resolution">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Video Bitrate (Kbps)</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_vBitrate">
+ <property name="maximum">
+ <number>99999</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_11">
+ <item>
+ <widget class="QLabel" name="label_audioCodec">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>85</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Audio Codec</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_audioCodec">
+ <property name="minimumSize">
+ <size>
+ <width>150</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>5</width>
+ <height>10</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_audioBitrate">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Audio Bitrate (Kbps)</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBox_aBitrate">
+ <property name="maximum">
+ <number>9999</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetDefaultConstraint</enum>
+ </property>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::MinimumExpanding</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>500</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QStackedWidget" name="stackedWidget">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>180</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>180</height>
+ </size>
+ </property>
+ <property name="currentIndex">
+ <number>-1</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/presetmanager.py b/src/presetmanager.py
new file mode 100644
index 0000000..ec3f5cd
--- /dev/null
+++ b/src/presetmanager.py
@@ -0,0 +1,290 @@
+from PyQt5 import QtGui, QtCore, QtWidgets
+import string
+import os
+
+import core
+
+
+class PresetManager(QtWidgets.QDialog):
+ def __init__(self, window, parent):
+ super().__init__(parent.window)
+ self.parent = parent
+ self.core = parent.core
+ self.settings = parent.settings
+ self.presetDir = self.core.presetDir
+ if not self.settings.value('presetDir'):
+ self.settings.setValue(
+ "presetDir",
+ os.path.join(self.core.dataDir, 'projects'))
+
+ self.findPresets()
+
+ # window
+ self.lastFilter = '*'
+ 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_import.clicked.connect(self.openImportDialog)
+ self.window.pushButton_export.clicked.connect(self.openExportDialog)
+ self.window.pushButton_close.clicked.connect(self.window.close)
+
+ # create filter box and preset list
+ self.drawFilterList()
+ self.window.comboBox_filter.currentIndexChanged.connect(
+ lambda: self.drawPresetList(
+ self.window.comboBox_filter.currentText(), self.window.lineEdit_search.text()
+ )
+ )
+
+ # make auto-completion for search bar
+ self.autocomplete = QtCore.QStringListModel()
+ completer = QtWidgets.QCompleter()
+ completer.setModel(self.autocomplete)
+ 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.drawPresetList('*')
+
+ def show(self):
+ '''Open a new preset manager window from the mainwindow'''
+ self.findPresets()
+ self.drawFilterList()
+ self.drawPresetList('*')
+ self.window.show()
+
+ def findPresets(self):
+ parseList = []
+ for dirpath, dirnames, filenames in os.walk(self.presetDir):
+ # anything without a subdirectory must be a preset folder
+ if dirnames:
+ continue
+ for preset in filenames:
+ compName = os.path.basename(os.path.dirname(dirpath))
+ compVers = os.path.basename(dirpath)
+ try:
+ 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 \
+ }
+
+ def drawPresetList(self, compFilter=None, presetFilter=''):
+ self.window.listWidget_presets.clear()
+ if compFilter:
+ self.lastFilter = str(compFilter)
+ else:
+ compFilter = str(self.lastFilter)
+ self.presetRows = []
+ presetNames = []
+ for component, presets in self.presets.items():
+ if compFilter != '*' and component != compFilter:
+ continue
+ for vers, preset in presets:
+ if not presetFilter or presetFilter in preset:
+ self.window.listWidget_presets.addItem('%s: %s' % (component, preset))
+ self.presetRows.append((component, vers, preset))
+ if preset not in presetNames:
+ presetNames.append(preset)
+ self.autocomplete.setStringList(presetNames)
+
+ def drawFilterList(self):
+ self.window.comboBox_filter.clear()
+ self.window.comboBox_filter.addItem('*')
+ for component in self.presets:
+ self.window.comboBox_filter.addItem(component)
+
+ def clearPreset(self, compI=None):
+ '''Functions on mainwindow level from the context menu'''
+ compI = self.parent.window.listWidget_componentList.currentRow()
+ self.core.clearPreset(compI, self.parent)
+
+ def openSavePresetDialog(self):
+ '''Functions on mainwindow level from the context menu'''
+ window = self.parent.window
+ selectedComponents = self.core.selectedComponents
+ componentList = self.parent.window.listWidget_componentList
+
+ if componentList.currentRow() == -1:
+ return
+ while True:
+ index = componentList.currentRow()
+ currentPreset = selectedComponents[index].currentPreset
+ newName, OK = QtGui.QInputDialog.getText(
+ self.parent.window,
+ 'Audio Visualizer',
+ 'New Preset Name:',
+ QtGui.QLineEdit.Normal,
+ currentPreset
+ )
+ if OK:
+ if core.Core.badName(newName):
+ self.warnMessage(self.parent.window)
+ continue
+ if newName:
+ if index != -1:
+ selectedComponents[index].currentPreset = newName
+ saveValueStore = \
+ selectedComponents[index].savePreset()
+ componentName = str(selectedComponents[index]).strip()
+ vers = selectedComponents[index].version()
+ self.createNewPreset(
+ componentName, vers, newName,
+ saveValueStore, window=self.parent.window)
+ self.openPreset(newName)
+ break
+
+ def createNewPreset(
+ self, compName, vers, filename, saveValueStore, **kwargs):
+ path = os.path.join(self.presetDir, compName, str(vers), filename)
+ if self.presetExists(path, **kwargs):
+ return
+ self.core.createPresetFile(compName, vers, filename, saveValueStore)
+
+ def presetExists(self, path, **kwargs):
+ if os.path.exists(path):
+ window = self.window \
+ if 'window' not in kwargs else kwargs['window']
+ ch = self.parent.showMessage(
+ msg="%s already exists! Overwrite it?" %
+ os.path.basename(path),
+ showCancel=True,
+ icon=QtGui.QMessageBox.Warning,
+ parent=window)
+ if not ch:
+ # user clicked cancel
+ return True
+
+ return False
+
+ def openPreset(self, presetName):
+ componentList = self.parent.window.listWidget_componentList
+ selectedComponents = self.parent.core.selectedComponents
+
+ index = componentList.currentRow()
+ if index == -1:
+ return
+ componentName = str(selectedComponents[index]).strip()
+ 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)
+
+ self.parent.updateComponentTitle(index)
+ self.parent.drawPreview()
+
+ def openDeletePresetDialog(self):
+ selected = self.window.listWidget_presets.selectedItems()
+ if not selected:
+ return
+ row = self.window.listWidget_presets.row(selected[0])
+ comp, vers, name = self.presetRows[row]
+ ch = self.parent.showMessage(
+ msg='Really delete %s?' % name,
+ showCancel=True,
+ icon=QtGui.QMessageBox.Warning,
+ parent=self.window
+ )
+ if not ch:
+ return
+ self.deletePreset(comp, vers, name)
+ self.findPresets()
+ self.drawPresetList()
+
+ def deletePreset(self, comp, vers, name):
+ filepath = os.path.join(self.presetDir, comp, str(vers), name)
+ os.remove(filepath)
+
+ def warnMessage(self, window=None):
+ print(window)
+ self.parent.showMessage(
+ msg='Preset names must contain only letters, '
+ 'numbers, and spaces.',
+ parent=window if window else self.window)
+
+ def openRenamePresetDialog(self):
+ presetList = self.window.listWidget_presets
+ if presetList.currentRow() == -1:
+ return
+
+ while True:
+ index = presetList.currentRow()
+ newName, OK = QtGui.QInputDialog.getText(
+ self.window,
+ 'Preset Manager',
+ 'Rename Preset:',
+ QtGui.QLineEdit.Normal,
+ self.presetRows[index][2]
+ )
+ if OK:
+ if core.Core.badName(newName):
+ self.warnMessage()
+ continue
+ if newName:
+ comp, vers, oldName = self.presetRows[index]
+ path = os.path.join(
+ self.presetDir, comp, str(vers))
+ newPath = os.path.join(path, newName)
+ oldPath = os.path.join(path, oldName)
+ if self.presetExists(newPath):
+ return
+ if os.path.exists(newPath):
+ os.remove(newPath)
+ os.rename(oldPath, newPath)
+ self.findPresets()
+ self.drawPresetList()
+ break
+
+ def openImportDialog(self):
+ filename = QtGui.QFileDialog.getOpenFileName(
+ self.window, "Import Preset File",
+ self.settings.value("presetDir"),
+ "Preset Files (*.avl)")
+ if filename:
+ # get installed path & ask user to overwrite if needed
+ path = ''
+ while True:
+ if path:
+ if self.presetExists(path):
+ break
+ else:
+ if os.path.exists(path):
+ os.remove(path)
+ success, path = self.core.importPreset(filename)
+ if success:
+ break
+
+ self.findPresets()
+ self.drawPresetList()
+ self.settings.setValue("presetDir", os.path.dirname(filename))
+
+ def openExportDialog(self):
+ if not self.window.listWidget_presets.selectedItems():
+ return
+ filename = QtGui.QFileDialog.getSaveFileName(
+ self.window, "Export Preset",
+ self.settings.value("presetDir"),
+ "Preset Files (*.avl)")
+ if filename:
+ index = self.window.listWidget_presets.currentRow()
+ comp, vers, name = self.presetRows[index]
+ if not self.core.exportPreset(filename, comp, vers, name):
+ self.parent.showMessage(
+ msg='Couldn\'t export %s.' % filename,
+ parent=self.window
+ )
+ self.settings.setValue("presetDir", os.path.dirname(filename))
diff --git a/src/presetmanager.ui b/src/presetmanager.ui
new file mode 100644
index 0000000..5257b1c
--- /dev/null
+++ b/src/presetmanager.ui
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>presetmanager</class>
+ <widget class="QWidget" name="presetmanager">
+ <property name="windowModality">
+ <enum>Qt::NonModal</enum>
+ </property>
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>497</width>
+ <height>377</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Preset Manager</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QLineEdit" name="lineEdit_search">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="placeholderText">
+ <string>Filter by name</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="comboBox_filter">
+ <property name="minimumSize">
+ <size>
+ <width>200</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QListWidget" name="listWidget_presets">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="tabKeyNavigation">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMinimumSize</enum>
+ </property>
+ <item>
+ <widget class="QPushButton" name="pushButton_import">
+ <property name="text">
+ <string>Import</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_export">
+ <property name="text">
+ <string>Export</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_rename">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Rename</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_delete">
+ <property name="text">
+ <string>Delete</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <item alignment="Qt::AlignRight">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:10pt; font-style:italic;&quot;&gt;Right-click components in the main window to create presets&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_close">
+ <property name="text">
+ <string>Close</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/preview_thread.py b/src/preview_thread.py
new file mode 100644
index 0000000..4a46d51
--- /dev/null
+++ b/src/preview_thread.py
@@ -0,0 +1,59 @@
+from PyQt5 import QtCore, QtGui, uic
+from PyQt5.QtCore import pyqtSignal, pyqtSlot
+from PIL import Image
+from PIL.ImageQt import ImageQt
+import core
+from queue import Queue, Empty
+import os
+from copy import copy
+
+
+class Worker(QtCore.QObject):
+
+ imageCreated = pyqtSignal(['QImage'])
+
+ def __init__(self, parent=None, queue=None):
+ QtCore.QObject.__init__(self)
+ parent.newTask.connect(self.createPreviewImage)
+ parent.processTask.connect(self.process)
+ self.parent = parent
+ self.core = core.Core()
+ self.queue = queue
+ self.core.settings = parent.settings
+ self.stackedWidget = parent.window.stackedWidget
+ self.background = Image.new("RGBA", (1920, 1080), (0, 0, 0, 0))
+ self.background.paste(Image.open(os.path.join(
+ self.core.wd, "background.png")))
+
+ @pyqtSlot(list)
+ def createPreviewImage(self, components):
+ dic = {
+ "components": components,
+ }
+ self.queue.put(dic)
+
+ @pyqtSlot()
+ def process(self):
+ try:
+ nextPreviewInformation = self.queue.get(block=False)
+ while self.queue.qsize() >= 2:
+ try:
+ self.queue.get(block=False)
+ except Empty:
+ continue
+
+ width = int(self.core.settings.value('outputWidth'))
+ height = int(self.core.settings.value('outputHeight'))
+ frame = copy(self.background)
+ frame = frame.resize((width, height))
+
+ components = nextPreviewInformation["components"]
+ for component in reversed(components):
+ frame = Image.alpha_composite(
+ frame, component.previewRender(self))
+
+ self._image = ImageQt(frame)
+ self.imageCreated.emit(QtGui.QImage(self._image))
+
+ except Empty:
+ True
diff --git a/src/video_thread.py b/src/video_thread.py
new file mode 100644
index 0000000..5ea6d21
--- /dev/null
+++ b/src/video_thread.py
@@ -0,0 +1,309 @@
+from PyQt5 import QtCore, QtGui, uic
+from PyQt5.QtCore import pyqtSignal, pyqtSlot
+from PIL import Image, ImageDraw, ImageFont
+from PIL.ImageQt import ImageQt
+import core
+import numpy
+import subprocess as sp
+import sys
+import os
+from queue import Queue, PriorityQueue
+from threading import Thread, Event
+import time
+from copy import copy
+import signal
+
+
+class Worker(QtCore.QObject):
+
+ imageCreated = pyqtSignal(['QImage'])
+ videoCreated = pyqtSignal()
+ progressBarUpdate = pyqtSignal(int)
+ progressBarSetText = pyqtSignal(str)
+ encoding = pyqtSignal(bool)
+
+ def __init__(self, parent=None):
+ QtCore.QObject.__init__(self)
+ self.core = core.Core()
+ self.core.settings = parent.settings
+ self.modules = parent.core.modules
+ self.parent = parent
+ parent.videoTask.connect(self.createVideo)
+ self.sampleSize = 1470 # 44100 / 30 = 1470
+ self.canceled = False
+ self.error = False
+ self.stopped = False
+
+ def renderNode(self):
+ while not self.stopped:
+ i = self.compositeQueue.get()
+ frame = None
+
+ for compNo, comp in reversed(list(enumerate(self.components))):
+ if compNo in self.staticComponents and \
+ self.staticComponents[compNo] is not None:
+ if frame is None:
+ frame = self.staticComponents[compNo]
+ else:
+ frame = Image.alpha_composite(
+ frame, self.staticComponents[compNo])
+ else:
+ if frame is None:
+ frame = comp.frameRender(compNo, i[0], i[1])
+ else:
+ frame = Image.alpha_composite(
+ frame, comp.frameRender(compNo, i[0], i[1]))
+
+ self.renderQueue.put([i[0], frame])
+ self.compositeQueue.task_done()
+
+ def renderDispatch(self):
+ print('Dispatching Frames for Compositing...')
+
+ for i in range(0, len(self.completeAudioArray), self.sampleSize):
+ self.compositeQueue.put([i, self.bgI])
+ # increment tracked video frame for next iteration
+ self.bgI += 1
+
+ def previewDispatch(self):
+ background = Image.new("RGBA", (1920, 1080), (0, 0, 0, 0))
+ background.paste(Image.open(os.path.join(
+ self.core.wd, "background.png")))
+ background = background.resize((self.width, self.height))
+
+ while not self.stopped:
+ i = self.previewQueue.get()
+ if time.time() - self.lastPreview >= 0.06 or i[0] == 0:
+ image = copy(background)
+ image = Image.alpha_composite(image, i[1])
+ self._image = ImageQt(image)
+ self.imageCreated.emit(QtGui.QImage(self._image))
+ self.lastPreview = time.time()
+
+ self.previewQueue.task_done()
+
+ @pyqtSlot(str, str, list)
+ def createVideo(self, inputFile, outputFile, components):
+ self.encoding.emit(True)
+ self.components = components
+ self.outputFile = outputFile
+ self.bgI = 0 # tracked video frame
+ self.reset()
+ self.width = int(self.core.settings.value('outputWidth'))
+ self.height = int(self.core.settings.value('outputHeight'))
+ progressBarValue = 0
+ self.progressBarUpdate.emit(progressBarValue)
+
+ self.progressBarSetText.emit('Loading audio file...')
+ self.completeAudioArray = self.core.readAudioFile(inputFile, self)
+
+ # test if user has libfdk_aac
+ encoders = sp.check_output(
+ self.core.FFMPEG_BIN + " -encoders -hide_banner",
+ shell=True)
+
+ encoders = encoders.decode("utf-8")
+
+ acodec = self.core.settings.value('outputAudioCodec')
+
+ options = self.core.encoder_options
+ containerName = self.core.settings.value('outputContainer')
+ vcodec = self.core.settings.value('outputVideoCodec')
+ vbitrate = str(self.core.settings.value('outputVideoBitrate'))+'k'
+ acodec = self.core.settings.value('outputAudioCodec')
+ abitrate = str(self.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]
+
+ #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
+
+ ffmpegCommand = [
+ self.core.FFMPEG_BIN,
+ '-thread_queue_size', '512',
+ '-y', # overwrite the output file if it already exists.
+ '-f', 'rawvideo',
+ '-vcodec', 'rawvideo',
+ '-s', str(self.width)+'x'+str(self.height), # size of one frame
+ '-pix_fmt', 'rgba',
+
+ # frames per second
+ '-r', self.core.settings.value('outputFrameRate'),
+ '-i', '-', # The input comes from a pipe
+ '-an',
+ '-i', inputFile,
+ '-vcodec', vencoder,
+ '-acodec', aencoder, # output audio codec
+ '-b:v', vbitrate,
+ '-b:a', abitrate,
+ '-pix_fmt', self.core.settings.value('outputVideoFormat'),
+ '-preset', self.core.settings.value('outputPreset'),
+ '-f', container
+ ]
+
+ if acodec == 'aac':
+ ffmpegCommand.append('-strict')
+ ffmpegCommand.append('-2')
+
+ ffmpegCommand.append(outputFile)
+
+ # ### Now start creating video for output ###
+ 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))
+ ]))
+ self.staticComponents = {}
+ numComps = len(self.components)
+ for compNo, comp in enumerate(self.components):
+ pStr = "Analyzing audio..."
+ self.progressBarSetText.emit(pStr)
+ properties = None
+ properties = comp.preFrameRender(
+ worker=self,
+ completeAudioArray=self.completeAudioArray,
+ sampleSize=self.sampleSize,
+ progressBarUpdate=self.progressBarUpdate,
+ progressBarSetText=self.progressBarSetText
+ )
+
+ if properties and 'static' in properties:
+ self.staticComponents[compNo] = copy(
+ comp.frameRender(compNo, 0, 0))
+ self.progressBarUpdate.emit(100)
+
+ # Create ffmpeg pipe and queues for frames
+ self.out_pipe = sp.Popen(
+ ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout)
+ self.compositeQueue = Queue()
+ self.compositeQueue.maxsize = 20
+ self.renderQueue = PriorityQueue()
+ self.renderQueue.maxsize = 20
+ self.previewQueue = PriorityQueue()
+
+ # Threads to render frames and send them back here for piping out
+ self.renderThreads = []
+ for i in range(3):
+ self.renderThreads.append(
+ Thread(target=self.renderNode, name="Render Thread"))
+ self.renderThreads[i].daemon = True
+ self.renderThreads[i].start()
+
+ self.dispatchThread = Thread(
+ target=self.renderDispatch, name="Render Dispatch Thread")
+ self.dispatchThread.daemon = True
+ self.dispatchThread.start()
+
+ self.previewDispatch = Thread(
+ target=self.previewDispatch, name="Render Dispatch Thread")
+ self.previewDispatch.daemon = True
+ self.previewDispatch.start()
+
+ frameBuffer = {}
+ self.lastPreview = 0.0
+ self.progressBarUpdate.emit(0)
+ pStr = "Exporting video..."
+ self.progressBarSetText.emit(pStr)
+ if not self.canceled:
+ for i in range(0, len(self.completeAudioArray), self.sampleSize):
+ while True:
+ if i in frameBuffer or self.canceled:
+ # if frame's in buffer, pipe it to ffmpeg
+ break
+ # else fetch the next frame & add to the buffer
+ data = self.renderQueue.get()
+ frameBuffer[data[0]] = data[1]
+ self.renderQueue.task_done()
+ if self.canceled:
+ break
+
+ try:
+ self.out_pipe.stdin.write(frameBuffer[i].tobytes())
+ self.previewQueue.put([i, frameBuffer[i]])
+ del frameBuffer[i]
+ except:
+ break
+
+ # increase progress bar value
+ if progressBarValue + 1 <= (i / len(self.completeAudioArray)) \
+ * 100:
+ progressBarValue = numpy.floor(
+ (i / len(self.completeAudioArray)) * 100)
+ self.progressBarUpdate.emit(progressBarValue)
+ pStr = "Exporting video: " + str(int(progressBarValue)) \
+ + "%"
+ self.progressBarSetText.emit(pStr)
+
+ numpy.seterr(all='print')
+
+ self.out_pipe.stdin.close()
+ if self.out_pipe.stderr is not None:
+ print(self.out_pipe.stderr.read())
+ self.out_pipe.stderr.close()
+ self.error = True
+ # out_pipe.terminate() # don't terminate ffmpeg too early
+ self.out_pipe.wait()
+ if self.canceled:
+ print("Export Canceled")
+ try:
+ os.remove(self.outputFile)
+ except:
+ pass
+ self.progressBarUpdate.emit(0)
+ self.progressBarSetText.emit('Export Canceled')
+
+ else:
+ if self.error:
+ print("Export Failed")
+ self.progressBarUpdate.emit(0)
+ self.progressBarSetText.emit('Export Failed')
+ else:
+ print("Export Complete")
+ self.progressBarUpdate.emit(100)
+ self.progressBarSetText.emit('Export Complete')
+
+ self.error = False
+ self.canceled = False
+ self.stopped = True
+ self.encoding.emit(False)
+ self.videoCreated.emit()
+
+ def updateProgress(self, pStr, pVal):
+ self.progressBarValue.emit(pVal)
+ self.progressBarSetText.emit(pStr)
+
+ def cancel(self):
+ self.canceled = True
+ self.core.cancel()
+
+ for comp in self.components:
+ comp.cancel()
+
+ try:
+ self.out_pipe.send_signal(signal.SIGINT)
+ except:
+ pass
+
+ def reset(self):
+ self.core.reset()
+ self.canceled = False
+ for comp in self.components:
+ comp.reset()