From eaee0ab233709c18324dbb25f38b59c95c447e3c Mon Sep 17 00:00:00 2001 From: DH4 Date: Fri, 26 May 2017 23:06:47 -0500 Subject: Removed hardcoded parameters. Defaults loaded at runtime. --- main.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'main.py') diff --git a/main.py b/main.py index 9f608d8..3915c71 100644 --- a/main.py +++ b/main.py @@ -36,6 +36,7 @@ class Command(QtCore.QObject): self.args = self.parser.parse_args() self.settings = QSettings('settings.ini', QSettings.IniFormat) + LoadDefaultSettings(self) # load colours as tuples from comma-separated strings self.textColor = core.Core.RGBFromString(self.settings.value("textColor", '255, 255, 255')) @@ -106,6 +107,8 @@ class Command(QtCore.QObject): self.settings.setValue("textColor", '%s,%s,%s' % self.textColor) sys.exit(0) + + class Main(QtCore.QObject): newTask = QtCore.pyqtSignal(str, str, QFont, int, int, int, int, tuple, tuple) @@ -119,6 +122,8 @@ class Main(QtCore.QObject): self.window = window self.core = core.Core() self.settings = QSettings('settings.ini', QSettings.IniFormat) + + LoadDefaultSettings(self) # load colors as tuples from a comma-separated string self.textColor = core.Core.RGBFromString(self.settings.value("textColor", '255, 255, 255')) @@ -314,6 +319,23 @@ class Main(QtCore.QObject): self.window.lineEdit_visColor.setText(RGBstring) window.pushButton_visColor.setStyleSheet(btnStyle) +def LoadDefaultSettings(self): + default = { + "outputWidth": 1280, + "outputHeight": 720, + "outputFrameRate": 30, + "outputAudioCodec": "aac", + "outputAudioBitrate": "192k", + "outputVideoCodec": "libx264", + "outputVideoFormat": "yuv420p", + "outputPreset": "medium", + "outputFormat": "mp4" + } + + for parm, value in default.items(): + if self.settings.value(parm) == None: + self.settings.setValue(parm,value) + if len(sys.argv) > 1: # command line mode app = QtGui.QApplication(sys.argv, False) -- cgit v1.2.3 From f2329e93660780fc261abdbbd9d43884fdcaf722 Mon Sep 17 00:00:00 2001 From: DH4 Date: Sat, 27 May 2017 03:06:17 -0500 Subject: Added automatic scaling of Image and bars. Set title x/y position, and font size based on scale. --- core.py | 27 +++++++++++++++++++-------- main.py | 10 ++++------ preview_thread.py | 1 + video_thread.py | 22 +++++++++++----------- 4 files changed, 35 insertions(+), 25 deletions(-) (limited to 'main.py') diff --git a/core.py b/core.py index 900a98f..d360ca6 100644 --- a/core.py +++ b/core.py @@ -45,7 +45,7 @@ class Core(): def drawBaseImage(self, backgroundFile, titleText, titleFont, fontSize, alignment,\ xOffset, yOffset, textColor, visColor): if backgroundFile == '': - im = Image.new("RGB", (1280, 720), "black") + im = Image.new("RGB", (int(self.settings.value('outputWidth')), int(self.settings.value('outputHeight'))), "black") else: im = Image.open(backgroundFile) @@ -53,8 +53,8 @@ class Core(): self.lastBackgroundImage = backgroundFile # resize if necessary - if not im.size == (1280, 720): - im = im.resize((1280, 720), Image.ANTIALIAS) + if not im.size == (int(self.settings.value('outputWidth')), int(self.settings.value('outputHeight'))): + im = im.resize((int(self.settings.value('outputWidth')), int(self.settings.value('outputHeight'))), Image.ANTIALIAS) self._image = ImageQt(im) @@ -89,21 +89,32 @@ class Core(): def drawBars(self, spectrum, image, color): - imTop = Image.new("RGBA", (1280, 360)) + width = int(self.settings.value('outputWidth')) + height = int(int(self.settings.value('outputHeight'))/2) + + imTop = Image.new("RGBA", (width, height)) draw = ImageDraw.Draw(imTop) r, g, b = color color2 = (r, g, b, 50) + + vH = height-height/8 + bF = int(self.settings.value('outputWidth')) / 64 + bH = bF / 2 + bQ = bF / 4 + + bP = int(self.settings.value('outputHeight')) / 800 + for j in range(0, 63): - draw.rectangle((10 + j * 20, 325, 10 + j * 20 + 20, 325 - spectrum[j * 4] * 1 - 10), fill=color2) - draw.rectangle((15 + j * 20, 320, 15 + j * 20 + 10, 320 - spectrum[j * 4] * 1), fill=color) + draw.rectangle((bH + j * bF, vH, bH + j * bF + bF, vH + bQ - spectrum[j * 4] * bP - bH), fill=color2) + draw.rectangle((bH + bQ + j * bF, vH - bQ , bH + bQ + j * bF + bH, vH - spectrum[j * 4] * bP), fill=color) imBottom = imTop.transpose(Image.FLIP_TOP_BOTTOM) - im = Image.new("RGB", (1280, 720), "black") + im = Image.new("RGB", (int(self.settings.value('outputWidth')), int(self.settings.value('outputHeight'))), "black") im.paste(image, (0, 0)) im.paste(imTop, (0, 0), mask=imTop) - im.paste(imBottom, (0, 360), mask=imBottom) + im.paste(imBottom, (0, int(vH+bF*.7)), mask=imBottom) return im diff --git a/main.py b/main.py index 3915c71..bfa8fbd 100644 --- a/main.py +++ b/main.py @@ -107,8 +107,6 @@ class Command(QtCore.QObject): self.settings.setValue("textColor", '%s,%s,%s' % self.textColor) sys.exit(0) - - class Main(QtCore.QObject): newTask = QtCore.pyqtSignal(str, str, QFont, int, int, int, int, tuple, tuple) @@ -122,7 +120,6 @@ class Main(QtCore.QObject): self.window = window self.core = core.Core() self.settings = QSettings('settings.ini', QSettings.IniFormat) - LoadDefaultSettings(self) # load colors as tuples from a comma-separated string @@ -168,9 +165,10 @@ class Main(QtCore.QObject): window.alignmentComboBox.addItem("Left") window.alignmentComboBox.addItem("Middle") window.alignmentComboBox.addItem("Right") - window.fontsizeSpinBox.setValue(35) - window.textXSpinBox.setValue(70) - window.textYSpinBox.setValue(375) + window.alignmentComboBox.setCurrentIndex(1) + window.fontsizeSpinBox.setValue(int(int(self.settings.value("outputHeight")) / 16 )) + window.textXSpinBox.setValue(int(int(self.settings.value('outputWidth'))/2)) + window.textYSpinBox.setValue(int(int(self.settings.value('outputHeight'))/2)) window.lineEdit_textColor.setText('%s,%s,%s' % self.textColor) window.lineEdit_visColor.setText('%s,%s,%s' % self.visColor) window.pushButton_textColor.clicked.connect(lambda: self.pickColor('text')) diff --git a/preview_thread.py b/preview_thread.py index 041d39e..8195712 100644 --- a/preview_thread.py +++ b/preview_thread.py @@ -17,6 +17,7 @@ class Worker(QtCore.QObject): parent.processTask.connect(self.process) self.core = core.Core() self.queue = queue + self.core.settings = parent.settings @pyqtSlot(str, str, QtGui.QFont, int, int, int, int, tuple, tuple) diff --git a/video_thread.py b/video_thread.py index fe1f6f6..5b9a896 100644 --- a/video_thread.py +++ b/video_thread.py @@ -15,10 +15,10 @@ class Worker(QtCore.QObject): def __init__(self, parent=None): QtCore.QObject.__init__(self) - self.settings = parent.settings - parent.videoTask.connect(self.createVideo) self.core = core.Core() - + self.core.settings = parent.settings + parent.videoTask.connect(self.createVideo) + @pyqtSlot(str, str, QtGui.QFont, int, int, int, int, tuple, tuple, str, str) def createVideo(self, backgroundImage, titleText, titleFont, fontSize, alignment,\ @@ -53,7 +53,7 @@ class Worker(QtCore.QObject): # test if user has libfdk_aac encoders = sp.check_output(self.core.FFMPEG_BIN + " -encoders -hide_banner", shell=True) - acodec = self.settings.value('outputAudioCodec') + acodec = self.core.settings.value('outputAudioCodec') if b'libfdk_aac' in encoders and acodec == 'aac': acodec = 'libfdk_aac' @@ -62,18 +62,18 @@ class Worker(QtCore.QObject): '-y', # (optional) means overwrite the output file if it already exists. '-f', 'rawvideo', '-vcodec', 'rawvideo', - '-s', self.settings.value('outputWidth')+'x'+self.settings.value('outputHeight'), # size of one frame + '-s', self.core.settings.value('outputWidth')+'x'+self.core.settings.value('outputHeight'), # size of one frame '-pix_fmt', 'rgb24', - '-r', self.settings.value('outputFrameRate'), # frames per second + '-r', self.core.settings.value('outputFrameRate'), # frames per second '-i', '-', # The input comes from a pipe '-an', '-i', inputFile, '-acodec', acodec, # output audio codec - '-b:a', self.settings.value('outputAudioBitrate'), - '-vcodec', self.settings.value('outputVideoCodec'), - '-pix_fmt', self.settings.value('outputVideoFormat'), - '-preset', self.settings.value('outputPreset'), - '-f', self.settings.value('outputFormat')] + '-b:a', self.core.settings.value('outputAudioBitrate'), + '-vcodec', self.core.settings.value('outputVideoCodec'), + '-pix_fmt', self.core.settings.value('outputVideoFormat'), + '-preset', self.core.settings.value('outputPreset'), + '-f', self.core.settings.value('outputFormat')] if acodec == 'aac': ffmpegCommand.append('-strict') -- cgit v1.2.3 From 1a8acdbed09cc751d587e99bfc848c29752104e5 Mon Sep 17 00:00:00 2001 From: DH4 Date: Sat, 27 May 2017 04:49:26 -0500 Subject: Fixed Scaling Bugs --- core.py | 19 +++++++++---------- main.py | 3 ++- 2 files changed, 11 insertions(+), 11 deletions(-) (limited to 'main.py') diff --git a/core.py b/core.py index d360ca6..249a373 100644 --- a/core.py +++ b/core.py @@ -65,9 +65,9 @@ class Core(): painter.setFont(font) painter.setPen(QColor(*textColor)) - yPosition = yOffset - fm = QtGui.QFontMetrics(font) + yPosition = yOffset + fm.height()/6 + if alignment == 0: #Left xPosition = xOffset if alignment == 1: #Middle @@ -92,21 +92,20 @@ class Core(): width = int(self.settings.value('outputWidth')) height = int(int(self.settings.value('outputHeight'))/2) - imTop = Image.new("RGBA", (width, height)) - draw = ImageDraw.Draw(imTop) - r, g, b = color - color2 = (r, g, b, 50) - vH = height-height/8 bF = int(self.settings.value('outputWidth')) / 64 bH = bF / 2 bQ = bF / 4 + imTop = Image.new("RGBA", (width, height)) + draw = ImageDraw.Draw(imTop) + r, g, b = color + color2 = (r, g, b, 50) bP = int(self.settings.value('outputHeight')) / 800 for j in range(0, 63): - draw.rectangle((bH + j * bF, vH, bH + j * bF + bF, vH + bQ - spectrum[j * 4] * bP - bH), fill=color2) - draw.rectangle((bH + bQ + j * bF, vH - bQ , bH + bQ + j * bF + bH, vH - spectrum[j * 4] * bP), fill=color) + 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) @@ -114,7 +113,7 @@ class Core(): im = Image.new("RGB", (int(self.settings.value('outputWidth')), int(self.settings.value('outputHeight'))), "black") im.paste(image, (0, 0)) im.paste(imTop, (0, 0), mask=imTop) - im.paste(imBottom, (0, int(vH+bF*.7)), mask=imBottom) + im.paste(imBottom, (0, int(vH+bF*1.8)), mask=imBottom) return im diff --git a/main.py b/main.py index bfa8fbd..a065680 100644 --- a/main.py +++ b/main.py @@ -166,9 +166,10 @@ class Main(QtCore.QObject): window.alignmentComboBox.addItem("Middle") window.alignmentComboBox.addItem("Right") window.alignmentComboBox.setCurrentIndex(1) - window.fontsizeSpinBox.setValue(int(int(self.settings.value("outputHeight")) / 16 )) + window.fontsizeSpinBox.setValue(int(int(self.settings.value("outputHeight")) / 14 )) window.textXSpinBox.setValue(int(int(self.settings.value('outputWidth'))/2)) window.textYSpinBox.setValue(int(int(self.settings.value('outputHeight'))/2)) + window.lineEdit_textColor.setText('%s,%s,%s' % self.textColor) window.lineEdit_visColor.setText('%s,%s,%s' % self.visColor) window.pushButton_textColor.clicked.connect(lambda: self.pickColor('text')) -- cgit v1.2.3 From fe13268a841d3b4d45f33359abb81993d990772c Mon Sep 17 00:00:00 2001 From: DH4 Date: Sat, 27 May 2017 14:32:08 -0500 Subject: Created a new UI, several new features to be implemented. FIXME: Resolution change requires an application restart. --- core.py | 15 +- main.py | 151 ++++---- main.ui | 38 -- mainwindow.ui | 1120 +++++++++++++++++++++++++++++++++++++++++++++++++++++ preview_thread.py | 7 +- 5 files changed, 1219 insertions(+), 112 deletions(-) create mode 100644 mainwindow.ui (limited to 'main.py') diff --git a/core.py b/core.py index 249a373..8292f5b 100644 --- a/core.py +++ b/core.py @@ -112,8 +112,19 @@ class Core(): im = Image.new("RGB", (int(self.settings.value('outputWidth')), int(self.settings.value('outputHeight'))), "black") im.paste(image, (0, 0)) - im.paste(imTop, (0, 0), mask=imTop) - im.paste(imBottom, (0, int(vH+bF*1.8)), mask=imBottom) + + layout = int(self.settings.value('visLayout')) + + if layout == 0: + im.paste(imTop, (0, 0), mask=imTop) + im.paste(imBottom, (0, int(vH+bF*1.8)), mask=imBottom) + + if layout == 1: + im.paste(imTop, (0, int(height+bF*1.5)), mask=imTop) + im.paste(imBottom, (0, int(0-bF*1.5)), mask=imBottom) + + if layout == 2: + im.paste(imTop, (0, int(height+bF*1.5)), mask=imTop) return im diff --git a/main.py b/main.py index a065680..4fe9315 100644 --- a/main.py +++ b/main.py @@ -139,36 +139,37 @@ class Main(QtCore.QObject): self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.processTask.emit) self.timer.start(500) - - window.pushButton_selectInput.clicked.connect(self.openInputFileDialog) - window.pushButton_selectOutput.clicked.connect(self.openOutputFileDialog) - window.pushButton_createVideo.clicked.connect(self.createAudioVisualisation) - window.pushButton_selectBackground.clicked.connect(self.openBackgroundFileDialog) - window.progressBar_create.setValue(0) + window.toolButton_selectAudioFile.clicked.connect(self.openInputFileDialog) + window.toolButton_selectBackground.clicked.connect(self.openBackgroundFileDialog) + window.toolButton_selectOutputFile.clicked.connect(self.openOutputFileDialog) + window.progressBar_createVideo.setValue(0) + window.pushButton_createVideo.clicked.connect(self.createAudioVisualisation) window.setWindowTitle("Audio Visualizer") - window.pushButton_selectInput.setText("Select Input Music File") - window.pushButton_selectOutput.setText("Select Output Video File") - window.pushButton_selectBackground.setText("Select Background Image") - window.label_font.setText("Title Font") - window.label_alignment.setText("Title Options") - window.label_colorOptions.setText("Colors") - window.label_fontsize.setText("Fontsize") - window.label_title.setText("Title Text") - window.label_textColor.setText("Text:") - window.label_visColor.setText("Visualizer:") - window.pushButton_createVideo.setText("Create Video") - window.groupBox_create.setTitle("Create") - window.groupBox_settings.setTitle("Settings") - window.groupBox_preview.setTitle("Preview") - - window.alignmentComboBox.addItem("Left") - window.alignmentComboBox.addItem("Middle") - window.alignmentComboBox.addItem("Right") - window.alignmentComboBox.setCurrentIndex(1) - window.fontsizeSpinBox.setValue(int(int(self.settings.value("outputHeight")) / 14 )) - window.textXSpinBox.setValue(int(int(self.settings.value('outputWidth'))/2)) - window.textYSpinBox.setValue(int(int(self.settings.value('outputHeight'))/2)) + window.comboBox_textAlign.addItem("Left") + window.comboBox_textAlign.addItem("Middle") + window.comboBox_textAlign.addItem("Right") + window.comboBox_textAlign.setCurrentIndex(1) + + window.comboBox_visLayout.addItem("Classic") + window.comboBox_visLayout.addItem("Split") + window.comboBox_visLayout.addItem("Bottom") + visLayoutValue = int(self.settings.value('visLayout')) + window.comboBox_visLayout.setCurrentIndex(visLayoutValue) + + currentRes = self.settings.value('outputWidth')+'x'+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) + + # FIXME This needs to be changed in a future commit. + # We should be setting these values somewhere else. + window.spinBox_fontSize.setValue(int(int(self.settings.value("outputHeight")) / 14 )) + window.spinBox_xTextAlign.setValue(int(int(self.settings.value('outputWidth'))/2)) + window.spinBox_yTextAlign.setValue(int(int(self.settings.value('outputHeight'))/2)) window.lineEdit_textColor.setText('%s,%s,%s' % self.textColor) window.lineEdit_visColor.setText('%s,%s,%s' % self.visColor) @@ -181,30 +182,31 @@ class Main(QtCore.QObject): titleFont = self.settings.value("titleFont") if not titleFont == None: - window.fontComboBox.setCurrentFont(QFont(titleFont)) + window.fontComboBox_titleFont.setCurrentFont(QFont(titleFont)) alignment = self.settings.value("alignment") if not alignment == None: - window.alignmentComboBox.setCurrentIndex(int(alignment)) + window.comboBox_textAlign.setCurrentIndex(int(alignment)) fontSize = self.settings.value("fontSize") if not fontSize == None: - window.fontsizeSpinBox.setValue(int(fontSize)) + window.spinBox_fontSize.setValue(int(fontSize)) xPosition = self.settings.value("xPosition") if not xPosition == None: - window.textXSpinBox.setValue(int(xPosition)) + window.spinBox_xTextAlign.setValue(int(xPosition)) yPosition = self.settings.value("yPosition") if not yPosition == None: - window.textYSpinBox.setValue(int(yPosition)) + window.spinBox_yTextAlign.setValue(int(yPosition)) - window.fontComboBox.currentFontChanged.connect(self.drawPreview) + window.fontComboBox_titleFont.currentFontChanged.connect(self.drawPreview) window.lineEdit_title.textChanged.connect(self.drawPreview) - window.alignmentComboBox.currentIndexChanged.connect(self.drawPreview) - window.textXSpinBox.valueChanged.connect(self.drawPreview) - window.textYSpinBox.valueChanged.connect(self.drawPreview) - window.fontsizeSpinBox.valueChanged.connect(self.drawPreview) + window.comboBox_textAlign.currentIndexChanged.connect(self.drawPreview) + window.comboBox_visLayout.currentIndexChanged.connect(self.drawPreview) + window.spinBox_xTextAlign.valueChanged.connect(self.drawPreview) + window.spinBox_yTextAlign.valueChanged.connect(self.drawPreview) + window.spinBox_fontSize.valueChanged.connect(self.drawPreview) window.lineEdit_textColor.textChanged.connect(self.drawPreview) window.lineEdit_visColor.textChanged.connect(self.drawPreview) - + self.drawPreview() window.show() @@ -214,11 +216,11 @@ class Main(QtCore.QObject): self.previewThread.quit() self.previewThread.wait() - self.settings.setValue("titleFont", self.window.fontComboBox.currentFont().toString()) - self.settings.setValue("alignment", str(self.window.alignmentComboBox.currentIndex())) - self.settings.setValue("fontSize", str(self.window.fontsizeSpinBox.value())) - self.settings.setValue("xPosition", str(self.window.textXSpinBox.value())) - self.settings.setValue("yPosition", str(self.window.textYSpinBox.value())) + self.settings.setValue("titleFont", self.window.fontComboBox_titleFont.currentFont().toString()) + self.settings.setValue("alignment", str(self.window.comboBox_textAlign.currentIndex())) + self.settings.setValue("fontSize", str(self.window.spinBox_fontSize.value())) + self.settings.setValue("xPosition", str(self.window.spinBox_xTextAlign.value())) + self.settings.setValue("yPosition", str(self.window.spinBox_yTextAlign.value())) self.settings.setValue("visColor", self.window.lineEdit_visColor.text()) self.settings.setValue("textColor", self.window.lineEdit_textColor.text()) @@ -230,7 +232,7 @@ class Main(QtCore.QObject): if not fileName == "": self.settings.setValue("inputDir", os.path.dirname(fileName)) - self.window.label_input.setText(fileName) + self.window.lineEdit_audioFile.setText(fileName) def openOutputFileDialog(self): outputDir = self.settings.value("outputDir", expanduser("~")) @@ -240,7 +242,7 @@ class Main(QtCore.QObject): if not fileName == "": self.settings.setValue("outputDir", os.path.dirname(fileName)) - self.window.label_output.setText(fileName) + self.window.lineEdit_outputFile.setText(fileName) def openBackgroundFileDialog(self): backgroundDir = self.settings.value("backgroundDir", expanduser("~")) @@ -250,7 +252,7 @@ class Main(QtCore.QObject): if not fileName == "": self.settings.setValue("backgroundDir", os.path.dirname(fileName)) - self.window.label_background.setText(fileName) + self.window.lineEdit_background.setText(fileName) self.drawPreview() def createAudioVisualisation(self): @@ -265,37 +267,45 @@ class Main(QtCore.QObject): self.videoWorker.progressBarSetText.connect(self.progressBarSetText) self.videoThread.start() - self.videoTask.emit(self.window.label_background.text(), + self.videoTask.emit(self.window.lineEdit_background.text(), self.window.lineEdit_title.text(), - self.window.fontComboBox.currentFont(), - self.window.fontsizeSpinBox.value(), - self.window.alignmentComboBox.currentIndex(), - self.window.textXSpinBox.value(), - self.window.textYSpinBox.value(), + self.window.fontComboBox_titleFont.currentFont(), + self.window.spinBox_fontSize.value(), + self.window.comboBox_textAlign.currentIndex(), + self.window.spinBox_xTextAlign.value(), + self.window.spinBox_yTextAlign.value(), core.Core.RGBFromString(self.window.lineEdit_textColor.text()), core.Core.RGBFromString(self.window.lineEdit_visColor.text()), - self.window.label_input.text(), - self.window.label_output.text()) + self.window.lineEdit_audioFile.text(), + self.window.lineEdit_outputFile.text()) def progressBarUpdated(self, value): - self.window.progressBar_create.setValue(value) + self.window.progressBar_createVideo.setValue(value) def progressBarSetText(self, value): - self.window.progressBar_create.setFormat(value) + self.window.progressBar_createVideo.setFormat(value) def videoCreated(self): self.videoThread.quit() self.videoThread.wait() + def updateResolution(self): + resIndex = int(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): - self.newTask.emit(self.window.label_background.text(), + self.settings.setValue('visLayout', self.window.comboBox_visLayout.currentIndex()) + self.newTask.emit(self.window.lineEdit_background.text(), self.window.lineEdit_title.text(), - self.window.fontComboBox.currentFont(), - self.window.fontsizeSpinBox.value(), - self.window.alignmentComboBox.currentIndex(), - self.window.textXSpinBox.value(), - self.window.textYSpinBox.value(), + self.window.fontComboBox_titleFont.currentFont(), + self.window.spinBox_fontSize.value(), + self.window.comboBox_textAlign.currentIndex(), + self.window.spinBox_xTextAlign.value(), + self.window.spinBox_yTextAlign.value(), core.Core.RGBFromString(self.window.lineEdit_textColor.text()), core.Core.RGBFromString(self.window.lineEdit_visColor.text())) # self.processTask.emit() @@ -304,7 +314,7 @@ class Main(QtCore.QObject): self._scaledPreviewImage = image self._previewPixmap = QtGui.QPixmap.fromImage(self._scaledPreviewImage) - self.window.label_preview.setPixmap(self._previewPixmap) + self.window.label_previewContainer.setPixmap(self._previewPixmap) def pickColor(self, colorTarget): color = QtGui.QColorDialog.getColor() @@ -319,6 +329,12 @@ class Main(QtCore.QObject): window.pushButton_visColor.setStyleSheet(btnStyle) def LoadDefaultSettings(self): + self.resolutions = [ + '1920x1080', + '1280x720', + '854x480' + ] + default = { "outputWidth": 1280, "outputHeight": 720, @@ -328,7 +344,8 @@ def LoadDefaultSettings(self): "outputVideoCodec": "libx264", "outputVideoFormat": "yuv420p", "outputPreset": "medium", - "outputFormat": "mp4" + "outputFormat": "mp4", + "visLayout": 0 } for parm, value in default.items(): @@ -345,12 +362,12 @@ else: # gui mode if __name__ == "__main__": app = QtGui.QApplication(sys.argv) - window = uic.loadUi("main.ui") + window = uic.loadUi("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) diff --git a/main.ui b/main.ui index c2892c5..5acb7eb 100644 --- a/main.ui +++ b/main.ui @@ -373,25 +373,6 @@ - - - - - 32 - 32 - - - - - - - - 32 - 32 - - - - @@ -405,25 +386,6 @@ - - - - - 32 - 32 - - - - - - - - 32 - 32 - - - - diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..3dbb817 --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,1120 @@ + + + MainWindow + + + + 0 + 0 + 1139 + 658 + + + + MainWindow + + + + false + + + + 9 + + + 0 + + + + + + + 0 + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 356 + 280 + + + + + 0 + 0 + + + + false + + + background-color:rgba(255, 255, 255, 15); + + + + + + + true + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 0 + 0 + + + + + + + + + + 3 + + + + + + 0 + 0 + + + + + 280 + 200 + + + + + + + Qt::ScrollBarAlwaysOn + + + Qt::ScrollBarAlwaysOff + + + + + + + + + 3 + + + + + + + Open Project + + + + + + + Save Project + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + 0 + 0 + + + + + 280 + 0 + + + + + + + + + + Add Component + + + + + + + Remove Component + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Open Preset + + + + + + + Save Preset + + + + + + + + + + + + + QLayout::SetFixedSize + + + 4 + + + + + 8 + + + 8 + + + + + 0 + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 80 + 0 + + + + Audio File + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + 340 + 28 + + + + + 16777215 + 28 + + + + + 0 + 0 + + + + + + + + + 0 + 28 + + + + + 16777215 + 28 + + + + ... + + + + + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + Background + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + 0 + 28 + + + + + 16777215 + 28 + + + + + + + + + 0 + 28 + + + + + 16777215 + 28 + + + + ... + + + + + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 0 + 0 + + + + Output File + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + 0 + 28 + + + + + 16777215 + 28 + + + + + + + + + 0 + 28 + + + + + 16777215 + 28 + + + + ... + + + + + + + + + + + + 0 + 0 + + + + + 98 + 0 + + + + Video Format + + + + + + + + + + + 0 + 0 + + + + Video Preset + + + + + + + + + + + + + + + 0 + 0 + + + + + 98 + 0 + + + + Video Codec + + + + + + + + 150 + 0 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 5 + + + + + + + + + 0 + 0 + + + + Resolution + + + + + + + + + + + + + + + 0 + 0 + + + + + 98 + 0 + + + + Audio Codec + + + + + + + + 150 + 0 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 10 + + + + + + + + + 0 + 0 + + + + Bitrate + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Maximum + + + + 20 + 40 + + + + + + + + + + + 0 + 0 + + + + + + + 0 + + + + + + + 0 + + + + + Title + + + + + + + + 0 + 0 + + + + + 300 + 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 + + + + + + + + + + + + 0 + 0 + + + + Font + + + + + + + + 0 + 0 + + + + + 140 + 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 + 0 + + + + Visualizer Layout + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + Visulizer Color + + + + + + + + 32 + 32 + + + + + + + + 32 + 32 + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 80 + + + + + + + + verticalSpacer_4 + + + + + + + + + 0 + + + 20 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + 24 + + + + + + + Qt::Horizontal + + + QSizePolicy::Minimum + + + + 10 + 20 + + + + + + + + Create video + + + + + + + Cancel + + + + + + + + + + + + diff --git a/preview_thread.py b/preview_thread.py index 8195712..593a70f 100644 --- a/preview_thread.py +++ b/preview_thread.py @@ -71,10 +71,7 @@ class Worker(QtCore.QObject): im = self.core.drawBars(spectrum, im, nextPreviewInformation["visColor"]) self._image = ImageQt(im) - self._previewImage = QtGui.QImage(self._image) - - self._scaledPreviewImage = self._previewImage.scaled(320, 180, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation) - - self.imageCreated.emit(self._scaledPreviewImage) + self.imageCreated.emit(QtGui.QImage(self._image)) + except Empty: True -- cgit v1.2.3 From 75c1c65c9d63515a1488b63e9df9971984e1f7e8 Mon Sep 17 00:00:00 2001 From: DH4 Date: Sun, 28 May 2017 06:34:34 -0500 Subject: Integration with tassaron2 modular design. True Alpha Rendering added, several bug fixes. --- components/__init__.py | 1 + components/original.py | 113 +++++ components/original.ui | 92 ++++ components/text.py | 59 +++ components/text.ui | 329 ++++++++++++++ core.py | 107 +---- main.py | 104 +++-- main.ui | 564 ------------------------ mainwindow.ui | 1120 +++++++++++++++++------------------------------- preview_thread.py | 42 +- video_thread.py | 82 ++-- 11 files changed, 1107 insertions(+), 1506 deletions(-) create mode 100644 components/__init__.py create mode 100644 components/original.py create mode 100644 components/original.ui create mode 100644 components/text.py create mode 100644 components/text.ui delete mode 100644 main.ui (limited to 'main.py') diff --git a/components/__init__.py b/components/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/components/__init__.py @@ -0,0 +1 @@ + diff --git a/components/original.py b/components/original.py new file mode 100644 index 0000000..d1caa7b --- /dev/null +++ b/components/original.py @@ -0,0 +1,113 @@ +''' Original Audio Visualization ''' +import numpy +from PIL import Image, ImageDraw +from PyQt4 import uic +import os, random + + +class Component: + def widget(self,parent): + self.parent = parent + + 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") + #visLayoutValue = int(self.settings.value('visLayout')) + page.comboBox_visLayout.setCurrentIndex(0) + page.comboBox_visLayout.currentIndexChanged.connect(self.update) + + return page + def update(self): + self.layout = self.page.comboBox_visLayout.currentIndex() + print(self.layout) + self.parent.drawPreview() + + def previewRender(self, previewWorker, widget): + 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 drawBars(width, height, spectrum, (255, 255, 255), self.layout) + + def preFrameRender(self, **kwargs): + for kwarg, value in kwargs.items(): + exec('self.%s = value' % kwarg) + self.smoothConstantDown = 0.08 + self.smoothConstantUp = 0.8 + self.lastSpectrum = None + + def frameRender(self, moduleNo, frameNo): + self.lastSpectrum = transformData(frameNo, self.completeAudioArray, self.sampleSize, + self.smoothConstantDown, self.smoothConstantUp, self.lastSpectrum) + width = int(self.worker.core.settings.value('outputWidth')) + height = int(self.worker.core.settings.value('outputHeight')) + return drawBars(width, height, self.lastSpectrum, (255,255,255), self.layout) + +def transformData(i, completeAudioArray, sampleSize, smoothConstantDown, smoothConstantUp, lastSpectrum): + if len(completeAudioArray) < (i + sampleSize): + sampleSize = len(completeAudioArray) - i + numpy.seterr(divide='ignore') + 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(width, height, spectrum, color, layout): + vH = height-height/8 + bF = width / 64 + bH = bF / 2 + bQ = bF / 4 + imTop = Image.new("RGBA", (width, height),(0,0,0,0)) + 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 = Image.new("RGBA", (width, height),(0,0,0,0)) + + 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 diff --git a/components/original.ui b/components/original.ui new file mode 100644 index 0000000..0e6dd98 --- /dev/null +++ b/components/original.ui @@ -0,0 +1,92 @@ + + + Form + + + + 0 + 0 + 584 + 169 + + + + Form + + + + + 10 + 10 + 567 + 29 + + + + + + + + 0 + 0 + + + + Visualizer Layout + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + Visualizer Color + + + + + + + + 32 + 32 + + + + + + + + 32 + 32 + + + + + + + + + + + + + diff --git a/components/text.py b/components/text.py new file mode 100644 index 0000000..68b02fe --- /dev/null +++ b/components/text.py @@ -0,0 +1,59 @@ +''' Title Text ''' +import numpy +from PIL import Image, ImageDraw +from PyQt4 import uic +import os + + +class Component: + def widget(self,parent): + page = uic.loadUi(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'text.ui')) + return page + def previewRender(self, previewWorker, widget): + width = int(previewWorker.core.settings.value('outputWidth')) + height = int(previewWorker.core.settings.value('outputHeight')) + im = Image.new("RGBA", (width, height),(0,0,0,0)) + + return im + + def preFrameRender(self, **kwargs): + pass + def frameRender(self, moduleNo, frameNo): + width = int(previewWorker.core.settings.value('outputWidth')) + height = int(previewWorker.core.settings.value('outputHeight')) + im = Image.new("RGBA", (width, height),(0,0,0,0)) + + return im + + ''' + self._image = ImageQt(im) + + self._image1 = QtGui.QImage(self._image) + painter = QPainter(self._image1) + font = titleFont + font.setPixelSize(fontSize) + painter.setFont(font) + painter.setPen(QColor(*textColor)) + + yPosition = yOffset + + fm = QtGui.QFontMetrics(font) + if alignment == 0: #Left + xPosition = xOffset + if alignment == 1: #Middle + xPosition = xOffset - fm.width(titleText)/2 + if alignment == 2: #Right + xPosition = xOffset - fm.width(titleText) + painter.drawText(xPosition, yPosition, titleText) + painter.end() + + buffer = QtCore.QBuffer() + buffer.open(QtCore.QIODevice.ReadWrite) + self._image1.save(buffer, "PNG") + + strio = io.BytesIO() + strio.write(buffer.data()) + buffer.close() + strio.seek(0) + return Image.open(strio) + ''' diff --git a/components/text.ui b/components/text.ui new file mode 100644 index 0000000..4431278 --- /dev/null +++ b/components/text.ui @@ -0,0 +1,329 @@ + + + Form + + + + 0 + 0 + 584 + 169 + + + + Form + + + + + 10 + 20 + 567 + 25 + + + + + 0 + + + + + Title + + + + + + + + 0 + 0 + + + + + 300 + 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 + + + + + + + + + 10 + 51 + 567 + 29 + + + + + + + + 0 + 0 + + + + Font + + + + + + + + 0 + 0 + + + + + 140 + 0 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + + 0 + 0 + + + + Font Size + + + + + + + 500 + + + + + + + + + 10 + 86 + 567 + 29 + + + + + + + + 0 + 0 + + + + Text Layout + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + Text Color + + + + + + + + 32 + 32 + + + + + + + + 32 + 32 + + + + + + + + + + + + + 10 + 121 + 567 + 29 + + + + + + + + diff --git a/core.py b/core.py index 8292f5b..c8bfbca 100644 --- a/core.py +++ b/core.py @@ -42,8 +42,7 @@ class Core(): else: return self.getVideoFrames(backgroundImage, preview) - def drawBaseImage(self, backgroundFile, titleText, titleFont, fontSize, alignment,\ - xOffset, yOffset, textColor, visColor): + def drawBaseImage(self, backgroundFile): if backgroundFile == '': im = Image.new("RGB", (int(self.settings.value('outputWidth')), int(self.settings.value('outputHeight'))), "black") else: @@ -51,81 +50,10 @@ class Core(): if self._image == None or not self.lastBackgroundImage == backgroundFile: self.lastBackgroundImage = backgroundFile - # resize if necessary if not im.size == (int(self.settings.value('outputWidth')), int(self.settings.value('outputHeight'))): im = im.resize((int(self.settings.value('outputWidth')), int(self.settings.value('outputHeight'))), Image.ANTIALIAS) - - self._image = ImageQt(im) - - self._image1 = QtGui.QImage(self._image) - painter = QPainter(self._image1) - font = titleFont - font.setPixelSize(fontSize) - painter.setFont(font) - painter.setPen(QColor(*textColor)) - - fm = QtGui.QFontMetrics(font) - yPosition = yOffset + fm.height()/6 - - if alignment == 0: #Left - xPosition = xOffset - if alignment == 1: #Middle - xPosition = xOffset - fm.width(titleText)/2 - if alignment == 2: #Right - xPosition = xOffset - fm.width(titleText) - painter.drawText(xPosition, yPosition, titleText) - painter.end() - - buffer = QtCore.QBuffer() - buffer.open(QtCore.QIODevice.ReadWrite) - self._image1.save(buffer, "PNG") - - strio = io.BytesIO() - strio.write(buffer.data()) - buffer.close() - strio.seek(0) - return Image.open(strio) - - def drawBars(self, spectrum, image, color): - - width = int(self.settings.value('outputWidth')) - height = int(int(self.settings.value('outputHeight'))/2) - - vH = height-height/8 - bF = int(self.settings.value('outputWidth')) / 64 - bH = bF / 2 - bQ = bF / 4 - imTop = Image.new("RGBA", (width, height)) - draw = ImageDraw.Draw(imTop) - r, g, b = color - color2 = (r, g, b, 50) - - bP = int(self.settings.value('outputHeight')) / 800 - - 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 = Image.new("RGB", (int(self.settings.value('outputWidth')), int(self.settings.value('outputHeight'))), "black") - im.paste(image, (0, 0)) - - layout = int(self.settings.value('visLayout')) - - if layout == 0: - im.paste(imTop, (0, 0), mask=imTop) - im.paste(imBottom, (0, int(vH+bF*1.8)), mask=imBottom) - - if layout == 1: - im.paste(imTop, (0, int(height+bF*1.5)), mask=imTop) - im.paste(imBottom, (0, int(0-bF*1.5)), mask=imBottom) - - if layout == 2: - im.paste(imTop, (0, int(height+bF*1.5)), mask=imTop) - + return im def readAudioFile(self, filename): @@ -159,41 +87,10 @@ class Core(): return completeAudioArray - 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 deleteTempDir(self): if self.tempDir and os.path.exists(self.tempDir): rmtree(self.tempDir) - def getVideoFrames(self, videoPath, firstOnly=False): self.tempDir = os.path.join(tempfile.gettempdir(), 'audio-visualizer-python-data') # recreate the temporary directory so it is empty diff --git a/main.py b/main.py index 4fe9315..b700ad7 100644 --- a/main.py +++ b/main.py @@ -10,15 +10,18 @@ import atexit from queue import Queue from PyQt4.QtCore import QSettings import signal +from importlib import import_module import preview_thread, core, video_thread class Command(QtCore.QObject): - videoTask = QtCore.pyqtSignal(str, str, QFont, int, int, int, int, tuple, tuple, str, str) + videoTask = QtCore.pyqtSignal(str, str, QFont, int, int, int, int, tuple, tuple, str, str, list) def __init__(self): QtCore.QObject.__init__(self) + self.modules = [] + self.selectedComponents = [] import argparse self.parser = argparse.ArgumentParser(description='Create a visualization for an audio file') @@ -90,7 +93,8 @@ class Command(QtCore.QObject): self.textColor, self.visColor, self.args.input, - self.args.output) + self.args.output, + self.selectedComponents) def videoCreated(self): self.videoThread.quit() @@ -109,9 +113,9 @@ class Command(QtCore.QObject): class Main(QtCore.QObject): - newTask = QtCore.pyqtSignal(str, str, QFont, int, int, int, int, tuple, tuple) + newTask = QtCore.pyqtSignal(str, list) processTask = QtCore.pyqtSignal() - videoTask = QtCore.pyqtSignal(str, str, QFont, int, int, int, int, tuple, tuple, str, str) + videoTask = QtCore.pyqtSignal(str, str, str, list) def __init__(self, window): QtCore.QObject.__init__(self) @@ -121,6 +125,8 @@ class Main(QtCore.QObject): self.core = core.Core() self.settings = QSettings('settings.ini', QSettings.IniFormat) LoadDefaultSettings(self) + + self.pages = [] # load colors as tuples from a comma-separated string self.textColor = core.Core.RGBFromString(self.settings.value("textColor", '255, 255, 255')) @@ -140,24 +146,26 @@ class Main(QtCore.QObject): self.timer.timeout.connect(self.processTask.emit) self.timer.start(500) + # begin decorating the window and connecting events window.toolButton_selectAudioFile.clicked.connect(self.openInputFileDialog) window.toolButton_selectBackground.clicked.connect(self.openBackgroundFileDialog) window.toolButton_selectOutputFile.clicked.connect(self.openOutputFileDialog) window.progressBar_createVideo.setValue(0) window.pushButton_createVideo.clicked.connect(self.createAudioVisualisation) window.setWindowTitle("Audio Visualizer") - window.comboBox_textAlign.addItem("Left") - window.comboBox_textAlign.addItem("Middle") - window.comboBox_textAlign.addItem("Right") - window.comboBox_textAlign.setCurrentIndex(1) - window.comboBox_visLayout.addItem("Classic") - window.comboBox_visLayout.addItem("Split") - window.comboBox_visLayout.addItem("Bottom") - visLayoutValue = int(self.settings.value('visLayout')) - window.comboBox_visLayout.setCurrentIndex(visLayoutValue) + self.modules = self.findComponents() + for component in self.modules: + window.comboBox_componentSelection.addItem(component.__doc__) + window.listWidget_componentList.clicked.connect(lambda _: self.changeComponentWidget()) + self.selectedComponents = [] + + self.window.pushButton_addComponent.clicked.connect( \ + lambda _: self.addComponent(self.window.comboBox_componentSelection.currentIndex()) + ) + self.window.pushButton_removeComponent.clicked.connect(lambda _: self.removeComponent()) - currentRes = self.settings.value('outputWidth')+'x'+self.settings.value('outputHeight') + 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: @@ -165,8 +173,12 @@ class Main(QtCore.QObject): window.comboBox_resolution.setCurrentIndex(currentRes) window.comboBox_resolution.currentIndexChanged.connect(self.updateResolution) - # FIXME This needs to be changed in a future commit. - # We should be setting these values somewhere else. + ''' + window.comboBox_textAlign.addItem("Left") + window.comboBox_textAlign.addItem("Middle") + window.comboBox_textAlign.addItem("Right") + window.comboBox_textAlign.setCurrentIndex(1) + window.spinBox_fontSize.setValue(int(int(self.settings.value("outputHeight")) / 14 )) window.spinBox_xTextAlign.setValue(int(int(self.settings.value('outputWidth'))/2)) window.spinBox_yTextAlign.setValue(int(int(self.settings.value('outputHeight'))/2)) @@ -206,7 +218,7 @@ class Main(QtCore.QObject): window.spinBox_fontSize.valueChanged.connect(self.drawPreview) window.lineEdit_textColor.textChanged.connect(self.drawPreview) window.lineEdit_visColor.textChanged.connect(self.drawPreview) - + ''' self.drawPreview() window.show() @@ -268,18 +280,10 @@ class Main(QtCore.QObject): self.videoThread.start() self.videoTask.emit(self.window.lineEdit_background.text(), - self.window.lineEdit_title.text(), - self.window.fontComboBox_titleFont.currentFont(), - self.window.spinBox_fontSize.value(), - self.window.comboBox_textAlign.currentIndex(), - self.window.spinBox_xTextAlign.value(), - self.window.spinBox_yTextAlign.value(), - core.Core.RGBFromString(self.window.lineEdit_textColor.text()), - core.Core.RGBFromString(self.window.lineEdit_visColor.text()), self.window.lineEdit_audioFile.text(), - self.window.lineEdit_outputFile.text()) + self.window.lineEdit_outputFile.text(), + self.selectedComponents) - def progressBarUpdated(self, value): self.window.progressBar_createVideo.setValue(value) @@ -298,16 +302,8 @@ class Main(QtCore.QObject): self.drawPreview def drawPreview(self): - self.settings.setValue('visLayout', self.window.comboBox_visLayout.currentIndex()) - self.newTask.emit(self.window.lineEdit_background.text(), - self.window.lineEdit_title.text(), - self.window.fontComboBox_titleFont.currentFont(), - self.window.spinBox_fontSize.value(), - self.window.comboBox_textAlign.currentIndex(), - self.window.spinBox_xTextAlign.value(), - self.window.spinBox_yTextAlign.value(), - core.Core.RGBFromString(self.window.lineEdit_textColor.text()), - core.Core.RGBFromString(self.window.lineEdit_visColor.text())) + #self.settings.setValue('visLayout', self.window.comboBox_visLayout.currentIndex()) + self.newTask.emit(self.window.lineEdit_background.text(), self.selectedComponents) # self.processTask.emit() def showPreviewImage(self, image): @@ -328,6 +324,40 @@ class Main(QtCore.QObject): self.window.lineEdit_visColor.setText(RGBstring) window.pushButton_visColor.setStyleSheet(btnStyle) + def findComponents(self): + def findComponents(): + srcPath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'components') + if os.path.exists(srcPath): + for f in os.listdir(srcPath): + name, ext = os.path.splitext(f) + if name.startswith("__"): + continue + elif ext == '.py': + yield name + return [import_module('components.%s' % name) for name in findComponents()] + + def addComponent(self, moduleIndex): + self.window.listWidget_componentList.addItem(self.modules[moduleIndex].__doc__) + self.selectedComponents.append(self.modules[moduleIndex].Component()) + self.selectedComponents[-1].page = self.selectedComponents[-1].widget(self) + self.pages.append(self.selectedComponents[-1].page) + self.window.stackedWidget.addWidget(self.pages[-1]) + self.selectedComponents[-1].update() + + def removeComponent(self): + for selected in self.window.listWidget_componentList.selectedItems(): + index = self.window.listWidget_componentList.row(selected) + self.window.stackedWidget.removeWidget(self.pages[index]) + self.window.listWidget_componentList.takeItem(index) + self.selectedComponents.pop(index) + print(self.selectedComponents) + self.drawPreview() + + def changeComponentWidget(self): + selected = self.window.listWidget_componentList.selectedItems() + index = self.window.listWidget_componentList.row(selected[0]) + self.window.stackedWidget.setCurrentIndex(index) + def LoadDefaultSettings(self): self.resolutions = [ '1920x1080', diff --git a/main.ui b/main.ui deleted file mode 100644 index 5acb7eb..0000000 --- a/main.ui +++ /dev/null @@ -1,564 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 635 - 600 - - - - - 0 - 0 - - - - - 635 - 600 - - - - MainWindow - - - - - 0 - 0 - - - - - - - - - - 0 - 0 - - - - - 0 - 200 - - - - - 16777215 - 16777215 - - - - GroupBox - - - - - - - - - - - 1 - 0 - - - - - 200 - 0 - - - - - 16777215 - 16777215 - - - - PushButton - - - - - - - - 2 - 0 - - - - QFrame::Box - - - - - - - - - - - - - - - 1 - 0 - - - - - 200 - 0 - - - - - 16777215 - 16777215 - - - - PushButton - - - - - - - - 2 - 0 - - - - QFrame::Box - - - - - - - - - - - - - - - 1 - 0 - - - - - 200 - 0 - - - - - 16777215 - 16777215 - - - - PushButton - - - - - - - - 2 - 0 - - - - QFrame::Box - - - - - - - - - - - - - - - 200 - 0 - - - - - 200 - 16777215 - - - - - 200 - 0 - - - - QFrame::NoFrame - - - - - - - - - - - - - - - - - - 200 - 0 - - - - - 200 - 16777215 - - - - - 200 - 0 - - - - QFrame::NoFrame - - - - - - - - - - - - - - - - - - - - - - 999 - - - - - - - Qt::LeftToRight - - - X - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - -99999 - - - 99999 - - - - - - - Y - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - -99999 - - - 99999 - - - - - - - - - - - - - - 200 - 0 - - - - - 200 - 16777215 - - - - - 200 - 0 - - - - QFrame::NoFrame - - - - - - - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - - - - - - 200 - 0 - - - - - 200 - 16777215 - - - - - 200 - 0 - - - - QFrame::NoFrame - - - - - - - - - - - - - - - - - - - 0 - 0 - - - - - 0 - 220 - - - - - 16777215 - 220 - - - - GroupBox - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - - 320 - 180 - - - - - 320 - 180 - - - - QFrame::Box - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - GroupBox - - - - - - - - 24 - - - Qt::AlignCenter - - - true - - - - - - - - 0 - 0 - - - - PushButton - - - - - - - - - - - - Qt::Vertical - - - - 20 - 0 - - - - - - - - - - - - diff --git a/mainwindow.ui b/mainwindow.ui index 3dbb817..ce8233e 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -6,8 +6,8 @@ 0 0 - 1139 - 658 + 1165 + 707 @@ -109,337 +109,46 @@ 3 - - - - 0 - 0 - - - - - 280 - 200 - - - - - - - Qt::ScrollBarAlwaysOn - - - Qt::ScrollBarAlwaysOff - - - - - - - - - 3 - - - - - - - Open Project - - - - - - - Save Project - - - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 10 - - - - - - - - - 0 - 0 - - - - - 280 - 0 - - - - - - - - - - Add Component - - - - - - - Remove Component - - - - - - - - - Qt::Vertical - - - - 20 - 40 - + + + 3 - - - - - - - Open Preset - - - - - - - Save Preset - - - - - - - - - - - - - QLayout::SetFixedSize - - - 4 - - - - - 8 - - - 8 - - - - - 0 - - - - - - 0 - 0 - - - - - 100 - 0 - - - - - 80 - 0 - - - - Audio File - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - 340 - 28 - - - - - 16777215 - 28 - - - - - 0 - 0 - - - - - - - - - 0 - 28 - - - - - 16777215 - 28 - - - - ... - - - - - - - - - - - - 0 - 0 - - - - - 100 - 0 - - - - Background - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - 0 - 28 - - - - - 16777215 - 28 - - - - - - - - - 0 - 28 - - - - - 16777215 - 28 - - - - ... - - + + + + + Open Project + + + + + + + Save Project + + + + - - - - - - - - 0 - 0 - + + + Qt::Vertical - - - 100 - 0 - + + QSizePolicy::Fixed - + - 0 - 0 + 20 + 10 - - Output File - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - + - + 0 @@ -448,259 +157,147 @@ - 0 - 28 - - - - - 16777215 - 28 - - - - - - - - - 0 - 28 - - - - - 16777215 - 28 - - - - ... - - - - - - - - - - - - 0 - 0 - - - - - 98 + 280 0 - - Video Format - - - - - - - - - - - 0 - 0 - - - - Video Preset - - + + + + + Add Component + + + + + + + Remove Component + + + + - - - - - - 0 - 0 - - - - - 98 - 0 - - - - Video Codec - - - - - - - - 150 - 0 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 5 - - - - - - - - - 0 - 0 - - - - Resolution - - - - - - - + + + + 0 + 0 + + + - + - - - - 0 - 0 - - - - - 98 - 0 - - + - Audio Codec - - - - - - - - 150 - 0 - + Open Preset - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 10 - - - - - - - - - 0 - 0 - - + - Bitrate - - - - - - - - - - - - Qt::Vertical - - - QSizePolicy::Maximum - - - - 20 - 40 - - - + Save Preset + + + + + + + + + + QLayout::SetFixedSize + + + 4 + + + 0 + - + - + 0 0 - - + + + 0 + 180 + + + + + 16777215 + 180 + + + + QTabWidget::North + + + QTabWidget::Rounded 0 - - + + + Input Settings + + + + 10 + - - + + 0 - + + + + 0 + 0 + + + + + 100 + 0 + + + + + 80 + 0 + + - Title + Audio File + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - + 0 @@ -709,158 +306,197 @@ - 300 - 0 + 340 + 28 - - Testing New GUI + + + 16777215 + 28 + + + + + 0 + 0 + - - - Qt::Horizontal - - - QSizePolicy::Fixed + + + + 0 + 28 + - + - 5 - 20 + 16777215 + 28 - + + ... + + + + + + - + 0 0 + + + 100 + 0 + + - X + Background + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - + - + 0 0 - + - 80 - 16777215 + 0 + 28 - + - 0 - 0 + 16777215 + 28 - - 0 - - - 999999999 - - - 0 - - - - Qt::Horizontal - - - QSizePolicy::Fixed + + + + 0 + 28 + - + - 5 - 20 + 16777215 + 28 - + + ... + + + + + + + + + Encoder Settings + + + + 10 + + + - + 0 0 + + + 98 + 0 + + - Y + Video Format - + + + + - + 0 0 - - - 80 - 16777215 - - - - 999999999 + + Video Preset + + + - + - + 0 0 + + + 98 + 0 + + - Font + Video Codec - - - - 0 - 0 - - + - 140 + 150 0 - + Qt::Horizontal @@ -870,13 +506,13 @@ 5 - 20 + 5 - + 0 @@ -884,39 +520,48 @@ - Font Size + Resolution - - - 500 - - + - + - + 0 0 + + + 98 + 0 + + - Text Layout + Audio Codec - + + + + 150 + 0 + + + - + Qt::Horizontal @@ -926,186 +571,193 @@ 5 - 20 + 10 - + + + + 0 + 0 + + - Text Color + Bitrate - - - - 32 - 32 - - - - - - - - 32 - 32 - - - + + + + + + + + Export Video + + + + 10 + + + - + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 0 + 0 + + + + Output File + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + 0 + 28 + + + + + 16777215 + 28 + + + + + + + + + 0 + 28 + + + + + 16777215 + 28 + + + + ... + + + + - + + + 0 + - - - - 0 - 0 - + + + + 0 + 0 + - - Visualizer Layout + + 24 - - - - + Qt::Horizontal - QSizePolicy::Fixed + QSizePolicy::Minimum - 5 + 10 20 - + - Visulizer Color + Create video - - - - 32 - 32 - - + - - - - - 32 - 32 - + Cancel - - - - - - - Qt::Vertical - - - QSizePolicy::Minimum - - - - 20 - 80 - - - - - - verticalSpacer_4 - - - - - - 0 - - - 20 - - - 0 - - - 0 - - + + + + 0 + 0 + + 0 - 0 + 180 - - 24 - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - + - 10 - 20 + 16777215 + 180 - - - - - - Create video - - - - - - - Cancel + + -1 diff --git a/preview_thread.py b/preview_thread.py index 593a70f..e8b2021 100644 --- a/preview_thread.py +++ b/preview_thread.py @@ -18,23 +18,17 @@ class Worker(QtCore.QObject): self.core = core.Core() self.queue = queue self.core.settings = parent.settings + self.stackedWidget = parent.window.stackedWidget - @pyqtSlot(str, str, QtGui.QFont, int, int, int, int, tuple, tuple) - def createPreviewImage(self, backgroundImage, titleText, titleFont, fontSize,\ - alignment, xOffset, yOffset, textColor, visColor): + @pyqtSlot(str, list) + def createPreviewImage(self, backgroundImage, components): # print('worker thread id: {}'.format(QtCore.QThread.currentThreadId())) dic = { "backgroundImage": backgroundImage, - "titleText": titleText, - "titleFont": titleFont, - "fontSize": fontSize, - "alignment": alignment, - "xoffset": xOffset, - "yoffset": yOffset, - "textColor" : textColor, - "visColor" : visColor + "components": components, } + print(components) self.queue.put(dic) @pyqtSlot() @@ -56,21 +50,21 @@ class Worker(QtCore.QObject): else: bgImage = bgImage[0] - im = self.core.drawBaseImage( - bgImage, - nextPreviewInformation["titleText"], - nextPreviewInformation["titleFont"], - nextPreviewInformation["fontSize"], - nextPreviewInformation["alignment"], - nextPreviewInformation["xoffset"], - nextPreviewInformation["yoffset"], - nextPreviewInformation["textColor"], - nextPreviewInformation["visColor"]) - spectrum = numpy.fromfunction(lambda x: 0.008*(x-128)**2, (255,), dtype="int16") + im = self.core.drawBaseImage(bgImage) + frame = Image.new("RGBA", (1280, 720),(0,0,0,255)) + frame.paste(im) - im = self.core.drawBars(spectrum, im, nextPreviewInformation["visColor"]) - self._image = ImageQt(im) + componentWidgets = [self.stackedWidget.widget(i) for i in range(self.stackedWidget.count())] + components = nextPreviewInformation["components"] + print(components) + print(componentWidgets) + for component, componentWidget in zip(components, componentWidgets): + print('drawing') + newFrame = Image.alpha_composite(frame,component.previewRender(self, componentWidget)) + frame = Image.alpha_composite(frame,newFrame) + + self._image = ImageQt(frame) self.imageCreated.emit(QtGui.QImage(self._image)) except Empty: diff --git a/video_thread.py b/video_thread.py index 5b9a896..ccb2730 100644 --- a/video_thread.py +++ b/video_thread.py @@ -17,24 +17,15 @@ class Worker(QtCore.QObject): QtCore.QObject.__init__(self) self.core = core.Core() self.core.settings = parent.settings + self.modules = parent.modules + self.stackedWidget = parent.window.stackedWidget parent.videoTask.connect(self.createVideo) - - @pyqtSlot(str, str, QtGui.QFont, int, int, int, int, tuple, tuple, str, str) - def createVideo(self, backgroundImage, titleText, titleFont, fontSize, alignment,\ - xOffset, yOffset, textColor, visColor, inputFile, outputFile): + @pyqtSlot(str, str, str, list) + def createVideo(self, backgroundImage, inputFile, outputFile, components): # print('worker thread id: {}'.format(QtCore.QThread.currentThreadId())) def getBackgroundAtIndex(i): - return self.core.drawBaseImage( - backgroundFrames[i], - titleText, - titleFont, - fontSize, - alignment, - xOffset, - yOffset, - textColor, - visColor) + return self.core.drawBaseImage(backgroundFrames[i]) progressBarValue = 0 self.progressBarUpdate.emit(progressBarValue) @@ -84,40 +75,47 @@ class Worker(QtCore.QObject): out_pipe = sp.Popen(ffmpegCommand, stdin=sp.PIPE,stdout=sys.stdout, stderr=sys.stdout) - smoothConstantDown = 0.08 - smoothConstantUp = 0.8 - lastSpectrum = None + # initialize components + componentWidgets = [self.stackedWidget.widget(i) for i in range(self.stackedWidget.count())] + + print('######################## Data') + print(components) + print(componentWidgets) sampleSize = 1470 - + for component, widget in zip(components, componentWidgets): + component.preFrameRender(worker=self, widget=widget, completeAudioArray=completeAudioArray, sampleSize=sampleSize) + numpy.seterr(divide='ignore') + frame = getBackgroundAtIndex(0) bgI = 0 + # create video for output for i in range(0, len(completeAudioArray), sampleSize): - # create video for output - lastSpectrum = self.core.transformData( - i, - completeAudioArray, - sampleSize, - smoothConstantDown, - smoothConstantUp, - lastSpectrum) - if imBackground != None: - im = self.core.drawBars(lastSpectrum, imBackground, visColor) - else: - im = self.core.drawBars(lastSpectrum, getBackgroundAtIndex(bgI), visColor) - if bgI < len(backgroundFrames)-1: - bgI += 1 + newFrame = Image.new("RGBA", (int(self.core.settings.value('outputWidth')), int(self.core.settings.value('outputHeight'))),(0,0,0,255)) + + if imBackground: + newFrame.paste(imBackground) + else: + newFrame.paste(getBackgroundAtIndex(bgI)) + + for compNo, comp in enumerate(components): + newFrame = Image.alpha_composite(newFrame,comp.frameRender(compNo, i)) + if not imBackground: + if bgI < len(backgroundFrames)-1: + bgI += 1 # write to out_pipe - try: - out_pipe.stdin.write(im.tobytes()) - finally: - True - - # increase progress bar value - if progressBarValue + 1 <= (i / len(completeAudioArray)) * 100: - progressBarValue = numpy.floor((i / len(completeAudioArray)) * 100) - self.progressBarUpdate.emit(progressBarValue) - self.progressBarSetText.emit('%s%%' % str(int(progressBarValue))) + try: + frame = Image.new("RGB", (int(self.core.settings.value('outputWidth')), int(self.core.settings.value('outputHeight'))),(0,0,0)) + frame.paste(newFrame) + out_pipe.stdin.write(frame.tobytes()) + finally: + True + + # increase progress bar value + if progressBarValue + 1 <= (i / len(completeAudioArray)) * 100: + progressBarValue = numpy.floor((i / len(completeAudioArray)) * 100) + self.progressBarUpdate.emit(progressBarValue) + self.progressBarSetText.emit('%s%%' % str(int(progressBarValue))) numpy.seterr(all='print') -- cgit v1.2.3 From d9a5f2dd34c0bf14bfc99b58183b00d43217d889 Mon Sep 17 00:00:00 2001 From: DH4 Date: Sun, 28 May 2017 07:36:34 -0500 Subject: Fixed Resolution Change in preview. Removed debugging print statements. --- main.py | 3 +-- preview_thread.py | 8 +++----- 2 files changed, 4 insertions(+), 7 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index b700ad7..77fc9cc 100644 --- a/main.py +++ b/main.py @@ -299,7 +299,7 @@ class Main(QtCore.QObject): res = self.resolutions[resIndex].split('x') self.settings.setValue('outputWidth',res[0]) self.settings.setValue('outputHeight',res[1]) - self.drawPreview + self.drawPreview() def drawPreview(self): #self.settings.setValue('visLayout', self.window.comboBox_visLayout.currentIndex()) @@ -350,7 +350,6 @@ class Main(QtCore.QObject): self.window.stackedWidget.removeWidget(self.pages[index]) self.window.listWidget_componentList.takeItem(index) self.selectedComponents.pop(index) - print(self.selectedComponents) self.drawPreview() def changeComponentWidget(self): diff --git a/preview_thread.py b/preview_thread.py index e8b2021..7a7e619 100644 --- a/preview_thread.py +++ b/preview_thread.py @@ -28,7 +28,6 @@ class Worker(QtCore.QObject): "backgroundImage": backgroundImage, "components": components, } - print(components) self.queue.put(dic) @pyqtSlot() @@ -51,16 +50,15 @@ class Worker(QtCore.QObject): bgImage = bgImage[0] im = self.core.drawBaseImage(bgImage) - frame = Image.new("RGBA", (1280, 720),(0,0,0,255)) + width = int(self.core.settings.value('outputWidth')) + height = int(self.core.settings.value('outputHeight')) + frame = Image.new("RGBA", (width, height),(0,0,0,255)) frame.paste(im) componentWidgets = [self.stackedWidget.widget(i) for i in range(self.stackedWidget.count())] components = nextPreviewInformation["components"] - print(components) - print(componentWidgets) for component, componentWidget in zip(components, componentWidgets): - print('drawing') newFrame = Image.alpha_composite(frame,component.previewRender(self, componentWidget)) frame = Image.alpha_composite(frame,newFrame) -- cgit v1.2.3 From e0eed5bff4316910a93937d904f1a913f365a252 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 28 May 2017 14:19:28 -0400 Subject: title text is now a component plus numerous bugs removed and added --- components/original.py | 46 +++- components/text.py | 160 +++++++++---- core.py | 20 +- main.py | 115 +++------- main.ui | 602 +++++++++++++++++++++++++++++++++++++++++++++++++ preview_thread.py | 6 +- video_thread.py | 11 +- 7 files changed, 798 insertions(+), 162 deletions(-) create mode 100644 main.ui (limited to 'main.py') diff --git a/components/original.py b/components/original.py index d1caa7b..e901c21 100644 --- a/components/original.py +++ b/components/original.py @@ -1,13 +1,18 @@ ''' Original Audio Visualization ''' import numpy from PIL import Image, ImageDraw -from PyQt4 import uic +from PyQt4 import uic, QtGui +from PyQt4.QtGui import QColor import os, random class Component: - def widget(self,parent): + def __str__(self): + return __doc__ + + 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") @@ -16,18 +21,24 @@ class Component: #visLayoutValue = int(self.settings.value('visLayout')) 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 return page + def update(self): self.layout = self.page.comboBox_visLayout.currentIndex() - print(self.layout) + self.visColor = RGBFromString(self.page.lineEdit_visColor.text()) self.parent.drawPreview() - def previewRender(self, previewWorker, widget): + 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 drawBars(width, height, spectrum, (255, 255, 255), self.layout) + return drawBars(width, height, spectrum, self.visColor, self.layout) def preFrameRender(self, **kwargs): for kwarg, value in kwargs.items(): @@ -41,7 +52,15 @@ class Component: self.smoothConstantDown, self.smoothConstantUp, self.lastSpectrum) width = int(self.worker.core.settings.value('outputWidth')) height = int(self.worker.core.settings.value('outputHeight')) - return drawBars(width, height, self.lastSpectrum, (255,255,255), self.layout) + return drawBars(width, height, self.lastSpectrum, self.visColor, self.layout) + + def pickColor(self): + color = QtGui.QColorDialog.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() + self.page.lineEdit_visColor.setText(RGBstring) + self.page.pushButton_visColor.setStyleSheet(btnStyle) def transformData(i, completeAudioArray, sampleSize, smoothConstantDown, smoothConstantUp, lastSpectrum): if len(completeAudioArray) < (i + sampleSize): @@ -111,3 +130,16 @@ def drawBars(width, height, spectrum, color, layout): im.paste(imTop, (0, y), mask=imTop) return im + +def RGBFromString(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) diff --git a/components/text.py b/components/text.py index 68b02fe..814e13f 100644 --- a/components/text.py +++ b/components/text.py @@ -1,59 +1,131 @@ ''' Title Text ''' -import numpy from PIL import Image, ImageDraw -from PyQt4 import uic -import os +from PyQt4.QtGui import QPainter, QColor, QFont +from PyQt4 import uic, QtGui, QtCore +from PIL.ImageQt import ImageQt +import os, io class Component: - def widget(self,parent): + def __str__(self): + return __doc__ + + 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.titleFont = None + self.alignment = 1 + self.fontSize = height / 16 + self.xPosition = width / 2 + self.yPosition = height / 2 + 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.comboBox_textAlign.setCurrentIndex(1) + + page.spinBox_fontSize.setValue(int(int(parent.settings.value("outputHeight")) / 14 )) + page.spinBox_xTextAlign.setValue(int(int(parent.settings.value('outputWidth'))/2)) + page.spinBox_yTextAlign.setValue(int(int(parent.settings.value('outputHeight'))/2)) + + page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor) + page.pushButton_textColor.clicked.connect(lambda: self.pickColor()) + btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*self.textColor).name() + page.pushButton_textColor.setStyleSheet(btnStyle) + + page.lineEdit_title.setText(self.title) + if not self.titleFont == None: + page.fontComboBox_titleFont.setCurrentFont(QFont(self.titleFont)) + 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 previewRender(self, previewWorker, widget): + + def update(self): + self.title = self.page.lineEdit_title.text() + self.alignment = self.page.comboBox_textAlign.currentIndex() + self.titleFont = self.page.fontComboBox_titleFont.currentFont() + self.fontSize = self.page.spinBox_fontSize.value() + self.xPosition = self.page.spinBox_xTextAlign.value() + self.yPosition = self.page.spinBox_yTextAlign.value() + self.textColor = RGBFromString(self.page.lineEdit_textColor.text()) + self.parent.drawPreview() + + def previewRender(self, previewWorker): width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) - im = Image.new("RGBA", (width, height),(0,0,0,0)) - - return im + return self.addText(width, height) def preFrameRender(self, **kwargs): - pass + for kwarg, value in kwargs.items(): + exec('self.%s = value' % kwarg) + def frameRender(self, moduleNo, frameNo): - width = int(previewWorker.core.settings.value('outputWidth')) - height = int(previewWorker.core.settings.value('outputHeight')) + 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): im = Image.new("RGBA", (width, height),(0,0,0,0)) + image = ImageQt(im) + + image1 = QtGui.QImage(image) + painter = QPainter(image1) + self.titleFont.setPixelSize(self.fontSize) + painter.setFont(self.titleFont) + painter.setPen(QColor(*self.textColor)) - return im + fm = QtGui.QFontMetrics(self.titleFont) + if self.alignment == 0: #Left + self.xPosition = self.xPosition + if self.alignment == 1: #Middle + self.xPosition = self.xPosition - fm.width(self.title)/2 + if self.alignment == 2: #Right + self.xPosition = self.xPosition - fm.width(self.title) + painter.drawText(self.xPosition, self.yPosition, self.title) + painter.end() - ''' - self._image = ImageQt(im) - - self._image1 = QtGui.QImage(self._image) - painter = QPainter(self._image1) - font = titleFont - font.setPixelSize(fontSize) - painter.setFont(font) - painter.setPen(QColor(*textColor)) - - yPosition = yOffset - - fm = QtGui.QFontMetrics(font) - if alignment == 0: #Left - xPosition = xOffset - if alignment == 1: #Middle - xPosition = xOffset - fm.width(titleText)/2 - if alignment == 2: #Right - xPosition = xOffset - fm.width(titleText) - painter.drawText(xPosition, yPosition, titleText) - painter.end() - - buffer = QtCore.QBuffer() - buffer.open(QtCore.QIODevice.ReadWrite) - self._image1.save(buffer, "PNG") - - strio = io.BytesIO() - strio.write(buffer.data()) - buffer.close() - strio.seek(0) - return Image.open(strio) - ''' + buffer = QtCore.QBuffer() + buffer.open(QtCore.QIODevice.ReadWrite) + image1.save(buffer, "PNG") + + strio = io.BytesIO() + strio.write(buffer.data()) + buffer.close() + strio.seek(0) + return Image.open(strio) + + def pickColor(self): + color = QtGui.QColorDialog.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() + self.page.lineEdit_textColor.setText(RGBstring) + self.page.pushButton_textColor.setStyleSheet(btnStyle) + +def RGBFromString(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) diff --git a/core.py b/core.py index c8bfbca..5478f93 100644 --- a/core.py +++ b/core.py @@ -1,11 +1,9 @@ import sys, io, os from PyQt4 import QtCore, QtGui, uic -from PyQt4.QtGui import QPainter, QColor from os.path import expanduser import subprocess as sp import numpy -from PIL import Image, ImageDraw, ImageFont -from PIL.ImageQt import ImageQt +from PIL import Image import tempfile from shutil import rmtree import atexit @@ -34,7 +32,7 @@ class Core(): def parseBaseImage(self, backgroundImage, preview=False): ''' determines if the base image is a single frame or list of frames ''' if backgroundImage == "": - return [] + return [''] else: _, bgExt = os.path.splitext(backgroundImage) if not bgExt == '.mp4': @@ -112,17 +110,3 @@ class Core(): shell=True ) return sorted([os.path.join(self.tempDir, f) for f in os.listdir(self.tempDir)]) - - @staticmethod - def RGBFromString(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) diff --git a/main.py b/main.py index 77fc9cc..09d8e46 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,6 @@ import sys, io, os from PyQt4 import QtCore, QtGui, uic -from PyQt4.QtGui import QPainter, QColor, QFont from os.path import expanduser -import subprocess as sp -import numpy -from PIL import Image, ImageDraw, ImageFont -from PIL.ImageQt import ImageQt import atexit from queue import Queue from PyQt4.QtCore import QSettings @@ -14,9 +9,11 @@ from importlib import import_module import preview_thread, core, video_thread +# FIXME: commandline functionality broken until we decide how to implement it +''' class Command(QtCore.QObject): - videoTask = QtCore.pyqtSignal(str, str, QFont, int, int, int, int, tuple, tuple, str, str, list) + videoTask = QtCore.pyqtSignal(str, str, str, list) def __init__(self): QtCore.QObject.__init__(self) @@ -110,7 +107,7 @@ class Command(QtCore.QObject): self.settings.setValue("visColor", '%s,%s,%s' % self.visColor) self.settings.setValue("textColor", '%s,%s,%s' % self.textColor) sys.exit(0) - +''' class Main(QtCore.QObject): newTask = QtCore.pyqtSignal(str, list) @@ -127,10 +124,6 @@ class Main(QtCore.QObject): LoadDefaultSettings(self) self.pages = [] - - # load colors as tuples from a comma-separated string - self.textColor = core.Core.RGBFromString(self.settings.value("textColor", '255, 255, 255')) - self.visColor = core.Core.RGBFromString(self.settings.value("visColor", '255, 255, 255')) self.previewQueue = Queue() @@ -174,49 +167,10 @@ class Main(QtCore.QObject): window.comboBox_resolution.currentIndexChanged.connect(self.updateResolution) ''' - window.comboBox_textAlign.addItem("Left") - window.comboBox_textAlign.addItem("Middle") - window.comboBox_textAlign.addItem("Right") - window.comboBox_textAlign.setCurrentIndex(1) - - window.spinBox_fontSize.setValue(int(int(self.settings.value("outputHeight")) / 14 )) - window.spinBox_xTextAlign.setValue(int(int(self.settings.value('outputWidth'))/2)) - window.spinBox_yTextAlign.setValue(int(int(self.settings.value('outputHeight'))/2)) - - window.lineEdit_textColor.setText('%s,%s,%s' % self.textColor) window.lineEdit_visColor.setText('%s,%s,%s' % self.visColor) - window.pushButton_textColor.clicked.connect(lambda: self.pickColor('text')) window.pushButton_visColor.clicked.connect(lambda: self.pickColor('vis')) - btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*self.textColor).name() - window.pushButton_textColor.setStyleSheet(btnStyle) btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*self.visColor).name() window.pushButton_visColor.setStyleSheet(btnStyle) - - titleFont = self.settings.value("titleFont") - if not titleFont == None: - window.fontComboBox_titleFont.setCurrentFont(QFont(titleFont)) - - alignment = self.settings.value("alignment") - if not alignment == None: - window.comboBox_textAlign.setCurrentIndex(int(alignment)) - fontSize = self.settings.value("fontSize") - if not fontSize == None: - window.spinBox_fontSize.setValue(int(fontSize)) - xPosition = self.settings.value("xPosition") - if not xPosition == None: - window.spinBox_xTextAlign.setValue(int(xPosition)) - yPosition = self.settings.value("yPosition") - if not yPosition == None: - window.spinBox_yTextAlign.setValue(int(yPosition)) - - window.fontComboBox_titleFont.currentFontChanged.connect(self.drawPreview) - window.lineEdit_title.textChanged.connect(self.drawPreview) - window.comboBox_textAlign.currentIndexChanged.connect(self.drawPreview) - window.comboBox_visLayout.currentIndexChanged.connect(self.drawPreview) - window.spinBox_xTextAlign.valueChanged.connect(self.drawPreview) - window.spinBox_yTextAlign.valueChanged.connect(self.drawPreview) - window.spinBox_fontSize.valueChanged.connect(self.drawPreview) - window.lineEdit_textColor.textChanged.connect(self.drawPreview) window.lineEdit_visColor.textChanged.connect(self.drawPreview) ''' self.drawPreview() @@ -227,7 +181,8 @@ class Main(QtCore.QObject): self.timer.stop() self.previewThread.quit() self.previewThread.wait() - + # TODO: replace remembered settings with presets/projects + ''' self.settings.setValue("titleFont", self.window.fontComboBox_titleFont.currentFont().toString()) self.settings.setValue("alignment", str(self.window.comboBox_textAlign.currentIndex())) self.settings.setValue("fontSize", str(self.window.spinBox_fontSize.value())) @@ -235,6 +190,7 @@ class Main(QtCore.QObject): self.settings.setValue("yPosition", str(self.window.spinBox_yTextAlign.value())) self.settings.setValue("visColor", self.window.lineEdit_visColor.text()) self.settings.setValue("textColor", self.window.lineEdit_textColor.text()) + ''' def openInputFileDialog(self): inputDir = self.settings.value("inputDir", expanduser("~")) @@ -268,21 +224,26 @@ class Main(QtCore.QObject): self.drawPreview() def createAudioVisualisation(self): - ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~")) - - 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.videoThread.start() - self.videoTask.emit(self.window.lineEdit_background.text(), - self.window.lineEdit_audioFile.text(), - self.window.lineEdit_outputFile.text(), - self.selectedComponents) + # create output video if mandatory settings are filled in + if self.window.lineEdit_audioFile.text() and self.window.lineEdit_outputFile.text(): + ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~")) + + 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.videoThread.start() + self.videoTask.emit(self.window.lineEdit_background.text(), + self.window.lineEdit_audioFile.text(), + self.window.lineEdit_outputFile.text(), + self.selectedComponents) + else: + # TODO: use QMessageBox or similar to alert user that fields are empty + pass def progressBarUpdated(self, value): self.window.progressBar_createVideo.setValue(value) @@ -312,18 +273,6 @@ class Main(QtCore.QObject): self.window.label_previewContainer.setPixmap(self._previewPixmap) - def pickColor(self, colorTarget): - color = QtGui.QColorDialog.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() - if colorTarget == 'text': - self.window.lineEdit_textColor.setText(RGBstring) - window.pushButton_textColor.setStyleSheet(btnStyle) - elif colorTarget == 'vis': - self.window.lineEdit_visColor.setText(RGBstring) - window.pushButton_visColor.setStyleSheet(btnStyle) - def findComponents(self): def findComponents(): srcPath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'components') @@ -339,8 +288,7 @@ class Main(QtCore.QObject): def addComponent(self, moduleIndex): self.window.listWidget_componentList.addItem(self.modules[moduleIndex].__doc__) self.selectedComponents.append(self.modules[moduleIndex].Component()) - self.selectedComponents[-1].page = self.selectedComponents[-1].widget(self) - self.pages.append(self.selectedComponents[-1].page) + self.pages.append(self.selectedComponents[-1].widget(self)) self.window.stackedWidget.addWidget(self.pages[-1]) self.selectedComponents[-1].update() @@ -381,6 +329,8 @@ def LoadDefaultSettings(self): if self.settings.value(parm) == None: self.settings.setValue(parm,value) + +''' ####### commandline functionality broken until we decide how to implement it if len(sys.argv) > 1: # command line mode app = QtGui.QApplication(sys.argv, False) @@ -388,8 +338,9 @@ if len(sys.argv) > 1: signal.signal(signal.SIGINT, command.cleanUp) sys.exit(app.exec_()) else: - # gui mode - if __name__ == "__main__": +''' +# gui mode +if __name__ == "__main__": app = QtGui.QApplication(sys.argv) window = uic.loadUi("mainwindow.ui") # window.adjustSize() diff --git a/main.ui b/main.ui new file mode 100644 index 0000000..c2892c5 --- /dev/null +++ b/main.ui @@ -0,0 +1,602 @@ + + + MainWindow + + + + 0 + 0 + 635 + 600 + + + + + 0 + 0 + + + + + 635 + 600 + + + + MainWindow + + + + + 0 + 0 + + + + + + + + + + 0 + 0 + + + + + 0 + 200 + + + + + 16777215 + 16777215 + + + + GroupBox + + + + + + + + + + + 1 + 0 + + + + + 200 + 0 + + + + + 16777215 + 16777215 + + + + PushButton + + + + + + + + 2 + 0 + + + + QFrame::Box + + + + + + + + + + + + + + + 1 + 0 + + + + + 200 + 0 + + + + + 16777215 + 16777215 + + + + PushButton + + + + + + + + 2 + 0 + + + + QFrame::Box + + + + + + + + + + + + + + + 1 + 0 + + + + + 200 + 0 + + + + + 16777215 + 16777215 + + + + PushButton + + + + + + + + 2 + 0 + + + + QFrame::Box + + + + + + + + + + + + + + + 200 + 0 + + + + + 200 + 16777215 + + + + + 200 + 0 + + + + QFrame::NoFrame + + + + + + + + + + + + + + + + + + 200 + 0 + + + + + 200 + 16777215 + + + + + 200 + 0 + + + + QFrame::NoFrame + + + + + + + + + + + + + + + + + + + + + + 999 + + + + + + + Qt::LeftToRight + + + X + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + -99999 + + + 99999 + + + + + + + Y + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + -99999 + + + 99999 + + + + + + + + + + + + + + 200 + 0 + + + + + 200 + 16777215 + + + + + 200 + 0 + + + + QFrame::NoFrame + + + + + + + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 32 + 32 + + + + + + + + 32 + 32 + + + + + + + + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 32 + 32 + + + + + + + + 32 + 32 + + + + + + + + + + + + + + + + + + 200 + 0 + + + + + 200 + 16777215 + + + + + 200 + 0 + + + + QFrame::NoFrame + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + 0 + 220 + + + + + 16777215 + 220 + + + + GroupBox + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + 320 + 180 + + + + + 320 + 180 + + + + QFrame::Box + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + GroupBox + + + + + + + + 24 + + + Qt::AlignCenter + + + true + + + + + + + + 0 + 0 + + + + PushButton + + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + + + diff --git a/preview_thread.py b/preview_thread.py index 7a7e619..b20e9a1 100644 --- a/preview_thread.py +++ b/preview_thread.py @@ -55,11 +55,9 @@ class Worker(QtCore.QObject): frame = Image.new("RGBA", (width, height),(0,0,0,255)) frame.paste(im) - - componentWidgets = [self.stackedWidget.widget(i) for i in range(self.stackedWidget.count())] components = nextPreviewInformation["components"] - for component, componentWidget in zip(components, componentWidgets): - newFrame = Image.alpha_composite(frame,component.previewRender(self, componentWidget)) + for component in components: + newFrame = Image.alpha_composite(frame,component.previewRender(self)) frame = Image.alpha_composite(frame,newFrame) self._image = ImageQt(frame) diff --git a/video_thread.py b/video_thread.py index ccb2730..8bef6ef 100644 --- a/video_thread.py +++ b/video_thread.py @@ -76,19 +76,16 @@ class Worker(QtCore.QObject): stdin=sp.PIPE,stdout=sys.stdout, stderr=sys.stdout) # initialize components - componentWidgets = [self.stackedWidget.widget(i) for i in range(self.stackedWidget.count())] - print('######################## Data') - print(components) - print(componentWidgets) + print('loaded components: ', [str(component) for component in components]) sampleSize = 1470 - for component, widget in zip(components, componentWidgets): - component.preFrameRender(worker=self, widget=widget, completeAudioArray=completeAudioArray, sampleSize=sampleSize) + for component in components: + component.preFrameRender(worker=self, completeAudioArray=completeAudioArray, sampleSize=sampleSize) + # create video for output numpy.seterr(divide='ignore') frame = getBackgroundAtIndex(0) bgI = 0 - # create video for output for i in range(0, len(completeAudioArray), sampleSize): newFrame = Image.new("RGBA", (int(self.core.settings.value('outputWidth')), int(self.core.settings.value('outputHeight'))),(0,0,0,255)) -- cgit v1.2.3 From e3079f7a67ce8939ebb861b9580c281f81331181 Mon Sep 17 00:00:00 2001 From: DH4 Date: Sun, 28 May 2017 14:19:06 -0500 Subject: Fixed Stack & list sync bug. --- main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'main.py') diff --git a/main.py b/main.py index 09d8e46..d165fc5 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,7 @@ from PyQt4 import QtCore, QtGui, uic from os.path import expanduser import atexit from queue import Queue -from PyQt4.QtCore import QSettings +from PyQt4.QtCore import QSettings, QModelIndex import signal from importlib import import_module @@ -286,9 +286,11 @@ class Main(QtCore.QObject): return [import_module('components.%s' % name) for name in findComponents()] def addComponent(self, moduleIndex): + index = len(self.pages) self.window.listWidget_componentList.addItem(self.modules[moduleIndex].__doc__) self.selectedComponents.append(self.modules[moduleIndex].Component()) self.pages.append(self.selectedComponents[-1].widget(self)) + self.window.listWidget_componentList.setCurrentRow(index) self.window.stackedWidget.addWidget(self.pages[-1]) self.selectedComponents[-1].update() @@ -298,6 +300,7 @@ class Main(QtCore.QObject): self.window.stackedWidget.removeWidget(self.pages[index]) self.window.listWidget_componentList.takeItem(index) self.selectedComponents.pop(index) + self.pages.pop(index) self.drawPreview() def changeComponentWidget(self): -- cgit v1.2.3 From 719e9a4ddf306b06bce7a5dcf0f3028731db0664 Mon Sep 17 00:00:00 2001 From: DH4 Date: Sun, 28 May 2017 15:05:08 -0500 Subject: Implemented change list order --- main.py | 31 +++++++++++++++++++++++++++++++ mainwindow.ui | 27 ++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 3 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index d165fc5..8a9ba8c 100644 --- a/main.py +++ b/main.py @@ -166,6 +166,9 @@ class Main(QtCore.QObject): window.comboBox_resolution.setCurrentIndex(currentRes) window.comboBox_resolution.currentIndexChanged.connect(self.updateResolution) + self.window.pushButton_listMoveUp.clicked.connect(self.moveComponentUp) + self.window.pushButton_listMoveDown.clicked.connect(self.moveComponentDown) + ''' window.lineEdit_visColor.setText('%s,%s,%s' % self.visColor) window.pushButton_visColor.clicked.connect(lambda: self.pickColor('vis')) @@ -292,6 +295,7 @@ class Main(QtCore.QObject): self.pages.append(self.selectedComponents[-1].widget(self)) self.window.listWidget_componentList.setCurrentRow(index) self.window.stackedWidget.addWidget(self.pages[-1]) + self.window.stackedWidget.setCurrentIndex(index) self.selectedComponents[-1].update() def removeComponent(self): @@ -308,6 +312,33 @@ class Main(QtCore.QObject): index = self.window.listWidget_componentList.row(selected[0]) self.window.stackedWidget.setCurrentIndex(index) + def moveComponentUp(self): + row = self.window.listWidget_componentList.currentRow() + if row > 0: + item = self.window.listWidget_componentList.takeItem(row) + self.window.listWidget_componentList.insertItem(row - 1, item) + page = self.pages[row] + self.pages.pop(row) + self.pages.insert(row - 1, page) + widget = self.window.stackedWidget.removeWidget(page) + self.window.stackedWidget.insertWidget(row - 1, page) + self.window.listWidget_componentList.setCurrentRow(row - 1) + self.window.stackedWidget.setCurrentIndex(row -1) + + def moveComponentDown(self): + row = self.window.listWidget_componentList.currentRow() + if row < len(self.pages): + item = self.window.listWidget_componentList.takeItem(row) + self.window.listWidget_componentList.insertItem(row + 1, item) + page = self.pages[row] + self.pages.pop(row) + self.pages.insert(row + 1, page) + widget = self.window.stackedWidget.removeWidget(page) + self.window.stackedWidget.insertWidget(row + 1, page) + self.window.listWidget_componentList.setCurrentRow(row + 1) + self.window.stackedWidget.setCurrentIndex(row + 1) + + def LoadDefaultSettings(self): self.resolutions = [ '1920x1080', diff --git a/mainwindow.ui b/mainwindow.ui index ce8233e..b15cc8e 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -129,6 +129,13 @@ + + + + Save As + + + @@ -142,7 +149,7 @@ 20 - 10 + 20 @@ -168,14 +175,28 @@ - Add Component + Add - Remove Component + Remove + + + + + + + Down + + + + + + + Up -- cgit v1.2.3 From b2e3716a2920aff875d7ec44ab1b9b1aa521101a Mon Sep 17 00:00:00 2001 From: DH4 Date: Sun, 28 May 2017 15:46:59 -0500 Subject: Fixed component list not affecting render order. FIXME Reverse the render order --- main.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index 8a9ba8c..7cae950 100644 --- a/main.py +++ b/main.py @@ -315,11 +315,14 @@ class Main(QtCore.QObject): def moveComponentUp(self): row = self.window.listWidget_componentList.currentRow() if row > 0: - item = self.window.listWidget_componentList.takeItem(row) - self.window.listWidget_componentList.insertItem(row - 1, item) + module = self.selectedComponents[row] + self.selectedComponents.pop(row) + self.selectedComponents.insert(row - 1,module) page = self.pages[row] self.pages.pop(row) self.pages.insert(row - 1, page) + item = self.window.listWidget_componentList.takeItem(row) + self.window.listWidget_componentList.insertItem(row - 1, item) widget = self.window.stackedWidget.removeWidget(page) self.window.stackedWidget.insertWidget(row - 1, page) self.window.listWidget_componentList.setCurrentRow(row - 1) @@ -327,12 +330,15 @@ class Main(QtCore.QObject): def moveComponentDown(self): row = self.window.listWidget_componentList.currentRow() - if row < len(self.pages): - item = self.window.listWidget_componentList.takeItem(row) - self.window.listWidget_componentList.insertItem(row + 1, item) + if row < len(self.pages) + 1: + module = self.selectedComponents[row] + self.selectedComponents.pop(row) + self.selectedComponents.insert(row + 1,module) page = self.pages[row] self.pages.pop(row) self.pages.insert(row + 1, page) + item = self.window.listWidget_componentList.takeItem(row) + self.window.listWidget_componentList.insertItem(row + 1, item) widget = self.window.stackedWidget.removeWidget(page) self.window.stackedWidget.insertWidget(row + 1, page) self.window.listWidget_componentList.setCurrentRow(row + 1) -- cgit v1.2.3 From 39944a56a860836c62b5358174be9e65bd66dc66 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 28 May 2017 19:08:50 -0400 Subject: create data directory structure --- main.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index 7cae950..7ddc4f2 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,12 @@ import sys, io, os -from PyQt4 import QtCore, QtGui, uic from os.path import expanduser import atexit from queue import Queue -from PyQt4.QtCore import QSettings, QModelIndex import signal from importlib import import_module +from PyQt4 import QtCore, QtGui, uic +from PyQt4.QtCore import QSettings, QModelIndex +from PyQt4.QtGui import QDesktopServices import preview_thread, core, video_thread @@ -123,6 +124,14 @@ class Main(QtCore.QObject): self.settings = QSettings('settings.ini', QSettings.IniFormat) LoadDefaultSettings(self) + # create data directory structure if needed + dataDir = QDesktopServices.storageLocation(QDesktopServices.DataLocation) + if not os.path.exists(dataDir): + os.makedirs(dataDir) + for neededDirectory in ('projects', 'presets'): + if not os.path.exists(os.path.join(dataDir, neededDirectory)): + os.mkdir(os.path.join(dataDir, neededDirectory)) + self.pages = [] self.previewQueue = Queue() @@ -382,6 +391,8 @@ else: # gui mode if __name__ == "__main__": app = QtGui.QApplication(sys.argv) + app.setApplicationName("audio-visualizer") + app.setOrganizationName("audio-visualizer") window = uic.loadUi("mainwindow.ui") # window.adjustSize() desc = QtGui.QDesktopWidget() -- cgit v1.2.3 From ce414ff96081d1c32fe04503b855fd04a32e82bb Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 28 May 2017 19:50:29 -0400 Subject: turned openPreset button into comboBox to fit a new design --- main.py | 18 +++++++++++------- mainwindow.ui | 10 ++++++---- 2 files changed, 17 insertions(+), 11 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index 7ddc4f2..d4c12d8 100644 --- a/main.py +++ b/main.py @@ -178,13 +178,11 @@ class Main(QtCore.QObject): self.window.pushButton_listMoveUp.clicked.connect(self.moveComponentUp) self.window.pushButton_listMoveDown.clicked.connect(self.moveComponentDown) - ''' - window.lineEdit_visColor.setText('%s,%s,%s' % self.visColor) - window.pushButton_visColor.clicked.connect(lambda: self.pickColor('vis')) - btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*self.visColor).name() - window.pushButton_visColor.setStyleSheet(btnStyle) - window.lineEdit_visColor.textChanged.connect(self.drawPreview) - ''' + self.window.pushButton_savePreset.clicked.connect(self.openSavePresetDialog) + self.window.comboBox_openPreset.currentIndexChanged.connect( \ + lambda _: self.openPreset(self.window.comboBox_openPreset.currentIndex()) + ) + self.drawPreview() window.show() @@ -353,6 +351,12 @@ class Main(QtCore.QObject): self.window.listWidget_componentList.setCurrentRow(row + 1) self.window.stackedWidget.setCurrentIndex(row + 1) + def openSavePresetDialog(self): + pass + + def openPreset(self, comboBoxIndex): + pass + def LoadDefaultSettings(self): self.resolutions = [ diff --git a/mainwindow.ui b/mainwindow.ui index b15cc8e..0dcce91 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -217,10 +217,12 @@ - - - Open Preset - + + + + Open Preset + + -- cgit v1.2.3 From c0920da4ffa0a78bac3eec7fdadba3a4183a8fed Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 28 May 2017 21:24:51 -0400 Subject: savePreset creates a file --- components/original.py | 3 +++ components/text.py | 3 +++ main.py | 32 ++++++++++++++++++++++++++------ 3 files changed, 32 insertions(+), 6 deletions(-) (limited to 'main.py') diff --git a/components/original.py b/components/original.py index e901c21..5655867 100644 --- a/components/original.py +++ b/components/original.py @@ -34,6 +34,9 @@ class Component: self.visColor = RGBFromString(self.page.lineEdit_visColor.text()) self.parent.drawPreview() + def savePreset(self): + return {} + def previewRender(self, previewWorker): spectrum = numpy.fromfunction(lambda x: 0.008*(x-128)**2, (255,), dtype="int16") width = int(previewWorker.core.settings.value('outputWidth')) diff --git a/components/text.py b/components/text.py index eab33b2..e900994 100644 --- a/components/text.py +++ b/components/text.py @@ -73,6 +73,9 @@ class Component: self.parent.drawPreview() + def savePreset(self): + return {} + def previewRender(self, previewWorker): width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) diff --git a/main.py b/main.py index d4c12d8..fe76e2c 100644 --- a/main.py +++ b/main.py @@ -125,12 +125,12 @@ class Main(QtCore.QObject): LoadDefaultSettings(self) # create data directory structure if needed - dataDir = QDesktopServices.storageLocation(QDesktopServices.DataLocation) - if not os.path.exists(dataDir): - os.makedirs(dataDir) + self.dataDir = QDesktopServices.storageLocation(QDesktopServices.DataLocation) + if not os.path.exists(self.dataDir): + os.makedirs(self.dataDir) for neededDirectory in ('projects', 'presets'): - if not os.path.exists(os.path.join(dataDir, neededDirectory)): - os.mkdir(os.path.join(dataDir, neededDirectory)) + if not os.path.exists(os.path.join(self.dataDir, neededDirectory)): + os.mkdir(os.path.join(self.dataDir, neededDirectory)) self.pages = [] @@ -352,7 +352,27 @@ class Main(QtCore.QObject): self.window.stackedWidget.setCurrentIndex(row + 1) def openSavePresetDialog(self): - pass + if self.window.listWidget_componentList.currentRow() == -1: + return + newName, OK = QtGui.QInputDialog.getText(QtGui.QWidget(), 'Audio Visualizer', 'New Preset Name:') + if OK and newName: + index = self.window.listWidget_componentList.currentRow() + if index != -1: + saveValueStore = self.selectedComponents[index].savePreset() + componentName = str(self.selectedComponents[index]).strip() + if hasattr(self.selectedComponents[index], 'version'): + vers = self.selectedComponents[index].version() + else: + vers = 1 + self.createPresetFile(componentName, vers, saveValueStore, newName) + + def createPresetFile(self, componentName, version, saveValueStore, filename): + dirname = os.path.join(self.dataDir, 'presets', componentName, str(version)) + if not os.path.exists(dirname): + os.makedirs(dirname) + with open(os.path.join(dirname, filename), 'w') as f: + for itemset in saveValueStore.items(): + f.write('%s=%s' % itemset) def openPreset(self, comboBoxIndex): pass -- cgit v1.2.3 From db7acbf3ea353d6c5b21de44b4f532b43339ac5c Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 28 May 2017 22:58:13 -0400 Subject: save empty presets, comboBox populates with preset names --- components/original.py | 3 +++ components/text.py | 3 +++ main.py | 18 ++++++++++++++---- 3 files changed, 20 insertions(+), 4 deletions(-) (limited to 'main.py') diff --git a/components/original.py b/components/original.py index 5655867..e543dac 100644 --- a/components/original.py +++ b/components/original.py @@ -34,6 +34,9 @@ class Component: self.visColor = RGBFromString(self.page.lineEdit_visColor.text()) self.parent.drawPreview() + def version(self): + return 1 + def savePreset(self): return {} diff --git a/components/text.py b/components/text.py index e900994..334fc80 100644 --- a/components/text.py +++ b/components/text.py @@ -55,6 +55,9 @@ class Component: self.page = page return page + def version(self): + return 1 + def update(self): self.title = self.page.lineEdit_title.text() self.alignment = self.page.comboBox_textAlign.currentIndex() diff --git a/main.py b/main.py index fe76e2c..5bb10f5 100644 --- a/main.py +++ b/main.py @@ -304,6 +304,7 @@ class Main(QtCore.QObject): self.window.stackedWidget.addWidget(self.pages[-1]) self.window.stackedWidget.setCurrentIndex(index) self.selectedComponents[-1].update() + self.updateOpenPresetComboBox(self.selectedComponents[-1]) def removeComponent(self): for selected in self.window.listWidget_componentList.selectedItems(): @@ -318,6 +319,7 @@ class Main(QtCore.QObject): selected = self.window.listWidget_componentList.selectedItems() index = self.window.listWidget_componentList.row(selected[0]) self.window.stackedWidget.setCurrentIndex(index) + self.updateOpenPresetComboBox(self.selectedComponents[index]) def moveComponentUp(self): row = self.window.listWidget_componentList.currentRow() @@ -351,6 +353,16 @@ class Main(QtCore.QObject): self.window.listWidget_componentList.setCurrentRow(row + 1) self.window.stackedWidget.setCurrentIndex(row + 1) + def updateOpenPresetComboBox(self, component): + self.window.comboBox_openPreset.clear() + self.window.comboBox_openPreset.addItem("Open Preset") + destination = os.path.join(self.dataDir, 'presets', + str(component).strip(), str(component.version())) + if not os.path.exists(destination): + os.makedirs(destination) + for f in os.listdir(destination): + self.window.comboBox_openPreset.addItem(f) + def openSavePresetDialog(self): if self.window.listWidget_componentList.currentRow() == -1: return @@ -360,10 +372,7 @@ class Main(QtCore.QObject): if index != -1: saveValueStore = self.selectedComponents[index].savePreset() componentName = str(self.selectedComponents[index]).strip() - if hasattr(self.selectedComponents[index], 'version'): - vers = self.selectedComponents[index].version() - else: - vers = 1 + vers = self.selectedComponents[index].version() self.createPresetFile(componentName, vers, saveValueStore, newName) def createPresetFile(self, componentName, version, saveValueStore, filename): @@ -373,6 +382,7 @@ class Main(QtCore.QObject): with open(os.path.join(dirname, filename), 'w') as f: for itemset in saveValueStore.items(): f.write('%s=%s' % itemset) + self.window.comboBox_openPreset.addItem(filename) def openPreset(self, comboBoxIndex): pass -- cgit v1.2.3 From 8dd7b7d59ab3ef3caf2bbd69dd0b2a7eb134edc7 Mon Sep 17 00:00:00 2001 From: tassaron Date: Mon, 29 May 2017 20:39:11 -0400 Subject: added component base class --- components/__base__.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++ components/original.py | 35 +++++++------------------------ components/text.py | 36 +++++++------------------------ main.py | 4 ++-- 4 files changed, 74 insertions(+), 58 deletions(-) create mode 100644 components/__base__.py (limited to 'main.py') diff --git a/components/__base__.py b/components/__base__.py new file mode 100644 index 0000000..87440bb --- /dev/null +++ b/components/__base__.py @@ -0,0 +1,57 @@ +from PyQt4 import QtGui + +class Component: + def __str__(self): + return self.__doc__ + + def preFrameRender(self, **kwargs): + for kwarg, value in kwargs.items(): + exec('self.%s = value' % kwarg) + + def pickColor(self): + color = QtGui.QColorDialog.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 + + 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 widgets signals + self.page = page + return page + + def update(self): + # read widget values + 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 + ''' diff --git a/components/original.py b/components/original.py index e901c21..4a149e2 100644 --- a/components/original.py +++ b/components/original.py @@ -1,15 +1,13 @@ -''' Original Audio Visualization ''' import numpy from PIL import Image, ImageDraw from PyQt4 import uic, QtGui from PyQt4.QtGui import QColor import os, random +from . import __base__ -class Component: - def __str__(self): - return __doc__ - +class Component(__base__.Component): + '''Original Audio Visualization''' def widget(self, parent): self.parent = parent self.visColor = (255,255,255) @@ -31,7 +29,7 @@ class Component: def update(self): self.layout = self.page.comboBox_visLayout.currentIndex() - self.visColor = RGBFromString(self.page.lineEdit_visColor.text()) + self.visColor = self.RGBFromString(self.page.lineEdit_visColor.text()) self.parent.drawPreview() def previewRender(self, previewWorker): @@ -41,8 +39,7 @@ class Component: return drawBars(width, height, spectrum, self.visColor, self.layout) def preFrameRender(self, **kwargs): - for kwarg, value in kwargs.items(): - exec('self.%s = value' % kwarg) + super().preFrameRender(**kwargs) self.smoothConstantDown = 0.08 self.smoothConstantUp = 0.8 self.lastSpectrum = None @@ -55,12 +52,9 @@ class Component: return drawBars(width, height, self.lastSpectrum, self.visColor, self.layout) def pickColor(self): - color = QtGui.QColorDialog.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() - self.page.lineEdit_visColor.setText(RGBstring) - self.page.pushButton_visColor.setStyleSheet(btnStyle) + RGBstring, btnStyle = super().pickColor() + self.page.lineEdit_visColor.setText(RGBstring) + self.page.pushButton_visColor.setStyleSheet(btnStyle) def transformData(i, completeAudioArray, sampleSize, smoothConstantDown, smoothConstantUp, lastSpectrum): if len(completeAudioArray) < (i + sampleSize): @@ -130,16 +124,3 @@ def drawBars(width, height, spectrum, color, layout): im.paste(imTop, (0, y), mask=imTop) return im - -def RGBFromString(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) diff --git a/components/text.py b/components/text.py index eab33b2..1f5e222 100644 --- a/components/text.py +++ b/components/text.py @@ -1,15 +1,13 @@ -''' Title Text ''' 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, io +from . import __base__ -class Component: - def __str__(self): - return __doc__ - +class Component(__base__.Component): + '''Title Text''' def widget(self, parent): height = int(parent.settings.value('outputHeight')) width = int(parent.settings.value('outputWidth')) @@ -62,7 +60,7 @@ class Component: self.fontSize = self.page.spinBox_fontSize.value() self.xPosition = self.page.spinBox_xTextAlign.value() self.yPosition = self.page.spinBox_yTextAlign.value() - self.textColor = RGBFromString(self.page.lineEdit_textColor.text()) + self.textColor = self.RGBFromString(self.page.lineEdit_textColor.text()) fm = QtGui.QFontMetrics(self.titleFont) if self.alignment == 0: #Left self.xPosition = self.xPosition @@ -77,10 +75,6 @@ class Component: width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) return self.addText(width, height) - - def preFrameRender(self, **kwargs): - for kwarg, value in kwargs.items(): - exec('self.%s = value' % kwarg) def frameRender(self, moduleNo, frameNo): width = int(self.worker.core.settings.value('outputWidth')) @@ -112,22 +106,6 @@ class Component: return Image.open(strio) def pickColor(self): - color = QtGui.QColorDialog.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() - self.page.lineEdit_textColor.setText(RGBstring) - self.page.pushButton_textColor.setStyleSheet(btnStyle) - -def RGBFromString(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) + RGBstring, btnStyle = super().pickColor() + self.page.lineEdit_textColor.setText(RGBstring) + self.page.pushButton_textColor.setStyleSheet(btnStyle) diff --git a/main.py b/main.py index 7cae950..1d22704 100644 --- a/main.py +++ b/main.py @@ -149,7 +149,7 @@ class Main(QtCore.QObject): self.modules = self.findComponents() for component in self.modules: - window.comboBox_componentSelection.addItem(component.__doc__) + window.comboBox_componentSelection.addItem(component.Component.__doc__) window.listWidget_componentList.clicked.connect(lambda _: self.changeComponentWidget()) self.selectedComponents = [] @@ -290,8 +290,8 @@ class Main(QtCore.QObject): def addComponent(self, moduleIndex): index = len(self.pages) - self.window.listWidget_componentList.addItem(self.modules[moduleIndex].__doc__) self.selectedComponents.append(self.modules[moduleIndex].Component()) + self.window.listWidget_componentList.addItem(self.selectedComponents[-1].__doc__) self.pages.append(self.selectedComponents[-1].widget(self)) self.window.listWidget_componentList.setCurrentRow(index) self.window.stackedWidget.addWidget(self.pages[-1]) -- cgit v1.2.3 From ca7e8bdb0dc998088aeb45a77987a78cc4656b34 Mon Sep 17 00:00:00 2001 From: tassaron Date: Tue, 30 May 2017 19:31:10 -0400 Subject: the most simple way of saving dictionaries --- components/__base__.py | 7 +++++++ components/original.py | 8 +++++--- main.py | 30 +++++++++++++++++++++++------- 3 files changed, 35 insertions(+), 10 deletions(-) (limited to 'main.py') diff --git a/components/__base__.py b/components/__base__.py index 87440bb..252ad03 100644 --- a/components/__base__.py +++ b/components/__base__.py @@ -3,6 +3,9 @@ from PyQt4 import QtGui class Component: def __str__(self): return self.__doc__ + + def version(self): + return 1 def preFrameRender(self, **kwargs): for kwarg, value in kwargs.items(): @@ -54,4 +57,8 @@ class Component: height = int(self.worker.core.settings.value('outputHeight')) image = Image.new("RGBA", (width, height), (0,0,0,0)) return image + + def version(self): + # change this number to identify new versions of your component + return 1 ''' diff --git a/components/original.py b/components/original.py index 47e53b8..40f51eb 100644 --- a/components/original.py +++ b/components/original.py @@ -32,11 +32,13 @@ class Component(__base__.Component): self.visColor = self.RGBFromString(self.page.lineEdit_visColor.text()) self.parent.drawPreview() - def version(self): - return 1 + def loadPreset(self, presetDict): + self.preFrameRender(**presetDict) def savePreset(self): - return {} + return { 'layout' : self.page.comboBox_visLayout.currentIndex(), + 'visColor' : self.page.lineEdit_visColor.text(), + } def previewRender(self, previewWorker): spectrum = numpy.fromfunction(lambda x: 0.008*(x-128)**2, (255,), dtype="int16") diff --git a/main.py b/main.py index 474ab29..77b56c3 100644 --- a/main.py +++ b/main.py @@ -179,9 +179,7 @@ class Main(QtCore.QObject): self.window.pushButton_listMoveDown.clicked.connect(self.moveComponentDown) self.window.pushButton_savePreset.clicked.connect(self.openSavePresetDialog) - self.window.comboBox_openPreset.currentIndexChanged.connect( \ - lambda _: self.openPreset(self.window.comboBox_openPreset.currentIndex()) - ) + self.window.comboBox_openPreset.currentIndexChanged.connect(self.openPreset) self.drawPreview() @@ -380,12 +378,30 @@ class Main(QtCore.QObject): if not os.path.exists(dirname): os.makedirs(dirname) with open(os.path.join(dirname, filename), 'w') as f: - for itemset in saveValueStore.items(): - f.write('%s=%s' % itemset) + f.write('%s' % repr(saveValueStore)) self.window.comboBox_openPreset.addItem(filename) - def openPreset(self, comboBoxIndex): - pass + def openPreset(self): + if self.window.comboBox_openPreset.currentIndex() < 1: + return + index = self.window.listWidget_componentList.currentRow() + if index == -1: + # no component selected + return + filename = self.window.comboBox_openPreset.itemText(self.window.comboBox_openPreset.currentIndex()) + componentName = str(self.selectedComponents[index]).strip() + version = self.selectedComponents[index].version() + dirname = os.path.join(self.dataDir, 'presets', componentName, str(version)) + filepath = os.path.join(dirname, filename) + if not os.path.exists(filepath): + self.window.comboBox_openPreset.removeItem(self.window.comboBox_openPreset.currentIndex()) + return + with open(filepath, 'r') as f: + for line in f: + saveValueStore = eval(line.strip()) + break + print(saveValueStore) + def LoadDefaultSettings(self): -- cgit v1.2.3 From 5295a6d9ae3d73c7dceb286f13c6a1429e55393c Mon Sep 17 00:00:00 2001 From: tassaron Date: Tue, 30 May 2017 22:05:56 -0400 Subject: presets are working except for font because it can't be represented as a string --- components/__base__.py | 21 +++++++++++++-------- components/original.py | 13 +++++++++---- components/text.py | 25 ++++++++++++++++++++----- main.py | 5 ++--- 4 files changed, 44 insertions(+), 20 deletions(-) (limited to 'main.py') diff --git a/components/__base__.py b/components/__base__.py index 252ad03..05d5cb6 100644 --- a/components/__base__.py +++ b/components/__base__.py @@ -5,18 +5,21 @@ class Component: return self.__doc__ def version(self): + # change this number to identify new versions of a component return 1 def preFrameRender(self, **kwargs): - for kwarg, value in kwargs.items(): - exec('self.%s = value' % kwarg) + for item in kwargs.items(): + exec('self.%s = %s' % item) def pickColor(self): color = QtGui.QColorDialog.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 + 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 ''' @@ -58,7 +61,9 @@ class Component: image = Image.new("RGBA", (width, height), (0,0,0,0)) return image - def version(self): - # change this number to identify new versions of your component - return 1 + def loadPreset(self, presetDict): + # update widgets using a preset dict + + def savePreset(self): + return {} ''' diff --git a/components/original.py b/components/original.py index 40f51eb..bebfdf2 100644 --- a/components/original.py +++ b/components/original.py @@ -32,12 +32,15 @@ class Component(__base__.Component): self.visColor = self.RGBFromString(self.page.lineEdit_visColor.text()) self.parent.drawPreview() - def loadPreset(self, presetDict): - self.preFrameRender(**presetDict) + def loadPreset(self, pr): + 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 { 'layout' : self.page.comboBox_visLayout.currentIndex(), - 'visColor' : self.page.lineEdit_visColor.text(), + return { 'layout' : self.layout, + 'visColor' : self.visColor, } def previewRender(self, previewWorker): @@ -61,6 +64,8 @@ class Component(__base__.Component): def pickColor(self): RGBstring, btnStyle = super().pickColor() + if not RGBstring: + return self.page.lineEdit_visColor.setText(RGBstring) self.page.pushButton_visColor.setStyleSheet(btnStyle) diff --git a/components/text.py b/components/text.py index c9359f2..23e65eb 100644 --- a/components/text.py +++ b/components/text.py @@ -53,9 +53,6 @@ class Component(__base__.Component): self.page = page return page - def version(self): - return 1 - def update(self): self.title = self.page.lineEdit_title.text() self.alignment = self.page.comboBox_textAlign.currentIndex() @@ -71,11 +68,27 @@ class Component(__base__.Component): self.xPosition = self.xPosition - fm.width(self.title)/2 if self.alignment == 2: #Right self.xPosition = self.xPosition - fm.width(self.title) - self.parent.drawPreview() + + def loadPreset(self, pr): + self.page.lineEdit_title.setText(pr['title']) + self.page.spinBox_fontSize.setValue(pr['fontSize']) + self.page.spinBox_xTextAlign.setValue(pr['xPosition']) + self.page.spinBox_yTextAlign.setValue(pr['yPosition']) + self.page.comboBox_textAlign.setCurrentIndex(pr['alignment']) + 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 {} + return { + 'title' : self.title, + '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')) @@ -117,5 +130,7 @@ class Component(__base__.Component): def pickColor(self): RGBstring, btnStyle = super().pickColor() + if not RGBstring: + return self.page.lineEdit_textColor.setText(RGBstring) self.page.pushButton_textColor.setStyleSheet(btnStyle) diff --git a/main.py b/main.py index 77b56c3..2dbefe7 100644 --- a/main.py +++ b/main.py @@ -400,9 +400,8 @@ class Main(QtCore.QObject): for line in f: saveValueStore = eval(line.strip()) break - print(saveValueStore) - - + self.selectedComponents[index].loadPreset(saveValueStore) + self.drawPreview() def LoadDefaultSettings(self): self.resolutions = [ -- cgit v1.2.3 From 9be8f742c6a694c3d85eacfedf03808f215295ad Mon Sep 17 00:00:00 2001 From: tassaron Date: Tue, 30 May 2017 22:34:25 -0400 Subject: get confirmation when overwriting presets --- main.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'main.py') diff --git a/main.py b/main.py index 2dbefe7..d3fa52c 100644 --- a/main.py +++ b/main.py @@ -377,7 +377,20 @@ class Main(QtCore.QObject): dirname = os.path.join(self.dataDir, 'presets', componentName, str(version)) if not os.path.exists(dirname): os.makedirs(dirname) - with open(os.path.join(dirname, filename), 'w') as f: + filepath = os.path.join(dirname, filename) + if os.path.exists(filepath): + msg = QtGui.QMessageBox() + msg.setIcon(QtGui.QMessageBox.Warning) + msg.setText("%s already exists! Overwrite it?" % filename) + msg.setStandardButtons(QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel) + ch = msg.exec_() + if ch != 1024: # 1024 = OK + return + # remove old copies of the preset + for i in range(0, self.windowcomboBox_openPreset.count()): + if self.window.comboBox_openPreset.itemText(i) == filename: + self.window.comboBox_openPreset.removeItem(i) + with open(filepath, 'w') as f: f.write('%s' % repr(saveValueStore)) self.window.comboBox_openPreset.addItem(filename) -- cgit v1.2.3 From c21d6f5ea7c6d33e2ded44b823d2dbb5b9384d78 Mon Sep 17 00:00:00 2001 From: DH4 Date: Wed, 31 May 2017 02:15:09 -0500 Subject: New rendering engine partially implemented. Also added a live preview during rendering. FIXME: spectrum is out of sync / rendering too quickly. --- components/original.py | 12 +- main.py | 1 + video_thread.py | 326 ++++++++++++++++++++++++++++++------------------- 3 files changed, 208 insertions(+), 131 deletions(-) (limited to 'main.py') diff --git a/components/original.py b/components/original.py index 47e53b8..46e7182 100644 --- a/components/original.py +++ b/components/original.py @@ -49,13 +49,17 @@ class Component(__base__.Component): self.smoothConstantDown = 0.08 self.smoothConstantUp = 0.8 self.lastSpectrum = None - + self.spectrumArray = {} + + for i in range(0, len(self.completeAudioArray), self.sampleSize): + spectrum = transformData(i, self.completeAudioArray, self.sampleSize, + self.smoothConstantDown, self.smoothConstantUp, self.lastSpectrum) + self.spectrumArray[i] = spectrum + def frameRender(self, moduleNo, frameNo): - self.lastSpectrum = transformData(frameNo, self.completeAudioArray, self.sampleSize, - self.smoothConstantDown, self.smoothConstantUp, self.lastSpectrum) width = int(self.worker.core.settings.value('outputWidth')) height = int(self.worker.core.settings.value('outputHeight')) - return drawBars(width, height, self.lastSpectrum, self.visColor, self.layout) + return drawBars(width, height, self.spectrumArray[frameNo], self.visColor, self.layout) def pickColor(self): RGBstring, btnStyle = super().pickColor() diff --git a/main.py b/main.py index 474ab29..4739465 100644 --- a/main.py +++ b/main.py @@ -245,6 +245,7 @@ class Main(QtCore.QObject): 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.videoThread.start() self.videoTask.emit(self.window.lineEdit_background.text(), diff --git a/video_thread.py b/video_thread.py index 93ca7bd..18b4e3e 100644 --- a/video_thread.py +++ b/video_thread.py @@ -6,136 +6,208 @@ import core import numpy import subprocess as sp import sys +from queue import Queue +from threading import Thread +import time class Worker(QtCore.QObject): - videoCreated = pyqtSignal() - progressBarUpdate = pyqtSignal(int) - progressBarSetText = pyqtSignal(str) - - def __init__(self, parent=None): - QtCore.QObject.__init__(self) - self.core = core.Core() - self.core.settings = parent.settings - self.modules = parent.modules - self.stackedWidget = parent.window.stackedWidget - parent.videoTask.connect(self.createVideo) - - @pyqtSlot(str, str, str, list) - def createVideo(self, backgroundImage, inputFile, outputFile, components): - # print('worker thread id: {}'.format(QtCore.QThread.currentThreadId())) - def getBackgroundAtIndex(i): - return self.core.drawBaseImage(backgroundFrames[i]) - - progressBarValue = 0 - self.progressBarUpdate.emit(progressBarValue) - self.progressBarSetText.emit('Loading background image…') - - backgroundFrames = self.core.parseBaseImage(backgroundImage) - if len(backgroundFrames) < 2: - # the base image is not a video so we can draw it now - imBackground = getBackgroundAtIndex(0) - else: - # base images will be drawn while drawing the audio bars - imBackground = None - - self.progressBarSetText.emit('Loading audio file…') - completeAudioArray = self.core.readAudioFile(inputFile) - - # test if user has libfdk_aac - encoders = sp.check_output(self.core.FFMPEG_BIN + " -encoders -hide_banner", shell=True) - acodec = self.core.settings.value('outputAudioCodec') - - if b'libfdk_aac' in encoders and acodec == 'aac': - acodec = 'libfdk_aac' - - ffmpegCommand = [ self.core.FFMPEG_BIN, - '-y', # (optional) means overwrite the output file if it already exists. - '-f', 'rawvideo', - '-vcodec', 'rawvideo', - '-s', self.core.settings.value('outputWidth')+'x'+self.core.settings.value('outputHeight'), # size of one frame - '-pix_fmt', 'rgb24', - '-r', self.core.settings.value('outputFrameRate'), # frames per second - '-i', '-', # The input comes from a pipe - '-an', - '-i', inputFile, - '-acodec', acodec, # output audio codec - '-b:a', self.core.settings.value('outputAudioBitrate'), - '-vcodec', self.core.settings.value('outputVideoCodec'), - '-pix_fmt', self.core.settings.value('outputVideoFormat'), - '-preset', self.core.settings.value('outputPreset'), - '-f', self.core.settings.value('outputFormat')] - - if acodec == 'aac': - ffmpegCommand.append('-strict') - ffmpegCommand.append('-2') - - ffmpegCommand.append(outputFile) - - out_pipe = sp.Popen(ffmpegCommand, - stdin=sp.PIPE,stdout=sys.stdout, stderr=sys.stdout) - - # initialize components - print('######################## Data') - print('loaded components:', - ["%s%s" % (num, str(component)) for num, component in enumerate(components)]) - staticComponents = {} - sampleSize = 1470 - for compNo, comp in enumerate(components): - properties = None - properties = comp.preFrameRender(worker=self, completeAudioArray=completeAudioArray, sampleSize=sampleSize) - if properties and 'static' in properties: - staticComponents[compNo] = None - - # create video for output - numpy.seterr(divide='ignore') - frame = getBackgroundAtIndex(0) - bgI = 0 - for i in range(0, len(completeAudioArray), sampleSize): - newFrame = Image.new("RGBA", (int(self.core.settings.value('outputWidth')), int(self.core.settings.value('outputHeight'))),(0,0,0,255)) - if imBackground: - newFrame.paste(imBackground) - else: - newFrame.paste(getBackgroundAtIndex(bgI)) - - # composite all frames returned by the components in order - for compNo, comp in enumerate(components): - if compNo in staticComponents and staticComponents[compNo] != None: - newFrame = Image.alpha_composite(newFrame,staticComponents[compNo]) + imageCreated = pyqtSignal(['QImage']) + videoCreated = pyqtSignal() + progressBarUpdate = pyqtSignal(int) + progressBarSetText = pyqtSignal(str) + + def __init__(self, parent=None): + QtCore.QObject.__init__(self) + self.core = core.Core() + self.core.settings = parent.settings + self.modules = parent.modules + self.stackedWidget = parent.window.stackedWidget + self.parent = parent + parent.videoTask.connect(self.createVideo) + self.sampleSize = 1470 + + def renderNode(self): + while True: + i = self.compositeQueue.get() + + frame = Image.new( + "RGBA", + (self.width, self.height), + (0, 0, 0, 255) + ) + + frame.paste(self.imBackground) + + if self.imBackground is not None: + frame.paste(self.imBackground) else: - newFrame = Image.alpha_composite(newFrame,comp.frameRender(compNo, i)) - if i == 0 and compNo in staticComponents: - staticComponents[compNo] = comp.frameRender(compNo, i) + frame.paste(self.getBackgroundAtIndex(i[1])) + + for compNo, comp in enumerate(self.components): + if compNo in self.staticComponents and self.staticComponents[compNo] != None: + frame = Image.alpha_composite(frame, self.staticComponents[compNo]) + else: + frame = Image.alpha_composite(frame, comp.frameRender(compNo, i[0])) + + # frame.paste(compFrame, mask=compFrame) - if not imBackground: + self.renderQueue.put([i[0], frame]) + self.compositeQueue.task_done() + + def renderDispatch(self): + print('Dispatching Frames for Compositing...') + if not self.imBackground: # increment background video frame for next iteration - if bgI < len(backgroundFrames)-1: - bgI += 1 - - # write to out_pipe - try: - frame = Image.new("RGB", (int(self.core.settings.value('outputWidth')), int(self.core.settings.value('outputHeight'))),(0,0,0)) - frame.paste(newFrame) - out_pipe.stdin.write(frame.tobytes()) - finally: - True - - # increase progress bar value - if progressBarValue + 1 <= (i / len(completeAudioArray)) * 100: - progressBarValue = numpy.floor((i / len(completeAudioArray)) * 100) - self.progressBarUpdate.emit(progressBarValue) - self.progressBarSetText.emit('%s%%' % str(int(progressBarValue))) - - numpy.seterr(all='print') - - out_pipe.stdin.close() - if out_pipe.stderr is not None: - print(out_pipe.stderr.read()) - out_pipe.stderr.close() - # out_pipe.terminate() # don't terminate ffmpeg too early - out_pipe.wait() - print("Video file created") - self.core.deleteTempDir() - self.progressBarUpdate.emit(100) - self.progressBarSetText.emit('100%') - self.videoCreated.emit() + if self.bgI < len(self.backgroundFrames)-1 and i != 0: + self.bgI += 1 + + for i in range(0, len(self.completeAudioArray), self.sampleSize): + self.compositeQueue.put([i, self.bgI]) + self.compositeQueue.join() + print('Compositing Complete.') + + def previewDispatch(self): + while True: + i = self.previewQueue.get() + if time.time() - self.lastPreview >= 0.05 or i[0] == 0: + self._image = ImageQt(i[1]) + self.imageCreated.emit(QtGui.QImage(self._image)) + lastPreview = time.time() + + self.previewQueue.task_done() + + + def getBackgroundAtIndex(self, i): + return self.core.drawBaseImage(self.backgroundFrames[i]) + + @pyqtSlot(str, str, str, list) + def createVideo(self, backgroundImage, inputFile, outputFile, components): + self.width = int(self.core.settings.value('outputWidth')) + self.height = int(self.core.settings.value('outputHeight')) + # print('worker thread id: {}'.format(QtCore.QThread.currentThreadId())) + self.components = components + progressBarValue = 0 + self.progressBarUpdate.emit(progressBarValue) + self.progressBarSetText.emit('Loading background image…') + + self.backgroundImage = backgroundImage + + self.backgroundFrames = self.core.parseBaseImage(backgroundImage) + if len(self.backgroundFrames) < 2: + # the base image is not a video so we can draw it now + self.imBackground = self.getBackgroundAtIndex(0) + else: + # base images will be drawn while drawing the audio bars + self.imBackground = None + self.bgI = 0 + + self.progressBarSetText.emit('Loading audio file…') + self.completeAudioArray = self.core.readAudioFile(inputFile) + + # test if user has libfdk_aac + encoders = sp.check_output(self.core.FFMPEG_BIN + " -encoders -hide_banner", shell=True) + acodec = self.core.settings.value('outputAudioCodec') + + if b'libfdk_aac' in encoders and acodec == 'aac': + acodec = 'libfdk_aac' + + ffmpegCommand = [ + self.core.FFMPEG_BIN, + '-y', # (optional) means 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', + '-r', self.core.settings.value('outputFrameRate'), # frames per second + '-i', '-', # The input comes from a pipe + '-an', + '-i', inputFile, + '-acodec', acodec, # output audio codec + '-b:a', self.core.settings.value('outputAudioBitrate'), + '-vcodec', self.core.settings.value('outputVideoCodec'), + '-pix_fmt', self.core.settings.value('outputVideoFormat'), + '-preset', self.core.settings.value('outputPreset'), + '-f', self.core.settings.value('outputFormat') + ] + + if acodec == 'aac': + ffmpegCommand.append('-strict') + ffmpegCommand.append('-2') + + ffmpegCommand.append(outputFile) + out_pipe = sp.Popen(ffmpegCommand, stdin=sp.PIPE,stdout=sys.stdout, stderr=sys.stdout) + + # create video for output + numpy.seterr(divide='ignore') + + self.compositeQueue = Queue() + self.compositeQueue.maxsize = 20 + self.renderQueue = Queue() + self.renderQueue.maxsize = 20 + self.previewQueue = Queue() + + for i in range(2): + t = Thread(target=self.renderNode) + t.daemon = True + t.start() + + self.dispatchThread = Thread(target=self.renderDispatch) + self.dispatchThread.daemon = True + self.dispatchThread.start() + + self.previewDispatch = Thread(target=self.previewDispatch) + self.previewDispatch.daemon = True + self.previewDispatch.start() + + frameBuffer = {} + self.lastPreview = 0.0 + + # initialize components + print('loaded components:', + ["%s%s" % (num, str(component)) for num, component in enumerate(components)]) + self.staticComponents = {} + for compNo, comp in enumerate(components): + properties = None + properties = comp.preFrameRender( + worker=self, + completeAudioArray=self.completeAudioArray, + sampleSize=self.sampleSize + ) + + if properties and 'static' in properties: + self.staticComponents[compNo] = comp.frameRender(compNo, 0) + + for i in range(0, len(self.completeAudioArray), self.sampleSize): + data = self.renderQueue.get() + frameBuffer[data[0]] = data[1] + + if i in frameBuffer: + try: + out_pipe.stdin.write(frameBuffer[i].tobytes()) + self.previewQueue.put([i, frameBuffer[i]]) + del frameBuffer[i] + finally: + True + self.renderQueue.task_done() + + # increase progress bar value + if progressBarValue + 1 <= (i / len(self.completeAudioArray)) * 100: + progressBarValue = numpy.floor((i / len(self.completeAudioArray)) * 100) + self.progressBarUpdate.emit(progressBarValue) + self.progressBarSetText.emit('%s%%' % str(int(progressBarValue))) + + numpy.seterr(all='print') + + out_pipe.stdin.close() + if out_pipe.stderr is not None: + print(out_pipe.stderr.read()) + out_pipe.stderr.close() + # out_pipe.terminate() # don't terminate ffmpeg too early + out_pipe.wait() + print("Video file created") + self.parent.drawPreview() + self.core.deleteTempDir() + self.progressBarUpdate.emit(100) + self.progressBarSetText.emit('100%') + self.videoCreated.emit() -- cgit v1.2.3 From f55d7d120639cc4b83850699b8de57e036977288 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 1 Jun 2017 13:17:36 -0400 Subject: saveable titleFont, xPosition glitches fixed --- components/text.py | 24 +++++++++++++++++------- main.py | 4 +++- 2 files changed, 20 insertions(+), 8 deletions(-) (limited to 'main.py') diff --git a/components/text.py b/components/text.py index 716030b..9237167 100644 --- a/components/text.py +++ b/components/text.py @@ -61,21 +61,29 @@ class Component(__base__.Component): 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() + + def getXY(self): + '''Returns true x, y after considering alignment settings''' fm = QtGui.QFontMetrics(self.titleFont) if self.alignment == 0: #Left - self.xPosition = self.xPosition + x = self.xPosition if self.alignment == 1: #Middle - self.xPosition = self.xPosition - fm.width(self.title)/2 + x = self.xPosition - fm.width(self.title)/2 if self.alignment == 2: #Right - self.xPosition = self.xPosition - fm.width(self.title) - self.parent.drawPreview() - + x = self.xPosition - fm.width(self.title) + return x, self.yPosition + + def loadPreset(self, pr): 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.comboBox_textAlign.setCurrentIndex(pr['alignment']) 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) @@ -83,6 +91,7 @@ class Component(__base__.Component): def savePreset(self): return { 'title' : self.title, + 'titleFont' : self.titleFont.toString(), 'alignment' : self.alignment, 'fontSize' : self.fontSize, 'xPosition' : self.xPosition, @@ -105,6 +114,7 @@ class Component(__base__.Component): return self.addText(width, height) def addText(self, width, height): + x, y = self.getXY() im = Image.new("RGBA", (width, height),(0,0,0,0)) image = ImageQt(im) @@ -112,7 +122,7 @@ class Component(__base__.Component): self.titleFont.setPixelSize(self.fontSize) painter.setFont(self.titleFont) painter.setPen(QColor(*self.textColor)) - painter.drawText(self.xPosition, self.yPosition, self.title) + painter.drawText(x, y, self.title) painter.end() buffer = QtCore.QBuffer() diff --git a/main.py b/main.py index 1cbe1db..c8ad4e8 100644 --- a/main.py +++ b/main.py @@ -388,12 +388,14 @@ class Main(QtCore.QObject): if ch != 1024: # 1024 = OK return # remove old copies of the preset - for i in range(0, self.windowcomboBox_openPreset.count()): + presetLen = self.window.comboBox_openPreset.count() + for i in range(0, presetLen): if self.window.comboBox_openPreset.itemText(i) == filename: self.window.comboBox_openPreset.removeItem(i) with open(filepath, 'w') as f: f.write('%s' % repr(saveValueStore)) self.window.comboBox_openPreset.addItem(filename) + self.window.comboBox_openPreset.setCurrentIndex(presetLen-1) def openPreset(self): if self.window.comboBox_openPreset.currentIndex() < 1: -- cgit v1.2.3 From 1a24922fee9cb86bf8814f57c0f8f2dd77f27a3f Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 1 Jun 2017 16:16:42 -0400 Subject: restrict presets to boring characters --- main.py | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index c8ad4e8..00ba96b 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,6 @@ -import sys, io, os +import sys, io, os, atexit, string, signal from os.path import expanduser -import atexit from queue import Queue -import signal from importlib import import_module from PyQt4 import QtCore, QtGui, uic from PyQt4.QtCore import QSettings, QModelIndex @@ -365,14 +363,28 @@ class Main(QtCore.QObject): def openSavePresetDialog(self): if self.window.listWidget_componentList.currentRow() == -1: return - newName, OK = QtGui.QInputDialog.getText(QtGui.QWidget(), 'Audio Visualizer', 'New Preset Name:') - if OK and newName: - index = self.window.listWidget_componentList.currentRow() - if index != -1: - saveValueStore = self.selectedComponents[index].savePreset() - componentName = str(self.selectedComponents[index]).strip() - vers = self.selectedComponents[index].version() - self.createPresetFile(componentName, vers, saveValueStore, newName) + while True: + newName, OK = QtGui.QInputDialog.getText(QtGui.QWidget(), 'Audio Visualizer', 'New Preset Name:') + badName = False + for letter in newName: + if letter in string.punctuation: + badName = True + if badName: + # some filesystems don't like bizarre characters + msg = QtGui.QMessageBox() + msg.setIcon(QtGui.QMessageBox.Information) + msg.setText("Preset names must contain only letters, numbers, and spaces.") + msg.setStandardButtons(QtGui.QMessageBox.Ok) + msg.exec_() + continue + if OK and newName: + index = self.window.listWidget_componentList.currentRow() + if index != -1: + saveValueStore = self.selectedComponents[index].savePreset() + componentName = str(self.selectedComponents[index]).strip() + vers = self.selectedComponents[index].version() + self.createPresetFile(componentName, vers, saveValueStore, newName) + break def createPresetFile(self, componentName, version, saveValueStore, filename): dirname = os.path.join(self.dataDir, 'presets', componentName, str(version)) @@ -388,21 +400,19 @@ class Main(QtCore.QObject): if ch != 1024: # 1024 = OK return # remove old copies of the preset - presetLen = self.window.comboBox_openPreset.count() - for i in range(0, presetLen): + for i in range(0, self.window.comboBox_openPreset.count()): if self.window.comboBox_openPreset.itemText(i) == filename: self.window.comboBox_openPreset.removeItem(i) with open(filepath, 'w') as f: f.write('%s' % repr(saveValueStore)) self.window.comboBox_openPreset.addItem(filename) - self.window.comboBox_openPreset.setCurrentIndex(presetLen-1) + self.window.comboBox_openPreset.setCurrentIndex(self.window.comboBox_openPreset.count()-1) def openPreset(self): if self.window.comboBox_openPreset.currentIndex() < 1: return index = self.window.listWidget_componentList.currentRow() if index == -1: - # no component selected return filename = self.window.comboBox_openPreset.itemText(self.window.comboBox_openPreset.currentIndex()) componentName = str(self.selectedComponents[index]).strip() -- cgit v1.2.3 From 907ba33e93b5e8c33be8c74dd787f78e1b3fa109 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 1 Jun 2017 17:34:04 -0400 Subject: a handy showMessage() method and starting on the project buttons --- main.py | 48 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 14 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index 00ba96b..dc18179 100644 --- a/main.py +++ b/main.py @@ -178,6 +178,8 @@ class Main(QtCore.QObject): self.window.pushButton_savePreset.clicked.connect(self.openSavePresetDialog) self.window.comboBox_openPreset.currentIndexChanged.connect(self.openPreset) + self.window.pushButton_saveProject.clicked.connect(self.openSaveProjectDialog) + #self.window.pushButton_openProject self.drawPreview() @@ -249,8 +251,7 @@ class Main(QtCore.QObject): self.window.lineEdit_outputFile.text(), self.selectedComponents) else: - # TODO: use QMessageBox or similar to alert user that fields are empty - pass + self.showMessage("You must select an audio file and output filename.") def progressBarUpdated(self, value): self.window.progressBar_createVideo.setValue(value) @@ -371,11 +372,7 @@ class Main(QtCore.QObject): badName = True if badName: # some filesystems don't like bizarre characters - msg = QtGui.QMessageBox() - msg.setIcon(QtGui.QMessageBox.Information) - msg.setText("Preset names must contain only letters, numbers, and spaces.") - msg.setStandardButtons(QtGui.QMessageBox.Ok) - msg.exec_() + self.showMessage("Preset names must contain only letters, numbers, and spaces.") continue if OK and newName: index = self.window.listWidget_componentList.currentRow() @@ -392,19 +389,15 @@ class Main(QtCore.QObject): os.makedirs(dirname) filepath = os.path.join(dirname, filename) if os.path.exists(filepath): - msg = QtGui.QMessageBox() - msg.setIcon(QtGui.QMessageBox.Warning) - msg.setText("%s already exists! Overwrite it?" % filename) - msg.setStandardButtons(QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel) - ch = msg.exec_() - if ch != 1024: # 1024 = OK + ch = self.showMessage("%s already exists! Overwrite it?" % filename, QtGui.QMessageBox.Warning, True) + if not ch: return # remove old copies of the preset for i in range(0, self.window.comboBox_openPreset.count()): if self.window.comboBox_openPreset.itemText(i) == filename: self.window.comboBox_openPreset.removeItem(i) with open(filepath, 'w') as f: - f.write('%s' % repr(saveValueStore)) + f.write(repr(saveValueStore)) self.window.comboBox_openPreset.addItem(filename) self.window.comboBox_openPreset.setCurrentIndex(self.window.comboBox_openPreset.count()-1) @@ -429,6 +422,33 @@ class Main(QtCore.QObject): self.selectedComponents[index].loadPreset(saveValueStore) self.drawPreview() + def openSaveProjectDialog(self): + outputDir = os.path.join(self.dataDir, 'projects') + filename = QtGui.QFileDialog.getSaveFileName(self.window, + "Create Project File", outputDir) + if not filename: + return + filepath = os.path.join(outputDir, filename) + with open(filepath, 'w') as f: + 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' % repr(saveValueStore)) + + def showMessage(self, string, icon=QtGui.QMessageBox.Information, showCancel=False): + msg = QtGui.QMessageBox() + msg.setIcon(icon) + msg.setText(string) + if 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 LoadDefaultSettings(self): self.resolutions = [ '1920x1080', -- cgit v1.2.3 From 00f5c885842c15ef86cae9c0fdabca997016182b Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 1 Jun 2017 18:47:47 -0400 Subject: components can be saved and loaded as projects --- main.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 6 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index dc18179..220d594 100644 --- a/main.py +++ b/main.py @@ -121,6 +121,7 @@ class Main(QtCore.QObject): self.core = core.Core() self.settings = QSettings('settings.ini', QSettings.IniFormat) LoadDefaultSettings(self) + self.currentProject = None # create data directory structure if needed self.dataDir = QDesktopServices.storageLocation(QDesktopServices.DataLocation) @@ -136,10 +137,8 @@ class Main(QtCore.QObject): 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) @@ -178,8 +177,9 @@ class Main(QtCore.QObject): self.window.pushButton_savePreset.clicked.connect(self.openSavePresetDialog) self.window.comboBox_openPreset.currentIndexChanged.connect(self.openPreset) - self.window.pushButton_saveProject.clicked.connect(self.openSaveProjectDialog) - #self.window.pushButton_openProject + self.window.pushButton_saveAs.clicked.connect(self.openSaveProjectDialog) + self.window.pushButton_saveProject.clicked.connect(self.saveCurrentProject) + self.window.pushButton_openProject.clicked.connect(self.openOpenProjectDialog) self.drawPreview() @@ -422,19 +422,55 @@ class Main(QtCore.QObject): self.selectedComponents[index].loadPreset(saveValueStore) self.drawPreview() + def saveCurrentProject(self): + if self.currentProject: + self.createProjectFile(self.currentProject) + else: + self.openSaveProjectDialog() + def openSaveProjectDialog(self): outputDir = os.path.join(self.dataDir, 'projects') - filename = QtGui.QFileDialog.getSaveFileName(self.window, - "Create Project File", outputDir) + filename = QtGui.QFileDialog.getSaveFileName(self.window, "Create Project File", outputDir) if not filename: return filepath = os.path.join(outputDir, filename) + self.currentProject = filepath + self.createProjectFile(filepath) + + def createProjectFile(self, filepath): with open(filepath, 'w') as f: 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' % repr(saveValueStore)) + + def openOpenProjectDialog(self): + inputDir = os.path.join(self.dataDir, 'projects') + filename = QtGui.QFileDialog.getOpenFileName(self.window, "Open Project File", inputDir) + if not filename: + return + filepath = os.path.join(inputDir, filename) + self.openProject(filepath) + + def openProject(self, filepath): + self.clear() + self.currentProject = filepath + compNames = [mod.Component.__doc__ for mod in self.modules] + with open(filepath, 'r') as f: + i = 0 + for line in f: + if i == 0: + compIndex = compNames.index(line.strip()) + self.addComponent(compIndex) + i += 1 + elif i == 1: + # version, not used yet + i += 1 + elif i == 2: + saveValueStore = eval(line.strip()) + self.selectedComponents[-1].loadPreset(saveValueStore) + i = 0 def showMessage(self, string, icon=QtGui.QMessageBox.Information, showCancel=False): msg = QtGui.QMessageBox() @@ -448,6 +484,14 @@ class Main(QtCore.QObject): if ch == 1024: return True return False + + def clear(self): + ''' empty out all components and fields, get a blank slate ''' + self.selectedComponents = [] + self.window.listWidget_componentList.clear() + for widget in self.pages: + self.window.stackedWidget.removeWidget(widget) + self.pages = [] def LoadDefaultSettings(self): self.resolutions = [ -- cgit v1.2.3 From 610db2060678b8b848785dc8f309944de2a96e9a Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 1 Jun 2017 19:54:50 -0400 Subject: settings.ini now saved/loaded with projects --- main.py | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index 220d594..470a0a9 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,4 @@ -import sys, io, os, atexit, string, signal +import sys, io, os, shutil, atexit, string, signal from os.path import expanduser from queue import Queue from importlib import import_module @@ -119,22 +119,21 @@ class Main(QtCore.QObject): # print('main thread id: {}'.format(QtCore.QThread.currentThreadId())) self.window = window self.core = core.Core() - self.settings = QSettings('settings.ini', QSettings.IniFormat) - LoadDefaultSettings(self) self.currentProject = None + self.pages = [] + self.selectedComponents = [] # create data directory structure if needed self.dataDir = QDesktopServices.storageLocation(QDesktopServices.DataLocation) if not os.path.exists(self.dataDir): os.makedirs(self.dataDir) - for neededDirectory in ('projects', 'presets'): + for neededDirectory in ('projects', 'project-settings', 'presets'): if not os.path.exists(os.path.join(self.dataDir, neededDirectory)): os.mkdir(os.path.join(self.dataDir, neededDirectory)) - - self.pages = [] + self.settings = QSettings(os.path.join(self.dataDir, 'settings.ini'), QSettings.IniFormat) + LoadDefaultSettings(self) self.previewQueue = Queue() - self.previewThread = QtCore.QThread(self) self.previewWorker = preview_thread.Worker(self, self.previewQueue) self.previewWorker.moveToThread(self.previewThread) @@ -152,12 +151,11 @@ class Main(QtCore.QObject): window.progressBar_createVideo.setValue(0) window.pushButton_createVideo.clicked.connect(self.createAudioVisualisation) window.setWindowTitle("Audio Visualizer") - + self.modules = self.findComponents() for component in self.modules: window.comboBox_componentSelection.addItem(component.Component.__doc__) window.listWidget_componentList.clicked.connect(lambda _: self.changeComponentWidget()) - self.selectedComponents = [] self.window.pushButton_addComponent.clicked.connect( \ lambda _: self.addComponent(self.window.comboBox_componentSelection.currentIndex()) @@ -189,16 +187,11 @@ class Main(QtCore.QObject): self.timer.stop() self.previewThread.quit() self.previewThread.wait() - # TODO: replace remembered settings with presets/projects - ''' - self.settings.setValue("titleFont", self.window.fontComboBox_titleFont.currentFont().toString()) - self.settings.setValue("alignment", str(self.window.comboBox_textAlign.currentIndex())) - self.settings.setValue("fontSize", str(self.window.spinBox_fontSize.value())) - self.settings.setValue("xPosition", str(self.window.spinBox_xTextAlign.value())) - self.settings.setValue("yPosition", str(self.window.spinBox_yTextAlign.value())) - self.settings.setValue("visColor", self.window.lineEdit_visColor.text()) - self.settings.setValue("textColor", self.window.lineEdit_textColor.text()) - ''' + backupPath = os.path.join(self.dataDir, 'settings.ini~') + settingsPath = os.path.join(self.dataDir, 'settings.ini') + if self.currentProject: + os.remove(settingsPath) + os.rename(backupPath, settingsPath) def openInputFileDialog(self): inputDir = self.settings.value("inputDir", expanduser("~")) @@ -444,6 +437,8 @@ class Main(QtCore.QObject): f.write('%s\n' % str(comp)) f.write('%s\n' % str(comp.version())) f.write('%s\n' % repr(saveValueStore)) + dir_ = os.path.join(self.dataDir, 'project-settings') + shutil.copyfile(self.settings.fileName(), os.path.join(dir_, os.path.basename('%s.ini' % filepath))) def openOpenProjectDialog(self): inputDir = os.path.join(self.dataDir, 'projects') @@ -471,6 +466,15 @@ class Main(QtCore.QObject): saveValueStore = eval(line.strip()) self.selectedComponents[-1].loadPreset(saveValueStore) i = 0 + projSettingsPath = os.path.join(self.dataDir, 'project-settings', '%s.ini' % os.path.basename(filepath)) + backupPath = os.path.join(self.dataDir, 'settings.ini~') + settingsPath = os.path.join(self.dataDir, 'settings.ini') + if os.path.exists(backupPath): + os.remove(backupPath) + os.rename(settingsPath, backupPath) + #os.remove(settingsPath) + shutil.copyfile(projSettingsPath, settingsPath) + self.settings.sync() def showMessage(self, string, icon=QtGui.QMessageBox.Information, showCancel=False): msg = QtGui.QMessageBox() @@ -510,7 +514,6 @@ def LoadDefaultSettings(self): "outputVideoFormat": "yuv420p", "outputPreset": "medium", "outputFormat": "mp4", - "visLayout": 0 } for parm, value in default.items(): -- cgit v1.2.3 From d31add0d955f58d1fa375d3166ac519a21f80c6b Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 1 Jun 2017 20:21:26 -0400 Subject: tidying up --- main.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index 470a0a9..ba62c2c 100644 --- a/main.py +++ b/main.py @@ -188,7 +188,7 @@ class Main(QtCore.QObject): self.previewThread.quit() self.previewThread.wait() backupPath = os.path.join(self.dataDir, 'settings.ini~') - settingsPath = os.path.join(self.dataDir, 'settings.ini') + settingsPath = self.settings.fileName() if self.currentProject: os.remove(settingsPath) os.rename(backupPath, settingsPath) @@ -327,6 +327,7 @@ class Main(QtCore.QObject): self.window.stackedWidget.insertWidget(row - 1, page) self.window.listWidget_componentList.setCurrentRow(row - 1) self.window.stackedWidget.setCurrentIndex(row -1) + self.drawPreview() def moveComponentDown(self): row = self.window.listWidget_componentList.currentRow() @@ -343,6 +344,7 @@ class Main(QtCore.QObject): self.window.stackedWidget.insertWidget(row + 1, page) self.window.listWidget_componentList.setCurrentRow(row + 1) self.window.stackedWidget.setCurrentIndex(row + 1) + self.drawPreview() def updateOpenPresetComboBox(self, component): self.window.comboBox_openPreset.clear() @@ -437,8 +439,8 @@ class Main(QtCore.QObject): f.write('%s\n' % str(comp)) f.write('%s\n' % str(comp.version())) f.write('%s\n' % repr(saveValueStore)) - dir_ = os.path.join(self.dataDir, 'project-settings') - shutil.copyfile(self.settings.fileName(), os.path.join(dir_, os.path.basename('%s.ini' % filepath))) + projSettingsPath = os.path.join(self.dataDir, 'project-settings', os.path.basename('%s.ini' % filepath)) + shutil.copyfile(self.settings.fileName(), projSettingsPath) def openOpenProjectDialog(self): inputDir = os.path.join(self.dataDir, 'projects') @@ -468,11 +470,10 @@ class Main(QtCore.QObject): i = 0 projSettingsPath = os.path.join(self.dataDir, 'project-settings', '%s.ini' % os.path.basename(filepath)) backupPath = os.path.join(self.dataDir, 'settings.ini~') - settingsPath = os.path.join(self.dataDir, 'settings.ini') + settingsPath = self.settings.fileName() if os.path.exists(backupPath): os.remove(backupPath) os.rename(settingsPath, backupPath) - #os.remove(settingsPath) shutil.copyfile(projSettingsPath, settingsPath) self.settings.sync() -- cgit v1.2.3 From 2768084b30da136dcfcb0c4e2e4264ad66083e4e Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 1 Jun 2017 20:31:15 -0400 Subject: resolution comboBox gets updated --- main.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'main.py') diff --git a/main.py b/main.py index ba62c2c..8bbbf08 100644 --- a/main.py +++ b/main.py @@ -354,7 +354,7 @@ class Main(QtCore.QObject): if not os.path.exists(destination): os.makedirs(destination) for f in os.listdir(destination): - self.window.comboBox_openPreset.addItem(f) + self.window.comboBox_openPreset.addItem(f) def openSavePresetDialog(self): if self.window.listWidget_componentList.currentRow() == -1: @@ -476,6 +476,11 @@ class Main(QtCore.QObject): os.rename(settingsPath, backupPath) shutil.copyfile(projSettingsPath, settingsPath) self.settings.sync() + currentRes = str(self.settings.value('outputWidth'))+'x'+str(self.settings.value('outputHeight')) + for i in range(self.window.comboBox_resolution.count()-1): + if self.window.comboBox_resolution.itemText(i) == currentRes: + self.window.comboBox_resolution.setCurrentIndex(i) + break def showMessage(self, string, icon=QtGui.QMessageBox.Information, showCancel=False): msg = QtGui.QMessageBox() -- cgit v1.2.3 From 7d8e9ab3b16546e91144e256e88f9f490abc7ec2 Mon Sep 17 00:00:00 2001 From: DH4 Date: Thu, 1 Jun 2017 22:46:45 -0500 Subject: Added aspect ratio scaling to preview area. --- background.jpg | Bin 0 -> 14782 bytes main.py | 35 +++++++++++++++++++---- mainwindow.ui | 86 ++++++++++++++++---------------------------------------- video_thread.py | 2 +- 4 files changed, 55 insertions(+), 68 deletions(-) create mode 100644 background.jpg (limited to 'main.py') diff --git a/background.jpg b/background.jpg new file mode 100644 index 0000000..f746432 Binary files /dev/null and b/background.jpg differ diff --git a/main.py b/main.py index 8bbbf08..f34cbee 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,7 @@ from os.path import expanduser from queue import Queue from importlib import import_module from PyQt4 import QtCore, QtGui, uic -from PyQt4.QtCore import QSettings, QModelIndex +from PyQt4.QtCore import QSettings, QModelIndex, Qt from PyQt4.QtGui import QDesktopServices import preview_thread, core, video_thread @@ -107,6 +107,29 @@ class Command(QtCore.QObject): self.settings.setValue("textColor", '%s,%s,%s' % self.textColor) sys.exit(0) ''' + +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) + #print point.x(), ' ', point.y() + painter.drawPixmap(point, scaledPix) + + def changePixmap(self, img): + self.pixmap = QtGui.QPixmap(img) + self.repaint() + class Main(QtCore.QObject): newTask = QtCore.pyqtSignal(str, list) @@ -151,6 +174,9 @@ class Main(QtCore.QObject): window.progressBar_createVideo.setValue(0) window.pushButton_createVideo.clicked.connect(self.createAudioVisualisation) window.setWindowTitle("Audio Visualizer") + + self.previewWindow = PreviewWindow(self, r"background.jpg") + window.verticalLayout_previewWrapper.addWidget(self.previewWindow) self.modules = self.findComponents() for component in self.modules: @@ -269,10 +295,7 @@ class Main(QtCore.QObject): # self.processTask.emit() def showPreviewImage(self, image): - self._scaledPreviewImage = image - self._previewPixmap = QtGui.QPixmap.fromImage(self._scaledPreviewImage) - - self.window.label_previewContainer.setPixmap(self._previewPixmap) + self.previewWindow.changePixmap(image) def findComponents(self): def findComponents(): @@ -548,7 +571,7 @@ if __name__ == "__main__": 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) + #window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0) main = Main(window) diff --git a/mainwindow.ui b/mainwindow.ui index 0dcce91..808073b 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -27,75 +27,24 @@ - + + + QLayout::SetDefaultConstraint + 0 - - - Qt::Vertical - - - QSizePolicy::Minimum - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - - 356 - 280 - - - - - 0 - 0 - - - - false - - - background-color:rgba(255, 255, 255, 15); - - - - - - - true - - - Qt::AlignCenter - - - - - + - Qt::Vertical + Qt::Horizontal - QSizePolicy::Minimum + QSizePolicy::MinimumExpanding - 0 + 420 0 @@ -105,16 +54,25 @@ + + QLayout::SetMinimumSize + 3 + + QLayout::SetMinimumSize + 3 + + QLayout::SetMinimumSize + @@ -141,10 +99,10 @@ - Qt::Vertical + Qt::Horizontal - QSizePolicy::Fixed + QSizePolicy::Minimum @@ -218,6 +176,12 @@ + + + 0 + 0 + + Open Preset diff --git a/video_thread.py b/video_thread.py index dee254a..a7c7ac6 100644 --- a/video_thread.py +++ b/video_thread.py @@ -63,7 +63,7 @@ class Worker(QtCore.QObject): def previewDispatch(self): while True: i = self.previewQueue.get() - if time.time() - self.lastPreview >= 0.05 or i[0] == 0: + if time.time() - self.lastPreview >= 0.06 or i[0] == 0: self._image = ImageQt(i[1]) self.imageCreated.emit(QtGui.QImage(self._image)) self.lastPreview = time.time() -- cgit v1.2.3 From 6bf36d0324ac4b04717a458adbb7172f717ec16a Mon Sep 17 00:00:00 2001 From: DH4 Date: Thu, 1 Jun 2017 23:24:13 -0500 Subject: Added ability to cancel export. --- main.py | 14 +++++++++++--- video_thread.py | 52 ++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 51 insertions(+), 15 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index f34cbee..bb42d1f 100644 --- a/main.py +++ b/main.py @@ -173,6 +173,7 @@ class Main(QtCore.QObject): 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) window.setWindowTitle("Audio Visualizer") self.previewWindow = PreviewWindow(self, r"background.jpg") @@ -250,6 +251,13 @@ class Main(QtCore.QObject): self.window.lineEdit_background.setText(fileName) self.drawPreview() + def stopVideo(self): + print('stop') + try: + self.videoWorker.stopVideo() + except: + pass + def createAudioVisualisation(self): # create output video if mandatory settings are filled in if self.window.lineEdit_audioFile.text() and self.window.lineEdit_outputFile.text(): @@ -262,8 +270,8 @@ class Main(QtCore.QObject): 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.imageCreated.connect(self.showPreviewImage) + self.videoThread.start() self.videoTask.emit(self.window.lineEdit_background.text(), self.window.lineEdit_audioFile.text(), @@ -271,7 +279,7 @@ class Main(QtCore.QObject): self.selectedComponents) else: self.showMessage("You must select an audio file and output filename.") - + def progressBarUpdated(self, value): self.window.progressBar_createVideo.setValue(value) diff --git a/video_thread.py b/video_thread.py index a7c7ac6..2e9eb13 100644 --- a/video_thread.py +++ b/video_thread.py @@ -6,10 +6,12 @@ import core import numpy import subprocess as sp import sys +import os from queue import Queue, PriorityQueue from threading import Thread import time from copy import copy +import signal class Worker(QtCore.QObject): @@ -27,6 +29,8 @@ class Worker(QtCore.QObject): self.parent = parent parent.videoTask.connect(self.createVideo) self.sampleSize = 1470 + self.canceled = False + self.error = False def renderNode(self): while True: @@ -80,8 +84,14 @@ class Worker(QtCore.QObject): background.paste(layer) return background + def stopVideo(self): + print('Stop Export') + self.canceled = True + self.out_pipe.send_signal(signal.SIGINT) + @pyqtSlot(str, str, str, list) def createVideo(self, backgroundImage, inputFile, outputFile, components): + self.outputFile = outputFile self.width = int(self.core.settings.value('outputWidth')) self.height = int(self.core.settings.value('outputHeight')) # print('worker thread id: {}'.format(QtCore.QThread.currentThreadId())) @@ -135,7 +145,9 @@ class Worker(QtCore.QObject): ffmpegCommand.append('-2') ffmpegCommand.append(outputFile) - out_pipe = sp.Popen(ffmpegCommand, stdin=sp.PIPE,stdout=sys.stdout, stderr=sys.stdout) + self.out_pipe = sp.Popen(ffmpegCommand, stdin=sp.PIPE,stdout=sys.stdout, stderr=sys.stdout) + + # create video for output numpy.seterr(divide='ignore') @@ -189,11 +201,11 @@ class Worker(QtCore.QObject): self.renderQueue.task_done() try: - out_pipe.stdin.write(frameBuffer[i].tobytes()) + self.out_pipe.stdin.write(frameBuffer[i].tobytes()) self.previewQueue.put([i, frameBuffer[i]]) del frameBuffer[i] - finally: - True + except: + break # increase progress bar value if progressBarValue + 1 <= (i / len(self.completeAudioArray)) * 100: @@ -203,15 +215,31 @@ class Worker(QtCore.QObject): numpy.seterr(all='print') - out_pipe.stdin.close() - if out_pipe.stderr is not None: - print(out_pipe.stderr.read()) - out_pipe.stderr.close() + 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 - out_pipe.wait() - print("Video file created") + self.out_pipe.wait() + if self.canceled: + print("Export Canceled") + os.remove(self.outputFile) + 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.parent.drawPreview() self.core.deleteTempDir() - self.progressBarUpdate.emit(100) - self.progressBarSetText.emit('100%') + self.videoCreated.emit() -- cgit v1.2.3 From 73a0492585e238d32869bfa9c53ddc95481ab1c5 Mon Sep 17 00:00:00 2001 From: DH4 Date: Fri, 2 Jun 2017 00:30:44 -0500 Subject: Cancel button stops pre-processing too. --- components/original.py | 9 ++++++ components/text.py | 6 ++++ core.py | 9 ++++++ main.py | 31 ++++++++++++++++--- mainwindow.ui | 3 ++ video_thread.py | 82 ++++++++++++++++++++++++++++++-------------------- 6 files changed, 103 insertions(+), 37 deletions(-) (limited to 'main.py') diff --git a/components/original.py b/components/original.py index 382c3ab..e1240e9 100644 --- a/components/original.py +++ b/components/original.py @@ -27,6 +27,7 @@ class Component(__base__.Component): page.pushButton_visColor.setStyleSheet(btnStyle) page.lineEdit_visColor.textChanged.connect(self.update) self.page = page + self.canceled = False return page def update(self): @@ -59,6 +60,8 @@ class Component(__base__.Component): self.spectrumArray = {} 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) @@ -142,3 +145,9 @@ class Component(__base__.Component): im.paste(imTop, (0, y), mask=imTop) return im + + def cancel(self): + self.canceled = True + + def reset(self): + self.canceled = False diff --git a/components/text.py b/components/text.py index 9237167..da2706b 100644 --- a/components/text.py +++ b/components/text.py @@ -141,3 +141,9 @@ class Component(__base__.Component): return self.page.lineEdit_textColor.setText(RGBstring) self.page.pushButton_textColor.setStyleSheet(btnStyle) + + def cancel(self): + self.canceled = True + + def reset(self): + self.canceled = False \ No newline at end of file diff --git a/core.py b/core.py index 5478f93..0bbe001 100644 --- a/core.py +++ b/core.py @@ -7,6 +7,7 @@ from PIL import Image import tempfile from shutil import rmtree import atexit +import time class Core(): @@ -67,6 +68,8 @@ class Core(): completeAudioArray = numpy.empty(0, dtype="int16") while True: + if self.canceled: + break # read 2 seconds of audio raw_audio = in_pipe.stdout.read(88200*4) if len(raw_audio) == 0: @@ -110,3 +113,9 @@ class Core(): shell=True ) return sorted([os.path.join(self.tempDir, f) for f in os.listdir(self.tempDir)]) + + def cancel(self): + self.canceled = True + + def reset(self): + self.canceled = False diff --git a/main.py b/main.py index bb42d1f..94020b2 100644 --- a/main.py +++ b/main.py @@ -253,14 +253,15 @@ class Main(QtCore.QObject): def stopVideo(self): print('stop') - try: - self.videoWorker.stopVideo() - except: - pass + 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.startExport = True + self.progressBarUpdated(-1) ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~")) self.videoThread = QtCore.QThread(self) @@ -281,7 +282,27 @@ class Main(QtCore.QObject): self.showMessage("You must select an audio file and output filename.") def progressBarUpdated(self, value): - self.window.progressBar_createVideo.setValue(value) + if value != -1: + self.window.progressBar_createVideo.setValue(value) + + if self.canceled: + self.window.pushButton_createVideo.setEnabled(True) + self.window.pushButton_Cancel.setEnabled(False) + self.startExport = False + return + + if value == 100 or value == 0: + if not self.startExport: + self.window.pushButton_createVideo.setEnabled(True) + self.window.pushButton_Cancel.setEnabled(False) + else: + if value == -1: + self.startExport = True + else: + self.startExport = False + self.window.pushButton_createVideo.setEnabled(False) + self.window.pushButton_Cancel.setEnabled(True) + def progressBarSetText(self, value): self.window.progressBar_createVideo.setFormat(value) diff --git a/mainwindow.ui b/mainwindow.ui index 808073b..6119e63 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -711,6 +711,9 @@ + + false + Cancel diff --git a/video_thread.py b/video_thread.py index 2e9eb13..504102a 100644 --- a/video_thread.py +++ b/video_thread.py @@ -82,20 +82,16 @@ class Worker(QtCore.QObject): ) layer = self.core.drawBaseImage(self.backgroundFrames[i]) background.paste(layer) - return background - - def stopVideo(self): - print('Stop Export') - self.canceled = True - self.out_pipe.send_signal(signal.SIGINT) + return background @pyqtSlot(str, str, str, list) def createVideo(self, backgroundImage, inputFile, outputFile, components): + self.components = components self.outputFile = outputFile + self.reset() self.width = int(self.core.settings.value('outputWidth')) self.height = int(self.core.settings.value('outputHeight')) # print('worker thread id: {}'.format(QtCore.QThread.currentThreadId())) - self.components = components progressBarValue = 0 self.progressBarUpdate.emit(progressBarValue) self.progressBarSetText.emit('Loading background image…') @@ -154,14 +150,14 @@ class Worker(QtCore.QObject): # initialize components print('loaded components:', - ["%s%s" % (num, str(component)) for num, component in enumerate(components)]) + ["%s%s" % (num, str(component)) for num, component in enumerate(self.components)]) self.staticComponents = {} - for compNo, comp in enumerate(components): + for compNo, comp in enumerate(self.components): properties = None properties = comp.preFrameRender( worker=self, completeAudioArray=self.completeAudioArray, - sampleSize=self.sampleSize + sampleSize=self.sampleSize, ) if properties and 'static' in properties: @@ -189,29 +185,29 @@ class Worker(QtCore.QObject): frameBuffer = {} self.lastPreview = 0.0 - - for i in range(0, len(self.completeAudioArray), self.sampleSize): - while True: - if i in frameBuffer: - # if frame's in buffer, pipe it to ffmpeg + if not self.canceled: + for i in range(0, len(self.completeAudioArray), self.sampleSize): + while True: + if i in frameBuffer: + # 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() + + try: + self.out_pipe.stdin.write(frameBuffer[i].tobytes()) + self.previewQueue.put([i, frameBuffer[i]]) + del frameBuffer[i] + except: break - # else fetch the next frame & add to the buffer - data = self.renderQueue.get() - frameBuffer[data[0]] = data[1] - self.renderQueue.task_done() - - 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) - self.progressBarSetText.emit('%s%%' % str(int(progressBarValue))) + # increase progress bar value + if progressBarValue + 1 <= (i / len(self.completeAudioArray)) * 100: + progressBarValue = numpy.floor((i / len(self.completeAudioArray)) * 100) + self.progressBarUpdate.emit(progressBarValue) + self.progressBarSetText.emit('%s%%' % str(int(progressBarValue))) numpy.seterr(all='print') @@ -224,7 +220,10 @@ class Worker(QtCore.QObject): self.out_pipe.wait() if self.canceled: print("Export Canceled") - os.remove(self.outputFile) + try: + os.remove(self.outputFile) + except: + pass self.progressBarUpdate.emit(0) self.progressBarSetText.emit('Export Canceled') else: @@ -243,3 +242,22 @@ class Worker(QtCore.QObject): self.core.deleteTempDir() self.videoCreated.emit() + + 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 From 53598f7a85e0238d5c2c42cd248876fb4e06eb16 Mon Sep 17 00:00:00 2001 From: DH4 Date: Fri, 2 Jun 2017 03:30:51 -0500 Subject: Progressbar enhancement. --- asd | Bin 0 -> 1310087 bytes asdf | Bin 0 -> 9685175 bytes components/original.py | 12 ++++++++++++ core.py | 35 +++++++++++++++++++++++++++++++++-- main.py | 23 ++++++----------------- video_thread.py | 19 +++++++++++++++---- 6 files changed, 66 insertions(+), 23 deletions(-) create mode 100644 asd create mode 100644 asdf (limited to 'main.py') diff --git a/asd b/asd new file mode 100644 index 0000000..39f0fa8 Binary files /dev/null and b/asd differ diff --git a/asdf b/asdf new file mode 100644 index 0000000..8cccca4 Binary files /dev/null and b/asdf differ diff --git a/components/original.py b/components/original.py index e1240e9..6903a5f 100644 --- a/components/original.py +++ b/components/original.py @@ -66,6 +66,14 @@ class Component(__base__.Component): 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, frameNo): width = int(self.worker.core.settings.value('outputWidth')) height = int(self.worker.core.settings.value('outputHeight')) @@ -151,3 +159,7 @@ class Component(__base__.Component): def reset(self): self.canceled = False + + + + diff --git a/core.py b/core.py index 0bbe001..96f5670 100644 --- a/core.py +++ b/core.py @@ -55,7 +55,24 @@ class Core(): return im - def readAudioFile(self, filename): + 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', @@ -67,16 +84,30 @@ class Core(): 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) - # print(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() diff --git a/main.py b/main.py index 94020b2..f13af7d 100644 --- a/main.py +++ b/main.py @@ -260,7 +260,7 @@ class Main(QtCore.QObject): # 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.startExport = True + self.changeEncodingStatus(True) self.progressBarUpdated(-1) ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~")) @@ -282,26 +282,15 @@ class Main(QtCore.QObject): self.showMessage("You must select an audio file and output filename.") def progressBarUpdated(self, value): - if value != -1: self.window.progressBar_createVideo.setValue(value) - - if self.canceled: - self.window.pushButton_createVideo.setEnabled(True) - self.window.pushButton_Cancel.setEnabled(False) - self.startExport = False - return - if value == 100 or value == 0: - if not self.startExport: - self.window.pushButton_createVideo.setEnabled(True) - self.window.pushButton_Cancel.setEnabled(False) - else: - if value == -1: - self.startExport = True - else: - self.startExport = False + def changeEncodingStatus(self, status): + if status: self.window.pushButton_createVideo.setEnabled(False) self.window.pushButton_Cancel.setEnabled(True) + else: + self.window.pushButton_createVideo.setEnabled(True) + self.window.pushButton_Cancel.setEnabled(False) def progressBarSetText(self, value): diff --git a/video_thread.py b/video_thread.py index 504102a..9f3eee2 100644 --- a/video_thread.py +++ b/video_thread.py @@ -107,8 +107,8 @@ class Worker(QtCore.QObject): self.imBackground = None self.bgI = 0 - self.progressBarSetText.emit('Loading audio file…') - self.completeAudioArray = self.core.readAudioFile(inputFile) + 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) @@ -152,12 +152,17 @@ class Worker(QtCore.QObject): print('loaded components:', ["%s%s" % (num, str(component)) for num, component in enumerate(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: @@ -207,7 +212,8 @@ class Worker(QtCore.QObject): if progressBarValue + 1 <= (i / len(self.completeAudioArray)) * 100: progressBarValue = numpy.floor((i / len(self.completeAudioArray)) * 100) self.progressBarUpdate.emit(progressBarValue) - self.progressBarSetText.emit('%s%%' % str(int(progressBarValue))) + pStr = "Exporting video: " + str(int(progressBarValue)) + "%" + self.progressBarSetText.emit(pStr) numpy.seterr(all='print') @@ -226,6 +232,7 @@ class Worker(QtCore.QObject): pass self.progressBarUpdate.emit(0) self.progressBarSetText.emit('Export Canceled') + else: if self.error: print("Export Failed") @@ -240,9 +247,13 @@ class Worker(QtCore.QObject): self.canceled = False self.parent.drawPreview() self.core.deleteTempDir() - + self.parent.changeEncodingStatus(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() -- cgit v1.2.3 From e33caa9179e972bc7ffdad0761d020e977559a5d Mon Sep 17 00:00:00 2001 From: DH4 Date: Fri, 2 Jun 2017 08:14:04 -0500 Subject: Threading changes. --- asd | Bin 1310087 -> 0 bytes asdf | Bin 9685175 -> 0 bytes main.py | 4 +--- video_thread.py | 31 ++++++++++++++++++------------- 4 files changed, 19 insertions(+), 16 deletions(-) delete mode 100644 asd delete mode 100644 asdf (limited to 'main.py') diff --git a/asd b/asd deleted file mode 100644 index 39f0fa8..0000000 Binary files a/asd and /dev/null differ diff --git a/asdf b/asdf deleted file mode 100644 index 8cccca4..0000000 Binary files a/asdf and /dev/null differ diff --git a/main.py b/main.py index f13af7d..cba4ce7 100644 --- a/main.py +++ b/main.py @@ -263,16 +263,13 @@ class Main(QtCore.QObject): self.changeEncodingStatus(True) self.progressBarUpdated(-1) ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~")) - 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.videoThread.start() self.videoTask.emit(self.window.lineEdit_background.text(), self.window.lineEdit_audioFile.text(), @@ -291,6 +288,7 @@ class Main(QtCore.QObject): else: self.window.pushButton_createVideo.setEnabled(True) self.window.pushButton_Cancel.setEnabled(False) + def progressBarSetText(self, value): diff --git a/video_thread.py b/video_thread.py index 9f3eee2..64bbd5f 100644 --- a/video_thread.py +++ b/video_thread.py @@ -8,7 +8,7 @@ import subprocess as sp import sys import os from queue import Queue, PriorityQueue -from threading import Thread +from threading import Thread, Event import time from copy import copy import signal @@ -31,9 +31,10 @@ class Worker(QtCore.QObject): self.sampleSize = 1470 self.canceled = False self.error = False + self.stopped = False def renderNode(self): - while True: + while not self.stopped: i = self.compositeQueue.get() if self.imBackground is not None: @@ -61,11 +62,9 @@ class Worker(QtCore.QObject): for i in range(0, len(self.completeAudioArray), self.sampleSize): self.compositeQueue.put([i, self.bgI]) - self.compositeQueue.join() - print('Compositing Complete.') def previewDispatch(self): - while True: + while not self.stopped: i = self.previewQueue.get() if time.time() - self.lastPreview >= 0.06 or i[0] == 0: self._image = ImageQt(i[1]) @@ -82,7 +81,7 @@ class Worker(QtCore.QObject): ) layer = self.core.drawBaseImage(self.backgroundFrames[i]) background.paste(layer) - return background + return background @pyqtSlot(str, str, str, list) def createVideo(self, backgroundImage, inputFile, outputFile, components): @@ -113,7 +112,7 @@ class Worker(QtCore.QObject): # test if user has libfdk_aac encoders = sp.check_output(self.core.FFMPEG_BIN + " -encoders -hide_banner", shell=True) acodec = self.core.settings.value('outputAudioCodec') - + if b'libfdk_aac' in encoders and acodec == 'aac': acodec = 'libfdk_aac' @@ -143,8 +142,6 @@ class Worker(QtCore.QObject): ffmpegCommand.append(outputFile) self.out_pipe = sp.Popen(ffmpegCommand, stdin=sp.PIPE,stdout=sys.stdout, stderr=sys.stdout) - - # create video for output numpy.seterr(divide='ignore') @@ -167,6 +164,7 @@ class Worker(QtCore.QObject): if properties and 'static' in properties: self.staticComponents[compNo] = copy(comp.frameRender(compNo, 0)) + self.progressBarUpdate.emit(100) self.compositeQueue = Queue() self.compositeQueue.maxsize = 20 @@ -174,11 +172,12 @@ class Worker(QtCore.QObject): self.renderQueue.maxsize = 20 self.previewQueue = PriorityQueue() + self.renderThreads = [] # create threads to render frames and send them back here for piping out for i in range(3): - t = Thread(target=self.renderNode, name="Render Thread") - t.daemon = True - t.start() + 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 @@ -190,6 +189,9 @@ class Worker(QtCore.QObject): 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: @@ -247,8 +249,11 @@ class Worker(QtCore.QObject): self.canceled = False self.parent.drawPreview() self.core.deleteTempDir() - self.parent.changeEncodingStatus(False) + self.stopped = True self.videoCreated.emit() + self.parent.changeEncodingStatus(False) + + return def updateProgress(self, pStr, pVal): self.progressBarValue.emit(pVal) -- cgit v1.2.3 From 4b566601772a00e354e0f144bb3dc76ed043be4f Mon Sep 17 00:00:00 2001 From: DH4 Date: Sat, 3 Jun 2017 00:07:30 -0500 Subject: Changed encoding update to signal/slot. --- main.py | 4 ++-- mainwindow.ui | 4 ++-- video_thread.py | 7 +++---- 3 files changed, 7 insertions(+), 8 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index cba4ce7..e104b2b 100644 --- a/main.py +++ b/main.py @@ -260,7 +260,6 @@ class Main(QtCore.QObject): # 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.changeEncodingStatus(True) self.progressBarUpdated(-1) ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~")) self.videoThread = QtCore.QThread(self) @@ -269,7 +268,8 @@ class Main(QtCore.QObject): 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.imageCreated.connect(self.showPreviewImage) + self.videoWorker.encoding.connect(self.changeEncodingStatus) self.videoThread.start() self.videoTask.emit(self.window.lineEdit_background.text(), self.window.lineEdit_audioFile.text(), diff --git a/mainwindow.ui b/mainwindow.ui index 6119e63..42a22a6 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -240,7 +240,7 @@ QTabWidget::Rounded - 0 + 2 @@ -712,7 +712,7 @@ - false + true Cancel diff --git a/video_thread.py b/video_thread.py index 64bbd5f..4032c27 100644 --- a/video_thread.py +++ b/video_thread.py @@ -19,6 +19,7 @@ class Worker(QtCore.QObject): videoCreated = pyqtSignal() progressBarUpdate = pyqtSignal(int) progressBarSetText = pyqtSignal(str) + encoding = pyqtSignal(bool) def __init__(self, parent=None): QtCore.QObject.__init__(self) @@ -85,6 +86,7 @@ class Worker(QtCore.QObject): @pyqtSlot(str, str, str, list) def createVideo(self, backgroundImage, inputFile, outputFile, components): + self.encoding.emit(True) self.components = components self.outputFile = outputFile self.reset() @@ -250,10 +252,8 @@ class Worker(QtCore.QObject): self.parent.drawPreview() self.core.deleteTempDir() self.stopped = True + self.encoding.emit(False) self.videoCreated.emit() - self.parent.changeEncodingStatus(False) - - return def updateProgress(self, pStr, pVal): self.progressBarValue.emit(pVal) @@ -273,7 +273,6 @@ class Worker(QtCore.QObject): def reset(self): self.core.reset() - self.canceled = False for comp in self.components: comp.reset() -- cgit v1.2.3 From fccdee45b291bbb4570650b6c1ff00dd21dbb43f Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 3 Jun 2017 08:46:18 -0400 Subject: absolute path to main ui, bg video fixed --- main.py | 2 +- video_thread.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index e104b2b..9fac283 100644 --- a/main.py +++ b/main.py @@ -580,7 +580,7 @@ if __name__ == "__main__": app = QtGui.QApplication(sys.argv) app.setApplicationName("audio-visualizer") app.setOrganizationName("audio-visualizer") - window = uic.loadUi("mainwindow.ui") + window = uic.loadUi(os.path.join(os.path.dirname(os.path.realpath(__file__)), "mainwindow.ui")) # window.adjustSize() desc = QtGui.QDesktopWidget() dpi = desc.physicalDpiX() diff --git a/video_thread.py b/video_thread.py index 4032c27..0d42406 100644 --- a/video_thread.py +++ b/video_thread.py @@ -56,13 +56,13 @@ class Worker(QtCore.QObject): def renderDispatch(self): print('Dispatching Frames for Compositing...') - if not self.imBackground: - # increment background video frame for next iteration - if self.bgI < len(self.backgroundFrames)-1 and i != 0: - self.bgI += 1 for i in range(0, len(self.completeAudioArray), self.sampleSize): self.compositeQueue.put([i, self.bgI]) + if not self.imBackground: + # increment background video frame for next iteration + if self.bgI < len(self.backgroundFrames)-1: + self.bgI += 1 def previewDispatch(self): while not self.stopped: -- cgit v1.2.3 From 0bef283f8d4538a6a3cff740a530973e745ad9c8 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 3 Jun 2017 10:01:47 -0400 Subject: saved project dirs --- components/text.py | 10 +++++--- main.py | 75 ++++++++++++++++++++++++------------------------------ 2 files changed, 40 insertions(+), 45 deletions(-) (limited to 'main.py') diff --git a/components/text.py b/components/text.py index da2706b..bfc9701 100644 --- a/components/text.py +++ b/components/text.py @@ -8,6 +8,10 @@ from . import __base__ class Component(__base__.Component): '''Title Text''' + def __init__(self): + super().__init__() + self.titleFont = QFont() + def widget(self, parent): height = int(parent.settings.value('outputHeight')) width = int(parent.settings.value('outputWidth')) @@ -36,8 +40,8 @@ class Component(__base__.Component): page.pushButton_textColor.setStyleSheet(btnStyle) page.lineEdit_title.setText(self.title) - if not self.titleFont == None: - page.fontComboBox_titleFont.setCurrentFont(QFont(self.titleFont)) + #if self.titleFont: + # page.fontComboBox_titleFont.setCurrentFont(QFont(self.titleFont)) page.comboBox_textAlign.setCurrentIndex(int(self.alignment)) page.spinBox_fontSize.setValue(int(self.fontSize)) page.spinBox_xTextAlign.setValue(int(self.xPosition)) @@ -146,4 +150,4 @@ class Component(__base__.Component): self.canceled = True def reset(self): - self.canceled = False \ No newline at end of file + self.canceled = False diff --git a/main.py b/main.py index 9fac283..df9a4f6 100644 --- a/main.py +++ b/main.py @@ -146,16 +146,18 @@ class Main(QtCore.QObject): self.pages = [] self.selectedComponents = [] - # create data directory structure if needed + # create data directory, load/create settings self.dataDir = QDesktopServices.storageLocation(QDesktopServices.DataLocation) - if not os.path.exists(self.dataDir): - os.makedirs(self.dataDir) - for neededDirectory in ('projects', 'project-settings', 'presets'): - if not os.path.exists(os.path.join(self.dataDir, neededDirectory)): - os.mkdir(os.path.join(self.dataDir, neededDirectory)) self.settings = QSettings(os.path.join(self.dataDir, 'settings.ini'), QSettings.IniFormat) LoadDefaultSettings(self) + if not os.path.exists(self.dataDir): + os.makedirs(self.dataDir) + presetDir = os.path.join(self.dataDir, 'presets') + for neededDirectory in (presetDir, self.settings.value("projectDir")): + if not os.path.exists(neededDirectory): + os.mkdir(neededDirectory) + # self.previewQueue = Queue() self.previewThread = QtCore.QThread(self) self.previewWorker = preview_thread.Worker(self, self.previewQueue) @@ -176,7 +178,7 @@ class Main(QtCore.QObject): window.pushButton_Cancel.clicked.connect(self.stopVideo) window.setWindowTitle("Audio Visualizer") - self.previewWindow = PreviewWindow(self, r"background.jpg") + self.previewWindow = PreviewWindow(self, os.path.join(os.path.dirname(os.path.realpath(__file__)), "background.jpg")) window.verticalLayout_previewWrapper.addWidget(self.previewWindow) self.modules = self.findComponents() @@ -206,19 +208,14 @@ class Main(QtCore.QObject): self.window.pushButton_saveProject.clicked.connect(self.saveCurrentProject) self.window.pushButton_openProject.clicked.connect(self.openOpenProjectDialog) + self.openProject(self.settings.value("lastProject")) self.drawPreview() - window.show() def cleanUp(self): self.timer.stop() self.previewThread.quit() self.previewThread.wait() - backupPath = os.path.join(self.dataDir, 'settings.ini~') - settingsPath = self.settings.fileName() - if self.currentProject: - os.remove(settingsPath) - os.rename(backupPath, settingsPath) def openInputFileDialog(self): inputDir = self.settings.value("inputDir", expanduser("~")) @@ -343,13 +340,15 @@ class Main(QtCore.QObject): self.window.listWidget_componentList.takeItem(index) self.selectedComponents.pop(index) self.pages.pop(index) + self.changeComponentWidget() self.drawPreview() def changeComponentWidget(self): selected = self.window.listWidget_componentList.selectedItems() - index = self.window.listWidget_componentList.row(selected[0]) - self.window.stackedWidget.setCurrentIndex(index) - self.updateOpenPresetComboBox(self.selectedComponents[index]) + if selected: + index = self.window.listWidget_componentList.row(selected[0]) + self.window.stackedWidget.setCurrentIndex(index) + self.updateOpenPresetComboBox(self.selectedComponents[index]) def moveComponentUp(self): row = self.window.listWidget_componentList.currentRow() @@ -463,35 +462,39 @@ class Main(QtCore.QObject): self.openSaveProjectDialog() def openSaveProjectDialog(self): - outputDir = os.path.join(self.dataDir, 'projects') - filename = QtGui.QFileDialog.getSaveFileName(self.window, "Create Project File", outputDir) + filename = QtGui.QFileDialog.getSaveFileName(self.window, + "Create Project File", self.settings.value("projectDir"), + "Project Files (*.avp)") if not filename: return - filepath = os.path.join(outputDir, filename) - self.currentProject = filepath - self.createProjectFile(filepath) + self.createProjectFile(filename) def createProjectFile(self, filepath): + if not filepath.endswith(".avp"): + filepath += '.avp' with open(filepath, 'w') as f: 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' % repr(saveValueStore)) - projSettingsPath = os.path.join(self.dataDir, 'project-settings', os.path.basename('%s.ini' % filepath)) - shutil.copyfile(self.settings.fileName(), projSettingsPath) + self.settings.setValue("projectDir", os.path.dirname(filepath)) + self.settings.setValue("lastProject", filepath) + self.currentProject = filepath def openOpenProjectDialog(self): - inputDir = os.path.join(self.dataDir, 'projects') - filename = QtGui.QFileDialog.getOpenFileName(self.window, "Open Project File", inputDir) - if not filename: - return - filepath = os.path.join(inputDir, filename) - self.openProject(filepath) + filename = QtGui.QFileDialog.getOpenFileName(self.window, + "Open Project File", self.settings.value("projectDir"), + "Project Files (*.avp)") + self.openProject(filename) def openProject(self, filepath): + if not filepath or not os.path.exists(filepath) or not filepath.endswith('.avp'): + return self.clear() self.currentProject = filepath + self.settings.setValue("lastProject", filepath) + self.settings.setValue("projectDir", os.path.dirname(filepath)) compNames = [mod.Component.__doc__ for mod in self.modules] with open(filepath, 'r') as f: i = 0 @@ -507,19 +510,6 @@ class Main(QtCore.QObject): saveValueStore = eval(line.strip()) self.selectedComponents[-1].loadPreset(saveValueStore) i = 0 - projSettingsPath = os.path.join(self.dataDir, 'project-settings', '%s.ini' % os.path.basename(filepath)) - backupPath = os.path.join(self.dataDir, 'settings.ini~') - settingsPath = self.settings.fileName() - if os.path.exists(backupPath): - os.remove(backupPath) - os.rename(settingsPath, backupPath) - shutil.copyfile(projSettingsPath, settingsPath) - self.settings.sync() - currentRes = str(self.settings.value('outputWidth'))+'x'+str(self.settings.value('outputHeight')) - for i in range(self.window.comboBox_resolution.count()-1): - if self.window.comboBox_resolution.itemText(i) == currentRes: - self.window.comboBox_resolution.setCurrentIndex(i) - break def showMessage(self, string, icon=QtGui.QMessageBox.Information, showCancel=False): msg = QtGui.QMessageBox() @@ -559,6 +549,7 @@ def LoadDefaultSettings(self): "outputVideoFormat": "yuv420p", "outputPreset": "medium", "outputFormat": "mp4", + "projectDir" : os.path.join(self.dataDir, 'projects'), } for parm, value in default.items(): -- cgit v1.2.3 From f0ab2f53d6e5b44fa4e763a204b0c5034808551b Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 3 Jun 2017 11:08:52 -0400 Subject: section structure in avp files --- main.py | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index df9a4f6..48d7925 100644 --- a/main.py +++ b/main.py @@ -473,6 +473,7 @@ class Main(QtCore.QObject): if not filepath.endswith(".avp"): filepath += '.avp' with open(filepath, 'w') as f: + f.write('[Components]\n') for comp in self.selectedComponents: saveValueStore = comp.savePreset() f.write('%s\n' % str(comp)) @@ -496,20 +497,35 @@ class Main(QtCore.QObject): self.settings.setValue("lastProject", filepath) self.settings.setValue("projectDir", os.path.dirname(filepath)) compNames = [mod.Component.__doc__ for mod in self.modules] + with open(filepath, 'r') as f: + validSections = ('Components') + section = '' + def parseLine(line): + line = line.strip() + newSection = '' + if line.startswith('[') and line.endswith(']') and line[1:-1] in validSections: + newSection = line[1:-1] + return line, newSection + i = 0 for line in f: - if i == 0: - compIndex = compNames.index(line.strip()) - self.addComponent(compIndex) - i += 1 - elif i == 1: - # version, not used yet - i += 1 - elif i == 2: - saveValueStore = eval(line.strip()) - self.selectedComponents[-1].loadPreset(saveValueStore) - i = 0 + line, newSection = parseLine(line) + if newSection: + section = str(newSection) + continue + if line and section == 'Components': + if i == 0: + compIndex = compNames.index(line.strip()) + self.addComponent(compIndex) + i += 1 + elif i == 1: + # version, not used yet + i += 1 + elif i == 2: + saveValueStore = eval(line.strip()) + self.selectedComponents[-1].loadPreset(saveValueStore) + i = 0 def showMessage(self, string, icon=QtGui.QMessageBox.Information, showCancel=False): msg = QtGui.QMessageBox() -- cgit v1.2.3 From 2cbae481c5216f9adedb135fec077370969bac36 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 3 Jun 2017 15:24:52 -0400 Subject: autosave to help restore unsaved projects in case of a crash --- core.py | 6 ++++ main.py | 124 +++++++++++++++++++++++++++++++++++++++------------------------- 2 files changed, 81 insertions(+), 49 deletions(-) (limited to 'main.py') diff --git a/core.py b/core.py index 96f5670..16ecb35 100644 --- a/core.py +++ b/core.py @@ -8,6 +8,7 @@ import tempfile from shutil import rmtree import atexit import time +from collections import OrderedDict class Core(): @@ -150,3 +151,8 @@ class Core(): def reset(self): self.canceled = False + + @staticmethod + def sortedStringDict(dictionary): + sorted_ = OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])) + return repr(sorted_) diff --git a/main.py b/main.py index 48d7925..1f3e1ec 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,8 @@ -import sys, io, os, shutil, atexit, string, signal +import sys, io, os, shutil, atexit, string, signal, filecmp from os.path import expanduser from queue import Queue from importlib import import_module +from collections import OrderedDict from PyQt4 import QtCore, QtGui, uic from PyQt4.QtCore import QSettings, QModelIndex, Qt from PyQt4.QtGui import QDesktopServices @@ -142,18 +143,18 @@ class Main(QtCore.QObject): # print('main thread id: {}'.format(QtCore.QThread.currentThreadId())) self.window = window self.core = core.Core() - self.currentProject = None self.pages = [] self.selectedComponents = [] # create data directory, load/create settings self.dataDir = QDesktopServices.storageLocation(QDesktopServices.DataLocation) + self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') + self.presetDir = os.path.join(self.dataDir, 'presets') self.settings = QSettings(os.path.join(self.dataDir, 'settings.ini'), QSettings.IniFormat) LoadDefaultSettings(self) if not os.path.exists(self.dataDir): os.makedirs(self.dataDir) - presetDir = os.path.join(self.dataDir, 'presets') - for neededDirectory in (presetDir, self.settings.value("projectDir")): + for neededDirectory in (self.presetDir, self.settings.value("projectDir")): if not os.path.exists(neededDirectory): os.mkdir(neededDirectory) @@ -208,14 +209,35 @@ class Main(QtCore.QObject): self.window.pushButton_saveProject.clicked.connect(self.saveCurrentProject) self.window.pushButton_openProject.clicked.connect(self.openOpenProjectDialog) - self.openProject(self.settings.value("lastProject")) - self.drawPreview() + # show the window and load current project window.show() + self.currentProject = self.settings.value("currentProject") + if self.currentProject and os.path.exists(self.autosavePath) \ + and filecmp.cmp(self.autosavePath, self.currentProject): + # delete autosave if it's identical to the project + os.remove(self.autosavePath) + + if self.currentProject and os.path.exists(self.autosavePath): + ch = self.showMessage("Restore unsaved changes in project '%s'?" % os.path.basename(self.currentProject)[:-4], True) + if ch: + os.remove(self.currentProject) + os.rename(self.autosavePath, self.currentProject) + else: + os.remove(self.autosavePath) + + self.openProject(self.currentProject) + self.drawPreview() def cleanUp(self): self.timer.stop() self.previewThread.quit() self.previewThread.wait() + self.autosave() + + def autosave(self): + if os.path.exists(self.autosavePath): + os.remove(self.autosavePath) + self.createProjectFile(self.autosavePath) def openInputFileDialog(self): inputDir = self.settings.value("inputDir", expanduser("~")) @@ -303,9 +325,9 @@ class Main(QtCore.QObject): self.drawPreview() def drawPreview(self): - #self.settings.setValue('visLayout', self.window.comboBox_visLayout.currentIndex()) self.newTask.emit(self.window.lineEdit_background.text(), self.selectedComponents) # self.processTask.emit() + self.autosave() def showPreviewImage(self, image): self.previewWindow.changePixmap(image) @@ -387,7 +409,7 @@ class Main(QtCore.QObject): def updateOpenPresetComboBox(self, component): self.window.comboBox_openPreset.clear() self.window.comboBox_openPreset.addItem("Open Preset") - destination = os.path.join(self.dataDir, 'presets', + destination = os.path.join(self.presetDir, str(component).strip(), str(component.version())) if not os.path.exists(destination): os.makedirs(destination) @@ -417,12 +439,12 @@ class Main(QtCore.QObject): break def createPresetFile(self, componentName, version, saveValueStore, filename): - dirname = os.path.join(self.dataDir, 'presets', componentName, str(version)) + dirname = os.path.join(self.presetDir, componentName, str(version)) if not os.path.exists(dirname): os.makedirs(dirname) filepath = os.path.join(dirname, filename) if os.path.exists(filepath): - ch = self.showMessage("%s already exists! Overwrite it?" % filename, QtGui.QMessageBox.Warning, True) + ch = self.showMessage("%s already exists! Overwrite it?" % filename, True, QtGui.QMessageBox.Warning) if not ch: return # remove old copies of the preset @@ -430,7 +452,7 @@ class Main(QtCore.QObject): if self.window.comboBox_openPreset.itemText(i) == filename: self.window.comboBox_openPreset.removeItem(i) with open(filepath, 'w') as f: - f.write(repr(saveValueStore)) + f.write(core.Core.sortedStringDict(saveValueStore)) self.window.comboBox_openPreset.addItem(filename) self.window.comboBox_openPreset.setCurrentIndex(self.window.comboBox_openPreset.count()-1) @@ -443,14 +465,14 @@ class Main(QtCore.QObject): filename = self.window.comboBox_openPreset.itemText(self.window.comboBox_openPreset.currentIndex()) componentName = str(self.selectedComponents[index]).strip() version = self.selectedComponents[index].version() - dirname = os.path.join(self.dataDir, 'presets', componentName, str(version)) + dirname = os.path.join(self.presetDir, componentName, str(version)) filepath = os.path.join(dirname, filename) if not os.path.exists(filepath): self.window.comboBox_openPreset.removeItem(self.window.comboBox_openPreset.currentIndex()) return with open(filepath, 'r') as f: for line in f: - saveValueStore = eval(line.strip()) + saveValueStore = dict(eval(line.strip())) break self.selectedComponents[index].loadPreset(saveValueStore) self.drawPreview() @@ -478,10 +500,11 @@ class Main(QtCore.QObject): saveValueStore = comp.savePreset() f.write('%s\n' % str(comp)) f.write('%s\n' % str(comp.version())) - f.write('%s\n' % repr(saveValueStore)) - self.settings.setValue("projectDir", os.path.dirname(filepath)) - self.settings.setValue("lastProject", filepath) - self.currentProject = filepath + f.write('%s\n' % core.Core.sortedStringDict(saveValueStore)) + if filepath != self.autosavePath: + self.settings.setValue("projectDir", os.path.dirname(filepath)) + self.settings.setValue("currentProject", filepath) + self.currentProject = filepath def openOpenProjectDialog(self): filename = QtGui.QFileDialog.getOpenFileName(self.window, @@ -494,40 +517,43 @@ class Main(QtCore.QObject): return self.clear() self.currentProject = filepath - self.settings.setValue("lastProject", filepath) + self.settings.setValue("currentProject", filepath) self.settings.setValue("projectDir", os.path.dirname(filepath)) compNames = [mod.Component.__doc__ for mod in self.modules] - - with open(filepath, 'r') as f: - validSections = ('Components') - section = '' - def parseLine(line): - line = line.strip() - newSection = '' - if line.startswith('[') and line.endswith(']') and line[1:-1] in validSections: - newSection = line[1:-1] - return line, newSection - - i = 0 - for line in f: - line, newSection = parseLine(line) - if newSection: - section = str(newSection) - continue - if line and section == 'Components': - if i == 0: - compIndex = compNames.index(line.strip()) - self.addComponent(compIndex) - i += 1 - elif i == 1: - # version, not used yet - i += 1 - elif i == 2: - saveValueStore = eval(line.strip()) - self.selectedComponents[-1].loadPreset(saveValueStore) - i = 0 - - def showMessage(self, string, icon=QtGui.QMessageBox.Information, showCancel=False): + try: + with open(filepath, 'r') as f: + validSections = ('Components') + section = '' + def parseLine(line): + line = line.strip() + newSection = '' + if line.startswith('[') and line.endswith(']') and line[1:-1] in validSections: + newSection = line[1:-1] + return line, newSection + + i = 0 + for line in f: + line, newSection = parseLine(line) + if newSection: + section = str(newSection) + continue + if line and section == 'Components': + if i == 0: + compIndex = compNames.index(line) + self.addComponent(compIndex) + i += 1 + elif i == 1: + # version, not used yet + i += 1 + elif i == 2: + saveValueStore = dict(eval(line)) + self.selectedComponents[-1].loadPreset(saveValueStore) + i = 0 + except: + self.clear() + self.showMessage("Project file '%s' is corrupted." % filepath) + + def showMessage(self, string, showCancel=False, icon=QtGui.QMessageBox.Information): msg = QtGui.QMessageBox() msg.setIcon(icon) msg.setText(string) -- cgit v1.2.3 From cf197904b82d6f5769f23c15d047d22a2bd46644 Mon Sep 17 00:00:00 2001 From: DH4 Date: Sat, 3 Jun 2017 16:46:52 -0500 Subject: Add component changed to menu. --- main.py | 17 +++++----- mainwindow.ui | 101 +++++++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 84 insertions(+), 34 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index 1f3e1ec..49fe469 100644 --- a/main.py +++ b/main.py @@ -5,7 +5,7 @@ from importlib import import_module from collections import OrderedDict from PyQt4 import QtCore, QtGui, uic from PyQt4.QtCore import QSettings, QModelIndex, Qt -from PyQt4.QtGui import QDesktopServices +from PyQt4.QtGui import QDesktopServices, QMenu import preview_thread, core, video_thread @@ -183,13 +183,14 @@ class Main(QtCore.QObject): window.verticalLayout_previewWrapper.addWidget(self.previewWindow) self.modules = self.findComponents() - for component in self.modules: - window.comboBox_componentSelection.addItem(component.Component.__doc__) - window.listWidget_componentList.clicked.connect(lambda _: self.changeComponentWidget()) + self.compMenu = QMenu() + for i, comp in enumerate(self.modules): + action = self.compMenu.addAction(comp.Component.__doc__) + action.triggered[()].connect( lambda item=i: self.addComponent(item)) - self.window.pushButton_addComponent.clicked.connect( \ - lambda _: self.addComponent(self.window.comboBox_componentSelection.currentIndex()) - ) + self.window.pushButton_addComponent.setMenu(self.compMenu) + window.listWidget_componentList.clicked.connect(lambda _: self.changeComponentWidget()) + self.window.pushButton_removeComponent.clicked.connect(lambda _: self.removeComponent()) currentRes = str(self.settings.value('outputWidth'))+'x'+str(self.settings.value('outputHeight')) @@ -408,7 +409,7 @@ class Main(QtCore.QObject): def updateOpenPresetComboBox(self, component): self.window.comboBox_openPreset.clear() - self.window.comboBox_openPreset.addItem("Open Preset") + self.window.comboBox_openPreset.addItem("Component Presets") destination = os.path.join(self.presetDir, str(component).strip(), str(component.version())) if not os.path.exists(destination): diff --git a/mainwindow.ui b/mainwindow.ui index eda5bb6..5e10028 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -141,27 +141,11 @@ 20 - 20 + 15 - - - - - 0 - 0 - - - - - 280 - 0 - - - - @@ -197,17 +181,63 @@ - - - - 0 - 0 - + + + 4 - + + 2 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + 1 + + + false + + + false + + + false + + + QAbstractItemView::NoDragDrop + + + + + + 2 + @@ -216,17 +246,36 @@ 0 + + + 180 + 0 + + - Open Preset + Component Presets + + + 0 + 0 + + + + Save + + + + + - Save Preset + Remove -- cgit v1.2.3 From a3557cbc4f192b520f7fccd849ebf4f70937cfee Mon Sep 17 00:00:00 2001 From: DH4 Date: Sat, 3 Jun 2017 19:10:27 -0500 Subject: UI Updates, encode lockout, added encoder-options.json. FIXME: Add encoder options to the UI. --- encoder-options.json | 191 ++++++++++++++++++++++++++++ main.py | 44 +++++++ mainwindow.ui | 350 ++++++++++++++++++++++++++++----------------------- 3 files changed, 427 insertions(+), 158 deletions(-) create mode 100644 encoder-options.json (limited to 'main.py') diff --git a/encoder-options.json b/encoder-options.json new file mode 100644 index 0000000..699ead4 --- /dev/null +++ b/encoder-options.json @@ -0,0 +1,191 @@ +{ + "containers":[ + { + "name": "MP4", + "container": "mp4", + "default-vcodec": "H264", + "default-acodec": "AAC", + "video-codecs": [ + { + "name": "H264", + "encoders": ["libx264"] + }, + { + "name": "H264 (nvenc)", + "encoders": ["nvenc_264"] + }, + { + "name": "MPEG4", + "encoders": ["mpeg4"] + } + ], + "audio-codecs": [ + { + "name": "AAC", + "encoders": ["libfdk_aac","aac"] + }, + { + "name": "AC3", + "encoders": ["ac3"] + }, + { + "name": "MP3", + "encoders": ["libmp3lame"] + } + ] + }, + { + "name": "MOV", + "container": "mov", + "default-vcodec": "H264", + "default-acodec": "AAC", + "video-codecs": [ + { + "name": "H264", + "encoders": ["libx264"] + }, + { + "name": "H264 (nvenc)", + "encoders": ["nvenc_264"] + }, + { + "name": "MPEG4", + "encoders": ["mpeg4"] + }, + { + "name": "XVID", + "encoders": ["libxvid"] + } + ], + "audio-codecs": [ + { + "name": "AAC", + "encoders": ["libfdk_aac","aac"] + }, + { + "name": "AC3", + "encoders": ["ac3"] + }, + { + "name": "MP3", + "encoders": ["libmp3lame"] + }, + { + "name": "PCM s16 LE", + "encoders": ["pcm_s16le"] + } + ] + }, + { + "name": "AVI", + "container": "avi", + "default-vcodec": "H264", + "default-acodec": "AAC", + "video-codecs": [ + { + "name": "H264", + "encoders": ["libx264"] + }, + { + "name": "H264 (nvenc)", + "encoders": ["nvenc_264"] + }, + { + "name": "MPEG4", + "encoders": ["mpeg4"] + }, + { + "name": "MPEG2", + "encoders": ["mp2video"] + }, + { + "name": "DV", + "encoders": ["dvvideo"] + }, + { + "name": "WMV", + "encoders": ["wmv2"] + } + ], + "audio-codecs": [ + { + "name": "AAC", + "encoders": ["libfdk_aac","aac"] + }, + { + "name": "AC3", + "encoders": ["ac3"] + }, + { + "name": "WMA", + "encoders": ["wmav2"] + }, + { + "name": "MP3", + "encoders": ["libmp3lame"] + }, + { + "name": "PCM s16 LE", + "encoders": ["pcm_s16le"] + } + ] + }, + { + "name": "WEBM", + "container": "webm", + "default-vcodec": "VP9", + "default-acodec": "Vorbis", + "video-codecs": [ + { + "name": "VP9", + "encoders": ["libvpx-vp9"] + }, + { + "name": "VP8", + "encoders": ["libvpx"] + } + ], + "audio-codecs": [ + { + "name": "Vorbis", + "encoders": ["vorbis"] + } + ] + }, + { + "name": "FLV", + "container": "flv", + "default-vcodec": "FLV", + "default-acodec": "Vorbis", + "video-codecs": [ + { + "name": "Sorenson (flv)", + "encoders": ["flv"] + }, + { + "name": "H264", + "encoders": ["libx264"] + }, + { + "name": "MPEG4", + "encoders": ["mpeg4"] + } + ], + "audio-codecs": [ + { + "name": "MP3", + "encoders": ["libmp3lame"] + }, + { + "name": "Vorbis", + "encoders": ["vorbis"] + }, + { + "name": "PCM s16 LE", + "encoders": ["pcm_s16le"] + } + ] + } + ] + +} \ No newline at end of file diff --git a/main.py b/main.py index 49fe469..c5080f3 100644 --- a/main.py +++ b/main.py @@ -305,9 +305,53 @@ class Main(QtCore.QObject): 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.comboBox_openPreset.setEnabled(False) + self.window.pushButton_removePreset.setEnabled(False) + self.window.pushButton_savePreset.setEnabled(False) + self.window.pushButton_openProject.setEnabled(False) + self.window.listWidget_componentList.setEnabled(False) + + self.window.label_background.setEnabled(False) + self.window.lineEdit_background.setEnabled(False) + self.window.toolButton_selectBackground.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.comboBox_openPreset.setEnabled(True) + self.window.pushButton_removePreset.setEnabled(True) + self.window.pushButton_savePreset.setEnabled(True) + self.window.pushButton_openProject.setEnabled(True) + self.window.listWidget_componentList.setEnabled(True) + + self.window.label_background.setEnabled(True) + self.window.lineEdit_background.setEnabled(True) + self.window.toolButton_selectBackground.setEnabled(True) diff --git a/mainwindow.ui b/mainwindow.ui index 5e10028..f9e8f5e 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -325,11 +325,11 @@ 0 - + - Input Settings + Export Video - + 10 @@ -348,10 +348,16 @@ - 100 + 85 0 + + + 80 + 16777215 + + 80 @@ -427,7 +433,7 @@ - 100 + 85 0 @@ -482,9 +488,142 @@ + + + + + + + + + 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 @@ -504,17 +643,40 @@ - 98 + 85 0 - Video Format + Container + + + + + + + + 150 + 0 + - + + + Qt::Horizontal + + + QSizePolicy::Minimum + + + + 5 + 5 + + + @@ -525,12 +687,25 @@ - Video Preset + Resolution - + + + + 0 + 0 + + + + + 0 + 0 + + + @@ -546,7 +721,7 @@ - 98 + 85 0 @@ -590,12 +765,12 @@ - Resolution + Video Bitrate - + @@ -611,7 +786,7 @@ - 98 + 85 0 @@ -655,158 +830,17 @@ - Bitrate + Audio Bitrate - + - - - Export Video - - - - 10 - - - - - - - - - - 0 - 0 - - - - - 100 - 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 - - - - - - - true - - - Cancel - - - - - - - - -- cgit v1.2.3 From 5b78a26d8069e48ad5ba88f457b563620be72173 Mon Sep 17 00:00:00 2001 From: DH4 Date: Sat, 3 Jun 2017 19:54:53 -0500 Subject: Add component inserts on top. --- main.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'main.py') diff --git a/main.py b/main.py index c5080f3..2aa7fa9 100644 --- a/main.py +++ b/main.py @@ -186,7 +186,7 @@ class Main(QtCore.QObject): self.compMenu = QMenu() for i, comp in enumerate(self.modules): action = self.compMenu.addAction(comp.Component.__doc__) - action.triggered[()].connect( lambda item=i: self.addComponent(item)) + action.triggered[()].connect( lambda item=i: self.insertComponent(item)) self.window.pushButton_addComponent.setMenu(self.compMenu) window.listWidget_componentList.clicked.connect(lambda _: self.changeComponentWidget()) @@ -400,6 +400,16 @@ class Main(QtCore.QObject): self.selectedComponents[-1].update() self.updateOpenPresetComboBox(self.selectedComponents[-1]) + def insertComponent(self, moduleIndex): + self.selectedComponents.insert(0, self.modules[moduleIndex].Component()) + self.window.listWidget_componentList.insertItem(0, self.selectedComponents[0].__doc__) + self.pages.insert(0, self.selectedComponents[0].widget(self)) + self.window.listWidget_componentList.setCurrentRow(0) + self.window.stackedWidget.insertWidget(0, self.pages[0]) + self.window.stackedWidget.setCurrentIndex(0) + self.selectedComponents[0].update() + self.updateOpenPresetComboBox(self.selectedComponents[0]) + def removeComponent(self): for selected in self.window.listWidget_componentList.selectedItems(): index = self.window.listWidget_componentList.row(selected) -- cgit v1.2.3 From 39e66ffa2d07b87b57ed90b369ab26aedf0a69e8 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 4 Jun 2017 13:00:36 -0400 Subject: video component almost working, rm hardcoded backgrounds --- background.jpg | Bin 14782 -> 0 bytes background.png | Bin 0 -> 3711 bytes components/__base__.py | 7 +++++ components/color.py | 16 +++++------ components/image.py | 14 +++++----- components/original.py | 20 ++++---------- components/text.py | 8 +----- components/video.py | 68 +++++++++++++++++++++++++++++----------------- core.py | 61 +++++------------------------------------ main.py | 50 +++++++++++----------------------- mainwindow.ui | 72 ++----------------------------------------------- preview_thread.py | 20 +++----------- video_thread.py | 46 +++++++++---------------------- 13 files changed, 110 insertions(+), 272 deletions(-) delete mode 100644 background.jpg create mode 100644 background.png (limited to 'main.py') diff --git a/background.jpg b/background.jpg deleted file mode 100644 index f746432..0000000 Binary files a/background.jpg and /dev/null differ diff --git a/background.png b/background.png new file mode 100644 index 0000000..7a33158 Binary files /dev/null and b/background.png differ diff --git a/components/__base__.py b/components/__base__.py index 45c148d..f564aad 100644 --- a/components/__base__.py +++ b/components/__base__.py @@ -7,6 +7,13 @@ class Component: def version(self): # change this number to identify new versions of a component return 1 + + def cancel(self): + # make sure your component responds to these variables in frameRender() + self.canceled = True + + def reset(self): + self.canceled = False def preFrameRender(self, **kwargs): for var, value in kwargs.items(): diff --git a/components/color.py b/components/color.py index ae818e2..c2a49e2 100644 --- a/components/color.py +++ b/components/color.py @@ -42,13 +42,17 @@ class Component(__base__.Component): self.x = self.page.spinBox_x.value() self.y = self.page.spinBox_y.value() self.parent.drawPreview() - + def previewRender(self, previewWorker): width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) return self.drawFrame(width, height) - - def frameRender(self, moduleNo, frameNo): + + 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) @@ -81,9 +85,3 @@ class Component(__base__.Component): else: self.page.lineEdit_color2.setText(RGBstring) self.page.pushButton_color2.setStyleSheet(btnStyle) - - def cancel(self): - self.canceled = True - - def reset(self): - self.canceled = False diff --git a/components/image.py b/components/image.py index 021bb9e..ffbb117 100644 --- a/components/image.py +++ b/components/image.py @@ -27,8 +27,12 @@ class Component(__base__.Component): width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) return self.drawFrame(width, height) - - def frameRender(self, moduleNo, frameNo): + + 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) @@ -49,12 +53,6 @@ class Component(__base__.Component): return { 'image' : self.imagePath, } - - def cancel(self): - self.canceled = True - - def reset(self): - self.canceled = False def pickImage(self): imgDir = self.settings.value("backgroundDir", os.path.expanduser("~")) diff --git a/components/original.py b/components/original.py index 6903a5f..b0a7579 100644 --- a/components/original.py +++ b/components/original.py @@ -58,6 +58,8 @@ class Component(__base__.Component): 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: @@ -72,12 +74,9 @@ class Component(__base__.Component): pStr = "Analyzing audio: "+ str(progress) +'%' self.progressBarSetText.emit(pStr) self.progressBarUpdate.emit(int(progress)) - - - def frameRender(self, moduleNo, frameNo): - width = int(self.worker.core.settings.value('outputWidth')) - height = int(self.worker.core.settings.value('outputHeight')) - return self.drawBars(width, height, self.spectrumArray[frameNo], self.visColor, self.layout) + + 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() @@ -154,12 +153,3 @@ class Component(__base__.Component): return im - def cancel(self): - self.canceled = True - - def reset(self): - self.canceled = False - - - - diff --git a/components/text.py b/components/text.py index d2e009d..2650935 100644 --- a/components/text.py +++ b/components/text.py @@ -104,7 +104,7 @@ class Component(__base__.Component): super().preFrameRender(**kwargs) return ['static'] - def frameRender(self, moduleNo, frameNo): + 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) @@ -137,9 +137,3 @@ class Component(__base__.Component): return self.page.lineEdit_textColor.setText(RGBstring) self.page.pushButton_textColor.setStyleSheet(btnStyle) - - def cancel(self): - self.canceled = True - - def reset(self): - self.canceled = False diff --git a/components/video.py b/components/video.py index 3d6ba7a..1a9f344 100644 --- a/components/video.py +++ b/components/video.py @@ -5,6 +5,10 @@ from . import __base__ class Component(__base__.Component): '''Video''' + def __init__(self): + super().__init__() + self.working = False + def widget(self, parent): self.parent = parent self.settings = parent.settings @@ -27,24 +31,35 @@ class Component(__base__.Component): self.width = int(previewWorker.core.settings.value('outputWidth')) self.height = int(previewWorker.core.settings.value('outputHeight')) frames = self.getVideoFrames(True) - if frames: - im = Image.open(frames[0]) - im = self.resize(im) - return im - else: - return Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0)) + if not hasattr(self, 'staticFrame') or not self.working and frames: + frame = Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0)) + if frames: + im = Image.open(frames[0]) + im = self.resize(im) + frame.paste(im) + if not self.working: + self.staticFrame = frame + return self.staticFrame def preFrameRender(self, **kwargs): - super().__init__(**kwargs) + super().preFrameRender(**kwargs) self.width = int(self.worker.core.settings.value('outputWidth')) self.height = int(self.worker.core.settings.value('outputHeight')) self.frames = self.getVideoFrames() + self.working = True - def frameRender(self, moduleNo, frameNo): - i = frameNo if frameNo < len(self.frames)-1 else len(self.frames)-1 - im = Image.open(self.frames[i]) - im = self.resize(im) - return im + def frameRender(self, moduleNo, arrayNo, frameNo): + print(frameNo) + try: + if frameNo < len(self.frames)-1: + self.staticFrame = Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0)) + im = Image.open(self.frames[frameNo]) + im = self.resize(im) + self.staticFrame.paste(im) + except FileNotFoundError: + print("Video component encountered an error") + self.frames = [] + return self.staticFrame def loadPreset(self, pr): self.page.lineEdit_video.setText(pr['video']) @@ -53,12 +68,6 @@ class Component(__base__.Component): return { 'video' : self.videoPath, } - - def cancel(self): - self.canceled = True - - def reset(self): - self.canceled = False def pickVideo(self): imgDir = self.settings.value("backgroundDir", os.path.expanduser("~")) @@ -69,19 +78,27 @@ class Component(__base__.Component): self.page.lineEdit_video.setText(filename) self.update() - def getVideoFrames(self, firstOnly=False): + def getVideoFrames(self, preview=False): # recreate the temporary directory so it is empty - # FIXME: don't dump too many frames at once + # FIXME: don't dump all the frames at once, don't dump more than sound length + # FIXME: make cancellable, report status to user, etc etc etc if not self.videoPath: return + name = os.path.basename(self.videoPath).split('.', 1)[0] + if preview: + filename = 'preview%s.jpg' % name + if os.path.exists(os.path.join(self.parent.core.tempDir, filename)): + return False + else: + filename = name+'-frame%05d.jpg' + + # recreate tempDir and dump needed frame(s) self.parent.core.deleteTempDir() os.mkdir(self.parent.core.tempDir) - if firstOnly: - filename = 'preview%s.jpg' % os.path.basename(self.videoPath).split('.', 1)[0] - options = '-ss 10 -vframes 1' + if preview: + options = '-ss 10 -vframes 1' else: - filename = '$frame%05d.jpg' - options = '' + options = '' #'-vframes 99999' subprocess.call( \ '%s -i "%s" -y %s "%s"' % ( \ self.parent.core.FFMPEG_BIN, @@ -91,6 +108,7 @@ class Component(__base__.Component): ), shell=True ) + print('### Got Preview Frame From %s ###' % name if preview else '### Finished Dumping Frames From %s ###' % name) return sorted([os.path.join(self.parent.core.tempDir, f) for f in os.listdir(self.parent.core.tempDir)]) def resize(self, im): diff --git a/core.py b/core.py index 16ecb35..ecbf12c 100644 --- a/core.py +++ b/core.py @@ -13,11 +13,8 @@ from collections import OrderedDict class Core(): def __init__(self): - self.lastBackgroundImage = "" - self._image = None - self.FFMPEG_BIN = self.findFfmpeg() - self.tempDir = None + self.tempDir = os.path.join(tempfile.gettempdir(), 'audio-visualizer-python-data') atexit.register(self.deleteTempDir) def findFfmpeg(self): @@ -31,31 +28,6 @@ class Core(): except: return "avconv" - def parseBaseImage(self, backgroundImage, preview=False): - ''' determines if the base image is a single frame or list of frames ''' - if backgroundImage == "": - return [''] - else: - _, bgExt = os.path.splitext(backgroundImage) - if not bgExt == '.mp4': - return [backgroundImage] - else: - return self.getVideoFrames(backgroundImage, preview) - - def drawBaseImage(self, backgroundFile): - if backgroundFile == '': - im = Image.new("RGB", (int(self.settings.value('outputWidth')), int(self.settings.value('outputHeight'))), "black") - else: - im = Image.open(backgroundFile) - - if self._image == None or not self.lastBackgroundImage == backgroundFile: - self.lastBackgroundImage = backgroundFile - # resize if necessary - if not im.size == (int(self.settings.value('outputWidth')), int(self.settings.value('outputHeight'))): - im = im.resize((int(self.settings.value('outputWidth')), int(self.settings.value('outputHeight'))), Image.ANTIALIAS) - - return im - def readAudioFile(self, filename, parent): command = [ self.FFMPEG_BIN, '-i', filename] @@ -121,30 +93,11 @@ class Core(): return completeAudioArray def deleteTempDir(self): - if self.tempDir and os.path.exists(self.tempDir): - rmtree(self.tempDir) - - def getVideoFrames(self, videoPath, firstOnly=False): - self.tempDir = os.path.join(tempfile.gettempdir(), 'audio-visualizer-python-data') - # recreate the temporary directory so it is empty - self.deleteTempDir() - os.mkdir(self.tempDir) - if firstOnly: - filename = 'preview%s.jpg' % os.path.basename(videoPath).split('.', 1)[0] - options = '-ss 10 -vframes 1' - else: - filename = '$frame%05d.jpg' - options = '' - sp.call( \ - '%s -i "%s" -y %s "%s"' % ( \ - self.FFMPEG_BIN, - videoPath, - options, - os.path.join(self.tempDir, filename) - ), - shell=True - ) - return sorted([os.path.join(self.tempDir, f) for f in os.listdir(self.tempDir)]) + try: + if os.path.exists(self.tempDir): + rmtree(self.tempDir) + except FileNotFoundError: + pass def cancel(self): self.canceled = True @@ -153,6 +106,6 @@ class Core(): self.canceled = False @staticmethod - def sortedStringDict(dictionary): + def stringOrderedDict(dictionary): sorted_ = OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])) return repr(sorted_) diff --git a/main.py b/main.py index 2aa7fa9..da72b1e 100644 --- a/main.py +++ b/main.py @@ -133,9 +133,9 @@ class PreviewWindow(QtGui.QLabel): class Main(QtCore.QObject): - newTask = QtCore.pyqtSignal(str, list) + newTask = QtCore.pyqtSignal(list) processTask = QtCore.pyqtSignal() - videoTask = QtCore.pyqtSignal(str, str, str, list) + videoTask = QtCore.pyqtSignal(str, str, list) def __init__(self, window): QtCore.QObject.__init__(self) @@ -172,14 +172,13 @@ class Main(QtCore.QObject): # begin decorating the window and connecting events window.toolButton_selectAudioFile.clicked.connect(self.openInputFileDialog) - window.toolButton_selectBackground.clicked.connect(self.openBackgroundFileDialog) 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) window.setWindowTitle("Audio Visualizer") - self.previewWindow = PreviewWindow(self, os.path.join(os.path.dirname(os.path.realpath(__file__)), "background.jpg")) + self.previewWindow = PreviewWindow(self, os.path.join(os.path.dirname(os.path.realpath(__file__)), "background.png")) window.verticalLayout_previewWrapper.addWidget(self.previewWindow) self.modules = self.findComponents() @@ -260,17 +259,6 @@ class Main(QtCore.QObject): self.settings.setValue("outputDir", os.path.dirname(fileName)) self.window.lineEdit_outputFile.setText(fileName) - def openBackgroundFileDialog(self): - backgroundDir = self.settings.value("backgroundDir", expanduser("~")) - - fileName = QtGui.QFileDialog.getOpenFileName(self.window, - "Open Background Image", backgroundDir, "Image Files (*.jpg *.png);; Video Files (*.mp4)"); - - if not fileName == "": - self.settings.setValue("backgroundDir", os.path.dirname(fileName)) - self.window.lineEdit_background.setText(fileName) - self.drawPreview() - def stopVideo(self): print('stop') self.videoWorker.cancel() @@ -291,8 +279,7 @@ class Main(QtCore.QObject): self.videoWorker.imageCreated.connect(self.showPreviewImage) self.videoWorker.encoding.connect(self.changeEncodingStatus) self.videoThread.start() - self.videoTask.emit(self.window.lineEdit_background.text(), - self.window.lineEdit_audioFile.text(), + self.videoTask.emit(self.window.lineEdit_audioFile.text(), self.window.lineEdit_outputFile.text(), self.selectedComponents) else: @@ -323,10 +310,6 @@ class Main(QtCore.QObject): self.window.pushButton_savePreset.setEnabled(False) self.window.pushButton_openProject.setEnabled(False) self.window.listWidget_componentList.setEnabled(False) - - self.window.label_background.setEnabled(False) - self.window.lineEdit_background.setEnabled(False) - self.window.toolButton_selectBackground.setEnabled(False) else: self.window.pushButton_createVideo.setEnabled(True) self.window.pushButton_Cancel.setEnabled(False) @@ -349,12 +332,6 @@ class Main(QtCore.QObject): self.window.pushButton_openProject.setEnabled(True) self.window.listWidget_componentList.setEnabled(True) - self.window.label_background.setEnabled(True) - self.window.lineEdit_background.setEnabled(True) - self.window.toolButton_selectBackground.setEnabled(True) - - - def progressBarSetText(self, value): self.window.progressBar_createVideo.setFormat(value) @@ -370,7 +347,7 @@ class Main(QtCore.QObject): self.drawPreview() def drawPreview(self): - self.newTask.emit(self.window.lineEdit_background.text(), self.selectedComponents) + self.newTask.emit(self.selectedComponents) # self.processTask.emit() self.autosave() @@ -381,7 +358,7 @@ class Main(QtCore.QObject): def findComponents(): srcPath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'components') if os.path.exists(srcPath): - for f in os.listdir(srcPath): + for f in sorted(os.listdir(srcPath)): name, ext = os.path.splitext(f) if name.startswith("__"): continue @@ -507,7 +484,7 @@ class Main(QtCore.QObject): if self.window.comboBox_openPreset.itemText(i) == filename: self.window.comboBox_openPreset.removeItem(i) with open(filepath, 'w') as f: - f.write(core.Core.sortedStringDict(saveValueStore)) + f.write(core.Core.stringOrderedDict(saveValueStore)) self.window.comboBox_openPreset.addItem(filename) self.window.comboBox_openPreset.setCurrentIndex(self.window.comboBox_openPreset.count()-1) @@ -550,12 +527,13 @@ class Main(QtCore.QObject): if not filepath.endswith(".avp"): filepath += '.avp' 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.Core.sortedStringDict(saveValueStore)) + f.write('%s\n' % core.Core.stringOrderedDict(saveValueStore)) if filepath != self.autosavePath: self.settings.setValue("projectDir", os.path.dirname(filepath)) self.settings.setValue("currentProject", filepath) @@ -604,14 +582,18 @@ class Main(QtCore.QObject): saveValueStore = dict(eval(line)) self.selectedComponents[-1].loadPreset(saveValueStore) i = 0 - except: + except (IndexError, ValueError, KeyError, NameError, SyntaxError, AttributeError, TypeError) as e: self.clear() - self.showMessage("Project file '%s' is corrupted." % filepath) + typ, value, _ = sys.exc_info() + msg = '%s: %s' % (typ.__name__, value) + self.showMessage("Project file '%s' is corrupted." % filepath, False, + QtGui.QMessageBox.Warning, msg) - def showMessage(self, string, showCancel=False, icon=QtGui.QMessageBox.Information): + def showMessage(self, string, showCancel=False, icon=QtGui.QMessageBox.Information, detail=None): msg = QtGui.QMessageBox() msg.setIcon(icon) msg.setText(string) + msg.setDetailedText(detail) if showCancel: msg.setStandardButtons(QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel) else: diff --git a/mainwindow.ui b/mainwindow.ui index f9e8f5e..b703997 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -6,8 +6,8 @@ 0 0 - 1008 - 575 + 1028 + 592 @@ -421,73 +421,6 @@ - - - - - - - 0 - 0 - - - - - 85 - 0 - - - - Background - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - 0 - 28 - - - - - 16777215 - 28 - - - - - - - - - 0 - 28 - - - - - 16777215 - 28 - - - - ... - - - - - @@ -621,7 +554,6 @@ - diff --git a/preview_thread.py b/preview_thread.py index 63d1ac5..5116707 100644 --- a/preview_thread.py +++ b/preview_thread.py @@ -22,10 +22,9 @@ class Worker(QtCore.QObject): @pyqtSlot(str, list) - def createPreviewImage(self, backgroundImage, components): + def createPreviewImage(self, components): # print('worker thread id: {}'.format(QtCore.QThread.currentThreadId())) dic = { - "backgroundImage": backgroundImage, "components": components, } self.queue.put(dic) @@ -40,25 +39,14 @@ class Worker(QtCore.QObject): except Empty: continue - bgImage = self.core.parseBaseImage(\ - nextPreviewInformation["backgroundImage"], - preview=True - ) - if bgImage == []: - bgImage = '' - else: - bgImage = bgImage[0] - - im = self.core.drawBaseImage(bgImage) width = int(self.core.settings.value('outputWidth')) height = int(self.core.settings.value('outputHeight')) - frame = Image.new("RGBA", (width, height),(0,0,0,255)) - frame.paste(im) + frame = Image.new("RGBA", (width, height),(0,0,0,0)) components = nextPreviewInformation["components"] for component in reversed(components): - newFrame = Image.alpha_composite(frame,component.previewRender(self)) - frame = Image.alpha_composite(frame,newFrame) + #newFrame = Image.alpha_composite(frame,) + frame = Image.alpha_composite(frame,component.previewRender(self)) self._image = ImageQt(frame) self.imageCreated.emit(QtGui.QImage(self._image)) diff --git a/video_thread.py b/video_thread.py index c97cc24..e74fffa 100644 --- a/video_thread.py +++ b/video_thread.py @@ -38,16 +38,17 @@ class Worker(QtCore.QObject): while not self.stopped: i = self.compositeQueue.get() - if self.imBackground is not None: - frame = self.imBackground - else: - frame = self.getBackgroundAtIndex(i[1]) + frame = Image.new( + "RGBA", + (self.width, self.height), + (0, 0, 0, 0) + ) for compNo, comp in reversed(list(enumerate(self.components))): if compNo in self.staticComponents and self.staticComponents[compNo] != None: frame = Image.alpha_composite(frame, self.staticComponents[compNo]) else: - frame = Image.alpha_composite(frame, comp.frameRender(compNo, i[0])) + frame = Image.alpha_composite(frame, comp.frameRender(compNo, i[0], i[1])) # frame.paste(compFrame, mask=compFrame) @@ -59,10 +60,8 @@ class Worker(QtCore.QObject): for i in range(0, len(self.completeAudioArray), self.sampleSize): self.compositeQueue.put([i, self.bgI]) - if not self.imBackground: - # increment background video frame for next iteration - if self.bgI < len(self.backgroundFrames)-1: - self.bgI += 1 + # increment tracked video frame for next iteration + self.bgI += 1 def previewDispatch(self): while not self.stopped: @@ -74,39 +73,18 @@ class Worker(QtCore.QObject): self.previewQueue.task_done() - def getBackgroundAtIndex(self, i): - background = Image.new( - "RGBA", - (self.width, self.height), - (0, 0, 0, 255) - ) - layer = self.core.drawBaseImage(self.backgroundFrames[i]) - background.paste(layer) - return background - - @pyqtSlot(str, str, str, list) - def createVideo(self, backgroundImage, inputFile, outputFile, components): + @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')) # print('worker thread id: {}'.format(QtCore.QThread.currentThreadId())) progressBarValue = 0 self.progressBarUpdate.emit(progressBarValue) - self.progressBarSetText.emit('Loading background image…') - - self.backgroundImage = backgroundImage - - self.backgroundFrames = self.core.parseBaseImage(backgroundImage) - if len(self.backgroundFrames) < 2: - # the base image is not a video so we can draw it now - self.imBackground = self.getBackgroundAtIndex(0) - else: - # base images will be drawn while drawing the audio bars - self.imBackground = None - self.bgI = 0 self.progressBarSetText.emit('Loading audio file...') self.completeAudioArray = self.core.readAudioFile(inputFile, self) @@ -165,7 +143,7 @@ class Worker(QtCore.QObject): ) if properties and 'static' in properties: - self.staticComponents[compNo] = copy(comp.frameRender(compNo, 0)) + self.staticComponents[compNo] = copy(comp.frameRender(compNo, 0, 0)) self.progressBarUpdate.emit(100) self.compositeQueue = Queue() -- cgit v1.2.3 From 277e86f2795093deaa12f294c29ac2f951ae65cd Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 4 Jun 2017 20:27:43 -0400 Subject: not dumping frames anymore, but not working yet either will finish later --- components/video.py | 69 +++++++++++++++++++++++++++++++++-------------------- core.py | 5 ++-- main.py | 2 +- video_thread.py | 1 - 4 files changed, 47 insertions(+), 30 deletions(-) (limited to 'main.py') diff --git a/components/video.py b/components/video.py index 1a9f344..97b824c 100644 --- a/components/video.py +++ b/components/video.py @@ -30,11 +30,11 @@ class Component(__base__.Component): def previewRender(self, previewWorker): self.width = int(previewWorker.core.settings.value('outputWidth')) self.height = int(previewWorker.core.settings.value('outputHeight')) - frames = self.getVideoFrames(True) - if not hasattr(self, 'staticFrame') or not self.working and frames: + frame1 = self.getPreviewFrame() + if not hasattr(self, 'staticFrame') or not self.working and frame1: frame = Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0)) - if frames: - im = Image.open(frames[0]) + if frame1: + im = Image.open(frame1) im = self.resize(im) frame.paste(im) if not self.working: @@ -77,39 +77,56 @@ class Component(__base__.Component): self.settings.setValue("backgroundDir", os.path.dirname(filename)) self.page.lineEdit_video.setText(filename) self.update() - - def getVideoFrames(self, preview=False): - # recreate the temporary directory so it is empty - # FIXME: don't dump all the frames at once, don't dump more than sound length - # FIXME: make cancellable, report status to user, etc etc etc + + def getPreviewFrame(self): if not self.videoPath: return name = os.path.basename(self.videoPath).split('.', 1)[0] - if preview: - filename = 'preview%s.jpg' % name - if os.path.exists(os.path.join(self.parent.core.tempDir, filename)): - return False - else: - filename = name+'-frame%05d.jpg' - - # recreate tempDir and dump needed frame(s) - self.parent.core.deleteTempDir() - os.mkdir(self.parent.core.tempDir) - if preview: - options = '-ss 10 -vframes 1' - else: - options = '' #'-vframes 99999' + filename = 'preview%s.jpg' % name + if os.path.exists(os.path.join(self.parent.core.tempDir, filename)): + # no, we don't need a new preview frame + return False + + # get a preview frame subprocess.call( \ '%s -i "%s" -y %s "%s"' % ( \ self.parent.core.FFMPEG_BIN, self.videoPath, - options, + '-ss 10 -vframes 1', os.path.join(self.parent.core.tempDir, filename) ), shell=True ) - print('### Got Preview Frame From %s ###' % name if preview else '### Finished Dumping Frames From %s ###' % name) - return sorted([os.path.join(self.parent.core.tempDir, f) for f in os.listdir(self.parent.core.tempDir)]) + print('### Got Preview Frame From %s ###' % name) + return os.path.join(self.parent.core.tempDir, filename) + + def getVideoFrames(self): + # FIXME: make cancellable, report status to user, etc etc etc + if not self.videoPath: + return + + command = [ + self.parent.core.FFMPEG_BIN, + '-i', self.videoPath, + '-f', 'image2pipe', + '-vcodec', 'rawvideo', '-', + '-pix_fmt', 'rgba', + ] + + # pipe in video frames from ffmpeg + in_pipe = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=8**10) + # maybe bufsize=4*self.width*self.height+100 ? + chunk = 4*self.width*self.height + + frames = [] + while True: + byteFrame = in_pipe.stdout.read(chunk) + if len(byteFrame) == 0: + break + img = Image.frombytes('RGBA', (self.width, self.height), byteFrame, 'raw', 'RGBa') + frames.append(img) + + return frames def resize(self, im): if im.size != (self.width, self.height): diff --git a/core.py b/core.py index ecbf12c..99403f1 100644 --- a/core.py +++ b/core.py @@ -15,6 +15,8 @@ class Core(): def __init__(self): self.FFMPEG_BIN = self.findFfmpeg() self.tempDir = os.path.join(tempfile.gettempdir(), 'audio-visualizer-python-data') + if not os.path.exists(self.tempDir): + os.makedirs(self.tempDir) atexit.register(self.deleteTempDir) def findFfmpeg(self): @@ -94,8 +96,7 @@ class Core(): def deleteTempDir(self): try: - if os.path.exists(self.tempDir): - rmtree(self.tempDir) + rmtree(self.tempDir) except FileNotFoundError: pass diff --git a/main.py b/main.py index da72b1e..637ece8 100644 --- a/main.py +++ b/main.py @@ -253,7 +253,7 @@ class Main(QtCore.QObject): outputDir = self.settings.value("outputDir", expanduser("~")) fileName = QtGui.QFileDialog.getSaveFileName(self.window, - "Set Output Video File", outputDir, "Video Files (*.mkv)"); + "Set Output Video File", outputDir, "Video Files (*.mkv *.mp4)"); if not fileName == "": self.settings.setValue("outputDir", os.path.dirname(fileName)) diff --git a/video_thread.py b/video_thread.py index e74fffa..0542bc2 100644 --- a/video_thread.py +++ b/video_thread.py @@ -228,7 +228,6 @@ class Worker(QtCore.QObject): self.error = False self.canceled = False self.parent.drawPreview() - self.core.deleteTempDir() self.stopped = True self.encoding.emit(False) self.videoCreated.emit() -- cgit v1.2.3 From be18deece5843ac8d2c7af64704e3fb360a05a25 Mon Sep 17 00:00:00 2001 From: DH4 Date: Mon, 5 Jun 2017 04:54:58 -0500 Subject: Performance Tuning. FIXME: Video component frames are rendered out of order. Video component creates a severe performance bottleneck. --- components/video.py | 21 ++++++++++----------- main.py | 4 ++-- video_thread.py | 20 ++++++++++---------- 3 files changed, 22 insertions(+), 23 deletions(-) (limited to 'main.py') diff --git a/components/video.py b/components/video.py index b009baa..422b952 100644 --- a/components/video.py +++ b/components/video.py @@ -36,8 +36,6 @@ class Component(__base__.Component): frame = Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0)) if frame1: im = Image.open(frame1) - self.realSize = im.size - im = self.resize(im) frame.paste(im) if not self.working: self.staticFrame = frame @@ -61,11 +59,9 @@ class Component(__base__.Component): return self.staticFrame # make a new frame - width, height = self.realSize - image = numpy.fromstring(byteFrame, dtype='uint8') - image = image.reshape((width, height, 4)) - image = Image.frombytes('RGBA', (width, height), image, 'raw', 'RGBa') - image = self.resize(image) + width = self.width + height = self.height + image = Image.frombytes('RGBA', (width, height), byteFrame) self.staticFrame = image return self.staticFrame @@ -80,7 +76,7 @@ class Component(__base__.Component): def pickVideo(self): imgDir = self.settings.value("backgroundDir", os.path.expanduser("~")) filename = QtGui.QFileDialog.getOpenFileName(self.page, - "Choose Video", imgDir, "Video Files (*.mp4)") + "Choose Video", imgDir, "Video Files (*.mp4 *.mov)") if filename: self.settings.setValue("backgroundDir", os.path.dirname(filename)) self.page.lineEdit_video.setText(filename) @@ -97,10 +93,11 @@ class Component(__base__.Component): # get a preview frame subprocess.call( \ - '%s -i "%s" -y %s "%s"' % ( \ + '%s -i "%s" -y %s %s "%s"' % ( \ self.parent.core.FFMPEG_BIN, self.videoPath, '-ss 10 -vframes 1', + '-filter:v scale='+str(self.width)+':'+str(self.height), os.path.join(self.parent.core.tempDir, filename) ), shell=True @@ -114,16 +111,18 @@ class Component(__base__.Component): command = [ self.parent.core.FFMPEG_BIN, + '-thread_queue_size', '512', '-i', self.videoPath, '-f', 'image2pipe', '-pix_fmt', 'rgba', + '-filter:v', 'scale='+str(self.width)+':'+str(self.height), '-vcodec', 'rawvideo', '-', ] # pipe in video frames from ffmpeg in_pipe = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8) - width, height = self.realSize - self.chunkSize = 4*width*height + #width, height = self.realSize + self.chunkSize = 4*self.width*self.height return in_pipe diff --git a/main.py b/main.py index 637ece8..36fc989 100644 --- a/main.py +++ b/main.py @@ -243,7 +243,7 @@ class Main(QtCore.QObject): inputDir = self.settings.value("inputDir", expanduser("~")) fileName = QtGui.QFileDialog.getOpenFileName(self.window, - "Open Music File", inputDir, "Music Files (*.mp3 *.wav *.ogg *.flac)"); + "Open Music File", inputDir, "Music Files (*.mp3 *.wav *.ogg *.fla *.aac)"); if not fileName == "": self.settings.setValue("inputDir", os.path.dirname(fileName)) @@ -253,7 +253,7 @@ class Main(QtCore.QObject): outputDir = self.settings.value("outputDir", expanduser("~")) fileName = QtGui.QFileDialog.getSaveFileName(self.window, - "Set Output Video File", outputDir, "Video Files (*.mkv *.mp4)"); + "Set Output Video File", outputDir, "Video Files (*.mp4 *.mov *.mkv *.avi *.webm *.flv)"); if not fileName == "": self.settings.setValue("outputDir", os.path.dirname(fileName)) diff --git a/video_thread.py b/video_thread.py index 0542bc2..ac4162c 100644 --- a/video_thread.py +++ b/video_thread.py @@ -37,20 +37,19 @@ class Worker(QtCore.QObject): def renderNode(self): while not self.stopped: i = self.compositeQueue.get() - - frame = Image.new( - "RGBA", - (self.width, self.height), - (0, 0, 0, 0) - ) + frame = None for compNo, comp in reversed(list(enumerate(self.components))): if compNo in self.staticComponents and self.staticComponents[compNo] != None: - frame = Image.alpha_composite(frame, self.staticComponents[compNo]) + if frame is None: + frame = self.staticComponents[compNo] + else: + frame = Image.alpha_composite(frame, self.staticComponents[compNo]) else: - frame = Image.alpha_composite(frame, comp.frameRender(compNo, i[0], i[1])) - - # frame.paste(compFrame, mask=compFrame) + 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() @@ -98,6 +97,7 @@ class Worker(QtCore.QObject): ffmpegCommand = [ self.core.FFMPEG_BIN, + '-thread_queue_size', '512', '-y', # (optional) means overwrite the output file if it already exists. '-f', 'rawvideo', '-vcodec', 'rawvideo', -- cgit v1.2.3 From 47509ae2b19e5a05211ae06114ba4675aa60a793 Mon Sep 17 00:00:00 2001 From: tassaron Date: Tue, 6 Jun 2017 01:40:26 -0400 Subject: added framebuffer to keep frames in order NOT WORKING: end of video detection --- components/video.py | 144 +++++++++++++++++++++++++--------------------------- main.py | 13 +++-- 2 files changed, 76 insertions(+), 81 deletions(-) (limited to 'main.py') diff --git a/components/video.py b/components/video.py index 422b952..0ae5348 100644 --- a/components/video.py +++ b/components/video.py @@ -1,15 +1,56 @@ from PIL import Image, ImageDraw from PyQt4 import uic, QtGui, QtCore -import os, subprocess -import numpy +import os, subprocess, threading +from queue import PriorityQueue from . import __base__ +class Video: + '''Video Component Frame-Fetcher''' + def __init__(self, ffmpeg, videoPath, width, height, frameRate, chunkSize, parent): + self.parent = parent + self.chunkSize = chunkSize + self.size = (width, height) + self.frameNo = -1 + self.command = [ + ffmpeg, + '-thread_queue_size', '512', + '-r', frameRate, + '-i', videoPath, + '-f', 'image2pipe', + '-pix_fmt', 'rgba', + '-filter:v', 'scale='+str(width)+':'+str(height), + '-vcodec', 'rawvideo', '-', + ] + + self.frameBuffer = PriorityQueue() + self.frameBuffer.maxsize = int(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 Image.frombytes('RGBA', self.size, image) + i, image = self.frameBuffer.get() + self.finishedFrames[i] = image + self.frameBuffer.task_done() + + def fillBuffer(self): + 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 + image = self.pipe.stdout.read(self.chunkSize) + print('creating frame #%s' % str(self.frameNo)) + self.frameBuffer.put((self.frameNo, image)) + class Component(__base__.Component): '''Video''' - def __init__(self): - super().__init__() - self.working = False - def widget(self, parent): self.parent = parent self.settings = parent.settings @@ -29,41 +70,22 @@ class Component(__base__.Component): self.parent.drawPreview() def previewRender(self, previewWorker): - self.width = int(previewWorker.core.settings.value('outputWidth')) - self.height = int(previewWorker.core.settings.value('outputHeight')) - frame1 = self.getPreviewFrame() - if not hasattr(self, 'staticFrame') or not self.working and frame1: - frame = Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0)) - if frame1: - im = Image.open(frame1) - frame.paste(im) - if not self.working: - self.staticFrame = frame - return self.staticFrame + width = int(previewWorker.core.settings.value('outputWidth')) + height = int(previewWorker.core.settings.value('outputHeight')) + self.chunkSize = 4*width*height + return self.getPreviewFrame(width, height) def preFrameRender(self, **kwargs): super().preFrameRender(**kwargs) - self.width = int(self.worker.core.settings.value('outputWidth')) - self.height = int(self.worker.core.settings.value('outputHeight')) - self.working = True - self.frames = self.getVideoFrames() + width = int(self.worker.core.settings.value('outputWidth')) + height = int(self.worker.core.settings.value('outputHeight')) + self.chunkSize = 4*width*height + self.video = Video(self.parent.core.FFMPEG_BIN, self.videoPath, + width, height, self.settings.value("outputFrameRate"), + self.chunkSize, self.parent) def frameRender(self, moduleNo, arrayNo, frameNo): - # don't make a new frame - if not self.working: - return self.staticFrame - byteFrame = self.frames.stdout.read(self.chunkSize) - if len(byteFrame) == 0: - self.working = False - self.frames.kill() - return self.staticFrame - - # make a new frame - width = self.width - height = self.height - image = Image.frombytes('RGBA', (width, height), byteFrame) - self.staticFrame = image - return self.staticFrame + return self.video.frame(frameNo) def loadPreset(self, pr): self.page.lineEdit_video.setText(pr['video']) @@ -82,51 +104,21 @@ class Component(__base__.Component): self.page.lineEdit_video.setText(filename) self.update() - def getPreviewFrame(self): - if not self.videoPath: - return - name = os.path.basename(self.videoPath).split('.', 1)[0] - filename = 'preview%s.jpg' % name - if os.path.exists(os.path.join(self.parent.core.tempDir, filename)): - # no, we don't need a new preview frame - return False - - # get a preview frame - subprocess.call( \ - '%s -i "%s" -y %s %s "%s"' % ( \ - self.parent.core.FFMPEG_BIN, - self.videoPath, - '-ss 10 -vframes 1', - '-filter:v scale='+str(self.width)+':'+str(self.height), - os.path.join(self.parent.core.tempDir, filename) - ), - shell=True - ) - print('### Got Preview Frame From %s ###' % name) - return os.path.join(self.parent.core.tempDir, filename) - - def getVideoFrames(self): - if not self.videoPath: - return - + def getPreviewFrame(self, width, height): command = [ self.parent.core.FFMPEG_BIN, '-thread_queue_size', '512', '-i', self.videoPath, '-f', 'image2pipe', '-pix_fmt', 'rgba', - '-filter:v', 'scale='+str(self.width)+':'+str(self.height), + '-filter:v', 'scale='+str(width)+':'+str(height), '-vcodec', 'rawvideo', '-', + '-ss', '90', + '-vframes', '1', ] - - # pipe in video frames from ffmpeg - in_pipe = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8) - #width, height = self.realSize - self.chunkSize = 4*self.width*self.height - - return in_pipe - - def resize(self, im): - if im.size != (self.width, self.height): - im = im.resize((self.width, self.height), Image.ANTIALIAS) - return im + pipe = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8) + byteFrame = pipe.stdout.read(self.chunkSize) + image = Image.frombytes('RGBA', (width, height), byteFrame) + pipe.stdout.close() + pipe.kill() + return image diff --git a/main.py b/main.py index 36fc989..c75a7f7 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,4 @@ -import sys, io, os, shutil, atexit, string, signal, filecmp +import sys, io, os, shutil, atexit, string, signal, filecmp, time from os.path import expanduser from queue import Queue from importlib import import_module @@ -145,6 +145,7 @@ class Main(QtCore.QObject): self.core = core.Core() self.pages = [] self.selectedComponents = [] + self.lastAutosave = time.time() # create data directory, load/create settings self.dataDir = QDesktopServices.storageLocation(QDesktopServices.DataLocation) @@ -235,9 +236,11 @@ class Main(QtCore.QObject): self.autosave() def autosave(self): - if os.path.exists(self.autosavePath): - os.remove(self.autosavePath) - self.createProjectFile(self.autosavePath) + if time.time() - self.lastAutosave >= 1.0: + if os.path.exists(self.autosavePath): + os.remove(self.autosavePath) + self.createProjectFile(self.autosavePath) + self.lastAutosave = time.time() def openInputFileDialog(self): inputDir = self.settings.value("inputDir", expanduser("~")) @@ -423,7 +426,7 @@ class Main(QtCore.QObject): def moveComponentDown(self): row = self.window.listWidget_componentList.currentRow() - if row < len(self.pages) + 1: + if row != -1 and row < len(self.pages)+1: module = self.selectedComponents[row] self.selectedComponents.pop(row) self.selectedComponents.insert(row + 1,module) -- cgit v1.2.3 From 231af74ea2b247bd73fcdfc44657b7fea2ab1620 Mon Sep 17 00:00:00 2001 From: DH4 Date: Tue, 6 Jun 2017 10:14:39 -0500 Subject: Code cleanup --- command.py | 122 +++++++++ components/__base__.py | 44 ++-- components/color.py | 55 ++-- components/image.py | 24 +- components/original.py | 78 ++++-- components/text.py | 23 +- components/video.py | 74 ++++-- core.py | 203 ++++++++------- main.py | 693 ++++--------------------------------------------- mainwindow.py | 586 +++++++++++++++++++++++++++++++++++++++++ preview_thread.py | 91 ++++--- video_thread.py | 68 +++-- 12 files changed, 1126 insertions(+), 935 deletions(-) create mode 100644 command.py create mode 100644 mainwindow.py (limited to 'main.py') diff --git a/command.py b/command.py new file mode 100644 index 0000000..a610d8c --- /dev/null +++ b/command.py @@ -0,0 +1,122 @@ +# FIXME: commandline functionality broken until we decide how to implement it +''' +class Command(QtCore.QObject): + + videoTask = QtCore.pyqtSignal(str, str, str, list) + + def __init__(self): + QtCore.QObject.__init__(self) + self.modules = [] + self.selectedComponents = [] + + import argparse + self.parser = argparse.ArgumentParser( + description='Create a visualization for an audio file') + self.parser.add_argument( + '-i', '--input', dest='input', help='input audio file', required=True) + self.parser.add_argument( + '-o', '--output', dest='output', + help='output video file', required=True) + self.parser.add_argument( + '-b', '--background', dest='bgimage', + help='background image file', required=True) + self.parser.add_argument( + '-t', '--text', dest='text', help='title text', required=True) + self.parser.add_argument( + '-f', '--font', dest='font', help='title font', required=False) + self.parser.add_argument( + '-s', '--fontsize', dest='fontsize', + help='title font size', required=False) + self.parser.add_argument( + '-c', '--textcolor', dest='textcolor', + help='title text color in r,g,b format', required=False) + self.parser.add_argument( + '-C', '--viscolor', dest='viscolor', + help='visualization color in r,g,b format', required=False) + self.parser.add_argument( + '-x', '--xposition', dest='xposition', + help='x position', required=False) + self.parser.add_argument( + '-y', '--yposition', dest='yposition', + help='y position', required=False) + self.parser.add_argument( + '-a', '--alignment', dest='alignment', + help='title alignment', required=False, + type=int, choices=[0, 1, 2]) + self.args = self.parser.parse_args() + + self.settings = QSettings('settings.ini', QSettings.IniFormat) + LoadDefaultSettings(self) + + # load colours as tuples from comma-separated strings + self.textColor = core.Core.RGBFromString( + self.settings.value("textColor", '255, 255, 255')) + self.visColor = core.Core.RGBFromString( + self.settings.value("visColor", '255, 255, 255')) + if self.args.textcolor: + self.textColor = core.Core.RGBFromString(self.args.textcolor) + if self.args.viscolor: + self.visColor = core.Core.RGBFromString(self.args.viscolor) + + # font settings + if self.args.font: + self.font = QFont(self.args.font) + else: + self.font = QFont(self.settings.value("titleFont", QFont())) + + if self.args.fontsize: + self.fontsize = int(self.args.fontsize) + else: + self.fontsize = int(self.settings.value("fontSize", 35)) + if self.args.alignment: + self.alignment = int(self.args.alignment) + else: + self.alignment = int(self.settings.value("alignment", 0)) + + if self.args.xposition: + self.textX = int(self.args.xposition) + else: + self.textX = int(self.settings.value("xPosition", 70)) + + if self.args.yposition: + self.textY = int(self.args.yposition) + else: + self.textY = int(self.settings.value("yPosition", 375)) + + ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~")) + + 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.bgimage, + self.args.text, + self.font, + self.fontsize, + self.alignment, + self.textX, + self.textY, + self.textColor, + self.visColor, + self.args.input, + self.args.output, + self.selectedComponents) + + def videoCreated(self): + self.videoThread.quit() + self.videoThread.wait() + self.cleanUp() + + def cleanUp(self): + self.settings.setValue("titleFont", self.font.toString()) + self.settings.setValue("alignment", str(self.alignment)) + self.settings.setValue("fontSize", str(self.fontsize)) + self.settings.setValue("xPosition", str(self.textX)) + self.settings.setValue("yPosition", str(self.textY)) + self.settings.setValue("visColor", '%s,%s,%s' % self.visColor) + self.settings.setValue("textColor", '%s,%s,%s' % self.textColor) + sys.exit(0) +''' diff --git a/components/__base__.py b/components/__base__.py index f564aad..94ac6f2 100644 --- a/components/__base__.py +++ b/components/__base__.py @@ -1,5 +1,6 @@ from PyQt4 import QtGui + class Component: def __str__(self): return self.__doc__ @@ -14,7 +15,7 @@ class Component: def reset(self): self.canceled = False - + def preFrameRender(self, **kwargs): for var, value in kwargs.items(): exec('self.%s = value' % var) @@ -22,32 +23,35 @@ class Component: def pickColor(self): color = QtGui.QColorDialog.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() + 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) + ''' 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')) + page = uic.loadUi(os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'example.ui')) # connect widgets signals self.page = page return page @@ -55,13 +59,13 @@ class Component: def update(self): # read widget values 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')) @@ -70,10 +74,10 @@ class Component: def loadPreset(self, presetDict): # update widgets using a preset dict - + def savePreset(self): return {} - + def cancel(self): self.canceled = True diff --git a/components/color.py b/components/color.py index c2a49e2..b050fbd 100644 --- a/components/color.py +++ b/components/color.py @@ -4,31 +4,39 @@ from PyQt4.QtGui import QColor import os from . import __base__ + class Component(__base__.Component): '''Color''' 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) + 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) - page.pushButton_color1.clicked.connect(lambda: self.pickColor(1)) - btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*self.color1).name() + + btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + % QColor(*self.color1).name() + + btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + % QColor(*self.color2).name() + page.pushButton_color1.setStyleSheet(btnStyle) - page.pushButton_color2.clicked.connect(lambda: self.pickColor(2)) - btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*self.color2).name() page.pushButton_color2.setStyleSheet(btnStyle) + 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.setValue(self.x) page.spinBox_x.setValue(self.y) - + page.lineEdit_color1.textChanged.connect(self.update) page.lineEdit_color2.textChanged.connect(self.update) page.spinBox_x.valueChanged.connect(self.update) @@ -42,39 +50,44 @@ class Component(__base__.Component): self.x = self.page.spinBox_x.value() self.y = self.page.spinBox_y.value() self.parent.drawPreview() - + 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 + r, g, b = self.color1 return Image.new("RGBA", (width, height), (r, g, b, 255)) def loadPreset(self, pr): self.page.lineEdit_color1.setText('%s,%s,%s' % pr['color1']) self.page.lineEdit_color2.setText('%s,%s,%s' % pr['color2']) - btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*pr['color1']).name() + + btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + % QColor(*pr['color1']).name() + + btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + % QColor(*pr['color2']).name() + self.page.pushButton_color1.setStyleSheet(btnStyle) - btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*pr['color2']).name() self.page.pushButton_color2.setStyleSheet(btnStyle) - + def savePreset(self): return { - 'color1' : self.color1, - 'color2' : self.color2, + 'color1': self.color1, + 'color2': self.color2, } - + def pickColor(self, num): RGBstring, btnStyle = super().pickColor() if not RGBstring: diff --git a/components/image.py b/components/image.py index ffbb117..f9a92ca 100644 --- a/components/image.py +++ b/components/image.py @@ -3,26 +3,28 @@ from PyQt4 import uic, QtGui, QtCore import os from . import __base__ + class Component(__base__.Component): '''Image''' 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')) + 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) - + self.page = page return page def update(self): self.imagePath = self.page.lineEdit_image.text() self.parent.drawPreview() - + def previewRender(self, previewWorker): width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) @@ -36,9 +38,9 @@ class Component(__base__.Component): 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 = Image.new("RGBA", (width, height), (0,0,0,0)) + frame = Image.new("RGBA", (width, height), (0, 0, 0, 0)) if self.imagePath and os.path.exists(self.imagePath): image = Image.open(self.imagePath) if image.size != (width, height): @@ -48,16 +50,16 @@ class Component(__base__.Component): def loadPreset(self, pr): self.page.lineEdit_image.setText(pr['image']) - + def savePreset(self): return { - 'image' : self.imagePath, + 'image': self.imagePath, } - + def pickImage(self): imgDir = self.settings.value("backgroundDir", os.path.expanduser("~")) - filename = QtGui.QFileDialog.getOpenFileName(self.page, - "Choose Image", imgDir, "Image Files (*.jpg *.png)") + filename = QtGui.QFileDialog.getOpenFileName( + self.page, "Choose Image", imgDir, "Image Files (*.jpg *.png)") if filename: self.settings.setValue("backgroundDir", os.path.dirname(filename)) self.page.lineEdit_image.setText(filename) diff --git a/components/original.py b/components/original.py index b0a7579..4d0e83b 100644 --- a/components/original.py +++ b/components/original.py @@ -2,7 +2,8 @@ import numpy from PIL import Image, ImageDraw from PyQt4 import uic, QtGui from PyQt4.QtGui import QColor -import os, random +import os +import random from . import __base__ import time from copy import copy @@ -12,24 +13,25 @@ class Component(__base__.Component): '''Original Audio Visualization''' def widget(self, parent): self.parent = parent - self.visColor = (255,255,255) + self.visColor = (255, 255, 255) - page = uic.loadUi(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'original.ui')) + 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") - #visLayoutValue = int(self.settings.value('visLayout')) 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() + 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()) @@ -37,21 +39,25 @@ class Component(__base__.Component): def loadPreset(self, pr): self.page.lineEdit_visColor.setText('%s,%s,%s' % pr['visColor']) - btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*pr['visColor']).name() + 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 { 'layout' : self.layout, - 'visColor' : self.visColor, - } + return { + 'layout': self.layout, + 'visColor': self.visColor, + } def previewRender(self, previewWorker): - spectrum = numpy.fromfunction(lambda x: 0.008*(x-128)**2, (255,), dtype="int16") + 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) - + return self.drawBars( + width, height, spectrum, self.visColor, self.layout) + def preFrameRender(self, **kwargs): super().preFrameRender(**kwargs) self.smoothConstantDown = 0.08 @@ -64,19 +70,24 @@ class Component(__base__.Component): 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.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) +'%' + 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) + return self.drawBars( + self.width, self.height, + self.spectrumArray[arrayNo], + self.visColor, self.layout) def pickColor(self): RGBstring, btnStyle = super().pickColor() @@ -85,14 +96,17 @@ class Component(__base__.Component): self.page.lineEdit_visColor.setText(RGBstring) self.page.pushButton_visColor.setStyleSheet(btnStyle) - def transformData(self, i, completeAudioArray, sampleSize, smoothConstantDown, smoothConstantUp, lastSpectrum): + 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') + 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) @@ -106,8 +120,13 @@ class Component(__base__.Component): 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) + 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 @@ -120,7 +139,7 @@ class Component(__base__.Component): bF = width / 64 bH = bF / 2 bQ = bF / 4 - imTop = Image.new("RGBA", (width, height),(0,0,0,0)) + imTop = Image.new("RGBA", (width, height), (0, 0, 0, 0)) draw = ImageDraw.Draw(imTop) r, g, b = color color2 = (r, g, b, 125) @@ -128,12 +147,17 @@ class Component(__base__.Component): 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) + 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 = Image.new("RGBA", (width, height),(0,0,0,0)) + im = Image.new("RGBA", (width, height), (0, 0, 0, 0)) if layout == 0: y = 0 - int(height/100*43) diff --git a/components/text.py b/components/text.py index 56a9502..6cdc0dd 100644 --- a/components/text.py +++ b/components/text.py @@ -25,9 +25,7 @@ class Component(__base__.Component): self.yPosition = height / 2 * 1.036 page = uic.loadUi(os.path.join( - os.path.dirname(os.path.realpath(__file__)), - 'text.ui' - )) + os.path.dirname(os.path.realpath(__file__)), 'text.ui')) page.comboBox_textAlign.addItem("Left") page.comboBox_textAlign.addItem("Middle") page.comboBox_textAlign.addItem("Right") @@ -61,7 +59,8 @@ class Component(__base__.Component): 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.textColor = self.RGBFromString( + self.page.lineEdit_textColor.text()) self.parent.drawPreview() def getXY(self): @@ -95,14 +94,14 @@ class Component(__base__.Component): def savePreset(self): return { - 'title': self.title, - 'titleFont': self.titleFont.toString(), - 'alignment': self.alignment, - 'fontSize': self.fontSize, - 'xPosition': self.xPosition, - 'yPosition': self.yPosition, - 'textColor': self.textColor - } + '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')) diff --git a/components/video.py b/components/video.py index de91264..67a96dd 100644 --- a/components/video.py +++ b/components/video.py @@ -1,12 +1,18 @@ from PIL import Image, ImageDraw from PyQt4 import uic, QtGui, QtCore -import os, subprocess, threading +import os +import subprocess +import threading from queue import PriorityQueue from . import __base__ + class Video: '''Video Component Frame-Fetcher''' - def __init__(self, ffmpeg, videoPath, width, height, frameRate, chunkSize, parent, loopVideo): + def __init__( + self, ffmpeg, videoPath, width, height, + frameRate, chunkSize, parent, loopVideo): + self.parent = parent self.chunkSize = chunkSize self.size = (width, height) @@ -27,15 +33,18 @@ class Video: '-filter:v', 'scale='+str(width)+':'+str(height), '-vcodec', 'rawvideo', '-', ] - + self.frameBuffer = PriorityQueue() self.frameBuffer.maxsize = int(frameRate) self.finishedFrames = {} - - self.thread = threading.Thread(target=self.fillBuffer, name=self.__doc__) + + 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: @@ -44,9 +53,12 @@ class Video: i, image = self.frameBuffer.get() self.finishedFrames[i] = image self.frameBuffer.task_done() - - def fillBuffer(self): - self.pipe = subprocess.Popen(self.command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8) + + def fillBuffer(self): + self.pipe = subprocess.Popen( + self.command, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, bufsize=10**8 + ) while True: if self.parent.canceled: break @@ -58,26 +70,29 @@ class Video: continue self.currentFrame = self.pipe.stdout.read(self.chunkSize) - #print('creating frame #%s' % str(self.frameNo)) if len(self.currentFrame) != 0: self.frameBuffer.put((self.frameNo, self.currentFrame)) self.lastFrame = self.currentFrame + class Component(__base__.Component): '''Video''' 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')) + 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) - + self.page = page return page @@ -85,46 +100,50 @@ class Component(__base__.Component): self.videoPath = self.page.lineEdit_video.text() self.loopVideo = self.page.checkBox_loop.isChecked() self.parent.drawPreview() - + def previewRender(self, previewWorker): width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) self.chunkSize = 4*width*height frame = self.getPreviewFrame(width, height) if not frame: - return Image.new("RGBA", (width, height),(0,0,0,0)) + return Image.new("RGBA", (width, height), (0, 0, 0, 0)) 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.chunkSize = 4*width*height - self.video = Video(self.parent.core.FFMPEG_BIN, self.videoPath, + self.video = Video( + self.parent.core.FFMPEG_BIN, self.videoPath, width, height, self.settings.value("outputFrameRate"), - self.chunkSize, self.parent, self.loopVideo) - + self.chunkSize, self.parent, self.loopVideo + ) + def frameRender(self, moduleNo, arrayNo, frameNo): return self.video.frame(frameNo) def loadPreset(self, pr): self.page.lineEdit_video.setText(pr['video']) - + def savePreset(self): return { - 'video' : self.videoPath, + 'video': self.videoPath, } - + def pickVideo(self): imgDir = self.settings.value("backgroundDir", os.path.expanduser("~")) - filename = QtGui.QFileDialog.getOpenFileName(self.page, - "Choose Video", imgDir, "Video Files (*.mp4 *.mov)") + filename = QtGui.QFileDialog.getOpenFileName( + self.page, "Choose Video", + imgDir, "Video Files (*.mp4 *.mov)" + ) 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 @@ -139,7 +158,10 @@ class Component(__base__.Component): '-ss', '90', '-vframes', '1', ] - pipe = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8) + pipe = subprocess.Popen( + command, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, bufsize=10**8 + ) byteFrame = pipe.stdout.read(self.chunkSize) image = Image.frombytes('RGBA', (width, height), byteFrame) pipe.stdout.close() diff --git a/core.py b/core.py index 99403f1..8ea884b 100644 --- a/core.py +++ b/core.py @@ -1,4 +1,6 @@ -import sys, io, os +import sys +import io +import os from PyQt4 import QtCore, QtGui, uic from os.path import expanduser import subprocess as sp @@ -10,103 +12,106 @@ import atexit import time from collections import OrderedDict + class Core(): - def __init__(self): - self.FFMPEG_BIN = self.findFfmpeg() - self.tempDir = os.path.join(tempfile.gettempdir(), 'audio-visualizer-python-data') - if not os.path.exists(self.tempDir): - os.makedirs(self.tempDir) - atexit.register(self.deleteTempDir) - - 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 deleteTempDir(self): - try: - rmtree(self.tempDir) - except FileNotFoundError: - pass - - def cancel(self): - self.canceled = True - - def reset(self): - self.canceled = False - - @staticmethod - def stringOrderedDict(dictionary): - sorted_ = OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])) - return repr(sorted_) + def __init__(self): + self.FFMPEG_BIN = self.findFfmpeg() + self.tempDir = os.path.join( + tempfile.gettempdir(), 'audio-visualizer-python-data') + if not os.path.exists(self.tempDir): + os.makedirs(self.tempDir) + atexit.register(self.deleteTempDir) + + 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 deleteTempDir(self): + try: + rmtree(self.tempDir) + except FileNotFoundError: + pass + + def cancel(self): + self.canceled = True + + def reset(self): + self.canceled = False + + @staticmethod + def stringOrderedDict(dictionary): + sorted_ = OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])) + return repr(sorted_) diff --git a/main.py b/main.py index c75a7f7..78c1d9b 100644 --- a/main.py +++ b/main.py @@ -1,668 +1,67 @@ -import sys, io, os, shutil, atexit, string, signal, filecmp, time -from os.path import expanduser -from queue import Queue from importlib import import_module from collections import OrderedDict -from PyQt4 import QtCore, QtGui, uic -from PyQt4.QtCore import QSettings, QModelIndex, Qt -from PyQt4.QtGui import QDesktopServices, QMenu +from PyQt4 import QtGui, uic +from PyQt4.QtCore import Qt +import sys +import io +import os +import atexit +import signal -import preview_thread, core, video_thread +import core +import preview_thread +import video_thread +from mainwindow import * -# FIXME: commandline functionality broken until we decide how to implement it -''' -class Command(QtCore.QObject): - - videoTask = QtCore.pyqtSignal(str, str, str, list) - - def __init__(self): - QtCore.QObject.__init__(self) - self.modules = [] - self.selectedComponents = [] - - import argparse - self.parser = argparse.ArgumentParser(description='Create a visualization for an audio file') - self.parser.add_argument('-i', '--input', dest='input', help='input audio file', required=True) - self.parser.add_argument('-o', '--output', dest='output', help='output video file', required=True) - self.parser.add_argument('-b', '--background', dest='bgimage', help='background image file', required=True) - self.parser.add_argument('-t', '--text', dest='text', help='title text', required=True) - self.parser.add_argument('-f', '--font', dest='font', help='title font', required=False) - self.parser.add_argument('-s', '--fontsize', dest='fontsize', help='title font size', required=False) - self.parser.add_argument('-c', '--textcolor', dest='textcolor', help='title text color in r,g,b format', required=False) - self.parser.add_argument('-C', '--viscolor', dest='viscolor', help='visualization color in r,g,b format', required=False) - self.parser.add_argument('-x', '--xposition', dest='xposition', help='x position', required=False) - self.parser.add_argument('-y', '--yposition', dest='yposition', help='y position', required=False) - self.parser.add_argument('-a', '--alignment', dest='alignment', help='title alignment', required=False, type=int, choices=[0, 1, 2]) - self.args = self.parser.parse_args() - - self.settings = QSettings('settings.ini', QSettings.IniFormat) - LoadDefaultSettings(self) - - # load colours as tuples from comma-separated strings - self.textColor = core.Core.RGBFromString(self.settings.value("textColor", '255, 255, 255')) - self.visColor = core.Core.RGBFromString(self.settings.value("visColor", '255, 255, 255')) - if self.args.textcolor: - self.textColor = core.Core.RGBFromString(self.args.textcolor) - if self.args.viscolor: - self.visColor = core.Core.RGBFromString(self.args.viscolor) - - # font settings - if self.args.font: - self.font = QFont(self.args.font) - else: - self.font = QFont(self.settings.value("titleFont", QFont())) - - if self.args.fontsize: - self.fontsize = int(self.args.fontsize) - else: - self.fontsize = int(self.settings.value("fontSize", 35)) - if self.args.alignment: - self.alignment = int(self.args.alignment) - else: - self.alignment = int(self.settings.value("alignment", 0)) - - if self.args.xposition: - self.textX = int(self.args.xposition) - else: - self.textX = int(self.settings.value("xPosition", 70)) - - if self.args.yposition: - self.textY = int(self.args.yposition) - else: - self.textY = int(self.settings.value("yPosition", 375)) - - ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~")) - - 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.bgimage, - self.args.text, - self.font, - self.fontsize, - self.alignment, - self.textX, - self.textY, - self.textColor, - self.visColor, - self.args.input, - self.args.output, - self.selectedComponents) - - def videoCreated(self): - self.videoThread.quit() - self.videoThread.wait() - self.cleanUp() - - def cleanUp(self): - self.settings.setValue("titleFont", self.font.toString()) - self.settings.setValue("alignment", str(self.alignment)) - self.settings.setValue("fontSize", str(self.fontsize)) - self.settings.setValue("xPosition", str(self.textX)) - self.settings.setValue("yPosition", str(self.textY)) - self.settings.setValue("visColor", '%s,%s,%s' % self.visColor) - self.settings.setValue("textColor", '%s,%s,%s' % self.textColor) - sys.exit(0) -''' - -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) - #print point.x(), ' ', point.y() - painter.drawPixmap(point, scaledPix) - - def changePixmap(self, img): - self.pixmap = QtGui.QPixmap(img) - self.repaint() - -class Main(QtCore.QObject): - - newTask = QtCore.pyqtSignal(list) - processTask = QtCore.pyqtSignal() - videoTask = QtCore.pyqtSignal(str, str, list) - - def __init__(self, window): - QtCore.QObject.__init__(self) - - # print('main thread id: {}'.format(QtCore.QThread.currentThreadId())) - self.window = window - self.core = core.Core() - self.pages = [] - self.selectedComponents = [] - self.lastAutosave = time.time() - - # create data directory, load/create settings - self.dataDir = QDesktopServices.storageLocation(QDesktopServices.DataLocation) - self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') - self.presetDir = os.path.join(self.dataDir, 'presets') - self.settings = QSettings(os.path.join(self.dataDir, 'settings.ini'), QSettings.IniFormat) - LoadDefaultSettings(self) - if not os.path.exists(self.dataDir): - os.makedirs(self.dataDir) - for neededDirectory in (self.presetDir, self.settings.value("projectDir")): - if not os.path.exists(neededDirectory): - os.mkdir(neededDirectory) - - # - 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 - 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) - window.setWindowTitle("Audio Visualizer") - - self.previewWindow = PreviewWindow(self, os.path.join(os.path.dirname(os.path.realpath(__file__)), "background.png")) - window.verticalLayout_previewWrapper.addWidget(self.previewWindow) - - self.modules = self.findComponents() - self.compMenu = QMenu() - for i, comp in enumerate(self.modules): - action = self.compMenu.addAction(comp.Component.__doc__) - action.triggered[()].connect( lambda item=i: self.insertComponent(item)) - - self.window.pushButton_addComponent.setMenu(self.compMenu) - window.listWidget_componentList.clicked.connect(lambda _: self.changeComponentWidget()) - - self.window.pushButton_removeComponent.clicked.connect(lambda _: self.removeComponent()) - - 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(self.moveComponentUp) - self.window.pushButton_listMoveDown.clicked.connect(self.moveComponentDown) - - self.window.pushButton_savePreset.clicked.connect(self.openSavePresetDialog) - self.window.comboBox_openPreset.currentIndexChanged.connect(self.openPreset) - self.window.pushButton_saveAs.clicked.connect(self.openSaveProjectDialog) - self.window.pushButton_saveProject.clicked.connect(self.saveCurrentProject) - self.window.pushButton_openProject.clicked.connect(self.openOpenProjectDialog) - - # show the window and load current project - window.show() - self.currentProject = self.settings.value("currentProject") - if self.currentProject and os.path.exists(self.autosavePath) \ - and filecmp.cmp(self.autosavePath, self.currentProject): - # delete autosave if it's identical to the project - os.remove(self.autosavePath) - - if self.currentProject and os.path.exists(self.autosavePath): - ch = self.showMessage("Restore unsaved changes in project '%s'?" % os.path.basename(self.currentProject)[:-4], True) - if ch: - os.remove(self.currentProject) - os.rename(self.autosavePath, self.currentProject) - else: - os.remove(self.autosavePath) - - self.openProject(self.currentProject) - self.drawPreview() - - def cleanUp(self): - self.timer.stop() - self.previewThread.quit() - self.previewThread.wait() - self.autosave() - - def autosave(self): - if time.time() - self.lastAutosave >= 1.0: - if os.path.exists(self.autosavePath): - os.remove(self.autosavePath) - self.createProjectFile(self.autosavePath) - self.lastAutosave = time.time() - - def openInputFileDialog(self): - inputDir = self.settings.value("inputDir", expanduser("~")) - - fileName = QtGui.QFileDialog.getOpenFileName(self.window, - "Open Music File", inputDir, "Music Files (*.mp3 *.wav *.ogg *.fla *.aac)"); - - 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", expanduser("~")) - - fileName = QtGui.QFileDialog.getSaveFileName(self.window, - "Set Output Video File", outputDir, "Video Files (*.mp4 *.mov *.mkv *.avi *.webm *.flv)"); - - 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) - ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~")) - 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() - self.videoTask.emit(self.window.lineEdit_audioFile.text(), - self.window.lineEdit_outputFile.text(), - self.selectedComponents) - else: - self.showMessage("You must select an audio file and output filename.") - - def progressBarUpdated(self, value): - self.window.progressBar_createVideo.setValue(value) - - 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.comboBox_openPreset.setEnabled(False) - self.window.pushButton_removePreset.setEnabled(False) - self.window.pushButton_savePreset.setEnabled(False) - self.window.pushButton_openProject.setEnabled(False) - self.window.listWidget_componentList.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.comboBox_openPreset.setEnabled(True) - self.window.pushButton_removePreset.setEnabled(True) - self.window.pushButton_savePreset.setEnabled(True) - self.window.pushButton_openProject.setEnabled(True) - self.window.listWidget_componentList.setEnabled(True) - - def progressBarSetText(self, value): - self.window.progressBar_createVideo.setFormat(value) - - def videoCreated(self): - self.videoThread.quit() - self.videoThread.wait() - - def updateResolution(self): - resIndex = int(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): - self.newTask.emit(self.selectedComponents) - # self.processTask.emit() - self.autosave() - - def showPreviewImage(self, image): - self.previewWindow.changePixmap(image) - - def findComponents(self): - def findComponents(): - srcPath = os.path.join(os.path.dirname(os.path.realpath(__file__)), '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 - return [import_module('components.%s' % name) for name in findComponents()] - - def addComponent(self, moduleIndex): - index = len(self.pages) - self.selectedComponents.append(self.modules[moduleIndex].Component()) - self.window.listWidget_componentList.addItem(self.selectedComponents[-1].__doc__) - self.pages.append(self.selectedComponents[-1].widget(self)) - self.window.listWidget_componentList.setCurrentRow(index) - self.window.stackedWidget.addWidget(self.pages[-1]) - self.window.stackedWidget.setCurrentIndex(index) - self.selectedComponents[-1].update() - self.updateOpenPresetComboBox(self.selectedComponents[-1]) - - def insertComponent(self, moduleIndex): - self.selectedComponents.insert(0, self.modules[moduleIndex].Component()) - self.window.listWidget_componentList.insertItem(0, self.selectedComponents[0].__doc__) - self.pages.insert(0, self.selectedComponents[0].widget(self)) - self.window.listWidget_componentList.setCurrentRow(0) - self.window.stackedWidget.insertWidget(0, self.pages[0]) - self.window.stackedWidget.setCurrentIndex(0) - self.selectedComponents[0].update() - self.updateOpenPresetComboBox(self.selectedComponents[0]) - - def removeComponent(self): - for selected in self.window.listWidget_componentList.selectedItems(): - index = self.window.listWidget_componentList.row(selected) - self.window.stackedWidget.removeWidget(self.pages[index]) - self.window.listWidget_componentList.takeItem(index) - self.selectedComponents.pop(index) - self.pages.pop(index) - self.changeComponentWidget() - self.drawPreview() - - def changeComponentWidget(self): - selected = self.window.listWidget_componentList.selectedItems() - if selected: - index = self.window.listWidget_componentList.row(selected[0]) - self.window.stackedWidget.setCurrentIndex(index) - self.updateOpenPresetComboBox(self.selectedComponents[index]) - - def moveComponentUp(self): - row = self.window.listWidget_componentList.currentRow() - if row > 0: - module = self.selectedComponents[row] - self.selectedComponents.pop(row) - self.selectedComponents.insert(row - 1,module) - page = self.pages[row] - self.pages.pop(row) - self.pages.insert(row - 1, page) - item = self.window.listWidget_componentList.takeItem(row) - self.window.listWidget_componentList.insertItem(row - 1, item) - widget = self.window.stackedWidget.removeWidget(page) - self.window.stackedWidget.insertWidget(row - 1, page) - self.window.listWidget_componentList.setCurrentRow(row - 1) - self.window.stackedWidget.setCurrentIndex(row -1) - self.drawPreview() - - def moveComponentDown(self): - row = self.window.listWidget_componentList.currentRow() - if row != -1 and row < len(self.pages)+1: - module = self.selectedComponents[row] - self.selectedComponents.pop(row) - self.selectedComponents.insert(row + 1,module) - page = self.pages[row] - self.pages.pop(row) - self.pages.insert(row + 1, page) - item = self.window.listWidget_componentList.takeItem(row) - self.window.listWidget_componentList.insertItem(row + 1, item) - widget = self.window.stackedWidget.removeWidget(page) - self.window.stackedWidget.insertWidget(row + 1, page) - self.window.listWidget_componentList.setCurrentRow(row + 1) - self.window.stackedWidget.setCurrentIndex(row + 1) - self.drawPreview() - - def updateOpenPresetComboBox(self, component): - self.window.comboBox_openPreset.clear() - self.window.comboBox_openPreset.addItem("Component Presets") - destination = os.path.join(self.presetDir, - str(component).strip(), str(component.version())) - if not os.path.exists(destination): - os.makedirs(destination) - for f in os.listdir(destination): - self.window.comboBox_openPreset.addItem(f) - - def openSavePresetDialog(self): - if self.window.listWidget_componentList.currentRow() == -1: - return - while True: - newName, OK = QtGui.QInputDialog.getText(QtGui.QWidget(), 'Audio Visualizer', 'New Preset Name:') - badName = False - for letter in newName: - if letter in string.punctuation: - badName = True - if badName: - # some filesystems don't like bizarre characters - self.showMessage("Preset names must contain only letters, numbers, and spaces.") - continue - if OK and newName: - index = self.window.listWidget_componentList.currentRow() - if index != -1: - saveValueStore = self.selectedComponents[index].savePreset() - componentName = str(self.selectedComponents[index]).strip() - vers = self.selectedComponents[index].version() - self.createPresetFile(componentName, vers, saveValueStore, newName) - break - - def createPresetFile(self, componentName, version, saveValueStore, filename): - dirname = os.path.join(self.presetDir, componentName, str(version)) - if not os.path.exists(dirname): - os.makedirs(dirname) - filepath = os.path.join(dirname, filename) - if os.path.exists(filepath): - ch = self.showMessage("%s already exists! Overwrite it?" % filename, True, QtGui.QMessageBox.Warning) - if not ch: - return - # remove old copies of the preset - for i in range(0, self.window.comboBox_openPreset.count()): - if self.window.comboBox_openPreset.itemText(i) == filename: - self.window.comboBox_openPreset.removeItem(i) - with open(filepath, 'w') as f: - f.write(core.Core.stringOrderedDict(saveValueStore)) - self.window.comboBox_openPreset.addItem(filename) - self.window.comboBox_openPreset.setCurrentIndex(self.window.comboBox_openPreset.count()-1) - - def openPreset(self): - if self.window.comboBox_openPreset.currentIndex() < 1: - return - index = self.window.listWidget_componentList.currentRow() - if index == -1: - return - filename = self.window.comboBox_openPreset.itemText(self.window.comboBox_openPreset.currentIndex()) - componentName = str(self.selectedComponents[index]).strip() - version = self.selectedComponents[index].version() - dirname = os.path.join(self.presetDir, componentName, str(version)) - filepath = os.path.join(dirname, filename) - if not os.path.exists(filepath): - self.window.comboBox_openPreset.removeItem(self.window.comboBox_openPreset.currentIndex()) - return - with open(filepath, 'r') as f: - for line in f: - saveValueStore = dict(eval(line.strip())) - break - self.selectedComponents[index].loadPreset(saveValueStore) - self.drawPreview() - - def saveCurrentProject(self): - if self.currentProject: - self.createProjectFile(self.currentProject) - else: - self.openSaveProjectDialog() - - def openSaveProjectDialog(self): - filename = QtGui.QFileDialog.getSaveFileName(self.window, - "Create Project File", self.settings.value("projectDir"), - "Project Files (*.avp)") - if not filename: - return - self.createProjectFile(filename) - - def createProjectFile(self, filepath): - if not filepath.endswith(".avp"): - filepath += '.avp' - 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.Core.stringOrderedDict(saveValueStore)) - if filepath != self.autosavePath: - self.settings.setValue("projectDir", os.path.dirname(filepath)) - self.settings.setValue("currentProject", filepath) - self.currentProject = filepath - - 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): - if not filepath or not os.path.exists(filepath) or not filepath.endswith('.avp'): - return - self.clear() - self.currentProject = filepath - self.settings.setValue("currentProject", filepath) - self.settings.setValue("projectDir", os.path.dirname(filepath)) - compNames = [mod.Component.__doc__ for mod in self.modules] - try: - with open(filepath, 'r') as f: - validSections = ('Components') - section = '' - def parseLine(line): - line = line.strip() - newSection = '' - if line.startswith('[') and line.endswith(']') and line[1:-1] in validSections: - newSection = line[1:-1] - return line, newSection - - i = 0 - for line in f: - line, newSection = parseLine(line) - if newSection: - section = str(newSection) - continue - if line and section == 'Components': - if i == 0: - compIndex = compNames.index(line) - self.addComponent(compIndex) - i += 1 - elif i == 1: - # version, not used yet - i += 1 - elif i == 2: - saveValueStore = dict(eval(line)) - self.selectedComponents[-1].loadPreset(saveValueStore) - i = 0 - except (IndexError, ValueError, KeyError, NameError, SyntaxError, AttributeError, TypeError) as e: - self.clear() - typ, value, _ = sys.exc_info() - msg = '%s: %s' % (typ.__name__, value) - self.showMessage("Project file '%s' is corrupted." % filepath, False, - QtGui.QMessageBox.Warning, msg) - - def showMessage(self, string, showCancel=False, icon=QtGui.QMessageBox.Information, detail=None): - msg = QtGui.QMessageBox() - msg.setIcon(icon) - msg.setText(string) - msg.setDetailedText(detail) - if 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 clear(self): - ''' empty out all components and fields, get a blank slate ''' - self.selectedComponents = [] - self.window.listWidget_componentList.clear() - for widget in self.pages: - self.window.stackedWidget.removeWidget(widget) - self.pages = [] def LoadDefaultSettings(self): - self.resolutions = [ - '1920x1080', - '1280x720', - '854x480' + self.resolutions = [ + '1920x1080', + '1280x720', + '854x480' ] - default = { - "outputWidth": 1280, - "outputHeight": 720, - "outputFrameRate": 30, - "outputAudioCodec": "aac", - "outputAudioBitrate": "192k", - "outputVideoCodec": "libx264", - "outputVideoFormat": "yuv420p", - "outputPreset": "medium", - "outputFormat": "mp4", - "projectDir" : os.path.join(self.dataDir, 'projects'), - } - - for parm, value in default.items(): - if self.settings.value(parm) == None: - self.settings.setValue(parm,value) - + default = { + "outputWidth": 1280, + "outputHeight": 720, + "outputFrameRate": 30, + "outputAudioCodec": "aac", + "outputAudioBitrate": "192k", + "outputVideoCodec": "libx264", + "outputVideoFormat": "yuv420p", + "outputPreset": "medium", + "outputFormat": "mp4", + "projectDir": os.path.join(self.dataDir, 'projects'), + } + + for parm, value in default.items(): + if self.settings.value(parm) is None: + self.settings.setValue(parm, value) -''' ####### commandline functionality broken until we decide how to implement it -if len(sys.argv) > 1: - # command line mode - app = QtGui.QApplication(sys.argv, False) - command = Command() - signal.signal(signal.SIGINT, command.cleanUp) - sys.exit(app.exec_()) -else: -''' -# gui mode if __name__ == "__main__": + ''' FIXME commandline functionality broken until we decide how to implement + if len(sys.argv) > 1: + # command line mode + app = QtGui.QApplication(sys.argv, False) + command = Command() + signal.signal(signal.SIGINT, command.cleanUp) + sys.exit(app.exec_()) + else: + ''' app = QtGui.QApplication(sys.argv) app.setApplicationName("audio-visualizer") app.setOrganizationName("audio-visualizer") - window = uic.loadUi(os.path.join(os.path.dirname(os.path.realpath(__file__)), "mainwindow.ui")) + window = uic.loadUi(os.path.join( + os.path.dirname(os.path.realpath(__file__)), "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 = Main(window) + # window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0) + + main = MainWindow(window) signal.signal(signal.SIGINT, main.cleanUp) atexit.register(main.cleanUp) diff --git a/mainwindow.py b/mainwindow.py new file mode 100644 index 0000000..b779298 --- /dev/null +++ b/mainwindow.py @@ -0,0 +1,586 @@ +from os.path import expanduser +from queue import Queue +from importlib import import_module +from collections import OrderedDict +from PyQt4 import QtCore, QtGui +from PyQt4.QtCore import QSettings, Qt +from PyQt4.QtGui import QDesktopServices, QMenu +import sys +import io +import os +import string +import signal +import filecmp +import time + +import core +import preview_thread +import video_thread +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(QtCore.QObject): + + newTask = QtCore.pyqtSignal(list) + processTask = QtCore.pyqtSignal() + videoTask = QtCore.pyqtSignal(str, str, list) + + def __init__(self, window): + QtCore.QObject.__init__(self) + + # print('main thread id: {}'.format(QtCore.QThread.currentThreadId())) + self.window = window + self.core = core.Core() + self.pages = [] + self.selectedComponents = [] + self.lastAutosave = time.time() + + # create data directory, load/create settings + self.dataDir = QDesktopServices.storageLocation( + QDesktopServices.DataLocation) + self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') + self.presetDir = os.path.join(self.dataDir, 'presets') + self.settings = QSettings( + os.path.join(self.dataDir, 'settings.ini'), QSettings.IniFormat) + LoadDefaultSettings(self) + if not os.path.exists(self.dataDir): + os.makedirs(self.dataDir) + for neededDirectory in ( + self.presetDir, self.settings.value("projectDir")): + if not os.path.exists(neededDirectory): + os.mkdir(neededDirectory) + + # + 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 + 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) + window.setWindowTitle("Audio Visualizer") + + self.previewWindow = PreviewWindow(self, os.path.join( + os.path.dirname(os.path.realpath(__file__)), "background.png")) + window.verticalLayout_previewWrapper.addWidget(self.previewWindow) + + self.modules = self.findComponents() + self.compMenu = QMenu() + for i, comp in enumerate(self.modules): + action = self.compMenu.addAction(comp.Component.__doc__) + action.triggered[()].connect( + lambda item=i: self.insertComponent(item)) + + self.window.pushButton_addComponent.setMenu(self.compMenu) + window.listWidget_componentList.clicked.connect( + lambda _: self.changeComponentWidget()) + + self.window.pushButton_removeComponent.clicked.connect( + lambda _: self.removeComponent()) + + 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( + self.moveComponentUp) + self.window.pushButton_listMoveDown.clicked.connect( + self.moveComponentDown) + self.window.pushButton_savePreset.clicked.connect( + self.openSavePresetDialog) + self.window.comboBox_openPreset.currentIndexChanged.connect( + self.openPreset) + self.window.pushButton_saveAs.clicked.connect( + self.openSaveProjectDialog) + self.window.pushButton_saveProject.clicked.connect( + self.saveCurrentProject) + self.window.pushButton_openProject.clicked.connect( + self.openOpenProjectDialog) + + # show the window and load current project + window.show() + self.currentProject = self.settings.value("currentProject") + if self.currentProject and os.path.exists(self.autosavePath) \ + and filecmp.cmp(self.autosavePath, self.currentProject): + # delete autosave if it's identical to the project + os.remove(self.autosavePath) + + if self.currentProject and os.path.exists(self.autosavePath): + ch = self.showMessage( + "Restore unsaved changes in project '%s'?" + % os.path.basename(self.currentProject)[:-4], True) + if ch: + os.remove(self.currentProject) + os.rename(self.autosavePath, self.currentProject) + else: + os.remove(self.autosavePath) + + self.openProject(self.currentProject) + self.drawPreview() + + def cleanUp(self): + self.timer.stop() + self.previewThread.quit() + self.previewThread.wait() + self.autosave() + + def autosave(self): + if time.time() - self.lastAutosave >= 1.0: + if os.path.exists(self.autosavePath): + os.remove(self.autosavePath) + self.createProjectFile(self.autosavePath) + self.lastAutosave = time.time() + + def openInputFileDialog(self): + inputDir = self.settings.value("inputDir", expanduser("~")) + + fileName = QtGui.QFileDialog.getOpenFileName( + self.window, "Open Music File", + inputDir, "Music Files (*.mp3 *.wav *.ogg *.fla *.aac)") + + 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", expanduser("~")) + + fileName = QtGui.QFileDialog.getSaveFileName( + self.window, "Set Output Video File", + outputDir, "Video Files (*.mp4 *.mov *.mkv *.avi *.webm *.flv)") + + 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) + ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~")) + 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() + self.videoTask.emit( + self.window.lineEdit_audioFile.text(), + self.window.lineEdit_outputFile.text(), + self.selectedComponents) + else: + self.showMessage( + "You must select an audio file and output filename.") + + def progressBarUpdated(self, value): + self.window.progressBar_createVideo.setValue(value) + + 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.comboBox_openPreset.setEnabled(False) + self.window.pushButton_removePreset.setEnabled(False) + self.window.pushButton_savePreset.setEnabled(False) + self.window.pushButton_openProject.setEnabled(False) + self.window.listWidget_componentList.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.comboBox_openPreset.setEnabled(True) + self.window.pushButton_removePreset.setEnabled(True) + self.window.pushButton_savePreset.setEnabled(True) + self.window.pushButton_openProject.setEnabled(True) + self.window.listWidget_componentList.setEnabled(True) + + def progressBarSetText(self, value): + self.window.progressBar_createVideo.setFormat(value) + + def videoCreated(self): + self.videoThread.quit() + self.videoThread.wait() + + def updateResolution(self): + resIndex = int(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): + self.newTask.emit(self.selectedComponents) + # self.processTask.emit() + self.autosave() + + def showPreviewImage(self, image): + self.previewWindow.changePixmap(image) + + def findComponents(self): + def findComponents(): + srcPath = os.path.join( + os.path.dirname(os.path.realpath(__file__)), '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 + return [ + import_module('components.%s' % name) + for name in findComponents()] + + def addComponent(self, moduleIndex): + index = len(self.pages) + self.selectedComponents.append(self.modules[moduleIndex].Component()) + self.window.listWidget_componentList.addItem( + self.selectedComponents[-1].__doc__) + self.pages.append(self.selectedComponents[-1].widget(self)) + self.window.listWidget_componentList.setCurrentRow(index) + self.window.stackedWidget.addWidget(self.pages[-1]) + self.window.stackedWidget.setCurrentIndex(index) + self.selectedComponents[-1].update() + self.updateOpenPresetComboBox(self.selectedComponents[-1]) + + def insertComponent(self, moduleIndex): + self.selectedComponents.insert( + 0, self.modules[moduleIndex].Component()) + self.window.listWidget_componentList.insertItem( + 0, self.selectedComponents[0].__doc__) + self.pages.insert(0, self.selectedComponents[0].widget(self)) + self.window.listWidget_componentList.setCurrentRow(0) + self.window.stackedWidget.insertWidget(0, self.pages[0]) + self.window.stackedWidget.setCurrentIndex(0) + self.selectedComponents[0].update() + self.updateOpenPresetComboBox(self.selectedComponents[0]) + + def removeComponent(self): + for selected in self.window.listWidget_componentList.selectedItems(): + index = self.window.listWidget_componentList.row(selected) + self.window.stackedWidget.removeWidget(self.pages[index]) + self.window.listWidget_componentList.takeItem(index) + self.selectedComponents.pop(index) + self.pages.pop(index) + self.changeComponentWidget() + self.drawPreview() + + def changeComponentWidget(self): + selected = self.window.listWidget_componentList.selectedItems() + if selected: + index = self.window.listWidget_componentList.row(selected[0]) + self.window.stackedWidget.setCurrentIndex(index) + self.updateOpenPresetComboBox(self.selectedComponents[index]) + + def moveComponentUp(self): + row = self.window.listWidget_componentList.currentRow() + if row > 0: + module = self.selectedComponents[row] + self.selectedComponents.pop(row) + self.selectedComponents.insert(row - 1, module) + page = self.pages[row] + self.pages.pop(row) + self.pages.insert(row - 1, page) + item = self.window.listWidget_componentList.takeItem(row) + self.window.listWidget_componentList.insertItem(row - 1, item) + widget = self.window.stackedWidget.removeWidget(page) + self.window.stackedWidget.insertWidget(row - 1, page) + self.window.listWidget_componentList.setCurrentRow(row - 1) + self.window.stackedWidget.setCurrentIndex(row - 1) + self.drawPreview() + + def moveComponentDown(self): + row = self.window.listWidget_componentList.currentRow() + if row != -1 and row < len(self.pages)+1: + module = self.selectedComponents[row] + self.selectedComponents.pop(row) + self.selectedComponents.insert(row + 1, module) + page = self.pages[row] + self.pages.pop(row) + self.pages.insert(row + 1, page) + item = self.window.listWidget_componentList.takeItem(row) + self.window.listWidget_componentList.insertItem(row + 1, item) + widget = self.window.stackedWidget.removeWidget(page) + self.window.stackedWidget.insertWidget(row + 1, page) + self.window.listWidget_componentList.setCurrentRow(row + 1) + self.window.stackedWidget.setCurrentIndex(row + 1) + self.drawPreview() + + def updateOpenPresetComboBox(self, component): + self.window.comboBox_openPreset.clear() + self.window.comboBox_openPreset.addItem("Component Presets") + destination = os.path.join( + self.presetDir, str(component).strip(), str(component.version())) + if not os.path.exists(destination): + os.makedirs(destination) + for f in os.listdir(destination): + self.window.comboBox_openPreset.addItem(f) + + def openSavePresetDialog(self): + if self.window.listWidget_componentList.currentRow() == -1: + return + while True: + newName, OK = QtGui.QInputDialog.getText( + QtGui.QWidget(), 'Audio Visualizer', 'New Preset Name:') + badName = False + for letter in newName: + if letter in string.punctuation: + badName = True + if badName: + # some filesystems don't like bizarre characters + self.showMessage("Preset names must contain only letters, \ + numbers, and spaces.") + continue + if OK and newName: + index = self.window.listWidget_componentList.currentRow() + if index != -1: + saveValueStore = \ + self.selectedComponents[index].savePreset() + componentName = str(self.selectedComponents[index]).strip() + vers = self.selectedComponents[index].version() + self.createPresetFile( + componentName, vers, saveValueStore, newName) + break + + def createPresetFile( + self, componentName, version, saveValueStore, filename): + dirname = os.path.join(self.presetDir, componentName, str(version)) + if not os.path.exists(dirname): + os.makedirs(dirname) + filepath = os.path.join(dirname, filename) + if os.path.exists(filepath): + ch = self.showMessage( + "%s already exists! Overwrite it?" % filename, + True, QtGui.QMessageBox.Warning) + if not ch: + return + # remove old copies of the preset + for i in range(0, self.window.comboBox_openPreset.count()): + if self.window.comboBox_openPreset.itemText(i) == filename: + self.window.comboBox_openPreset.removeItem(i) + with open(filepath, 'w') as f: + f.write(core.Core.stringOrderedDict(saveValueStore)) + self.window.comboBox_openPreset.addItem(filename) + self.window.comboBox_openPreset.setCurrentIndex( + self.window.comboBox_openPreset.count()-1) + + def openPreset(self): + if self.window.comboBox_openPreset.currentIndex() < 1: + return + index = self.window.listWidget_componentList.currentRow() + if index == -1: + return + filename = self.window.comboBox_openPreset.itemText( + self.window.comboBox_openPreset.currentIndex()) + componentName = str(self.selectedComponents[index]).strip() + version = self.selectedComponents[index].version() + dirname = os.path.join(self.presetDir, componentName, str(version)) + filepath = os.path.join(dirname, filename) + if not os.path.exists(filepath): + self.window.comboBox_openPreset.removeItem( + self.window.comboBox_openPreset.currentIndex()) + return + with open(filepath, 'r') as f: + for line in f: + saveValueStore = dict(eval(line.strip())) + break + self.selectedComponents[index].loadPreset(saveValueStore) + self.drawPreview() + + def saveCurrentProject(self): + if self.currentProject: + self.createProjectFile(self.currentProject) + else: + self.openSaveProjectDialog() + + def openSaveProjectDialog(self): + filename = QtGui.QFileDialog.getSaveFileName( + self.window, "Create Project File", + self.settings.value("projectDir"), + "Project Files (*.avp)") + if not filename: + return + self.createProjectFile(filename) + + def createProjectFile(self, filepath): + if not filepath.endswith(".avp"): + filepath += '.avp' + 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.Core.stringOrderedDict(saveValueStore)) + if filepath != self.autosavePath: + self.settings.setValue("projectDir", os.path.dirname(filepath)) + self.settings.setValue("currentProject", filepath) + self.currentProject = filepath + + 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): + if not filepath or not os.path.exists(filepath) \ + or not filepath.endswith('.avp'): + return + self.clear() + self.currentProject = filepath + self.settings.setValue("currentProject", filepath) + self.settings.setValue("projectDir", os.path.dirname(filepath)) + compNames = [mod.Component.__doc__ for mod in self.modules] + try: + with open(filepath, 'r') as f: + validSections = ('Components') + section = '' + + def parseLine(line): + line = line.strip() + newSection = '' + + if line.startswith('[') and line.endswith(']') \ + and line[1:-1] in validSections: + newSection = line[1:-1] + + return line, newSection + + i = 0 + for line in f: + line, newSection = parseLine(line) + if newSection: + section = str(newSection) + continue + if line and section == 'Components': + if i == 0: + compIndex = compNames.index(line) + self.addComponent(compIndex) + i += 1 + elif i == 1: + # version, not used yet + i += 1 + elif i == 2: + saveValueStore = dict(eval(line)) + self.selectedComponents[-1].loadPreset( + saveValueStore) + i = 0 + except (IndexError, ValueError, KeyError, NameError, + SyntaxError, AttributeError, TypeError) as e: + self.clear() + typ, value, _ = sys.exc_info() + msg = '%s: %s' % (typ.__name__, value) + self.showMessage( + "Project file '%s' is corrupted." % filepath, False, + QtGui.QMessageBox.Warning, msg) + + def showMessage( + self, string, showCancel=False, + icon=QtGui.QMessageBox.Information, detail=None): + msg = QtGui.QMessageBox() + msg.setIcon(icon) + msg.setText(string) + msg.setDetailedText(detail) + if 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 clear(self): + ''' empty out all components and fields, get a blank slate ''' + self.selectedComponents = [] + self.window.listWidget_componentList.clear() + for widget in self.pages: + self.window.stackedWidget.removeWidget(widget) + self.pages = [] diff --git a/preview_thread.py b/preview_thread.py index 04683ae..d54dba5 100644 --- a/preview_thread.py +++ b/preview_thread.py @@ -9,53 +9,52 @@ import numpy 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.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(os.path.dirname(os.path.realpath(__file__)),"background.png"))) - - - - @pyqtSlot(str, list) - def createPreviewImage(self, components): - # print('worker thread id: {}'.format(QtCore.QThread.currentThreadId())) - dic = { - "components": components, - } - self.queue.put(dic) - - @pyqtSlot() - def process(self): - try: - nextPreviewInformation = self.queue.get(block=False) - while self.queue.qsize() >= 2: + 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.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( + os.path.dirname(os.path.realpath(__file__)), "background.png"))) + + @pyqtSlot(str, list) + def createPreviewImage(self, components): + dic = { + "components": components, + } + self.queue.put(dic) + + @pyqtSlot() + def process(self): try: - self.queue.get(block=False) + 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: - 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): - #newFrame = Image.alpha_composite(frame,) - frame = Image.alpha_composite(frame,component.previewRender(self)) - - self._image = ImageQt(frame) - self.imageCreated.emit(QtGui.QImage(self._image)) - - except Empty: - True + True diff --git a/video_thread.py b/video_thread.py index e880263..5897ff0 100644 --- a/video_thread.py +++ b/video_thread.py @@ -13,6 +13,7 @@ import time from copy import copy import signal + class Worker(QtCore.QObject): imageCreated = pyqtSignal(['QImage']) @@ -40,16 +41,19 @@ class Worker(QtCore.QObject): frame = None for compNo, comp in reversed(list(enumerate(self.components))): - if compNo in self.staticComponents and self.staticComponents[compNo] != None: + 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]) + 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])) + frame = Image.alpha_composite( + frame, comp.frameRender(compNo, i[0], i[1])) self.renderQueue.put([i[0], frame]) self.compositeQueue.task_done() @@ -63,8 +67,9 @@ class Worker(QtCore.QObject): self.bgI += 1 def previewDispatch(self): - background = Image.new("RGBA", (1920, 1080),(0,0,0,0)) - background.paste(Image.open(os.path.join(os.path.dirname(os.path.realpath(__file__)), "background.png"))) + background = Image.new("RGBA", (1920, 1080), (0, 0, 0, 0)) + background.paste(Image.open(os.path.join( + os.path.dirname(os.path.realpath(__file__)), "background.png"))) background = background.resize((self.width, self.height)) while not self.stopped: @@ -83,11 +88,10 @@ class Worker(QtCore.QObject): self.encoding.emit(True) self.components = components self.outputFile = outputFile - self.bgI = 0 # tracked video frame + self.bgI = 0 # tracked video frame self.reset() self.width = int(self.core.settings.value('outputWidth')) self.height = int(self.core.settings.value('outputHeight')) - # print('worker thread id: {}'.format(QtCore.QThread.currentThreadId())) progressBarValue = 0 self.progressBarUpdate.emit(progressBarValue) @@ -95,21 +99,24 @@ class Worker(QtCore.QObject): 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 = sp.check_output( + self.core.FFMPEG_BIN + " -encoders -hide_banner", shell=True) acodec = self.core.settings.value('outputAudioCodec') - + if b'libfdk_aac' in encoders and acodec == 'aac': acodec = 'libfdk_aac' ffmpegCommand = [ self.core.FFMPEG_BIN, '-thread_queue_size', '512', - '-y', # (optional) means overwrite the output file if it already exists. + '-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', - '-r', self.core.settings.value('outputFrameRate'), # frames per second + + # frames per second + '-r', self.core.settings.value('outputFrameRate'), '-i', '-', # The input comes from a pipe '-an', '-i', inputFile, @@ -126,14 +133,16 @@ class Worker(QtCore.QObject): ffmpegCommand.append('-2') ffmpegCommand.append(outputFile) - self.out_pipe = sp.Popen(ffmpegCommand, stdin=sp.PIPE,stdout=sys.stdout, stderr=sys.stdout) + self.out_pipe = sp.Popen( + ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout) # create video for output numpy.seterr(divide='ignore') # initialize components print('loaded components:', - ["%s%s" % (num, str(component)) for num, component in enumerate(self.components)]) + ["%s%s" % (num, str(component)) for num, + component in enumerate(self.components)]) self.staticComponents = {} numComps = len(self.components) for compNo, comp in enumerate(self.components): @@ -149,7 +158,8 @@ class Worker(QtCore.QObject): ) if properties and 'static' in properties: - self.staticComponents[compNo] = copy(comp.frameRender(compNo, 0, 0)) + self.staticComponents[compNo] = copy( + comp.frameRender(compNo, 0, 0)) self.progressBarUpdate.emit(100) self.compositeQueue = Queue() @@ -159,17 +169,20 @@ class Worker(QtCore.QObject): self.previewQueue = PriorityQueue() self.renderThreads = [] - # create threads to render frames and send them back here for piping out + # Threads to render frames and send them back here for piping out for i in range(3): - self.renderThreads.append(Thread(target=self.renderNode, name="Render Thread")) + 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 = 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 = Thread( + target=self.previewDispatch, name="Render Dispatch Thread") self.previewDispatch.daemon = True self.previewDispatch.start() @@ -197,10 +210,13 @@ class Worker(QtCore.QObject): break # increase progress bar value - if progressBarValue + 1 <= (i / len(self.completeAudioArray)) * 100: - progressBarValue = numpy.floor((i / len(self.completeAudioArray)) * 100) + 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)) + "%" + pStr = "Exporting video: " + str(int(progressBarValue)) \ + + "%" self.progressBarSetText.emit(pStr) numpy.seterr(all='print') @@ -220,7 +236,7 @@ class Worker(QtCore.QObject): pass self.progressBarUpdate.emit(0) self.progressBarSetText.emit('Export Canceled') - + else: if self.error: print("Export Failed") @@ -230,14 +246,14 @@ class Worker(QtCore.QObject): print("Export Complete") self.progressBarUpdate.emit(100) self.progressBarSetText.emit('Export Complete') - + self.error = False self.canceled = False self.parent.drawPreview() self.stopped = True self.encoding.emit(False) self.videoCreated.emit() - + def updateProgress(self, pStr, pVal): self.progressBarValue.emit(pVal) self.progressBarSetText.emit(pStr) @@ -245,10 +261,10 @@ class Worker(QtCore.QObject): 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: -- cgit v1.2.3 From e6beca94a383ab916baf294ec2d703a47b4117fc Mon Sep 17 00:00:00 2001 From: DH4 Date: Wed, 7 Jun 2017 11:59:59 -0500 Subject: Added Encoder Settings, FIXME: Add bitrate options. --- core.py | 8 ++ encoder-options.json | 215 ++++++++++++++++++--------------------------------- main.py | 5 +- mainwindow.py | 71 +++++++++++++++-- video_thread.py | 35 +++++++-- 5 files changed, 183 insertions(+), 151 deletions(-) (limited to 'main.py') diff --git a/core.py b/core.py index 8ea884b..7b3c69a 100644 --- a/core.py +++ b/core.py @@ -11,6 +11,7 @@ from shutil import rmtree import atexit import time from collections import OrderedDict +import json class Core(): @@ -22,6 +23,13 @@ class Core(): if not os.path.exists(self.tempDir): os.makedirs(self.tempDir) atexit.register(self.deleteTempDir) + self.wd = os.path.dirname(os.path.realpath(__file__)) + self.loadEncoderOptions() + + 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": diff --git a/encoder-options.json b/encoder-options.json index 699ead4..53aa0e5 100644 --- a/encoder-options.json +++ b/encoder-options.json @@ -6,32 +6,14 @@ "default-vcodec": "H264", "default-acodec": "AAC", "video-codecs": [ - { - "name": "H264", - "encoders": ["libx264"] - }, - { - "name": "H264 (nvenc)", - "encoders": ["nvenc_264"] - }, - { - "name": "MPEG4", - "encoders": ["mpeg4"] - } + "H264", + "H264 (nvenc)", + "MPEG4" ], "audio-codecs": [ - { - "name": "AAC", - "encoders": ["libfdk_aac","aac"] - }, - { - "name": "AC3", - "encoders": ["ac3"] - }, - { - "name": "MP3", - "encoders": ["libmp3lame"] - } + "AAC", + "AC3", + "MP3" ] }, { @@ -40,40 +22,37 @@ "default-vcodec": "H264", "default-acodec": "AAC", "video-codecs": [ - { - "name": "H264", - "encoders": ["libx264"] - }, - { - "name": "H264 (nvenc)", - "encoders": ["nvenc_264"] - }, - { - "name": "MPEG4", - "encoders": ["mpeg4"] - }, - { - "name": "XVID", - "encoders": ["libxvid"] - } + "H264", + "H264 (nvenc)", + "MPEG4", + "XVID" ], "audio-codecs": [ - { - "name": "AAC", - "encoders": ["libfdk_aac","aac"] - }, - { - "name": "AC3", - "encoders": ["ac3"] - }, - { - "name": "MP3", - "encoders": ["libmp3lame"] - }, - { - "name": "PCM s16 LE", - "encoders": ["pcm_s16le"] - } + "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" ] }, { @@ -82,52 +61,19 @@ "default-vcodec": "H264", "default-acodec": "AAC", "video-codecs": [ - { - "name": "H264", - "encoders": ["libx264"] - }, - { - "name": "H264 (nvenc)", - "encoders": ["nvenc_264"] - }, - { - "name": "MPEG4", - "encoders": ["mpeg4"] - }, - { - "name": "MPEG2", - "encoders": ["mp2video"] - }, - { - "name": "DV", - "encoders": ["dvvideo"] - }, - { - "name": "WMV", - "encoders": ["wmv2"] - } + "H264", + "H264 (nvenc)", + "MPEG4", + "MPEG2", + "DV", + "WMV" ], "audio-codecs": [ - { - "name": "AAC", - "encoders": ["libfdk_aac","aac"] - }, - { - "name": "AC3", - "encoders": ["ac3"] - }, - { - "name": "WMA", - "encoders": ["wmav2"] - }, - { - "name": "MP3", - "encoders": ["libmp3lame"] - }, - { - "name": "PCM s16 LE", - "encoders": ["pcm_s16le"] - } + "AAC", + "AC3", + "MP3", + "PCM s16 LE", + "WMA" ] }, { @@ -136,20 +82,11 @@ "default-vcodec": "VP9", "default-acodec": "Vorbis", "video-codecs": [ - { - "name": "VP9", - "encoders": ["libvpx-vp9"] - }, - { - "name": "VP8", - "encoders": ["libvpx"] - } + "VP9", + "VP8" ], "audio-codecs": [ - { - "name": "Vorbis", - "encoders": ["vorbis"] - } + "Vorbis" ] }, { @@ -158,34 +95,36 @@ "default-vcodec": "FLV", "default-acodec": "Vorbis", "video-codecs": [ - { - "name": "Sorenson (flv)", - "encoders": ["flv"] - }, - { - "name": "H264", - "encoders": ["libx264"] - }, - { - "name": "MPEG4", - "encoders": ["mpeg4"] - } + "Sorenson (flv)", + "H264", + "H264 (nvenc)", + "MPEG4" ], "audio-codecs": [ - { - "name": "MP3", - "encoders": ["libmp3lame"] - }, - { - "name": "Vorbis", - "encoders": ["vorbis"] - }, - { - "name": "PCM s16 LE", - "encoders": ["pcm_s16le"] - } + "MP3", + "PCM s16 LE", + "Vorbis" ] } - ] - + ], + "video-codecs":{ + "H264": ["libx264"], + "H264 (nvenc)": ["nvenc_264"], + "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/main.py b/main.py index 78c1d9b..3082e95 100644 --- a/main.py +++ b/main.py @@ -25,12 +25,13 @@ def LoadDefaultSettings(self): "outputWidth": 1280, "outputHeight": 720, "outputFrameRate": 30, - "outputAudioCodec": "aac", + "outputAudioCodec": "AAC", "outputAudioBitrate": "192k", - "outputVideoCodec": "libx264", + "outputVideoCodec": "H264", "outputVideoFormat": "yuv420p", "outputPreset": "medium", "outputFormat": "mp4", + "outputContainer": "MP4", "projectDir": os.path.join(self.dataDir, 'projects'), } diff --git a/mainwindow.py b/mainwindow.py index b779298..6fbc3ed 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -101,6 +101,37 @@ class MainWindow(QtCore.QObject): window.pushButton_Cancel.clicked.connect(self.stopVideo) window.setWindowTitle("Audio Visualizer") + 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 + ) + self.previewWindow = PreviewWindow(self, os.path.join( os.path.dirname(os.path.realpath(__file__)), "background.png")) window.verticalLayout_previewWrapper.addWidget(self.previewWindow) @@ -123,11 +154,11 @@ class MainWindow(QtCore.QObject): 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) + if res == currentRes: + currentRes = i + window.comboBox_resolution.setCurrentIndex(currentRes) + window.comboBox_resolution.currentIndexChanged.connect( + self.updateResolution) self.window.pushButton_listMoveUp.clicked.connect( self.moveComponentUp) @@ -171,6 +202,34 @@ class MainWindow(QtCore.QObject): self.previewThread.wait() self.autosave() + 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 + aCodecWidget = self.window.comboBox_audioCodec + currentVideoCodec = vCodecWidget.currentIndex() + currentVideoCodec = vCodecWidget.itemText(currentVideoCodec) + currentAudioCodec = aCodecWidget.currentIndex() + currentAudioCodec = aCodecWidget.itemText(currentAudioCodec) + self.settings.setValue('outputVideoCodec', currentVideoCodec) + self.settings.setValue('outputAudioCodec', currentAudioCodec) + def autosave(self): if time.time() - self.lastAutosave >= 1.0: if os.path.exists(self.autosavePath): @@ -285,7 +344,7 @@ class MainWindow(QtCore.QObject): self.videoThread.wait() def updateResolution(self): - resIndex = int(window.comboBox_resolution.currentIndex()) + 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]) diff --git a/video_thread.py b/video_thread.py index 5897ff0..d8694a4 100644 --- a/video_thread.py +++ b/video_thread.py @@ -101,10 +101,35 @@ class Worker(QtCore.QObject): # 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') acodec = self.core.settings.value('outputAudioCodec') - if b'libfdk_aac' in encoders and acodec == 'aac': - acodec = 'libfdk_aac' + for cont in options['containers']: + if cont['name'] == containerName: + container = cont['container'] + + 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, @@ -120,12 +145,12 @@ class Worker(QtCore.QObject): '-i', '-', # The input comes from a pipe '-an', '-i', inputFile, - '-acodec', acodec, # output audio codec + '-vcodec', vencoder, + '-acodec', aencoder, # output audio codec '-b:a', self.core.settings.value('outputAudioBitrate'), - '-vcodec', self.core.settings.value('outputVideoCodec'), '-pix_fmt', self.core.settings.value('outputVideoFormat'), '-preset', self.core.settings.value('outputPreset'), - '-f', self.core.settings.value('outputFormat') + '-f', container ] if acodec == 'aac': -- cgit v1.2.3 From 02795503d09743b5225eed7e7b7112208dfc28d0 Mon Sep 17 00:00:00 2001 From: DH4 Date: Wed, 7 Jun 2017 12:33:22 -0500 Subject: Added Bitrate Selection --- encoder-options.json | 4 ++-- main.py | 3 ++- mainwindow.py | 18 ++++++++++++++++++ mainwindow.ui | 16 ++++++++++++---- video_thread.py | 5 ++++- 5 files changed, 38 insertions(+), 8 deletions(-) (limited to 'main.py') diff --git a/encoder-options.json b/encoder-options.json index 53aa0e5..78bc940 100644 --- a/encoder-options.json +++ b/encoder-options.json @@ -109,7 +109,7 @@ ], "video-codecs":{ "H264": ["libx264"], - "H264 (nvenc)": ["nvenc_264"], + "H264 (nvenc)": ["h264_nvenc", "nvenc_h264"], "MPEG4": ["mpeg4"], "VP9": ["libvpx-vp9"], "VP8": ["libvpx"], @@ -120,7 +120,7 @@ "WMV": ["wmv2"] }, "audio-codecs": { - "AAC": ["libfdk_aac","aac"], + "AAC": ["libfdk_aac", "aac"], "AC3": ["ac3"], "MP3": ["libmp3lame"], "PCM s16 LE": ["pcm_s16le"], diff --git a/main.py b/main.py index 3082e95..c771aca 100644 --- a/main.py +++ b/main.py @@ -26,8 +26,9 @@ def LoadDefaultSettings(self): "outputHeight": 720, "outputFrameRate": 30, "outputAudioCodec": "AAC", - "outputAudioBitrate": "192k", + "outputAudioBitrate": "192", "outputVideoCodec": "H264", + "outputVideoBitrate": "2500", "outputVideoFormat": "yuv420p", "outputPreset": "medium", "outputFormat": "mp4", diff --git a/mainwindow.py b/mainwindow.py index 6fbc3ed..78809be 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -132,6 +132,16 @@ class MainWindow(QtCore.QObject): 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( os.path.dirname(os.path.realpath(__file__)), "background.png")) window.verticalLayout_previewWrapper.addWidget(self.previewWindow) @@ -159,6 +169,8 @@ class MainWindow(QtCore.QObject): window.comboBox_resolution.setCurrentIndex(currentRes) window.comboBox_resolution.currentIndexChanged.connect( self.updateResolution) + + self.window.pushButton_listMoveUp.clicked.connect( self.moveComponentUp) @@ -222,13 +234,19 @@ class MainWindow(QtCore.QObject): 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): if time.time() - self.lastAutosave >= 1.0: diff --git a/mainwindow.ui b/mainwindow.ui index d501110..c010caf 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -697,12 +697,16 @@ - Video Bitrate + Video Bitrate (Kbps) - + + + 99999 + + @@ -762,12 +766,16 @@ - Audio Bitrate + Audio Bitrate (Kbps) - + + + 9999 + + diff --git a/video_thread.py b/video_thread.py index d8694a4..f5354be 100644 --- a/video_thread.py +++ b/video_thread.py @@ -109,7 +109,9 @@ class Worker(QtCore.QObject): 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: @@ -147,7 +149,8 @@ class Worker(QtCore.QObject): '-i', inputFile, '-vcodec', vencoder, '-acodec', aencoder, # output audio codec - '-b:a', self.core.settings.value('outputAudioBitrate'), + '-b:v', vbitrate, + '-b:a', abitrate, '-pix_fmt', self.core.settings.value('outputVideoFormat'), '-preset', self.core.settings.value('outputPreset'), '-f', container -- cgit v1.2.3 From 28f07272cc174efe8e219abed683d22f72f18d94 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 11 Jun 2017 17:03:40 -0400 Subject: using tab in component list updates widget and more understandable function names for writing/reading presets --- components/video.py | 11 +++++++++-- core.py | 14 +++++++++----- main.py | 1 - mainwindow.py | 7 +++---- presetmanager.py | 3 +-- 5 files changed, 22 insertions(+), 14 deletions(-) (limited to 'main.py') diff --git a/components/video.py b/components/video.py index c529658..bd1bf96 100644 --- a/components/video.py +++ b/components/video.py @@ -10,8 +10,15 @@ from . import __base__ class Video: '''Video Component Frame-Fetcher''' def __init__(self, **kwargs): - mandatoryArgs = ['ffmpeg', 'videoPath', 'width', 'height', - 'frameRate', 'chunkSize', 'parent'] + mandatoryArgs = [ + 'ffmpeg', # path to ffmpeg, usually core.FFMPEG_BIN + 'videoPath', + 'width', + 'height', + 'frameRate', # frames per second + 'chunkSize', # number of bytes in one frame + 'parent' + ] for arg in mandatoryArgs: try: exec('self.%s = kwargs[arg]' % arg) diff --git a/core.py b/core.py index e69de50..4f30973 100644 --- a/core.py +++ b/core.py @@ -71,7 +71,7 @@ class Core(): return endI def updateComponent(self, i): - print('updating %s' % self.selectedComponents[i]) + # print('updating %s' % self.selectedComponents[i]) self.selectedComponents[i].update() def moduleIndexFor(self, compIndex): @@ -89,7 +89,7 @@ class Core(): with open(internalPath, 'r') as f: internalData = [line for line in f] try: - saveValueStore = dict(eval(internalData[0].strip())) + saveValueStore = Core.presetFromString(internalData[0].strip()) self.createPresetFile( compName, vers, origName, saveValueStore, @@ -120,7 +120,7 @@ class Core(): f.write('[Components]\n') f.write('%s\n' % compName) f.write('%s\n' % str(vers)) - f.write(Core.stringOrderedDict(saveValueStore)) + f.write(Core.presetToString(saveValueStore)) def createProjectFile(self, filepath): '''Create a project file (.avp) using the current program state''' @@ -136,7 +136,7 @@ class Core(): saveValueStore = comp.savePreset() f.write('%s\n' % str(comp)) f.write('%s\n' % str(comp.version())) - f.write('%s\n' % Core.stringOrderedDict(saveValueStore)) + f.write('%s\n' % Core.presetToString(saveValueStore)) return True except: return False @@ -244,6 +244,10 @@ class Core(): return badName @staticmethod - def stringOrderedDict(dictionary): + def presetToString(dictionary): sorted_ = OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])) return repr(sorted_) + + @staticmethod + def presetFromString(string): + return dict(eval(string)) diff --git a/main.py b/main.py index c771aca..7c0727b 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,4 @@ from importlib import import_module -from collections import OrderedDict from PyQt4 import QtGui, uic from PyQt4.QtCore import Qt import sys diff --git a/mainwindow.py b/mainwindow.py index 2f04559..dbbc631 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -1,6 +1,5 @@ from os.path import expanduser from queue import Queue -from collections import OrderedDict from PyQt4 import QtCore, QtGui, uic from PyQt4.QtCore import QSettings, Qt from PyQt4.QtGui import QMenu @@ -159,8 +158,8 @@ class MainWindow(QtCore.QObject): self.window.pushButton_addComponent.setMenu(self.compMenu) componentList.dropEvent = self.componentListChanged - componentList.clicked.connect( - lambda _: self.changeComponentWidget()) + componentList.itemSelectionChanged.connect( + self.changeComponentWidget) self.window.pushButton_removeComponent.clicked.connect( lambda _: self.removeComponent()) @@ -567,7 +566,7 @@ class MainWindow(QtCore.QObject): # version, not used yet i += 1 elif i == 2: - saveValueStore = dict(eval(line)) + saveValueStore = core.Core.presetFromString(line) self.core.selectedComponents[-1].loadPreset( saveValueStore) self.updateComponentTitle(-1) diff --git a/presetmanager.py b/presetmanager.py index 3036f7a..6708d11 100644 --- a/presetmanager.py +++ b/presetmanager.py @@ -1,5 +1,4 @@ from PyQt4 import QtGui, QtCore -from collections import OrderedDict import string import os @@ -171,7 +170,7 @@ class PresetManager(QtGui.QDialog): return with open(filepath, 'r') as f: for line in f: - saveValueStore = dict(eval(line.strip())) + saveValueStore = core.Core.presetFromString(line.strip()) break selectedComponents[index].loadPreset( saveValueStore, -- cgit v1.2.3 From 044fddfa9c5063f61e4a97993efe7cd5b2bae066 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 18 Jun 2017 14:46:08 -0400 Subject: basic commandline functionality using 3 args needs more args so components can be modified without gui --- command.py | 215 +++++++++++++++++++++++++------------------------------- core.py | 40 +++++++---- main.py | 54 ++++++++------ mainwindow.py | 76 +++++++++++--------- video_thread.py | 2 - 5 files changed, 196 insertions(+), 191 deletions(-) (limited to 'main.py') diff --git a/command.py b/command.py index a610d8c..1b07afc 100644 --- a/command.py +++ b/command.py @@ -1,122 +1,97 @@ -# FIXME: commandline functionality broken until we decide how to implement it -''' +from PyQt4 import QtCore +from PyQt4.QtCore import QSettings +import argparse +import os + +import core +import video_thread +from main import LoadDefaultSettings + + class Command(QtCore.QObject): - videoTask = QtCore.pyqtSignal(str, str, str, list) - - def __init__(self): - QtCore.QObject.__init__(self) - self.modules = [] - self.selectedComponents = [] - - import argparse - self.parser = argparse.ArgumentParser( - description='Create a visualization for an audio file') - self.parser.add_argument( - '-i', '--input', dest='input', help='input audio file', required=True) - self.parser.add_argument( - '-o', '--output', dest='output', - help='output video file', required=True) - self.parser.add_argument( - '-b', '--background', dest='bgimage', - help='background image file', required=True) - self.parser.add_argument( - '-t', '--text', dest='text', help='title text', required=True) - self.parser.add_argument( - '-f', '--font', dest='font', help='title font', required=False) - self.parser.add_argument( - '-s', '--fontsize', dest='fontsize', - help='title font size', required=False) - self.parser.add_argument( - '-c', '--textcolor', dest='textcolor', - help='title text color in r,g,b format', required=False) - self.parser.add_argument( - '-C', '--viscolor', dest='viscolor', - help='visualization color in r,g,b format', required=False) - self.parser.add_argument( - '-x', '--xposition', dest='xposition', - help='x position', required=False) - self.parser.add_argument( - '-y', '--yposition', dest='yposition', - help='y position', required=False) - self.parser.add_argument( - '-a', '--alignment', dest='alignment', - help='title alignment', required=False, - type=int, choices=[0, 1, 2]) - self.args = self.parser.parse_args() - - self.settings = QSettings('settings.ini', QSettings.IniFormat) - LoadDefaultSettings(self) - - # load colours as tuples from comma-separated strings - self.textColor = core.Core.RGBFromString( - self.settings.value("textColor", '255, 255, 255')) - self.visColor = core.Core.RGBFromString( - self.settings.value("visColor", '255, 255, 255')) - if self.args.textcolor: - self.textColor = core.Core.RGBFromString(self.args.textcolor) - if self.args.viscolor: - self.visColor = core.Core.RGBFromString(self.args.viscolor) - - # font settings - if self.args.font: - self.font = QFont(self.args.font) - else: - self.font = QFont(self.settings.value("titleFont", QFont())) - - if self.args.fontsize: - self.fontsize = int(self.args.fontsize) - else: - self.fontsize = int(self.settings.value("fontSize", 35)) - if self.args.alignment: - self.alignment = int(self.args.alignment) - else: - self.alignment = int(self.settings.value("alignment", 0)) - - if self.args.xposition: - self.textX = int(self.args.xposition) - else: - self.textX = int(self.settings.value("xPosition", 70)) - - if self.args.yposition: - self.textY = int(self.args.yposition) - else: - self.textY = int(self.settings.value("yPosition", 375)) - - ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~")) - - 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.bgimage, - self.args.text, - self.font, - self.fontsize, - self.alignment, - self.textX, - self.textY, - self.textColor, - self.visColor, - self.args.input, - self.args.output, - self.selectedComponents) - - def videoCreated(self): - self.videoThread.quit() - self.videoThread.wait() - self.cleanUp() - - def cleanUp(self): - self.settings.setValue("titleFont", self.font.toString()) - self.settings.setValue("alignment", str(self.alignment)) - self.settings.setValue("fontSize", str(self.fontsize)) - self.settings.setValue("xPosition", str(self.textX)) - self.settings.setValue("yPosition", str(self.textY)) - self.settings.setValue("visColor", '%s,%s,%s' % self.visColor) - self.settings.setValue("textColor", '%s,%s,%s' % self.textColor) - sys.exit(0) -''' + videoTask = QtCore.pyqtSignal(str, str, list) + + def __init__(self): + QtCore.QObject.__init__(self) + self.core = core.Core() + self.dataDir = self.core.dataDir + + self.parser = argparse.ArgumentParser( + description='Create a visualization for an audio file') + self.parser.add_argument( + '-i', '--input', help='input audio file', required=True) + self.parser.add_argument( + '-o', '--output', help='output video file', required=True) + + # optional arguments + self.parser.add_argument( + 'projpath', metavar='path-to-project', + help='open a project file (.avp)', nargs='?') + + ''' + self.parser.add_argument( + '-b', '--background', dest='bgimage', + help='background image file', required=True) + self.parser.add_argument( + '-t', '--text', dest='text', help='title text', required=True) + self.parser.add_argument( + '-f', '--font', dest='font', help='title font', required=False) + self.parser.add_argument( + '-s', '--fontsize', dest='fontsize', + help='title font size', required=False) + self.parser.add_argument( + '-c', '--textcolor', dest='textcolor', + help='title text color in r,g,b format', required=False) + self.parser.add_argument( + '-C', '--viscolor', dest='viscolor', + help='visualization color in r,g,b format', required=False) + self.parser.add_argument( + '-x', '--xposition', dest='xposition', + help='x position', required=False) + self.parser.add_argument( + '-y', '--yposition', dest='yposition', + help='y position', required=False) + self.parser.add_argument( + '-a', '--alignment', dest='alignment', + help='title alignment', required=False, + type=int, choices=[0, 1, 2]) + ''' + + 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.createAudioVisualisation() + + 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, + self.core.selectedComponents) + + def videoCreated(self): + self.videoThread.quit() + self.videoThread.wait() + self.cleanUp() + + def showMessage(self, **kwargs): + print(kwargs['msg']) + if 'detail' in kwargs: + print(kwargs['detail']) + + def drawPreview(self, *args): + pass + + def cleanUp(self, *args): + pass diff --git a/core.py b/core.py index e4a7a6c..5e4071a 100644 --- a/core.py +++ b/core.py @@ -37,6 +37,7 @@ class Core(): '*.wav', '*.ogg', '*.fla', + '*.flac', '*.aac', ]) self.imageFormats = Core.appendUppercase([ @@ -76,9 +77,10 @@ class Core(): for i, component in enumerate(self.selectedComponents): component.compPos = i - def insertComponent(self, compPos, moduleIndex): + def insertComponent(self, compPos, moduleIndex, loader): + '''Creates a new component''' if compPos < 0: - compPos = len(self.selectedComponents) -1 + compPos = len(self.selectedComponents) if len(self.selectedComponents) > 50: return None @@ -87,8 +89,14 @@ class Core(): 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): @@ -115,11 +123,8 @@ class Core(): index = compNames.index(compName) return self.moduleIndexes[index] - def clearPreset(self, compIndex, loader=None): - '''Clears a preset from a component''' + def clearPreset(self, compIndex): self.selectedComponents[compIndex].currentPreset = None - if loader: - loader.updateComponentTitle(compIndex) def openPreset(self, filepath, compIndex, presetName): '''Applies a preset to a specific component''' @@ -148,9 +153,10 @@ class Core(): return saveValueStore def openProject(self, loader, filepath): - '''loader is the object calling this method (mainwindow/command) - which implements an insertComponent method''' + '''loader is the object calling this method which must have + its own showMessage(**kwargs) method for displaying errors''' errcode, data = self.parseAvFile(filepath) + print(data) if errcode == 0: try: for i, tup in enumerate(data['Components']): @@ -169,10 +175,13 @@ class Core(): # saved preset was renamed or deleted clearThis = True - # insert component into the loader - i = loader.insertComponent( - self.moduleIndexFor(name), -1) + # 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: @@ -190,7 +199,9 @@ class Core(): (self.selectedComponents[i], e)) if clearThis: - self.clearPreset(i, loader) + self.clearPreset(i) + if hasattr(loader, 'updateComponentTitle'): + loader.updateComponentTitle(i) except: errcode = 1 data = sys.exc_info() @@ -202,7 +213,8 @@ class Core(): # probably just an old version, still loadable print('file missing value: %s' % value) return - loader.createNewProject() + if hasattr(loader, 'createNewProject'): + loader.createNewProject() msg = '%s: %s' % (typ.__name__, value) loader.showMessage( msg="Project file '%s' is corrupted." % filepath, diff --git a/main.py b/main.py index 7c0727b..140392c 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,5 @@ -from importlib import import_module from PyQt4 import QtGui, uic -from PyQt4.QtCore import Qt import sys -import io import os import atexit import signal @@ -10,7 +7,6 @@ import signal import core import preview_thread import video_thread -from mainwindow import * def LoadDefaultSettings(self): @@ -36,34 +32,50 @@ def LoadDefaultSettings(self): } for parm, value in default.items(): + print(parm, self.settings.value(parm)) if self.settings.value(parm) is None: self.settings.setValue(parm, value) if __name__ == "__main__": - ''' FIXME commandline functionality broken until we decide how to implement - if len(sys.argv) > 1: - # command line mode - app = QtGui.QApplication(sys.argv, False) - command = Command() - signal.signal(signal.SIGINT, command.cleanUp) - sys.exit(app.exec_()) + 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") - window = uic.loadUi(os.path.join( - os.path.dirname(os.path.realpath(__file__)), "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) + if mode == 'cmd': + from command import * + + main = Command() + + elif mode == 'gui': + from mainwindow import * + + window = uic.loadUi(os.path.join( + os.path.dirname(os.path.realpath(__file__)), "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) + main = MainWindow(window, proj) + # applicable to both modes signal.signal(signal.SIGINT, main.cleanUp) atexit.register(main.cleanUp) diff --git a/mainwindow.py b/mainwindow.py index f722158..2a8762d 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -45,7 +45,7 @@ class MainWindow(QtCore.QObject): processTask = QtCore.pyqtSignal() videoTask = QtCore.pyqtSignal(str, str, list) - def __init__(self, window): + def __init__(self, window, project): QtCore.QObject.__init__(self) # print('main thread id: {}'.format(QtCore.QThread.currentThreadId())) @@ -151,7 +151,7 @@ class MainWindow(QtCore.QObject): for i, comp in enumerate(self.core.modules): action = self.compMenu.addAction(comp.Component.__doc__) action.triggered[()].connect( - lambda item=i: self.insertComponent(item)) + lambda item=i: self.core.insertComponent(0, item, self)) self.window.pushButton_addComponent.setMenu(self.compMenu) @@ -211,24 +211,36 @@ class MainWindow(QtCore.QObject): self.openPresetManager ) - # Show the window and load current project window.show() - self.currentProject = self.settings.value("currentProject") - if self.autosaveExists(identical=True): - # delete autosave if it's identical to the project - 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: + if project and project != self.autosavePath: + # 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() + self.drawPreview(True) def cleanUp(self): self.timer.stop() @@ -240,7 +252,8 @@ class MainWindow(QtCore.QObject): appName = 'Audio Visualizer' if self.currentProject: appName += ' - %s' % \ - os.path.basename(self.currentProject)[:-4] + os.path.splitext( + os.path.basename(self.currentProject))[0] self.window.setWindowTitle(appName) @QtCore.pyqtSlot(int, dict) @@ -252,7 +265,6 @@ class MainWindow(QtCore.QObject): else: modified = (presetStore != self.core.savedPresets[name]) else: - print(pos, presetStore) modified = bool(presetStore) if pos < 0: pos = len(self.core.selectedComponents)-1 @@ -306,10 +318,14 @@ class MainWindow(QtCore.QObject): self.lastAutosave = time.time() def autosaveExists(self, identical=True): - if self.currentProject and os.path.exists(self.autosavePath) \ - and filecmp.cmp( - self.autosavePath, self.currentProject) == identical: - return 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): @@ -411,6 +427,7 @@ class MainWindow(QtCore.QObject): 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) @@ -437,19 +454,11 @@ class MainWindow(QtCore.QObject): def showPreviewImage(self, image): self.previewWindow.changePixmap(image) - def insertComponent(self, moduleIndex, compPos=0): + def insertComponent(self, index): componentList = self.window.listWidget_componentList stackedWidget = self.window.stackedWidget - if compPos < 0: - compPos = componentList.count() - - index = self.core.insertComponent( - compPos, moduleIndex) - if index == None: - self.showMessage(msg="Too many components!") - return None - row = componentList.insertItem( + componentList.insertItem( index, self.core.selectedComponents[index].__doc__) componentList.setCurrentRow(index) @@ -458,11 +467,10 @@ class MainWindow(QtCore.QObject): self.core.selectedComponents[index].modified.connect( self.updateComponentTitle) - self.pages.insert(index, self.core.selectedComponents[index].widget(self)) + self.pages.insert(index, self.core.selectedComponents[index].page) stackedWidget.insertWidget(index, self.pages[index]) stackedWidget.setCurrentIndex(index) - self.core.updateComponent(index) return index def removeComponent(self): diff --git a/video_thread.py b/video_thread.py index d7220f1..2255259 100644 --- a/video_thread.py +++ b/video_thread.py @@ -27,7 +27,6 @@ class Worker(QtCore.QObject): self.core = core.Core() self.core.settings = parent.settings self.modules = parent.core.modules - self.stackedWidget = parent.window.stackedWidget self.parent = parent parent.videoTask.connect(self.createVideo) self.sampleSize = 1470 @@ -280,7 +279,6 @@ class Worker(QtCore.QObject): self.error = False self.canceled = False - self.parent.drawPreview() self.stopped = True self.encoding.emit(False) self.videoCreated.emit() -- cgit v1.2.3 From 5c74d496a960042ed4a4279328dc81e23dfdc1d9 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 22 Jun 2017 18:40:34 -0400 Subject: preset-loading and basic args from commandline also made some docstrings more informative --- command.py | 30 ++++++++++++---------- components/__base__.py | 69 +++++++++++++++++++++++++++++++++++--------------- components/image.py | 17 +++++++++++++ components/original.py | 12 +++++++++ components/text.py | 10 ++++++++ components/video.py | 16 ++++++++++++ core.py | 22 ++++++++++------ main.py | 12 ++++----- mainwindow.py | 1 - video_thread.py | 29 +++++++++++---------- 10 files changed, 156 insertions(+), 62 deletions(-) (limited to 'main.py') diff --git a/command.py b/command.py index d56c64b..97eddd2 100644 --- a/command.py +++ b/command.py @@ -36,7 +36,7 @@ class Command(QtCore.QObject): help='open a project file (.avp)', nargs='?') self.parser.add_argument( '-c', '--comp', metavar=('LAYER', 'NAME', 'ARG'), - help='create/edit component NAME at LAYER.' + help='create component NAME at LAYER.' '"help" for information about possible args', nargs=3, action='append') @@ -80,12 +80,18 @@ class Command(QtCore.QObject): if self.args.comp: for comp in self.args.comp: pos, name, arg = comp + 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() + quit(1) modI = self.core.moduleIndexFor(realName) - self.core.insertComponent(int(pos), modI, self) + i = self.core.insertComponent(pos, modI, self) + self.core.selectedComponents[i].command(arg) self.createAudioVisualisation() @@ -99,12 +105,12 @@ class Command(QtCore.QObject): self.videoTask.emit( self.args.input, self.args.output, - self.core.selectedComponents) + list(reversed(self.core.selectedComponents)) + ) def videoCreated(self): self.videoThread.quit() self.videoThread.wait() - self.cleanUp() def showMessage(self, **kwargs): print(kwargs['msg']) @@ -114,22 +120,20 @@ class Command(QtCore.QObject): def drawPreview(self, *args): pass - def cleanUp(self, *args): - pass - def parseCompName(self, name): '''Deduces a proper component name out of a commandline arg''' - compFileNames = [ \ - os.path.splitext(os.path.basename( - mod.__file__))[0] \ - for mod in self.core.modules \ - ] 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] diff --git a/components/__base__.py b/components/__base__.py index 88f22d4..e43a517 100644 --- a/components/__base__.py +++ b/components/__base__.py @@ -1,5 +1,6 @@ from PyQt4 import QtGui, QtCore from PIL import Image +import os class Component(QtCore.QObject): @@ -7,11 +8,12 @@ class Component(QtCore.QObject): # modified = QtCore.pyqtSignal(int, bool) - def __init__(self, moduleIndex, compPos): + def __init__(self, moduleIndex, compPos, core): super().__init__() self.currentPreset = None self.moduleIndex = moduleIndex self.compPos = compPos + self.core = core def __str__(self): return self.__doc__ @@ -32,24 +34,59 @@ class Component(QtCore.QObject): # read your widget values, then call super().update() def loadPreset(self, presetDict, presetName): - '''Children should take (presetDict, presetName=None) as args''' - - # Use super().loadPreset(presetDict, presetName) - # Then update your widgets using the preset dict + '''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 savePreset(self): - return {} - ''' + 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( + 'To open a preset for this component:\n' + ' "preset=Preset Name"\n') + 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() @@ -63,7 +100,7 @@ class Component(QtCore.QObject): return None, None def RGBFromString(self, string): - ''' turns an RGB string like "255, 255, 255" into a tuple ''' + ''' 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: @@ -83,14 +120,10 @@ class Component(QtCore.QObject): self.parent = parent page = uic.loadUi(os.path.join( os.path.dirname(os.path.realpath(__file__)), 'example.ui')) - # connect widgets signals + # --- 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')) @@ -102,12 +135,6 @@ class Component(QtCore.QObject): height = int(self.worker.core.settings.value('outputHeight')) image = Image.new("RGBA", (width, height), (0,0,0,0)) return image - - def cancel(self): - self.canceled = True - - def reset(self): - self.canceled = False ''' class BadComponentInit(Exception): diff --git a/components/image.py b/components/image.py index b6aa29b..d0e1894 100644 --- a/components/image.py +++ b/components/image.py @@ -92,3 +92,20 @@ class Component(__base__.Component): 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='): + if os.path.exists(arg): + try: + Image.open(arg) + self.imagePath = arg + self.stretched = True + return True + except OSError as e: + print("Not a supported image format") + quit(1) + super().command(arg) + + def commandHelp(self): + print('Give a complete filepath to an image to load that ' + 'image with default settings.') diff --git a/components/original.py b/components/original.py index 5e2f9d4..328d64f 100644 --- a/components/original.py +++ b/components/original.py @@ -183,3 +183,15 @@ class Component(__base__.Component): return im + def command(self, arg): + if not arg.startswith('preset='): + if arg == 'classic': + self.layout = 0; return + elif arg == 'split': + self.layout = 1; return + elif arg == 'bottom': + self.layout = 2; return + super().command(arg) + + def commandHelp(self): + print('Give a layout name: classic, split, or bottom') diff --git a/components/text.py b/components/text.py index f8ef7b3..6c465b1 100644 --- a/components/text.py +++ b/components/text.py @@ -146,3 +146,13 @@ class Component(__base__.Component): return self.page.lineEdit_textColor.setText(RGBstring) self.page.pushButton_textColor.setStyleSheet(btnStyle) + + def commandHelp(self, arg): + print('Enter a string to use as centred white text. ' + 'Use quotes around the string to escape spaces.') + + def command(self, arg): + if not arg.startswith('preset='): + self.title = arg + return True + super().command(arg) diff --git a/components/video.py b/components/video.py index 3d43a18..dd385b4 100644 --- a/components/video.py +++ b/components/video.py @@ -221,6 +221,22 @@ class Component(__base__.Component): width, height = scale(self.scale, width, height, int) self.chunkSize = 4*width*height + def command(self, arg): + if not arg.startswith('preset='): + if os.path.exists(arg): + if os.path.splitext(arg)[1] in self.core.videoFormats: + self.videoPath = arg + self.scale = 100 + return True + else: + print("Not a supported video format") + quit(1) + super().command(arg) + + def commandHelp(self): + print('Give a complete filepath to a video to load that ' + 'video with default settings.') + def scale(scale, width, height, returntype=None): width = (float(width) / 100.0) * float(scale) height = (float(height) / 100.0) * float(scale) diff --git a/core.py b/core.py index 2dde464..42eb44e 100644 --- a/core.py +++ b/core.py @@ -86,7 +86,7 @@ class Core(): return None component = self.modules[moduleIndex].Component( - moduleIndex, compPos) + moduleIndex, compPos, self) self.selectedComponents.insert( compPos, component) @@ -142,6 +142,10 @@ class Core(): self.savedPresets[presetName] = dict(saveValueStore) return True + def getPresetDir(self, comp): + return os.path.join( + self.presetDir, str(comp), str(comp.version())) + def getPreset(self, filepath): '''Returns the preset dict stored at this filepath''' if not os.path.exists(filepath): @@ -153,10 +157,11 @@ class Core(): 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''' + ''' loader is the object calling this method which must have + its own showMessage(**kwargs) method for displaying errors. + ''' errcode, data = self.parseAvFile(filepath) - print(data) + #print(data) if errcode == 0: try: for i, tup in enumerate(data['Components']): @@ -224,12 +229,14 @@ class Core(): def parseAvFile(self, filepath): '''Parses an avp (project) or avl (preset package) file. - Returns data usable by another method.''' + 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 given avp or avl line is a section header''' + '''Decides if a file line is a section header''' validSections = ('Components') line = line.strip() newSection = '' @@ -313,8 +320,7 @@ class Core(): 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 - the args for the filepath.''' + 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): diff --git a/main.py b/main.py index 140392c..e04d002 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,6 @@ from PyQt4 import QtGui, uic import sys import os -import atexit -import signal import core import preview_thread @@ -32,7 +30,7 @@ def LoadDefaultSettings(self): } for parm, value in default.items(): - print(parm, self.settings.value(parm)) + #print(parm, self.settings.value(parm)) if self.settings.value(parm) is None: self.settings.setValue(parm, value) @@ -62,6 +60,8 @@ if __name__ == "__main__": elif mode == 'gui': from mainwindow import * + import atexit + import signal window = uic.loadUi(os.path.join( os.path.dirname(os.path.realpath(__file__)), "mainwindow.ui")) @@ -73,10 +73,10 @@ if __name__ == "__main__": window.resize(window.width() * (dpi / 96), window.height() * (dpi / 96)) # window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0) + signal.signal(signal.SIGINT, main.cleanUp) + atexit.register(main.cleanUp) + main = MainWindow(window, proj) # applicable to both modes - signal.signal(signal.SIGINT, main.cleanUp) - atexit.register(main.cleanUp) - sys.exit(app.exec_()) diff --git a/mainwindow.py b/mainwindow.py index 2a8762d..6023831 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -597,7 +597,6 @@ class MainWindow(QtCore.QObject): self.openProject(filename) def openProject(self, filepath, prompt=True): - print('opening', filepath) if not filepath or not os.path.exists(filepath) \ or not filepath.endswith('.avp'): self.updateWindowTitle() diff --git a/video_thread.py b/video_thread.py index 2255259..e6c6531 100644 --- a/video_thread.py +++ b/video_thread.py @@ -29,7 +29,7 @@ class Worker(QtCore.QObject): self.modules = parent.core.modules self.parent = parent parent.videoTask.connect(self.createVideo) - self.sampleSize = 1470 + self.sampleSize = 1470 # 44100 / 30 = 1470 self.canceled = False self.error = False self.stopped = False @@ -99,7 +99,8 @@ class Worker(QtCore.QObject): # test if user has libfdk_aac encoders = sp.check_output( - self.core.FFMPEG_BIN + " -encoders -hide_banner", shell=True) + self.core.FFMPEG_BIN + " -encoders -hide_banner", + shell=True) encoders = encoders.decode("utf-8") @@ -120,15 +121,15 @@ class Worker(QtCore.QObject): vencoders = options['video-codecs'][vcodec] aencoders = options['audio-codecs'][acodec] - print(encoders) + #print(encoders) for encoder in vencoders: - print(encoder) + #print(encoder) if encoder in encoders: vencoder = encoder break for encoder in aencoders: - print(encoder) + #print(encoder) if encoder in encoders: aencoder = encoder break @@ -161,16 +162,15 @@ class Worker(QtCore.QObject): ffmpegCommand.append('-2') ffmpegCommand.append(outputFile) - self.out_pipe = sp.Popen( - ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout) - # create video for output + # ### Now start creating video for output ### numpy.seterr(divide='ignore') - # initialize components - print('loaded components:', - ["%s%s" % (num, str(component)) for num, - component in enumerate(self.components)]) + # 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): @@ -190,14 +190,17 @@ class Worker(QtCore.QObject): 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() - self.renderThreads = [] # 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")) -- cgit v1.2.3 From b21a953dda4ec54d494c813af8f687d53d3675d9 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 22 Jun 2017 19:59:31 -0400 Subject: bugfixes --- command.py | 36 ++++++------------------------------ components/__base__.py | 4 ++++ components/text.py | 4 +++- components/video.py | 1 + core.py | 2 +- main.py | 4 ++-- 6 files changed, 17 insertions(+), 34 deletions(-) (limited to 'main.py') diff --git a/command.py b/command.py index 97eddd2..65fe782 100644 --- a/command.py +++ b/command.py @@ -16,13 +16,14 @@ class Command(QtCore.QObject): 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 ~/Pictures/thisWeeksPicture.jpg ' - '-c 1 vis classic') + '-c 1 video "preset=My Logo" -c 2 vis classic') self.parser.add_argument( '-i', '--input', metavar='SOUND', help='input audio file', required=True) @@ -40,35 +41,6 @@ class Command(QtCore.QObject): '"help" for information about possible args', nargs=3, action='append') - ''' - self.parser.add_argument( - '-b', '--background', dest='bgimage', - help='background image file', required=True) - self.parser.add_argument( - '-t', '--text', dest='text', help='title text', required=True) - self.parser.add_argument( - '-f', '--font', dest='font', help='title font', required=False) - self.parser.add_argument( - '-s', '--fontsize', dest='fontsize', - help='title font size', required=False) - self.parser.add_argument( - '-c', '--textcolor', dest='textcolor', - help='title text color in r,g,b format', required=False) - self.parser.add_argument( - '-C', '--viscolor', dest='viscolor', - help='visualization color in r,g,b format', required=False) - self.parser.add_argument( - '-x', '--xposition', dest='xposition', - help='x position', required=False) - self.parser.add_argument( - '-y', '--yposition', dest='yposition', - help='y position', required=False) - self.parser.add_argument( - '-a', '--alignment', dest='alignment', - help='title alignment', required=False, - type=int, choices=[0, 1, 2]) - ''' - self.args = self.parser.parse_args() self.settings = QSettings( os.path.join(self.dataDir, 'settings.ini'), QSettings.IniFormat) @@ -76,6 +48,9 @@ class Command(QtCore.QObject): 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: @@ -111,6 +86,7 @@ class Command(QtCore.QObject): def videoCreated(self): self.videoThread.quit() self.videoThread.wait() + quit(0) def showMessage(self, **kwargs): print(kwargs['msg']) diff --git a/components/__base__.py b/components/__base__.py index e43a517..bdf6fdd 100644 --- a/components/__base__.py +++ b/components/__base__.py @@ -124,6 +124,10 @@ class Component(QtCore.QObject): 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')) diff --git a/components/text.py b/components/text.py index 6c465b1..536a9ba 100644 --- a/components/text.py +++ b/components/text.py @@ -19,12 +19,14 @@ class Component(__base__.Component): 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 - self.xPosition = width / 2 + 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( diff --git a/components/video.py b/components/video.py index dd385b4..66c98ce 100644 --- a/components/video.py +++ b/components/video.py @@ -227,6 +227,7 @@ class Component(__base__.Component): if os.path.splitext(arg)[1] in self.core.videoFormats: self.videoPath = arg self.scale = 100 + self.loopVideo = True return True else: print("Not a supported video format") diff --git a/core.py b/core.py index 42eb44e..2177071 100644 --- a/core.py +++ b/core.py @@ -80,7 +80,7 @@ class Core(): def insertComponent(self, compPos, moduleIndex, loader): '''Creates a new component''' - if compPos < 0: + if compPos < 0 or compPos > len(self.selectedComponents): compPos = len(self.selectedComponents) if len(self.selectedComponents) > 50: return None diff --git a/main.py b/main.py index e04d002..3fd4234 100644 --- a/main.py +++ b/main.py @@ -73,10 +73,10 @@ if __name__ == "__main__": 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) - main = MainWindow(window, proj) - # applicable to both modes sys.exit(app.exec_()) -- cgit v1.2.3 From 8c9914850e9987d4f05e8b88dedb058ffbb4f53f Mon Sep 17 00:00:00 2001 From: DH4 Date: Fri, 23 Jun 2017 02:39:56 -0500 Subject: cx_freeze Path Updates --- core.py | 8 ++++++- main.py | 11 ++++++++-- mainwindow.py | 6 ++---- preview_thread.py | 2 +- setup.py | 63 ++++++++++++++++++++++++++++++++++++------------------- video_thread.py | 2 +- 6 files changed, 62 insertions(+), 30 deletions(-) (limited to 'main.py') diff --git a/core.py b/core.py index e4a7a6c..e5a9b70 100644 --- a/core.py +++ b/core.py @@ -22,7 +22,13 @@ class Core(): self.dataDir = QDesktopServices.storageLocation( QDesktopServices.DataLocation) self.presetDir = os.path.join(self.dataDir, 'presets') - self.wd = os.path.dirname(os.path.realpath(__file__)) + 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', diff --git a/main.py b/main.py index 7c0727b..08add50 100644 --- a/main.py +++ b/main.py @@ -52,8 +52,15 @@ if __name__ == "__main__": app = QtGui.QApplication(sys.argv) app.setApplicationName("audio-visualizer") app.setOrganizationName("audio-visualizer") - window = uic.loadUi(os.path.join( - os.path.dirname(os.path.realpath(__file__)), "mainwindow.ui")) + + 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() diff --git a/mainwindow.py b/mainwindow.py index e1553f6..d21ca49 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -63,9 +63,7 @@ class MainWindow(QtGui.QMainWindow): LoadDefaultSettings(self) self.presetManager = PresetManager( uic.loadUi( - os.path.join(os.path.dirname(os.path.realpath(__file__)), - 'presetmanager.ui')), - self) + os.path.join(self.core.wd, 'presetmanager.ui')), self) if not os.path.exists(self.dataDir): os.makedirs(self.dataDir) @@ -143,7 +141,7 @@ class MainWindow(QtGui.QMainWindow): window.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings) self.previewWindow = PreviewWindow(self, os.path.join( - os.path.dirname(os.path.realpath(__file__)), "background.png")) + self.core.wd, "background.png")) window.verticalLayout_previewWrapper.addWidget(self.previewWindow) # Make component buttons diff --git a/preview_thread.py b/preview_thread.py index e3e8279..eabf715 100644 --- a/preview_thread.py +++ b/preview_thread.py @@ -23,7 +23,7 @@ class Worker(QtCore.QObject): self.stackedWidget = parent.window.stackedWidget self.background = Image.new("RGBA", (1920, 1080), (0, 0, 0, 0)) self.background.paste(Image.open(os.path.join( - os.path.dirname(os.path.realpath(__file__)), "background.png"))) + self.core.wd, "background.png"))) @pyqtSlot(str, list) def createPreviewImage(self, components): diff --git a/setup.py b/setup.py index 0d9cbc4..48034dc 100644 --- a/setup.py +++ b/setup.py @@ -1,30 +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", - "ctypes", - "curses", - "distutils", - "email", - "html", - "http", - "json", - "xmlrpc", - "nose" - ], include_files = ["main.ui"]) -import sys -base = 'Win32GUI' if sys.platform=='win32' else None +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') + Executable( + 'main.py', + base=base, + targetName='audio-visualizer-python' + ) ] -setup(name='audio-visualizer-python', - version = '1.0', - description = 'a little GUI tool to render visualization videos of audio files', - options = dict(build_exe = buildOptions), - executables = executables) + +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/video_thread.py b/video_thread.py index d7220f1..9740641 100644 --- a/video_thread.py +++ b/video_thread.py @@ -69,7 +69,7 @@ class Worker(QtCore.QObject): def previewDispatch(self): background = Image.new("RGBA", (1920, 1080), (0, 0, 0, 0)) background.paste(Image.open(os.path.join( - os.path.dirname(os.path.realpath(__file__)), "background.png"))) + self.core.wd, "background.png"))) background = background.resize((self.width, self.height)) while not self.stopped: -- cgit v1.2.3 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 (limited to 'main.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