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.*
*.exe
ffmpeg
*.bak
*~

View File

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

View File

@ -3,16 +3,21 @@
on making a valid component.
'''
from PyQt5 import uic, QtCore, QtWidgets
from PyQt5.QtGui import QColor
import os
import sys
import math
import time
from toolkit.frame import BlankFrame
from toolkit import (
getWidgetValue, setWidgetValue, connectWidget, rgbFromString
)
class ComponentMetaclass(type(QtCore.QObject)):
'''
Checks the validity of each Component class imported, and
mutates some attributes for easier use by the core program.
Checks the validity of each Component class and mutates some attrs.
E.g., takes only major version from version string & decorates methods
'''
@ -171,8 +176,17 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self._trackedWidgets = {}
self._presetNames = {}
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._lockedError = None
self._lockedSize = None
# Stop lengthy processes in response to this variable
self.canceled = False
@ -181,12 +195,16 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
return self.__class__.name
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' % (
self.__class__.name, str(self.__class__.version), self.savePreset()
self.__class__.name, str(self.__class__.version), preset
)
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# Critical Methods
# Render Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
def previewRender(self):
@ -197,7 +215,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
Must call super() when subclassing
Triggered only before a video is exported (video_thread.py)
self.worker = the video thread worker
self.audioFile = filepath to the main input audio file
self.completeAudioArray = a list of audio samples
self.sampleSize = number of audio samples per video frame
self.progressBarUpdate = signal to set progress bar number
@ -273,14 +291,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
widgets['spinBox'].extend(
self.page.findChildren(QtWidgets.QDoubleSpinBox)
)
for widget in widgets['lineEdit']:
widget.textChanged.connect(self.update)
for widget in widgets['checkBox']:
widget.stateChanged.connect(self.update)
for widget in widgets['spinBox']:
widget.valueChanged.connect(self.update)
for widget in widgets['comboBox']:
widget.currentIndexChanged.connect(self.update)
for widgetList in widgets.values():
for widget in widgetList:
connectWidget(widget, self.update)
def update(self):
'''
@ -289,15 +302,24 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
Call super() at the END if you need to subclass this.
'''
for attr, widget in self._trackedWidgets.items():
if type(widget) == QtWidgets.QLineEdit:
setattr(self, attr, widget.text())
elif type(widget) == QtWidgets.QSpinBox \
or type(widget) == QtWidgets.QDoubleSpinBox:
setattr(self, attr, widget.value())
elif type(widget) == QtWidgets.QCheckBox:
setattr(self, attr, widget.isChecked())
elif type(widget) == QtWidgets.QComboBox:
setattr(self, attr, widget.currentIndex())
if attr in self._colorWidgets:
# Color Widgets: text stored as tuple & update the button color
rgbTuple = rgbFromString(widget.text())
btnStyle = (
"QPushButton { background-color : %s; outline: none; }"
% QColor(*rgbTuple).name())
self._colorWidgets[attr].setStyleSheet(btnStyle)
setattr(self, attr, rgbTuple)
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:
self.parent.drawPreview()
saveValueStore = self.savePreset()
@ -313,27 +335,40 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self.currentPreset = presetName \
if presetName is not None else presetDict['preset']
for attr, widget in self._trackedWidgets.items():
val = presetDict[
attr if attr not in self._presetNames
key = attr if attr not in self._presetNames \
else self._presetNames[attr]
]
if type(widget) == QtWidgets.QLineEdit:
widget.setText(val)
elif type(widget) == QtWidgets.QSpinBox \
or type(widget) == QtWidgets.QDoubleSpinBox:
widget.setValue(val)
elif type(widget) == QtWidgets.QCheckBox:
widget.setChecked(val)
elif type(widget) == QtWidgets.QComboBox:
widget.setCurrentIndex(val)
val = presetDict[key]
if attr in self._colorWidgets:
widget.setText('%s,%s,%s' % val)
btnStyle = (
"QPushButton { background-color : %s; outline: none; }"
% QColor(*val).name()
)
self._colorWidgets[attr].setStyleSheet(btnStyle)
elif attr in self._relativeWidgets:
self._relativeValues[attr] = val
pixelVal = self.pixelValForAttr(attr, val)
setWidgetValue(widget, pixelVal)
else:
setWidgetValue(widget, val)
def savePreset(self):
saveValueStore = {}
for attr, widget in self._trackedWidgets.items():
saveValueStore[
presetAttrName = (
attr if attr not in self._presetNames
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
def commandHelp(self):
@ -372,7 +407,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self._trackedWidgets = trackDict
for kwarg in kwargs:
try:
if kwarg in ('presetNames', 'commandArgs'):
if kwarg in (
'presetNames',
'commandArgs',
'colorWidgets',
'relativeWidgets',
):
setattr(self, '_%s' % kwarg, kwargs[kwarg])
else:
raise ComponentError(
@ -380,29 +420,79 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
except ComponentError:
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):
self._lockedProperties = propList
def lockError(self, msg):
self._lockedError = msg
def lockSize(self, w, h):
self._lockedSize = (w, h)
def unlockProperties(self):
self._lockedProperties = None
def unlockError(self):
self._lockedError = None
def unlockSize(self):
self._lockedSize = None
def loadUi(self, filename):
'''Load a Qt Designer ui file to use for this component's widget'''
return uic.loadUi(os.path.join(self.core.componentsPath, filename))
@property
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
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):
'''Stop any lengthy process in response to this variable.'''
@ -413,6 +503,67 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self.unlockProperties()
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):
'''Gives the MainWindow a traceback to display, and cancels the export.'''
@ -420,36 +571,43 @@ class ComponentError(RuntimeError):
prevErrors = []
lastTime = time.time()
def __init__(self, caller, name):
print('##### ComponentError by %s: %s' % (caller.name, name))
def __init__(self, caller, name, msg=None):
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:
ComponentError.prevErrors.pop()
ComponentError.prevErrors.insert(0, name)
curTime = time.time()
if name in ComponentError.prevErrors[1:] \
and curTime - ComponentError.lastTime < 0.2:
# Don't create multiple windows for quickly repeated messages
and curTime - ComponentError.lastTime < 1.0:
return
ComponentError.lastTime = time.time()
from toolkit import formatTraceback
import sys
if sys.exc_info()[0] is not None:
string = (
"%s component's %s encountered %s %s." % (
"%s component (#%s): %s encountered %s %s: %s" % (
caller.__class__.name,
str(caller.compPos),
name,
'an' if any([
sys.exc_info()[0].__name__.startswith(vowel)
for vowel in ('A', 'I')
for vowel in ('A', 'I', 'U', 'O', 'E')
]) else 'a',
sys.exc_info()[0].__name__,
str(sys.exc_info()[1])
)
)
detail = formatTraceback(sys.exc_info()[2])
else:
string = name
detail = "Methods:\n%s" % (
detail = "Attributes:\n%s" % (
"\n".join(
[m for m in dir(caller) if not m.startswith('_')]
)

View File

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

View File

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

View File

@ -8,7 +8,6 @@ from copy import copy
from component import Component
from toolkit.frame import BlankFrame
from toolkit import rgbFromString, pickColor
class Component(Component):
@ -18,8 +17,10 @@ class Component(Component):
def names(*args):
return ['Original Audio Visualization']
def properties(self):
return ['pcm']
def widget(self, *args):
self.visColor = (255, 255, 255)
self.scale = 20
self.y = 0
super().widget(*args)
@ -30,34 +31,18 @@ class Component(Component):
self.page.comboBox_visLayout.addItem("Top")
self.page.comboBox_visLayout.setCurrentIndex(0)
self.page.lineEdit_visColor.setText('%s,%s,%s' % self.visColor)
self.page.pushButton_visColor.clicked.connect(lambda: self.pickColor())
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.visColor).name()
self.page.pushButton_visColor.setStyleSheet(btnStyle)
self.page.lineEdit_visColor.setText('255,255,255')
self.trackWidgets({
'visColor': self.page.lineEdit_visColor,
'layout': self.page.comboBox_visLayout,
'scale': self.page.spinBox_scale,
'y': self.page.spinBox_y,
})
def update(self):
self.visColor = rgbFromString(self.page.lineEdit_visColor.text())
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
}, colorWidgets={
'visColor': self.page.pushButton_visColor,
}, relativeWidgets=[
'y',
])
def previewRender(self):
spectrum = numpy.fromfunction(
@ -96,13 +81,6 @@ class Component(Component):
self.spectrumArray[arrayNo],
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(
self, i, completeAudioArray, sampleSize,
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 toolkit.frame import FramePainter
from toolkit import rgbFromString, pickColor
class Component(Component):
name = 'Title Text'
version = '1.0.0'
version = '1.0.1'
def __init__(self, *args):
super().__init__(*args)
@ -18,53 +17,47 @@ class Component(Component):
def widget(self, *args):
super().widget(*args)
height = int(self.settings.value('outputHeight'))
width = int(self.settings.value('outputWidth'))
self.textColor = (255, 255, 255)
self.title = 'Text'
self.alignment = 1
self.fontSize = height / 13.5
fm = QtGui.QFontMetrics(self.titleFont)
self.xPosition = width / 2 - fm.width(self.title)/2
self.yPosition = height / 2 * 1.036
self.fontSize = self.height / 13.5
self.page.comboBox_textAlign.addItem("Left")
self.page.comboBox_textAlign.addItem("Middle")
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.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_xTextAlign.setValue(int(self.xPosition))
self.page.spinBox_yTextAlign.setValue(int(self.yPosition))
self.page.lineEdit_title.setText(self.title)
self.page.pushButton_center.clicked.connect(self.centerXY)
self.page.fontComboBox_titleFont.currentFontChanged.connect(
self.update
)
self.trackWidgets({
'textColor': self.page.lineEdit_textColor,
'title': self.page.lineEdit_title,
'alignment': self.page.comboBox_textAlign,
'fontSize': self.page.spinBox_fontSize,
'xPosition': self.page.spinBox_xTextAlign,
'yPosition': self.page.spinBox_yTextAlign,
})
}, colorWidgets={
'textColor': self.page.pushButton_textColor,
}, relativeWidgets=[
'xPosition', 'yPosition', 'fontSize',
])
self.centerXY()
def update(self):
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()
def centerXY(self):
self.setRelativeWidget('xPosition', 0.5)
self.setRelativeWidget('yPosition', 0.5)
def getXY(self):
'''Returns true x, y after considering alignment settings'''
fm = QtGui.QFontMetrics(self.titleFont)
@ -86,15 +79,10 @@ class Component(Component):
font = QFont()
font.fromString(pr['titleFont'])
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):
saveValueStore = super().savePreset()
saveValueStore['titleFont'] = self.titleFont.toString()
saveValueStore['textColor'] = self.textColor
return saveValueStore
def previewRender(self):
@ -122,13 +110,6 @@ class Component(Component):
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):
print('Enter a string to use as centred white text:')
print(' "title=User Error"')

