From b1713d38fa91e39f142b0c234b6405229aa149e1 Mon Sep 17 00:00:00 2001 From: tassaron Date: Mon, 17 Jul 2017 22:07:33 -0400 Subject: combined toolkit.py & frame.py into toolkit package --- src/toolkit/frame.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/toolkit/frame.py (limited to 'src/toolkit/frame.py') diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py new file mode 100644 index 0000000..cddb611 --- /dev/null +++ b/src/toolkit/frame.py @@ -0,0 +1,66 @@ +''' + Common tools for drawing compatible frames in a Component's frameRender() +''' +from PyQt5 import QtGui +from PIL import Image +from PIL.ImageQt import ImageQt +import sys +import os + + +class Frame: + '''Controller class for all frames.''' + + +class FramePainter(QtGui.QPainter): + ''' + A QPainter for a blank frame, which can be converted into a + Pillow image with finalize() + ''' + def __init__(self, width, height): + image = BlankFrame(width, height) + self.image = QtGui.QImage(ImageQt(image)) + super().__init__(self.image) + + def setPen(self, RgbTuple): + super().setPen(PaintColor(*RgbTuple)) + + def finalize(self): + self.end() + imBytes = self.image.bits().asstring(self.image.byteCount()) + + return Image.frombytes( + 'RGBA', (self.image.width(), self.image.height()), imBytes + ) + + +class PaintColor(QtGui.QColor): + '''Reverse the painter colour if the hardware stores RGB values backward''' + def __init__(self, r, g, b, a=255): + if sys.byteorder == 'big': + super().__init__(r, g, b, a) + else: + super().__init__(b, g, r, a) + + +def FloodFrame(width, height, RgbaTuple): + return Image.new("RGBA", (width, height), RgbaTuple) + + +def BlankFrame(width, height): + '''The base frame used by each component to start drawing.''' + return FloodFrame(width, height, (0, 0, 0, 0)) + + +def Checkerboard(width, height): + ''' + A checkerboard to represent transparency to the user. + TODO: Would be cool to generate this image with numpy instead. + ''' + image = FloodFrame(1920, 1080, (0, 0, 0, 0)) + image.paste(Image.open( + os.path.join(Frame.core.wd, "background.png")), + (0, 0) + ) + image = image.resize((width, height)) + return image -- cgit v1.2.3 From f454814867443ceeeca2a3a2c2a676947184503c Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 20 Jul 2017 20:31:38 -0400 Subject: ffmpeg functions moved to toolkit, component format simplified component methods are auto-decorated & settings are now class variables --- freeze.py | 7 +- setup.py | 15 +- src/command.py | 10 +- src/component.py | 167 +++++++++++++------- src/components/color.py | 8 +- src/components/image.py | 11 +- src/components/original.py | 11 +- src/components/sound.py | 14 +- src/components/text.py | 8 +- src/components/video.py | 23 ++- src/core.py | 379 ++++++++------------------------------------- src/mainwindow.py | 81 ++++++---- src/presetmanager.py | 20 +-- src/preview_thread.py | 4 +- src/toolkit/common.py | 12 +- src/toolkit/core.py | 18 +++ src/toolkit/ffmpeg.py | 284 +++++++++++++++++++++++++++++++++ src/toolkit/frame.py | 6 +- src/video_thread.py | 45 ++++-- 19 files changed, 628 insertions(+), 495 deletions(-) create mode 100644 src/toolkit/core.py create mode 100644 src/toolkit/ffmpeg.py (limited to 'src/toolkit/frame.py') diff --git a/freeze.py b/freeze.py index c9b7918..3281cad 100644 --- a/freeze.py +++ b/freeze.py @@ -2,8 +2,8 @@ from cx_Freeze import setup, Executable import sys import os -# Dependencies are automatically detected, but it might need -# fine tuning. +from setup import VERSION + deps = [os.path.join('src', p) for p in os.listdir('src') if p] deps.append('ffmpeg.exe' if sys.platform == 'win32' else 'ffmpeg') @@ -39,7 +39,6 @@ buildOptions = dict( include_files=deps, ) - base = 'Win32GUI' if sys.platform == 'win32' else None executables = [ @@ -53,7 +52,7 @@ executables = [ setup( name='audio-visualizer-python', - version='2.0', + version=VERSION, description='GUI tool to render visualization videos of audio files', options=dict(build_exe=buildOptions), executables=executables diff --git a/setup.py b/setup.py index 6ef688a..5abb976 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,9 @@ from setuptools import setup import os +VERSION = '2.0.0.rc1' + + def package_files(directory): paths = [] for (path, directories, filenames) in os.walk(directory): @@ -12,7 +15,7 @@ def package_files(directory): setup( name='audio_visualizer_python', - version='2.0.0rc1', + version=VERSION, url='https://github.com/djfun/audio-visualizer-python/tree/feature-newgui', license='MIT', description='Create audio visualization videos from a GUI or commandline', @@ -20,8 +23,7 @@ setup( "them as Projects to continue editing later. Different components can " "be added and layered to add visualizers, images, videos, gradients, " "text, etc. Use Projects created in the GUI with commandline mode to " - "automate your video production workflow without learning any complex " - "syntax.", + "automate your video production workflow without any complex syntax.", classifiers=[ 'Development Status :: 4 - Beta', 'License :: OSI Approved :: MIT License', @@ -29,10 +31,13 @@ setup( 'Intended Audience :: End Users/Desktop', 'Topic :: Multimedia :: Video :: Non-Linear Editor', ], - keywords=['visualizer', 'visualization', 'commandline video', - 'video editor', 'ffmpeg', 'podcast'], + keywords=[ + 'visualizer', 'visualization', 'commandline video', + 'video editor', 'ffmpeg', 'podcast' + ], packages=[ 'avpython', + 'avpython.toolkit', 'avpython.components' ], package_dir={'avpython': 'src'}, diff --git a/src/command.py b/src/command.py index 84d798d..046a1bf 100644 --- a/src/command.py +++ b/src/command.py @@ -9,8 +9,8 @@ import os import sys import time -import core -from toolkit import LoadDefaultSettings +from core import Core +from toolkit import loadDefaultSettings class Command(QtCore.QObject): @@ -19,7 +19,7 @@ class Command(QtCore.QObject): def __init__(self): QtCore.QObject.__init__(self) - self.core = core.Core() + self.core = Core() self.dataDir = self.core.dataDir self.canceled = False @@ -54,8 +54,8 @@ class Command(QtCore.QObject): nargs='*', action='append') self.args = self.parser.parse_args() - self.settings = self.core.settings - LoadDefaultSettings(self) + self.settings = Core.settings + loadDefaultSettings(self) if self.args.projpath: projPath = self.args.projpath diff --git a/src/component.py b/src/component.py index 7842bd6..92cc65c 100644 --- a/src/component.py +++ b/src/component.py @@ -1,33 +1,87 @@ ''' - Base classes for components to import. + Base classes for components to import. Read comments for some documentation + on making a valid component. ''' from PyQt5 import uic, QtCore, QtWidgets import os +from core import Core +from toolkit.common import getPresetDir -class Component(QtCore.QObject): + +class ComponentMetaclass(type(QtCore.QObject)): + ''' + Checks the validity of each Component class imported, and + mutates some attributes for easier use by the core program. + E.g., takes only major version from version string & decorates methods + ''' + def __new__(cls, name, parents, attrs): + # print('Creating %s component' % attrs['name']) + + # Turn certain class methods into properties and classmethods + for key in ('error', 'properties', 'audio', 'commandHelp'): + if key not in attrs: + continue + attrs[key] = property(attrs[key]) + + for key in ('names'): + if key not in attrs: + continue + attrs[key] = classmethod(key) + + # Turn version string into a number + try: + if 'version' not in attrs: + print( + 'No version attribute in %s. Defaulting to 1' % + attrs['name']) + attrs['version'] = 1 + else: + attrs['version'] = int(attrs['version'].split('.')[0]) + except ValueError: + print('%s component has an invalid version string:\n%s' % ( + attrs['name'], str(attrs['version']))) + except KeyError: + print('%s component has no version string.' % attrs['name']) + else: + return super().__new__(cls, name, parents, attrs) + quit(1) + + +class Component(QtCore.QObject, metaclass=ComponentMetaclass): ''' - A class for components to inherit. Read comments for documentation - on making a valid component. All subclasses must implement this signal: - modified = QtCore.pyqtSignal(int, bool) + The base class for components to inherit. ''' - def __init__(self, moduleIndex, compPos, core): + name = 'Component' + version = '1.0.0' + # The 1st number (before dot, aka the major version) is used to determine + # preset compatibility; the rest is ignored so it can be non-numeric. + + modified = QtCore.pyqtSignal(int, dict) + # ^ Signal used to tell core program that the component state changed, + # you shouldn't need to use this directly, it is used by self.update() + + def __init__(self, moduleIndex, compPos): super().__init__() self.currentPreset = None - self.canceled = False self.moduleIndex = moduleIndex self.compPos = compPos - self.core = core + + # Stop lengthy processes in response to this variable + self.canceled = False def __str__(self): - return self.__doc__ + return self.__class__.name - def version(self): - ''' - Change this number to identify new versions of a component - ''' - return 1 + def __repr__(self): + return '%s\n%s\n%s' % ( + self.__class__.name, str(self.__class__.version), self.savePreset() + ) + + # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ + # Properties + # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ def properties(self): ''' @@ -43,19 +97,32 @@ class Component(QtCore.QObject): ''' return - def cancel(self): + def audio(self): ''' - Stop any lengthy process in response to this variable + Return audio to mix into master as a tuple with two elements: + The first element can be: + - A string (path to audio file), + - Or an object that returns audio data through a pipe + The second element must be a dictionary of ffmpeg filters/options + to apply to the input stream. See the filter docs for ideas: + https://ffmpeg.org/ffmpeg-filters.html ''' - self.canceled = True - def reset(self): - self.canceled = False - - def update(self): + def names(): ''' - Read your widget values from self.page, then call super().update() + Alternative names for renaming a component between project files. ''' + return [] + + def commandHelp(self): + '''Help text as string for this component's commandline arguments''' + + # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ + # Methods + # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ + + def update(self): + '''Read widget values from self.page, then call super().update()''' self.parent.drawPreview() saveValueStore = self.savePreset() saveValueStore['preset'] = self.currentPreset @@ -92,7 +159,7 @@ class Component(QtCore.QObject): ''' if arg.startswith('preset='): _, preset = arg.split('=', 1) - path = os.path.join(self.core.getPresetDir(self), preset) + path = os.path.join(getPresetDir(self), preset) if not os.path.exists(path): print('Couldn\'t locate preset "%s"' % preset) quit(1) @@ -106,14 +173,19 @@ class Component(QtCore.QObject): self.__doc__, 'Usage:\n' 'Open a preset for this component:\n' ' "preset=Preset Name"') - self.commandHelp() + print(self.commandHelp) quit(0) - def commandHelp(self): - '''Print help text for this Component's commandline arguments''' - def loadUi(self, filename): - return uic.loadUi(os.path.join(self.core.componentsPath, filename)) + '''Load a Qt Designer ui file to use for this component's widget''' + return uic.loadUi(os.path.join(Core.componentsPath, filename)) + + def cancel(self): + '''Stop any lengthy process in response to this variable.''' + self.canceled = True + + def reset(self): + self.canceled = False ''' ### Reference methods for creating a new component @@ -121,47 +193,34 @@ class Component(QtCore.QObject): def widget(self, parent): self.parent = parent - page = self.loadUi('example.ui') + self.settings = parent.settings + self.page = self.loadUi('example.ui') # --- connect widget signals here --- - self.page = page - return page + return self.page def previewRender(self, previewWorker): - width = int(previewWorker.core.settings.value('outputWidth')) + width = int(self.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) - from frame import BlankFrame + from toolkit.frame import BlankFrame image = BlankFrame(width, height) return image def frameRender(self, layerNo, frameNo): audioArrayIndex = frameNo * self.sampleSize - width = int(self.worker.core.settings.value('outputWidth')) - height = int(self.worker.core.settings.value('outputHeight')) - from frame import BlankFrame + width = int(self.settings.value('outputWidth')) + height = int(self.settings.value('outputHeight')) + from toolkit.frame import BlankFrame image = BlankFrame(width, height) return image - - def audio(self): - \''' - Return audio to mix into master as a tuple with two elements: - The first element can be: - - A string (path to audio file), - - Or an object that returns audio data through a pipe - The second element must be a dictionary of ffmpeg filters/options - to apply to the input stream. See the filter docs for ideas: - https://ffmpeg.org/ffmpeg-filters.html - \''' - - @classmethod - def names(cls): - \''' - Alternative names for renaming a component between project files. - \''' - return [] ''' class BadComponentInit(Exception): + ''' + General purpose exception components can raise to indicate + a Python issue with e.g., dynamic creation of instances or something. + Decorative for now, may have future use for logging. + ''' def __init__(self, arg, name): string = '''################################ Mandatory argument "%s" not specified diff --git a/src/components/color.py b/src/components/color.py index 8d2526d..03371e7 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -10,13 +10,12 @@ from toolkit import rgbFromString, pickColor class Component(Component): - '''Color''' - - modified = QtCore.pyqtSignal(int, dict) + name = 'Color' + version = '1.0.0' def widget(self, parent): self.parent = parent - self.settings = self.parent.core.settings + self.settings = parent.settings page = self.loadUi('color.ui') self.color1 = (0, 0, 0) @@ -211,7 +210,6 @@ class Component(Component): def savePreset(self): return { - 'preset': self.currentPreset, 'color1': self.color1, 'color2': self.color2, 'x': self.x, diff --git a/src/components/image.py b/src/components/image.py index 7f3f610..591e03e 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -2,18 +2,18 @@ from PIL import Image, ImageDraw, ImageEnhance from PyQt5 import QtGui, QtCore, QtWidgets import os +from core import Core from component import Component from toolkit.frame import BlankFrame class Component(Component): - '''Image''' - - modified = QtCore.pyqtSignal(int, dict) + name = 'Image' + version = '1.0.0' def widget(self, parent): self.parent = parent - self.settings = self.parent.core.settings + self.settings = parent.settings page = self.loadUi('image.ui') page.lineEdit_image.textChanged.connect(self.update) @@ -102,7 +102,6 @@ class Component(Component): def savePreset(self): return { - 'preset': self.currentPreset, 'image': self.imagePath, 'scale': self.scale, 'color': self.color, @@ -117,7 +116,7 @@ class Component(Component): imgDir = self.settings.value("componentDir", os.path.expanduser("~")) filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.page, "Choose Image", imgDir, - "Image Files (%s)" % " ".join(self.core.imageFormats)) + "Image Files (%s)" % " ".join(Core.imageFormats)) if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_image.setText(filename) diff --git a/src/components/original.py b/src/components/original.py index 586204a..ae40df3 100644 --- a/src/components/original.py +++ b/src/components/original.py @@ -12,17 +12,15 @@ from toolkit import rgbFromString, pickColor class Component(Component): - '''Classic Visualizer''' + name = 'Classic Visualizer' + version = '1.0.0' - modified = QtCore.pyqtSignal(int, dict) - - @classmethod - def names(cls): + def names(): return ['Original Audio Visualization'] def widget(self, parent): self.parent = parent - self.settings = self.parent.core.settings + self.settings = parent.settings self.visColor = (255, 255, 255) self.scale = 20 self.y = 0 @@ -68,7 +66,6 @@ class Component(Component): def savePreset(self): return { - 'preset': self.currentPreset, 'layout': self.layout, 'visColor': self.visColor, 'scale': self.scale, diff --git a/src/components/sound.py b/src/components/sound.py index 5b06405..677a22f 100644 --- a/src/components/sound.py +++ b/src/components/sound.py @@ -1,14 +1,14 @@ from PyQt5 import QtGui, QtCore, QtWidgets import os +from core import Core from component import Component from toolkit.frame import BlankFrame class Component(Component): - '''Sound''' - - modified = QtCore.pyqtSignal(int, dict) + name = 'Sound' + version = '1.0.0' def widget(self, parent): self.parent = parent @@ -32,8 +32,8 @@ class Component(Component): super().update() def previewRender(self, previewWorker): - width = int(previewWorker.core.settings.value('outputWidth')) - height = int(previewWorker.core.settings.value('outputHeight')) + width = int(self.settings.value('outputWidth')) + height = int(self.settings.value('outputHeight')) return BlankFrame(width, height) def preFrameRender(self, **kwargs): @@ -67,7 +67,7 @@ class Component(Component): sndDir = self.settings.value("componentDir", os.path.expanduser("~")) filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.page, "Choose Sound", sndDir, - "Audio Files (%s)" % " ".join(self.core.audioFormats)) + "Audio Files (%s)" % " ".join(Core.audioFormats)) if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_sound.setText(filename) @@ -101,7 +101,7 @@ class Component(Component): key, arg = arg.split('=', 1) if key == 'path': if '*%s' % os.path.splitext(arg)[1] \ - not in self.core.audioFormats: + not in Core.audioFormats: print("Not a supported audio format") quit(1) self.page.lineEdit_sound.setText(arg) diff --git a/src/components/text.py b/src/components/text.py index fc3ef5f..d511f22 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -9,9 +9,8 @@ from toolkit import rgbFromString, pickColor class Component(Component): - '''Title Text''' - - modified = QtCore.pyqtSignal(int, dict) + name = 'Title Text' + version = '1.0.0' def __init__(self, *args): super().__init__(*args) @@ -19,7 +18,7 @@ class Component(Component): def widget(self, parent): self.parent = parent - self.settings = self.parent.core.settings + self.settings = parent.settings height = int(self.settings.value('outputHeight')) width = int(self.settings.value('outputWidth')) @@ -106,7 +105,6 @@ class Component(Component): def savePreset(self): return { - 'preset': self.currentPreset, 'title': self.title, 'titleFont': self.titleFont.toString(), 'alignment': self.alignment, diff --git a/src/components/video.py b/src/components/video.py index a9f334e..b35c2e5 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -6,6 +6,7 @@ import subprocess import threading from queue import PriorityQueue +from core import Core from component import Component, BadComponentInit from toolkit.frame import BlankFrame from toolkit import openPipe, checkOutput @@ -106,9 +107,8 @@ class Video: class Component(Component): - '''Video''' - - modified = QtCore.pyqtSignal(int, dict) + name = 'Video' + version = '1.0.0' def widget(self, parent): self.parent = parent @@ -154,8 +154,8 @@ class Component(Component): super().update() def previewRender(self, previewWorker): - width = int(previewWorker.core.settings.value('outputWidth')) - height = int(previewWorker.core.settings.value('outputHeight')) + width = int(self.settings.value('outputWidth')) + height = int(self.settings.value('outputHeight')) self.updateChunksize(width, height) frame = self.getPreviewFrame(width, height) if not frame: @@ -190,7 +190,7 @@ class Component(Component): def testAudioStream(self): # test if an audio stream really exists audioTestCommand = [ - self.core.FFMPEG_BIN, + Core.FFMPEG_BIN, '-i', self.videoPath, '-vn', '-f', 'null', '-' ] @@ -209,12 +209,12 @@ class Component(Component): def preFrameRender(self, **kwargs): super().preFrameRender(**kwargs) - width = int(self.worker.core.settings.value('outputWidth')) - height = int(self.worker.core.settings.value('outputHeight')) + width = int(self.settings.value('outputWidth')) + height = int(self.settings.value('outputHeight')) self.blankFrame_ = BlankFrame(width, height) self.updateChunksize(width, height) self.video = Video( - ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath, + ffmpeg=Core.FFMPEG_BIN, videoPath=self.videoPath, width=width, height=height, chunkSize=self.chunkSize, frameRate=int(self.settings.value("outputFrameRate")), parent=self.parent, loopVideo=self.loopVideo, @@ -240,7 +240,6 @@ class Component(Component): def savePreset(self): return { - 'preset': self.currentPreset, 'video': self.videoPath, 'loop': self.loopVideo, 'useAudio': self.useAudio, @@ -255,7 +254,7 @@ class Component(Component): imgDir = self.settings.value("componentDir", os.path.expanduser("~")) filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.page, "Choose Video", - imgDir, "Video Files (%s)" % " ".join(self.core.videoFormats) + imgDir, "Video Files (%s)" % " ".join(Core.videoFormats) ) if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) @@ -298,7 +297,7 @@ class Component(Component): if not arg.startswith('preset=') and '=' in arg: key, arg = arg.split('=', 1) if key == 'path' and os.path.exists(arg): - if '*%s' % os.path.splitext(arg)[1] in self.core.videoFormats: + if '*%s' % os.path.splitext(arg)[1] in Core.videoFormats: self.page.lineEdit_video.setText(arg) self.page.spinBox_scale.setValue(100) self.page.checkBox_loop.setChecked(True) diff --git a/src/core.py b/src/core.py index 07c1f71..dd2ef18 100644 --- a/src/core.py +++ b/src/core.py @@ -1,46 +1,56 @@ ''' Home to the Core class which tracks program state. Used by GUI & commandline ''' +from PyQt5 import QtCore, QtGui, uic import sys import os -from PyQt5 import QtCore, QtGui, uic -import subprocess as sp -import numpy import json from importlib import import_module -from PyQt5.QtCore import QStandardPaths import toolkit -from toolkit.frame import Frame +from toolkit.ffmpeg import findFfmpeg import video_thread class Core: ''' MainWindow and Command module both use an instance of this class - to store the program state. This object tracks the components, - opens projects and presets, and stores settings/paths to data. + to store the main program state. This object tracks the components + as an instance, has methods for managing the components and for + opening/creating project files and presets. ''' - def __init__(self): - Frame.core = self - self.dataDir = QStandardPaths.writableLocation( - QStandardPaths.AppConfigLocation - ) - self.presetDir = os.path.join(self.dataDir, 'presets') + + @classmethod + def storeSettings(cls): + ''' + Stores settings/paths to directories as class variables + ''' if getattr(sys, 'frozen', False): # frozen - self.wd = os.path.dirname(sys.executable) + wd = os.path.dirname(sys.executable) else: - # unfrozen - self.wd = os.path.dirname(os.path.realpath(__file__)) - self.componentsPath = os.path.join(self.wd, 'components') - self.settings = QtCore.QSettings( - os.path.join(self.dataDir, 'settings.ini'), - QtCore.QSettings.IniFormat - ) + wd = os.path.dirname(os.path.realpath(__file__)) - self.loadEncoderOptions() - self.videoFormats = toolkit.appendUppercase([ + dataDir = QtCore.QStandardPaths.writableLocation( + QtCore.QStandardPaths.AppConfigLocation + ) + with open(os.path.join(wd, 'encoder-options.json')) as json_file: + encoderOptions = json.load(json_file) + + settings = { + 'wd': wd, + 'dataDir': dataDir, + 'settings': QtCore.QSettings( + os.path.join(dataDir, 'settings.ini'), + QtCore.QSettings.IniFormat), + 'presetDir': os.path.join(dataDir, 'presets'), + 'componentsPath': os.path.join(wd, 'components'), + 'encoderOptions': encoderOptions, + 'FFMPEG_BIN': findFfmpeg(), + 'canceled': False, + } + + settings['videoFormats'] = toolkit.appendUppercase([ '*.mp4', '*.mov', '*.mkv', @@ -48,7 +58,7 @@ class Core: '*.webm', '*.flv', ]) - self.audioFormats = toolkit.appendUppercase([ + settings['audioFormats'] = toolkit.appendUppercase([ '*.mp3', '*.wav', '*.ogg', @@ -56,7 +66,7 @@ class Core: '*.flac', '*.aac', ]) - self.imageFormats = toolkit.appendUppercase([ + settings['imageFormats'] = toolkit.appendUppercase([ '*.png', '*.jpg', '*.tif', @@ -68,15 +78,22 @@ class Core: '*.xpm', ]) - self.FFMPEG_BIN = self.findFfmpeg() + # Register all settings as class variables + for classvar, val in settings.items(): + setattr(cls, classvar, val) + # Make settings accessible to the toolkit package + toolkit.init(settings) + + def __init__(self): + Core.storeSettings() + self.findComponents() self.selectedComponents = [] - # copies of named presets to detect modification - self.savedPresets = {} + self.savedPresets = {} # copies of presets to detect modification def findComponents(self): def findComponents(): - for f in sorted(os.listdir(self.componentsPath)): + for f in sorted(os.listdir(Core.componentsPath)): name, ext = os.path.splitext(f) if name.startswith("__"): continue @@ -88,7 +105,7 @@ class Core: ] # store canonical module names and indexes self.moduleIndexes = [i for i in range(len(self.modules))] - self.compNames = [mod.Component.__doc__ for mod in self.modules] + self.compNames = [mod.Component.name for mod in self.modules] self.altCompNames = [] # store alternative names for modules for i, mod in enumerate(self.modules): @@ -108,7 +125,7 @@ class Core: return None component = self.modules[moduleIndex].Component( - moduleIndex, compPos, self + moduleIndex, compPos ) self.selectedComponents.insert( compPos, @@ -171,10 +188,6 @@ class Core: self.savedPresets[presetName] = dict(saveValueStore) return True - def getPresetDir(self, comp): - return os.path.join( - self.presetDir, str(comp), str(comp.version())) - def getPreset(self, filepath): '''Returns the preset dict stored at this filepath''' if not os.path.exists(filepath): @@ -204,7 +217,7 @@ class Core: widget.blockSignals(False) for key, value in data['Settings']: - self.settings.setValue(key, value) + Core.settings.setValue(key, value) for tup in data['Components']: name, vers, preset = tup @@ -215,7 +228,7 @@ class Core: if 'preset' in preset and preset['preset'] is not None: nam = preset['preset'] filepath2 = os.path.join( - self.presetDir, name, str(vers), nam) + Core.presetDir, name, str(vers), nam) origSaveValueStore = self.getPreset(filepath2) if origSaveValueStore: self.savedPresets[nam] = dict(origSaveValueStore) @@ -336,7 +349,7 @@ class Core: presetName = preset['preset'] \ if preset['preset'] else os.path.basename(filepath)[:-4] newPath = os.path.join( - self.presetDir, + Core.presetDir, name, vers, presetName @@ -354,7 +367,7 @@ class Core: def exportPreset(self, exportPath, compName, vers, origName): internalPath = os.path.join( - self.presetDir, compName, str(vers), origName + Core.presetDir, compName, str(vers), origName ) if not os.path.exists(internalPath): return @@ -378,7 +391,7 @@ class Core: '''Create a preset file (.avl) at filepath using args. Or if filepath is empty, create an internal preset using args''' if not filepath: - dirname = os.path.join(self.presetDir, compName, str(vers)) + dirname = os.path.join(Core.presetDir, compName, str(vers)) if not os.path.exists(dirname): os.makedirs(dirname) filepath = os.path.join(dirname, presetName) @@ -417,13 +430,13 @@ class Core: saveValueStore = comp.savePreset() saveValueStore['preset'] = comp.currentPreset f.write('%s\n' % str(comp)) - f.write('%s\n' % str(comp.version())) + f.write('%s\n' % str(comp.version)) f.write('%s\n' % toolkit.presetToString(saveValueStore)) f.write('\n[Settings]\n') - for key in self.settings.allKeys(): + for key in Core.settings.allKeys(): if key in settingsKeys: - f.write('%s=%s\n' % (key, self.settings.value(key))) + f.write('%s=%s\n' % (key, Core.settings.value(key))) if window: f.write('\n[WindowFields]\n') @@ -438,280 +451,8 @@ class Core: except: return False - def loadEncoderOptions(self): - file_path = os.path.join(self.wd, 'encoder-options.json') - with open(file_path) as json_file: - self.encoder_options = json.load(json_file) - - def findFfmpeg(self): - if getattr(sys, 'frozen', False): - # The application is frozen - if sys.platform == "win32": - return os.path.join(self.wd, 'ffmpeg.exe') - else: - return os.path.join(self.wd, 'ffmpeg') - - else: - if sys.platform == "win32": - return "ffmpeg" - else: - try: - with open(os.devnull, "w") as f: - toolkit.checkOutput( - ['ffmpeg', '-version'], stderr=f - ) - return "ffmpeg" - except sp.CalledProcessError: - return "avconv" - - def createFfmpegCommand(self, inputFile, outputFile, duration): - ''' - Constructs the major ffmpeg command used to export the video - ''' - safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters - duration = "{0:.3f}".format(duration + 0.1) # used by input sources - - # Test if user has libfdk_aac - encoders = toolkit.checkOutput( - "%s -encoders -hide_banner" % self.FFMPEG_BIN, shell=True - ) - encoders = encoders.decode("utf-8") - - acodec = self.settings.value('outputAudioCodec') - - options = self.encoder_options - containerName = self.settings.value('outputContainer') - vcodec = self.settings.value('outputVideoCodec') - vbitrate = str(self.settings.value('outputVideoBitrate'))+'k' - acodec = self.settings.value('outputAudioCodec') - abitrate = str(self.settings.value('outputAudioBitrate'))+'k' - - for cont in options['containers']: - if cont['name'] == containerName: - container = cont['container'] - break - - vencoders = options['video-codecs'][vcodec] - aencoders = options['audio-codecs'][acodec] - - for encoder in vencoders: - if encoder in encoders: - vencoder = encoder - break - - for encoder in aencoders: - if encoder in encoders: - aencoder = encoder - break - - ffmpegCommand = [ - self.FFMPEG_BIN, - '-thread_queue_size', '512', - '-y', # overwrite the output file if it already exists. - - # INPUT VIDEO - '-f', 'rawvideo', - '-vcodec', 'rawvideo', - '-s', '%sx%s' % ( - self.settings.value('outputWidth'), - self.settings.value('outputHeight'), - ), - '-pix_fmt', 'rgba', - '-r', self.settings.value('outputFrameRate'), - '-t', duration, - '-i', '-', # the video input comes from a pipe - '-an', # the video input has no sound - - # INPUT SOUND - '-t', duration, - '-i', inputFile - ] - - # Add extra audio inputs and any needed avfilters - # NOTE: Global filters are currently hard-coded here for debugging use - globalFilters = 0 # increase to add global filters - extraAudio = [ - comp.audio() for comp in self.selectedComponents - if 'audio' in comp.properties() - ] - if extraAudio or globalFilters > 0: - # Add -i options for extra input files - extraFilters = {} - for streamNo, params in enumerate(reversed(extraAudio)): - extraInputFile, params = params - ffmpegCommand.extend([ - '-t', safeDuration, - # Tell ffmpeg about shorter clips (seemingly not needed) - # streamDuration = self.getAudioDuration(extraInputFile) - # if streamDuration > float(safeDuration) - # else "{0:.3f}".format(streamDuration), - '-i', extraInputFile - ]) - # Construct dataset of extra filters we'll need to add later - for ffmpegFilter in params: - if streamNo + 2 not in extraFilters: - extraFilters[streamNo + 2] = [] - extraFilters[streamNo + 2].append(( - ffmpegFilter, params[ffmpegFilter] - )) - - # Start creating avfilters! Popen-style, so don't use semicolons; - extraFilterCommand = [] - - if globalFilters <= 0: - # Dictionary of last-used tmp labels for a given stream number - tmpInputs = {streamNo: -1 for streamNo in extraFilters} - else: - # Insert blank entries for global filters into extraFilters - # so the per-stream filters know what input to source later - for streamNo in range(len(extraAudio), 0, -1): - if streamNo + 1 not in extraFilters: - extraFilters[streamNo + 1] = [] - # Also filter the primary audio track - extraFilters[1] = [] - tmpInputs = { - streamNo: globalFilters - 1 - for streamNo in extraFilters - } - - # Add the global filters! - # NOTE: list length must = globalFilters, currently hardcoded - if tmpInputs: - extraFilterCommand.extend([ - '[%s:a] ashowinfo [%stmp0]' % ( - str(streamNo), - str(streamNo) - ) - for streamNo in tmpInputs - ]) - - # Now add the per-stream filters! - for streamNo, paramList in extraFilters.items(): - for param in paramList: - source = '[%s:a]' % str(streamNo) \ - if tmpInputs[streamNo] == -1 else \ - '[%stmp%s]' % ( - str(streamNo), str(tmpInputs[streamNo]) - ) - tmpInputs[streamNo] = tmpInputs[streamNo] + 1 - extraFilterCommand.append( - '%s %s%s [%stmp%s]' % ( - source, param[0], param[1], str(streamNo), - str(tmpInputs[streamNo]) - ) - ) - - # Join all the filters together and combine into 1 stream - extraFilterCommand = "; ".join(extraFilterCommand) + '; ' \ - if tmpInputs else '' - ffmpegCommand.extend([ - '-filter_complex', - extraFilterCommand + - '%s amix=inputs=%s:duration=first [a]' - % ( - "".join([ - '[%stmp%s]' % (str(i), tmpInputs[i]) - if i in extraFilters else '[%s:a]' % str(i) - for i in range(1, len(extraAudio) + 2) - ]), - str(len(extraAudio) + 1) - ), - ]) - - # Only map audio from the filters, and video from the pipe - ffmpegCommand.extend([ - '-map', '0:v', - '-map', '[a]', - ]) - - ffmpegCommand.extend([ - # OUTPUT - '-vcodec', vencoder, - '-acodec', aencoder, - '-b:v', vbitrate, - '-b:a', abitrate, - '-pix_fmt', self.settings.value('outputVideoFormat'), - '-preset', self.settings.value('outputPreset'), - '-f', container - ]) - - if acodec == 'aac': - ffmpegCommand.append('-strict') - ffmpegCommand.append('-2') - - ffmpegCommand.append(outputFile) - return ffmpegCommand - - def getAudioDuration(self, filename): - command = [self.FFMPEG_BIN, '-i', filename] - - try: - fileInfo = toolkit.checkOutput(command, stderr=sp.STDOUT) - except sp.CalledProcessError as ex: - fileInfo = ex.output - - info = fileInfo.decode("utf-8").split('\n') - for line in info: - if 'Duration' in line: - d = line.split(',')[0] - d = d.split(' ')[3] - d = d.split(':') - duration = float(d[0])*3600 + float(d[1])*60 + float(d[2]) - return duration - - def readAudioFile(self, filename, parent): - duration = self.getAudioDuration(filename) - - command = [ - self.FFMPEG_BIN, - '-i', filename, - '-f', 's16le', - '-acodec', 'pcm_s16le', - '-ar', '44100', # ouput will have 44100 Hz - '-ac', '1', # mono (set to '2' for stereo) - '-'] - in_pipe = toolkit.openPipe( - command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8 - ) - - completeAudioArray = numpy.empty(0, dtype="int16") - - progress = 0 - lastPercent = None - while True: - if self.canceled: - break - # read 2 seconds of audio - progress += 4 - raw_audio = in_pipe.stdout.read(88200*4) - if len(raw_audio) == 0: - break - audio_array = numpy.fromstring(raw_audio, dtype="int16") - completeAudioArray = numpy.append(completeAudioArray, audio_array) - - percent = int(100*(progress/duration)) - if percent >= 100: - percent = 100 - - if lastPercent != percent: - string = 'Loading audio file: '+str(percent)+'%' - parent.progressBarSetText.emit(string) - parent.progressBarUpdate.emit(percent) - - lastPercent = percent - - in_pipe.kill() - in_pipe.wait() - - # add 0s the end - completeAudioArrayCopy = numpy.zeros( - len(completeAudioArray) + 44100, dtype="int16") - completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray - completeAudioArray = completeAudioArrayCopy - - return (completeAudioArray, duration) - def newVideoWorker(self, loader, audioFile, outputPath): + '''loader is MainWindow or Command object which must own the thread''' self.videoThread = QtCore.QThread(loader) videoWorker = video_thread.Worker( loader, audioFile, outputPath, self.selectedComponents @@ -727,7 +468,9 @@ class Core: self.videoThread.wait() def cancel(self): - self.canceled = True + Core.canceled = True + toolkit.cancel() def reset(self): - self.canceled = False + Core.canceled = False + toolkit.reset() diff --git a/src/mainwindow.py b/src/mainwindow.py index ca8e697..9944d1a 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -14,13 +14,17 @@ import signal import filecmp import time -import core +from core import Core import preview_thread from presetmanager import PresetManager -from toolkit import LoadDefaultSettings, disableWhenEncoding, checkOutput +from toolkit import loadDefaultSettings, disableWhenEncoding, checkOutput class PreviewWindow(QtWidgets.QLabel): + ''' + Paints the preview QLabel and maintains the aspect ratio when the + window is resized. + ''' def __init__(self, parent, img): super(PreviewWindow, self).__init__() self.parent = parent @@ -47,6 +51,14 @@ class PreviewWindow(QtWidgets.QLabel): class MainWindow(QtWidgets.QMainWindow): + ''' + The MainWindow wraps many Core methods in order to update the GUI + accordingly. E.g., instead of self.core.openProject(), it will use + self.openProject() and update the window titlebar within the wrapper. + + MainWindow manages the autosave feature, although Core has the + primary functions for opening and creating project files. + ''' createVideo = QtCore.pyqtSignal() newTask = QtCore.pyqtSignal(list) # for the preview window @@ -57,25 +69,26 @@ class MainWindow(QtWidgets.QMainWindow): # print('main thread id: {}'.format(QtCore.QThread.currentThreadId())) self.window = window - self.core = core.Core() + self.core = Core() self.pages = [] # widgets of component settings self.lastAutosave = time.time() self.encoding = False # Create data directory, load/create settings - self.dataDir = self.core.dataDir + self.dataDir = Core.dataDir + self.presetDir = Core.presetDir self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') - self.settings = self.core.settings - LoadDefaultSettings(self) + self.settings = Core.settings + loadDefaultSettings(self) self.presetManager = PresetManager( uic.loadUi( - os.path.join(self.core.wd, 'presetmanager.ui')), self) + os.path.join(Core.wd, 'presetmanager.ui')), self) if not os.path.exists(self.dataDir): os.makedirs(self.dataDir) for neededDirectory in ( - self.core.presetDir, self.settings.value("projectDir")): + self.presetDir, self.settings.value("projectDir")): if not os.path.exists(neededDirectory): os.mkdir(neededDirectory) @@ -120,7 +133,7 @@ class MainWindow(QtWidgets.QMainWindow): window.pushButton_Cancel.clicked.connect(self.stopVideo) - for i, container in enumerate(self.core.encoder_options['containers']): + for i, container in enumerate(Core.encoderOptions['containers']): window.comboBox_videoContainer.addItem(container['name']) if container['name'] == self.settings.value('outputContainer'): selectedContainer = i @@ -160,14 +173,14 @@ class MainWindow(QtWidgets.QMainWindow): window.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings) self.previewWindow = PreviewWindow(self, os.path.join( - self.core.wd, "background.png")) + Core.wd, "background.png")) window.verticalLayout_previewWrapper.addWidget(self.previewWindow) # Make component buttons self.compMenu = QMenu() self.compActions = [] for i, comp in enumerate(self.core.modules): - action = self.compMenu.addAction(comp.Component.__doc__) + action = self.compMenu.addAction(comp.Component.name) action.triggered.connect( lambda _, item=i: self.core.insertComponent(0, item, self) ) @@ -336,8 +349,14 @@ class MainWindow(QtWidgets.QMainWindow): "Ctrl+Down", self.window, activated=lambda: self.moveComponent(1) ) - QtWidgets.QShortcut("Ctrl+Home", self.window, self.moveComponentTop) - QtWidgets.QShortcut("Ctrl+End", self.window, self.moveComponentBottom) + QtWidgets.QShortcut( + "Ctrl+Home", self.window, + activated=lambda: self.moveComponent('top') + ) + QtWidgets.QShortcut( + "Ctrl+End", self.window, + activated=lambda: self.moveComponent('bottom') + ) QtWidgets.QShortcut("Ctrl+r", self.window, self.removeComponent) @QtCore.pyqtSlot() @@ -389,7 +408,7 @@ class MainWindow(QtWidgets.QMainWindow): vCodecWidget.clear() aCodecWidget.clear() - for container in self.core.encoder_options['containers']: + for container in Core.encoderOptions['containers']: if container['name'] == name: for vCodec in container['video-codecs']: vCodecWidget.addItem(vCodec) @@ -397,6 +416,7 @@ class MainWindow(QtWidgets.QMainWindow): aCodecWidget.addItem(aCodec) def updateCodecSettings(self): + '''Updates settings.ini to match encoder option widgets''' vCodecWidget = self.window.comboBox_videoCodec vBitrateWidget = self.window.spinBox_vBitrate aBitrateWidget = self.window.spinBox_aBitrate @@ -416,11 +436,12 @@ class MainWindow(QtWidgets.QMainWindow): if not self.currentProject: if os.path.exists(self.autosavePath): os.remove(self.autosavePath) - elif force or time.time() - self.lastAutosave >= 0.1: + elif force or time.time() - self.lastAutosave >= 0.2: self.core.createProjectFile(self.autosavePath, self.window) self.lastAutosave = time.time() def autosaveExists(self, identical=True): + '''Determines if creating the autosave should be blocked.''' try: if self.currentProject and os.path.exists(self.autosavePath) \ and filecmp.cmp( @@ -432,6 +453,7 @@ class MainWindow(QtWidgets.QMainWindow): return False def saveProjectChanges(self): + '''Overwrites project file with autosave file''' try: os.remove(self.currentProject) os.rename(self.autosavePath, self.currentProject) @@ -447,7 +469,7 @@ class MainWindow(QtWidgets.QMainWindow): fileName, _ = QtWidgets.QFileDialog.getOpenFileName( self.window, "Open Audio File", - inputDir, "Audio Files (%s)" % " ".join(self.core.audioFormats)) + inputDir, "Audio Files (%s)" % " ".join(Core.audioFormats)) if fileName: self.settings.setValue("inputDir", os.path.dirname(fileName)) @@ -460,7 +482,7 @@ class MainWindow(QtWidgets.QMainWindow): self.window, "Set Output Video File", outputDir, "Video Files (%s);; All Files (*)" % " ".join( - self.core.videoFormats)) + Core.videoFormats)) if fileName: self.settings.setValue("outputDir", os.path.dirname(fileName)) @@ -587,10 +609,11 @@ class MainWindow(QtWidgets.QMainWindow): def showFfmpegCommand(self): from textwrap import wrap - command = self.core.createFfmpegCommand( + from toolkit.ffmpeg import createFfmpegCommand + command = createFfmpegCommand( self.window.lineEdit_audioFile.text(), self.window.lineEdit_outputFile.text(), - self.core.getAudioDuration(self.window.lineEdit_audioFile.text()) + self.core.selectedComponents ) lines = wrap(" ".join(command), 49) self.showMessage( @@ -603,7 +626,7 @@ class MainWindow(QtWidgets.QMainWindow): componentList.insertItem( index, - self.core.selectedComponents[index].__doc__) + self.core.selectedComponents[index].name) componentList.setCurrentRow(index) # connect to signal that adds an asterisk when modified @@ -632,6 +655,10 @@ class MainWindow(QtWidgets.QMainWindow): def moveComponent(self, change): '''Moves a component relatively from its current position''' componentList = self.window.listWidget_componentList + if change == 'top': + change = -componentList.currentRow() + elif change == 'bottom': + change = len(componentList)-componentList.currentRow()-1 stackedWidget = self.window.stackedWidget row = componentList.currentRow() @@ -650,21 +677,9 @@ class MainWindow(QtWidgets.QMainWindow): stackedWidget.setCurrentIndex(newRow) self.drawPreview() - @disableWhenEncoding - def moveComponentTop(self): - componentList = self.window.listWidget_componentList - row = -componentList.currentRow() - self.moveComponent(row) - - @disableWhenEncoding - def moveComponentBottom(self): - componentList = self.window.listWidget_componentList - row = len(componentList)-componentList.currentRow()-1 - self.moveComponent(row) - @disableWhenEncoding def dragComponent(self, event): - '''Drop event for the component listwidget''' + '''Used as Qt drop event for the component listwidget''' componentList = self.window.listWidget_componentList modelIndexes = [ diff --git a/src/presetmanager.py b/src/presetmanager.py index 6e003a1..825fdee 100644 --- a/src/presetmanager.py +++ b/src/presetmanager.py @@ -15,11 +15,11 @@ class PresetManager(QtWidgets.QDialog): self.parent = parent self.core = parent.core self.settings = parent.settings - self.presetDir = self.core.presetDir + self.presetDir = parent.presetDir if not self.settings.value('presetDir'): self.settings.setValue( "presetDir", - os.path.join(self.core.dataDir, 'projects')) + os.path.join(parent.dataDir, 'projects')) self.findPresets() @@ -161,7 +161,7 @@ class PresetManager(QtWidgets.QDialog): selectedComponents[index].savePreset() saveValueStore['preset'] = newName componentName = str(selectedComponents[index]).strip() - vers = selectedComponents[index].version() + vers = selectedComponents[index].version self.createNewPreset( componentName, vers, newName, saveValueStore, window=self.parent.window) @@ -195,13 +195,13 @@ class PresetManager(QtWidgets.QDialog): def openPreset(self, presetName, compPos=None): componentList = self.parent.window.listWidget_componentList - selectedComponents = self.parent.core.selectedComponents + selectedComponents = self.core.selectedComponents index = compPos if compPos is not None else componentList.currentRow() if index == -1: return componentName = str(selectedComponents[index]).strip() - version = selectedComponents[index].version() + version = selectedComponents[index].version dirname = os.path.join(self.presetDir, componentName, str(version)) filepath = os.path.join(dirname, presetName) self.core.openPreset(filepath, index, presetName) @@ -243,6 +243,7 @@ class PresetManager(QtWidgets.QDialog): parent=window if window else self.window) def openRenamePresetDialog(self): + # TODO: maintain consistency by changing this to call createNewPreset() presetList = self.window.listWidget_presets if presetList.currentRow() == -1: return @@ -273,11 +274,12 @@ class PresetManager(QtWidgets.QDialog): os.rename(oldPath, newPath) self.findPresets() self.drawPresetList() - for i, comp in enumerate(self.core.selectedComponents): - if comp.currentPreset == oldName: - comp.currentPreset = newName - self.parent.updateComponentTitle(i, True) + if toolkit.getPresetDir(comp) == path \ + and comp.currentPreset == oldName: + self.core.openPreset(newPath, i, newName) + self.parent.updateComponentTitle(i, False) + self.parent.drawPreview() break def openImportDialog(self): diff --git a/src/preview_thread.py b/src/preview_thread.py index c28e048..3fc73b3 100644 --- a/src/preview_thread.py +++ b/src/preview_thread.py @@ -22,8 +22,8 @@ class Worker(QtCore.QObject): parent.newTask.connect(self.createPreviewImage) parent.processTask.connect(self.process) self.parent = parent - self.core = self.parent.core - self.settings = self.parent.core.settings + self.core = parent.core + self.settings = parent.settings self.queue = queue width = int(self.settings.value('outputWidth')) diff --git a/src/toolkit/common.py b/src/toolkit/common.py index e3a1649..763d582 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py @@ -8,6 +8,13 @@ import sys import subprocess from collections import OrderedDict +from toolkit.core import * + + +def getPresetDir(comp): + '''Get the preset subdirectory for a particular version of a component''' + return os.path.join(Core.presetDir, str(comp), str(comp.version)) + def badName(name): '''Returns whether a name contains non-alphanumeric chars''' @@ -103,8 +110,9 @@ def rgbFromString(string): return (255, 255, 255) -def LoadDefaultSettings(self): - ''' Runs once at each program start-up. Fills in default settings +def loadDefaultSettings(self): + ''' + Runs once at each program start-up. Fills in default settings for any settings not found in settings.ini ''' self.resolutions = [ diff --git a/src/toolkit/core.py b/src/toolkit/core.py new file mode 100644 index 0000000..a96a684 --- /dev/null +++ b/src/toolkit/core.py @@ -0,0 +1,18 @@ +class Core: + '''A very complicated class for tracking settings''' + + +def init(settings): + global Core + for classvar, val in settings.items(): + setattr(Core, classvar, val) + + +def cancel(): + global Core + Core.canceled = True + + +def reset(): + global Core + Core.canceled = False diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py new file mode 100644 index 0000000..89d4e9d --- /dev/null +++ b/src/toolkit/ffmpeg.py @@ -0,0 +1,284 @@ +''' + Tools for using ffmpeg +''' +import numpy +import sys +import os +import subprocess as sp + +from toolkit.common import Core, checkOutput, openPipe + + +def findFfmpeg(): + if getattr(sys, 'frozen', False): + # The application is frozen + if sys.platform == "win32": + return os.path.join(Core.wd, 'ffmpeg.exe') + else: + return os.path.join(Core.wd, 'ffmpeg') + + else: + if sys.platform == "win32": + return "ffmpeg" + else: + try: + with open(os.devnull, "w") as f: + checkOutput( + ['ffmpeg', '-version'], stderr=f + ) + return "ffmpeg" + except sp.CalledProcessError: + return "avconv" + + +def createFfmpegCommand(inputFile, outputFile, components, duration=-1): + ''' + Constructs the major ffmpeg command used to export the video + ''' + if duration == -1: + duration = getAudioDuration(inputFile) + + safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters + duration = "{0:.3f}".format(duration + 0.1) # used by input sources + + # Test if user has libfdk_aac + encoders = checkOutput( + "%s -encoders -hide_banner" % Core.FFMPEG_BIN, shell=True + ) + encoders = encoders.decode("utf-8") + + acodec = Core.settings.value('outputAudioCodec') + + options = Core.encoderOptions + containerName = Core.settings.value('outputContainer') + vcodec = Core.settings.value('outputVideoCodec') + vbitrate = str(Core.settings.value('outputVideoBitrate'))+'k' + acodec = Core.settings.value('outputAudioCodec') + abitrate = str(Core.settings.value('outputAudioBitrate'))+'k' + + for cont in options['containers']: + if cont['name'] == containerName: + container = cont['container'] + break + + vencoders = options['video-codecs'][vcodec] + aencoders = options['audio-codecs'][acodec] + + for encoder in vencoders: + if encoder in encoders: + vencoder = encoder + break + + for encoder in aencoders: + if encoder in encoders: + aencoder = encoder + break + + ffmpegCommand = [ + Core.FFMPEG_BIN, + '-thread_queue_size', '512', + '-y', # overwrite the output file if it already exists. + + # INPUT VIDEO + '-f', 'rawvideo', + '-vcodec', 'rawvideo', + '-s', '%sx%s' % ( + Core.settings.value('outputWidth'), + Core.settings.value('outputHeight'), + ), + '-pix_fmt', 'rgba', + '-r', Core.settings.value('outputFrameRate'), + '-t', duration, + '-i', '-', # the video input comes from a pipe + '-an', # the video input has no sound + + # INPUT SOUND + '-t', duration, + '-i', inputFile + ] + + # Add extra audio inputs and any needed avfilters + # NOTE: Global filters are currently hard-coded here for debugging use + globalFilters = 0 # increase to add global filters + extraAudio = [ + comp.audio for comp in components + if 'audio' in comp.properties + ] + if extraAudio or globalFilters > 0: + # Add -i options for extra input files + extraFilters = {} + for streamNo, params in enumerate(reversed(extraAudio)): + extraInputFile, params = params + ffmpegCommand.extend([ + '-t', safeDuration, + # Tell ffmpeg about shorter clips (seemingly not needed) + # streamDuration = getAudioDuration(extraInputFile) + # if streamDuration > float(safeDuration) + # else "{0:.3f}".format(streamDuration), + '-i', extraInputFile + ]) + # Construct dataset of extra filters we'll need to add later + for ffmpegFilter in params: + if streamNo + 2 not in extraFilters: + extraFilters[streamNo + 2] = [] + extraFilters[streamNo + 2].append(( + ffmpegFilter, params[ffmpegFilter] + )) + + # Start creating avfilters! Popen-style, so don't use semicolons; + extraFilterCommand = [] + + if globalFilters <= 0: + # Dictionary of last-used tmp labels for a given stream number + tmpInputs = {streamNo: -1 for streamNo in extraFilters} + else: + # Insert blank entries for global filters into extraFilters + # so the per-stream filters know what input to source later + for streamNo in range(len(extraAudio), 0, -1): + if streamNo + 1 not in extraFilters: + extraFilters[streamNo + 1] = [] + # Also filter the primary audio track + extraFilters[1] = [] + tmpInputs = { + streamNo: globalFilters - 1 + for streamNo in extraFilters + } + + # Add the global filters! + # NOTE: list length must = globalFilters, currently hardcoded + if tmpInputs: + extraFilterCommand.extend([ + '[%s:a] ashowinfo [%stmp0]' % ( + str(streamNo), + str(streamNo) + ) + for streamNo in tmpInputs + ]) + + # Now add the per-stream filters! + for streamNo, paramList in extraFilters.items(): + for param in paramList: + source = '[%s:a]' % str(streamNo) \ + if tmpInputs[streamNo] == -1 else \ + '[%stmp%s]' % ( + str(streamNo), str(tmpInputs[streamNo]) + ) + tmpInputs[streamNo] = tmpInputs[streamNo] + 1 + extraFilterCommand.append( + '%s %s%s [%stmp%s]' % ( + source, param[0], param[1], str(streamNo), + str(tmpInputs[streamNo]) + ) + ) + + # Join all the filters together and combine into 1 stream + extraFilterCommand = "; ".join(extraFilterCommand) + '; ' \ + if tmpInputs else '' + ffmpegCommand.extend([ + '-filter_complex', + extraFilterCommand + + '%s amix=inputs=%s:duration=first [a]' + % ( + "".join([ + '[%stmp%s]' % (str(i), tmpInputs[i]) + if i in extraFilters else '[%s:a]' % str(i) + for i in range(1, len(extraAudio) + 2) + ]), + str(len(extraAudio) + 1) + ), + ]) + + # Only map audio from the filters, and video from the pipe + ffmpegCommand.extend([ + '-map', '0:v', + '-map', '[a]', + ]) + + ffmpegCommand.extend([ + # OUTPUT + '-vcodec', vencoder, + '-acodec', aencoder, + '-b:v', vbitrate, + '-b:a', abitrate, + '-pix_fmt', Core.settings.value('outputVideoFormat'), + '-preset', Core.settings.value('outputPreset'), + '-f', container + ]) + + if acodec == 'aac': + ffmpegCommand.append('-strict') + ffmpegCommand.append('-2') + + ffmpegCommand.append(outputFile) + return ffmpegCommand + + +def getAudioDuration(filename): + command = [Core.FFMPEG_BIN, '-i', filename] + + try: + fileInfo = checkOutput(command, stderr=sp.STDOUT) + except sp.CalledProcessError as ex: + fileInfo = ex.output + + info = fileInfo.decode("utf-8").split('\n') + for line in info: + if 'Duration' in line: + d = line.split(',')[0] + d = d.split(' ')[3] + d = d.split(':') + duration = float(d[0])*3600 + float(d[1])*60 + float(d[2]) + return duration + + +def readAudioFile(filename, parent): + duration = getAudioDuration(filename) + + command = [ + Core.FFMPEG_BIN, + '-i', filename, + '-f', 's16le', + '-acodec', 'pcm_s16le', + '-ar', '44100', # ouput will have 44100 Hz + '-ac', '1', # mono (set to '2' for stereo) + '-'] + in_pipe = openPipe( + command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8 + ) + + completeAudioArray = numpy.empty(0, dtype="int16") + + progress = 0 + lastPercent = None + while True: + if Core.canceled: + return + # read 2 seconds of audio + progress += 4 + raw_audio = in_pipe.stdout.read(88200*4) + if len(raw_audio) == 0: + break + audio_array = numpy.fromstring(raw_audio, dtype="int16") + completeAudioArray = numpy.append(completeAudioArray, audio_array) + + percent = int(100*(progress/duration)) + if percent >= 100: + percent = 100 + + if lastPercent != percent: + string = 'Loading audio file: '+str(percent)+'%' + parent.progressBarSetText.emit(string) + parent.progressBarUpdate.emit(percent) + + lastPercent = percent + + in_pipe.kill() + in_pipe.wait() + + # add 0s the end + completeAudioArrayCopy = numpy.zeros( + len(completeAudioArray) + 44100, dtype="int16") + completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray + completeAudioArray = completeAudioArrayCopy + + return (completeAudioArray, duration) diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index cddb611..83fd59e 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -7,9 +7,7 @@ from PIL.ImageQt import ImageQt import sys import os - -class Frame: - '''Controller class for all frames.''' +from toolkit.common import Core class FramePainter(QtGui.QPainter): @@ -59,7 +57,7 @@ def Checkerboard(width, height): ''' image = FloodFrame(1920, 1080, (0, 0, 0, 0)) image.paste(Image.open( - os.path.join(Frame.core.wd, "background.png")), + os.path.join(Core.wd, "background.png")), (0, 0) ) image = image.resize((width, height)) diff --git a/src/video_thread.py b/src/video_thread.py index 1f2eaf5..8517b92 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -5,9 +5,9 @@ are emitted to update MainWindow's progress bar, detail text, and preview. Export can be cancelled with cancel() ''' -from PyQt5 import QtCore, QtGui, uic +from PyQt5 import QtCore, QtGui from PyQt5.QtCore import pyqtSignal, pyqtSlot -from PIL import Image, ImageDraw, ImageFont +from PIL import Image from PIL.ImageQt import ImageQt import numpy import subprocess as sp @@ -19,6 +19,7 @@ import time import signal from toolkit import openPipe +from toolkit.ffmpeg import readAudioFile, createFfmpegCommand from toolkit.frame import Checkerboard @@ -33,7 +34,7 @@ class Worker(QtCore.QObject): def __init__(self, parent, inputFile, outputFile, components): QtCore.QObject.__init__(self) self.core = parent.core - self.settings = parent.core.settings + self.settings = parent.settings self.modules = parent.core.modules parent.createVideo.connect(self.createVideo) @@ -133,12 +134,17 @@ class Worker(QtCore.QObject): # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ self.progressBarSetText.emit("Loading audio file...") - self.completeAudioArray, duration = self.core.readAudioFile( + audioFileTraits = readAudioFile( self.inputFile, self ) + if audioFileTraits is None: + self.cancelExport() + return + self.completeAudioArray, duration = audioFileTraits self.progressBarUpdate.emit(0) self.progressBarSetText.emit("Starting components...") + canceledByComponent = False print('Loaded Components:', ", ".join([ "%s) %s" % (num, str(component)) for num, component in enumerate(reversed(self.components)) @@ -153,14 +159,15 @@ class Worker(QtCore.QObject): progressBarSetText=self.progressBarSetText ) - if 'error' in comp.properties(): + if 'error' in comp.properties: self.cancel() self.canceled = True + canceledByComponent = True errMsg = "Component #%s encountered an error!" % compNo \ - if comp.error() is None else 'Component #%s (%s): %s' % ( + if comp.error is None else 'Component #%s (%s): %s' % ( str(compNo), str(comp), - comp.error() + comp.error ) self.parent.showMessage( msg=errMsg, @@ -168,17 +175,16 @@ class Worker(QtCore.QObject): parent=None # MainWindow is in a different thread ) break - if 'static' in comp.properties(): + if 'static' in comp.properties: self.staticComponents[compNo] = \ comp.frameRender(compNo, 0).copy() if self.canceled: - print('Export cancelled by component #%s (%s): %s' % ( - compNo, str(comp), comp.error() - )) - self.progressBarSetText.emit('Export Canceled') - self.encoding.emit(False) - self.videoCreated.emit() + if canceledByComponent: + print('Export cancelled by component #%s (%s): %s' % ( + compNo, str(comp), comp.error + )) + self.cancelExport() return # Merge consecutive static component frames together @@ -192,8 +198,8 @@ class Worker(QtCore.QObject): ) self.staticComponents[compNo] = None - ffmpegCommand = self.core.createFfmpegCommand( - self.inputFile, self.outputFile, duration + ffmpegCommand = createFfmpegCommand( + self.inputFile, self.outputFile, self.components, duration ) print('###### FFMPEG COMMAND ######\n%s' % " ".join(ffmpegCommand)) print('############################') @@ -280,7 +286,6 @@ class Worker(QtCore.QObject): pass self.progressBarUpdate.emit(0) self.progressBarSetText.emit('Export Canceled') - else: if self.error: print("Export Failed") @@ -297,6 +302,12 @@ class Worker(QtCore.QObject): self.encoding.emit(False) self.videoCreated.emit() + def cancelExport(self): + self.progressBarUpdate.emit(0) + self.progressBarSetText.emit('Export Canceled') + self.encoding.emit(False) + self.videoCreated.emit() + def updateProgress(self, pStr, pVal): self.progressBarValue.emit(pVal) self.progressBarSetText.emit(pStr) -- cgit v1.2.3 From bf0890e7c87c730b8970c1a20c5b6a9a1a55d203 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 23 Jul 2017 01:53:54 -0400 Subject: components auto-connect & track widgets, less autosave spam importing toolkit from live interpreter now works --- setup.py | 2 +- src/__init__.py | 12 +++ src/command.py | 2 - src/component.py | 196 +++++++++++++++++++++++++++++++++------------ src/components/color.py | 137 +++++++++++-------------------- src/components/image.py | 77 +++++------------- src/components/original.py | 59 ++++++-------- src/components/sound.py | 50 +++--------- src/components/text.py | 81 ++++++++----------- src/components/video.py | 98 +++++++---------------- src/core.py | 196 ++++++++++++++++++++++++++++----------------- src/main.py | 23 ++---- src/mainwindow.py | 125 +++++++++++++++++++---------- src/mainwindow.ui | 3 + src/presetmanager.py | 15 ++-- src/preview_thread.py | 17 ++-- src/toolkit/common.py | 56 +++---------- src/toolkit/core.py | 18 ----- src/toolkit/ffmpeg.py | 46 ++++++++--- src/toolkit/frame.py | 4 +- src/video_thread.py | 7 +- 21 files changed, 604 insertions(+), 620 deletions(-) delete mode 100644 src/toolkit/core.py (limited to 'src/toolkit/frame.py') diff --git a/setup.py b/setup.py index a2d8495..d4f226b 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup import os -__version__ = '2.0.0.rc1' +__version__ = '2.0.0.rc2' def package_files(directory): diff --git a/src/__init__.py b/src/__init__.py index 8b13789..2f4cffa 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1 +1,13 @@ +import sys +import os + +if getattr(sys, 'frozen', False): + # frozen + wd = os.path.dirname(sys.executable) +else: + # unfrozen + wd = os.path.dirname(os.path.realpath(__file__)) + +# make relative imports work when using /src as a package +sys.path.insert(0, wd) diff --git a/src/command.py b/src/command.py index 046a1bf..ca186e5 100644 --- a/src/command.py +++ b/src/command.py @@ -10,7 +10,6 @@ import sys import time from core import Core -from toolkit import loadDefaultSettings class Command(QtCore.QObject): @@ -55,7 +54,6 @@ class Command(QtCore.QObject): self.args = self.parser.parse_args() self.settings = Core.settings - loadDefaultSettings(self) if self.args.projpath: projPath = self.args.projpath diff --git a/src/component.py b/src/component.py index 92cc65c..bec2df5 100644 --- a/src/component.py +++ b/src/component.py @@ -5,8 +5,28 @@ from PyQt5 import uic, QtCore, QtWidgets import os -from core import Core -from toolkit.common import getPresetDir +from presetmanager import getPresetDir + + +def commandWrapper(func): + '''Intercepts each component's command() method to check for global args''' + def decorator(self, arg): + if arg.startswith('preset='): + _, preset = arg.split('=', 1) + path = os.path.join(getPresetDir(self), preset) + if not os.path.exists(path): + print('Couldn\'t locate preset "%s"' % preset) + quit(1) + else: + print('Opening "%s" preset on layer %s' % ( + preset, self.compPos) + ) + self.core.openPreset(path, self.compPos, preset) + # Don't call the component's command() method + return + else: + return func(self, arg) + return decorator class ComponentMetaclass(type(QtCore.QObject)): @@ -16,10 +36,14 @@ class ComponentMetaclass(type(QtCore.QObject)): E.g., takes only major version from version string & decorates methods ''' def __new__(cls, name, parents, attrs): - # print('Creating %s component' % attrs['name']) + if 'ui' not in attrs: + # use module name as ui filename by default + attrs['ui'] = '%s.ui' % os.path.splitext( + attrs['__module__'].split('.')[-1] + )[0] # Turn certain class methods into properties and classmethods - for key in ('error', 'properties', 'audio', 'commandHelp'): + for key in ('error', 'properties', 'audio'): if key not in attrs: continue attrs[key] = property(attrs[key]) @@ -29,6 +53,10 @@ class ComponentMetaclass(type(QtCore.QObject)): continue attrs[key] = classmethod(key) + # Do not apply these mutations to the base class + if parents[0] != QtCore.QObject: + attrs['command'] = commandWrapper(attrs['command']) + # Turn version string into a number try: if 'version' not in attrs: @@ -54,19 +82,24 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): ''' name = 'Component' + # ui = 'nameOfNonDefaultUiFile' version = '1.0.0' - # The 1st number (before dot, aka the major version) is used to determine + # The major version (before the first dot) is used to determine # preset compatibility; the rest is ignored so it can be non-numeric. modified = QtCore.pyqtSignal(int, dict) # ^ Signal used to tell core program that the component state changed, # you shouldn't need to use this directly, it is used by self.update() - def __init__(self, moduleIndex, compPos): + def __init__(self, moduleIndex, compPos, core): super().__init__() - self.currentPreset = None self.moduleIndex = moduleIndex self.compPos = compPos + self.core = core + self.currentPreset = None + + self._trackedWidgets = {} + self._presetNames = {} # Stop lengthy processes in response to this variable self.canceled = False @@ -114,28 +147,103 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): ''' return [] - def commandHelp(self): - '''Help text as string for this component's commandline arguments''' - # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # Methods # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ - def update(self): - '''Read widget values from self.page, then call super().update()''' - self.parent.drawPreview() - saveValueStore = self.savePreset() - saveValueStore['preset'] = self.currentPreset - self.modified.emit(self.compPos, saveValueStore) + def widget(self, parent): + ''' + Call super().widget(*args) to create the component widget + which also auto-connects any common widgets (e.g., checkBoxes) + to self.update(). Then in a subclass connect special actions + (e.g., pushButtons to select a file/colour) and initialize + ''' + self.parent = parent + self.settings = parent.settings + self.page = self.loadUi(self.__class__.ui) + + # Connect widget signals + widgets = { + 'lineEdit': self.page.findChildren(QtWidgets.QLineEdit), + 'checkBox': self.page.findChildren(QtWidgets.QCheckBox), + 'spinBox': self.page.findChildren(QtWidgets.QSpinBox), + 'comboBox': self.page.findChildren(QtWidgets.QComboBox), + } + widgets['spinBox'].extend( + self.page.findChildren(QtWidgets.QDoubleSpinBox) + ) + for widget in widgets['lineEdit']: + widget.textChanged.connect(self.update) + for widget in widgets['checkBox']: + widget.stateChanged.connect(self.update) + for widget in widgets['spinBox']: + widget.valueChanged.connect(self.update) + for widget in widgets['comboBox']: + widget.currentIndexChanged.connect(self.update) + + def trackWidgets(self, trackDict, presetNames=None): + ''' + Name widgets to track in update(), savePreset(), and loadPreset() + Accepts a dict with attribute names as keys and widgets as values. + Optional: a dict of attribute names to map to preset variable names + ''' + self._trackedWidgets = trackDict + if type(presetNames) is dict: + self._presetNames = presetNames - def loadPreset(self, presetDict, presetName): + def update(self): ''' - Subclasses take (presetDict, presetName=None) as args. - Must use super().loadPreset(presetDict, presetName) first, + Reads all tracked widget values into instance attributes + and tells the MainWindow that the component was modified. + Call at the END of your method if you need to subclass this. + ''' + for attr, widget in self._trackedWidgets.items(): + if type(widget) == QtWidgets.QLineEdit: + setattr(self, attr, widget.text()) + elif type(widget) == QtWidgets.QSpinBox \ + or type(widget) == QtWidgets.QDoubleSpinBox: + setattr(self, attr, widget.value()) + elif type(widget) == QtWidgets.QCheckBox: + setattr(self, attr, widget.isChecked()) + elif type(widget) == QtWidgets.QComboBox: + setattr(self, attr, widget.currentIndex()) + if not self.core.openingProject: + self.parent.drawPreview() + saveValueStore = self.savePreset() + saveValueStore['preset'] = self.currentPreset + self.modified.emit(self.compPos, saveValueStore) + + def loadPreset(self, presetDict, presetName=None): + ''' + Subclasses should take (presetDict, *args) as args. + Must use super().loadPreset(presetDict, *args) first, then update self.page widgets using the preset dict. ''' self.currentPreset = presetName \ if presetName is not None else presetDict['preset'] + for attr, widget in self._trackedWidgets.items(): + val = presetDict[ + attr if attr not in self._presetNames + else self._presetNames[attr] + ] + if type(widget) == QtWidgets.QLineEdit: + widget.setText(val) + elif type(widget) == QtWidgets.QSpinBox \ + or type(widget) == QtWidgets.QDoubleSpinBox: + widget.setValue(val) + elif type(widget) == QtWidgets.QCheckBox: + widget.setChecked(val) + elif type(widget) == QtWidgets.QComboBox: + widget.setCurrentIndex(val) + + def savePreset(self): + saveValueStore = {} + for attr, widget in self._trackedWidgets.items(): + saveValueStore[ + attr if attr not in self._presetNames + else self._presetNames[attr] + ] = getattr(self, attr) + return saveValueStore def preFrameRender(self, **kwargs): ''' @@ -151,34 +259,27 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): for key, value in kwargs.items(): setattr(self, key, value) - def command(self, arg): + def commandHelp(self): + '''Help text as string for this component's commandline arguments''' + + def command(self, arg=''): ''' - Configure a component using argument from the commandline. - Use super().command(arg) at the end of a subclass's method, - if no arguments are found in that method first + Configure a component using an arg from the commandline. This is + never called if global args like 'preset=' are found in the arg. + So simply check for any non-global args in your component and + call super().command() at the end to get a Help message. ''' - if arg.startswith('preset='): - _, preset = arg.split('=', 1) - path = os.path.join(getPresetDir(self), preset) - if not os.path.exists(path): - print('Couldn\'t locate preset "%s"' % preset) - quit(1) - else: - print('Opening "%s" preset on layer %s' % ( - preset, self.compPos) - ) - self.core.openPreset(path, self.compPos, preset) - else: - print( - self.__doc__, 'Usage:\n' - 'Open a preset for this component:\n' - ' "preset=Preset Name"') - print(self.commandHelp) - quit(0) + print( + self.__class__.name, 'Usage:\n' + 'Open a preset for this component:\n' + ' "preset=Preset Name"' + ) + self.commandHelp() + quit(0) def loadUi(self, filename): '''Load a Qt Designer ui file to use for this component's widget''' - return uic.loadUi(os.path.join(Core.componentsPath, filename)) + return uic.loadUi(os.path.join(self.core.componentsPath, filename)) def cancel(self): '''Stop any lengthy process in response to this variable.''' @@ -191,16 +292,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): ### Reference methods for creating a new component ### (Inherit from this class and define these) - def widget(self, parent): - self.parent = parent - self.settings = parent.settings - self.page = self.loadUi('example.ui') - # --- connect widget signals here --- - return self.page - def previewRender(self, previewWorker): width = int(self.settings.value('outputWidth')) - height = int(previewWorker.core.settings.value('outputHeight')) + height = int(self.settings.value('outputHeight')) from toolkit.frame import BlankFrame image = BlankFrame(width, height) return image @@ -217,7 +311,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): class BadComponentInit(Exception): ''' - General purpose exception components can raise to indicate + General purpose exception that components can raise to indicate a Python issue with e.g., dynamic creation of instances or something. Decorative for now, may have future use for logging. ''' diff --git a/src/components/color.py b/src/components/color.py index 03371e7..8257ed9 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -13,18 +13,15 @@ class Component(Component): name = 'Color' version = '1.0.0' - def widget(self, parent): - self.parent = parent - self.settings = parent.settings - page = self.loadUi('color.ui') - + def widget(self, *args): self.color1 = (0, 0, 0) self.color2 = (133, 133, 133) self.x = 0 self.y = 0 + super().widget(*args) - page.lineEdit_color1.setText('%s,%s,%s' % self.color1) - page.lineEdit_color2.setText('%s,%s,%s' % self.color2) + self.page.lineEdit_color1.setText('%s,%s,%s' % self.color1) + self.page.lineEdit_color2.setText('%s,%s,%s' % self.color2) btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*self.color1).name() @@ -32,68 +29,55 @@ class Component(Component): btnStyle2 = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*self.color2).name() - page.pushButton_color1.setStyleSheet(btnStyle1) - page.pushButton_color2.setStyleSheet(btnStyle2) - page.pushButton_color1.clicked.connect(lambda: self.pickColor(1)) - page.pushButton_color2.clicked.connect(lambda: self.pickColor(2)) + self.page.pushButton_color1.setStyleSheet(btnStyle1) + self.page.pushButton_color2.setStyleSheet(btnStyle2) + self.page.pushButton_color1.clicked.connect(lambda: self.pickColor(1)) + self.page.pushButton_color2.clicked.connect(lambda: self.pickColor(2)) # disable color #2 until non-default 'fill' option gets changed - page.lineEdit_color2.setDisabled(True) - page.pushButton_color2.setDisabled(True) - page.spinBox_x.valueChanged.connect(self.update) - page.spinBox_y.valueChanged.connect(self.update) - page.spinBox_width.setValue( + self.page.lineEdit_color2.setDisabled(True) + self.page.pushButton_color2.setDisabled(True) + self.page.spinBox_width.setValue( int(self.settings.value("outputWidth"))) - page.spinBox_height.setValue( + self.page.spinBox_height.setValue( int(self.settings.value("outputHeight"))) - page.lineEdit_color1.textChanged.connect(self.update) - page.lineEdit_color2.textChanged.connect(self.update) - page.spinBox_x.valueChanged.connect(self.update) - page.spinBox_y.valueChanged.connect(self.update) - page.spinBox_width.valueChanged.connect(self.update) - page.spinBox_height.valueChanged.connect(self.update) - page.checkBox_trans.stateChanged.connect(self.update) - self.fillLabels = [ 'Solid', 'Linear Gradient', 'Radial Gradient', ] for label in self.fillLabels: - page.comboBox_fill.addItem(label) - page.comboBox_fill.setCurrentIndex(0) - page.comboBox_fill.currentIndexChanged.connect(self.update) - page.comboBox_spread.currentIndexChanged.connect(self.update) - page.spinBox_radialGradient_end.valueChanged.connect(self.update) - page.spinBox_radialGradient_start.valueChanged.connect(self.update) - page.spinBox_radialGradient_spread.valueChanged.connect(self.update) - page.spinBox_linearGradient_end.valueChanged.connect(self.update) - page.spinBox_linearGradient_start.valueChanged.connect(self.update) - page.checkBox_stretch.stateChanged.connect(self.update) - - self.page = page - return page + self.page.comboBox_fill.addItem(label) + self.page.comboBox_fill.setCurrentIndex(0) + + self.trackWidgets( + { + 'x': self.page.spinBox_x, + 'y': self.page.spinBox_y, + 'sizeWidth': self.page.spinBox_width, + 'sizeHeight': self.page.spinBox_height, + 'trans': self.page.checkBox_trans, + 'spread': self.page.comboBox_spread, + 'stretch': self.page.checkBox_stretch, + 'RG_start': self.page.spinBox_radialGradient_start, + 'LG_start': self.page.spinBox_linearGradient_start, + 'RG_end': self.page.spinBox_radialGradient_end, + 'LG_end': self.page.spinBox_linearGradient_end, + 'RG_centre': self.page.spinBox_radialGradient_spread, + 'fillType': self.page.comboBox_fill, + }, presetNames={ + 'sizeWidth': 'width', + 'sizeHeight': 'height', + } + ) def update(self): self.color1 = rgbFromString(self.page.lineEdit_color1.text()) self.color2 = rgbFromString(self.page.lineEdit_color2.text()) - self.x = self.page.spinBox_x.value() - self.y = self.page.spinBox_y.value() - self.sizeWidth = self.page.spinBox_width.value() - self.sizeHeight = self.page.spinBox_height.value() - self.trans = self.page.checkBox_trans.isChecked() - self.spread = self.page.comboBox_spread.currentIndex() - - self.RG_start = self.page.spinBox_radialGradient_start.value() - self.RG_end = self.page.spinBox_radialGradient_end.value() - self.RG_centre = self.page.spinBox_radialGradient_spread.value() - self.stretch = self.page.checkBox_stretch.isChecked() - self.LG_start = self.page.spinBox_linearGradient_start.value() - self.LG_end = self.page.spinBox_linearGradient_end.value() - - self.fillType = self.page.comboBox_fill.currentIndex() - if self.fillType == 0: + + fillType = self.page.comboBox_fill.currentIndex() + if fillType == 0: self.page.lineEdit_color2.setEnabled(False) self.page.pushButton_color2.setEnabled(False) self.page.checkBox_trans.setEnabled(False) @@ -105,10 +89,10 @@ class Component(Component): self.page.checkBox_trans.setEnabled(True) self.page.checkBox_stretch.setEnabled(True) self.page.comboBox_spread.setEnabled(True) - if self.trans: + if self.page.checkBox_trans.isChecked(): self.page.lineEdit_color2.setEnabled(False) self.page.pushButton_color2.setEnabled(False) - self.page.fillWidget.setCurrentIndex(self.fillType) + self.page.fillWidget.setCurrentIndex(fillType) super().update() @@ -181,25 +165,11 @@ class Component(Component): return image.finalize() - def loadPreset(self, pr, presetName=None): - super().loadPreset(pr, presetName) + def loadPreset(self, pr, *args): + super().loadPreset(pr, *args) - self.page.comboBox_fill.setCurrentIndex(pr['fillType']) self.page.lineEdit_color1.setText('%s,%s,%s' % pr['color1']) self.page.lineEdit_color2.setText('%s,%s,%s' % pr['color2']) - self.page.spinBox_x.setValue(pr['x']) - self.page.spinBox_y.setValue(pr['y']) - self.page.spinBox_width.setValue(pr['width']) - self.page.spinBox_height.setValue(pr['height']) - self.page.checkBox_trans.setChecked(pr['trans']) - - self.page.spinBox_radialGradient_start.setValue(pr['RG_start']) - self.page.spinBox_radialGradient_end.setValue(pr['RG_end']) - self.page.spinBox_radialGradient_spread.setValue(pr['RG_centre']) - self.page.spinBox_linearGradient_start.setValue(pr['LG_start']) - self.page.spinBox_linearGradient_end.setValue(pr['LG_end']) - self.page.checkBox_stretch.setChecked(pr['stretch']) - self.page.comboBox_spread.setCurrentIndex(pr['spread']) btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*pr['color1']).name() @@ -209,23 +179,10 @@ class Component(Component): self.page.pushButton_color2.setStyleSheet(btnStyle2) def savePreset(self): - return { - 'color1': self.color1, - 'color2': self.color2, - 'x': self.x, - 'y': self.y, - 'fillType': self.fillType, - 'width': self.sizeWidth, - 'height': self.sizeHeight, - 'trans': self.trans, - 'stretch': self.stretch, - 'spread': self.spread, - 'RG_start': self.RG_start, - 'RG_end': self.RG_end, - 'RG_centre': self.RG_centre, - 'LG_start': self.LG_start, - 'LG_end': self.LG_end, - } + saveValueStore = super().savePreset() + saveValueStore['color1'] = self.color1 + saveValueStore['color2'] = self.color2 + return saveValueStore def pickColor(self, num): RGBstring, btnStyle = pickColor() @@ -242,7 +199,7 @@ class Component(Component): print('Specify a color:\n color=255,255,255') def command(self, arg): - if not arg.startswith('preset=') and '=' in arg: + if '=' in arg: key, arg = arg.split('=', 1) if key == 'color': self.page.lineEdit_color1.setText(arg) diff --git a/src/components/image.py b/src/components/image.py index 591e03e..a705904 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -2,7 +2,6 @@ from PIL import Image, ImageDraw, ImageEnhance from PyQt5 import QtGui, QtCore, QtWidgets import os -from core import Core from component import Component from toolkit.frame import BlankFrame @@ -11,35 +10,26 @@ class Component(Component): name = 'Image' version = '1.0.0' - def widget(self, parent): - self.parent = parent - self.settings = parent.settings - page = self.loadUi('image.ui') - - page.lineEdit_image.textChanged.connect(self.update) - page.pushButton_image.clicked.connect(self.pickImage) - page.spinBox_scale.valueChanged.connect(self.update) - page.spinBox_rotate.valueChanged.connect(self.update) - page.spinBox_color.valueChanged.connect(self.update) - page.checkBox_stretch.stateChanged.connect(self.update) - page.checkBox_mirror.stateChanged.connect(self.update) - page.spinBox_x.valueChanged.connect(self.update) - page.spinBox_y.valueChanged.connect(self.update) - - self.page = page - return page - - def update(self): - self.imagePath = self.page.lineEdit_image.text() - self.scale = self.page.spinBox_scale.value() - self.rotate = self.page.spinBox_rotate.value() - self.color = self.page.spinBox_color.value() - self.xPosition = self.page.spinBox_x.value() - self.yPosition = self.page.spinBox_y.value() - self.stretched = self.page.checkBox_stretch.isChecked() - self.mirror = self.page.checkBox_mirror.isChecked() - - super().update() + def widget(self, *args): + super().widget(*args) + self.page.pushButton_image.clicked.connect(self.pickImage) + self.trackWidgets( + { + 'imagePath': self.page.lineEdit_image, + 'scale': self.page.spinBox_scale, + 'rotate': self.page.spinBox_rotate, + 'color': self.page.spinBox_color, + 'xPosition': self.page.spinBox_x, + 'yPosition': self.page.spinBox_y, + 'stretched': self.page.checkBox_stretch, + 'mirror': self.page.checkBox_mirror, + }, + presetNames={ + 'imagePath': 'image', + 'xPosition': 'x', + 'yPosition': 'y', + }, + ) def previewRender(self, previewWorker): width = int(self.settings.value('outputWidth')) @@ -89,41 +79,18 @@ class Component(Component): return frame - def loadPreset(self, pr, presetName=None): - super().loadPreset(pr, presetName) - self.page.lineEdit_image.setText(pr['image']) - self.page.spinBox_scale.setValue(pr['scale']) - self.page.spinBox_color.setValue(pr['color']) - self.page.spinBox_rotate.setValue(pr['rotate']) - self.page.spinBox_x.setValue(pr['x']) - self.page.spinBox_y.setValue(pr['y']) - self.page.checkBox_stretch.setChecked(pr['stretched']) - self.page.checkBox_mirror.setChecked(pr['mirror']) - - def savePreset(self): - return { - 'image': self.imagePath, - 'scale': self.scale, - 'color': self.color, - 'rotate': self.rotate, - 'stretched': self.stretched, - 'mirror': self.mirror, - 'x': self.xPosition, - 'y': self.yPosition, - } - def pickImage(self): imgDir = self.settings.value("componentDir", os.path.expanduser("~")) filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.page, "Choose Image", imgDir, - "Image Files (%s)" % " ".join(Core.imageFormats)) + "Image Files (%s)" % " ".join(self.core.imageFormats)) if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_image.setText(filename) self.update() def command(self, arg): - if not arg.startswith('preset=') and '=' in arg: + if '=' in arg: key, arg = arg.split('=', 1) if key == 'path' and os.path.exists(arg): try: diff --git a/src/components/original.py b/src/components/original.py index ae40df3..2bda878 100644 --- a/src/components/original.py +++ b/src/components/original.py @@ -18,59 +18,46 @@ class Component(Component): def names(): return ['Original Audio Visualization'] - def widget(self, parent): - self.parent = parent - self.settings = parent.settings + def widget(self, *args): self.visColor = (255, 255, 255) self.scale = 20 self.y = 0 - self.canceled = False - - page = self.loadUi('original.ui') - page.comboBox_visLayout.addItem("Classic") - page.comboBox_visLayout.addItem("Split") - page.comboBox_visLayout.addItem("Bottom") - page.comboBox_visLayout.addItem("Top") - page.comboBox_visLayout.setCurrentIndex(0) - page.comboBox_visLayout.currentIndexChanged.connect(self.update) - page.lineEdit_visColor.setText('%s,%s,%s' % self.visColor) - page.pushButton_visColor.clicked.connect(lambda: self.pickColor()) + super().widget(*args) + + self.page.comboBox_visLayout.addItem("Classic") + self.page.comboBox_visLayout.addItem("Split") + self.page.comboBox_visLayout.addItem("Bottom") + self.page.comboBox_visLayout.addItem("Top") + self.page.comboBox_visLayout.setCurrentIndex(0) + + self.page.lineEdit_visColor.setText('%s,%s,%s' % self.visColor) + self.page.pushButton_visColor.clicked.connect(lambda: self.pickColor()) btnStyle = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*self.visColor).name() - page.pushButton_visColor.setStyleSheet(btnStyle) - page.lineEdit_visColor.textChanged.connect(self.update) - page.spinBox_scale.valueChanged.connect(self.update) - page.spinBox_y.valueChanged.connect(self.update) + self.page.pushButton_visColor.setStyleSheet(btnStyle) - self.page = page - return page + self.trackWidgets({ + 'layout': self.page.comboBox_visLayout, + 'scale': self.page.spinBox_scale, + 'y': self.page.spinBox_y, + }) def update(self): - self.layout = self.page.comboBox_visLayout.currentIndex() self.visColor = rgbFromString(self.page.lineEdit_visColor.text()) - self.scale = self.page.spinBox_scale.value() - self.y = self.page.spinBox_y.value() - super().update() - def loadPreset(self, pr, presetName=None): - super().loadPreset(pr, presetName) + def loadPreset(self, pr, *args): + super().loadPreset(pr, *args) self.page.lineEdit_visColor.setText('%s,%s,%s' % pr['visColor']) btnStyle = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*pr['visColor']).name() self.page.pushButton_visColor.setStyleSheet(btnStyle) - self.page.comboBox_visLayout.setCurrentIndex(pr['layout']) - self.page.spinBox_scale.setValue(pr['scale']) - self.page.spinBox_y.setValue(pr['y']) def savePreset(self): - return { - 'layout': self.layout, - 'visColor': self.visColor, - 'scale': self.scale, - 'y': self.y, - } + saveValueStore = super().savePreset() + saveValueStore['visColor'] = self.visColor + return saveValueStore def previewRender(self, previewWorker): spectrum = numpy.fromfunction( @@ -206,7 +193,7 @@ class Component(Component): return im def command(self, arg): - if not arg.startswith('preset=') and '=' in arg: + if '=' in arg: key, arg = arg.split('=', 1) try: if key == 'color': diff --git a/src/components/sound.py b/src/components/sound.py index 677a22f..dd3cbab 100644 --- a/src/components/sound.py +++ b/src/components/sound.py @@ -10,26 +10,15 @@ class Component(Component): name = 'Sound' version = '1.0.0' - def widget(self, parent): - self.parent = parent - self.settings = parent.settings - page = self.loadUi('sound.ui') - - page.lineEdit_sound.textChanged.connect(self.update) - page.pushButton_sound.clicked.connect(self.pickSound) - page.checkBox_chorus.stateChanged.connect(self.update) - page.spinBox_delay.valueChanged.connect(self.update) - page.spinBox_volume.valueChanged.connect(self.update) - - self.page = page - return page - - def update(self): - self.sound = self.page.lineEdit_sound.text() - self.delay = self.page.spinBox_delay.value() - self.volume = self.page.spinBox_volume.value() - self.chorus = self.page.checkBox_chorus.isChecked() - super().update() + def widget(self, *args): + super().widget(*args) + self.page.pushButton_sound.clicked.connect(self.pickSound) + self.trackWidgets({ + 'sound': self.page.lineEdit_sound, + 'chorus': self.page.checkBox_chorus, + 'delay': self.page.spinBox_delay, + 'volume': self.page.spinBox_volume, + }) def previewRender(self, previewWorker): width = int(self.settings.value('outputWidth')) @@ -67,7 +56,7 @@ class Component(Component): sndDir = self.settings.value("componentDir", os.path.expanduser("~")) filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.page, "Choose Sound", sndDir, - "Audio Files (%s)" % " ".join(Core.audioFormats)) + "Audio Files (%s)" % " ".join(self.core.audioFormats)) if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_sound.setText(filename) @@ -78,30 +67,15 @@ class Component(Component): height = int(self.settings.value('outputHeight')) return BlankFrame(width, height) - def loadPreset(self, pr, presetName=None): - super().loadPreset(pr, presetName) - self.page.lineEdit_sound.setText(pr['sound']) - self.page.checkBox_chorus.setChecked(pr['chorus']) - self.page.spinBox_delay.setValue(pr['delay']) - self.page.spinBox_volume.setValue(pr['volume']) - - def savePreset(self): - return { - 'sound': self.sound, - 'chorus': self.chorus, - 'delay': self.delay, - 'volume': self.volume, - } - def commandHelp(self): print('Path to audio file:\n path=/filepath/to/sound.ogg') def command(self, arg): - if not arg.startswith('preset=') and '=' in arg: + if '=' in arg: key, arg = arg.split('=', 1) if key == 'path': if '*%s' % os.path.splitext(arg)[1] \ - not in Core.audioFormats: + not in self.core.audioFormats: print("Not a supported audio format") quit(1) self.page.lineEdit_sound.setText(arg) diff --git a/src/components/text.py b/src/components/text.py index d511f22..1d64617 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -16,12 +16,10 @@ class Component(Component): super().__init__(*args) self.titleFont = QFont() - def widget(self, parent): - self.parent = parent - self.settings = parent.settings + def widget(self, *args): + super().widget(*args) height = int(self.settings.value('outputHeight')) width = int(self.settings.value('outputWidth')) - self.textColor = (255, 255, 255) self.title = 'Text' self.alignment = 1 @@ -30,40 +28,35 @@ class Component(Component): self.xPosition = width / 2 - fm.width(self.title)/2 self.yPosition = height / 2 * 1.036 - page = self.loadUi('text.ui') - page.comboBox_textAlign.addItem("Left") - page.comboBox_textAlign.addItem("Middle") - page.comboBox_textAlign.addItem("Right") + self.page.comboBox_textAlign.addItem("Left") + self.page.comboBox_textAlign.addItem("Middle") + self.page.comboBox_textAlign.addItem("Right") - page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor) - page.pushButton_textColor.clicked.connect(self.pickColor) + self.page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor) + self.page.pushButton_textColor.clicked.connect(self.pickColor) btnStyle = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*self.textColor).name() - page.pushButton_textColor.setStyleSheet(btnStyle) - - page.lineEdit_title.setText(self.title) - page.comboBox_textAlign.setCurrentIndex(int(self.alignment)) - page.spinBox_fontSize.setValue(int(self.fontSize)) - page.spinBox_xTextAlign.setValue(int(self.xPosition)) - page.spinBox_yTextAlign.setValue(int(self.yPosition)) - - page.fontComboBox_titleFont.currentFontChanged.connect(self.update) - page.lineEdit_title.textChanged.connect(self.update) - page.comboBox_textAlign.currentIndexChanged.connect(self.update) - page.spinBox_xTextAlign.valueChanged.connect(self.update) - page.spinBox_yTextAlign.valueChanged.connect(self.update) - page.spinBox_fontSize.valueChanged.connect(self.update) - page.lineEdit_textColor.textChanged.connect(self.update) - self.page = page - return page + self.page.pushButton_textColor.setStyleSheet(btnStyle) + + self.page.lineEdit_title.setText(self.title) + self.page.comboBox_textAlign.setCurrentIndex(int(self.alignment)) + self.page.spinBox_fontSize.setValue(int(self.fontSize)) + self.page.spinBox_xTextAlign.setValue(int(self.xPosition)) + self.page.spinBox_yTextAlign.setValue(int(self.yPosition)) + + self.page.fontComboBox_titleFont.currentFontChanged.connect( + self.update + ) + self.trackWidgets({ + 'title': self.page.lineEdit_title, + 'alignment': self.page.comboBox_textAlign, + 'fontSize': self.page.spinBox_fontSize, + 'xPosition': self.page.spinBox_xTextAlign, + 'yPosition': self.page.spinBox_yTextAlign, + }) def update(self): - self.title = self.page.lineEdit_title.text() - self.alignment = self.page.comboBox_textAlign.currentIndex() self.titleFont = self.page.fontComboBox_titleFont.currentFont() - self.fontSize = self.page.spinBox_fontSize.value() - self.xPosition = self.page.spinBox_xTextAlign.value() - self.yPosition = self.page.spinBox_yTextAlign.value() self.textColor = rgbFromString( self.page.lineEdit_textColor.text()) btnStyle = "QPushButton { background-color : %s; outline: none; }" \ @@ -87,32 +80,22 @@ class Component(Component): x = self.xPosition - offset return x, self.yPosition - def loadPreset(self, pr, presetName=None): - super().loadPreset(pr, presetName) + def loadPreset(self, pr, *args): + super().loadPreset(pr, *args) - self.page.lineEdit_title.setText(pr['title']) font = QFont() font.fromString(pr['titleFont']) self.page.fontComboBox_titleFont.setCurrentFont(font) - self.page.spinBox_fontSize.setValue(pr['fontSize']) - self.page.comboBox_textAlign.setCurrentIndex(pr['alignment']) - self.page.spinBox_xTextAlign.setValue(pr['xPosition']) - self.page.spinBox_yTextAlign.setValue(pr['yPosition']) self.page.lineEdit_textColor.setText('%s,%s,%s' % pr['textColor']) btnStyle = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*pr['textColor']).name() self.page.pushButton_textColor.setStyleSheet(btnStyle) def savePreset(self): - return { - 'title': self.title, - 'titleFont': self.titleFont.toString(), - 'alignment': self.alignment, - 'fontSize': self.fontSize, - 'xPosition': self.xPosition, - 'yPosition': self.yPosition, - 'textColor': self.textColor - } + saveValueStore = super().savePreset() + saveValueStore['titleFont'] = self.titleFont.toString() + saveValueStore['textColor'] = self.textColor + return saveValueStore def previewRender(self, previewWorker): width = int(self.settings.value('outputWidth')) @@ -158,7 +141,7 @@ class Component(Component): print('Set custom x, y position:\n x=500 y=500') def command(self, arg): - if not arg.startswith('preset=') and '=' in arg: + if '=' in arg: key, arg = arg.split('=', 1) if key == 'color': self.page.lineEdit_textColor.setText(arg) diff --git a/src/components/video.py b/src/components/video.py index 8758b12..677e3ee 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -9,6 +9,7 @@ from queue import PriorityQueue from core import Core from component import Component, BadComponentInit from toolkit.frame import BlankFrame +from toolkit.ffmpeg import testAudioStream from toolkit import openPipe, checkOutput @@ -16,7 +17,7 @@ class Video: '''Video Component Frame-Fetcher''' def __init__(self, **kwargs): mandatoryArgs = [ - 'ffmpeg', # path to ffmpeg, usually Core.FFMPEG_BIN + 'ffmpeg', # path to ffmpeg, usually self.core.FFMPEG_BIN 'videoPath', 'width', 'height', @@ -110,47 +111,40 @@ class Component(Component): name = 'Video' version = '1.0.0' - def widget(self, parent): - self.parent = parent - self.settings = parent.settings - page = self.loadUi('video.ui') + def widget(self, *args): self.videoPath = '' self.badVideo = False self.badAudio = False self.x = 0 self.y = 0 self.loopVideo = False - - page.lineEdit_video.textChanged.connect(self.update) - page.pushButton_video.clicked.connect(self.pickVideo) - page.checkBox_loop.stateChanged.connect(self.update) - page.checkBox_distort.stateChanged.connect(self.update) - page.checkBox_useAudio.stateChanged.connect(self.update) - page.spinBox_scale.valueChanged.connect(self.update) - page.spinBox_volume.valueChanged.connect(self.update) - page.spinBox_x.valueChanged.connect(self.update) - page.spinBox_y.valueChanged.connect(self.update) - - self.page = page - return page + super().widget(*args) + self.page.pushButton_video.clicked.connect(self.pickVideo) + self.trackWidgets( + { + 'videoPath': self.page.lineEdit_video, + 'loopVideo': self.page.checkBox_loop, + 'useAudio': self.page.checkBox_useAudio, + 'distort': self.page.checkBox_distort, + 'scale': self.page.spinBox_scale, + 'volume': self.page.spinBox_volume, + 'xPosition': self.page.spinBox_x, + 'yPosition': self.page.spinBox_y, + }, presetNames={ + 'videoPath': 'video', + 'loopVideo': 'loop', + 'xPosition': 'x', + 'yPosition': 'y', + } + ) def update(self): - self.videoPath = self.page.lineEdit_video.text() - self.loopVideo = self.page.checkBox_loop.isChecked() - self.useAudio = self.page.checkBox_useAudio.isChecked() - self.distort = self.page.checkBox_distort.isChecked() - self.scale = self.page.spinBox_scale.value() - self.volume = self.page.spinBox_volume.value() - self.xPosition = self.page.spinBox_x.value() - self.yPosition = self.page.spinBox_y.value() - - if self.useAudio: + if self.page.checkBox_useAudio.isChecked(): self.page.label_volume.setEnabled(True) self.page.spinBox_volume.setEnabled(True) else: self.page.label_volume.setEnabled(False) self.page.spinBox_volume.setEnabled(False) - super().update() def previewRender(self, previewWorker): @@ -188,18 +182,7 @@ class Component(Component): return "The video selected is corrupt!" def testAudioStream(self): - # test if an audio stream really exists - audioTestCommand = [ - Core.FFMPEG_BIN, - '-i', self.videoPath, - '-vn', '-f', 'null', '-' - ] - try: - checkOutput(audioTestCommand, stderr=subprocess.DEVNULL) - except subprocess.CalledProcessError: - self.badAudio = True - else: - self.badAudio = False + self.badAudio = testAudioStream(self.videoPath) def audio(self): params = {} @@ -214,7 +197,7 @@ class Component(Component): self.blankFrame_ = BlankFrame(width, height) self.updateChunksize(width, height) self.video = Video( - ffmpeg=Core.FFMPEG_BIN, videoPath=self.videoPath, + ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath, width=width, height=height, chunkSize=self.chunkSize, frameRate=int(self.settings.value("outputFrameRate")), parent=self.parent, loopVideo=self.loopVideo, @@ -227,34 +210,11 @@ class Component(Component): else: return self.blankFrame_ - def loadPreset(self, pr, presetName=None): - super().loadPreset(pr, presetName) - self.page.lineEdit_video.setText(pr['video']) - self.page.checkBox_loop.setChecked(pr['loop']) - self.page.checkBox_useAudio.setChecked(pr['useAudio']) - self.page.checkBox_distort.setChecked(pr['distort']) - self.page.spinBox_scale.setValue(pr['scale']) - self.page.spinBox_volume.setValue(pr['volume']) - self.page.spinBox_x.setValue(pr['x']) - self.page.spinBox_y.setValue(pr['y']) - - def savePreset(self): - return { - 'video': self.videoPath, - 'loop': self.loopVideo, - 'useAudio': self.useAudio, - 'distort': self.distort, - 'scale': self.scale, - 'volume': self.volume, - 'x': self.xPosition, - 'y': self.yPosition, - } - def pickVideo(self): imgDir = self.settings.value("componentDir", os.path.expanduser("~")) filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.page, "Choose Video", - imgDir, "Video Files (%s)" % " ".join(Core.videoFormats) + imgDir, "Video Files (%s)" % " ".join(self.core.videoFormats) ) if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) @@ -266,7 +226,7 @@ class Component(Component): return command = [ - self.parent.core.FFMPEG_BIN, + self.core.FFMPEG_BIN, '-thread_queue_size', '512', '-i', self.videoPath, '-f', 'image2pipe', @@ -294,10 +254,10 @@ class Component(Component): self.chunkSize = 4*width*height def command(self, arg): - if not arg.startswith('preset=') and '=' in arg: + if '=' in arg: key, arg = arg.split('=', 1) if key == 'path' and os.path.exists(arg): - if '*%s' % os.path.splitext(arg)[1] in Core.videoFormats: + if '*%s' % os.path.splitext(arg)[1] in self.core.videoFormats: self.page.lineEdit_video.setText(arg) self.page.spinBox_scale.setValue(100) self.page.checkBox_loop.setChecked(True) diff --git a/src/core.py b/src/core.py index f6cf5eb..eb6398b 100644 --- a/src/core.py +++ b/src/core.py @@ -1,5 +1,6 @@ ''' Home to the Core class which tracks program state. Used by GUI & commandline + to create a list of components and create a video thread to export. ''' from PyQt5 import QtCore, QtGui, uic import sys @@ -8,7 +9,6 @@ import json from importlib import import_module import toolkit -from toolkit.ffmpeg import findFfmpeg import video_thread @@ -16,82 +16,21 @@ class Core: ''' MainWindow and Command module both use an instance of this class to store the core program state. This object tracks the components, - talks to the components and handles opening/creating project files - and presets. The class also stores constants as class variables. + talks to the components, handles opening/creating project files + and presets, and creates the video thread to export. + This class also stores constants as class variables. ''' - @classmethod - def storeSettings(cls): - '''Store settings/paths to directories as class variables.''' - if getattr(sys, 'frozen', False): - # frozen - wd = os.path.dirname(sys.executable) - else: - wd = os.path.dirname(os.path.realpath(__file__)) - - dataDir = QtCore.QStandardPaths.writableLocation( - QtCore.QStandardPaths.AppConfigLocation - ) - with open(os.path.join(wd, 'encoder-options.json')) as json_file: - encoderOptions = json.load(json_file) - - settings = { - 'wd': wd, - 'dataDir': dataDir, - 'settings': QtCore.QSettings( - os.path.join(dataDir, 'settings.ini'), - QtCore.QSettings.IniFormat), - 'presetDir': os.path.join(dataDir, 'presets'), - 'componentsPath': os.path.join(wd, 'components'), - 'encoderOptions': encoderOptions, - 'FFMPEG_BIN': findFfmpeg(), - 'canceled': False, - } - - settings['videoFormats'] = toolkit.appendUppercase([ - '*.mp4', - '*.mov', - '*.mkv', - '*.avi', - '*.webm', - '*.flv', - ]) - settings['audioFormats'] = toolkit.appendUppercase([ - '*.mp3', - '*.wav', - '*.ogg', - '*.fla', - '*.flac', - '*.aac', - ]) - settings['imageFormats'] = toolkit.appendUppercase([ - '*.png', - '*.jpg', - '*.tif', - '*.tiff', - '*.gif', - '*.bmp', - '*.ico', - '*.xbm', - '*.xpm', - ]) - - # Register all settings as class variables - for classvar, val in settings.items(): - setattr(cls, classvar, val) - # Make settings accessible to the toolkit package - toolkit.init(settings) - def __init__(self): - Core.storeSettings() - self.findComponents() self.selectedComponents = [] self.savedPresets = {} # copies of presets to detect modification + self.openingProject = False def findComponents(self): + '''Imports all the component modules''' def findComponents(): - for f in sorted(os.listdir(Core.componentsPath)): + for f in os.listdir(Core.componentsPath): name, ext = os.path.splitext(f) if name.startswith("__"): continue @@ -104,8 +43,13 @@ class Core: # store canonical module names and indexes self.moduleIndexes = [i for i in range(len(self.modules))] self.compNames = [mod.Component.name for mod in self.modules] - self.altCompNames = [] + # alphabetize modules by Component name + sortedModules = sorted(zip(self.compNames, self.modules)) + self.compNames = [y[0] for y in sortedModules] + self.modules = [y[1] for y in sortedModules] + # store alternative names for modules + self.altCompNames = [] for i, mod in enumerate(self.modules): if hasattr(mod.Component, 'names'): for name in mod.Component.names(): @@ -116,14 +60,17 @@ class Core: component.compPos = i def insertComponent(self, compPos, moduleIndex, loader): - '''Creates a new component''' + ''' + Creates a new component using these args: + (compPos, moduleIndex in self.modules, MWindow/Command/Core obj) + ''' if compPos < 0 or compPos > len(self.selectedComponents): compPos = len(self.selectedComponents) if len(self.selectedComponents) > 50: return None component = self.modules[moduleIndex].Component( - moduleIndex, compPos + moduleIndex, compPos, self ) self.selectedComponents.insert( compPos, @@ -206,6 +153,7 @@ class Core: errcode, data = self.parseAvFile(filepath) if errcode == 0: + self.openingProject = True try: if hasattr(loader, 'window'): for widget, value in data['WindowFields']: @@ -239,7 +187,8 @@ class Core: i = self.insertComponent( -1, self.moduleIndexFor(name), - loader) + loader + ) if i is None: loader.showMessage(msg="Too many components!") break @@ -284,6 +233,7 @@ class Core: showCancel=False, icon='Warning', detail=msg) + self.openingProject = False def parseAvFile(self, filepath): '''Parses an avp (project) or avl (preset package) file. @@ -467,8 +417,106 @@ class Core: def cancel(self): Core.canceled = True - toolkit.cancel() def reset(self): Core.canceled = False - toolkit.reset() + + @classmethod + def storeSettings(cls): + '''Store settings/paths to directories as class variables''' + from __init__ import wd + from toolkit.ffmpeg import findFfmpeg + + cls.wd = wd + dataDir = QtCore.QStandardPaths.writableLocation( + QtCore.QStandardPaths.AppConfigLocation + ) + with open(os.path.join(wd, 'encoder-options.json')) as json_file: + encoderOptions = json.load(json_file) + + settings = { + 'dataDir': dataDir, + 'settings': QtCore.QSettings( + os.path.join(dataDir, 'settings.ini'), + QtCore.QSettings.IniFormat), + 'presetDir': os.path.join(dataDir, 'presets'), + 'componentsPath': os.path.join(wd, 'components'), + 'encoderOptions': encoderOptions, + 'resolutions': [ + '1920x1080', + '1280x720', + '854x480', + ], + 'windowHasFocus': False, + 'FFMPEG_BIN': findFfmpeg(), + 'canceled': False, + } + + settings['videoFormats'] = toolkit.appendUppercase([ + '*.mp4', + '*.mov', + '*.mkv', + '*.avi', + '*.webm', + '*.flv', + ]) + settings['audioFormats'] = toolkit.appendUppercase([ + '*.mp3', + '*.wav', + '*.ogg', + '*.fla', + '*.flac', + '*.aac', + ]) + settings['imageFormats'] = toolkit.appendUppercase([ + '*.png', + '*.jpg', + '*.tif', + '*.tiff', + '*.gif', + '*.bmp', + '*.ico', + '*.xbm', + '*.xpm', + ]) + + # Register all settings as class variables + for classvar, val in settings.items(): + setattr(cls, classvar, val) + + cls.loadDefaultSettings() + + @classmethod + def loadDefaultSettings(cls): + defaultSettings = { + "outputWidth": 1280, + "outputHeight": 720, + "outputFrameRate": 30, + "outputAudioCodec": "AAC", + "outputAudioBitrate": "192", + "outputVideoCodec": "H264", + "outputVideoBitrate": "2500", + "outputVideoFormat": "yuv420p", + "outputPreset": "medium", + "outputFormat": "mp4", + "outputContainer": "MP4", + "projectDir": os.path.join(cls.dataDir, 'projects'), + "pref_insertCompAtTop": True, + } + + for parm, value in defaultSettings.items(): + if cls.settings.value(parm) is None: + cls.settings.setValue(parm, value) + + # Allow manual editing of prefs. (Surprisingly necessary as Qt seems to + # store True as 'true' but interprets a manually-added 'true' as str.) + for key in cls.settings.allKeys(): + if not key.startswith('pref_'): + continue + val = cls.settings.value(key) + if val in ('true', 'false'): + cls.settings.setValue(key, True if val == 'true' else False) + + +# always store settings in class variables even if a Core object is not created +Core.storeSettings() diff --git a/src/main.py b/src/main.py index 6a9a25e..977da3b 100644 --- a/src/main.py +++ b/src/main.py @@ -2,22 +2,17 @@ from PyQt5 import uic, QtWidgets import sys import os +from __init__ import wd -def main(): - if getattr(sys, 'frozen', False): - # frozen - wd = os.path.dirname(sys.executable) - else: - # unfrozen - wd = os.path.dirname(os.path.realpath(__file__)) - # make local imports work everywhere - sys.path.insert(0, wd) +def main(): + app = QtWidgets.QApplication(sys.argv) + app.setApplicationName("audio-visualizer") + # Determine mode mode = 'GUI' if len(sys.argv) > 2: mode = 'commandline' - elif len(sys.argv) == 2: if sys.argv[1].startswith('-'): mode = 'commandline' @@ -28,11 +23,7 @@ def main(): # normal gui launch proj = None - print('Starting Audio Visualizer in %s mode' % mode) - app = QtWidgets.QApplication(sys.argv) - app.setApplicationName("audio-visualizer") - # app.setOrganizationName("audio-visualizer") - + # Launch program if mode == 'commandline': from command import Command @@ -61,9 +52,7 @@ def main(): signal.signal(signal.SIGINT, main.cleanUp) atexit.register(main.cleanUp) - # applicable to both modes sys.exit(app.exec_()) - if __name__ == "__main__": main() diff --git a/src/mainwindow.py b/src/mainwindow.py index 2d598ae..f333513 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -17,7 +17,7 @@ import time from core import Core import preview_thread from presetmanager import PresetManager -from toolkit import loadDefaultSettings, disableWhenEncoding, checkOutput +from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput class PreviewWindow(QtWidgets.QLabel): @@ -25,6 +25,7 @@ class PreviewWindow(QtWidgets.QLabel): Paints the preview QLabel and maintains the aspect ratio when the window is resized. ''' + def __init__(self, parent, img): super(PreviewWindow, self).__init__() self.parent = parent @@ -49,6 +50,14 @@ class PreviewWindow(QtWidgets.QLabel): self.pixmap = QtGui.QPixmap(img) self.repaint() + @QtCore.pyqtSlot(str) + def threadError(self, msg): + self.parent.showMessage( + msg=msg, + icon='Warning', + parent=self + ) + class MainWindow(QtWidgets.QMainWindow): ''' @@ -66,13 +75,16 @@ class MainWindow(QtWidgets.QMainWindow): def __init__(self, window, project): QtWidgets.QMainWindow.__init__(self) - # print('main thread id: {}'.format(QtCore.QThread.currentThreadId())) self.window = window self.core = Core() - self.pages = [] # widgets of component settings + # widgets of component settings + self.pages = [] self.lastAutosave = time.time() + # list of previous five autosave times, used to reduce update spam + self.autosaveTimes = [] + self.autosaveCooldown = 0.2 self.encoding = False # Create data directory, load/create settings @@ -80,7 +92,6 @@ class MainWindow(QtWidgets.QMainWindow): self.presetDir = Core.presetDir self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') self.settings = Core.settings - loadDefaultSettings(self) self.presetManager = PresetManager( uic.loadUi( os.path.join(Core.wd, 'presetmanager.ui')), self) @@ -92,13 +103,17 @@ class MainWindow(QtWidgets.QMainWindow): if not os.path.exists(neededDirectory): os.mkdir(neededDirectory) - # Make queues/timers for the preview thread + # Create the preview window and its thread, queues, and timers + self.previewWindow = PreviewWindow(self, os.path.join( + Core.wd, "background.png")) + window.verticalLayout_previewWrapper.addWidget(self.previewWindow) + self.previewQueue = Queue() self.previewThread = QtCore.QThread(self) self.previewWorker = preview_thread.Worker(self, self.previewQueue) + self.previewWorker.error.connect(self.previewWindow.threadError) self.previewWorker.moveToThread(self.previewThread) self.previewWorker.imageCreated.connect(self.showPreviewImage) - self.previewWorker.error.connect(self.cleanUp) self.previewThread.start() self.timer = QtCore.QTimer(self) @@ -106,6 +121,7 @@ class MainWindow(QtWidgets.QMainWindow): self.timer.start(500) # Begin decorating the window and connecting events + self.window.installEventFilter(self) componentList = self.window.listWidget_componentList if sys.platform == 'darwin': @@ -168,14 +184,9 @@ class MainWindow(QtWidgets.QMainWindow): window.spinBox_vBitrate.setValue(vBitrate) window.spinBox_aBitrate.setValue(aBitrate) - window.spinBox_vBitrate.valueChanged.connect(self.updateCodecSettings) window.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings) - self.previewWindow = PreviewWindow(self, os.path.join( - Core.wd, "background.png")) - window.verticalLayout_previewWrapper.addWidget(self.previewWindow) - # Make component buttons self.compMenu = QMenu() for i, comp in enumerate(self.core.modules): @@ -204,7 +215,7 @@ class MainWindow(QtWidgets.QMainWindow): currentRes = str(self.settings.value('outputWidth'))+'x' + \ str(self.settings.value('outputHeight')) - for i, res in enumerate(self.resolutions): + for i, res in enumerate(Core.resolutions): window.comboBox_resolution.addItem(res) if res == currentRes: currentRes = i @@ -375,6 +386,7 @@ class MainWindow(QtWidgets.QMainWindow): self.previewThread.quit() self.previewThread.wait() + @disableWhenOpeningProject def updateWindowTitle(self): appName = 'Audio Visualizer' try: @@ -442,13 +454,29 @@ class MainWindow(QtWidgets.QMainWindow): self.settings.setValue('outputVideoBitrate', currentVideoBitrate) self.settings.setValue('outputAudioBitrate', currentAudioBitrate) + @disableWhenOpeningProject def autosave(self, force=False): if not self.currentProject: if os.path.exists(self.autosavePath): os.remove(self.autosavePath) - elif force or time.time() - self.lastAutosave >= 0.2: + elif force or time.time() - self.lastAutosave >= self.autosaveCooldown: self.core.createProjectFile(self.autosavePath, self.window) self.lastAutosave = time.time() + if len(self.autosaveTimes) >= 5: + # Do some math to reduce autosave spam. This gives a smooth + # curve up to 5 seconds cooldown and maintains that for 30 secs + # if a component is continuously updated + timeDiff = self.lastAutosave - self.autosaveTimes.pop() + if not force and timeDiff >= 1.0 \ + and timeDiff <= 10.0: + if self.autosaveCooldown / 4.0 < 0.5: + self.autosaveCooldown += 1.0 + self.autosaveCooldown = ( + 5.0 * (self.autosaveCooldown / 5.0) + ) + (self.autosaveCooldown / 5.0) * 2 + elif force or timeDiff >= self.autosaveCooldown * 5: + self.autosaveCooldown = 0.2 + self.autosaveTimes.insert(0, self.lastAutosave) def autosaveExists(self, identical=True): '''Determines if creating the autosave should be blocked.''' @@ -602,15 +630,20 @@ class MainWindow(QtWidgets.QMainWindow): def updateResolution(self): resIndex = int(self.window.comboBox_resolution.currentIndex()) - res = self.resolutions[resIndex].split('x') + res = Core.resolutions[resIndex].split('x') self.settings.setValue('outputWidth', res[0]) self.settings.setValue('outputHeight', res[1]) self.drawPreview() - def drawPreview(self, force=False): + def drawPreview(self, force=False, **kwargs): + '''Use autosave keyword arg to force saving or not saving if needed''' self.newTask.emit(self.core.selectedComponents) # self.processTask.emit() - self.autosave(force) + if force or 'autosave' in kwargs: + if force or kwargs['autosave']: + self.autosave(True) + else: + self.autosave() self.updateWindowTitle() @QtCore.pyqtSlot(QtGui.QImage) @@ -685,9 +718,13 @@ class MainWindow(QtWidgets.QMainWindow): stackedWidget.insertWidget(newRow, page) componentList.setCurrentRow(newRow) stackedWidget.setCurrentIndex(newRow) - self.drawPreview() + self.drawPreview(True) - def getComponentListRects(self): + def getComponentListMousePos(self, position): + ''' + Given a QPos, returns the component index under the mouse cursor + or -1 if no component is there. + ''' componentList = self.window.listWidget_componentList modelIndexes = [ @@ -698,20 +735,23 @@ class MainWindow(QtWidgets.QMainWindow): componentList.visualRect(modelIndex) for modelIndex in modelIndexes ] - return rects + mousePos = [rect.contains(position) for rect in rects] + if not any(mousePos): + # Not clicking a component + mousePos = -1 + else: + mousePos = mousePos.index(True) + return mousePos @disableWhenEncoding def dragComponent(self, event): '''Used as Qt drop event for the component listwidget''' componentList = self.window.listWidget_componentList - rects = self.getComponentListRects() - - rowPos = [rect.contains(event.pos()) for rect in rects] - if not any(rowPos): - return - - i = rowPos.index(True) - change = (componentList.currentRow() - i) * -1 + mousePos = self.getComponentListMousePos(event.pos()) + if mousePos > -1: + change = (componentList.currentRow() - mousePos) * -1 + else: + change = (componentList.count() - componentList.currentRow() -1) self.moveComponent(change) def changeComponentWidget(self): @@ -814,9 +854,7 @@ class MainWindow(QtWidgets.QMainWindow): self.settings.setValue("projectDir", os.path.dirname(filepath)) # actually load the project using core method self.core.openProject(self, filepath) - if self.window.listWidget_componentList.count() == 0: - self.drawPreview() - self.autosave(True) + self.drawPreview(autosave=False) self.updateWindowTitle() def showMessage(self, **kwargs): @@ -843,20 +881,11 @@ class MainWindow(QtWidgets.QMainWindow): def componentContextMenu(self, QPos): '''Appears when right-clicking the component list''' componentList = self.window.listWidget_componentList - index = componentList.currentRow() - self.menu = QMenu() parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0)) - rects = self.getComponentListRects() - rowPos = [rect.contains(QPos) for rect in rects] - if not any(rowPos): - # Insert components at the top if clicking nothing - rowPos = 0 - else: - rowPos = rowPos.index(True) - - if index == rowPos: + index = self.getComponentListMousePos(QPos) + if index > -1: # Show preset menu if clicking a component self.presetManager.findPresets() menuItem = self.menu.addAction("Save Preset") @@ -891,13 +920,23 @@ class MainWindow(QtWidgets.QMainWindow): # "Add Component" submenu self.submenu = QMenu("Add") self.menu.addMenu(self.submenu) + insertCompAtTop = self.settings.value("pref_insertCompAtTop") for i, comp in enumerate(self.core.modules): menuItem = self.submenu.addAction(comp.Component.name) menuItem.triggered.connect( lambda _, item=i: self.core.insertComponent( - rowPos, item, self + 0 if insertCompAtTop else index, item, self ) - ) + ) self.menu.move(parentPosition + QPos) self.menu.show() + + def eventFilter(self, object, event): + if event.type() == QtCore.QEvent.WindowActivate \ + or event.type() == QtCore.QEvent.FocusIn: + Core.windowHasFocus = True + elif event.type()== QtCore.QEvent.WindowDeactivate \ + or event.type() == QtCore.QEvent.FocusOut: + Core.windowHasFocus = False + return False diff --git a/src/mainwindow.ui b/src/mainwindow.ui index b491323..b43d375 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -22,6 +22,9 @@ 0 + + Qt::StrongFocus + MainWindow diff --git a/src/presetmanager.py b/src/presetmanager.py index 64e2203..643e180 100644 --- a/src/presetmanager.py +++ b/src/presetmanager.py @@ -6,7 +6,8 @@ from PyQt5 import QtCore, QtWidgets import string import os -import toolkit +from toolkit import badName +from core import Core class PresetManager(QtWidgets.QDialog): @@ -151,7 +152,7 @@ class PresetManager(QtWidgets.QDialog): currentPreset ) if OK: - if toolkit.badName(newName): + if badName(newName): self.warnMessage(self.parent.window) continue if newName: @@ -236,7 +237,6 @@ class PresetManager(QtWidgets.QDialog): os.remove(filepath) def warnMessage(self, window=None): - print(window) self.parent.showMessage( msg='Preset names must contain only letters, ' 'numbers, and spaces.', @@ -272,7 +272,7 @@ class PresetManager(QtWidgets.QDialog): self.presetRows[index][2] ) if OK: - if toolkit.badName(newName): + if badName(newName): self.warnMessage() continue if newName: @@ -289,7 +289,7 @@ class PresetManager(QtWidgets.QDialog): self.findPresets() self.drawPresetList() for i, comp in enumerate(self.core.selectedComponents): - if toolkit.getPresetDir(comp) == path \ + if getPresetDir(comp) == path \ and comp.currentPreset == oldName: self.core.openPreset(newPath, i, newName) self.parent.updateComponentTitle(i, False) @@ -338,3 +338,8 @@ class PresetManager(QtWidgets.QDialog): def clearPresetListSelection(self): self.window.listWidget_presets.setCurrentRow(-1) + + +def getPresetDir(comp): + '''Get the preset subdir for a particular version of a component''' + return os.path.join(Core.presetDir, str(comp), str(comp.version)) diff --git a/src/preview_thread.py b/src/preview_thread.py index 3fc73b3..9917e4b 100644 --- a/src/preview_thread.py +++ b/src/preview_thread.py @@ -10,12 +10,13 @@ from queue import Queue, Empty import os from toolkit.frame import Checkerboard +from toolkit import disableWhenOpeningProject class Worker(QtCore.QObject): imageCreated = pyqtSignal(QtGui.QImage) - error = pyqtSignal() + error = pyqtSignal(str) def __init__(self, parent=None, queue=None): QtCore.QObject.__init__(self) @@ -30,6 +31,7 @@ class Worker(QtCore.QObject): height = int(self.settings.value('outputHeight')) self.background = Checkerboard(width, height) + @disableWhenOpeningProject @pyqtSlot(list) def createPreviewImage(self, components): dic = { @@ -48,7 +50,6 @@ class Worker(QtCore.QObject): self.queue.get(block=False) except Empty: continue - if self.background.width != width \ or self.background.height != height: self.background = Checkerboard(width, height) @@ -65,20 +66,12 @@ class Worker(QtCore.QObject): except ValueError as e: errMsg = "Bad frame returned by %s's preview renderer. " \ - "%s. New frame size was %s*%s; should be %s*%s. " \ - "This is a fatal error." % ( + "%s. New frame size was %s*%s; should be %s*%s." % ( str(component), str(e).capitalize(), newFrame.width, newFrame.height, width, height ) - print(errMsg) - self.parent.showMessage( - msg=errMsg, - detail=str(e), - icon='Warning', - parent=None # MainWindow is in a different thread - ) - self.error.emit() + self.error.emit(errMsg) break except RuntimeError as e: print(e) diff --git a/src/toolkit/common.py b/src/toolkit/common.py index 763d582..5fe601f 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py @@ -8,13 +8,6 @@ import sys import subprocess from collections import OrderedDict -from toolkit.core import * - - -def getPresetDir(comp): - '''Get the preset subdirectory for a particular version of a component''' - return os.path.join(Core.presetDir, str(comp), str(comp.version)) - def badName(name): '''Returns whether a name contains non-alphanumeric chars''' @@ -66,14 +59,20 @@ def openPipe(commandList, **kwargs): def disableWhenEncoding(func): - ''' Blocks calls to a function while the video is being exported - in MainWindow. - ''' - def decorator(*args, **kwargs): - if args[0].encoding: + def decorator(self, *args, **kwargs): + if self.encoding: return else: - return func(*args, **kwargs) + return func(self, *args, **kwargs) + return decorator + + +def disableWhenOpeningProject(func): + def decorator(self, *args, **kwargs): + if self.core.openingProject: + return + else: + return func(self, *args, **kwargs) return decorator @@ -108,34 +107,3 @@ def rgbFromString(string): return tup except: return (255, 255, 255) - - -def loadDefaultSettings(self): - ''' - Runs once at each program start-up. Fills in default settings - for any settings not found in settings.ini - ''' - self.resolutions = [ - '1920x1080', - '1280x720', - '854x480' - ] - - default = { - "outputWidth": 1280, - "outputHeight": 720, - "outputFrameRate": 30, - "outputAudioCodec": "AAC", - "outputAudioBitrate": "192", - "outputVideoCodec": "H264", - "outputVideoBitrate": "2500", - "outputVideoFormat": "yuv420p", - "outputPreset": "medium", - "outputFormat": "mp4", - "outputContainer": "MP4", - "projectDir": os.path.join(self.dataDir, 'projects'), - } - - for parm, value in default.items(): - if self.settings.value(parm) is None: - self.settings.setValue(parm, value) diff --git a/src/toolkit/core.py b/src/toolkit/core.py deleted file mode 100644 index a96a684..0000000 --- a/src/toolkit/core.py +++ /dev/null @@ -1,18 +0,0 @@ -class Core: - '''A very complicated class for tracking settings''' - - -def init(settings): - global Core - for classvar, val in settings.items(): - setattr(Core, classvar, val) - - -def cancel(): - global Core - Core.canceled = True - - -def reset(): - global Core - Core.canceled = False diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index cc59a6c..30dc0b3 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -4,18 +4,19 @@ import numpy import sys import os -import subprocess as sp +import subprocess -from toolkit.common import Core, checkOutput, openPipe +import core +from toolkit.common import checkOutput, openPipe def findFfmpeg(): if getattr(sys, 'frozen', False): # The application is frozen if sys.platform == "win32": - return os.path.join(Core.wd, 'ffmpeg.exe') + return os.path.join(core.Core.wd, 'ffmpeg.exe') else: - return os.path.join(Core.wd, 'ffmpeg') + return os.path.join(core.Core.wd, 'ffmpeg') else: if sys.platform == "win32": @@ -27,7 +28,7 @@ def findFfmpeg(): ['ffmpeg', '-version'], stderr=f ) return "ffmpeg" - except sp.CalledProcessError: + except subprocess.CalledProcessError: return "avconv" @@ -37,9 +38,9 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1): ''' if duration == -1: duration = getAudioDuration(inputFile) - safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters duration = "{0:.3f}".format(duration + 0.1) # used by input sources + Core = core.Core # Test if user has libfdk_aac encoders = checkOutput( @@ -213,12 +214,28 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1): return ffmpegCommand +def testAudioStream(filename): + '''Test if an audio stream definitely exists''' + audioTestCommand = [ + core.Core.FFMPEG_BIN, + '-i', filename, + '-vn', '-f', 'null', '-' + ] + try: + checkOutput(audioTestCommand, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError: + return True + else: + return False + + def getAudioDuration(filename): - command = [Core.FFMPEG_BIN, '-i', filename] + '''Try to get duration of audio file as float, or False if not possible''' + command = [core.Core.FFMPEG_BIN, '-i', filename] try: - fileInfo = checkOutput(command, stderr=sp.STDOUT) - except sp.CalledProcessError as ex: + fileInfo = checkOutput(command, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as ex: fileInfo = ex.output info = fileInfo.decode("utf-8").split('\n') @@ -236,13 +253,17 @@ def getAudioDuration(filename): def readAudioFile(filename, parent): + ''' + Creates the completeAudioArray given to components + and used to draw the classic visualizer. + ''' duration = getAudioDuration(filename) if not duration: print('Audio file doesn\'t exist or unreadable.') return command = [ - Core.FFMPEG_BIN, + core.Core.FFMPEG_BIN, '-i', filename, '-f', 's16le', '-acodec', 'pcm_s16le', @@ -250,7 +271,8 @@ def readAudioFile(filename, parent): '-ac', '1', # mono (set to '2' for stereo) '-'] in_pipe = openPipe( - command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8 + command, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8 ) completeAudioArray = numpy.empty(0, dtype="int16") @@ -258,7 +280,7 @@ def readAudioFile(filename, parent): progress = 0 lastPercent = None while True: - if Core.canceled: + if core.Core.canceled: return # read 2 seconds of audio progress += 4 diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index 83fd59e..ca2a054 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -7,7 +7,7 @@ from PIL.ImageQt import ImageQt import sys import os -from toolkit.common import Core +import core class FramePainter(QtGui.QPainter): @@ -57,7 +57,7 @@ def Checkerboard(width, height): ''' image = FloodFrame(1920, 1080, (0, 0, 0, 0)) image.paste(Image.open( - os.path.join(Core.wd, "background.png")), + os.path.join(core.Core.wd, "background.png")), (0, 0) ) image = image.resize((width, height)) diff --git a/src/video_thread.py b/src/video_thread.py index 8517b92..7fe3e02 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -18,6 +18,7 @@ from threading import Thread, Event import time import signal +import core from toolkit import openPipe from toolkit.ffmpeg import readAudioFile, createFfmpegCommand from toolkit.frame import Checkerboard @@ -104,7 +105,8 @@ class Worker(QtCore.QObject): while not self.stopped: audioI, frame = self.previewQueue.get() - if time.time() - self.lastPreview >= 0.06 or audioI == 0: + if core.Core.windowHasFocus \ + and time.time() - self.lastPreview >= 0.06 or audioI == 0: image = Image.alpha_composite(background.copy(), frame) self.imageCreated.emit(QtGui.QImage(ImageQt(image))) self.lastPreview = time.time() @@ -231,7 +233,8 @@ class Worker(QtCore.QObject): self.lastPreview = 0.0 self.previewDispatch = Thread( - target=self.previewDispatch, name="Render Dispatch Thread") + target=self.previewDispatch, name="Render Dispatch Thread" + ) self.previewDispatch.daemon = True self.previewDispatch.start() -- cgit v1.2.3 From d92fc6373fd070f0ea303e9795eb7648d5cd9e90 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 23 Jul 2017 22:55:41 -0400 Subject: ComponentError exception wraps previewRender probably where errors are likeliest to be found --- src/command.py | 6 +++ src/component.py | 119 +++++++++++++++++++++++++++--------------------- src/components/video.py | 6 +-- src/core.py | 3 ++ src/mainwindow.py | 8 ++-- src/toolkit/frame.py | 18 ++++++++ src/video_thread.py | 4 +- 7 files changed, 104 insertions(+), 60 deletions(-) (limited to 'src/toolkit/frame.py') diff --git a/src/command.py b/src/command.py index ca186e5..74ca821 100644 --- a/src/command.py +++ b/src/command.py @@ -146,6 +146,12 @@ class Command(QtCore.QObject): if 'detail' in kwargs: print(kwargs['detail']) + @QtCore.pyqtSlot(str, str) + def videoThreadError(self, msg, detail): + print(msg) + print(detail) + quit(1) + def drawPreview(self, *args): pass diff --git a/src/component.py b/src/component.py index 8b5f1b8..41cb5eb 100644 --- a/src/component.py +++ b/src/component.py @@ -6,54 +6,64 @@ from PyQt5 import uic, QtCore, QtWidgets import os -def commandWrapper(func): - '''Intercepts each component's command() method to check for global args''' - def decorator(self, arg): - if arg.startswith('preset='): - from presetmanager import getPresetDir - _, preset = arg.split('=', 1) - path = os.path.join(getPresetDir(self), preset) - if not os.path.exists(path): - print('Couldn\'t locate preset "%s"' % preset) - quit(1) - else: - print('Opening "%s" preset on layer %s' % ( - preset, self.compPos) - ) - self.core.openPreset(path, self.compPos, preset) - # Don't call the component's command() method - return - else: - return func(self, arg) - return decorator - - -def propertiesWrapper(func): - '''Intercepts the usual properties if the properties are locked.''' - def decorator(self): - if self._lockedProperties is not None: - return self._lockedProperties - else: - return func(self) - return decorator - - -def errorWrapper(func): - '''Intercepts the usual error message if it is locked.''' - def decorator(self): - if self._lockedError is not None: - return self._lockedError - else: - return func(self) - return decorator - - class ComponentMetaclass(type(QtCore.QObject)): ''' Checks the validity of each Component class imported, and mutates some attributes for easier use by the core program. E.g., takes only major version from version string & decorates methods ''' + + def renderWrapper(func): + def decorator(self, *args, **kwargs): + try: + return func(self, *args, **kwargs) + except: + from toolkit.frame import BlankFrame + try: + raise ComponentError(self, 'renderer', immediate=True) + except ComponentError: + return BlankFrame() + return decorator + + def commandWrapper(func): + '''Intercepts each component's command() method to check for global args''' + def decorator(self, arg): + if arg.startswith('preset='): + from presetmanager import getPresetDir + _, preset = arg.split('=', 1) + path = os.path.join(getPresetDir(self), preset) + if not os.path.exists(path): + print('Couldn\'t locate preset "%s"' % preset) + quit(1) + else: + print('Opening "%s" preset on layer %s' % ( + preset, self.compPos) + ) + self.core.openPreset(path, self.compPos, preset) + # Don't call the component's command() method + return + else: + return func(self, arg) + return decorator + + def propertiesWrapper(func): + '''Intercepts the usual properties if the properties are locked.''' + def decorator(self): + if self._lockedProperties is not None: + return self._lockedProperties + else: + return func(self) + return decorator + + def errorWrapper(func): + '''Intercepts the usual error message if it is locked.''' + def decorator(self): + if self._lockedError is not None: + return self._lockedError + else: + return func(self) + return decorator + def __new__(cls, name, parents, attrs): if 'ui' not in attrs: # Use module name as ui filename by default @@ -62,7 +72,11 @@ class ComponentMetaclass(type(QtCore.QObject)): )[0] # if parents[0] == QtCore.QObject: else: - decorate = ('names', 'error', 'audio', 'command', 'properties') + decorate = ( + 'names', # Class methods + 'error', 'audio', 'properties', # Properties + 'previewRender', 'command', + ) # Auto-decorate methods for key in decorate: @@ -76,13 +90,16 @@ class ComponentMetaclass(type(QtCore.QObject)): attrs[key] = property(attrs[key]) if key == 'command': - attrs[key] = commandWrapper(attrs[key]) + attrs[key] = cls.commandWrapper(attrs[key]) + + if key == 'previewRender': + attrs[key] = cls.renderWrapper(attrs[key]) if key == 'properties': - attrs[key] = propertiesWrapper(attrs[key]) + attrs[key] = cls.propertiesWrapper(attrs[key]) if key == 'error': - attrs[key] = errorWrapper(attrs[key]) + attrs[key] = cls.errorWrapper(attrs[key]) # Turn version string into a number try: @@ -223,11 +240,11 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): if kwarg in ('presetNames', 'commandArgs'): setattr(self, '_%s' % kwarg, kwargs[kwarg]) else: - raise BadComponentInit( + raise ComponentError( self, 'Nonsensical keywords to trackWidgets.', immediate=True) - except BadComponentInit: + except ComponentError: continue def update(self): @@ -366,7 +383,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): ''' -class BadComponentInit(AttributeError): +class ComponentError(RuntimeError): ''' Indicates a Python error in constructing a component. Raising this locks the component into an error state, @@ -397,9 +414,7 @@ class BadComponentInit(AttributeError): ) if immediate: - caller.parent.showMessage( - msg=string, detail=detail, icon='Warning' - ) + caller._error.emit(string, detail) else: caller.lockProperties(['error']) caller.lockError((string, detail)) diff --git a/src/components/video.py b/src/components/video.py index d3696d4..383531e 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -7,7 +7,7 @@ import threading from queue import PriorityQueue from core import Core -from component import Component, BadComponentInit +from component import Component, ComponentError from toolkit.frame import BlankFrame from toolkit.ffmpeg import testAudioStream from toolkit import openPipe, checkOutput @@ -195,14 +195,14 @@ class Component(Component): self.updateChunksize(width, height) try: self.video = Video( - ffmpeg=self.core.FFMPEG_BIN, #videoPath=self.videoPath, + ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath, width=width, height=height, chunkSize=self.chunkSize, frameRate=int(self.settings.value("outputFrameRate")), parent=self.parent, loopVideo=self.loopVideo, component=self, scale=self.scale ) if os.path.exists(self.videoPath) else None except KeyError: - raise BadComponentInit(self, 'Frame Fetcher initialization') + raise ComponentError(self, 'Frame Fetcher initialization') def frameRender(self, layerNo, frameNo): if self.video: diff --git a/src/core.py b/src/core.py index 2f9c36c..4c08c04 100644 --- a/src/core.py +++ b/src/core.py @@ -76,6 +76,9 @@ class Core: component ) self.componentListChanged() + self.selectedComponents[compPos]._error.connect( + loader.videoThreadError + ) # init component's widget for loading/saving presets self.selectedComponents[compPos].widget(loader) diff --git a/src/mainwindow.py b/src/mainwindow.py index a32c1b4..03b8dde 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -578,7 +578,11 @@ class MainWindow(QtWidgets.QMainWindow): detail=detail, icon='Warning', ) - self.stopVideo() + try: + self.stopVideo() + except AttributeError as e: + if 'videoWorker' not in str(e): + raise def changeEncodingStatus(self, status): self.encoding = status @@ -684,8 +688,6 @@ class MainWindow(QtWidgets.QMainWindow): # connect to signal that adds an asterisk when modified self.core.selectedComponents[index].modified.connect( self.updateComponentTitle) - self.core.selectedComponents[index]._error.connect( - self.videoThreadError) self.pages.insert(index, self.core.selectedComponents[index].page) stackedWidget.insertWidget(index, self.pages[index]) diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index ca2a054..b66e037 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -41,15 +41,33 @@ class PaintColor(QtGui.QColor): super().__init__(b, g, r, a) +def defaultSize(framefunc): + '''Makes width/height arguments optional''' + def decorator(*args): + if len(args) < 2: + newArgs = list(args) + if len(args) == 0 or len(args) == 1: + height = int(core.Core.settings.value("outputHeight")) + newArgs.append(height) + if len(args) == 0: + width = int(core.Core.settings.value("outputWidth")) + newArgs.insert(0, width) + args = tuple(newArgs) + return framefunc(*args) + return decorator + + def FloodFrame(width, height, RgbaTuple): return Image.new("RGBA", (width, height), RgbaTuple) +@defaultSize def BlankFrame(width, height): '''The base frame used by each component to start drawing.''' return FloodFrame(width, height, (0, 0, 0, 0)) +@defaultSize def Checkerboard(width, height): ''' A checkerboard to represent transparency to the user. diff --git a/src/video_thread.py b/src/video_thread.py index 68eae4f..dd957e5 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -18,7 +18,7 @@ from threading import Thread, Event import time import signal -from component import BadComponentInit +from component import ComponentError from toolkit import openPipe from toolkit.ffmpeg import readAudioFile, createFfmpegCommand from toolkit.frame import Checkerboard @@ -160,7 +160,7 @@ class Worker(QtCore.QObject): progressBarUpdate=self.progressBarUpdate, progressBarSetText=self.progressBarSetText ) - except BadComponentInit: + except ComponentError: pass if 'error' in comp.properties(): -- cgit v1.2.3 From 1297af61c9ce00b6dd76f8ec690baedf5bf887c7 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 29 Jul 2017 20:27:46 -0400 Subject: waveform component is working, preview is glitchy --- src/components/original.py | 3 + src/components/video.py | 10 ++-- src/components/waveform.py | 134 +++++++++++++++++++++++++++++++-------------- src/components/waveform.ui | 95 +++++++++++++++++++++++++++++++- src/toolkit/common.py | 21 ------- src/toolkit/ffmpeg.py | 30 ++++++++-- src/toolkit/frame.py | 12 ++++ src/video_thread.py | 38 +++++++++---- 8 files changed, 256 insertions(+), 87 deletions(-) (limited to 'src/toolkit/frame.py') diff --git a/src/components/original.py b/src/components/original.py index 3d1a574..621af6f 100644 --- a/src/components/original.py +++ b/src/components/original.py @@ -18,6 +18,9 @@ class Component(Component): def names(*args): return ['Original Audio Visualization'] + def properties(self): + return ['pcm'] + def widget(self, *args): self.visColor = (255, 255, 255) self.scale = 20 diff --git a/src/components/video.py b/src/components/video.py index d3460ff..6cd16e5 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -4,10 +4,10 @@ import os import math import subprocess -from component import Component, ComponentError -from toolkit.frame import BlankFrame -from toolkit.ffmpeg import testAudioStream, FfmpegVideo -from toolkit import openPipe, closePipe, checkOutput, scale +from component import Component +from toolkit.frame import BlankFrame, scale +from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo +from toolkit import checkOutput class Component(Component): @@ -132,7 +132,7 @@ class Component(Component): ] command.extend(self.makeFfmpegFilter()) command.extend([ - '-vcodec', 'rawvideo', '-', + '-codec:v', 'rawvideo', '-', '-ss', '90', '-frames:v', '1', ]) diff --git a/src/components/waveform.py b/src/components/waveform.py index 487a3bb..375b3fc 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -5,10 +5,10 @@ import os import math import subprocess -from component import Component, ComponentError -from toolkit.frame import BlankFrame -from toolkit import openPipe, checkOutput, rgbFromString -from toolkit.ffmpeg import FfmpegVideo +from component import Component +from toolkit.frame import BlankFrame, scale +from toolkit import checkOutput, rgbFromString, pickColor +from toolkit.ffmpeg import openPipe, closePipe, getAudioDuration, FfmpegVideo class Component(Component): @@ -21,17 +21,27 @@ class Component(Component): self.page.lineEdit_color.setText('%s,%s,%s' % self.color) btnStyle = "QPushButton { background-color : %s; outline: none; }" \ - % QColor(*self.color1).name() - self.page.lineEdit_color.setStylesheet(btnStyle) + % QColor(*self.color).name() + self.page.pushButton_color.setStyleSheet(btnStyle) self.page.pushButton_color.clicked.connect(lambda: self.pickColor()) + self.page.spinBox_scale.valueChanged.connect(self.updateChunksize) + + if hasattr(self.parent, 'window'): + self.parent.window.lineEdit_audioFile.textChanged.connect( + self.update + ) self.trackWidgets( { 'mode': self.page.comboBox_mode, + 'amplitude': self.page.comboBox_amplitude, 'x': self.page.spinBox_x, 'y': self.page.spinBox_y, 'mirror': self.page.checkBox_mirror, 'scale': self.page.spinBox_scale, + 'opacity': self.page.spinBox_opacity, + 'compress': self.page.checkBox_compress, + 'mono': self.page.checkBox_mono, } ) @@ -42,6 +52,26 @@ class Component(Component): self.page.pushButton_color.setStyleSheet(btnStyle) super().update() + def loadPreset(self, pr, *args): + super().loadPreset(pr, *args) + + self.page.lineEdit_color.setText('%s,%s,%s' % pr['color']) + btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + % QColor(*pr['color']).name() + self.page.pushButton_color.setStyleSheet(btnStyle) + + def savePreset(self): + saveValueStore = super().savePreset() + saveValueStore['color'] = self.color + return saveValueStore + + def pickColor(self): + RGBstring, btnStyle = pickColor() + if not RGBstring: + return + self.page.lineEdit_color.setText(RGBstring) + self.page.pushButton_color.setStyleSheet(btnStyle) + def previewRender(self): self.updateChunksize() frame = self.getPreviewFrame(self.width, self.height) @@ -53,10 +83,11 @@ class Component(Component): def preFrameRender(self, **kwargs): super().preFrameRender(**kwargs) self.updateChunksize() + w, h = scale(self.scale, self.width, self.height, str) self.video = FfmpegVideo( inputPath=self.audioFile, - filter_=makeFfmpegFilter(), - width=self.width, height=self.height, + filter_=self.makeFfmpegFilter(), + width=w, height=h, chunkSize=self.chunkSize, frameRate=int(self.settings.value("outputFrameRate")), parent=self.parent, component=self, @@ -65,7 +96,7 @@ class Component(Component): def frameRender(self, frameNo): if FfmpegVideo.threadError is not None: raise FfmpegVideo.threadError - return finalizeFrame(self.video.frame(frameNo)) + return self.finalizeFrame(self.video.frame(frameNo)) def postFrameRender(self): closePipe(self.video.pipe) @@ -74,18 +105,25 @@ class Component(Component): inputFile = self.parent.window.lineEdit_audioFile.text() if not inputFile or not os.path.exists(inputFile): return + duration = getAudioDuration(inputFile) + if not duration: + return + startPt = duration / 3 command = [ self.core.FFMPEG_BIN, '-thread_queue_size', '512', + '-r', self.settings.value("outputFrameRate"), + '-ss', "{0:.3f}".format(startPt), '-i', inputFile, '-f', 'image2pipe', '-pix_fmt', 'rgba', ] - command.extend(self.makeFfmpegFilter()) + command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt)) command.extend([ - '-vcodec', 'rawvideo', '-', - '-ss', '90', + '-an', + '-s:v', '%sx%s' % scale(self.scale, self.width, self.height, str), + '-codec:v', 'rawvideo', '-', '-frames:v', '1', ]) pipe = openPipe( @@ -95,45 +133,57 @@ class Component(Component): byteFrame = pipe.stdout.read(self.chunkSize) closePipe(pipe) - frame = finalizeFrame(self, byteFrame, width, height) + frame = self.finalizeFrame(byteFrame) return frame - def makeFfmpegFilter(self): + def makeFfmpegFilter(self, preview=False, startPt=0): w, h = scale(self.scale, self.width, self.height, str) + if self.amplitude == 0: + amplitude = 'lin' + elif self.amplitude == 1: + amplitude = 'log' + elif self.amplitude == 2: + amplitude = 'sqrt' + elif self.amplitude == 3: + amplitude = 'cbrt' + hexcolor = QColor(*self.color).name() + opacity = "{0:.1f}".format(self.opacity / 100) + return [ '-filter_complex', - '[0:a] showwaves=s=%sx%s:mode=%s,format=rgba [v]' % ( - w, h, self.mode, + '[0:a] %s%s' + 'showwaves=r=30:s=%sx%s:mode=%s:colors=%s@%s:scale=%s%s%s [v1]; ' + '[v1] scale=%s:%s%s [v]' % ( + 'compand=gain=2,' if self.compress else '', + 'aformat=channel_layouts=mono,' if self.mono else '', + self.settings.value("outputWidth"), + self.settings.value("outputHeight"), + str(self.page.comboBox_mode.currentText()).lower(), + hexcolor, opacity, amplitude, + ', drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=4:color=%s@%s' % ( + hexcolor, opacity + ) if self.mode < 2 else '', + ', hflip' if self.mirror else'', + w, h, + ', trim=duration=%s' % "{0:.3f}".format(startPt + 1) if preview else '', ), '-map', '[v]', - '-map', '0:a', ] def updateChunksize(self): - if self.scale != 100: - width, height = scale(self.scale, self.width, self.height, int) - else: - width, height = self.width, self.height + width, height = scale(self.scale, self.width, self.height, int) self.chunkSize = 4 * width * height - -def scale(scale, width, height, returntype=None): - width = (float(width) / 100.0) * float(scale) - height = (float(height) / 100.0) * float(scale) - if returntype == str: - return (str(math.ceil(width)), str(math.ceil(height))) - elif returntype == int: - return (math.ceil(width), math.ceil(height)) - else: - return (width, height) - - -def finalizeFrame(self, imageData, width, height): - # frombytes goes here - if self.scale != 100 \ - or self.x != 0 or self.y != 0: - frame = BlankFrame(width, height) - frame.paste(image, box=(self.x, self.y)) - else: - frame = image - return frame + def finalizeFrame(self, imageData): + image = Image.frombytes( + 'RGBA', + scale(self.scale, self.width, self.height, int), + imageData + ) + if self.scale != 100 \ + or self.x != 0 or self.y != 0: + frame = BlankFrame(self.width, self.height) + frame.paste(image, box=(self.x, self.y)) + else: + frame = image + return frame diff --git a/src/components/waveform.ui b/src/components/waveform.ui index 5d62150..0e40380 100644 --- a/src/components/waveform.ui +++ b/src/components/waveform.ui @@ -226,9 +226,31 @@ - + - Mirror + Opacity + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + QAbstractSpinBox::UpDownArrows + + + % + + + 10 + + + 400 + + + 100 @@ -263,6 +285,75 @@ + + + + + + Compress + + + + + + + Mono + + + + + + + Mirror + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Amplitude + + + + + + + + Linear + + + + + Logarithmic + + + + + Square root + + + + + Cubic root + + + + + + diff --git a/src/toolkit/common.py b/src/toolkit/common.py index 128ed08..5d424e0 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py @@ -6,22 +6,9 @@ import string import os import sys import subprocess -import signal -import math from collections import OrderedDict -def scale(scale, width, height, returntype=None): - width = (float(width) / 100.0) * float(scale) - height = (float(height) / 100.0) * float(scale) - if returntype == str: - return (str(math.ceil(width)), str(math.ceil(height))) - elif returntype == int: - return (math.ceil(width), math.ceil(height)) - else: - return (width, height) - - def badName(name): '''Returns whether a name contains non-alphanumeric chars''' return any([letter in string.punctuation for letter in name]) @@ -69,14 +56,6 @@ def checkOutput(commandList, **kwargs): return subprocess.check_output(commandList, **kwargs) -@pipeWrapper -def openPipe(commandList, **kwargs): - return subprocess.Popen(commandList, **kwargs) - -def closePipe(pipe): - pipe.stdout.close() - pipe.send_signal(signal.SIGINT) - def disableWhenEncoding(func): def decorator(self, *args, **kwargs): if self.encoding: diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index fea9d4e..e37282f 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -6,10 +6,12 @@ import sys import os import subprocess import threading +import signal from queue import PriorityQueue import core -from toolkit.common import checkOutput, openPipe +from toolkit.common import checkOutput, pipeWrapper +from component import ComponentError class FfmpegVideo: @@ -60,7 +62,8 @@ class FfmpegVideo: kwargs['filter_'] ) self.command.extend([ - '-vcodec', 'rawvideo', '-', + '-s:v', '%sx%s' % (self.width, self.height), + '-codec:v', 'rawvideo', '-', ]) self.frameBuffer = PriorityQueue() @@ -85,9 +88,11 @@ class FfmpegVideo: self.frameBuffer.task_done() def fillBuffer(self): + import sys + print(self.command) self.pipe = openPipe( self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, bufsize=10**8 + stderr=sys.__stdout__, bufsize=10**8 ) while True: if self.parent.canceled: @@ -100,7 +105,7 @@ class FfmpegVideo: self.frameBuffer.put((self.frameNo-1, self.lastFrame)) continue except AttributeError: - Video.threadError = ComponentError(self.component, 'video') + FfmpegVideo.threadError = ComponentError(self.component, 'video') break self.currentFrame = self.pipe.stdout.read(self.chunkSize) @@ -109,6 +114,16 @@ class FfmpegVideo: self.lastFrame = self.currentFrame +@pipeWrapper +def openPipe(commandList, **kwargs): + return subprocess.Popen(commandList, **kwargs) + + +def closePipe(pipe): + pipe.stdout.close() + pipe.send_signal(signal.SIGINT) + + def findFfmpeg(): if getattr(sys, 'frozen', False): # The application is frozen @@ -347,7 +362,12 @@ def getAudioDuration(filename): except subprocess.CalledProcessError as ex: fileInfo = ex.output - info = fileInfo.decode("utf-8").split('\n') + try: + info = fileInfo.decode("utf-8").split('\n') + except UnicodeDecodeError as e: + print('Unicode error:', str(e)) + return False + for line in info: if 'Duration' in line: d = line.split(',')[0] diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index b66e037..f42d4c9 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -6,6 +6,7 @@ from PIL import Image from PIL.ImageQt import ImageQt import sys import os +import math import core @@ -41,6 +42,17 @@ class PaintColor(QtGui.QColor): super().__init__(b, g, r, a) +def scale(scale, width, height, returntype=None): + width = (float(width) / 100.0) * float(scale) + height = (float(height) / 100.0) * float(scale) + if returntype == str: + return (str(math.ceil(width)), str(math.ceil(height))) + elif returntype == int: + return (math.ceil(width), math.ceil(height)) + else: + return (width, height) + + def defaultSize(framefunc): '''Makes width/height arguments optional''' def decorator(*args): diff --git a/src/video_thread.py b/src/video_thread.py index f27ec21..5963def 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -19,9 +19,11 @@ import time import signal from component import ComponentError -from toolkit import openPipe -from toolkit.ffmpeg import readAudioFile, createFfmpegCommand from toolkit.frame import Checkerboard +from toolkit.ffmpeg import ( + openPipe, readAudioFile, + getAudioDuration, createFfmpegCommand +) class Worker(QtCore.QObject): @@ -132,15 +134,24 @@ class Worker(QtCore.QObject): # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # READ AUDIO, INITIALIZE COMPONENTS, OPEN A PIPE TO FFMPEG # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ - - self.progressBarSetText.emit("Loading audio file...") - audioFileTraits = readAudioFile( - self.inputFile, self - ) - if audioFileTraits is None: - self.cancelExport() - return - self.completeAudioArray, duration = audioFileTraits + if any([ + True if 'pcm' in comp.properties() else False + for comp in self.components + ]): + self.progressBarSetText.emit("Loading audio file...") + audioFileTraits = readAudioFile( + self.inputFile, self + ) + if audioFileTraits is None: + self.cancelExport() + return + self.completeAudioArray, duration = audioFileTraits + else: + duration = getAudioDuration(self.inputFile) + class FakeList: + def __len__(self): + return int((duration * 44100) + 44100) - 1470 + self.completeAudioArray = FakeList() self.progressBarUpdate.emit(0) self.progressBarSetText.emit("Starting components...") @@ -284,7 +295,10 @@ class Worker(QtCore.QObject): numpy.seterr(all='print') - self.out_pipe.stdin.close() + try: + self.out_pipe.stdin.close() + except BrokenPipeError: + print('Broken pipe to ffmpeg!') if self.out_pipe.stderr is not None: print(self.out_pipe.stderr.read()) self.out_pipe.stderr.close() -- cgit v1.2.3 From 3c1b52205f183e9a2c943c5f666ed2c01db3aaf5 Mon Sep 17 00:00:00 2001 From: tassaron Date: Tue, 1 Aug 2017 17:57:39 -0400 Subject: 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(-) (limited to 'src/toolkit/frame.py') 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: -- cgit v1.2.3 From 354637d34c201b9389b9085889275d6850873d08 Mon Sep 17 00:00:00 2001 From: tassaron Date: Tue, 8 Aug 2017 05:57:19 -0400 Subject: relative stroke px size & no Qt pen on stroke --- src/components/text.py | 3 ++- src/toolkit/frame.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) (limited to 'src/toolkit/frame.py') diff --git a/src/components/text.py b/src/components/text.py index c50c812..46fb001 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -53,7 +53,7 @@ class Component(Component): 'textColor': self.page.pushButton_textColor, 'strokeColor': self.page.pushButton_strokeColor, }, relativeWidgets=[ - 'xPosition', 'yPosition', 'fontSize', + 'xPosition', 'yPosition', 'fontSize', 'stroke' ]) self.centerXY() @@ -147,6 +147,7 @@ class Component(Component): path = QtGui.QPainterPath() path.addText(x, y, font, self.title) path = outliner.createStroke(path) + image.setPen(QtCore.Qt.NoPen) image.setBrush(PaintColor(*self.strokeColor)) image.drawPath(path) diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index c007188..7e83d58 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -21,8 +21,11 @@ class FramePainter(QtGui.QPainter): self.image = QtGui.QImage(ImageQt(image)) super().__init__(self.image) - def setPen(self, RgbTuple): - super().setPen(PaintColor(*RgbTuple)) + def setPen(self, penStyle): + if type(penStyle) is tuple: + super().setPen(PaintColor(*penStyle)) + else: + super().setPen(penStyle) def finalize(self): self.end() -- cgit v1.2.3 From 1c4afc96d69789f16284c067ffd7098dc7b2ca70 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 10 Aug 2017 16:04:41 -0400 Subject: using the builtin logging module --- src/component.py | 14 ++++++--- src/components/spectrum.py | 16 ++++++---- src/components/video.py | 19 +++++++++--- src/components/waveform.py | 18 +++++++++--- src/core.py | 73 +++++++++++++++++++++++++++++++++++++++++----- src/main.py | 6 ++++ src/mainwindow.py | 57 +++++++++++++++++++++++++++--------- src/preview_thread.py | 9 ++++-- src/toolkit/ffmpeg.py | 19 +++++++----- src/toolkit/frame.py | 6 ++++ src/video_thread.py | 24 ++++++++++----- 11 files changed, 206 insertions(+), 55 deletions(-) (limited to 'src/toolkit/frame.py') diff --git a/src/component.py b/src/component.py index 5b6f9a7..a1e24db 100644 --- a/src/component.py +++ b/src/component.py @@ -8,6 +8,7 @@ import os import sys import math import time +import logging from toolkit.frame import BlankFrame from toolkit import ( @@ -15,6 +16,9 @@ from toolkit import ( ) +log = logging.getLogger('AVP.ComponentHandler') + + class ComponentMetaclass(type(QtCore.QObject)): ''' Checks the validity of each Component class and mutates some attrs. @@ -135,17 +139,17 @@ class ComponentMetaclass(type(QtCore.QObject)): # Turn version string into a number try: if 'version' not in attrs: - print( + log.error( 'No version attribute in %s. Defaulting to 1' % attrs['name']) attrs['version'] = 1 else: attrs['version'] = int(attrs['version'].split('.')[0]) except ValueError: - print('%s component has an invalid version string:\n%s' % ( + log.critical('%s component has an invalid version string:\n%s' % ( attrs['name'], str(attrs['version']))) except KeyError: - print('%s component has no version string.' % attrs['name']) + log.critical('%s component has no version string.' % attrs['name']) else: return super().__new__(cls, name, parents, attrs) quit(1) @@ -546,6 +550,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): and oldRelativeVal != newRelativeVal: # Float changed without pixel value changing, which # means the pixel value needs to be updated + log.debug('Updating %s #%s\'s relative widget: %s' % ( + self.name, self.compPos, attr)) self._trackedWidgets[attr].blockSignals(True) self.updateRelativeWidgetMaximum(attr) pixelVal = self.pixelValForAttr(attr, oldRelativeVal) @@ -576,7 +582,7 @@ class ComponentError(RuntimeError): msg = str(sys.exc_info()[1]) else: msg = 'Unknown error.' - print("##### ComponentError by %s's %s: %s" % ( + log.error("ComponentError by %s's %s: %s" % ( caller.name, name, msg)) # Don't create multiple windows for quickly repeated messages diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 666e20a..32763c0 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -4,6 +4,7 @@ import os import math import subprocess import time +import logging from component import Component from toolkit.frame import BlankFrame, scale @@ -13,6 +14,9 @@ from toolkit.ffmpeg import ( ) +log = logging.getLogger('AVP.Components.Spectrum') + + class Component(Component): name = 'Spectrum' version = '1.0.0' @@ -68,6 +72,7 @@ class Component(Component): if not changedSize \ and not self.changedOptions \ and self.previewFrame is not None: + log.debug('Comp #%s is reusing old preview frame' % self.compPos) return self.previewFrame frame = self.getPreviewFrame() @@ -131,13 +136,14 @@ class Component(Component): '-frames:v', '1', ]) logFilename = os.path.join( - self.core.dataDir, 'preview_%s.log' % str(self.compPos)) - with open(logFilename, 'w') as log: - log.write(" ".join(command) + '\n\n') - with open(logFilename, 'a') as log: + self.core.logDir, 'preview_%s.log' % str(self.compPos)) + log.debug('Creating ffmpeg process (log at %s)' % logFilename) + with open(logFilename, 'w') as logf: + logf.write(" ".join(command) + '\n\n') + with open(logFilename, 'a') as logf: pipe = openPipe( command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, - stderr=log, bufsize=10**8 + stderr=logf, bufsize=10**8 ) byteFrame = pipe.stdout.read(self.chunkSize) closePipe(pipe) diff --git a/src/components/video.py b/src/components/video.py index b6bdd52..a189f60 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -3,6 +3,7 @@ from PyQt5 import QtGui, QtCore, QtWidgets import os import math import subprocess +import logging from component import Component from toolkit.frame import BlankFrame, scale @@ -10,6 +11,9 @@ from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo from toolkit import checkOutput +log = logging.getLogger('AVP.Components.Video') + + class Component(Component): name = 'Video' version = '1.0.0' @@ -134,10 +138,17 @@ class Component(Component): '-ss', '90', '-frames:v', '1', ]) - pipe = openPipe( - command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, bufsize=10**8 - ) + + logFilename = os.path.join( + self.core.logDir, 'preview_%s.log' % str(self.compPos)) + log.debug('Creating ffmpeg process (log at %s)' % logFilename) + with open(logFilename, 'w') as logf: + logf.write(" ".join(command) + '\n\n') + with open(logFilename, 'a') as logf: + pipe = openPipe( + command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, + stderr=logf, bufsize=10**8 + ) byteFrame = pipe.stdout.read(self.chunkSize) closePipe(pipe) diff --git a/src/components/waveform.py b/src/components/waveform.py index 71cbcac..1517be2 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -4,6 +4,7 @@ from PyQt5.QtGui import QColor import os import math import subprocess +import logging from component import Component from toolkit.frame import BlankFrame, scale @@ -13,6 +14,9 @@ from toolkit.ffmpeg import ( ) +log = logging.getLogger('AVP.Components.Waveform') + + class Component(Component): name = 'Waveform' version = '1.0.0' @@ -106,10 +110,16 @@ class Component(Component): '-codec:v', 'rawvideo', '-', '-frames:v', '1', ]) - pipe = openPipe( - command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, bufsize=10**8 - ) + logFilename = os.path.join( + self.core.logDir, 'preview_%s.log' % str(self.compPos)) + log.debug('Creating ffmpeg process (log at %s)' % logFilename) + with open(logFilename, 'w') as logf: + logf.write(" ".join(command) + '\n\n') + with open(logFilename, 'a') as logf: + pipe = openPipe( + command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, + stderr=logf, bufsize=10**8 + ) byteFrame = pipe.stdout.read(self.chunkSize) closePipe(pipe) diff --git a/src/core.py b/src/core.py index 61905eb..4023542 100644 --- a/src/core.py +++ b/src/core.py @@ -7,11 +7,17 @@ import sys import os import json from importlib import import_module +import logging import toolkit import video_thread +log = logging.getLogger('AVP.Core') +STDOUT_LOGLVL = logging.WARNING +FILE_LOGLVL = logging.DEBUG + + class Core: ''' MainWindow and Command module both use an instance of this class @@ -35,6 +41,7 @@ class Core: continue elif ext == '.py': yield name + log.debug('Importing component modules') self.modules = [ import_module('components.%s' % name) for name in findComponents() @@ -67,7 +74,7 @@ class Core: compPos = len(self.selectedComponents) if len(self.selectedComponents) > 50: return None - + log.debug('Inserting Component from module #%s' % moduleIndex) component = self.modules[moduleIndex].Component( moduleIndex, compPos, self ) @@ -104,7 +111,7 @@ class Core: self.componentListChanged() def updateComponent(self, i): - # print('updating %s' % self.selectedComponents[i]) + log.debug('Updating %s #%s' % (self.selectedComponents[i], str(i))) self.selectedComponents[i].update() def moduleIndexFor(self, compName): @@ -125,12 +132,17 @@ class Core: if not saveValueStore: return False try: - self.selectedComponents[compIndex].loadPreset( + comp = self.selectedComponents[compIndex] + comp.loadPreset( saveValueStore, presetName ) except KeyError as e: - print('preset missing value: %s' % e) + log.warning( + '%s #%s\'s preset is missing value: %s' % ( + comp.name, str(compIndex), str(e) + ) + ) self.savedPresets[presetName] = dict(saveValueStore) return True @@ -206,7 +218,7 @@ class Core: preset['preset'] ) except KeyError as e: - print('%s missing value: %s' % ( + log.warning('%s missing value: %s' % ( self.selectedComponents[i], e) ) @@ -224,7 +236,7 @@ class Core: typ, value, tb = data if typ.__name__ == 'KeyError': # probably just an old version, still loadable - print('file missing value: %s' % value) + log.warning('Project file missing value: %s' % value) return if hasattr(loader, 'createNewProject'): loader.createNewProject(prompt=False) @@ -244,6 +256,7 @@ class Core: Returns dictionary with section names as the keys, each one contains a list of tuples: (compName, version, compPresetDict) ''' + log.debug('Parsing av file: %s' % filepath) validSections = ( 'Components', 'Settings', @@ -362,6 +375,7 @@ class Core: def createProjectFile(self, filepath, window=None): '''Create a project file (.avp) using the current program state''' + log.info('Creating %s' % filepath) settingsKeys = [ 'componentDir', 'inputDir', @@ -374,9 +388,8 @@ class Core: filepath += '.avp' if os.path.exists(filepath): os.remove(filepath) - with open(filepath, 'w') as f: - print('creating %s' % filepath) + with open(filepath, 'w') as f: f.write('[Components]\n') for comp in self.selectedComponents: saveValueStore = comp.savePreset() @@ -443,6 +456,7 @@ class Core: 'settings': QtCore.QSettings( os.path.join(dataDir, 'settings.ini'), QtCore.QSettings.IniFormat), + 'logDir': os.path.join(dataDir, 'log'), 'presetDir': os.path.join(dataDir, 'presets'), 'componentsPath': os.path.join(wd, 'components'), 'encoderOptions': encoderOptions, @@ -489,6 +503,13 @@ class Core: setattr(cls, classvar, val) cls.loadDefaultSettings() + if not os.path.exists(cls.dataDir): + os.makedirs(cls.dataDir) + for neededDirectory in ( + cls.presetDir, cls.logDir, cls.settings.value("projectDir")): + if not os.path.exists(neededDirectory): + os.mkdir(neededDirectory) + cls.makeLogger() @classmethod def loadDefaultSettings(cls): @@ -522,6 +543,42 @@ class Core: if val in ('true', 'false'): cls.settings.setValue(key, True if val == 'true' else False) + @staticmethod + def makeLogger(): + logFilename = os.path.join(Core.logDir, 'avp_debug.log') + libLogFilename = os.path.join(Core.logDir, 'global_debug.log') + # delete old logs + for log in (logFilename, libLogFilename): + if os.path.exists(log): + os.remove(log) + + # create file handlers to capture every log message somewhere + logFile = logging.FileHandler(logFilename) + logFile.setLevel(FILE_LOGLVL) + libLogFile = logging.FileHandler(libLogFilename) + libLogFile.setLevel(FILE_LOGLVL) + + # send some critical log messages to stdout as well + logStream = logging.StreamHandler() + logStream.setLevel(STDOUT_LOGLVL) + + # create formatters and put everything together + fileFormatter = logging.Formatter( + '[%(asctime)s] <%(name)s> %(levelname)s: %(message)s' + ) + streamFormatter = logging.Formatter( + '<%(name)s> %(message)s' + ) + logFile.setFormatter(fileFormatter) + libLogFile.setFormatter(fileFormatter) + logStream.setFormatter(streamFormatter) + log = logging.getLogger('AVP') + log.setLevel(FILE_LOGLVL) + log.addHandler(logFile) + log.addHandler(logStream) + libLog = logging.getLogger() + libLog.setLevel(FILE_LOGLVL) + libLog.addHandler(libLogFile) # always store settings in class variables even if a Core object is not created Core.storeSettings() diff --git a/src/main.py b/src/main.py index 421a09f..3a6fbe7 100644 --- a/src/main.py +++ b/src/main.py @@ -1,10 +1,14 @@ from PyQt5 import uic, QtWidgets import sys import os +import logging from __init__ import wd +log = logging.getLogger('AVP.Entrypoint') + + def main(): app = QtWidgets.QApplication(sys.argv) app.setApplicationName("audio-visualizer") @@ -28,6 +32,7 @@ def main(): from command import Command main = Command() + log.debug("Finished creating command object") elif mode == 'GUI': from mainwindow import MainWindow @@ -48,6 +53,7 @@ def main(): # window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0) main = MainWindow(window, proj) + log.debug("Finished creating main window") window.raise_() signal.signal(signal.SIGINT, main.cleanUp) diff --git a/src/mainwindow.py b/src/mainwindow.py index 789a6e7..114015c 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -13,6 +13,7 @@ import os import signal import filecmp import time +import logging from core import Core import preview_thread @@ -20,11 +21,15 @@ from presetmanager import PresetManager from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput +log = logging.getLogger('AVP.MainWindow') + + class PreviewWindow(QtWidgets.QLabel): ''' Paints the preview QLabel and maintains the aspect ratio when the window is resized. ''' + log = logging.getLogger('AVP.MainWindow.Preview') def __init__(self, parent, img): super(PreviewWindow, self).__init__() @@ -58,11 +63,15 @@ class PreviewWindow(QtWidgets.QLabel): if i >= 0: component = self.parent.core.selectedComponents[i] if not hasattr(component, 'previewClickEvent'): + self.log.info('Ignored click event') return pos = (event.x(), event.y()) size = (self.width(), self.height()) + butt = event.button() + self.log.info('Click event for #%s: %s button %s' % ( + i, pos, butt)) component.previewClickEvent( - pos, size, event.button() + pos, size, butt ) self.parent.core.updateComponent(i) @@ -91,9 +100,10 @@ class MainWindow(QtWidgets.QMainWindow): def __init__(self, window, project): QtWidgets.QMainWindow.__init__(self) - # print('main thread id: {}'.format(QtCore.QThread.currentThreadId())) self.window = window self.core = Core() + log.debug( + 'Main thread id: {}'.format(QtCore.QThread.currentThreadId())) # widgets of component settings self.pages = [] @@ -103,27 +113,23 @@ class MainWindow(QtWidgets.QMainWindow): self.autosaveCooldown = 0.2 self.encoding = False - # Create data directory, load/create settings + # Find settings created by Core object self.dataDir = Core.dataDir self.presetDir = Core.presetDir self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') self.settings = Core.settings + self.presetManager = PresetManager( uic.loadUi( os.path.join(Core.wd, 'presetmanager.ui')), self) - if not os.path.exists(self.dataDir): - os.makedirs(self.dataDir) - for neededDirectory in ( - self.presetDir, self.settings.value("projectDir")): - if not os.path.exists(neededDirectory): - os.mkdir(neededDirectory) - # Create the preview window and its thread, queues, and timers + log.debug('Creating preview window') self.previewWindow = PreviewWindow(self, os.path.join( Core.wd, "background.png")) window.verticalLayout_previewWrapper.addWidget(self.previewWindow) + log.debug('Starting preview thread') self.previewQueue = Queue() self.previewThread = QtCore.QThread(self) self.previewWorker = preview_thread.Worker(self, self.previewQueue) @@ -132,6 +138,7 @@ class MainWindow(QtWidgets.QMainWindow): self.previewWorker.imageCreated.connect(self.showPreviewImage) self.previewThread.start() + log.debug('Starting preview timer') self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.processTask.emit) self.timer.start(500) @@ -141,6 +148,8 @@ class MainWindow(QtWidgets.QMainWindow): componentList = self.window.listWidget_componentList if sys.platform == 'darwin': + log.debug( + 'Darwin detected: showing progress label below progress bar') window.progressBar_createVideo.setTextVisible(False) else: window.progressLabel.setHidden(True) @@ -276,6 +285,7 @@ class MainWindow(QtWidgets.QMainWindow): ) self.updateWindowTitle() + log.debug('Showing main window') window.show() if project and project != self.autosavePath: @@ -398,6 +408,7 @@ class MainWindow(QtWidgets.QMainWindow): @QtCore.pyqtSlot() def cleanUp(self, *args): + log.info('Ending the preview thread') self.timer.stop() self.previewThread.quit() self.previewThread.wait() @@ -414,11 +425,12 @@ class MainWindow(QtWidgets.QMainWindow): appName += '*' except AttributeError: pass + log.debug('Setting window title to %s' % appName) self.window.setWindowTitle(appName) @QtCore.pyqtSlot(int, dict) def updateComponentTitle(self, pos, presetStore=False): - if type(presetStore) == dict: + if type(presetStore) is dict: name = presetStore['preset'] if name is None or name not in self.core.savedPresets: modified = False @@ -428,11 +440,20 @@ class MainWindow(QtWidgets.QMainWindow): modified = bool(presetStore) if pos < 0: pos = len(self.core.selectedComponents)-1 - title = str(self.core.selectedComponents[pos]) + name = str(self.core.selectedComponents[pos]) + title = str(name) if self.core.selectedComponents[pos].currentPreset: title += ' - %s' % self.core.selectedComponents[pos].currentPreset if modified: title += '*' + if type(presetStore) is bool: + log.debug('Forcing %s #%s\'s modified status to %s: %s' % ( + name, pos, modified, title + )) + else: + log.debug('Setting %s #%s\'s title: %s' % ( + name, pos, title + )) self.window.listWidget_componentList.item(pos).setText(title) def updateCodecs(self): @@ -493,6 +514,8 @@ class MainWindow(QtWidgets.QMainWindow): elif force or timeDiff >= self.autosaveCooldown * 5: self.autosaveCooldown = 0.2 self.autosaveTimes.insert(0, self.lastAutosave) + else: + log.debug('Autosave rejected by cooldown') def autosaveExists(self, identical=True): '''Determines if creating the autosave should be blocked.''' @@ -500,9 +523,14 @@ class MainWindow(QtWidgets.QMainWindow): if self.currentProject and os.path.exists(self.autosavePath) \ and filecmp.cmp( self.autosavePath, self.currentProject) == identical: + log.debug( + 'Autosave found %s to be identical' % \ + 'not' if not identical else '' + ) return True except FileNotFoundError: - print('project file couldn\'t be located:', self.currentProject) + log.error( + 'Project file couldn\'t be located:', self.currentProject) return identical return False @@ -543,7 +571,7 @@ class MainWindow(QtWidgets.QMainWindow): self.window.lineEdit_outputFile.setText(fileName) def stopVideo(self): - print('stop') + log.info('Export cancelled') self.videoWorker.cancel() self.canceled = True @@ -773,6 +801,7 @@ class MainWindow(QtWidgets.QMainWindow): mousePos = -1 else: mousePos = mousePos.index(True) + log.debug('Click component list row %s' % mousePos) return mousePos @disableWhenEncoding diff --git a/src/preview_thread.py b/src/preview_thread.py index bb22f0c..9615884 100644 --- a/src/preview_thread.py +++ b/src/preview_thread.py @@ -8,11 +8,15 @@ from PIL import Image from PIL.ImageQt import ImageQt from queue import Queue, Empty import os +import logging from toolkit.frame import Checkerboard from toolkit import disableWhenOpeningProject +log = logging.getLogger("AVP.PreviewThread") + + class Worker(QtCore.QObject): imageCreated = pyqtSignal(QtGui.QImage) @@ -55,7 +59,7 @@ class Worker(QtCore.QObject): self.background = Checkerboard(width, height) frame = self.background.copy() - + log.debug('Creating new preview frame') components = nextPreviewInformation["components"] for component in reversed(components): try: @@ -73,10 +77,11 @@ class Worker(QtCore.QObject): newFrame.width, newFrame.height, width, height ) + log.critical(errMsg) self.error.emit(errMsg) break except RuntimeError as e: - print(e) + log.error(str(e)) else: self.frame = ImageQt(frame) self.imageCreated.emit(QtGui.QImage(self.frame)) diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index 3421049..6ab445c 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -8,12 +8,16 @@ import subprocess import threading import signal from queue import PriorityQueue +import logging import core from toolkit.common import checkOutput, pipeWrapper from component import ComponentError +log = logging.getLogger('AVP.Toolkit.Ffmpeg') + + class FfmpegVideo: '''Opens a pipe to ffmpeg and stores a buffer of raw video frames.''' @@ -88,13 +92,14 @@ class FfmpegVideo: def fillBuffer(self): logFilename = os.path.join( - core.Core.dataDir, 'extra_%s.log' % str(self.component.compPos)) - with open(logFilename, 'w') as log: - log.write(" ".join(self.command) + '\n\n') - with open(logFilename, 'a') as log: + core.Core.logDir, 'render_%s.log' % str(self.component.compPos)) + log.debug('Creating ffmpeg process (log at %s)' % logFilename) + with open(logFilename, 'w') as logf: + logf.write(" ".join(self.command) + '\n\n') + with open(logFilename, 'a') as logf: self.pipe = openPipe( self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, - stderr=log, bufsize=10**8 + stderr=logf, bufsize=10**8 ) while True: if self.parent.canceled: @@ -375,7 +380,7 @@ def getAudioDuration(filename): try: info = fileInfo.decode("utf-8").split('\n') except UnicodeDecodeError as e: - print('Unicode error:', str(e)) + log.error('Unicode error:', str(e)) return False for line in info: @@ -398,7 +403,7 @@ def readAudioFile(filename, videoWorker): ''' duration = getAudioDuration(filename) if not duration: - print('Audio file doesn\'t exist or unreadable.') + log.error('Audio file doesn\'t exist or unreadable.') return command = [ diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index 7e83d58..02f9229 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -7,10 +7,14 @@ from PIL.ImageQt import ImageQt import sys import os import math +import logging import core +log = logging.getLogger('AVP.Toolkit.Frame') + + class FramePainter(QtGui.QPainter): ''' A QPainter for a blank frame, which can be converted into a @@ -79,6 +83,7 @@ def FloodFrame(width, height, RgbaTuple): @defaultSize def BlankFrame(width, height): '''The base frame used by each component to start drawing.''' + log.debug('Creating new %s*%s blank frame' % (width, height)) return FloodFrame(width, height, (0, 0, 0, 0)) @@ -88,6 +93,7 @@ def Checkerboard(width, height): A checkerboard to represent transparency to the user. TODO: Would be cool to generate this image with numpy instead. ''' + log.debug('Creating new %s*%s checkerboard' % (width, height)) image = FloodFrame(1920, 1080, (0, 0, 0, 0)) image.paste(Image.open( os.path.join(core.Core.wd, "background.png")), diff --git a/src/video_thread.py b/src/video_thread.py index 5963def..e7e4136 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -17,6 +17,7 @@ from queue import Queue, PriorityQueue from threading import Thread, Event import time import signal +import logging from component import ComponentError from toolkit.frame import Checkerboard @@ -26,6 +27,9 @@ from toolkit.ffmpeg import ( ) +log = logging.getLogger("AVP.VideoThread") + + class Worker(QtCore.QObject): imageCreated = pyqtSignal(['QImage']) @@ -92,7 +96,7 @@ class Worker(QtCore.QObject): by a renderNode later. All indices are multiples of self.sampleSize sampleSize * frameNo = audioI, AKA audio data starting at frameNo ''' - print('Dispatching Frames for Compositing...') + log.debug('Dispatching Frames for Compositing...') for audioI in range(0, len(self.completeAudioArray), self.sampleSize): self.compositeQueue.put(audioI) @@ -156,10 +160,12 @@ class Worker(QtCore.QObject): self.progressBarUpdate.emit(0) self.progressBarSetText.emit("Starting components...") canceledByComponent = False - print('Loaded Components:', ", ".join([ + initText = ", ".join([ "%s) %s" % (num, str(component)) for num, component in enumerate(reversed(self.components)) - ])) + ]) + print('Loaded Components:', initText) + log.info('Calling preFrameRender for %s' % initText) self.staticComponents = {} for compNo, comp in enumerate(reversed(self.components)): try: @@ -191,6 +197,7 @@ class Worker(QtCore.QObject): compError[0] ) ) + log.critical(errMsg) comp._error.emit(errMsg, compError[1]) break if 'static' in compProps: @@ -199,7 +206,7 @@ class Worker(QtCore.QObject): if self.canceled: if canceledByComponent: - print('Export cancelled by component #%s (%s): %s' % ( + log.critical('Export cancelled by component #%s (%s): %s' % ( compNo, comp.name, 'No message.' if comp.error() is None else ( @@ -224,8 +231,11 @@ class Worker(QtCore.QObject): ffmpegCommand = createFfmpegCommand( self.inputFile, self.outputFile, self.components, duration ) - print('###### FFMPEG COMMAND ######\n%s' % " ".join(ffmpegCommand)) + cmd = " ".join(ffmpegCommand) + print('###### FFMPEG COMMAND ######\n%s' % cmd) print('############################') + log.info('Opening pipe to ffmpeg') + log.info(cmd) self.out_pipe = openPipe( ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout ) @@ -298,9 +308,9 @@ class Worker(QtCore.QObject): try: self.out_pipe.stdin.close() except BrokenPipeError: - print('Broken pipe to ffmpeg!') + log.error('Broken pipe to ffmpeg!') if self.out_pipe.stderr is not None: - print(self.out_pipe.stderr.read()) + log.error(self.out_pipe.stderr.read()) self.out_pipe.stderr.close() self.error = True self.out_pipe.wait() -- cgit v1.2.3 From 282f1c4b12b485a567f0d055832a5bf4409404a3 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 12 Aug 2017 09:44:11 -0400 Subject: move previewWindow class into new file and cache frequently-created blank frames --- src/mainwindow.py | 61 +-------------------------------------------------- src/preview_win.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/toolkit/frame.py | 37 +++++++++++++++++++++---------- 3 files changed, 88 insertions(+), 72 deletions(-) create mode 100644 src/preview_win.py (limited to 'src/toolkit/frame.py') diff --git a/src/mainwindow.py b/src/mainwindow.py index 114015c..1abb108 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -17,6 +17,7 @@ import logging from core import Core import preview_thread +from preview_win import PreviewWindow from presetmanager import PresetManager from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput @@ -24,66 +25,6 @@ from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput log = logging.getLogger('AVP.MainWindow') -class PreviewWindow(QtWidgets.QLabel): - ''' - Paints the preview QLabel and maintains the aspect ratio when the - window is resized. - ''' - log = logging.getLogger('AVP.MainWindow.Preview') - - def __init__(self, parent, img): - super(PreviewWindow, self).__init__() - self.parent = parent - self.setFrameStyle(QtWidgets.QFrame.StyledPanel) - self.pixmap = QtGui.QPixmap(img) - - def paintEvent(self, event): - size = self.size() - painter = QtGui.QPainter(self) - point = QtCore.QPoint(0, 0) - scaledPix = self.pixmap.scaled( - size, - QtCore.Qt.KeepAspectRatio, - transformMode=QtCore.Qt.SmoothTransformation) - - # start painting the label from left upper corner - point.setX((size.width() - scaledPix.width())/2) - point.setY((size.height() - scaledPix.height())/2) - painter.drawPixmap(point, scaledPix) - - def changePixmap(self, img): - self.pixmap = QtGui.QPixmap(img) - self.repaint() - - def mousePressEvent(self, event): - if self.parent.encoding: - return - - i = self.parent.window.listWidget_componentList.currentRow() - if i >= 0: - component = self.parent.core.selectedComponents[i] - if not hasattr(component, 'previewClickEvent'): - self.log.info('Ignored click event') - return - pos = (event.x(), event.y()) - size = (self.width(), self.height()) - butt = event.button() - self.log.info('Click event for #%s: %s button %s' % ( - i, pos, butt)) - component.previewClickEvent( - pos, size, butt - ) - self.parent.core.updateComponent(i) - - @QtCore.pyqtSlot(str) - def threadError(self, msg): - self.parent.showMessage( - msg=msg, - icon='Critical', - parent=self - ) - - class MainWindow(QtWidgets.QMainWindow): ''' The MainWindow wraps many Core methods in order to update the GUI diff --git a/src/preview_win.py b/src/preview_win.py new file mode 100644 index 0000000..40c19c6 --- /dev/null +++ b/src/preview_win.py @@ -0,0 +1,62 @@ +from PyQt5 import QtCore, QtGui, QtWidgets +import logging + + +class PreviewWindow(QtWidgets.QLabel): + ''' + Paints the preview QLabel in MainWindow and maintains the aspect ratio + when the window is resized. + ''' + log = logging.getLogger('AVP.PreviewWindow') + + def __init__(self, parent, img): + super(PreviewWindow, self).__init__() + self.parent = parent + self.setFrameStyle(QtWidgets.QFrame.StyledPanel) + self.pixmap = QtGui.QPixmap(img) + + def paintEvent(self, event): + size = self.size() + painter = QtGui.QPainter(self) + point = QtCore.QPoint(0, 0) + scaledPix = self.pixmap.scaled( + size, + QtCore.Qt.KeepAspectRatio, + transformMode=QtCore.Qt.SmoothTransformation) + + # start painting the label from left upper corner + point.setX((size.width() - scaledPix.width())/2) + point.setY((size.height() - scaledPix.height())/2) + painter.drawPixmap(point, scaledPix) + + def changePixmap(self, img): + self.pixmap = QtGui.QPixmap(img) + self.repaint() + + def mousePressEvent(self, event): + if self.parent.encoding: + return + + i = self.parent.window.listWidget_componentList.currentRow() + if i >= 0: + component = self.parent.core.selectedComponents[i] + if not hasattr(component, 'previewClickEvent'): + self.log.info('Ignored click event') + return + pos = (event.x(), event.y()) + size = (self.width(), self.height()) + butt = event.button() + self.log.info('Click event for #%s: %s button %s' % ( + i, pos, butt)) + component.previewClickEvent( + pos, size, butt + ) + self.parent.core.updateComponent(i) + + @QtCore.pyqtSlot(str) + def threadError(self, msg): + self.parent.showMessage( + msg=msg, + icon='Critical', + parent=self + ) diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index 02f9229..e4332eb 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -77,27 +77,40 @@ def defaultSize(framefunc): def FloodFrame(width, height, RgbaTuple): + log.debug('Creating new %s*%s %s flood frame' % ( + width, height, + 'blank' if RgbaTuple[3] == 0 else RgbaTuple + ) + ) return Image.new("RGBA", (width, height), RgbaTuple) @defaultSize -def BlankFrame(width, height): +def BlankFrame(width, height, blankFrames={}): '''The base frame used by each component to start drawing.''' - log.debug('Creating new %s*%s blank frame' % (width, height)) - return FloodFrame(width, height, (0, 0, 0, 0)) + try: + return blankFrames[(width, height)] + except KeyError: + newFrame = FloodFrame(width, height, (0, 0, 0, 0)) + blankFrames[(width, height)] = newFrame + return newFrame @defaultSize -def Checkerboard(width, height): +def Checkerboard(width, height, checkerboards={}): ''' A checkerboard to represent transparency to the user. TODO: Would be cool to generate this image with numpy instead. ''' - log.debug('Creating new %s*%s checkerboard' % (width, height)) - image = FloodFrame(1920, 1080, (0, 0, 0, 0)) - image.paste(Image.open( - os.path.join(core.Core.wd, "background.png")), - (0, 0) - ) - image = image.resize((width, height)) - return image + try: + return checkerboards[(width, height)] + except KeyError: + log.debug('Creating new %s*%s checkerboard' % (width, height)) + image = FloodFrame(1920, 1080, (0, 0, 0, 0)) + image.paste(Image.open( + os.path.join(core.Core.wd, "background.png")), + (0, 0) + ) + image = image.resize((width, height)) + checkerboards[(width, height)] = image + return image -- cgit v1.2.3 From d6b6083f80ae609c801ef63285718325cd71d0c9 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 12 Aug 2017 22:51:46 -0400 Subject: rv pointless optimization & remove circular imports (again...) the last commit does showcase an enlightening bug however --- src/core.py | 2 +- src/toolkit/ffmpeg.py | 2 +- src/toolkit/frame.py | 34 ++++++++++++++-------------------- 3 files changed, 16 insertions(+), 22 deletions(-) (limited to 'src/toolkit/frame.py') diff --git a/src/core.py b/src/core.py index 4023542..2b85f7e 100644 --- a/src/core.py +++ b/src/core.py @@ -10,7 +10,6 @@ from importlib import import_module import logging import toolkit -import video_thread log = logging.getLogger('AVP.Core') @@ -418,6 +417,7 @@ class Core: def newVideoWorker(self, loader, audioFile, outputPath): '''loader is MainWindow or Command object which must own the thread''' + import video_thread self.videoThread = QtCore.QThread(loader) videoWorker = video_thread.Worker( loader, audioFile, outputPath, self.selectedComponents diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index 6ab445c..afcb37c 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -12,7 +12,6 @@ import logging import core from toolkit.common import checkOutput, pipeWrapper -from component import ComponentError log = logging.getLogger('AVP.Toolkit.Ffmpeg') @@ -91,6 +90,7 @@ class FfmpegVideo: self.frameBuffer.task_done() def fillBuffer(self): + from component import ComponentError logFilename = os.path.join( core.Core.logDir, 'render_%s.log' % str(self.component.compPos)) log.debug('Creating ffmpeg process (log at %s)' % logFilename) diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index e4332eb..6174072 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -86,31 +86,25 @@ def FloodFrame(width, height, RgbaTuple): @defaultSize -def BlankFrame(width, height, blankFrames={}): +def BlankFrame(width, height): '''The base frame used by each component to start drawing.''' - try: - return blankFrames[(width, height)] - except KeyError: - newFrame = FloodFrame(width, height, (0, 0, 0, 0)) - blankFrames[(width, height)] = newFrame - return newFrame + newFrame = FloodFrame(width, height, (0, 0, 0, 0)) + blankFrames[(width, height)] = newFrame + return newFrame @defaultSize -def Checkerboard(width, height, checkerboards={}): +def Checkerboard(width, height): ''' A checkerboard to represent transparency to the user. TODO: Would be cool to generate this image with numpy instead. ''' - try: - return checkerboards[(width, height)] - except KeyError: - log.debug('Creating new %s*%s checkerboard' % (width, height)) - image = FloodFrame(1920, 1080, (0, 0, 0, 0)) - image.paste(Image.open( - os.path.join(core.Core.wd, "background.png")), - (0, 0) - ) - image = image.resize((width, height)) - checkerboards[(width, height)] = image - return image + log.debug('Creating new %s*%s checkerboard' % (width, height)) + image = FloodFrame(1920, 1080, (0, 0, 0, 0)) + image.paste(Image.open( + os.path.join(core.Core.wd, "background.png")), + (0, 0) + ) + image = image.resize((width, height)) + checkerboards[(width, height)] = image + return image -- cgit v1.2.3 From 3f2834529fc31e0f00440237dd4a0a374f378718 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 12 Aug 2017 23:03:56 -0400 Subject: fix --- src/toolkit/frame.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) (limited to 'src/toolkit/frame.py') diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index 6174072..63774a6 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -32,12 +32,12 @@ class FramePainter(QtGui.QPainter): super().setPen(penStyle) def finalize(self): - self.end() imBytes = self.image.bits().asstring(self.image.byteCount()) - - return Image.frombytes( + frame = Image.frombytes( 'RGBA', (self.image.width(), self.image.height()), imBytes ) + self.end() + return frame class PaintColor(QtGui.QColor): @@ -78,19 +78,14 @@ def defaultSize(framefunc): def FloodFrame(width, height, RgbaTuple): log.debug('Creating new %s*%s %s flood frame' % ( - width, height, - 'blank' if RgbaTuple[3] == 0 else RgbaTuple - ) - ) + width, height, RgbaTuple)) return Image.new("RGBA", (width, height), RgbaTuple) @defaultSize def BlankFrame(width, height): '''The base frame used by each component to start drawing.''' - newFrame = FloodFrame(width, height, (0, 0, 0, 0)) - blankFrames[(width, height)] = newFrame - return newFrame + return FloodFrame(width, height, (0, 0, 0, 0)) @defaultSize @@ -106,5 +101,4 @@ def Checkerboard(width, height): (0, 0) ) image = image.resize((width, height)) - checkerboards[(width, height)] = image return image -- cgit v1.2.3 From bed07479f1b4bf24a0b9c84217d41ebbe880a8fb Mon Sep 17 00:00:00 2001 From: tassaron Date: Mon, 14 Aug 2017 10:10:32 -0400 Subject: faster Spectrum preview & custom VERBOSE loglvl --- src/__init__.py | 23 ++++++++++++++++++++ src/component.py | 5 +++++ src/components/spectrum.py | 52 +++++++++++++++++++++++----------------------- src/core.py | 10 +++++---- src/mainwindow.py | 6 +++--- src/toolkit/frame.py | 3 ++- src/video_thread.py | 4 ++-- 7 files changed, 67 insertions(+), 36 deletions(-) (limited to 'src/toolkit/frame.py') diff --git a/src/__init__.py b/src/__init__.py index 2f4cffa..73f174a 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,5 +1,28 @@ import sys import os +import logging + + +class Logger(logging.getLoggerClass()): + ''' + Custom Logger class to handle custom VERBOSE log level. + Levels used in this program are as follows: + VERBOSE Annoyingly frequent debug messages (e.g, in loops) + DEBUG Ordinary debug information + INFO Expected events that are expensive or irreversible + WARNING A non-fatal error or suspicious behaviour + ERROR Any error that would interrupt the user + CRITICAL Things that really shouldn't happen at all + ''' + def __init__(self, name, level=logging.NOTSET): + super().__init__(name, level) + logging.addLevelName(5, "VERBOSE") + + def verbose(self, msg, *args, **kwargs): + if self.isEnabledFor(5): + self._log(5, msg, args, **kwargs) +logging.setLoggerClass(Logger) +logging.VERBOSE = 5 if getattr(sys, 'frozen', False): diff --git a/src/component.py b/src/component.py index d011f1e..cf3085c 100644 --- a/src/component.py +++ b/src/component.py @@ -39,6 +39,11 @@ class ComponentMetaclass(type(QtCore.QObject)): def renderWrapper(func): def renderWrapper(self, *args, **kwargs): try: + log.verbose('### %s #%s renders%s frame %s###' % ( + self.__class__.name, str(self.compPos), + '' if args else ' a preview', + '' if not args else '%s ' % args[0], + )) return func(self, *args, **kwargs) except Exception as e: try: diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 32763c0..246b839 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -27,6 +27,8 @@ class Component(Component): self._image = BlankFrame(self.width, self.height) self.chunkSize = 4 * self.width * self.height self.changedOptions = True + self.previewSize = (214, 120) + self.previewPipe = None if hasattr(self.parent, 'window'): # update preview when audio file changes (if genericPreview is off) @@ -72,7 +74,8 @@ class Component(Component): if not changedSize \ and not self.changedOptions \ and self.previewFrame is not None: - log.debug('Comp #%s is reusing old preview frame' % self.compPos) + log.debug( + 'Spectrum #%s is reusing old preview frame' % self.compPos) return self.previewFrame frame = self.getPreviewFrame() @@ -86,6 +89,7 @@ class Component(Component): def preFrameRender(self, **kwargs): super().preFrameRender(**kwargs) + self.previewPipe.wait() self.updateChunksize() w, h = scale(self.scale, self.width, self.height, str) self.video = FfmpegVideo( @@ -141,18 +145,21 @@ class Component(Component): with open(logFilename, 'w') as logf: logf.write(" ".join(command) + '\n\n') with open(logFilename, 'a') as logf: - pipe = openPipe( + self.previewPipe = openPipe( command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=logf, bufsize=10**8 ) - byteFrame = pipe.stdout.read(self.chunkSize) - closePipe(pipe) + byteFrame = self.previewPipe.stdout.read(self.chunkSize) + closePipe(self.previewPipe) frame = self.finalizeFrame(byteFrame) return frame def makeFfmpegFilter(self, preview=False, startPt=0): - w, h = scale(self.scale, self.width, self.height, str) + if preview: + w, h = self.previewSize + else: + w, h = (self.width, self.height) color = self.page.comboBox_color.currentText().lower() genericPreview = self.settings.value("pref_genericPreview") @@ -173,8 +180,7 @@ class Component(Component): 'showspectrum=s=%sx%s:slide=scroll:win_func=%s:' 'color=%s:scale=%s,' 'colorkey=color=black:similarity=0.1:blend=0.5' % ( - self.settings.value("outputWidth"), - self.settings.value("outputHeight"), + w, h, self.page.comboBox_window.currentText(), color, amplitude, ) @@ -197,8 +203,7 @@ class Component(Component): filter_ = ( 'ahistogram=r=%s:s=%sx%s:dmode=separate:ascale=%s:scale=%s' % ( self.settings.value("outputFrameRate"), - self.settings.value("outputWidth"), - self.settings.value("outputHeight"), + w, h, amplitude, display ) ) @@ -214,8 +219,7 @@ class Component(Component): m = self.page.comboBox_mode.currentText() filter_ = ( 'avectorscope=s=%sx%s:draw=%s:m=%s:scale=%s:zoom=%s' % ( - self.settings.value("outputWidth"), - self.settings.value("outputHeight"), + w, h, 'line'if self.draw else 'dot', m, amplitude, str(self.zoom), ) @@ -225,8 +229,7 @@ class Component(Component): 'showcqt=r=%s:s=%sx%s:count=30:text=0:tc=%s,' 'colorkey=color=black:similarity=0.1:blend=0.5 ' % ( self.settings.value("outputFrameRate"), - self.settings.value("outputWidth"), - self.settings.value("outputHeight"), + w, h, str(self.tc), ) ) @@ -235,28 +238,28 @@ class Component(Component): 'aphasemeter=r=%s:s=%sx%s:video=1 [atrash][vtmp1]; ' '[atrash] anullsink; ' '[vtmp1] colorkey=color=black:similarity=0.1:blend=0.5, ' - 'crop=in_w/8:in_h:(in_w/8)*7:0 '% ( + 'crop=in_w/8:in_h:(in_w/8)*7:0 ' % ( self.settings.value("outputFrameRate"), - self.settings.value("outputWidth"), - self.settings.value("outputHeight"), + w, h, ) ) return [ '-filter_complex', '%s%s%s%s [v1]; ' - '[v1] %sscale=%s:%s%s%s%s [v]' % ( + '[v1] %s%s%s%s%s [v]' % ( exampleSound() if preview and genericPreview else '[0:a] ', 'compand=gain=4,' if self.compress else '', 'aformat=channel_layouts=mono,' if self.mono else '', filter_, 'hflip, ' if self.mirror else '', - w, h, - ', hue=h=%s:s=10' % str(self.hue) if self.hue > 0 else '', - ', trim=start=%s:end=%s' % ( + 'trim=start=%s:end=%s, ' % ( "{0:.3f}".format(startPt + 12), "{0:.3f}".format(startPt + 12.5) ) if preview else '', + 'scale=%sx%s' % scale( + self.scale, self.width, self.height, str), + ', hue=h=%s:s=10' % str(self.hue) if self.hue > 0 else '', ', convolution=-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2:-2 ' '-1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2' if self.filterType == 3 else '' @@ -281,10 +284,7 @@ class Component(Component): self._image = image except ValueError: image = self._image - if self.scale != 100 \ - or self.x != 0 or self.y != 0: - frame = BlankFrame(self.width, self.height) - frame.paste(image, box=(self.x, self.y)) - else: - frame = image + + frame = BlankFrame(self.width, self.height) + frame.paste(image, box=(self.x, self.y)) return frame diff --git a/src/core.py b/src/core.py index 2b85f7e..4dfb210 100644 --- a/src/core.py +++ b/src/core.py @@ -562,9 +562,10 @@ class Core: logStream = logging.StreamHandler() logStream.setLevel(STDOUT_LOGLVL) - # create formatters and put everything together + # create formatters for each stream fileFormatter = logging.Formatter( - '[%(asctime)s] <%(name)s> %(levelname)s: %(message)s' + '[%(asctime)s] %(threadName)-10.10s %(name)-23.23s %(levelname)s: ' + '%(message)s' ) streamFormatter = logging.Formatter( '<%(name)s> %(message)s' @@ -572,13 +573,14 @@ class Core: logFile.setFormatter(fileFormatter) libLogFile.setFormatter(fileFormatter) logStream.setFormatter(streamFormatter) + log = logging.getLogger('AVP') - log.setLevel(FILE_LOGLVL) log.addHandler(logFile) log.addHandler(logStream) libLog = logging.getLogger() - libLog.setLevel(FILE_LOGLVL) libLog.addHandler(libLogFile) + # lowest level must be explicitly set on the root Logger + libLog.setLevel(0) # always store settings in class variables even if a Core object is not created Core.storeSettings() diff --git a/src/mainwindow.py b/src/mainwindow.py index 1abb108..af6e190 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -44,7 +44,7 @@ class MainWindow(QtWidgets.QMainWindow): self.window = window self.core = Core() log.debug( - 'Main thread id: {}'.format(QtCore.QThread.currentThreadId())) + 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId()))) # widgets of component settings self.pages = [] @@ -465,8 +465,8 @@ class MainWindow(QtWidgets.QMainWindow): and filecmp.cmp( self.autosavePath, self.currentProject) == identical: log.debug( - 'Autosave found %s to be identical' % \ - 'not' if not identical else '' + 'Autosave found %s to be identical' + % 'not' if not identical else '' ) return True except FileNotFoundError: diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index 63774a6..ad8537c 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -21,6 +21,7 @@ class FramePainter(QtGui.QPainter): Pillow image with finalize() ''' def __init__(self, width, height): + log.verbose('Creating new FramePainter') image = BlankFrame(width, height) self.image = QtGui.QImage(ImageQt(image)) super().__init__(self.image) @@ -77,7 +78,7 @@ def defaultSize(framefunc): def FloodFrame(width, height, RgbaTuple): - log.debug('Creating new %s*%s %s flood frame' % ( + log.verbose('Creating new %s*%s %s flood frame' % ( width, height, RgbaTuple)) return Image.new("RGBA", (width, height), RgbaTuple) diff --git a/src/video_thread.py b/src/video_thread.py index 5acbda4..87fb9bd 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -212,7 +212,7 @@ class Worker(QtCore.QObject): compError[0] ) ) - log.critical(errMsg) + log.error(errMsg) comp._error.emit(errMsg, compError[1]) break if 'static' in compProps: @@ -221,7 +221,7 @@ class Worker(QtCore.QObject): if self.canceled: if canceledByComponent: - log.critical('Export cancelled by component #%s (%s): %s' % ( + log.error('Export cancelled by component #%s (%s): %s' % ( compNo, comp.name, 'No message.' if comp.error() is None else ( -- cgit v1.2.3 From a1d7cbb984f2a6c2ea976daa8914a2c9845ee21c Mon Sep 17 00:00:00 2001 From: tassaron Date: Tue, 15 Aug 2017 22:20:25 -0400 Subject: undoable edits for normal component settings; TODO: merge small edits --- src/background.png | Bin 45367 -> 0 bytes src/component.py | 77 +++++++++++++++++++++++++++++++++++++++++------- src/components/color.py | 3 -- src/components/color.ui | 6 ++++ src/components/text.py | 4 --- src/components/text.ui | 6 ++++ src/core.py | 20 ++++++++----- src/gui/background.png | Bin 0 -> 45367 bytes src/gui/mainwindow.py | 34 +++++++++++++++------ src/toolkit/common.py | 12 ++++++++ src/toolkit/frame.py | 2 +- 11 files changed, 130 insertions(+), 34 deletions(-) delete mode 100644 src/background.png create mode 100644 src/gui/background.png (limited to 'src/toolkit/frame.py') diff --git a/src/background.png b/src/background.png deleted file mode 100644 index fb58593..0000000 Binary files a/src/background.png and /dev/null differ diff --git a/src/component.py b/src/component.py index 0e5144c..dcba082 100644 --- a/src/component.py +++ b/src/component.py @@ -12,7 +12,7 @@ import logging from toolkit.frame import BlankFrame from toolkit import ( - getWidgetValue, setWidgetValue, connectWidget, rgbFromString + getWidgetValue, setWidgetValue, connectWidget, rgbFromString, blockSignals ) @@ -305,14 +305,46 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): def update(self): ''' - Reads all tracked widget values into instance attributes - and tells the MainWindow that the component was modified. - Call super() at the END if you need to subclass this. + A component update triggered by the user changing a widget value + Call super() at the END when subclassing this. ''' - for attr, widget in self._trackedWidgets.items(): + oldWidgetVals = { + attr: getattr(self, attr) + for attr in self._trackedWidgets + } + newWidgetVals = { + attr: getWidgetValue(widget) + if attr not in self._colorWidgets else rgbFromString(widget.text()) + for attr, widget in self._trackedWidgets.items() + } + if any([val != oldWidgetVals[attr] + for attr, val in newWidgetVals.items() + ]): + action = ComponentUpdate(self, oldWidgetVals, newWidgetVals) + self.parent.undoStack.push(action) + + def _update(self): + '''An internal component update that is not undoable''' + + newWidgetVals = { + attr: getWidgetValue(widget) + for attr, widget in self._trackedWidgets.items() + } + self.setAttrs(newWidgetVals) + self.sendUpdateSignal() + + def setAttrs(self, attrDict): + ''' + Sets attrs (linked to trackedWidgets) in this preset to + the values in the attrDict. Mutates certain widget values if needed + ''' + for attr, val in attrDict.items(): if attr in self._colorWidgets: # Color Widgets: text stored as tuple & update the button color - rgbTuple = rgbFromString(widget.text()) + if type(val) is tuple: + rgbTuple = val + else: + rgbTuple = rgbFromString(val) btnStyle = ( "QPushButton { background-color : %s; outline: none; }" % QColor(*rgbTuple).name()) @@ -322,12 +354,11 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): elif attr in self._relativeWidgets: # Relative widgets: number scales to fit export resolution self.updateRelativeWidget(attr) - setattr(self, attr, self._trackedWidgets[attr].value()) + setattr(self, attr, val) else: # Normal tracked widget - setattr(self, attr, getWidgetValue(widget)) - self.sendUpdateSignal() + setattr(self, attr, val) def sendUpdateSignal(self): if not self.core.openingProject: @@ -541,7 +572,6 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): pixelVal = self.pixelValForAttr(attr, floatVal) self._trackedWidgets[attr].setValue(pixelVal) - def updateRelativeWidget(self, attr): try: oldUserValue = getattr(self, attr) @@ -628,3 +658,30 @@ class ComponentError(RuntimeError): super().__init__(string) caller.lockError(string) caller._error.emit(string, detail) + + +class ComponentUpdate(QtWidgets.QUndoCommand): + '''Command object for making a component action undoable''' + def __init__(self, parent, oldWidgetVals, newWidgetVals): + super().__init__( + 'Changed %s component #%s' % ( + parent.name, parent.compPos + ) + ) + self.parent = parent + self.oldWidgetVals = oldWidgetVals + self.newWidgetVals = newWidgetVals + + def redo(self): + self.parent.setAttrs(self.newWidgetVals) + self.parent.sendUpdateSignal() + + def undo(self): + self.parent.setAttrs(self.oldWidgetVals) + with blockSignals(self.parent): + for attr, widget in self.parent._trackedWidgets.items(): + val = self.oldWidgetVals[attr] + if attr in self.parent._colorWidgets: + val = '%s,%s,%s' % val + setWidgetValue(widget, val) + self.parent.sendUpdateSignal() diff --git a/src/components/color.py b/src/components/color.py index 5d1233e..d09cee8 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -17,9 +17,6 @@ class Component(Component): self.y = 0 super().widget(*args) - 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) self.page.pushButton_color2.setDisabled(True) diff --git a/src/components/color.ui b/src/components/color.ui index a9dacea..1865e60 100644 --- a/src/components/color.ui +++ b/src/components/color.ui @@ -73,6 +73,9 @@ 0 + + 0,0,0 + 12 @@ -146,6 +149,9 @@ 0 + + 133,133,133 + 12 diff --git a/src/components/text.py b/src/components/text.py index 4d4f5d3..d3afd5c 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -13,8 +13,6 @@ class Component(Component): def widget(self, *args): super().widget(*args) - self.textColor = (255, 255, 255) - self.strokeColor = (0, 0, 0) self.title = 'Text' self.alignment = 1 self.titleFont = QFont() @@ -25,8 +23,6 @@ class Component(Component): 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_strokeColor.setText('%s,%s,%s' % self.strokeColor) self.page.spinBox_fontSize.setValue(int(self.fontSize)) self.page.lineEdit_title.setText(self.title) diff --git a/src/components/text.ui b/src/components/text.ui index 13d3467..b62e0ed 100644 --- a/src/components/text.ui +++ b/src/components/text.ui @@ -427,6 +427,9 @@ Qt::NoFocus + + 255,255,255 + @@ -485,6 +488,9 @@ Qt::NoFocus + + 0,0,0 + diff --git a/src/core.py b/src/core.py index 20b9c1d..cee0f56 100644 --- a/src/core.py +++ b/src/core.py @@ -94,12 +94,11 @@ class Core: compPos, component ) - self.componentListChanged() - if moduleIndex > -1: - self.updateComponent(compPos) - if hasattr(loader, 'insertComponent'): loader.insertComponent(compPos) + + self.componentListChanged() + self.updateComponent(compPos) return compPos def moveComponent(self, startI, endI): @@ -119,7 +118,7 @@ class Core: def updateComponent(self, i): log.debug('Updating %s #%s' % (self.selectedComponents[i], str(i))) - self.selectedComponents[i].update() + self.selectedComponents[i]._update() def moduleIndexFor(self, compName): try: @@ -540,6 +539,7 @@ class Core: "projectDir": os.path.join(cls.dataDir, 'projects'), "pref_insertCompAtTop": True, "pref_genericPreview": True, + "pref_undoLimit": 10, } for parm, value in cls.defaultSettings.items(): @@ -552,8 +552,14 @@ class Core: if not key.startswith('pref_'): continue val = cls.settings.value(key) - if val in ('true', 'false'): - cls.settings.setValue(key, True if val == 'true' else False) + try: + val = int(val) + except ValueError: + if val == 'true': + val = True + elif val == 'false': + val = False + cls.settings.setValue(key, val) @staticmethod def makeLogger(): diff --git a/src/gui/background.png b/src/gui/background.png new file mode 100644 index 0000000..fb58593 Binary files /dev/null and b/src/gui/background.png differ diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 2edb750..47111a0 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -42,13 +42,22 @@ class MainWindow(QtWidgets.QMainWindow): def __init__(self, window, project): QtWidgets.QMainWindow.__init__(self) + log.debug( + 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId()))) self.window = window self.core = Core() Core.mode = 'GUI' - log.debug( - 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId()))) + # Find settings created by Core object + self.dataDir = Core.dataDir + self.presetDir = Core.presetDir + self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') + self.settings = Core.settings + + # Create stack of undoable user actions self.undoStack = QtWidgets.QUndoStack(self) + undoLimit = self.settings.value("pref_undoLimit") + self.undoStack.setUndoLimit(undoLimit) # widgets of component settings self.pages = [] @@ -58,12 +67,6 @@ class MainWindow(QtWidgets.QMainWindow): self.autosaveCooldown = 0.2 self.encoding = False - # Find settings created by Core object - self.dataDir = Core.dataDir - self.presetDir = Core.presetDir - self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') - self.settings = Core.settings - self.presetManager = PresetManager( uic.loadUi( os.path.join(Core.wd, 'gui', 'presetmanager.ui')), self) @@ -302,6 +305,7 @@ class MainWindow(QtWidgets.QMainWindow): QtWidgets.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog) QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog) QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject) + QtWidgets.QShortcut("Ctrl+Z", self.window, self.undoStack.undo) QtWidgets.QShortcut("Ctrl+Y", self.window, self.undoStack.redo) QtWidgets.QShortcut("Ctrl+Shift+Z", self.window, self.undoStack.redo) @@ -353,6 +357,9 @@ class MainWindow(QtWidgets.QMainWindow): QtWidgets.QShortcut( "Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand ) + QtWidgets.QShortcut( + "Ctrl+Alt+Shift+U", self.window, self.showUndoStack + ) @QtCore.pyqtSlot() def cleanUp(self, *args): @@ -658,6 +665,14 @@ class MainWindow(QtWidgets.QMainWindow): def showPreviewImage(self, image): self.previewWindow.changePixmap(image) + def showUndoStack(self): + dialog = QtWidgets.QDialog(self.window) + undoView = QtWidgets.QUndoView(self.undoStack) + layout = QtWidgets.QVBoxLayout() + layout.addWidget(undoView) + dialog.setLayout(layout) + dialog.show() + def showFfmpegCommand(self): from textwrap import wrap from toolkit.ffmpeg import createFfmpegCommand @@ -784,6 +799,7 @@ class MainWindow(QtWidgets.QMainWindow): field.blockSignals(False) self.progressBarUpdated(0) self.progressBarSetText('') + self.undoStack.clear() @disableWhenEncoding def createNewProject(self, prompt=True): @@ -847,7 +863,7 @@ class MainWindow(QtWidgets.QMainWindow): def openProject(self, filepath, prompt=True): if not filepath or not os.path.exists(filepath) \ - or not filepath.endswith('.avp'): + or not filepath.endswith('.avp'): return self.clear() diff --git a/src/toolkit/common.py b/src/toolkit/common.py index eba57d9..51ad023 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py @@ -9,6 +9,18 @@ import subprocess from collections import OrderedDict +class blockSignals: + '''A context manager to temporarily block a Qt widget from updating''' + def __init__(self, widget): + self.widget = widget + + def __enter__(self): + self.widget.blockSignals(True) + + def __exit__(self, *args): + self.widget.blockSignals(False) + + def badName(name): '''Returns whether a name contains non-alphanumeric chars''' return any([letter in string.punctuation for letter in name]) diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index ad8537c..2104978 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -98,7 +98,7 @@ def Checkerboard(width, height): log.debug('Creating new %s*%s checkerboard' % (width, height)) image = FloodFrame(1920, 1080, (0, 0, 0, 0)) image.paste(Image.open( - os.path.join(core.Core.wd, "background.png")), + os.path.join(core.Core.wd, 'gui', "background.png")), (0, 0) ) image = image.resize((width, height)) -- cgit v1.2.3 From 85d3b779d07ad92b0f540ea52185777c3c3f5e48 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 26 Aug 2017 21:23:44 -0400 Subject: fixed too-large Color sizes, fixed a redoing bug, rm pointless things and now Ctrl+Alt+Shift+A gives a bunch of debug info --- src/component.py | 30 +++++++++++------------ src/components/color.py | 2 +- src/components/color.ui | 4 ++-- src/components/text.py | 13 ++++++---- src/core.py | 8 +++++-- src/gui/mainwindow.py | 63 +++++++++++++++++++++++++++++-------------------- src/gui/preview_win.py | 1 + src/main.py | 5 ---- src/toolkit/ffmpeg.py | 2 +- src/toolkit/frame.py | 3 --- 10 files changed, 72 insertions(+), 59 deletions(-) (limited to 'src/toolkit/frame.py') diff --git a/src/component.py b/src/component.py index 35fc717..de4b6a7 100644 --- a/src/component.py +++ b/src/component.py @@ -41,10 +41,8 @@ class ComponentMetaclass(type(QtCore.QObject)): def renderWrapper(self, *args, **kwargs): try: log.verbose( - '### %s #%s renders%s frame %s###', + '### %s #%s renders a preview frame ###', self.__class__.name, str(self.compPos), - '' if args else ' a preview', - '' if not args else '%s ' % args[0], ) return func(self, *args, **kwargs) except Exception as e: @@ -198,8 +196,8 @@ class ComponentMetaclass(type(QtCore.QObject)): 'names', # Class methods 'error', 'audio', 'properties', # Properties 'preFrameRender', 'previewRender', - 'frameRender', 'command', - 'loadPreset', 'update', 'widget', + 'loadPreset', 'command', + 'update', 'widget', ) # Auto-decorate methods @@ -212,7 +210,7 @@ class ComponentMetaclass(type(QtCore.QObject)): attrs[key] = property(attrs[key]) elif key == 'command': attrs[key] = cls.commandWrapper(attrs[key]) - elif key in ('previewRender', 'frameRender'): + elif key == 'previewRender': attrs[key] = cls.renderWrapper(attrs[key]) elif key == 'preFrameRender': attrs[key] = cls.initializationWrapper(attrs[key]) @@ -298,16 +296,19 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): return self.__class__.name def __repr__(self): + import pprint try: preset = self.savePreset() except Exception as e: preset = '%s occurred while saving preset' % str(e) return ( - 'Component(%s, %s, Core)\n' - 'Name: %s v%s\n Preset: %s' % ( + 'Component(module %s, pos %s) (%s)\n' + 'Name: %s v%s\nPreset: %s' % ( self.moduleIndex, self.compPos, - self.__class__.name, str(self.__class__.version), preset + object.__repr__(self), + self.__class__.name, str(self.__class__.version), + pprint.pformat(preset) ) ) @@ -886,12 +887,11 @@ class ComponentUpdate(QtWidgets.QUndoCommand): def redo(self): if self.undone: log.debug('Redoing component update') - self.parent.oldAttrs = self.relativeWidgetValsAfterUndo - self.setWidgetValues(self.modifiedVals) - self.parent.update(auto=True) - self.parent.oldAttrs = None - else: - self.parent.setAttrs(self.modifiedVals) + self.parent.oldAttrs = self.relativeWidgetValsAfterUndo + self.setWidgetValues(self.modifiedVals) + self.parent.update(auto=True) + self.parent.oldAttrs = None + if not self.undone: self.relativeWidgetValsAfterRedo = { attr: copy(getattr(self.parent, attr)) for attr in self.parent._relativeWidgets diff --git a/src/components/color.py b/src/components/color.py index a55aa10..7d4f86d 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -102,7 +102,7 @@ class Component(Component): # Return a solid image at x, y if self.fillType == 0: frame = BlankFrame(width, height) - image = Image.new("RGBA", shapeSize, (r, g, b, 255)) + image = FloodFrame(self.sizeWidth, self.sizeHeight, (r, g, b, 255)) frame.paste(image, box=(self.x, self.y)) return frame diff --git a/src/components/color.ui b/src/components/color.ui index 1865e60..c1713fb 100644 --- a/src/components/color.ui +++ b/src/components/color.ui @@ -204,7 +204,7 @@ 0 - 999999999 + 19200 0 @@ -239,7 +239,7 @@ - 999999999 + 10800 diff --git a/src/components/text.py b/src/components/text.py index 92f0599..32a108e 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -2,10 +2,13 @@ from PIL import ImageEnhance, ImageFilter, ImageChops from PyQt5.QtGui import QColor, QFont from PyQt5 import QtGui, QtCore, QtWidgets import os +import logging from component import Component from toolkit.frame import FramePainter, PaintColor +log = logging.getLogger('AVP.Components.Text') + class Component(Component): name = 'Title Text' @@ -76,16 +79,15 @@ class Component(Component): def getXY(self): '''Returns true x, y after considering alignment settings''' fm = QtGui.QFontMetrics(self.titleFont) - if self.alignment == 0: # Left - x = int(self.xPosition) + x = self.pixelValForAttr('xPosition') if self.alignment == 1: # Middle offset = int(fm.width(self.title)/2) - x = self.xPosition - offset - + x -= offset if self.alignment == 2: # Right offset = fm.width(self.title) - x = self.xPosition - offset + x -= offset + return x, self.yPosition def loadPreset(self, pr, *args): @@ -137,6 +139,7 @@ class Component(Component): image = FramePainter(width, height) x, y = self.getXY() + log.debug('Text position translates to %s, %s', x, y) if self.stroke > 0: outliner = QtGui.QPainterPathStroker() outliner.setWidth(self.stroke) diff --git a/src/core.py b/src/core.py index 784f3b8..b9e2335 100644 --- a/src/core.py +++ b/src/core.py @@ -14,7 +14,7 @@ import toolkit log = logging.getLogger('AVP.Core') STDOUT_LOGLVL = logging.VERBOSE -FILE_LOGLVL = logging.VERBOSE +FILE_LOGLVL = logging.DEBUG class Core: @@ -32,6 +32,11 @@ class Core: self.savedPresets = {} # copies of presets to detect modification self.openingProject = False + def __repr__(self): + return "\n=~=~=~=\n".join( + [repr(comp) for comp in self.selectedComponents] + ) + def importComponents(self): def findComponents(): for f in os.listdir(Core.componentsPath): @@ -482,7 +487,6 @@ class Core: '854x480', ], 'FFMPEG_BIN': findFfmpeg(), - 'windowHasFocus': False, 'canceled': False, } diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 3b204b7..d7fde5c 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -11,6 +11,7 @@ from queue import Queue import sys import os import signal +import atexit import filecmp import time import logging @@ -49,6 +50,13 @@ class MainWindow(QtWidgets.QMainWindow): self.window = window self.core = Core() Core.mode = 'GUI' + # widgets of component settings + self.pages = [] + self.lastAutosave = time.time() + # list of previous five autosave times, used to reduce update spam + self.autosaveTimes = [] + self.autosaveCooldown = 0.2 + self.encoding = False # Find settings created by Core object self.dataDir = Core.dataDir @@ -56,19 +64,16 @@ class MainWindow(QtWidgets.QMainWindow): self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') self.settings = Core.settings + # Register clean-up functions + signal.signal(signal.SIGINT, self.terminate) + atexit.register(self.cleanUp) + # Create stack of undoable user actions self.undoStack = QtWidgets.QUndoStack(self) undoLimit = self.settings.value("pref_undoLimit") self.undoStack.setUndoLimit(undoLimit) - # widgets of component settings - self.pages = [] - self.lastAutosave = time.time() - # list of previous five autosave times, used to reduce update spam - self.autosaveTimes = [] - self.autosaveCooldown = 0.2 - self.encoding = False - + # Create Preset Manager self.presetManager = PresetManager( uic.loadUi( os.path.join(Core.wd, 'gui', 'presetmanager.ui')), self) @@ -97,7 +102,6 @@ class MainWindow(QtWidgets.QMainWindow): self.timer.start(timeout) # Begin decorating the window and connecting events - self.window.installEventFilter(self) componentList = self.window.listWidget_componentList style = window.pushButton_undo.style() @@ -391,24 +395,41 @@ class MainWindow(QtWidgets.QMainWindow): activated=lambda: self.moveComponent('bottom') ) - # Debug Hotkeys QtWidgets.QShortcut( - "Ctrl+Alt+Shift+R", self.window, self.drawPreview + "Ctrl+Shift+F", self.window, self.showFfmpegCommand ) QtWidgets.QShortcut( - "Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand + "Ctrl+Shift+U", self.window, self.showUndoStack ) - QtWidgets.QShortcut( - "Ctrl+Alt+Shift+U", self.window, self.showUndoStack + + if log.isEnabledFor(logging.DEBUG): + QtWidgets.QShortcut( + "Ctrl+Alt+Shift+R", self.window, self.drawPreview + ) + QtWidgets.QShortcut( + "Ctrl+Alt+Shift+A", self.window, lambda: log.debug(repr(self)) + ) + + def __repr__(self): + return ( + '\n%s\n' + '#####\n' + 'Preview thread is %s\n' % ( + repr(self.core), + 'live' if self.previewThread.isRunning() else 'dead', + ) ) - @QtCore.pyqtSlot() def cleanUp(self, *args): log.info('Ending the preview thread') self.timer.stop() self.previewThread.quit() self.previewThread.wait() + def terminate(self, *args): + self.cleanUp() + sys.exit(0) + @disableWhenOpeningProject def updateWindowTitle(self): appName = 'Audio Visualizer' @@ -542,7 +563,7 @@ class MainWindow(QtWidgets.QMainWindow): return True except FileNotFoundError: log.error( - 'Project file couldn\'t be located:', self.currentProject) + 'Project file couldn\'t be located: %s', self.currentProject) return identical return False @@ -639,6 +660,7 @@ class MainWindow(QtWidgets.QMainWindow): detail=detail, icon='Critical', ) + log.info('%s', repr(self)) def changeEncodingStatus(self, status): self.encoding = status @@ -1017,12 +1039,3 @@ class MainWindow(QtWidgets.QMainWindow): self.menu.move(parentPosition + QPos) self.menu.show() - - def eventFilter(self, object, event): - if event.type() == QtCore.QEvent.WindowActivate \ - or event.type() == QtCore.QEvent.FocusIn: - Core.windowHasFocus = True - elif event.type() == QtCore.QEvent.WindowDeactivate \ - or event.type() == QtCore.QEvent.FocusOut: - Core.windowHasFocus = False - return False diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py index c6b9a32..49a22eb 100644 --- a/src/gui/preview_win.py +++ b/src/gui/preview_win.py @@ -60,3 +60,4 @@ class PreviewWindow(QtWidgets.QLabel): icon='Critical', parent=self ) + log.info('%', repr(self.parent)) diff --git a/src/main.py b/src/main.py index 6d18af3..f767de1 100644 --- a/src/main.py +++ b/src/main.py @@ -36,8 +36,6 @@ def main(): elif mode == 'GUI': from gui.mainwindow import MainWindow - import atexit - import signal window = uic.loadUi(os.path.join(wd, "gui", "mainwindow.ui")) # window.adjustSize() @@ -56,9 +54,6 @@ def main(): log.debug("Finished creating main window") window.raise_() - signal.signal(signal.SIGINT, main.cleanUp) - atexit.register(main.cleanUp) - sys.exit(app.exec_()) if __name__ == "__main__": diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index f007f90..a77831e 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -157,7 +157,7 @@ def findFfmpeg(): ['ffmpeg', '-version'], stderr=f ) return "ffmpeg" - except subprocess.CalledProcessError: + except (subprocess.CalledProcessError, FileNotFoundError): return "avconv" diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index 2104978..aefb55f 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -21,7 +21,6 @@ class FramePainter(QtGui.QPainter): Pillow image with finalize() ''' def __init__(self, width, height): - log.verbose('Creating new FramePainter') image = BlankFrame(width, height) self.image = QtGui.QImage(ImageQt(image)) super().__init__(self.image) @@ -78,8 +77,6 @@ def defaultSize(framefunc): def FloodFrame(width, height, RgbaTuple): - log.verbose('Creating new %s*%s %s flood frame' % ( - width, height, RgbaTuple)) return Image.new("RGBA", (width, height), RgbaTuple) -- cgit v1.2.3 From 8411857030d92e448d5c64682f396e677161afbe Mon Sep 17 00:00:00 2001 From: tassaron Date: Mon, 28 Aug 2017 18:54:54 -0400 Subject: ctrl-c ends commandline mode properly --- setup.py | 2 +- src/command.py | 9 +++++++++ src/components/spectrum.py | 3 ++- src/core.py | 10 ++++------ src/toolkit/frame.py | 1 + src/video_thread.py | 11 ++++++++--- 6 files changed, 25 insertions(+), 11 deletions(-) (limited to 'src/toolkit/frame.py') diff --git a/setup.py b/setup.py index dd546e2..cdf4c4a 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup import os -__version__ = '2.0.0.rc4' +__version__ = '2.0.0rc5' def package_files(directory): diff --git a/src/command.py b/src/command.py index 4116c5a..cd3c6c3 100644 --- a/src/command.py +++ b/src/command.py @@ -8,6 +8,7 @@ import argparse import os import sys import time +import signal from core import Core @@ -91,6 +92,9 @@ class Command(QtCore.QObject): for arg in args: self.core.selectedComponents[i].command(arg) + # ctrl-c stops the export thread + signal.signal(signal.SIGINT, self.stopVideo) + if self.args.export and self.args.projpath: errcode, data = self.core.parseAvFile(projPath) for key, value in data['WindowFields']: @@ -124,6 +128,11 @@ class Command(QtCore.QObject): self.worker.progressBarSetText.connect(self.progressBarSetText) self.createVideo.emit() + def stopVideo(self, *args): + self.worker.error = True + self.worker.cancelExport() + self.worker.cancel() + @QtCore.pyqtSlot(str) def progressBarSetText(self, value): if 'Export ' in value: diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 77cb086..6675f5b 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -98,7 +98,8 @@ class Component(Component): def preFrameRender(self, **kwargs): super().preFrameRender(**kwargs) - self.previewPipe.wait() + if self.previewPipe is not None: + self.previewPipe.wait() self.updateChunksize() w, h = scale(self.scale, self.width, self.height, str) self.video = FfmpegVideo( diff --git a/src/core.py b/src/core.py index 1a90296..d7445c9 100644 --- a/src/core.py +++ b/src/core.py @@ -13,8 +13,8 @@ import toolkit log = logging.getLogger('AVP.Core') -STDOUT_LOGLVL = logging.INFO -FILE_LOGLVL = logging.VERBOSE +STDOUT_LOGLVL = logging.WARNING +FILE_LOGLVL = None class Core: @@ -77,8 +77,7 @@ class Core: if compPos < 0 or compPos > len(self.selectedComponents): compPos = len(self.selectedComponents) if len(self.selectedComponents) > 50: - return None - + return -1 if type(component) is int: # create component using module index in self.modules moduleIndex = int(component) @@ -188,7 +187,6 @@ class Core: for key, value in data['Settings']: Core.settings.setValue(key, value) - for tup in data['Components']: name, vers, preset = tup clearThis = False @@ -213,7 +211,7 @@ class Core: self.moduleIndexFor(name), loader ) - if i is None: + if i == -1: loader.showMessage(msg="Too many components!") break diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index aefb55f..0e200b5 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -32,6 +32,7 @@ class FramePainter(QtGui.QPainter): super().setPen(penStyle) def finalize(self): + log.verbose("Finalizing FramePainter") imBytes = self.image.bits().asstring(self.image.byteCount()) frame = Image.frombytes( 'RGBA', (self.image.width(), self.image.height()), imBytes diff --git a/src/video_thread.py b/src/video_thread.py index 823ac73..91ebe93 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -252,9 +252,14 @@ class Worker(QtCore.QObject): print('############################') log.info('Opening pipe to ffmpeg') log.info(cmd) - self.out_pipe = openPipe( - ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout - ) + try: + self.out_pipe = openPipe( + ffmpegCommand, + stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout + ) + except sp.CalledProcessError: + log.critical('Ffmpeg pipe couldn\'t be created!') + raise # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # START CREATING THE VIDEO -- cgit v1.2.3 From 05d2ebc3c69f5a876d602004f69202c5ba8b09f7 Mon Sep 17 00:00:00 2001 From: tassaron Date: Fri, 22 Apr 2022 17:09:50 -0400 Subject: make pip-installable as a package --- MANIFEST.in | 7 ++++++ setup.py | 61 ++++++++++++++++++++++++++-------------------- src/__init__.py | 6 ++--- src/__main__.py | 4 +-- src/component.py | 4 +-- src/components/color.py | 4 +-- src/components/image.py | 4 +-- src/components/life.py | 4 +-- src/components/original.py | 4 +-- src/components/sound.py | 4 +-- src/components/spectrum.py | 8 +++--- src/components/text.py | 4 +-- src/components/video.py | 8 +++--- src/components/waveform.py | 8 +++--- src/core.py | 12 ++++----- src/gui/actions.py | 2 +- src/gui/mainwindow.py | 13 +++++----- src/gui/presetmanager.py | 6 ++--- src/gui/preview_thread.py | 4 +-- src/main.py | 11 ++++----- src/toolkit/__init__.py | 2 +- src/toolkit/ffmpeg.py | 8 +++--- src/toolkit/frame.py | 2 +- src/video_thread.py | 8 +++--- 24 files changed, 106 insertions(+), 92 deletions(-) create mode 100644 MANIFEST.in (limited to 'src/toolkit/frame.py') diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2b2d794 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +recursive-include src/tests +include src/components/*.ui +include src/gui/*.ui +include src/gui/background.png +include src/encoder-options.json +global-exclude src/components/__template__.ui +global-exclude *.py[cod] diff --git a/setup.py b/setup.py index cdf4c4a..5e01229 100644 --- a/setup.py +++ b/setup.py @@ -1,29 +1,39 @@ -from setuptools import setup -import os +from setuptools import setup, find_packages +from importlib import import_module +from os import path +import re -__version__ = '2.0.0rc5' +def getTextFromFile(filename, fallback): + try: + with open( + path.join(path.abspath(path.dirname(__file__)), filename), encoding="utf-8" + ) as f: + output = f.read() + except Exception: + output = fallback + return output -def package_files(directory): - paths = [] - for (path, directories, filenames) in os.walk(directory): - for filename in filenames: - paths.append(os.path.join('..', path, filename)) - return paths +PACKAGE_NAME = 'avp' +SOURCE_DIRECTORY = 'src' +SOURCE_PACKAGE_REGEX = re.compile(rf'^{SOURCE_DIRECTORY}') +PACKAGE_DESCRIPTION = 'Create audio visualization videos from a GUI or commandline' + + +avp = import_module(SOURCE_DIRECTORY) +source_packages = find_packages(include=[SOURCE_DIRECTORY, f'{SOURCE_DIRECTORY}.*']) +proj_packages = [SOURCE_PACKAGE_REGEX.sub(PACKAGE_NAME, name) for name in source_packages] setup( name='audio_visualizer_python', - version=__version__, + version=avp.__version__, url='https://github.com/djfun/audio-visualizer-python/tree/feature-newgui', license='MIT', - description='Create audio visualization videos from a GUI or commandline', - long_description="Create customized audio visualization videos and save " - "them as Projects to continue editing later. Different components can " - "be added and layered to add visualizers, images, videos, gradients, " - "text, etc. Use Projects created in the GUI with commandline mode to " - "automate your video production workflow without any complex syntax.", + description=PACKAGE_DESCRIPTION, + author=getTextFromFile('AUTHORS', 'djfun, tassaron'), + long_description=getTextFromFile('README.md', PACKAGE_DESCRIPTION), classifiers=[ 'Development Status :: 4 - Beta', 'License :: OSI Approved :: MIT License', @@ -35,19 +45,18 @@ setup( 'visualizer', 'visualization', 'commandline video', 'video editor', 'ffmpeg', 'podcast' ], - packages=[ - 'avpython', - 'avpython.toolkit', - 'avpython.components' + packages=proj_packages, + package_dir={PACKAGE_NAME: SOURCE_DIRECTORY}, + include_package_data=True, + install_requires=[ + 'Pillow-SIMD', + 'PyQt5', + 'numpy', + 'pytest' ], - package_dir={'avpython': 'src'}, - package_data={ - 'avpython': package_files('src'), - }, - install_requires=['Pillow-SIMD', 'PyQt5', 'numpy'], entry_points={ 'gui_scripts': [ - 'avp = avpython.main:main' + f'avp = {PACKAGE_NAME}.main:main' ], } ) diff --git a/src/__init__.py b/src/__init__.py index 73f174a..08131ce 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -3,6 +3,9 @@ import os import logging +__version__ = '2.0.0rc6' + + class Logger(logging.getLoggerClass()): ''' Custom Logger class to handle custom VERBOSE log level. @@ -31,6 +34,3 @@ if getattr(sys, 'frozen', False): else: # unfrozen wd = os.path.dirname(os.path.realpath(__file__)) - -# make relative imports work when using /src as a package -sys.path.insert(0, wd) diff --git a/src/__main__.py b/src/__main__.py index 3babeae..3206bc8 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -1,5 +1,5 @@ -# Allows for launching with python3 -m avpython +# Allows for launching with python3 -m avp -from avpython.main import main +from .main import main main() diff --git a/src/component.py b/src/component.py index f3ee188..33c7657 100644 --- a/src/component.py +++ b/src/component.py @@ -11,8 +11,8 @@ import time import logging from copy import copy -from toolkit.frame import BlankFrame -from toolkit import ( +from .toolkit.frame import BlankFrame +from .toolkit import ( getWidgetValue, setWidgetValue, connectWidget, rgbFromString, blockSignals ) diff --git a/src/components/color.py b/src/components/color.py index 7d4f86d..6336194 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -4,8 +4,8 @@ from PyQt5.QtGui import QColor from PIL.ImageQt import ImageQt import os -from component import Component -from toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor +from ..component import Component +from ..toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor class Component(Component): diff --git a/src/components/image.py b/src/components/image.py index dd363bf..42f9564 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -2,8 +2,8 @@ from PIL import Image, ImageDraw, ImageEnhance from PyQt5 import QtGui, QtCore, QtWidgets import os -from component import Component -from toolkit.frame import BlankFrame +from ..component import Component +from ..toolkit.frame import BlankFrame class Component(Component): diff --git a/src/components/life.py b/src/components/life.py index 7a610eb..94704bc 100644 --- a/src/components/life.py +++ b/src/components/life.py @@ -4,8 +4,8 @@ from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter import os import math -from component import Component -from toolkit.frame import BlankFrame, scale +from ..component import Component +from ..toolkit.frame import BlankFrame, scale class Component(Component): diff --git a/src/components/original.py b/src/components/original.py index f886374..80228fe 100644 --- a/src/components/original.py +++ b/src/components/original.py @@ -6,8 +6,8 @@ import os import time from copy import copy -from component import Component -from toolkit.frame import BlankFrame +from ..component import Component +from ..toolkit.frame import BlankFrame class Component(Component): diff --git a/src/components/sound.py b/src/components/sound.py index 18d2a65..118ea23 100644 --- a/src/components/sound.py +++ b/src/components/sound.py @@ -1,8 +1,8 @@ from PyQt5 import QtGui, QtCore, QtWidgets import os -from component import Component -from toolkit.frame import BlankFrame +from ..component import Component +from ..toolkit.frame import BlankFrame class Component(Component): diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 6675f5b..d1f8fb6 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -6,10 +6,10 @@ import subprocess import time import logging -from component import Component -from toolkit.frame import BlankFrame, scale -from toolkit import checkOutput, connectWidget -from toolkit.ffmpeg import ( +from ..component import Component +from ..toolkit.frame import BlankFrame, scale +from ..toolkit import checkOutput, connectWidget +from ..toolkit.ffmpeg import ( openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound ) diff --git a/src/components/text.py b/src/components/text.py index 32a108e..e8c5a9c 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -4,8 +4,8 @@ from PyQt5 import QtGui, QtCore, QtWidgets import os import logging -from component import Component -from toolkit.frame import FramePainter, PaintColor +from ..component import Component +from ..toolkit.frame import FramePainter, PaintColor log = logging.getLogger('AVP.Components.Text') diff --git a/src/components/video.py b/src/components/video.py index 8ad21b5..070940d 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -5,10 +5,10 @@ import math import subprocess import logging -from component import Component -from toolkit.frame import BlankFrame, scale -from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo -from toolkit import checkOutput +from ..component import Component +from ..toolkit.frame import BlankFrame, scale +from ..toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo +from ..toolkit import checkOutput log = logging.getLogger('AVP.Components.Video') diff --git a/src/components/waveform.py b/src/components/waveform.py index cbfc47f..1a6035f 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -6,10 +6,10 @@ import math import subprocess import logging -from component import Component -from toolkit.frame import BlankFrame, scale -from toolkit import checkOutput -from toolkit.ffmpeg import ( +from ..component import Component +from ..toolkit.frame import BlankFrame, scale +from ..toolkit import checkOutput +from ..toolkit.ffmpeg import ( openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound ) diff --git a/src/core.py b/src/core.py index d7445c9..bc6f9b4 100644 --- a/src/core.py +++ b/src/core.py @@ -9,12 +9,12 @@ import json from importlib import import_module import logging -import toolkit +from . import toolkit log = logging.getLogger('AVP.Core') STDOUT_LOGLVL = logging.WARNING -FILE_LOGLVL = None +FILE_LOGLVL = logging.ERROR class Core: @@ -47,7 +47,7 @@ class Core: yield name log.debug('Importing component modules') self.modules = [ - import_module('components.%s' % name) + import_module('.components.%s' % name, __package__) for name in findComponents() ] # store canonical module names and indexes @@ -426,7 +426,7 @@ class Core: def newVideoWorker(self, loader, audioFile, outputPath): '''loader is MainWindow or Command object which must own the thread''' - import video_thread + from . import video_thread self.videoThread = QtCore.QThread(loader) videoWorker = video_thread.Worker( loader, audioFile, outputPath, self.selectedComponents @@ -450,8 +450,8 @@ class Core: @classmethod def storeSettings(cls): '''Store settings/paths to directories as class variables''' - from __init__ import wd - from toolkit.ffmpeg import findFfmpeg + from .__init__ import wd + from .toolkit.ffmpeg import findFfmpeg cls.wd = wd dataDir = QtCore.QStandardPaths.writableLocation( diff --git a/src/gui/actions.py b/src/gui/actions.py index 8e867b9..eb7b953 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py @@ -5,7 +5,7 @@ from PyQt5.QtWidgets import QUndoCommand import os from copy import copy -from core import Core +from ..core import Core # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 75534c2..da8370d 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -16,12 +16,12 @@ import filecmp import time import logging -from core import Core -import gui.preview_thread as preview_thread -from gui.preview_win import PreviewWindow -from gui.presetmanager import PresetManager -from gui.actions import * -from toolkit import ( +from ..core import Core +from . import preview_thread +from .preview_win import PreviewWindow +from .presetmanager import PresetManager +from .actions import * +from ..toolkit import ( disableWhenEncoding, disableWhenOpeningProject, checkOutput, blockSignals ) @@ -65,7 +65,6 @@ class MainWindow(QtWidgets.QMainWindow): self.settings = Core.settings # Register clean-up functions - signal.signal(signal.SIGINT, self.terminate) atexit.register(self.cleanUp) # Create stack of undoable user actions diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index 2445760..1e47a7f 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py @@ -7,9 +7,9 @@ import string import os import logging -from toolkit import badName -from core import Core -from gui.actions import * +from ..toolkit import badName +from ..core import Core +from .actions import * log = logging.getLogger('AVP.Gui.PresetManager') diff --git a/src/gui/preview_thread.py b/src/gui/preview_thread.py index d3e0581..7829476 100644 --- a/src/gui/preview_thread.py +++ b/src/gui/preview_thread.py @@ -10,8 +10,8 @@ from queue import Queue, Empty import os import logging -from toolkit.frame import Checkerboard -from toolkit import disableWhenOpeningProject +from ..toolkit.frame import Checkerboard +from ..toolkit import disableWhenOpeningProject log = logging.getLogger("AVP.Gui.PreviewThread") diff --git a/src/main.py b/src/main.py index 126e4a8..5fabda3 100644 --- a/src/main.py +++ b/src/main.py @@ -3,7 +3,7 @@ import sys import os import logging -from __init__ import wd +from .__init__ import wd log = logging.getLogger('AVP.Main') @@ -12,6 +12,7 @@ log = logging.getLogger('AVP.Main') def main(): app = QtWidgets.QApplication(sys.argv) app.setApplicationName("audio-visualizer") + proj = None # Determine mode mode = 'GUI' @@ -23,19 +24,17 @@ def main(): else: # opening a project file with gui proj = sys.argv[1] - else: - # normal gui launch - proj = None # Launch program if mode == 'commandline': - from command import Command + from .command import Command main = Command() + main.parseArgs() log.debug("Finished creating command object") elif mode == 'GUI': - from gui.mainwindow import MainWindow + from .gui.mainwindow import MainWindow window = uic.loadUi(os.path.join(wd, "gui", "mainwindow.ui")) # window.adjustSize() diff --git a/src/toolkit/__init__.py b/src/toolkit/__init__.py index 3fca275..55e5f84 100644 --- a/src/toolkit/__init__.py +++ b/src/toolkit/__init__.py @@ -1 +1 @@ -from toolkit.common import * +from .common import * diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index 419d491..3298c04 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -10,8 +10,8 @@ import signal from queue import PriorityQueue import logging -import core -from toolkit.common import checkOutput, pipeWrapper +from .. import core +from .common import checkOutput, pipeWrapper log = logging.getLogger('AVP.Toolkit.Ffmpeg') @@ -90,7 +90,7 @@ class FfmpegVideo: self.frameBuffer.task_done() def fillBuffer(self): - from component import ComponentError + from ..component import ComponentError if core.Core.logEnabled: logFilename = os.path.join( core.Core.logDir, 'render_%s.log' % str(self.component.compPos) @@ -144,7 +144,7 @@ def openPipe(commandList, **kwargs): def closePipe(pipe): pipe.stdout.close() - pipe.send_signal(signal.SIGINT) + pipe.send_signal(signal.SIGTERM) def findFfmpeg(): diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index 0e200b5..f2511fe 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -9,7 +9,7 @@ import os import math import logging -import core +from .. import core log = logging.getLogger('AVP.Toolkit.Frame') diff --git a/src/video_thread.py b/src/video_thread.py index 0a39f28..31331a3 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -19,9 +19,9 @@ import time import signal import logging -from component import ComponentError -from toolkit.frame import Checkerboard -from toolkit.ffmpeg import ( +from .component import ComponentError +from .toolkit.frame import Checkerboard +from .toolkit.ffmpeg import ( openPipe, readAudioFile, getAudioDuration, createFfmpegCommand ) @@ -400,7 +400,7 @@ class Worker(QtCore.QObject): comp.cancel() try: - self.out_pipe.send_signal(signal.SIGINT) + self.out_pipe.send_signal(signal.SIGTERM) except Exception: pass -- cgit v1.2.3 From 973c28a2947798dfd7efe59ac9b55f2585504679 Mon Sep 17 00:00:00 2001 From: tassaron Date: Wed, 27 Apr 2022 12:42:45 -0400 Subject: fix segmentation fault when rendering Color component Pillow's ImageQt is a subclass of QImage --- src/toolkit/frame.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/toolkit/frame.py') diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index f2511fe..520bd43 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -22,7 +22,8 @@ class FramePainter(QtGui.QPainter): ''' def __init__(self, width, height): image = BlankFrame(width, height) - self.image = QtGui.QImage(ImageQt(image)) + log.debug("Creating QImage from PIL image object") + self.image = ImageQt(image) super().__init__(self.image) def setPen(self, penStyle): -- cgit v1.2.3