aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/command.py42
-rw-r--r--src/component.py61
-rw-r--r--src/components/color.py17
-rw-r--r--src/components/image.py29
-rw-r--r--src/components/original.py11
-rw-r--r--src/components/sound.py89
-rw-r--r--src/components/sound.ui122
-rw-r--r--src/components/text.py36
-rw-r--r--src/components/video.py67
-rw-r--r--src/components/video.ui17
-rw-r--r--src/core.py135
-rw-r--r--src/frame.py23
-rw-r--r--src/main.py11
-rw-r--r--src/mainwindow.py30
-rw-r--r--src/presetmanager.py1
-rw-r--r--src/preview_thread.py47
-rw-r--r--src/video_thread.py234
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: