From f454814867443ceeeca2a3a2c2a676947184503c Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 20 Jul 2017 20:31:38 -0400
Subject: ffmpeg functions moved to toolkit, component format simplified
component methods are auto-decorated & settings are now class variables
---
src/toolkit/ffmpeg.py | 284 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 284 insertions(+)
create mode 100644 src/toolkit/ffmpeg.py
(limited to 'src/toolkit/ffmpeg.py')
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
new file mode 100644
index 0000000..89d4e9d
--- /dev/null
+++ b/src/toolkit/ffmpeg.py
@@ -0,0 +1,284 @@
+'''
+ Tools for using ffmpeg
+'''
+import numpy
+import sys
+import os
+import subprocess as sp
+
+from toolkit.common import Core, checkOutput, openPipe
+
+
+def findFfmpeg():
+ if getattr(sys, 'frozen', False):
+ # The application is frozen
+ if sys.platform == "win32":
+ return os.path.join(Core.wd, 'ffmpeg.exe')
+ else:
+ return os.path.join(Core.wd, 'ffmpeg')
+
+ else:
+ if sys.platform == "win32":
+ return "ffmpeg"
+ else:
+ try:
+ with open(os.devnull, "w") as f:
+ checkOutput(
+ ['ffmpeg', '-version'], stderr=f
+ )
+ return "ffmpeg"
+ except sp.CalledProcessError:
+ return "avconv"
+
+
+def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
+ '''
+ Constructs the major ffmpeg command used to export the video
+ '''
+ if duration == -1:
+ duration = getAudioDuration(inputFile)
+
+ safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters
+ duration = "{0:.3f}".format(duration + 0.1) # used by input sources
+
+ # Test if user has libfdk_aac
+ encoders = checkOutput(
+ "%s -encoders -hide_banner" % Core.FFMPEG_BIN, shell=True
+ )
+ encoders = encoders.decode("utf-8")
+
+ acodec = Core.settings.value('outputAudioCodec')
+
+ options = Core.encoderOptions
+ containerName = Core.settings.value('outputContainer')
+ vcodec = Core.settings.value('outputVideoCodec')
+ vbitrate = str(Core.settings.value('outputVideoBitrate'))+'k'
+ acodec = Core.settings.value('outputAudioCodec')
+ abitrate = str(Core.settings.value('outputAudioBitrate'))+'k'
+
+ for cont in options['containers']:
+ if cont['name'] == containerName:
+ container = cont['container']
+ break
+
+ vencoders = options['video-codecs'][vcodec]
+ aencoders = options['audio-codecs'][acodec]
+
+ for encoder in vencoders:
+ if encoder in encoders:
+ vencoder = encoder
+ break
+
+ for encoder in aencoders:
+ if encoder in encoders:
+ aencoder = encoder
+ break
+
+ ffmpegCommand = [
+ Core.FFMPEG_BIN,
+ '-thread_queue_size', '512',
+ '-y', # overwrite the output file if it already exists.
+
+ # INPUT VIDEO
+ '-f', 'rawvideo',
+ '-vcodec', 'rawvideo',
+ '-s', '%sx%s' % (
+ Core.settings.value('outputWidth'),
+ Core.settings.value('outputHeight'),
+ ),
+ '-pix_fmt', 'rgba',
+ '-r', Core.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
+ ]
+
+ # Add extra audio inputs and any needed avfilters
+ # NOTE: Global filters are currently hard-coded here for debugging use
+ globalFilters = 0 # increase to add global filters
+ extraAudio = [
+ comp.audio for comp in components
+ if 'audio' in comp.properties
+ ]
+ if extraAudio or globalFilters > 0:
+ # Add -i options for extra input files
+ extraFilters = {}
+ for streamNo, params in enumerate(reversed(extraAudio)):
+ extraInputFile, params = params
+ ffmpegCommand.extend([
+ '-t', safeDuration,
+ # Tell ffmpeg about shorter clips (seemingly not needed)
+ # streamDuration = getAudioDuration(extraInputFile)
+ # if streamDuration > float(safeDuration)
+ # else "{0:.3f}".format(streamDuration),
+ '-i', extraInputFile
+ ])
+ # Construct dataset of extra filters we'll need to add later
+ for ffmpegFilter in params:
+ if streamNo + 2 not in extraFilters:
+ extraFilters[streamNo + 2] = []
+ extraFilters[streamNo + 2].append((
+ ffmpegFilter, params[ffmpegFilter]
+ ))
+
+ # Start creating avfilters! Popen-style, so don't use semicolons;
+ extraFilterCommand = []
+
+ if globalFilters <= 0:
+ # Dictionary of last-used tmp labels for a given stream number
+ tmpInputs = {streamNo: -1 for streamNo in extraFilters}
+ else:
+ # Insert blank entries for global filters into extraFilters
+ # so the per-stream filters know what input to source later
+ for streamNo in range(len(extraAudio), 0, -1):
+ if streamNo + 1 not in extraFilters:
+ extraFilters[streamNo + 1] = []
+ # Also filter the primary audio track
+ extraFilters[1] = []
+ tmpInputs = {
+ streamNo: globalFilters - 1
+ for streamNo in extraFilters
+ }
+
+ # Add the global filters!
+ # NOTE: list length must = globalFilters, currently hardcoded
+ if tmpInputs:
+ extraFilterCommand.extend([
+ '[%s:a] ashowinfo [%stmp0]' % (
+ str(streamNo),
+ str(streamNo)
+ )
+ for streamNo in tmpInputs
+ ])
+
+ # Now add the per-stream filters!
+ for streamNo, paramList in extraFilters.items():
+ for param in paramList:
+ source = '[%s:a]' % str(streamNo) \
+ if tmpInputs[streamNo] == -1 else \
+ '[%stmp%s]' % (
+ str(streamNo), str(tmpInputs[streamNo])
+ )
+ tmpInputs[streamNo] = tmpInputs[streamNo] + 1
+ extraFilterCommand.append(
+ '%s %s%s [%stmp%s]' % (
+ source, param[0], param[1], str(streamNo),
+ str(tmpInputs[streamNo])
+ )
+ )
+
+ # Join all the filters together and combine into 1 stream
+ extraFilterCommand = "; ".join(extraFilterCommand) + '; ' \
+ if tmpInputs else ''
+ ffmpegCommand.extend([
+ '-filter_complex',
+ extraFilterCommand +
+ '%s amix=inputs=%s:duration=first [a]'
+ % (
+ "".join([
+ '[%stmp%s]' % (str(i), tmpInputs[i])
+ if i in extraFilters else '[%s:a]' % str(i)
+ for i in range(1, len(extraAudio) + 2)
+ ]),
+ str(len(extraAudio) + 1)
+ ),
+ ])
+
+ # Only map audio from the filters, and video from the pipe
+ ffmpegCommand.extend([
+ '-map', '0:v',
+ '-map', '[a]',
+ ])
+
+ ffmpegCommand.extend([
+ # OUTPUT
+ '-vcodec', vencoder,
+ '-acodec', aencoder,
+ '-b:v', vbitrate,
+ '-b:a', abitrate,
+ '-pix_fmt', Core.settings.value('outputVideoFormat'),
+ '-preset', Core.settings.value('outputPreset'),
+ '-f', container
+ ])
+
+ if acodec == 'aac':
+ ffmpegCommand.append('-strict')
+ ffmpegCommand.append('-2')
+
+ ffmpegCommand.append(outputFile)
+ return ffmpegCommand
+
+
+def getAudioDuration(filename):
+ command = [Core.FFMPEG_BIN, '-i', filename]
+
+ try:
+ fileInfo = checkOutput(command, stderr=sp.STDOUT)
+ except sp.CalledProcessError as ex:
+ fileInfo = ex.output
+
+ info = fileInfo.decode("utf-8").split('\n')
+ for line in info:
+ if 'Duration' in line:
+ d = line.split(',')[0]
+ d = d.split(' ')[3]
+ d = d.split(':')
+ duration = float(d[0])*3600 + float(d[1])*60 + float(d[2])
+ return duration
+
+
+def readAudioFile(filename, parent):
+ duration = getAudioDuration(filename)
+
+ command = [
+ Core.FFMPEG_BIN,
+ '-i', filename,
+ '-f', 's16le',
+ '-acodec', 'pcm_s16le',
+ '-ar', '44100', # ouput will have 44100 Hz
+ '-ac', '1', # mono (set to '2' for stereo)
+ '-']
+ in_pipe = openPipe(
+ command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8
+ )
+
+ completeAudioArray = numpy.empty(0, dtype="int16")
+
+ progress = 0
+ lastPercent = None
+ while True:
+ if Core.canceled:
+ return
+ # read 2 seconds of audio
+ progress += 4
+ raw_audio = in_pipe.stdout.read(88200*4)
+ if len(raw_audio) == 0:
+ break
+ audio_array = numpy.fromstring(raw_audio, dtype="int16")
+ completeAudioArray = numpy.append(completeAudioArray, audio_array)
+
+ percent = int(100*(progress/duration))
+ if percent >= 100:
+ percent = 100
+
+ if lastPercent != percent:
+ string = 'Loading audio file: '+str(percent)+'%'
+ parent.progressBarSetText.emit(string)
+ parent.progressBarUpdate.emit(percent)
+
+ lastPercent = percent
+
+ in_pipe.kill()
+ in_pipe.wait()
+
+ # add 0s the end
+ completeAudioArrayCopy = numpy.zeros(
+ len(completeAudioArray) + 44100, dtype="int16")
+ completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray
+ completeAudioArray = completeAudioArrayCopy
+
+ return (completeAudioArray, duration)
--
cgit v1.2.3
From 450b944b87487aa60a935bbeee3908e2a62cd45b Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 20 Jul 2017 22:37:15 -0400
Subject: add component in context menu, del/ins hotkeys
+ preset manager uses mainwindow component list
---
freeze.py | 4 +-
setup.py | 4 +-
src/__init__.py | 1 +
src/components/video.py | 4 +-
src/core.py | 10 ++--
src/mainwindow.py | 135 +++++++++++++++++++++++++++++-------------------
src/presetmanager.py | 23 +++++++--
src/toolkit/ffmpeg.py | 9 +++-
8 files changed, 122 insertions(+), 68 deletions(-)
(limited to 'src/toolkit/ffmpeg.py')
diff --git a/freeze.py b/freeze.py
index 3281cad..520b445 100644
--- a/freeze.py
+++ b/freeze.py
@@ -2,7 +2,7 @@ from cx_Freeze import setup, Executable
import sys
import os
-from setup import VERSION
+from setup import __version__
deps = [os.path.join('src', p) for p in os.listdir('src') if p]
@@ -52,7 +52,7 @@ executables = [
setup(
name='audio-visualizer-python',
- version=VERSION,
+ version=__version__,
description='GUI tool to render visualization videos of audio files',
options=dict(build_exe=buildOptions),
executables=executables
diff --git a/setup.py b/setup.py
index 5abb976..a2d8495 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup
import os
-VERSION = '2.0.0.rc1'
+__version__ = '2.0.0.rc1'
def package_files(directory):
@@ -15,7 +15,7 @@ def package_files(directory):
setup(
name='audio_visualizer_python',
- version=VERSION,
+ version=__version__,
url='https://github.com/djfun/audio-visualizer-python/tree/feature-newgui',
license='MIT',
description='Create audio visualization videos from a GUI or commandline',
diff --git a/src/__init__.py b/src/__init__.py
index e69de29..8b13789 100644
--- a/src/__init__.py
+++ b/src/__init__.py
@@ -0,0 +1 @@
+
diff --git a/src/components/video.py b/src/components/video.py
index b35c2e5..8758b12 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -16,7 +16,7 @@ class Video:
'''Video Component Frame-Fetcher'''
def __init__(self, **kwargs):
mandatoryArgs = [
- 'ffmpeg', # path to ffmpeg, usually core.FFMPEG_BIN
+ 'ffmpeg', # path to ffmpeg, usually Core.FFMPEG_BIN
'videoPath',
'width',
'height',
@@ -28,7 +28,7 @@ class Video:
]
for arg in mandatoryArgs:
try:
- exec('self.%s = kwargs[arg]' % arg)
+ setattr(self, arg, kwargs[arg])
except KeyError:
raise BadComponentInit(arg, self.__doc__)
diff --git a/src/core.py b/src/core.py
index dd2ef18..f6cf5eb 100644
--- a/src/core.py
+++ b/src/core.py
@@ -15,16 +15,14 @@ import video_thread
class Core:
'''
MainWindow and Command module both use an instance of this class
- to store the main program state. This object tracks the components
- as an instance, has methods for managing the components and for
- opening/creating project files and presets.
+ to store the core program state. This object tracks the components,
+ talks to the components and handles opening/creating project files
+ and presets. The class also stores constants as class variables.
'''
@classmethod
def storeSettings(cls):
- '''
- Stores settings/paths to directories as class variables
- '''
+ '''Store settings/paths to directories as class variables.'''
if getattr(sys, 'frozen', False):
# frozen
wd = os.path.dirname(sys.executable)
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 9944d1a..2d598ae 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -178,7 +178,6 @@ class MainWindow(QtWidgets.QMainWindow):
# Make component buttons
self.compMenu = QMenu()
- self.compActions = []
for i, comp in enumerate(self.core.modules):
action = self.compMenu.addAction(comp.Component.name)
action.triggered.connect(
@@ -191,6 +190,9 @@ class MainWindow(QtWidgets.QMainWindow):
componentList.itemSelectionChanged.connect(
self.changeComponentWidget
)
+ componentList.itemSelectionChanged.connect(
+ self.presetManager.clearPresetListSelection
+ )
self.window.pushButton_removeComponent.clicked.connect(
lambda: self.removeComponent()
)
@@ -313,22 +315,23 @@ class MainWindow(QtWidgets.QMainWindow):
)
self.settings.setValue("ffmpegMsgShown", True)
- # Setup Hotkeys
+ # Hotkeys for projects
QtWidgets.QShortcut("Ctrl+S", self.window, self.saveCurrentProject)
QtWidgets.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog)
QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog)
QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject)
- QtWidgets.QShortcut(
- "Ctrl+Alt+Shift+R", self.window, self.drawPreview
- )
- QtWidgets.QShortcut(
- "Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand
- )
- QtWidgets.QShortcut(
- "Ctrl+T", self.window,
- activated=lambda: self.window.pushButton_addComponent.click()
- )
+ # Hotkeys for component list
+ for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert):
+ QtWidgets.QShortcut(
+ inskey, self.window,
+ activated=lambda: self.window.pushButton_addComponent.click()
+ )
+ for delkey in ("Ctrl+R", QtCore.Qt.Key_Delete):
+ QtWidgets.QShortcut(
+ delkey, self.window.listWidget_componentList,
+ self.removeComponent
+ )
QtWidgets.QShortcut(
"Ctrl+Space", self.window,
activated=lambda: self.window.listWidget_componentList.setFocus()
@@ -342,22 +345,29 @@ class MainWindow(QtWidgets.QMainWindow):
)
QtWidgets.QShortcut(
- "Ctrl+Up", self.window,
+ "Ctrl+Up", self.window.listWidget_componentList,
activated=lambda: self.moveComponent(-1)
)
QtWidgets.QShortcut(
- "Ctrl+Down", self.window,
+ "Ctrl+Down", self.window.listWidget_componentList,
activated=lambda: self.moveComponent(1)
)
QtWidgets.QShortcut(
- "Ctrl+Home", self.window,
+ "Ctrl+Home", self.window.listWidget_componentList,
activated=lambda: self.moveComponent('top')
)
QtWidgets.QShortcut(
- "Ctrl+End", self.window,
+ "Ctrl+End", self.window.listWidget_componentList,
activated=lambda: self.moveComponent('bottom')
)
- QtWidgets.QShortcut("Ctrl+r", self.window, self.removeComponent)
+
+ # Debug Hotkeys
+ QtWidgets.QShortcut(
+ "Ctrl+Alt+Shift+R", self.window, self.drawPreview
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand
+ )
@QtCore.pyqtSlot()
def cleanUp(self):
@@ -677,9 +687,7 @@ class MainWindow(QtWidgets.QMainWindow):
stackedWidget.setCurrentIndex(newRow)
self.drawPreview()
- @disableWhenEncoding
- def dragComponent(self, event):
- '''Used as Qt drop event for the component listwidget'''
+ def getComponentListRects(self):
componentList = self.window.listWidget_componentList
modelIndexes = [
@@ -690,6 +698,13 @@ class MainWindow(QtWidgets.QMainWindow):
componentList.visualRect(modelIndex)
for modelIndex in modelIndexes
]
+ return rects
+
+ @disableWhenEncoding
+ def dragComponent(self, event):
+ '''Used as Qt drop event for the component listwidget'''
+ componentList = self.window.listWidget_componentList
+ rects = self.getComponentListRects()
rowPos = [rect.contains(event.pos()) for rect in rects]
if not any(rowPos):
@@ -826,47 +841,63 @@ class MainWindow(QtWidgets.QMainWindow):
@disableWhenEncoding
def componentContextMenu(self, QPos):
- '''Appears when right-clicking a component in the list'''
+ '''Appears when right-clicking the component list'''
componentList = self.window.listWidget_componentList
- if not componentList.selectedItems():
- return
-
- # don't show menu if clicking empty space
- parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0))
index = componentList.currentRow()
- modelIndex = componentList.model().index(index)
- if not componentList.visualRect(modelIndex).contains(QPos):
- return
- self.presetManager.findPresets()
self.menu = QMenu()
- menuItem = self.menu.addAction("Save Preset")
- menuItem.triggered.connect(
- self.presetManager.openSavePresetDialog
- )
+ parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0))
- # submenu for opening presets
- try:
- presets = self.presetManager.presets[
- str(self.core.selectedComponents[index])
- ]
- self.submenu = QMenu("Open Preset")
- self.menu.addMenu(self.submenu)
-
- for version, presetName in presets:
- menuItem = self.submenu.addAction(presetName)
+ rects = self.getComponentListRects()
+ rowPos = [rect.contains(QPos) for rect in rects]
+ if not any(rowPos):
+ # Insert components at the top if clicking nothing
+ rowPos = 0
+ else:
+ rowPos = rowPos.index(True)
+
+ if index == rowPos:
+ # Show preset menu if clicking a component
+ self.presetManager.findPresets()
+ menuItem = self.menu.addAction("Save Preset")
+ menuItem.triggered.connect(
+ self.presetManager.openSavePresetDialog
+ )
+
+ # submenu for opening presets
+ try:
+ presets = self.presetManager.presets[
+ str(self.core.selectedComponents[index])
+ ]
+ self.presetSubmenu = QMenu("Open Preset")
+ self.menu.addMenu(self.presetSubmenu)
+
+ for version, presetName in presets:
+ menuItem = self.presetSubmenu.addAction(presetName)
+ menuItem.triggered.connect(
+ lambda _, presetName=presetName:
+ self.presetManager.openPreset(presetName)
+ )
+ except KeyError:
+ pass
+
+ if self.core.selectedComponents[index].currentPreset:
+ menuItem = self.menu.addAction("Clear Preset")
menuItem.triggered.connect(
- lambda _, presetName=presetName:
- self.presetManager.openPreset(presetName)
+ self.presetManager.clearPreset
)
- except KeyError:
- pass
+ self.menu.addSeparator()
- if self.core.selectedComponents[index].currentPreset:
- menuItem = self.menu.addAction("Clear Preset")
+ # "Add Component" submenu
+ self.submenu = QMenu("Add")
+ self.menu.addMenu(self.submenu)
+ for i, comp in enumerate(self.core.modules):
+ menuItem = self.submenu.addAction(comp.Component.name)
menuItem.triggered.connect(
- self.presetManager.clearPreset
- )
+ lambda _, item=i: self.core.insertComponent(
+ rowPos, item, self
+ )
+ )
self.menu.move(parentPosition + QPos)
self.menu.show()
diff --git a/src/presetmanager.py b/src/presetmanager.py
index 825fdee..64e2203 100644
--- a/src/presetmanager.py
+++ b/src/presetmanager.py
@@ -245,11 +245,25 @@ class PresetManager(QtWidgets.QDialog):
def openRenamePresetDialog(self):
# TODO: maintain consistency by changing this to call createNewPreset()
presetList = self.window.listWidget_presets
- if presetList.currentRow() == -1:
- return
+ index = presetList.currentRow()
+ if index == -1:
+ # check if component selected in MainWindow has preset loaded
+ componentList = self.parent.window.listWidget_componentList
+ compIndex = componentList.currentRow()
+ if compIndex == -1:
+ return
+ preset = self.core.selectedComponents[compIndex].currentPreset
+ if not preset:
+ return
+ else:
+ for i, tup in enumerate(self.presetRows):
+ if preset == tup[2]:
+ index = i
+ break
+ else:
+ return
while True:
- index = presetList.currentRow()
newName, OK = QtWidgets.QInputDialog.getText(
self.window,
'Preset Manager',
@@ -321,3 +335,6 @@ class PresetManager(QtWidgets.QDialog):
parent=self.window
)
self.settings.setValue("presetDir", os.path.dirname(filename))
+
+ def clearPresetListSelection(self):
+ self.window.listWidget_presets.setCurrentRow(-1)
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index 89d4e9d..cc59a6c 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -113,7 +113,7 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
'-t', safeDuration,
# Tell ffmpeg about shorter clips (seemingly not needed)
# streamDuration = getAudioDuration(extraInputFile)
- # if streamDuration > float(safeDuration)
+ # if streamDuration and streamDuration > float(safeDuration)
# else "{0:.3f}".format(streamDuration),
'-i', extraInputFile
])
@@ -228,11 +228,18 @@ def getAudioDuration(filename):
d = d.split(' ')[3]
d = d.split(':')
duration = float(d[0])*3600 + float(d[1])*60 + float(d[2])
+ break
+ else:
+ # String not found in output
+ return False
return duration
def readAudioFile(filename, parent):
duration = getAudioDuration(filename)
+ if not duration:
+ print('Audio file doesn\'t exist or unreadable.')
+ return
command = [
Core.FFMPEG_BIN,
--
cgit v1.2.3
From bf0890e7c87c730b8970c1a20c5b6a9a1a55d203 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 23 Jul 2017 01:53:54 -0400
Subject: components auto-connect & track widgets, less autosave spam
importing toolkit from live interpreter now works
---
setup.py | 2 +-
src/__init__.py | 12 +++
src/command.py | 2 -
src/component.py | 196 +++++++++++++++++++++++++++++++++------------
src/components/color.py | 137 +++++++++++--------------------
src/components/image.py | 77 +++++-------------
src/components/original.py | 59 ++++++--------
src/components/sound.py | 50 +++---------
src/components/text.py | 81 ++++++++-----------
src/components/video.py | 98 +++++++----------------
src/core.py | 196 ++++++++++++++++++++++++++++-----------------
src/main.py | 23 ++----
src/mainwindow.py | 125 +++++++++++++++++++----------
src/mainwindow.ui | 3 +
src/presetmanager.py | 15 ++--
src/preview_thread.py | 17 ++--
src/toolkit/common.py | 56 +++----------
src/toolkit/core.py | 18 -----
src/toolkit/ffmpeg.py | 46 ++++++++---
src/toolkit/frame.py | 4 +-
src/video_thread.py | 7 +-
21 files changed, 604 insertions(+), 620 deletions(-)
delete mode 100644 src/toolkit/core.py
(limited to 'src/toolkit/ffmpeg.py')
diff --git a/setup.py b/setup.py
index a2d8495..d4f226b 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup
import os
-__version__ = '2.0.0.rc1'
+__version__ = '2.0.0.rc2'
def package_files(directory):
diff --git a/src/__init__.py b/src/__init__.py
index 8b13789..2f4cffa 100644
--- a/src/__init__.py
+++ b/src/__init__.py
@@ -1 +1,13 @@
+import sys
+import os
+
+if getattr(sys, 'frozen', False):
+ # frozen
+ wd = os.path.dirname(sys.executable)
+else:
+ # unfrozen
+ wd = os.path.dirname(os.path.realpath(__file__))
+
+# make relative imports work when using /src as a package
+sys.path.insert(0, wd)
diff --git a/src/command.py b/src/command.py
index 046a1bf..ca186e5 100644
--- a/src/command.py
+++ b/src/command.py
@@ -10,7 +10,6 @@ import sys
import time
from core import Core
-from toolkit import loadDefaultSettings
class Command(QtCore.QObject):
@@ -55,7 +54,6 @@ class Command(QtCore.QObject):
self.args = self.parser.parse_args()
self.settings = Core.settings
- loadDefaultSettings(self)
if self.args.projpath:
projPath = self.args.projpath
diff --git a/src/component.py b/src/component.py
index 92cc65c..bec2df5 100644
--- a/src/component.py
+++ b/src/component.py
@@ -5,8 +5,28 @@
from PyQt5 import uic, QtCore, QtWidgets
import os
-from core import Core
-from toolkit.common import getPresetDir
+from presetmanager import getPresetDir
+
+
+def commandWrapper(func):
+ '''Intercepts each component's command() method to check for global args'''
+ def decorator(self, arg):
+ if arg.startswith('preset='):
+ _, preset = arg.split('=', 1)
+ path = os.path.join(getPresetDir(self), preset)
+ if not os.path.exists(path):
+ print('Couldn\'t locate preset "%s"' % preset)
+ quit(1)
+ else:
+ print('Opening "%s" preset on layer %s' % (
+ preset, self.compPos)
+ )
+ self.core.openPreset(path, self.compPos, preset)
+ # Don't call the component's command() method
+ return
+ else:
+ return func(self, arg)
+ return decorator
class ComponentMetaclass(type(QtCore.QObject)):
@@ -16,10 +36,14 @@ class ComponentMetaclass(type(QtCore.QObject)):
E.g., takes only major version from version string & decorates methods
'''
def __new__(cls, name, parents, attrs):
- # print('Creating %s component' % attrs['name'])
+ if 'ui' not in attrs:
+ # use module name as ui filename by default
+ attrs['ui'] = '%s.ui' % os.path.splitext(
+ attrs['__module__'].split('.')[-1]
+ )[0]
# Turn certain class methods into properties and classmethods
- for key in ('error', 'properties', 'audio', 'commandHelp'):
+ for key in ('error', 'properties', 'audio'):
if key not in attrs:
continue
attrs[key] = property(attrs[key])
@@ -29,6 +53,10 @@ class ComponentMetaclass(type(QtCore.QObject)):
continue
attrs[key] = classmethod(key)
+ # Do not apply these mutations to the base class
+ if parents[0] != QtCore.QObject:
+ attrs['command'] = commandWrapper(attrs['command'])
+
# Turn version string into a number
try:
if 'version' not in attrs:
@@ -54,19 +82,24 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
name = 'Component'
+ # ui = 'nameOfNonDefaultUiFile'
version = '1.0.0'
- # The 1st number (before dot, aka the major version) is used to determine
+ # The major version (before the first dot) is used to determine
# preset compatibility; the rest is ignored so it can be non-numeric.
modified = QtCore.pyqtSignal(int, dict)
# ^ Signal used to tell core program that the component state changed,
# you shouldn't need to use this directly, it is used by self.update()
- def __init__(self, moduleIndex, compPos):
+ def __init__(self, moduleIndex, compPos, core):
super().__init__()
- self.currentPreset = None
self.moduleIndex = moduleIndex
self.compPos = compPos
+ self.core = core
+ self.currentPreset = None
+
+ self._trackedWidgets = {}
+ self._presetNames = {}
# Stop lengthy processes in response to this variable
self.canceled = False
@@ -114,28 +147,103 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
return []
- def commandHelp(self):
- '''Help text as string for this component's commandline arguments'''
-
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
- def update(self):
- '''Read 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 widget(self, parent):
+ '''
+ Call super().widget(*args) to create the component widget
+ which also auto-connects any common widgets (e.g., checkBoxes)
+ to self.update(). Then in a subclass connect special actions
+ (e.g., pushButtons to select a file/colour) and initialize
+ '''
+ self.parent = parent
+ self.settings = parent.settings
+ self.page = self.loadUi(self.__class__.ui)
+
+ # Connect widget signals
+ widgets = {
+ 'lineEdit': self.page.findChildren(QtWidgets.QLineEdit),
+ 'checkBox': self.page.findChildren(QtWidgets.QCheckBox),
+ 'spinBox': self.page.findChildren(QtWidgets.QSpinBox),
+ 'comboBox': self.page.findChildren(QtWidgets.QComboBox),
+ }
+ widgets['spinBox'].extend(
+ self.page.findChildren(QtWidgets.QDoubleSpinBox)
+ )
+ for widget in widgets['lineEdit']:
+ widget.textChanged.connect(self.update)
+ for widget in widgets['checkBox']:
+ widget.stateChanged.connect(self.update)
+ for widget in widgets['spinBox']:
+ widget.valueChanged.connect(self.update)
+ for widget in widgets['comboBox']:
+ widget.currentIndexChanged.connect(self.update)
+
+ def trackWidgets(self, trackDict, presetNames=None):
+ '''
+ Name widgets to track in update(), savePreset(), and loadPreset()
+ Accepts a dict with attribute names as keys and widgets as values.
+ Optional: a dict of attribute names to map to preset variable names
+ '''
+ self._trackedWidgets = trackDict
+ if type(presetNames) is dict:
+ self._presetNames = presetNames
- def loadPreset(self, presetDict, presetName):
+ def update(self):
'''
- Subclasses take (presetDict, presetName=None) as args.
- Must use super().loadPreset(presetDict, presetName) first,
+ Reads all tracked widget values into instance attributes
+ and tells the MainWindow that the component was modified.
+ Call at the END of your method if you need to subclass this.
+ '''
+ for attr, widget in self._trackedWidgets.items():
+ if type(widget) == QtWidgets.QLineEdit:
+ setattr(self, attr, widget.text())
+ elif type(widget) == QtWidgets.QSpinBox \
+ or type(widget) == QtWidgets.QDoubleSpinBox:
+ setattr(self, attr, widget.value())
+ elif type(widget) == QtWidgets.QCheckBox:
+ setattr(self, attr, widget.isChecked())
+ elif type(widget) == QtWidgets.QComboBox:
+ setattr(self, attr, widget.currentIndex())
+ if not self.core.openingProject:
+ self.parent.drawPreview()
+ saveValueStore = self.savePreset()
+ saveValueStore['preset'] = self.currentPreset
+ self.modified.emit(self.compPos, saveValueStore)
+
+ def loadPreset(self, presetDict, presetName=None):
+ '''
+ Subclasses should take (presetDict, *args) as args.
+ Must use super().loadPreset(presetDict, *args) first,
then update self.page widgets using the preset dict.
'''
self.currentPreset = presetName \
if presetName is not None else presetDict['preset']
+ for attr, widget in self._trackedWidgets.items():
+ val = presetDict[
+ attr if attr not in self._presetNames
+ else self._presetNames[attr]
+ ]
+ if type(widget) == QtWidgets.QLineEdit:
+ widget.setText(val)
+ elif type(widget) == QtWidgets.QSpinBox \
+ or type(widget) == QtWidgets.QDoubleSpinBox:
+ widget.setValue(val)
+ elif type(widget) == QtWidgets.QCheckBox:
+ widget.setChecked(val)
+ elif type(widget) == QtWidgets.QComboBox:
+ widget.setCurrentIndex(val)
+
+ def savePreset(self):
+ saveValueStore = {}
+ for attr, widget in self._trackedWidgets.items():
+ saveValueStore[
+ attr if attr not in self._presetNames
+ else self._presetNames[attr]
+ ] = getattr(self, attr)
+ return saveValueStore
def preFrameRender(self, **kwargs):
'''
@@ -151,34 +259,27 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
for key, value in kwargs.items():
setattr(self, key, value)
- def command(self, arg):
+ def commandHelp(self):
+ '''Help text as string for this component's commandline arguments'''
+
+ def command(self, arg=''):
'''
- Configure a component using argument from the commandline.
- Use super().command(arg) at the end of a subclass's method,
- if no arguments are found in that method first
+ Configure a component using an arg from the commandline. This is
+ never called if global args like 'preset=' are found in the arg.
+ So simply check for any non-global args in your component and
+ call super().command() at the end to get a Help message.
'''
- if arg.startswith('preset='):
- _, preset = arg.split('=', 1)
- path = os.path.join(getPresetDir(self), preset)
- if not os.path.exists(path):
- print('Couldn\'t locate preset "%s"' % preset)
- quit(1)
- else:
- print('Opening "%s" preset on layer %s' % (
- preset, self.compPos)
- )
- self.core.openPreset(path, self.compPos, preset)
- else:
- print(
- self.__doc__, 'Usage:\n'
- 'Open a preset for this component:\n'
- ' "preset=Preset Name"')
- print(self.commandHelp)
- quit(0)
+ print(
+ self.__class__.name, 'Usage:\n'
+ 'Open a preset for this component:\n'
+ ' "preset=Preset Name"'
+ )
+ self.commandHelp()
+ quit(0)
def loadUi(self, filename):
'''Load a Qt Designer ui file to use for this component's widget'''
- return uic.loadUi(os.path.join(Core.componentsPath, filename))
+ return uic.loadUi(os.path.join(self.core.componentsPath, filename))
def cancel(self):
'''Stop any lengthy process in response to this variable.'''
@@ -191,16 +292,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
### Reference methods for creating a new component
### (Inherit from this class and define these)
- def widget(self, parent):
- self.parent = parent
- self.settings = parent.settings
- self.page = self.loadUi('example.ui')
- # --- connect widget signals here ---
- return self.page
-
def previewRender(self, previewWorker):
width = int(self.settings.value('outputWidth'))
- height = int(previewWorker.core.settings.value('outputHeight'))
+ height = int(self.settings.value('outputHeight'))
from toolkit.frame import BlankFrame
image = BlankFrame(width, height)
return image
@@ -217,7 +311,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
class BadComponentInit(Exception):
'''
- General purpose exception components can raise to indicate
+ General purpose exception that components can raise to indicate
a Python issue with e.g., dynamic creation of instances or something.
Decorative for now, may have future use for logging.
'''
diff --git a/src/components/color.py b/src/components/color.py
index 03371e7..8257ed9 100644
--- a/src/components/color.py
+++ b/src/components/color.py
@@ -13,18 +13,15 @@ class Component(Component):
name = 'Color'
version = '1.0.0'
- def widget(self, parent):
- self.parent = parent
- self.settings = parent.settings
- page = self.loadUi('color.ui')
-
+ def widget(self, *args):
self.color1 = (0, 0, 0)
self.color2 = (133, 133, 133)
self.x = 0
self.y = 0
+ super().widget(*args)
- page.lineEdit_color1.setText('%s,%s,%s' % self.color1)
- page.lineEdit_color2.setText('%s,%s,%s' % self.color2)
+ self.page.lineEdit_color1.setText('%s,%s,%s' % self.color1)
+ self.page.lineEdit_color2.setText('%s,%s,%s' % self.color2)
btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.color1).name()
@@ -32,68 +29,55 @@ class Component(Component):
btnStyle2 = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.color2).name()
- page.pushButton_color1.setStyleSheet(btnStyle1)
- page.pushButton_color2.setStyleSheet(btnStyle2)
- page.pushButton_color1.clicked.connect(lambda: self.pickColor(1))
- page.pushButton_color2.clicked.connect(lambda: self.pickColor(2))
+ self.page.pushButton_color1.setStyleSheet(btnStyle1)
+ self.page.pushButton_color2.setStyleSheet(btnStyle2)
+ self.page.pushButton_color1.clicked.connect(lambda: self.pickColor(1))
+ self.page.pushButton_color2.clicked.connect(lambda: self.pickColor(2))
# disable color #2 until non-default 'fill' option gets changed
- page.lineEdit_color2.setDisabled(True)
- page.pushButton_color2.setDisabled(True)
- page.spinBox_x.valueChanged.connect(self.update)
- page.spinBox_y.valueChanged.connect(self.update)
- page.spinBox_width.setValue(
+ self.page.lineEdit_color2.setDisabled(True)
+ self.page.pushButton_color2.setDisabled(True)
+ self.page.spinBox_width.setValue(
int(self.settings.value("outputWidth")))
- page.spinBox_height.setValue(
+ self.page.spinBox_height.setValue(
int(self.settings.value("outputHeight")))
- page.lineEdit_color1.textChanged.connect(self.update)
- page.lineEdit_color2.textChanged.connect(self.update)
- page.spinBox_x.valueChanged.connect(self.update)
- page.spinBox_y.valueChanged.connect(self.update)
- page.spinBox_width.valueChanged.connect(self.update)
- page.spinBox_height.valueChanged.connect(self.update)
- page.checkBox_trans.stateChanged.connect(self.update)
-
self.fillLabels = [
'Solid',
'Linear Gradient',
'Radial Gradient',
]
for label in self.fillLabels:
- page.comboBox_fill.addItem(label)
- page.comboBox_fill.setCurrentIndex(0)
- page.comboBox_fill.currentIndexChanged.connect(self.update)
- page.comboBox_spread.currentIndexChanged.connect(self.update)
- page.spinBox_radialGradient_end.valueChanged.connect(self.update)
- page.spinBox_radialGradient_start.valueChanged.connect(self.update)
- page.spinBox_radialGradient_spread.valueChanged.connect(self.update)
- page.spinBox_linearGradient_end.valueChanged.connect(self.update)
- page.spinBox_linearGradient_start.valueChanged.connect(self.update)
- page.checkBox_stretch.stateChanged.connect(self.update)
-
- self.page = page
- return page
+ self.page.comboBox_fill.addItem(label)
+ self.page.comboBox_fill.setCurrentIndex(0)
+
+ self.trackWidgets(
+ {
+ 'x': self.page.spinBox_x,
+ 'y': self.page.spinBox_y,
+ 'sizeWidth': self.page.spinBox_width,
+ 'sizeHeight': self.page.spinBox_height,
+ 'trans': self.page.checkBox_trans,
+ 'spread': self.page.comboBox_spread,
+ 'stretch': self.page.checkBox_stretch,
+ 'RG_start': self.page.spinBox_radialGradient_start,
+ 'LG_start': self.page.spinBox_linearGradient_start,
+ 'RG_end': self.page.spinBox_radialGradient_end,
+ 'LG_end': self.page.spinBox_linearGradient_end,
+ 'RG_centre': self.page.spinBox_radialGradient_spread,
+ 'fillType': self.page.comboBox_fill,
+ }, presetNames={
+ 'sizeWidth': 'width',
+ 'sizeHeight': 'height',
+ }
+ )
def update(self):
self.color1 = rgbFromString(self.page.lineEdit_color1.text())
self.color2 = rgbFromString(self.page.lineEdit_color2.text())
- self.x = self.page.spinBox_x.value()
- self.y = self.page.spinBox_y.value()
- self.sizeWidth = self.page.spinBox_width.value()
- self.sizeHeight = self.page.spinBox_height.value()
- self.trans = self.page.checkBox_trans.isChecked()
- self.spread = self.page.comboBox_spread.currentIndex()
-
- self.RG_start = self.page.spinBox_radialGradient_start.value()
- self.RG_end = self.page.spinBox_radialGradient_end.value()
- self.RG_centre = self.page.spinBox_radialGradient_spread.value()
- self.stretch = self.page.checkBox_stretch.isChecked()
- self.LG_start = self.page.spinBox_linearGradient_start.value()
- self.LG_end = self.page.spinBox_linearGradient_end.value()
-
- self.fillType = self.page.comboBox_fill.currentIndex()
- if self.fillType == 0:
+
+ fillType = self.page.comboBox_fill.currentIndex()
+ if fillType == 0:
self.page.lineEdit_color2.setEnabled(False)
self.page.pushButton_color2.setEnabled(False)
self.page.checkBox_trans.setEnabled(False)
@@ -105,10 +89,10 @@ class Component(Component):
self.page.checkBox_trans.setEnabled(True)
self.page.checkBox_stretch.setEnabled(True)
self.page.comboBox_spread.setEnabled(True)
- if self.trans:
+ if self.page.checkBox_trans.isChecked():
self.page.lineEdit_color2.setEnabled(False)
self.page.pushButton_color2.setEnabled(False)
- self.page.fillWidget.setCurrentIndex(self.fillType)
+ self.page.fillWidget.setCurrentIndex(fillType)
super().update()
@@ -181,25 +165,11 @@ class Component(Component):
return image.finalize()
- def loadPreset(self, pr, presetName=None):
- super().loadPreset(pr, presetName)
+ def loadPreset(self, pr, *args):
+ super().loadPreset(pr, *args)
- self.page.comboBox_fill.setCurrentIndex(pr['fillType'])
self.page.lineEdit_color1.setText('%s,%s,%s' % pr['color1'])
self.page.lineEdit_color2.setText('%s,%s,%s' % pr['color2'])
- self.page.spinBox_x.setValue(pr['x'])
- self.page.spinBox_y.setValue(pr['y'])
- self.page.spinBox_width.setValue(pr['width'])
- self.page.spinBox_height.setValue(pr['height'])
- self.page.checkBox_trans.setChecked(pr['trans'])
-
- self.page.spinBox_radialGradient_start.setValue(pr['RG_start'])
- self.page.spinBox_radialGradient_end.setValue(pr['RG_end'])
- self.page.spinBox_radialGradient_spread.setValue(pr['RG_centre'])
- self.page.spinBox_linearGradient_start.setValue(pr['LG_start'])
- self.page.spinBox_linearGradient_end.setValue(pr['LG_end'])
- self.page.checkBox_stretch.setChecked(pr['stretch'])
- self.page.comboBox_spread.setCurrentIndex(pr['spread'])
btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*pr['color1']).name()
@@ -209,23 +179,10 @@ class Component(Component):
self.page.pushButton_color2.setStyleSheet(btnStyle2)
def savePreset(self):
- return {
- 'color1': self.color1,
- 'color2': self.color2,
- 'x': self.x,
- 'y': self.y,
- 'fillType': self.fillType,
- 'width': self.sizeWidth,
- 'height': self.sizeHeight,
- 'trans': self.trans,
- 'stretch': self.stretch,
- 'spread': self.spread,
- 'RG_start': self.RG_start,
- 'RG_end': self.RG_end,
- 'RG_centre': self.RG_centre,
- 'LG_start': self.LG_start,
- 'LG_end': self.LG_end,
- }
+ saveValueStore = super().savePreset()
+ saveValueStore['color1'] = self.color1
+ saveValueStore['color2'] = self.color2
+ return saveValueStore
def pickColor(self, num):
RGBstring, btnStyle = pickColor()
@@ -242,7 +199,7 @@ class Component(Component):
print('Specify a color:\n color=255,255,255')
def command(self, arg):
- if not arg.startswith('preset=') and '=' in arg:
+ if '=' in arg:
key, arg = arg.split('=', 1)
if key == 'color':
self.page.lineEdit_color1.setText(arg)
diff --git a/src/components/image.py b/src/components/image.py
index 591e03e..a705904 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -2,7 +2,6 @@ from PIL import Image, ImageDraw, ImageEnhance
from PyQt5 import QtGui, QtCore, QtWidgets
import os
-from core import Core
from component import Component
from toolkit.frame import BlankFrame
@@ -11,35 +10,26 @@ class Component(Component):
name = 'Image'
version = '1.0.0'
- def widget(self, parent):
- self.parent = parent
- self.settings = parent.settings
- page = self.loadUi('image.ui')
-
- page.lineEdit_image.textChanged.connect(self.update)
- page.pushButton_image.clicked.connect(self.pickImage)
- page.spinBox_scale.valueChanged.connect(self.update)
- page.spinBox_rotate.valueChanged.connect(self.update)
- page.spinBox_color.valueChanged.connect(self.update)
- page.checkBox_stretch.stateChanged.connect(self.update)
- page.checkBox_mirror.stateChanged.connect(self.update)
- page.spinBox_x.valueChanged.connect(self.update)
- page.spinBox_y.valueChanged.connect(self.update)
-
- self.page = page
- return page
-
- def update(self):
- self.imagePath = self.page.lineEdit_image.text()
- self.scale = self.page.spinBox_scale.value()
- self.rotate = self.page.spinBox_rotate.value()
- self.color = self.page.spinBox_color.value()
- self.xPosition = self.page.spinBox_x.value()
- self.yPosition = self.page.spinBox_y.value()
- self.stretched = self.page.checkBox_stretch.isChecked()
- self.mirror = self.page.checkBox_mirror.isChecked()
-
- super().update()
+ def widget(self, *args):
+ super().widget(*args)
+ self.page.pushButton_image.clicked.connect(self.pickImage)
+ self.trackWidgets(
+ {
+ 'imagePath': self.page.lineEdit_image,
+ 'scale': self.page.spinBox_scale,
+ 'rotate': self.page.spinBox_rotate,
+ 'color': self.page.spinBox_color,
+ 'xPosition': self.page.spinBox_x,
+ 'yPosition': self.page.spinBox_y,
+ 'stretched': self.page.checkBox_stretch,
+ 'mirror': self.page.checkBox_mirror,
+ },
+ presetNames={
+ 'imagePath': 'image',
+ 'xPosition': 'x',
+ 'yPosition': 'y',
+ },
+ )
def previewRender(self, previewWorker):
width = int(self.settings.value('outputWidth'))
@@ -89,41 +79,18 @@ class Component(Component):
return frame
- def loadPreset(self, pr, presetName=None):
- super().loadPreset(pr, presetName)
- self.page.lineEdit_image.setText(pr['image'])
- self.page.spinBox_scale.setValue(pr['scale'])
- self.page.spinBox_color.setValue(pr['color'])
- self.page.spinBox_rotate.setValue(pr['rotate'])
- self.page.spinBox_x.setValue(pr['x'])
- self.page.spinBox_y.setValue(pr['y'])
- self.page.checkBox_stretch.setChecked(pr['stretched'])
- self.page.checkBox_mirror.setChecked(pr['mirror'])
-
- def savePreset(self):
- return {
- 'image': self.imagePath,
- 'scale': self.scale,
- 'color': self.color,
- 'rotate': self.rotate,
- 'stretched': self.stretched,
- 'mirror': self.mirror,
- 'x': self.xPosition,
- 'y': self.yPosition,
- }
-
def pickImage(self):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Image", imgDir,
- "Image Files (%s)" % " ".join(Core.imageFormats))
+ "Image Files (%s)" % " ".join(self.core.imageFormats))
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_image.setText(filename)
self.update()
def command(self, arg):
- if not arg.startswith('preset=') and '=' in arg:
+ if '=' in arg:
key, arg = arg.split('=', 1)
if key == 'path' and os.path.exists(arg):
try:
diff --git a/src/components/original.py b/src/components/original.py
index ae40df3..2bda878 100644
--- a/src/components/original.py
+++ b/src/components/original.py
@@ -18,59 +18,46 @@ class Component(Component):
def names():
return ['Original Audio Visualization']
- def widget(self, parent):
- self.parent = parent
- self.settings = parent.settings
+ def widget(self, *args):
self.visColor = (255, 255, 255)
self.scale = 20
self.y = 0
- self.canceled = False
-
- page = self.loadUi('original.ui')
- page.comboBox_visLayout.addItem("Classic")
- page.comboBox_visLayout.addItem("Split")
- page.comboBox_visLayout.addItem("Bottom")
- page.comboBox_visLayout.addItem("Top")
- page.comboBox_visLayout.setCurrentIndex(0)
- page.comboBox_visLayout.currentIndexChanged.connect(self.update)
- page.lineEdit_visColor.setText('%s,%s,%s' % self.visColor)
- page.pushButton_visColor.clicked.connect(lambda: self.pickColor())
+ super().widget(*args)
+
+ self.page.comboBox_visLayout.addItem("Classic")
+ self.page.comboBox_visLayout.addItem("Split")
+ self.page.comboBox_visLayout.addItem("Bottom")
+ self.page.comboBox_visLayout.addItem("Top")
+ self.page.comboBox_visLayout.setCurrentIndex(0)
+
+ self.page.lineEdit_visColor.setText('%s,%s,%s' % self.visColor)
+ self.page.pushButton_visColor.clicked.connect(lambda: self.pickColor())
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.visColor).name()
- page.pushButton_visColor.setStyleSheet(btnStyle)
- page.lineEdit_visColor.textChanged.connect(self.update)
- page.spinBox_scale.valueChanged.connect(self.update)
- page.spinBox_y.valueChanged.connect(self.update)
+ self.page.pushButton_visColor.setStyleSheet(btnStyle)
- self.page = page
- return page
+ self.trackWidgets({
+ 'layout': self.page.comboBox_visLayout,
+ 'scale': self.page.spinBox_scale,
+ 'y': self.page.spinBox_y,
+ })
def update(self):
- self.layout = self.page.comboBox_visLayout.currentIndex()
self.visColor = rgbFromString(self.page.lineEdit_visColor.text())
- self.scale = self.page.spinBox_scale.value()
- self.y = self.page.spinBox_y.value()
-
super().update()
- def loadPreset(self, pr, presetName=None):
- super().loadPreset(pr, presetName)
+ def loadPreset(self, pr, *args):
+ super().loadPreset(pr, *args)
self.page.lineEdit_visColor.setText('%s,%s,%s' % pr['visColor'])
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*pr['visColor']).name()
self.page.pushButton_visColor.setStyleSheet(btnStyle)
- self.page.comboBox_visLayout.setCurrentIndex(pr['layout'])
- self.page.spinBox_scale.setValue(pr['scale'])
- self.page.spinBox_y.setValue(pr['y'])
def savePreset(self):
- return {
- 'layout': self.layout,
- 'visColor': self.visColor,
- 'scale': self.scale,
- 'y': self.y,
- }
+ saveValueStore = super().savePreset()
+ saveValueStore['visColor'] = self.visColor
+ return saveValueStore
def previewRender(self, previewWorker):
spectrum = numpy.fromfunction(
@@ -206,7 +193,7 @@ class Component(Component):
return im
def command(self, arg):
- if not arg.startswith('preset=') and '=' in arg:
+ if '=' in arg:
key, arg = arg.split('=', 1)
try:
if key == 'color':
diff --git a/src/components/sound.py b/src/components/sound.py
index 677a22f..dd3cbab 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -10,26 +10,15 @@ class Component(Component):
name = 'Sound'
version = '1.0.0'
- 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)
- page.checkBox_chorus.stateChanged.connect(self.update)
- page.spinBox_delay.valueChanged.connect(self.update)
- page.spinBox_volume.valueChanged.connect(self.update)
-
- self.page = page
- return page
-
- def update(self):
- self.sound = self.page.lineEdit_sound.text()
- self.delay = self.page.spinBox_delay.value()
- self.volume = self.page.spinBox_volume.value()
- self.chorus = self.page.checkBox_chorus.isChecked()
- super().update()
+ def widget(self, *args):
+ super().widget(*args)
+ self.page.pushButton_sound.clicked.connect(self.pickSound)
+ self.trackWidgets({
+ 'sound': self.page.lineEdit_sound,
+ 'chorus': self.page.checkBox_chorus,
+ 'delay': self.page.spinBox_delay,
+ 'volume': self.page.spinBox_volume,
+ })
def previewRender(self, previewWorker):
width = int(self.settings.value('outputWidth'))
@@ -67,7 +56,7 @@ class Component(Component):
sndDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Sound", sndDir,
- "Audio Files (%s)" % " ".join(Core.audioFormats))
+ "Audio Files (%s)" % " ".join(self.core.audioFormats))
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_sound.setText(filename)
@@ -78,30 +67,15 @@ class Component(Component):
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'])
- self.page.checkBox_chorus.setChecked(pr['chorus'])
- self.page.spinBox_delay.setValue(pr['delay'])
- self.page.spinBox_volume.setValue(pr['volume'])
-
- def savePreset(self):
- return {
- 'sound': self.sound,
- 'chorus': self.chorus,
- 'delay': self.delay,
- 'volume': self.volume,
- }
-
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:
+ if '=' in arg:
key, arg = arg.split('=', 1)
if key == 'path':
if '*%s' % os.path.splitext(arg)[1] \
- not in Core.audioFormats:
+ not in self.core.audioFormats:
print("Not a supported audio format")
quit(1)
self.page.lineEdit_sound.setText(arg)
diff --git a/src/components/text.py b/src/components/text.py
index d511f22..1d64617 100644
--- a/src/components/text.py
+++ b/src/components/text.py
@@ -16,12 +16,10 @@ class Component(Component):
super().__init__(*args)
self.titleFont = QFont()
- def widget(self, parent):
- self.parent = parent
- self.settings = parent.settings
+ def widget(self, *args):
+ super().widget(*args)
height = int(self.settings.value('outputHeight'))
width = int(self.settings.value('outputWidth'))
-
self.textColor = (255, 255, 255)
self.title = 'Text'
self.alignment = 1
@@ -30,40 +28,35 @@ class Component(Component):
self.xPosition = width / 2 - fm.width(self.title)/2
self.yPosition = height / 2 * 1.036
- page = self.loadUi('text.ui')
- page.comboBox_textAlign.addItem("Left")
- page.comboBox_textAlign.addItem("Middle")
- page.comboBox_textAlign.addItem("Right")
+ self.page.comboBox_textAlign.addItem("Left")
+ self.page.comboBox_textAlign.addItem("Middle")
+ self.page.comboBox_textAlign.addItem("Right")
- page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor)
- page.pushButton_textColor.clicked.connect(self.pickColor)
+ self.page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor)
+ self.page.pushButton_textColor.clicked.connect(self.pickColor)
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.textColor).name()
- page.pushButton_textColor.setStyleSheet(btnStyle)
-
- page.lineEdit_title.setText(self.title)
- page.comboBox_textAlign.setCurrentIndex(int(self.alignment))
- page.spinBox_fontSize.setValue(int(self.fontSize))
- page.spinBox_xTextAlign.setValue(int(self.xPosition))
- page.spinBox_yTextAlign.setValue(int(self.yPosition))
-
- page.fontComboBox_titleFont.currentFontChanged.connect(self.update)
- page.lineEdit_title.textChanged.connect(self.update)
- page.comboBox_textAlign.currentIndexChanged.connect(self.update)
- page.spinBox_xTextAlign.valueChanged.connect(self.update)
- page.spinBox_yTextAlign.valueChanged.connect(self.update)
- page.spinBox_fontSize.valueChanged.connect(self.update)
- page.lineEdit_textColor.textChanged.connect(self.update)
- self.page = page
- return page
+ self.page.pushButton_textColor.setStyleSheet(btnStyle)
+
+ self.page.lineEdit_title.setText(self.title)
+ self.page.comboBox_textAlign.setCurrentIndex(int(self.alignment))
+ self.page.spinBox_fontSize.setValue(int(self.fontSize))
+ self.page.spinBox_xTextAlign.setValue(int(self.xPosition))
+ self.page.spinBox_yTextAlign.setValue(int(self.yPosition))
+
+ self.page.fontComboBox_titleFont.currentFontChanged.connect(
+ self.update
+ )
+ self.trackWidgets({
+ 'title': self.page.lineEdit_title,
+ 'alignment': self.page.comboBox_textAlign,
+ 'fontSize': self.page.spinBox_fontSize,
+ 'xPosition': self.page.spinBox_xTextAlign,
+ 'yPosition': self.page.spinBox_yTextAlign,
+ })
def update(self):
- self.title = self.page.lineEdit_title.text()
- self.alignment = self.page.comboBox_textAlign.currentIndex()
self.titleFont = self.page.fontComboBox_titleFont.currentFont()
- self.fontSize = self.page.spinBox_fontSize.value()
- self.xPosition = self.page.spinBox_xTextAlign.value()
- self.yPosition = self.page.spinBox_yTextAlign.value()
self.textColor = rgbFromString(
self.page.lineEdit_textColor.text())
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
@@ -87,32 +80,22 @@ class Component(Component):
x = self.xPosition - offset
return x, self.yPosition
- def loadPreset(self, pr, presetName=None):
- super().loadPreset(pr, presetName)
+ def loadPreset(self, pr, *args):
+ super().loadPreset(pr, *args)
- self.page.lineEdit_title.setText(pr['title'])
font = QFont()
font.fromString(pr['titleFont'])
self.page.fontComboBox_titleFont.setCurrentFont(font)
- self.page.spinBox_fontSize.setValue(pr['fontSize'])
- self.page.comboBox_textAlign.setCurrentIndex(pr['alignment'])
- self.page.spinBox_xTextAlign.setValue(pr['xPosition'])
- self.page.spinBox_yTextAlign.setValue(pr['yPosition'])
self.page.lineEdit_textColor.setText('%s,%s,%s' % pr['textColor'])
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*pr['textColor']).name()
self.page.pushButton_textColor.setStyleSheet(btnStyle)
def savePreset(self):
- return {
- 'title': self.title,
- 'titleFont': self.titleFont.toString(),
- 'alignment': self.alignment,
- 'fontSize': self.fontSize,
- 'xPosition': self.xPosition,
- 'yPosition': self.yPosition,
- 'textColor': self.textColor
- }
+ saveValueStore = super().savePreset()
+ saveValueStore['titleFont'] = self.titleFont.toString()
+ saveValueStore['textColor'] = self.textColor
+ return saveValueStore
def previewRender(self, previewWorker):
width = int(self.settings.value('outputWidth'))
@@ -158,7 +141,7 @@ class Component(Component):
print('Set custom x, y position:\n x=500 y=500')
def command(self, arg):
- if not arg.startswith('preset=') and '=' in arg:
+ if '=' in arg:
key, arg = arg.split('=', 1)
if key == 'color':
self.page.lineEdit_textColor.setText(arg)
diff --git a/src/components/video.py b/src/components/video.py
index 8758b12..677e3ee 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -9,6 +9,7 @@ from queue import PriorityQueue
from core import Core
from component import Component, BadComponentInit
from toolkit.frame import BlankFrame
+from toolkit.ffmpeg import testAudioStream
from toolkit import openPipe, checkOutput
@@ -16,7 +17,7 @@ class Video:
'''Video Component Frame-Fetcher'''
def __init__(self, **kwargs):
mandatoryArgs = [
- 'ffmpeg', # path to ffmpeg, usually Core.FFMPEG_BIN
+ 'ffmpeg', # path to ffmpeg, usually self.core.FFMPEG_BIN
'videoPath',
'width',
'height',
@@ -110,47 +111,40 @@ class Component(Component):
name = 'Video'
version = '1.0.0'
- def widget(self, parent):
- self.parent = parent
- self.settings = parent.settings
- page = self.loadUi('video.ui')
+ def widget(self, *args):
self.videoPath = ''
self.badVideo = False
self.badAudio = False
self.x = 0
self.y = 0
self.loopVideo = False
-
- page.lineEdit_video.textChanged.connect(self.update)
- page.pushButton_video.clicked.connect(self.pickVideo)
- page.checkBox_loop.stateChanged.connect(self.update)
- page.checkBox_distort.stateChanged.connect(self.update)
- page.checkBox_useAudio.stateChanged.connect(self.update)
- page.spinBox_scale.valueChanged.connect(self.update)
- page.spinBox_volume.valueChanged.connect(self.update)
- page.spinBox_x.valueChanged.connect(self.update)
- page.spinBox_y.valueChanged.connect(self.update)
-
- self.page = page
- return page
+ super().widget(*args)
+ self.page.pushButton_video.clicked.connect(self.pickVideo)
+ self.trackWidgets(
+ {
+ 'videoPath': self.page.lineEdit_video,
+ 'loopVideo': self.page.checkBox_loop,
+ 'useAudio': self.page.checkBox_useAudio,
+ 'distort': self.page.checkBox_distort,
+ 'scale': self.page.spinBox_scale,
+ 'volume': self.page.spinBox_volume,
+ 'xPosition': self.page.spinBox_x,
+ 'yPosition': self.page.spinBox_y,
+ }, presetNames={
+ 'videoPath': 'video',
+ 'loopVideo': 'loop',
+ 'xPosition': 'x',
+ 'yPosition': 'y',
+ }
+ )
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.volume = self.page.spinBox_volume.value()
- self.xPosition = self.page.spinBox_x.value()
- self.yPosition = self.page.spinBox_y.value()
-
- if self.useAudio:
+ if self.page.checkBox_useAudio.isChecked():
self.page.label_volume.setEnabled(True)
self.page.spinBox_volume.setEnabled(True)
else:
self.page.label_volume.setEnabled(False)
self.page.spinBox_volume.setEnabled(False)
-
super().update()
def previewRender(self, previewWorker):
@@ -188,18 +182,7 @@ class Component(Component):
return "The video selected is corrupt!"
def testAudioStream(self):
- # test if an audio stream really exists
- audioTestCommand = [
- 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
+ self.badAudio = testAudioStream(self.videoPath)
def audio(self):
params = {}
@@ -214,7 +197,7 @@ class Component(Component):
self.blankFrame_ = BlankFrame(width, height)
self.updateChunksize(width, height)
self.video = Video(
- ffmpeg=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,
@@ -227,34 +210,11 @@ class Component(Component):
else:
return self.blankFrame_
- def loadPreset(self, pr, presetName=None):
- super().loadPreset(pr, presetName)
- self.page.lineEdit_video.setText(pr['video'])
- self.page.checkBox_loop.setChecked(pr['loop'])
- self.page.checkBox_useAudio.setChecked(pr['useAudio'])
- self.page.checkBox_distort.setChecked(pr['distort'])
- self.page.spinBox_scale.setValue(pr['scale'])
- self.page.spinBox_volume.setValue(pr['volume'])
- self.page.spinBox_x.setValue(pr['x'])
- self.page.spinBox_y.setValue(pr['y'])
-
- def savePreset(self):
- return {
- 'video': self.videoPath,
- 'loop': self.loopVideo,
- 'useAudio': self.useAudio,
- 'distort': self.distort,
- 'scale': self.scale,
- 'volume': self.volume,
- 'x': self.xPosition,
- 'y': self.yPosition,
- }
-
def pickVideo(self):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Video",
- imgDir, "Video Files (%s)" % " ".join(Core.videoFormats)
+ imgDir, "Video Files (%s)" % " ".join(self.core.videoFormats)
)
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
@@ -266,7 +226,7 @@ class Component(Component):
return
command = [
- self.parent.core.FFMPEG_BIN,
+ self.core.FFMPEG_BIN,
'-thread_queue_size', '512',
'-i', self.videoPath,
'-f', 'image2pipe',
@@ -294,10 +254,10 @@ class Component(Component):
self.chunkSize = 4*width*height
def command(self, arg):
- if not arg.startswith('preset=') and '=' in arg:
+ if '=' in arg:
key, arg = arg.split('=', 1)
if key == 'path' and os.path.exists(arg):
- if '*%s' % os.path.splitext(arg)[1] in 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)
diff --git a/src/core.py b/src/core.py
index f6cf5eb..eb6398b 100644
--- a/src/core.py
+++ b/src/core.py
@@ -1,5 +1,6 @@
'''
Home to the Core class which tracks program state. Used by GUI & commandline
+ to create a list of components and create a video thread to export.
'''
from PyQt5 import QtCore, QtGui, uic
import sys
@@ -8,7 +9,6 @@ import json
from importlib import import_module
import toolkit
-from toolkit.ffmpeg import findFfmpeg
import video_thread
@@ -16,82 +16,21 @@ class Core:
'''
MainWindow and Command module both use an instance of this class
to store the core program state. This object tracks the components,
- talks to the components and handles opening/creating project files
- and presets. The class also stores constants as class variables.
+ talks to the components, handles opening/creating project files
+ and presets, and creates the video thread to export.
+ This class also stores constants as class variables.
'''
- @classmethod
- def storeSettings(cls):
- '''Store settings/paths to directories as class variables.'''
- if getattr(sys, 'frozen', False):
- # frozen
- wd = os.path.dirname(sys.executable)
- else:
- wd = os.path.dirname(os.path.realpath(__file__))
-
- dataDir = QtCore.QStandardPaths.writableLocation(
- QtCore.QStandardPaths.AppConfigLocation
- )
- with open(os.path.join(wd, 'encoder-options.json')) as json_file:
- encoderOptions = json.load(json_file)
-
- settings = {
- 'wd': wd,
- 'dataDir': dataDir,
- 'settings': QtCore.QSettings(
- os.path.join(dataDir, 'settings.ini'),
- QtCore.QSettings.IniFormat),
- 'presetDir': os.path.join(dataDir, 'presets'),
- 'componentsPath': os.path.join(wd, 'components'),
- 'encoderOptions': encoderOptions,
- 'FFMPEG_BIN': findFfmpeg(),
- 'canceled': False,
- }
-
- settings['videoFormats'] = toolkit.appendUppercase([
- '*.mp4',
- '*.mov',
- '*.mkv',
- '*.avi',
- '*.webm',
- '*.flv',
- ])
- settings['audioFormats'] = toolkit.appendUppercase([
- '*.mp3',
- '*.wav',
- '*.ogg',
- '*.fla',
- '*.flac',
- '*.aac',
- ])
- settings['imageFormats'] = toolkit.appendUppercase([
- '*.png',
- '*.jpg',
- '*.tif',
- '*.tiff',
- '*.gif',
- '*.bmp',
- '*.ico',
- '*.xbm',
- '*.xpm',
- ])
-
- # Register all settings as class variables
- for classvar, val in settings.items():
- setattr(cls, classvar, val)
- # Make settings accessible to the toolkit package
- toolkit.init(settings)
-
def __init__(self):
- Core.storeSettings()
-
self.findComponents()
self.selectedComponents = []
self.savedPresets = {} # copies of presets to detect modification
+ self.openingProject = False
def findComponents(self):
+ '''Imports all the component modules'''
def findComponents():
- for f in sorted(os.listdir(Core.componentsPath)):
+ for f in os.listdir(Core.componentsPath):
name, ext = os.path.splitext(f)
if name.startswith("__"):
continue
@@ -104,8 +43,13 @@ class Core:
# store canonical module names and indexes
self.moduleIndexes = [i for i in range(len(self.modules))]
self.compNames = [mod.Component.name for mod in self.modules]
- self.altCompNames = []
+ # alphabetize modules by Component name
+ sortedModules = sorted(zip(self.compNames, self.modules))
+ self.compNames = [y[0] for y in sortedModules]
+ self.modules = [y[1] for y in sortedModules]
+
# store alternative names for modules
+ self.altCompNames = []
for i, mod in enumerate(self.modules):
if hasattr(mod.Component, 'names'):
for name in mod.Component.names():
@@ -116,14 +60,17 @@ class Core:
component.compPos = i
def insertComponent(self, compPos, moduleIndex, loader):
- '''Creates a new component'''
+ '''
+ Creates a new component using these args:
+ (compPos, moduleIndex in self.modules, MWindow/Command/Core obj)
+ '''
if compPos < 0 or compPos > len(self.selectedComponents):
compPos = len(self.selectedComponents)
if len(self.selectedComponents) > 50:
return None
component = self.modules[moduleIndex].Component(
- moduleIndex, compPos
+ moduleIndex, compPos, self
)
self.selectedComponents.insert(
compPos,
@@ -206,6 +153,7 @@ class Core:
errcode, data = self.parseAvFile(filepath)
if errcode == 0:
+ self.openingProject = True
try:
if hasattr(loader, 'window'):
for widget, value in data['WindowFields']:
@@ -239,7 +187,8 @@ class Core:
i = self.insertComponent(
-1,
self.moduleIndexFor(name),
- loader)
+ loader
+ )
if i is None:
loader.showMessage(msg="Too many components!")
break
@@ -284,6 +233,7 @@ class Core:
showCancel=False,
icon='Warning',
detail=msg)
+ self.openingProject = False
def parseAvFile(self, filepath):
'''Parses an avp (project) or avl (preset package) file.
@@ -467,8 +417,106 @@ class Core:
def cancel(self):
Core.canceled = True
- toolkit.cancel()
def reset(self):
Core.canceled = False
- toolkit.reset()
+
+ @classmethod
+ def storeSettings(cls):
+ '''Store settings/paths to directories as class variables'''
+ from __init__ import wd
+ from toolkit.ffmpeg import findFfmpeg
+
+ cls.wd = wd
+ dataDir = QtCore.QStandardPaths.writableLocation(
+ QtCore.QStandardPaths.AppConfigLocation
+ )
+ with open(os.path.join(wd, 'encoder-options.json')) as json_file:
+ encoderOptions = json.load(json_file)
+
+ settings = {
+ 'dataDir': dataDir,
+ 'settings': QtCore.QSettings(
+ os.path.join(dataDir, 'settings.ini'),
+ QtCore.QSettings.IniFormat),
+ 'presetDir': os.path.join(dataDir, 'presets'),
+ 'componentsPath': os.path.join(wd, 'components'),
+ 'encoderOptions': encoderOptions,
+ 'resolutions': [
+ '1920x1080',
+ '1280x720',
+ '854x480',
+ ],
+ 'windowHasFocus': False,
+ 'FFMPEG_BIN': findFfmpeg(),
+ 'canceled': False,
+ }
+
+ settings['videoFormats'] = toolkit.appendUppercase([
+ '*.mp4',
+ '*.mov',
+ '*.mkv',
+ '*.avi',
+ '*.webm',
+ '*.flv',
+ ])
+ settings['audioFormats'] = toolkit.appendUppercase([
+ '*.mp3',
+ '*.wav',
+ '*.ogg',
+ '*.fla',
+ '*.flac',
+ '*.aac',
+ ])
+ settings['imageFormats'] = toolkit.appendUppercase([
+ '*.png',
+ '*.jpg',
+ '*.tif',
+ '*.tiff',
+ '*.gif',
+ '*.bmp',
+ '*.ico',
+ '*.xbm',
+ '*.xpm',
+ ])
+
+ # Register all settings as class variables
+ for classvar, val in settings.items():
+ setattr(cls, classvar, val)
+
+ cls.loadDefaultSettings()
+
+ @classmethod
+ def loadDefaultSettings(cls):
+ defaultSettings = {
+ "outputWidth": 1280,
+ "outputHeight": 720,
+ "outputFrameRate": 30,
+ "outputAudioCodec": "AAC",
+ "outputAudioBitrate": "192",
+ "outputVideoCodec": "H264",
+ "outputVideoBitrate": "2500",
+ "outputVideoFormat": "yuv420p",
+ "outputPreset": "medium",
+ "outputFormat": "mp4",
+ "outputContainer": "MP4",
+ "projectDir": os.path.join(cls.dataDir, 'projects'),
+ "pref_insertCompAtTop": True,
+ }
+
+ for parm, value in defaultSettings.items():
+ if cls.settings.value(parm) is None:
+ cls.settings.setValue(parm, value)
+
+ # Allow manual editing of prefs. (Surprisingly necessary as Qt seems to
+ # store True as 'true' but interprets a manually-added 'true' as str.)
+ for key in cls.settings.allKeys():
+ if not key.startswith('pref_'):
+ continue
+ val = cls.settings.value(key)
+ if val in ('true', 'false'):
+ cls.settings.setValue(key, True if val == 'true' else False)
+
+
+# always store settings in class variables even if a Core object is not created
+Core.storeSettings()
diff --git a/src/main.py b/src/main.py
index 6a9a25e..977da3b 100644
--- a/src/main.py
+++ b/src/main.py
@@ -2,22 +2,17 @@ from PyQt5 import uic, QtWidgets
import sys
import os
+from __init__ import wd
-def main():
- if getattr(sys, 'frozen', False):
- # frozen
- wd = os.path.dirname(sys.executable)
- else:
- # unfrozen
- wd = os.path.dirname(os.path.realpath(__file__))
- # make local imports work everywhere
- sys.path.insert(0, wd)
+def main():
+ app = QtWidgets.QApplication(sys.argv)
+ app.setApplicationName("audio-visualizer")
+ # Determine mode
mode = 'GUI'
if len(sys.argv) > 2:
mode = 'commandline'
-
elif len(sys.argv) == 2:
if sys.argv[1].startswith('-'):
mode = 'commandline'
@@ -28,11 +23,7 @@ def 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")
-
+ # Launch program
if mode == 'commandline':
from command import Command
@@ -61,9 +52,7 @@ def main():
signal.signal(signal.SIGINT, main.cleanUp)
atexit.register(main.cleanUp)
- # applicable to both modes
sys.exit(app.exec_())
-
if __name__ == "__main__":
main()
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 2d598ae..f333513 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -17,7 +17,7 @@ import time
from core import Core
import preview_thread
from presetmanager import PresetManager
-from toolkit import loadDefaultSettings, disableWhenEncoding, checkOutput
+from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput
class PreviewWindow(QtWidgets.QLabel):
@@ -25,6 +25,7 @@ class PreviewWindow(QtWidgets.QLabel):
Paints the preview QLabel and maintains the aspect ratio when the
window is resized.
'''
+
def __init__(self, parent, img):
super(PreviewWindow, self).__init__()
self.parent = parent
@@ -49,6 +50,14 @@ class PreviewWindow(QtWidgets.QLabel):
self.pixmap = QtGui.QPixmap(img)
self.repaint()
+ @QtCore.pyqtSlot(str)
+ def threadError(self, msg):
+ self.parent.showMessage(
+ msg=msg,
+ icon='Warning',
+ parent=self
+ )
+
class MainWindow(QtWidgets.QMainWindow):
'''
@@ -66,13 +75,16 @@ class MainWindow(QtWidgets.QMainWindow):
def __init__(self, window, project):
QtWidgets.QMainWindow.__init__(self)
-
# print('main thread id: {}'.format(QtCore.QThread.currentThreadId()))
self.window = window
self.core = Core()
- self.pages = [] # widgets of component settings
+ # widgets of component settings
+ self.pages = []
self.lastAutosave = time.time()
+ # list of previous five autosave times, used to reduce update spam
+ self.autosaveTimes = []
+ self.autosaveCooldown = 0.2
self.encoding = False
# Create data directory, load/create settings
@@ -80,7 +92,6 @@ class MainWindow(QtWidgets.QMainWindow):
self.presetDir = Core.presetDir
self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
self.settings = Core.settings
- loadDefaultSettings(self)
self.presetManager = PresetManager(
uic.loadUi(
os.path.join(Core.wd, 'presetmanager.ui')), self)
@@ -92,13 +103,17 @@ class MainWindow(QtWidgets.QMainWindow):
if not os.path.exists(neededDirectory):
os.mkdir(neededDirectory)
- # Make queues/timers for the preview thread
+ # Create the preview window and its thread, queues, and timers
+ self.previewWindow = PreviewWindow(self, os.path.join(
+ Core.wd, "background.png"))
+ window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
+
self.previewQueue = Queue()
self.previewThread = QtCore.QThread(self)
self.previewWorker = preview_thread.Worker(self, self.previewQueue)
+ self.previewWorker.error.connect(self.previewWindow.threadError)
self.previewWorker.moveToThread(self.previewThread)
self.previewWorker.imageCreated.connect(self.showPreviewImage)
- self.previewWorker.error.connect(self.cleanUp)
self.previewThread.start()
self.timer = QtCore.QTimer(self)
@@ -106,6 +121,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.timer.start(500)
# Begin decorating the window and connecting events
+ self.window.installEventFilter(self)
componentList = self.window.listWidget_componentList
if sys.platform == 'darwin':
@@ -168,14 +184,9 @@ class MainWindow(QtWidgets.QMainWindow):
window.spinBox_vBitrate.setValue(vBitrate)
window.spinBox_aBitrate.setValue(aBitrate)
-
window.spinBox_vBitrate.valueChanged.connect(self.updateCodecSettings)
window.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings)
- self.previewWindow = PreviewWindow(self, os.path.join(
- Core.wd, "background.png"))
- window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
-
# Make component buttons
self.compMenu = QMenu()
for i, comp in enumerate(self.core.modules):
@@ -204,7 +215,7 @@ class MainWindow(QtWidgets.QMainWindow):
currentRes = str(self.settings.value('outputWidth'))+'x' + \
str(self.settings.value('outputHeight'))
- for i, res in enumerate(self.resolutions):
+ for i, res in enumerate(Core.resolutions):
window.comboBox_resolution.addItem(res)
if res == currentRes:
currentRes = i
@@ -375,6 +386,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.previewThread.quit()
self.previewThread.wait()
+ @disableWhenOpeningProject
def updateWindowTitle(self):
appName = 'Audio Visualizer'
try:
@@ -442,13 +454,29 @@ class MainWindow(QtWidgets.QMainWindow):
self.settings.setValue('outputVideoBitrate', currentVideoBitrate)
self.settings.setValue('outputAudioBitrate', currentAudioBitrate)
+ @disableWhenOpeningProject
def autosave(self, force=False):
if not self.currentProject:
if os.path.exists(self.autosavePath):
os.remove(self.autosavePath)
- elif force or time.time() - self.lastAutosave >= 0.2:
+ elif force or time.time() - self.lastAutosave >= self.autosaveCooldown:
self.core.createProjectFile(self.autosavePath, self.window)
self.lastAutosave = time.time()
+ if len(self.autosaveTimes) >= 5:
+ # Do some math to reduce autosave spam. This gives a smooth
+ # curve up to 5 seconds cooldown and maintains that for 30 secs
+ # if a component is continuously updated
+ timeDiff = self.lastAutosave - self.autosaveTimes.pop()
+ if not force and timeDiff >= 1.0 \
+ and timeDiff <= 10.0:
+ if self.autosaveCooldown / 4.0 < 0.5:
+ self.autosaveCooldown += 1.0
+ self.autosaveCooldown = (
+ 5.0 * (self.autosaveCooldown / 5.0)
+ ) + (self.autosaveCooldown / 5.0) * 2
+ elif force or timeDiff >= self.autosaveCooldown * 5:
+ self.autosaveCooldown = 0.2
+ self.autosaveTimes.insert(0, self.lastAutosave)
def autosaveExists(self, identical=True):
'''Determines if creating the autosave should be blocked.'''
@@ -602,15 +630,20 @@ class MainWindow(QtWidgets.QMainWindow):
def updateResolution(self):
resIndex = int(self.window.comboBox_resolution.currentIndex())
- res = self.resolutions[resIndex].split('x')
+ res = Core.resolutions[resIndex].split('x')
self.settings.setValue('outputWidth', res[0])
self.settings.setValue('outputHeight', res[1])
self.drawPreview()
- def drawPreview(self, force=False):
+ def drawPreview(self, force=False, **kwargs):
+ '''Use autosave keyword arg to force saving or not saving if needed'''
self.newTask.emit(self.core.selectedComponents)
# self.processTask.emit()
- self.autosave(force)
+ if force or 'autosave' in kwargs:
+ if force or kwargs['autosave']:
+ self.autosave(True)
+ else:
+ self.autosave()
self.updateWindowTitle()
@QtCore.pyqtSlot(QtGui.QImage)
@@ -685,9 +718,13 @@ class MainWindow(QtWidgets.QMainWindow):
stackedWidget.insertWidget(newRow, page)
componentList.setCurrentRow(newRow)
stackedWidget.setCurrentIndex(newRow)
- self.drawPreview()
+ self.drawPreview(True)
- def getComponentListRects(self):
+ def getComponentListMousePos(self, position):
+ '''
+ Given a QPos, returns the component index under the mouse cursor
+ or -1 if no component is there.
+ '''
componentList = self.window.listWidget_componentList
modelIndexes = [
@@ -698,20 +735,23 @@ class MainWindow(QtWidgets.QMainWindow):
componentList.visualRect(modelIndex)
for modelIndex in modelIndexes
]
- return rects
+ mousePos = [rect.contains(position) for rect in rects]
+ if not any(mousePos):
+ # Not clicking a component
+ mousePos = -1
+ else:
+ mousePos = mousePos.index(True)
+ return mousePos
@disableWhenEncoding
def dragComponent(self, event):
'''Used as Qt drop event for the component listwidget'''
componentList = self.window.listWidget_componentList
- rects = self.getComponentListRects()
-
- rowPos = [rect.contains(event.pos()) for rect in rects]
- if not any(rowPos):
- return
-
- i = rowPos.index(True)
- change = (componentList.currentRow() - i) * -1
+ mousePos = self.getComponentListMousePos(event.pos())
+ if mousePos > -1:
+ change = (componentList.currentRow() - mousePos) * -1
+ else:
+ change = (componentList.count() - componentList.currentRow() -1)
self.moveComponent(change)
def changeComponentWidget(self):
@@ -814,9 +854,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.settings.setValue("projectDir", os.path.dirname(filepath))
# actually load the project using core method
self.core.openProject(self, filepath)
- if self.window.listWidget_componentList.count() == 0:
- self.drawPreview()
- self.autosave(True)
+ self.drawPreview(autosave=False)
self.updateWindowTitle()
def showMessage(self, **kwargs):
@@ -843,20 +881,11 @@ class MainWindow(QtWidgets.QMainWindow):
def componentContextMenu(self, QPos):
'''Appears when right-clicking the component list'''
componentList = self.window.listWidget_componentList
- index = componentList.currentRow()
-
self.menu = QMenu()
parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0))
- rects = self.getComponentListRects()
- rowPos = [rect.contains(QPos) for rect in rects]
- if not any(rowPos):
- # Insert components at the top if clicking nothing
- rowPos = 0
- else:
- rowPos = rowPos.index(True)
-
- if index == rowPos:
+ index = self.getComponentListMousePos(QPos)
+ if index > -1:
# Show preset menu if clicking a component
self.presetManager.findPresets()
menuItem = self.menu.addAction("Save Preset")
@@ -891,13 +920,23 @@ class MainWindow(QtWidgets.QMainWindow):
# "Add Component" submenu
self.submenu = QMenu("Add")
self.menu.addMenu(self.submenu)
+ insertCompAtTop = self.settings.value("pref_insertCompAtTop")
for i, comp in enumerate(self.core.modules):
menuItem = self.submenu.addAction(comp.Component.name)
menuItem.triggered.connect(
lambda _, item=i: self.core.insertComponent(
- rowPos, item, self
+ 0 if insertCompAtTop else index, item, self
)
- )
+ )
self.menu.move(parentPosition + QPos)
self.menu.show()
+
+ def eventFilter(self, object, event):
+ if event.type() == QtCore.QEvent.WindowActivate \
+ or event.type() == QtCore.QEvent.FocusIn:
+ Core.windowHasFocus = True
+ elif event.type()== QtCore.QEvent.WindowDeactivate \
+ or event.type() == QtCore.QEvent.FocusOut:
+ Core.windowHasFocus = False
+ return False
diff --git a/src/mainwindow.ui b/src/mainwindow.ui
index b491323..b43d375 100644
--- a/src/mainwindow.ui
+++ b/src/mainwindow.ui
@@ -22,6 +22,9 @@
0
+
+ Qt::StrongFocus
+
MainWindow
diff --git a/src/presetmanager.py b/src/presetmanager.py
index 64e2203..643e180 100644
--- a/src/presetmanager.py
+++ b/src/presetmanager.py
@@ -6,7 +6,8 @@ from PyQt5 import QtCore, QtWidgets
import string
import os
-import toolkit
+from toolkit import badName
+from core import Core
class PresetManager(QtWidgets.QDialog):
@@ -151,7 +152,7 @@ class PresetManager(QtWidgets.QDialog):
currentPreset
)
if OK:
- if toolkit.badName(newName):
+ if badName(newName):
self.warnMessage(self.parent.window)
continue
if newName:
@@ -236,7 +237,6 @@ class PresetManager(QtWidgets.QDialog):
os.remove(filepath)
def warnMessage(self, window=None):
- print(window)
self.parent.showMessage(
msg='Preset names must contain only letters, '
'numbers, and spaces.',
@@ -272,7 +272,7 @@ class PresetManager(QtWidgets.QDialog):
self.presetRows[index][2]
)
if OK:
- if toolkit.badName(newName):
+ if badName(newName):
self.warnMessage()
continue
if newName:
@@ -289,7 +289,7 @@ class PresetManager(QtWidgets.QDialog):
self.findPresets()
self.drawPresetList()
for i, comp in enumerate(self.core.selectedComponents):
- if toolkit.getPresetDir(comp) == path \
+ if getPresetDir(comp) == path \
and comp.currentPreset == oldName:
self.core.openPreset(newPath, i, newName)
self.parent.updateComponentTitle(i, False)
@@ -338,3 +338,8 @@ class PresetManager(QtWidgets.QDialog):
def clearPresetListSelection(self):
self.window.listWidget_presets.setCurrentRow(-1)
+
+
+def getPresetDir(comp):
+ '''Get the preset subdir for a particular version of a component'''
+ return os.path.join(Core.presetDir, str(comp), str(comp.version))
diff --git a/src/preview_thread.py b/src/preview_thread.py
index 3fc73b3..9917e4b 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -10,12 +10,13 @@ from queue import Queue, Empty
import os
from toolkit.frame import Checkerboard
+from toolkit import disableWhenOpeningProject
class Worker(QtCore.QObject):
imageCreated = pyqtSignal(QtGui.QImage)
- error = pyqtSignal()
+ error = pyqtSignal(str)
def __init__(self, parent=None, queue=None):
QtCore.QObject.__init__(self)
@@ -30,6 +31,7 @@ class Worker(QtCore.QObject):
height = int(self.settings.value('outputHeight'))
self.background = Checkerboard(width, height)
+ @disableWhenOpeningProject
@pyqtSlot(list)
def createPreviewImage(self, components):
dic = {
@@ -48,7 +50,6 @@ class Worker(QtCore.QObject):
self.queue.get(block=False)
except Empty:
continue
-
if self.background.width != width \
or self.background.height != height:
self.background = Checkerboard(width, height)
@@ -65,20 +66,12 @@ class Worker(QtCore.QObject):
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." % (
+ "%s. New frame size was %s*%s; should be %s*%s." % (
str(component), str(e).capitalize(),
newFrame.width, newFrame.height,
width, height
)
- print(errMsg)
- self.parent.showMessage(
- msg=errMsg,
- detail=str(e),
- icon='Warning',
- parent=None # MainWindow is in a different thread
- )
- self.error.emit()
+ self.error.emit(errMsg)
break
except RuntimeError as e:
print(e)
diff --git a/src/toolkit/common.py b/src/toolkit/common.py
index 763d582..5fe601f 100644
--- a/src/toolkit/common.py
+++ b/src/toolkit/common.py
@@ -8,13 +8,6 @@ import sys
import subprocess
from collections import OrderedDict
-from toolkit.core import *
-
-
-def getPresetDir(comp):
- '''Get the preset subdirectory for a particular version of a component'''
- return os.path.join(Core.presetDir, str(comp), str(comp.version))
-
def badName(name):
'''Returns whether a name contains non-alphanumeric chars'''
@@ -66,14 +59,20 @@ def openPipe(commandList, **kwargs):
def disableWhenEncoding(func):
- ''' Blocks calls to a function while the video is being exported
- in MainWindow.
- '''
- def decorator(*args, **kwargs):
- if args[0].encoding:
+ def decorator(self, *args, **kwargs):
+ if self.encoding:
return
else:
- return func(*args, **kwargs)
+ return func(self, *args, **kwargs)
+ return decorator
+
+
+def disableWhenOpeningProject(func):
+ def decorator(self, *args, **kwargs):
+ if self.core.openingProject:
+ return
+ else:
+ return func(self, *args, **kwargs)
return decorator
@@ -108,34 +107,3 @@ def rgbFromString(string):
return tup
except:
return (255, 255, 255)
-
-
-def loadDefaultSettings(self):
- '''
- Runs once at each program start-up. Fills in default settings
- for any settings not found in settings.ini
- '''
- self.resolutions = [
- '1920x1080',
- '1280x720',
- '854x480'
- ]
-
- default = {
- "outputWidth": 1280,
- "outputHeight": 720,
- "outputFrameRate": 30,
- "outputAudioCodec": "AAC",
- "outputAudioBitrate": "192",
- "outputVideoCodec": "H264",
- "outputVideoBitrate": "2500",
- "outputVideoFormat": "yuv420p",
- "outputPreset": "medium",
- "outputFormat": "mp4",
- "outputContainer": "MP4",
- "projectDir": os.path.join(self.dataDir, 'projects'),
- }
-
- for parm, value in default.items():
- if self.settings.value(parm) is None:
- self.settings.setValue(parm, value)
diff --git a/src/toolkit/core.py b/src/toolkit/core.py
deleted file mode 100644
index a96a684..0000000
--- a/src/toolkit/core.py
+++ /dev/null
@@ -1,18 +0,0 @@
-class Core:
- '''A very complicated class for tracking settings'''
-
-
-def init(settings):
- global Core
- for classvar, val in settings.items():
- setattr(Core, classvar, val)
-
-
-def cancel():
- global Core
- Core.canceled = True
-
-
-def reset():
- global Core
- Core.canceled = False
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index cc59a6c..30dc0b3 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -4,18 +4,19 @@
import numpy
import sys
import os
-import subprocess as sp
+import subprocess
-from toolkit.common import Core, checkOutput, openPipe
+import core
+from toolkit.common import checkOutput, openPipe
def findFfmpeg():
if getattr(sys, 'frozen', False):
# The application is frozen
if sys.platform == "win32":
- return os.path.join(Core.wd, 'ffmpeg.exe')
+ return os.path.join(core.Core.wd, 'ffmpeg.exe')
else:
- return os.path.join(Core.wd, 'ffmpeg')
+ return os.path.join(core.Core.wd, 'ffmpeg')
else:
if sys.platform == "win32":
@@ -27,7 +28,7 @@ def findFfmpeg():
['ffmpeg', '-version'], stderr=f
)
return "ffmpeg"
- except sp.CalledProcessError:
+ except subprocess.CalledProcessError:
return "avconv"
@@ -37,9 +38,9 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
'''
if duration == -1:
duration = getAudioDuration(inputFile)
-
safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters
duration = "{0:.3f}".format(duration + 0.1) # used by input sources
+ Core = core.Core
# Test if user has libfdk_aac
encoders = checkOutput(
@@ -213,12 +214,28 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
return ffmpegCommand
+def testAudioStream(filename):
+ '''Test if an audio stream definitely exists'''
+ audioTestCommand = [
+ core.Core.FFMPEG_BIN,
+ '-i', filename,
+ '-vn', '-f', 'null', '-'
+ ]
+ try:
+ checkOutput(audioTestCommand, stderr=subprocess.DEVNULL)
+ except subprocess.CalledProcessError:
+ return True
+ else:
+ return False
+
+
def getAudioDuration(filename):
- command = [Core.FFMPEG_BIN, '-i', filename]
+ '''Try to get duration of audio file as float, or False if not possible'''
+ command = [core.Core.FFMPEG_BIN, '-i', filename]
try:
- fileInfo = checkOutput(command, stderr=sp.STDOUT)
- except sp.CalledProcessError as ex:
+ fileInfo = checkOutput(command, stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as ex:
fileInfo = ex.output
info = fileInfo.decode("utf-8").split('\n')
@@ -236,13 +253,17 @@ def getAudioDuration(filename):
def readAudioFile(filename, parent):
+ '''
+ Creates the completeAudioArray given to components
+ and used to draw the classic visualizer.
+ '''
duration = getAudioDuration(filename)
if not duration:
print('Audio file doesn\'t exist or unreadable.')
return
command = [
- Core.FFMPEG_BIN,
+ core.Core.FFMPEG_BIN,
'-i', filename,
'-f', 's16le',
'-acodec', 'pcm_s16le',
@@ -250,7 +271,8 @@ def readAudioFile(filename, parent):
'-ac', '1', # mono (set to '2' for stereo)
'-']
in_pipe = openPipe(
- command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8
+ command,
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8
)
completeAudioArray = numpy.empty(0, dtype="int16")
@@ -258,7 +280,7 @@ def readAudioFile(filename, parent):
progress = 0
lastPercent = None
while True:
- if Core.canceled:
+ if core.Core.canceled:
return
# read 2 seconds of audio
progress += 4
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index 83fd59e..ca2a054 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -7,7 +7,7 @@ from PIL.ImageQt import ImageQt
import sys
import os
-from toolkit.common import Core
+import core
class FramePainter(QtGui.QPainter):
@@ -57,7 +57,7 @@ def Checkerboard(width, height):
'''
image = FloodFrame(1920, 1080, (0, 0, 0, 0))
image.paste(Image.open(
- os.path.join(Core.wd, "background.png")),
+ os.path.join(core.Core.wd, "background.png")),
(0, 0)
)
image = image.resize((width, height))
diff --git a/src/video_thread.py b/src/video_thread.py
index 8517b92..7fe3e02 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -18,6 +18,7 @@ from threading import Thread, Event
import time
import signal
+import core
from toolkit import openPipe
from toolkit.ffmpeg import readAudioFile, createFfmpegCommand
from toolkit.frame import Checkerboard
@@ -104,7 +105,8 @@ class Worker(QtCore.QObject):
while not self.stopped:
audioI, frame = self.previewQueue.get()
- if time.time() - self.lastPreview >= 0.06 or audioI == 0:
+ if core.Core.windowHasFocus \
+ and time.time() - self.lastPreview >= 0.06 or audioI == 0:
image = Image.alpha_composite(background.copy(), frame)
self.imageCreated.emit(QtGui.QImage(ImageQt(image)))
self.lastPreview = time.time()
@@ -231,7 +233,8 @@ class Worker(QtCore.QObject):
self.lastPreview = 0.0
self.previewDispatch = Thread(
- target=self.previewDispatch, name="Render Dispatch Thread")
+ target=self.previewDispatch, name="Render Dispatch Thread"
+ )
self.previewDispatch.daemon = True
self.previewDispatch.start()
--
cgit v1.2.3
From d38109453cea17a31c335837c0029ad51fa3dda1 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 23 Jul 2017 17:14:21 -0400
Subject: better component error messages
fatal errors cancel the export instead of crashing
---
src/component.py | 157 ++++++++++++++++++++++++++++++++++-----------
src/components/original.py | 2 +-
src/components/sound.py | 2 +
src/components/video.py | 24 +++----
src/core.py | 10 ++-
src/mainwindow.py | 15 ++++-
src/toolkit/common.py | 8 +++
src/toolkit/ffmpeg.py | 2 +-
src/video_thread.py | 52 ++++++++-------
9 files changed, 190 insertions(+), 82 deletions(-)
(limited to 'src/toolkit/ffmpeg.py')
diff --git a/src/component.py b/src/component.py
index bec2df5..8b5f1b8 100644
--- a/src/component.py
+++ b/src/component.py
@@ -5,13 +5,12 @@
from PyQt5 import uic, QtCore, QtWidgets
import os
-from presetmanager import getPresetDir
-
def commandWrapper(func):
'''Intercepts each component's command() method to check for global args'''
def decorator(self, arg):
if arg.startswith('preset='):
+ from presetmanager import getPresetDir
_, preset = arg.split('=', 1)
path = os.path.join(getPresetDir(self), preset)
if not os.path.exists(path):
@@ -29,6 +28,26 @@ def commandWrapper(func):
return decorator
+def propertiesWrapper(func):
+ '''Intercepts the usual properties if the properties are locked.'''
+ def decorator(self):
+ if self._lockedProperties is not None:
+ return self._lockedProperties
+ else:
+ return func(self)
+ return decorator
+
+
+def errorWrapper(func):
+ '''Intercepts the usual error message if it is locked.'''
+ def decorator(self):
+ if self._lockedError is not None:
+ return self._lockedError
+ else:
+ return func(self)
+ return decorator
+
+
class ComponentMetaclass(type(QtCore.QObject)):
'''
Checks the validity of each Component class imported, and
@@ -37,25 +56,33 @@ class ComponentMetaclass(type(QtCore.QObject)):
'''
def __new__(cls, name, parents, attrs):
if 'ui' not in attrs:
- # use module name as ui filename by default
+ # Use module name as ui filename by default
attrs['ui'] = '%s.ui' % os.path.splitext(
attrs['__module__'].split('.')[-1]
)[0]
- # Turn certain class methods into properties and classmethods
- for key in ('error', 'properties', 'audio'):
- if key not in attrs:
- continue
- attrs[key] = property(attrs[key])
+ # if parents[0] == QtCore.QObject: else:
+ decorate = ('names', 'error', 'audio', 'command', 'properties')
- for key in ('names'):
+ # Auto-decorate methods
+ for key in decorate:
if key not in attrs:
continue
- attrs[key] = classmethod(key)
- # Do not apply these mutations to the base class
- if parents[0] != QtCore.QObject:
- attrs['command'] = commandWrapper(attrs['command'])
+ if key in ('names'):
+ attrs[key] = classmethod(attrs[key])
+
+ if key in ('audio'):
+ attrs[key] = property(attrs[key])
+
+ if key == 'command':
+ attrs[key] = commandWrapper(attrs[key])
+
+ if key == 'properties':
+ attrs[key] = propertiesWrapper(attrs[key])
+
+ if key == 'error':
+ attrs[key] = errorWrapper(attrs[key])
# Turn version string into a number
try:
@@ -83,13 +110,13 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
name = 'Component'
# ui = 'nameOfNonDefaultUiFile'
+
version = '1.0.0'
# The major version (before the first dot) is used to determine
# preset compatibility; the rest is ignored so it can be non-numeric.
modified = QtCore.pyqtSignal(int, dict)
- # ^ Signal used to tell core program that the component state changed,
- # you shouldn't need to use this directly, it is used by self.update()
+ _error = QtCore.pyqtSignal(str, str)
def __init__(self, moduleIndex, compPos, core):
super().__init__()
@@ -100,6 +127,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self._trackedWidgets = {}
self._presetNames = {}
+ self._commandArgs = {}
+ self._lockedProperties = None
+ self._lockedError = None
# Stop lengthy processes in response to this variable
self.canceled = False
@@ -127,6 +157,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def error(self):
'''
Return a string containing an error message, or None for a default.
+ Or tuple of two strings for a message with details.
'''
return
@@ -141,12 +172,6 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
https://ffmpeg.org/ffmpeg-filters.html
'''
- def names():
- '''
- Alternative names for renaming a component between project files.
- '''
- return []
-
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
@@ -181,15 +206,29 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
for widget in widgets['comboBox']:
widget.currentIndexChanged.connect(self.update)
- def trackWidgets(self, trackDict, presetNames=None):
+ def trackWidgets(self, trackDict, **kwargs):
'''
- Name widgets to track in update(), savePreset(), and loadPreset()
- Accepts a dict with attribute names as keys and widgets as values.
- Optional: a dict of attribute names to map to preset variable names
+ Name widgets to track in update(), savePreset(), loadPreset(), and
+ command(). Requires a dict of attr names as keys, widgets as values
+
+ Optional args:
+ 'presetNames': preset variable names to replace attr names
+ 'commandArgs': arg keywords that differ from attr names
+
+ NOTE: Any kwarg key set to None will selectively disable tracking.
'''
self._trackedWidgets = trackDict
- if type(presetNames) is dict:
- self._presetNames = presetNames
+ for kwarg in kwargs:
+ try:
+ if kwarg in ('presetNames', 'commandArgs'):
+ setattr(self, '_%s' % kwarg, kwargs[kwarg])
+ else:
+ raise BadComponentInit(
+ self,
+ 'Nonsensical keywords to trackWidgets.',
+ immediate=True)
+ except BadComponentInit:
+ continue
def update(self):
'''
@@ -277,6 +316,22 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self.commandHelp()
quit(0)
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # "Private" Methods
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+
+ def lockProperties(self, propList):
+ self._lockedProperties = propList
+
+ def lockError(self, msg):
+ self._lockedError = msg
+
+ def unlockProperties(self):
+ self._lockedProperties = None
+
+ def unlockError(self):
+ self._lockedError = None
+
def loadUi(self, filename):
'''Load a Qt Designer ui file to use for this component's widget'''
return uic.loadUi(os.path.join(self.core.componentsPath, filename))
@@ -287,6 +342,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def reset(self):
self.canceled = False
+ self.unlockProperties()
+ self.unlockError()
'''
### Reference methods for creating a new component
@@ -309,16 +366,40 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
-class BadComponentInit(Exception):
+class BadComponentInit(AttributeError):
'''
- General purpose exception that components can raise to indicate
- a Python issue with e.g., dynamic creation of instances or something.
- Decorative for now, may have future use for logging.
+ Indicates a Python error in constructing a component.
+ Raising this locks the component into an error state,
+ and gives the MainWindow a traceback to display.
'''
- def __init__(self, arg, name):
- string = '''################################
-Mandatory argument "%s" not specified
- in %s instance initialization
-###################################'''
- print(string % (arg, name))
- quit()
+ def __init__(self, caller, name, immediate=False):
+ from toolkit import formatTraceback
+ import sys
+ if sys.exc_info()[0] is not None:
+ string = (
+ "%s component's %s encountered %s %s." % (
+ caller.__class__.name,
+ name,
+ 'an' if any([
+ sys.exc_info()[0].__name__.startswith(vowel)
+ for vowel in ('A', 'I')
+ ]) else 'a',
+ sys.exc_info()[0].__name__,
+ )
+ )
+ detail = formatTraceback(sys.exc_info()[2])
+ else:
+ string = name
+ detail = "Methods:\n%s" % (
+ "\n".join(
+ [m for m in dir(caller) if not m.startswith('_')]
+ )
+ )
+
+ if immediate:
+ caller.parent.showMessage(
+ msg=string, detail=detail, icon='Warning'
+ )
+ else:
+ caller.lockProperties(['error'])
+ caller.lockError((string, detail))
diff --git a/src/components/original.py b/src/components/original.py
index 2bda878..570465d 100644
--- a/src/components/original.py
+++ b/src/components/original.py
@@ -15,7 +15,7 @@ class Component(Component):
name = 'Classic Visualizer'
version = '1.0.0'
- def names():
+ def names(*args):
return ['Original Audio Visualization']
def widget(self, *args):
diff --git a/src/components/sound.py b/src/components/sound.py
index dd3cbab..b3a627a 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -18,6 +18,8 @@ class Component(Component):
'chorus': self.page.checkBox_chorus,
'delay': self.page.spinBox_delay,
'volume': self.page.spinBox_volume,
+ }, commandArgs={
+ 'sound': None,
})
def previewRender(self, previewWorker):
diff --git a/src/components/video.py b/src/components/video.py
index 677e3ee..d3696d4 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -14,7 +14,7 @@ from toolkit import openPipe, checkOutput
class Video:
- '''Video Component Frame-Fetcher'''
+ '''Opens a pipe to ffmpeg and stores a buffer of raw video frames.'''
def __init__(self, **kwargs):
mandatoryArgs = [
'ffmpeg', # path to ffmpeg, usually self.core.FFMPEG_BIN
@@ -28,10 +28,7 @@ class Video:
'component', # component object
]
for arg in mandatoryArgs:
- try:
- setattr(self, arg, kwargs[arg])
- except KeyError:
- raise BadComponentInit(arg, self.__doc__)
+ setattr(self, arg, kwargs[arg])
self.frameNo = -1
self.currentFrame = 'None'
@@ -196,13 +193,16 @@ class Component(Component):
height = int(self.settings.value('outputHeight'))
self.blankFrame_ = BlankFrame(width, height)
self.updateChunksize(width, height)
- self.video = Video(
- 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,
- component=self, scale=self.scale
- ) if os.path.exists(self.videoPath) else None
+ try:
+ self.video = Video(
+ 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,
+ component=self, scale=self.scale
+ ) if os.path.exists(self.videoPath) else None
+ except KeyError:
+ raise BadComponentInit(self, 'Frame Fetcher initialization')
def frameRender(self, layerNo, frameNo):
if self.video:
diff --git a/src/core.py b/src/core.py
index eb6398b..2f9c36c 100644
--- a/src/core.py
+++ b/src/core.py
@@ -22,13 +22,12 @@ class Core:
'''
def __init__(self):
- self.findComponents()
+ self.importComponents()
self.selectedComponents = []
self.savedPresets = {} # copies of presets to detect modification
self.openingProject = False
- def findComponents(self):
- '''Imports all the component modules'''
+ def importComponents(self):
def findComponents():
for f in os.listdir(Core.componentsPath):
name, ext = os.path.splitext(f)
@@ -225,9 +224,8 @@ class Core:
return
if hasattr(loader, 'createNewProject'):
loader.createNewProject(prompt=False)
- import traceback
- msg = '%s: %s\n\nTraceback:\n' % (typ.__name__, value)
- msg += "\n".join(traceback.format_tb(tb))
+ msg = '%s: %s\n\n' % (typ.__name__, value)
+ msg += toolkit.formatTraceback(tb)
loader.showMessage(
msg="Project file '%s' is corrupted." % filepath,
showCancel=False,
diff --git a/src/mainwindow.py b/src/mainwindow.py
index f333513..a32c1b4 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -571,6 +571,15 @@ class MainWindow(QtWidgets.QMainWindow):
self.videoWorker.encoding.connect(self.changeEncodingStatus)
self.createVideo.emit()
+ @QtCore.pyqtSlot(str, str)
+ def videoThreadError(self, msg, detail):
+ self.showMessage(
+ msg=msg,
+ detail=detail,
+ icon='Warning',
+ )
+ self.stopVideo()
+
def changeEncodingStatus(self, status):
self.encoding = status
if status:
@@ -675,6 +684,8 @@ class MainWindow(QtWidgets.QMainWindow):
# connect to signal that adds an asterisk when modified
self.core.selectedComponents[index].modified.connect(
self.updateComponentTitle)
+ self.core.selectedComponents[index]._error.connect(
+ self.videoThreadError)
self.pages.insert(index, self.core.selectedComponents[index].page)
stackedWidget.insertWidget(index, self.pages[index])
@@ -751,7 +762,7 @@ class MainWindow(QtWidgets.QMainWindow):
if mousePos > -1:
change = (componentList.currentRow() - mousePos) * -1
else:
- change = (componentList.count() - componentList.currentRow() -1)
+ change = (componentList.count() - componentList.currentRow() - 1)
self.moveComponent(change)
def changeComponentWidget(self):
@@ -936,7 +947,7 @@ class MainWindow(QtWidgets.QMainWindow):
if event.type() == QtCore.QEvent.WindowActivate \
or event.type() == QtCore.QEvent.FocusIn:
Core.windowHasFocus = True
- elif event.type()== QtCore.QEvent.WindowDeactivate \
+ elif event.type() == QtCore.QEvent.WindowDeactivate \
or event.type() == QtCore.QEvent.FocusOut:
Core.windowHasFocus = False
return False
diff --git a/src/toolkit/common.py b/src/toolkit/common.py
index 5fe601f..251a2c1 100644
--- a/src/toolkit/common.py
+++ b/src/toolkit/common.py
@@ -107,3 +107,11 @@ def rgbFromString(string):
return tup
except:
return (255, 255, 255)
+
+
+def formatTraceback(tb=None):
+ import traceback
+ if tb is None:
+ import sys
+ tb = sys.exc_info()[2]
+ return 'Traceback:\n%s' % "\n".join(traceback.format_tb(tb))
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index 30dc0b3..8f5ae87 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -103,7 +103,7 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
globalFilters = 0 # increase to add global filters
extraAudio = [
comp.audio for comp in components
- if 'audio' in comp.properties
+ if 'audio' in comp.properties()
]
if extraAudio or globalFilters > 0:
# Add -i options for extra input files
diff --git a/src/video_thread.py b/src/video_thread.py
index 7fe3e02..68eae4f 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -18,7 +18,7 @@ from threading import Thread, Event
import time
import signal
-import core
+from component import BadComponentInit
from toolkit import openPipe
from toolkit.ffmpeg import readAudioFile, createFfmpegCommand
from toolkit.frame import Checkerboard
@@ -105,8 +105,7 @@ class Worker(QtCore.QObject):
while not self.stopped:
audioI, frame = self.previewQueue.get()
- if core.Core.windowHasFocus \
- and time.time() - self.lastPreview >= 0.06 or audioI == 0:
+ if time.time() - self.lastPreview >= 0.06 or audioI == 0:
image = Image.alpha_composite(background.copy(), frame)
self.imageCreated.emit(QtGui.QImage(ImageQt(image)))
self.lastPreview = time.time()
@@ -153,39 +152,48 @@ class Worker(QtCore.QObject):
]))
self.staticComponents = {}
for compNo, comp in enumerate(reversed(self.components)):
- comp.preFrameRender(
- worker=self,
- completeAudioArray=self.completeAudioArray,
- sampleSize=self.sampleSize,
- progressBarUpdate=self.progressBarUpdate,
- progressBarSetText=self.progressBarSetText
- )
+ try:
+ comp.preFrameRender(
+ worker=self,
+ completeAudioArray=self.completeAudioArray,
+ sampleSize=self.sampleSize,
+ progressBarUpdate=self.progressBarUpdate,
+ progressBarSetText=self.progressBarSetText
+ )
+ except BadComponentInit:
+ pass
- if 'error' in comp.properties:
+ if 'error' in comp.properties():
self.cancel()
self.canceled = True
canceledByComponent = True
- errMsg = "Component #%s encountered an error!" % compNo \
- if comp.error is None else 'Component #%s (%s): %s' % (
+ compError = comp.error() \
+ if type(comp.error()) is tuple else (comp.error(), '')
+ errMsg = (
+ "Component #%s encountered an error!" % compNo
+ if comp.error() is None else
+ 'Export cancelled by 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
+ compError[0]
)
+ )
+ comp._error.emit(errMsg, compError[1])
break
- if 'static' in comp.properties:
+ if 'static' in comp.properties():
self.staticComponents[compNo] = \
comp.frameRender(compNo, 0).copy()
if self.canceled:
if canceledByComponent:
print('Export cancelled by component #%s (%s): %s' % (
- compNo, str(comp), comp.error
- ))
+ compNo,
+ comp.name,
+ 'No message.' if comp.error() is None else (
+ comp.error() if type(comp.error()) is str
+ else comp.error()[0])
+ )
+ )
self.cancelExport()
return
--
cgit v1.2.3
From 15d70474d4df16cd03f4eb672d409166f793eabf Mon Sep 17 00:00:00 2001
From: tassaron
Date: Tue, 25 Jul 2017 22:02:47 -0400
Subject: error can be locked within properties()
and simplified the componenterrors again
---
src/component.py | 52 ++++++++++++++++--------------------------------
src/components/video.py | 35 ++++++++++++++------------------
src/mainwindow.py | 10 +++++-----
src/presetmanager.pyc | Bin 0 -> 10936 bytes
src/toolkit/ffmpeg.py | 4 ++--
src/video_thread.py | 11 ++++++----
6 files changed, 46 insertions(+), 66 deletions(-)
create mode 100644 src/presetmanager.pyc
(limited to 'src/toolkit/ffmpeg.py')
diff --git a/src/component.py b/src/component.py
index 7a768ed..5de67d1 100644
--- a/src/component.py
+++ b/src/component.py
@@ -19,7 +19,7 @@ class ComponentMetaclass(type(QtCore.QObject)):
return func(self, *args, **kwargs)
except Exception:
try:
- raise ComponentInitError(self, 'initialization process')
+ raise ComponentError(self, 'initialization process')
except ComponentError:
return
return initializationWrapper
@@ -63,7 +63,13 @@ class ComponentMetaclass(type(QtCore.QObject)):
if self._lockedProperties is not None:
return self._lockedProperties
else:
- return func(self)
+ try:
+ return func(self)
+ except Exception:
+ try:
+ raise ComponentError(self, 'properties')
+ except ComponentError:
+ return []
return propertiesWrapper
def errorWrapper(func):
@@ -396,18 +402,18 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
-class ComponentException(RuntimeError):
- '''A base class for component errors'''
+class ComponentError(RuntimeError):
+ '''Gives the MainWindow a traceback to display, and cancels the export.'''
- _prevErrors = []
+ prevErrors = []
- def __init__(self, caller, name, immediate):
+ def __init__(self, caller, name):
print('ComponentError by %s: %s' % (caller.name, name))
super().__init__()
- if len(ComponentException._prevErrors) > 1:
- ComponentException._prevErrors.pop()
- ComponentException._prevErrors.insert(0, name)
- if name in ComponentException._prevErrors[1:]:
+ if len(ComponentError.prevErrors) > 1:
+ ComponentError.prevErrors.pop()
+ ComponentError.prevErrors.insert(0, name)
+ if name in ComponentError.prevErrors[1:]:
# Don't create multiple windows for repeated messages
return
@@ -434,28 +440,4 @@ class ComponentException(RuntimeError):
)
)
- if immediate:
- caller._error.emit(string, detail)
- else:
- caller.lockProperties(['error'])
- caller.lockError((string, detail))
-
-
-class ComponentError(ComponentException):
- '''
- Use for general Python errors caused by a component at any time.
- Raising this gives the MainWindow a traceback to display and
- cancels any export in progress.
- '''
- def __init__(self, caller, name):
- ComponentException.__init__(self, caller, name, True)
-
-
-class ComponentInitError(ComponentError):
- '''
- Use for Python errors in preFrameRender, while the export is starting.
- This will end the video thread in a clean way by locking the component
- into an error state so the export definitely won't begin.
- '''
- def __init__(self, caller, name):
- ComponentException.__init__(self, caller, name, False)
+ caller._error.emit(string, detail)
diff --git a/src/components/video.py b/src/components/video.py
index 6b0a04a..8872fbf 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -154,33 +154,28 @@ class Component(Component):
return frame
def properties(self):
- # TODO: Disallow selecting the same video you're exporting to
props = []
- if not self.videoPath or self.badVideo \
- or not os.path.exists(self.videoPath):
- return ['error']
+
+ if not self.videoPath:
+ self.lockError("There is no video selected.")
+ elif self.badVideo:
+ self.lockError("Could not identify an audio stream in this video.")
+ elif not os.path.exists(self.videoPath):
+ self.lockError("The video selected does not exist!")
+ elif (os.path.realpath(self.videoPath) ==
+ os.path.realpath(
+ self.parent.window.lineEdit_outputFile.text())):
+ self.lockError("Input and output paths match.")
if self.useAudio:
props.append('audio')
- self.testAudioStream()
- if self.badAudio:
- return ['error']
+ if not testAudioStream(self.videoPath) \
+ and self.error() is None:
+ self.lockError(
+ "Could not identify an audio stream in this video.")
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):
- self.badAudio = testAudioStream(self.videoPath)
-
def audio(self):
params = {}
if self.volume != 1.0:
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 3cc5d26..e478d19 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -573,16 +573,16 @@ class MainWindow(QtWidgets.QMainWindow):
@QtCore.pyqtSlot(str, str)
def videoThreadError(self, msg, detail):
- self.showMessage(
- msg=msg,
- detail=detail,
- icon='Warning',
- )
try:
self.stopVideo()
except AttributeError as e:
if 'videoWorker' not in str(e):
raise
+ self.showMessage(
+ msg=msg,
+ detail=detail,
+ icon='Warning',
+ )
def changeEncodingStatus(self, status):
self.encoding = status
diff --git a/src/presetmanager.pyc b/src/presetmanager.pyc
new file mode 100644
index 0000000..97069d2
Binary files /dev/null and b/src/presetmanager.pyc differ
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index 8f5ae87..8d63659 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -224,9 +224,9 @@ def testAudioStream(filename):
try:
checkOutput(audioTestCommand, stderr=subprocess.DEVNULL)
except subprocess.CalledProcessError:
- return True
- else:
return False
+ else:
+ return True
def getAudioDuration(filename):
diff --git a/src/video_thread.py b/src/video_thread.py
index 8cbe8a8..48f3729 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -163,24 +163,27 @@ class Worker(QtCore.QObject):
except ComponentError:
pass
- if 'error' in comp.properties():
+ compProps = comp.properties()
+ if 'error' in compProps or comp.error() is not None:
self.cancel()
self.canceled = True
canceledByComponent = True
compError = comp.error() \
if type(comp.error()) is tuple else (comp.error(), '')
errMsg = (
- "Component #%s encountered an error!" % compNo
+ "Component #%s (%s) encountered an error!" % (
+ str(compNo), comp.name
+ )
if comp.error() is None else
'Export cancelled by component #%s (%s): %s' % (
str(compNo),
- str(comp),
+ comp.name,
compError[0]
)
)
comp._error.emit(errMsg, compError[1])
break
- if 'static' in comp.properties():
+ if 'static' in compProps:
self.staticComponents[compNo] = \
comp.frameRender(compNo, 0).copy()
--
cgit v1.2.3
From de1324a6a75eb2a9f97d8a6b416077cfc73b2bc9 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 27 Jul 2017 17:49:08 -0400
Subject: fixed video component eating stdout
+ made height/width into properties to simplify render methods
---
src/component.py | 150 +++++++++++++++++++++++++--------------------
src/components/color.py | 12 ++--
src/components/image.py | 12 ++--
src/components/original.py | 10 +--
src/components/sound.py | 10 ---
src/components/text.py | 13 ++--
src/components/video.py | 67 ++++++++++----------
src/preview_thread.py | 2 +-
src/toolkit/ffmpeg.py | 6 +-
src/video_thread.py | 14 +++--
10 files changed, 141 insertions(+), 155 deletions(-)
(limited to 'src/toolkit/ffmpeg.py')
diff --git a/src/component.py b/src/component.py
index 5de67d1..1c5ccb3 100644
--- a/src/component.py
+++ b/src/component.py
@@ -4,6 +4,9 @@
'''
from PyQt5 import uic, QtCore, QtWidgets
import os
+import time
+
+from toolkit.frame import BlankFrame
class ComponentMetaclass(type(QtCore.QObject)):
@@ -28,10 +31,12 @@ class ComponentMetaclass(type(QtCore.QObject)):
def renderWrapper(self, *args, **kwargs):
try:
return func(self, *args, **kwargs)
- except Exception:
- from toolkit.frame import BlankFrame
+ except Exception as e:
try:
- raise ComponentError(self, 'renderer')
+ if e.__name__.startswith('Component'):
+ raise
+ else:
+ raise ComponentError(self, 'renderer')
except ComponentError:
return BlankFrame()
return renderWrapper
@@ -93,7 +98,7 @@ class ComponentMetaclass(type(QtCore.QObject)):
'names', # Class methods
'error', 'audio', 'properties', # Properties
'preFrameRender', 'previewRender',
- 'command',
+ 'frameRender', 'command',
)
# Auto-decorate methods
@@ -110,7 +115,7 @@ class ComponentMetaclass(type(QtCore.QObject)):
if key == 'command':
attrs[key] = cls.commandWrapper(attrs[key])
- if key == 'previewRender':
+ if key in ('previewRender', 'frameRender'):
attrs[key] = cls.renderWrapper(attrs[key])
if key == 'preFrameRender':
@@ -180,6 +185,37 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self.__class__.name, str(self.__class__.version), self.savePreset()
)
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # Critical Methods
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+
+ def previewRender(self):
+ image = BlankFrame(self.width, self.height)
+ return image
+
+ def preFrameRender(self, **kwargs):
+ '''
+ Must call super() when subclassing
+ 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
+ for a long initialization procedure (i.e., for a visualizer)
+ '''
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+
+ def frameRender(self, frameNo):
+ audioArrayIndex = frameNo * self.sampleSize
+ image = BlankFrame(self.width, self.height)
+ return image
+
+ def renderFinished(self):
+ pass
+
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# Properties
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
@@ -196,6 +232,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
Return a string containing an error message, or None for a default.
Or tuple of two strings for a message with details.
+ Alternatively use lockError(msgString) within properties()
+ to skip this method entirely.
'''
return
@@ -211,7 +249,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
- # Methods
+ # Idle Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
def widget(self, parent):
@@ -244,33 +282,11 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
for widget in widgets['comboBox']:
widget.currentIndexChanged.connect(self.update)
- def trackWidgets(self, trackDict, **kwargs):
- '''
- Name widgets to track in update(), savePreset(), loadPreset(), and
- command(). Requires a dict of attr names as keys, widgets as values
-
- Optional args:
- 'presetNames': preset variable names to replace attr names
- 'commandArgs': arg keywords that differ from attr names
-
- NOTE: Any kwarg key set to None will selectively disable tracking.
- '''
- self._trackedWidgets = trackDict
- for kwarg in kwargs:
- try:
- if kwarg in ('presetNames', 'commandArgs'):
- setattr(self, '_%s' % kwarg, kwargs[kwarg])
- else:
- raise ComponentError(
- self, 'Nonsensical keywords to trackWidgets.')
- except ComponentError:
- continue
-
def update(self):
'''
Reads all tracked widget values into instance attributes
and tells the MainWindow that the component was modified.
- Call at the END of your method if you need to subclass this.
+ Call super() at the END if you need to subclass this.
'''
for attr, widget in self._trackedWidgets.items():
if type(widget) == QtWidgets.QLineEdit:
@@ -320,20 +336,6 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
] = getattr(self, attr)
return saveValueStore
- def preFrameRender(self, **kwargs):
- '''
- Triggered only before a video is exported (video_thread.py)
- self.worker = the video thread worker
- self.completeAudioArray = a list of audio samples
- self.sampleSize = number of audio samples per video frame
- self.progressBarUpdate = signal to set progress bar number
- self.progressBarSetText = signal to set progress bar text
- Use the latter two signals to update the MainWindow if needed
- for a long initialization procedure (i.e., for a visualizer)
- '''
- for key, value in kwargs.items():
- setattr(self, key, value)
-
def commandHelp(self):
'''Help text as string for this component's commandline arguments'''
@@ -356,6 +358,28 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
# "Private" Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ def trackWidgets(self, trackDict, **kwargs):
+ '''
+ Name widgets to track in update(), savePreset(), loadPreset(), and
+ command(). Requires a dict of attr names as keys, widgets as values
+
+ Optional args:
+ 'presetNames': preset variable names to replace attr names
+ 'commandArgs': arg keywords that differ from attr names
+
+ NOTE: Any kwarg key set to None will selectively disable tracking.
+ '''
+ self._trackedWidgets = trackDict
+ for kwarg in kwargs:
+ try:
+ if kwarg in ('presetNames', 'commandArgs'):
+ setattr(self, '_%s' % kwarg, kwargs[kwarg])
+ else:
+ raise ComponentError(
+ self, 'Nonsensical keywords to trackWidgets.')
+ except ComponentError:
+ continue
+
def lockProperties(self, propList):
self._lockedProperties = propList
@@ -372,6 +396,14 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''Load a Qt Designer ui file to use for this component's widget'''
return uic.loadUi(os.path.join(self.core.componentsPath, filename))
+ @property
+ def width(self):
+ return int(self.settings.value('outputWidth'))
+
+ @property
+ def height(self):
+ return int(self.settings.value('outputHeight'))
+
def cancel(self):
'''Stop any lengthy process in response to this variable.'''
self.canceled = True
@@ -381,41 +413,24 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self.unlockProperties()
self.unlockError()
- '''
- ### Reference methods for creating a new component
- ### (Inherit from this class and define these)
-
- def previewRender(self, previewWorker):
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
- from toolkit.frame import BlankFrame
- image = BlankFrame(width, height)
- return image
-
- def frameRender(self, layerNo, frameNo):
- audioArrayIndex = frameNo * self.sampleSize
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
- from toolkit.frame import BlankFrame
- image = BlankFrame(width, height)
- return image
- '''
-
class ComponentError(RuntimeError):
'''Gives the MainWindow a traceback to display, and cancels the export.'''
prevErrors = []
+ lastTime = time.time()
def __init__(self, caller, name):
- print('ComponentError by %s: %s' % (caller.name, name))
- super().__init__()
+ print('##### ComponentError by %s: %s' % (caller.name, name))
if len(ComponentError.prevErrors) > 1:
ComponentError.prevErrors.pop()
ComponentError.prevErrors.insert(0, name)
- if name in ComponentError.prevErrors[1:]:
- # Don't create multiple windows for repeated messages
+ curTime = time.time()
+ if name in ComponentError.prevErrors[1:] \
+ and curTime - ComponentError.lastTime < 0.2:
+ # Don't create multiple windows for quickly repeated messages
return
+ ComponentError.lastTime = time.time()
from toolkit import formatTraceback
import sys
@@ -440,4 +455,5 @@ class ComponentError(RuntimeError):
)
)
+ super().__init__(string)
caller._error.emit(string, detail)
diff --git a/src/components/color.py b/src/components/color.py
index 8257ed9..2abd79a 100644
--- a/src/components/color.py
+++ b/src/components/color.py
@@ -96,18 +96,14 @@ class Component(Component):
super().update()
- def previewRender(self, previewWorker):
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
- return self.drawFrame(width, height)
+ def previewRender(self):
+ return self.drawFrame(self.width, self.height)
def properties(self):
return ['static']
- def frameRender(self, layerNo, frameNo):
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
- return self.drawFrame(width, height)
+ def frameRender(self, frameNo):
+ return self.drawFrame(self.width, self.height)
def drawFrame(self, width, height):
r, g, b = self.color1
diff --git a/src/components/image.py b/src/components/image.py
index a705904..a96f127 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -31,10 +31,8 @@ class Component(Component):
},
)
- def previewRender(self, previewWorker):
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
- return self.drawFrame(width, height)
+ def previewRender(self):
+ return self.drawFrame(self.width, self.height)
def properties(self):
props = ['static']
@@ -48,10 +46,8 @@ class Component(Component):
if not os.path.exists(self.imagePath):
return "The image selected does not exist!"
- def frameRender(self, layerNo, frameNo):
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
- return self.drawFrame(width, height)
+ def frameRender(self, frameNo):
+ return self.drawFrame(self.width, self.height)
def drawFrame(self, width, height):
frame = BlankFrame(width, height)
diff --git a/src/components/original.py b/src/components/original.py
index 570465d..3d1a574 100644
--- a/src/components/original.py
+++ b/src/components/original.py
@@ -59,13 +59,11 @@ class Component(Component):
saveValueStore['visColor'] = self.visColor
return saveValueStore
- def previewRender(self, previewWorker):
+ def previewRender(self):
spectrum = numpy.fromfunction(
lambda x: float(self.scale)/2500*(x-128)**2, (255,), dtype="int16")
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
return self.drawBars(
- width, height, spectrum, self.visColor, self.layout
+ self.width, self.height, spectrum, self.visColor, self.layout
)
def preFrameRender(self, **kwargs):
@@ -74,8 +72,6 @@ class Component(Component):
self.smoothConstantUp = 0.8
self.lastSpectrum = None
self.spectrumArray = {}
- 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:
@@ -93,7 +89,7 @@ class Component(Component):
self.progressBarSetText.emit(pStr)
self.progressBarUpdate.emit(int(progress))
- def frameRender(self, layerNo, frameNo):
+ def frameRender(self, frameNo):
arrayNo = frameNo * self.sampleSize
return self.drawBars(
self.width, self.height,
diff --git a/src/components/sound.py b/src/components/sound.py
index fcd9e4e..aff43d3 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -21,11 +21,6 @@ class Component(Component):
'sound': None,
})
- def previewRender(self, previewWorker):
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
- return BlankFrame(width, height)
-
def preFrameRender(self, **kwargs):
pass
@@ -63,11 +58,6 @@ class Component(Component):
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 commandHelp(self):
print('Path to audio file:\n path=/filepath/to/sound.ogg')
diff --git a/src/components/text.py b/src/components/text.py
index 1d64617..8a302ff 100644
--- a/src/components/text.py
+++ b/src/components/text.py
@@ -97,10 +97,8 @@ class Component(Component):
saveValueStore['textColor'] = self.textColor
return saveValueStore
- def previewRender(self, previewWorker):
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
- return self.addText(width, height)
+ def previewRender(self):
+ return self.addText(self.width, self.height)
def properties(self):
props = ['static']
@@ -111,13 +109,10 @@ class Component(Component):
def error(self):
return "No text provided."
- def frameRender(self, layerNo, frameNo):
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
- return self.addText(width, height)
+ def frameRender(self, frameNo):
+ return self.addText(self.width, self.height)
def addText(self, width, height):
-
image = FramePainter(width, height)
self.titleFont.setPixelSize(self.fontSize)
image.setFont(self.titleFont)
diff --git a/src/components/video.py b/src/components/video.py
index 8872fbf..48ac557 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -3,10 +3,11 @@ from PyQt5 import QtGui, QtCore, QtWidgets
import os
import math
import subprocess
+import signal
import threading
from queue import PriorityQueue
-from component import Component
+from component import Component, ComponentError
from toolkit.frame import BlankFrame
from toolkit.ffmpeg import testAudioStream
from toolkit import openPipe, checkOutput
@@ -14,6 +15,10 @@ from toolkit import openPipe, checkOutput
class Video:
'''Opens a pipe to ffmpeg and stores a buffer of raw video frames.'''
+
+ # error from the thread used to fill the buffer
+ threadError = None
+
def __init__(self, **kwargs):
mandatoryArgs = [
'ffmpeg', # path to ffmpeg, usually self.core.FFMPEG_BIN
@@ -71,8 +76,8 @@ class Video:
self.frameBuffer.task_done()
def fillBuffer(self):
- pipe = openPipe(
- self.command, stdout=subprocess.PIPE,
+ self.pipe = openPipe(
+ self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, bufsize=10**8
)
while True:
@@ -85,19 +90,11 @@ class Video:
if len(self.currentFrame) == 0:
self.frameBuffer.put((self.frameNo-1, self.lastFrame))
continue
- except AttributeError as e:
- self.parent.showMessage(
- msg='%s couldn\'t be loaded. '
- 'This is a fatal error.' % os.path.basename(
- self.videoPath
- ),
- detail=str(e),
- icon='Warning'
- )
- self.parent.stopVideo()
+ except AttributeError:
+ Video.threadError = ComponentError(self.component, 'video')
break
- self.currentFrame = pipe.stdout.read(self.chunkSize)
+ self.currentFrame = self.pipe.stdout.read(self.chunkSize)
if len(self.currentFrame) != 0:
self.frameBuffer.put((self.frameNo, self.currentFrame))
self.lastFrame = self.currentFrame
@@ -143,13 +140,11 @@ class Component(Component):
self.page.spinBox_volume.setEnabled(False)
super().update()
- def previewRender(self, previewWorker):
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
- self.updateChunksize(width, height)
- frame = self.getPreviewFrame(width, height)
+ def previewRender(self):
+ self.updateChunksize()
+ frame = self.getPreviewFrame(self.width, self.height)
if not frame:
- return BlankFrame(width, height)
+ return BlankFrame(self.width, self.height)
else:
return frame
@@ -184,23 +179,23 @@ class Component(Component):
def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs)
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
- self.blankFrame_ = BlankFrame(width, height)
- self.updateChunksize(width, height)
+ self.updateChunksize()
self.video = Video(
ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath,
- width=width, height=height, chunkSize=self.chunkSize,
+ width=self.width, height=self.height, chunkSize=self.chunkSize,
frameRate=int(self.settings.value("outputFrameRate")),
parent=self.parent, loopVideo=self.loopVideo,
component=self, scale=self.scale
) if os.path.exists(self.videoPath) else None
- def frameRender(self, layerNo, frameNo):
- if self.video:
- return self.video.frame(frameNo)
- else:
- return self.blankFrame_
+ def frameRender(self, frameNo):
+ if Video.threadError is not None:
+ raise Video.threadError
+ return self.video.frame(frameNo)
+
+ def renderFinished(self):
+ self.video.pipe.stdout.close()
+ self.video.pipe.send_signal(signal.SIGINT)
def pickVideo(self):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
@@ -230,20 +225,20 @@ class Component(Component):
'-vframes', '1',
]
pipe = openPipe(
- command, stdout=subprocess.PIPE,
+ command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, bufsize=10**8
)
byteFrame = pipe.stdout.read(self.chunkSize)
- frame = finalizeFrame(self, byteFrame, width, height)
pipe.stdout.close()
- pipe.kill()
+ pipe.send_signal(signal.SIGINT)
+ frame = finalizeFrame(self, byteFrame, width, height)
return frame
- def updateChunksize(self, width, height):
+ def updateChunksize(self):
if self.scale != 100 and not self.distort:
- width, height = scale(self.scale, width, height, int)
- self.chunkSize = 4*width*height
+ width, height = scale(self.scale, self.width, self.height, int)
+ self.chunkSize = 4 * width * height
def command(self, arg):
if '=' in arg:
diff --git a/src/preview_thread.py b/src/preview_thread.py
index 9917e4b..0a6a856 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -59,7 +59,7 @@ class Worker(QtCore.QObject):
components = nextPreviewInformation["components"]
for component in reversed(components):
try:
- newFrame = component.previewRender(self)
+ newFrame = component.previewRender()
frame = Image.alpha_composite(
frame, newFrame
)
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index 8d63659..2fffc7b 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -252,7 +252,7 @@ def getAudioDuration(filename):
return duration
-def readAudioFile(filename, parent):
+def readAudioFile(filename, videoWorker):
'''
Creates the completeAudioArray given to components
and used to draw the classic visualizer.
@@ -296,8 +296,8 @@ def readAudioFile(filename, parent):
if lastPercent != percent:
string = 'Loading audio file: '+str(percent)+'%'
- parent.progressBarSetText.emit(string)
- parent.progressBarUpdate.emit(percent)
+ videoWorker.progressBarSetText.emit(string)
+ videoWorker.progressBarUpdate.emit(percent)
lastPercent = percent
diff --git a/src/video_thread.py b/src/video_thread.py
index c5a3c09..8c7d585 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -60,8 +60,7 @@ class Worker(QtCore.QObject):
audioI = self.compositeQueue.get()
bgI = int(audioI / self.sampleSize)
frame = None
- for compNo, comp in reversed(list(enumerate(self.components))):
- layerNo = len(self.components) - compNo - 1
+ for layerNo, comp in enumerate(reversed((self.components))):
if layerNo in self.staticComponents:
if self.staticComponents[layerNo] is None:
# this layer was merged into a following layer
@@ -76,10 +75,10 @@ class Worker(QtCore.QObject):
else:
# animated component
if frame is None: # bottom-most layer
- frame = comp.frameRender(compNo, bgI)
+ frame = comp.frameRender(bgI)
else:
frame = Image.alpha_composite(
- frame, comp.frameRender(compNo, bgI)
+ frame, comp.frameRender(bgI)
)
self.renderQueue.put([audioI, frame])
@@ -185,7 +184,7 @@ class Worker(QtCore.QObject):
break
if 'static' in compProps:
self.staticComponents[compNo] = \
- comp.frameRender(compNo, 0).copy()
+ comp.frameRender(0).copy()
if self.canceled:
if canceledByComponent:
@@ -290,8 +289,11 @@ class Worker(QtCore.QObject):
print(self.out_pipe.stderr.read())
self.out_pipe.stderr.close()
self.error = True
- # out_pipe.terminate() # don't terminate ffmpeg too early
self.out_pipe.wait()
+
+ for comp in reversed(self.components):
+ comp.renderFinished()
+
if self.canceled:
print("Export Canceled")
try:
--
cgit v1.2.3