From e92e9d79f95ad67e83074ef318278c3486601eac Mon Sep 17 00:00:00 2001
From: DH4
Date: Fri, 23 Jun 2017 17:38:05 -0500
Subject: QT5 Conversion + Directory Structure
---
src/video_thread.py | 309 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 309 insertions(+)
create mode 100644 src/video_thread.py
(limited to 'src/video_thread.py')
diff --git a/src/video_thread.py b/src/video_thread.py
new file mode 100644
index 0000000..5ea6d21
--- /dev/null
+++ b/src/video_thread.py
@@ -0,0 +1,309 @@
+from PyQt5 import QtCore, QtGui, uic
+from PyQt5.QtCore import pyqtSignal, pyqtSlot
+from PIL import Image, ImageDraw, ImageFont
+from PIL.ImageQt import ImageQt
+import core
+import numpy
+import subprocess as sp
+import sys
+import os
+from queue import Queue, PriorityQueue
+from threading import Thread, Event
+import time
+from copy import copy
+import signal
+
+
+class Worker(QtCore.QObject):
+
+ imageCreated = pyqtSignal(['QImage'])
+ videoCreated = pyqtSignal()
+ progressBarUpdate = pyqtSignal(int)
+ progressBarSetText = pyqtSignal(str)
+ encoding = pyqtSignal(bool)
+
+ def __init__(self, parent=None):
+ QtCore.QObject.__init__(self)
+ self.core = core.Core()
+ self.core.settings = parent.settings
+ self.modules = parent.core.modules
+ self.parent = parent
+ parent.videoTask.connect(self.createVideo)
+ self.sampleSize = 1470 # 44100 / 30 = 1470
+ self.canceled = False
+ self.error = False
+ self.stopped = False
+
+ def renderNode(self):
+ while not self.stopped:
+ i = self.compositeQueue.get()
+ frame = None
+
+ for compNo, comp in reversed(list(enumerate(self.components))):
+ if compNo in self.staticComponents and \
+ self.staticComponents[compNo] is not None:
+ if frame is None:
+ frame = self.staticComponents[compNo]
+ else:
+ frame = Image.alpha_composite(
+ frame, self.staticComponents[compNo])
+ else:
+ if frame is None:
+ frame = comp.frameRender(compNo, i[0], i[1])
+ else:
+ frame = Image.alpha_composite(
+ frame, comp.frameRender(compNo, i[0], i[1]))
+
+ self.renderQueue.put([i[0], frame])
+ self.compositeQueue.task_done()
+
+ def renderDispatch(self):
+ print('Dispatching Frames for Compositing...')
+
+ for i in range(0, len(self.completeAudioArray), self.sampleSize):
+ self.compositeQueue.put([i, self.bgI])
+ # increment tracked video frame for next iteration
+ self.bgI += 1
+
+ def previewDispatch(self):
+ background = Image.new("RGBA", (1920, 1080), (0, 0, 0, 0))
+ background.paste(Image.open(os.path.join(
+ self.core.wd, "background.png")))
+ background = background.resize((self.width, self.height))
+
+ while not self.stopped:
+ i = self.previewQueue.get()
+ if time.time() - self.lastPreview >= 0.06 or i[0] == 0:
+ image = copy(background)
+ image = Image.alpha_composite(image, i[1])
+ self._image = ImageQt(image)
+ self.imageCreated.emit(QtGui.QImage(self._image))
+ self.lastPreview = time.time()
+
+ self.previewQueue.task_done()
+
+ @pyqtSlot(str, str, list)
+ def createVideo(self, inputFile, outputFile, components):
+ self.encoding.emit(True)
+ self.components = components
+ self.outputFile = outputFile
+ self.bgI = 0 # tracked video frame
+ self.reset()
+ self.width = int(self.core.settings.value('outputWidth'))
+ self.height = int(self.core.settings.value('outputHeight'))
+ progressBarValue = 0
+ self.progressBarUpdate.emit(progressBarValue)
+
+ self.progressBarSetText.emit('Loading audio file...')
+ self.completeAudioArray = self.core.readAudioFile(inputFile, self)
+
+ # test if user has libfdk_aac
+ encoders = sp.check_output(
+ self.core.FFMPEG_BIN + " -encoders -hide_banner",
+ shell=True)
+
+ encoders = encoders.decode("utf-8")
+
+ acodec = self.core.settings.value('outputAudioCodec')
+
+ options = self.core.encoder_options
+ containerName = self.core.settings.value('outputContainer')
+ vcodec = self.core.settings.value('outputVideoCodec')
+ vbitrate = str(self.core.settings.value('outputVideoBitrate'))+'k'
+ acodec = self.core.settings.value('outputAudioCodec')
+ abitrate = str(self.core.settings.value('outputAudioBitrate'))+'k'
+
+ for cont in options['containers']:
+ if cont['name'] == containerName:
+ container = cont['container']
+ break
+
+ vencoders = options['video-codecs'][vcodec]
+ aencoders = options['audio-codecs'][acodec]
+
+ #print(encoders)
+ for encoder in vencoders:
+ #print(encoder)
+ if encoder in encoders:
+ vencoder = encoder
+ break
+
+ for encoder in aencoders:
+ #print(encoder)
+ if encoder in encoders:
+ aencoder = encoder
+ break
+
+ ffmpegCommand = [
+ self.core.FFMPEG_BIN,
+ '-thread_queue_size', '512',
+ '-y', # overwrite the output file if it already exists.
+ '-f', 'rawvideo',
+ '-vcodec', 'rawvideo',
+ '-s', str(self.width)+'x'+str(self.height), # size of one frame
+ '-pix_fmt', 'rgba',
+
+ # frames per second
+ '-r', self.core.settings.value('outputFrameRate'),
+ '-i', '-', # The input comes from a pipe
+ '-an',
+ '-i', inputFile,
+ '-vcodec', vencoder,
+ '-acodec', aencoder, # output audio codec
+ '-b:v', vbitrate,
+ '-b:a', abitrate,
+ '-pix_fmt', self.core.settings.value('outputVideoFormat'),
+ '-preset', self.core.settings.value('outputPreset'),
+ '-f', container
+ ]
+
+ if acodec == 'aac':
+ ffmpegCommand.append('-strict')
+ ffmpegCommand.append('-2')
+
+ ffmpegCommand.append(outputFile)
+
+ # ### Now start creating video for output ###
+ numpy.seterr(divide='ignore')
+
+ # Call preFrameRender on all components
+ print('Loaded Components:', ", ".join(
+ ["%s) %s" % (num, str(component)) \
+ for num, component in enumerate(reversed(self.components))
+ ]))
+ self.staticComponents = {}
+ numComps = len(self.components)
+ for compNo, comp in enumerate(self.components):
+ pStr = "Analyzing audio..."
+ self.progressBarSetText.emit(pStr)
+ properties = None
+ properties = comp.preFrameRender(
+ worker=self,
+ completeAudioArray=self.completeAudioArray,
+ sampleSize=self.sampleSize,
+ progressBarUpdate=self.progressBarUpdate,
+ progressBarSetText=self.progressBarSetText
+ )
+
+ if properties and 'static' in properties:
+ self.staticComponents[compNo] = copy(
+ comp.frameRender(compNo, 0, 0))
+ self.progressBarUpdate.emit(100)
+
+ # Create ffmpeg pipe and queues for frames
+ self.out_pipe = sp.Popen(
+ ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout)
+ self.compositeQueue = Queue()
+ self.compositeQueue.maxsize = 20
+ self.renderQueue = PriorityQueue()
+ self.renderQueue.maxsize = 20
+ self.previewQueue = PriorityQueue()
+
+ # Threads to render frames and send them back here for piping out
+ self.renderThreads = []
+ for i in range(3):
+ self.renderThreads.append(
+ Thread(target=self.renderNode, name="Render Thread"))
+ self.renderThreads[i].daemon = True
+ self.renderThreads[i].start()
+
+ self.dispatchThread = Thread(
+ target=self.renderDispatch, name="Render Dispatch Thread")
+ self.dispatchThread.daemon = True
+ self.dispatchThread.start()
+
+ self.previewDispatch = Thread(
+ target=self.previewDispatch, name="Render Dispatch Thread")
+ self.previewDispatch.daemon = True
+ self.previewDispatch.start()
+
+ frameBuffer = {}
+ self.lastPreview = 0.0
+ self.progressBarUpdate.emit(0)
+ pStr = "Exporting video..."
+ self.progressBarSetText.emit(pStr)
+ if not self.canceled:
+ for i in range(0, len(self.completeAudioArray), self.sampleSize):
+ while True:
+ if i in frameBuffer or self.canceled:
+ # if frame's in buffer, pipe it to ffmpeg
+ break
+ # else fetch the next frame & add to the buffer
+ data = self.renderQueue.get()
+ frameBuffer[data[0]] = data[1]
+ self.renderQueue.task_done()
+ if self.canceled:
+ break
+
+ try:
+ self.out_pipe.stdin.write(frameBuffer[i].tobytes())
+ self.previewQueue.put([i, frameBuffer[i]])
+ del frameBuffer[i]
+ except:
+ break
+
+ # increase progress bar value
+ if progressBarValue + 1 <= (i / len(self.completeAudioArray)) \
+ * 100:
+ progressBarValue = numpy.floor(
+ (i / len(self.completeAudioArray)) * 100)
+ self.progressBarUpdate.emit(progressBarValue)
+ pStr = "Exporting video: " + str(int(progressBarValue)) \
+ + "%"
+ self.progressBarSetText.emit(pStr)
+
+ numpy.seterr(all='print')
+
+ self.out_pipe.stdin.close()
+ if self.out_pipe.stderr is not None:
+ print(self.out_pipe.stderr.read())
+ self.out_pipe.stderr.close()
+ self.error = True
+ # out_pipe.terminate() # don't terminate ffmpeg too early
+ self.out_pipe.wait()
+ if self.canceled:
+ print("Export Canceled")
+ try:
+ os.remove(self.outputFile)
+ except:
+ pass
+ self.progressBarUpdate.emit(0)
+ self.progressBarSetText.emit('Export Canceled')
+
+ else:
+ if self.error:
+ print("Export Failed")
+ self.progressBarUpdate.emit(0)
+ self.progressBarSetText.emit('Export Failed')
+ else:
+ print("Export Complete")
+ self.progressBarUpdate.emit(100)
+ self.progressBarSetText.emit('Export Complete')
+
+ self.error = False
+ self.canceled = False
+ self.stopped = True
+ self.encoding.emit(False)
+ self.videoCreated.emit()
+
+ def updateProgress(self, pStr, pVal):
+ self.progressBarValue.emit(pVal)
+ self.progressBarSetText.emit(pStr)
+
+ def cancel(self):
+ self.canceled = True
+ self.core.cancel()
+
+ for comp in self.components:
+ comp.cancel()
+
+ try:
+ self.out_pipe.send_signal(signal.SIGINT)
+ except:
+ pass
+
+ def reset(self):
+ self.core.reset()
+ self.canceled = False
+ for comp in self.components:
+ comp.reset()
--
cgit v1.2.3
From 680214f5180a12f2250d8e266df9375ce99b9f80 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Fri, 23 Jun 2017 23:00:24 -0400
Subject: qt5 fixes
also pep8 compliance
---
src/command.py | 15 +++----
src/components/__base__.py | 14 +++----
src/components/color.py | 15 ++++---
src/components/image.py | 2 +-
src/components/video.py | 14 ++++---
src/core.py | 28 +++++++------
src/main.py | 6 +--
src/mainwindow.py | 100 ++++++++++++++++++++++++++-------------------
src/presetmanager.py | 55 +++++++++++++------------
src/video_thread.py | 11 ++---
10 files changed, 143 insertions(+), 117 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/command.py b/src/command.py
index 1a1e810..2f71f31 100644
--- a/src/command.py
+++ b/src/command.py
@@ -22,9 +22,9 @@ class Command(QtCore.QObject):
self.parser = argparse.ArgumentParser(
description='Create a visualization for an audio file',
epilog='EXAMPLE COMMAND: main.py myvideotemplate.avp '
- '-i ~/Music/song.mp3 -o ~/video.mp4 '
- '-c 0 image path=~/Pictures/thisWeeksPicture.jpg '
- '-c 1 video "preset=My Logo" -c 2 vis layout=classic')
+ '-i ~/Music/song.mp3 -o ~/video.mp4 '
+ '-c 0 image path=~/Pictures/thisWeeksPicture.jpg '
+ '-c 1 video "preset=My Logo" -c 2 vis layout=classic')
self.parser.add_argument(
'-i', '--input', metavar='SOUND',
help='input audio file')
@@ -113,10 +113,11 @@ class Command(QtCore.QObject):
if name.capitalize() in compName:
return compName
- compFileNames = [ \
- os.path.splitext(os.path.basename(
- mod.__file__))[0] \
- for mod in self.core.modules \
+ compFileNames = [
+ os.path.splitext(
+ os.path.basename(mod.__file__)
+ )[0]
+ for mod in self.core.modules
]
for i, compFileName in enumerate(compFileNames):
if name.lower() in compFileName:
diff --git a/src/components/__base__.py b/src/components/__base__.py
index a4677b1..a24af40 100644
--- a/src/components/__base__.py
+++ b/src/components/__base__.py
@@ -39,7 +39,7 @@ class Component(QtCore.QObject):
then update self.page widgets using the preset dict.
'''
self.currentPreset = presetName \
- if presetName != None else presetDict['preset']
+ if presetName is not None else presetDict['preset']
def preFrameRender(self, **kwargs):
'''Triggered only before a video is exported (video_thread.py)
@@ -66,8 +66,8 @@ class Component(QtCore.QObject):
print('Couldn\'t locate preset "%s"' % preset)
quit(1)
else:
- print('Opening "%s" preset on layer %s' % \
- (preset, self.compPos))
+ print('Opening "%s" preset on layer %s' % (
+ preset, self.compPos))
self.core.openPreset(path, self.compPos, preset)
else:
print(
@@ -88,8 +88,8 @@ class Component(QtCore.QObject):
and return this as an RGB string and QPushButton stylesheet.
In a subclass apply stylesheet to any color selection widgets
'''
- dialog = QtGui.QColorDialog()
- dialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True)
+ dialog = QtWidgets.QColorDialog()
+ dialog.setOption(QtWidgets.QColorDialog.ShowAlphaChannel, True)
color = dialog.getColor()
if color.isValid():
RGBstring = '%s,%s,%s' % (
@@ -142,10 +142,10 @@ class Component(QtCore.QObject):
return image
'''
+
class BadComponentInit(Exception):
def __init__(self, arg, name):
- string = \
-'''################################
+ string = '''################################
Mandatory argument "%s" not specified
in %s instance initialization
###################################'''
diff --git a/src/components/color.py b/src/components/color.py
index 8f9a1d1..2e3902a 100644
--- a/src/components/color.py
+++ b/src/components/color.py
@@ -53,7 +53,7 @@ class Component(__base__.Component):
page.spinBox_height.valueChanged.connect(self.update)
page.checkBox_trans.stateChanged.connect(self.update)
- self.fillLabels = [ \
+ self.fillLabels = [
'Solid',
'Linear Gradient',
'Radial Gradient',
@@ -126,8 +126,8 @@ class Component(__base__.Component):
r, g, b = self.color1
shapeSize = (self.sizeWidth, self.sizeHeight)
# in default state, skip all this logic and return a plain fill
- if self.fillType==0 and shapeSize == (width, height) \
- and self.x == 0 and self.y == 0:
+ if self.fillType == 0 and shapeSize == (width, height) \
+ and self.x == 0 and self.y == 0:
return Image.new("RGBA", (width, height), (r, g, b, 255))
frame = self.blankFrame(width, height)
@@ -143,9 +143,11 @@ class Component(__base__.Component):
image = ImageQt(frame)
painter = QtGui.QPainter(image)
if self.stretch:
- w = width; h = height
+ w = width
+ h = height
else:
- w = self.sizeWidth; h = self.sizeWidth
+ w = self.sizeWidth
+ h = self.sizeWidth
if self.fillType == 1: # Linear Gradient
brush = QtGui.QLinearGradient(
@@ -170,7 +172,8 @@ class Component(__base__.Component):
else:
brush.setColorAt(1.0, QColor(*self.color2))
painter.setBrush(brush)
- painter.drawRect(self.x, self.y,
+ painter.drawRect(
+ self.x, self.y,
self.sizeWidth, self.sizeHeight)
painter.end()
imBytes = image.bits().asstring(image.numBytes())
diff --git a/src/components/image.py b/src/components/image.py
index 8ca88d3..3517af6 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -85,7 +85,7 @@ class Component(__base__.Component):
def pickImage(self):
imgDir = self.settings.value("backgroundDir", os.path.expanduser("~"))
- filename = QtGui.QFileDialog.getOpenFileName(
+ filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Image", imgDir,
"Image Files (%s)" % " ".join(self.imageFormats))
if filename:
diff --git a/src/components/video.py b/src/components/video.py
index 58ce7a3..0090426 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -41,8 +41,8 @@ class Video:
'-i', self.videoPath,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',
- '-filter:v', 'scale=%s:%s' %
- scale(self.scale, self.width, self.height, str),
+ '-filter:v', 'scale=%s:%s' % scale(
+ self.scale, self.width, self.height, str),
'-vcodec', 'rawvideo', '-',
]
@@ -180,7 +180,7 @@ class Component(__base__.Component):
def pickVideo(self):
imgDir = self.settings.value("backgroundDir", os.path.expanduser("~"))
- filename = QtGui.QFileDialog.getOpenFileName(
+ filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Video",
imgDir, "Video Files (%s)" % " ".join(self.videoFormats)
)
@@ -199,8 +199,8 @@ class Component(__base__.Component):
'-i', self.videoPath,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',
- '-filter:v', 'scale=%s:%s' %
- scale(self.scale, width, height, str),
+ '-filter:v', 'scale=%s:%s' % scale(
+ self.scale, width, height, str),
'-vcodec', 'rawvideo', '-',
'-ss', '90',
'-vframes', '1',
@@ -238,6 +238,7 @@ class Component(__base__.Component):
def commandHelp(self):
print('Load a video:\n path=/filepath/to/video.mp4')
+
def scale(scale, width, height, returntype=None):
width = (float(width) / 100.0) * float(scale)
height = (float(height) / 100.0) * float(scale)
@@ -248,6 +249,7 @@ def scale(scale, width, height, returntype=None):
else:
return (width, height)
+
def finalizeFrame(self, imageData, width, height):
if self.distort:
try:
@@ -265,7 +267,7 @@ def finalizeFrame(self, imageData, width, height):
imageData)
if self.scale != 100 \
- or self.xPosition != 0 or self.yPosition != 0:
+ or self.xPosition != 0 or self.yPosition != 0:
frame = self.blankFrame(width, height)
frame.paste(image, box=(self.xPosition, self.yPosition))
else:
diff --git a/src/core.py b/src/core.py
index bb5d351..670a3c5 100644
--- a/src/core.py
+++ b/src/core.py
@@ -179,7 +179,7 @@ class Core():
clearThis = False
# add loaded named presets to savedPresets dict
- if 'preset' in preset and preset['preset'] != None:
+ if 'preset' in preset and preset['preset'] is not None:
nam = preset['preset']
filepath2 = os.path.join(
self.presetDir, name, str(vers), nam)
@@ -195,12 +195,12 @@ class Core():
-1,
self.moduleIndexFor(name),
loader)
- if i == None:
+ if i is None:
loader.showMessage(msg="Too many components!")
break
try:
- if 'preset' in preset and preset['preset'] != None:
+ if 'preset' in preset and preset['preset'] is not None:
self.selectedComponents[i].loadPreset(
preset
)
@@ -210,8 +210,8 @@ class Core():
preset['preset']
)
except KeyError as e:
- print('%s missing value %s' %
- (self.selectedComponents[i], e))
+ print('%s missing value %s' % (
+ self.selectedComponents[i], e))
if clearThis:
self.clearPreset(i)
@@ -221,7 +221,6 @@ class Core():
errcode = 1
data = sys.exc_info()
-
if errcode == 1:
typ, value, _ = data
if typ.__name__ == KeyError:
@@ -274,11 +273,11 @@ class Core():
i += 1
elif i == 2:
lastCompPreset = Core.presetFromString(line)
- data[section].append(
- (lastCompName,
+ data[section].append((
+ lastCompName,
lastCompVers,
- lastCompPreset)
- )
+ lastCompPreset
+ ))
i = 0
return 0, data
except:
@@ -309,7 +308,9 @@ class Core():
return False, ''
def exportPreset(self, exportPath, compName, vers, origName):
- internalPath = os.path.join(self.presetDir, compName, str(vers), origName)
+ internalPath = os.path.join(
+ self.presetDir, compName, str(vers), origName
+ )
if not os.path.exists(internalPath):
return
if os.path.exists(exportPath):
@@ -328,7 +329,7 @@ class Core():
return False
def createPresetFile(
- self, compName, vers, presetName, saveValueStore, filepath=''):
+ self, compName, vers, presetName, saveValueStore, filepath=''):
'''Create a preset file (.avl) at filepath using args.
Or if filepath is empty, create an internal preset using args'''
if not filepath:
@@ -463,7 +464,8 @@ class Core():
@staticmethod
def presetToString(dictionary):
'''Alphabetizes a dict into OrderedDict & returns string repr'''
- return repr(OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])))
+ return repr(
+ OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])))
@staticmethod
def presetFromString(string):
diff --git a/src/main.py b/src/main.py
index 4bf26db..58fdb46 100644
--- a/src/main.py
+++ b/src/main.py
@@ -30,7 +30,6 @@ def LoadDefaultSettings(self):
}
for parm, value in default.items():
- #print(parm, self.settings.value(parm))
if self.settings.value(parm) is None:
self.settings.setValue(parm, value)
@@ -51,7 +50,7 @@ if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
app.setApplicationName("audio-visualizer")
- app.setOrganizationName("audio-visualizer")
+ # app.setOrganizationName("audio-visualizer")
if mode == 'cmd':
from command import *
@@ -76,7 +75,8 @@ if __name__ == "__main__":
dpi = desc.physicalDpiX()
topMargin = 0 if (dpi == 96) else int(10 * (dpi / 96))
- window.resize(window.width() * (dpi / 96), window.height() * (dpi / 96))
+ window.resize(
+ window.width() * (dpi / 96), window.height() * (dpi / 96))
# window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0)
main = MainWindow(window, proj)
diff --git a/src/mainwindow.py b/src/mainwindow.py
index a52a0f4..7a9e397 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -116,7 +116,6 @@ class MainWindow(QtWidgets.QMainWindow):
codec = window.comboBox_videoCodec.itemText(i)
if codec == self.settings.value('outputVideoCodec'):
window.comboBox_videoCodec.setCurrentIndex(i)
- #print(codec)
for i in range(window.comboBox_audioCodec.count()):
codec = window.comboBox_audioCodec.itemText(i)
@@ -146,10 +145,11 @@ 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.__doc__)
action.triggered.connect(
- lambda item=i: self.core.insertComponent(0, item, self))
+ lambda _, item=i: self.core.insertComponent(0, item, self))
self.window.pushButton_addComponent.setMenu(self.compMenu)
@@ -160,9 +160,10 @@ class MainWindow(QtWidgets.QMainWindow):
self.window.pushButton_removeComponent.clicked.connect(
lambda _: self.removeComponent())
- componentList.setContextMenuPolicy(
- QtCore.Qt.CustomContextMenu)
- componentList.customContextMenuRequested.connect(self.componentContextMenu)
+ componentList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ componentList.customContextMenuRequested.connect(
+ self.componentContextMenu
+ )
currentRes = str(self.settings.value('outputWidth'))+'x' + \
str(self.settings.value('outputHeight'))
@@ -245,19 +246,30 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog)
QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject)
- QtWidgets.QShortcut("Ctrl+T", self.window, activated=lambda:
- self.window.pushButton_addComponent.click())
- QtWidgets.QShortcut("Ctrl+Space", self.window, activated=lambda:
- self.window.listWidget_componentList.setFocus())
- QtWidgets.QShortcut("Ctrl+Shift+S", self.window,
- self.presetManager.openSavePresetDialog)
- QtWidgets.QShortcut("Ctrl+Shift+C", self.window,
- self.presetManager.clearPreset)
-
- QtWidgets.QShortcut("Ctrl+Up", self.window,
- activated=lambda: self.moveComponent(-1))
- QtWidgets.QShortcut("Ctrl+Down", self.window,
- activated=lambda: self.moveComponent(1))
+ QtWidgets.QShortcut(
+ "Ctrl+T", self.window,
+ activated=lambda: self.window.pushButton_addComponent.click()
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+Space", self.window,
+ activated=lambda: self.window.listWidget_componentList.setFocus()
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+Shift+S", self.window,
+ self.presetManager.openSavePresetDialog
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+Shift+C", self.window, self.presetManager.clearPreset
+ )
+
+ QtWidgets.QShortcut(
+ "Ctrl+Up", self.window,
+ activated=lambda: self.moveComponent(-1)
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+Down", self.window,
+ activated=lambda: self.moveComponent(1)
+ )
QtWidgets.QShortcut("Ctrl+Home", self.window, self.moveComponentTop)
QtWidgets.QShortcut("Ctrl+End", self.window, self.moveComponentBottom)
QtWidgets.QShortcut("Ctrl+r", self.window, self.removeComponent)
@@ -280,7 +292,7 @@ class MainWindow(QtWidgets.QMainWindow):
def updateComponentTitle(self, pos, presetStore=False):
if type(presetStore) == dict:
name = presetStore['preset']
- if name == None or name not in self.core.savedPresets:
+ if name is None or name not in self.core.savedPresets:
modified = False
else:
modified = (presetStore != self.core.savedPresets[name])
@@ -362,21 +374,22 @@ class MainWindow(QtWidgets.QMainWindow):
def openInputFileDialog(self):
inputDir = self.settings.value("inputDir", os.path.expanduser("~"))
- fileName = QtGui.QFileDialog.getOpenFileName(
+ fileName, _ = QtWidgets.QFileDialog.getOpenFileName(
self.window, "Open Audio File",
inputDir, "Audio Files (%s)" % " ".join(self.core.audioFormats))
- if not fileName == "":
+ if fileName:
self.settings.setValue("inputDir", os.path.dirname(fileName))
self.window.lineEdit_audioFile.setText(fileName)
def openOutputFileDialog(self):
outputDir = self.settings.value("outputDir", os.path.expanduser("~"))
- fileName = QtGui.QFileDialog.getSaveFileName(
+ fileName, _ = QtWidgets.QFileDialog.getSaveFileName(
self.window, "Set Output Video File",
outputDir,
- "Video Files (%s);; All Files (*)" % " ".join(self.core.videoFormats))
+ "Video Files (%s);; All Files (*)" % " ".join(
+ self.core.videoFormats))
if not fileName == "":
self.settings.setValue("outputDir", os.path.dirname(fileName))
@@ -547,13 +560,13 @@ class MainWindow(QtWidgets.QMainWindow):
'''Drop event for the component listwidget'''
componentList = self.window.listWidget_componentList
- modelIndexes = [ \
- componentList.model().index(i) \
- for i in range(componentList.count()) \
+ modelIndexes = [
+ componentList.model().index(i)
+ for i in range(componentList.count())
]
- rects = [ \
- componentList.visualRect(modelIndex) \
- for modelIndex in modelIndexes \
+ rects = [
+ componentList.visualRect(modelIndex)
+ for modelIndex in modelIndexes
]
rowPos = [rect.contains(event.pos()) for rect in rects]
@@ -602,9 +615,10 @@ class MainWindow(QtWidgets.QMainWindow):
if self.autosaveExists(identical=False):
ch = self.showMessage(
msg="You have unsaved changes in project '%s'. "
- "Save before %s?" % \
- (os.path.basename(self.currentProject)[:-4],
- phrase),
+ "Save before %s?" % (
+ os.path.basename(self.currentProject)[:-4],
+ phrase
+ ),
showCancel=True)
if ch:
success = self.saveProjectChanges()
@@ -613,7 +627,7 @@ class MainWindow(QtWidgets.QMainWindow):
os.remove(self.autosavePath)
def openSaveProjectDialog(self):
- filename = QtGui.QFileDialog.getSaveFileName(
+ filename, _ = QtWidgets.QFileDialog.getSaveFileName(
self.window, "Create Project File",
self.settings.value("projectDir"),
"Project Files (*.avp)")
@@ -628,7 +642,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.core.createProjectFile(filename)
def openOpenProjectDialog(self):
- filename = QtGui.QFileDialog.getOpenFileName(
+ filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.window, "Open Project File",
self.settings.value("projectDir"),
"Project Files (*.avp)")
@@ -657,17 +671,19 @@ class MainWindow(QtWidgets.QMainWindow):
def showMessage(self, **kwargs):
parent = kwargs['parent'] if 'parent' in kwargs else self.window
- msg = QtGui.QMessageBox(parent)
+ msg = QtWidgets.QMessageBox(parent)
msg.setModal(True)
msg.setText(kwargs['msg'])
msg.setIcon(
- kwargs['icon'] if 'icon' in kwargs else QtGui.QMessageBox.Information)
+ kwargs['icon']
+ if 'icon' in kwargs else QtWidgets.QMessageBox.Information
+ )
msg.setDetailedText(kwargs['detail'] if 'detail' in kwargs else None)
if 'showCancel'in kwargs and kwargs['showCancel']:
msg.setStandardButtons(
- QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel)
+ QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
else:
- msg.setStandardButtons(QtGui.QMessageBox.Ok)
+ msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
ch = msg.exec_()
if ch == 1024:
return True
@@ -687,7 +703,7 @@ class MainWindow(QtWidgets.QMainWindow):
return
self.presetManager.findPresets()
- self.menu = QtGui.QMenu()
+ self.menu = QMenu()
menuItem = self.menu.addAction("Save Preset")
menuItem.triggered.connect(
self.presetManager.openSavePresetDialog
@@ -695,8 +711,10 @@ class MainWindow(QtWidgets.QMainWindow):
# submenu for opening presets
try:
- presets = self.presetManager.presets[str(self.core.selectedComponents[index])]
- self.submenu = QtGui.QMenu("Open Preset")
+ presets = self.presetManager.presets[
+ str(self.core.selectedComponents[index])
+ ]
+ self.submenu = QMenu("Open Preset")
self.menu.addMenu(self.submenu)
for version, presetName in presets:
diff --git a/src/presetmanager.py b/src/presetmanager.py
index ec3f5cd..97f6e0e 100644
--- a/src/presetmanager.py
+++ b/src/presetmanager.py
@@ -1,4 +1,4 @@
-from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt5 import QtCore, QtWidgets
import string
import os
@@ -21,13 +21,15 @@ class PresetManager(QtWidgets.QDialog):
# window
self.lastFilter = '*'
- self.presetRows = [] # list of (comp, vers, name) tuples
+ self.presetRows = [] # list of (comp, vers, name) tuples
self.window = window
self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
# connect button signals
- self.window.pushButton_delete.clicked.connect(self.openDeletePresetDialog)
- self.window.pushButton_rename.clicked.connect(self.openRenamePresetDialog)
+ self.window.pushButton_delete.clicked.connect(
+ self.openDeletePresetDialog)
+ self.window.pushButton_rename.clicked.connect(
+ self.openRenamePresetDialog)
self.window.pushButton_import.clicked.connect(self.openImportDialog)
self.window.pushButton_export.clicked.connect(self.openExportDialog)
self.window.pushButton_close.clicked.connect(self.window.close)
@@ -36,7 +38,8 @@ class PresetManager(QtWidgets.QDialog):
self.drawFilterList()
self.window.comboBox_filter.currentIndexChanged.connect(
lambda: self.drawPresetList(
- self.window.comboBox_filter.currentText(), self.window.lineEdit_search.text()
+ self.window.comboBox_filter.currentText(),
+ self.window.lineEdit_search.text()
)
)
@@ -47,7 +50,8 @@ class PresetManager(QtWidgets.QDialog):
self.window.lineEdit_search.setCompleter(completer)
self.window.lineEdit_search.textChanged.connect(
lambda: self.drawPresetList(
- self.window.comboBox_filter.currentText(), self.window.lineEdit_search.text()
+ self.window.comboBox_filter.currentText(),
+ self.window.lineEdit_search.text()
)
)
self.drawPresetList('*')
@@ -72,16 +76,14 @@ class PresetManager(QtWidgets.QDialog):
parseList.append((compName, int(compVers), preset))
except ValueError:
continue
- self.presets =\
- {
- compName : \
- [
- (vers, preset) \
- for name, vers, preset in parseList \
- if name == compName \
- ] \
- for compName, _, __ in parseList \
- }
+ self.presets = {
+ compName: [
+ (vers, preset)
+ for name, vers, preset in parseList
+ if name == compName
+ ]
+ for compName, _, __ in parseList
+ }
def drawPresetList(self, compFilter=None, presetFilter=''):
self.window.listWidget_presets.clear()
@@ -96,7 +98,8 @@ class PresetManager(QtWidgets.QDialog):
continue
for vers, preset in presets:
if not presetFilter or presetFilter in preset:
- self.window.listWidget_presets.addItem('%s: %s' % (component, preset))
+ self.window.listWidget_presets.addItem(
+ '%s: %s' % (component, preset))
self.presetRows.append((component, vers, preset))
if preset not in presetNames:
presetNames.append(preset)
@@ -124,11 +127,11 @@ class PresetManager(QtWidgets.QDialog):
while True:
index = componentList.currentRow()
currentPreset = selectedComponents[index].currentPreset
- newName, OK = QtGui.QInputDialog.getText(
+ newName, OK = QtWidgets.QInputDialog.getText(
self.parent.window,
'Audio Visualizer',
'New Preset Name:',
- QtGui.QLineEdit.Normal,
+ QtWidgets.QLineEdit.Normal,
currentPreset
)
if OK:
@@ -149,7 +152,7 @@ class PresetManager(QtWidgets.QDialog):
break
def createNewPreset(
- self, compName, vers, filename, saveValueStore, **kwargs):
+ self, compName, vers, filename, saveValueStore, **kwargs):
path = os.path.join(self.presetDir, compName, str(vers), filename)
if self.presetExists(path, **kwargs):
return
@@ -163,7 +166,7 @@ class PresetManager(QtWidgets.QDialog):
msg="%s already exists! Overwrite it?" %
os.path.basename(path),
showCancel=True,
- icon=QtGui.QMessageBox.Warning,
+ icon=QtWidgets.QMessageBox.Warning,
parent=window)
if not ch:
# user clicked cancel
@@ -196,7 +199,7 @@ class PresetManager(QtWidgets.QDialog):
ch = self.parent.showMessage(
msg='Really delete %s?' % name,
showCancel=True,
- icon=QtGui.QMessageBox.Warning,
+ icon=QtWidgets.QMessageBox.Warning,
parent=self.window
)
if not ch:
@@ -223,11 +226,11 @@ class PresetManager(QtWidgets.QDialog):
while True:
index = presetList.currentRow()
- newName, OK = QtGui.QInputDialog.getText(
+ newName, OK = QtWidgets.QInputDialog.getText(
self.window,
'Preset Manager',
'Rename Preset:',
- QtGui.QLineEdit.Normal,
+ QtWidgets.QLineEdit.Normal,
self.presetRows[index][2]
)
if OK:
@@ -250,7 +253,7 @@ class PresetManager(QtWidgets.QDialog):
break
def openImportDialog(self):
- filename = QtGui.QFileDialog.getOpenFileName(
+ filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.window, "Import Preset File",
self.settings.value("presetDir"),
"Preset Files (*.avl)")
@@ -275,7 +278,7 @@ class PresetManager(QtWidgets.QDialog):
def openExportDialog(self):
if not self.window.listWidget_presets.selectedItems():
return
- filename = QtGui.QFileDialog.getSaveFileName(
+ filename, _ = QtWidgets.QFileDialog.getSaveFileName(
self.window, "Export Preset",
self.settings.value("presetDir"),
"Preset Files (*.avl)")
diff --git a/src/video_thread.py b/src/video_thread.py
index 5ea6d21..b45381c 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -121,15 +121,12 @@ class Worker(QtCore.QObject):
vencoders = options['video-codecs'][vcodec]
aencoders = options['audio-codecs'][acodec]
- #print(encoders)
for encoder in vencoders:
- #print(encoder)
if encoder in encoders:
vencoder = encoder
break
for encoder in aencoders:
- #print(encoder)
if encoder in encoders:
aencoder = encoder
break
@@ -167,10 +164,10 @@ class Worker(QtCore.QObject):
numpy.seterr(divide='ignore')
# Call preFrameRender on all components
- print('Loaded Components:', ", ".join(
- ["%s) %s" % (num, str(component)) \
- for num, component in enumerate(reversed(self.components))
- ]))
+ print('Loaded Components:', ", ".join([
+ "%s) %s" % (num, str(component))
+ for num, component in enumerate(reversed(self.components))
+ ]))
self.staticComponents = {}
numComps = len(self.components)
for compNo, comp in enumerate(self.components):
--
cgit v1.2.3
From a2838a0c3898f999e71f76e6e8d5691155438aea Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 25 Jun 2017 10:36:32 -0400
Subject: disable some hotkeys while encoding, more friendly error messages
---
src/components/video.py | 28 +++++++-----
src/main.py | 9 ++++
src/mainwindow.py | 111 +++++++++++++++++++++++++++++++-----------------
src/preview_thread.py | 9 ++--
src/video_thread.py | 9 ++--
5 files changed, 107 insertions(+), 59 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/components/video.py b/src/components/video.py
index 3e87d2e..44f88a5 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -86,12 +86,14 @@ class Video:
continue
except AttributeError as e:
self.parent.showMessage(
- msg='%s couldn\'t be loaded.' % os.path.basename(
+ msg='%s couldn\'t be loaded. '
+ 'This is a fatal error.' % os.path.basename(
self.videoPath
),
detail=str(e)
)
self.parent.stopVideo()
+ break
self.currentFrame = pipe.stdout.read(self.chunkSize)
if len(self.currentFrame) != 0:
@@ -258,20 +260,24 @@ def scale(scale, width, height, returntype=None):
def finalizeFrame(self, imageData, width, height):
- if self.distort:
- try:
+ try:
+ if self.distort:
image = Image.frombytes(
'RGBA',
(width, height),
imageData)
- except ValueError:
- print('#### ignored invalid data caused by distortion ####')
- image = self.blankFrame(width, height)
- else:
- image = Image.frombytes(
- 'RGBA',
- scale(self.scale, width, height, int),
- imageData)
+ else:
+ image = Image.frombytes(
+ 'RGBA',
+ scale(self.scale, width, height, int),
+ imageData)
+
+ except ValueError:
+ print(
+ '### BAD VIDEO SELECTED ###\n'
+ 'Video will not export with these settings'
+ )
+ return self.blankFrame(width, height)
if self.scale != 100 \
or self.xPosition != 0 or self.yPosition != 0:
diff --git a/src/main.py b/src/main.py
index a8dd562..5b54fc7 100644
--- a/src/main.py
+++ b/src/main.py
@@ -7,6 +7,15 @@ import preview_thread
import video_thread
+def disableWhenEncoding(func):
+ def decorator(*args):
+ if args[0].encoding:
+ return
+ else:
+ return func(*args)
+ return decorator
+
+
def LoadDefaultSettings(self):
self.resolutions = [
'1920x1080',
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 7fae4ea..76c2b62 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -12,7 +12,7 @@ import core
import preview_thread
import video_thread
from presetmanager import PresetManager
-from main import LoadDefaultSettings
+from main import LoadDefaultSettings, disableWhenEncoding
class PreviewWindow(QtWidgets.QLabel):
@@ -54,6 +54,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.pages = [] # widgets of component settings
self.lastAutosave = time.time()
+ self.encoding = False
# Create data directory, load/create settings
self.dataDir = self.core.dataDir
@@ -149,16 +150,18 @@ class MainWindow(QtWidgets.QMainWindow):
for i, comp in enumerate(self.core.modules):
action = self.compMenu.addAction(comp.Component.__doc__)
action.triggered.connect(
- lambda _, item=i: self.core.insertComponent(0, item, self))
+ lambda _, item=i: self.core.insertComponent(0, item, self)
+ )
self.window.pushButton_addComponent.setMenu(self.compMenu)
componentList.dropEvent = self.dragComponent
componentList.itemSelectionChanged.connect(
- self.changeComponentWidget)
-
+ self.changeComponentWidget
+ )
self.window.pushButton_removeComponent.clicked.connect(
- lambda _: self.removeComponent())
+ lambda: self.removeComponent()
+ )
componentList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
componentList.customContextMenuRequested.connect(
@@ -173,7 +176,8 @@ class MainWindow(QtWidgets.QMainWindow):
currentRes = i
window.comboBox_resolution.setCurrentIndex(currentRes)
window.comboBox_resolution.currentIndexChanged.connect(
- self.updateResolution)
+ self.updateResolution
+ )
self.window.pushButton_listMoveUp.clicked.connect(
lambda: self.moveComponent(-1)
@@ -185,14 +189,17 @@ class MainWindow(QtWidgets.QMainWindow):
# Configure the Projects Menu
self.projectMenu = QMenu()
self.window.menuButton_newProject = self.projectMenu.addAction(
- "New Project")
+ "New Project"
+ )
self.window.menuButton_newProject.triggered.connect(
- self.createNewProject)
-
+ lambda: self.createNewProject()
+ )
self.window.menuButton_openProject = self.projectMenu.addAction(
- "Open Project")
+ "Open Project"
+ )
self.window.menuButton_openProject.triggered.connect(
- self.openOpenProjectDialog)
+ lambda: self.openOpenProjectDialog()
+ )
action = self.projectMenu.addAction("Save Project")
action.triggered.connect(self.saveCurrentProject)
@@ -207,6 +214,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.openPresetManager
)
+ self.updateWindowTitle()
window.show()
if project and project != self.autosavePath:
@@ -282,12 +290,15 @@ class MainWindow(QtWidgets.QMainWindow):
def updateWindowTitle(self):
appName = 'Audio Visualizer'
- if self.currentProject:
- appName += ' - %s' % \
- os.path.splitext(
- os.path.basename(self.currentProject))[0]
- if self.autosaveExists(identical=False):
- appName += '*'
+ try:
+ if self.currentProject:
+ appName += ' - %s' % \
+ os.path.splitext(
+ os.path.basename(self.currentProject))[0]
+ if self.autosaveExists(identical=False):
+ appName += '*'
+ except AttributeError:
+ pass
self.window.setWindowTitle(appName)
@QtCore.pyqtSlot(int, dict)
@@ -347,7 +358,7 @@ class MainWindow(QtWidgets.QMainWindow):
if not self.currentProject:
if os.path.exists(self.autosavePath):
os.remove(self.autosavePath)
- elif force or time.time() - self.lastAutosave >= 2.0:
+ elif force or time.time() - self.lastAutosave >= 0.1:
self.core.createProjectFile(self.autosavePath)
self.lastAutosave = time.time()
@@ -393,7 +404,7 @@ class MainWindow(QtWidgets.QMainWindow):
"Video Files (%s);; All Files (*)" % " ".join(
self.core.videoFormats))
- if not fileName == "":
+ if fileName:
self.settings.setValue("outputDir", os.path.dirname(fileName))
self.window.lineEdit_outputFile.setText(fileName)
@@ -404,33 +415,50 @@ class MainWindow(QtWidgets.QMainWindow):
def createAudioVisualisation(self):
# create output video if mandatory settings are filled in
- if self.window.lineEdit_audioFile.text() and \
- self.window.lineEdit_outputFile.text():
- self.canceled = False
- self.progressBarUpdated(-1)
- self.videoThread = QtCore.QThread(self)
- self.videoWorker = video_thread.Worker(self)
- self.videoWorker.moveToThread(self.videoThread)
- self.videoWorker.videoCreated.connect(self.videoCreated)
- self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated)
- self.videoWorker.progressBarSetText.connect(
- self.progressBarSetText)
- self.videoWorker.imageCreated.connect(self.showPreviewImage)
- self.videoWorker.encoding.connect(self.changeEncodingStatus)
- self.videoThread.start()
- outputPath = self.window.lineEdit_outputFile.text()
+ audioFile = self.window.lineEdit_audioFile.text()
+ outputPath = self.window.lineEdit_outputFile.text()
+
+ if audioFile and outputPath and self.core.selectedComponents:
if not os.path.dirname(outputPath):
outputPath = os.path.join(
os.path.expanduser("~"), outputPath)
- self.videoTask.emit(
- self.window.lineEdit_audioFile.text(),
- outputPath,
- self.core.selectedComponents)
+ if outputPath and os.path.isdir(outputPath):
+ self.showMessage(
+ msg='Chosen filename matches a directory, which '
+ 'cannot be overwritten. Please choose a different '
+ 'filename or move the directory.'
+ )
+ return
else:
- self.showMessage(
- msg="You must select an audio file and output filename.")
+ if not audioFile or not outputPath:
+ self.showMessage(
+ msg="You must select an audio file and output filename."
+ )
+ elif not self.core.selectedComponents:
+ self.showMessage(
+ msg="Not enough components."
+ )
+ return
+
+ self.canceled = False
+ self.progressBarUpdated(-1)
+ self.videoThread = QtCore.QThread(self)
+ self.videoWorker = video_thread.Worker(self)
+ self.videoWorker.moveToThread(self.videoThread)
+ self.videoWorker.videoCreated.connect(self.videoCreated)
+ self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated)
+ self.videoWorker.progressBarSetText.connect(
+ self.progressBarSetText)
+ self.videoWorker.imageCreated.connect(self.showPreviewImage)
+ self.videoWorker.encoding.connect(self.changeEncodingStatus)
+ self.videoThread.start()
+ self.videoTask.emit(
+ audioFile,
+ outputPath,
+ self.core.selectedComponents)
def changeEncodingStatus(self, status):
+ self.encoding = status
if status:
self.window.pushButton_createVideo.setEnabled(False)
self.window.pushButton_Cancel.setEnabled(True)
@@ -598,6 +626,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.window.stackedWidget.removeWidget(widget)
self.pages = []
+ @disableWhenEncoding
def createNewProject(self):
self.openSaveChangesDialog('starting a new project')
@@ -644,6 +673,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.core.createProjectFile(filename)
self.updateWindowTitle()
+ @disableWhenEncoding
def openOpenProjectDialog(self):
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.window, "Open Project File",
@@ -669,6 +699,7 @@ class MainWindow(QtWidgets.QMainWindow):
if self.window.listWidget_componentList.count() == 0:
self.drawPreview()
self.autosave(True)
+ self.updateWindowTitle()
def showMessage(self, **kwargs):
parent = kwargs['parent'] if 'parent' in kwargs else self.window
diff --git a/src/preview_thread.py b/src/preview_thread.py
index ac5751d..769656b 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -50,16 +50,15 @@ class Worker(QtCore.QObject):
components = nextPreviewInformation["components"]
for component in reversed(components):
try:
- newFrame = component.previewRender(self)
frame = Image.alpha_composite(
- frame, newFrame)
- except ValueError:
+ frame, component.previewRender(self)
+ )
+ except ValueError as e:
self.parent.showMessage(
msg="Bad frame returned by %s's previewRender method. "
"This is a fatal error." %
str(component),
- detail="bad frame: w%s, h%s" % (
- newFrame.width, newFrame.height)
+ detail=str(e)
)
quit(1)
diff --git a/src/video_thread.py b/src/video_thread.py
index b45381c..9b0bf56 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -2,7 +2,6 @@ from PyQt5 import QtCore, QtGui, uic
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PIL import Image, ImageDraw, ImageFont
from PIL.ImageQt import ImageQt
-import core
import numpy
import subprocess as sp
import sys
@@ -13,6 +12,8 @@ import time
from copy import copy
import signal
+import core
+
class Worker(QtCore.QObject):
@@ -87,8 +88,10 @@ class Worker(QtCore.QObject):
self.encoding.emit(True)
self.components = components
self.outputFile = outputFile
- self.bgI = 0 # tracked video frame
+
self.reset()
+
+ self.bgI = 0 # tracked video frame
self.width = int(self.core.settings.value('outputWidth'))
self.height = int(self.core.settings.value('outputHeight'))
progressBarValue = 0
@@ -171,7 +174,7 @@ class Worker(QtCore.QObject):
self.staticComponents = {}
numComps = len(self.components)
for compNo, comp in enumerate(self.components):
- pStr = "Analyzing audio..."
+ pStr = "Starting components..."
self.progressBarSetText.emit(pStr)
properties = None
properties = comp.preFrameRender(
--
cgit v1.2.3
From ba0409829de62b745d6f87749572a416061a42b4 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Tue, 4 Jul 2017 19:52:52 -0400
Subject: moved functions into toolkit, fixed CMD appearing on Windows
---
src/command.py | 2 +-
src/components/video.py | 5 +--
src/core.py | 65 +++++++++++++-----------------------
src/core.pyc | Bin 0 -> 15050 bytes
src/main.py | 35 --------------------
src/mainwindow.py | 4 +--
src/presetmanager.py | 5 +--
src/toolkit.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++++
src/video_thread.py | 3 +-
9 files changed, 119 insertions(+), 85 deletions(-)
create mode 100644 src/core.pyc
create mode 100644 src/toolkit.py
(limited to 'src/video_thread.py')
diff --git a/src/command.py b/src/command.py
index 3eea1b6..ee0e48d 100644
--- a/src/command.py
+++ b/src/command.py
@@ -5,7 +5,7 @@ import sys
import core
import video_thread
-from main import LoadDefaultSettings
+from toolkit import LoadDefaultSettings
class Command(QtCore.QObject):
diff --git a/src/components/video.py b/src/components/video.py
index 175cf29..19a9106 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -8,6 +8,7 @@ from queue import PriorityQueue
from component import Component, BadComponentInit
from frame import BlankFrame
+from toolkit import openPipe
class Video:
@@ -72,7 +73,7 @@ class Video:
self.frameBuffer.task_done()
def fillBuffer(self):
- pipe = subprocess.Popen(
+ pipe = openPipe(
self.command, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, bufsize=10**8
)
@@ -217,7 +218,7 @@ class Component(Component):
'-ss', '90',
'-vframes', '1',
]
- pipe = subprocess.Popen(
+ pipe = openPipe(
command, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, bufsize=10**8
)
diff --git a/src/core.py b/src/core.py
index 3fa67db..9ea9666 100644
--- a/src/core.py
+++ b/src/core.py
@@ -1,21 +1,24 @@
+'''
+ Home to the Core class which tracks the program state
+'''
import sys
import os
from PyQt5 import QtCore, QtGui, uic
-from os.path import expanduser
import subprocess as sp
import numpy
-from PIL import Image
-from shutil import rmtree
-import time
-from collections import OrderedDict
import json
from importlib import import_module
from PyQt5.QtCore import QStandardPaths
-import string
+import toolkit
-class Core():
+class Core:
+ '''
+ MainWindow and Command module both use an instance of this class
+ to store the program state. This object tracks the components,
+ opens projects and presets, and stores settings/paths to data.
+ '''
def __init__(self):
self.dataDir = QStandardPaths.writableLocation(
QStandardPaths.AppConfigLocation
@@ -34,7 +37,7 @@ class Core():
)
self.loadEncoderOptions()
- self.videoFormats = Core.appendUppercase([
+ self.videoFormats = toolkit.appendUppercase([
'*.mp4',
'*.mov',
'*.mkv',
@@ -42,7 +45,7 @@ class Core():
'*.webm',
'*.flv',
])
- self.audioFormats = Core.appendUppercase([
+ self.audioFormats = toolkit.appendUppercase([
'*.mp3',
'*.wav',
'*.ogg',
@@ -50,7 +53,7 @@ class Core():
'*.flac',
'*.aac',
])
- self.imageFormats = Core.appendUppercase([
+ self.imageFormats = toolkit.appendUppercase([
'*.png',
'*.jpg',
'*.tif',
@@ -175,7 +178,7 @@ class Core():
return False
with open(filepath, 'r') as f:
for line in f:
- saveValueStore = Core.presetFromString(line.strip())
+ saveValueStore = toolkit.presetFromString(line.strip())
break
return saveValueStore
@@ -307,7 +310,7 @@ class Core():
lastCompVers = str(line)
i += 1
elif i == 2:
- lastCompPreset = Core.presetFromString(line)
+ lastCompPreset = toolkit.presetFromString(line)
data[section].append((
lastCompName,
lastCompVers,
@@ -357,7 +360,7 @@ class Core():
with open(internalPath, 'r') as f:
internalData = [line for line in f]
try:
- saveValueStore = Core.presetFromString(internalData[0].strip())
+ saveValueStore = toolkit.presetFromString(internalData[0].strip())
self.createPresetFile(
compName, vers,
origName, saveValueStore,
@@ -387,7 +390,7 @@ class Core():
f.write('[Components]\n')
f.write('%s\n' % compName)
f.write('%s\n' % str(vers))
- f.write(Core.presetToString(saveValueStore))
+ f.write(toolkit.presetToString(saveValueStore))
def createProjectFile(self, filepath, window=None):
'''Create a project file (.avp) using the current program state'''
@@ -411,7 +414,7 @@ class Core():
saveValueStore = comp.savePreset()
f.write('%s\n' % str(comp))
f.write('%s\n' % str(comp.version()))
- f.write('%s\n' % Core.presetToString(saveValueStore))
+ f.write('%s\n' % toolkit.presetToString(saveValueStore))
f.write('\n[Settings]\n')
for key in self.settings.allKeys():
@@ -450,7 +453,9 @@ class Core():
else:
try:
with open(os.devnull, "w") as f:
- sp.check_call(['ffmpeg', '-version'], stdout=f, stderr=f)
+ sp.check_call(
+ ['ffmpeg', '-version'], stdout=f, stderr=f
+ )
return "ffmpeg"
except:
return "avconv"
@@ -459,10 +464,9 @@ class Core():
command = [self.FFMPEG_BIN, '-i', filename]
try:
- fileInfo = sp.check_output(command, stderr=sp.STDOUT, shell=False)
+ fileInfo = toolkit.checkOutput(command, stderr=sp.STDOUT)
except sp.CalledProcessError as ex:
fileInfo = ex.output
- pass
info = fileInfo.decode("utf-8").split('\n')
for line in info:
@@ -480,7 +484,7 @@ class Core():
'-ar', '44100', # ouput will have 44100 Hz
'-ac', '1', # mono (set to '2' for stereo)
'-']
- in_pipe = sp.Popen(
+ in_pipe = toolkit.openPipe(
command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8)
completeAudioArray = numpy.empty(0, dtype="int16")
@@ -525,26 +529,3 @@ class Core():
def reset(self):
self.canceled = False
-
- @staticmethod
- def badName(name):
- '''Returns whether a name contains non-alphanumeric chars'''
- return any([letter in string.punctuation for letter in name])
-
- @staticmethod
- def presetToString(dictionary):
- '''Alphabetizes a dict into OrderedDict & returns string repr'''
- return repr(
- OrderedDict(sorted(dictionary.items(), key=lambda t: t[0]))
- )
-
- @staticmethod
- def presetFromString(string):
- '''Turns a string repr of OrderedDict into a regular dict'''
- return dict(eval(string))
-
- @staticmethod
- def appendUppercase(lst):
- for form, i in zip(lst, range(len(lst))):
- lst.append(form.upper())
- return lst
diff --git a/src/core.pyc b/src/core.pyc
new file mode 100644
index 0000000..ce68831
Binary files /dev/null and b/src/core.pyc differ
diff --git a/src/main.py b/src/main.py
index bae9adf..b0ece29 100644
--- a/src/main.py
+++ b/src/main.py
@@ -7,41 +7,6 @@ import preview_thread
import video_thread
-def disableWhenEncoding(func):
- def decorator(*args, **kwargs):
- if args[0].encoding:
- return
- else:
- return func(*args, **kwargs)
- return decorator
-
-
-def LoadDefaultSettings(self):
- self.resolutions = [
- '1920x1080',
- '1280x720',
- '854x480'
- ]
-
- default = {
- "outputWidth": 1280,
- "outputHeight": 720,
- "outputFrameRate": 30,
- "outputAudioCodec": "AAC",
- "outputAudioBitrate": "192",
- "outputVideoCodec": "H264",
- "outputVideoBitrate": "2500",
- "outputVideoFormat": "yuv420p",
- "outputPreset": "medium",
- "outputFormat": "mp4",
- "outputContainer": "MP4",
- "projectDir": os.path.join(self.dataDir, 'projects'),
- }
-
- for parm, value in default.items():
- if self.settings.value(parm) is None:
- self.settings.setValue(parm, value)
-
if __name__ == "__main__":
mode = 'gui'
if len(sys.argv) > 2:
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 5068108..e8a3221 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -1,6 +1,6 @@
-from queue import Queue
from PyQt5 import QtCore, QtGui, uic, QtWidgets
from PyQt5.QtWidgets import QMenu, QShortcut
+from queue import Queue
import sys
import os
import signal
@@ -11,7 +11,7 @@ import core
import preview_thread
import video_thread
from presetmanager import PresetManager
-from main import LoadDefaultSettings, disableWhenEncoding
+from toolkit import LoadDefaultSettings, disableWhenEncoding
class PreviewWindow(QtWidgets.QLabel):
diff --git a/src/presetmanager.py b/src/presetmanager.py
index 68679ec..805b93e 100644
--- a/src/presetmanager.py
+++ b/src/presetmanager.py
@@ -3,6 +3,7 @@ import string
import os
import core
+import toolkit
class PresetManager(QtWidgets.QDialog):
@@ -147,7 +148,7 @@ class PresetManager(QtWidgets.QDialog):
currentPreset
)
if OK:
- if core.Core.badName(newName):
+ if toolkit.badName(newName):
self.warnMessage(self.parent.window)
continue
if newName:
@@ -252,7 +253,7 @@ class PresetManager(QtWidgets.QDialog):
self.presetRows[index][2]
)
if OK:
- if core.Core.badName(newName):
+ if toolkit.badName(newName):
self.warnMessage()
continue
if newName:
diff --git a/src/toolkit.py b/src/toolkit.py
new file mode 100644
index 0000000..8dce645
--- /dev/null
+++ b/src/toolkit.py
@@ -0,0 +1,85 @@
+'''
+ Common functions
+'''
+import string
+import os
+import sys
+import subprocess
+from collections import OrderedDict
+
+
+def badName(name):
+ '''Returns whether a name contains non-alphanumeric chars'''
+ return any([letter in string.punctuation for letter in name])
+
+
+def presetToString(dictionary):
+ '''Alphabetizes a dict into OrderedDict & returns string repr'''
+ return repr(
+ OrderedDict(sorted(dictionary.items(), key=lambda t: t[0]))
+ )
+
+
+def presetFromString(string):
+ '''Turns a string repr of OrderedDict into a regular dict'''
+ return dict(eval(string))
+
+
+def appendUppercase(lst):
+ for form, i in zip(lst, range(len(lst))):
+ lst.append(form.upper())
+ return lst
+
+
+def checkOutput(commandList, **kwargs):
+ _subprocess(subprocess.check_output)
+
+
+def openPipe(commandList, **kwargs):
+ _subprocess(subprocess.Popen)
+
+
+def _subprocess(func, commandList, **kwargs):
+ if not sys.platform == 'win32':
+ # Stop CMD window from appearing on Windows
+ # http://code.activestate.com/recipes/409002/
+ startupinfo = subprocess.STARTUPINFO()
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+ kwargs['startupinfo'] = startupinfo
+ return func(commandList, shell=False, **kwargs)
+
+
+def disableWhenEncoding(func):
+ def decorator(*args, **kwargs):
+ if args[0].encoding:
+ return
+ else:
+ return func(*args, **kwargs)
+ return decorator
+
+
+def LoadDefaultSettings(self):
+ self.resolutions = [
+ '1920x1080',
+ '1280x720',
+ '854x480'
+ ]
+
+ default = {
+ "outputWidth": 1280,
+ "outputHeight": 720,
+ "outputFrameRate": 30,
+ "outputAudioCodec": "AAC",
+ "outputAudioBitrate": "192",
+ "outputVideoCodec": "H264",
+ "outputVideoBitrate": "2500",
+ "outputVideoFormat": "yuv420p",
+ "outputPreset": "medium",
+ "outputFormat": "mp4",
+ "outputContainer": "MP4",
+ "projectDir": os.path.join(self.dataDir, 'projects'),
+ }
+
+ for parm, value in default.items():
+ if self.settings.value(parm) is None:
+ self.settings.setValue(parm, value)
diff --git a/src/video_thread.py b/src/video_thread.py
index 9b0bf56..aed4d60 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -13,6 +13,7 @@ from copy import copy
import signal
import core
+from toolkit import openPipe
class Worker(QtCore.QObject):
@@ -191,7 +192,7 @@ class Worker(QtCore.QObject):
self.progressBarUpdate.emit(100)
# Create ffmpeg pipe and queues for frames
- self.out_pipe = sp.Popen(
+ self.out_pipe = openPipe(
ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout)
self.compositeQueue = Queue()
self.compositeQueue.maxsize = 20
--
cgit v1.2.3
From 9986b1c829caa12bcea120bb37ebb57ab5e0e874 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 6 Jul 2017 12:40:03 -0400
Subject: image options to mirror & saturate colours
and some friendly docstrings
---
freeze.py | 1 +
src/command.py | 5 ++++
src/components/image.py | 21 +++++++++++++++-
src/components/image.ui | 64 ++++++++++++++++++++++++++++++++++++++++++++++++-
src/core.py | 2 +-
src/mainwindow.py | 9 ++++++-
src/presetmanager.py | 4 ++++
src/preview_thread.py | 18 ++++++++++----
src/video_thread.py | 7 ++++++
9 files changed, 122 insertions(+), 9 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/freeze.py b/freeze.py
index a81f325..3266f45 100644
--- a/freeze.py
+++ b/freeze.py
@@ -33,6 +33,7 @@ buildOptions = dict(
"PIL.Image",
"PIL.ImageQt",
"PIL.ImageDraw",
+ "PIL.ImageEnhance",
],
include_files=deps,
)
diff --git a/src/command.py b/src/command.py
index ee0e48d..be194d8 100644
--- a/src/command.py
+++ b/src/command.py
@@ -1,3 +1,8 @@
+'''
+ When using commandline mode, this module's object handles interpreting
+ the arguments and giving them to Core, which tracks the main program state.
+ Then it immediately exports a video.
+'''
from PyQt5 import QtCore
import argparse
import os
diff --git a/src/components/image.py b/src/components/image.py
index 4ccfc80..c9da137 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -1,4 +1,4 @@
-from PIL import Image, ImageDraw
+from PIL import Image, ImageDraw, ImageEnhance
from PyQt5 import QtGui, QtCore, QtWidgets
import os
@@ -20,7 +20,9 @@ class Component(Component):
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)
@@ -31,9 +33,11 @@ class Component(Component):
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()
self.parent.drawPreview()
super().update()
@@ -56,33 +60,48 @@ class Component(Component):
frame = BlankFrame(width, height)
if self.imagePath and os.path.exists(self.imagePath):
image = Image.open(self.imagePath)
+
+ # Modify image's appearance
+ if self.color != 100:
+ image = ImageEnhance.Color(image).enhance(
+ float(self.color / 100)
+ )
+ if self.mirror:
+ image = image.transpose(Image.FLIP_LEFT_RIGHT)
if self.stretched and image.size != (width, height):
image = image.resize((width, height), Image.ANTIALIAS)
if self.scale != 100:
newHeight = int((image.height / 100) * self.scale)
newWidth = int((image.width / 100) * self.scale)
image = image.resize((newWidth, newHeight), Image.ANTIALIAS)
+
+ # Paste image at correct position
frame.paste(image, box=(self.xPosition, self.yPosition))
if self.rotate != 0:
frame = frame.rotate(self.rotate)
+
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 {
'preset': self.currentPreset,
'image': self.imagePath,
'scale': self.scale,
+ 'color': self.color,
'rotate': self.rotate,
'stretched': self.stretched,
+ 'mirror': self.mirror,
'x': self.xPosition,
'y': self.yPosition,
}
diff --git a/src/components/image.ui b/src/components/image.ui
index 33488f8..e549ed0 100644
--- a/src/components/image.ui
+++ b/src/components/image.ui
@@ -208,10 +208,17 @@
+ -
+
+
+ Mirror
+
+
+
-
- Rotation
+ Rotate
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
@@ -290,6 +297,61 @@
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Color
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ QAbstractSpinBox::UpDownArrows
+
+
+ %
+
+
+ 0
+
+
+ 999
+
+
+ 1
+
+
+ 100
+
+
+
+
+
-
diff --git a/src/core.py b/src/core.py
index 9ea9666..5623039 100644
--- a/src/core.py
+++ b/src/core.py
@@ -1,5 +1,5 @@
'''
- Home to the Core class which tracks the program state
+ Home to the Core class which tracks program state. Used by GUI & commandline
'''
import sys
import os
diff --git a/src/mainwindow.py b/src/mainwindow.py
index e8a3221..1c6bbc4 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -1,3 +1,9 @@
+'''
+ When using GUI mode, this module's object (the main window) takes
+ user input to construct a program state (stored in the Core object).
+ This shows a preview of the video being created and allows for saving
+ projects and exporting the video at a later time.
+'''
from PyQt5 import QtCore, QtGui, uic, QtWidgets
from PyQt5.QtWidgets import QMenu, QShortcut
from queue import Queue
@@ -79,6 +85,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.previewWorker = preview_thread.Worker(self, self.previewQueue)
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)
@@ -296,11 +303,11 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QShortcut("Ctrl+End", self.window, self.moveComponentBottom)
QtWidgets.QShortcut("Ctrl+r", self.window, self.removeComponent)
+ @QtCore.pyqtSlot()
def cleanUp(self):
self.timer.stop()
self.previewThread.quit()
self.previewThread.wait()
- self.autosave()
def updateWindowTitle(self):
appName = 'Audio Visualizer'
diff --git a/src/presetmanager.py b/src/presetmanager.py
index 805b93e..40aa73f 100644
--- a/src/presetmanager.py
+++ b/src/presetmanager.py
@@ -1,3 +1,7 @@
+'''
+ Preset manager object handles all interactions with presets, including
+ the context menu accessed from MainWindow.
+'''
from PyQt5 import QtCore, QtWidgets
import string
import os
diff --git a/src/preview_thread.py b/src/preview_thread.py
index e58f04e..afb5e50 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -1,3 +1,7 @@
+'''
+ Thread that runs to create QImages for MainWindow's preview label.
+ Processes a queue of component lists.
+'''
from PyQt5 import QtCore, QtGui, uic
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PIL import Image
@@ -11,6 +15,7 @@ from copy import copy
class Worker(QtCore.QObject):
imageCreated = pyqtSignal(['QImage'])
+ error = pyqtSignal()
def __init__(self, parent=None, queue=None):
QtCore.QObject.__init__(self)
@@ -59,12 +64,15 @@ class Worker(QtCore.QObject):
"This is a fatal error." %
str(component),
detail=str(e),
- icon='Warning'
+ icon='Warning',
+ parent=None # mainwindow is in a different thread
)
- quit(1)
-
- self._image = ImageQt(frame)
- self.imageCreated.emit(QtGui.QImage(self._image))
+ from frame import BlankFrame
+ self.imageCreated.emit(ImageQt(BlankFrame))
+ self.error.emit()
+ break
+ else:
+ self.imageCreated.emit(ImageQt(frame))
except Empty:
True
diff --git a/src/video_thread.py b/src/video_thread.py
index aed4d60..d35a37a 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -1,3 +1,10 @@
+'''
+ Thread created to export a video. It has a slot to begin export using
+ an input file, output path, and component list. During export multiple
+ threads are created to render the video as quickly as possible. Signals
+ are emitted to update MainWindow's progress bar, detail text, and preview.
+ Export can be cancelled with cancel() + reset()
+'''
from PyQt5 import QtCore, QtGui, uic
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PIL import Image, ImageDraw, ImageFont
--
cgit v1.2.3
From 94d4acc1f4f4abe4029e8f9c050932b67cae8cec Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 9 Jul 2017 01:10:06 -0400
Subject: more comments + warnings for outdated dependencies
---
MANIFEST | 2 --
README.md | 8 +++--
src/component.py | 55 +++++++++++++++++++--------------
src/components/color.py | 5 ++-
src/components/image.py | 2 +-
src/components/original.py | 3 +-
src/components/text.py | 2 +-
src/components/video.py | 2 +-
src/core.py | 8 ++---
src/frame.py | 15 ++++++---
src/mainwindow.py | 34 +++++++++++++++++++-
src/preview_thread.py | 12 +++++---
src/video_thread.py | 77 ++++++++++++++++++++++++++++------------------
13 files changed, 148 insertions(+), 77 deletions(-)
delete mode 100644 MANIFEST
(limited to 'src/video_thread.py')
diff --git a/MANIFEST b/MANIFEST
deleted file mode 100644
index a0c51f7..0000000
--- a/MANIFEST
+++ /dev/null
@@ -1,2 +0,0 @@
-# file GENERATED by distutils, do NOT edit
-freeze.py
diff --git a/README.md b/README.md
index b82f3b4..658a22d 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@ audio-visualizer-python
This is a little GUI tool which creates an audio visualization video from an input audio file. Different components can be added and layered to change the resulting video and add images, videos, gradients, text, etc. The component setup can be saved as a Project and exporting can be automated using commandline options.
-The program works on Linux (Ubuntu 16.04), Windows (Windows 7), and Mac OS X. If you encounter problems running it or have other bug reports or features that you wish to see implemented, please fork the project and send me a pull request and/or file an issue on this project.
+The program works on Linux, macOS, and Windows. If you encounter problems running it or have other bug reports or features that you wish to see implemented, please fork the project and send me a pull request and/or file an issue on this project.
I also need a good name that is not as generic as "audio-visualizer-python"!
@@ -11,6 +11,8 @@ Dependencies
------------
Python 3, PyQt5, pillow-simd, numpy, and ffmpeg 3.3
+**Note:** Pillow may be used as a drop-in replacement for Pillow-SIMD if problems are encountered installing. However this will result in much slower video export times.
+
Installation
------------
### Manual installation on Ubuntu 16.04
@@ -23,7 +25,7 @@ Installation
Download audio-visualizer-python from this repository and run it with `python3 main.py`.
### Manual installation on Windows
-* **Not Recommended.** [Compiling Pillow is difficult on Windows](http://pillow.readthedocs.io/en/3.1.x/installation.html#building-on-windows) and required for a manual installation.
+* **Warning:** [Compiling Pillow is difficult on Windows](http://pillow.readthedocs.io/en/3.1.x/installation.html#building-on-windows) and required for the best experience.
* Download and install Python 3.6 from [https://www.python.org/downloads/windows/](https://www.python.org/downloads/windows/)
* Add Python to your system PATH (it will ask during the installation process).
* Brave treacherous valley of getting prerequisites to [compile Pillow on Windows](https://www.pypkg.com/pypi/pillow-simd/f/winbuild/README.md). This is necessary because binary builds for Pillow-SIMD are not available.
@@ -34,7 +36,7 @@ Download audio-visualizer-python from this repository and run it with `python3 m
Download audio-visualizer-python from this repository and run it from the command line with `python main.py`.
-### Manual installation on macOS [Outdated]
+### Manual installation on macOS **[Outdated]**
* Install [Homebrew](http://brew.sh/)
* Use the following commands to install the needed dependencies:
diff --git a/src/component.py b/src/component.py
index 6637eac..648a6d6 100644
--- a/src/component.py
+++ b/src/component.py
@@ -6,8 +6,11 @@ import os
class Component(QtCore.QObject):
- ''' A class for components to inherit.'''
- # modified = QtCore.pyqtSignal(int, bool)
+ '''
+ A class for components to inherit. Read comments for documentation
+ on making a valid component. All subclasses must implement this signal:
+ modified = QtCore.pyqtSignal(int, bool)
+ '''
def __init__(self, moduleIndex, compPos, core):
super().__init__()
@@ -36,30 +39,32 @@ class Component(QtCore.QObject):
# read your widget values, then call super().update()
def loadPreset(self, presetDict, presetName):
- '''Subclasses take (presetDict, presetName=None) as args.
- Must use super().loadPreset(presetDict, presetName) first,
- then update self.page widgets using the preset dict.
+ '''
+ Subclasses take (presetDict, presetName=None) as args.
+ Must use super().loadPreset(presetDict, presetName) first,
+ then update self.page widgets using the preset dict.
'''
self.currentPreset = presetName \
if presetName is not None else presetDict['preset']
def preFrameRender(self, **kwargs):
- '''Triggered only before a video is exported (video_thread.py)
- self.worker = the video thread worker
- self.completeAudioArray = a list of audio samples
- self.sampleSize = number of audio samples per video frame
- self.progressBarUpdate = signal to set progress bar number
- self.progressBarSetText = signal to set progress bar text
- Use the latter two signals to update the MainProgram if needed
- for a long initialization procedure (i.e., for a visualizer)
+ ''' 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 var, value in kwargs.items():
exec('self.%s = value' % var)
def command(self, arg):
- '''Configure a component using argument from the commandline.
- Use super().command(arg) at the end of a subclass's method,
- if no arguments are found in that method first
+ '''
+ Configure a component using argument from the commandline.
+ Use super().command(arg) at the end of a subclass's method,
+ if no arguments are found in that method first
'''
if arg.startswith('preset='):
_, preset = arg.split('=', 1)
@@ -84,9 +89,10 @@ class Component(QtCore.QObject):
'''Print help text for this Component's commandline arguments'''
def pickColor(self):
- '''Use color picker to get color input from the user,
- and return this as an RGB string and QPushButton stylesheet.
- In a subclass apply stylesheet to any color selection widgets
+ '''
+ Use color picker to get color input from the user,
+ and return this as an RGB string and QPushButton stylesheet.
+ In a subclass apply stylesheet to any color selection widgets
'''
dialog = QtWidgets.QColorDialog()
dialog.setOption(QtWidgets.QColorDialog.ShowAlphaChannel, True)
@@ -101,7 +107,7 @@ class Component(QtCore.QObject):
return None, None
def RGBFromString(self, string):
- ''' Turns an RGB string like "255, 255, 255" into a tuple '''
+ '''Turns an RGB string like "255, 255, 255" into a tuple'''
try:
tup = tuple([int(i) for i in string.split(',')])
if len(tup) != 3:
@@ -135,13 +141,16 @@ class Component(QtCore.QObject):
def previewRender(self, previewWorker):
width = int(previewWorker.core.settings.value('outputWidth'))
height = int(previewWorker.core.settings.value('outputHeight'))
- image = Image.new("RGBA", (width, height), (0,0,0,0))
+ from frame import BlankFrame
+ image = BlankFrame(width, height)
return image
- def frameRender(self, moduleNo, frameNo):
+ def frameRender(self, layerNo, frameNo):
+ audioArrayIndex = frameNo * self.sampleSize
width = int(self.worker.core.settings.value('outputWidth'))
height = int(self.worker.core.settings.value('outputHeight'))
- image = Image.new("RGBA", (width, height), (0,0,0,0))
+ from frame import BlankFrame
+ image = BlankFrame(width, height)
return image
@classmethod
diff --git a/src/components/color.py b/src/components/color.py
index 4a10263..b87f3e9 100644
--- a/src/components/color.py
+++ b/src/components/color.py
@@ -104,6 +104,9 @@ class Component(Component):
self.page.checkBox_trans.setEnabled(True)
self.page.checkBox_stretch.setEnabled(True)
self.page.comboBox_spread.setEnabled(True)
+ if self.trans:
+ self.page.lineEdit_color2.setEnabled(False)
+ self.page.pushButton_color2.setEnabled(False)
self.page.fillWidget.setCurrentIndex(self.fillType)
self.parent.drawPreview()
@@ -118,7 +121,7 @@ class Component(Component):
super().preFrameRender(**kwargs)
return ['static']
- def frameRender(self, moduleNo, arrayNo, frameNo):
+ def frameRender(self, layerNo, frameNo):
width = int(self.worker.core.settings.value('outputWidth'))
height = int(self.worker.core.settings.value('outputHeight'))
return self.drawFrame(width, height)
diff --git a/src/components/image.py b/src/components/image.py
index c9da137..6edd893 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -51,7 +51,7 @@ class Component(Component):
super().preFrameRender(**kwargs)
return ['static']
- def frameRender(self, moduleNo, arrayNo, frameNo):
+ def frameRender(self, layerNo, frameNo):
width = int(self.worker.core.settings.value('outputWidth'))
height = int(self.worker.core.settings.value('outputHeight'))
return self.drawFrame(width, height)
diff --git a/src/components/original.py b/src/components/original.py
index 82cdc1d..638095d 100644
--- a/src/components/original.py
+++ b/src/components/original.py
@@ -107,7 +107,8 @@ class Component(Component):
self.progressBarSetText.emit(pStr)
self.progressBarUpdate.emit(int(progress))
- def frameRender(self, moduleNo, arrayNo, frameNo):
+ def frameRender(self, layerNo, frameNo):
+ arrayNo = frameNo * self.sampleSize
return self.drawBars(
self.width, self.height,
self.spectrumArray[arrayNo],
diff --git a/src/components/text.py b/src/components/text.py
index 97d7d07..2b1884f 100644
--- a/src/components/text.py
+++ b/src/components/text.py
@@ -123,7 +123,7 @@ class Component(Component):
super().preFrameRender(**kwargs)
return ['static']
- def frameRender(self, moduleNo, arrayNo, frameNo):
+ def frameRender(self, layerNo, frameNo):
width = int(self.worker.core.settings.value('outputWidth'))
height = int(self.worker.core.settings.value('outputHeight'))
return self.addText(width, height)
diff --git a/src/components/video.py b/src/components/video.py
index 19a9106..e6890e0 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -165,7 +165,7 @@ class Component(Component):
component=self, scale=self.scale
) if os.path.exists(self.videoPath) else None
- def frameRender(self, moduleNo, arrayNo, frameNo):
+ def frameRender(self, layerNo, frameNo):
if self.video:
return self.video.frame(frameNo)
else:
diff --git a/src/core.py b/src/core.py
index 5623039..9792e88 100644
--- a/src/core.py
+++ b/src/core.py
@@ -449,15 +449,15 @@ class Core:
else:
if sys.platform == "win32":
- return "ffmpeg.exe"
+ return "ffmpeg"
else:
try:
with open(os.devnull, "w") as f:
- sp.check_call(
- ['ffmpeg', '-version'], stdout=f, stderr=f
+ toolkit.checkOutput(
+ ['ffmpeg', '-version'], stderr=f
)
return "ffmpeg"
- except:
+ except sp.CalledProcessError:
return "avconv"
def readAudioFile(self, filename, parent):
diff --git a/src/frame.py b/src/frame.py
index 6d6d299..57d33b0 100644
--- a/src/frame.py
+++ b/src/frame.py
@@ -8,17 +8,17 @@ import sys
class FramePainter(QtGui.QPainter):
+ '''
+ A QPainter for a blank frame, which can be converted into a
+ Pillow image with finalize()
+ '''
def __init__(self, width, height):
image = BlankFrame(width, height)
self.image = ImageQt(image)
super().__init__(self.image)
def setPen(self, RgbTuple):
- if sys.byteorder == 'big':
- color = QtGui.QColor(*RgbTuple)
- else:
- color = QtGui.QColor(*RgbTuple[::-1])
- super().setPen(QtGui.QColor(color))
+ super().setPen(PaintColor(*RgbTuple))
def finalize(self):
self.end()
@@ -28,15 +28,20 @@ class FramePainter(QtGui.QPainter):
'RGBA', (self.image.width(), self.image.height()), imBytes
)
+
class PaintColor(QtGui.QColor):
+ '''Reverse the painter colour if the hardware stores RGB values backward'''
def __init__(self, r, g, b, a=255):
if sys.byteorder == 'big':
super().__init__(r, g, b, a)
else:
super().__init__(b, g, r, a)
+
def FloodFrame(width, height, RgbaTuple):
return Image.new("RGBA", (width, height), RgbaTuple)
+
def BlankFrame(width, height):
+ '''The base frame used by each component to start drawing'''
return FloodFrame(width, height, (0, 0, 0, 0))
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 1c6bbc4..165b5bd 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -6,6 +6,7 @@
'''
from PyQt5 import QtCore, QtGui, uic, QtWidgets
from PyQt5.QtWidgets import QMenu, QShortcut
+from PIL import Image
from queue import Queue
import sys
import os
@@ -17,7 +18,7 @@ import core
import preview_thread
import video_thread
from presetmanager import PresetManager
-from toolkit import LoadDefaultSettings, disableWhenEncoding
+from toolkit import LoadDefaultSettings, disableWhenEncoding, checkOutput
class PreviewWindow(QtWidgets.QLabel):
@@ -269,6 +270,37 @@ class MainWindow(QtWidgets.QMainWindow):
self.openProject(self.currentProject, prompt=False)
self.drawPreview(True)
+ # verify Pillow version
+ if not self.settings.value("pilMsgShown") \
+ and 'post' not in Image.PILLOW_VERSION:
+ self.showMessage(
+ msg="You are using the standard version of the "
+ "Python imaging library (Pillow %s). Upgrade "
+ "to the Pillow-SIMD fork to enable hardware accelerations "
+ "and export videos faster." % Image.PILLOW_VERSION
+ )
+ self.settings.setValue("pilMsgShown", True)
+
+ # verify Ffmpeg version
+ if not self.settings.value("ffmpegMsgShown"):
+ try:
+ with open(os.devnull, "w") as f:
+ ffmpegVers = checkOutput(
+ ['ffmpeg', '-version'], stderr=f
+ )
+ goodVersion = str(ffmpegVers).split()[2].startswith('3')
+ except:
+ goodVersion = False
+ else:
+ goodVersion = True
+
+ if not goodVersion:
+ self.showMessage(
+ msg="You're using an old version of Ffmpeg. "
+ "Some features may not work as expected."
+ )
+ self.settings.setValue("ffmpegMsgShown", True)
+
# Setup Hotkeys
QtWidgets.QShortcut("Ctrl+S", self.window, self.saveCurrentProject)
QtWidgets.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog)
diff --git a/src/preview_thread.py b/src/preview_thread.py
index afb5e50..95a26ec 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -9,7 +9,8 @@ from PIL.ImageQt import ImageQt
import core
from queue import Queue, Empty
import os
-from copy import copy
+
+from frame import FloodFrame
class Worker(QtCore.QObject):
@@ -22,11 +23,13 @@ class Worker(QtCore.QObject):
parent.newTask.connect(self.createPreviewImage)
parent.processTask.connect(self.process)
self.parent = parent
- self.core = core.Core()
+ self.core = self.parent.core
self.queue = queue
self.core.settings = parent.settings
self.stackedWidget = parent.window.stackedWidget
- self.background = Image.new("RGBA", (1920, 1080), (0, 0, 0, 0))
+
+ # create checkerboard background to represent transparency
+ self.background = FloodFrame(1920, 1080, (0, 0, 0, 0))
self.background.paste(Image.open(os.path.join(
self.core.wd, "background.png")))
@@ -49,7 +52,7 @@ class Worker(QtCore.QObject):
width = int(self.core.settings.value('outputWidth'))
height = int(self.core.settings.value('outputHeight'))
- frame = copy(self.background)
+ frame = self.background.copy()
frame = frame.resize((width, height))
components = nextPreviewInformation["components"]
@@ -58,6 +61,7 @@ class Worker(QtCore.QObject):
frame = Image.alpha_composite(
frame, component.previewRender(self)
)
+
except ValueError as e:
self.parent.showMessage(
msg="Bad frame returned by %s's previewRender method. "
diff --git a/src/video_thread.py b/src/video_thread.py
index d35a37a..e7f1ac7 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -3,7 +3,7 @@
an input file, output path, and component list. During export multiple
threads are created to render the video as quickly as possible. Signals
are emitted to update MainWindow's progress bar, detail text, and preview.
- Export can be cancelled with cancel() + reset()
+ Export can be cancelled with cancel()
'''
from PyQt5 import QtCore, QtGui, uic
from PyQt5.QtCore import pyqtSignal, pyqtSlot
@@ -16,11 +16,11 @@ import os
from queue import Queue, PriorityQueue
from threading import Thread, Event
import time
-from copy import copy
import signal
import core
from toolkit import openPipe
+from frame import FloodFrame
class Worker(QtCore.QObject):
@@ -44,49 +44,65 @@ class Worker(QtCore.QObject):
self.stopped = False
def renderNode(self):
+ '''
+ Grabs audio data indices at frames to export, from compositeQueue.
+ Sends it to the components' frameRender methods in layer order
+ to create subframes & composite them into the final frame.
+ The resulting frames are collected in the renderQueue
+ '''
while not self.stopped:
- i = self.compositeQueue.get()
+ audioI = self.compositeQueue.get()
+ bgI = int(audioI / self.sampleSize)
frame = None
for compNo, comp in reversed(list(enumerate(self.components))):
if compNo in self.staticComponents and \
self.staticComponents[compNo] is not None:
- if frame is None:
+ # static component
+ if frame is None: # bottom-most layer
frame = self.staticComponents[compNo]
else:
frame = Image.alpha_composite(
- frame, self.staticComponents[compNo])
+ frame, self.staticComponents[compNo]
+ )
else:
- if frame is None:
- frame = comp.frameRender(compNo, i[0], i[1])
+ # animated component
+ if frame is None: # bottom-most layer
+ frame = comp.frameRender(compNo, bgI)
else:
frame = Image.alpha_composite(
- frame, comp.frameRender(compNo, i[0], i[1]))
+ frame, comp.frameRender(compNo, bgI)
+ )
- self.renderQueue.put([i[0], frame])
+ self.renderQueue.put([audioI, frame])
self.compositeQueue.task_done()
def renderDispatch(self):
+ '''
+ Places audio data indices in the compositeQueue, to be used
+ by a renderNode later. All indices are multiples of self.sampleSize
+ sampleSize * frameNo = audioI, AKA audio data starting at frameNo
+ '''
print('Dispatching Frames for Compositing...')
- for i in range(0, len(self.completeAudioArray), self.sampleSize):
- self.compositeQueue.put([i, self.bgI])
- # increment tracked video frame for next iteration
- self.bgI += 1
+ for audioI in range(0, len(self.completeAudioArray), self.sampleSize):
+ self.compositeQueue.put(audioI)
def previewDispatch(self):
- background = Image.new("RGBA", (1920, 1080), (0, 0, 0, 0))
+ '''
+ Grabs frames from the previewQueue, adds them to the checkerboard
+ and emits a final QImage to the MainWindow for the live preview
+ '''
+ background = FloodFrame(1920, 1080, (0, 0, 0, 0))
background.paste(Image.open(os.path.join(
self.core.wd, "background.png")))
background = background.resize((self.width, self.height))
while not self.stopped:
- i = self.previewQueue.get()
- if time.time() - self.lastPreview >= 0.06 or i[0] == 0:
- image = copy(background)
- image = Image.alpha_composite(image, i[1])
- self._image = ImageQt(image)
- self.imageCreated.emit(QtGui.QImage(self._image))
+ audioI, frame = self.previewQueue.get()
+ if time.time() - self.lastPreview >= 0.06 or audioI == 0:
+ image = Image.alpha_composite(background.copy(), frame)
+ self.imageCreated.emit(ImageQt(image))
self.lastPreview = time.time()
self.previewQueue.task_done()
@@ -99,7 +115,6 @@ class Worker(QtCore.QObject):
self.reset()
- self.bgI = 0 # tracked video frame
self.width = int(self.core.settings.value('outputWidth'))
self.height = int(self.core.settings.value('outputHeight'))
progressBarValue = 0
@@ -194,8 +209,8 @@ class Worker(QtCore.QObject):
)
if properties and 'static' in properties:
- self.staticComponents[compNo] = copy(
- comp.frameRender(compNo, 0, 0))
+ self.staticComponents[compNo] = \
+ comp.frameRender(compNo, 0).copy()
self.progressBarUpdate.emit(100)
# Create ffmpeg pipe and queues for frames
@@ -231,9 +246,10 @@ class Worker(QtCore.QObject):
pStr = "Exporting video..."
self.progressBarSetText.emit(pStr)
if not self.canceled:
- for i in range(0, len(self.completeAudioArray), self.sampleSize):
+ for audioI in range(
+ 0, len(self.completeAudioArray), self.sampleSize):
while True:
- if i in frameBuffer or self.canceled:
+ if audioI in frameBuffer or self.canceled:
# if frame's in buffer, pipe it to ffmpeg
break
# else fetch the next frame & add to the buffer
@@ -244,15 +260,16 @@ class Worker(QtCore.QObject):
break
try:
- self.out_pipe.stdin.write(frameBuffer[i].tobytes())
- self.previewQueue.put([i, frameBuffer[i]])
- del frameBuffer[i]
+ self.out_pipe.stdin.write(frameBuffer[audioI].tobytes())
+ self.previewQueue.put([audioI, frameBuffer[audioI]])
+ del frameBuffer[audioI]
except:
break
# increase progress bar value
- if progressBarValue + 1 <= (i / len(self.completeAudioArray)) \
- * 100:
+ if progressBarValue + 1 <= (
+ audioI / len(self.completeAudioArray)
+ ) * 100:
progressBarValue = numpy.floor(
(i / len(self.completeAudioArray)) * 100)
self.progressBarUpdate.emit(progressBarValue)
--
cgit v1.2.3
From f6fbc8d2423ac5ae683a7613b53648db3e02e323 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 9 Jul 2017 14:31:19 -0400
Subject: a basic Sound component for mixing sounds
to be greatly expanded...
---
src/component.py | 20 ++++--
src/components/image.py | 3 +-
src/components/sound.py | 74 +++++++++++++++++++++
src/components/sound.ui | 122 ++++++++++++++++++++++++++++++++++
src/core.py | 5 +-
src/frame.py | 2 +-
src/mainwindow.py | 2 +
src/preview_thread.py | 9 ++-
src/video_thread.py | 169 ++++++++++++++++++++++++++++--------------------
9 files changed, 325 insertions(+), 81 deletions(-)
create mode 100644 src/components/sound.py
create mode 100644 src/components/sound.ui
(limited to 'src/video_thread.py')
diff --git a/src/component.py b/src/component.py
index 648a6d6..306072c 100644
--- a/src/component.py
+++ b/src/component.py
@@ -48,14 +48,18 @@ class Component(QtCore.QObject):
if presetName is not None else presetDict['preset']
def preFrameRender(self, **kwargs):
- ''' Triggered only before a video is exported (video_thread.py)
+ '''
+ Triggered only before a video is exported (video_thread.py)
self.worker = the video thread worker
self.completeAudioArray = a list of audio samples
self.sampleSize = number of audio samples per video frame
self.progressBarUpdate = signal to set progress bar number
self.progressBarSetText = signal to set progress bar text
- Use the latter two signals to update the MainWindow if needed
+ Use the latter two signals to update the MainWindow if needed
for a long initialization procedure (i.e., for a visualizer)
+
+ Return a list of properties to signify if your component is
+ non-animated ('static') or returns sound ('audio').
'''
for var, value in kwargs.items():
exec('self.%s = value' % var)
@@ -135,8 +139,8 @@ class Component(QtCore.QObject):
return page
def update(self):
- super().update()
self.parent.drawPreview()
+ super().update()
def previewRender(self, previewWorker):
width = int(previewWorker.core.settings.value('outputWidth'))
@@ -153,9 +157,17 @@ class Component(QtCore.QObject):
image = BlankFrame(width, height)
return image
+ def audio(self):
+ \'''
+ Return audio to mix into master as a string (path to audio file),
+ or an object that returns raw audio data [future feature].
+ \'''
+
@classmethod
def names(cls):
- # Alternative names for renaming a component between project files
+ \'''
+ Alternative names for renaming a component between project files.
+ \'''
return []
'''
diff --git a/src/components/image.py b/src/components/image.py
index 6edd893..55fa6dd 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -42,7 +42,6 @@ class Component(Component):
super().update()
def previewRender(self, previewWorker):
- self.imageFormats = previewWorker.core.imageFormats
width = int(previewWorker.core.settings.value('outputWidth'))
height = int(previewWorker.core.settings.value('outputHeight'))
return self.drawFrame(width, height)
@@ -110,7 +109,7 @@ class Component(Component):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Image", imgDir,
- "Image Files (%s)" % " ".join(self.imageFormats))
+ "Image Files (%s)" % " ".join(self.core.imageFormats))
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_image.setText(filename)
diff --git a/src/components/sound.py b/src/components/sound.py
new file mode 100644
index 0000000..d3589b3
--- /dev/null
+++ b/src/components/sound.py
@@ -0,0 +1,74 @@
+from PyQt5 import QtGui, QtCore, QtWidgets
+import os
+
+from component import Component
+from frame import BlankFrame
+
+
+class Component(Component):
+ '''Sound'''
+
+ modified = QtCore.pyqtSignal(int, dict)
+
+ def widget(self, parent):
+ self.parent = parent
+ self.settings = parent.settings
+ page = self.loadUi('sound.ui')
+
+ page.lineEdit_sound.textChanged.connect(self.update)
+ page.pushButton_sound.clicked.connect(self.pickSound)
+
+ self.page = page
+ return page
+
+ def update(self):
+ self.sound = self.page.lineEdit_sound.text()
+ super().update()
+
+ def previewRender(self, previewWorker):
+ width = int(previewWorker.core.settings.value('outputWidth'))
+ height = int(previewWorker.core.settings.value('outputHeight'))
+ return self.frameRender(self.compPos, 0)
+
+ def preFrameRender(self, **kwargs):
+ # super().preFrameRender(**kwargs)
+ return ['static', 'audio']
+
+ def audio(self):
+ return self.sound
+
+ def pickSound(self):
+ sndDir = self.settings.value("componentDir", os.path.expanduser("~"))
+ filename, _ = QtWidgets.QFileDialog.getOpenFileName(
+ self.page, "Choose Sound", sndDir,
+ "Audio Files (%s)" % " ".join(self.core.audioFormats))
+ if filename:
+ self.settings.setValue("componentDir", os.path.dirname(filename))
+ self.page.lineEdit_sound.setText(filename)
+ self.update()
+
+ def frameRender(self, layerNo, frameNo):
+ width = int(self.core.settings.value('outputWidth'))
+ height = int(self.core.settings.value('outputHeight'))
+ return BlankFrame(width, height)
+
+ def loadPreset(self, pr, presetName=None):
+ super().loadPreset(pr, presetName)
+ self.page.lineEdit_sound.setText(pr['sound'])
+
+ def savePreset(self):
+ return {
+ 'preset': self.currentPreset,
+ 'sound': self.sound,
+ }
+
+ def commandHelp(self):
+ print('Path to audio file:\n path=/filepath/to/sound.ogg')
+
+ def command(self, arg):
+ if not arg.startswith('preset=') and '=' in arg:
+ key, arg = arg.split('=', 1)
+ if key == 'path':
+ self.page.lineEdit_sound.setText(arg)
+ return
+ super().command(arg)
diff --git a/src/components/sound.ui b/src/components/sound.ui
new file mode 100644
index 0000000..5fc00c1
--- /dev/null
+++ b/src/components/sound.ui
@@ -0,0 +1,122 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 586
+ 197
+
+
+
+ Form
+
+
+
-
+
+
+ 4
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 31
+ 0
+
+
+
+ Audio File
+
+
+
+ -
+
+
+
+ 1
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 1
+ 0
+
+
+
+
+ 32
+ 32
+
+
+
+ ...
+
+
+
+ 32
+ 32
+
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
diff --git a/src/core.py b/src/core.py
index 9792e88..db430d1 100644
--- a/src/core.py
+++ b/src/core.py
@@ -485,7 +485,8 @@ class Core:
'-ac', '1', # mono (set to '2' for stereo)
'-']
in_pipe = toolkit.openPipe(
- command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8)
+ command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8
+ )
completeAudioArray = numpy.empty(0, dtype="int16")
@@ -495,7 +496,7 @@ class Core:
if self.canceled:
break
# read 2 seconds of audio
- progress = progress + 4
+ progress += 4
raw_audio = in_pipe.stdout.read(88200*4)
if len(raw_audio) == 0:
break
diff --git a/src/frame.py b/src/frame.py
index 57d33b0..c066cdb 100644
--- a/src/frame.py
+++ b/src/frame.py
@@ -14,7 +14,7 @@ class FramePainter(QtGui.QPainter):
'''
def __init__(self, width, height):
image = BlankFrame(width, height)
- self.image = ImageQt(image)
+ self.image = QtGui.QImage(ImageQt(image))
super().__init__(self.image)
def setPen(self, RgbTuple):
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 165b5bd..3cd45d6 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -557,9 +557,11 @@ class MainWindow(QtWidgets.QMainWindow):
self.window.progressLabel.setHidden(True)
self.drawPreview(True)
+ @QtCore.pyqtSlot(int)
def progressBarUpdated(self, value):
self.window.progressBar_createVideo.setValue(value)
+ @QtCore.pyqtSlot(str)
def progressBarSetText(self, value):
if sys.platform == 'darwin':
self.window.progressLabel.setText(value)
diff --git a/src/preview_thread.py b/src/preview_thread.py
index 95a26ec..a72845b 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -69,10 +69,13 @@ class Worker(QtCore.QObject):
str(component),
detail=str(e),
icon='Warning',
- parent=None # mainwindow is in a different thread
+ parent=None # MainWindow is in a different thread
+ )
+ self.imageCreated.emit(
+ QtGui.QImage(ImageQt(
+ FloodFrame(width, height, (0, 0, 0, 0))
+ ))
)
- from frame import BlankFrame
- self.imageCreated.emit(ImageQt(BlankFrame))
self.error.emit()
break
else:
diff --git a/src/video_thread.py b/src/video_thread.py
index e7f1ac7..bd94be3 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -19,7 +19,7 @@ import time
import signal
import core
-from toolkit import openPipe
+from toolkit import openPipe, checkOutput
from frame import FloodFrame
@@ -102,32 +102,71 @@ class Worker(QtCore.QObject):
audioI, frame = self.previewQueue.get()
if time.time() - self.lastPreview >= 0.06 or audioI == 0:
image = Image.alpha_composite(background.copy(), frame)
- self.imageCreated.emit(ImageQt(image))
+ self.imageCreated.emit(QtGui.QImage(ImageQt(image)))
self.lastPreview = time.time()
self.previewQueue.task_done()
@pyqtSlot(str, str, list)
def createVideo(self, inputFile, outputFile, components):
+ numpy.seterr(divide='ignore')
self.encoding.emit(True)
self.components = components
self.outputFile = outputFile
-
- self.reset()
-
+ self.extraAudio = []
self.width = int(self.core.settings.value('outputWidth'))
self.height = int(self.core.settings.value('outputHeight'))
+
+ self.compositeQueue = Queue()
+ self.compositeQueue.maxsize = 20
+ self.renderQueue = PriorityQueue()
+ self.renderQueue.maxsize = 20
+ self.previewQueue = PriorityQueue()
+
+ self.reset()
progressBarValue = 0
self.progressBarUpdate.emit(progressBarValue)
- self.progressBarSetText.emit('Loading audio file...')
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # READ AUDIO AND INITIALIZE COMPONENTS
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+
+ self.progressBarSetText.emit("Loading audio file...")
self.completeAudioArray = self.core.readAudioFile(inputFile, self)
- # test if user has libfdk_aac
- encoders = sp.check_output(
- self.core.FFMPEG_BIN + " -encoders -hide_banner",
- shell=True)
+ self.progressBarUpdate.emit(0)
+ self.progressBarSetText.emit("Starting components...")
+ print('Loaded Components:', ", ".join([
+ "%s) %s" % (num, str(component))
+ for num, component in enumerate(reversed(self.components))
+ ]))
+ self.staticComponents = {}
+ numComps = len(self.components)
+ for compNo, comp in enumerate(self.components):
+ properties = None
+ properties = comp.preFrameRender(
+ worker=self,
+ completeAudioArray=self.completeAudioArray,
+ sampleSize=self.sampleSize,
+ progressBarUpdate=self.progressBarUpdate,
+ progressBarSetText=self.progressBarSetText
+ )
+
+ if properties:
+ if 'static' in properties:
+ self.staticComponents[compNo] = \
+ comp.frameRender(compNo, 0).copy()
+ if 'audio' in properties:
+ self.extraAudio.append(comp.audio())
+
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # DEDUCE ENCODERS
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # test if user has libfdk_aac
+ encoders = checkOutput(
+ "%s -encoders -hide_banner" % self.core.FFMPEG_BIN, shell=True
+ )
encoders = encoders.decode("utf-8")
acodec = self.core.settings.value('outputAudioCodec')
@@ -157,72 +196,66 @@ class Worker(QtCore.QObject):
aencoder = encoder
break
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # CREATE PIPE TO FFMPEG
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+
ffmpegCommand = [
self.core.FFMPEG_BIN,
'-thread_queue_size', '512',
'-y', # overwrite the output file if it already exists.
+
+ # INPUT VIDEO
'-f', 'rawvideo',
'-vcodec', 'rawvideo',
'-s', str(self.width)+'x'+str(self.height), # size of one frame
'-pix_fmt', 'rgba',
-
- # frames per second
'-r', self.core.settings.value('outputFrameRate'),
- '-i', '-', # The input comes from a pipe
- '-an',
- '-i', inputFile,
+ '-i', '-', # the video input comes from a pipe
+ '-an', # the video input has no sound
+
+ # INPUT SOUND
+ '-i', inputFile
+ ]
+
+ if self.extraAudio:
+ for extraInputFile in self.extraAudio:
+ ffmpegCommand.extend([
+ '-i', extraInputFile
+ ])
+ ffmpegCommand.extend([
+ '-filter_complex',
+ 'amix=inputs=%s:duration=longest:dropout_transition=3' % str(
+ len(self.extraAudio) + 1
+ )
+ ])
+
+ ffmpegCommand.extend([
+ # OUTPUT
'-vcodec', vencoder,
- '-acodec', aencoder, # output audio codec
+ '-acodec', aencoder,
'-b:v', vbitrate,
'-b:a', abitrate,
'-pix_fmt', self.core.settings.value('outputVideoFormat'),
'-preset', self.core.settings.value('outputPreset'),
'-f', container
- ]
+ ])
+ print(ffmpegCommand)
if acodec == 'aac':
ffmpegCommand.append('-strict')
ffmpegCommand.append('-2')
ffmpegCommand.append(outputFile)
-
- # ### Now start creating video for output ###
- numpy.seterr(divide='ignore')
-
- # Call preFrameRender on all components
- print('Loaded Components:', ", ".join([
- "%s) %s" % (num, str(component))
- for num, component in enumerate(reversed(self.components))
- ]))
- self.staticComponents = {}
- numComps = len(self.components)
- for compNo, comp in enumerate(self.components):
- pStr = "Starting components..."
- self.progressBarSetText.emit(pStr)
- properties = None
- properties = comp.preFrameRender(
- worker=self,
- completeAudioArray=self.completeAudioArray,
- sampleSize=self.sampleSize,
- progressBarUpdate=self.progressBarUpdate,
- progressBarSetText=self.progressBarSetText
- )
-
- if properties and 'static' in properties:
- self.staticComponents[compNo] = \
- comp.frameRender(compNo, 0).copy()
- self.progressBarUpdate.emit(100)
-
- # Create ffmpeg pipe and queues for frames
self.out_pipe = openPipe(
- ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout)
- self.compositeQueue = Queue()
- self.compositeQueue.maxsize = 20
- self.renderQueue = PriorityQueue()
- self.renderQueue.maxsize = 20
- self.previewQueue = PriorityQueue()
+ ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
+ )
+
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # START CREATING THE VIDEO
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
- # Threads to render frames and send them back here for piping out
+ # Make three renderNodes in new threads to create the frames
self.renderThreads = []
for i in range(3):
self.renderThreads.append(
@@ -235,16 +268,17 @@ class Worker(QtCore.QObject):
self.dispatchThread.daemon = True
self.dispatchThread.start()
+ self.lastPreview = 0.0
self.previewDispatch = Thread(
target=self.previewDispatch, name="Render Dispatch Thread")
self.previewDispatch.daemon = True
self.previewDispatch.start()
+ # Begin piping into ffmpeg!
frameBuffer = {}
- self.lastPreview = 0.0
- self.progressBarUpdate.emit(0)
- pStr = "Exporting video..."
- self.progressBarSetText.emit(pStr)
+ progressBarValue = 0
+ self.progressBarUpdate.emit(progressBarValue)
+ self.progressBarSetText.emit("Exporting video...")
if not self.canceled:
for audioI in range(
0, len(self.completeAudioArray), self.sampleSize):
@@ -253,29 +287,26 @@ class Worker(QtCore.QObject):
# if frame's in buffer, pipe it to ffmpeg
break
# else fetch the next frame & add to the buffer
- data = self.renderQueue.get()
- frameBuffer[data[0]] = data[1]
+ audioI_, frame = self.renderQueue.get()
+ frameBuffer[audioI_] = frame
self.renderQueue.task_done()
if self.canceled:
break
try:
self.out_pipe.stdin.write(frameBuffer[audioI].tobytes())
- self.previewQueue.put([audioI, frameBuffer[audioI]])
- del frameBuffer[audioI]
+ self.previewQueue.put([audioI, frameBuffer.pop(audioI)])
except:
break
# increase progress bar value
- if progressBarValue + 1 <= (
- audioI / len(self.completeAudioArray)
- ) * 100:
- progressBarValue = numpy.floor(
- (i / len(self.completeAudioArray)) * 100)
+ completion = (audioI / len(self.completeAudioArray)) * 100
+ if progressBarValue + 1 <= completion:
+ progressBarValue = numpy.floor(completion)
self.progressBarUpdate.emit(progressBarValue)
- pStr = "Exporting video: " + str(int(progressBarValue)) \
- + "%"
- self.progressBarSetText.emit(pStr)
+ self.progressBarSetText.emit(
+ "Exporting video: %s%%" % str(int(progressBarValue))
+ )
numpy.seterr(all='print')
--
cgit v1.2.3
From 4c3920e6309b4e67e3d8d809dd0b5b6cd245fd0c Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 9 Jul 2017 21:27:29 -0400
Subject: separated creation of ffmpeg command
for future use to sllow editing the command before starting the export
---
src/component.py | 10 +++--
src/components/color.py | 3 +-
src/components/image.py | 3 +-
src/components/sound.py | 4 +-
src/components/text.py | 3 +-
src/core.py | 93 ++++++++++++++++++++++++++++++++++++++++
src/video_thread.py | 110 +++++-------------------------------------------
7 files changed, 116 insertions(+), 110 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/component.py b/src/component.py
index 306072c..7c2f753 100644
--- a/src/component.py
+++ b/src/component.py
@@ -27,6 +27,13 @@ class Component(QtCore.QObject):
# change this number to identify new versions of a component
return 1
+ def properties(self):
+ '''
+ Return a list of properties to signify if your component is
+ non-animated ('static') or returns sound ('audio').
+ '''
+ return []
+
def cancel(self):
# please stop any lengthy process in response to this variable
self.canceled = True
@@ -57,9 +64,6 @@ class Component(QtCore.QObject):
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)
-
- Return a list of properties to signify if your component is
- non-animated ('static') or returns sound ('audio').
'''
for var, value in kwargs.items():
exec('self.%s = value' % var)
diff --git a/src/components/color.py b/src/components/color.py
index b87f3e9..82b45b3 100644
--- a/src/components/color.py
+++ b/src/components/color.py
@@ -117,8 +117,7 @@ class Component(Component):
height = int(previewWorker.core.settings.value('outputHeight'))
return self.drawFrame(width, height)
- def preFrameRender(self, **kwargs):
- super().preFrameRender(**kwargs)
+ def properties(self):
return ['static']
def frameRender(self, layerNo, frameNo):
diff --git a/src/components/image.py b/src/components/image.py
index 55fa6dd..94dcb83 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -46,8 +46,7 @@ class Component(Component):
height = int(previewWorker.core.settings.value('outputHeight'))
return self.drawFrame(width, height)
- def preFrameRender(self, **kwargs):
- super().preFrameRender(**kwargs)
+ def properties(self):
return ['static']
def frameRender(self, layerNo, frameNo):
diff --git a/src/components/sound.py b/src/components/sound.py
index d3589b3..1f43c83 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -31,7 +31,9 @@ class Component(Component):
return self.frameRender(self.compPos, 0)
def preFrameRender(self, **kwargs):
- # super().preFrameRender(**kwargs)
+ pass
+
+ def properties(self):
return ['static', 'audio']
def audio(self):
diff --git a/src/components/text.py b/src/components/text.py
index 2b1884f..fb6a90e 100644
--- a/src/components/text.py
+++ b/src/components/text.py
@@ -119,8 +119,7 @@ class Component(Component):
height = int(previewWorker.core.settings.value('outputHeight'))
return self.addText(width, height)
- def preFrameRender(self, **kwargs):
- super().preFrameRender(**kwargs)
+ def properties(self):
return ['static']
def frameRender(self, layerNo, frameNo):
diff --git a/src/core.py b/src/core.py
index db430d1..3d64c3b 100644
--- a/src/core.py
+++ b/src/core.py
@@ -460,6 +460,99 @@ class Core:
except sp.CalledProcessError:
return "avconv"
+ def createFfmpegCommand(self, inputFile, outputFile):
+ '''
+ Constructs the major ffmpeg command used to export the video
+ '''
+
+ # Test if user has libfdk_aac
+ encoders = toolkit.checkOutput(
+ "%s -encoders -hide_banner" % self.FFMPEG_BIN, shell=True
+ )
+ encoders = encoders.decode("utf-8")
+
+ acodec = self.settings.value('outputAudioCodec')
+
+ options = self.encoder_options
+ containerName = self.settings.value('outputContainer')
+ vcodec = self.settings.value('outputVideoCodec')
+ vbitrate = str(self.settings.value('outputVideoBitrate'))+'k'
+ acodec = self.settings.value('outputAudioCodec')
+ abitrate = str(self.settings.value('outputAudioBitrate'))+'k'
+
+ for cont in options['containers']:
+ if cont['name'] == containerName:
+ container = cont['container']
+ break
+
+ vencoders = options['video-codecs'][vcodec]
+ aencoders = options['audio-codecs'][acodec]
+
+ for encoder in vencoders:
+ if encoder in encoders:
+ vencoder = encoder
+ break
+
+ for encoder in aencoders:
+ if encoder in encoders:
+ aencoder = encoder
+ break
+
+ ffmpegCommand = [
+ self.FFMPEG_BIN,
+ '-thread_queue_size', '512',
+ '-y', # overwrite the output file if it already exists.
+
+ # INPUT VIDEO
+ '-f', 'rawvideo',
+ '-vcodec', 'rawvideo',
+ '-s', '%sx%s' % (
+ self.settings.value('outputWidth'),
+ self.settings.value('outputHeight'),
+ ),
+ '-pix_fmt', 'rgba',
+ '-r', self.settings.value('outputFrameRate'),
+ '-i', '-', # the video input comes from a pipe
+ '-an', # the video input has no sound
+
+ # INPUT SOUND
+ '-i', inputFile
+ ]
+
+ extraAudio = [
+ comp.audio() for comp in self.selectedComponents
+ if 'audio' in comp.properties()
+ ]
+ if extraAudio:
+ for extraInputFile in extraAudio:
+ ffmpegCommand.extend([
+ '-i', extraInputFile
+ ])
+ ffmpegCommand.extend([
+ '-filter_complex',
+ 'amix=inputs=%s:duration=longest:dropout_transition=3' % str(
+ len(extraAudio) + 1
+ )
+ ])
+
+ ffmpegCommand.extend([
+ # OUTPUT
+ '-vcodec', vencoder,
+ '-acodec', aencoder,
+ '-b:v', vbitrate,
+ '-b:a', abitrate,
+ '-pix_fmt', self.settings.value('outputVideoFormat'),
+ '-preset', self.settings.value('outputPreset'),
+ '-f', container
+ ])
+
+ if acodec == 'aac':
+ ffmpegCommand.append('-strict')
+ ffmpegCommand.append('-2')
+
+ ffmpegCommand.append(outputFile)
+ return ffmpegCommand
+
def readAudioFile(self, filename, parent):
command = [self.FFMPEG_BIN, '-i', filename]
diff --git a/src/video_thread.py b/src/video_thread.py
index bd94be3..dde71da 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -33,8 +33,8 @@ class Worker(QtCore.QObject):
def __init__(self, parent=None):
QtCore.QObject.__init__(self)
- self.core = core.Core()
- self.core.settings = parent.settings
+ self.core = parent.core
+ self.settings = parent.core.settings
self.modules = parent.core.modules
self.parent = parent
parent.videoTask.connect(self.createVideo)
@@ -114,8 +114,8 @@ class Worker(QtCore.QObject):
self.components = components
self.outputFile = outputFile
self.extraAudio = []
- self.width = int(self.core.settings.value('outputWidth'))
- self.height = int(self.core.settings.value('outputHeight'))
+ self.width = int(self.settings.value('outputWidth'))
+ self.height = int(self.settings.value('outputHeight'))
self.compositeQueue = Queue()
self.compositeQueue.maxsize = 20
@@ -128,7 +128,7 @@ class Worker(QtCore.QObject):
self.progressBarUpdate.emit(progressBarValue)
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
- # READ AUDIO AND INITIALIZE COMPONENTS
+ # READ AUDIO, INITIALIZE COMPONENTS, OPEN A PIPE TO FFMPEG
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
self.progressBarSetText.emit("Loading audio file...")
@@ -143,8 +143,7 @@ class Worker(QtCore.QObject):
self.staticComponents = {}
numComps = len(self.components)
for compNo, comp in enumerate(self.components):
- properties = None
- properties = comp.preFrameRender(
+ comp.preFrameRender(
worker=self,
completeAudioArray=self.completeAudioArray,
sampleSize=self.sampleSize,
@@ -152,101 +151,12 @@ class Worker(QtCore.QObject):
progressBarSetText=self.progressBarSetText
)
- if properties:
- if 'static' in properties:
- self.staticComponents[compNo] = \
- comp.frameRender(compNo, 0).copy()
- if 'audio' in properties:
- self.extraAudio.append(comp.audio())
+ if 'static' in comp.properties():
+ self.staticComponents[compNo] = \
+ comp.frameRender(compNo, 0).copy()
- # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
- # DEDUCE ENCODERS
- # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
-
- # test if user has libfdk_aac
- encoders = checkOutput(
- "%s -encoders -hide_banner" % self.core.FFMPEG_BIN, shell=True
- )
- encoders = encoders.decode("utf-8")
-
- acodec = self.core.settings.value('outputAudioCodec')
-
- options = self.core.encoder_options
- containerName = self.core.settings.value('outputContainer')
- vcodec = self.core.settings.value('outputVideoCodec')
- vbitrate = str(self.core.settings.value('outputVideoBitrate'))+'k'
- acodec = self.core.settings.value('outputAudioCodec')
- abitrate = str(self.core.settings.value('outputAudioBitrate'))+'k'
-
- for cont in options['containers']:
- if cont['name'] == containerName:
- container = cont['container']
- break
-
- vencoders = options['video-codecs'][vcodec]
- aencoders = options['audio-codecs'][acodec]
-
- for encoder in vencoders:
- if encoder in encoders:
- vencoder = encoder
- break
-
- for encoder in aencoders:
- if encoder in encoders:
- aencoder = encoder
- break
-
- # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
- # CREATE PIPE TO FFMPEG
- # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
-
- ffmpegCommand = [
- self.core.FFMPEG_BIN,
- '-thread_queue_size', '512',
- '-y', # overwrite the output file if it already exists.
-
- # INPUT VIDEO
- '-f', 'rawvideo',
- '-vcodec', 'rawvideo',
- '-s', str(self.width)+'x'+str(self.height), # size of one frame
- '-pix_fmt', 'rgba',
- '-r', self.core.settings.value('outputFrameRate'),
- '-i', '-', # the video input comes from a pipe
- '-an', # the video input has no sound
-
- # INPUT SOUND
- '-i', inputFile
- ]
-
- if self.extraAudio:
- for extraInputFile in self.extraAudio:
- ffmpegCommand.extend([
- '-i', extraInputFile
- ])
- ffmpegCommand.extend([
- '-filter_complex',
- 'amix=inputs=%s:duration=longest:dropout_transition=3' % str(
- len(self.extraAudio) + 1
- )
- ])
-
- ffmpegCommand.extend([
- # OUTPUT
- '-vcodec', vencoder,
- '-acodec', aencoder,
- '-b:v', vbitrate,
- '-b:a', abitrate,
- '-pix_fmt', self.core.settings.value('outputVideoFormat'),
- '-preset', self.core.settings.value('outputPreset'),
- '-f', container
- ])
+ ffmpegCommand = self.core.createFfmpegCommand(inputFile, outputFile)
print(ffmpegCommand)
-
- if acodec == 'aac':
- ffmpegCommand.append('-strict')
- ffmpegCommand.append('-2')
-
- ffmpegCommand.append(outputFile)
self.out_pipe = openPipe(
ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
)
--
cgit v1.2.3
From 2e37dafd7036973a315b525f131850a6fb6d0b35 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Tue, 11 Jul 2017 06:06:22 -0400
Subject: fixed various bugs
---
src/component.py | 9 ++++++++-
src/components/image.py | 10 +++++++++-
src/components/sound.py | 8 ++++----
src/components/text.py | 10 +++++-----
src/components/video.py | 21 +++++++++++++++++++++
src/components/video.ui | 17 +++++++++--------
src/core.py | 4 ++--
src/mainwindow.py | 4 ++++
src/preview_thread.py | 26 ++++++++++++--------------
src/video_thread.py | 9 +++++++++
10 files changed, 83 insertions(+), 35 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/component.py b/src/component.py
index 7c2f753..eea82d7 100644
--- a/src/component.py
+++ b/src/component.py
@@ -30,10 +30,17 @@ class Component(QtCore.QObject):
def properties(self):
'''
Return a list of properties to signify if your component is
- non-animated ('static') or returns sound ('audio').
+ non-animated ('static'), returns sound ('audio'), or has
+ encountered an error in configuration ('error').
'''
return []
+ def error(self):
+ '''
+ Return a string containing an error message, or None for a default.
+ '''
+ return
+
def cancel(self):
# please stop any lengthy process in response to this variable
self.canceled = True
diff --git a/src/components/image.py b/src/components/image.py
index 94dcb83..07abc3f 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -47,7 +47,15 @@ class Component(Component):
return self.drawFrame(width, height)
def properties(self):
- return ['static']
+ props = ['static']
+ if not os.path.exists(self.imagePath):
+ props.append('error')
+ return props
+
+ def error(self):
+ if not os.path.exists(self.imagePath):
+ return "The image path selected on " \
+ "layer %s no longer exists!" % str(self.compPos)
def frameRender(self, layerNo, frameNo):
width = int(self.worker.core.settings.value('outputWidth'))
diff --git a/src/components/sound.py b/src/components/sound.py
index 1f43c83..9c114a8 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -28,7 +28,7 @@ class Component(Component):
def previewRender(self, previewWorker):
width = int(previewWorker.core.settings.value('outputWidth'))
height = int(previewWorker.core.settings.value('outputHeight'))
- return self.frameRender(self.compPos, 0)
+ return BlankFrame(width, height)
def preFrameRender(self, **kwargs):
pass
@@ -37,7 +37,7 @@ class Component(Component):
return ['static', 'audio']
def audio(self):
- return self.sound
+ return (self.sound, {})
def pickSound(self):
sndDir = self.settings.value("componentDir", os.path.expanduser("~"))
@@ -50,8 +50,8 @@ class Component(Component):
self.update()
def frameRender(self, layerNo, frameNo):
- width = int(self.core.settings.value('outputWidth'))
- height = int(self.core.settings.value('outputHeight'))
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
return BlankFrame(width, height)
def loadPreset(self, pr, presetName=None):
diff --git a/src/components/text.py b/src/components/text.py
index fb6a90e..ed50064 100644
--- a/src/components/text.py
+++ b/src/components/text.py
@@ -75,15 +75,15 @@ class Component(Component):
'''Returns true x, y after considering alignment settings'''
fm = QtGui.QFontMetrics(self.titleFont)
if self.alignment == 0: # Left
- x = self.xPosition
+ x = int(self.xPosition)
if self.alignment == 1: # Middle
offset = fm.width(self.title)/2
- x = self.xPosition - offset
+ x = int(self.xPosition - offset)
if self.alignment == 2: # Right
offset = fm.width(self.title)
- x = self.xPosition - offset
+ x = int(self.xPosition - offset)
return x, self.yPosition
def loadPreset(self, pr, presetName=None):
@@ -128,12 +128,12 @@ class Component(Component):
return self.addText(width, height)
def addText(self, width, height):
- x, y = self.getXY()
- image = FramePainter(width, height)
+ image = FramePainter(width, height)
self.titleFont.setPixelSize(self.fontSize)
image.setFont(self.titleFont)
image.setPen(self.textColor)
+ x, y = self.getXY()
image.drawText(x, y, self.title)
return image.finalize()
diff --git a/src/components/video.py b/src/components/video.py
index e6890e0..5303e3a 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -123,6 +123,7 @@ class Component(Component):
page.pushButton_video.clicked.connect(self.pickVideo)
page.checkBox_loop.stateChanged.connect(self.update)
page.checkBox_distort.stateChanged.connect(self.update)
+ page.checkBox_useAudio.stateChanged.connect(self.update)
page.spinBox_scale.valueChanged.connect(self.update)
page.spinBox_x.valueChanged.connect(self.update)
page.spinBox_y.valueChanged.connect(self.update)
@@ -133,6 +134,7 @@ class Component(Component):
def update(self):
self.videoPath = self.page.lineEdit_video.text()
self.loopVideo = self.page.checkBox_loop.isChecked()
+ self.useAudio = self.page.checkBox_useAudio.isChecked()
self.distort = self.page.checkBox_distort.isChecked()
self.scale = self.page.spinBox_scale.value()
self.xPosition = self.page.spinBox_x.value()
@@ -151,6 +153,23 @@ class Component(Component):
else:
return frame
+ def properties(self):
+ props = []
+ if self.useAudio:
+ # props.append('audio')
+ pass
+ if not os.path.exists(self.videoPath):
+ props.append('error')
+ return props
+
+ def error(self):
+ if not os.path.exists(self.videoPath):
+ return "The video path selected on " \
+ "layer %s no longer exists!" % str(self.compPos)
+
+ def audio(self):
+ return (self.videoPath, {})
+
def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs)
width = int(self.worker.core.settings.value('outputWidth'))
@@ -175,6 +194,7 @@ class Component(Component):
super().loadPreset(pr, presetName)
self.page.lineEdit_video.setText(pr['video'])
self.page.checkBox_loop.setChecked(pr['loop'])
+ self.page.checkBox_useAudio.setChecked(pr['useAudio'])
self.page.checkBox_distort.setChecked(pr['distort'])
self.page.spinBox_scale.setValue(pr['scale'])
self.page.spinBox_x.setValue(pr['x'])
@@ -185,6 +205,7 @@ class Component(Component):
'preset': self.currentPreset,
'video': self.videoPath,
'loop': self.loopVideo,
+ 'useAudio': self.useAudio,
'distort': self.distort,
'scale': self.scale,
'x': self.xPosition,
diff --git a/src/components/video.ui b/src/components/video.ui
index f05e8a5..97b7d6f 100644
--- a/src/components/video.ui
+++ b/src/components/video.ui
@@ -190,16 +190,20 @@
-
-
+
+
+ Use Audio
+
+
+
+ -
+
Qt::Horizontal
-
- QSizePolicy::Fixed
-
- 5
+ 40
20
@@ -256,9 +260,6 @@
- -
-
-
diff --git a/src/core.py b/src/core.py
index 3d64c3b..450e43b 100644
--- a/src/core.py
+++ b/src/core.py
@@ -524,7 +524,7 @@ class Core:
if 'audio' in comp.properties()
]
if extraAudio:
- for extraInputFile in extraAudio:
+ for extraInputFile, params in extraAudio:
ffmpegCommand.extend([
'-i', extraInputFile
])
@@ -532,7 +532,7 @@ class Core:
'-filter_complex',
'amix=inputs=%s:duration=longest:dropout_transition=3' % str(
len(extraAudio) + 1
- )
+ ),
])
ffmpegCommand.extend([
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 3cd45d6..d21ba0a 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -713,6 +713,10 @@ class MainWindow(QtWidgets.QMainWindow):
def saveCurrentProject(self):
if self.currentProject:
self.core.createProjectFile(self.currentProject, self.window)
+ try:
+ os.remove(self.autosavePath)
+ except FileNotFoundError:
+ pass
self.updateWindowTitle()
else:
self.openSaveProjectDialog()
diff --git a/src/preview_thread.py b/src/preview_thread.py
index a72845b..fb3b792 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -25,8 +25,8 @@ class Worker(QtCore.QObject):
self.parent = parent
self.core = self.parent.core
self.queue = queue
- self.core.settings = parent.settings
- self.stackedWidget = parent.window.stackedWidget
+ self.width = int(self.core.settings.value('outputWidth'))
+ self.height = int(self.core.settings.value('outputHeight'))
# create checkerboard background to represent transparency
self.background = FloodFrame(1920, 1080, (0, 0, 0, 0))
@@ -50,10 +50,10 @@ class Worker(QtCore.QObject):
except Empty:
continue
- width = int(self.core.settings.value('outputWidth'))
- height = int(self.core.settings.value('outputHeight'))
+ if self.background.width != self.width:
+ self.background = self.background.resize(
+ (self.width, self.height))
frame = self.background.copy()
- frame = frame.resize((width, height))
components = nextPreviewInformation["components"]
for component in reversed(components):
@@ -63,23 +63,21 @@ class Worker(QtCore.QObject):
)
except ValueError as e:
+ errMsg = "Bad frame returned by %s's preview renderer. " \
+ "%s. This is a fatal error." % (
+ str(component), str(e).capitalize()
+ )
+ print(errMsg)
self.parent.showMessage(
- msg="Bad frame returned by %s's previewRender method. "
- "This is a fatal error." %
- str(component),
+ msg=errMsg,
detail=str(e),
icon='Warning',
parent=None # MainWindow is in a different thread
)
- self.imageCreated.emit(
- QtGui.QImage(ImageQt(
- FloodFrame(width, height, (0, 0, 0, 0))
- ))
- )
self.error.emit()
break
else:
- self.imageCreated.emit(ImageQt(frame))
+ self.imageCreated.emit(QtGui.QImage(ImageQt(frame)))
except Empty:
True
diff --git a/src/video_thread.py b/src/video_thread.py
index dde71da..b00d512 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -151,6 +151,15 @@ class Worker(QtCore.QObject):
progressBarSetText=self.progressBarSetText
)
+ if 'error' in comp.properties():
+ self.canceled = True
+ errMsg = "Component #%s encountered an error!" % compNo \
+ if comp.error() is None else comp.error()
+ self.parent.showMessage(
+ msg=errMsg,
+ icon='Warning',
+ parent=None # MainWindow is in a different thread
+ )
if 'static' in comp.properties():
self.staticComponents[compNo] = \
comp.frameRender(compNo, 0).copy()
--
cgit v1.2.3
From 8811b699a9c2d6b78af1e2a332d3031aef73aec4 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 13 Jul 2017 00:05:11 -0400
Subject: merge consecutive static components
---
src/components/color.py | 13 +++++++------
src/components/image.py | 16 ++++++++--------
src/components/original.py | 9 +++++----
src/components/text.py | 21 +++++++++++----------
src/components/video.py | 6 +++---
src/core.py | 2 ++
src/frame.py | 21 ++++++++++++++++++++-
src/mainwindow.py | 2 ++
src/preview_thread.py | 38 +++++++++++++++++++++++---------------
src/video_thread.py | 27 +++++++++++++++++++--------
10 files changed, 100 insertions(+), 55 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/components/color.py b/src/components/color.py
index 82b45b3..da3bcf9 100644
--- a/src/components/color.py
+++ b/src/components/color.py
@@ -15,6 +15,7 @@ class Component(Component):
def widget(self, parent):
self.parent = parent
+ self.settings = self.parent.core.settings
page = self.loadUi('color.ui')
self.color1 = (0, 0, 0)
@@ -42,9 +43,9 @@ class Component(Component):
page.spinBox_x.valueChanged.connect(self.update)
page.spinBox_y.valueChanged.connect(self.update)
page.spinBox_width.setValue(
- int(parent.settings.value("outputWidth")))
+ int(self.settings.value("outputWidth")))
page.spinBox_height.setValue(
- int(parent.settings.value("outputHeight")))
+ int(self.settings.value("outputHeight")))
page.lineEdit_color1.textChanged.connect(self.update)
page.lineEdit_color2.textChanged.connect(self.update)
@@ -113,16 +114,16 @@ class Component(Component):
super().update()
def previewRender(self, previewWorker):
- width = int(previewWorker.core.settings.value('outputWidth'))
- height = int(previewWorker.core.settings.value('outputHeight'))
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
return self.drawFrame(width, height)
def properties(self):
return ['static']
def frameRender(self, layerNo, frameNo):
- width = int(self.worker.core.settings.value('outputWidth'))
- height = int(self.worker.core.settings.value('outputHeight'))
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
return self.drawFrame(width, height)
def drawFrame(self, width, height):
diff --git a/src/components/image.py b/src/components/image.py
index 07abc3f..6465bc9 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -13,7 +13,7 @@ class Component(Component):
def widget(self, parent):
self.parent = parent
- self.settings = parent.settings
+ self.settings = self.parent.core.settings
page = self.loadUi('image.ui')
page.lineEdit_image.textChanged.connect(self.update)
@@ -42,24 +42,24 @@ class Component(Component):
super().update()
def previewRender(self, previewWorker):
- width = int(previewWorker.core.settings.value('outputWidth'))
- height = int(previewWorker.core.settings.value('outputHeight'))
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
return self.drawFrame(width, height)
def properties(self):
props = ['static']
- if not os.path.exists(self.imagePath):
+ if self.imagePath and not os.path.exists(self.imagePath):
props.append('error')
return props
def error(self):
if not os.path.exists(self.imagePath):
- return "The image path selected on " \
- "layer %s no longer exists!" % str(self.compPos)
+ return "The image selected on " \
+ "layer %s does not exist!" % str(self.compPos)
def frameRender(self, layerNo, frameNo):
- width = int(self.worker.core.settings.value('outputWidth'))
- height = int(self.worker.core.settings.value('outputHeight'))
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
return self.drawFrame(width, height)
def drawFrame(self, width, height):
diff --git a/src/components/original.py b/src/components/original.py
index 638095d..3599c30 100644
--- a/src/components/original.py
+++ b/src/components/original.py
@@ -21,6 +21,7 @@ class Component(Component):
def widget(self, parent):
self.parent = parent
+ self.settings = self.parent.core.settings
self.visColor = (255, 255, 255)
self.scale = 20
self.y = 0
@@ -76,8 +77,8 @@ class Component(Component):
def previewRender(self, previewWorker):
spectrum = numpy.fromfunction(
lambda x: float(self.scale)/2500*(x-128)**2, (255,), dtype="int16")
- width = int(previewWorker.core.settings.value('outputWidth'))
- height = int(previewWorker.core.settings.value('outputHeight'))
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
return self.drawBars(
width, height, spectrum, self.visColor, self.layout
)
@@ -88,8 +89,8 @@ class Component(Component):
self.smoothConstantUp = 0.8
self.lastSpectrum = None
self.spectrumArray = {}
- self.width = int(self.worker.core.settings.value('outputWidth'))
- self.height = int(self.worker.core.settings.value('outputHeight'))
+ self.width = int(self.settings.value('outputWidth'))
+ self.height = int(self.settings.value('outputHeight'))
for i in range(0, len(self.completeAudioArray), self.sampleSize):
if self.canceled:
diff --git a/src/components/text.py b/src/components/text.py
index ed50064..4435b80 100644
--- a/src/components/text.py
+++ b/src/components/text.py
@@ -17,10 +17,11 @@ class Component(Component):
self.titleFont = QFont()
def widget(self, parent):
- height = int(parent.settings.value('outputHeight'))
- width = int(parent.settings.value('outputWidth'))
-
self.parent = parent
+ self.settings = self.parent.core.settings
+ height = int(self.settings.value('outputHeight'))
+ width = int(self.settings.value('outputWidth'))
+
self.textColor = (255, 255, 255)
self.title = 'Text'
self.alignment = 1
@@ -78,12 +79,12 @@ class Component(Component):
x = int(self.xPosition)
if self.alignment == 1: # Middle
- offset = fm.width(self.title)/2
- x = int(self.xPosition - offset)
+ offset = int(fm.width(self.title)/2)
+ x = self.xPosition - offset
if self.alignment == 2: # Right
offset = fm.width(self.title)
- x = int(self.xPosition - offset)
+ x = self.xPosition - offset
return x, self.yPosition
def loadPreset(self, pr, presetName=None):
@@ -115,16 +116,16 @@ class Component(Component):
}
def previewRender(self, previewWorker):
- width = int(previewWorker.core.settings.value('outputWidth'))
- height = int(previewWorker.core.settings.value('outputHeight'))
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
return self.addText(width, height)
def properties(self):
return ['static']
def frameRender(self, layerNo, frameNo):
- width = int(self.worker.core.settings.value('outputWidth'))
- height = int(self.worker.core.settings.value('outputHeight'))
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
return self.addText(width, height)
def addText(self, width, height):
diff --git a/src/components/video.py b/src/components/video.py
index 5303e3a..49bd145 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -158,14 +158,14 @@ class Component(Component):
if self.useAudio:
# props.append('audio')
pass
- if not os.path.exists(self.videoPath):
+ if self.videoPath and not os.path.exists(self.videoPath):
props.append('error')
return props
def error(self):
if not os.path.exists(self.videoPath):
- return "The video path selected on " \
- "layer %s no longer exists!" % str(self.compPos)
+ return "The video selected on " \
+ "layer %s does not exist!" % str(self.compPos)
def audio(self):
return (self.videoPath, {})
diff --git a/src/core.py b/src/core.py
index 450e43b..64f55eb 100644
--- a/src/core.py
+++ b/src/core.py
@@ -11,6 +11,7 @@ from importlib import import_module
from PyQt5.QtCore import QStandardPaths
import toolkit
+from frame import Frame
class Core:
@@ -20,6 +21,7 @@ class Core:
opens projects and presets, and stores settings/paths to data.
'''
def __init__(self):
+ Frame.core = self
self.dataDir = QStandardPaths.writableLocation(
QStandardPaths.AppConfigLocation
)
diff --git a/src/frame.py b/src/frame.py
index c066cdb..cddb611 100644
--- a/src/frame.py
+++ b/src/frame.py
@@ -5,6 +5,11 @@ from PyQt5 import QtGui
from PIL import Image
from PIL.ImageQt import ImageQt
import sys
+import os
+
+
+class Frame:
+ '''Controller class for all frames.'''
class FramePainter(QtGui.QPainter):
@@ -43,5 +48,19 @@ def FloodFrame(width, height, RgbaTuple):
def BlankFrame(width, height):
- '''The base frame used by each component to start drawing'''
+ '''The base frame used by each component to start drawing.'''
return FloodFrame(width, height, (0, 0, 0, 0))
+
+
+def Checkerboard(width, height):
+ '''
+ A checkerboard to represent transparency to the user.
+ TODO: Would be cool to generate this image with numpy instead.
+ '''
+ image = FloodFrame(1920, 1080, (0, 0, 0, 0))
+ image.paste(Image.open(
+ os.path.join(Frame.core.wd, "background.png")),
+ (0, 0)
+ )
+ image = image.resize((width, height))
+ return image
diff --git a/src/mainwindow.py b/src/mainwindow.py
index d21ba0a..771b6b8 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -306,6 +306,7 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog)
QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog)
QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject)
+ QtWidgets.QShortcut("Ctrl+Alt+Shift+R", self.window, self.drawPreview)
QtWidgets.QShortcut(
"Ctrl+T", self.window,
@@ -585,6 +586,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.autosave(force)
self.updateWindowTitle()
+ @QtCore.pyqtSlot(QtGui.QImage)
def showPreviewImage(self, image):
self.previewWindow.changePixmap(image)
diff --git a/src/preview_thread.py b/src/preview_thread.py
index fb3b792..4ffb7f6 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -10,12 +10,12 @@ import core
from queue import Queue, Empty
import os
-from frame import FloodFrame
+from frame import Checkerboard
class Worker(QtCore.QObject):
- imageCreated = pyqtSignal(['QImage'])
+ imageCreated = pyqtSignal(QtGui.QImage)
error = pyqtSignal()
def __init__(self, parent=None, queue=None):
@@ -24,14 +24,12 @@ class Worker(QtCore.QObject):
parent.processTask.connect(self.process)
self.parent = parent
self.core = self.parent.core
+ self.settings = self.parent.core.settings
self.queue = queue
- self.width = int(self.core.settings.value('outputWidth'))
- self.height = int(self.core.settings.value('outputHeight'))
- # create checkerboard background to represent transparency
- self.background = FloodFrame(1920, 1080, (0, 0, 0, 0))
- self.background.paste(Image.open(os.path.join(
- self.core.wd, "background.png")))
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
+ self.background = Checkerboard(width, height)
@pyqtSlot(list)
def createPreviewImage(self, components):
@@ -42,6 +40,8 @@ class Worker(QtCore.QObject):
@pyqtSlot()
def process(self):
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
try:
nextPreviewInformation = self.queue.get(block=False)
while self.queue.qsize() >= 2:
@@ -50,22 +50,27 @@ class Worker(QtCore.QObject):
except Empty:
continue
- if self.background.width != self.width:
- self.background = self.background.resize(
- (self.width, self.height))
+ if self.background.width != width \
+ or self.background.height != height:
+ self.background = Checkerboard(width, height)
+
frame = self.background.copy()
components = nextPreviewInformation["components"]
for component in reversed(components):
try:
+ newFrame = component.previewRender(self)
frame = Image.alpha_composite(
- frame, component.previewRender(self)
+ frame, newFrame
)
except ValueError as e:
errMsg = "Bad frame returned by %s's preview renderer. " \
- "%s. This is a fatal error." % (
- str(component), str(e).capitalize()
+ "%s. New frame size was %s*%s; should be %s*%s. " \
+ "This is a fatal error." % (
+ str(component), str(e).capitalize(),
+ newFrame.width, newFrame.height,
+ width, height
)
print(errMsg)
self.parent.showMessage(
@@ -76,8 +81,11 @@ class Worker(QtCore.QObject):
)
self.error.emit()
break
+ except RuntimeError as e:
+ print(e)
else:
- self.imageCreated.emit(QtGui.QImage(ImageQt(frame)))
+ self.frame = ImageQt(frame)
+ self.imageCreated.emit(QtGui.QImage(self.frame))
except Empty:
True
diff --git a/src/video_thread.py b/src/video_thread.py
index b00d512..f736013 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -20,7 +20,7 @@ import signal
import core
from toolkit import openPipe, checkOutput
-from frame import FloodFrame
+from frame import Checkerboard
class Worker(QtCore.QObject):
@@ -56,8 +56,10 @@ class Worker(QtCore.QObject):
frame = None
for compNo, comp in reversed(list(enumerate(self.components))):
- if compNo in self.staticComponents and \
- self.staticComponents[compNo] is not None:
+ if compNo in self.staticComponents:
+ if self.staticComponents[compNo] is None:
+ # this layer was merged into a following layer
+ continue
# static component
if frame is None: # bottom-most layer
frame = self.staticComponents[compNo]
@@ -93,10 +95,7 @@ class Worker(QtCore.QObject):
Grabs frames from the previewQueue, adds them to the checkerboard
and emits a final QImage to the MainWindow for the live preview
'''
- background = FloodFrame(1920, 1080, (0, 0, 0, 0))
- background.paste(Image.open(os.path.join(
- self.core.wd, "background.png")))
- background = background.resize((self.width, self.height))
+ background = Checkerboard(self.width, self.height)
while not self.stopped:
audioI, frame = self.previewQueue.get()
@@ -164,8 +163,20 @@ class Worker(QtCore.QObject):
self.staticComponents[compNo] = \
comp.frameRender(compNo, 0).copy()
+ # Merge consecutive static component frames together
+ for compNo in range(len(self.components), 0, -1):
+ if compNo not in self.staticComponents \
+ or compNo - 1 not in self.staticComponents:
+ continue
+ self.staticComponents[compNo - 1] = Image.alpha_composite(
+ self.staticComponents.pop(compNo),
+ self.staticComponents[compNo - 1]
+ )
+ self.staticComponents[compNo] = None
+
ffmpegCommand = self.core.createFfmpegCommand(inputFile, outputFile)
- print(ffmpegCommand)
+ print('###### FFMPEG COMMAND ######\n %s' % " ".join(ffmpegCommand))
+ print('###### -------------- ######')
self.out_pipe = openPipe(
ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
)
--
cgit v1.2.3
From b7931572a73d408dceecc4b17b784a0338e0e35b Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 13 Jul 2017 14:46:22 -0400
Subject: added option to include audio from Video components
---
src/components/video.py | 8 +++-----
src/core.py | 16 ++++++++++++++--
src/video_thread.py | 4 ++--
3 files changed, 19 insertions(+), 9 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/components/video.py b/src/components/video.py
index 49bd145..53487b1 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -143,7 +143,6 @@ class Component(Component):
super().update()
def previewRender(self, previewWorker):
- self.videoFormats = previewWorker.core.videoFormats
width = int(previewWorker.core.settings.value('outputWidth'))
height = int(previewWorker.core.settings.value('outputHeight'))
self.updateChunksize(width, height)
@@ -156,8 +155,7 @@ class Component(Component):
def properties(self):
props = []
if self.useAudio:
- # props.append('audio')
- pass
+ props.append('audio')
if self.videoPath and not os.path.exists(self.videoPath):
props.append('error')
return props
@@ -168,7 +166,7 @@ class Component(Component):
"layer %s does not exist!" % str(self.compPos)
def audio(self):
- return (self.videoPath, {})
+ return (self.videoPath, {'map': '-v'})
def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs)
@@ -216,7 +214,7 @@ class Component(Component):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Video",
- imgDir, "Video Files (%s)" % " ".join(self.videoFormats)
+ imgDir, "Video Files (%s)" % " ".join(self.core.videoFormats)
)
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
diff --git a/src/core.py b/src/core.py
index 64f55eb..d72760d 100644
--- a/src/core.py
+++ b/src/core.py
@@ -526,13 +526,25 @@ class Core:
if 'audio' in comp.properties()
]
if extraAudio:
- for extraInputFile, params in extraAudio:
+ unwantedVideoStreams = []
+ for compNo, params in enumerate(extraAudio):
+ extraInputFile, params = params
ffmpegCommand.extend([
'-i', extraInputFile
])
+ if 'map' in params and params['map'] == '-v':
+ # a video stream to remove
+ unwantedVideoStreams.append(compNo + 1)
+
+ if unwantedVideoStreams:
+ ffmpegCommand.extend(['-map', '0'])
+ for compNo in unwantedVideoStreams:
+ ffmpegCommand.extend([
+ '-map', '-%s:v' % str(compNo)
+ ])
ffmpegCommand.extend([
'-filter_complex',
- 'amix=inputs=%s:duration=longest:dropout_transition=3' % str(
+ 'amix=inputs=%s:duration=first:dropout_transition=3' % str(
len(extraAudio) + 1
),
])
diff --git a/src/video_thread.py b/src/video_thread.py
index f736013..bfb0cc4 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -175,8 +175,8 @@ class Worker(QtCore.QObject):
self.staticComponents[compNo] = None
ffmpegCommand = self.core.createFfmpegCommand(inputFile, outputFile)
- print('###### FFMPEG COMMAND ######\n %s' % " ".join(ffmpegCommand))
- print('###### -------------- ######')
+ print('###### FFMPEG COMMAND ######\n%s' % " ".join(ffmpegCommand))
+ print('############################')
self.out_pipe = openPipe(
ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
)
--
cgit v1.2.3
From 06c27a48bc3f52e15c15445d822e8a6f523ab98f Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 13 Jul 2017 17:03:25 -0400
Subject: more error messages for blank components
---
src/components/image.py | 7 ++++---
src/components/sound.py | 11 ++++++++++-
src/components/text.py | 8 +++++++-
src/components/video.py | 13 ++++++++++---
src/video_thread.py | 28 ++++++++++++++++++++++------
5 files changed, 53 insertions(+), 14 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/components/image.py b/src/components/image.py
index 6465bc9..6a70424 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -48,14 +48,15 @@ class Component(Component):
def properties(self):
props = ['static']
- if self.imagePath and not os.path.exists(self.imagePath):
+ if not os.path.exists(self.imagePath):
props.append('error')
return props
def error(self):
+ if not self.imagePath:
+ return "There is no image selected."
if not os.path.exists(self.imagePath):
- return "The image selected on " \
- "layer %s does not exist!" % str(self.compPos)
+ return "The image selected does not exist!"
def frameRender(self, layerNo, frameNo):
width = int(self.settings.value('outputWidth'))
diff --git a/src/components/sound.py b/src/components/sound.py
index 9c114a8..2ffb682 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -34,7 +34,16 @@ class Component(Component):
pass
def properties(self):
- return ['static', 'audio']
+ props = ['static', 'audio']
+ if not os.path.exists(self.sound):
+ props.append('error')
+ return props
+
+ def error(self):
+ if not self.sound:
+ return "No audio file selected."
+ if not os.path.exists(self.sound):
+ return "The audio file selected no longer exists!"
def audio(self):
return (self.sound, {})
diff --git a/src/components/text.py b/src/components/text.py
index 4435b80..c52bdc5 100644
--- a/src/components/text.py
+++ b/src/components/text.py
@@ -121,7 +121,13 @@ class Component(Component):
return self.addText(width, height)
def properties(self):
- return ['static']
+ props = ['static']
+ if not self.title:
+ props.append('error')
+ return props
+
+ def error(self):
+ return "No text provided."
def frameRender(self, layerNo, frameNo):
width = int(self.settings.value('outputWidth'))
diff --git a/src/components/video.py b/src/components/video.py
index 53487b1..8861d70 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -115,6 +115,7 @@ class Component(Component):
self.settings = parent.settings
page = self.loadUi('video.ui')
self.videoPath = ''
+ self.badVideo = False
self.x = 0
self.y = 0
self.loopVideo = False
@@ -156,14 +157,18 @@ class Component(Component):
props = []
if self.useAudio:
props.append('audio')
- if self.videoPath and not os.path.exists(self.videoPath):
+ if not self.videoPath or self.badVideo \
+ or not os.path.exists(self.videoPath):
props.append('error')
return props
def error(self):
+ if not self.videoPath:
+ return "There is no video selected."
if not os.path.exists(self.videoPath):
- return "The video selected on " \
- "layer %s does not exist!" % str(self.compPos)
+ return "The video selected does not exist!"
+ if self.badVideo:
+ return "The video selected is corrupt!"
def audio(self):
return (self.videoPath, {'map': '-v'})
@@ -300,6 +305,7 @@ def finalizeFrame(self, imageData, width, height):
'### BAD VIDEO SELECTED ###\n'
'Video will not export with these settings'
)
+ self.badVideo = True
return BlankFrame(width, height)
if self.scale != 100 \
@@ -308,4 +314,5 @@ def finalizeFrame(self, imageData, width, height):
frame.paste(image, box=(self.xPosition, self.yPosition))
else:
frame = image
+ self.badVideo = False
return frame
diff --git a/src/video_thread.py b/src/video_thread.py
index bfb0cc4..9ce9cc8 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -141,7 +141,7 @@ class Worker(QtCore.QObject):
]))
self.staticComponents = {}
numComps = len(self.components)
- for compNo, comp in enumerate(self.components):
+ for compNo, comp in enumerate(reversed(self.components)):
comp.preFrameRender(
worker=self,
completeAudioArray=self.completeAudioArray,
@@ -151,26 +151,41 @@ class Worker(QtCore.QObject):
)
if 'error' in comp.properties():
+ self.cancel()
self.canceled = True
errMsg = "Component #%s encountered an error!" % compNo \
- if comp.error() is None else comp.error()
+ if comp.error() is None else 'Component #%s (%s): %s' % (
+ str(compNo),
+ str(comp),
+ comp.error()
+ )
self.parent.showMessage(
msg=errMsg,
icon='Warning',
parent=None # MainWindow is in a different thread
)
+ break
if 'static' in comp.properties():
self.staticComponents[compNo] = \
comp.frameRender(compNo, 0).copy()
+ if self.canceled:
+ print('Export cancelled by component #%s (%s): %s' % (
+ compNo, str(comp), comp.error()
+ ))
+ self.progressBarSetText.emit('Export Canceled')
+ self.encoding.emit(False)
+ self.videoCreated.emit()
+ return
+
# Merge consecutive static component frames together
- for compNo in range(len(self.components), 0, -1):
+ for compNo in range(len(self.components)):
if compNo not in self.staticComponents \
- or compNo - 1 not in self.staticComponents:
+ or compNo + 1 not in self.staticComponents:
continue
- self.staticComponents[compNo - 1] = Image.alpha_composite(
+ self.staticComponents[compNo + 1] = Image.alpha_composite(
self.staticComponents.pop(compNo),
- self.staticComponents[compNo - 1]
+ self.staticComponents[compNo + 1]
)
self.staticComponents[compNo] = None
@@ -278,6 +293,7 @@ class Worker(QtCore.QObject):
def cancel(self):
self.canceled = True
+ self.stopped = True
self.core.cancel()
for comp in self.components:
--
cgit v1.2.3
From d7b678f66d1bb1d5c7ccbbf0c8871b66cc1f8750 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 13 Jul 2017 19:31:00 -0400
Subject: staticComponents list is reversed now
---
src/core.py | 8 ++++----
src/video_thread.py | 10 +++++-----
2 files changed, 9 insertions(+), 9 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/core.py b/src/core.py
index d72760d..3f0a6ad 100644
--- a/src/core.py
+++ b/src/core.py
@@ -527,20 +527,20 @@ class Core:
]
if extraAudio:
unwantedVideoStreams = []
- for compNo, params in enumerate(extraAudio):
+ for streamNo, params in enumerate(extraAudio):
extraInputFile, params = params
ffmpegCommand.extend([
'-i', extraInputFile
])
if 'map' in params and params['map'] == '-v':
# a video stream to remove
- unwantedVideoStreams.append(compNo + 1)
+ unwantedVideoStreams.append(streamNo + 1)
if unwantedVideoStreams:
ffmpegCommand.extend(['-map', '0'])
- for compNo in unwantedVideoStreams:
+ for streamNo in unwantedVideoStreams:
ffmpegCommand.extend([
- '-map', '-%s:v' % str(compNo)
+ '-map', '-%s:v' % str(streamNo)
])
ffmpegCommand.extend([
'-filter_complex',
diff --git a/src/video_thread.py b/src/video_thread.py
index 9ce9cc8..b0562db 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -54,18 +54,18 @@ class Worker(QtCore.QObject):
audioI = self.compositeQueue.get()
bgI = int(audioI / self.sampleSize)
frame = None
-
for compNo, comp in reversed(list(enumerate(self.components))):
- if compNo in self.staticComponents:
- if self.staticComponents[compNo] is None:
+ layerNo = len(self.components) - compNo
+ if layerNo in self.staticComponents:
+ if self.staticComponents[layerNo] is None:
# this layer was merged into a following layer
continue
# static component
if frame is None: # bottom-most layer
- frame = self.staticComponents[compNo]
+ frame = self.staticComponents[layerNo]
else:
frame = Image.alpha_composite(
- frame, self.staticComponents[compNo]
+ frame, self.staticComponents[layerNo]
)
else:
# animated component
--
cgit v1.2.3
From 62ab09e3f36dcaf6c1a4680dc6c4d048fb2e165c Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sat, 15 Jul 2017 01:00:03 -0400
Subject: Video comp verifies audio streams, videoThread moved into Core
off-by-1 bug fixed in exporting, & use fewer threads for fewer CPUs
---
src/command.py | 22 ++++++++--------------
src/components/video.py | 25 ++++++++++++++++++++-----
src/core.py | 16 ++++++++++++++++
src/mainwindow.py | 22 ++++++----------------
src/video_thread.py | 35 +++++++++++++++++++++++------------
5 files changed, 73 insertions(+), 47 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/command.py b/src/command.py
index be194d8..41618f8 100644
--- a/src/command.py
+++ b/src/command.py
@@ -9,13 +9,12 @@ import os
import sys
import core
-import video_thread
from toolkit import LoadDefaultSettings
class Command(QtCore.QObject):
- videoTask = QtCore.pyqtSignal(str, str, list)
+ createVideo = QtCore.pyqtSignal()
def __init__(self):
QtCore.QObject.__init__(self)
@@ -112,21 +111,16 @@ class Command(QtCore.QObject):
quit(1)
def createAudioVisualisation(self, input, output):
- self.videoThread = QtCore.QThread(self)
- self.videoWorker = video_thread.Worker(self)
- self.videoWorker.moveToThread(self.videoThread)
- self.videoWorker.videoCreated.connect(self.videoCreated)
-
- self.videoThread.start()
- self.videoTask.emit(
- input,
- output,
- list(reversed(self.core.selectedComponents))
+ self.core.selectedComponents = list(
+ reversed(self.core.selectedComponents))
+ self.core.componentListChanged()
+ self.worker = self.core.newVideoWorker(
+ self, input, output
)
+ self.worker.videoCreated.connect(self.videoCreated)
+ self.createVideo.emit()
def videoCreated(self):
- self.videoThread.quit()
- self.videoThread.wait()
quit(0)
def showMessage(self, **kwargs):
diff --git a/src/components/video.py b/src/components/video.py
index 8aa1420..b3b6a59 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -8,7 +8,7 @@ from queue import PriorityQueue
from component import Component, BadComponentInit
from frame import BlankFrame
-from toolkit import openPipe
+from toolkit import openPipe, checkOutput
class Video:
@@ -155,14 +155,29 @@ class Component(Component):
def properties(self):
props = []
- if self.useAudio:
- props.append('audio')
if not self.videoPath or self.badVideo \
or not os.path.exists(self.videoPath):
- props.append('error')
+ return ['error']
+
+ if self.useAudio:
+ props.append('audio')
+ # test if an audio stream really exists
+ audioTestCommand = [
+ self.core.FFMPEG_BIN,
+ '-i', self.videoPath,
+ '-vn', '-f', 'null', '-'
+ ]
+ try:
+ checkOutput(audioTestCommand, stderr=subprocess.DEVNULL)
+ except subprocess.CalledProcessError:
+ self.badAudio = True
+ return ['error']
+
return props
def error(self):
+ if hasattr(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):
@@ -180,7 +195,7 @@ class Component(Component):
self.blankFrame_ = BlankFrame(width, height)
self.updateChunksize(width, height)
self.video = Video(
- ffmpeg=self.parent.core.FFMPEG_BIN, videoPath=self.videoPath,
+ ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath,
width=width, height=height, chunkSize=self.chunkSize,
frameRate=int(self.settings.value("outputFrameRate")),
parent=self.parent, loopVideo=self.loopVideo,
diff --git a/src/core.py b/src/core.py
index 2500fa6..55bf261 100644
--- a/src/core.py
+++ b/src/core.py
@@ -12,6 +12,7 @@ from PyQt5.QtCore import QStandardPaths
import toolkit
from frame import Frame
+import video_thread
class Core:
@@ -633,6 +634,21 @@ class Core:
return completeAudioArray
+ def newVideoWorker(self, loader, audioFile, outputPath):
+ self.videoThread = QtCore.QThread(loader)
+ videoWorker = video_thread.Worker(
+ loader, audioFile, outputPath, self.selectedComponents
+ )
+ videoWorker.moveToThread(self.videoThread)
+ videoWorker.videoCreated.connect(self.videoCreated)
+
+ self.videoThread.start()
+ return videoWorker
+
+ def videoCreated(self):
+ self.videoThread.quit()
+ self.videoThread.wait()
+
def cancel(self):
self.canceled = True
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 771b6b8..76ed179 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -16,7 +16,6 @@ import time
import core
import preview_thread
-import video_thread
from presetmanager import PresetManager
from toolkit import LoadDefaultSettings, disableWhenEncoding, checkOutput
@@ -49,9 +48,9 @@ class PreviewWindow(QtWidgets.QLabel):
class MainWindow(QtWidgets.QMainWindow):
- newTask = QtCore.pyqtSignal(list)
+ createVideo = QtCore.pyqtSignal()
+ newTask = QtCore.pyqtSignal(list) # for the preview window
processTask = QtCore.pyqtSignal()
- videoTask = QtCore.pyqtSignal(str, str, list)
def __init__(self, window, project):
QtWidgets.QMainWindow.__init__(self)
@@ -497,20 +496,15 @@ class MainWindow(QtWidgets.QMainWindow):
self.canceled = False
self.progressBarUpdated(-1)
- self.videoThread = QtCore.QThread(self)
- self.videoWorker = video_thread.Worker(self)
- self.videoWorker.moveToThread(self.videoThread)
- self.videoWorker.videoCreated.connect(self.videoCreated)
+ self.videoWorker = self.core.newVideoWorker(
+ self, audioFile, outputPath
+ )
self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated)
self.videoWorker.progressBarSetText.connect(
self.progressBarSetText)
self.videoWorker.imageCreated.connect(self.showPreviewImage)
self.videoWorker.encoding.connect(self.changeEncodingStatus)
- self.videoThread.start()
- self.videoTask.emit(
- audioFile,
- outputPath,
- self.core.selectedComponents)
+ self.createVideo.emit()
def changeEncodingStatus(self, status):
self.encoding = status
@@ -569,10 +563,6 @@ class MainWindow(QtWidgets.QMainWindow):
else:
self.window.progressBar_createVideo.setFormat(value)
- def videoCreated(self):
- self.videoThread.quit()
- self.videoThread.wait()
-
def updateResolution(self):
resIndex = int(self.window.comboBox_resolution.currentIndex())
res = self.resolutions[resIndex].split('x')
diff --git a/src/video_thread.py b/src/video_thread.py
index b0562db..5295a3b 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -19,7 +19,7 @@ import time
import signal
import core
-from toolkit import openPipe, checkOutput
+from toolkit import openPipe
from frame import Checkerboard
@@ -31,13 +31,19 @@ class Worker(QtCore.QObject):
progressBarSetText = pyqtSignal(str)
encoding = pyqtSignal(bool)
- def __init__(self, parent=None):
+
+ def __init__(self, parent, inputFile, outputFile, components):
QtCore.QObject.__init__(self)
self.core = parent.core
self.settings = parent.core.settings
self.modules = parent.core.modules
+ parent.createVideo.connect(self.createVideo)
+
self.parent = parent
- parent.videoTask.connect(self.createVideo)
+ self.components = components
+ self.outputFile = outputFile
+ self.inputFile = inputFile
+
self.sampleSize = 1470 # 44100 / 30 = 1470
self.canceled = False
self.error = False
@@ -55,7 +61,7 @@ class Worker(QtCore.QObject):
bgI = int(audioI / self.sampleSize)
frame = None
for compNo, comp in reversed(list(enumerate(self.components))):
- layerNo = len(self.components) - compNo
+ layerNo = len(self.components) - compNo - 1
if layerNo in self.staticComponents:
if self.staticComponents[layerNo] is None:
# this layer was merged into a following layer
@@ -106,12 +112,10 @@ class Worker(QtCore.QObject):
self.previewQueue.task_done()
- @pyqtSlot(str, str, list)
- def createVideo(self, inputFile, outputFile, components):
+ @pyqtSlot()
+ def createVideo(self):
numpy.seterr(divide='ignore')
self.encoding.emit(True)
- self.components = components
- self.outputFile = outputFile
self.extraAudio = []
self.width = int(self.settings.value('outputWidth'))
self.height = int(self.settings.value('outputHeight'))
@@ -131,7 +135,7 @@ class Worker(QtCore.QObject):
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
self.progressBarSetText.emit("Loading audio file...")
- self.completeAudioArray = self.core.readAudioFile(inputFile, self)
+ self.completeAudioArray = self.core.readAudioFile(self.inputFile, self)
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit("Starting components...")
@@ -189,7 +193,9 @@ class Worker(QtCore.QObject):
)
self.staticComponents[compNo] = None
- ffmpegCommand = self.core.createFfmpegCommand(inputFile, outputFile)
+ ffmpegCommand = self.core.createFfmpegCommand(
+ self.inputFile, self.outputFile
+ )
print('###### FFMPEG COMMAND ######\n%s' % " ".join(ffmpegCommand))
print('############################')
self.out_pipe = openPipe(
@@ -200,9 +206,14 @@ class Worker(QtCore.QObject):
# START CREATING THE VIDEO
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
- # Make three renderNodes in new threads to create the frames
+ # Make 2 or 3 renderNodes in new threads to create the frames
self.renderThreads = []
- for i in range(3):
+ try:
+ numCpus = len(os.sched_getaffinity(0))
+ except:
+ numCpus = os.cpu_count()
+
+ for i in range(2 if numCpus <= 2 else 3):
self.renderThreads.append(
Thread(target=self.renderNode, name="Render Thread"))
self.renderThreads[i].daemon = True
--
cgit v1.2.3
From bcb8f27c2e4434d2296dcd66bf279b76ee0d0a4f Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sat, 15 Jul 2017 13:13:53 -0400
Subject: use -t on inputs so ffmpeg knows when to stop filters
+ better feedback in cmd mode
---
src/command.py | 20 ++++++++++++++++++++
src/components/sound.py | 5 +++++
src/components/video.py | 38 ++++++++++++++++++++++++++------------
src/core.py | 8 ++++++--
src/main.py | 11 ++++++-----
src/video_thread.py | 8 ++++----
6 files changed, 67 insertions(+), 23 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/command.py b/src/command.py
index 41618f8..84d798d 100644
--- a/src/command.py
+++ b/src/command.py
@@ -7,6 +7,7 @@ from PyQt5 import QtCore
import argparse
import os
import sys
+import time
import core
from toolkit import LoadDefaultSettings
@@ -118,8 +119,27 @@ class Command(QtCore.QObject):
self, input, output
)
self.worker.videoCreated.connect(self.videoCreated)
+ self.lastProgressUpdate = time.time()
+ self.worker.progressBarSetText.connect(self.progressBarSetText)
self.createVideo.emit()
+ @QtCore.pyqtSlot(str)
+ def progressBarSetText(self, value):
+ if 'Export ' in value:
+ # Don't duplicate completion/failure messages
+ return
+ if not value.startswith('Exporting') \
+ and time.time() - self.lastProgressUpdate >= 0.05:
+ # Show most messages very often
+ print(value)
+ elif time.time() - self.lastProgressUpdate >= 2.0:
+ # Give user time to read ffmpeg's output during the export
+ print('##### %s' % value)
+ else:
+ return
+ self.lastProgressUpdate = time.time()
+
+ @QtCore.pyqtSlot()
def videoCreated(self):
quit(0)
diff --git a/src/components/sound.py b/src/components/sound.py
index fedc32b..4a5714b 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -79,6 +79,11 @@ class Component(Component):
if not arg.startswith('preset=') and '=' in arg:
key, arg = arg.split('=', 1)
if key == 'path':
+ if '*%s' % os.path.splitext(arg)[1] \
+ not in self.core.audioFormats:
+ print("Not a supported audio format")
+ quit(1)
self.page.lineEdit_sound.setText(arg)
return
+
super().command(arg)
diff --git a/src/components/video.py b/src/components/video.py
index b3b6a59..0b93293 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -116,6 +116,7 @@ class Component(Component):
page = self.loadUi('video.ui')
self.videoPath = ''
self.badVideo = False
+ self.badAudio = False
self.x = 0
self.y = 0
self.loopVideo = False
@@ -161,22 +162,14 @@ class Component(Component):
if self.useAudio:
props.append('audio')
- # test if an audio stream really exists
- audioTestCommand = [
- self.core.FFMPEG_BIN,
- '-i', self.videoPath,
- '-vn', '-f', 'null', '-'
- ]
- try:
- checkOutput(audioTestCommand, stderr=subprocess.DEVNULL)
- except subprocess.CalledProcessError:
- self.badAudio = True
+ self.testAudioStream()
+ if self.badAudio:
return ['error']
return props
def error(self):
- if hasattr(self, 'badAudio'):
+ if self.badAudio:
return "Could not identify an audio stream in this video."
if not self.videoPath:
return "There is no video selected."
@@ -185,6 +178,20 @@ class Component(Component):
if self.badVideo:
return "The video selected is corrupt!"
+ def testAudioStream(self):
+ # test if an audio stream really exists
+ audioTestCommand = [
+ self.core.FFMPEG_BIN,
+ '-i', self.videoPath,
+ '-vn', '-f', 'null', '-'
+ ]
+ try:
+ checkOutput(audioTestCommand, stderr=subprocess.DEVNULL)
+ except subprocess.CalledProcessError:
+ self.badAudio = True
+ else:
+ self.badAudio = False
+
def audio(self):
return (self.videoPath, {'map': '-v'})
@@ -277,7 +284,7 @@ class Component(Component):
if not arg.startswith('preset=') and '=' in arg:
key, arg = arg.split('=', 1)
if key == 'path' and os.path.exists(arg):
- if os.path.splitext(arg)[1] in self.core.videoFormats:
+ if '*%s' % os.path.splitext(arg)[1] in self.core.videoFormats:
self.page.lineEdit_video.setText(arg)
self.page.spinBox_scale.setValue(100)
self.page.checkBox_loop.setChecked(True)
@@ -285,10 +292,17 @@ class Component(Component):
else:
print("Not a supported video format")
quit(1)
+ elif arg == 'audio':
+ if not self.page.lineEdit_video.text():
+ print("'audio' option must follow a video selection")
+ quit(1)
+ self.page.checkBox_useAudio.setChecked(True)
+ return
super().command(arg)
def commandHelp(self):
print('Load a video:\n path=/filepath/to/video.mp4')
+ print('Using audio:\n path=/filepath/to/video.mp4 audio')
def scale(scale, width, height, returntype=None):
diff --git a/src/core.py b/src/core.py
index 55bf261..4c12209 100644
--- a/src/core.py
+++ b/src/core.py
@@ -464,10 +464,11 @@ class Core:
except sp.CalledProcessError:
return "avconv"
- def createFfmpegCommand(self, inputFile, outputFile):
+ def createFfmpegCommand(self, inputFile, outputFile, duration):
'''
Constructs the major ffmpeg command used to export the video
'''
+ duration = str(duration)
# Test if user has libfdk_aac
encoders = toolkit.checkOutput(
@@ -516,10 +517,12 @@ class Core:
),
'-pix_fmt', 'rgba',
'-r', self.settings.value('outputFrameRate'),
+ '-t', duration,
'-i', '-', # the video input comes from a pipe
'-an', # the video input has no sound
# INPUT SOUND
+ '-t', duration,
'-i', inputFile
]
@@ -532,6 +535,7 @@ class Core:
for streamNo, params in enumerate(extraAudio):
extraInputFile, params = params
ffmpegCommand.extend([
+ '-t', duration,
'-i', extraInputFile
])
if 'map' in params and params['map'] == '-v':
@@ -632,7 +636,7 @@ class Core:
completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray
completeAudioArray = completeAudioArrayCopy
- return completeAudioArray
+ return (completeAudioArray, duration)
def newVideoWorker(self, loader, audioFile, outputPath):
self.videoThread = QtCore.QThread(loader)
diff --git a/src/main.py b/src/main.py
index b0ece29..2216d2a 100644
--- a/src/main.py
+++ b/src/main.py
@@ -8,13 +8,13 @@ import video_thread
if __name__ == "__main__":
- mode = 'gui'
+ mode = 'GUI'
if len(sys.argv) > 2:
- mode = 'cmd'
+ mode = 'commandline'
elif len(sys.argv) == 2:
if sys.argv[1].startswith('-'):
- mode = 'cmd'
+ mode = 'commandline'
else:
# opening a project file with gui
proj = sys.argv[1]
@@ -22,16 +22,17 @@ if __name__ == "__main__":
# normal gui launch
proj = None
+ print('Starting Audio Visualizer in %s mode' % mode)
app = QtWidgets.QApplication(sys.argv)
app.setApplicationName("audio-visualizer")
# app.setOrganizationName("audio-visualizer")
- if mode == 'cmd':
+ if mode == 'commandline':
from command import *
main = Command()
- elif mode == 'gui':
+ elif mode == 'GUI':
from mainwindow import *
import atexit
import signal
diff --git a/src/video_thread.py b/src/video_thread.py
index 5295a3b..674765a 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -31,7 +31,6 @@ class Worker(QtCore.QObject):
progressBarSetText = pyqtSignal(str)
encoding = pyqtSignal(bool)
-
def __init__(self, parent, inputFile, outputFile, components):
QtCore.QObject.__init__(self)
self.core = parent.core
@@ -135,7 +134,9 @@ class Worker(QtCore.QObject):
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
self.progressBarSetText.emit("Loading audio file...")
- self.completeAudioArray = self.core.readAudioFile(self.inputFile, self)
+ self.completeAudioArray, duration = self.core.readAudioFile(
+ self.inputFile, self
+ )
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit("Starting components...")
@@ -144,7 +145,6 @@ class Worker(QtCore.QObject):
for num, component in enumerate(reversed(self.components))
]))
self.staticComponents = {}
- numComps = len(self.components)
for compNo, comp in enumerate(reversed(self.components)):
comp.preFrameRender(
worker=self,
@@ -194,7 +194,7 @@ class Worker(QtCore.QObject):
self.staticComponents[compNo] = None
ffmpegCommand = self.core.createFfmpegCommand(
- self.inputFile, self.outputFile
+ self.inputFile, self.outputFile, duration
)
print('###### FFMPEG COMMAND ######\n%s' % " ".join(ffmpegCommand))
print('############################')
--
cgit v1.2.3
From 17c8a6703a8093d31c6772ba3b8d9ee01adaa0da Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sat, 15 Jul 2017 18:59:22 -0400
Subject: trying to make setup.py work
---
setup.py | 53 +++++++++++++++++++++++++++++++++------------------
src/__init__.py | 0
src/__main__.py | 3 +++
src/main.py | 29 +++++++++++++++-------------
src/presetmanager.py | 1 -
src/preview_thread.py | 1 -
src/video_thread.py | 1 -
7 files changed, 53 insertions(+), 35 deletions(-)
create mode 100644 src/__init__.py
create mode 100644 src/__main__.py
(limited to 'src/video_thread.py')
diff --git a/setup.py b/setup.py
index fde3461..4ef6077 100644
--- a/setup.py
+++ b/setup.py
@@ -1,19 +1,34 @@
-+from setuptools import setup, find_packages
-
- -# Dependencies are automatically detected, but it might need +setup(name='audio_visualizer_python',
- -# fine tuning. + version='1.0',
- -buildOptions = dict(packages = [], excludes = [ + description='a little GUI tool to render visualization \
- - "apport", + videos of audio files',
- - "apt", + license='MIT',
- - "ctypes", + url='https://github.com/djfun/audio-visualizer-python',
- - "curses", + packages=find_packages(),
- - "distutils", + package_data={
- - "email", + 'src': ['*'],
- - "html", + },
- - "http", + install_requires=['pillow-simd', 'numpy', ''],
- - "json", + entry_points={
- - "xmlrpc", + 'gui_scripts': [
- - "nose" + 'audio-visualizer-python = avpython.main:main'
- - ], include_files = ["main.ui"]) + ]
- - + }
- -import sys + )
\ No newline at end of file
+from setuptools import setup
+import os
+
+
+def package_files(directory):
+ paths = []
+ for (path, directories, filenames) in os.walk(directory):
+ for filename in filenames:
+ paths.append(os.path.join('..', path, filename))
+ return paths
+
+
+setup(
+ name='audio_visualizer_python',
+ version='2.0.0',
+ description='A little GUI tool to create audio visualization " \
+ "videos out of audio files',
+ license='MIT',
+ url='https://github.com/djfun/audio-visualizer-python',
+ packages=[
+ 'avpython',
+ 'avpython.components'
+ ],
+ package_dir={'avpython': 'src'},
+ package_data={
+ 'avpython': package_files('src'),
+ },
+ install_requires=['olefile', 'Pillow-SIMD', 'PyQt5', 'numpy'],
+ entry_points={
+ 'gui_scripts': [
+ 'avp = avpython.main:main'
+ ],
+ }
+)
diff --git a/src/__init__.py b/src/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/__main__.py b/src/__main__.py
new file mode 100644
index 0000000..a68739e
--- /dev/null
+++ b/src/__main__.py
@@ -0,0 +1,3 @@
+from avpython.main import main
+
+main()
\ No newline at end of file
diff --git a/src/main.py b/src/main.py
index 2216d2a..317237c 100644
--- a/src/main.py
+++ b/src/main.py
@@ -2,12 +2,18 @@ from PyQt5 import uic, QtWidgets
import sys
import os
-import core
-import preview_thread
-import video_thread
+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.append(wd)
-if __name__ == "__main__":
mode = 'GUI'
if len(sys.argv) > 2:
mode = 'commandline'
@@ -28,22 +34,15 @@ if __name__ == "__main__":
# app.setOrganizationName("audio-visualizer")
if mode == 'commandline':
- from command import *
+ from command import Command
main = Command()
elif mode == 'GUI':
- from mainwindow import *
+ from mainwindow import MainWindow
import atexit
import signal
- if getattr(sys, 'frozen', False):
- # frozen
- wd = os.path.dirname(sys.executable)
- else:
- # unfrozen
- wd = os.path.dirname(os.path.realpath(__file__))
-
window = uic.loadUi(os.path.join(wd, "mainwindow.ui"))
# window.adjustSize()
desc = QtWidgets.QDesktopWidget()
@@ -64,3 +63,7 @@ if __name__ == "__main__":
# applicable to both modes
sys.exit(app.exec_())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/presetmanager.py b/src/presetmanager.py
index 0028203..6e003a1 100644
--- a/src/presetmanager.py
+++ b/src/presetmanager.py
@@ -6,7 +6,6 @@ from PyQt5 import QtCore, QtWidgets
import string
import os
-import core
import toolkit
diff --git a/src/preview_thread.py b/src/preview_thread.py
index 4ffb7f6..6c33aff 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -6,7 +6,6 @@ from PyQt5 import QtCore, QtGui, uic
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PIL import Image
from PIL.ImageQt import ImageQt
-import core
from queue import Queue, Empty
import os
diff --git a/src/video_thread.py b/src/video_thread.py
index 674765a..60db99f 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -18,7 +18,6 @@ from threading import Thread, Event
import time
import signal
-import core
from toolkit import openPipe
from frame import Checkerboard
--
cgit v1.2.3
From b1713d38fa91e39f142b0c234b6405229aa149e1 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Mon, 17 Jul 2017 22:07:33 -0400
Subject: combined toolkit.py & frame.py into toolkit package
---
README.md | 2 +-
src/__main__.py | 4 +-
src/component.py | 31 -----------
src/components/color.py | 9 +--
src/components/image.py | 2 +-
src/components/original.py | 7 ++-
src/components/sound.py | 2 +-
src/components/text.py | 7 ++-
src/components/video.py | 2 +-
src/core.py | 2 +-
src/frame.py | 66 ----------------------
src/preview_thread.py | 2 +-
src/toolkit.py | 99 ---------------------------------
src/toolkit/__init__.py | 1 +
src/toolkit/common.py | 133 +++++++++++++++++++++++++++++++++++++++++++++
src/toolkit/frame.py | 66 ++++++++++++++++++++++
src/video_thread.py | 2 +-
17 files changed, 223 insertions(+), 214 deletions(-)
delete mode 100644 src/frame.py
delete mode 100644 src/toolkit.py
create mode 100644 src/toolkit/__init__.py
create mode 100644 src/toolkit/common.py
create mode 100644 src/toolkit/frame.py
(limited to 'src/video_thread.py')
diff --git a/README.md b/README.md
index 9149b4f..5f4e1e7 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ Dependencies
------------
Python 3.4, FFmpeg 3.3, PyQt5, Pillow-SIMD, NumPy
-**Note:** Pillow may be used as a drop-in replacement for Pillow-SIMD if problems are encountered installing. However this will result in much slower video export times. For help troubleshooting installation problems, the * For any problems with installing Pillow-SIMD, see the [Pillow installation guide](http://pillow.readthedocs.io/en/3.1.x/installation.html).
+**Note:** Pillow may be used as a drop-in replacement for Pillow-SIMD if problems are encountered installing. However this will result in much slower video export times. For help installing Pillow-SIMD, see the [Pillow installation guide](http://pillow.readthedocs.io/en/3.1.x/installation.html).
Installation
------------
diff --git a/src/__main__.py b/src/__main__.py
index a68739e..3babeae 100644
--- a/src/__main__.py
+++ b/src/__main__.py
@@ -1,3 +1,5 @@
+# Allows for launching with python3 -m avpython
+
from avpython.main import main
-main()
\ No newline at end of file
+main()
diff --git a/src/component.py b/src/component.py
index adb170e..7842bd6 100644
--- a/src/component.py
+++ b/src/component.py
@@ -112,37 +112,6 @@ class Component(QtCore.QObject):
def commandHelp(self):
'''Print help text for this Component's commandline arguments'''
- def pickColor(self):
- '''
- Use color picker to get color input from the user,
- and return this as an RGB string and QPushButton stylesheet.
- In a subclass apply stylesheet to any color selection widgets
- '''
- dialog = QtWidgets.QColorDialog()
- dialog.setOption(QtWidgets.QColorDialog.ShowAlphaChannel, True)
- color = dialog.getColor()
- if color.isValid():
- RGBstring = '%s,%s,%s' % (
- str(color.red()), str(color.green()), str(color.blue()))
- btnStyle = "QPushButton{background-color: %s; outline: none;}" \
- % color.name()
- return RGBstring, btnStyle
- else:
- return None, None
-
- def RGBFromString(self, string):
- '''Turns an RGB string like "255, 255, 255" into a tuple'''
- try:
- tup = tuple([int(i) for i in string.split(',')])
- if len(tup) != 3:
- raise ValueError
- for i in tup:
- if i > 255 or i < 0:
- raise ValueError
- return tup
- except:
- return (255, 255, 255)
-
def loadUi(self, filename):
return uic.loadUi(os.path.join(self.core.componentsPath, filename))
diff --git a/src/components/color.py b/src/components/color.py
index ef4dd95..8d2526d 100644
--- a/src/components/color.py
+++ b/src/components/color.py
@@ -5,7 +5,8 @@ from PIL.ImageQt import ImageQt
import os
from component import Component
-from frame import BlankFrame, FloodFrame, FramePainter, PaintColor
+from toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor
+from toolkit import rgbFromString, pickColor
class Component(Component):
@@ -76,8 +77,8 @@ class Component(Component):
return page
def update(self):
- self.color1 = self.RGBFromString(self.page.lineEdit_color1.text())
- self.color2 = self.RGBFromString(self.page.lineEdit_color2.text())
+ 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()
@@ -229,7 +230,7 @@ class Component(Component):
}
def pickColor(self, num):
- RGBstring, btnStyle = super().pickColor()
+ RGBstring, btnStyle = pickColor()
if not RGBstring:
return
if num == 1:
diff --git a/src/components/image.py b/src/components/image.py
index c0d1c0d..7f3f610 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -3,7 +3,7 @@ from PyQt5 import QtGui, QtCore, QtWidgets
import os
from component import Component
-from frame import BlankFrame
+from toolkit.frame import BlankFrame
class Component(Component):
diff --git a/src/components/original.py b/src/components/original.py
index f5776a4..586204a 100644
--- a/src/components/original.py
+++ b/src/components/original.py
@@ -7,7 +7,8 @@ import time
from copy import copy
from component import Component
-from frame import BlankFrame
+from toolkit.frame import BlankFrame
+from toolkit import rgbFromString, pickColor
class Component(Component):
@@ -48,7 +49,7 @@ class Component(Component):
def update(self):
self.layout = self.page.comboBox_visLayout.currentIndex()
- self.visColor = self.RGBFromString(self.page.lineEdit_visColor.text())
+ self.visColor = rgbFromString(self.page.lineEdit_visColor.text())
self.scale = self.page.spinBox_scale.value()
self.y = self.page.spinBox_y.value()
@@ -116,7 +117,7 @@ class Component(Component):
self.visColor, self.layout)
def pickColor(self):
- RGBstring, btnStyle = super().pickColor()
+ RGBstring, btnStyle = pickColor()
if not RGBstring:
return
self.page.lineEdit_visColor.setText(RGBstring)
diff --git a/src/components/sound.py b/src/components/sound.py
index bd7d002..5b06405 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -2,7 +2,7 @@ from PyQt5 import QtGui, QtCore, QtWidgets
import os
from component import Component
-from frame import BlankFrame
+from toolkit.frame import BlankFrame
class Component(Component):
diff --git a/src/components/text.py b/src/components/text.py
index 19460e5..fc3ef5f 100644
--- a/src/components/text.py
+++ b/src/components/text.py
@@ -4,7 +4,8 @@ from PyQt5 import QtGui, QtCore, QtWidgets
import os
from component import Component
-from frame import FramePainter
+from toolkit.frame import FramePainter
+from toolkit import rgbFromString, pickColor
class Component(Component):
@@ -64,7 +65,7 @@ class Component(Component):
self.fontSize = self.page.spinBox_fontSize.value()
self.xPosition = self.page.spinBox_xTextAlign.value()
self.yPosition = self.page.spinBox_yTextAlign.value()
- self.textColor = self.RGBFromString(
+ self.textColor = rgbFromString(
self.page.lineEdit_textColor.text())
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.textColor).name()
@@ -146,7 +147,7 @@ class Component(Component):
return image.finalize()
def pickColor(self):
- RGBstring, btnStyle = super().pickColor()
+ RGBstring, btnStyle = pickColor()
if not RGBstring:
return
self.page.lineEdit_textColor.setText(RGBstring)
diff --git a/src/components/video.py b/src/components/video.py
index 9e3db30..a9f334e 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -7,7 +7,7 @@ import threading
from queue import PriorityQueue
from component import Component, BadComponentInit
-from frame import BlankFrame
+from toolkit.frame import BlankFrame
from toolkit import openPipe, checkOutput
diff --git a/src/core.py b/src/core.py
index a0a028b..07c1f71 100644
--- a/src/core.py
+++ b/src/core.py
@@ -11,7 +11,7 @@ from importlib import import_module
from PyQt5.QtCore import QStandardPaths
import toolkit
-from frame import Frame
+from toolkit.frame import Frame
import video_thread
diff --git a/src/frame.py b/src/frame.py
deleted file mode 100644
index cddb611..0000000
--- a/src/frame.py
+++ /dev/null
@@ -1,66 +0,0 @@
-'''
- Common tools for drawing compatible frames in a Component's frameRender()
-'''
-from PyQt5 import QtGui
-from PIL import Image
-from PIL.ImageQt import ImageQt
-import sys
-import os
-
-
-class Frame:
- '''Controller class for all frames.'''
-
-
-class FramePainter(QtGui.QPainter):
- '''
- A QPainter for a blank frame, which can be converted into a
- Pillow image with finalize()
- '''
- def __init__(self, width, height):
- image = BlankFrame(width, height)
- self.image = QtGui.QImage(ImageQt(image))
- super().__init__(self.image)
-
- def setPen(self, RgbTuple):
- super().setPen(PaintColor(*RgbTuple))
-
- def finalize(self):
- self.end()
- imBytes = self.image.bits().asstring(self.image.byteCount())
-
- return Image.frombytes(
- 'RGBA', (self.image.width(), self.image.height()), imBytes
- )
-
-
-class PaintColor(QtGui.QColor):
- '''Reverse the painter colour if the hardware stores RGB values backward'''
- def __init__(self, r, g, b, a=255):
- if sys.byteorder == 'big':
- super().__init__(r, g, b, a)
- else:
- super().__init__(b, g, r, a)
-
-
-def FloodFrame(width, height, RgbaTuple):
- return Image.new("RGBA", (width, height), RgbaTuple)
-
-
-def BlankFrame(width, height):
- '''The base frame used by each component to start drawing.'''
- return FloodFrame(width, height, (0, 0, 0, 0))
-
-
-def Checkerboard(width, height):
- '''
- A checkerboard to represent transparency to the user.
- TODO: Would be cool to generate this image with numpy instead.
- '''
- image = FloodFrame(1920, 1080, (0, 0, 0, 0))
- image.paste(Image.open(
- os.path.join(Frame.core.wd, "background.png")),
- (0, 0)
- )
- image = image.resize((width, height))
- return image
diff --git a/src/preview_thread.py b/src/preview_thread.py
index 6c33aff..c28e048 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -9,7 +9,7 @@ from PIL.ImageQt import ImageQt
from queue import Queue, Empty
import os
-from frame import Checkerboard
+from toolkit.frame import Checkerboard
class Worker(QtCore.QObject):
diff --git a/src/toolkit.py b/src/toolkit.py
deleted file mode 100644
index 5493f37..0000000
--- a/src/toolkit.py
+++ /dev/null
@@ -1,99 +0,0 @@
-'''
- Common functions
-'''
-import string
-import os
-import sys
-import subprocess
-from collections import OrderedDict
-
-
-def badName(name):
- '''Returns whether a name contains non-alphanumeric chars'''
- return any([letter in string.punctuation for letter in name])
-
-
-def alphabetizeDict(dictionary):
- '''Alphabetizes a dict into OrderedDict '''
- return OrderedDict(sorted(dictionary.items(), key=lambda t: t[0]))
-
-
-def presetToString(dictionary):
- '''Returns string repr of a preset'''
- return repr(alphabetizeDict(dictionary))
-
-
-def presetFromString(string):
- '''Turns a string repr of OrderedDict into a regular dict'''
- return dict(eval(string))
-
-
-def appendUppercase(lst):
- for form, i in zip(lst, range(len(lst))):
- lst.append(form.upper())
- return lst
-
-
-def hideCmdWin(func):
- ''' Stops CMD window from appearing on Windows.
- Adapted from here: http://code.activestate.com/recipes/409002/
- '''
- def decorator(commandList, **kwargs):
- if sys.platform == 'win32':
- startupinfo = subprocess.STARTUPINFO()
- startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
- kwargs['startupinfo'] = startupinfo
- return func(commandList, **kwargs)
- return decorator
-
-
-@hideCmdWin
-def checkOutput(commandList, **kwargs):
- return subprocess.check_output(commandList, **kwargs)
-
-
-@hideCmdWin
-def openPipe(commandList, **kwargs):
- return subprocess.Popen(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:
- return
- else:
- return func(*args, **kwargs)
- return decorator
-
-
-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/__init__.py b/src/toolkit/__init__.py
new file mode 100644
index 0000000..3fca275
--- /dev/null
+++ b/src/toolkit/__init__.py
@@ -0,0 +1 @@
+from toolkit.common import *
diff --git a/src/toolkit/common.py b/src/toolkit/common.py
new file mode 100644
index 0000000..e3a1649
--- /dev/null
+++ b/src/toolkit/common.py
@@ -0,0 +1,133 @@
+'''
+ Common functions
+'''
+from PyQt5 import QtWidgets
+import string
+import os
+import sys
+import subprocess
+from collections import OrderedDict
+
+
+def badName(name):
+ '''Returns whether a name contains non-alphanumeric chars'''
+ return any([letter in string.punctuation for letter in name])
+
+
+def alphabetizeDict(dictionary):
+ '''Alphabetizes a dict into OrderedDict '''
+ return OrderedDict(sorted(dictionary.items(), key=lambda t: t[0]))
+
+
+def presetToString(dictionary):
+ '''Returns string repr of a preset'''
+ return repr(alphabetizeDict(dictionary))
+
+
+def presetFromString(string):
+ '''Turns a string repr of OrderedDict into a regular dict'''
+ return dict(eval(string))
+
+
+def appendUppercase(lst):
+ for form, i in zip(lst, range(len(lst))):
+ lst.append(form.upper())
+ return lst
+
+
+def hideCmdWin(func):
+ ''' Stops CMD window from appearing on Windows.
+ Adapted from here: http://code.activestate.com/recipes/409002/
+ '''
+ def decorator(commandList, **kwargs):
+ if sys.platform == 'win32':
+ startupinfo = subprocess.STARTUPINFO()
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+ kwargs['startupinfo'] = startupinfo
+ return func(commandList, **kwargs)
+ return decorator
+
+
+@hideCmdWin
+def checkOutput(commandList, **kwargs):
+ return subprocess.check_output(commandList, **kwargs)
+
+
+@hideCmdWin
+def openPipe(commandList, **kwargs):
+ return subprocess.Popen(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:
+ return
+ else:
+ return func(*args, **kwargs)
+ return decorator
+
+
+def pickColor():
+ '''
+ Use color picker to get color input from the user,
+ and return this as an RGB string and QPushButton stylesheet.
+ In a subclass apply stylesheet to any color selection widgets
+ '''
+ dialog = QtWidgets.QColorDialog()
+ dialog.setOption(QtWidgets.QColorDialog.ShowAlphaChannel, True)
+ color = dialog.getColor()
+ if color.isValid():
+ RGBstring = '%s,%s,%s' % (
+ str(color.red()), str(color.green()), str(color.blue()))
+ btnStyle = "QPushButton{background-color: %s; outline: none;}" \
+ % color.name()
+ return RGBstring, btnStyle
+ else:
+ return None, None
+
+
+def rgbFromString(string):
+ '''Turns an RGB string like "255, 255, 255" into a tuple'''
+ try:
+ tup = tuple([int(i) for i in string.split(',')])
+ if len(tup) != 3:
+ raise ValueError
+ for i in tup:
+ if i > 255 or i < 0:
+ raise ValueError
+ return tup
+ except:
+ return (255, 255, 255)
+
+
+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/frame.py b/src/toolkit/frame.py
new file mode 100644
index 0000000..cddb611
--- /dev/null
+++ b/src/toolkit/frame.py
@@ -0,0 +1,66 @@
+'''
+ Common tools for drawing compatible frames in a Component's frameRender()
+'''
+from PyQt5 import QtGui
+from PIL import Image
+from PIL.ImageQt import ImageQt
+import sys
+import os
+
+
+class Frame:
+ '''Controller class for all frames.'''
+
+
+class FramePainter(QtGui.QPainter):
+ '''
+ A QPainter for a blank frame, which can be converted into a
+ Pillow image with finalize()
+ '''
+ def __init__(self, width, height):
+ image = BlankFrame(width, height)
+ self.image = QtGui.QImage(ImageQt(image))
+ super().__init__(self.image)
+
+ def setPen(self, RgbTuple):
+ super().setPen(PaintColor(*RgbTuple))
+
+ def finalize(self):
+ self.end()
+ imBytes = self.image.bits().asstring(self.image.byteCount())
+
+ return Image.frombytes(
+ 'RGBA', (self.image.width(), self.image.height()), imBytes
+ )
+
+
+class PaintColor(QtGui.QColor):
+ '''Reverse the painter colour if the hardware stores RGB values backward'''
+ def __init__(self, r, g, b, a=255):
+ if sys.byteorder == 'big':
+ super().__init__(r, g, b, a)
+ else:
+ super().__init__(b, g, r, a)
+
+
+def FloodFrame(width, height, RgbaTuple):
+ return Image.new("RGBA", (width, height), RgbaTuple)
+
+
+def BlankFrame(width, height):
+ '''The base frame used by each component to start drawing.'''
+ return FloodFrame(width, height, (0, 0, 0, 0))
+
+
+def Checkerboard(width, height):
+ '''
+ A checkerboard to represent transparency to the user.
+ TODO: Would be cool to generate this image with numpy instead.
+ '''
+ image = FloodFrame(1920, 1080, (0, 0, 0, 0))
+ image.paste(Image.open(
+ os.path.join(Frame.core.wd, "background.png")),
+ (0, 0)
+ )
+ image = image.resize((width, height))
+ return image
diff --git a/src/video_thread.py b/src/video_thread.py
index 60db99f..1f2eaf5 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -19,7 +19,7 @@ import time
import signal
from toolkit import openPipe
-from frame import Checkerboard
+from toolkit.frame import Checkerboard
class Worker(QtCore.QObject):
--
cgit v1.2.3
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
---
freeze.py | 7 +-
setup.py | 15 +-
src/command.py | 10 +-
src/component.py | 167 +++++++++++++-------
src/components/color.py | 8 +-
src/components/image.py | 11 +-
src/components/original.py | 11 +-
src/components/sound.py | 14 +-
src/components/text.py | 8 +-
src/components/video.py | 23 ++-
src/core.py | 379 ++++++++-------------------------------------
src/mainwindow.py | 81 ++++++----
src/presetmanager.py | 20 +--
src/preview_thread.py | 4 +-
src/toolkit/common.py | 12 +-
src/toolkit/core.py | 18 +++
src/toolkit/ffmpeg.py | 284 +++++++++++++++++++++++++++++++++
src/toolkit/frame.py | 6 +-
src/video_thread.py | 45 ++++--
19 files changed, 628 insertions(+), 495 deletions(-)
create mode 100644 src/toolkit/core.py
create mode 100644 src/toolkit/ffmpeg.py
(limited to 'src/video_thread.py')
diff --git a/freeze.py b/freeze.py
index c9b7918..3281cad 100644
--- a/freeze.py
+++ b/freeze.py
@@ -2,8 +2,8 @@ from cx_Freeze import setup, Executable
import sys
import os
-# Dependencies are automatically detected, but it might need
-# fine tuning.
+from setup import VERSION
+
deps = [os.path.join('src', p) for p in os.listdir('src') if p]
deps.append('ffmpeg.exe' if sys.platform == 'win32' else 'ffmpeg')
@@ -39,7 +39,6 @@ buildOptions = dict(
include_files=deps,
)
-
base = 'Win32GUI' if sys.platform == 'win32' else None
executables = [
@@ -53,7 +52,7 @@ executables = [
setup(
name='audio-visualizer-python',
- version='2.0',
+ 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 6ef688a..5abb976 100644
--- a/setup.py
+++ b/setup.py
@@ -2,6 +2,9 @@ from setuptools import setup
import os
+VERSION = '2.0.0.rc1'
+
+
def package_files(directory):
paths = []
for (path, directories, filenames) in os.walk(directory):
@@ -12,7 +15,7 @@ def package_files(directory):
setup(
name='audio_visualizer_python',
- version='2.0.0rc1',
+ 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',
@@ -20,8 +23,7 @@ setup(
"them as Projects to continue editing later. Different components can "
"be added and layered to add visualizers, images, videos, gradients, "
"text, etc. Use Projects created in the GUI with commandline mode to "
- "automate your video production workflow without learning any complex "
- "syntax.",
+ "automate your video production workflow without any complex syntax.",
classifiers=[
'Development Status :: 4 - Beta',
'License :: OSI Approved :: MIT License',
@@ -29,10 +31,13 @@ setup(
'Intended Audience :: End Users/Desktop',
'Topic :: Multimedia :: Video :: Non-Linear Editor',
],
- keywords=['visualizer', 'visualization', 'commandline video',
- 'video editor', 'ffmpeg', 'podcast'],
+ keywords=[
+ 'visualizer', 'visualization', 'commandline video',
+ 'video editor', 'ffmpeg', 'podcast'
+ ],
packages=[
'avpython',
+ 'avpython.toolkit',
'avpython.components'
],
package_dir={'avpython': 'src'},
diff --git a/src/command.py b/src/command.py
index 84d798d..046a1bf 100644
--- a/src/command.py
+++ b/src/command.py
@@ -9,8 +9,8 @@ import os
import sys
import time
-import core
-from toolkit import LoadDefaultSettings
+from core import Core
+from toolkit import loadDefaultSettings
class Command(QtCore.QObject):
@@ -19,7 +19,7 @@ class Command(QtCore.QObject):
def __init__(self):
QtCore.QObject.__init__(self)
- self.core = core.Core()
+ self.core = Core()
self.dataDir = self.core.dataDir
self.canceled = False
@@ -54,8 +54,8 @@ class Command(QtCore.QObject):
nargs='*', action='append')
self.args = self.parser.parse_args()
- self.settings = self.core.settings
- LoadDefaultSettings(self)
+ 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 7842bd6..92cc65c 100644
--- a/src/component.py
+++ b/src/component.py
@@ -1,33 +1,87 @@
'''
- Base classes for components to import.
+ Base classes for components to import. Read comments for some documentation
+ on making a valid component.
'''
from PyQt5 import uic, QtCore, QtWidgets
import os
+from core import Core
+from toolkit.common import getPresetDir
-class Component(QtCore.QObject):
+
+class ComponentMetaclass(type(QtCore.QObject)):
+ '''
+ Checks the validity of each Component class imported, and
+ mutates some attributes for easier use by the core program.
+ E.g., takes only major version from version string & decorates methods
+ '''
+ def __new__(cls, name, parents, attrs):
+ # print('Creating %s component' % attrs['name'])
+
+ # Turn certain class methods into properties and classmethods
+ for key in ('error', 'properties', 'audio', 'commandHelp'):
+ if key not in attrs:
+ continue
+ attrs[key] = property(attrs[key])
+
+ for key in ('names'):
+ if key not in attrs:
+ continue
+ attrs[key] = classmethod(key)
+
+ # Turn version string into a number
+ try:
+ if 'version' not in attrs:
+ print(
+ 'No version attribute in %s. Defaulting to 1' %
+ attrs['name'])
+ attrs['version'] = 1
+ else:
+ attrs['version'] = int(attrs['version'].split('.')[0])
+ except ValueError:
+ print('%s component has an invalid version string:\n%s' % (
+ attrs['name'], str(attrs['version'])))
+ except KeyError:
+ print('%s component has no version string.' % attrs['name'])
+ else:
+ return super().__new__(cls, name, parents, attrs)
+ quit(1)
+
+
+class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
- A class for components to inherit. Read comments for documentation
- on making a valid component. All subclasses must implement this signal:
- modified = QtCore.pyqtSignal(int, bool)
+ The base class for components to inherit.
'''
- def __init__(self, moduleIndex, compPos, core):
+ name = 'Component'
+ version = '1.0.0'
+ # The 1st number (before dot, aka the major version) 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):
super().__init__()
self.currentPreset = None
- self.canceled = False
self.moduleIndex = moduleIndex
self.compPos = compPos
- self.core = core
+
+ # Stop lengthy processes in response to this variable
+ self.canceled = False
def __str__(self):
- return self.__doc__
+ return self.__class__.name
- def version(self):
- '''
- Change this number to identify new versions of a component
- '''
- return 1
+ def __repr__(self):
+ return '%s\n%s\n%s' % (
+ self.__class__.name, str(self.__class__.version), self.savePreset()
+ )
+
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # Properties
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
def properties(self):
'''
@@ -43,19 +97,32 @@ class Component(QtCore.QObject):
'''
return
- def cancel(self):
+ def audio(self):
'''
- Stop any lengthy process in response to this variable
+ Return audio to mix into master as a tuple with two elements:
+ The first element can be:
+ - A string (path to audio file),
+ - Or an object that returns audio data through a pipe
+ The second element must be a dictionary of ffmpeg filters/options
+ to apply to the input stream. See the filter docs for ideas:
+ https://ffmpeg.org/ffmpeg-filters.html
'''
- self.canceled = True
- def reset(self):
- self.canceled = False
-
- def update(self):
+ def names():
'''
- Read your widget values from self.page, then call super().update()
+ Alternative names for renaming a component between project files.
'''
+ 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
@@ -92,7 +159,7 @@ class Component(QtCore.QObject):
'''
if arg.startswith('preset='):
_, preset = arg.split('=', 1)
- path = os.path.join(self.core.getPresetDir(self), preset)
+ path = os.path.join(getPresetDir(self), preset)
if not os.path.exists(path):
print('Couldn\'t locate preset "%s"' % preset)
quit(1)
@@ -106,14 +173,19 @@ class Component(QtCore.QObject):
self.__doc__, 'Usage:\n'
'Open a preset for this component:\n'
' "preset=Preset Name"')
- self.commandHelp()
+ print(self.commandHelp)
quit(0)
- def commandHelp(self):
- '''Print help text for this Component's commandline arguments'''
-
def loadUi(self, filename):
- return uic.loadUi(os.path.join(self.core.componentsPath, filename))
+ '''Load a Qt Designer ui file to use for this component's widget'''
+ return uic.loadUi(os.path.join(Core.componentsPath, filename))
+
+ def cancel(self):
+ '''Stop any lengthy process in response to this variable.'''
+ self.canceled = True
+
+ def reset(self):
+ self.canceled = False
'''
### Reference methods for creating a new component
@@ -121,47 +193,34 @@ class Component(QtCore.QObject):
def widget(self, parent):
self.parent = parent
- page = self.loadUi('example.ui')
+ self.settings = parent.settings
+ self.page = self.loadUi('example.ui')
# --- connect widget signals here ---
- self.page = page
- return page
+ return self.page
def previewRender(self, previewWorker):
- width = int(previewWorker.core.settings.value('outputWidth'))
+ width = int(self.settings.value('outputWidth'))
height = int(previewWorker.core.settings.value('outputHeight'))
- from frame import BlankFrame
+ from toolkit.frame import BlankFrame
image = BlankFrame(width, height)
return image
def frameRender(self, layerNo, frameNo):
audioArrayIndex = frameNo * self.sampleSize
- width = int(self.worker.core.settings.value('outputWidth'))
- height = int(self.worker.core.settings.value('outputHeight'))
- from frame import BlankFrame
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
+ from toolkit.frame import BlankFrame
image = BlankFrame(width, height)
return image
-
- def audio(self):
- \'''
- Return audio to mix into master as a tuple with two elements:
- The first element can be:
- - A string (path to audio file),
- - Or an object that returns audio data through a pipe
- The second element must be a dictionary of ffmpeg filters/options
- to apply to the input stream. See the filter docs for ideas:
- https://ffmpeg.org/ffmpeg-filters.html
- \'''
-
- @classmethod
- def names(cls):
- \'''
- Alternative names for renaming a component between project files.
- \'''
- return []
'''
class BadComponentInit(Exception):
+ '''
+ General purpose exception 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.
+ '''
def __init__(self, arg, name):
string = '''################################
Mandatory argument "%s" not specified
diff --git a/src/components/color.py b/src/components/color.py
index 8d2526d..03371e7 100644
--- a/src/components/color.py
+++ b/src/components/color.py
@@ -10,13 +10,12 @@ from toolkit import rgbFromString, pickColor
class Component(Component):
- '''Color'''
-
- modified = QtCore.pyqtSignal(int, dict)
+ name = 'Color'
+ version = '1.0.0'
def widget(self, parent):
self.parent = parent
- self.settings = self.parent.core.settings
+ self.settings = parent.settings
page = self.loadUi('color.ui')
self.color1 = (0, 0, 0)
@@ -211,7 +210,6 @@ class Component(Component):
def savePreset(self):
return {
- 'preset': self.currentPreset,
'color1': self.color1,
'color2': self.color2,
'x': self.x,
diff --git a/src/components/image.py b/src/components/image.py
index 7f3f610..591e03e 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -2,18 +2,18 @@ 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
class Component(Component):
- '''Image'''
-
- modified = QtCore.pyqtSignal(int, dict)
+ name = 'Image'
+ version = '1.0.0'
def widget(self, parent):
self.parent = parent
- self.settings = self.parent.core.settings
+ self.settings = parent.settings
page = self.loadUi('image.ui')
page.lineEdit_image.textChanged.connect(self.update)
@@ -102,7 +102,6 @@ class Component(Component):
def savePreset(self):
return {
- 'preset': self.currentPreset,
'image': self.imagePath,
'scale': self.scale,
'color': self.color,
@@ -117,7 +116,7 @@ class Component(Component):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Image", imgDir,
- "Image Files (%s)" % " ".join(self.core.imageFormats))
+ "Image Files (%s)" % " ".join(Core.imageFormats))
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_image.setText(filename)
diff --git a/src/components/original.py b/src/components/original.py
index 586204a..ae40df3 100644
--- a/src/components/original.py
+++ b/src/components/original.py
@@ -12,17 +12,15 @@ from toolkit import rgbFromString, pickColor
class Component(Component):
- '''Classic Visualizer'''
+ name = 'Classic Visualizer'
+ version = '1.0.0'
- modified = QtCore.pyqtSignal(int, dict)
-
- @classmethod
- def names(cls):
+ def names():
return ['Original Audio Visualization']
def widget(self, parent):
self.parent = parent
- self.settings = self.parent.core.settings
+ self.settings = parent.settings
self.visColor = (255, 255, 255)
self.scale = 20
self.y = 0
@@ -68,7 +66,6 @@ class Component(Component):
def savePreset(self):
return {
- 'preset': self.currentPreset,
'layout': self.layout,
'visColor': self.visColor,
'scale': self.scale,
diff --git a/src/components/sound.py b/src/components/sound.py
index 5b06405..677a22f 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -1,14 +1,14 @@
from PyQt5 import QtGui, QtCore, QtWidgets
import os
+from core import Core
from component import Component
from toolkit.frame import BlankFrame
class Component(Component):
- '''Sound'''
-
- modified = QtCore.pyqtSignal(int, dict)
+ name = 'Sound'
+ version = '1.0.0'
def widget(self, parent):
self.parent = parent
@@ -32,8 +32,8 @@ class Component(Component):
super().update()
def previewRender(self, previewWorker):
- width = int(previewWorker.core.settings.value('outputWidth'))
- height = int(previewWorker.core.settings.value('outputHeight'))
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
return BlankFrame(width, height)
def preFrameRender(self, **kwargs):
@@ -67,7 +67,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(self.core.audioFormats))
+ "Audio Files (%s)" % " ".join(Core.audioFormats))
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_sound.setText(filename)
@@ -101,7 +101,7 @@ class Component(Component):
key, arg = arg.split('=', 1)
if key == 'path':
if '*%s' % os.path.splitext(arg)[1] \
- not in self.core.audioFormats:
+ not in 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 fc3ef5f..d511f22 100644
--- a/src/components/text.py
+++ b/src/components/text.py
@@ -9,9 +9,8 @@ from toolkit import rgbFromString, pickColor
class Component(Component):
- '''Title Text'''
-
- modified = QtCore.pyqtSignal(int, dict)
+ name = 'Title Text'
+ version = '1.0.0'
def __init__(self, *args):
super().__init__(*args)
@@ -19,7 +18,7 @@ class Component(Component):
def widget(self, parent):
self.parent = parent
- self.settings = self.parent.core.settings
+ self.settings = parent.settings
height = int(self.settings.value('outputHeight'))
width = int(self.settings.value('outputWidth'))
@@ -106,7 +105,6 @@ class Component(Component):
def savePreset(self):
return {
- 'preset': self.currentPreset,
'title': self.title,
'titleFont': self.titleFont.toString(),
'alignment': self.alignment,
diff --git a/src/components/video.py b/src/components/video.py
index a9f334e..b35c2e5 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -6,6 +6,7 @@ import subprocess
import threading
from queue import PriorityQueue
+from core import Core
from component import Component, BadComponentInit
from toolkit.frame import BlankFrame
from toolkit import openPipe, checkOutput
@@ -106,9 +107,8 @@ class Video:
class Component(Component):
- '''Video'''
-
- modified = QtCore.pyqtSignal(int, dict)
+ name = 'Video'
+ version = '1.0.0'
def widget(self, parent):
self.parent = parent
@@ -154,8 +154,8 @@ class Component(Component):
super().update()
def previewRender(self, previewWorker):
- width = int(previewWorker.core.settings.value('outputWidth'))
- height = int(previewWorker.core.settings.value('outputHeight'))
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
self.updateChunksize(width, height)
frame = self.getPreviewFrame(width, height)
if not frame:
@@ -190,7 +190,7 @@ class Component(Component):
def testAudioStream(self):
# test if an audio stream really exists
audioTestCommand = [
- self.core.FFMPEG_BIN,
+ Core.FFMPEG_BIN,
'-i', self.videoPath,
'-vn', '-f', 'null', '-'
]
@@ -209,12 +209,12 @@ class Component(Component):
def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs)
- width = int(self.worker.core.settings.value('outputWidth'))
- height = int(self.worker.core.settings.value('outputHeight'))
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
self.blankFrame_ = BlankFrame(width, height)
self.updateChunksize(width, height)
self.video = Video(
- ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath,
+ ffmpeg=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,
@@ -240,7 +240,6 @@ class Component(Component):
def savePreset(self):
return {
- 'preset': self.currentPreset,
'video': self.videoPath,
'loop': self.loopVideo,
'useAudio': self.useAudio,
@@ -255,7 +254,7 @@ class Component(Component):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Video",
- imgDir, "Video Files (%s)" % " ".join(self.core.videoFormats)
+ imgDir, "Video Files (%s)" % " ".join(Core.videoFormats)
)
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
@@ -298,7 +297,7 @@ class Component(Component):
if not arg.startswith('preset=') and '=' in arg:
key, arg = arg.split('=', 1)
if key == 'path' and os.path.exists(arg):
- if '*%s' % os.path.splitext(arg)[1] in self.core.videoFormats:
+ if '*%s' % os.path.splitext(arg)[1] in 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 07c1f71..dd2ef18 100644
--- a/src/core.py
+++ b/src/core.py
@@ -1,46 +1,56 @@
'''
Home to the Core class which tracks program state. Used by GUI & commandline
'''
+from PyQt5 import QtCore, QtGui, uic
import sys
import os
-from PyQt5 import QtCore, QtGui, uic
-import subprocess as sp
-import numpy
import json
from importlib import import_module
-from PyQt5.QtCore import QStandardPaths
import toolkit
-from toolkit.frame import Frame
+from toolkit.ffmpeg import findFfmpeg
import video_thread
class Core:
'''
MainWindow and Command module both use an instance of this class
- to store the program state. This object tracks the components,
- opens projects and presets, and stores settings/paths to data.
+ 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.
'''
- def __init__(self):
- Frame.core = self
- self.dataDir = QStandardPaths.writableLocation(
- QStandardPaths.AppConfigLocation
- )
- self.presetDir = os.path.join(self.dataDir, 'presets')
+
+ @classmethod
+ def storeSettings(cls):
+ '''
+ Stores settings/paths to directories as class variables
+ '''
if getattr(sys, 'frozen', False):
# frozen
- self.wd = os.path.dirname(sys.executable)
+ wd = os.path.dirname(sys.executable)
else:
- # unfrozen
- self.wd = os.path.dirname(os.path.realpath(__file__))
- self.componentsPath = os.path.join(self.wd, 'components')
- self.settings = QtCore.QSettings(
- os.path.join(self.dataDir, 'settings.ini'),
- QtCore.QSettings.IniFormat
- )
+ wd = os.path.dirname(os.path.realpath(__file__))
- self.loadEncoderOptions()
- self.videoFormats = toolkit.appendUppercase([
+ 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',
@@ -48,7 +58,7 @@ class Core:
'*.webm',
'*.flv',
])
- self.audioFormats = toolkit.appendUppercase([
+ settings['audioFormats'] = toolkit.appendUppercase([
'*.mp3',
'*.wav',
'*.ogg',
@@ -56,7 +66,7 @@ class Core:
'*.flac',
'*.aac',
])
- self.imageFormats = toolkit.appendUppercase([
+ settings['imageFormats'] = toolkit.appendUppercase([
'*.png',
'*.jpg',
'*.tif',
@@ -68,15 +78,22 @@ class Core:
'*.xpm',
])
- self.FFMPEG_BIN = self.findFfmpeg()
+ # 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 = []
- # copies of named presets to detect modification
- self.savedPresets = {}
+ self.savedPresets = {} # copies of presets to detect modification
def findComponents(self):
def findComponents():
- for f in sorted(os.listdir(self.componentsPath)):
+ for f in sorted(os.listdir(Core.componentsPath)):
name, ext = os.path.splitext(f)
if name.startswith("__"):
continue
@@ -88,7 +105,7 @@ class Core:
]
# store canonical module names and indexes
self.moduleIndexes = [i for i in range(len(self.modules))]
- self.compNames = [mod.Component.__doc__ for mod in self.modules]
+ self.compNames = [mod.Component.name for mod in self.modules]
self.altCompNames = []
# store alternative names for modules
for i, mod in enumerate(self.modules):
@@ -108,7 +125,7 @@ class Core:
return None
component = self.modules[moduleIndex].Component(
- moduleIndex, compPos, self
+ moduleIndex, compPos
)
self.selectedComponents.insert(
compPos,
@@ -171,10 +188,6 @@ class Core:
self.savedPresets[presetName] = dict(saveValueStore)
return True
- def getPresetDir(self, comp):
- return os.path.join(
- self.presetDir, str(comp), str(comp.version()))
-
def getPreset(self, filepath):
'''Returns the preset dict stored at this filepath'''
if not os.path.exists(filepath):
@@ -204,7 +217,7 @@ class Core:
widget.blockSignals(False)
for key, value in data['Settings']:
- self.settings.setValue(key, value)
+ Core.settings.setValue(key, value)
for tup in data['Components']:
name, vers, preset = tup
@@ -215,7 +228,7 @@ class Core:
if 'preset' in preset and preset['preset'] is not None:
nam = preset['preset']
filepath2 = os.path.join(
- self.presetDir, name, str(vers), nam)
+ Core.presetDir, name, str(vers), nam)
origSaveValueStore = self.getPreset(filepath2)
if origSaveValueStore:
self.savedPresets[nam] = dict(origSaveValueStore)
@@ -336,7 +349,7 @@ class Core:
presetName = preset['preset'] \
if preset['preset'] else os.path.basename(filepath)[:-4]
newPath = os.path.join(
- self.presetDir,
+ Core.presetDir,
name,
vers,
presetName
@@ -354,7 +367,7 @@ class Core:
def exportPreset(self, exportPath, compName, vers, origName):
internalPath = os.path.join(
- self.presetDir, compName, str(vers), origName
+ Core.presetDir, compName, str(vers), origName
)
if not os.path.exists(internalPath):
return
@@ -378,7 +391,7 @@ class Core:
'''Create a preset file (.avl) at filepath using args.
Or if filepath is empty, create an internal preset using args'''
if not filepath:
- dirname = os.path.join(self.presetDir, compName, str(vers))
+ dirname = os.path.join(Core.presetDir, compName, str(vers))
if not os.path.exists(dirname):
os.makedirs(dirname)
filepath = os.path.join(dirname, presetName)
@@ -417,13 +430,13 @@ class Core:
saveValueStore = comp.savePreset()
saveValueStore['preset'] = comp.currentPreset
f.write('%s\n' % str(comp))
- f.write('%s\n' % str(comp.version()))
+ f.write('%s\n' % str(comp.version))
f.write('%s\n' % toolkit.presetToString(saveValueStore))
f.write('\n[Settings]\n')
- for key in self.settings.allKeys():
+ for key in Core.settings.allKeys():
if key in settingsKeys:
- f.write('%s=%s\n' % (key, self.settings.value(key)))
+ f.write('%s=%s\n' % (key, Core.settings.value(key)))
if window:
f.write('\n[WindowFields]\n')
@@ -438,280 +451,8 @@ class Core:
except:
return False
- def loadEncoderOptions(self):
- file_path = os.path.join(self.wd, 'encoder-options.json')
- with open(file_path) as json_file:
- self.encoder_options = json.load(json_file)
-
- def findFfmpeg(self):
- if getattr(sys, 'frozen', False):
- # The application is frozen
- if sys.platform == "win32":
- return os.path.join(self.wd, 'ffmpeg.exe')
- else:
- return os.path.join(self.wd, 'ffmpeg')
-
- else:
- if sys.platform == "win32":
- return "ffmpeg"
- else:
- try:
- with open(os.devnull, "w") as f:
- toolkit.checkOutput(
- ['ffmpeg', '-version'], stderr=f
- )
- return "ffmpeg"
- except sp.CalledProcessError:
- return "avconv"
-
- def createFfmpegCommand(self, inputFile, outputFile, duration):
- '''
- Constructs the major ffmpeg command used to export the video
- '''
- 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 = toolkit.checkOutput(
- "%s -encoders -hide_banner" % self.FFMPEG_BIN, shell=True
- )
- encoders = encoders.decode("utf-8")
-
- acodec = self.settings.value('outputAudioCodec')
-
- options = self.encoder_options
- containerName = self.settings.value('outputContainer')
- vcodec = self.settings.value('outputVideoCodec')
- vbitrate = str(self.settings.value('outputVideoBitrate'))+'k'
- acodec = self.settings.value('outputAudioCodec')
- abitrate = str(self.settings.value('outputAudioBitrate'))+'k'
-
- for cont in options['containers']:
- if cont['name'] == containerName:
- container = cont['container']
- break
-
- vencoders = options['video-codecs'][vcodec]
- aencoders = options['audio-codecs'][acodec]
-
- for encoder in vencoders:
- if encoder in encoders:
- vencoder = encoder
- break
-
- for encoder in aencoders:
- if encoder in encoders:
- aencoder = encoder
- break
-
- ffmpegCommand = [
- self.FFMPEG_BIN,
- '-thread_queue_size', '512',
- '-y', # overwrite the output file if it already exists.
-
- # INPUT VIDEO
- '-f', 'rawvideo',
- '-vcodec', 'rawvideo',
- '-s', '%sx%s' % (
- self.settings.value('outputWidth'),
- self.settings.value('outputHeight'),
- ),
- '-pix_fmt', 'rgba',
- '-r', self.settings.value('outputFrameRate'),
- '-t', duration,
- '-i', '-', # the video input comes from a pipe
- '-an', # the video input has no sound
-
- # INPUT SOUND
- '-t', duration,
- '-i', inputFile
- ]
-
- # 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 self.selectedComponents
- 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 = self.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', self.settings.value('outputVideoFormat'),
- '-preset', self.settings.value('outputPreset'),
- '-f', container
- ])
-
- if acodec == 'aac':
- ffmpegCommand.append('-strict')
- ffmpegCommand.append('-2')
-
- ffmpegCommand.append(outputFile)
- return ffmpegCommand
-
- def getAudioDuration(self, filename):
- command = [self.FFMPEG_BIN, '-i', filename]
-
- try:
- fileInfo = toolkit.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(self, filename, parent):
- duration = self.getAudioDuration(filename)
-
- command = [
- self.FFMPEG_BIN,
- '-i', filename,
- '-f', 's16le',
- '-acodec', 'pcm_s16le',
- '-ar', '44100', # ouput will have 44100 Hz
- '-ac', '1', # mono (set to '2' for stereo)
- '-']
- in_pipe = toolkit.openPipe(
- command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8
- )
-
- completeAudioArray = numpy.empty(0, dtype="int16")
-
- progress = 0
- lastPercent = None
- while True:
- if self.canceled:
- break
- # read 2 seconds of audio
- progress += 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)
-
def newVideoWorker(self, loader, audioFile, outputPath):
+ '''loader is MainWindow or Command object which must own the thread'''
self.videoThread = QtCore.QThread(loader)
videoWorker = video_thread.Worker(
loader, audioFile, outputPath, self.selectedComponents
@@ -727,7 +468,9 @@ class Core:
self.videoThread.wait()
def cancel(self):
- self.canceled = True
+ Core.canceled = True
+ toolkit.cancel()
def reset(self):
- self.canceled = False
+ Core.canceled = False
+ toolkit.reset()
diff --git a/src/mainwindow.py b/src/mainwindow.py
index ca8e697..9944d1a 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -14,13 +14,17 @@ import signal
import filecmp
import time
-import core
+from core import Core
import preview_thread
from presetmanager import PresetManager
-from toolkit import LoadDefaultSettings, disableWhenEncoding, checkOutput
+from toolkit import loadDefaultSettings, disableWhenEncoding, checkOutput
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
@@ -47,6 +51,14 @@ class PreviewWindow(QtWidgets.QLabel):
class MainWindow(QtWidgets.QMainWindow):
+ '''
+ The MainWindow wraps many Core methods in order to update the GUI
+ accordingly. E.g., instead of self.core.openProject(), it will use
+ self.openProject() and update the window titlebar within the wrapper.
+
+ MainWindow manages the autosave feature, although Core has the
+ primary functions for opening and creating project files.
+ '''
createVideo = QtCore.pyqtSignal()
newTask = QtCore.pyqtSignal(list) # for the preview window
@@ -57,25 +69,26 @@ class MainWindow(QtWidgets.QMainWindow):
# print('main thread id: {}'.format(QtCore.QThread.currentThreadId()))
self.window = window
- self.core = core.Core()
+ self.core = Core()
self.pages = [] # widgets of component settings
self.lastAutosave = time.time()
self.encoding = False
# Create data directory, load/create settings
- self.dataDir = self.core.dataDir
+ self.dataDir = Core.dataDir
+ self.presetDir = Core.presetDir
self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
- self.settings = self.core.settings
- LoadDefaultSettings(self)
+ self.settings = Core.settings
+ loadDefaultSettings(self)
self.presetManager = PresetManager(
uic.loadUi(
- os.path.join(self.core.wd, 'presetmanager.ui')), self)
+ os.path.join(Core.wd, 'presetmanager.ui')), self)
if not os.path.exists(self.dataDir):
os.makedirs(self.dataDir)
for neededDirectory in (
- self.core.presetDir, self.settings.value("projectDir")):
+ self.presetDir, self.settings.value("projectDir")):
if not os.path.exists(neededDirectory):
os.mkdir(neededDirectory)
@@ -120,7 +133,7 @@ class MainWindow(QtWidgets.QMainWindow):
window.pushButton_Cancel.clicked.connect(self.stopVideo)
- for i, container in enumerate(self.core.encoder_options['containers']):
+ for i, container in enumerate(Core.encoderOptions['containers']):
window.comboBox_videoContainer.addItem(container['name'])
if container['name'] == self.settings.value('outputContainer'):
selectedContainer = i
@@ -160,14 +173,14 @@ class MainWindow(QtWidgets.QMainWindow):
window.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings)
self.previewWindow = PreviewWindow(self, os.path.join(
- self.core.wd, "background.png"))
+ Core.wd, "background.png"))
window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
# Make component buttons
self.compMenu = QMenu()
self.compActions = []
for i, comp in enumerate(self.core.modules):
- action = self.compMenu.addAction(comp.Component.__doc__)
+ action = self.compMenu.addAction(comp.Component.name)
action.triggered.connect(
lambda _, item=i: self.core.insertComponent(0, item, self)
)
@@ -336,8 +349,14 @@ class MainWindow(QtWidgets.QMainWindow):
"Ctrl+Down", self.window,
activated=lambda: self.moveComponent(1)
)
- QtWidgets.QShortcut("Ctrl+Home", self.window, self.moveComponentTop)
- QtWidgets.QShortcut("Ctrl+End", self.window, self.moveComponentBottom)
+ QtWidgets.QShortcut(
+ "Ctrl+Home", self.window,
+ activated=lambda: self.moveComponent('top')
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+End", self.window,
+ activated=lambda: self.moveComponent('bottom')
+ )
QtWidgets.QShortcut("Ctrl+r", self.window, self.removeComponent)
@QtCore.pyqtSlot()
@@ -389,7 +408,7 @@ class MainWindow(QtWidgets.QMainWindow):
vCodecWidget.clear()
aCodecWidget.clear()
- for container in self.core.encoder_options['containers']:
+ for container in Core.encoderOptions['containers']:
if container['name'] == name:
for vCodec in container['video-codecs']:
vCodecWidget.addItem(vCodec)
@@ -397,6 +416,7 @@ class MainWindow(QtWidgets.QMainWindow):
aCodecWidget.addItem(aCodec)
def updateCodecSettings(self):
+ '''Updates settings.ini to match encoder option widgets'''
vCodecWidget = self.window.comboBox_videoCodec
vBitrateWidget = self.window.spinBox_vBitrate
aBitrateWidget = self.window.spinBox_aBitrate
@@ -416,11 +436,12 @@ class MainWindow(QtWidgets.QMainWindow):
if not self.currentProject:
if os.path.exists(self.autosavePath):
os.remove(self.autosavePath)
- elif force or time.time() - self.lastAutosave >= 0.1:
+ elif force or time.time() - self.lastAutosave >= 0.2:
self.core.createProjectFile(self.autosavePath, self.window)
self.lastAutosave = time.time()
def autosaveExists(self, identical=True):
+ '''Determines if creating the autosave should be blocked.'''
try:
if self.currentProject and os.path.exists(self.autosavePath) \
and filecmp.cmp(
@@ -432,6 +453,7 @@ class MainWindow(QtWidgets.QMainWindow):
return False
def saveProjectChanges(self):
+ '''Overwrites project file with autosave file'''
try:
os.remove(self.currentProject)
os.rename(self.autosavePath, self.currentProject)
@@ -447,7 +469,7 @@ class MainWindow(QtWidgets.QMainWindow):
fileName, _ = QtWidgets.QFileDialog.getOpenFileName(
self.window, "Open Audio File",
- inputDir, "Audio Files (%s)" % " ".join(self.core.audioFormats))
+ inputDir, "Audio Files (%s)" % " ".join(Core.audioFormats))
if fileName:
self.settings.setValue("inputDir", os.path.dirname(fileName))
@@ -460,7 +482,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.window, "Set Output Video File",
outputDir,
"Video Files (%s);; All Files (*)" % " ".join(
- self.core.videoFormats))
+ Core.videoFormats))
if fileName:
self.settings.setValue("outputDir", os.path.dirname(fileName))
@@ -587,10 +609,11 @@ class MainWindow(QtWidgets.QMainWindow):
def showFfmpegCommand(self):
from textwrap import wrap
- command = self.core.createFfmpegCommand(
+ from toolkit.ffmpeg import createFfmpegCommand
+ command = createFfmpegCommand(
self.window.lineEdit_audioFile.text(),
self.window.lineEdit_outputFile.text(),
- self.core.getAudioDuration(self.window.lineEdit_audioFile.text())
+ self.core.selectedComponents
)
lines = wrap(" ".join(command), 49)
self.showMessage(
@@ -603,7 +626,7 @@ class MainWindow(QtWidgets.QMainWindow):
componentList.insertItem(
index,
- self.core.selectedComponents[index].__doc__)
+ self.core.selectedComponents[index].name)
componentList.setCurrentRow(index)
# connect to signal that adds an asterisk when modified
@@ -632,6 +655,10 @@ class MainWindow(QtWidgets.QMainWindow):
def moveComponent(self, change):
'''Moves a component relatively from its current position'''
componentList = self.window.listWidget_componentList
+ if change == 'top':
+ change = -componentList.currentRow()
+ elif change == 'bottom':
+ change = len(componentList)-componentList.currentRow()-1
stackedWidget = self.window.stackedWidget
row = componentList.currentRow()
@@ -650,21 +677,9 @@ class MainWindow(QtWidgets.QMainWindow):
stackedWidget.setCurrentIndex(newRow)
self.drawPreview()
- @disableWhenEncoding
- def moveComponentTop(self):
- componentList = self.window.listWidget_componentList
- row = -componentList.currentRow()
- self.moveComponent(row)
-
- @disableWhenEncoding
- def moveComponentBottom(self):
- componentList = self.window.listWidget_componentList
- row = len(componentList)-componentList.currentRow()-1
- self.moveComponent(row)
-
@disableWhenEncoding
def dragComponent(self, event):
- '''Drop event for the component listwidget'''
+ '''Used as Qt drop event for the component listwidget'''
componentList = self.window.listWidget_componentList
modelIndexes = [
diff --git a/src/presetmanager.py b/src/presetmanager.py
index 6e003a1..825fdee 100644
--- a/src/presetmanager.py
+++ b/src/presetmanager.py
@@ -15,11 +15,11 @@ class PresetManager(QtWidgets.QDialog):
self.parent = parent
self.core = parent.core
self.settings = parent.settings
- self.presetDir = self.core.presetDir
+ self.presetDir = parent.presetDir
if not self.settings.value('presetDir'):
self.settings.setValue(
"presetDir",
- os.path.join(self.core.dataDir, 'projects'))
+ os.path.join(parent.dataDir, 'projects'))
self.findPresets()
@@ -161,7 +161,7 @@ class PresetManager(QtWidgets.QDialog):
selectedComponents[index].savePreset()
saveValueStore['preset'] = newName
componentName = str(selectedComponents[index]).strip()
- vers = selectedComponents[index].version()
+ vers = selectedComponents[index].version
self.createNewPreset(
componentName, vers, newName,
saveValueStore, window=self.parent.window)
@@ -195,13 +195,13 @@ class PresetManager(QtWidgets.QDialog):
def openPreset(self, presetName, compPos=None):
componentList = self.parent.window.listWidget_componentList
- selectedComponents = self.parent.core.selectedComponents
+ selectedComponents = self.core.selectedComponents
index = compPos if compPos is not None else componentList.currentRow()
if index == -1:
return
componentName = str(selectedComponents[index]).strip()
- version = selectedComponents[index].version()
+ version = selectedComponents[index].version
dirname = os.path.join(self.presetDir, componentName, str(version))
filepath = os.path.join(dirname, presetName)
self.core.openPreset(filepath, index, presetName)
@@ -243,6 +243,7 @@ class PresetManager(QtWidgets.QDialog):
parent=window if window else self.window)
def openRenamePresetDialog(self):
+ # TODO: maintain consistency by changing this to call createNewPreset()
presetList = self.window.listWidget_presets
if presetList.currentRow() == -1:
return
@@ -273,11 +274,12 @@ class PresetManager(QtWidgets.QDialog):
os.rename(oldPath, newPath)
self.findPresets()
self.drawPresetList()
-
for i, comp in enumerate(self.core.selectedComponents):
- if comp.currentPreset == oldName:
- comp.currentPreset = newName
- self.parent.updateComponentTitle(i, True)
+ if toolkit.getPresetDir(comp) == path \
+ and comp.currentPreset == oldName:
+ self.core.openPreset(newPath, i, newName)
+ self.parent.updateComponentTitle(i, False)
+ self.parent.drawPreview()
break
def openImportDialog(self):
diff --git a/src/preview_thread.py b/src/preview_thread.py
index c28e048..3fc73b3 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -22,8 +22,8 @@ class Worker(QtCore.QObject):
parent.newTask.connect(self.createPreviewImage)
parent.processTask.connect(self.process)
self.parent = parent
- self.core = self.parent.core
- self.settings = self.parent.core.settings
+ self.core = parent.core
+ self.settings = parent.settings
self.queue = queue
width = int(self.settings.value('outputWidth'))
diff --git a/src/toolkit/common.py b/src/toolkit/common.py
index e3a1649..763d582 100644
--- a/src/toolkit/common.py
+++ b/src/toolkit/common.py
@@ -8,6 +8,13 @@ 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'''
@@ -103,8 +110,9 @@ def rgbFromString(string):
return (255, 255, 255)
-def LoadDefaultSettings(self):
- ''' Runs once at each program start-up. Fills in default settings
+def loadDefaultSettings(self):
+ '''
+ Runs once at each program start-up. Fills in default settings
for any settings not found in settings.ini
'''
self.resolutions = [
diff --git a/src/toolkit/core.py b/src/toolkit/core.py
new file mode 100644
index 0000000..a96a684
--- /dev/null
+++ b/src/toolkit/core.py
@@ -0,0 +1,18 @@
+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
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)
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index cddb611..83fd59e 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -7,9 +7,7 @@ from PIL.ImageQt import ImageQt
import sys
import os
-
-class Frame:
- '''Controller class for all frames.'''
+from toolkit.common import Core
class FramePainter(QtGui.QPainter):
@@ -59,7 +57,7 @@ def Checkerboard(width, height):
'''
image = FloodFrame(1920, 1080, (0, 0, 0, 0))
image.paste(Image.open(
- os.path.join(Frame.core.wd, "background.png")),
+ os.path.join(Core.wd, "background.png")),
(0, 0)
)
image = image.resize((width, height))
diff --git a/src/video_thread.py b/src/video_thread.py
index 1f2eaf5..8517b92 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -5,9 +5,9 @@
are emitted to update MainWindow's progress bar, detail text, and preview.
Export can be cancelled with cancel()
'''
-from PyQt5 import QtCore, QtGui, uic
+from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import pyqtSignal, pyqtSlot
-from PIL import Image, ImageDraw, ImageFont
+from PIL import Image
from PIL.ImageQt import ImageQt
import numpy
import subprocess as sp
@@ -19,6 +19,7 @@ import time
import signal
from toolkit import openPipe
+from toolkit.ffmpeg import readAudioFile, createFfmpegCommand
from toolkit.frame import Checkerboard
@@ -33,7 +34,7 @@ class Worker(QtCore.QObject):
def __init__(self, parent, inputFile, outputFile, components):
QtCore.QObject.__init__(self)
self.core = parent.core
- self.settings = parent.core.settings
+ self.settings = parent.settings
self.modules = parent.core.modules
parent.createVideo.connect(self.createVideo)
@@ -133,12 +134,17 @@ class Worker(QtCore.QObject):
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
self.progressBarSetText.emit("Loading audio file...")
- self.completeAudioArray, duration = self.core.readAudioFile(
+ audioFileTraits = readAudioFile(
self.inputFile, self
)
+ if audioFileTraits is None:
+ self.cancelExport()
+ return
+ self.completeAudioArray, duration = audioFileTraits
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit("Starting components...")
+ canceledByComponent = False
print('Loaded Components:', ", ".join([
"%s) %s" % (num, str(component))
for num, component in enumerate(reversed(self.components))
@@ -153,14 +159,15 @@ class Worker(QtCore.QObject):
progressBarSetText=self.progressBarSetText
)
- 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' % (
+ if comp.error is None else 'Component #%s (%s): %s' % (
str(compNo),
str(comp),
- comp.error()
+ comp.error
)
self.parent.showMessage(
msg=errMsg,
@@ -168,17 +175,16 @@ class Worker(QtCore.QObject):
parent=None # MainWindow is in a different thread
)
break
- if 'static' in comp.properties():
+ if 'static' in comp.properties:
self.staticComponents[compNo] = \
comp.frameRender(compNo, 0).copy()
if self.canceled:
- print('Export cancelled by component #%s (%s): %s' % (
- compNo, str(comp), comp.error()
- ))
- self.progressBarSetText.emit('Export Canceled')
- self.encoding.emit(False)
- self.videoCreated.emit()
+ if canceledByComponent:
+ print('Export cancelled by component #%s (%s): %s' % (
+ compNo, str(comp), comp.error
+ ))
+ self.cancelExport()
return
# Merge consecutive static component frames together
@@ -192,8 +198,8 @@ class Worker(QtCore.QObject):
)
self.staticComponents[compNo] = None
- ffmpegCommand = self.core.createFfmpegCommand(
- self.inputFile, self.outputFile, duration
+ ffmpegCommand = createFfmpegCommand(
+ self.inputFile, self.outputFile, self.components, duration
)
print('###### FFMPEG COMMAND ######\n%s' % " ".join(ffmpegCommand))
print('############################')
@@ -280,7 +286,6 @@ class Worker(QtCore.QObject):
pass
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit('Export Canceled')
-
else:
if self.error:
print("Export Failed")
@@ -297,6 +302,12 @@ class Worker(QtCore.QObject):
self.encoding.emit(False)
self.videoCreated.emit()
+ def cancelExport(self):
+ self.progressBarUpdate.emit(0)
+ self.progressBarSetText.emit('Export Canceled')
+ self.encoding.emit(False)
+ self.videoCreated.emit()
+
def updateProgress(self, pStr, pVal):
self.progressBarValue.emit(pVal)
self.progressBarSetText.emit(pStr)
--
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/video_thread.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/video_thread.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 d92fc6373fd070f0ea303e9795eb7648d5cd9e90 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 23 Jul 2017 22:55:41 -0400
Subject: ComponentError exception wraps previewRender
probably where errors are likeliest to be found
---
src/command.py | 6 +++
src/component.py | 119 +++++++++++++++++++++++++++---------------------
src/components/video.py | 6 +--
src/core.py | 3 ++
src/mainwindow.py | 8 ++--
src/toolkit/frame.py | 18 ++++++++
src/video_thread.py | 4 +-
7 files changed, 104 insertions(+), 60 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/command.py b/src/command.py
index ca186e5..74ca821 100644
--- a/src/command.py
+++ b/src/command.py
@@ -146,6 +146,12 @@ class Command(QtCore.QObject):
if 'detail' in kwargs:
print(kwargs['detail'])
+ @QtCore.pyqtSlot(str, str)
+ def videoThreadError(self, msg, detail):
+ print(msg)
+ print(detail)
+ quit(1)
+
def drawPreview(self, *args):
pass
diff --git a/src/component.py b/src/component.py
index 8b5f1b8..41cb5eb 100644
--- a/src/component.py
+++ b/src/component.py
@@ -6,54 +6,64 @@ from PyQt5 import uic, QtCore, QtWidgets
import os
-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):
- 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
-
-
-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
mutates some attributes for easier use by the core program.
E.g., takes only major version from version string & decorates methods
'''
+
+ def renderWrapper(func):
+ def decorator(self, *args, **kwargs):
+ try:
+ return func(self, *args, **kwargs)
+ except:
+ from toolkit.frame import BlankFrame
+ try:
+ raise ComponentError(self, 'renderer', immediate=True)
+ except ComponentError:
+ return BlankFrame()
+ return decorator
+
+ 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):
+ 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
+
+ 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
+
def __new__(cls, name, parents, attrs):
if 'ui' not in attrs:
# Use module name as ui filename by default
@@ -62,7 +72,11 @@ class ComponentMetaclass(type(QtCore.QObject)):
)[0]
# if parents[0] == QtCore.QObject: else:
- decorate = ('names', 'error', 'audio', 'command', 'properties')
+ decorate = (
+ 'names', # Class methods
+ 'error', 'audio', 'properties', # Properties
+ 'previewRender', 'command',
+ )
# Auto-decorate methods
for key in decorate:
@@ -76,13 +90,16 @@ class ComponentMetaclass(type(QtCore.QObject)):
attrs[key] = property(attrs[key])
if key == 'command':
- attrs[key] = commandWrapper(attrs[key])
+ attrs[key] = cls.commandWrapper(attrs[key])
+
+ if key == 'previewRender':
+ attrs[key] = cls.renderWrapper(attrs[key])
if key == 'properties':
- attrs[key] = propertiesWrapper(attrs[key])
+ attrs[key] = cls.propertiesWrapper(attrs[key])
if key == 'error':
- attrs[key] = errorWrapper(attrs[key])
+ attrs[key] = cls.errorWrapper(attrs[key])
# Turn version string into a number
try:
@@ -223,11 +240,11 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
if kwarg in ('presetNames', 'commandArgs'):
setattr(self, '_%s' % kwarg, kwargs[kwarg])
else:
- raise BadComponentInit(
+ raise ComponentError(
self,
'Nonsensical keywords to trackWidgets.',
immediate=True)
- except BadComponentInit:
+ except ComponentError:
continue
def update(self):
@@ -366,7 +383,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
-class BadComponentInit(AttributeError):
+class ComponentError(RuntimeError):
'''
Indicates a Python error in constructing a component.
Raising this locks the component into an error state,
@@ -397,9 +414,7 @@ class BadComponentInit(AttributeError):
)
if immediate:
- caller.parent.showMessage(
- msg=string, detail=detail, icon='Warning'
- )
+ caller._error.emit(string, detail)
else:
caller.lockProperties(['error'])
caller.lockError((string, detail))
diff --git a/src/components/video.py b/src/components/video.py
index d3696d4..383531e 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -7,7 +7,7 @@ import threading
from queue import PriorityQueue
from core import Core
-from component import Component, BadComponentInit
+from component import Component, ComponentError
from toolkit.frame import BlankFrame
from toolkit.ffmpeg import testAudioStream
from toolkit import openPipe, checkOutput
@@ -195,14 +195,14 @@ class Component(Component):
self.updateChunksize(width, height)
try:
self.video = Video(
- ffmpeg=self.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,
component=self, scale=self.scale
) if os.path.exists(self.videoPath) else None
except KeyError:
- raise BadComponentInit(self, 'Frame Fetcher initialization')
+ raise ComponentError(self, 'Frame Fetcher initialization')
def frameRender(self, layerNo, frameNo):
if self.video:
diff --git a/src/core.py b/src/core.py
index 2f9c36c..4c08c04 100644
--- a/src/core.py
+++ b/src/core.py
@@ -76,6 +76,9 @@ class Core:
component
)
self.componentListChanged()
+ self.selectedComponents[compPos]._error.connect(
+ loader.videoThreadError
+ )
# init component's widget for loading/saving presets
self.selectedComponents[compPos].widget(loader)
diff --git a/src/mainwindow.py b/src/mainwindow.py
index a32c1b4..03b8dde 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -578,7 +578,11 @@ class MainWindow(QtWidgets.QMainWindow):
detail=detail,
icon='Warning',
)
- self.stopVideo()
+ try:
+ self.stopVideo()
+ except AttributeError as e:
+ if 'videoWorker' not in str(e):
+ raise
def changeEncodingStatus(self, status):
self.encoding = status
@@ -684,8 +688,6 @@ 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])
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index ca2a054..b66e037 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -41,15 +41,33 @@ class PaintColor(QtGui.QColor):
super().__init__(b, g, r, a)
+def defaultSize(framefunc):
+ '''Makes width/height arguments optional'''
+ def decorator(*args):
+ if len(args) < 2:
+ newArgs = list(args)
+ if len(args) == 0 or len(args) == 1:
+ height = int(core.Core.settings.value("outputHeight"))
+ newArgs.append(height)
+ if len(args) == 0:
+ width = int(core.Core.settings.value("outputWidth"))
+ newArgs.insert(0, width)
+ args = tuple(newArgs)
+ return framefunc(*args)
+ return decorator
+
+
def FloodFrame(width, height, RgbaTuple):
return Image.new("RGBA", (width, height), RgbaTuple)
+@defaultSize
def BlankFrame(width, height):
'''The base frame used by each component to start drawing.'''
return FloodFrame(width, height, (0, 0, 0, 0))
+@defaultSize
def Checkerboard(width, height):
'''
A checkerboard to represent transparency to the user.
diff --git a/src/video_thread.py b/src/video_thread.py
index 68eae4f..dd957e5 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -18,7 +18,7 @@ from threading import Thread, Event
import time
import signal
-from component import BadComponentInit
+from component import ComponentError
from toolkit import openPipe
from toolkit.ffmpeg import readAudioFile, createFfmpegCommand
from toolkit.frame import Checkerboard
@@ -160,7 +160,7 @@ class Worker(QtCore.QObject):
progressBarUpdate=self.progressBarUpdate,
progressBarSetText=self.progressBarSetText
)
- except BadComponentInit:
+ except ComponentError:
pass
if 'error' in comp.properties():
--
cgit v1.2.3
From 661526b0739115594fda4c0e876398cdc940fbe1 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Tue, 25 Jul 2017 17:44:59 -0400
Subject: repeated errors don't cause repeated windows
---
src/component.py | 15 ++++++++++--
src/components/sound.py | 1 -
src/components/video.py | 4 ++--
src/core.py | 15 ++++++------
src/mainwindow.py | 4 ++--
src/presetmanager.py | 61 +++++++++++++++++++++++++++----------------------
src/video_thread.py | 8 +++----
7 files changed, 63 insertions(+), 45 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/component.py b/src/component.py
index 48e9c1a..7a768ed 100644
--- a/src/component.py
+++ b/src/component.py
@@ -17,7 +17,7 @@ class ComponentMetaclass(type(QtCore.QObject)):
def initializationWrapper(self, *args, **kwargs):
try:
return func(self, *args, **kwargs)
- except:
+ except Exception:
try:
raise ComponentInitError(self, 'initialization process')
except ComponentError:
@@ -28,7 +28,7 @@ class ComponentMetaclass(type(QtCore.QObject)):
def renderWrapper(self, *args, **kwargs):
try:
return func(self, *args, **kwargs)
- except:
+ except Exception:
from toolkit.frame import BlankFrame
try:
raise ComponentError(self, 'renderer')
@@ -398,8 +398,19 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
class ComponentException(RuntimeError):
'''A base class for component errors'''
+
+ _prevErrors = []
+
def __init__(self, caller, name, immediate):
+ 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:]:
+ # Don't create multiple windows for repeated messages
+ return
+
from toolkit import formatTraceback
import sys
if sys.exc_info()[0] is not None:
diff --git a/src/components/sound.py b/src/components/sound.py
index b3a627a..fcd9e4e 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -1,7 +1,6 @@
from PyQt5 import QtGui, QtCore, QtWidgets
import os
-from core import Core
from component import Component
from toolkit.frame import BlankFrame
diff --git a/src/components/video.py b/src/components/video.py
index 153fc4d..6b0a04a 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -6,8 +6,7 @@ import subprocess
import threading
from queue import PriorityQueue
-from core import Core
-from component import Component, ComponentError
+from component import Component
from toolkit.frame import BlankFrame
from toolkit.ffmpeg import testAudioStream
from toolkit import openPipe, checkOutput
@@ -155,6 +154,7 @@ 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):
diff --git a/src/core.py b/src/core.py
index 4c08c04..b371d64 100644
--- a/src/core.py
+++ b/src/core.py
@@ -215,7 +215,7 @@ class Core:
if hasattr(loader, 'updateComponentTitle'):
loader.updateComponentTitle(i, modified)
- except:
+ except Exception:
errcode = 1
data = sys.exc_info()
@@ -237,9 +237,10 @@ class Core:
self.openingProject = False
def parseAvFile(self, filepath):
- '''Parses an avp (project) or avl (preset package) file.
- Returns dictionary with section names as the keys, each one
- contains a list of tuples: (compName, version, compPresetDict)
+ '''
+ Parses an avp (project) or avl (preset package) file.
+ Returns dictionary with section names as the keys, each one
+ contains a list of tuples: (compName, version, compPresetDict)
'''
validSections = (
'Components',
@@ -287,7 +288,7 @@ class Core:
data[section].append((key, value.strip()))
return 0, data
- except:
+ except Exception:
return 1, sys.exc_info()
def importPreset(self, filepath):
@@ -332,7 +333,7 @@ class Core:
exportPath
)
return True
- except:
+ except Exception:
return False
def createPresetFile(
@@ -397,7 +398,7 @@ class Core:
)
)
return True
- except:
+ except Exception:
return False
def newVideoWorker(self, loader, audioFile, outputPath):
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 03b8dde..3cc5d26 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -314,7 +314,7 @@ class MainWindow(QtWidgets.QMainWindow):
['ffmpeg', '-version'], stderr=f
)
goodVersion = str(ffmpegVers).split()[2].startswith('3')
- except:
+ except Exception:
goodVersion = False
else:
goodVersion = True
@@ -381,7 +381,7 @@ class MainWindow(QtWidgets.QMainWindow):
)
@QtCore.pyqtSlot()
- def cleanUp(self):
+ def cleanUp(self, *args):
self.timer.stop()
self.previewThread.quit()
self.previewThread.wait()
diff --git a/src/presetmanager.py b/src/presetmanager.py
index e602c16..b1eeb34 100644
--- a/src/presetmanager.py
+++ b/src/presetmanager.py
@@ -211,10 +211,9 @@ class PresetManager(QtWidgets.QDialog):
self.parent.drawPreview()
def openDeletePresetDialog(self):
- selected = self.window.listWidget_presets.selectedItems()
- if not selected:
+ row = self.getPresetRow()
+ if row == -1:
return
- row = self.window.listWidget_presets.row(selected[0])
comp, vers, name = self.presetRows[row]
ch = self.parent.showMessage(
msg='Really delete %s?' % name,
@@ -242,32 +241,40 @@ class PresetManager(QtWidgets.QDialog):
'numbers, and spaces.',
parent=window if window else self.window)
+ def getPresetRow(self):
+ row = self.window.listWidget_presets.currentRow()
+ if row > -1:
+ return row
+
+ # check if component selected in MainWindow has preset loaded
+ componentList = self.parent.window.listWidget_componentList
+ compIndex = componentList.currentRow()
+ if compIndex == -1:
+ return compIndex
+
+ preset = self.core.selectedComponents[compIndex].currentPreset
+ if preset is None:
+ return -1
+ else:
+ rowTuple = (
+ self.core.selectedComponents[compIndex].name,
+ self.core.selectedComponents[compIndex].version,
+ preset
+ )
+ for i, tup in enumerate(self.presetRows):
+ if rowTuple == tup:
+ index = i
+ break
+ else:
+ return -1
+ return index
+
def openRenamePresetDialog(self):
# TODO: maintain consistency by changing this to call createNewPreset()
presetList = self.window.listWidget_presets
- index = presetList.currentRow()
+ index = self.getPresetRow()
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 preset is None:
- return
- else:
- rowTuple = (
- self.core.selectedComponents[compIndex].name,
- self.core.selectedComponents[compIndex].version,
- preset
- )
- for i, tup in enumerate(self.presetRows):
- if rowTuple == tup:
- index = i
- break
- else:
- return
+ return
while True:
newName, OK = QtWidgets.QInputDialog.getText(
@@ -326,14 +333,14 @@ class PresetManager(QtWidgets.QDialog):
self.settings.setValue("presetDir", os.path.dirname(filename))
def openExportDialog(self):
- if not self.window.listWidget_presets.selectedItems():
+ index = self.getPresetRow()
+ if index == -1:
return
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
self.window, "Export Preset",
self.settings.value("presetDir"),
"Preset Files (*.avl)")
if filename:
- index = self.window.listWidget_presets.currentRow()
comp, vers, name = self.presetRows[index]
if not self.core.exportPreset(filename, comp, vers, name):
self.parent.showMessage(
diff --git a/src/video_thread.py b/src/video_thread.py
index dd957e5..8cbe8a8 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -225,7 +225,7 @@ class Worker(QtCore.QObject):
self.renderThreads = []
try:
numCpus = len(os.sched_getaffinity(0))
- except:
+ except Exception:
numCpus = os.cpu_count()
for i in range(2 if numCpus <= 2 else 3):
@@ -268,7 +268,7 @@ class Worker(QtCore.QObject):
try:
self.out_pipe.stdin.write(frameBuffer[audioI].tobytes())
self.previewQueue.put([audioI, frameBuffer.pop(audioI)])
- except:
+ except Exception:
break
# increase progress bar value
@@ -293,7 +293,7 @@ class Worker(QtCore.QObject):
print("Export Canceled")
try:
os.remove(self.outputFile)
- except:
+ except Exception:
pass
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit('Export Canceled')
@@ -333,7 +333,7 @@ class Worker(QtCore.QObject):
try:
self.out_pipe.send_signal(signal.SIGINT)
- except:
+ except Exception:
pass
def reset(self):
--
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/video_thread.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 4329b0e947471ced7ca0b3460a5f40e2703117e9 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Tue, 25 Jul 2017 22:14:34 -0400
Subject: don't]] always trigger error()
---
src/video_thread.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'src/video_thread.py')
diff --git a/src/video_thread.py b/src/video_thread.py
index 48f3729..c5a3c09 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -164,7 +164,7 @@ class Worker(QtCore.QObject):
pass
compProps = comp.properties()
- if 'error' in compProps or comp.error() is not None:
+ if 'error' in compProps or comp._lockedError is not None:
self.cancel()
self.canceled = True
canceledByComponent = True
--
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/video_thread.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
From 6ecb6df23628de65c9efd8cac4810fdf74238c3d Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 27 Jul 2017 22:15:41 -0400
Subject: some minor bugfixes
---
src/component.py | 5 +++--
src/components/sound.py | 3 ---
src/components/video.py | 14 +++++++++-----
src/mainwindow.py | 2 +-
src/video_thread.py | 2 +-
5 files changed, 14 insertions(+), 12 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/component.py b/src/component.py
index 1c5ccb3..03023e7 100644
--- a/src/component.py
+++ b/src/component.py
@@ -33,7 +33,7 @@ class ComponentMetaclass(type(QtCore.QObject)):
return func(self, *args, **kwargs)
except Exception as e:
try:
- if e.__name__.startswith('Component'):
+ if e.__class__.__name__.startswith('Component'):
raise
else:
raise ComponentError(self, 'renderer')
@@ -213,7 +213,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
image = BlankFrame(self.width, self.height)
return image
- def renderFinished(self):
+ def postFrameRender(self):
pass
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
@@ -456,4 +456,5 @@ class ComponentError(RuntimeError):
)
super().__init__(string)
+ caller.lockError(string)
caller._error.emit(string, detail)
diff --git a/src/components/sound.py b/src/components/sound.py
index aff43d3..26ecf93 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -21,9 +21,6 @@ class Component(Component):
'sound': None,
})
- def preFrameRender(self, **kwargs):
- pass
-
def properties(self):
props = ['static', 'audio']
if not os.path.exists(self.sound):
diff --git a/src/components/video.py b/src/components/video.py
index 48ac557..b2487c1 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -59,7 +59,7 @@ class Video:
self.thread = threading.Thread(
target=self.fillBuffer,
- name=self.__doc__
+ name='Video Frame-Fetcher'
)
self.thread.daemon = True
self.thread.start()
@@ -150,6 +150,10 @@ class Component(Component):
def properties(self):
props = []
+ if hasattr(self.parent, 'window'):
+ outputFile = self.parent.window.lineEdit_outputFile.text()
+ else:
+ outputFile = str(self.parent.args.output)
if not self.videoPath:
self.lockError("There is no video selected.")
@@ -157,9 +161,7 @@ class Component(Component):
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())):
+ elif os.path.realpath(self.videoPath) == os.path.realpath(outputFile):
self.lockError("Input and output paths match.")
if self.useAudio:
@@ -193,7 +195,7 @@ class Component(Component):
raise Video.threadError
return self.video.frame(frameNo)
- def renderFinished(self):
+ def postFrameRender(self):
self.video.pipe.stdout.close()
self.video.pipe.send_signal(signal.SIGINT)
@@ -238,6 +240,8 @@ class Component(Component):
def updateChunksize(self):
if self.scale != 100 and not self.distort:
width, height = scale(self.scale, self.width, self.height, int)
+ else:
+ width, height = self.width, self.height
self.chunkSize = 4 * width * height
def command(self, arg):
diff --git a/src/mainwindow.py b/src/mainwindow.py
index e478d19..070131c 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -54,7 +54,7 @@ class PreviewWindow(QtWidgets.QLabel):
def threadError(self, msg):
self.parent.showMessage(
msg=msg,
- icon='Warning',
+ icon='Critical',
parent=self
)
diff --git a/src/video_thread.py b/src/video_thread.py
index 8c7d585..32e8a38 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -292,7 +292,7 @@ class Worker(QtCore.QObject):
self.out_pipe.wait()
for comp in reversed(self.components):
- comp.renderFinished()
+ comp.postFrameRender()
if self.canceled:
print("Export Canceled")
--
cgit v1.2.3
From c1457b6dad4640b17679dd802e372bd46a13d2a5 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sat, 29 Jul 2017 13:08:28 -0400
Subject: starting work on Waveform component
split Video class out of Video component for reuse in Waveform
---
.gitignore | 2 +
src/component.py | 7 +-
src/components/video.py | 198 ++++++++-----------------------
src/components/waveform.py | 139 ++++++++++++++++++++++
src/components/waveform.ui | 283 +++++++++++++++++++++++++++++++++++++++++++++
src/toolkit/common.py | 37 ++++--
src/toolkit/ffmpeg.py | 99 ++++++++++++++++
src/video_thread.py | 2 +-
8 files changed, 607 insertions(+), 160 deletions(-)
create mode 100644 src/components/waveform.py
create mode 100644 src/components/waveform.ui
(limited to 'src/video_thread.py')
diff --git a/.gitignore b/.gitignore
index bfdd0e7..7cec615 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,5 @@ env/*
*.tar.*
*.exe
ffmpeg
+*.bak
+*~
diff --git a/src/component.py b/src/component.py
index 03023e7..fc8fbd3 100644
--- a/src/component.py
+++ b/src/component.py
@@ -197,7 +197,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
Must call super() when subclassing
Triggered only before a video is exported (video_thread.py)
- self.worker = the video thread worker
+ self.audioFile = filepath to the main input audio file
self.completeAudioArray = a list of audio samples
self.sampleSize = number of audio samples per video frame
self.progressBarUpdate = signal to set progress bar number
@@ -436,7 +436,7 @@ class ComponentError(RuntimeError):
import sys
if sys.exc_info()[0] is not None:
string = (
- "%s component's %s encountered %s %s." % (
+ "%s component's %s encountered %s %s: %s" % (
caller.__class__.name,
name,
'an' if any([
@@ -444,12 +444,13 @@ class ComponentError(RuntimeError):
for vowel in ('A', 'I')
]) else 'a',
sys.exc_info()[0].__name__,
+ str(sys.exc_info()[1])
)
)
detail = formatTraceback(sys.exc_info()[2])
else:
string = name
- detail = "Methods:\n%s" % (
+ detail = "Attributes:\n%s" % (
"\n".join(
[m for m in dir(caller) if not m.startswith('_')]
)
diff --git a/src/components/video.py b/src/components/video.py
index b2487c1..d3460ff 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -1,103 +1,13 @@
-from PIL import Image, ImageDraw
+from PIL import Image
from PyQt5 import QtGui, QtCore, QtWidgets
import os
import math
import subprocess
-import signal
-import threading
-from queue import PriorityQueue
from component import Component, ComponentError
from toolkit.frame import BlankFrame
-from toolkit.ffmpeg import testAudioStream
-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
- 'videoPath',
- 'width',
- 'height',
- 'scale', # percentage scale
- 'frameRate', # frames per second
- 'chunkSize', # number of bytes in one frame
- 'parent', # mainwindow object
- 'component', # component object
- ]
- for arg in mandatoryArgs:
- setattr(self, arg, kwargs[arg])
-
- self.frameNo = -1
- self.currentFrame = 'None'
- if 'loopVideo' in kwargs and kwargs['loopVideo']:
- self.loopValue = '-1'
- else:
- self.loopValue = '0'
- self.command = [
- self.ffmpeg,
- '-thread_queue_size', '512',
- '-r', str(self.frameRate),
- '-stream_loop', self.loopValue,
- '-i', self.videoPath,
- '-f', 'image2pipe',
- '-pix_fmt', 'rgba',
- '-filter_complex', '[0:v] scale=%s:%s' % scale(
- self.scale, self.width, self.height, str),
- '-vcodec', 'rawvideo', '-',
- ]
-
- self.frameBuffer = PriorityQueue()
- self.frameBuffer.maxsize = self.frameRate
- self.finishedFrames = {}
-
- self.thread = threading.Thread(
- target=self.fillBuffer,
- name='Video Frame-Fetcher'
- )
- self.thread.daemon = True
- self.thread.start()
-
- def frame(self, num):
- while True:
- if num in self.finishedFrames:
- image = self.finishedFrames.pop(num)
- return finalizeFrame(
- self.component, image, self.width, self.height)
-
- i, image = self.frameBuffer.get()
- self.finishedFrames[i] = image
- self.frameBuffer.task_done()
-
- def fillBuffer(self):
- self.pipe = openPipe(
- self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
- stderr=subprocess.DEVNULL, bufsize=10**8
- )
- while True:
- if self.parent.canceled:
- break
- self.frameNo += 1
-
- # If we run out of frames, use the last good frame and loop.
- try:
- if len(self.currentFrame) == 0:
- self.frameBuffer.put((self.frameNo-1, self.lastFrame))
- continue
- except AttributeError:
- Video.threadError = ComponentError(self.component, 'video')
- break
-
- self.currentFrame = self.pipe.stdout.read(self.chunkSize)
- if len(self.currentFrame) != 0:
- self.frameBuffer.put((self.frameNo, self.currentFrame))
- self.lastFrame = self.currentFrame
+from toolkit.ffmpeg import testAudioStream, FfmpegVideo
+from toolkit import openPipe, closePipe, checkOutput, scale
class Component(Component):
@@ -182,22 +92,21 @@ class Component(Component):
def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs)
self.updateChunksize()
- self.video = Video(
- ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath,
+ self.video = FfmpegVideo(
+ inputPath=self.videoPath, filter_=self.makeFfmpegFilter(),
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
+ component=self
) if os.path.exists(self.videoPath) else None
def frameRender(self, frameNo):
- if Video.threadError is not None:
- raise Video.threadError
- return self.video.frame(frameNo)
+ if FfmpegVideo.threadError is not None:
+ raise FfmpegVideo.threadError
+ return self.finalizeFrame(self.video.frame(frameNo))
def postFrameRender(self):
- self.video.pipe.stdout.close()
- self.video.pipe.send_signal(signal.SIGINT)
+ closePipe(self.video.pipe)
def pickVideo(self):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
@@ -220,23 +129,30 @@ class Component(Component):
'-i', self.videoPath,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',
- '-filter_complex', '[0:v] scale=%s:%s' % scale(
- self.scale, width, height, str),
+ ]
+ command.extend(self.makeFfmpegFilter())
+ command.extend([
'-vcodec', 'rawvideo', '-',
'-ss', '90',
- '-vframes', '1',
- ]
+ '-frames:v', '1',
+ ])
pipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, bufsize=10**8
)
byteFrame = pipe.stdout.read(self.chunkSize)
- pipe.stdout.close()
- pipe.send_signal(signal.SIGINT)
+ closePipe(pipe)
- frame = finalizeFrame(self, byteFrame, width, height)
+ frame = self.finalizeFrame(byteFrame)
return frame
+ def makeFfmpegFilter(self):
+ return [
+ '-filter_complex',
+ '[0:v] scale=%s:%s' % scale(
+ self.scale, self.width, self.height, str),
+ ]
+
def updateChunksize(self):
if self.scale != 100 and not self.distort:
width, height = scale(self.scale, self.width, self.height, int)
@@ -268,44 +184,32 @@ class Component(Component):
print('Load a video:\n path=/filepath/to/video.mp4')
print('Using audio:\n path=/filepath/to/video.mp4 audio')
+ def finalizeFrame(self, imageData):
+ try:
+ if self.distort:
+ image = Image.frombytes(
+ 'RGBA',
+ (self.width, self.height),
+ imageData)
+ else:
+ image = Image.frombytes(
+ 'RGBA',
+ scale(self.scale, self.width, self.height, int),
+ imageData)
+
+ except ValueError:
+ print(
+ '### BAD VIDEO SELECTED ###\n'
+ 'Video will not export with these settings'
+ )
+ self.badVideo = True
+ return BlankFrame(self.width, self.height)
-def scale(scale, width, height, returntype=None):
- width = (float(width) / 100.0) * float(scale)
- height = (float(height) / 100.0) * float(scale)
- if returntype == str:
- return (str(math.ceil(width)), str(math.ceil(height)))
- elif returntype == int:
- return (math.ceil(width), math.ceil(height))
- else:
- return (width, height)
-
-
-def finalizeFrame(self, imageData, width, height):
- try:
- if self.distort:
- image = Image.frombytes(
- 'RGBA',
- (width, height),
- imageData)
+ if self.scale != 100 \
+ or self.xPosition != 0 or self.yPosition != 0:
+ frame = BlankFrame(self.width, self.height)
+ frame.paste(image, box=(self.xPosition, self.yPosition))
else:
- image = Image.frombytes(
- 'RGBA',
- scale(self.scale, width, height, int),
- imageData)
-
- except ValueError:
- print(
- '### BAD VIDEO SELECTED ###\n'
- 'Video will not export with these settings'
- )
- self.badVideo = True
- return BlankFrame(width, height)
-
- if self.scale != 100 \
- or self.xPosition != 0 or self.yPosition != 0:
- frame = BlankFrame(width, height)
- frame.paste(image, box=(self.xPosition, self.yPosition))
- else:
- frame = image
- self.badVideo = False
- return frame
+ frame = image
+ self.badVideo = False
+ return frame
diff --git a/src/components/waveform.py b/src/components/waveform.py
new file mode 100644
index 0000000..487a3bb
--- /dev/null
+++ b/src/components/waveform.py
@@ -0,0 +1,139 @@
+from PIL import Image
+from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt5.QtGui import QColor
+import os
+import math
+import subprocess
+
+from component import Component, ComponentError
+from toolkit.frame import BlankFrame
+from toolkit import openPipe, checkOutput, rgbFromString
+from toolkit.ffmpeg import FfmpegVideo
+
+
+class Component(Component):
+ name = 'Waveform'
+ version = '1.0.0'
+
+ def widget(self, *args):
+ self.color = (255, 255, 255)
+ super().widget(*args)
+
+ self.page.lineEdit_color.setText('%s,%s,%s' % self.color)
+ btnStyle = "QPushButton { background-color : %s; outline: none; }" \
+ % QColor(*self.color1).name()
+ self.page.lineEdit_color.setStylesheet(btnStyle)
+ self.page.pushButton_color.clicked.connect(lambda: self.pickColor())
+
+ self.trackWidgets(
+ {
+ 'mode': self.page.comboBox_mode,
+ 'x': self.page.spinBox_x,
+ 'y': self.page.spinBox_y,
+ 'mirror': self.page.checkBox_mirror,
+ 'scale': self.page.spinBox_scale,
+ }
+ )
+
+ def update(self):
+ self.color = rgbFromString(self.page.lineEdit_color.text())
+ btnStyle = "QPushButton { background-color : %s; outline: none; }" \
+ % QColor(*self.color).name()
+ self.page.pushButton_color.setStyleSheet(btnStyle)
+ super().update()
+
+ def previewRender(self):
+ self.updateChunksize()
+ frame = self.getPreviewFrame(self.width, self.height)
+ if not frame:
+ return BlankFrame(self.width, self.height)
+ else:
+ return frame
+
+ def preFrameRender(self, **kwargs):
+ super().preFrameRender(**kwargs)
+ self.updateChunksize()
+ self.video = FfmpegVideo(
+ inputPath=self.audioFile,
+ filter_=makeFfmpegFilter(),
+ width=self.width, height=self.height,
+ chunkSize=self.chunkSize,
+ frameRate=int(self.settings.value("outputFrameRate")),
+ parent=self.parent, component=self,
+ )
+
+ def frameRender(self, frameNo):
+ if FfmpegVideo.threadError is not None:
+ raise FfmpegVideo.threadError
+ return finalizeFrame(self.video.frame(frameNo))
+
+ def postFrameRender(self):
+ closePipe(self.video.pipe)
+
+ def getPreviewFrame(self, width, height):
+ inputFile = self.parent.window.lineEdit_audioFile.text()
+ if not inputFile or not os.path.exists(inputFile):
+ return
+
+ command = [
+ self.core.FFMPEG_BIN,
+ '-thread_queue_size', '512',
+ '-i', inputFile,
+ '-f', 'image2pipe',
+ '-pix_fmt', 'rgba',
+ ]
+ command.extend(self.makeFfmpegFilter())
+ command.extend([
+ '-vcodec', 'rawvideo', '-',
+ '-ss', '90',
+ '-frames:v', '1',
+ ])
+ pipe = openPipe(
+ command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL, bufsize=10**8
+ )
+ byteFrame = pipe.stdout.read(self.chunkSize)
+ closePipe(pipe)
+
+ frame = finalizeFrame(self, byteFrame, width, height)
+ return frame
+
+ def makeFfmpegFilter(self):
+ w, h = scale(self.scale, self.width, self.height, str)
+ return [
+ '-filter_complex',
+ '[0:a] showwaves=s=%sx%s:mode=%s,format=rgba [v]' % (
+ w, h, self.mode,
+ ),
+ '-map', '[v]',
+ '-map', '0:a',
+ ]
+
+ def updateChunksize(self):
+ if self.scale != 100:
+ width, height = scale(self.scale, self.width, self.height, int)
+ else:
+ width, height = self.width, self.height
+ self.chunkSize = 4 * width * height
+
+
+def scale(scale, width, height, returntype=None):
+ width = (float(width) / 100.0) * float(scale)
+ height = (float(height) / 100.0) * float(scale)
+ if returntype == str:
+ return (str(math.ceil(width)), str(math.ceil(height)))
+ elif returntype == int:
+ return (math.ceil(width), math.ceil(height))
+ else:
+ return (width, height)
+
+
+def finalizeFrame(self, imageData, width, height):
+ # frombytes goes here
+ if self.scale != 100 \
+ or self.x != 0 or self.y != 0:
+ frame = BlankFrame(width, height)
+ frame.paste(image, box=(self.x, self.y))
+ else:
+ frame = image
+ return frame
diff --git a/src/components/waveform.ui b/src/components/waveform.ui
new file mode 100644
index 0000000..5d62150
--- /dev/null
+++ b/src/components/waveform.ui
@@ -0,0 +1,283 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 586
+ 197
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 197
+
+
+
+ Form
+
+
+ -
+
+
+ 4
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 31
+ 0
+
+
+
+ Mode
+
+
+
+ -
+
+
-
+
+ Cline
+
+
+ -
+
+ Line
+
+
+ -
+
+ P2p
+
+
+ -
+
+ Point
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ X
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+ -10000
+
+
+ 10000
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Y
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+
+ 0
+ 0
+
+
+
+ -10000
+
+
+ 10000
+
+
+ 0
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Wave Color
+
+
+
+ -
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 32
+ 32
+
+
+
+
+
+
+ false
+
+
+ false
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Mirror
+
+
+
+ -
+
+
+ Scale
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ QAbstractSpinBox::UpDownArrows
+
+
+ %
+
+
+ 10
+
+
+ 400
+
+
+ 100
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
diff --git a/src/toolkit/common.py b/src/toolkit/common.py
index 251a2c1..128ed08 100644
--- a/src/toolkit/common.py
+++ b/src/toolkit/common.py
@@ -6,9 +6,22 @@ import string
import os
import sys
import subprocess
+import signal
+import math
from collections import OrderedDict
+def scale(scale, width, height, returntype=None):
+ width = (float(width) / 100.0) * float(scale)
+ height = (float(height) / 100.0) * float(scale)
+ if returntype == str:
+ return (str(math.ceil(width)), str(math.ceil(height)))
+ elif returntype == int:
+ return (math.ceil(width), math.ceil(height))
+ else:
+ return (width, height)
+
+
def badName(name):
'''Returns whether a name contains non-alphanumeric chars'''
return any([letter in string.punctuation for letter in name])
@@ -34,29 +47,35 @@ def appendUppercase(lst):
lst.append(form.upper())
return lst
-
-def hideCmdWin(func):
- ''' Stops CMD window from appearing on Windows.
- Adapted from here: http://code.activestate.com/recipes/409002/
- '''
- def decorator(commandList, **kwargs):
+def pipeWrapper(func):
+ '''A decorator to insert proper kwargs into Popen objects.'''
+ def pipeWrapper(commandList, **kwargs):
if sys.platform == 'win32':
+ # Stop CMD window from appearing on Windows
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
kwargs['startupinfo'] = startupinfo
+
+ if 'bufsize' not in kwargs:
+ kwargs['bufsize'] = 10**8
+ if 'stdin' not in kwargs:
+ kwargs['stdin'] = subprocess.DEVNULL
return func(commandList, **kwargs)
- return decorator
+ return pipeWrapper
-@hideCmdWin
+@pipeWrapper
def checkOutput(commandList, **kwargs):
return subprocess.check_output(commandList, **kwargs)
-@hideCmdWin
+@pipeWrapper
def openPipe(commandList, **kwargs):
return subprocess.Popen(commandList, **kwargs)
+def closePipe(pipe):
+ pipe.stdout.close()
+ pipe.send_signal(signal.SIGINT)
def disableWhenEncoding(func):
def decorator(self, *args, **kwargs):
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index b8bc679..fea9d4e 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -5,11 +5,110 @@ import numpy
import sys
import os
import subprocess
+import threading
+from queue import PriorityQueue
import core
from toolkit.common import checkOutput, openPipe
+class FfmpegVideo:
+ '''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 = [
+ 'inputPath',
+ 'filter_',
+ 'width',
+ 'height',
+ 'frameRate', # frames per second
+ 'chunkSize', # number of bytes in one frame
+ 'parent', # mainwindow object
+ 'component', # component object
+ ]
+ for arg in mandatoryArgs:
+ setattr(self, arg, kwargs[arg])
+
+ self.frameNo = -1
+ self.currentFrame = 'None'
+ self.map_ = None
+
+ if 'loopVideo' in kwargs and kwargs['loopVideo']:
+ self.loopValue = '-1'
+ else:
+ self.loopValue = '0'
+ if 'filter_' in kwargs:
+ if kwargs['filter_'][0] != '-filter_complex':
+ kwargs['filter_'].insert(0, '-filter_complex')
+ else:
+ kwargs['filter_'] = None
+
+ self.command = [
+ core.Core.FFMPEG_BIN,
+ '-thread_queue_size', '512',
+ '-r', str(self.frameRate),
+ '-stream_loop', self.loopValue,
+ '-i', self.inputPath,
+ '-f', 'image2pipe',
+ '-pix_fmt', 'rgba',
+ ]
+ if type(kwargs['filter_']) is list:
+ self.command.extend(
+ kwargs['filter_']
+ )
+ self.command.extend([
+ '-vcodec', 'rawvideo', '-',
+ ])
+
+ self.frameBuffer = PriorityQueue()
+ self.frameBuffer.maxsize = self.frameRate
+ self.finishedFrames = {}
+
+ self.thread = threading.Thread(
+ target=self.fillBuffer,
+ name='FFmpeg Frame-Fetcher'
+ )
+ self.thread.daemon = True
+ self.thread.start()
+
+ def frame(self, num):
+ while True:
+ if num in self.finishedFrames:
+ image = self.finishedFrames.pop(num)
+ return image
+
+ i, image = self.frameBuffer.get()
+ self.finishedFrames[i] = image
+ self.frameBuffer.task_done()
+
+ def fillBuffer(self):
+ self.pipe = openPipe(
+ self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL, bufsize=10**8
+ )
+ while True:
+ if self.parent.canceled:
+ break
+ self.frameNo += 1
+
+ # If we run out of frames, use the last good frame and loop.
+ try:
+ if len(self.currentFrame) == 0:
+ self.frameBuffer.put((self.frameNo-1, self.lastFrame))
+ continue
+ except AttributeError:
+ Video.threadError = ComponentError(self.component, 'video')
+ break
+
+ self.currentFrame = self.pipe.stdout.read(self.chunkSize)
+ if len(self.currentFrame) != 0:
+ self.frameBuffer.put((self.frameNo, self.currentFrame))
+ self.lastFrame = self.currentFrame
+
+
def findFfmpeg():
if getattr(sys, 'frozen', False):
# The application is frozen
diff --git a/src/video_thread.py b/src/video_thread.py
index 32e8a38..f27ec21 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -153,7 +153,7 @@ class Worker(QtCore.QObject):
for compNo, comp in enumerate(reversed(self.components)):
try:
comp.preFrameRender(
- worker=self,
+ audioFile=self.inputFile,
completeAudioArray=self.completeAudioArray,
sampleSize=self.sampleSize,
progressBarUpdate=self.progressBarUpdate,
--
cgit v1.2.3
From 1297af61c9ce00b6dd76f8ec690baedf5bf887c7 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sat, 29 Jul 2017 20:27:46 -0400
Subject: waveform component is working, preview is glitchy
---
src/components/original.py | 3 +
src/components/video.py | 10 ++--
src/components/waveform.py | 134 +++++++++++++++++++++++++++++++--------------
src/components/waveform.ui | 95 +++++++++++++++++++++++++++++++-
src/toolkit/common.py | 21 -------
src/toolkit/ffmpeg.py | 30 ++++++++--
src/toolkit/frame.py | 12 ++++
src/video_thread.py | 38 +++++++++----
8 files changed, 256 insertions(+), 87 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/components/original.py b/src/components/original.py
index 3d1a574..621af6f 100644
--- a/src/components/original.py
+++ b/src/components/original.py
@@ -18,6 +18,9 @@ class Component(Component):
def names(*args):
return ['Original Audio Visualization']
+ def properties(self):
+ return ['pcm']
+
def widget(self, *args):
self.visColor = (255, 255, 255)
self.scale = 20
diff --git a/src/components/video.py b/src/components/video.py
index d3460ff..6cd16e5 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -4,10 +4,10 @@ import os
import math
import subprocess
-from component import Component, ComponentError
-from toolkit.frame import BlankFrame
-from toolkit.ffmpeg import testAudioStream, FfmpegVideo
-from toolkit import openPipe, closePipe, checkOutput, scale
+from component import Component
+from toolkit.frame import BlankFrame, scale
+from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo
+from toolkit import checkOutput
class Component(Component):
@@ -132,7 +132,7 @@ class Component(Component):
]
command.extend(self.makeFfmpegFilter())
command.extend([
- '-vcodec', 'rawvideo', '-',
+ '-codec:v', 'rawvideo', '-',
'-ss', '90',
'-frames:v', '1',
])
diff --git a/src/components/waveform.py b/src/components/waveform.py
index 487a3bb..375b3fc 100644
--- a/src/components/waveform.py
+++ b/src/components/waveform.py
@@ -5,10 +5,10 @@ import os
import math
import subprocess
-from component import Component, ComponentError
-from toolkit.frame import BlankFrame
-from toolkit import openPipe, checkOutput, rgbFromString
-from toolkit.ffmpeg import FfmpegVideo
+from component import Component
+from toolkit.frame import BlankFrame, scale
+from toolkit import checkOutput, rgbFromString, pickColor
+from toolkit.ffmpeg import openPipe, closePipe, getAudioDuration, FfmpegVideo
class Component(Component):
@@ -21,17 +21,27 @@ class Component(Component):
self.page.lineEdit_color.setText('%s,%s,%s' % self.color)
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
- % QColor(*self.color1).name()
- self.page.lineEdit_color.setStylesheet(btnStyle)
+ % QColor(*self.color).name()
+ self.page.pushButton_color.setStyleSheet(btnStyle)
self.page.pushButton_color.clicked.connect(lambda: self.pickColor())
+ self.page.spinBox_scale.valueChanged.connect(self.updateChunksize)
+
+ if hasattr(self.parent, 'window'):
+ self.parent.window.lineEdit_audioFile.textChanged.connect(
+ self.update
+ )
self.trackWidgets(
{
'mode': self.page.comboBox_mode,
+ 'amplitude': self.page.comboBox_amplitude,
'x': self.page.spinBox_x,
'y': self.page.spinBox_y,
'mirror': self.page.checkBox_mirror,
'scale': self.page.spinBox_scale,
+ 'opacity': self.page.spinBox_opacity,
+ 'compress': self.page.checkBox_compress,
+ 'mono': self.page.checkBox_mono,
}
)
@@ -42,6 +52,26 @@ class Component(Component):
self.page.pushButton_color.setStyleSheet(btnStyle)
super().update()
+ def loadPreset(self, pr, *args):
+ super().loadPreset(pr, *args)
+
+ self.page.lineEdit_color.setText('%s,%s,%s' % pr['color'])
+ btnStyle = "QPushButton { background-color : %s; outline: none; }" \
+ % QColor(*pr['color']).name()
+ self.page.pushButton_color.setStyleSheet(btnStyle)
+
+ def savePreset(self):
+ saveValueStore = super().savePreset()
+ saveValueStore['color'] = self.color
+ return saveValueStore
+
+ def pickColor(self):
+ RGBstring, btnStyle = pickColor()
+ if not RGBstring:
+ return
+ self.page.lineEdit_color.setText(RGBstring)
+ self.page.pushButton_color.setStyleSheet(btnStyle)
+
def previewRender(self):
self.updateChunksize()
frame = self.getPreviewFrame(self.width, self.height)
@@ -53,10 +83,11 @@ class Component(Component):
def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs)
self.updateChunksize()
+ w, h = scale(self.scale, self.width, self.height, str)
self.video = FfmpegVideo(
inputPath=self.audioFile,
- filter_=makeFfmpegFilter(),
- width=self.width, height=self.height,
+ filter_=self.makeFfmpegFilter(),
+ width=w, height=h,
chunkSize=self.chunkSize,
frameRate=int(self.settings.value("outputFrameRate")),
parent=self.parent, component=self,
@@ -65,7 +96,7 @@ class Component(Component):
def frameRender(self, frameNo):
if FfmpegVideo.threadError is not None:
raise FfmpegVideo.threadError
- return finalizeFrame(self.video.frame(frameNo))
+ return self.finalizeFrame(self.video.frame(frameNo))
def postFrameRender(self):
closePipe(self.video.pipe)
@@ -74,18 +105,25 @@ class Component(Component):
inputFile = self.parent.window.lineEdit_audioFile.text()
if not inputFile or not os.path.exists(inputFile):
return
+ duration = getAudioDuration(inputFile)
+ if not duration:
+ return
+ startPt = duration / 3
command = [
self.core.FFMPEG_BIN,
'-thread_queue_size', '512',
+ '-r', self.settings.value("outputFrameRate"),
+ '-ss', "{0:.3f}".format(startPt),
'-i', inputFile,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',
]
- command.extend(self.makeFfmpegFilter())
+ command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt))
command.extend([
- '-vcodec', 'rawvideo', '-',
- '-ss', '90',
+ '-an',
+ '-s:v', '%sx%s' % scale(self.scale, self.width, self.height, str),
+ '-codec:v', 'rawvideo', '-',
'-frames:v', '1',
])
pipe = openPipe(
@@ -95,45 +133,57 @@ class Component(Component):
byteFrame = pipe.stdout.read(self.chunkSize)
closePipe(pipe)
- frame = finalizeFrame(self, byteFrame, width, height)
+ frame = self.finalizeFrame(byteFrame)
return frame
- def makeFfmpegFilter(self):
+ def makeFfmpegFilter(self, preview=False, startPt=0):
w, h = scale(self.scale, self.width, self.height, str)
+ if self.amplitude == 0:
+ amplitude = 'lin'
+ elif self.amplitude == 1:
+ amplitude = 'log'
+ elif self.amplitude == 2:
+ amplitude = 'sqrt'
+ elif self.amplitude == 3:
+ amplitude = 'cbrt'
+ hexcolor = QColor(*self.color).name()
+ opacity = "{0:.1f}".format(self.opacity / 100)
+
return [
'-filter_complex',
- '[0:a] showwaves=s=%sx%s:mode=%s,format=rgba [v]' % (
- w, h, self.mode,
+ '[0:a] %s%s'
+ 'showwaves=r=30:s=%sx%s:mode=%s:colors=%s@%s:scale=%s%s%s [v1]; '
+ '[v1] scale=%s:%s%s [v]' % (
+ 'compand=gain=2,' if self.compress else '',
+ 'aformat=channel_layouts=mono,' if self.mono else '',
+ self.settings.value("outputWidth"),
+ self.settings.value("outputHeight"),
+ str(self.page.comboBox_mode.currentText()).lower(),
+ hexcolor, opacity, amplitude,
+ ', drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=4:color=%s@%s' % (
+ hexcolor, opacity
+ ) if self.mode < 2 else '',
+ ', hflip' if self.mirror else'',
+ w, h,
+ ', trim=duration=%s' % "{0:.3f}".format(startPt + 1) if preview else '',
),
'-map', '[v]',
- '-map', '0:a',
]
def updateChunksize(self):
- if self.scale != 100:
- width, height = scale(self.scale, self.width, self.height, int)
- else:
- width, height = self.width, self.height
+ width, height = scale(self.scale, self.width, self.height, int)
self.chunkSize = 4 * width * height
-
-def scale(scale, width, height, returntype=None):
- width = (float(width) / 100.0) * float(scale)
- height = (float(height) / 100.0) * float(scale)
- if returntype == str:
- return (str(math.ceil(width)), str(math.ceil(height)))
- elif returntype == int:
- return (math.ceil(width), math.ceil(height))
- else:
- return (width, height)
-
-
-def finalizeFrame(self, imageData, width, height):
- # frombytes goes here
- if self.scale != 100 \
- or self.x != 0 or self.y != 0:
- frame = BlankFrame(width, height)
- frame.paste(image, box=(self.x, self.y))
- else:
- frame = image
- return frame
+ def finalizeFrame(self, imageData):
+ image = Image.frombytes(
+ 'RGBA',
+ scale(self.scale, self.width, self.height, int),
+ imageData
+ )
+ if self.scale != 100 \
+ or self.x != 0 or self.y != 0:
+ frame = BlankFrame(self.width, self.height)
+ frame.paste(image, box=(self.x, self.y))
+ else:
+ frame = image
+ return frame
diff --git a/src/components/waveform.ui b/src/components/waveform.ui
index 5d62150..0e40380 100644
--- a/src/components/waveform.ui
+++ b/src/components/waveform.ui
@@ -226,9 +226,31 @@
-
-
+
- Mirror
+ Opacity
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ QAbstractSpinBox::UpDownArrows
+
+
+ %
+
+
+ 10
+
+
+ 400
+
+
+ 100
@@ -263,6 +285,75 @@
+ -
+
+
-
+
+
+ Compress
+
+
+
+ -
+
+
+ Mono
+
+
+
+ -
+
+
+ Mirror
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Amplitude
+
+
+
+ -
+
+
-
+
+ Linear
+
+
+ -
+
+ Logarithmic
+
+
+ -
+
+ Square root
+
+
+ -
+
+ Cubic root
+
+
+
+
+
+
-
diff --git a/src/toolkit/common.py b/src/toolkit/common.py
index 128ed08..5d424e0 100644
--- a/src/toolkit/common.py
+++ b/src/toolkit/common.py
@@ -6,22 +6,9 @@ import string
import os
import sys
import subprocess
-import signal
-import math
from collections import OrderedDict
-def scale(scale, width, height, returntype=None):
- width = (float(width) / 100.0) * float(scale)
- height = (float(height) / 100.0) * float(scale)
- if returntype == str:
- return (str(math.ceil(width)), str(math.ceil(height)))
- elif returntype == int:
- return (math.ceil(width), math.ceil(height))
- else:
- return (width, height)
-
-
def badName(name):
'''Returns whether a name contains non-alphanumeric chars'''
return any([letter in string.punctuation for letter in name])
@@ -69,14 +56,6 @@ def checkOutput(commandList, **kwargs):
return subprocess.check_output(commandList, **kwargs)
-@pipeWrapper
-def openPipe(commandList, **kwargs):
- return subprocess.Popen(commandList, **kwargs)
-
-def closePipe(pipe):
- pipe.stdout.close()
- pipe.send_signal(signal.SIGINT)
-
def disableWhenEncoding(func):
def decorator(self, *args, **kwargs):
if self.encoding:
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index fea9d4e..e37282f 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -6,10 +6,12 @@ import sys
import os
import subprocess
import threading
+import signal
from queue import PriorityQueue
import core
-from toolkit.common import checkOutput, openPipe
+from toolkit.common import checkOutput, pipeWrapper
+from component import ComponentError
class FfmpegVideo:
@@ -60,7 +62,8 @@ class FfmpegVideo:
kwargs['filter_']
)
self.command.extend([
- '-vcodec', 'rawvideo', '-',
+ '-s:v', '%sx%s' % (self.width, self.height),
+ '-codec:v', 'rawvideo', '-',
])
self.frameBuffer = PriorityQueue()
@@ -85,9 +88,11 @@ class FfmpegVideo:
self.frameBuffer.task_done()
def fillBuffer(self):
+ import sys
+ print(self.command)
self.pipe = openPipe(
self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
- stderr=subprocess.DEVNULL, bufsize=10**8
+ stderr=sys.__stdout__, bufsize=10**8
)
while True:
if self.parent.canceled:
@@ -100,7 +105,7 @@ class FfmpegVideo:
self.frameBuffer.put((self.frameNo-1, self.lastFrame))
continue
except AttributeError:
- Video.threadError = ComponentError(self.component, 'video')
+ FfmpegVideo.threadError = ComponentError(self.component, 'video')
break
self.currentFrame = self.pipe.stdout.read(self.chunkSize)
@@ -109,6 +114,16 @@ class FfmpegVideo:
self.lastFrame = self.currentFrame
+@pipeWrapper
+def openPipe(commandList, **kwargs):
+ return subprocess.Popen(commandList, **kwargs)
+
+
+def closePipe(pipe):
+ pipe.stdout.close()
+ pipe.send_signal(signal.SIGINT)
+
+
def findFfmpeg():
if getattr(sys, 'frozen', False):
# The application is frozen
@@ -347,7 +362,12 @@ def getAudioDuration(filename):
except subprocess.CalledProcessError as ex:
fileInfo = ex.output
- info = fileInfo.decode("utf-8").split('\n')
+ try:
+ info = fileInfo.decode("utf-8").split('\n')
+ except UnicodeDecodeError as e:
+ print('Unicode error:', str(e))
+ return False
+
for line in info:
if 'Duration' in line:
d = line.split(',')[0]
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index b66e037..f42d4c9 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -6,6 +6,7 @@ from PIL import Image
from PIL.ImageQt import ImageQt
import sys
import os
+import math
import core
@@ -41,6 +42,17 @@ class PaintColor(QtGui.QColor):
super().__init__(b, g, r, a)
+def scale(scale, width, height, returntype=None):
+ width = (float(width) / 100.0) * float(scale)
+ height = (float(height) / 100.0) * float(scale)
+ if returntype == str:
+ return (str(math.ceil(width)), str(math.ceil(height)))
+ elif returntype == int:
+ return (math.ceil(width), math.ceil(height))
+ else:
+ return (width, height)
+
+
def defaultSize(framefunc):
'''Makes width/height arguments optional'''
def decorator(*args):
diff --git a/src/video_thread.py b/src/video_thread.py
index f27ec21..5963def 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -19,9 +19,11 @@ import time
import signal
from component import ComponentError
-from toolkit import openPipe
-from toolkit.ffmpeg import readAudioFile, createFfmpegCommand
from toolkit.frame import Checkerboard
+from toolkit.ffmpeg import (
+ openPipe, readAudioFile,
+ getAudioDuration, createFfmpegCommand
+)
class Worker(QtCore.QObject):
@@ -132,15 +134,24 @@ class Worker(QtCore.QObject):
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# READ AUDIO, INITIALIZE COMPONENTS, OPEN A PIPE TO FFMPEG
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
-
- self.progressBarSetText.emit("Loading audio file...")
- audioFileTraits = readAudioFile(
- self.inputFile, self
- )
- if audioFileTraits is None:
- self.cancelExport()
- return
- self.completeAudioArray, duration = audioFileTraits
+ if any([
+ True if 'pcm' in comp.properties() else False
+ for comp in self.components
+ ]):
+ self.progressBarSetText.emit("Loading audio file...")
+ audioFileTraits = readAudioFile(
+ self.inputFile, self
+ )
+ if audioFileTraits is None:
+ self.cancelExport()
+ return
+ self.completeAudioArray, duration = audioFileTraits
+ else:
+ duration = getAudioDuration(self.inputFile)
+ class FakeList:
+ def __len__(self):
+ return int((duration * 44100) + 44100) - 1470
+ self.completeAudioArray = FakeList()
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit("Starting components...")
@@ -284,7 +295,10 @@ class Worker(QtCore.QObject):
numpy.seterr(all='print')
- self.out_pipe.stdin.close()
+ try:
+ self.out_pipe.stdin.close()
+ except BrokenPipeError:
+ print('Broken pipe to ffmpeg!')
if self.out_pipe.stderr is not None:
print(self.out_pipe.stderr.read())
self.out_pipe.stderr.close()
--
cgit v1.2.3
From 1c4afc96d69789f16284c067ffd7098dc7b2ca70 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 10 Aug 2017 16:04:41 -0400
Subject: using the builtin logging module
---
src/component.py | 14 ++++++---
src/components/spectrum.py | 16 ++++++----
src/components/video.py | 19 +++++++++---
src/components/waveform.py | 18 +++++++++---
src/core.py | 73 +++++++++++++++++++++++++++++++++++++++++-----
src/main.py | 6 ++++
src/mainwindow.py | 57 +++++++++++++++++++++++++++---------
src/preview_thread.py | 9 ++++--
src/toolkit/ffmpeg.py | 19 +++++++-----
src/toolkit/frame.py | 6 ++++
src/video_thread.py | 24 ++++++++++-----
11 files changed, 206 insertions(+), 55 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/component.py b/src/component.py
index 5b6f9a7..a1e24db 100644
--- a/src/component.py
+++ b/src/component.py
@@ -8,6 +8,7 @@ import os
import sys
import math
import time
+import logging
from toolkit.frame import BlankFrame
from toolkit import (
@@ -15,6 +16,9 @@ from toolkit import (
)
+log = logging.getLogger('AVP.ComponentHandler')
+
+
class ComponentMetaclass(type(QtCore.QObject)):
'''
Checks the validity of each Component class and mutates some attrs.
@@ -135,17 +139,17 @@ class ComponentMetaclass(type(QtCore.QObject)):
# Turn version string into a number
try:
if 'version' not in attrs:
- print(
+ log.error(
'No version attribute in %s. Defaulting to 1' %
attrs['name'])
attrs['version'] = 1
else:
attrs['version'] = int(attrs['version'].split('.')[0])
except ValueError:
- print('%s component has an invalid version string:\n%s' % (
+ log.critical('%s component has an invalid version string:\n%s' % (
attrs['name'], str(attrs['version'])))
except KeyError:
- print('%s component has no version string.' % attrs['name'])
+ log.critical('%s component has no version string.' % attrs['name'])
else:
return super().__new__(cls, name, parents, attrs)
quit(1)
@@ -546,6 +550,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
and oldRelativeVal != newRelativeVal:
# Float changed without pixel value changing, which
# means the pixel value needs to be updated
+ log.debug('Updating %s #%s\'s relative widget: %s' % (
+ self.name, self.compPos, attr))
self._trackedWidgets[attr].blockSignals(True)
self.updateRelativeWidgetMaximum(attr)
pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
@@ -576,7 +582,7 @@ class ComponentError(RuntimeError):
msg = str(sys.exc_info()[1])
else:
msg = 'Unknown error.'
- print("##### ComponentError by %s's %s: %s" % (
+ log.error("ComponentError by %s's %s: %s" % (
caller.name, name, msg))
# Don't create multiple windows for quickly repeated messages
diff --git a/src/components/spectrum.py b/src/components/spectrum.py
index 666e20a..32763c0 100644
--- a/src/components/spectrum.py
+++ b/src/components/spectrum.py
@@ -4,6 +4,7 @@ import os
import math
import subprocess
import time
+import logging
from component import Component
from toolkit.frame import BlankFrame, scale
@@ -13,6 +14,9 @@ from toolkit.ffmpeg import (
)
+log = logging.getLogger('AVP.Components.Spectrum')
+
+
class Component(Component):
name = 'Spectrum'
version = '1.0.0'
@@ -68,6 +72,7 @@ class Component(Component):
if not changedSize \
and not self.changedOptions \
and self.previewFrame is not None:
+ log.debug('Comp #%s is reusing old preview frame' % self.compPos)
return self.previewFrame
frame = self.getPreviewFrame()
@@ -131,13 +136,14 @@ class Component(Component):
'-frames:v', '1',
])
logFilename = os.path.join(
- self.core.dataDir, 'preview_%s.log' % str(self.compPos))
- with open(logFilename, 'w') as log:
- log.write(" ".join(command) + '\n\n')
- with open(logFilename, 'a') as log:
+ self.core.logDir, 'preview_%s.log' % str(self.compPos))
+ log.debug('Creating ffmpeg process (log at %s)' % logFilename)
+ with open(logFilename, 'w') as logf:
+ logf.write(" ".join(command) + '\n\n')
+ with open(logFilename, 'a') as logf:
pipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
- stderr=log, bufsize=10**8
+ stderr=logf, bufsize=10**8
)
byteFrame = pipe.stdout.read(self.chunkSize)
closePipe(pipe)
diff --git a/src/components/video.py b/src/components/video.py
index b6bdd52..a189f60 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -3,6 +3,7 @@ from PyQt5 import QtGui, QtCore, QtWidgets
import os
import math
import subprocess
+import logging
from component import Component
from toolkit.frame import BlankFrame, scale
@@ -10,6 +11,9 @@ from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo
from toolkit import checkOutput
+log = logging.getLogger('AVP.Components.Video')
+
+
class Component(Component):
name = 'Video'
version = '1.0.0'
@@ -134,10 +138,17 @@ class Component(Component):
'-ss', '90',
'-frames:v', '1',
])
- pipe = openPipe(
- command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
- stderr=subprocess.DEVNULL, bufsize=10**8
- )
+
+ logFilename = os.path.join(
+ self.core.logDir, 'preview_%s.log' % str(self.compPos))
+ log.debug('Creating ffmpeg process (log at %s)' % logFilename)
+ with open(logFilename, 'w') as logf:
+ logf.write(" ".join(command) + '\n\n')
+ with open(logFilename, 'a') as logf:
+ pipe = openPipe(
+ command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
+ stderr=logf, bufsize=10**8
+ )
byteFrame = pipe.stdout.read(self.chunkSize)
closePipe(pipe)
diff --git a/src/components/waveform.py b/src/components/waveform.py
index 71cbcac..1517be2 100644
--- a/src/components/waveform.py
+++ b/src/components/waveform.py
@@ -4,6 +4,7 @@ from PyQt5.QtGui import QColor
import os
import math
import subprocess
+import logging
from component import Component
from toolkit.frame import BlankFrame, scale
@@ -13,6 +14,9 @@ from toolkit.ffmpeg import (
)
+log = logging.getLogger('AVP.Components.Waveform')
+
+
class Component(Component):
name = 'Waveform'
version = '1.0.0'
@@ -106,10 +110,16 @@ class Component(Component):
'-codec:v', 'rawvideo', '-',
'-frames:v', '1',
])
- pipe = openPipe(
- command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
- stderr=subprocess.DEVNULL, bufsize=10**8
- )
+ logFilename = os.path.join(
+ self.core.logDir, 'preview_%s.log' % str(self.compPos))
+ log.debug('Creating ffmpeg process (log at %s)' % logFilename)
+ with open(logFilename, 'w') as logf:
+ logf.write(" ".join(command) + '\n\n')
+ with open(logFilename, 'a') as logf:
+ pipe = openPipe(
+ command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
+ stderr=logf, bufsize=10**8
+ )
byteFrame = pipe.stdout.read(self.chunkSize)
closePipe(pipe)
diff --git a/src/core.py b/src/core.py
index 61905eb..4023542 100644
--- a/src/core.py
+++ b/src/core.py
@@ -7,11 +7,17 @@ import sys
import os
import json
from importlib import import_module
+import logging
import toolkit
import video_thread
+log = logging.getLogger('AVP.Core')
+STDOUT_LOGLVL = logging.WARNING
+FILE_LOGLVL = logging.DEBUG
+
+
class Core:
'''
MainWindow and Command module both use an instance of this class
@@ -35,6 +41,7 @@ class Core:
continue
elif ext == '.py':
yield name
+ log.debug('Importing component modules')
self.modules = [
import_module('components.%s' % name)
for name in findComponents()
@@ -67,7 +74,7 @@ class Core:
compPos = len(self.selectedComponents)
if len(self.selectedComponents) > 50:
return None
-
+ log.debug('Inserting Component from module #%s' % moduleIndex)
component = self.modules[moduleIndex].Component(
moduleIndex, compPos, self
)
@@ -104,7 +111,7 @@ class Core:
self.componentListChanged()
def updateComponent(self, i):
- # print('updating %s' % self.selectedComponents[i])
+ log.debug('Updating %s #%s' % (self.selectedComponents[i], str(i)))
self.selectedComponents[i].update()
def moduleIndexFor(self, compName):
@@ -125,12 +132,17 @@ class Core:
if not saveValueStore:
return False
try:
- self.selectedComponents[compIndex].loadPreset(
+ comp = self.selectedComponents[compIndex]
+ comp.loadPreset(
saveValueStore,
presetName
)
except KeyError as e:
- print('preset missing value: %s' % e)
+ log.warning(
+ '%s #%s\'s preset is missing value: %s' % (
+ comp.name, str(compIndex), str(e)
+ )
+ )
self.savedPresets[presetName] = dict(saveValueStore)
return True
@@ -206,7 +218,7 @@ class Core:
preset['preset']
)
except KeyError as e:
- print('%s missing value: %s' % (
+ log.warning('%s missing value: %s' % (
self.selectedComponents[i], e)
)
@@ -224,7 +236,7 @@ class Core:
typ, value, tb = data
if typ.__name__ == 'KeyError':
# probably just an old version, still loadable
- print('file missing value: %s' % value)
+ log.warning('Project file missing value: %s' % value)
return
if hasattr(loader, 'createNewProject'):
loader.createNewProject(prompt=False)
@@ -244,6 +256,7 @@ class Core:
Returns dictionary with section names as the keys, each one
contains a list of tuples: (compName, version, compPresetDict)
'''
+ log.debug('Parsing av file: %s' % filepath)
validSections = (
'Components',
'Settings',
@@ -362,6 +375,7 @@ class Core:
def createProjectFile(self, filepath, window=None):
'''Create a project file (.avp) using the current program state'''
+ log.info('Creating %s' % filepath)
settingsKeys = [
'componentDir',
'inputDir',
@@ -374,9 +388,8 @@ class Core:
filepath += '.avp'
if os.path.exists(filepath):
os.remove(filepath)
- with open(filepath, 'w') as f:
- print('creating %s' % filepath)
+ with open(filepath, 'w') as f:
f.write('[Components]\n')
for comp in self.selectedComponents:
saveValueStore = comp.savePreset()
@@ -443,6 +456,7 @@ class Core:
'settings': QtCore.QSettings(
os.path.join(dataDir, 'settings.ini'),
QtCore.QSettings.IniFormat),
+ 'logDir': os.path.join(dataDir, 'log'),
'presetDir': os.path.join(dataDir, 'presets'),
'componentsPath': os.path.join(wd, 'components'),
'encoderOptions': encoderOptions,
@@ -489,6 +503,13 @@ class Core:
setattr(cls, classvar, val)
cls.loadDefaultSettings()
+ if not os.path.exists(cls.dataDir):
+ os.makedirs(cls.dataDir)
+ for neededDirectory in (
+ cls.presetDir, cls.logDir, cls.settings.value("projectDir")):
+ if not os.path.exists(neededDirectory):
+ os.mkdir(neededDirectory)
+ cls.makeLogger()
@classmethod
def loadDefaultSettings(cls):
@@ -522,6 +543,42 @@ class Core:
if val in ('true', 'false'):
cls.settings.setValue(key, True if val == 'true' else False)
+ @staticmethod
+ def makeLogger():
+ logFilename = os.path.join(Core.logDir, 'avp_debug.log')
+ libLogFilename = os.path.join(Core.logDir, 'global_debug.log')
+ # delete old logs
+ for log in (logFilename, libLogFilename):
+ if os.path.exists(log):
+ os.remove(log)
+
+ # create file handlers to capture every log message somewhere
+ logFile = logging.FileHandler(logFilename)
+ logFile.setLevel(FILE_LOGLVL)
+ libLogFile = logging.FileHandler(libLogFilename)
+ libLogFile.setLevel(FILE_LOGLVL)
+
+ # send some critical log messages to stdout as well
+ logStream = logging.StreamHandler()
+ logStream.setLevel(STDOUT_LOGLVL)
+
+ # create formatters and put everything together
+ fileFormatter = logging.Formatter(
+ '[%(asctime)s] <%(name)s> %(levelname)s: %(message)s'
+ )
+ streamFormatter = logging.Formatter(
+ '<%(name)s> %(message)s'
+ )
+ logFile.setFormatter(fileFormatter)
+ libLogFile.setFormatter(fileFormatter)
+ logStream.setFormatter(streamFormatter)
+ log = logging.getLogger('AVP')
+ log.setLevel(FILE_LOGLVL)
+ log.addHandler(logFile)
+ log.addHandler(logStream)
+ libLog = logging.getLogger()
+ libLog.setLevel(FILE_LOGLVL)
+ libLog.addHandler(libLogFile)
# 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 421a09f..3a6fbe7 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,10 +1,14 @@
from PyQt5 import uic, QtWidgets
import sys
import os
+import logging
from __init__ import wd
+log = logging.getLogger('AVP.Entrypoint')
+
+
def main():
app = QtWidgets.QApplication(sys.argv)
app.setApplicationName("audio-visualizer")
@@ -28,6 +32,7 @@ def main():
from command import Command
main = Command()
+ log.debug("Finished creating command object")
elif mode == 'GUI':
from mainwindow import MainWindow
@@ -48,6 +53,7 @@ def main():
# window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0)
main = MainWindow(window, proj)
+ log.debug("Finished creating main window")
window.raise_()
signal.signal(signal.SIGINT, main.cleanUp)
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 789a6e7..114015c 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -13,6 +13,7 @@ import os
import signal
import filecmp
import time
+import logging
from core import Core
import preview_thread
@@ -20,11 +21,15 @@ from presetmanager import PresetManager
from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput
+log = logging.getLogger('AVP.MainWindow')
+
+
class PreviewWindow(QtWidgets.QLabel):
'''
Paints the preview QLabel and maintains the aspect ratio when the
window is resized.
'''
+ log = logging.getLogger('AVP.MainWindow.Preview')
def __init__(self, parent, img):
super(PreviewWindow, self).__init__()
@@ -58,11 +63,15 @@ class PreviewWindow(QtWidgets.QLabel):
if i >= 0:
component = self.parent.core.selectedComponents[i]
if not hasattr(component, 'previewClickEvent'):
+ self.log.info('Ignored click event')
return
pos = (event.x(), event.y())
size = (self.width(), self.height())
+ butt = event.button()
+ self.log.info('Click event for #%s: %s button %s' % (
+ i, pos, butt))
component.previewClickEvent(
- pos, size, event.button()
+ pos, size, butt
)
self.parent.core.updateComponent(i)
@@ -91,9 +100,10 @@ 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()
+ log.debug(
+ 'Main thread id: {}'.format(QtCore.QThread.currentThreadId()))
# widgets of component settings
self.pages = []
@@ -103,27 +113,23 @@ class MainWindow(QtWidgets.QMainWindow):
self.autosaveCooldown = 0.2
self.encoding = False
- # Create data directory, load/create settings
+ # Find settings created by Core object
self.dataDir = Core.dataDir
self.presetDir = Core.presetDir
self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
self.settings = Core.settings
+
self.presetManager = PresetManager(
uic.loadUi(
os.path.join(Core.wd, 'presetmanager.ui')), self)
- if not os.path.exists(self.dataDir):
- os.makedirs(self.dataDir)
- for neededDirectory in (
- self.presetDir, self.settings.value("projectDir")):
- if not os.path.exists(neededDirectory):
- os.mkdir(neededDirectory)
-
# Create the preview window and its thread, queues, and timers
+ log.debug('Creating preview window')
self.previewWindow = PreviewWindow(self, os.path.join(
Core.wd, "background.png"))
window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
+ log.debug('Starting preview thread')
self.previewQueue = Queue()
self.previewThread = QtCore.QThread(self)
self.previewWorker = preview_thread.Worker(self, self.previewQueue)
@@ -132,6 +138,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.previewWorker.imageCreated.connect(self.showPreviewImage)
self.previewThread.start()
+ log.debug('Starting preview timer')
self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self.processTask.emit)
self.timer.start(500)
@@ -141,6 +148,8 @@ class MainWindow(QtWidgets.QMainWindow):
componentList = self.window.listWidget_componentList
if sys.platform == 'darwin':
+ log.debug(
+ 'Darwin detected: showing progress label below progress bar')
window.progressBar_createVideo.setTextVisible(False)
else:
window.progressLabel.setHidden(True)
@@ -276,6 +285,7 @@ class MainWindow(QtWidgets.QMainWindow):
)
self.updateWindowTitle()
+ log.debug('Showing main window')
window.show()
if project and project != self.autosavePath:
@@ -398,6 +408,7 @@ class MainWindow(QtWidgets.QMainWindow):
@QtCore.pyqtSlot()
def cleanUp(self, *args):
+ log.info('Ending the preview thread')
self.timer.stop()
self.previewThread.quit()
self.previewThread.wait()
@@ -414,11 +425,12 @@ class MainWindow(QtWidgets.QMainWindow):
appName += '*'
except AttributeError:
pass
+ log.debug('Setting window title to %s' % appName)
self.window.setWindowTitle(appName)
@QtCore.pyqtSlot(int, dict)
def updateComponentTitle(self, pos, presetStore=False):
- if type(presetStore) == dict:
+ if type(presetStore) is dict:
name = presetStore['preset']
if name is None or name not in self.core.savedPresets:
modified = False
@@ -428,11 +440,20 @@ class MainWindow(QtWidgets.QMainWindow):
modified = bool(presetStore)
if pos < 0:
pos = len(self.core.selectedComponents)-1
- title = str(self.core.selectedComponents[pos])
+ name = str(self.core.selectedComponents[pos])
+ title = str(name)
if self.core.selectedComponents[pos].currentPreset:
title += ' - %s' % self.core.selectedComponents[pos].currentPreset
if modified:
title += '*'
+ if type(presetStore) is bool:
+ log.debug('Forcing %s #%s\'s modified status to %s: %s' % (
+ name, pos, modified, title
+ ))
+ else:
+ log.debug('Setting %s #%s\'s title: %s' % (
+ name, pos, title
+ ))
self.window.listWidget_componentList.item(pos).setText(title)
def updateCodecs(self):
@@ -493,6 +514,8 @@ class MainWindow(QtWidgets.QMainWindow):
elif force or timeDiff >= self.autosaveCooldown * 5:
self.autosaveCooldown = 0.2
self.autosaveTimes.insert(0, self.lastAutosave)
+ else:
+ log.debug('Autosave rejected by cooldown')
def autosaveExists(self, identical=True):
'''Determines if creating the autosave should be blocked.'''
@@ -500,9 +523,14 @@ class MainWindow(QtWidgets.QMainWindow):
if self.currentProject and os.path.exists(self.autosavePath) \
and filecmp.cmp(
self.autosavePath, self.currentProject) == identical:
+ log.debug(
+ 'Autosave found %s to be identical' % \
+ 'not' if not identical else ''
+ )
return True
except FileNotFoundError:
- print('project file couldn\'t be located:', self.currentProject)
+ log.error(
+ 'Project file couldn\'t be located:', self.currentProject)
return identical
return False
@@ -543,7 +571,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.window.lineEdit_outputFile.setText(fileName)
def stopVideo(self):
- print('stop')
+ log.info('Export cancelled')
self.videoWorker.cancel()
self.canceled = True
@@ -773,6 +801,7 @@ class MainWindow(QtWidgets.QMainWindow):
mousePos = -1
else:
mousePos = mousePos.index(True)
+ log.debug('Click component list row %s' % mousePos)
return mousePos
@disableWhenEncoding
diff --git a/src/preview_thread.py b/src/preview_thread.py
index bb22f0c..9615884 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -8,11 +8,15 @@ from PIL import Image
from PIL.ImageQt import ImageQt
from queue import Queue, Empty
import os
+import logging
from toolkit.frame import Checkerboard
from toolkit import disableWhenOpeningProject
+log = logging.getLogger("AVP.PreviewThread")
+
+
class Worker(QtCore.QObject):
imageCreated = pyqtSignal(QtGui.QImage)
@@ -55,7 +59,7 @@ class Worker(QtCore.QObject):
self.background = Checkerboard(width, height)
frame = self.background.copy()
-
+ log.debug('Creating new preview frame')
components = nextPreviewInformation["components"]
for component in reversed(components):
try:
@@ -73,10 +77,11 @@ class Worker(QtCore.QObject):
newFrame.width, newFrame.height,
width, height
)
+ log.critical(errMsg)
self.error.emit(errMsg)
break
except RuntimeError as e:
- print(e)
+ log.error(str(e))
else:
self.frame = ImageQt(frame)
self.imageCreated.emit(QtGui.QImage(self.frame))
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index 3421049..6ab445c 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -8,12 +8,16 @@ import subprocess
import threading
import signal
from queue import PriorityQueue
+import logging
import core
from toolkit.common import checkOutput, pipeWrapper
from component import ComponentError
+log = logging.getLogger('AVP.Toolkit.Ffmpeg')
+
+
class FfmpegVideo:
'''Opens a pipe to ffmpeg and stores a buffer of raw video frames.'''
@@ -88,13 +92,14 @@ class FfmpegVideo:
def fillBuffer(self):
logFilename = os.path.join(
- core.Core.dataDir, 'extra_%s.log' % str(self.component.compPos))
- with open(logFilename, 'w') as log:
- log.write(" ".join(self.command) + '\n\n')
- with open(logFilename, 'a') as log:
+ core.Core.logDir, 'render_%s.log' % str(self.component.compPos))
+ log.debug('Creating ffmpeg process (log at %s)' % logFilename)
+ with open(logFilename, 'w') as logf:
+ logf.write(" ".join(self.command) + '\n\n')
+ with open(logFilename, 'a') as logf:
self.pipe = openPipe(
self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
- stderr=log, bufsize=10**8
+ stderr=logf, bufsize=10**8
)
while True:
if self.parent.canceled:
@@ -375,7 +380,7 @@ def getAudioDuration(filename):
try:
info = fileInfo.decode("utf-8").split('\n')
except UnicodeDecodeError as e:
- print('Unicode error:', str(e))
+ log.error('Unicode error:', str(e))
return False
for line in info:
@@ -398,7 +403,7 @@ def readAudioFile(filename, videoWorker):
'''
duration = getAudioDuration(filename)
if not duration:
- print('Audio file doesn\'t exist or unreadable.')
+ log.error('Audio file doesn\'t exist or unreadable.')
return
command = [
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index 7e83d58..02f9229 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -7,10 +7,14 @@ from PIL.ImageQt import ImageQt
import sys
import os
import math
+import logging
import core
+log = logging.getLogger('AVP.Toolkit.Frame')
+
+
class FramePainter(QtGui.QPainter):
'''
A QPainter for a blank frame, which can be converted into a
@@ -79,6 +83,7 @@ def FloodFrame(width, height, RgbaTuple):
@defaultSize
def BlankFrame(width, height):
'''The base frame used by each component to start drawing.'''
+ log.debug('Creating new %s*%s blank frame' % (width, height))
return FloodFrame(width, height, (0, 0, 0, 0))
@@ -88,6 +93,7 @@ def Checkerboard(width, height):
A checkerboard to represent transparency to the user.
TODO: Would be cool to generate this image with numpy instead.
'''
+ log.debug('Creating new %s*%s checkerboard' % (width, height))
image = FloodFrame(1920, 1080, (0, 0, 0, 0))
image.paste(Image.open(
os.path.join(core.Core.wd, "background.png")),
diff --git a/src/video_thread.py b/src/video_thread.py
index 5963def..e7e4136 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -17,6 +17,7 @@ from queue import Queue, PriorityQueue
from threading import Thread, Event
import time
import signal
+import logging
from component import ComponentError
from toolkit.frame import Checkerboard
@@ -26,6 +27,9 @@ from toolkit.ffmpeg import (
)
+log = logging.getLogger("AVP.VideoThread")
+
+
class Worker(QtCore.QObject):
imageCreated = pyqtSignal(['QImage'])
@@ -92,7 +96,7 @@ class Worker(QtCore.QObject):
by a renderNode later. All indices are multiples of self.sampleSize
sampleSize * frameNo = audioI, AKA audio data starting at frameNo
'''
- print('Dispatching Frames for Compositing...')
+ log.debug('Dispatching Frames for Compositing...')
for audioI in range(0, len(self.completeAudioArray), self.sampleSize):
self.compositeQueue.put(audioI)
@@ -156,10 +160,12 @@ class Worker(QtCore.QObject):
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit("Starting components...")
canceledByComponent = False
- print('Loaded Components:', ", ".join([
+ initText = ", ".join([
"%s) %s" % (num, str(component))
for num, component in enumerate(reversed(self.components))
- ]))
+ ])
+ print('Loaded Components:', initText)
+ log.info('Calling preFrameRender for %s' % initText)
self.staticComponents = {}
for compNo, comp in enumerate(reversed(self.components)):
try:
@@ -191,6 +197,7 @@ class Worker(QtCore.QObject):
compError[0]
)
)
+ log.critical(errMsg)
comp._error.emit(errMsg, compError[1])
break
if 'static' in compProps:
@@ -199,7 +206,7 @@ class Worker(QtCore.QObject):
if self.canceled:
if canceledByComponent:
- print('Export cancelled by component #%s (%s): %s' % (
+ log.critical('Export cancelled by component #%s (%s): %s' % (
compNo,
comp.name,
'No message.' if comp.error() is None else (
@@ -224,8 +231,11 @@ class Worker(QtCore.QObject):
ffmpegCommand = createFfmpegCommand(
self.inputFile, self.outputFile, self.components, duration
)
- print('###### FFMPEG COMMAND ######\n%s' % " ".join(ffmpegCommand))
+ cmd = " ".join(ffmpegCommand)
+ print('###### FFMPEG COMMAND ######\n%s' % cmd)
print('############################')
+ log.info('Opening pipe to ffmpeg')
+ log.info(cmd)
self.out_pipe = openPipe(
ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
)
@@ -298,9 +308,9 @@ class Worker(QtCore.QObject):
try:
self.out_pipe.stdin.close()
except BrokenPipeError:
- print('Broken pipe to ffmpeg!')
+ log.error('Broken pipe to ffmpeg!')
if self.out_pipe.stderr is not None:
- print(self.out_pipe.stderr.read())
+ log.error(self.out_pipe.stderr.read())
self.out_pipe.stderr.close()
self.error = True
self.out_pipe.wait()
--
cgit v1.2.3
From a233d36ce29be459cc9cc041e77f96b9f40d0ed0 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 13 Aug 2017 15:43:42 -0400
Subject: graceful renderNode crash, code clean-up
---
src/components/life.py | 4 +--
src/video_thread.py | 88 +++++++++++++++++++++++++++++---------------------
2 files changed, 54 insertions(+), 38 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/components/life.py b/src/components/life.py
index 02dd76b..2383d30 100644
--- a/src/components/life.py
+++ b/src/components/life.py
@@ -124,7 +124,7 @@ class Component(Component):
self.tickGrids = {0: self.startingGrid}
tick = 0
for frameNo in range(
- self.tickRate, len(self.completeAudioArray), self.sampleSize
+ self.tickRate, self.audioArrayLen, self.sampleSize
):
if self.parent.canceled:
break
@@ -133,7 +133,7 @@ class Component(Component):
self.tickGrids[tick] = self.gridForTick(tick)
# update progress bar
- progress = int(100*(frameNo/len(self.completeAudioArray)))
+ progress = int(100*(frameNo/self.audioArrayLen))
if progress >= 100:
progress = 100
pStr = "Computing evolution: "+str(progress)+'%'
diff --git a/src/video_thread.py b/src/video_thread.py
index e7e4136..63d06ee 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -32,7 +32,7 @@ log = logging.getLogger("AVP.VideoThread")
class Worker(QtCore.QObject):
- imageCreated = pyqtSignal(['QImage'])
+ imageCreated = pyqtSignal('QImage')
videoCreated = pyqtSignal()
progressBarUpdate = pyqtSignal(int)
progressBarSetText = pyqtSignal(str)
@@ -50,6 +50,7 @@ class Worker(QtCore.QObject):
self.outputFile = outputFile
self.inputFile = inputFile
+ self.hertz = 44100
self.sampleSize = 1470 # 44100 / 30 = 1470
self.canceled = False
self.error = False
@@ -62,30 +63,40 @@ class Worker(QtCore.QObject):
to create subframes & composite them into the final frame.
The resulting frames are collected in the renderQueue
'''
+ def err():
+ self.closePipe()
+ self.cancelExport()
+ self.error = True
+ comp._error.emit('A render node failed critically.', str(e))
+
while not self.stopped:
audioI = self.compositeQueue.get()
bgI = int(audioI / self.sampleSize)
frame = None
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
- continue
- # static component
- if frame is None: # bottom-most layer
- frame = self.staticComponents[layerNo]
- else:
- frame = Image.alpha_composite(
- frame, self.staticComponents[layerNo]
- )
- else:
- # animated component
- if frame is None: # bottom-most layer
- frame = comp.frameRender(bgI)
+ try:
+ if layerNo in self.staticComponents:
+ if self.staticComponents[layerNo] is None:
+ # this layer was merged into a following layer
+ continue
+ # static component
+ if frame is None: # bottom-most layer
+ frame = self.staticComponents[layerNo]
+ else:
+ frame = Image.alpha_composite(
+ frame, self.staticComponents[layerNo]
+ )
+
else:
- frame = Image.alpha_composite(
- frame, comp.frameRender(bgI)
- )
+ # animated component
+ if frame is None: # bottom-most layer
+ frame = comp.frameRender(bgI)
+ else:
+ frame = Image.alpha_composite(
+ frame, comp.frameRender(bgI)
+ )
+ except Exception as e:
+ err()
self.renderQueue.put([audioI, frame])
self.compositeQueue.task_done()
@@ -98,7 +109,7 @@ class Worker(QtCore.QObject):
'''
log.debug('Dispatching Frames for Compositing...')
- for audioI in range(0, len(self.completeAudioArray), self.sampleSize):
+ for audioI in range(0, self.audioArrayLen, self.sampleSize):
self.compositeQueue.put(audioI)
def previewDispatch(self):
@@ -150,17 +161,18 @@ class Worker(QtCore.QObject):
self.cancelExport()
return
self.completeAudioArray, duration = audioFileTraits
+ self.audioArrayLen = len(self.completeAudioArray)
else:
duration = getAudioDuration(self.inputFile)
- class FakeList:
- def __len__(self):
- return int((duration * 44100) + 44100) - 1470
- self.completeAudioArray = FakeList()
+ self.completeAudioArray = []
+ self.audioArrayLen = int(
+ ((duration * self.hertz) +
+ self.hertz) - self.sampleSize)
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit("Starting components...")
canceledByComponent = False
- initText = ", ".join([
+ initText = ", ".join([
"%s) %s" % (num, str(component))
for num, component in enumerate(reversed(self.components))
])
@@ -172,6 +184,7 @@ class Worker(QtCore.QObject):
comp.preFrameRender(
audioFile=self.inputFile,
completeAudioArray=self.completeAudioArray,
+ audioArrayLen=self.audioArrayLen,
sampleSize=self.sampleSize,
progressBarUpdate=self.progressBarUpdate,
progressBarSetText=self.progressBarSetText
@@ -276,7 +289,7 @@ class Worker(QtCore.QObject):
self.progressBarSetText.emit("Exporting video...")
if not self.canceled:
for audioI in range(
- 0, len(self.completeAudioArray), self.sampleSize):
+ 0, self.audioArrayLen, self.sampleSize):
while True:
if audioI in frameBuffer or self.canceled:
# if frame's in buffer, pipe it to ffmpeg
@@ -295,7 +308,7 @@ class Worker(QtCore.QObject):
break
# increase progress bar value
- completion = (audioI / len(self.completeAudioArray)) * 100
+ completion = (audioI / self.audioArrayLen) * 100
if progressBarValue + 1 <= completion:
progressBarValue = numpy.floor(completion)
self.progressBarUpdate.emit(progressBarValue)
@@ -305,15 +318,7 @@ class Worker(QtCore.QObject):
numpy.seterr(all='print')
- try:
- self.out_pipe.stdin.close()
- except BrokenPipeError:
- log.error('Broken pipe to ffmpeg!')
- if self.out_pipe.stderr is not None:
- log.error(self.out_pipe.stderr.read())
- self.out_pipe.stderr.close()
- self.error = True
- self.out_pipe.wait()
+ self.closePipe()
for comp in reversed(self.components):
comp.postFrameRender()
@@ -342,6 +347,17 @@ class Worker(QtCore.QObject):
self.encoding.emit(False)
self.videoCreated.emit()
+ def closePipe(self):
+ try:
+ self.out_pipe.stdin.close()
+ except BrokenPipeError:
+ log.error('Broken pipe to ffmpeg!')
+ if self.out_pipe.stderr is not None:
+ log.error(self.out_pipe.stderr.read())
+ self.out_pipe.stderr.close()
+ self.error = True
+ self.out_pipe.wait()
+
def cancelExport(self):
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit('Export Canceled')
--
cgit v1.2.3
From 9c8792df9bad068fed8a9a1777b2774c103c9ce4 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 13 Aug 2017 15:54:36 -0400
Subject: made an authors file
---
AUTHORS | 11 +++++++++++
src/video_thread.py | 4 +++-
2 files changed, 14 insertions(+), 1 deletion(-)
create mode 100644 AUTHORS
(limited to 'src/video_thread.py')
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..417d97e
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,11 @@
+Original version created by Martin Kaistra
+Version 2 created by tassaron and DH4
+
+Contributors:
+* Martin Kaistra
+* Brianna Rainey
+* DH4
+
+Pull Requests By:
+* HunterwolfAT
+* rikai
diff --git a/src/video_thread.py b/src/video_thread.py
index 63d06ee..5acbda4 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -67,7 +67,9 @@ class Worker(QtCore.QObject):
self.closePipe()
self.cancelExport()
self.error = True
- comp._error.emit('A render node failed critically.', str(e))
+ msg = 'A render node failed critically.'
+ log.critical(msg)
+ comp._error.emit(msg, str(e))
while not self.stopped:
audioI = self.compositeQueue.get()
--
cgit v1.2.3
From bed07479f1b4bf24a0b9c84217d41ebbe880a8fb Mon Sep 17 00:00:00 2001
From: tassaron
Date: Mon, 14 Aug 2017 10:10:32 -0400
Subject: faster Spectrum preview & custom VERBOSE loglvl
---
src/__init__.py | 23 ++++++++++++++++++++
src/component.py | 5 +++++
src/components/spectrum.py | 52 +++++++++++++++++++++++-----------------------
src/core.py | 10 +++++----
src/mainwindow.py | 6 +++---
src/toolkit/frame.py | 3 ++-
src/video_thread.py | 4 ++--
7 files changed, 67 insertions(+), 36 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/__init__.py b/src/__init__.py
index 2f4cffa..73f174a 100644
--- a/src/__init__.py
+++ b/src/__init__.py
@@ -1,5 +1,28 @@
import sys
import os
+import logging
+
+
+class Logger(logging.getLoggerClass()):
+ '''
+ Custom Logger class to handle custom VERBOSE log level.
+ Levels used in this program are as follows:
+ VERBOSE Annoyingly frequent debug messages (e.g, in loops)
+ DEBUG Ordinary debug information
+ INFO Expected events that are expensive or irreversible
+ WARNING A non-fatal error or suspicious behaviour
+ ERROR Any error that would interrupt the user
+ CRITICAL Things that really shouldn't happen at all
+ '''
+ def __init__(self, name, level=logging.NOTSET):
+ super().__init__(name, level)
+ logging.addLevelName(5, "VERBOSE")
+
+ def verbose(self, msg, *args, **kwargs):
+ if self.isEnabledFor(5):
+ self._log(5, msg, args, **kwargs)
+logging.setLoggerClass(Logger)
+logging.VERBOSE = 5
if getattr(sys, 'frozen', False):
diff --git a/src/component.py b/src/component.py
index d011f1e..cf3085c 100644
--- a/src/component.py
+++ b/src/component.py
@@ -39,6 +39,11 @@ class ComponentMetaclass(type(QtCore.QObject)):
def renderWrapper(func):
def renderWrapper(self, *args, **kwargs):
try:
+ log.verbose('### %s #%s renders%s frame %s###' % (
+ self.__class__.name, str(self.compPos),
+ '' if args else ' a preview',
+ '' if not args else '%s ' % args[0],
+ ))
return func(self, *args, **kwargs)
except Exception as e:
try:
diff --git a/src/components/spectrum.py b/src/components/spectrum.py
index 32763c0..246b839 100644
--- a/src/components/spectrum.py
+++ b/src/components/spectrum.py
@@ -27,6 +27,8 @@ class Component(Component):
self._image = BlankFrame(self.width, self.height)
self.chunkSize = 4 * self.width * self.height
self.changedOptions = True
+ self.previewSize = (214, 120)
+ self.previewPipe = None
if hasattr(self.parent, 'window'):
# update preview when audio file changes (if genericPreview is off)
@@ -72,7 +74,8 @@ class Component(Component):
if not changedSize \
and not self.changedOptions \
and self.previewFrame is not None:
- log.debug('Comp #%s is reusing old preview frame' % self.compPos)
+ log.debug(
+ 'Spectrum #%s is reusing old preview frame' % self.compPos)
return self.previewFrame
frame = self.getPreviewFrame()
@@ -86,6 +89,7 @@ class Component(Component):
def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs)
+ self.previewPipe.wait()
self.updateChunksize()
w, h = scale(self.scale, self.width, self.height, str)
self.video = FfmpegVideo(
@@ -141,18 +145,21 @@ class Component(Component):
with open(logFilename, 'w') as logf:
logf.write(" ".join(command) + '\n\n')
with open(logFilename, 'a') as logf:
- pipe = openPipe(
+ self.previewPipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=logf, bufsize=10**8
)
- byteFrame = pipe.stdout.read(self.chunkSize)
- closePipe(pipe)
+ byteFrame = self.previewPipe.stdout.read(self.chunkSize)
+ closePipe(self.previewPipe)
frame = self.finalizeFrame(byteFrame)
return frame
def makeFfmpegFilter(self, preview=False, startPt=0):
- w, h = scale(self.scale, self.width, self.height, str)
+ if preview:
+ w, h = self.previewSize
+ else:
+ w, h = (self.width, self.height)
color = self.page.comboBox_color.currentText().lower()
genericPreview = self.settings.value("pref_genericPreview")
@@ -173,8 +180,7 @@ class Component(Component):
'showspectrum=s=%sx%s:slide=scroll:win_func=%s:'
'color=%s:scale=%s,'
'colorkey=color=black:similarity=0.1:blend=0.5' % (
- self.settings.value("outputWidth"),
- self.settings.value("outputHeight"),
+ w, h,
self.page.comboBox_window.currentText(),
color, amplitude,
)
@@ -197,8 +203,7 @@ class Component(Component):
filter_ = (
'ahistogram=r=%s:s=%sx%s:dmode=separate:ascale=%s:scale=%s' % (
self.settings.value("outputFrameRate"),
- self.settings.value("outputWidth"),
- self.settings.value("outputHeight"),
+ w, h,
amplitude, display
)
)
@@ -214,8 +219,7 @@ class Component(Component):
m = self.page.comboBox_mode.currentText()
filter_ = (
'avectorscope=s=%sx%s:draw=%s:m=%s:scale=%s:zoom=%s' % (
- self.settings.value("outputWidth"),
- self.settings.value("outputHeight"),
+ w, h,
'line'if self.draw else 'dot',
m, amplitude, str(self.zoom),
)
@@ -225,8 +229,7 @@ class Component(Component):
'showcqt=r=%s:s=%sx%s:count=30:text=0:tc=%s,'
'colorkey=color=black:similarity=0.1:blend=0.5 ' % (
self.settings.value("outputFrameRate"),
- self.settings.value("outputWidth"),
- self.settings.value("outputHeight"),
+ w, h,
str(self.tc),
)
)
@@ -235,28 +238,28 @@ class Component(Component):
'aphasemeter=r=%s:s=%sx%s:video=1 [atrash][vtmp1]; '
'[atrash] anullsink; '
'[vtmp1] colorkey=color=black:similarity=0.1:blend=0.5, '
- 'crop=in_w/8:in_h:(in_w/8)*7:0 '% (
+ 'crop=in_w/8:in_h:(in_w/8)*7:0 ' % (
self.settings.value("outputFrameRate"),
- self.settings.value("outputWidth"),
- self.settings.value("outputHeight"),
+ w, h,
)
)
return [
'-filter_complex',
'%s%s%s%s [v1]; '
- '[v1] %sscale=%s:%s%s%s%s [v]' % (
+ '[v1] %s%s%s%s%s [v]' % (
exampleSound() if preview and genericPreview else '[0:a] ',
'compand=gain=4,' if self.compress else '',
'aformat=channel_layouts=mono,' if self.mono else '',
filter_,
'hflip, ' if self.mirror else '',
- w, h,
- ', hue=h=%s:s=10' % str(self.hue) if self.hue > 0 else '',
- ', trim=start=%s:end=%s' % (
+ 'trim=start=%s:end=%s, ' % (
"{0:.3f}".format(startPt + 12),
"{0:.3f}".format(startPt + 12.5)
) if preview else '',
+ 'scale=%sx%s' % scale(
+ self.scale, self.width, self.height, str),
+ ', hue=h=%s:s=10' % str(self.hue) if self.hue > 0 else '',
', convolution=-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2:-2 '
'-1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2'
if self.filterType == 3 else ''
@@ -281,10 +284,7 @@ class Component(Component):
self._image = image
except ValueError:
image = self._image
- if self.scale != 100 \
- or self.x != 0 or self.y != 0:
- frame = BlankFrame(self.width, self.height)
- frame.paste(image, box=(self.x, self.y))
- else:
- frame = image
+
+ frame = BlankFrame(self.width, self.height)
+ frame.paste(image, box=(self.x, self.y))
return frame
diff --git a/src/core.py b/src/core.py
index 2b85f7e..4dfb210 100644
--- a/src/core.py
+++ b/src/core.py
@@ -562,9 +562,10 @@ class Core:
logStream = logging.StreamHandler()
logStream.setLevel(STDOUT_LOGLVL)
- # create formatters and put everything together
+ # create formatters for each stream
fileFormatter = logging.Formatter(
- '[%(asctime)s] <%(name)s> %(levelname)s: %(message)s'
+ '[%(asctime)s] %(threadName)-10.10s %(name)-23.23s %(levelname)s: '
+ '%(message)s'
)
streamFormatter = logging.Formatter(
'<%(name)s> %(message)s'
@@ -572,13 +573,14 @@ class Core:
logFile.setFormatter(fileFormatter)
libLogFile.setFormatter(fileFormatter)
logStream.setFormatter(streamFormatter)
+
log = logging.getLogger('AVP')
- log.setLevel(FILE_LOGLVL)
log.addHandler(logFile)
log.addHandler(logStream)
libLog = logging.getLogger()
- libLog.setLevel(FILE_LOGLVL)
libLog.addHandler(libLogFile)
+ # lowest level must be explicitly set on the root Logger
+ libLog.setLevel(0)
# always store settings in class variables even if a Core object is not created
Core.storeSettings()
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 1abb108..af6e190 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -44,7 +44,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.window = window
self.core = Core()
log.debug(
- 'Main thread id: {}'.format(QtCore.QThread.currentThreadId()))
+ 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId())))
# widgets of component settings
self.pages = []
@@ -465,8 +465,8 @@ class MainWindow(QtWidgets.QMainWindow):
and filecmp.cmp(
self.autosavePath, self.currentProject) == identical:
log.debug(
- 'Autosave found %s to be identical' % \
- 'not' if not identical else ''
+ 'Autosave found %s to be identical'
+ % 'not' if not identical else ''
)
return True
except FileNotFoundError:
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index 63774a6..ad8537c 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -21,6 +21,7 @@ class FramePainter(QtGui.QPainter):
Pillow image with finalize()
'''
def __init__(self, width, height):
+ log.verbose('Creating new FramePainter')
image = BlankFrame(width, height)
self.image = QtGui.QImage(ImageQt(image))
super().__init__(self.image)
@@ -77,7 +78,7 @@ def defaultSize(framefunc):
def FloodFrame(width, height, RgbaTuple):
- log.debug('Creating new %s*%s %s flood frame' % (
+ log.verbose('Creating new %s*%s %s flood frame' % (
width, height, RgbaTuple))
return Image.new("RGBA", (width, height), RgbaTuple)
diff --git a/src/video_thread.py b/src/video_thread.py
index 5acbda4..87fb9bd 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -212,7 +212,7 @@ class Worker(QtCore.QObject):
compError[0]
)
)
- log.critical(errMsg)
+ log.error(errMsg)
comp._error.emit(errMsg, compError[1])
break
if 'static' in compProps:
@@ -221,7 +221,7 @@ class Worker(QtCore.QObject):
if self.canceled:
if canceledByComponent:
- log.critical('Export cancelled by component #%s (%s): %s' % (
+ log.error('Export cancelled by component #%s (%s): %s' % (
compNo,
comp.name,
'No message.' if comp.error() is None else (
--
cgit v1.2.3
From d4b63e4d4612db262424fe10c83f8eaa4f741f24 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sat, 19 Aug 2017 20:45:44 -0400
Subject: remove % from log calls
---
src/component.py | 32 +++++++++++++++++---------------
src/core.py | 19 ++++++++++---------
src/gui/actions.py | 3 ++-
src/gui/mainwindow.py | 26 +++++++++++++++++++++-----
src/gui/presetmanager.py | 2 +-
src/toolkit/common.py | 16 ++++++++++------
src/toolkit/ffmpeg.py | 2 +-
src/video_thread.py | 7 ++++---
8 files changed, 66 insertions(+), 41 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/component.py b/src/component.py
index ba86422..992a82e 100644
--- a/src/component.py
+++ b/src/component.py
@@ -40,11 +40,11 @@ class ComponentMetaclass(type(QtCore.QObject)):
def renderWrapper(func):
def renderWrapper(self, *args, **kwargs):
try:
- log.verbose('### %s #%s renders%s frame %s###' % (
+ log.verbose('### %s #%s renders%s frame %s###',
self.__class__.name, str(self.compPos),
'' if args else ' a preview',
'' if not args else '%s ' % args[0],
- ))
+ )
return func(self, *args, **kwargs)
except Exception as e:
try:
@@ -170,7 +170,7 @@ class ComponentMetaclass(type(QtCore.QObject)):
def __exit__(self, *args):
for widgetList in self.comp._allWidgets.values():
for widget in widgetList:
- log.verbose('Connecting %s' % str(
+ log.verbose('Connecting %s', str(
widget.__class__.__name__))
connectWidget(widget, self.comp.update)
@@ -230,16 +230,18 @@ class ComponentMetaclass(type(QtCore.QObject)):
try:
if 'version' not in attrs:
log.error(
- 'No version attribute in %s. Defaulting to 1' %
+ 'No version attribute in %s. Defaulting to 1',
attrs['name'])
attrs['version'] = 1
else:
attrs['version'] = int(attrs['version'].split('.')[0])
except ValueError:
- log.critical('%s component has an invalid version string:\n%s' % (
- attrs['name'], str(attrs['version'])))
+ log.critical(
+ '%s component has an invalid version string:\n%s',
+ attrs['name'], str(attrs['version'])
+ )
except KeyError:
- log.critical('%s component has no version string.' % attrs['name'])
+ log.critical('%s component has no version string.', attrs['name'])
else:
return super().__new__(cls, name, parents, attrs)
quit(1)
@@ -384,9 +386,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
self.parent = parent
self.settings = parent.settings
- log.verbose('Creating UI for %s #%s\'s widget' % (
+ log.verbose('Creating UI for %s #%s\'s widget',
self.name, self.compPos
- ))
+ )
self.page = self.loadUi(self.__class__.ui)
# Find all normal widgets which will be connected after subclass method
@@ -702,7 +704,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
can make determining the 'previous' value tricky.
'''
if self.oldAttrs is not None:
- log.verbose('Using nonstandard oldAttr for %s' % attr)
+ log.verbose('Using nonstandard oldAttr for %s', attr)
return self.oldAttrs[attr]
else:
return getattr(self, attr)
@@ -723,8 +725,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
and oldRelativeVal != newRelativeVal:
# Float changed without pixel value changing, which
# means the pixel value needs to be updated
- log.debug('Updating %s #%s\'s relative widget: %s' % (
- self.name, self.compPos, attr))
+ log.debug(
+ 'Updating %s #%s\'s relative widget: %s',
+ self.name, self.compPos, attr)
with blockSignals(self._trackedWidgets[attr]):
self.updateRelativeWidgetMaximum(attr)
pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
@@ -828,9 +831,8 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
self.modifiedVals[attr] = val
else:
log.warning(
- '%s component settings changed at once. (%s)' % (
- len(self.modifiedVals), repr(self.modifiedVals)
- )
+ '%s component settings changed at once. (%s)',
+ len(self.modifiedVals), repr(self.modifiedVals)
)
def id(self):
diff --git a/src/core.py b/src/core.py
index 169716c..bfb8272 100644
--- a/src/core.py
+++ b/src/core.py
@@ -77,7 +77,8 @@ class Core:
if type(component) is int:
# create component using module index in self.modules
moduleIndex = int(component)
- log.debug('Creating new component from module #%s' % moduleIndex)
+ log.debug(
+ 'Creating new component from module #%s', str(moduleIndex))
component = self.modules[moduleIndex].Component(
moduleIndex, compPos, self
)
@@ -85,7 +86,7 @@ class Core:
else:
moduleIndex = -1
log.debug(
- 'Inserting previously-created %s component' % component.name)
+ 'Inserting previously-created %s component', component.name)
component._error.connect(
loader.videoThreadError
@@ -117,8 +118,9 @@ class Core:
self.componentListChanged()
def updateComponent(self, i):
- log.debug('Auto-updating %s #%s' % (
- self.selectedComponents[i], str(i)))
+ log.debug(
+ 'Auto-updating %s #%s',
+ self.selectedComponents[i], str(i))
self.selectedComponents[i].update(auto=True)
def moduleIndexFor(self, compName):
@@ -146,9 +148,8 @@ class Core:
)
except KeyError as e:
log.warning(
- '%s #%s\'s preset is missing value: %s' % (
- comp.name, str(compIndex), str(e)
- )
+ '%s #%s\'s preset is missing value: %s',
+ comp.name, str(compIndex), str(e)
)
self.savedPresets[presetName] = dict(saveValueStore)
@@ -266,7 +267,7 @@ class Core:
Returns dictionary with section names as the keys, each one
contains a list of tuples: (compName, version, compPresetDict)
'''
- log.debug('Parsing av file: %s' % filepath)
+ log.debug('Parsing av file: %s', filepath)
validSections = (
'Components',
'Settings',
@@ -385,7 +386,7 @@ class Core:
def createProjectFile(self, filepath, window=None):
'''Create a project file (.avp) using the current program state'''
- log.info('Creating %s' % filepath)
+ log.info('Creating %s', filepath)
settingsKeys = [
'componentDir',
'inputDir',
diff --git a/src/gui/actions.py b/src/gui/actions.py
index 1444569..f101bd7 100644
--- a/src/gui/actions.py
+++ b/src/gui/actions.py
@@ -3,6 +3,7 @@
'''
from PyQt5.QtWidgets import QUndoCommand
import os
+from copy import copy
from core import Core
@@ -132,7 +133,7 @@ class OpenPreset(QUndoCommand):
comp = self.parent.core.selectedComponents[compI]
self.store = comp.savePreset()
- self.store['preset'] = str(comp.currentPreset)
+ self.store['preset'] = copy(comp.currentPreset)
def redo(self):
self.parent._openPreset(self.presetName, self.compI)
diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py
index 76c53af..833d2d1 100644
--- a/src/gui/mainwindow.py
+++ b/src/gui/mainwindow.py
@@ -387,30 +387,46 @@ class MainWindow(QtWidgets.QMainWindow):
@QtCore.pyqtSlot(int, dict)
def updateComponentTitle(self, pos, presetStore=False):
+ '''
+ Sets component title to modified or unmodified when given boolean.
+ If given a preset dict, compares it against the component to
+ determine if it is modified.
+ A component with no preset is always unmodified.
+ '''
if type(presetStore) is dict:
name = presetStore['preset']
if name is None or name not in self.core.savedPresets:
modified = False
else:
modified = (presetStore != self.core.savedPresets[name])
+ if modified:
+ log.verbose(
+ 'Differing values between presets: %s',
+ ", ".join([
+ '%s: %s' % item for item in presetStore.items()
+ if val != self.core.savedPresets[name][key]
+ ])
+ )
else:
modified = bool(presetStore)
if pos < 0:
pos = len(self.core.selectedComponents)-1
- name = str(self.core.selectedComponents[pos])
+ name = self.core.selectedComponents[pos].name
title = str(name)
if self.core.selectedComponents[pos].currentPreset:
title += ' - %s' % self.core.selectedComponents[pos].currentPreset
if modified:
title += '*'
if type(presetStore) is bool:
- log.debug('Forcing %s #%s\'s modified status to %s: %s' % (
+ log.debug(
+ 'Forcing %s #%s\'s modified status to %s: %s',
name, pos, modified, title
- ))
+ )
else:
- log.debug('Setting %s #%s\'s title: %s' % (
+ log.debug(
+ 'Setting %s #%s\'s title: %s',
name, pos, title
- ))
+ )
self.window.listWidget_componentList.item(pos).setText(title)
def updateCodecs(self):
diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py
index befa7cd..2445760 100644
--- a/src/gui/presetmanager.py
+++ b/src/gui/presetmanager.py
@@ -210,7 +210,7 @@ class PresetManager(QtWidgets.QDialog):
def _openPreset(self, presetName, index):
selectedComponents = self.core.selectedComponents
- componentName = str(selectedComponents[index]).strip()
+ componentName = selectedComponents[index].name.strip()
version = selectedComponents[index].version
dirname = os.path.join(self.presetDir, componentName, str(version))
filepath = os.path.join(dirname, presetName)
diff --git a/src/toolkit/common.py b/src/toolkit/common.py
index 74143e8..95aeab3 100644
--- a/src/toolkit/common.py
+++ b/src/toolkit/common.py
@@ -29,15 +29,19 @@ class blockSignals:
)
def __enter__(self):
- log.verbose('Blocking signals for %s' % ", ".join([
- str(w.__class__.__name__) for w in self.widgets
- ]))
+ log.verbose(
+ 'Blocking signals for %s',
+ ", ".join([
+ str(w.__class__.__name__) for w in self.widgets
+ ])
+ )
self.oldStates = [w.signalsBlocked() for w in self.widgets]
for w in self.widgets:
w.blockSignals(True)
def __exit__(self, *args):
- log.verbose('Resetting blockSignals to %s' % sum(self.oldStates))
+ log.verbose(
+ 'Resetting blockSignals to %s', str(bool(sum(self.oldStates))))
for w, state in zip(self.widgets, self.oldStates):
w.blockSignals(state)
@@ -153,7 +157,7 @@ def connectWidget(widget, func):
elif type(widget) == QtWidgets.QComboBox:
widget.currentIndexChanged.connect(func)
else:
- log.warning('Failed to connect %s ' % str(widget.__class__.__name__))
+ log.warning('Failed to connect %s ', str(widget.__class__.__name__))
return False
return True
@@ -171,7 +175,7 @@ def setWidgetValue(widget, val):
elif type(widget) == QtWidgets.QComboBox:
widget.setCurrentIndex(val)
else:
- log.warning('Failed to set %s ' % str(widget.__class__.__name__))
+ log.warning('Failed to set %s ', str(widget.__class__.__name__))
return False
return True
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index 8fe9148..f007f90 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -93,7 +93,7 @@ class FfmpegVideo:
from component import ComponentError
logFilename = os.path.join(
core.Core.logDir, 'render_%s.log' % str(self.component.compPos))
- log.debug('Creating ffmpeg process (log at %s)' % logFilename)
+ log.debug('Creating ffmpeg process (log at %s)', logFilename)
with open(logFilename, 'w') as logf:
logf.write(" ".join(self.command) + '\n\n')
with open(logFilename, 'a') as logf:
diff --git a/src/video_thread.py b/src/video_thread.py
index 87fb9bd..823ac73 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -179,7 +179,7 @@ class Worker(QtCore.QObject):
for num, component in enumerate(reversed(self.components))
])
print('Loaded Components:', initText)
- log.info('Calling preFrameRender for %s' % initText)
+ log.info('Calling preFrameRender for %s', initText)
self.staticComponents = {}
for compNo, comp in enumerate(reversed(self.components)):
try:
@@ -221,12 +221,13 @@ class Worker(QtCore.QObject):
if self.canceled:
if canceledByComponent:
- log.error('Export cancelled by component #%s (%s): %s' % (
+ log.error(
+ 'Export cancelled by component #%s (%s): %s',
compNo,
comp.name,
'No message.' if comp.error() is None else (
comp.error() if type(comp.error()) is str
- else comp.error()[0])
+ else comp.error()[0]
)
)
self.cancelExport()
--
cgit v1.2.3
From 8411857030d92e448d5c64682f396e677161afbe Mon Sep 17 00:00:00 2001
From: tassaron
Date: Mon, 28 Aug 2017 18:54:54 -0400
Subject: ctrl-c ends commandline mode properly
---
setup.py | 2 +-
src/command.py | 9 +++++++++
src/components/spectrum.py | 3 ++-
src/core.py | 10 ++++------
src/toolkit/frame.py | 1 +
src/video_thread.py | 11 ++++++++---
6 files changed, 25 insertions(+), 11 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/setup.py b/setup.py
index dd546e2..cdf4c4a 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup
import os
-__version__ = '2.0.0.rc4'
+__version__ = '2.0.0rc5'
def package_files(directory):
diff --git a/src/command.py b/src/command.py
index 4116c5a..cd3c6c3 100644
--- a/src/command.py
+++ b/src/command.py
@@ -8,6 +8,7 @@ import argparse
import os
import sys
import time
+import signal
from core import Core
@@ -91,6 +92,9 @@ class Command(QtCore.QObject):
for arg in args:
self.core.selectedComponents[i].command(arg)
+ # ctrl-c stops the export thread
+ signal.signal(signal.SIGINT, self.stopVideo)
+
if self.args.export and self.args.projpath:
errcode, data = self.core.parseAvFile(projPath)
for key, value in data['WindowFields']:
@@ -124,6 +128,11 @@ class Command(QtCore.QObject):
self.worker.progressBarSetText.connect(self.progressBarSetText)
self.createVideo.emit()
+ def stopVideo(self, *args):
+ self.worker.error = True
+ self.worker.cancelExport()
+ self.worker.cancel()
+
@QtCore.pyqtSlot(str)
def progressBarSetText(self, value):
if 'Export ' in value:
diff --git a/src/components/spectrum.py b/src/components/spectrum.py
index 77cb086..6675f5b 100644
--- a/src/components/spectrum.py
+++ b/src/components/spectrum.py
@@ -98,7 +98,8 @@ class Component(Component):
def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs)
- self.previewPipe.wait()
+ if self.previewPipe is not None:
+ self.previewPipe.wait()
self.updateChunksize()
w, h = scale(self.scale, self.width, self.height, str)
self.video = FfmpegVideo(
diff --git a/src/core.py b/src/core.py
index 1a90296..d7445c9 100644
--- a/src/core.py
+++ b/src/core.py
@@ -13,8 +13,8 @@ import toolkit
log = logging.getLogger('AVP.Core')
-STDOUT_LOGLVL = logging.INFO
-FILE_LOGLVL = logging.VERBOSE
+STDOUT_LOGLVL = logging.WARNING
+FILE_LOGLVL = None
class Core:
@@ -77,8 +77,7 @@ class Core:
if compPos < 0 or compPos > len(self.selectedComponents):
compPos = len(self.selectedComponents)
if len(self.selectedComponents) > 50:
- return None
-
+ return -1
if type(component) is int:
# create component using module index in self.modules
moduleIndex = int(component)
@@ -188,7 +187,6 @@ class Core:
for key, value in data['Settings']:
Core.settings.setValue(key, value)
-
for tup in data['Components']:
name, vers, preset = tup
clearThis = False
@@ -213,7 +211,7 @@ class Core:
self.moduleIndexFor(name),
loader
)
- if i is None:
+ if i == -1:
loader.showMessage(msg="Too many components!")
break
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index aefb55f..0e200b5 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -32,6 +32,7 @@ class FramePainter(QtGui.QPainter):
super().setPen(penStyle)
def finalize(self):
+ log.verbose("Finalizing FramePainter")
imBytes = self.image.bits().asstring(self.image.byteCount())
frame = Image.frombytes(
'RGBA', (self.image.width(), self.image.height()), imBytes
diff --git a/src/video_thread.py b/src/video_thread.py
index 823ac73..91ebe93 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -252,9 +252,14 @@ class Worker(QtCore.QObject):
print('############################')
log.info('Opening pipe to ffmpeg')
log.info(cmd)
- self.out_pipe = openPipe(
- ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
- )
+ try:
+ self.out_pipe = openPipe(
+ ffmpegCommand,
+ stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
+ )
+ except sp.CalledProcessError:
+ log.critical('Ffmpeg pipe couldn\'t be created!')
+ raise
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# START CREATING THE VIDEO
--
cgit v1.2.3
From 1c9f5e4ae685a957a29100e7ade1ac365149c218 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 21 Apr 2022 01:05:55 -0400
Subject: fix crash if ffmpeg is not installed (tested on windows 11)
---
src/toolkit/ffmpeg.py | 3 +++
src/video_thread.py | 12 +++++++++---
2 files changed, 12 insertions(+), 3 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index 0a536bc..3f083ba 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -383,6 +383,9 @@ def getAudioDuration(filename):
fileInfo = checkOutput(command, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as ex:
fileInfo = ex.output
+ except FileNotFoundError:
+ # ffmpeg is possibly not installed
+ return False
try:
info = fileInfo.decode("utf-8").split('\n')
diff --git a/src/video_thread.py b/src/video_thread.py
index 91ebe93..4a07fb2 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -244,9 +244,15 @@ class Worker(QtCore.QObject):
)
self.staticComponents[compNo] = None
- ffmpegCommand = createFfmpegCommand(
- self.inputFile, self.outputFile, self.components, duration
- )
+ try:
+ ffmpegCommand = createFfmpegCommand(
+ self.inputFile, self.outputFile, self.components, duration
+ )
+ except sp.CalledProcessError as e:
+ self.components[0]._error.emit("Ffmpeg could not be found. Is it installed?", str(e))
+ self.cancelExport()
+ return
+
cmd = " ".join(ffmpegCommand)
print('###### FFMPEG COMMAND ######\n%s' % cmd)
print('############################')
--
cgit v1.2.3
From 2a66c3b77bb8c438a3f278da2d4d90a7ee476feb Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 21 Apr 2022 16:01:34 -0400
Subject: fail gracefully if createFfmpegCommand returns empty
---
src/video_thread.py | 23 ++++++++++++++++-------
1 file changed, 16 insertions(+), 7 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/video_thread.py b/src/video_thread.py
index 4a07fb2..0a39f28 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -249,13 +249,21 @@ class Worker(QtCore.QObject):
self.inputFile, self.outputFile, self.components, duration
)
except sp.CalledProcessError as e:
+ #FIXME video_thread should own this error signal, not components
self.components[0]._error.emit("Ffmpeg could not be found. Is it installed?", str(e))
- self.cancelExport()
+ self.error = True
return
cmd = " ".join(ffmpegCommand)
print('###### FFMPEG COMMAND ######\n%s' % cmd)
print('############################')
+ if not cmd:
+ #FIXME video_thread should own this error signal, not components
+ self.components[0]._error.emit("The ffmpeg command could not be generated.", "")
+ log.critical("Cancelling render process due to failure while generating the ffmpeg command.")
+ self.failExport()
+ return
+
log.info('Opening pipe to ffmpeg')
log.info(cmd)
try:
@@ -264,7 +272,7 @@ class Worker(QtCore.QObject):
stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
)
except sp.CalledProcessError:
- log.critical('Ffmpeg pipe couldn\'t be created!')
+ log.critical('Ffmpeg pipe couldn\'t be created!', exc_info=True)
raise
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
@@ -347,9 +355,7 @@ class Worker(QtCore.QObject):
self.progressBarSetText.emit('Export Canceled')
else:
if self.error:
- print("Export Failed")
- self.progressBarUpdate.emit(0)
- self.progressBarSetText.emit('Export Failed')
+ self.failExport()
else:
print("Export Complete")
self.progressBarUpdate.emit(100)
@@ -372,12 +378,15 @@ class Worker(QtCore.QObject):
self.error = True
self.out_pipe.wait()
- def cancelExport(self):
+ def cancelExport(self, message='Export Canceled'):
self.progressBarUpdate.emit(0)
- self.progressBarSetText.emit('Export Canceled')
+ self.progressBarSetText.emit(message)
self.encoding.emit(False)
self.videoCreated.emit()
+ def failExport(self):
+ self.cancelExport('Export Failed')
+
def updateProgress(self, pStr, pVal):
self.progressBarValue.emit(pVal)
self.progressBarSetText.emit(pStr)
--
cgit v1.2.3
From 05d2ebc3c69f5a876d602004f69202c5ba8b09f7 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Fri, 22 Apr 2022 17:09:50 -0400
Subject: make pip-installable as a package
---
MANIFEST.in | 7 ++++++
setup.py | 61 ++++++++++++++++++++++++++--------------------
src/__init__.py | 6 ++---
src/__main__.py | 4 +--
src/component.py | 4 +--
src/components/color.py | 4 +--
src/components/image.py | 4 +--
src/components/life.py | 4 +--
src/components/original.py | 4 +--
src/components/sound.py | 4 +--
src/components/spectrum.py | 8 +++---
src/components/text.py | 4 +--
src/components/video.py | 8 +++---
src/components/waveform.py | 8 +++---
src/core.py | 12 ++++-----
src/gui/actions.py | 2 +-
src/gui/mainwindow.py | 13 +++++-----
src/gui/presetmanager.py | 6 ++---
src/gui/preview_thread.py | 4 +--
src/main.py | 11 ++++-----
src/toolkit/__init__.py | 2 +-
src/toolkit/ffmpeg.py | 8 +++---
src/toolkit/frame.py | 2 +-
src/video_thread.py | 8 +++---
24 files changed, 106 insertions(+), 92 deletions(-)
create mode 100644 MANIFEST.in
(limited to 'src/video_thread.py')
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..2b2d794
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,7 @@
+recursive-include src/tests
+include src/components/*.ui
+include src/gui/*.ui
+include src/gui/background.png
+include src/encoder-options.json
+global-exclude src/components/__template__.ui
+global-exclude *.py[cod]
diff --git a/setup.py b/setup.py
index cdf4c4a..5e01229 100644
--- a/setup.py
+++ b/setup.py
@@ -1,29 +1,39 @@
-from setuptools import setup
-import os
+from setuptools import setup, find_packages
+from importlib import import_module
+from os import path
+import re
-__version__ = '2.0.0rc5'
+def getTextFromFile(filename, fallback):
+ try:
+ with open(
+ path.join(path.abspath(path.dirname(__file__)), filename), encoding="utf-8"
+ ) as f:
+ output = f.read()
+ except Exception:
+ output = fallback
+ return output
-def package_files(directory):
- paths = []
- for (path, directories, filenames) in os.walk(directory):
- for filename in filenames:
- paths.append(os.path.join('..', path, filename))
- return paths
+PACKAGE_NAME = 'avp'
+SOURCE_DIRECTORY = 'src'
+SOURCE_PACKAGE_REGEX = re.compile(rf'^{SOURCE_DIRECTORY}')
+PACKAGE_DESCRIPTION = 'Create audio visualization videos from a GUI or commandline'
+
+
+avp = import_module(SOURCE_DIRECTORY)
+source_packages = find_packages(include=[SOURCE_DIRECTORY, f'{SOURCE_DIRECTORY}.*'])
+proj_packages = [SOURCE_PACKAGE_REGEX.sub(PACKAGE_NAME, name) for name in source_packages]
setup(
name='audio_visualizer_python',
- version=__version__,
+ version=avp.__version__,
url='https://github.com/djfun/audio-visualizer-python/tree/feature-newgui',
license='MIT',
- description='Create audio visualization videos from a GUI or commandline',
- long_description="Create customized audio visualization videos and save "
- "them as Projects to continue editing later. Different components can "
- "be added and layered to add visualizers, images, videos, gradients, "
- "text, etc. Use Projects created in the GUI with commandline mode to "
- "automate your video production workflow without any complex syntax.",
+ description=PACKAGE_DESCRIPTION,
+ author=getTextFromFile('AUTHORS', 'djfun, tassaron'),
+ long_description=getTextFromFile('README.md', PACKAGE_DESCRIPTION),
classifiers=[
'Development Status :: 4 - Beta',
'License :: OSI Approved :: MIT License',
@@ -35,19 +45,18 @@ setup(
'visualizer', 'visualization', 'commandline video',
'video editor', 'ffmpeg', 'podcast'
],
- packages=[
- 'avpython',
- 'avpython.toolkit',
- 'avpython.components'
+ packages=proj_packages,
+ package_dir={PACKAGE_NAME: SOURCE_DIRECTORY},
+ include_package_data=True,
+ install_requires=[
+ 'Pillow-SIMD',
+ 'PyQt5',
+ 'numpy',
+ 'pytest'
],
- package_dir={'avpython': 'src'},
- package_data={
- 'avpython': package_files('src'),
- },
- install_requires=['Pillow-SIMD', 'PyQt5', 'numpy'],
entry_points={
'gui_scripts': [
- 'avp = avpython.main:main'
+ f'avp = {PACKAGE_NAME}.main:main'
],
}
)
diff --git a/src/__init__.py b/src/__init__.py
index 73f174a..08131ce 100644
--- a/src/__init__.py
+++ b/src/__init__.py
@@ -3,6 +3,9 @@ import os
import logging
+__version__ = '2.0.0rc6'
+
+
class Logger(logging.getLoggerClass()):
'''
Custom Logger class to handle custom VERBOSE log level.
@@ -31,6 +34,3 @@ if getattr(sys, 'frozen', False):
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/__main__.py b/src/__main__.py
index 3babeae..3206bc8 100644
--- a/src/__main__.py
+++ b/src/__main__.py
@@ -1,5 +1,5 @@
-# Allows for launching with python3 -m avpython
+# Allows for launching with python3 -m avp
-from avpython.main import main
+from .main import main
main()
diff --git a/src/component.py b/src/component.py
index f3ee188..33c7657 100644
--- a/src/component.py
+++ b/src/component.py
@@ -11,8 +11,8 @@ import time
import logging
from copy import copy
-from toolkit.frame import BlankFrame
-from toolkit import (
+from .toolkit.frame import BlankFrame
+from .toolkit import (
getWidgetValue, setWidgetValue, connectWidget, rgbFromString, blockSignals
)
diff --git a/src/components/color.py b/src/components/color.py
index 7d4f86d..6336194 100644
--- a/src/components/color.py
+++ b/src/components/color.py
@@ -4,8 +4,8 @@ from PyQt5.QtGui import QColor
from PIL.ImageQt import ImageQt
import os
-from component import Component
-from toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor
+from ..component import Component
+from ..toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor
class Component(Component):
diff --git a/src/components/image.py b/src/components/image.py
index dd363bf..42f9564 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -2,8 +2,8 @@ from PIL import Image, ImageDraw, ImageEnhance
from PyQt5 import QtGui, QtCore, QtWidgets
import os
-from component import Component
-from toolkit.frame import BlankFrame
+from ..component import Component
+from ..toolkit.frame import BlankFrame
class Component(Component):
diff --git a/src/components/life.py b/src/components/life.py
index 7a610eb..94704bc 100644
--- a/src/components/life.py
+++ b/src/components/life.py
@@ -4,8 +4,8 @@ from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter
import os
import math
-from component import Component
-from toolkit.frame import BlankFrame, scale
+from ..component import Component
+from ..toolkit.frame import BlankFrame, scale
class Component(Component):
diff --git a/src/components/original.py b/src/components/original.py
index f886374..80228fe 100644
--- a/src/components/original.py
+++ b/src/components/original.py
@@ -6,8 +6,8 @@ import os
import time
from copy import copy
-from component import Component
-from toolkit.frame import BlankFrame
+from ..component import Component
+from ..toolkit.frame import BlankFrame
class Component(Component):
diff --git a/src/components/sound.py b/src/components/sound.py
index 18d2a65..118ea23 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -1,8 +1,8 @@
from PyQt5 import QtGui, QtCore, QtWidgets
import os
-from component import Component
-from toolkit.frame import BlankFrame
+from ..component import Component
+from ..toolkit.frame import BlankFrame
class Component(Component):
diff --git a/src/components/spectrum.py b/src/components/spectrum.py
index 6675f5b..d1f8fb6 100644
--- a/src/components/spectrum.py
+++ b/src/components/spectrum.py
@@ -6,10 +6,10 @@ import subprocess
import time
import logging
-from component import Component
-from toolkit.frame import BlankFrame, scale
-from toolkit import checkOutput, connectWidget
-from toolkit.ffmpeg import (
+from ..component import Component
+from ..toolkit.frame import BlankFrame, scale
+from ..toolkit import checkOutput, connectWidget
+from ..toolkit.ffmpeg import (
openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound
)
diff --git a/src/components/text.py b/src/components/text.py
index 32a108e..e8c5a9c 100644
--- a/src/components/text.py
+++ b/src/components/text.py
@@ -4,8 +4,8 @@ from PyQt5 import QtGui, QtCore, QtWidgets
import os
import logging
-from component import Component
-from toolkit.frame import FramePainter, PaintColor
+from ..component import Component
+from ..toolkit.frame import FramePainter, PaintColor
log = logging.getLogger('AVP.Components.Text')
diff --git a/src/components/video.py b/src/components/video.py
index 8ad21b5..070940d 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -5,10 +5,10 @@ import math
import subprocess
import logging
-from component import Component
-from toolkit.frame import BlankFrame, scale
-from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo
-from toolkit import checkOutput
+from ..component import Component
+from ..toolkit.frame import BlankFrame, scale
+from ..toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo
+from ..toolkit import checkOutput
log = logging.getLogger('AVP.Components.Video')
diff --git a/src/components/waveform.py b/src/components/waveform.py
index cbfc47f..1a6035f 100644
--- a/src/components/waveform.py
+++ b/src/components/waveform.py
@@ -6,10 +6,10 @@ import math
import subprocess
import logging
-from component import Component
-from toolkit.frame import BlankFrame, scale
-from toolkit import checkOutput
-from toolkit.ffmpeg import (
+from ..component import Component
+from ..toolkit.frame import BlankFrame, scale
+from ..toolkit import checkOutput
+from ..toolkit.ffmpeg import (
openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound
)
diff --git a/src/core.py b/src/core.py
index d7445c9..bc6f9b4 100644
--- a/src/core.py
+++ b/src/core.py
@@ -9,12 +9,12 @@ import json
from importlib import import_module
import logging
-import toolkit
+from . import toolkit
log = logging.getLogger('AVP.Core')
STDOUT_LOGLVL = logging.WARNING
-FILE_LOGLVL = None
+FILE_LOGLVL = logging.ERROR
class Core:
@@ -47,7 +47,7 @@ class Core:
yield name
log.debug('Importing component modules')
self.modules = [
- import_module('components.%s' % name)
+ import_module('.components.%s' % name, __package__)
for name in findComponents()
]
# store canonical module names and indexes
@@ -426,7 +426,7 @@ class Core:
def newVideoWorker(self, loader, audioFile, outputPath):
'''loader is MainWindow or Command object which must own the thread'''
- import video_thread
+ from . import video_thread
self.videoThread = QtCore.QThread(loader)
videoWorker = video_thread.Worker(
loader, audioFile, outputPath, self.selectedComponents
@@ -450,8 +450,8 @@ class Core:
@classmethod
def storeSettings(cls):
'''Store settings/paths to directories as class variables'''
- from __init__ import wd
- from toolkit.ffmpeg import findFfmpeg
+ from .__init__ import wd
+ from .toolkit.ffmpeg import findFfmpeg
cls.wd = wd
dataDir = QtCore.QStandardPaths.writableLocation(
diff --git a/src/gui/actions.py b/src/gui/actions.py
index 8e867b9..eb7b953 100644
--- a/src/gui/actions.py
+++ b/src/gui/actions.py
@@ -5,7 +5,7 @@ from PyQt5.QtWidgets import QUndoCommand
import os
from copy import copy
-from core import Core
+from ..core import Core
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py
index 75534c2..da8370d 100644
--- a/src/gui/mainwindow.py
+++ b/src/gui/mainwindow.py
@@ -16,12 +16,12 @@ import filecmp
import time
import logging
-from core import Core
-import gui.preview_thread as preview_thread
-from gui.preview_win import PreviewWindow
-from gui.presetmanager import PresetManager
-from gui.actions import *
-from toolkit import (
+from ..core import Core
+from . import preview_thread
+from .preview_win import PreviewWindow
+from .presetmanager import PresetManager
+from .actions import *
+from ..toolkit import (
disableWhenEncoding, disableWhenOpeningProject, checkOutput, blockSignals
)
@@ -65,7 +65,6 @@ class MainWindow(QtWidgets.QMainWindow):
self.settings = Core.settings
# Register clean-up functions
- signal.signal(signal.SIGINT, self.terminate)
atexit.register(self.cleanUp)
# Create stack of undoable user actions
diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py
index 2445760..1e47a7f 100644
--- a/src/gui/presetmanager.py
+++ b/src/gui/presetmanager.py
@@ -7,9 +7,9 @@ import string
import os
import logging
-from toolkit import badName
-from core import Core
-from gui.actions import *
+from ..toolkit import badName
+from ..core import Core
+from .actions import *
log = logging.getLogger('AVP.Gui.PresetManager')
diff --git a/src/gui/preview_thread.py b/src/gui/preview_thread.py
index d3e0581..7829476 100644
--- a/src/gui/preview_thread.py
+++ b/src/gui/preview_thread.py
@@ -10,8 +10,8 @@ from queue import Queue, Empty
import os
import logging
-from toolkit.frame import Checkerboard
-from toolkit import disableWhenOpeningProject
+from ..toolkit.frame import Checkerboard
+from ..toolkit import disableWhenOpeningProject
log = logging.getLogger("AVP.Gui.PreviewThread")
diff --git a/src/main.py b/src/main.py
index 126e4a8..5fabda3 100644
--- a/src/main.py
+++ b/src/main.py
@@ -3,7 +3,7 @@ import sys
import os
import logging
-from __init__ import wd
+from .__init__ import wd
log = logging.getLogger('AVP.Main')
@@ -12,6 +12,7 @@ log = logging.getLogger('AVP.Main')
def main():
app = QtWidgets.QApplication(sys.argv)
app.setApplicationName("audio-visualizer")
+ proj = None
# Determine mode
mode = 'GUI'
@@ -23,19 +24,17 @@ def main():
else:
# opening a project file with gui
proj = sys.argv[1]
- else:
- # normal gui launch
- proj = None
# Launch program
if mode == 'commandline':
- from command import Command
+ from .command import Command
main = Command()
+ main.parseArgs()
log.debug("Finished creating command object")
elif mode == 'GUI':
- from gui.mainwindow import MainWindow
+ from .gui.mainwindow import MainWindow
window = uic.loadUi(os.path.join(wd, "gui", "mainwindow.ui"))
# window.adjustSize()
diff --git a/src/toolkit/__init__.py b/src/toolkit/__init__.py
index 3fca275..55e5f84 100644
--- a/src/toolkit/__init__.py
+++ b/src/toolkit/__init__.py
@@ -1 +1 @@
-from toolkit.common import *
+from .common import *
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index 419d491..3298c04 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -10,8 +10,8 @@ import signal
from queue import PriorityQueue
import logging
-import core
-from toolkit.common import checkOutput, pipeWrapper
+from .. import core
+from .common import checkOutput, pipeWrapper
log = logging.getLogger('AVP.Toolkit.Ffmpeg')
@@ -90,7 +90,7 @@ class FfmpegVideo:
self.frameBuffer.task_done()
def fillBuffer(self):
- from component import ComponentError
+ from ..component import ComponentError
if core.Core.logEnabled:
logFilename = os.path.join(
core.Core.logDir, 'render_%s.log' % str(self.component.compPos)
@@ -144,7 +144,7 @@ def openPipe(commandList, **kwargs):
def closePipe(pipe):
pipe.stdout.close()
- pipe.send_signal(signal.SIGINT)
+ pipe.send_signal(signal.SIGTERM)
def findFfmpeg():
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index 0e200b5..f2511fe 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -9,7 +9,7 @@ import os
import math
import logging
-import core
+from .. import core
log = logging.getLogger('AVP.Toolkit.Frame')
diff --git a/src/video_thread.py b/src/video_thread.py
index 0a39f28..31331a3 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -19,9 +19,9 @@ import time
import signal
import logging
-from component import ComponentError
-from toolkit.frame import Checkerboard
-from toolkit.ffmpeg import (
+from .component import ComponentError
+from .toolkit.frame import Checkerboard
+from .toolkit.ffmpeg import (
openPipe, readAudioFile,
getAudioDuration, createFfmpegCommand
)
@@ -400,7 +400,7 @@ class Worker(QtCore.QObject):
comp.cancel()
try:
- self.out_pipe.send_signal(signal.SIGINT)
+ self.out_pipe.send_signal(signal.SIGTERM)
except Exception:
pass
--
cgit v1.2.3
From fe3251c528df7eff51be6ecbb18261990b524944 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Mon, 25 Apr 2022 13:52:03 -0400
Subject: add more logging to the video export
---
src/video_thread.py | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/video_thread.py b/src/video_thread.py
index 31331a3..2fe264a 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -132,6 +132,7 @@ class Worker(QtCore.QObject):
@pyqtSlot()
def createVideo(self):
+ log.debug("Video worker received signal to createVideo")
numpy.seterr(divide='ignore')
self.encoding.emit(True)
self.extraAudio = []
@@ -151,6 +152,7 @@ class Worker(QtCore.QObject):
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# READ AUDIO, INITIALIZE COMPONENTS, OPEN A PIPE TO FFMPEG
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ log.debug("Determining length of audio...")
if any([
True if 'pcm' in comp.properties() else False
for comp in self.components
@@ -192,7 +194,11 @@ class Worker(QtCore.QObject):
progressBarSetText=self.progressBarSetText
)
except ComponentError:
- pass
+ log.warning(
+ '#%s %s encountered an error in its preFrameRender method',
+ compNo,
+ comp
+ )
compProps = comp.properties()
if 'error' in compProps or comp._lockedError is not None:
@@ -216,9 +222,11 @@ class Worker(QtCore.QObject):
comp._error.emit(errMsg, compError[1])
break
if 'static' in compProps:
+ log.info('Saving static frame from #%s %s', compNo, comp)
self.staticComponents[compNo] = \
comp.frameRender(0).copy()
+ log.debug("Checking if a component wishes to cancel the export...")
if self.canceled:
if canceledByComponent:
log.error(
@@ -233,7 +241,7 @@ class Worker(QtCore.QObject):
self.cancelExport()
return
- # Merge consecutive static component frames together
+ log.info("Merging consecutive static component frames")
for compNo in range(len(self.components)):
if compNo not in self.staticComponents \
or compNo + 1 not in self.staticComponents:
--
cgit v1.2.3
From 069edd9086ad7a99c78c5637af23d50a633396cf Mon Sep 17 00:00:00 2001
From: tassaron
Date: Fri, 29 Apr 2022 12:58:26 -0400
Subject: use super().__init__ in the modern python3 style
---
src/command.py | 2 +-
src/gui/preview_thread.py | 4 ++--
src/gui/preview_win.py | 2 +-
src/video_thread.py | 4 ++--
4 files changed, 6 insertions(+), 6 deletions(-)
(limited to 'src/video_thread.py')
diff --git a/src/command.py b/src/command.py
index 267117e..cc13684 100644
--- a/src/command.py
+++ b/src/command.py
@@ -25,7 +25,7 @@ class Command(QtCore.QObject):
createVideo = QtCore.pyqtSignal()
def __init__(self):
- super()
+ super().__init__()
self.core = core.Core()
core.Core.mode = 'commandline'
self.dataDir = self.core.dataDir
diff --git a/src/gui/preview_thread.py b/src/gui/preview_thread.py
index 7829476..614b584 100644
--- a/src/gui/preview_thread.py
+++ b/src/gui/preview_thread.py
@@ -23,10 +23,10 @@ class Worker(QtCore.QObject):
error = pyqtSignal(str)
def __init__(self, parent=None, queue=None):
- QtCore.QObject.__init__(self)
+ super().__init__()
parent.newTask.connect(self.createPreviewImage)
parent.processTask.connect(self.process)
- self.parent = parent
+ #self.parent = parent
self.core = parent.core
self.settings = parent.settings
self.queue = queue
diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py
index 27e0a59..426ff66 100644
--- a/src/gui/preview_win.py
+++ b/src/gui/preview_win.py
@@ -10,7 +10,7 @@ class PreviewWindow(QtWidgets.QLabel):
when the window is resized.
'''
def __init__(self, parent, img):
- super(PreviewWindow, self).__init__()
+ super().__init__()
self.parent = parent
self.setFrameStyle(QtWidgets.QFrame.StyledPanel)
self.pixmap = QtGui.QPixmap(img)
diff --git a/src/video_thread.py b/src/video_thread.py
index 2fe264a..4a28261 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -39,13 +39,13 @@ class Worker(QtCore.QObject):
encoding = pyqtSignal(bool)
def __init__(self, parent, inputFile, outputFile, components):
- QtCore.QObject.__init__(self)
+ super().__init__()
self.core = parent.core
self.settings = parent.settings
self.modules = parent.core.modules
parent.createVideo.connect(self.createVideo)
- self.parent = parent
+ #self.parent = parent
self.components = components
self.outputFile = outputFile
self.inputFile = inputFile
--
cgit v1.2.3
From a0d8e7bc0543aa9eb82d6d378ce6a8f5d0f85c11 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sat, 30 Apr 2022 22:07:06 -0400
Subject: fix progress bar percentage not increasing numpy.floor now returns a
float if given a float
---
src/video_thread.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'src/video_thread.py')
diff --git a/src/video_thread.py b/src/video_thread.py
index 4a28261..5a28beb 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -340,7 +340,7 @@ class Worker(QtCore.QObject):
# increase progress bar value
completion = (audioI / self.audioArrayLen) * 100
if progressBarValue + 1 <= completion:
- progressBarValue = numpy.floor(completion)
+ progressBarValue = numpy.floor(completion).astype(int)
self.progressBarUpdate.emit(progressBarValue)
self.progressBarSetText.emit(
"Exporting video: %s%%" % str(int(progressBarValue))
--
cgit v1.2.3