ffmpeg functions moved to toolkit, component format simplified

component methods are auto-decorated & settings are now class variables
This commit is contained in:
tassaron 2017-07-20 20:31:38 -04:00
parent b1713d38fa
commit f454814867
19 changed files with 628 additions and 495 deletions

View File

@ -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

View File

@ -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'},

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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()

View File

@ -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 = [

View File

@ -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):

View File

@ -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'))

View File

@ -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 = [

18
src/toolkit/core.py Normal file
View File

@ -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

284
src/toolkit/ffmpeg.py Normal file
View File

@ -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)

View File

@ -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))

View File

@ -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)