View File

@ -19,6 +19,36 @@
<property name="leftMargin">
<number>4</number>
</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>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
@ -81,6 +111,9 @@
</item>
<item>
<widget class="QSpinBox" name="spinBox_fontSize">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>500</number>
</property>
@ -90,6 +123,55 @@
</item>
<item>
<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>
<widget class="QLabel" name="label_textLayout">
<property name="sizePolicy">
@ -123,64 +205,9 @@
</spacer>
</item>
<item>
<widget class="QLabel" name="label_textColor">
<widget class="QPushButton" name="pushButton_center">
<property name="text">
<string>Text Color</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>
<string>Center</string>
</property>
</widget>
</item>

View File

@ -1,103 +1,13 @@
from PIL import Image, ImageDraw
from PIL import Image
from PyQt5 import QtGui, QtCore, QtWidgets
import os
import math
import subprocess
import signal
import threading
from queue import PriorityQueue
from component import Component, ComponentError
from toolkit.frame import BlankFrame
from toolkit.ffmpeg import testAudioStream
from toolkit import openPipe, checkOutput
class Video:
'''Opens a pipe to ffmpeg and stores a buffer of raw video frames.'''
# error from the thread used to fill the buffer
threadError = None
def __init__(self, **kwargs):
mandatoryArgs = [
'ffmpeg', # path to ffmpeg, usually self.core.FFMPEG_BIN
'videoPath',
'width',
'height',
'scale', # percentage scale
'frameRate', # frames per second
'chunkSize', # number of bytes in one frame
'parent', # mainwindow object
'component', # component object
]
for arg in mandatoryArgs:
setattr(self, arg, kwargs[arg])
self.frameNo = -1
self.currentFrame = 'None'
if 'loopVideo' in kwargs and kwargs['loopVideo']:
self.loopValue = '-1'
else:
self.loopValue = '0'
self.command = [
self.ffmpeg,
'-thread_queue_size', '512',
'-r', str(self.frameRate),
'-stream_loop', self.loopValue,
'-i', self.videoPath,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',
'-filter_complex', '[0:v] scale=%s:%s' % scale(
self.scale, self.width, self.height, str),
'-vcodec', 'rawvideo', '-',
]
self.frameBuffer = PriorityQueue()
self.frameBuffer.maxsize = self.frameRate
self.finishedFrames = {}
self.thread = threading.Thread(
target=self.fillBuffer,
name='Video Frame-Fetcher'
)
self.thread.daemon = True
self.thread.start()
def frame(self, num):
while True:
if num in self.finishedFrames:
image = self.finishedFrames.pop(num)
return finalizeFrame(
self.component, image, self.width, self.height)
i, image = self.frameBuffer.get()
self.finishedFrames[i] = image
self.frameBuffer.task_done()
def fillBuffer(self):
self.pipe = openPipe(
self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, bufsize=10**8
)
while True:
if self.parent.canceled:
break
self.frameNo += 1
# If we run out of frames, use the last good frame and loop.
try:
if len(self.currentFrame) == 0:
self.frameBuffer.put((self.frameNo-1, self.lastFrame))
continue
except AttributeError:
Video.threadError = ComponentError(self.component, 'video')
break
self.currentFrame = self.pipe.stdout.read(self.chunkSize)
if len(self.currentFrame) != 0:
self.frameBuffer.put((self.frameNo, self.currentFrame))
self.lastFrame = self.currentFrame
from component import Component
from toolkit.frame import BlankFrame, scale
from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo
from toolkit import checkOutput
class Component(Component):
@ -106,30 +16,30 @@ class Component(Component):
def widget(self, *args):
self.videoPath = ''
self.badVideo = False
self.badAudio = False
self.x = 0
self.y = 0
self.loopVideo = False
super().widget(*args)
self._image = BlankFrame(self.width, self.height)
self.page.pushButton_video.clicked.connect(self.pickVideo)
self.trackWidgets(
{
'videoPath': self.page.lineEdit_video,
'loopVideo': self.page.checkBox_loop,
'useAudio': self.page.checkBox_useAudio,
'distort': self.page.checkBox_distort,
'scale': self.page.spinBox_scale,
'volume': self.page.spinBox_volume,
'xPosition': self.page.spinBox_x,
'yPosition': self.page.spinBox_y,
}, presetNames={
'videoPath': 'video',
'loopVideo': 'loop',
'xPosition': 'x',
'yPosition': 'y',
}
)
self.trackWidgets({
'videoPath': self.page.lineEdit_video,
'loopVideo': self.page.checkBox_loop,
'useAudio': self.page.checkBox_useAudio,
'distort': self.page.checkBox_distort,
'scale': self.page.spinBox_scale,
'volume': self.page.spinBox_volume,
'xPosition': self.page.spinBox_x,
'yPosition': self.page.spinBox_y,
}, presetNames={
'videoPath': 'video',
'loopVideo': 'loop',
'xPosition': 'x',
'yPosition': 'y',
}, relativeWidgets=[
'xPosition', 'yPosition',
])
def update(self):
if self.page.checkBox_useAudio.isChecked():
@ -157,8 +67,6 @@ class Component(Component):
if not self.videoPath:
self.lockError("There is no video selected.")
elif self.badVideo:
self.lockError("Could not identify an audio stream in this video.")
elif not os.path.exists(self.videoPath):
self.lockError("The video selected does not exist!")
elif os.path.realpath(self.videoPath) == os.path.realpath(outputFile):
@ -182,22 +90,21 @@ class Component(Component):
def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs)
self.updateChunksize()
self.video = Video(
ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath,
self.video = FfmpegVideo(
inputPath=self.videoPath, filter_=self.makeFfmpegFilter(),
width=self.width, height=self.height, chunkSize=self.chunkSize,
frameRate=int(self.settings.value("outputFrameRate")),
parent=self.parent, loopVideo=self.loopVideo,
component=self, scale=self.scale
component=self
) if os.path.exists(self.videoPath) else None
def frameRender(self, frameNo):
if Video.threadError is not None:
raise Video.threadError
return self.video.frame(frameNo)
if FfmpegVideo.threadError is not None:
raise FfmpegVideo.threadError
return self.finalizeFrame(self.video.frame(frameNo))
def postFrameRender(self):
self.video.pipe.stdout.close()
self.video.pipe.send_signal(signal.SIGINT)
closePipe(self.video.pipe)
def pickVideo(self):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
@ -220,23 +127,30 @@ class Component(Component):
'-i', self.videoPath,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',
'-filter_complex', '[0:v] scale=%s:%s' % scale(
self.scale, width, height, str),
'-vcodec', 'rawvideo', '-',
'-ss', '90',
'-vframes', '1',
]
command.extend(self.makeFfmpegFilter())
command.extend([
'-codec:v', 'rawvideo', '-',
'-ss', '90',
'-frames:v', '1',
])
pipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, bufsize=10**8
)
byteFrame = pipe.stdout.read(self.chunkSize)
pipe.stdout.close()
pipe.send_signal(signal.SIGINT)
closePipe(pipe)
frame = finalizeFrame(self, byteFrame, width, height)
frame = self.finalizeFrame(byteFrame)
return frame
def makeFfmpegFilter(self):
return [
'-filter_complex',
'[0:v] scale=%s:%s' % scale(
self.scale, self.width, self.height, str),
]
def updateChunksize(self):
if self.scale != 100 and not self.distort:
width, height = scale(self.scale, self.width, self.height, int)
@ -268,44 +182,27 @@ class Component(Component):
print('Load a video:\n path=/filepath/to/video.mp4')
print('Using audio:\n path=/filepath/to/video.mp4 audio')
def finalizeFrame(self, imageData):
try:
if self.distort:
image = Image.frombytes(
'RGBA',
(self.width, self.height),
imageData)
else:
image = Image.frombytes(
'RGBA',
scale(self.scale, self.width, self.height, int),
imageData)
self._image = image
except ValueError:
# use last good frame
image = self._image
def scale(scale, width, height, returntype=None):
width = (float(width) / 100.0) * float(scale)
height = (float(height) / 100.0) * float(scale)
if returntype == str:
return (str(math.ceil(width)), str(math.ceil(height)))
elif returntype == int:
return (math.ceil(width), math.ceil(height))
else:
return (width, height)
def finalizeFrame(self, imageData, width, height):
try:
if self.distort:
image = Image.frombytes(
'RGBA',
(width, height),
imageData)
if self.scale != 100 \
or self.xPosition != 0 or self.yPosition != 0:
frame = BlankFrame(self.width, self.height)
frame.paste(image, box=(self.xPosition, self.yPosition))
else:
image = Image.frombytes(
'RGBA',
scale(self.scale, width, height, int),
imageData)
except ValueError:
print(
'### BAD VIDEO SELECTED ###\n'
'Video will not export with these settings'
)
self.badVideo = True
return BlankFrame(width, height)
if self.scale != 100 \
or self.xPosition != 0 or self.yPosition != 0:
frame = BlankFrame(width, height)
frame.paste(image, box=(self.xPosition, self.yPosition))
else:
frame = image
self.badVideo = False
return frame
frame = image
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']:
widget = eval('loader.window.%s' % widget)
widget.blockSignals(True)
widget.setText(value)
toolkit.setWidgetValue(widget, value)
widget.blockSignals(False)
for key, value in data['Settings']:
@ -451,8 +451,8 @@ class Core:
'1280x720',
'854x480',
],
'windowHasFocus': False,
'FFMPEG_BIN': findFfmpeg(),
'windowHasFocus': False,
'canceled': False,
}
@ -492,7 +492,7 @@ class Core:
@classmethod
def loadDefaultSettings(cls):
defaultSettings = {
cls.defaultSettings = {
"outputWidth": 1280,
"outputHeight": 720,
"outputFrameRate": 30,
@ -506,9 +506,10 @@ class Core:
"outputContainer": "MP4",
"projectDir": os.path.join(cls.dataDir, 'projects'),
"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:
cls.settings.setValue(parm, value)

View File

@ -581,7 +581,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.showMessage(
msg=msg,
detail=detail,
icon='Warning',
icon='Critical',
)
def changeEncodingStatus(self, status):
@ -644,9 +644,12 @@ class MainWindow(QtWidgets.QMainWindow):
def updateResolution(self):
resIndex = int(self.window.comboBox_resolution.currentIndex())
res = Core.resolutions[resIndex].split('x')
changed = res[0] != self.settings.value("outputWidth")
self.settings.setValue('outputWidth', res[0])
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):
'''Use autosave keyword arg to force saving or not saving if needed'''
@ -791,6 +794,8 @@ class MainWindow(QtWidgets.QMainWindow):
field.blockSignals(True)
field.setText('')
field.blockSignals(False)
self.progressBarUpdated(0)
self.progressBarSetText('')
@disableWhenEncoding
def createNewProject(self, prompt=True):

View File

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

View File

@ -34,30 +34,28 @@ def appendUppercase(lst):
lst.append(form.upper())
return lst
def hideCmdWin(func):
''' Stops CMD window from appearing on Windows.
Adapted from here: http://code.activestate.com/recipes/409002/
'''
def decorator(commandList, **kwargs):
def pipeWrapper(func):
'''A decorator to insert proper kwargs into Popen objects.'''
def pipeWrapper(commandList, **kwargs):
if sys.platform == 'win32':
# Stop CMD window from appearing on Windows
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
kwargs['startupinfo'] = startupinfo
if 'bufsize' not in kwargs:
kwargs['bufsize'] = 10**8
if 'stdin' not in kwargs:
kwargs['stdin'] = subprocess.DEVNULL
return func(commandList, **kwargs)
return decorator
return pipeWrapper
@hideCmdWin
@pipeWrapper
def checkOutput(commandList, **kwargs):
return subprocess.check_output(commandList, **kwargs)
@hideCmdWin
def openPipe(commandList, **kwargs):
return subprocess.Popen(commandList, **kwargs)
def disableWhenEncoding(func):
def decorator(self, *args, **kwargs):
if self.encoding:
@ -76,25 +74,6 @@ def disableWhenOpeningProject(func):
return decorator
def pickColor():
'''
Use color picker to get color input from the user,
and return this as an RGB string and QPushButton stylesheet.
In a subclass apply stylesheet to any color selection widgets
'''
dialog = QtWidgets.QColorDialog()
dialog.setOption(QtWidgets.QColorDialog.ShowAlphaChannel, True)
color = dialog.getColor()
if color.isValid():
RGBstring = '%s,%s,%s' % (
str(color.red()), str(color.green()), str(color.blue()))
btnStyle = "QPushButton{background-color: %s; outline: none;}" \
% color.name()
return RGBstring, btnStyle
else:
return None, None
def rgbFromString(string):
'''Turns an RGB string like "255, 255, 255" into a tuple'''
try:
@ -115,3 +94,46 @@ def formatTraceback(tb=None):
import sys
tb = sys.exc_info()[2]
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 os
import subprocess
import threading
import signal
from queue import PriorityQueue
import core
from toolkit.common import checkOutput, openPipe
from toolkit.common import checkOutput, pipeWrapper
from component import ComponentError
class FfmpegVideo:
'''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():
@ -248,7 +372,12 @@ def getAudioDuration(filename):
except subprocess.CalledProcessError as ex:
fileInfo = ex.output
info = fileInfo.decode("utf-8").split('\n')
try:
info = fileInfo.decode("utf-8").split('\n')
except UnicodeDecodeError as e:
print('Unicode error:', str(e))
return False
for line in info:
if 'Duration' in line:
d = line.split(',')[0]
@ -321,3 +450,10 @@ def readAudioFile(filename, videoWorker):
completeAudioArray = completeAudioArrayCopy
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
import sys
import os
import math
import core
@ -41,6 +42,17 @@ class PaintColor(QtGui.QColor):
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):
'''Makes width/height arguments optional'''
def decorator(*args):

View File

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