new Waveform & Spectrum component + relative coords for everything

NOTE: projects and presets saved from old versions will not load correctly anymore
This commit is contained in:
Brianna 2017-08-03 21:16:51 -04:00 committed by GitHub
commit 20905230fe
20 changed files with 2484 additions and 482 deletions

2
.gitignore vendored
View File

@ -11,3 +11,5 @@ env/*
*.tar.* *.tar.*
*.exe *.exe
ffmpeg ffmpeg
*.bak
*~

View File

@ -2,7 +2,7 @@ from setuptools import setup
import os import os
__version__ = '2.0.0.rc2' __version__ = '2.0.0.rc3'
def package_files(directory): def package_files(directory):

View File

@ -3,16 +3,21 @@
on making a valid component. on making a valid component.
''' '''
from PyQt5 import uic, QtCore, QtWidgets from PyQt5 import uic, QtCore, QtWidgets
from PyQt5.QtGui import QColor
import os import os
import sys
import math
import time import time
from toolkit.frame import BlankFrame from toolkit.frame import BlankFrame
from toolkit import (
getWidgetValue, setWidgetValue, connectWidget, rgbFromString
)
class ComponentMetaclass(type(QtCore.QObject)): class ComponentMetaclass(type(QtCore.QObject)):
''' '''
Checks the validity of each Component class imported, and Checks the validity of each Component class and mutates some attrs.
mutates some attributes for easier use by the core program.
E.g., takes only major version from version string & decorates methods E.g., takes only major version from version string & decorates methods
''' '''
@ -171,8 +176,17 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self._trackedWidgets = {} self._trackedWidgets = {}
self._presetNames = {} self._presetNames = {}
self._commandArgs = {} self._commandArgs = {}
self._colorWidgets = {}
self._colorFuncs = {}
self._relativeWidgets = {}
# pixel values stored as floats
self._relativeValues = {}
# maximum values of spinBoxes at 1080p (Core.resolutions[0])
self._relativeMaximums = {}
self._lockedProperties = None self._lockedProperties = None
self._lockedError = None self._lockedError = None
self._lockedSize = None
# Stop lengthy processes in response to this variable # Stop lengthy processes in response to this variable
self.canceled = False self.canceled = False
@ -181,12 +195,16 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
return self.__class__.name return self.__class__.name
def __repr__(self): def __repr__(self):
try:
preset = self.savePreset()
except Exception as e:
preset = '%s occured while saving preset' % str(e)
return '%s\n%s\n%s' % ( return '%s\n%s\n%s' % (
self.__class__.name, str(self.__class__.version), self.savePreset() self.__class__.name, str(self.__class__.version), preset
) )
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# Critical Methods # Render Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
def previewRender(self): def previewRender(self):
@ -197,7 +215,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
''' '''
Must call super() when subclassing Must call super() when subclassing
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.audioFile = filepath to the main input audio file
self.completeAudioArray = a list of audio samples self.completeAudioArray = a list of audio samples
self.sampleSize = number of audio samples per video frame self.sampleSize = number of audio samples per video frame
self.progressBarUpdate = signal to set progress bar number self.progressBarUpdate = signal to set progress bar number
@ -273,14 +291,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
widgets['spinBox'].extend( widgets['spinBox'].extend(
self.page.findChildren(QtWidgets.QDoubleSpinBox) self.page.findChildren(QtWidgets.QDoubleSpinBox)
) )
for widget in widgets['lineEdit']: for widgetList in widgets.values():
widget.textChanged.connect(self.update) for widget in widgetList:
for widget in widgets['checkBox']: connectWidget(widget, self.update)
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 update(self): def update(self):
''' '''
@ -289,15 +302,24 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
Call super() at the END if you need to subclass this. Call super() at the END if you need to subclass this.
''' '''
for attr, widget in self._trackedWidgets.items(): for attr, widget in self._trackedWidgets.items():
if type(widget) == QtWidgets.QLineEdit: if attr in self._colorWidgets:
setattr(self, attr, widget.text()) # Color Widgets: text stored as tuple & update the button color
elif type(widget) == QtWidgets.QSpinBox \ rgbTuple = rgbFromString(widget.text())
or type(widget) == QtWidgets.QDoubleSpinBox: btnStyle = (
setattr(self, attr, widget.value()) "QPushButton { background-color : %s; outline: none; }"
elif type(widget) == QtWidgets.QCheckBox: % QColor(*rgbTuple).name())
setattr(self, attr, widget.isChecked()) self._colorWidgets[attr].setStyleSheet(btnStyle)
elif type(widget) == QtWidgets.QComboBox: setattr(self, attr, rgbTuple)
setattr(self, attr, widget.currentIndex())
elif attr in self._relativeWidgets:
# Relative widgets: number scales to fit export resolution
self.updateRelativeWidget(attr)
setattr(self, attr, self._trackedWidgets[attr].value())
else:
# Normal tracked widget
setattr(self, attr, getWidgetValue(widget))
if not self.core.openingProject: if not self.core.openingProject:
self.parent.drawPreview() self.parent.drawPreview()
saveValueStore = self.savePreset() saveValueStore = self.savePreset()
@ -313,27 +335,40 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self.currentPreset = presetName \ self.currentPreset = presetName \
if presetName is not None else presetDict['preset'] if presetName is not None else presetDict['preset']
for attr, widget in self._trackedWidgets.items(): for attr, widget in self._trackedWidgets.items():
val = presetDict[ key = attr if attr not in self._presetNames \
attr if attr not in self._presetNames
else self._presetNames[attr] else self._presetNames[attr]
] val = presetDict[key]
if type(widget) == QtWidgets.QLineEdit:
widget.setText(val) if attr in self._colorWidgets:
elif type(widget) == QtWidgets.QSpinBox \ widget.setText('%s,%s,%s' % val)
or type(widget) == QtWidgets.QDoubleSpinBox: btnStyle = (
widget.setValue(val) "QPushButton { background-color : %s; outline: none; }"
elif type(widget) == QtWidgets.QCheckBox: % QColor(*val).name()
widget.setChecked(val) )
elif type(widget) == QtWidgets.QComboBox: self._colorWidgets[attr].setStyleSheet(btnStyle)
widget.setCurrentIndex(val) elif attr in self._relativeWidgets:
self._relativeValues[attr] = val
pixelVal = self.pixelValForAttr(attr, val)
setWidgetValue(widget, pixelVal)
else:
setWidgetValue(widget, val)
def savePreset(self): def savePreset(self):
saveValueStore = {} saveValueStore = {}
for attr, widget in self._trackedWidgets.items(): for attr, widget in self._trackedWidgets.items():
saveValueStore[ presetAttrName = (
attr if attr not in self._presetNames attr if attr not in self._presetNames
else self._presetNames[attr] else self._presetNames[attr]
] = getattr(self, attr) )
if attr in self._relativeWidgets:
try:
val = self._relativeValues[attr]
except AttributeError:
val = self.floatValForAttr(attr)
else:
val = getattr(self, attr)
saveValueStore[presetAttrName] = val
return saveValueStore return saveValueStore
def commandHelp(self): def commandHelp(self):
@ -372,7 +407,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self._trackedWidgets = trackDict self._trackedWidgets = trackDict
for kwarg in kwargs: for kwarg in kwargs:
try: try:
if kwarg in ('presetNames', 'commandArgs'): if kwarg in (
'presetNames',
'commandArgs',
'colorWidgets',
'relativeWidgets',
):
setattr(self, '_%s' % kwarg, kwargs[kwarg]) setattr(self, '_%s' % kwarg, kwargs[kwarg])
else: else:
raise ComponentError( raise ComponentError(
@ -380,29 +420,79 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
except ComponentError: except ComponentError:
continue continue
if kwarg == 'colorWidgets':
def makeColorFunc(attr):
def pickColor_():
self.pickColor(
self._trackedWidgets[attr],
self._colorWidgets[attr]
)
return pickColor_
self._colorFuncs = {
attr: makeColorFunc(attr) for attr in kwargs[kwarg]
}
for attr, func in self._colorFuncs.items():
self._colorWidgets[attr].clicked.connect(func)
self._colorWidgets[attr].setStyleSheet(
"QPushButton {"
"background-color : #FFFFFF; outline: none; }"
)
if kwarg == 'relativeWidgets':
# store maximum values of spinBoxes to be scaled appropriately
for attr in kwargs[kwarg]:
self._relativeMaximums[attr] = \
self._trackedWidgets[attr].maximum()
self.updateRelativeWidgetMaximum(attr)
def pickColor(self, textWidget, button):
'''Use color picker to get color input from the user.'''
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()
textWidget.setText(RGBstring)
button.setStyleSheet(btnStyle)
def lockProperties(self, propList): def lockProperties(self, propList):
self._lockedProperties = propList self._lockedProperties = propList
def lockError(self, msg): def lockError(self, msg):
self._lockedError = msg self._lockedError = msg
def lockSize(self, w, h):
self._lockedSize = (w, h)
def unlockProperties(self): def unlockProperties(self):
self._lockedProperties = None self._lockedProperties = None
def unlockError(self): def unlockError(self):
self._lockedError = None self._lockedError = None
def unlockSize(self):
self._lockedSize = None
def loadUi(self, filename): def loadUi(self, filename):
'''Load a Qt Designer ui file to use for this component's widget''' '''Load a Qt Designer ui file to use for this component's widget'''
return uic.loadUi(os.path.join(self.core.componentsPath, filename)) return uic.loadUi(os.path.join(self.core.componentsPath, filename))
@property @property
def width(self): def width(self):
return int(self.settings.value('outputWidth')) if self._lockedSize is None:
return int(self.settings.value('outputWidth'))
else:
return self._lockedSize[0]
@property @property
def height(self): def height(self):
return int(self.settings.value('outputHeight')) if self._lockedSize is None:
return int(self.settings.value('outputHeight'))
else:
return self._lockedSize[1]
def cancel(self): def cancel(self):
'''Stop any lengthy process in response to this variable.''' '''Stop any lengthy process in response to this variable.'''
@ -413,6 +503,67 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self.unlockProperties() self.unlockProperties()
self.unlockError() self.unlockError()
def relativeWidgetAxis(func):
def relativeWidgetAxis(self, attr, *args, **kwargs):
if 'axis' not in kwargs:
axis = self.width
if 'height' in attr.lower() \
or 'ypos' in attr.lower() or attr == 'y':
axis = self.height
kwargs['axis'] = axis
return func(self, attr, *args, **kwargs)
return relativeWidgetAxis
@relativeWidgetAxis
def pixelValForAttr(self, attr, val=None, **kwargs):
if val is None:
val = self._relativeValues[attr]
return math.ceil(kwargs['axis'] * val)
@relativeWidgetAxis
def floatValForAttr(self, attr, val=None, **kwargs):
if val is None:
val = self._trackedWidgets[attr].value()
return val / kwargs['axis']
def setRelativeWidget(self, attr, floatVal):
'''Set a relative widget using a float'''
pixelVal = self.pixelValForAttr(attr, floatVal)
self._trackedWidgets[attr].setValue(pixelVal)
def updateRelativeWidget(self, attr):
try:
oldUserValue = getattr(self, attr)
except AttributeError:
oldUserValue = self._trackedWidgets[attr].value()
newUserValue = self._trackedWidgets[attr].value()
newRelativeVal = self.floatValForAttr(attr, newUserValue)
if attr in self._relativeValues:
oldRelativeVal = self._relativeValues[attr]
if oldUserValue == newUserValue \
and oldRelativeVal != newRelativeVal:
# Float changed without pixel value changing, which
# means the pixel value needs to be updated
self._trackedWidgets[attr].blockSignals(True)
self.updateRelativeWidgetMaximum(attr)
pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
self._trackedWidgets[attr].setValue(pixelVal)
self._trackedWidgets[attr].blockSignals(False)
if attr not in self._relativeValues \
or oldUserValue != newUserValue:
self._relativeValues[attr] = newRelativeVal
def updateRelativeWidgetMaximum(self, attr):
maxRes = int(self.core.resolutions[0].split('x')[0])
newMaximumValue = self.width * (
self._relativeMaximums[attr] /
maxRes
)
self._trackedWidgets[attr].setMaximum(int(newMaximumValue))
class ComponentError(RuntimeError): class ComponentError(RuntimeError):
'''Gives the MainWindow a traceback to display, and cancels the export.''' '''Gives the MainWindow a traceback to display, and cancels the export.'''
@ -420,36 +571,43 @@ class ComponentError(RuntimeError):
prevErrors = [] prevErrors = []
lastTime = time.time() lastTime = time.time()
def __init__(self, caller, name): def __init__(self, caller, name, msg=None):
print('##### ComponentError by %s: %s' % (caller.name, name)) if msg is None and sys.exc_info()[0] is not None:
msg = str(sys.exc_info()[1])
else:
msg = 'Unknown error.'
print("##### ComponentError by %s's %s: %s" % (
caller.name, name, msg))
# Don't create multiple windows for quickly repeated messages
if len(ComponentError.prevErrors) > 1: if len(ComponentError.prevErrors) > 1:
ComponentError.prevErrors.pop() ComponentError.prevErrors.pop()
ComponentError.prevErrors.insert(0, name) ComponentError.prevErrors.insert(0, name)
curTime = time.time() curTime = time.time()
if name in ComponentError.prevErrors[1:] \ if name in ComponentError.prevErrors[1:] \
and curTime - ComponentError.lastTime < 0.2: and curTime - ComponentError.lastTime < 1.0:
# Don't create multiple windows for quickly repeated messages
return return
ComponentError.lastTime = time.time() ComponentError.lastTime = time.time()
from toolkit import formatTraceback from toolkit import formatTraceback
import sys
if sys.exc_info()[0] is not None: if sys.exc_info()[0] is not None:
string = ( string = (
"%s component's %s encountered %s %s." % ( "%s component (#%s): %s encountered %s %s: %s" % (
caller.__class__.name, caller.__class__.name,
str(caller.compPos),
name, name,
'an' if any([ 'an' if any([
sys.exc_info()[0].__name__.startswith(vowel) sys.exc_info()[0].__name__.startswith(vowel)
for vowel in ('A', 'I') for vowel in ('A', 'I', 'U', 'O', 'E')
]) else 'a', ]) else 'a',
sys.exc_info()[0].__name__, sys.exc_info()[0].__name__,
str(sys.exc_info()[1])
) )
) )
detail = formatTraceback(sys.exc_info()[2]) detail = formatTraceback(sys.exc_info()[2])
else: else:
string = name string = name
detail = "Methods:\n%s" % ( detail = "Attributes:\n%s" % (
"\n".join( "\n".join(
[m for m in dir(caller) if not m.startswith('_')] [m for m in dir(caller) if not m.startswith('_')]
) )

View File

@ -6,7 +6,6 @@ import os
from component import Component from component import Component
from toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor from toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor
from toolkit import rgbFromString, pickColor
class Component(Component): class Component(Component):
@ -14,25 +13,12 @@ class Component(Component):
version = '1.0.0' version = '1.0.0'
def widget(self, *args): def widget(self, *args):
self.color1 = (0, 0, 0)
self.color2 = (133, 133, 133)
self.x = 0 self.x = 0
self.y = 0 self.y = 0
super().widget(*args) super().widget(*args)
self.page.lineEdit_color1.setText('%s,%s,%s' % self.color1) self.page.lineEdit_color1.setText('0,0,0')
self.page.lineEdit_color2.setText('%s,%s,%s' % self.color2) self.page.lineEdit_color2.setText('133,133,133')
btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.color1).name()
btnStyle2 = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.color2).name()
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 # disable color #2 until non-default 'fill' option gets changed
self.page.lineEdit_color2.setDisabled(True) self.page.lineEdit_color2.setDisabled(True)
@ -51,31 +37,36 @@ class Component(Component):
self.page.comboBox_fill.addItem(label) self.page.comboBox_fill.addItem(label)
self.page.comboBox_fill.setCurrentIndex(0) self.page.comboBox_fill.setCurrentIndex(0)
self.trackWidgets( self.trackWidgets({
{ 'x': self.page.spinBox_x,
'x': self.page.spinBox_x, 'y': self.page.spinBox_y,
'y': self.page.spinBox_y, 'sizeWidth': self.page.spinBox_width,
'sizeWidth': self.page.spinBox_width, 'sizeHeight': self.page.spinBox_height,
'sizeHeight': self.page.spinBox_height, 'trans': self.page.checkBox_trans,
'trans': self.page.checkBox_trans, 'spread': self.page.comboBox_spread,
'spread': self.page.comboBox_spread, 'stretch': self.page.checkBox_stretch,
'stretch': self.page.checkBox_stretch, 'RG_start': self.page.spinBox_radialGradient_start,
'RG_start': self.page.spinBox_radialGradient_start, 'LG_start': self.page.spinBox_linearGradient_start,
'LG_start': self.page.spinBox_linearGradient_start, 'RG_end': self.page.spinBox_radialGradient_end,
'RG_end': self.page.spinBox_radialGradient_end, 'LG_end': self.page.spinBox_linearGradient_end,
'LG_end': self.page.spinBox_linearGradient_end, 'RG_centre': self.page.spinBox_radialGradient_spread,
'RG_centre': self.page.spinBox_radialGradient_spread, 'fillType': self.page.comboBox_fill,
'fillType': self.page.comboBox_fill, 'color1': self.page.lineEdit_color1,
}, presetNames={ 'color2': self.page.lineEdit_color2,
'sizeWidth': 'width', }, presetNames={
'sizeHeight': 'height', 'sizeWidth': 'width',
} 'sizeHeight': 'height',
) }, colorWidgets={
'color1': self.page.pushButton_color1,
'color2': self.page.pushButton_color2,
}, relativeWidgets=[
'x', 'y',
'sizeWidth', 'sizeHeight',
'LG_start', 'LG_end',
'RG_start', 'RG_end', 'RG_centre',
])
def update(self): def update(self):
self.color1 = rgbFromString(self.page.lineEdit_color1.text())
self.color2 = rgbFromString(self.page.lineEdit_color2.text())
fillType = self.page.comboBox_fill.currentIndex() fillType = self.page.comboBox_fill.currentIndex()
if fillType == 0: if fillType == 0:
self.page.lineEdit_color2.setEnabled(False) self.page.lineEdit_color2.setEnabled(False)
@ -161,36 +152,6 @@ class Component(Component):
return image.finalize() return image.finalize()
def loadPreset(self, pr, *args):
super().loadPreset(pr, *args)
self.page.lineEdit_color1.setText('%s,%s,%s' % pr['color1'])
self.page.lineEdit_color2.setText('%s,%s,%s' % pr['color2'])
btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*pr['color1']).name()
btnStyle2 = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*pr['color2']).name()
self.page.pushButton_color1.setStyleSheet(btnStyle1)
self.page.pushButton_color2.setStyleSheet(btnStyle2)
def savePreset(self):
saveValueStore = super().savePreset()
saveValueStore['color1'] = self.color1
saveValueStore['color2'] = self.color2
return saveValueStore
def pickColor(self, num):
RGBstring, btnStyle = pickColor()
if not RGBstring:
return
if num == 1:
self.page.lineEdit_color1.setText(RGBstring)
self.page.pushButton_color1.setStyleSheet(btnStyle)
else:
self.page.lineEdit_color2.setText(RGBstring)
self.page.pushButton_color2.setStyleSheet(btnStyle)
def commandHelp(self): def commandHelp(self):
print('Specify a color:\n color=255,255,255') print('Specify a color:\n color=255,255,255')

View File

@ -13,23 +13,22 @@ class Component(Component):
def widget(self, *args): def widget(self, *args):
super().widget(*args) super().widget(*args)
self.page.pushButton_image.clicked.connect(self.pickImage) self.page.pushButton_image.clicked.connect(self.pickImage)
self.trackWidgets( self.trackWidgets({
{ 'imagePath': self.page.lineEdit_image,
'imagePath': self.page.lineEdit_image, 'scale': self.page.spinBox_scale,
'scale': self.page.spinBox_scale, 'rotate': self.page.spinBox_rotate,
'rotate': self.page.spinBox_rotate, 'color': self.page.spinBox_color,
'color': self.page.spinBox_color, 'xPosition': self.page.spinBox_x,
'xPosition': self.page.spinBox_x, 'yPosition': self.page.spinBox_y,
'yPosition': self.page.spinBox_y, 'stretched': self.page.checkBox_stretch,
'stretched': self.page.checkBox_stretch, 'mirror': self.page.checkBox_mirror,
'mirror': self.page.checkBox_mirror, }, presetNames={
}, 'imagePath': 'image',
presetNames={ 'xPosition': 'x',
'imagePath': 'image', 'yPosition': 'y',
'xPosition': 'x', }, relativeWidgets=[
'yPosition': 'y', 'xPosition', 'yPosition', 'scale'
}, ])
)
def previewRender(self): def previewRender(self):
return self.drawFrame(self.width, self.height) return self.drawFrame(self.width, self.height)

View File

@ -8,7 +8,6 @@ from copy import copy
from component import Component from component import Component
from toolkit.frame import BlankFrame from toolkit.frame import BlankFrame
from toolkit import rgbFromString, pickColor
class Component(Component): class Component(Component):
@ -18,8 +17,10 @@ class Component(Component):
def names(*args): def names(*args):
return ['Original Audio Visualization'] return ['Original Audio Visualization']
def properties(self):
return ['pcm']
def widget(self, *args): def widget(self, *args):
self.visColor = (255, 255, 255)
self.scale = 20 self.scale = 20
self.y = 0 self.y = 0
super().widget(*args) super().widget(*args)
@ -30,34 +31,18 @@ class Component(Component):
self.page.comboBox_visLayout.addItem("Top") self.page.comboBox_visLayout.addItem("Top")
self.page.comboBox_visLayout.setCurrentIndex(0) self.page.comboBox_visLayout.setCurrentIndex(0)
self.page.lineEdit_visColor.setText('%s,%s,%s' % self.visColor) self.page.lineEdit_visColor.setText('255,255,255')
self.page.pushButton_visColor.clicked.connect(lambda: self.pickColor())
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.visColor).name()
self.page.pushButton_visColor.setStyleSheet(btnStyle)
self.trackWidgets({ self.trackWidgets({
'visColor': self.page.lineEdit_visColor,
'layout': self.page.comboBox_visLayout, 'layout': self.page.comboBox_visLayout,
'scale': self.page.spinBox_scale, 'scale': self.page.spinBox_scale,
'y': self.page.spinBox_y, 'y': self.page.spinBox_y,
}) }, colorWidgets={
'visColor': self.page.pushButton_visColor,
def update(self): }, relativeWidgets=[
self.visColor = rgbFromString(self.page.lineEdit_visColor.text()) 'y',
super().update() ])
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)
def savePreset(self):
saveValueStore = super().savePreset()
saveValueStore['visColor'] = self.visColor
return saveValueStore
def previewRender(self): def previewRender(self):
spectrum = numpy.fromfunction( spectrum = numpy.fromfunction(
@ -96,13 +81,6 @@ class Component(Component):
self.spectrumArray[arrayNo], self.spectrumArray[arrayNo],
self.visColor, self.layout) self.visColor, self.layout)
def pickColor(self):
RGBstring, btnStyle = pickColor()
if not RGBstring:
return
self.page.lineEdit_visColor.setText(RGBstring)
self.page.pushButton_visColor.setStyleSheet(btnStyle)
def transformData( def transformData(
self, i, completeAudioArray, sampleSize, self, i, completeAudioArray, sampleSize,
smoothConstantDown, smoothConstantUp, lastSpectrum): smoothConstantDown, smoothConstantUp, lastSpectrum):

284
src/components/spectrum.py Normal file
View File

@ -0,0 +1,284 @@
from PIL import Image
from PyQt5 import QtGui, QtCore, QtWidgets
import os
import math
import subprocess
import time
from component import Component
from toolkit.frame import BlankFrame, scale
from toolkit import checkOutput, connectWidget
from toolkit.ffmpeg import (
openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound
)
class Component(Component):
name = 'Spectrum'
version = '1.0.0'
def widget(self, *args):
self.previewFrame = None
super().widget(*args)
self._image = BlankFrame(self.width, self.height)
self.chunkSize = 4 * self.width * self.height
self.changedOptions = True
if hasattr(self.parent, 'window'):
# update preview when audio file changes (if genericPreview is off)
self.parent.window.lineEdit_audioFile.textChanged.connect(
self.update
)
self.trackWidgets({
'filterType': self.page.comboBox_filterType,
'window': self.page.comboBox_window,
'mode': self.page.comboBox_mode,
'amplitude': self.page.comboBox_amplitude0,
'amplitude1': self.page.comboBox_amplitude1,
'amplitude2': self.page.comboBox_amplitude2,
'display': self.page.comboBox_display,
'zoom': self.page.spinBox_zoom,
'tc': self.page.spinBox_tc,
'x': self.page.spinBox_x,
'y': self.page.spinBox_y,
'mirror': self.page.checkBox_mirror,
'draw': self.page.checkBox_draw,
'scale': self.page.spinBox_scale,
'color': self.page.comboBox_color,
'compress': self.page.checkBox_compress,
'mono': self.page.checkBox_mono,
'hue': self.page.spinBox_hue,
}, relativeWidgets=[
'x', 'y',
])
for widget in self._trackedWidgets.values():
connectWidget(widget, lambda: self.changed())
def changed(self):
self.changedOptions = True
def update(self):
self.page.stackedWidget.setCurrentIndex(
self.page.comboBox_filterType.currentIndex())
super().update()
def previewRender(self):
changedSize = self.updateChunksize()
if not changedSize \
and not self.changedOptions \
and self.previewFrame is not None:
return self.previewFrame
frame = self.getPreviewFrame()
self.changedOptions = False
if not frame:
self.previewFrame = None
return BlankFrame(self.width, self.height)
else:
self.previewFrame = frame
return frame
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_=self.makeFfmpegFilter(),
width=w, height=h,
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 self.finalizeFrame(self.video.frame(frameNo))
def postFrameRender(self):
closePipe(self.video.pipe)
def getPreviewFrame(self):
genericPreview = self.settings.value("pref_genericPreview")
startPt = 0
if not genericPreview:
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',
os.path.join(self.core.wd, 'background.png')
if genericPreview else inputFile,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',
]
command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt))
command.extend([
'-an',
'-s:v', '%sx%s' % scale(self.scale, self.width, self.height, str),
'-codec:v', 'rawvideo', '-',
'-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:
pipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=log, bufsize=10**8
)
byteFrame = pipe.stdout.read(self.chunkSize)
closePipe(pipe)
frame = self.finalizeFrame(byteFrame)
return frame
def makeFfmpegFilter(self, preview=False, startPt=0):
w, h = scale(self.scale, self.width, self.height, str)
color = self.page.comboBox_color.currentText().lower()
genericPreview = self.settings.value("pref_genericPreview")
if self.filterType == 0: # Spectrum
if self.amplitude == 0:
amplitude = 'sqrt'
elif self.amplitude == 1:
amplitude = 'cbrt'
elif self.amplitude == 2:
amplitude = '4thrt'
elif self.amplitude == 3:
amplitude = '5thrt'
elif self.amplitude == 4:
amplitude = 'lin'
elif self.amplitude == 5:
amplitude = 'log'
filter_ = (
'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"),
self.page.comboBox_window.currentText(),
color, amplitude,
)
)
elif self.filterType == 1: # Histogram
if self.amplitude1 == 0:
amplitude = 'log'
elif self.amplitude1 == 1:
amplitude = 'lin'
if self.display == 0:
display = 'log'
elif self.display == 1:
display = 'sqrt'
elif self.display == 2:
display = 'cbrt'
elif self.display == 3:
display = 'lin'
elif self.display == 4:
display = 'rlog'
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"),
amplitude, display
)
)
elif self.filterType == 2: # Vector Scope
if self.amplitude2 == 0:
amplitude = 'log'
elif self.amplitude2 == 1:
amplitude = 'sqrt'
elif self.amplitude2 == 2:
amplitude = 'cbrt'
elif self.amplitude2 == 3:
amplitude = 'lin'
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"),
'line'if self.draw else 'dot',
m, amplitude, str(self.zoom),
)
)
elif self.filterType == 3: # Musical Scale
filter_ = (
'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"),
str(self.tc),
)
)
elif self.filterType == 4: # Phase
filter_ = (
'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 '% (
self.settings.value("outputFrameRate"),
self.settings.value("outputWidth"),
self.settings.value("outputHeight"),
)
)
return [
'-filter_complex',
'%s%s%s%s [v1]; '
'[v1] %sscale=%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' % (
"{0:.3f}".format(startPt + 12),
"{0:.3f}".format(startPt + 12.5)
) if preview 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 ''
),
'-map', '[v]',
]
def updateChunksize(self):
width, height = scale(self.scale, self.width, self.height, int)
oldChunkSize = int(self.chunkSize)
self.chunkSize = 4 * width * height
changed = self.chunkSize != oldChunkSize
return changed
def finalizeFrame(self, imageData):
try:
image = Image.frombytes(
'RGBA',
scale(self.scale, self.width, self.height, int),
imageData
)
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
return frame

946
src/components/spectrum.ui Normal file
View File

@ -0,0 +1,946 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>586</width>
<height>197</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>197</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>4</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QLabel" name="label_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Type</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_filterType">
<item>
<property name="text">
<string>Spectrum</string>
</property>
</item>
<item>
<property name="text">
<string>Histogram</string>
</property>
</item>
<item>
<property name="text">
<string>Vector Scope</string>
</property>
</item>
<item>
<property name="text">
<string>Musical Scale</string>
</property>
</item>
<item>
<property name="text">
<string>Phase</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_9">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_xTitleAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>X</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_x">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_yTitleAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Y</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_y">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QCheckBox" name="checkBox_compress">
<property name="text">
<string>Compress</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_mono">
<property name="text">
<string>Mono</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_mirror">
<property name="text">
<string>Mirror</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_11">
<property name="text">
<string>Hue</string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_hue">
<property name="suffix">
<string>° </string>
</property>
<property name="maximum">
<number>359</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Scale</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_scale">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::UpDownArrows</enum>
</property>
<property name="suffix">
<string>%</string>
</property>
<property name="minimum">
<number>10</number>
</property>
<property name="maximum">
<number>400</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="page">
<widget class="QWidget" name="verticalLayoutWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>561</width>
<height>66</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<widget class="QLabel" name="label_textColor">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>31</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Window</string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_window">
<item>
<property name="text">
<string>hann</string>
</property>
</item>
<item>
<property name="text">
<string>gauss</string>
</property>
</item>
<item>
<property name="text">
<string>tukey</string>
</property>
</item>
<item>
<property name="text">
<string>dolph</string>
</property>
</item>
<item>
<property name="text">
<string>cauchy</string>
</property>
</item>
<item>
<property name="text">
<string>parzen</string>
</property>
</item>
<item>
<property name="text">
<string>poisson</string>
</property>
</item>
<item>
<property name="text">
<string>rect</string>
</property>
</item>
<item>
<property name="text">
<string>bartlett</string>
</property>
</item>
<item>
<property name="text">
<string>hanning</string>
</property>
</item>
<item>
<property name="text">
<string>hamming</string>
</property>
</item>
<item>
<property name="text">
<string>blackman</string>
</property>
</item>
<item>
<property name="text">
<string>welch</string>
</property>
</item>
<item>
<property name="text">
<string>flattop</string>
</property>
</item>
<item>
<property name="text">
<string>bharris</string>
</property>
</item>
<item>
<property name="text">
<string>bnuttall</string>
</property>
</item>
<item>
<property name="text">
<string>lanczos</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Amplitude</string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_amplitude0">
<item>
<property name="text">
<string>Square root</string>
</property>
</item>
<item>
<property name="text">
<string>Cubic root</string>
</property>
</item>
<item>
<property name="text">
<string>4thrt</string>
</property>
</item>
<item>
<property name="text">
<string>5thrt</string>
</property>
</item>
<item>
<property name="text">
<string>Linear</string>
</property>
</item>
<item>
<property name="text">
<string>Logarithmic</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Color </string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_color">
<item>
<property name="text">
<string>Channel</string>
</property>
</item>
<item>
<property name="text">
<string>Intensity</string>
</property>
</item>
<item>
<property name="text">
<string>Rainbow</string>
</property>
</item>
<item>
<property name="text">
<string>Moreland</string>
</property>
</item>
<item>
<property name="text">
<string>Nebulae</string>
</property>
</item>
<item>
<property name="text">
<string>Fire</string>
</property>
</item>
<item>
<property name="text">
<string>Fiery</string>
</property>
</item>
<item>
<property name="text">
<string>Fruit</string>
</property>
</item>
<item>
<property name="text">
<string>Cool</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="page_2">
<widget class="QWidget" name="verticalLayoutWidget_2">
<property name="geometry">
<rect>
<x>-1</x>
<y>-1</y>
<width>561</width>
<height>31</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_6">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Display Scale</string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_display">
<item>
<property name="text">
<string>Logarithmic</string>
</property>
</item>
<item>
<property name="text">
<string>Square root</string>
</property>
</item>
<item>
<property name="text">
<string>Cubic root</string>
</property>
</item>
<item>
<property name="text">
<string>Linear</string>
</property>
</item>
<item>
<property name="text">
<string>Reverse Log</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Amplitude</string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_amplitude1">
<item>
<property name="text">
<string>Logarithmic</string>
</property>
</item>
<item>
<property name="text">
<string>Linear</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="page_3">
<widget class="QWidget" name="verticalLayoutWidget_3">
<property name="geometry">
<rect>
<x>-1</x>
<y>-1</y>
<width>585</width>
<height>64</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_9">
<property name="text">
<string>Mode</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_mode">
<item>
<property name="text">
<string>lissajous</string>
</property>
</item>
<item>
<property name="text">
<string>lissajous_xy</string>
</property>
</item>
<item>
<property name="text">
<string>polar</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="label_7">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Amplitude</string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_amplitude2">
<item>
<property name="text">
<string>Linear</string>
</property>
</item>
<item>
<property name="text">
<string>Square root</string>
</property>
</item>
<item>
<property name="text">
<string>Cubic root</string>
</property>
</item>
<item>
<property name="text">
<string>Logarithmic</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="label_8">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Zoom</string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_zoom">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_draw">
<property name="text">
<string>Line</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="page_4">
<widget class="QWidget" name="verticalLayoutWidget_4">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>561</width>
<height>31</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLabel" name="label_10">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Timeclamp</string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="spinBox_tc">
<property name="suffix">
<string>s</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>0.002000000000000</double>
</property>
<property name="maximum">
<double>1.000000000000000</double>
</property>
<property name="singleStep">
<double>0.010000000000000</double>
</property>
<property name="value">
<double>0.017000000000000</double>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="page_5">
<widget class="QWidget" name="verticalLayoutWidget_5">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>551</width>
<height>31</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_11"/>
</item>
</layout>
</widget>
</widget>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -5,12 +5,11 @@ import os
from component import Component from component import Component
from toolkit.frame import FramePainter from toolkit.frame import FramePainter
from toolkit import rgbFromString, pickColor
class Component(Component): class Component(Component):
name = 'Title Text' name = 'Title Text'
version = '1.0.0' version = '1.0.1'
def __init__(self, *args): def __init__(self, *args):
super().__init__(*args) super().__init__(*args)
@ -18,53 +17,47 @@ class Component(Component):
def widget(self, *args): def widget(self, *args):
super().widget(*args) super().widget(*args)
height = int(self.settings.value('outputHeight'))
width = int(self.settings.value('outputWidth'))
self.textColor = (255, 255, 255) self.textColor = (255, 255, 255)
self.title = 'Text' self.title = 'Text'
self.alignment = 1 self.alignment = 1
self.fontSize = height / 13.5 self.fontSize = self.height / 13.5
fm = QtGui.QFontMetrics(self.titleFont)
self.xPosition = width / 2 - fm.width(self.title)/2
self.yPosition = height / 2 * 1.036
self.page.comboBox_textAlign.addItem("Left") self.page.comboBox_textAlign.addItem("Left")
self.page.comboBox_textAlign.addItem("Middle") self.page.comboBox_textAlign.addItem("Middle")
self.page.comboBox_textAlign.addItem("Right") self.page.comboBox_textAlign.addItem("Right")
self.page.comboBox_textAlign.setCurrentIndex(int(self.alignment))
self.page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor) 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()
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_fontSize.setValue(int(self.fontSize))
self.page.spinBox_xTextAlign.setValue(int(self.xPosition)) self.page.lineEdit_title.setText(self.title)
self.page.spinBox_yTextAlign.setValue(int(self.yPosition))
self.page.pushButton_center.clicked.connect(self.centerXY)
self.page.fontComboBox_titleFont.currentFontChanged.connect( self.page.fontComboBox_titleFont.currentFontChanged.connect(
self.update self.update
) )
self.trackWidgets({ self.trackWidgets({
'textColor': self.page.lineEdit_textColor,
'title': self.page.lineEdit_title, 'title': self.page.lineEdit_title,
'alignment': self.page.comboBox_textAlign, 'alignment': self.page.comboBox_textAlign,
'fontSize': self.page.spinBox_fontSize, 'fontSize': self.page.spinBox_fontSize,
'xPosition': self.page.spinBox_xTextAlign, 'xPosition': self.page.spinBox_xTextAlign,
'yPosition': self.page.spinBox_yTextAlign, 'yPosition': self.page.spinBox_yTextAlign,
}) }, colorWidgets={
'textColor': self.page.pushButton_textColor,
}, relativeWidgets=[
'xPosition', 'yPosition', 'fontSize',
])
self.centerXY()
def update(self): def update(self):
self.titleFont = self.page.fontComboBox_titleFont.currentFont() self.titleFont = self.page.fontComboBox_titleFont.currentFont()
self.textColor = rgbFromString(
self.page.lineEdit_textColor.text())
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.textColor).name()
self.page.pushButton_textColor.setStyleSheet(btnStyle)
super().update() super().update()
def centerXY(self):
self.setRelativeWidget('xPosition', 0.5)
self.setRelativeWidget('yPosition', 0.5)
def getXY(self): def getXY(self):
'''Returns true x, y after considering alignment settings''' '''Returns true x, y after considering alignment settings'''
fm = QtGui.QFontMetrics(self.titleFont) fm = QtGui.QFontMetrics(self.titleFont)
@ -86,15 +79,10 @@ class Component(Component):
font = QFont() font = QFont()
font.fromString(pr['titleFont']) font.fromString(pr['titleFont'])
self.page.fontComboBox_titleFont.setCurrentFont(font) self.page.fontComboBox_titleFont.setCurrentFont(font)
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): def savePreset(self):
saveValueStore = super().savePreset() saveValueStore = super().savePreset()
saveValueStore['titleFont'] = self.titleFont.toString() saveValueStore['titleFont'] = self.titleFont.toString()
saveValueStore['textColor'] = self.textColor
return saveValueStore return saveValueStore
def previewRender(self): def previewRender(self):
@ -122,13 +110,6 @@ class Component(Component):
return image.finalize() return image.finalize()
def pickColor(self):
RGBstring, btnStyle = pickColor()
if not RGBstring:
return
self.page.lineEdit_textColor.setText(RGBstring)
self.page.pushButton_textColor.setStyleSheet(btnStyle)
def commandHelp(self): def commandHelp(self):
print('Enter a string to use as centred white text:') print('Enter a string to use as centred white text:')
print(' "title=User Error"') print(' "title=User Error"')

View File

@ -19,6 +19,36 @@
<property name="leftMargin"> <property name="leftMargin">
<number>4</number> <number>4</number>
</property> </property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_title">
<property name="text">
<string>Title</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_title">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Testing New GUI</string>
</property>
</widget>
</item>
</layout>
</item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_8"> <layout class="QHBoxLayout" name="horizontalLayout_8">
<item> <item>
@ -81,6 +111,9 @@
</item> </item>
<item> <item>
<widget class="QSpinBox" name="spinBox_fontSize"> <widget class="QSpinBox" name="spinBox_fontSize">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum"> <property name="maximum">
<number>500</number> <number>500</number>
</property> </property>
@ -90,6 +123,55 @@
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_12"> <layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<widget class="QLabel" name="label_textColor">
<property name="text">
<string>Text Color</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_textColor"/>
</item>
<item>
<widget class="QPushButton" name="pushButton_textColor">
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="MaximumSize" stdset="0">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<property name="leftMargin">
<number>0</number>
</property>
<item> <item>
<widget class="QLabel" name="label_textLayout"> <widget class="QLabel" name="label_textLayout">
<property name="sizePolicy"> <property name="sizePolicy">
@ -123,64 +205,9 @@
</spacer> </spacer>
</item> </item>
<item> <item>
<widget class="QLabel" name="label_textColor"> <widget class="QPushButton" name="pushButton_center">
<property name="text"> <property name="text">
<string>Text Color</string> <string>Center</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_textColor">
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="MaximumSize" stdset="0">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_textColor"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<property name="leftMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_title">
<property name="text">
<string>Title</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_title">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Testing New GUI</string>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -1,103 +1,13 @@
from PIL import Image, ImageDraw from PIL import Image
from PyQt5 import QtGui, QtCore, QtWidgets from PyQt5 import QtGui, QtCore, QtWidgets
import os import os
import math import math
import subprocess import subprocess
import signal
import threading
from queue import PriorityQueue
from component import Component, ComponentError from component import Component
from toolkit.frame import BlankFrame from toolkit.frame import BlankFrame, scale
from toolkit.ffmpeg import testAudioStream from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo
from toolkit import openPipe, checkOutput from toolkit import 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
class Component(Component): class Component(Component):
@ -106,30 +16,30 @@ class Component(Component):
def widget(self, *args): def widget(self, *args):
self.videoPath = '' self.videoPath = ''
self.badVideo = False
self.badAudio = False self.badAudio = False
self.x = 0 self.x = 0
self.y = 0 self.y = 0
self.loopVideo = False self.loopVideo = False
super().widget(*args) super().widget(*args)
self._image = BlankFrame(self.width, self.height)
self.page.pushButton_video.clicked.connect(self.pickVideo) self.page.pushButton_video.clicked.connect(self.pickVideo)
self.trackWidgets( self.trackWidgets({
{ 'videoPath': self.page.lineEdit_video,
'videoPath': self.page.lineEdit_video, 'loopVideo': self.page.checkBox_loop,
'loopVideo': self.page.checkBox_loop, 'useAudio': self.page.checkBox_useAudio,
'useAudio': self.page.checkBox_useAudio, 'distort': self.page.checkBox_distort,
'distort': self.page.checkBox_distort, 'scale': self.page.spinBox_scale,
'scale': self.page.spinBox_scale, 'volume': self.page.spinBox_volume,
'volume': self.page.spinBox_volume, 'xPosition': self.page.spinBox_x,
'xPosition': self.page.spinBox_x, 'yPosition': self.page.spinBox_y,
'yPosition': self.page.spinBox_y, }, presetNames={
}, presetNames={ 'videoPath': 'video',
'videoPath': 'video', 'loopVideo': 'loop',
'loopVideo': 'loop', 'xPosition': 'x',
'xPosition': 'x', 'yPosition': 'y',
'yPosition': 'y', }, relativeWidgets=[
} 'xPosition', 'yPosition',
) ])
def update(self): def update(self):
if self.page.checkBox_useAudio.isChecked(): if self.page.checkBox_useAudio.isChecked():
@ -157,8 +67,6 @@ class Component(Component):
if not self.videoPath: if not self.videoPath:
self.lockError("There is no video selected.") 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): elif not os.path.exists(self.videoPath):
self.lockError("The video selected does not exist!") self.lockError("The video selected does not exist!")
elif os.path.realpath(self.videoPath) == os.path.realpath(outputFile): elif os.path.realpath(self.videoPath) == os.path.realpath(outputFile):
@ -182,22 +90,21 @@ class Component(Component):
def preFrameRender(self, **kwargs): def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs) super().preFrameRender(**kwargs)
self.updateChunksize() self.updateChunksize()
self.video = Video( self.video = FfmpegVideo(
ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath, inputPath=self.videoPath, filter_=self.makeFfmpegFilter(),
width=self.width, height=self.height, chunkSize=self.chunkSize, width=self.width, height=self.height, chunkSize=self.chunkSize,
frameRate=int(self.settings.value("outputFrameRate")), frameRate=int(self.settings.value("outputFrameRate")),
parent=self.parent, loopVideo=self.loopVideo, parent=self.parent, loopVideo=self.loopVideo,
component=self, scale=self.scale component=self
) if os.path.exists(self.videoPath) else None ) if os.path.exists(self.videoPath) else None
def frameRender(self, frameNo): def frameRender(self, frameNo):
if Video.threadError is not None: if FfmpegVideo.threadError is not None:
raise Video.threadError raise FfmpegVideo.threadError
return self.video.frame(frameNo) return self.finalizeFrame(self.video.frame(frameNo))
def postFrameRender(self): def postFrameRender(self):
self.video.pipe.stdout.close() closePipe(self.video.pipe)
self.video.pipe.send_signal(signal.SIGINT)
def pickVideo(self): def pickVideo(self):
imgDir = self.settings.value("componentDir", os.path.expanduser("~")) imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
@ -220,23 +127,30 @@ class Component(Component):
'-i', self.videoPath, '-i', self.videoPath,
'-f', 'image2pipe', '-f', 'image2pipe',
'-pix_fmt', 'rgba', '-pix_fmt', 'rgba',
'-filter_complex', '[0:v] scale=%s:%s' % scale(
self.scale, width, height, str),
'-vcodec', 'rawvideo', '-',
'-ss', '90',
'-vframes', '1',
] ]
command.extend(self.makeFfmpegFilter())
command.extend([
'-codec:v', 'rawvideo', '-',
'-ss', '90',
'-frames:v', '1',
])
pipe = openPipe( pipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, bufsize=10**8 stderr=subprocess.DEVNULL, bufsize=10**8
) )
byteFrame = pipe.stdout.read(self.chunkSize) byteFrame = pipe.stdout.read(self.chunkSize)
pipe.stdout.close() closePipe(pipe)
pipe.send_signal(signal.SIGINT)
frame = finalizeFrame(self, byteFrame, width, height) frame = self.finalizeFrame(byteFrame)
return frame return frame
def makeFfmpegFilter(self):
return [
'-filter_complex',
'[0:v] scale=%s:%s' % scale(
self.scale, self.width, self.height, str),
]
def updateChunksize(self): def updateChunksize(self):
if self.scale != 100 and not self.distort: if self.scale != 100 and not self.distort:
width, height = scale(self.scale, self.width, self.height, int) width, height = scale(self.scale, self.width, self.height, int)
@ -268,44 +182,27 @@ class Component(Component):
print('Load a video:\n path=/filepath/to/video.mp4') print('Load a video:\n path=/filepath/to/video.mp4')
print('Using audio:\n path=/filepath/to/video.mp4 audio') 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)
self._image = image
except ValueError:
# use last good frame
image = self._image
def scale(scale, width, height, returntype=None): if self.scale != 100 \
width = (float(width) / 100.0) * float(scale) or self.xPosition != 0 or self.yPosition != 0:
height = (float(height) / 100.0) * float(scale) frame = BlankFrame(self.width, self.height)
if returntype == str: frame.paste(image, box=(self.xPosition, self.yPosition))
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)
else: else:
image = Image.frombytes( frame = image
'RGBA', return frame
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

194
src/components/waveform.py Normal file
View File

@ -0,0 +1,194 @@
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
from toolkit.frame import BlankFrame, scale
from toolkit import checkOutput
from toolkit.ffmpeg import (
openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound
)
class Component(Component):
name = 'Waveform'
version = '1.0.0'
def widget(self, *args):
super().widget(*args)
self._image = BlankFrame(self.width, self.height)
self.page.lineEdit_color.setText('255,255,255')
if hasattr(self.parent, 'window'):
self.parent.window.lineEdit_audioFile.textChanged.connect(
self.update
)
self.trackWidgets({
'color': self.page.lineEdit_color,
'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,
}, colorWidgets={
'color': self.page.pushButton_color,
}, relativeWidgets=[
'x', 'y',
])
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()
w, h = scale(self.scale, self.width, self.height, str)
self.video = FfmpegVideo(
inputPath=self.audioFile,
filter_=self.makeFfmpegFilter(),
width=w, height=h,
chunkSize=self.chunkSize,
frameRate=int(self.settings.value("outputFrameRate")),
parent=self.parent, component=self, debug=True,
)
def frameRender(self, frameNo):
if FfmpegVideo.threadError is not None:
raise FfmpegVideo.threadError
return self.finalizeFrame(self.video.frame(frameNo))
def postFrameRender(self):
closePipe(self.video.pipe)
def getPreviewFrame(self, width, height):
genericPreview = self.settings.value("pref_genericPreview")
startPt = 0
if not genericPreview:
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
if startPt + 3 > duration:
startPt += startPt - 3
command = [
self.core.FFMPEG_BIN,
'-thread_queue_size', '512',
'-r', self.settings.value("outputFrameRate"),
'-ss', "{0:.3f}".format(startPt),
'-i',
os.path.join(self.core.wd, 'background.png')
if genericPreview else inputFile,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',
]
command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt))
command.extend([
'-an',
'-s:v', '%sx%s' % scale(self.scale, self.width, self.height, str),
'-codec:v', 'rawvideo', '-',
'-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 = self.finalizeFrame(byteFrame)
return frame
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)
genericPreview = self.settings.value("pref_genericPreview")
if self.mode < 3:
filter_ = 'showwaves=r=%s:s=%sx%s:mode=%s:colors=%s@%s:scale=%s' % (
self.settings.value("outputFrameRate"),
self.settings.value("outputWidth"),
self.settings.value("outputHeight"),
self.page.comboBox_mode.currentText().lower()
if self.mode != 3 else 'p2p',
hexcolor, opacity, amplitude,
)
elif self.mode > 2:
filter_ = (
'showfreqs=s=%sx%s:mode=%s:colors=%s@%s'
':ascale=%s:fscale=%s' % (
self.settings.value("outputWidth"),
self.settings.value("outputHeight"),
'line' if self.mode == 4 else 'bar',
hexcolor, opacity, amplitude,
'log' if self.mono else 'lin'
)
)
return [
'-filter_complex',
'%s%s%s'
'%s%s%s [v1]; '
'[v1] scale=%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 and self.mode < 3 else '',
filter_,
', 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 + 3)
if preview else '',
),
'-map', '[v]',
]
def updateChunksize(self):
width, height = scale(self.scale, self.width, self.height, int)
self.chunkSize = 4 * width * height
def finalizeFrame(self, imageData):
try:
image = Image.frombytes(
'RGBA',
scale(self.scale, self.width, self.height, int),
imageData
)
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
return frame

383
src/components/waveform.ui Normal file
View File

@ -0,0 +1,383 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>586</width>
<height>197</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>197</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>4</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QLabel" name="label_textColor">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>31</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Mode</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_mode">
<item>
<property name="text">
<string>Cline</string>
</property>
</item>
<item>
<property name="text">
<string>Line</string>
</property>
</item>
<item>
<property name="text">
<string>Point</string>
</property>
</item>
<item>
<property name="text">
<string>Frequency Bar</string>
</property>
</item>
<item>
<property name="text">
<string>Frequency Line</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_9">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_xTitleAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>X</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_x">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_yTitleAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Y</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_y">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Color</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_color">
<property name="inputMethodHints">
<set>Qt::ImhNone</set>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_color">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="default">
<bool>false</bool>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Opacity</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_opacity">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::UpDownArrows</enum>
</property>
<property name="suffix">
<string>%</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Scale</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_scale">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::UpDownArrows</enum>
</property>
<property name="suffix">
<string>%</string>
</property>
<property name="minimum">
<number>10</number>
</property>
<property name="maximum">
<number>400</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QCheckBox" name="checkBox_compress">
<property name="text">
<string>Compress</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_mono">
<property name="text">
<string>Mono</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_mirror">
<property name="text">
<string>Mirror</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Amplitude</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_amplitude">
<item>
<property name="text">
<string>Linear</string>
</property>
</item>
<item>
<property name="text">
<string>Logarithmic</string>
</property>
</item>
<item>
<property name="text">
<string>Square root</string>
</property>
</item>
<item>
<property name="text">
<string>Cubic root</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -161,7 +161,7 @@ class Core:
for widget, value in data['WindowFields']: for widget, value in data['WindowFields']:
widget = eval('loader.window.%s' % widget) widget = eval('loader.window.%s' % widget)
widget.blockSignals(True) widget.blockSignals(True)
widget.setText(value) toolkit.setWidgetValue(widget, value)
widget.blockSignals(False) widget.blockSignals(False)
for key, value in data['Settings']: for key, value in data['Settings']:
@ -451,8 +451,8 @@ class Core:
'1280x720', '1280x720',
'854x480', '854x480',
], ],
'windowHasFocus': False,
'FFMPEG_BIN': findFfmpeg(), 'FFMPEG_BIN': findFfmpeg(),
'windowHasFocus': False,
'canceled': False, 'canceled': False,
} }
@ -492,7 +492,7 @@ class Core:
@classmethod @classmethod
def loadDefaultSettings(cls): def loadDefaultSettings(cls):
defaultSettings = { cls.defaultSettings = {
"outputWidth": 1280, "outputWidth": 1280,
"outputHeight": 720, "outputHeight": 720,
"outputFrameRate": 30, "outputFrameRate": 30,
@ -506,9 +506,10 @@ class Core:
"outputContainer": "MP4", "outputContainer": "MP4",
"projectDir": os.path.join(cls.dataDir, 'projects'), "projectDir": os.path.join(cls.dataDir, 'projects'),
"pref_insertCompAtTop": True, "pref_insertCompAtTop": True,
"pref_genericPreview": True,
} }
for parm, value in defaultSettings.items(): for parm, value in cls.defaultSettings.items():
if cls.settings.value(parm) is None: if cls.settings.value(parm) is None:
cls.settings.setValue(parm, value) cls.settings.setValue(parm, value)

View File

@ -581,7 +581,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.showMessage( self.showMessage(
msg=msg, msg=msg,
detail=detail, detail=detail,
icon='Warning', icon='Critical',
) )
def changeEncodingStatus(self, status): def changeEncodingStatus(self, status):
@ -644,9 +644,12 @@ class MainWindow(QtWidgets.QMainWindow):
def updateResolution(self): def updateResolution(self):
resIndex = int(self.window.comboBox_resolution.currentIndex()) resIndex = int(self.window.comboBox_resolution.currentIndex())
res = Core.resolutions[resIndex].split('x') res = Core.resolutions[resIndex].split('x')
changed = res[0] != self.settings.value("outputWidth")
self.settings.setValue('outputWidth', res[0]) self.settings.setValue('outputWidth', res[0])
self.settings.setValue('outputHeight', res[1]) self.settings.setValue('outputHeight', res[1])
self.drawPreview() if changed:
for i in range(len(self.core.selectedComponents)):
self.core.updateComponent(i)
def drawPreview(self, force=False, **kwargs): def drawPreview(self, force=False, **kwargs):
'''Use autosave keyword arg to force saving or not saving if needed''' '''Use autosave keyword arg to force saving or not saving if needed'''
@ -791,6 +794,8 @@ class MainWindow(QtWidgets.QMainWindow):
field.blockSignals(True) field.blockSignals(True)
field.setText('') field.setText('')
field.blockSignals(False) field.blockSignals(False)
self.progressBarUpdated(0)
self.progressBarSetText('')
@disableWhenEncoding @disableWhenEncoding
def createNewProject(self, prompt=True): def createNewProject(self, prompt=True):

View File

@ -59,7 +59,9 @@ class Worker(QtCore.QObject):
components = nextPreviewInformation["components"] components = nextPreviewInformation["components"]
for component in reversed(components): for component in reversed(components):
try: try:
component.lockSize(width, height)
newFrame = component.previewRender() newFrame = component.previewRender()
component.unlockSize()
frame = Image.alpha_composite( frame = Image.alpha_composite(
frame, newFrame frame, newFrame
) )

View File

@ -34,30 +34,28 @@ def appendUppercase(lst):
lst.append(form.upper()) lst.append(form.upper())
return lst return lst
def pipeWrapper(func):
def hideCmdWin(func): '''A decorator to insert proper kwargs into Popen objects.'''
''' Stops CMD window from appearing on Windows. def pipeWrapper(commandList, **kwargs):
Adapted from here: http://code.activestate.com/recipes/409002/
'''
def decorator(commandList, **kwargs):
if sys.platform == 'win32': if sys.platform == 'win32':
# Stop CMD window from appearing on Windows
startupinfo = subprocess.STARTUPINFO() startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
kwargs['startupinfo'] = startupinfo 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 func(commandList, **kwargs)
return decorator return pipeWrapper
@hideCmdWin @pipeWrapper
def checkOutput(commandList, **kwargs): def checkOutput(commandList, **kwargs):
return subprocess.check_output(commandList, **kwargs) return subprocess.check_output(commandList, **kwargs)
@hideCmdWin
def openPipe(commandList, **kwargs):
return subprocess.Popen(commandList, **kwargs)
def disableWhenEncoding(func): def disableWhenEncoding(func):
def decorator(self, *args, **kwargs): def decorator(self, *args, **kwargs):
if self.encoding: if self.encoding:
@ -76,25 +74,6 @@ def disableWhenOpeningProject(func):
return decorator 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): def rgbFromString(string):
'''Turns an RGB string like "255, 255, 255" into a tuple''' '''Turns an RGB string like "255, 255, 255" into a tuple'''
try: try:
@ -115,3 +94,46 @@ def formatTraceback(tb=None):
import sys import sys
tb = sys.exc_info()[2] tb = sys.exc_info()[2]
return 'Traceback:\n%s' % "\n".join(traceback.format_tb(tb)) return 'Traceback:\n%s' % "\n".join(traceback.format_tb(tb))
def connectWidget(widget, func):
if type(widget) == QtWidgets.QLineEdit:
widget.textChanged.connect(func)
elif type(widget) == QtWidgets.QSpinBox \
or type(widget) == QtWidgets.QDoubleSpinBox:
widget.valueChanged.connect(func)
elif type(widget) == QtWidgets.QCheckBox:
widget.stateChanged.connect(func)
elif type(widget) == QtWidgets.QComboBox:
widget.currentIndexChanged.connect(func)
else:
return False
return True
def setWidgetValue(widget, val):
'''Generic setValue method for use with any typical QtWidget'''
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)
else:
return False
return True
def getWidgetValue(widget):
if type(widget) == QtWidgets.QLineEdit:
return widget.text()
elif type(widget) == QtWidgets.QSpinBox \
or type(widget) == QtWidgets.QDoubleSpinBox:
return widget.value()
elif type(widget) == QtWidgets.QCheckBox:
return widget.isChecked()
elif type(widget) == QtWidgets.QComboBox:
return widget.currentIndex()

View File

@ -5,9 +5,133 @@ import numpy
import sys import sys
import os import os
import subprocess import subprocess
import threading
import signal
from queue import PriorityQueue
import core import core
from toolkit.common import checkOutput, openPipe from toolkit.common import checkOutput, pipeWrapper
from component import ComponentError
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([
'-codec:v', '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):
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:
self.pipe = openPipe(
self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=log, 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:
FfmpegVideo.threadError = ComponentError(
self.component, 'video',
"Video seemed playable but wasn't."
)
break
try:
self.currentFrame = self.pipe.stdout.read(self.chunkSize)
except ValueError:
FfmpegVideo.threadError = ComponentError(
self.component, 'video')
if len(self.currentFrame) != 0:
self.frameBuffer.put((self.frameNo, self.currentFrame))
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(): def findFfmpeg():
@ -248,7 +372,12 @@ def getAudioDuration(filename):
except subprocess.CalledProcessError as ex: except subprocess.CalledProcessError as ex:
fileInfo = ex.output 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: for line in info:
if 'Duration' in line: if 'Duration' in line:
d = line.split(',')[0] d = line.split(',')[0]
@ -321,3 +450,10 @@ def readAudioFile(filename, videoWorker):
completeAudioArray = completeAudioArrayCopy completeAudioArray = completeAudioArrayCopy
return (completeAudioArray, duration) return (completeAudioArray, duration)
def exampleSound():
return (
'aevalsrc=tan(random(1)*PI*t)*sin(random(0)*2*PI*t),'
'apulsator=offset_l=0.5:offset_r=0.5,'
)

View File

@ -6,6 +6,7 @@ from PIL import Image
from PIL.ImageQt import ImageQt from PIL.ImageQt import ImageQt
import sys import sys
import os import os
import math
import core import core
@ -41,6 +42,17 @@ class PaintColor(QtGui.QColor):
super().__init__(b, g, r, a) super().__init__(b, g, r, a)
def scale(scalePercent, width, height, returntype=None):
width = (float(width) / 100.0) * float(scalePercent)
height = (float(height) / 100.0) * float(scalePercent)
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): def defaultSize(framefunc):
'''Makes width/height arguments optional''' '''Makes width/height arguments optional'''
def decorator(*args): def decorator(*args):

View File

@ -19,9 +19,11 @@ import time
import signal import signal
from component import ComponentError from component import ComponentError
from toolkit import openPipe
from toolkit.ffmpeg import readAudioFile, createFfmpegCommand
from toolkit.frame import Checkerboard from toolkit.frame import Checkerboard
from toolkit.ffmpeg import (
openPipe, readAudioFile,
getAudioDuration, createFfmpegCommand
)
class Worker(QtCore.QObject): class Worker(QtCore.QObject):
@ -132,15 +134,24 @@ class Worker(QtCore.QObject):
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# READ AUDIO, INITIALIZE COMPONENTS, OPEN A PIPE TO FFMPEG # READ AUDIO, INITIALIZE COMPONENTS, OPEN A PIPE TO FFMPEG
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
if any([
self.progressBarSetText.emit("Loading audio file...") True if 'pcm' in comp.properties() else False
audioFileTraits = readAudioFile( for comp in self.components
self.inputFile, self ]):
) self.progressBarSetText.emit("Loading audio file...")
if audioFileTraits is None: audioFileTraits = readAudioFile(
self.cancelExport() self.inputFile, self
return )
self.completeAudioArray, duration = audioFileTraits 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.progressBarUpdate.emit(0)
self.progressBarSetText.emit("Starting components...") self.progressBarSetText.emit("Starting components...")
@ -153,7 +164,7 @@ class Worker(QtCore.QObject):
for compNo, comp in enumerate(reversed(self.components)): for compNo, comp in enumerate(reversed(self.components)):
try: try:
comp.preFrameRender( comp.preFrameRender(
worker=self, audioFile=self.inputFile,
completeAudioArray=self.completeAudioArray, completeAudioArray=self.completeAudioArray,
sampleSize=self.sampleSize, sampleSize=self.sampleSize,
progressBarUpdate=self.progressBarUpdate, progressBarUpdate=self.progressBarUpdate,
@ -284,7 +295,10 @@ class Worker(QtCore.QObject):
numpy.seterr(all='print') 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: if self.out_pipe.stderr is not None:
print(self.out_pipe.stderr.read()) print(self.out_pipe.stderr.read())
self.out_pipe.stderr.close() self.out_pipe.stderr.close()