From c1457b6dad4640b17679dd802e372bd46a13d2a5 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 29 Jul 2017 13:08:28 -0400 Subject: [PATCH 01/15] starting work on Waveform component split Video class out of Video component for reuse in Waveform --- .gitignore | 2 + src/component.py | 7 +- src/components/video.py | 196 +++++++------------------ src/components/waveform.py | 139 ++++++++++++++++++ src/components/waveform.ui | 283 +++++++++++++++++++++++++++++++++++++ src/toolkit/common.py | 37 +++-- src/toolkit/ffmpeg.py | 99 +++++++++++++ src/video_thread.py | 2 +- 8 files changed, 606 insertions(+), 159 deletions(-) create mode 100644 src/components/waveform.py create mode 100644 src/components/waveform.ui diff --git a/.gitignore b/.gitignore index bfdd0e7..7cec615 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ env/* *.tar.* *.exe ffmpeg +*.bak +*~ diff --git a/src/component.py b/src/component.py index 03023e7..fc8fbd3 100644 --- a/src/component.py +++ b/src/component.py @@ -197,7 +197,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): ''' Must call super() when subclassing Triggered only before a video is exported (video_thread.py) - self.worker = the video thread worker + self.audioFile = filepath to the main input audio file self.completeAudioArray = a list of audio samples self.sampleSize = number of audio samples per video frame self.progressBarUpdate = signal to set progress bar number @@ -436,7 +436,7 @@ class ComponentError(RuntimeError): import sys if sys.exc_info()[0] is not None: string = ( - "%s component's %s encountered %s %s." % ( + "%s component's %s encountered %s %s: %s" % ( caller.__class__.name, name, 'an' if any([ @@ -444,12 +444,13 @@ class ComponentError(RuntimeError): for vowel in ('A', 'I') ]) else 'a', sys.exc_info()[0].__name__, + str(sys.exc_info()[1]) ) ) detail = formatTraceback(sys.exc_info()[2]) else: string = name - detail = "Methods:\n%s" % ( + detail = "Attributes:\n%s" % ( "\n".join( [m for m in dir(caller) if not m.startswith('_')] ) diff --git a/src/components/video.py b/src/components/video.py index b2487c1..d3460ff 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -1,103 +1,13 @@ -from PIL import Image, ImageDraw +from PIL import Image from PyQt5 import QtGui, QtCore, QtWidgets import os import math import subprocess -import signal -import threading -from queue import PriorityQueue from component import Component, ComponentError from toolkit.frame import BlankFrame -from toolkit.ffmpeg import testAudioStream -from toolkit import openPipe, checkOutput - - -class Video: - '''Opens a pipe to ffmpeg and stores a buffer of raw video frames.''' - - # error from the thread used to fill the buffer - threadError = None - - def __init__(self, **kwargs): - mandatoryArgs = [ - 'ffmpeg', # path to ffmpeg, usually self.core.FFMPEG_BIN - 'videoPath', - 'width', - 'height', - 'scale', # percentage scale - 'frameRate', # frames per second - 'chunkSize', # number of bytes in one frame - 'parent', # mainwindow object - 'component', # component object - ] - for arg in mandatoryArgs: - setattr(self, arg, kwargs[arg]) - - self.frameNo = -1 - self.currentFrame = 'None' - if 'loopVideo' in kwargs and kwargs['loopVideo']: - self.loopValue = '-1' - else: - self.loopValue = '0' - self.command = [ - self.ffmpeg, - '-thread_queue_size', '512', - '-r', str(self.frameRate), - '-stream_loop', self.loopValue, - '-i', self.videoPath, - '-f', 'image2pipe', - '-pix_fmt', 'rgba', - '-filter_complex', '[0:v] scale=%s:%s' % scale( - self.scale, self.width, self.height, str), - '-vcodec', 'rawvideo', '-', - ] - - self.frameBuffer = PriorityQueue() - self.frameBuffer.maxsize = self.frameRate - self.finishedFrames = {} - - self.thread = threading.Thread( - target=self.fillBuffer, - name='Video Frame-Fetcher' - ) - self.thread.daemon = True - self.thread.start() - - def frame(self, num): - while True: - if num in self.finishedFrames: - image = self.finishedFrames.pop(num) - return finalizeFrame( - self.component, image, self.width, self.height) - - i, image = self.frameBuffer.get() - self.finishedFrames[i] = image - self.frameBuffer.task_done() - - def fillBuffer(self): - self.pipe = openPipe( - self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, bufsize=10**8 - ) - while True: - if self.parent.canceled: - break - self.frameNo += 1 - - # If we run out of frames, use the last good frame and loop. - try: - if len(self.currentFrame) == 0: - self.frameBuffer.put((self.frameNo-1, self.lastFrame)) - continue - except AttributeError: - Video.threadError = ComponentError(self.component, 'video') - break - - self.currentFrame = self.pipe.stdout.read(self.chunkSize) - if len(self.currentFrame) != 0: - self.frameBuffer.put((self.frameNo, self.currentFrame)) - self.lastFrame = self.currentFrame +from toolkit.ffmpeg import testAudioStream, FfmpegVideo +from toolkit import openPipe, closePipe, checkOutput, scale class Component(Component): @@ -182,22 +92,21 @@ class Component(Component): def preFrameRender(self, **kwargs): super().preFrameRender(**kwargs) self.updateChunksize() - self.video = Video( - ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath, + self.video = FfmpegVideo( + inputPath=self.videoPath, filter_=self.makeFfmpegFilter(), width=self.width, height=self.height, chunkSize=self.chunkSize, frameRate=int(self.settings.value("outputFrameRate")), parent=self.parent, loopVideo=self.loopVideo, - component=self, scale=self.scale + component=self ) if os.path.exists(self.videoPath) else None def frameRender(self, frameNo): - if Video.threadError is not None: - raise Video.threadError - return self.video.frame(frameNo) + if FfmpegVideo.threadError is not None: + raise FfmpegVideo.threadError + return self.finalizeFrame(self.video.frame(frameNo)) def postFrameRender(self): - self.video.pipe.stdout.close() - self.video.pipe.send_signal(signal.SIGINT) + closePipe(self.video.pipe) def pickVideo(self): imgDir = self.settings.value("componentDir", os.path.expanduser("~")) @@ -220,23 +129,30 @@ class Component(Component): '-i', self.videoPath, '-f', 'image2pipe', '-pix_fmt', 'rgba', - '-filter_complex', '[0:v] scale=%s:%s' % scale( - self.scale, width, height, str), + ] + command.extend(self.makeFfmpegFilter()) + command.extend([ '-vcodec', 'rawvideo', '-', '-ss', '90', - '-vframes', '1', - ] + '-frames:v', '1', + ]) pipe = openPipe( command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8 ) byteFrame = pipe.stdout.read(self.chunkSize) - pipe.stdout.close() - pipe.send_signal(signal.SIGINT) + closePipe(pipe) - frame = finalizeFrame(self, byteFrame, width, height) + frame = self.finalizeFrame(byteFrame) return frame + def makeFfmpegFilter(self): + return [ + '-filter_complex', + '[0:v] scale=%s:%s' % scale( + self.scale, self.width, self.height, str), + ] + def updateChunksize(self): if self.scale != 100 and not self.distort: width, height = scale(self.scale, self.width, self.height, int) @@ -268,44 +184,32 @@ class Component(Component): print('Load a video:\n path=/filepath/to/video.mp4') print('Using audio:\n path=/filepath/to/video.mp4 audio') + def finalizeFrame(self, imageData): + try: + if self.distort: + image = Image.frombytes( + 'RGBA', + (self.width, self.height), + imageData) + else: + image = Image.frombytes( + 'RGBA', + scale(self.scale, self.width, self.height, int), + imageData) -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) + except ValueError: + print( + '### BAD VIDEO SELECTED ###\n' + 'Video will not export with these settings' + ) + self.badVideo = True + return BlankFrame(self.width, self.height) - -def finalizeFrame(self, imageData, width, height): - try: - if self.distort: - image = Image.frombytes( - 'RGBA', - (width, height), - imageData) + if self.scale != 100 \ + or self.xPosition != 0 or self.yPosition != 0: + frame = BlankFrame(self.width, self.height) + frame.paste(image, box=(self.xPosition, self.yPosition)) else: - image = Image.frombytes( - 'RGBA', - scale(self.scale, width, height, int), - imageData) - - except ValueError: - print( - '### BAD VIDEO SELECTED ###\n' - 'Video will not export with these settings' - ) - self.badVideo = True - return BlankFrame(width, height) - - if self.scale != 100 \ - or self.xPosition != 0 or self.yPosition != 0: - frame = BlankFrame(width, height) - frame.paste(image, box=(self.xPosition, self.yPosition)) - else: - frame = image - self.badVideo = False - return frame + frame = image + self.badVideo = False + return frame diff --git a/src/components/waveform.py b/src/components/waveform.py new file mode 100644 index 0000000..487a3bb --- /dev/null +++ b/src/components/waveform.py @@ -0,0 +1,139 @@ +from PIL import Image +from PyQt5 import QtGui, QtCore, QtWidgets +from PyQt5.QtGui import QColor +import os +import math +import subprocess + +from component import Component, ComponentError +from toolkit.frame import BlankFrame +from toolkit import openPipe, checkOutput, rgbFromString +from toolkit.ffmpeg import FfmpegVideo + + +class Component(Component): + name = 'Waveform' + version = '1.0.0' + + def widget(self, *args): + self.color = (255, 255, 255) + super().widget(*args) + + self.page.lineEdit_color.setText('%s,%s,%s' % self.color) + btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + % QColor(*self.color1).name() + self.page.lineEdit_color.setStylesheet(btnStyle) + self.page.pushButton_color.clicked.connect(lambda: self.pickColor()) + + self.trackWidgets( + { + 'mode': self.page.comboBox_mode, + 'x': self.page.spinBox_x, + 'y': self.page.spinBox_y, + 'mirror': self.page.checkBox_mirror, + 'scale': self.page.spinBox_scale, + } + ) + + def update(self): + self.color = rgbFromString(self.page.lineEdit_color.text()) + btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + % QColor(*self.color).name() + self.page.pushButton_color.setStyleSheet(btnStyle) + super().update() + + def previewRender(self): + self.updateChunksize() + frame = self.getPreviewFrame(self.width, self.height) + if not frame: + return BlankFrame(self.width, self.height) + else: + return frame + + def preFrameRender(self, **kwargs): + super().preFrameRender(**kwargs) + self.updateChunksize() + self.video = FfmpegVideo( + inputPath=self.audioFile, + filter_=makeFfmpegFilter(), + width=self.width, height=self.height, + chunkSize=self.chunkSize, + frameRate=int(self.settings.value("outputFrameRate")), + parent=self.parent, component=self, + ) + + def frameRender(self, frameNo): + if FfmpegVideo.threadError is not None: + raise FfmpegVideo.threadError + return finalizeFrame(self.video.frame(frameNo)) + + def postFrameRender(self): + closePipe(self.video.pipe) + + def getPreviewFrame(self, width, height): + inputFile = self.parent.window.lineEdit_audioFile.text() + if not inputFile or not os.path.exists(inputFile): + return + + command = [ + self.core.FFMPEG_BIN, + '-thread_queue_size', '512', + '-i', inputFile, + '-f', 'image2pipe', + '-pix_fmt', 'rgba', + ] + command.extend(self.makeFfmpegFilter()) + command.extend([ + '-vcodec', 'rawvideo', '-', + '-ss', '90', + '-frames:v', '1', + ]) + pipe = openPipe( + command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, bufsize=10**8 + ) + byteFrame = pipe.stdout.read(self.chunkSize) + closePipe(pipe) + + frame = finalizeFrame(self, byteFrame, width, height) + return frame + + def makeFfmpegFilter(self): + w, h = scale(self.scale, self.width, self.height, str) + return [ + '-filter_complex', + '[0:a] showwaves=s=%sx%s:mode=%s,format=rgba [v]' % ( + w, h, self.mode, + ), + '-map', '[v]', + '-map', '0:a', + ] + + def updateChunksize(self): + if self.scale != 100: + width, height = scale(self.scale, self.width, self.height, int) + else: + width, height = self.width, self.height + self.chunkSize = 4 * width * height + + +def scale(scale, width, height, returntype=None): + width = (float(width) / 100.0) * float(scale) + height = (float(height) / 100.0) * float(scale) + if returntype == str: + return (str(math.ceil(width)), str(math.ceil(height))) + elif returntype == int: + return (math.ceil(width), math.ceil(height)) + else: + return (width, height) + + +def finalizeFrame(self, imageData, width, height): + # frombytes goes here + if self.scale != 100 \ + or self.x != 0 or self.y != 0: + frame = BlankFrame(width, height) + frame.paste(image, box=(self.x, self.y)) + else: + frame = image + return frame diff --git a/src/components/waveform.ui b/src/components/waveform.ui new file mode 100644 index 0000000..5d62150 --- /dev/null +++ b/src/components/waveform.ui @@ -0,0 +1,283 @@ + + + Form + + + + 0 + 0 + 586 + 197 + + + + + 0 + 0 + + + + + 0 + 197 + + + + Form + + + + + + 4 + + + + + + + + 0 + 0 + + + + + 31 + 0 + + + + Mode + + + + + + + + Cline + + + + + Line + + + + + P2p + + + + + Point + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + + 0 + 0 + + + + X + + + + + + + + 0 + 0 + + + + + 80 + 16777215 + + + + -10000 + + + 10000 + + + + + + + + 0 + 0 + + + + Y + + + + + + + + 0 + 0 + + + + + 80 + 16777215 + + + + + 0 + 0 + + + + -10000 + + + 10000 + + + 0 + + + + + + + + + + + + + Wave Color + + + + + + + + + + + 0 + 0 + + + + + 32 + 32 + + + + + + + false + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Mirror + + + + + + + Scale + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + QAbstractSpinBox::UpDownArrows + + + % + + + 10 + + + 400 + + + 100 + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/toolkit/common.py b/src/toolkit/common.py index 251a2c1..128ed08 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py @@ -6,9 +6,22 @@ import string import os import sys import subprocess +import signal +import math from collections import OrderedDict +def scale(scale, width, height, returntype=None): + width = (float(width) / 100.0) * float(scale) + height = (float(height) / 100.0) * float(scale) + if returntype == str: + return (str(math.ceil(width)), str(math.ceil(height))) + elif returntype == int: + return (math.ceil(width), math.ceil(height)) + else: + return (width, height) + + def badName(name): '''Returns whether a name contains non-alphanumeric chars''' return any([letter in string.punctuation for letter in name]) @@ -34,29 +47,35 @@ def appendUppercase(lst): lst.append(form.upper()) return lst - -def hideCmdWin(func): - ''' Stops CMD window from appearing on Windows. - Adapted from here: http://code.activestate.com/recipes/409002/ - ''' - def decorator(commandList, **kwargs): +def pipeWrapper(func): + '''A decorator to insert proper kwargs into Popen objects.''' + def pipeWrapper(commandList, **kwargs): if sys.platform == 'win32': + # Stop CMD window from appearing on Windows startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW kwargs['startupinfo'] = startupinfo + + if 'bufsize' not in kwargs: + kwargs['bufsize'] = 10**8 + if 'stdin' not in kwargs: + kwargs['stdin'] = subprocess.DEVNULL return func(commandList, **kwargs) - return decorator + return pipeWrapper -@hideCmdWin +@pipeWrapper def checkOutput(commandList, **kwargs): return subprocess.check_output(commandList, **kwargs) -@hideCmdWin +@pipeWrapper def openPipe(commandList, **kwargs): return subprocess.Popen(commandList, **kwargs) +def closePipe(pipe): + pipe.stdout.close() + pipe.send_signal(signal.SIGINT) def disableWhenEncoding(func): def decorator(self, *args, **kwargs): diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index b8bc679..fea9d4e 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -5,11 +5,110 @@ import numpy import sys import os import subprocess +import threading +from queue import PriorityQueue import core from toolkit.common import checkOutput, openPipe +class FfmpegVideo: + '''Opens a pipe to ffmpeg and stores a buffer of raw video frames.''' + + # error from the thread used to fill the buffer + threadError = None + + def __init__(self, **kwargs): + mandatoryArgs = [ + 'inputPath', + 'filter_', + 'width', + 'height', + 'frameRate', # frames per second + 'chunkSize', # number of bytes in one frame + 'parent', # mainwindow object + 'component', # component object + ] + for arg in mandatoryArgs: + setattr(self, arg, kwargs[arg]) + + self.frameNo = -1 + self.currentFrame = 'None' + self.map_ = None + + if 'loopVideo' in kwargs and kwargs['loopVideo']: + self.loopValue = '-1' + else: + self.loopValue = '0' + if 'filter_' in kwargs: + if kwargs['filter_'][0] != '-filter_complex': + kwargs['filter_'].insert(0, '-filter_complex') + else: + kwargs['filter_'] = None + + self.command = [ + core.Core.FFMPEG_BIN, + '-thread_queue_size', '512', + '-r', str(self.frameRate), + '-stream_loop', self.loopValue, + '-i', self.inputPath, + '-f', 'image2pipe', + '-pix_fmt', 'rgba', + ] + if type(kwargs['filter_']) is list: + self.command.extend( + kwargs['filter_'] + ) + self.command.extend([ + '-vcodec', 'rawvideo', '-', + ]) + + self.frameBuffer = PriorityQueue() + self.frameBuffer.maxsize = self.frameRate + self.finishedFrames = {} + + self.thread = threading.Thread( + target=self.fillBuffer, + name='FFmpeg Frame-Fetcher' + ) + self.thread.daemon = True + self.thread.start() + + def frame(self, num): + while True: + if num in self.finishedFrames: + image = self.finishedFrames.pop(num) + return image + + i, image = self.frameBuffer.get() + self.finishedFrames[i] = image + self.frameBuffer.task_done() + + def fillBuffer(self): + self.pipe = openPipe( + self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, bufsize=10**8 + ) + while True: + if self.parent.canceled: + break + self.frameNo += 1 + + # If we run out of frames, use the last good frame and loop. + try: + if len(self.currentFrame) == 0: + self.frameBuffer.put((self.frameNo-1, self.lastFrame)) + continue + except AttributeError: + Video.threadError = ComponentError(self.component, 'video') + break + + self.currentFrame = self.pipe.stdout.read(self.chunkSize) + if len(self.currentFrame) != 0: + self.frameBuffer.put((self.frameNo, self.currentFrame)) + self.lastFrame = self.currentFrame + + def findFfmpeg(): if getattr(sys, 'frozen', False): # The application is frozen diff --git a/src/video_thread.py b/src/video_thread.py index 32e8a38..f27ec21 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -153,7 +153,7 @@ class Worker(QtCore.QObject): for compNo, comp in enumerate(reversed(self.components)): try: comp.preFrameRender( - worker=self, + audioFile=self.inputFile, completeAudioArray=self.completeAudioArray, sampleSize=self.sampleSize, progressBarUpdate=self.progressBarUpdate, From 1297af61c9ce00b6dd76f8ec690baedf5bf887c7 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 29 Jul 2017 20:27:46 -0400 Subject: [PATCH 02/15] waveform component is working, preview is glitchy --- src/components/original.py | 3 + src/components/video.py | 10 +-- src/components/waveform.py | 134 +++++++++++++++++++++++++------------ src/components/waveform.ui | 95 +++++++++++++++++++++++++- src/toolkit/common.py | 21 ------ src/toolkit/ffmpeg.py | 30 +++++++-- src/toolkit/frame.py | 12 ++++ src/video_thread.py | 38 +++++++---- 8 files changed, 256 insertions(+), 87 deletions(-) diff --git a/src/components/original.py b/src/components/original.py index 3d1a574..621af6f 100644 --- a/src/components/original.py +++ b/src/components/original.py @@ -18,6 +18,9 @@ class Component(Component): def names(*args): return ['Original Audio Visualization'] + def properties(self): + return ['pcm'] + def widget(self, *args): self.visColor = (255, 255, 255) self.scale = 20 diff --git a/src/components/video.py b/src/components/video.py index d3460ff..6cd16e5 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -4,10 +4,10 @@ import os import math import subprocess -from component import Component, ComponentError -from toolkit.frame import BlankFrame -from toolkit.ffmpeg import testAudioStream, FfmpegVideo -from toolkit import openPipe, closePipe, checkOutput, scale +from component import Component +from toolkit.frame import BlankFrame, scale +from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo +from toolkit import checkOutput class Component(Component): @@ -132,7 +132,7 @@ class Component(Component): ] command.extend(self.makeFfmpegFilter()) command.extend([ - '-vcodec', 'rawvideo', '-', + '-codec:v', 'rawvideo', '-', '-ss', '90', '-frames:v', '1', ]) diff --git a/src/components/waveform.py b/src/components/waveform.py index 487a3bb..375b3fc 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -5,10 +5,10 @@ import os import math import subprocess -from component import Component, ComponentError -from toolkit.frame import BlankFrame -from toolkit import openPipe, checkOutput, rgbFromString -from toolkit.ffmpeg import FfmpegVideo +from component import Component +from toolkit.frame import BlankFrame, scale +from toolkit import checkOutput, rgbFromString, pickColor +from toolkit.ffmpeg import openPipe, closePipe, getAudioDuration, FfmpegVideo class Component(Component): @@ -21,17 +21,27 @@ class Component(Component): self.page.lineEdit_color.setText('%s,%s,%s' % self.color) btnStyle = "QPushButton { background-color : %s; outline: none; }" \ - % QColor(*self.color1).name() - self.page.lineEdit_color.setStylesheet(btnStyle) + % QColor(*self.color).name() + self.page.pushButton_color.setStyleSheet(btnStyle) self.page.pushButton_color.clicked.connect(lambda: self.pickColor()) + self.page.spinBox_scale.valueChanged.connect(self.updateChunksize) + + if hasattr(self.parent, 'window'): + self.parent.window.lineEdit_audioFile.textChanged.connect( + self.update + ) self.trackWidgets( { 'mode': self.page.comboBox_mode, + 'amplitude': self.page.comboBox_amplitude, 'x': self.page.spinBox_x, 'y': self.page.spinBox_y, 'mirror': self.page.checkBox_mirror, 'scale': self.page.spinBox_scale, + 'opacity': self.page.spinBox_opacity, + 'compress': self.page.checkBox_compress, + 'mono': self.page.checkBox_mono, } ) @@ -42,6 +52,26 @@ class Component(Component): self.page.pushButton_color.setStyleSheet(btnStyle) super().update() + def loadPreset(self, pr, *args): + super().loadPreset(pr, *args) + + self.page.lineEdit_color.setText('%s,%s,%s' % pr['color']) + btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + % QColor(*pr['color']).name() + self.page.pushButton_color.setStyleSheet(btnStyle) + + def savePreset(self): + saveValueStore = super().savePreset() + saveValueStore['color'] = self.color + return saveValueStore + + def pickColor(self): + RGBstring, btnStyle = pickColor() + if not RGBstring: + return + self.page.lineEdit_color.setText(RGBstring) + self.page.pushButton_color.setStyleSheet(btnStyle) + def previewRender(self): self.updateChunksize() frame = self.getPreviewFrame(self.width, self.height) @@ -53,10 +83,11 @@ class Component(Component): def preFrameRender(self, **kwargs): super().preFrameRender(**kwargs) self.updateChunksize() + w, h = scale(self.scale, self.width, self.height, str) self.video = FfmpegVideo( inputPath=self.audioFile, - filter_=makeFfmpegFilter(), - width=self.width, height=self.height, + filter_=self.makeFfmpegFilter(), + width=w, height=h, chunkSize=self.chunkSize, frameRate=int(self.settings.value("outputFrameRate")), parent=self.parent, component=self, @@ -65,7 +96,7 @@ class Component(Component): def frameRender(self, frameNo): if FfmpegVideo.threadError is not None: raise FfmpegVideo.threadError - return finalizeFrame(self.video.frame(frameNo)) + return self.finalizeFrame(self.video.frame(frameNo)) def postFrameRender(self): closePipe(self.video.pipe) @@ -74,18 +105,25 @@ class Component(Component): inputFile = self.parent.window.lineEdit_audioFile.text() if not inputFile or not os.path.exists(inputFile): return + duration = getAudioDuration(inputFile) + if not duration: + return + startPt = duration / 3 command = [ self.core.FFMPEG_BIN, '-thread_queue_size', '512', + '-r', self.settings.value("outputFrameRate"), + '-ss', "{0:.3f}".format(startPt), '-i', inputFile, '-f', 'image2pipe', '-pix_fmt', 'rgba', ] - command.extend(self.makeFfmpegFilter()) + command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt)) command.extend([ - '-vcodec', 'rawvideo', '-', - '-ss', '90', + '-an', + '-s:v', '%sx%s' % scale(self.scale, self.width, self.height, str), + '-codec:v', 'rawvideo', '-', '-frames:v', '1', ]) pipe = openPipe( @@ -95,45 +133,57 @@ class Component(Component): byteFrame = pipe.stdout.read(self.chunkSize) closePipe(pipe) - frame = finalizeFrame(self, byteFrame, width, height) + frame = self.finalizeFrame(byteFrame) return frame - def makeFfmpegFilter(self): + def makeFfmpegFilter(self, preview=False, startPt=0): w, h = scale(self.scale, self.width, self.height, str) + if self.amplitude == 0: + amplitude = 'lin' + elif self.amplitude == 1: + amplitude = 'log' + elif self.amplitude == 2: + amplitude = 'sqrt' + elif self.amplitude == 3: + amplitude = 'cbrt' + hexcolor = QColor(*self.color).name() + opacity = "{0:.1f}".format(self.opacity / 100) + return [ '-filter_complex', - '[0:a] showwaves=s=%sx%s:mode=%s,format=rgba [v]' % ( - w, h, self.mode, + '[0:a] %s%s' + 'showwaves=r=30:s=%sx%s:mode=%s:colors=%s@%s:scale=%s%s%s [v1]; ' + '[v1] scale=%s:%s%s [v]' % ( + 'compand=gain=2,' if self.compress else '', + 'aformat=channel_layouts=mono,' if self.mono else '', + self.settings.value("outputWidth"), + self.settings.value("outputHeight"), + str(self.page.comboBox_mode.currentText()).lower(), + hexcolor, opacity, amplitude, + ', drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=4:color=%s@%s' % ( + hexcolor, opacity + ) if self.mode < 2 else '', + ', hflip' if self.mirror else'', + w, h, + ', trim=duration=%s' % "{0:.3f}".format(startPt + 1) if preview else '', ), '-map', '[v]', - '-map', '0:a', ] def updateChunksize(self): - if self.scale != 100: - width, height = scale(self.scale, self.width, self.height, int) - else: - width, height = self.width, self.height + width, height = scale(self.scale, self.width, self.height, int) self.chunkSize = 4 * width * height - -def scale(scale, width, height, returntype=None): - width = (float(width) / 100.0) * float(scale) - height = (float(height) / 100.0) * float(scale) - if returntype == str: - return (str(math.ceil(width)), str(math.ceil(height))) - elif returntype == int: - return (math.ceil(width), math.ceil(height)) - else: - return (width, height) - - -def finalizeFrame(self, imageData, width, height): - # frombytes goes here - if self.scale != 100 \ - or self.x != 0 or self.y != 0: - frame = BlankFrame(width, height) - frame.paste(image, box=(self.x, self.y)) - else: - frame = image - return frame + def finalizeFrame(self, imageData): + image = Image.frombytes( + 'RGBA', + scale(self.scale, self.width, self.height, int), + imageData + ) + if self.scale != 100 \ + or self.x != 0 or self.y != 0: + frame = BlankFrame(self.width, self.height) + frame.paste(image, box=(self.x, self.y)) + else: + frame = image + return frame diff --git a/src/components/waveform.ui b/src/components/waveform.ui index 5d62150..0e40380 100644 --- a/src/components/waveform.ui +++ b/src/components/waveform.ui @@ -226,9 +226,31 @@ - + - Mirror + Opacity + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + QAbstractSpinBox::UpDownArrows + + + % + + + 10 + + + 400 + + + 100 @@ -263,6 +285,75 @@ + + + + + + Compress + + + + + + + Mono + + + + + + + Mirror + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Amplitude + + + + + + + + Linear + + + + + Logarithmic + + + + + Square root + + + + + Cubic root + + + + + + diff --git a/src/toolkit/common.py b/src/toolkit/common.py index 128ed08..5d424e0 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py @@ -6,22 +6,9 @@ import string import os import sys import subprocess -import signal -import math from collections import OrderedDict -def scale(scale, width, height, returntype=None): - width = (float(width) / 100.0) * float(scale) - height = (float(height) / 100.0) * float(scale) - if returntype == str: - return (str(math.ceil(width)), str(math.ceil(height))) - elif returntype == int: - return (math.ceil(width), math.ceil(height)) - else: - return (width, height) - - def badName(name): '''Returns whether a name contains non-alphanumeric chars''' return any([letter in string.punctuation for letter in name]) @@ -69,14 +56,6 @@ def checkOutput(commandList, **kwargs): return subprocess.check_output(commandList, **kwargs) -@pipeWrapper -def openPipe(commandList, **kwargs): - return subprocess.Popen(commandList, **kwargs) - -def closePipe(pipe): - pipe.stdout.close() - pipe.send_signal(signal.SIGINT) - def disableWhenEncoding(func): def decorator(self, *args, **kwargs): if self.encoding: diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index fea9d4e..e37282f 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -6,10 +6,12 @@ import sys import os import subprocess import threading +import signal from queue import PriorityQueue import core -from toolkit.common import checkOutput, openPipe +from toolkit.common import checkOutput, pipeWrapper +from component import ComponentError class FfmpegVideo: @@ -60,7 +62,8 @@ class FfmpegVideo: kwargs['filter_'] ) self.command.extend([ - '-vcodec', 'rawvideo', '-', + '-s:v', '%sx%s' % (self.width, self.height), + '-codec:v', 'rawvideo', '-', ]) self.frameBuffer = PriorityQueue() @@ -85,9 +88,11 @@ class FfmpegVideo: self.frameBuffer.task_done() def fillBuffer(self): + import sys + print(self.command) self.pipe = openPipe( self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, bufsize=10**8 + stderr=sys.__stdout__, bufsize=10**8 ) while True: if self.parent.canceled: @@ -100,7 +105,7 @@ class FfmpegVideo: self.frameBuffer.put((self.frameNo-1, self.lastFrame)) continue except AttributeError: - Video.threadError = ComponentError(self.component, 'video') + FfmpegVideo.threadError = ComponentError(self.component, 'video') break self.currentFrame = self.pipe.stdout.read(self.chunkSize) @@ -109,6 +114,16 @@ class FfmpegVideo: self.lastFrame = self.currentFrame +@pipeWrapper +def openPipe(commandList, **kwargs): + return subprocess.Popen(commandList, **kwargs) + + +def closePipe(pipe): + pipe.stdout.close() + pipe.send_signal(signal.SIGINT) + + def findFfmpeg(): if getattr(sys, 'frozen', False): # The application is frozen @@ -347,7 +362,12 @@ def getAudioDuration(filename): except subprocess.CalledProcessError as ex: fileInfo = ex.output - info = fileInfo.decode("utf-8").split('\n') + try: + info = fileInfo.decode("utf-8").split('\n') + except UnicodeDecodeError as e: + print('Unicode error:', str(e)) + return False + for line in info: if 'Duration' in line: d = line.split(',')[0] diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index b66e037..f42d4c9 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -6,6 +6,7 @@ from PIL import Image from PIL.ImageQt import ImageQt import sys import os +import math import core @@ -41,6 +42,17 @@ class PaintColor(QtGui.QColor): super().__init__(b, g, r, a) +def scale(scale, width, height, returntype=None): + width = (float(width) / 100.0) * float(scale) + height = (float(height) / 100.0) * float(scale) + if returntype == str: + return (str(math.ceil(width)), str(math.ceil(height))) + elif returntype == int: + return (math.ceil(width), math.ceil(height)) + else: + return (width, height) + + def defaultSize(framefunc): '''Makes width/height arguments optional''' def decorator(*args): diff --git a/src/video_thread.py b/src/video_thread.py index f27ec21..5963def 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -19,9 +19,11 @@ import time import signal from component import ComponentError -from toolkit import openPipe -from toolkit.ffmpeg import readAudioFile, createFfmpegCommand from toolkit.frame import Checkerboard +from toolkit.ffmpeg import ( + openPipe, readAudioFile, + getAudioDuration, createFfmpegCommand +) class Worker(QtCore.QObject): @@ -132,15 +134,24 @@ class Worker(QtCore.QObject): # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # READ AUDIO, INITIALIZE COMPONENTS, OPEN A PIPE TO FFMPEG # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ - - self.progressBarSetText.emit("Loading audio file...") - audioFileTraits = readAudioFile( - self.inputFile, self - ) - if audioFileTraits is None: - self.cancelExport() - return - self.completeAudioArray, duration = audioFileTraits + if any([ + True if 'pcm' in comp.properties() else False + for comp in self.components + ]): + self.progressBarSetText.emit("Loading audio file...") + audioFileTraits = readAudioFile( + self.inputFile, self + ) + if audioFileTraits is None: + self.cancelExport() + return + self.completeAudioArray, duration = audioFileTraits + else: + duration = getAudioDuration(self.inputFile) + class FakeList: + def __len__(self): + return int((duration * 44100) + 44100) - 1470 + self.completeAudioArray = FakeList() self.progressBarUpdate.emit(0) self.progressBarSetText.emit("Starting components...") @@ -284,7 +295,10 @@ class Worker(QtCore.QObject): numpy.seterr(all='print') - self.out_pipe.stdin.close() + try: + self.out_pipe.stdin.close() + except BrokenPipeError: + print('Broken pipe to ffmpeg!') if self.out_pipe.stderr is not None: print(self.out_pipe.stderr.read()) self.out_pipe.stderr.close() From db1ea1fc4edf19589e82171d48c417742c61c74b Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 29 Jul 2017 23:45:37 -0400 Subject: [PATCH 03/15] generic preview sound for waveform component with secret preference to use the audio file again --- src/component.py | 2 +- src/components/waveform.py | 38 +++++++++++++++++++++++++------------- src/core.py | 1 + src/mainwindow.py | 2 ++ src/toolkit/ffmpeg.py | 14 ++++++++++---- 5 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/component.py b/src/component.py index fc8fbd3..6d49406 100644 --- a/src/component.py +++ b/src/component.py @@ -427,7 +427,7 @@ class ComponentError(RuntimeError): ComponentError.prevErrors.insert(0, name) curTime = time.time() 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 ComponentError.lastTime = time.time() diff --git a/src/components/waveform.py b/src/components/waveform.py index 375b3fc..b4b19e9 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -90,7 +90,7 @@ class Component(Component): width=w, height=h, chunkSize=self.chunkSize, frameRate=int(self.settings.value("outputFrameRate")), - parent=self.parent, component=self, + parent=self.parent, component=self, debug=True, ) def frameRender(self, frameNo): @@ -102,20 +102,25 @@ class Component(Component): closePipe(self.video.pipe) def getPreviewFrame(self, width, height): - inputFile = self.parent.window.lineEdit_audioFile.text() - if not inputFile or not os.path.exists(inputFile): - return - duration = getAudioDuration(inputFile) - if not duration: - return - startPt = duration / 3 + 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', inputFile, + '-i', + os.path.join(self.core.wd, 'background.png') + if genericPreview else inputFile, '-f', 'image2pipe', '-pix_fmt', 'rgba', ] @@ -148,13 +153,19 @@ class Component(Component): amplitude = 'cbrt' hexcolor = QColor(*self.color).name() opacity = "{0:.1f}".format(self.opacity / 100) + genericPreview = self.settings.value("pref_genericPreview") return [ '-filter_complex', - '[0:a] %s%s' + '%s%s%s' 'showwaves=r=30:s=%sx%s:mode=%s:colors=%s@%s:scale=%s%s%s [v1]; ' - '[v1] scale=%s:%s%s [v]' % ( - 'compand=gain=2,' if self.compress else '', + '[v1] scale=%s:%s%s,setpts=2.0*PTS [v]' % ( + 'aevalsrc=sin(1*2*PI*t)*sin(880*2*PI*t),' + if preview and genericPreview else '[0:a] ', + 'compand=.3|.3:1|1:-90/-60|-60/-40|-40/-30|-20/-20:6:0:-90:0.2' + ',' if self.compress and not preview else ( + 'compand=gain=5,' if self.compress else '' + ), 'aformat=channel_layouts=mono,' if self.mono else '', self.settings.value("outputWidth"), self.settings.value("outputHeight"), @@ -165,7 +176,8 @@ class Component(Component): ) if self.mode < 2 else '', ', hflip' if self.mirror else'', w, h, - ', trim=duration=%s' % "{0:.3f}".format(startPt + 1) if preview else '', + ', trim=duration=%s' % "{0:.3f}".format(startPt + 1) + if preview else '', ), '-map', '[v]', ] diff --git a/src/core.py b/src/core.py index 1c29774..24bf097 100644 --- a/src/core.py +++ b/src/core.py @@ -506,6 +506,7 @@ class Core: "outputContainer": "MP4", "projectDir": os.path.join(cls.dataDir, 'projects'), "pref_insertCompAtTop": True, + "pref_genericPreview": True, } for parm, value in defaultSettings.items(): diff --git a/src/mainwindow.py b/src/mainwindow.py index 070131c..a97081e 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -791,6 +791,8 @@ class MainWindow(QtWidgets.QMainWindow): field.blockSignals(True) field.setText('') field.blockSignals(False) + self.progressBarUpdated(0) + self.progressBarSetText('') @disableWhenEncoding def createNewProject(self, prompt=True): diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index e37282f..4ea2863 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -37,6 +37,7 @@ class FfmpegVideo: self.frameNo = -1 self.currentFrame = 'None' self.map_ = None + self.debug = False if 'loopVideo' in kwargs and kwargs['loopVideo']: self.loopValue = '-1' @@ -47,6 +48,8 @@ class FfmpegVideo: kwargs['filter_'].insert(0, '-filter_complex') else: kwargs['filter_'] = None + if 'debug' in kwargs: + self.debug = True self.command = [ core.Core.FFMPEG_BIN, @@ -62,7 +65,6 @@ class FfmpegVideo: kwargs['filter_'] ) self.command.extend([ - '-s:v', '%sx%s' % (self.width, self.height), '-codec:v', 'rawvideo', '-', ]) @@ -88,11 +90,15 @@ class FfmpegVideo: self.frameBuffer.task_done() def fillBuffer(self): - import sys - print(self.command) + if self.debug: + print(" ".join([word for word in self.command])) + err = sys.__stdout__ + else: + err = subprocess.DEVNULL + self.pipe = openPipe( self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, - stderr=sys.__stdout__, bufsize=10**8 + stderr=err, bufsize=10**8 ) while True: if self.parent.canceled: From b6b45d12702f18f041acf65b0d5e34714835ecb4 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 30 Jul 2017 13:04:02 -0400 Subject: [PATCH 04/15] added Spectrum component with many options tweaked Waveform, added some ffmpeg logging, made generic widget functions --- src/component.py | 54 ++-- src/components/spectrum.py | 239 +++++++++++++++ src/components/spectrum.ui | 582 +++++++++++++++++++++++++++++++++++++ src/components/waveform.py | 48 ++- src/components/waveform.ui | 21 +- src/mainwindow.py | 2 +- src/toolkit/common.py | 43 +++ src/toolkit/ffmpeg.py | 41 ++- 8 files changed, 959 insertions(+), 71 deletions(-) create mode 100644 src/components/spectrum.py create mode 100644 src/components/spectrum.ui diff --git a/src/component.py b/src/component.py index 6d49406..1a5a5a4 100644 --- a/src/component.py +++ b/src/component.py @@ -4,9 +4,11 @@ ''' from PyQt5 import uic, QtCore, QtWidgets import os +import sys import time from toolkit.frame import BlankFrame +from toolkit import getWidgetValue, setWidgetValue, connectWidget class ComponentMetaclass(type(QtCore.QObject)): @@ -273,14 +275,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 +286,7 @@ 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()) + setattr(self, attr, getWidgetValue(widget)) if not self.core.openingProject: self.parent.drawPreview() saveValueStore = self.savePreset() @@ -313,19 +302,10 @@ 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] + setWidgetValue(widget, val) def savePreset(self): saveValueStore = {} @@ -420,24 +400,30 @@ 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 < 1.0: - # Don't create multiple windows for quickly repeated messages 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" % ( + "%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) diff --git a/src/components/spectrum.py b/src/components/spectrum.py new file mode 100644 index 0000000..261d9cc --- /dev/null +++ b/src/components/spectrum.py @@ -0,0 +1,239 @@ +from PIL import Image +from PyQt5 import QtGui, QtCore, QtWidgets +from PyQt5.QtGui import QColor +import os +import math +import subprocess +import time + +from component import Component +from toolkit.frame import BlankFrame, scale +from toolkit import checkOutput, rgbFromString, pickColor, connectWidget +from toolkit.ffmpeg import ( + openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound +) + + +class Component(Component): + name = 'Spectrum' + version = '1.0.0' + + def widget(self, *args): + self.color = (255, 255, 255) + self.previewFrame = None + super().widget(*args) + 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, + '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, + 'color': self.page.comboBox_color, + 'compress': self.page.checkBox_compress, + 'mono': self.page.checkBox_mono, + } + ) + for widget in self._trackedWidgets.values(): + connectWidget(widget, lambda: self.changed()) + + def changed(self): + self.changedOptions = True + + def update(self): + count = self.page.stackedWidget.count() + i = self.page.comboBox_filterType.currentIndex() + self.page.stackedWidget.setCurrentIndex(i if i < count else count - 1) + 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) + 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' + color = self.page.comboBox_color.currentText().lower() + genericPreview = self.settings.value("pref_genericPreview") + + if self.filterType == 0: # Spectrum + filter_ = ( + 'showspectrum=s=%sx%s:slide=scroll:win_func=%s:' + 'color=%s:scale=%s' % ( + self.settings.value("outputWidth"), + self.settings.value("outputHeight"), + self.page.comboBox_window.currentText(), + color, amplitude, + ) + ) + elif self.filterType == 1: # Histogram + filter_ = ( + 'ahistogram=r=%s:s=%sx%s:dmode=separate' % ( + self.settings.value("outputFrameRate"), + self.settings.value("outputWidth"), + self.settings.value("outputHeight"), + ) + ) + elif self.filterType == 2: # Vector Scope + filter_ = ( + 'avectorscope=s=%sx%s:draw=line:m=polar:scale=log' % ( + self.settings.value("outputWidth"), + self.settings.value("outputHeight"), + ) + ) + elif self.filterType == 3: # Musical Scale + filter_ = ( + 'showcqt=r=%s:s=%sx%s:count=30:text=0' % ( + self.settings.value("outputFrameRate"), + self.settings.value("outputWidth"), + self.settings.value("outputHeight"), + ) + ) + elif self.filterType == 4: # Phase + filter_ = ( + 'aphasemeter=r=%s:s=%sx%s:mpc=white:video=1[atrash][vtmp]; ' + '[atrash] anullsink; [vtmp] null' % ( + self.settings.value("outputFrameRate"), + self.settings.value("outputWidth"), + self.settings.value("outputHeight"), + ) + ) + + return [ + '-filter_complex', + '%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 else '', + filter_, + ', hflip' if self.mirror else'', + w, h, + ', trim=start=%s:end=%s' % ( + "{0:.3f}".format(startPt + 15), + "{0:.3f}".format(startPt + 15.5) + ) if preview 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): + image = Image.frombytes( + 'RGBA', + scale(self.scale, self.width, self.height, int), + imageData + ) + if self.scale != 100 \ + or self.x != 0 or self.y != 0: + frame = BlankFrame(self.width, self.height) + frame.paste(image, box=(self.x, self.y)) + else: + frame = image + return frame diff --git a/src/components/spectrum.ui b/src/components/spectrum.ui new file mode 100644 index 0000000..59ca0b8 --- /dev/null +++ b/src/components/spectrum.ui @@ -0,0 +1,582 @@ + + + Form + + + + 0 + 0 + 586 + 197 + + + + + 0 + 0 + + + + + 0 + 197 + + + + Form + + + + + + 4 + + + + + + + + 0 + 0 + + + + Type + + + + + + + + Spectrum + + + + + Histogram + + + + + Vector Scope + + + + + Musical Scale + + + + + Phase + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + + 0 + 0 + + + + X + + + + + + + + 0 + 0 + + + + + 80 + 16777215 + + + + -10000 + + + 10000 + + + + + + + + 0 + 0 + + + + Y + + + + + + + + 0 + 0 + + + + + 80 + 16777215 + + + + + 0 + 0 + + + + -10000 + + + 10000 + + + 0 + + + + + + + + + + + Compress + + + + + + + Mono + + + + + + + Mirror + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Scale + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + QAbstractSpinBox::UpDownArrows + + + % + + + 10 + + + 400 + + + 100 + + + + + + + + + + 0 + 0 + + + + false + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + + + + 0 + 0 + 561 + 72 + + + + + QLayout::SetMaximumSize + + + 0 + + + + + QLayout::SetDefaultConstraint + + + + + + 0 + 0 + + + + + 31 + 0 + + + + Window + + + 4 + + + + + + + + hann + + + + + gauss + + + + + tukey + + + + + dolph + + + + + cauchy + + + + + parzen + + + + + poisson + + + + + rect + + + + + bartlett + + + + + hanning + + + + + hamming + + + + + blackman + + + + + welch + + + + + flattop + + + + + bharris + + + + + bnuttall + + + + + lanczos + + + + + + + + + 0 + 0 + + + + Amplitude + + + 4 + + + + + + + + Square root + + + + + Cubic root + + + + + 4thrt + + + + + 5thrt + + + + + Linear + + + + + Logarithmic + + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 10 + 20 + + + + + + + + + + + + + 0 + 0 + + + + Color + + + 4 + + + + + + + + Channel + + + + + Intensity + + + + + Rainbow + + + + + Moreland + + + + + Nebulae + + + + + Fire + + + + + Fiery + + + + + Fruit + + + + + Cool + + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 10 + 20 + + + + + + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + diff --git a/src/components/waveform.py b/src/components/waveform.py index b4b19e9..6c5133d 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -8,7 +8,9 @@ import subprocess from component import Component from toolkit.frame import BlankFrame, scale from toolkit import checkOutput, rgbFromString, pickColor -from toolkit.ffmpeg import openPipe, closePipe, getAudioDuration, FfmpegVideo +from toolkit.ffmpeg import ( + openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound +) class Component(Component): @@ -112,6 +114,8 @@ class Component(Component): if not duration: return startPt = duration / 3 + if startPt + 3 > duration: + startPt += startPt - 3 command = [ self.core.FFMPEG_BIN, @@ -154,29 +158,43 @@ class Component(Component): 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' - 'showwaves=r=30:s=%sx%s:mode=%s:colors=%s@%s:scale=%s%s%s [v1]; ' - '[v1] scale=%s:%s%s,setpts=2.0*PTS [v]' % ( - 'aevalsrc=sin(1*2*PI*t)*sin(880*2*PI*t),' - if preview and genericPreview else '[0:a] ', - 'compand=.3|.3:1|1:-90/-60|-60/-40|-40/-30|-20/-20:6:0:-90:0.2' - ',' if self.compress and not preview else ( - 'compand=gain=5,' if self.compress else '' - ), - 'aformat=channel_layouts=mono,' if self.mono else '', - self.settings.value("outputWidth"), - self.settings.value("outputHeight"), - str(self.page.comboBox_mode.currentText()).lower(), - hexcolor, opacity, amplitude, + '%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 + 1) + ', trim=duration=%s' % "{0:.3f}".format(startPt + 3) if preview else '', ), '-map', '[v]', diff --git a/src/components/waveform.ui b/src/components/waveform.ui index 0e40380..5473f33 100644 --- a/src/components/waveform.ui +++ b/src/components/waveform.ui @@ -66,12 +66,17 @@ - P2p + Point - Point + Frequency Bar + + + + + Frequency Line @@ -180,12 +185,16 @@ - Wave Color + Color - + + + Qt::ImhNone + + @@ -244,10 +253,10 @@ % - 10 + 0 - 400 + 100 100 diff --git a/src/mainwindow.py b/src/mainwindow.py index a97081e..d9e95e2 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -581,7 +581,7 @@ class MainWindow(QtWidgets.QMainWindow): self.showMessage( msg=msg, detail=detail, - icon='Warning', + icon='Critical', ) def changeEncodingStatus(self, status): diff --git a/src/toolkit/common.py b/src/toolkit/common.py index 5d424e0..db278c0 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py @@ -113,3 +113,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() diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index 4ea2863..3421049 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -37,7 +37,6 @@ class FfmpegVideo: self.frameNo = -1 self.currentFrame = 'None' self.map_ = None - self.debug = False if 'loopVideo' in kwargs and kwargs['loopVideo']: self.loopValue = '-1' @@ -48,8 +47,6 @@ class FfmpegVideo: kwargs['filter_'].insert(0, '-filter_complex') else: kwargs['filter_'] = None - if 'debug' in kwargs: - self.debug = True self.command = [ core.Core.FFMPEG_BIN, @@ -90,16 +87,15 @@ class FfmpegVideo: self.frameBuffer.task_done() def fillBuffer(self): - if self.debug: - print(" ".join([word for word in self.command])) - err = sys.__stdout__ - else: - err = subprocess.DEVNULL - - self.pipe = openPipe( - self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, - stderr=err, bufsize=10**8 - ) + 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 @@ -111,10 +107,18 @@ class FfmpegVideo: self.frameBuffer.put((self.frameNo-1, self.lastFrame)) continue except AttributeError: - FfmpegVideo.threadError = ComponentError(self.component, 'video') + FfmpegVideo.threadError = ComponentError( + self.component, 'video', + "Video seemed playable but wasn't." + ) break - self.currentFrame = self.pipe.stdout.read(self.chunkSize) + 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 @@ -446,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,' + ) From 65420ce2855a24d54755a7a47804c2fb5f6d427e Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 30 Jul 2017 21:29:06 -0400 Subject: [PATCH 05/15] more options for the Spectrum component --- src/component.py | 2 +- src/components/spectrum.py | 99 +++++++--- src/components/spectrum.ui | 370 ++++++++++++++++++++++++++++++++++++- 3 files changed, 437 insertions(+), 34 deletions(-) diff --git a/src/component.py b/src/component.py index 1a5a5a4..36ad9d3 100644 --- a/src/component.py +++ b/src/component.py @@ -427,7 +427,7 @@ class ComponentError(RuntimeError): 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]) diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 261d9cc..d1ad297 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -1,6 +1,5 @@ from PIL import Image from PyQt5 import QtGui, QtCore, QtWidgets -from PyQt5.QtGui import QColor import os import math import subprocess @@ -8,7 +7,7 @@ import time from component import Component from toolkit.frame import BlankFrame, scale -from toolkit import checkOutput, rgbFromString, pickColor, connectWidget +from toolkit import checkOutput, connectWidget from toolkit.ffmpeg import ( openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound ) @@ -19,7 +18,6 @@ class Component(Component): version = '1.0.0' def widget(self, *args): - self.color = (255, 255, 255) self.previewFrame = None super().widget(*args) self.chunkSize = 4 * self.width * self.height @@ -35,14 +33,22 @@ class Component(Component): { 'filterType': self.page.comboBox_filterType, 'window': self.page.comboBox_window, - 'amplitude': self.page.comboBox_amplitude, + '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, } ) for widget in self._trackedWidgets.values(): @@ -52,9 +58,8 @@ class Component(Component): self.changedOptions = True def update(self): - count = self.page.stackedWidget.count() - i = self.page.comboBox_filterType.currentIndex() - self.page.stackedWidget.setCurrentIndex(i if i < count else count - 1) + self.page.stackedWidget.setCurrentIndex( + self.page.comboBox_filterType.currentIndex()) super().update() def previewRender(self): @@ -141,25 +146,26 @@ class Component(Component): def makeFfmpegFilter(self, preview=False, startPt=0): w, h = scale(self.scale, self.width, self.height, str) - 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' 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' % ( + '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(), @@ -167,32 +173,61 @@ class Component(Component): ) ) 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' % ( + '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=line:m=polar:scale=log' % ( + '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' % ( + '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:mpc=white:video=1[atrash][vtmp]; ' - '[atrash] anullsink; [vtmp] null' % ( + 'aphasemeter=r=%s:s=%sx%s:video=1 [atrash][vtmp1]; ' + '[atrash] anullsink; ' + '[vtmp1] colorkey=color=black:similarity=0.1:blend=0.5 ' % ( self.settings.value("outputFrameRate"), self.settings.value("outputWidth"), self.settings.value("outputHeight"), @@ -201,18 +236,22 @@ class Component(Component): return [ '-filter_complex', - '%s%s%s%s%s [v1]; ' - '[v1] scale=%s:%s%s [v]' % ( + '%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'', + '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 + 15), - "{0:.3f}".format(startPt + 15.5) + "{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]', ] diff --git a/src/components/spectrum.ui b/src/components/spectrum.ui index 59ca0b8..c6a8a15 100644 --- a/src/components/spectrum.ui +++ b/src/components/spectrum.ui @@ -31,6 +31,9 @@ 4 + + + @@ -208,6 +211,26 @@ + + + + Hue + + + 4 + + + + + + + ° + + + 359 + + + @@ -272,7 +295,7 @@ 0 0 561 - 72 + 66 @@ -415,7 +438,7 @@ - + Square root @@ -554,7 +577,348 @@ - + + + + + -1 + -1 + 561 + 31 + + + + + + + + + + 0 + 0 + + + + Display Scale + + + 4 + + + + + + + + Logarithmic + + + + + Square root + + + + + Cubic root + + + + + Linear + + + + + Reverse Log + + + + + + + + + 0 + 0 + + + + Amplitude + + + 4 + + + + + + + + Logarithmic + + + + + Linear + + + + + + + + Qt::Horizontal + + + QSizePolicy::Minimum + + + + 40 + 20 + + + + + + + + + + + + + + -1 + -1 + 585 + 64 + + + + + + + + + Mode + + + + + + + + lissajous + + + + + lissajous_xy + + + + + polar + + + + + + + + + 0 + 0 + + + + Amplitude + + + 4 + + + + + + + + Linear + + + + + Square root + + + + + Cubic root + + + + + Logarithmic + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 0 + 0 + + + + Zoom + + + 4 + + + + + + + 1 + + + 10 + + + + + + + Line + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + 0 + 0 + 561 + 31 + + + + + + + + + + 0 + 0 + + + + Timeclamp + + + 4 + + + + + + + s + + + 3 + + + 0.002000000000000 + + + 1.000000000000000 + + + 0.010000000000000 + + + 0.017000000000000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + 0 + 0 + 551 + 31 + + + + + + + + + From a472246dab69d0676c3c78ecd61659e432c960b4 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 30 Jul 2017 21:59:42 -0400 Subject: [PATCH 06/15] crop aphasermeter so it scales to look bigger --- src/components/spectrum.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/spectrum.py b/src/components/spectrum.py index d1ad297..8ab8404 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -227,7 +227,8 @@ class Component(Component): filter_ = ( 'aphasemeter=r=%s:s=%sx%s:video=1 [atrash][vtmp1]; ' '[atrash] anullsink; ' - '[vtmp1] colorkey=color=black:similarity=0.1:blend=0.5 ' % ( + '[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"), From 3c1b52205f183e9a2c943c5f666ed2c01db3aaf5 Mon Sep 17 00:00:00 2001 From: tassaron Date: Tue, 1 Aug 2017 17:57:39 -0400 Subject: [PATCH 07/15] component class now tracks colorwidgets so adding new color-selection widgets is now simple --- setup.py | 2 +- src/component.py | 73 ++++++++++++++++++++++++++++++++++---- src/components/color.py | 58 +++++------------------------- src/components/original.py | 35 +++--------------- src/components/text.py | 27 ++------------ src/components/waveform.py | 40 +++------------------ src/toolkit/common.py | 19 ---------- src/toolkit/frame.py | 6 ++-- 8 files changed, 90 insertions(+), 170 deletions(-) diff --git a/setup.py b/setup.py index d4f226b..4a4511f 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup import os -__version__ = '2.0.0.rc2' +__version__ = '2.0.0.rc3' def package_files(directory): diff --git a/src/component.py b/src/component.py index 36ad9d3..d47aeae 100644 --- a/src/component.py +++ b/src/component.py @@ -3,18 +3,20 @@ on making a valid component. ''' from PyQt5 import uic, QtCore, QtWidgets +from PyQt5.QtGui import QColor import os import sys import time from toolkit.frame import BlankFrame -from toolkit import getWidgetValue, setWidgetValue, connectWidget +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 ''' @@ -173,6 +175,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): self._trackedWidgets = {} self._presetNames = {} self._commandArgs = {} + self._colorWidgets = {} + self._relativeWidgets = {} self._lockedProperties = None self._lockedError = None @@ -188,7 +192,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): ) # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ - # Critical Methods + # Render Methods # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ def previewRender(self): @@ -286,7 +290,17 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): Call super() at the END if you need to subclass this. ''' for attr, widget in self._trackedWidgets.items(): - setattr(self, attr, getWidgetValue(widget)) + if attr in self._colorWidgets: + rgbTuple = rgbFromString(widget.text()) + setattr(self, attr, rgbTuple) + btnStyle = ( + "QPushButton { background-color : %s; outline: none; }" + % QColor(*rgbTuple).name() + ) + self._colorWidgets[attr].setStyleSheet(btnStyle) + else: + setattr(self, attr, getWidgetValue(widget)) + if not self.core.openingProject: self.parent.drawPreview() saveValueStore = self.savePreset() @@ -305,7 +319,16 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): key = attr if attr not in self._presetNames \ else self._presetNames[attr] val = presetDict[key] - setWidgetValue(widget, val) + + 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) + else: + setWidgetValue(widget, val) def savePreset(self): saveValueStore = {} @@ -352,7 +375,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( @@ -360,6 +388,37 @@ 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; }" + ) + + 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 diff --git a/src/components/color.py b/src/components/color.py index 2abd79a..d6fffc6 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -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) @@ -66,16 +52,18 @@ class Component(Component): '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, + }, ) 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 +149,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') diff --git a/src/components/original.py b/src/components/original.py index 621af6f..950ac7b 100644 --- a/src/components/original.py +++ b/src/components/original.py @@ -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): @@ -22,7 +21,6 @@ class Component(Component): return ['pcm'] def widget(self, *args): - self.visColor = (255, 255, 255) self.scale = 20 self.y = 0 super().widget(*args) @@ -33,35 +31,17 @@ 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, + }, colorWidgets={ + 'visColor': self.page.pushButton_visColor, }) - 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 - def previewRender(self): spectrum = numpy.fromfunction( lambda x: float(self.scale)/2500*(x-128)**2, (255,), dtype="int16") @@ -99,13 +79,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): diff --git a/src/components/text.py b/src/components/text.py index 8a302ff..1fe3467 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -5,7 +5,6 @@ import os from component import Component from toolkit.frame import FramePainter -from toolkit import rgbFromString, pickColor class Component(Component): @@ -33,11 +32,6 @@ class Component(Component): self.page.comboBox_textAlign.addItem("Right") 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)) @@ -48,21 +42,18 @@ class Component(Component): 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, }) 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 getXY(self): @@ -86,15 +77,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 +108,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"') diff --git a/src/components/waveform.py b/src/components/waveform.py index 6c5133d..9c3cf86 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -7,7 +7,7 @@ import subprocess from component import Component from toolkit.frame import BlankFrame, scale -from toolkit import checkOutput, rgbFromString, pickColor +from toolkit import checkOutput from toolkit.ffmpeg import ( openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound ) @@ -18,15 +18,9 @@ class Component(Component): version = '1.0.0' def widget(self, *args): - self.color = (255, 255, 255) super().widget(*args) - self.page.lineEdit_color.setText('%s,%s,%s' % self.color) - btnStyle = "QPushButton { background-color : %s; outline: none; }" \ - % QColor(*self.color).name() - self.page.pushButton_color.setStyleSheet(btnStyle) - self.page.pushButton_color.clicked.connect(lambda: self.pickColor()) - self.page.spinBox_scale.valueChanged.connect(self.updateChunksize) + self.page.lineEdit_color.setText('255,255,255') if hasattr(self.parent, 'window'): self.parent.window.lineEdit_audioFile.textChanged.connect( @@ -35,6 +29,7 @@ class Component(Component): self.trackWidgets( { + 'color': self.page.lineEdit_color, 'mode': self.page.comboBox_mode, 'amplitude': self.page.comboBox_amplitude, 'x': self.page.spinBox_x, @@ -44,36 +39,11 @@ class Component(Component): 'opacity': self.page.spinBox_opacity, 'compress': self.page.checkBox_compress, 'mono': self.page.checkBox_mono, + }, colorWidgets={ + 'color': self.page.pushButton_color, } ) - def update(self): - self.color = rgbFromString(self.page.lineEdit_color.text()) - btnStyle = "QPushButton { background-color : %s; outline: none; }" \ - % QColor(*self.color).name() - self.page.pushButton_color.setStyleSheet(btnStyle) - super().update() - - def loadPreset(self, pr, *args): - super().loadPreset(pr, *args) - - self.page.lineEdit_color.setText('%s,%s,%s' % pr['color']) - btnStyle = "QPushButton { background-color : %s; outline: none; }" \ - % QColor(*pr['color']).name() - self.page.pushButton_color.setStyleSheet(btnStyle) - - def savePreset(self): - saveValueStore = super().savePreset() - saveValueStore['color'] = self.color - return saveValueStore - - def pickColor(self): - RGBstring, btnStyle = pickColor() - if not RGBstring: - return - self.page.lineEdit_color.setText(RGBstring) - self.page.pushButton_color.setStyleSheet(btnStyle) - def previewRender(self): self.updateChunksize() frame = self.getPreviewFrame(self.width, self.height) diff --git a/src/toolkit/common.py b/src/toolkit/common.py index db278c0..eba57d9 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py @@ -74,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: diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index f42d4c9..c007188 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -42,9 +42,9 @@ class PaintColor(QtGui.QColor): super().__init__(b, g, r, a) -def scale(scale, width, height, returntype=None): - width = (float(width) / 100.0) * float(scale) - height = (float(height) / 100.0) * float(scale) +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: From 5784cdbcf87556b61519782cd1fc27065ffbc631 Mon Sep 17 00:00:00 2001 From: tassaron Date: Tue, 1 Aug 2017 21:57:36 -0400 Subject: [PATCH 08/15] x/y pixel values update to match output resolution --- src/component.py | 39 +++++++++++++++++++++++++++++++++++--- src/components/color.py | 3 +++ src/components/image.py | 3 +++ src/components/original.py | 2 ++ src/components/spectrum.py | 3 +++ src/components/text.py | 19 +++++++++++-------- src/components/video.py | 3 +++ src/components/waveform.py | 3 +++ src/mainwindow.py | 5 ++++- 9 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/component.py b/src/component.py index d47aeae..5dfe2ab 100644 --- a/src/component.py +++ b/src/component.py @@ -6,6 +6,7 @@ from PyQt5 import uic, QtCore, QtWidgets from PyQt5.QtGui import QColor import os import sys +import math import time from toolkit.frame import BlankFrame @@ -176,7 +177,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): self._presetNames = {} self._commandArgs = {} self._colorWidgets = {} + self._colorFuncs = {} self._relativeWidgets = {} + self._relativeValues = {} self._lockedProperties = None self._lockedError = None @@ -291,14 +294,44 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): ''' for attr, widget in self._trackedWidgets.items(): if attr in self._colorWidgets: + # Color Widgets: text stored as tuple & update the button color rgbTuple = rgbFromString(widget.text()) - setattr(self, attr, rgbTuple) btnStyle = ( "QPushButton { background-color : %s; outline: none; }" - % QColor(*rgbTuple).name() - ) + % 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 + if self._relativeWidgets[attr] == 'x': + dimension = self.width + else: + dimension = self.height + try: + oldUserValue = getattr(self, attr) + except AttributeError: + oldUserValue = self._trackedWidgets[attr].value() + newUserValue = self._trackedWidgets[attr].value() + newRelativeVal = newUserValue / dimension + + if attr in self._relativeValues: + if oldUserValue == newUserValue: + oldRelativeVal = self._relativeValues[attr] + if oldRelativeVal != newRelativeVal: + # Float changed without pixel value changing, which + # means the pixel value needs to be updated + self._trackedWidgets[attr].blockSignals(True) + self._trackedWidgets[attr].setValue( + math.ceil(dimension * oldRelativeVal)) + self._trackedWidgets[attr].blockSignals(False) + if oldUserValue != newUserValue \ + or attr not in self._relativeValues: + self._relativeValues[attr] = newRelativeVal + setattr(self, attr, self._trackedWidgets[attr].value()) + else: + # Normal tracked widget setattr(self, attr, getWidgetValue(widget)) if not self.core.openingProject: diff --git a/src/components/color.py b/src/components/color.py index d6fffc6..703caca 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -60,6 +60,9 @@ class Component(Component): }, colorWidgets={ 'color1': self.page.pushButton_color1, 'color2': self.page.pushButton_color2, + }, relativeWidgets={ + 'x': 'x', + 'y': 'y', }, ) diff --git a/src/components/image.py b/src/components/image.py index a96f127..2ffa5a1 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -28,6 +28,9 @@ class Component(Component): 'imagePath': 'image', 'xPosition': 'x', 'yPosition': 'y', + }, relativeWidgets={ + 'xPosition': 'x', + 'yPosition': 'y', }, ) diff --git a/src/components/original.py b/src/components/original.py index 950ac7b..67e3239 100644 --- a/src/components/original.py +++ b/src/components/original.py @@ -40,6 +40,8 @@ class Component(Component): 'y': self.page.spinBox_y, }, colorWidgets={ 'visColor': self.page.pushButton_visColor, + }, relativeWidgets={ + 'y': 'y', }) def previewRender(self): diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 8ab8404..2cc641d 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -49,6 +49,9 @@ class Component(Component): 'compress': self.page.checkBox_compress, 'mono': self.page.checkBox_mono, 'hue': self.page.spinBox_hue, + }, relativeWidgets={ + 'x': 'x', + 'y': 'y', } ) for widget in self._trackedWidgets.values(): diff --git a/src/components/text.py b/src/components/text.py index 1fe3467..0f87038 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -17,15 +17,12 @@ class Component(Component): def widget(self, *args): super().widget(*args) - height = int(self.settings.value('outputHeight')) - width = int(self.settings.value('outputWidth')) + # 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") @@ -35,8 +32,11 @@ class Component(Component): 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)) + + fm = QtGui.QFontMetrics(self.titleFont) + self.page.spinBox_xTextAlign.setValue( + self.width / 2 - fm.width(self.title)/2) + self.page.spinBox_yTextAlign.setValue(self.height / 2 * 1.036) self.page.fontComboBox_titleFont.currentFontChanged.connect( self.update @@ -50,6 +50,9 @@ class Component(Component): 'yPosition': self.page.spinBox_yTextAlign, }, colorWidgets={ 'textColor': self.page.pushButton_textColor, + }, relativeWidgets={ + 'xPosition': 'x', + 'yPosition': 'y', }) def update(self): diff --git a/src/components/video.py b/src/components/video.py index 6cd16e5..3569d17 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -38,6 +38,9 @@ class Component(Component): 'loopVideo': 'loop', 'xPosition': 'x', 'yPosition': 'y', + }, relativeWidgets={ + 'xPosition': 'x', + 'yPosition': 'y', } ) diff --git a/src/components/waveform.py b/src/components/waveform.py index 9c3cf86..a25116b 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -41,6 +41,9 @@ class Component(Component): 'mono': self.page.checkBox_mono, }, colorWidgets={ 'color': self.page.pushButton_color, + }, relativeWidgets={ + 'x': 'x', + 'y': 'y', } ) diff --git a/src/mainwindow.py b/src/mainwindow.py index d9e95e2..1c8806d 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -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''' From 8812c37213987a5e842af8b8dfcd090ca4ec8610 Mon Sep 17 00:00:00 2001 From: tassaron Date: Tue, 1 Aug 2017 22:04:51 -0400 Subject: [PATCH 09/15] width/height fields should be relative too --- src/components/color.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/color.py b/src/components/color.py index 703caca..2b100d9 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -63,6 +63,8 @@ class Component(Component): }, relativeWidgets={ 'x': 'x', 'y': 'y', + 'sizeWidth': 'x', + 'sizeHeight': 'y', }, ) From 62431a3cfebdc8490b7010d71b8e646dd6bd0d35 Mon Sep 17 00:00:00 2001 From: tassaron Date: Tue, 1 Aug 2017 22:07:49 -0400 Subject: [PATCH 10/15] fontsize is also relative --- src/components/text.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/text.py b/src/components/text.py index 0f87038..be4de4a 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -53,6 +53,7 @@ class Component(Component): }, relativeWidgets={ 'xPosition': 'x', 'yPosition': 'y', + 'fontSize': 'y', }) def update(self): From 6611492b30a7daf7bdbe77f6e12f9d322bdd90c1 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 3 Aug 2017 00:44:46 -0400 Subject: [PATCH 11/15] relative gradients & last good frame used for preview errors --- src/components/color.py | 5 +++++ src/components/spectrum.py | 15 ++++++++++----- src/components/text.py | 2 -- src/components/video.py | 15 ++++----------- src/components/waveform.py | 15 ++++++++++----- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/components/color.py b/src/components/color.py index 2b100d9..f5d618e 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -65,6 +65,11 @@ class Component(Component): 'y': 'y', 'sizeWidth': 'x', 'sizeHeight': 'y', + 'RG_start': 'x', + 'LG_start': 'x', + 'RG_end': 'x', + 'LG_end': 'x', + 'RG_centre': 'x', }, ) diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 2cc641d..9a0c59a 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -20,6 +20,7 @@ class Component(Component): 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 @@ -268,11 +269,15 @@ class Component(Component): return changed def finalizeFrame(self, imageData): - image = Image.frombytes( - 'RGBA', - scale(self.scale, self.width, self.height, int), - 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) diff --git a/src/components/text.py b/src/components/text.py index be4de4a..2a5d433 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -17,8 +17,6 @@ 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 diff --git a/src/components/video.py b/src/components/video.py index 3569d17..2cd67c6 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -16,12 +16,12 @@ 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( { @@ -70,8 +70,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): @@ -199,14 +197,10 @@ class Component(Component): 'RGBA', scale(self.scale, self.width, self.height, int), imageData) - + self._image = image except ValueError: - print( - '### BAD VIDEO SELECTED ###\n' - 'Video will not export with these settings' - ) - self.badVideo = True - return BlankFrame(self.width, self.height) + # use last good frame + image = self._image if self.scale != 100 \ or self.xPosition != 0 or self.yPosition != 0: @@ -214,5 +208,4 @@ class Component(Component): frame.paste(image, box=(self.xPosition, self.yPosition)) else: frame = image - self.badVideo = False return frame diff --git a/src/components/waveform.py b/src/components/waveform.py index a25116b..526e6fb 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -19,6 +19,7 @@ class Component(Component): def widget(self, *args): super().widget(*args) + self._image = BlankFrame(self.width, self.height) self.page.lineEdit_color.setText('255,255,255') @@ -178,11 +179,15 @@ class Component(Component): self.chunkSize = 4 * width * height def finalizeFrame(self, imageData): - image = Image.frombytes( - 'RGBA', - scale(self.scale, self.width, self.height, int), - 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) From 219e846984bb10e9674432fa7aeac4157635c743 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 3 Aug 2017 12:16:57 -0400 Subject: [PATCH 12/15] relativeWidgets might as well be a list --- src/component.py | 5 +-- src/components/color.py | 63 +++++++++++++++++--------------------- src/components/image.py | 36 ++++++++++------------ src/components/original.py | 6 ++-- src/components/spectrum.py | 47 +++++++++++++--------------- src/components/text.py | 8 ++--- src/components/video.py | 37 ++++++++++------------ src/components/waveform.py | 35 ++++++++++----------- 8 files changed, 106 insertions(+), 131 deletions(-) diff --git a/src/component.py b/src/component.py index 5dfe2ab..c5bc44b 100644 --- a/src/component.py +++ b/src/component.py @@ -304,10 +304,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): elif attr in self._relativeWidgets: # Relative widgets: number scales to fit export resolution - if self._relativeWidgets[attr] == 'x': - dimension = self.width - else: - dimension = self.height + dimension = self.width try: oldUserValue = getattr(self, attr) except AttributeError: diff --git a/src/components/color.py b/src/components/color.py index f5d618e..5d1233e 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -37,41 +37,34 @@ 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, - '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': 'x', - 'y': 'y', - 'sizeWidth': 'x', - 'sizeHeight': 'y', - 'RG_start': 'x', - 'LG_start': 'x', - 'RG_end': 'x', - 'LG_end': 'x', - 'RG_centre': 'x', - }, - ) + 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): fillType = self.page.comboBox_fill.currentIndex() diff --git a/src/components/image.py b/src/components/image.py index 2ffa5a1..19c4796 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -13,26 +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', - }, relativeWidgets={ - '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, + }, presetNames={ + 'mirror': self.page.checkBox_mirror, + 'imagePath': 'image', + 'xPosition': 'x', + 'yPosition': 'y', + }, relativeWidgets=[ + 'xPosition', 'yPosition', + ]) def previewRender(self): return self.drawFrame(self.width, self.height) diff --git a/src/components/original.py b/src/components/original.py index 67e3239..f886374 100644 --- a/src/components/original.py +++ b/src/components/original.py @@ -40,9 +40,9 @@ class Component(Component): 'y': self.page.spinBox_y, }, colorWidgets={ 'visColor': self.page.pushButton_visColor, - }, relativeWidgets={ - 'y': 'y', - }) + }, relativeWidgets=[ + 'y', + ]) def previewRender(self): spectrum = numpy.fromfunction( diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 9a0c59a..666e20a 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -30,31 +30,28 @@ class Component(Component): 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': 'x', - 'y': 'y', - } - ) + 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()) diff --git a/src/components/text.py b/src/components/text.py index 2a5d433..b7c244e 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -48,11 +48,9 @@ class Component(Component): 'yPosition': self.page.spinBox_yTextAlign, }, colorWidgets={ 'textColor': self.page.pushButton_textColor, - }, relativeWidgets={ - 'xPosition': 'x', - 'yPosition': 'y', - 'fontSize': 'y', - }) + }, relativeWidgets=[ + 'xPosition', 'yPosition', 'fontSize', + ]) def update(self): self.titleFont = self.page.fontComboBox_titleFont.currentFont() diff --git a/src/components/video.py b/src/components/video.py index 2cd67c6..b6bdd52 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -23,26 +23,23 @@ class Component(Component): 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', - }, relativeWidgets={ - '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(): diff --git a/src/components/waveform.py b/src/components/waveform.py index 526e6fb..71cbcac 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -28,25 +28,22 @@ class Component(Component): 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': 'x', - 'y': 'y', - } - ) + 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() From ae8a547b77a618c793929701f9c1fa72d3300110 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 3 Aug 2017 18:08:49 -0400 Subject: [PATCH 13/15] max spinbox vals scale relatively & less errors when spamming res change w/h attrs are locked during render so preview thread always get correctly-sized frame --- src/component.py | 92 ++++++++++++++++++++++++++++++----------- src/components/image.py | 2 +- src/components/text.ui | 3 ++ src/core.py | 6 +-- src/preview_thread.py | 2 + 5 files changed, 77 insertions(+), 28 deletions(-) diff --git a/src/component.py b/src/component.py index c5bc44b..ea4b5ec 100644 --- a/src/component.py +++ b/src/component.py @@ -179,9 +179,14 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): 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 @@ -190,8 +195,12 @@ 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 ) # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ @@ -304,27 +313,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): elif attr in self._relativeWidgets: # Relative widgets: number scales to fit export resolution - dimension = self.width - try: - oldUserValue = getattr(self, attr) - except AttributeError: - oldUserValue = self._trackedWidgets[attr].value() - newUserValue = self._trackedWidgets[attr].value() - newRelativeVal = newUserValue / dimension - - if attr in self._relativeValues: - if oldUserValue == newUserValue: - oldRelativeVal = self._relativeValues[attr] - if oldRelativeVal != newRelativeVal: - # Float changed without pixel value changing, which - # means the pixel value needs to be updated - self._trackedWidgets[attr].blockSignals(True) - self._trackedWidgets[attr].setValue( - math.ceil(dimension * oldRelativeVal)) - self._trackedWidgets[attr].blockSignals(False) - if oldUserValue != newUserValue \ - or attr not in self._relativeValues: - self._relativeValues[attr] = newRelativeVal + self.updateRelativeWidget(attr) setattr(self, attr, self._trackedWidgets[attr].value()) else: @@ -436,6 +425,13 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): "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() @@ -455,23 +451,35 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): 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.''' @@ -482,6 +490,42 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): self.unlockProperties() self.unlockError() + def updateRelativeWidget(self, attr): + dimension = self.width + if 'height' in attr.lower() \ + or 'ypos' in attr.lower() or attr == 'y': + dimension = self.height + try: + oldUserValue = getattr(self, attr) + except AttributeError: + oldUserValue = self._trackedWidgets[attr].value() + newUserValue = self._trackedWidgets[attr].value() + newRelativeVal = newUserValue / dimension + + 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) + self._trackedWidgets[attr].setValue( + math.ceil(dimension * oldRelativeVal)) + 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.''' diff --git a/src/components/image.py b/src/components/image.py index 19c4796..555dfb1 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -21,8 +21,8 @@ class Component(Component): 'xPosition': self.page.spinBox_x, 'yPosition': self.page.spinBox_y, 'stretched': self.page.checkBox_stretch, - }, presetNames={ 'mirror': self.page.checkBox_mirror, + }, presetNames={ 'imagePath': 'image', 'xPosition': 'x', 'yPosition': 'y', diff --git a/src/components/text.ui b/src/components/text.ui index 05e7f8e..bb5e5af 100644 --- a/src/components/text.ui +++ b/src/components/text.ui @@ -81,6 +81,9 @@ + + 1 + 500 diff --git a/src/core.py b/src/core.py index 24bf097..afb1e45 100644 --- a/src/core.py +++ b/src/core.py @@ -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, @@ -509,7 +509,7 @@ class Core: "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) diff --git a/src/preview_thread.py b/src/preview_thread.py index 0a6a856..bb22f0c 100644 --- a/src/preview_thread.py +++ b/src/preview_thread.py @@ -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 ) From 98a47a21d986ccede574baececd179be7550c9d6 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 3 Aug 2017 20:43:23 -0400 Subject: [PATCH 14/15] save presets as floats so project resolution is not relevant unfortunately this breaks old projects and presets --- src/component.py | 56 ++++++++++++++--- src/components/text.py | 18 +++--- src/components/text.ui | 138 ++++++++++++++++++++++++----------------- src/core.py | 2 +- 4 files changed, 139 insertions(+), 75 deletions(-) diff --git a/src/component.py b/src/component.py index ea4b5ec..5b38473 100644 --- a/src/component.py +++ b/src/component.py @@ -346,16 +346,29 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): % 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): @@ -490,17 +503,42 @@ 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): - dimension = self.width - if 'height' in attr.lower() \ - or 'ypos' in attr.lower() or attr == 'y': - dimension = self.height try: oldUserValue = getattr(self, attr) except AttributeError: oldUserValue = self._trackedWidgets[attr].value() newUserValue = self._trackedWidgets[attr].value() - newRelativeVal = newUserValue / dimension + newRelativeVal = self.floatValForAttr(attr, newUserValue) if attr in self._relativeValues: oldRelativeVal = self._relativeValues[attr] @@ -510,8 +548,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): # means the pixel value needs to be updated self._trackedWidgets[attr].blockSignals(True) self.updateRelativeWidgetMaximum(attr) - self._trackedWidgets[attr].setValue( - math.ceil(dimension * oldRelativeVal)) + pixelVal = self.pixelValForAttr(attr, oldRelativeVal) + self._trackedWidgets[attr].setValue(pixelVal) self._trackedWidgets[attr].blockSignals(False) if attr not in self._relativeValues \ diff --git a/src/components/text.py b/src/components/text.py index b7c244e..c3f3bdc 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -9,7 +9,7 @@ from toolkit.frame import FramePainter class Component(Component): name = 'Title Text' - version = '1.0.0' + version = '1.0.1' def __init__(self, *args): super().__init__(*args) @@ -25,20 +25,17 @@ class Component(Component): 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.lineEdit_title.setText(self.title) - self.page.comboBox_textAlign.setCurrentIndex(int(self.alignment)) self.page.spinBox_fontSize.setValue(int(self.fontSize)) + self.page.lineEdit_title.setText(self.title) - fm = QtGui.QFontMetrics(self.titleFont) - self.page.spinBox_xTextAlign.setValue( - self.width / 2 - fm.width(self.title)/2) - self.page.spinBox_yTextAlign.setValue(self.height / 2 * 1.036) - + 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, @@ -51,11 +48,16 @@ class Component(Component): }, relativeWidgets=[ 'xPosition', 'yPosition', 'fontSize', ]) + self.centerXY() def update(self): self.titleFont = self.page.fontComboBox_titleFont.currentFont() 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) diff --git a/src/components/text.ui b/src/components/text.ui index bb5e5af..f76979c 100644 --- a/src/components/text.ui +++ b/src/components/text.ui @@ -19,6 +19,36 @@ 4 + + + + + + Title + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + Testing New GUI + + + + + @@ -93,6 +123,55 @@ + + + + Text Color + + + + + + + + + + + 32 + 32 + + + + + + + + 32 + 32 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 0 + @@ -126,64 +205,9 @@ - + - Text Color - - - - - - - - 32 - 32 - - - - - - - - 32 - 32 - - - - - - - - - - - - - 0 - - - - - Title - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - Testing New GUI + Center diff --git a/src/core.py b/src/core.py index afb1e45..61905eb 100644 --- a/src/core.py +++ b/src/core.py @@ -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']: From d04ddba484f1c8993971f79d5ee14b0cc7a512fb Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 3 Aug 2017 20:50:22 -0400 Subject: [PATCH 15/15] image scale needs to be relative --- src/components/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/image.py b/src/components/image.py index 555dfb1..1555541 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -27,7 +27,7 @@ class Component(Component): 'xPosition': 'x', 'yPosition': 'y', }, relativeWidgets=[ - 'xPosition', 'yPosition', + 'xPosition', 'yPosition', 'scale' ]) def previewRender(self):