From e92e9d79f95ad67e83074ef318278c3486601eac Mon Sep 17 00:00:00 2001
From: DH4
Date: Fri, 23 Jun 2017 17:38:05 -0500
Subject: QT5 Conversion + Directory Structure
---
background.png | Bin 45367 -> 0 bytes
command.py | 126 -------
components/__base__.py | 153 ---------
components/__init__.py | 1 -
components/color.py | 246 --------------
components/color.ui | 660 ------------------------------------
components/image.py | 111 -------
components/image.ui | 259 ---------------
components/original.py | 204 ------------
components/original.ui | 108 ------
components/text.py | 176 ----------
components/text.ui | 316 ------------------
components/video.py | 273 ---------------
components/video.ui | 266 ---------------
core.py | 476 --------------------------
encoder-options.json | 130 --------
freeze.py | 51 +++
main.py | 88 -----
mainwindow.py | 721 ----------------------------------------
mainwindow.ui | 809 ---------------------------------------------
presetmanager.py | 290 ----------------
presetmanager.ui | 150 ---------
preview_thread.py | 59 ----
setup.py | 70 ++--
src/background.png | Bin 0 -> 45367 bytes
src/command.py | 126 +++++++
src/components/__base__.py | 153 +++++++++
src/components/__init__.py | 1 +
src/components/color.py | 246 ++++++++++++++
src/components/color.ui | 660 ++++++++++++++++++++++++++++++++++++
src/components/image.py | 111 +++++++
src/components/image.ui | 259 +++++++++++++++
src/components/original.py | 204 ++++++++++++
src/components/original.ui | 108 ++++++
src/components/text.py | 176 ++++++++++
src/components/text.ui | 316 ++++++++++++++++++
src/components/video.py | 273 +++++++++++++++
src/components/video.ui | 266 +++++++++++++++
src/core.py | 477 ++++++++++++++++++++++++++
src/encoder-options.json | 130 ++++++++
src/main.py | 88 +++++
src/mainwindow.py | 718 ++++++++++++++++++++++++++++++++++++++++
src/mainwindow.ui | 809 +++++++++++++++++++++++++++++++++++++++++++++
src/presetmanager.py | 290 ++++++++++++++++
src/presetmanager.ui | 150 +++++++++
src/preview_thread.py | 59 ++++
src/video_thread.py | 309 +++++++++++++++++
video_thread.py | 309 -----------------
48 files changed, 5999 insertions(+), 5982 deletions(-)
delete mode 100644 background.png
delete mode 100644 command.py
delete mode 100644 components/__base__.py
delete mode 100644 components/__init__.py
delete mode 100644 components/color.py
delete mode 100644 components/color.ui
delete mode 100644 components/image.py
delete mode 100644 components/image.ui
delete mode 100644 components/original.py
delete mode 100644 components/original.ui
delete mode 100644 components/text.py
delete mode 100644 components/text.ui
delete mode 100644 components/video.py
delete mode 100644 components/video.ui
delete mode 100644 core.py
delete mode 100644 encoder-options.json
create mode 100644 freeze.py
delete mode 100644 main.py
delete mode 100644 mainwindow.py
delete mode 100644 mainwindow.ui
delete mode 100644 presetmanager.py
delete mode 100644 presetmanager.ui
delete mode 100644 preview_thread.py
create mode 100644 src/background.png
create mode 100644 src/command.py
create mode 100644 src/components/__base__.py
create mode 100644 src/components/__init__.py
create mode 100644 src/components/color.py
create mode 100644 src/components/color.ui
create mode 100644 src/components/image.py
create mode 100644 src/components/image.ui
create mode 100644 src/components/original.py
create mode 100644 src/components/original.ui
create mode 100644 src/components/text.py
create mode 100644 src/components/text.ui
create mode 100644 src/components/video.py
create mode 100644 src/components/video.ui
create mode 100644 src/core.py
create mode 100644 src/encoder-options.json
create mode 100644 src/main.py
create mode 100644 src/mainwindow.py
create mode 100644 src/mainwindow.ui
create mode 100644 src/presetmanager.py
create mode 100644 src/presetmanager.ui
create mode 100644 src/preview_thread.py
create mode 100644 src/video_thread.py
delete mode 100644 video_thread.py
diff --git a/background.png b/background.png
deleted file mode 100644
index fb58593..0000000
Binary files a/background.png and /dev/null differ
diff --git a/command.py b/command.py
deleted file mode 100644
index 1a1e810..0000000
--- a/command.py
+++ /dev/null
@@ -1,126 +0,0 @@
-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/components/__base__.py b/components/__base__.py
deleted file mode 100644
index bef7f0e..0000000
--- a/components/__base__.py
+++ /dev/null
@@ -1,153 +0,0 @@
-from PyQt4 import QtGui, QtCore
-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/components/__init__.py b/components/__init__.py
deleted file mode 100644
index 8b13789..0000000
--- a/components/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/components/color.py b/components/color.py
deleted file mode 100644
index 5ffcdea..0000000
--- a/components/color.py
+++ /dev/null
@@ -1,246 +0,0 @@
-from PIL import Image, ImageDraw
-from PyQt4 import uic, QtGui, QtCore
-from PyQt4.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/components/color.ui b/components/color.ui
deleted file mode 100644
index a9dacea..0000000
--- a/components/color.ui
+++ /dev/null
@@ -1,660 +0,0 @@
-
-
- Form
-
-
-
- 0
- 0
- 586
- 197
-
-
-
- Form
-
-
- -
-
-
- 4
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 31
- 0
-
-
-
- Color #1
-
-
-
- -
-
-
-
- 32
- 32
-
-
-
-
-
-
-
- 32
- 32
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 1
- 0
-
-
-
- 12
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 5
- 20
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 31
- 0
-
-
-
- Color #2
-
-
-
- -
-
-
-
- 32
- 32
-
-
-
-
-
-
-
- 32
- 32
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 1
- 0
-
-
-
- 12
-
-
-
-
-
- -
-
-
- 0
-
-
-
-
-
-
- 0
- 0
-
-
-
- Width
-
-
- Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 80
- 16777215
-
-
-
-
- 0
- 0
-
-
-
- 0
-
-
- 999999999
-
-
- 0
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Height
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 80
- 16777215
-
-
-
- 999999999
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 5
- 20
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- X
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 80
- 16777215
-
-
-
-
- 0
- 0
-
-
-
- -10000
-
-
- 10000
-
-
- 0
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Y
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 80
- 16777215
-
-
-
- -10000
-
-
- 10000
-
-
-
-
-
- -
-
-
- 0
-
-
-
-
-
-
- 0
- 0
-
-
-
- Fill
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- -1
-
-
- QComboBox::AdjustToContentsOnFirstShow
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Transparent
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Stretch
-
-
-
- -
-
-
-
-
- Pad
-
-
- -
-
- Reflect
-
-
- -
-
- Repeat
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Minimum
-
-
-
- 40
- 20
-
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- 0
-
-
- 2
-
-
-
-
-
-
- -1
- 0
- 561
- 31
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- Start
-
-
-
- -
-
-
- -10000
-
-
- 10000
-
-
- 10
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- End
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- -10000
-
-
- 10000
-
-
- 10
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
-
-
-
-
-
- -1
- -1
- 561
- 31
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Start
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- -10000
-
-
- 10000
-
-
- 10
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- End
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- -10000
-
-
- 10000
-
-
- 10
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Centre
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- QAbstractSpinBox::PlusMinus
-
-
- -10000
-
-
- 10000
-
-
- 3
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/components/image.py b/components/image.py
deleted file mode 100644
index f8ae64e..0000000
--- a/components/image.py
+++ /dev/null
@@ -1,111 +0,0 @@
-from PIL import Image, ImageDraw
-from PyQt4 import uic, QtGui, QtCore
-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/components/image.ui b/components/image.ui
deleted file mode 100644
index 6df03a5..0000000
--- a/components/image.ui
+++ /dev/null
@@ -1,259 +0,0 @@
-
-
- Form
-
-
-
- 0
- 0
- 586
- 197
-
-
-
- Form
-
-
- -
-
-
- 4
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 31
- 0
-
-
-
- Image
-
-
-
- -
-
-
-
- 1
- 0
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 1
- 0
-
-
-
-
- 32
- 32
-
-
-
- ...
-
-
-
- 32
- 32
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 5
- 20
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- X
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 80
- 16777215
-
-
-
- -10000
-
-
- 10000
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Y
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 80
- 16777215
-
-
-
-
- 0
- 0
-
-
-
- -1000
-
-
- 1000
-
-
- 0
-
-
-
-
-
-
-
- -
-
-
-
-
-
- Stretch
-
-
- false
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 5
- 20
-
-
-
-
- -
-
-
- Scale
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- QAbstractSpinBox::UpDownArrows
-
-
- %
-
-
- 10
-
-
- 400
-
-
- 100
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
-
-
-
-
-
diff --git a/components/original.py b/components/original.py
deleted file mode 100644
index 6222157..0000000
--- a/components/original.py
+++ /dev/null
@@ -1,204 +0,0 @@
-import numpy
-from PIL import Image, ImageDraw
-from PyQt4 import uic, QtGui, QtCore
-from PyQt4.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/components/original.ui b/components/original.ui
deleted file mode 100644
index 5808653..0000000
--- a/components/original.ui
+++ /dev/null
@@ -1,108 +0,0 @@
-
-
- Form
-
-
-
- 0
- 0
- 633
- 178
-
-
-
-
- 180
- 0
-
-
-
- Form
-
-
- -
-
-
- 4
-
-
-
-
-
-
- 0
- 0
-
-
-
- Visualizer Layout
-
-
-
- -
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 5
- 20
-
-
-
-
- -
-
-
- Visualizer Color
-
-
-
- -
-
-
-
- 32
- 32
-
-
-
-
-
-
-
- 32
- 32
-
-
-
-
- -
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
-
-
-
-
-
diff --git a/components/text.py b/components/text.py
deleted file mode 100644
index 2375dcd..0000000
--- a/components/text.py
+++ /dev/null
@@ -1,176 +0,0 @@
-from PIL import Image, ImageDraw
-from PyQt4.QtGui import QPainter, QColor, QFont
-from PyQt4 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/components/text.ui b/components/text.ui
deleted file mode 100644
index 05e7f8e..0000000
--- a/components/text.ui
+++ /dev/null
@@ -1,316 +0,0 @@
-
-
- Form
-
-
-
- 0
- 0
- 586
- 197
-
-
-
- Form
-
-
- -
-
-
- 4
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- Font
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 0
- 0
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 5
- 20
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Font Size
-
-
-
- -
-
-
- 500
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- Text Layout
-
-
-
- -
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 5
- 20
-
-
-
-
- -
-
-
- Text Color
-
-
-
- -
-
-
-
- 32
- 32
-
-
-
-
-
-
-
- 32
- 32
-
-
-
-
- -
-
-
-
-
- -
-
-
- 0
-
-
-
-
-
- Title
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 0
- 0
-
-
-
- Testing New GUI
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 5
- 20
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- X
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 80
- 16777215
-
-
-
-
- 0
- 0
-
-
-
- 0
-
-
- 999999999
-
-
- 0
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 5
- 20
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Y
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 80
- 16777215
-
-
-
- 999999999
-
-
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
-
-
-
-
-
diff --git a/components/video.py b/components/video.py
deleted file mode 100644
index 1d250bd..0000000
--- a/components/video.py
+++ /dev/null
@@ -1,273 +0,0 @@
-from PIL import Image, ImageDraw
-from PyQt4 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/components/video.ui b/components/video.ui
deleted file mode 100644
index f05e8a5..0000000
--- a/components/video.ui
+++ /dev/null
@@ -1,266 +0,0 @@
-
-
- Form
-
-
-
- 0
- 0
- 586
- 197
-
-
-
- Form
-
-
- -
-
-
- 4
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 31
- 0
-
-
-
- Video
-
-
-
- -
-
-
-
- 1
- 0
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 1
- 0
-
-
-
-
- 32
- 32
-
-
-
- ...
-
-
-
- 32
- 32
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 5
- 20
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- X
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 80
- 16777215
-
-
-
- -10000
-
-
- 10000
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Y
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 80
- 16777215
-
-
-
-
- 0
- 0
-
-
-
- -10000
-
-
- 10000
-
-
- 0
-
-
-
-
-
-
-
- -
-
-
-
-
-
- Loop
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 5
- 20
-
-
-
-
- -
-
-
- Distort by scale
-
-
-
- -
-
-
- Scale
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- QAbstractSpinBox::UpDownArrows
-
-
- %
-
-
- 10
-
-
- 400
-
-
- 100
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
-
-
-
-
-
diff --git a/core.py b/core.py
deleted file mode 100644
index de6ed99..0000000
--- a/core.py
+++ /dev/null
@@ -1,476 +0,0 @@
-import sys
-import io
-import os
-from PyQt4 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 PyQt4.QtGui import QDesktopServices
-import string
-
-
-class Core():
-
- def __init__(self):
- self.FFMPEG_BIN = self.findFfmpeg()
- self.dataDir = QDesktopServices.storageLocation(
- QDesktopServices.DataLocation)
- 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/encoder-options.json b/encoder-options.json
deleted file mode 100644
index 78bc940..0000000
--- a/encoder-options.json
+++ /dev/null
@@ -1,130 +0,0 @@
-{
- "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/freeze.py b/freeze.py
new file mode 100644
index 0000000..48034dc
--- /dev/null
+++ b/freeze.py
@@ -0,0 +1,51 @@
+from cx_Freeze import setup, Executable
+import sys
+
+# Dependencies are automatically detected, but it might need
+# fine tuning.
+
+buildOptions = dict(
+ packages=[],
+ excludes=[
+ "apport",
+ "apt",
+ "curses",
+ "distutils",
+ "email",
+ "html",
+ "http",
+ "xmlrpc",
+ "nose"
+ ],
+ include_files=[
+ "mainwindow.ui",
+ "presetmanager.ui",
+ "background.png",
+ "encoder-options.json",
+ "components/"
+ ],
+ includes=[
+ 'numpy.core._methods',
+ 'numpy.lib.format'
+ ]
+)
+
+
+base = 'Win32GUI' if sys.platform == 'win32' else None
+
+executables = [
+ Executable(
+ 'main.py',
+ base=base,
+ targetName='audio-visualizer-python'
+ )
+]
+
+
+setup(
+ name='audio-visualizer-python',
+ version='1.0',
+ description='GUI tool to render visualization videos of audio files',
+ options=dict(build_exe=buildOptions),
+ executables=executables
+)
diff --git a/main.py b/main.py
deleted file mode 100644
index 106bd29..0000000
--- a/main.py
+++ /dev/null
@@ -1,88 +0,0 @@
-from PyQt4 import QtGui, uic
-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 = QtGui.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 = QtGui.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/mainwindow.py b/mainwindow.py
deleted file mode 100644
index cdc2a51..0000000
--- a/mainwindow.py
+++ /dev/null
@@ -1,721 +0,0 @@
-from queue import Queue
-from PyQt4 import QtCore, QtGui, uic
-from PyQt4.QtCore import QSettings, Qt
-from PyQt4.QtGui 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(QtGui.QLabel):
- def __init__(self, parent, img):
- super(PreviewWindow, self).__init__()
- self.parent = parent
- self.setFrameStyle(QtGui.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(QtGui.QMainWindow):
-
- newTask = QtCore.pyqtSignal(list)
- processTask = QtCore.pyqtSignal()
- videoTask = QtCore.pyqtSignal(str, str, list)
-
- def __init__(self, window, project):
- QtGui.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.connect(
- componentList,
- QtCore.SIGNAL("customContextMenuRequested(QPoint)"),
- 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
- QtGui.QShortcut("Ctrl+S", self.window, self.saveCurrentProject)
- QtGui.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog)
- QtGui.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog)
- QtGui.QShortcut("Ctrl+N", self.window, self.createNewProject)
-
- QtGui.QShortcut("Ctrl+T", self.window, activated=lambda:
- self.window.pushButton_addComponent.click())
- QtGui.QShortcut("Ctrl+Space", self.window, activated=lambda:
- self.window.listWidget_componentList.setFocus())
- QtGui.QShortcut("Ctrl+Shift+S", self.window,
- self.presetManager.openSavePresetDialog)
- QtGui.QShortcut("Ctrl+Shift+C", self.window,
- self.presetManager.clearPreset)
-
- QtGui.QShortcut("Ctrl+Up", self.window,
- activated=lambda: self.moveComponent(-1))
- QtGui.QShortcut("Ctrl+Down", self.window,
- activated=lambda: self.moveComponent(1))
- QtGui.QShortcut("Ctrl+Home", self.window, self.moveComponentTop)
- QtGui.QShortcut("Ctrl+End", self.window, self.moveComponentBottom)
- QtGui.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/mainwindow.ui b/mainwindow.ui
deleted file mode 100644
index 4a12fd5..0000000
--- a/mainwindow.ui
+++ /dev/null
@@ -1,809 +0,0 @@
-
-
- MainWindow
-
-
-
- 0
- 0
- 1008
- 575
-
-
-
-
- 0
- 0
-
-
-
-
- 0
- 0
-
-
-
- MainWindow
-
-
-
-
- 0
- 0
-
-
-
- false
-
-
-
- 9
-
-
- 0
-
- -
-
-
-
-
-
- Qt::Vertical
-
-
- QSizePolicy::MinimumExpanding
-
-
-
- 0
- 360
-
-
-
-
- -
-
-
- QLayout::SetDefaultConstraint
-
-
- 0
-
-
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::MinimumExpanding
-
-
-
- 420
- 0
-
-
-
-
-
-
- -
-
-
- QLayout::SetMinimumSize
-
-
- 3
-
-
-
-
-
- QLayout::SetMinimumSize
-
-
- 3
-
-
-
-
-
- QLayout::SetMinimumSize
-
-
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 140
- 20
-
-
-
-
- -
-
-
- Projects
-
-
-
- -
-
-
- Presets
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Minimum
-
-
-
- 20
- 2
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 0
- 0
-
-
-
-
- 16777215
- 16777215
-
-
-
- true
-
-
- QFrame::StyledPanel
-
-
- QFrame::Sunken
-
-
- 1
-
-
- true
-
-
- true
-
-
- false
-
-
- QAbstractItemView::InternalMove
-
-
- Qt::MoveAction
-
-
-
- -
-
-
-
-
-
- Add
-
-
-
- -
-
-
- Remove
-
-
-
- -
-
-
- Up
-
-
-
- -
-
-
- Down
-
-
-
-
-
-
-
- -
-
-
- 4
-
-
- 2
-
-
-
-
-
-
-
- -
-
-
- QLayout::SetFixedSize
-
-
- 4
-
-
- 0
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 500
- 0
-
-
-
-
- 16777215
- 180
-
-
-
- QTabWidget::North
-
-
- QTabWidget::Rounded
-
-
- 0
-
-
-
- Export Video
-
-
-
- 10
-
-
-
-
-
- 0
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 85
- 0
-
-
-
-
- 80
- 16777215
-
-
-
-
- 80
- 0
-
-
-
- Audio File
-
-
- Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 0
- 28
-
-
-
-
- 16777215
- 28
-
-
-
-
- 0
- 0
-
-
-
-
- -
-
-
-
- 0
- 28
-
-
-
-
- 16777215
- 28
-
-
-
- ...
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 85
- 0
-
-
-
-
- 0
- 0
-
-
-
- Output File
-
-
- Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 0
- 28
-
-
-
-
- 16777215
- 28
-
-
-
-
- -
-
-
-
- 0
- 28
-
-
-
-
- 16777215
- 28
-
-
-
- ...
-
-
-
-
-
-
-
- -
-
-
- 0
-
-
-
-
-
-
- 0
- 0
-
-
-
- 24
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Minimum
-
-
-
- 10
- 20
-
-
-
-
- -
-
-
- Create Video
-
-
-
- -
-
-
- false
-
-
- Cancel
-
-
-
-
-
-
-
-
-
-
-
- Encoder Settings
-
-
-
- 10
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 85
- 0
-
-
-
- Container
-
-
-
- -
-
-
-
- 150
- 0
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Minimum
-
-
-
- 5
- 5
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Resolution
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 0
- 0
-
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 85
- 0
-
-
-
- Video Codec
-
-
-
- -
-
-
-
- 150
- 0
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 5
- 5
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Video Bitrate (Kbps)
-
-
-
- -
-
-
- 99999
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 85
- 0
-
-
-
- Audio Codec
-
-
-
- -
-
-
-
- 150
- 0
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 5
- 10
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Audio Bitrate (Kbps)
-
-
-
- -
-
-
- 9999
-
-
-
-
-
-
-
-
-
- -
-
-
- QLayout::SetDefaultConstraint
-
-
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::MinimumExpanding
-
-
-
- 500
- 0
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 0
- 180
-
-
-
-
- 16777215
- 180
-
-
-
- -1
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/presetmanager.py b/presetmanager.py
deleted file mode 100644
index 3b02714..0000000
--- a/presetmanager.py
+++ /dev/null
@@ -1,290 +0,0 @@
-from PyQt4 import QtGui, QtCore
-import string
-import os
-
-import core
-
-
-class PresetManager(QtGui.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 = QtGui.QStringListModel()
- completer = QtGui.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/presetmanager.ui b/presetmanager.ui
deleted file mode 100644
index 5257b1c..0000000
--- a/presetmanager.ui
+++ /dev/null
@@ -1,150 +0,0 @@
-
-
- presetmanager
-
-
- Qt::NonModal
-
-
- true
-
-
-
- 0
- 0
- 497
- 377
-
-
-
- Preset Manager
-
-
- -
-
-
-
-
-
-
-
-
- Filter by name
-
-
-
- -
-
-
-
- 200
- 0
-
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- true
-
-
-
-
-
- -
-
-
- QLayout::SetMinimumSize
-
-
-
-
-
- Import
-
-
-
- -
-
-
- Export
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- true
-
-
- Rename
-
-
-
- -
-
-
- Delete
-
-
-
-
-
- -
-
-
-
-
-
- <html><head/><body><p><span style=" font-size:10pt; font-style:italic;">Right-click components in the main window to create presets</span></p></body></html>
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Close
-
-
-
-
-
-
-
-
-
-
diff --git a/preview_thread.py b/preview_thread.py
deleted file mode 100644
index eabf715..0000000
--- a/preview_thread.py
+++ /dev/null
@@ -1,59 +0,0 @@
-from PyQt4 import QtCore, QtGui, uic
-from PyQt4.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(str, 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/setup.py b/setup.py
index 48034dc..fde3461 100644
--- a/setup.py
+++ b/setup.py
@@ -1,51 +1,19 @@
-from cx_Freeze import setup, Executable
-import sys
-
-# Dependencies are automatically detected, but it might need
-# fine tuning.
-
-buildOptions = dict(
- packages=[],
- excludes=[
- "apport",
- "apt",
- "curses",
- "distutils",
- "email",
- "html",
- "http",
- "xmlrpc",
- "nose"
- ],
- include_files=[
- "mainwindow.ui",
- "presetmanager.ui",
- "background.png",
- "encoder-options.json",
- "components/"
- ],
- includes=[
- 'numpy.core._methods',
- 'numpy.lib.format'
- ]
-)
-
-
-base = 'Win32GUI' if sys.platform == 'win32' else None
-
-executables = [
- Executable(
- 'main.py',
- base=base,
- targetName='audio-visualizer-python'
- )
-]
-
-
-setup(
- name='audio-visualizer-python',
- version='1.0',
- description='GUI tool to render visualization videos of audio files',
- options=dict(build_exe=buildOptions),
- executables=executables
-)
++from setuptools import setup, find_packages
+
+ -# Dependencies are automatically detected, but it might need +setup(name='audio_visualizer_python',
+ -# fine tuning. + version='1.0',
+ -buildOptions = dict(packages = [], excludes = [ + description='a little GUI tool to render visualization \
+ - "apport", + videos of audio files',
+ - "apt", + license='MIT',
+ - "ctypes", + url='https://github.com/djfun/audio-visualizer-python',
+ - "curses", + packages=find_packages(),
+ - "distutils", + package_data={
+ - "email", + 'src': ['*'],
+ - "html", + },
+ - "http", + install_requires=['pillow-simd', 'numpy', ''],
+ - "json", + entry_points={
+ - "xmlrpc", + 'gui_scripts': [
+ - "nose" + 'audio-visualizer-python = avpython.main:main'
+ - ], include_files = ["main.ui"]) + ]
+ - + }
+ -import sys + )
\ No newline at end of file
diff --git a/src/background.png b/src/background.png
new file mode 100644
index 0000000..fb58593
Binary files /dev/null and b/src/background.png 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 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 586
+ 197
+
+
+
+ Form
+
+
+ -
+
+
+ 4
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 31
+ 0
+
+
+
+ Color #1
+
+
+
+ -
+
+
+
+ 32
+ 32
+
+
+
+
+
+
+
+ 32
+ 32
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 1
+ 0
+
+
+
+ 12
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 31
+ 0
+
+
+
+ Color #2
+
+
+
+ -
+
+
+
+ 32
+ 32
+
+
+
+
+
+
+
+ 32
+ 32
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 1
+ 0
+
+
+
+ 12
+
+
+
+
+
+ -
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Width
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+
+ 0
+ 0
+
+
+
+ 0
+
+
+ 999999999
+
+
+ 0
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Height
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+ 999999999
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ X
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+
+ 0
+ 0
+
+
+
+ -10000
+
+
+ 10000
+
+
+ 0
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Y
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+ -10000
+
+
+ 10000
+
+
+
+
+
+ -
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Fill
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ -1
+
+
+ QComboBox::AdjustToContentsOnFirstShow
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Transparent
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Stretch
+
+
+
+ -
+
+
-
+
+ Pad
+
+
+ -
+
+ Reflect
+
+
+ -
+
+ Repeat
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Minimum
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ 0
+
+
+ 2
+
+
+
+
+
+
+ -1
+ 0
+ 561
+ 31
+
+
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Start
+
+
+
+ -
+
+
+ -10000
+
+
+ 10000
+
+
+ 10
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ End
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ -10000
+
+
+ 10000
+
+
+ 10
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
+
+
+ -1
+ -1
+ 561
+ 31
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Start
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ -10000
+
+
+ 10000
+
+
+ 10
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ End
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ -10000
+
+
+ 10000
+
+
+ 10
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Centre
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ QAbstractSpinBox::PlusMinus
+
+
+ -10000
+
+
+ 10000
+
+
+ 3
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 586
+ 197
+
+
+
+ Form
+
+
+ -
+
+
+ 4
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 31
+ 0
+
+
+
+ Image
+
+
+
+ -
+
+
+
+ 1
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 1
+ 0
+
+
+
+
+ 32
+ 32
+
+
+
+ ...
+
+
+
+ 32
+ 32
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ X
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+ -10000
+
+
+ 10000
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Y
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+
+ 0
+ 0
+
+
+
+ -1000
+
+
+ 1000
+
+
+ 0
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Stretch
+
+
+ false
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+ Scale
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ QAbstractSpinBox::UpDownArrows
+
+
+ %
+
+
+ 10
+
+
+ 400
+
+
+ 100
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
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 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 633
+ 178
+
+
+
+
+ 180
+ 0
+
+
+
+ Form
+
+
+ -
+
+
+ 4
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Visualizer Layout
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+ Visualizer Color
+
+
+
+ -
+
+
+
+ 32
+ 32
+
+
+
+
+
+
+
+ 32
+ 32
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
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 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 586
+ 197
+
+
+
+ Form
+
+
+ -
+
+
+ 4
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Font
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Font Size
+
+
+
+ -
+
+
+ 500
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Text Layout
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+ Text Color
+
+
+
+ -
+
+
+
+ 32
+ 32
+
+
+
+
+
+
+
+ 32
+ 32
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ 0
+
+
-
+
+
+ Title
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ Testing New GUI
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ X
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+
+ 0
+ 0
+
+
+
+ 0
+
+
+ 999999999
+
+
+ 0
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Y
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+ 999999999
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
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 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 586
+ 197
+
+
+
+ Form
+
+
+ -
+
+
+ 4
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 31
+ 0
+
+
+
+ Video
+
+
+
+ -
+
+
+
+ 1
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 1
+ 0
+
+
+
+
+ 32
+ 32
+
+
+
+ ...
+
+
+
+ 32
+ 32
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ X
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+ -10000
+
+
+ 10000
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Y
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+
+ 0
+ 0
+
+
+
+ -10000
+
+
+ 10000
+
+
+ 0
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Loop
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+ Distort by scale
+
+
+
+ -
+
+
+ Scale
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ QAbstractSpinBox::UpDownArrows
+
+
+ %
+
+
+ 10
+
+
+ 400
+
+
+ 100
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+
+
+
+
+
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 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 1008
+ 575
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ MainWindow
+
+
+
+
+ 0
+ 0
+
+
+
+ false
+
+
+
+ 9
+
+
+ 0
+
+ -
+
+
-
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::MinimumExpanding
+
+
+
+ 0
+ 360
+
+
+
+
+ -
+
+
+ QLayout::SetDefaultConstraint
+
+
+ 0
+
+
-
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::MinimumExpanding
+
+
+
+ 420
+ 0
+
+
+
+
+
+
+ -
+
+
+ QLayout::SetMinimumSize
+
+
+ 3
+
+
-
+
+
+ QLayout::SetMinimumSize
+
+
+ 3
+
+
-
+
+
+ QLayout::SetMinimumSize
+
+
-
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 140
+ 20
+
+
+
+
+ -
+
+
+ Projects
+
+
+
+ -
+
+
+ Presets
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Minimum
+
+
+
+ 20
+ 2
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ true
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Sunken
+
+
+ 1
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+ QAbstractItemView::InternalMove
+
+
+ Qt::MoveAction
+
+
+
+ -
+
+
-
+
+
+ Add
+
+
+
+ -
+
+
+ Remove
+
+
+
+ -
+
+
+ Up
+
+
+
+ -
+
+
+ Down
+
+
+
+
+
+
+
+ -
+
+
+ 4
+
+
+ 2
+
+
+
+
+
+
+
+ -
+
+
+ QLayout::SetFixedSize
+
+
+ 4
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 500
+ 0
+
+
+
+
+ 16777215
+ 180
+
+
+
+ QTabWidget::North
+
+
+ QTabWidget::Rounded
+
+
+ 0
+
+
+
+ Export Video
+
+
+
+ 10
+
+
-
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 85
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+
+ 80
+ 0
+
+
+
+ Audio File
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 28
+
+
+
+
+ 16777215
+ 28
+
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 28
+
+
+
+
+ 16777215
+ 28
+
+
+
+ ...
+
+
+
+
+
+ -
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 85
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ Output File
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 28
+
+
+
+
+ 16777215
+ 28
+
+
+
+
+ -
+
+
+
+ 0
+ 28
+
+
+
+
+ 16777215
+ 28
+
+
+
+ ...
+
+
+
+
+
+
+
+ -
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ 24
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Minimum
+
+
+
+ 10
+ 20
+
+
+
+
+ -
+
+
+ Create Video
+
+
+
+ -
+
+
+ false
+
+
+ Cancel
+
+
+
+
+
+
+
+
+
+
+
+ Encoder Settings
+
+
+
+ 10
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 85
+ 0
+
+
+
+ Container
+
+
+
+ -
+
+
+
+ 150
+ 0
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Minimum
+
+
+
+ 5
+ 5
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Resolution
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 85
+ 0
+
+
+
+ Video Codec
+
+
+
+ -
+
+
+
+ 150
+ 0
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 5
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Video Bitrate (Kbps)
+
+
+
+ -
+
+
+ 99999
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 85
+ 0
+
+
+
+ Audio Codec
+
+
+
+ -
+
+
+
+ 150
+ 0
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 10
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Audio Bitrate (Kbps)
+
+
+
+ -
+
+
+ 9999
+
+
+
+
+
+
+
+
+
+ -
+
+
+ QLayout::SetDefaultConstraint
+
+
-
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::MinimumExpanding
+
+
+
+ 500
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 180
+
+
+
+
+ 16777215
+ 180
+
+
+
+ -1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+ presetmanager
+
+
+ Qt::NonModal
+
+
+ true
+
+
+
+ 0
+ 0
+ 497
+ 377
+
+
+
+ Preset Manager
+
+
+ -
+
+
-
+
+
+
+
+
+ Filter by name
+
+
+
+ -
+
+
+
+ 200
+ 0
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ true
+
+
+
+
+
+ -
+
+
+ QLayout::SetMinimumSize
+
+
-
+
+
+ Import
+
+
+
+ -
+
+
+ Export
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ true
+
+
+ Rename
+
+
+
+ -
+
+
+ Delete
+
+
+
+
+
+ -
+
+
-
+
+
+ <html><head/><body><p><span style=" font-size:10pt; font-style:italic;">Right-click components in the main window to create presets</span></p></body></html>
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Close
+
+
+
+
+
+
+
+
+
+
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()
diff --git a/video_thread.py b/video_thread.py
deleted file mode 100644
index 265feee..0000000
--- a/video_thread.py
+++ /dev/null
@@ -1,309 +0,0 @@
-from PyQt4 import QtCore, QtGui, uic
-from PyQt4.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()
--
cgit v1.2.3