diff --git a/command.py b/command.py index d56c64b..97eddd2 100644 --- a/command.py +++ b/command.py @@ -36,7 +36,7 @@ class Command(QtCore.QObject): help='open a project file (.avp)', nargs='?') self.parser.add_argument( '-c', '--comp', metavar=('LAYER', 'NAME', 'ARG'), - help='create/edit component NAME at LAYER.' + help='create component NAME at LAYER.' '"help" for information about possible args', nargs=3, action='append') @@ -80,12 +80,18 @@ class Command(QtCore.QObject): if self.args.comp: for comp in self.args.comp: pos, name, arg = comp + try: + pos = int(pos) + except ValueError: + print(pos, 'is not a layer number.') + quit(1) realName = self.parseCompName(name) if not realName: print(name, 'is not a valid component name.') - quit() + quit(1) modI = self.core.moduleIndexFor(realName) - self.core.insertComponent(int(pos), modI, self) + i = self.core.insertComponent(pos, modI, self) + self.core.selectedComponents[i].command(arg) self.createAudioVisualisation() @@ -99,12 +105,12 @@ class Command(QtCore.QObject): self.videoTask.emit( self.args.input, self.args.output, - self.core.selectedComponents) + list(reversed(self.core.selectedComponents)) + ) def videoCreated(self): self.videoThread.quit() self.videoThread.wait() - self.cleanUp() def showMessage(self, **kwargs): print(kwargs['msg']) @@ -114,22 +120,20 @@ class Command(QtCore.QObject): def drawPreview(self, *args): pass - def cleanUp(self, *args): - pass - def parseCompName(self, name): '''Deduces a proper component name out of a commandline arg''' - compFileNames = [ \ - os.path.splitext(os.path.basename( - mod.__file__))[0] \ - for mod in self.core.modules \ - ] if name.title() in self.core.compNames: return name.title() for compName in self.core.compNames: if name.capitalize() in compName: return compName + + compFileNames = [ \ + os.path.splitext(os.path.basename( + mod.__file__))[0] \ + for mod in self.core.modules \ + ] for i, compFileName in enumerate(compFileNames): if name.lower() in compFileName: return self.core.compNames[i] diff --git a/components/__base__.py b/components/__base__.py index 88f22d4..e43a517 100644 --- a/components/__base__.py +++ b/components/__base__.py @@ -1,5 +1,6 @@ from PyQt4 import QtGui, QtCore from PIL import Image +import os class Component(QtCore.QObject): @@ -7,11 +8,12 @@ class Component(QtCore.QObject): # modified = QtCore.pyqtSignal(int, bool) - 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 def __str__(self): return self.__doc__ @@ -32,24 +34,59 @@ class Component(QtCore.QObject): # read your widget values, then call super().update() def loadPreset(self, presetDict, presetName): - '''Children should take (presetDict, presetName=None) as args''' - - # Use super().loadPreset(presetDict, presetName) - # Then update your widgets using the preset dict + '''Subclasses take (presetDict, presetName=None) as args. + Must use super().loadPreset(presetDict, presetName) first, + then update self.page widgets using the preset dict. + ''' self.currentPreset = presetName \ if presetName != None else presetDict['preset'] - ''' - def savePreset(self): - return {} - ''' + def preFrameRender(self, **kwargs): + '''Triggered only before a video is exported (video_thread.py) + self.worker = the video thread worker + self.completeAudioArray = a list of audio samples + self.sampleSize = number of audio samples per video frame + self.progressBarUpdate = signal to set progress bar number + self.progressBarSetText = signal to set progress bar text + Use the latter two signals to update the MainProgram if needed + for a long initialization procedure (i.e., for a visualizer) + ''' for var, value in kwargs.items(): exec('self.%s = value' % var) + def command(self, arg): + '''Configure a component using argument from the commandline. + Use super().command(arg) at the end of a subclass's method, + if no arguments are found in that method first + ''' + if arg.startswith('preset='): + _, preset = arg.split('=', 1) + path = os.path.join(self.core.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( + 'To open a preset for this component:\n' + ' "preset=Preset Name"\n') + self.commandHelp() + quit(0) + + def commandHelp(self): + '''Print help text for this Component's commandline arguments''' + def blankFrame(self, width, height): return Image.new("RGBA", (width, height), (0, 0, 0, 0)) def pickColor(self): + '''Use color picker to get color input from the user, + and return this as an RGB string and QPushButton stylesheet. + In a subclass apply stylesheet to any color selection widgets + ''' dialog = QtGui.QColorDialog() dialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True) color = dialog.getColor() @@ -63,7 +100,7 @@ class Component(QtCore.QObject): return None, None def RGBFromString(self, string): - ''' turns an RGB string like "255, 255, 255" into a tuple ''' + ''' Turns an RGB string like "255, 255, 255" into a tuple ''' try: tup = tuple([int(i) for i in string.split(',')]) if len(tup) != 3: @@ -83,14 +120,10 @@ class Component(QtCore.QObject): self.parent = parent page = uic.loadUi(os.path.join( os.path.dirname(os.path.realpath(__file__)), 'example.ui')) - # connect widgets signals + # --- connect widget signals here --- self.page = page return page - def update(self): - super().update() - self.parent.drawPreview() - def previewRender(self, previewWorker): width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) @@ -102,12 +135,6 @@ class Component(QtCore.QObject): height = int(self.worker.core.settings.value('outputHeight')) image = Image.new("RGBA", (width, height), (0,0,0,0)) return image - - def cancel(self): - self.canceled = True - - def reset(self): - self.canceled = False ''' class BadComponentInit(Exception): diff --git a/components/image.py b/components/image.py index b6aa29b..d0e1894 100644 --- a/components/image.py +++ b/components/image.py @@ -92,3 +92,20 @@ class Component(__base__.Component): self.settings.setValue("backgroundDir", os.path.dirname(filename)) self.page.lineEdit_image.setText(filename) self.update() + + def command(self, arg): + if not arg.startswith('preset='): + if os.path.exists(arg): + try: + Image.open(arg) + self.imagePath = arg + self.stretched = True + return True + except OSError as e: + print("Not a supported image format") + quit(1) + super().command(arg) + + def commandHelp(self): + print('Give a complete filepath to an image to load that ' + 'image with default settings.') diff --git a/components/original.py b/components/original.py index 5e2f9d4..328d64f 100644 --- a/components/original.py +++ b/components/original.py @@ -183,3 +183,15 @@ class Component(__base__.Component): return im + def command(self, arg): + if not arg.startswith('preset='): + if arg == 'classic': + self.layout = 0; return + elif arg == 'split': + self.layout = 1; return + elif arg == 'bottom': + self.layout = 2; return + super().command(arg) + + def commandHelp(self): + print('Give a layout name: classic, split, or bottom') diff --git a/components/text.py b/components/text.py index f8ef7b3..6c465b1 100644 --- a/components/text.py +++ b/components/text.py @@ -146,3 +146,13 @@ class Component(__base__.Component): return self.page.lineEdit_textColor.setText(RGBstring) self.page.pushButton_textColor.setStyleSheet(btnStyle) + + def commandHelp(self, arg): + print('Enter a string to use as centred white text. ' + 'Use quotes around the string to escape spaces.') + + def command(self, arg): + if not arg.startswith('preset='): + self.title = arg + return True + super().command(arg) diff --git a/components/video.py b/components/video.py index 3d43a18..dd385b4 100644 --- a/components/video.py +++ b/components/video.py @@ -221,6 +221,22 @@ class Component(__base__.Component): width, height = scale(self.scale, width, height, int) self.chunkSize = 4*width*height + def command(self, arg): + if not arg.startswith('preset='): + if os.path.exists(arg): + if os.path.splitext(arg)[1] in self.core.videoFormats: + self.videoPath = arg + self.scale = 100 + return True + else: + print("Not a supported video format") + quit(1) + super().command(arg) + + def commandHelp(self): + print('Give a complete filepath to a video to load that ' + 'video with default settings.') + def scale(scale, width, height, returntype=None): width = (float(width) / 100.0) * float(scale) height = (float(height) / 100.0) * float(scale) diff --git a/core.py b/core.py index 2dde464..42eb44e 100644 --- a/core.py +++ b/core.py @@ -86,7 +86,7 @@ class Core(): return None component = self.modules[moduleIndex].Component( - moduleIndex, compPos) + moduleIndex, compPos, self) self.selectedComponents.insert( compPos, component) @@ -142,6 +142,10 @@ 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): @@ -153,10 +157,11 @@ class Core(): return saveValueStore def openProject(self, loader, filepath): - '''loader is the object calling this method which must have - its own showMessage(**kwargs) method for displaying errors''' + ''' loader is the object calling this method which must have + its own showMessage(**kwargs) method for displaying errors. + ''' errcode, data = self.parseAvFile(filepath) - print(data) + #print(data) if errcode == 0: try: for i, tup in enumerate(data['Components']): @@ -224,12 +229,14 @@ class Core(): def parseAvFile(self, filepath): '''Parses an avp (project) or avl (preset package) file. - Returns data usable by another method.''' + Returns dictionary with section names as the keys, each one + contains a list of tuples: (compName, version, compPresetDict) + ''' data = {} try: with open(filepath, 'r') as f: def parseLine(line): - '''Decides if a given avp or avl line is a section header''' + '''Decides if a file line is a section header''' validSections = ('Components') line = line.strip() newSection = '' @@ -313,8 +320,7 @@ class Core(): def createPresetFile( self, compName, vers, presetName, saveValueStore, filepath=''): '''Create a preset file (.avl) at filepath using args. - Or if filepath is empty, create an internal preset using - the args for the filepath.''' + Or if filepath is empty, create an internal preset using args''' if not filepath: dirname = os.path.join(self.presetDir, compName, str(vers)) if not os.path.exists(dirname): diff --git a/main.py b/main.py index 140392c..e04d002 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,6 @@ from PyQt4 import QtGui, uic import sys import os -import atexit -import signal import core import preview_thread @@ -32,7 +30,7 @@ def LoadDefaultSettings(self): } for parm, value in default.items(): - print(parm, self.settings.value(parm)) + #print(parm, self.settings.value(parm)) if self.settings.value(parm) is None: self.settings.setValue(parm, value) @@ -62,6 +60,8 @@ if __name__ == "__main__": elif mode == 'gui': from mainwindow import * + import atexit + import signal window = uic.loadUi(os.path.join( os.path.dirname(os.path.realpath(__file__)), "mainwindow.ui")) @@ -73,10 +73,10 @@ if __name__ == "__main__": window.resize(window.width() * (dpi / 96), window.height() * (dpi / 96)) # window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0) + signal.signal(signal.SIGINT, main.cleanUp) + atexit.register(main.cleanUp) + main = MainWindow(window, proj) # applicable to both modes - signal.signal(signal.SIGINT, main.cleanUp) - atexit.register(main.cleanUp) - sys.exit(app.exec_()) diff --git a/mainwindow.py b/mainwindow.py index 2a8762d..6023831 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -597,7 +597,6 @@ class MainWindow(QtCore.QObject): self.openProject(filename) def openProject(self, filepath, prompt=True): - print('opening', filepath) if not filepath or not os.path.exists(filepath) \ or not filepath.endswith('.avp'): self.updateWindowTitle() diff --git a/video_thread.py b/video_thread.py index 2255259..e6c6531 100644 --- a/video_thread.py +++ b/video_thread.py @@ -29,7 +29,7 @@ class Worker(QtCore.QObject): self.modules = parent.core.modules self.parent = parent parent.videoTask.connect(self.createVideo) - self.sampleSize = 1470 + self.sampleSize = 1470 # 44100 / 30 = 1470 self.canceled = False self.error = False self.stopped = False @@ -99,7 +99,8 @@ class Worker(QtCore.QObject): # test if user has libfdk_aac encoders = sp.check_output( - self.core.FFMPEG_BIN + " -encoders -hide_banner", shell=True) + self.core.FFMPEG_BIN + " -encoders -hide_banner", + shell=True) encoders = encoders.decode("utf-8") @@ -120,15 +121,15 @@ class Worker(QtCore.QObject): vencoders = options['video-codecs'][vcodec] aencoders = options['audio-codecs'][acodec] - print(encoders) + #print(encoders) for encoder in vencoders: - print(encoder) + #print(encoder) if encoder in encoders: vencoder = encoder break for encoder in aencoders: - print(encoder) + #print(encoder) if encoder in encoders: aencoder = encoder break @@ -161,16 +162,15 @@ class Worker(QtCore.QObject): ffmpegCommand.append('-2') ffmpegCommand.append(outputFile) - self.out_pipe = sp.Popen( - ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout) - # create video for output + # ### Now start creating video for output ### numpy.seterr(divide='ignore') - # initialize components - print('loaded components:', - ["%s%s" % (num, str(component)) for num, - component in enumerate(self.components)]) + # Call preFrameRender on all components + print('Loaded Components:', ", ".join( + ["%s) %s" % (num, str(component)) \ + for num, component in enumerate(reversed(self.components)) + ])) self.staticComponents = {} numComps = len(self.components) for compNo, comp in enumerate(self.components): @@ -190,14 +190,17 @@ class Worker(QtCore.QObject): comp.frameRender(compNo, 0, 0)) self.progressBarUpdate.emit(100) + # Create ffmpeg pipe and queues for frames + self.out_pipe = sp.Popen( + ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout) self.compositeQueue = Queue() self.compositeQueue.maxsize = 20 self.renderQueue = PriorityQueue() self.renderQueue.maxsize = 20 self.previewQueue = PriorityQueue() - self.renderThreads = [] # Threads to render frames and send them back here for piping out + self.renderThreads = [] for i in range(3): self.renderThreads.append( Thread(target=self.renderNode, name="Render Thread"))