diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/command.py | 42 | ||||
| -rw-r--r-- | src/component.py | 61 | ||||
| -rw-r--r-- | src/components/color.py | 17 | ||||
| -rw-r--r-- | src/components/image.py | 29 | ||||
| -rw-r--r-- | src/components/original.py | 11 | ||||
| -rw-r--r-- | src/components/sound.py | 89 | ||||
| -rw-r--r-- | src/components/sound.ui | 122 | ||||
| -rw-r--r-- | src/components/text.py | 36 | ||||
| -rw-r--r-- | src/components/video.py | 67 | ||||
| -rw-r--r-- | src/components/video.ui | 17 | ||||
| -rw-r--r-- | src/core.py | 135 | ||||
| -rw-r--r-- | src/frame.py | 23 | ||||
| -rw-r--r-- | src/main.py | 11 | ||||
| -rw-r--r-- | src/mainwindow.py | 30 | ||||
| -rw-r--r-- | src/presetmanager.py | 1 | ||||
| -rw-r--r-- | src/preview_thread.py | 47 | ||||
| -rw-r--r-- | src/video_thread.py | 234 |
17 files changed, 721 insertions, 251 deletions
diff --git a/src/command.py b/src/command.py index be194d8..84d798d 100644 --- a/src/command.py +++ b/src/command.py @@ -7,15 +7,15 @@ from PyQt5 import QtCore import argparse import os import sys +import time import core -import video_thread from toolkit import LoadDefaultSettings class Command(QtCore.QObject): - videoTask = QtCore.pyqtSignal(str, str, list) + createVideo = QtCore.pyqtSignal() def __init__(self): QtCore.QObject.__init__(self) @@ -112,21 +112,35 @@ class Command(QtCore.QObject): quit(1) def createAudioVisualisation(self, input, output): - self.videoThread = QtCore.QThread(self) - self.videoWorker = video_thread.Worker(self) - self.videoWorker.moveToThread(self.videoThread) - self.videoWorker.videoCreated.connect(self.videoCreated) - - self.videoThread.start() - self.videoTask.emit( - input, - output, - list(reversed(self.core.selectedComponents)) + self.core.selectedComponents = list( + reversed(self.core.selectedComponents)) + self.core.componentListChanged() + self.worker = self.core.newVideoWorker( + self, input, output ) + self.worker.videoCreated.connect(self.videoCreated) + self.lastProgressUpdate = time.time() + self.worker.progressBarSetText.connect(self.progressBarSetText) + self.createVideo.emit() + + @QtCore.pyqtSlot(str) + def progressBarSetText(self, value): + if 'Export ' in value: + # Don't duplicate completion/failure messages + return + if not value.startswith('Exporting') \ + and time.time() - self.lastProgressUpdate >= 0.05: + # Show most messages very often + print(value) + elif time.time() - self.lastProgressUpdate >= 2.0: + # Give user time to read ffmpeg's output during the export + print('##### %s' % value) + else: + return + self.lastProgressUpdate = time.time() + @QtCore.pyqtSlot() def videoCreated(self): - self.videoThread.quit() - self.videoThread.wait() quit(0) def showMessage(self, **kwargs): diff --git a/src/component.py b/src/component.py index 648a6d6..2b297d1 100644 --- a/src/component.py +++ b/src/component.py @@ -24,19 +24,42 @@ class Component(QtCore.QObject): return self.__doc__ def version(self): - # change this number to identify new versions of a component + ''' + Change this number to identify new versions of a component + ''' return 1 + def properties(self): + ''' + Return a list of properties to signify if your component is + non-animated ('static'), returns sound ('audio'), or has + encountered an error in configuration ('error'). + ''' + return [] + + def error(self): + ''' + Return a string containing an error message, or None for a default. + ''' + return + def cancel(self): - # please stop any lengthy process in response to this variable + ''' + 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() + ''' + Read your widget values from self.page, then call super().update() + ''' + self.parent.drawPreview() + saveValueStore = self.savePreset() + saveValueStore['preset'] = self.currentPreset + self.modified.emit(self.compPos, saveValueStore) def loadPreset(self, presetDict, presetName): ''' @@ -48,17 +71,18 @@ class Component(QtCore.QObject): if presetName is not None else presetDict['preset'] def preFrameRender(self, **kwargs): - ''' Triggered only before a video is exported (video_thread.py) + ''' + 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 MainWindow if needed + Use the latter two signals to update the MainWindow if needed for a long initialization procedure (i.e., for a visualizer) ''' - for var, value in kwargs.items(): - exec('self.%s = value' % var) + for key, value in kwargs.items(): + setattr(self, key, value) def command(self, arg): ''' @@ -128,16 +152,11 @@ class Component(QtCore.QObject): def widget(self, parent): self.parent = parent - page = uic.loadUi(os.path.join( - os.path.dirname(os.path.realpath(__file__)), 'example.ui')) + page = self.loadUi('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')) @@ -153,9 +172,21 @@ class Component(QtCore.QObject): image = BlankFrame(width, height) return image + def audio(self): + \''' + Return audio to mix into master as a tuple with two elements: + The first element can be: + - A string (path to audio file), + - Or an object that returns audio data through a pipe + The second element must be a dictionary of ffmpeg parameters + to apply to the input stream. + \''' + @classmethod def names(cls): - # Alternative names for renaming a component between project files + \''' + Alternative names for renaming a component between project files. + \''' return [] ''' diff --git a/src/components/color.py b/src/components/color.py index b87f3e9..ef4dd95 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -15,6 +15,7 @@ class Component(Component): def widget(self, parent): self.parent = parent + self.settings = self.parent.core.settings page = self.loadUi('color.ui') self.color1 = (0, 0, 0) @@ -42,9 +43,9 @@ class Component(Component): page.spinBox_x.valueChanged.connect(self.update) page.spinBox_y.valueChanged.connect(self.update) page.spinBox_width.setValue( - int(parent.settings.value("outputWidth"))) + int(self.settings.value("outputWidth"))) page.spinBox_height.setValue( - int(parent.settings.value("outputHeight"))) + int(self.settings.value("outputHeight"))) page.lineEdit_color1.textChanged.connect(self.update) page.lineEdit_color2.textChanged.connect(self.update) @@ -109,21 +110,19 @@ class Component(Component): self.page.pushButton_color2.setEnabled(False) 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')) + width = int(self.settings.value('outputWidth')) + height = int(self.settings.value('outputHeight')) return self.drawFrame(width, height) - def preFrameRender(self, **kwargs): - super().preFrameRender(**kwargs) + def properties(self): return ['static'] def frameRender(self, layerNo, frameNo): - width = int(self.worker.core.settings.value('outputWidth')) - height = int(self.worker.core.settings.value('outputHeight')) + width = int(self.settings.value('outputWidth')) + height = int(self.settings.value('outputHeight')) return self.drawFrame(width, height) def drawFrame(self, width, height): diff --git a/src/components/image.py b/src/components/image.py index 6edd893..c0d1c0d 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -13,7 +13,7 @@ class Component(Component): def widget(self, parent): self.parent = parent - self.settings = parent.settings + self.settings = self.parent.core.settings page = self.loadUi('image.ui') page.lineEdit_image.textChanged.connect(self.update) @@ -38,22 +38,29 @@ class Component(Component): self.yPosition = self.page.spinBox_y.value() self.stretched = self.page.checkBox_stretch.isChecked() self.mirror = self.page.checkBox_mirror.isChecked() - self.parent.drawPreview() + super().update() def previewRender(self, previewWorker): - self.imageFormats = previewWorker.core.imageFormats - width = int(previewWorker.core.settings.value('outputWidth')) - height = int(previewWorker.core.settings.value('outputHeight')) + width = int(self.settings.value('outputWidth')) + height = int(self.settings.value('outputHeight')) return self.drawFrame(width, height) - def preFrameRender(self, **kwargs): - super().preFrameRender(**kwargs) - return ['static'] + def properties(self): + props = ['static'] + if not os.path.exists(self.imagePath): + props.append('error') + return props + + def error(self): + if not self.imagePath: + return "There is no image selected." + if not os.path.exists(self.imagePath): + return "The image selected does not exist!" def frameRender(self, layerNo, frameNo): - width = int(self.worker.core.settings.value('outputWidth')) - height = int(self.worker.core.settings.value('outputHeight')) + width = int(self.settings.value('outputWidth')) + height = int(self.settings.value('outputHeight')) return self.drawFrame(width, height) def drawFrame(self, width, height): @@ -110,7 +117,7 @@ class Component(Component): imgDir = self.settings.value("componentDir", os.path.expanduser("~")) filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.page, "Choose Image", imgDir, - "Image Files (%s)" % " ".join(self.imageFormats)) + "Image Files (%s)" % " ".join(self.core.imageFormats)) if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_image.setText(filename) diff --git a/src/components/original.py b/src/components/original.py index 638095d..f5776a4 100644 --- a/src/components/original.py +++ b/src/components/original.py @@ -21,6 +21,7 @@ class Component(Component): def widget(self, parent): self.parent = parent + self.settings = self.parent.core.settings self.visColor = (255, 255, 255) self.scale = 20 self.y = 0 @@ -50,7 +51,7 @@ class Component(Component): self.visColor = self.RGBFromString(self.page.lineEdit_visColor.text()) self.scale = self.page.spinBox_scale.value() self.y = self.page.spinBox_y.value() - self.parent.drawPreview() + super().update() def loadPreset(self, pr, presetName=None): @@ -76,8 +77,8 @@ class Component(Component): def previewRender(self, previewWorker): spectrum = numpy.fromfunction( lambda x: float(self.scale)/2500*(x-128)**2, (255,), dtype="int16") - width = int(previewWorker.core.settings.value('outputWidth')) - height = int(previewWorker.core.settings.value('outputHeight')) + width = int(self.settings.value('outputWidth')) + height = int(self.settings.value('outputHeight')) return self.drawBars( width, height, spectrum, self.visColor, self.layout ) @@ -88,8 +89,8 @@ class Component(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')) + self.width = int(self.settings.value('outputWidth')) + self.height = int(self.settings.value('outputHeight')) for i in range(0, len(self.completeAudioArray), self.sampleSize): if self.canceled: diff --git a/src/components/sound.py b/src/components/sound.py new file mode 100644 index 0000000..4a5714b --- /dev/null +++ b/src/components/sound.py @@ -0,0 +1,89 @@ +from PyQt5 import QtGui, QtCore, QtWidgets +import os + +from component import Component +from frame import BlankFrame + + +class Component(Component): + '''Sound''' + + modified = QtCore.pyqtSignal(int, dict) + + def widget(self, parent): + self.parent = parent + self.settings = parent.settings + page = self.loadUi('sound.ui') + + page.lineEdit_sound.textChanged.connect(self.update) + page.pushButton_sound.clicked.connect(self.pickSound) + + self.page = page + return page + + def update(self): + self.sound = self.page.lineEdit_sound.text() + super().update() + + def previewRender(self, previewWorker): + width = int(previewWorker.core.settings.value('outputWidth')) + height = int(previewWorker.core.settings.value('outputHeight')) + return BlankFrame(width, height) + + def preFrameRender(self, **kwargs): + pass + + def properties(self): + props = ['static', 'audio'] + if not os.path.exists(self.sound): + props.append('error') + return props + + def error(self): + if not self.sound: + return "No audio file selected." + if not os.path.exists(self.sound): + return "The audio file selected no longer exists!" + + def audio(self): + return (self.sound, {}) + + def pickSound(self): + sndDir = self.settings.value("componentDir", os.path.expanduser("~")) + filename, _ = QtWidgets.QFileDialog.getOpenFileName( + self.page, "Choose Sound", sndDir, + "Audio Files (%s)" % " ".join(self.core.audioFormats)) + if filename: + self.settings.setValue("componentDir", os.path.dirname(filename)) + self.page.lineEdit_sound.setText(filename) + self.update() + + def frameRender(self, layerNo, frameNo): + width = int(self.settings.value('outputWidth')) + height = int(self.settings.value('outputHeight')) + return BlankFrame(width, height) + + def loadPreset(self, pr, presetName=None): + super().loadPreset(pr, presetName) + self.page.lineEdit_sound.setText(pr['sound']) + + def savePreset(self): + return { + 'sound': self.sound, + } + + def commandHelp(self): + print('Path to audio file:\n path=/filepath/to/sound.ogg') + + def command(self, arg): + if not arg.startswith('preset=') and '=' in arg: + key, arg = arg.split('=', 1) + if key == 'path': + if '*%s' % os.path.splitext(arg)[1] \ + not in self.core.audioFormats: + print("Not a supported audio format") + quit(1) + self.page.lineEdit_sound.setText(arg) + return + + super().command(arg) diff --git a/src/components/sound.ui b/src/components/sound.ui new file mode 100644 index 0000000..5fc00c1 --- /dev/null +++ b/src/components/sound.ui @@ -0,0 +1,122 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>Form</class> + <widget class="QWidget" name="Form"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>586</width> + <height>197</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>4</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_8"> + <item> + <widget class="QLabel" name="label_textColor"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>31</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>Audio File</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="lineEdit_sound"> + <property name="minimumSize"> + <size> + <width>1</width> + <height>0</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="pushButton_sound"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>1</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + <property name="text"> + <string>...</string> + </property> + <property name="MaximumSize" stdset="0"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/components/text.py b/src/components/text.py index 2b1884f..19460e5 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -17,10 +17,11 @@ class Component(Component): self.titleFont = QFont() def widget(self, parent): - height = int(parent.settings.value('outputHeight')) - width = int(parent.settings.value('outputWidth')) - self.parent = parent + self.settings = self.parent.core.settings + height = int(self.settings.value('outputHeight')) + width = int(self.settings.value('outputWidth')) + self.textColor = (255, 255, 255) self.title = 'Text' self.alignment = 1 @@ -68,17 +69,17 @@ class Component(Component): btnStyle = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*self.textColor).name() self.page.pushButton_textColor.setStyleSheet(btnStyle) - 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 + x = int(self.xPosition) if self.alignment == 1: # Middle - offset = fm.width(self.title)/2 + offset = int(fm.width(self.title)/2) x = self.xPosition - offset if self.alignment == 2: # Right @@ -115,26 +116,31 @@ class Component(Component): } def previewRender(self, previewWorker): - width = int(previewWorker.core.settings.value('outputWidth')) - height = int(previewWorker.core.settings.value('outputHeight')) + width = int(self.settings.value('outputWidth')) + height = int(self.settings.value('outputHeight')) return self.addText(width, height) - def preFrameRender(self, **kwargs): - super().preFrameRender(**kwargs) - return ['static'] + def properties(self): + props = ['static'] + if not self.title: + props.append('error') + return props + + def error(self): + return "No text provided." def frameRender(self, layerNo, frameNo): - width = int(self.worker.core.settings.value('outputWidth')) - height = int(self.worker.core.settings.value('outputHeight')) + width = int(self.settings.value('outputWidth')) + height = int(self.settings.value('outputHeight')) return self.addText(width, height) def addText(self, width, height): - x, y = self.getXY() - image = FramePainter(width, height) + image = FramePainter(width, height) self.titleFont.setPixelSize(self.fontSize) image.setFont(self.titleFont) image.setPen(self.textColor) + x, y = self.getXY() image.drawText(x, y, self.title) return image.finalize() diff --git a/src/components/video.py b/src/components/video.py index e6890e0..0b93293 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -8,7 +8,7 @@ from queue import PriorityQueue from component import Component, BadComponentInit from frame import BlankFrame -from toolkit import openPipe +from toolkit import openPipe, checkOutput class Video: @@ -115,6 +115,8 @@ class Component(Component): self.settings = parent.settings page = self.loadUi('video.ui') self.videoPath = '' + self.badVideo = False + self.badAudio = False self.x = 0 self.y = 0 self.loopVideo = False @@ -123,6 +125,7 @@ class Component(Component): page.pushButton_video.clicked.connect(self.pickVideo) page.checkBox_loop.stateChanged.connect(self.update) page.checkBox_distort.stateChanged.connect(self.update) + page.checkBox_useAudio.stateChanged.connect(self.update) page.spinBox_scale.valueChanged.connect(self.update) page.spinBox_x.valueChanged.connect(self.update) page.spinBox_y.valueChanged.connect(self.update) @@ -133,15 +136,15 @@ class Component(Component): def update(self): self.videoPath = self.page.lineEdit_video.text() self.loopVideo = self.page.checkBox_loop.isChecked() + self.useAudio = self.page.checkBox_useAudio.isChecked() self.distort = self.page.checkBox_distort.isChecked() self.scale = self.page.spinBox_scale.value() self.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) @@ -151,6 +154,47 @@ class Component(Component): else: return frame + def properties(self): + props = [] + if not self.videoPath or self.badVideo \ + or not os.path.exists(self.videoPath): + return ['error'] + + if self.useAudio: + props.append('audio') + self.testAudioStream() + if self.badAudio: + return ['error'] + + return props + + def error(self): + if self.badAudio: + return "Could not identify an audio stream in this video." + if not self.videoPath: + return "There is no video selected." + if not os.path.exists(self.videoPath): + return "The video selected does not exist!" + if self.badVideo: + return "The video selected is corrupt!" + + def testAudioStream(self): + # test if an audio stream really exists + audioTestCommand = [ + self.core.FFMPEG_BIN, + '-i', self.videoPath, + '-vn', '-f', 'null', '-' + ] + try: + checkOutput(audioTestCommand, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError: + self.badAudio = True + else: + self.badAudio = False + + def audio(self): + return (self.videoPath, {'map': '-v'}) + def preFrameRender(self, **kwargs): super().preFrameRender(**kwargs) width = int(self.worker.core.settings.value('outputWidth')) @@ -158,7 +202,7 @@ class Component(Component): self.blankFrame_ = BlankFrame(width, height) self.updateChunksize(width, height) self.video = Video( - ffmpeg=self.parent.core.FFMPEG_BIN, videoPath=self.videoPath, + ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath, width=width, height=height, chunkSize=self.chunkSize, frameRate=int(self.settings.value("outputFrameRate")), parent=self.parent, loopVideo=self.loopVideo, @@ -175,6 +219,7 @@ class Component(Component): super().loadPreset(pr, presetName) self.page.lineEdit_video.setText(pr['video']) self.page.checkBox_loop.setChecked(pr['loop']) + self.page.checkBox_useAudio.setChecked(pr['useAudio']) self.page.checkBox_distort.setChecked(pr['distort']) self.page.spinBox_scale.setValue(pr['scale']) self.page.spinBox_x.setValue(pr['x']) @@ -185,6 +230,7 @@ class Component(Component): 'preset': self.currentPreset, 'video': self.videoPath, 'loop': self.loopVideo, + 'useAudio': self.useAudio, 'distort': self.distort, 'scale': self.scale, 'x': self.xPosition, @@ -195,7 +241,7 @@ class Component(Component): imgDir = self.settings.value("componentDir", os.path.expanduser("~")) filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.page, "Choose Video", - imgDir, "Video Files (%s)" % " ".join(self.videoFormats) + imgDir, "Video Files (%s)" % " ".join(self.core.videoFormats) ) if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) @@ -238,7 +284,7 @@ class Component(Component): if not arg.startswith('preset=') and '=' in arg: key, arg = arg.split('=', 1) if key == 'path' and os.path.exists(arg): - if os.path.splitext(arg)[1] in self.core.videoFormats: + if '*%s' % os.path.splitext(arg)[1] in self.core.videoFormats: self.page.lineEdit_video.setText(arg) self.page.spinBox_scale.setValue(100) self.page.checkBox_loop.setChecked(True) @@ -246,10 +292,17 @@ class Component(Component): else: print("Not a supported video format") quit(1) + elif arg == 'audio': + if not self.page.lineEdit_video.text(): + print("'audio' option must follow a video selection") + quit(1) + self.page.checkBox_useAudio.setChecked(True) + return super().command(arg) def commandHelp(self): print('Load a video:\n path=/filepath/to/video.mp4') + print('Using audio:\n path=/filepath/to/video.mp4 audio') def scale(scale, width, height, returntype=None): @@ -281,6 +334,7 @@ def finalizeFrame(self, imageData, width, height): '### BAD VIDEO SELECTED ###\n' 'Video will not export with these settings' ) + self.badVideo = True return BlankFrame(width, height) if self.scale != 100 \ @@ -289,4 +343,5 @@ def finalizeFrame(self, imageData, width, height): frame.paste(image, box=(self.xPosition, self.yPosition)) else: frame = image + self.badVideo = False return frame diff --git a/src/components/video.ui b/src/components/video.ui index f05e8a5..97b7d6f 100644 --- a/src/components/video.ui +++ b/src/components/video.ui @@ -190,16 +190,20 @@ </widget> </item> <item> - <spacer name="horizontalSpacer_10"> + <widget class="QCheckBox" name="checkBox_useAudio"> + <property name="text"> + <string>Use Audio</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> - <property name="sizeType"> - <enum>QSizePolicy::Fixed</enum> - </property> <property name="sizeHint" stdset="0"> <size> - <width>5</width> + <width>40</width> <height>20</height> </size> </property> @@ -256,9 +260,6 @@ </property> </spacer> </item> - <item> - <widget class="QWidget" name="widget" native="true"/> - </item> </layout> </widget> <resources/> diff --git a/src/core.py b/src/core.py index 9792e88..4c12209 100644 --- a/src/core.py +++ b/src/core.py @@ -11,6 +11,8 @@ from importlib import import_module from PyQt5.QtCore import QStandardPaths import toolkit +from frame import Frame +import video_thread class Core: @@ -20,6 +22,7 @@ class Core: opens projects and presets, and stores settings/paths to data. ''' def __init__(self): + Frame.core = self self.dataDir = QStandardPaths.writableLocation( QStandardPaths.AppConfigLocation ) @@ -412,6 +415,7 @@ class Core: f.write('[Components]\n') for comp in self.selectedComponents: saveValueStore = comp.savePreset() + saveValueStore['preset'] = comp.currentPreset f.write('%s\n' % str(comp)) f.write('%s\n' % str(comp.version())) f.write('%s\n' % toolkit.presetToString(saveValueStore)) @@ -460,6 +464,115 @@ class Core: except sp.CalledProcessError: return "avconv" + def createFfmpegCommand(self, inputFile, outputFile, duration): + ''' + Constructs the major ffmpeg command used to export the video + ''' + duration = str(duration) + + # Test if user has libfdk_aac + encoders = toolkit.checkOutput( + "%s -encoders -hide_banner" % self.FFMPEG_BIN, shell=True + ) + encoders = encoders.decode("utf-8") + + acodec = self.settings.value('outputAudioCodec') + + options = self.encoder_options + containerName = self.settings.value('outputContainer') + vcodec = self.settings.value('outputVideoCodec') + vbitrate = str(self.settings.value('outputVideoBitrate'))+'k' + acodec = self.settings.value('outputAudioCodec') + abitrate = str(self.settings.value('outputAudioBitrate'))+'k' + + for cont in options['containers']: + if cont['name'] == containerName: + container = cont['container'] + break + + vencoders = options['video-codecs'][vcodec] + aencoders = options['audio-codecs'][acodec] + + for encoder in vencoders: + if encoder in encoders: + vencoder = encoder + break + + for encoder in aencoders: + if encoder in encoders: + aencoder = encoder + break + + ffmpegCommand = [ + self.FFMPEG_BIN, + '-thread_queue_size', '512', + '-y', # overwrite the output file if it already exists. + + # INPUT VIDEO + '-f', 'rawvideo', + '-vcodec', 'rawvideo', + '-s', '%sx%s' % ( + self.settings.value('outputWidth'), + self.settings.value('outputHeight'), + ), + '-pix_fmt', 'rgba', + '-r', self.settings.value('outputFrameRate'), + '-t', duration, + '-i', '-', # the video input comes from a pipe + '-an', # the video input has no sound + + # INPUT SOUND + '-t', duration, + '-i', inputFile + ] + + extraAudio = [ + comp.audio() for comp in self.selectedComponents + if 'audio' in comp.properties() + ] + if extraAudio: + unwantedVideoStreams = [] + for streamNo, params in enumerate(extraAudio): + extraInputFile, params = params + ffmpegCommand.extend([ + '-t', duration, + '-i', extraInputFile + ]) + if 'map' in params and params['map'] == '-v': + # a video stream to remove + unwantedVideoStreams.append(streamNo + 1) + + if unwantedVideoStreams: + ffmpegCommand.extend(['-map', '0']) + for streamNo in unwantedVideoStreams: + ffmpegCommand.extend([ + '-map', '-%s:v' % str(streamNo) + ]) + ffmpegCommand.extend([ + '-filter_complex', + 'amix=inputs=%s:duration=first:dropout_transition=3' % str( + len(extraAudio) + 1 + ), + ]) + + ffmpegCommand.extend([ + # OUTPUT + '-vcodec', vencoder, + '-acodec', aencoder, + '-b:v', vbitrate, + '-b:a', abitrate, + '-pix_fmt', self.settings.value('outputVideoFormat'), + '-preset', self.settings.value('outputPreset'), + '-f', container + ]) + + if acodec == 'aac': + ffmpegCommand.append('-strict') + ffmpegCommand.append('-2') + + ffmpegCommand.append(outputFile) + return ffmpegCommand + def readAudioFile(self, filename, parent): command = [self.FFMPEG_BIN, '-i', filename] @@ -485,7 +598,8 @@ class Core: '-ac', '1', # mono (set to '2' for stereo) '-'] in_pipe = toolkit.openPipe( - command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8) + command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8 + ) completeAudioArray = numpy.empty(0, dtype="int16") @@ -495,7 +609,7 @@ class Core: if self.canceled: break # read 2 seconds of audio - progress = progress + 4 + progress += 4 raw_audio = in_pipe.stdout.read(88200*4) if len(raw_audio) == 0: break @@ -522,7 +636,22 @@ class Core: completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray completeAudioArray = completeAudioArrayCopy - return completeAudioArray + return (completeAudioArray, duration) + + def newVideoWorker(self, loader, audioFile, outputPath): + self.videoThread = QtCore.QThread(loader) + videoWorker = video_thread.Worker( + loader, audioFile, outputPath, self.selectedComponents + ) + videoWorker.moveToThread(self.videoThread) + videoWorker.videoCreated.connect(self.videoCreated) + + self.videoThread.start() + return videoWorker + + def videoCreated(self): + self.videoThread.quit() + self.videoThread.wait() def cancel(self): self.canceled = True diff --git a/src/frame.py b/src/frame.py index 57d33b0..cddb611 100644 --- a/src/frame.py +++ b/src/frame.py @@ -5,6 +5,11 @@ from PyQt5 import QtGui from PIL import Image from PIL.ImageQt import ImageQt import sys +import os + + +class Frame: + '''Controller class for all frames.''' class FramePainter(QtGui.QPainter): @@ -14,7 +19,7 @@ class FramePainter(QtGui.QPainter): ''' def __init__(self, width, height): image = BlankFrame(width, height) - self.image = ImageQt(image) + self.image = QtGui.QImage(ImageQt(image)) super().__init__(self.image) def setPen(self, RgbTuple): @@ -43,5 +48,19 @@ def FloodFrame(width, height, RgbaTuple): def BlankFrame(width, height): - '''The base frame used by each component to start drawing''' + '''The base frame used by each component to start drawing.''' return FloodFrame(width, height, (0, 0, 0, 0)) + + +def Checkerboard(width, height): + ''' + A checkerboard to represent transparency to the user. + TODO: Would be cool to generate this image with numpy instead. + ''' + image = FloodFrame(1920, 1080, (0, 0, 0, 0)) + image.paste(Image.open( + os.path.join(Frame.core.wd, "background.png")), + (0, 0) + ) + image = image.resize((width, height)) + return image diff --git a/src/main.py b/src/main.py index b0ece29..2216d2a 100644 --- a/src/main.py +++ b/src/main.py @@ -8,13 +8,13 @@ import video_thread if __name__ == "__main__": - mode = 'gui' + mode = 'GUI' if len(sys.argv) > 2: - mode = 'cmd' + mode = 'commandline' elif len(sys.argv) == 2: if sys.argv[1].startswith('-'): - mode = 'cmd' + mode = 'commandline' else: # opening a project file with gui proj = sys.argv[1] @@ -22,16 +22,17 @@ if __name__ == "__main__": # normal gui launch proj = None + print('Starting Audio Visualizer in %s mode' % mode) app = QtWidgets.QApplication(sys.argv) app.setApplicationName("audio-visualizer") # app.setOrganizationName("audio-visualizer") - if mode == 'cmd': + if mode == 'commandline': from command import * main = Command() - elif mode == 'gui': + elif mode == 'GUI': from mainwindow import * import atexit import signal diff --git a/src/mainwindow.py b/src/mainwindow.py index 165b5bd..76ed179 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -16,7 +16,6 @@ import time import core import preview_thread -import video_thread from presetmanager import PresetManager from toolkit import LoadDefaultSettings, disableWhenEncoding, checkOutput @@ -49,9 +48,9 @@ class PreviewWindow(QtWidgets.QLabel): class MainWindow(QtWidgets.QMainWindow): - newTask = QtCore.pyqtSignal(list) + createVideo = QtCore.pyqtSignal() + newTask = QtCore.pyqtSignal(list) # for the preview window processTask = QtCore.pyqtSignal() - videoTask = QtCore.pyqtSignal(str, str, list) def __init__(self, window, project): QtWidgets.QMainWindow.__init__(self) @@ -306,6 +305,7 @@ class MainWindow(QtWidgets.QMainWindow): QtWidgets.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog) QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog) QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject) + QtWidgets.QShortcut("Ctrl+Alt+Shift+R", self.window, self.drawPreview) QtWidgets.QShortcut( "Ctrl+T", self.window, @@ -496,20 +496,15 @@ class MainWindow(QtWidgets.QMainWindow): self.canceled = False self.progressBarUpdated(-1) - self.videoThread = QtCore.QThread(self) - self.videoWorker = video_thread.Worker(self) - self.videoWorker.moveToThread(self.videoThread) - self.videoWorker.videoCreated.connect(self.videoCreated) + self.videoWorker = self.core.newVideoWorker( + self, audioFile, outputPath + ) self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated) self.videoWorker.progressBarSetText.connect( self.progressBarSetText) self.videoWorker.imageCreated.connect(self.showPreviewImage) self.videoWorker.encoding.connect(self.changeEncodingStatus) - self.videoThread.start() - self.videoTask.emit( - audioFile, - outputPath, - self.core.selectedComponents) + self.createVideo.emit() def changeEncodingStatus(self, status): self.encoding = status @@ -557,19 +552,17 @@ class MainWindow(QtWidgets.QMainWindow): self.window.progressLabel.setHidden(True) self.drawPreview(True) + @QtCore.pyqtSlot(int) def progressBarUpdated(self, value): self.window.progressBar_createVideo.setValue(value) + @QtCore.pyqtSlot(str) def progressBarSetText(self, value): if sys.platform == 'darwin': self.window.progressLabel.setText(value) else: self.window.progressBar_createVideo.setFormat(value) - def videoCreated(self): - self.videoThread.quit() - self.videoThread.wait() - def updateResolution(self): resIndex = int(self.window.comboBox_resolution.currentIndex()) res = self.resolutions[resIndex].split('x') @@ -583,6 +576,7 @@ class MainWindow(QtWidgets.QMainWindow): self.autosave(force) self.updateWindowTitle() + @QtCore.pyqtSlot(QtGui.QImage) def showPreviewImage(self, image): self.previewWindow.changePixmap(image) @@ -711,6 +705,10 @@ class MainWindow(QtWidgets.QMainWindow): def saveCurrentProject(self): if self.currentProject: self.core.createProjectFile(self.currentProject, self.window) + try: + os.remove(self.autosavePath) + except FileNotFoundError: + pass self.updateWindowTitle() else: self.openSaveProjectDialog() diff --git a/src/presetmanager.py b/src/presetmanager.py index 40aa73f..0028203 100644 --- a/src/presetmanager.py +++ b/src/presetmanager.py @@ -160,6 +160,7 @@ class PresetManager(QtWidgets.QDialog): selectedComponents[index].currentPreset = newName saveValueStore = \ selectedComponents[index].savePreset() + saveValueStore['preset'] = newName componentName = str(selectedComponents[index]).strip() vers = selectedComponents[index].version() self.createNewPreset( diff --git a/src/preview_thread.py b/src/preview_thread.py index 95a26ec..4ffb7f6 100644 --- a/src/preview_thread.py +++ b/src/preview_thread.py @@ -10,12 +10,12 @@ import core from queue import Queue, Empty import os -from frame import FloodFrame +from frame import Checkerboard class Worker(QtCore.QObject): - imageCreated = pyqtSignal(['QImage']) + imageCreated = pyqtSignal(QtGui.QImage) error = pyqtSignal() def __init__(self, parent=None, queue=None): @@ -24,14 +24,12 @@ class Worker(QtCore.QObject): parent.processTask.connect(self.process) self.parent = parent self.core = self.parent.core + self.settings = self.parent.core.settings self.queue = queue - self.core.settings = parent.settings - self.stackedWidget = parent.window.stackedWidget - # create checkerboard background to represent transparency - self.background = FloodFrame(1920, 1080, (0, 0, 0, 0)) - self.background.paste(Image.open(os.path.join( - self.core.wd, "background.png"))) + width = int(self.settings.value('outputWidth')) + height = int(self.settings.value('outputHeight')) + self.background = Checkerboard(width, height) @pyqtSlot(list) def createPreviewImage(self, components): @@ -42,6 +40,8 @@ class Worker(QtCore.QObject): @pyqtSlot() def process(self): + width = int(self.settings.value('outputWidth')) + height = int(self.settings.value('outputHeight')) try: nextPreviewInformation = self.queue.get(block=False) while self.queue.qsize() >= 2: @@ -50,33 +50,42 @@ class Worker(QtCore.QObject): except Empty: continue - width = int(self.core.settings.value('outputWidth')) - height = int(self.core.settings.value('outputHeight')) + if self.background.width != width \ + or self.background.height != height: + self.background = Checkerboard(width, height) + frame = self.background.copy() - frame = frame.resize((width, height)) components = nextPreviewInformation["components"] for component in reversed(components): try: + newFrame = component.previewRender(self) frame = Image.alpha_composite( - frame, component.previewRender(self) + frame, newFrame ) except ValueError as e: + errMsg = "Bad frame returned by %s's preview renderer. " \ + "%s. New frame size was %s*%s; should be %s*%s. " \ + "This is a fatal error." % ( + str(component), str(e).capitalize(), + newFrame.width, newFrame.height, + width, height + ) + print(errMsg) self.parent.showMessage( - msg="Bad frame returned by %s's previewRender method. " - "This is a fatal error." % - str(component), + msg=errMsg, detail=str(e), icon='Warning', - parent=None # mainwindow is in a different thread + parent=None # MainWindow is in a different thread ) - from frame import BlankFrame - self.imageCreated.emit(ImageQt(BlankFrame)) self.error.emit() break + except RuntimeError as e: + print(e) else: - self.imageCreated.emit(ImageQt(frame)) + self.frame = ImageQt(frame) + self.imageCreated.emit(QtGui.QImage(self.frame)) except Empty: True diff --git a/src/video_thread.py b/src/video_thread.py index e7f1ac7..674765a 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -20,7 +20,7 @@ import signal import core from toolkit import openPipe -from frame import FloodFrame +from frame import Checkerboard class Worker(QtCore.QObject): @@ -31,13 +31,18 @@ class Worker(QtCore.QObject): progressBarSetText = pyqtSignal(str) encoding = pyqtSignal(bool) - def __init__(self, parent=None): + def __init__(self, parent, inputFile, outputFile, components): QtCore.QObject.__init__(self) - self.core = core.Core() - self.core.settings = parent.settings + self.core = parent.core + self.settings = parent.core.settings self.modules = parent.core.modules + parent.createVideo.connect(self.createVideo) + self.parent = parent - parent.videoTask.connect(self.createVideo) + self.components = components + self.outputFile = outputFile + self.inputFile = inputFile + self.sampleSize = 1470 # 44100 / 30 = 1470 self.canceled = False self.error = False @@ -54,16 +59,18 @@ class Worker(QtCore.QObject): audioI = self.compositeQueue.get() bgI = int(audioI / self.sampleSize) frame = None - for compNo, comp in reversed(list(enumerate(self.components))): - if compNo in self.staticComponents and \ - self.staticComponents[compNo] is not None: + layerNo = len(self.components) - compNo - 1 + if layerNo in self.staticComponents: + if self.staticComponents[layerNo] is None: + # this layer was merged into a following layer + continue # static component if frame is None: # bottom-most layer - frame = self.staticComponents[compNo] + frame = self.staticComponents[layerNo] else: frame = Image.alpha_composite( - frame, self.staticComponents[compNo] + frame, self.staticComponents[layerNo] ) else: # animated component @@ -93,114 +100,53 @@ class Worker(QtCore.QObject): Grabs frames from the previewQueue, adds them to the checkerboard and emits a final QImage to the MainWindow for the live preview ''' - background = FloodFrame(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)) + background = Checkerboard(self.width, self.height) while not self.stopped: audioI, frame = self.previewQueue.get() if time.time() - self.lastPreview >= 0.06 or audioI == 0: image = Image.alpha_composite(background.copy(), frame) - self.imageCreated.emit(ImageQt(image)) + self.imageCreated.emit(QtGui.QImage(ImageQt(image))) self.lastPreview = time.time() self.previewQueue.task_done() - @pyqtSlot(str, str, list) - def createVideo(self, inputFile, outputFile, components): + @pyqtSlot() + def createVideo(self): + numpy.seterr(divide='ignore') self.encoding.emit(True) - self.components = components - self.outputFile = outputFile + self.extraAudio = [] + self.width = int(self.settings.value('outputWidth')) + self.height = int(self.settings.value('outputHeight')) - self.reset() + self.compositeQueue = Queue() + self.compositeQueue.maxsize = 20 + self.renderQueue = PriorityQueue() + self.renderQueue.maxsize = 20 + self.previewQueue = PriorityQueue() - self.width = int(self.core.settings.value('outputWidth')) - self.height = int(self.core.settings.value('outputHeight')) + self.reset() 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' + # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ + # READ AUDIO, INITIALIZE COMPONENTS, OPEN A PIPE TO FFMPEG + # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ - for cont in options['containers']: - if cont['name'] == containerName: - container = cont['container'] - break - - vencoders = options['video-codecs'][vcodec] - aencoders = options['audio-codecs'][acodec] - - for encoder in vencoders: - if encoder in encoders: - vencoder = encoder - break - - for encoder in aencoders: - if encoder in encoders: - aencoder = encoder - break + self.progressBarSetText.emit("Loading audio file...") + self.completeAudioArray, duration = self.core.readAudioFile( + self.inputFile, self + ) - 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 + self.progressBarUpdate.emit(0) + self.progressBarSetText.emit("Starting 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 = "Starting components..." - self.progressBarSetText.emit(pStr) - properties = None - properties = comp.preFrameRender( + for compNo, comp in enumerate(reversed(self.components)): + comp.preFrameRender( worker=self, completeAudioArray=self.completeAudioArray, sampleSize=self.sampleSize, @@ -208,23 +154,66 @@ class Worker(QtCore.QObject): progressBarSetText=self.progressBarSetText ) - if properties and 'static' in properties: + if 'error' in comp.properties(): + self.cancel() + self.canceled = True + errMsg = "Component #%s encountered an error!" % compNo \ + if comp.error() is None else 'Component #%s (%s): %s' % ( + str(compNo), + str(comp), + comp.error() + ) + self.parent.showMessage( + msg=errMsg, + icon='Warning', + parent=None # MainWindow is in a different thread + ) + break + if 'static' in comp.properties(): self.staticComponents[compNo] = \ comp.frameRender(compNo, 0).copy() - self.progressBarUpdate.emit(100) - # Create ffmpeg pipe and queues for frames + if self.canceled: + print('Export cancelled by component #%s (%s): %s' % ( + compNo, str(comp), comp.error() + )) + self.progressBarSetText.emit('Export Canceled') + self.encoding.emit(False) + self.videoCreated.emit() + return + + # Merge consecutive static component frames together + for compNo in range(len(self.components)): + if compNo not in self.staticComponents \ + or compNo + 1 not in self.staticComponents: + continue + self.staticComponents[compNo + 1] = Image.alpha_composite( + self.staticComponents.pop(compNo), + self.staticComponents[compNo + 1] + ) + self.staticComponents[compNo] = None + + ffmpegCommand = self.core.createFfmpegCommand( + self.inputFile, self.outputFile, duration + ) + print('###### FFMPEG COMMAND ######\n%s' % " ".join(ffmpegCommand)) + print('############################') self.out_pipe = openPipe( - 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() + ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout + ) + + # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ + # START CREATING THE VIDEO + # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ - # Threads to render frames and send them back here for piping out + # Make 2 or 3 renderNodes in new threads to create the frames self.renderThreads = [] - for i in range(3): + try: + numCpus = len(os.sched_getaffinity(0)) + except: + numCpus = os.cpu_count() + + for i in range(2 if numCpus <= 2 else 3): self.renderThreads.append( Thread(target=self.renderNode, name="Render Thread")) self.renderThreads[i].daemon = True @@ -235,16 +224,17 @@ class Worker(QtCore.QObject): self.dispatchThread.daemon = True self.dispatchThread.start() + self.lastPreview = 0.0 self.previewDispatch = Thread( target=self.previewDispatch, name="Render Dispatch Thread") self.previewDispatch.daemon = True self.previewDispatch.start() + # Begin piping into ffmpeg! frameBuffer = {} - self.lastPreview = 0.0 - self.progressBarUpdate.emit(0) - pStr = "Exporting video..." - self.progressBarSetText.emit(pStr) + progressBarValue = 0 + self.progressBarUpdate.emit(progressBarValue) + self.progressBarSetText.emit("Exporting video...") if not self.canceled: for audioI in range( 0, len(self.completeAudioArray), self.sampleSize): @@ -253,29 +243,26 @@ class Worker(QtCore.QObject): # 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] + audioI_, frame = self.renderQueue.get() + frameBuffer[audioI_] = frame self.renderQueue.task_done() if self.canceled: break try: self.out_pipe.stdin.write(frameBuffer[audioI].tobytes()) - self.previewQueue.put([audioI, frameBuffer[audioI]]) - del frameBuffer[audioI] + self.previewQueue.put([audioI, frameBuffer.pop(audioI)]) except: break # increase progress bar value - if progressBarValue + 1 <= ( - audioI / len(self.completeAudioArray) - ) * 100: - progressBarValue = numpy.floor( - (i / len(self.completeAudioArray)) * 100) + completion = (audioI / len(self.completeAudioArray)) * 100 + if progressBarValue + 1 <= completion: + progressBarValue = numpy.floor(completion) self.progressBarUpdate.emit(progressBarValue) - pStr = "Exporting video: " + str(int(progressBarValue)) \ - + "%" - self.progressBarSetText.emit(pStr) + self.progressBarSetText.emit( + "Exporting video: %s%%" % str(int(progressBarValue)) + ) numpy.seterr(all='print') @@ -317,6 +304,7 @@ class Worker(QtCore.QObject): def cancel(self): self.canceled = True + self.stopped = True self.core.cancel() for comp in self.components: |
