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