ffmpeg functions moved to toolkit, component format simplified
component methods are auto-decorated & settings are now class variables
This commit is contained in:
parent
b1713d38fa
commit
f454814867
|
@ -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
|
||||
|
|
15
setup.py
15
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'},
|
||||
|
|
|
@ -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
|
||||
|
|
167
src/component.py
167
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)):
|
||||
'''
|
||||
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)
|
||||
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):
|
||||
'''
|
||||
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 names():
|
||||
'''
|
||||
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 your widget values from self.page, then call super().update()
|
||||
'''
|
||||
'''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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
379
src/core.py
379
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()
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
Reference in New Issue