Compare commits
213 Commits
master
...
flickerfix
Author | SHA1 | Date |
---|---|---|
tassaron | d78d263996 | |
tassaron | d04ddba484 | |
tassaron | 98a47a21d9 | |
tassaron | ae8a547b77 | |
tassaron | 219e846984 | |
tassaron | 6611492b30 | |
tassaron | 62431a3cfe | |
tassaron | 8812c37213 | |
tassaron | 5784cdbcf8 | |
tassaron | 3c1b52205f | |
tassaron | a472246dab | |
tassaron | 65420ce285 | |
tassaron | b6b45d1270 | |
tassaron | db1ea1fc4e | |
tassaron | 1297af61c9 | |
tassaron | c1457b6dad | |
tassaron | 6f8f178778 | |
Brianna | ae2af28808 | |
tassaron | 6ecb6df236 | |
tassaron | 6fc0398602 | |
tassaron | de1324a6a7 | |
tassaron | 4329b0e947 | |
tassaron | 03a36d4297 | |
tassaron | 15d70474d4 | |
tassaron | 661526b073 | |
tassaron | d25dee6afc | |
DH4 | c799305eff | |
rikai | c517140a51 | |
tassaron | d92fc6373f | |
tassaron | d38109453c | |
tassaron | bf0890e7c8 | |
tassaron | 450b944b87 | |
tassaron | f454814867 | |
tassaron | b1713d38fa | |
Brianna | 4becfe3b51 | |
tassaron | aa464632c6 | |
tassaron | ec0abd1902 | |
Brianna | f420ad69f5 | |
tassaron | 17c8a6703a | |
tassaron | bcb8f27c2e | |
tassaron | 62ab09e3f3 | |
tassaron | cbbb787615 | |
tassaron | d7b678f66d | |
tassaron | 06c27a48bc | |
tassaron | b7931572a7 | |
tassaron | 8811b699a9 | |
tassaron | 2e37dafd70 | |
tassaron | 4c3920e630 | |
tassaron | f6fbc8d242 | |
tassaron | 94d4acc1f4 | |
tassaron | f027fd4353 | |
tassaron | 9986b1c829 | |
tassaron | 3de45b3629 | |
tassaron | 3f7ead0d1f | |
tassaron | 52f0f96f16 | |
tassaron | ad4dc052d8 | |
tassaron | 134779f6e6 | |
tassaron | 63daa31382 | |
tassaron | ba0409829d | |
tassaron | 3a6d7ae421 | |
tassaron | 0da275bf1b | |
tassaron | 38557f29f9 | |
DH4 | 0a9002b42c | |
tassaron | a95ecd7e42 | |
tassaron | 0c394d77e3 | |
tassaron | 7b6ef6349b | |
tassaron | 4eb2bc9a41 | |
Brianna | 9fe370d472 | |
tassaron | 252639e9a2 | |
tassaron | f284acbf19 | |
Brianna | f86c33d0e5 | |
tassaron | 2c82a65d1b | |
tassaron | fc2951379c | |
tassaron | 6a1a5cd6eb | |
tassaron | 675a06dd4c | |
Brianna | 55423ca4aa | |
tassaron | a2838a0c38 | |
tassaron | 45b55d8e2f | |
tassaron | e32ba958cb | |
Brianna | 1bb67d1513 | |
tassaron | 4d955c5a06 | |
DH4 | 83d55593d0 | |
tassaron | 680214f518 | |
DH4 | e92e9d79f9 | |
tassaron | 68ac0cf755 | |
DH4 | f3da72ea54 | |
DH4 | 84ceff7f54 | |
tassaron | f8628333af | |
tassaron | 407ba57e4e | |
Brianna | 5607bff61f | |
Brianna | 7d4fb78438 | |
DH4 | 8c9914850e | |
DH4 | 60d62599f7 | |
tassaron | 3c903794e3 | |
tassaron | 49cda1bf3a | |
tassaron | b21a953dda | |
tassaron | 5c74d496a9 | |
tassaron | 82011de966 | |
tassaron | 044fddfa9c | |
tassaron | 2c63b05376 | |
tassaron | ffc5966042 | |
tassaron | aa9926590b | |
tassaron | 4de39ebe07 | |
tassaron | 4b1058781d | |
Brianna | fc7ee6d8e5 | |
tassaron | ee8031925f | |
tassaron | c05efc73ee | |
tassaron | 8603fa12e3 | |
tassaron | cb639e5c7c | |
tassaron | 8846af57ba | |
tassaron | 807e37bddd | |
tassaron | 2ad14b7d6c | |
tassaron | 307d499f9a | |
tassaron | dbbefbf70e | |
tassaron | 28f07272cc | |
tassaron | be5d47f863 | |
tassaron | 59c2c090ab | |
tassaron | b048312882 | |
tassaron | d3f979ef24 | |
tassaron | c51d86dd74 | |
tassaron | 4fc73f1e09 | |
tassaron | bb1e54b31e | |
tassaron | 6079c4fd24 | |
tassaron | 292d21c203 | |
tassaron | 6093e701e1 | |
tassaron | 6f2f02b709 | |
DH4 | acf2290025 | |
DH4 | 02795503d0 | |
DH4 | fed9481d9a | |
DH4 | e6beca94a3 | |
tassaron | c946133da9 | |
DH4 | 231af74ea2 | |
DH4 | 0948afd6e8 | |
tassaron | 6c78c96d80 | |
DH4 | 6a1deb9b78 | |
DH4 | 0d1e7459e1 | |
DH4 | 4920fcc034 | |
DH4 | 7946e98f22 | |
tassaron | 47509ae2b1 | |
DH4 | be18deece5 | |
DH4 | 0a8a2fdf71 | |
tassaron | e58a1d0b2d | |
tassaron | 277e86f279 | |
tassaron | 39e66ffa2d | |
tassaron | 443c65455a | |
tassaron | cfb8e17b63 | |
tassaron | 5480b20d40 | |
DH4 | 5b78a26d80 | |
tassaron | 825b7af6e3 | |
DH4 | e6d119769f | |
DH4 | a3557cbc4f | |
DH4 | cf197904b8 | |
Brianna | 2d515540aa | |
DH4 | aae04194a0 | |
tassaron | 2cbae481c5 | |
DH4 | d417833193 | |
DH4 | c008571ae2 | |
tassaron | f0ab2f53d6 | |
tassaron | 0bef283f8d | |
tassaron | fccdee45b2 | |
DH4 | 4b56660177 | |
DH4 | e33caa9179 | |
DH4 | 53598f7a85 | |
DH4 | 73a0492585 | |
DH4 | 6bf36d0324 | |
DH4 | 7d8e9ab3b1 | |
Brianna | 30f2ea12df | |
tassaron | 2768084b30 | |
tassaron | d31add0d95 | |
tassaron | 610db20606 | |
tassaron | 00f5c88584 | |
tassaron | 907ba33e93 | |
tassaron | 1a24922fee | |
tassaron | f55d7d1206 | |
Brianna | 11e5ec0439 | |
DH4 | fcbe211bf1 | |
DH4 | 43073cbd42 | |
DH4 | 6620f48bfd | |
tassaron | 2afdba04fd | |
tassaron | 0b210fb4f0 | |
Brianna | a1ae1dfde9 | |
DH4 | 1eeb763dc3 | |
DH4 | c21d6f5ea7 | |
tassaron | 9be8f742c6 | |
tassaron | 5295a6d9ae | |
tassaron | ca7e8bdb0d | |
Brianna | 7240f25deb | |
Brianna | 369ac2a855 | |
tassaron | d1852619df | |
tassaron | 8dd7b7d59a | |
tassaron | 37fd68fd2b | |
tassaron | 75f1e8af76 | |
Brianna | 025bc2c2e6 | |
tassaron | db7acbf3ea | |
tassaron | c0920da4ff | |
tassaron | ce414ff960 | |
tassaron | 39944a56a8 | |
DH4 | 6433f6d580 | |
DH4 | b2e3716a29 | |
tassaron | d9641d8db3 | |
tassaron | fa89cd38f2 | |
DH4 | 719e9a4ddf | |
DH4 | e3079f7a67 | |
tassaron | 5101b439df | |
tassaron | 5ed79ff5c6 | |
tassaron | e0eed5bff4 | |
DH4 | d9a5f2dd34 | |
DH4 | 75c1c65c9d | |
DH4 | fe13268a84 | |
DH4 | 86c6ac8762 | |
DH4 | 1a8acdbed0 | |
DH4 | f2329e9366 | |
DH4 | eaee0ab233 |
|
@ -1,3 +1,15 @@
|
|||
__pycache__
|
||||
settings.ini
|
||||
build/*
|
||||
*.py[cod]
|
||||
build/*
|
||||
dist/*
|
||||
env/*
|
||||
.vscode/*
|
||||
*.mkv
|
||||
*.mp4
|
||||
*.zip
|
||||
*.tar
|
||||
*.tar.*
|
||||
*.exe
|
||||
ffmpeg
|
||||
*.bak
|
||||
*~
|
||||
|
|
43
README.md
43
README.md
|
@ -1,38 +1,45 @@
|
|||
audio-visualizer-python
|
||||
=======================
|
||||
**We need a good name that is not as generic as "audio-visualizer-python"!**
|
||||
|
||||
This is a little GUI tool which creates an audio visualization video from an input audio.
|
||||
You can also give it a background image and set a title text.
|
||||
This is a little GUI tool which creates an audio visualization video from an input audio file. Different components can be added and layered to change the resulting video and add images, videos, gradients, text, etc. Encoding options can be changed with a variety of different output containers.
|
||||
|
||||
I have tested the program on Linux (Ubuntu 14.10) and Windows (Windows 7), it should also work on Mac OS X. If you encounter problems
|
||||
running it or have other bug reports or features, that you wish to see implemented, please fork the project and send me a pull request and/or file an issue on this project.
|
||||
Projects can be created from the GUI and used in commandline mode for easy automation of video production. Create a template project named `template` with your typical visualizers and watermarks, and add text to the top layer from commandline:
|
||||
`avp template -c 99 text "title=Episode 371" -i /this/weeks/audio.ogg -o out`
|
||||
|
||||
I also need a good name that is not as generic as "audio-visualizer-python"!
|
||||
For more information use `avp --help` or for help with a particular component use `avp -c 0 componentName help`.
|
||||
|
||||
The program works on Linux, macOS, and Windows. If you encounter problems running it or have other bug reports or features that you wish to see implemented, please fork the project and submit a pull request and/or file an issue on this project.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
You need Python 3, PyQt4, PIL (or Pillow), numpy and the program ffmpeg, which is used to read the audio and render the video.
|
||||
Python 3.4, FFmpeg 3.3, PyQt5, Pillow-SIMD, NumPy
|
||||
|
||||
**Note:** Pillow may be used as a drop-in replacement for Pillow-SIMD if problems are encountered installing. However this will result in much slower video export times. For help installing Pillow-SIMD, see the [Pillow installation guide](http://pillow.readthedocs.io/en/3.1.x/installation.html).
|
||||
|
||||
Installation
|
||||
------------
|
||||
### Manual installation on Ubuntu
|
||||
* Get all the python stuff: `sudo apt-get install python3 python3-pyqt4 python3-pil python3-numpy`
|
||||
* Get ffmpeg/avconv:
|
||||
You can either use `avconv` from the standard repositories (package `libav-tools`) or get `ffmpeg` from the [website](http://ffmpeg.org/) or from a PPA (e.g. [https://launchpad.net/~jon-severinsson/+archive/ubuntu/ffmpeg](https://launchpad.net/~jon-severinsson/+archive/ubuntu/ffmpeg). The program does automatically detect if you don't have the ffmpeg binary and tries to use avconv instead.
|
||||
### Manual installation on Ubuntu 16.04
|
||||
* Install pip: `sudo apt-get install python3-pip`
|
||||
* If Pillow is installed, it must be removed. Nothing should break because Pillow-SIMD is simply a drop-in replacement with better performance.
|
||||
* Download audio-visualizer-python from this repository and run `sudo pip3 install .` in this directory
|
||||
* Install `ffmpeg` from the [website](http://ffmpeg.org/) or from a PPA (e.g. [https://launchpad.net/~jonathonf/+archive/ubuntu/ffmpeg-3](https://launchpad.net/~jonathonf/+archive/ubuntu/ffmpeg-3)). NOTE: `ffmpeg` in the standard repos is too old (v2.8). Old versions and `avconv` may be used but full functionality is only guaranteed with `ffmpeg` 3.3 or higher.
|
||||
|
||||
Download audio-visualizer-python from this repository and run it with `python3 main.py`.
|
||||
Run the program with `avp` or `python3 -m avpython`
|
||||
|
||||
### Manual installation on Windows
|
||||
* Download and install Python 3.4 from [https://www.python.org/downloads/windows/](https://www.python.org/downloads/windows/)
|
||||
* Download and install PyQt4 for Python 3.4 and Qt4 from [http://www.riverbankcomputing.co.uk/software/pyqt/download](http://www.riverbankcomputing.co.uk/software/pyqt/download)
|
||||
* Download and install numpy from [http://www.scipy.org/scipylib/download.html](http://www.scipy.org/scipylib/download.html). There is an installer available, make sure to get the one for Python 3.4
|
||||
* Download and install Pillow from [https://pypi.python.org/pypi/Pillow/3.3.0](https://pypi.python.org/pypi/Pillow/3.3.0)
|
||||
* **Warning:** [Compiling Pillow is difficult on Windows](http://pillow.readthedocs.io/en/3.1.x/installation.html#building-on-windows) and required for the best experience.
|
||||
* Download and install Python 3.6 from [https://www.python.org/downloads/windows/](https://www.python.org/downloads/windows/)
|
||||
* Add Python to your system PATH (it will ask during the installation process).
|
||||
* Brave treacherous valley of getting prerequisites to [compile Pillow on Windows](https://www.pypkg.com/pypi/pillow-simd/f/winbuild/README.md). This is necessary because binary builds for Pillow-SIMD are not available.
|
||||
* **Alternative:** install Pillow instead of Pillow-SIMD, for which binaries *are* available. However this will result in much slower video export times.
|
||||
* Open command prompt and run: `pip install pyqt5 numpy pillow-simd`
|
||||
* Download and install ffmpeg from [https://www.ffmpeg.org/download.html](https://www.ffmpeg.org/download.html). You can use the static builds.
|
||||
* Add ffmpeg to your system PATH environment variable.
|
||||
* Add ffmpeg to your system PATH, too. [How to edit the PATH on Windows.](https://www.java.com/en/download/help/path.xml)
|
||||
|
||||
Download audio-visualizer-python from this repository and run it from the command line with `C:\Python34\python.exe main.py`.
|
||||
Download audio-visualizer-python from this repository and run it from the command line with `python main.py`.
|
||||
|
||||
### Manual installation on macOS
|
||||
### Manual installation on macOS **[Outdated]**
|
||||
|
||||
* Install [Homebrew](http://brew.sh/)
|
||||
* Use the following commands to install the needed dependencies:
|
||||
|
|
210
core.py
210
core.py
|
@ -1,210 +0,0 @@
|
|||
import sys, io, os
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from PyQt4.QtGui import QPainter, QColor
|
||||
from os.path import expanduser
|
||||
import subprocess as sp
|
||||
import numpy
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from PIL.ImageQt import ImageQt
|
||||
import tempfile
|
||||
from shutil import rmtree
|
||||
import atexit
|
||||
|
||||
class Core():
|
||||
|
||||
def __init__(self):
|
||||
self.lastBackgroundImage = ""
|
||||
self._image = None
|
||||
|
||||
self.FFMPEG_BIN = self.findFfmpeg()
|
||||
self.tempDir = None
|
||||
atexit.register(self.deleteTempDir)
|
||||
|
||||
def findFfmpeg(self):
|
||||
if sys.platform == "win32":
|
||||
return "ffmpeg.exe"
|
||||
else:
|
||||
try:
|
||||
with open(os.devnull, "w") as f:
|
||||
sp.check_call(['ffmpeg', '-version'], stdout=f, stderr=f)
|
||||
return "ffmpeg"
|
||||
except:
|
||||
return "avconv"
|
||||
|
||||
def parseBaseImage(self, backgroundImage, preview=False):
|
||||
''' determines if the base image is a single frame or list of frames '''
|
||||
if backgroundImage == "":
|
||||
return []
|
||||
else:
|
||||
_, bgExt = os.path.splitext(backgroundImage)
|
||||
if not bgExt == '.mp4':
|
||||
return [backgroundImage]
|
||||
else:
|
||||
return self.getVideoFrames(backgroundImage, preview)
|
||||
|
||||
def drawBaseImage(self, backgroundFile, titleText, titleFont, fontSize, alignment,\
|
||||
xOffset, yOffset, textColor, visColor):
|
||||
if backgroundFile == '':
|
||||
im = Image.new("RGB", (1280, 720), "black")
|
||||
else:
|
||||
im = Image.open(backgroundFile)
|
||||
|
||||
if self._image == None or not self.lastBackgroundImage == backgroundFile:
|
||||
self.lastBackgroundImage = backgroundFile
|
||||
|
||||
# resize if necessary
|
||||
if not im.size == (1280, 720):
|
||||
im = im.resize((1280, 720), Image.ANTIALIAS)
|
||||
|
||||
self._image = ImageQt(im)
|
||||
|
||||
self._image1 = QtGui.QImage(self._image)
|
||||
painter = QPainter(self._image1)
|
||||
font = titleFont
|
||||
font.setPixelSize(fontSize)
|
||||
painter.setFont(font)
|
||||
painter.setPen(QColor(*textColor))
|
||||
|
||||
yPosition = yOffset
|
||||
|
||||
fm = QtGui.QFontMetrics(font)
|
||||
if alignment == 0: #Left
|
||||
xPosition = xOffset
|
||||
if alignment == 1: #Middle
|
||||
xPosition = xOffset - fm.width(titleText)/2
|
||||
if alignment == 2: #Right
|
||||
xPosition = xOffset - fm.width(titleText)
|
||||
painter.drawText(xPosition, yPosition, titleText)
|
||||
painter.end()
|
||||
|
||||
buffer = QtCore.QBuffer()
|
||||
buffer.open(QtCore.QIODevice.ReadWrite)
|
||||
self._image1.save(buffer, "PNG")
|
||||
|
||||
strio = io.BytesIO()
|
||||
strio.write(buffer.data())
|
||||
buffer.close()
|
||||
strio.seek(0)
|
||||
return Image.open(strio)
|
||||
|
||||
def drawBars(self, spectrum, image, color):
|
||||
|
||||
imTop = Image.new("RGBA", (1280, 360))
|
||||
draw = ImageDraw.Draw(imTop)
|
||||
r, g, b = color
|
||||
color2 = (r, g, b, 50)
|
||||
for j in range(0, 63):
|
||||
draw.rectangle((10 + j * 20, 325, 10 + j * 20 + 20, 325 - spectrum[j * 4] * 1 - 10), fill=color2)
|
||||
draw.rectangle((15 + j * 20, 320, 15 + j * 20 + 10, 320 - spectrum[j * 4] * 1), fill=color)
|
||||
|
||||
|
||||
imBottom = imTop.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
|
||||
im = Image.new("RGB", (1280, 720), "black")
|
||||
im.paste(image, (0, 0))
|
||||
im.paste(imTop, (0, 0), mask=imTop)
|
||||
im.paste(imBottom, (0, 360), mask=imBottom)
|
||||
|
||||
return im
|
||||
|
||||
def readAudioFile(self, 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 = sp.Popen(command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8)
|
||||
|
||||
completeAudioArray = numpy.empty(0, dtype="int16")
|
||||
|
||||
while True:
|
||||
# read 2 seconds of audio
|
||||
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)
|
||||
# print(audio_array)
|
||||
|
||||
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
|
||||
|
||||
def transformData(self, i, completeAudioArray, sampleSize, smoothConstantDown, smoothConstantUp, lastSpectrum):
|
||||
if len(completeAudioArray) < (i + sampleSize):
|
||||
sampleSize = len(completeAudioArray) - i
|
||||
|
||||
window = numpy.hanning(sampleSize)
|
||||
data = completeAudioArray[i:i+sampleSize][::1] * window
|
||||
paddedSampleSize = 2048
|
||||
paddedData = numpy.pad(data, (0, paddedSampleSize - sampleSize), 'constant')
|
||||
spectrum = numpy.fft.fft(paddedData)
|
||||
sample_rate = 44100
|
||||
frequencies = numpy.fft.fftfreq(len(spectrum), 1./sample_rate)
|
||||
|
||||
y = abs(spectrum[0:int(paddedSampleSize/2) - 1])
|
||||
|
||||
# filter the noise away
|
||||
# y[y<80] = 0
|
||||
|
||||
y = 20 * numpy.log10(y)
|
||||
y[numpy.isinf(y)] = 0
|
||||
|
||||
if lastSpectrum is not None:
|
||||
lastSpectrum[y < lastSpectrum] = y[y < lastSpectrum] * smoothConstantDown + lastSpectrum[y < lastSpectrum] * (1 - smoothConstantDown)
|
||||
lastSpectrum[y >= lastSpectrum] = y[y >= lastSpectrum] * smoothConstantUp + lastSpectrum[y >= lastSpectrum] * (1 - smoothConstantUp)
|
||||
else:
|
||||
lastSpectrum = y
|
||||
|
||||
x = frequencies[0:int(paddedSampleSize/2) - 1]
|
||||
|
||||
return lastSpectrum
|
||||
|
||||
def deleteTempDir(self):
|
||||
if self.tempDir and os.path.exists(self.tempDir):
|
||||
rmtree(self.tempDir)
|
||||
|
||||
|
||||
def getVideoFrames(self, videoPath, firstOnly=False):
|
||||
self.tempDir = os.path.join(tempfile.gettempdir(), 'audio-visualizer-python-data')
|
||||
# recreate the temporary directory so it is empty
|
||||
self.deleteTempDir()
|
||||
os.mkdir(self.tempDir)
|
||||
if firstOnly:
|
||||
filename = 'preview%s.jpg' % os.path.basename(videoPath).split('.', 1)[0]
|
||||
options = '-ss 10 -vframes 1'
|
||||
else:
|
||||
filename = '$frame%05d.jpg'
|
||||
options = ''
|
||||
sp.call( \
|
||||
'%s -i "%s" -y %s "%s"' % ( \
|
||||
self.FFMPEG_BIN,
|
||||
videoPath,
|
||||
options,
|
||||
os.path.join(self.tempDir, filename)
|
||||
),
|
||||
shell=True
|
||||
)
|
||||
return sorted([os.path.join(self.tempDir, f) for f in os.listdir(self.tempDir)])
|
||||
|
||||
@staticmethod
|
||||
def RGBFromString(string):
|
||||
''' turns an RGB string like "255, 255, 255" into a tuple '''
|
||||
try:
|
||||
tup = tuple([int(i) for i in string.split(',')])
|
||||
if len(tup) != 3:
|
||||
raise ValueError
|
||||
for i in tup:
|
||||
if i > 255 or i < 0:
|
||||
raise ValueError
|
||||
return tup
|
||||
except:
|
||||
return (255, 255, 255)
|
|
@ -0,0 +1,59 @@
|
|||
from cx_Freeze import setup, Executable
|
||||
import sys
|
||||
import os
|
||||
|
||||
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')
|
||||
|
||||
buildOptions = dict(
|
||||
excludes=[
|
||||
"apport",
|
||||
"apt",
|
||||
"curses",
|
||||
"distutils",
|
||||
"email",
|
||||
"html",
|
||||
"http",
|
||||
"xmlrpc",
|
||||
"nose",
|
||||
'tkinter',
|
||||
],
|
||||
includes=[
|
||||
"encodings",
|
||||
"json",
|
||||
"filecmp",
|
||||
"numpy.core._methods",
|
||||
"numpy.lib.format",
|
||||
"PyQt5.QtCore",
|
||||
"PyQt5.QtGui",
|
||||
"PyQt5.QtWidgets",
|
||||
"PyQt5.uic",
|
||||
"PIL.Image",
|
||||
"PIL.ImageQt",
|
||||
"PIL.ImageDraw",
|
||||
"PIL.ImageEnhance",
|
||||
],
|
||||
include_files=deps,
|
||||
)
|
||||
|
||||
base = 'Win32GUI' if sys.platform == 'win32' else None
|
||||
|
||||
executables = [
|
||||
Executable(
|
||||
'src/main.py',
|
||||
base=base,
|
||||
targetName='audio-visualizer-python'
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
setup(
|
||||
name='audio-visualizer-python',
|
||||
version=__version__,
|
||||
description='GUI tool to render visualization videos of audio files',
|
||||
options=dict(build_exe=buildOptions),
|
||||
executables=executables
|
||||
)
|
341
main.py
341
main.py
|
@ -1,341 +0,0 @@
|
|||
import sys, io, os
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from PyQt4.QtGui import QPainter, QColor, QFont
|
||||
from os.path import expanduser
|
||||
import subprocess as sp
|
||||
import numpy
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from PIL.ImageQt import ImageQt
|
||||
import atexit
|
||||
from queue import Queue
|
||||
from PyQt4.QtCore import QSettings
|
||||
import signal
|
||||
|
||||
import preview_thread, core, video_thread
|
||||
|
||||
class Command(QtCore.QObject):
|
||||
|
||||
videoTask = QtCore.pyqtSignal(str, str, QFont, int, int, int, int, tuple, tuple, str, str)
|
||||
|
||||
def __init__(self):
|
||||
QtCore.QObject.__init__(self)
|
||||
|
||||
import argparse
|
||||
self.parser = argparse.ArgumentParser(description='Create a visualization for an audio file')
|
||||
self.parser.add_argument('-i', '--input', dest='input', help='input audio file', required=True)
|
||||
self.parser.add_argument('-o', '--output', dest='output', help='output video file', required=True)
|
||||
self.parser.add_argument('-b', '--background', dest='bgimage', help='background image file', required=True)
|
||||
self.parser.add_argument('-t', '--text', dest='text', help='title text', required=True)
|
||||
self.parser.add_argument('-f', '--font', dest='font', help='title font', required=False)
|
||||
self.parser.add_argument('-s', '--fontsize', dest='fontsize', help='title font size', required=False)
|
||||
self.parser.add_argument('-c', '--textcolor', dest='textcolor', help='title text color in r,g,b format', required=False)
|
||||
self.parser.add_argument('-C', '--viscolor', dest='viscolor', help='visualization color in r,g,b format', required=False)
|
||||
self.parser.add_argument('-x', '--xposition', dest='xposition', help='x position', required=False)
|
||||
self.parser.add_argument('-y', '--yposition', dest='yposition', help='y position', required=False)
|
||||
self.parser.add_argument('-a', '--alignment', dest='alignment', help='title alignment', required=False, type=int, choices=[0, 1, 2])
|
||||
self.args = self.parser.parse_args()
|
||||
|
||||
self.settings = QSettings('settings.ini', QSettings.IniFormat)
|
||||
|
||||
# load colours as tuples from comma-separated strings
|
||||
self.textColor = core.Core.RGBFromString(self.settings.value("textColor", '255, 255, 255'))
|
||||
self.visColor = core.Core.RGBFromString(self.settings.value("visColor", '255, 255, 255'))
|
||||
if self.args.textcolor:
|
||||
self.textColor = core.Core.RGBFromString(self.args.textcolor)
|
||||
if self.args.viscolor:
|
||||
self.visColor = core.Core.RGBFromString(self.args.viscolor)
|
||||
|
||||
# font settings
|
||||
if self.args.font:
|
||||
self.font = QFont(self.args.font)
|
||||
else:
|
||||
self.font = QFont(self.settings.value("titleFont", QFont()))
|
||||
|
||||
if self.args.fontsize:
|
||||
self.fontsize = int(self.args.fontsize)
|
||||
else:
|
||||
self.fontsize = int(self.settings.value("fontSize", 35))
|
||||
if self.args.alignment:
|
||||
self.alignment = int(self.args.alignment)
|
||||
else:
|
||||
self.alignment = int(self.settings.value("alignment", 0))
|
||||
|
||||
if self.args.xposition:
|
||||
self.textX = int(self.args.xposition)
|
||||
else:
|
||||
self.textX = int(self.settings.value("xPosition", 70))
|
||||
|
||||
if self.args.yposition:
|
||||
self.textY = int(self.args.yposition)
|
||||
else:
|
||||
self.textY = int(self.settings.value("yPosition", 375))
|
||||
|
||||
ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~"))
|
||||
|
||||
self.videoThread = QtCore.QThread(self)
|
||||
self.videoWorker = video_thread.Worker(self)
|
||||
|
||||
self.videoWorker.moveToThread(self.videoThread)
|
||||
self.videoWorker.videoCreated.connect(self.videoCreated)
|
||||
|
||||
self.videoThread.start()
|
||||
self.videoTask.emit(self.args.bgimage,
|
||||
self.args.text,
|
||||
self.font,
|
||||
self.fontsize,
|
||||
self.alignment,
|
||||
self.textX,
|
||||
self.textY,
|
||||
self.textColor,
|
||||
self.visColor,
|
||||
self.args.input,
|
||||
self.args.output)
|
||||
|
||||
def videoCreated(self):
|
||||
self.videoThread.quit()
|
||||
self.videoThread.wait()
|
||||
self.cleanUp()
|
||||
|
||||
def cleanUp(self):
|
||||
self.settings.setValue("titleFont", self.font.toString())
|
||||
self.settings.setValue("alignment", str(self.alignment))
|
||||
self.settings.setValue("fontSize", str(self.fontsize))
|
||||
self.settings.setValue("xPosition", str(self.textX))
|
||||
self.settings.setValue("yPosition", str(self.textY))
|
||||
self.settings.setValue("visColor", '%s,%s,%s' % self.visColor)
|
||||
self.settings.setValue("textColor", '%s,%s,%s' % self.textColor)
|
||||
sys.exit(0)
|
||||
|
||||
class Main(QtCore.QObject):
|
||||
|
||||
newTask = QtCore.pyqtSignal(str, str, QFont, int, int, int, int, tuple, tuple)
|
||||
processTask = QtCore.pyqtSignal()
|
||||
videoTask = QtCore.pyqtSignal(str, str, QFont, int, int, int, int, tuple, tuple, str, str)
|
||||
|
||||
def __init__(self, window):
|
||||
QtCore.QObject.__init__(self)
|
||||
|
||||
# print('main thread id: {}'.format(QtCore.QThread.currentThreadId()))
|
||||
self.window = window
|
||||
self.core = core.Core()
|
||||
self.settings = QSettings('settings.ini', QSettings.IniFormat)
|
||||
|
||||
# load colors as tuples from a comma-separated string
|
||||
self.textColor = core.Core.RGBFromString(self.settings.value("textColor", '255, 255, 255'))
|
||||
self.visColor = core.Core.RGBFromString(self.settings.value("visColor", '255, 255, 255'))
|
||||
|
||||
self.previewQueue = Queue()
|
||||
|
||||
self.previewThread = QtCore.QThread(self)
|
||||
self.previewWorker = preview_thread.Worker(self, self.previewQueue)
|
||||
|
||||
self.previewWorker.moveToThread(self.previewThread)
|
||||
self.previewWorker.imageCreated.connect(self.showPreviewImage)
|
||||
|
||||
self.previewThread.start()
|
||||
|
||||
self.timer = QtCore.QTimer(self)
|
||||
self.timer.timeout.connect(self.processTask.emit)
|
||||
self.timer.start(500)
|
||||
|
||||
window.pushButton_selectInput.clicked.connect(self.openInputFileDialog)
|
||||
window.pushButton_selectOutput.clicked.connect(self.openOutputFileDialog)
|
||||
window.pushButton_createVideo.clicked.connect(self.createAudioVisualisation)
|
||||
window.pushButton_selectBackground.clicked.connect(self.openBackgroundFileDialog)
|
||||
|
||||
window.progressBar_create.setValue(0)
|
||||
window.setWindowTitle("Audio Visualizer")
|
||||
window.pushButton_selectInput.setText("Select Input Music File")
|
||||
window.pushButton_selectOutput.setText("Select Output Video File")
|
||||
window.pushButton_selectBackground.setText("Select Background Image")
|
||||
window.label_font.setText("Title Font")
|
||||
window.label_alignment.setText("Title Options")
|
||||
window.label_colorOptions.setText("Colors")
|
||||
window.label_fontsize.setText("Fontsize")
|
||||
window.label_title.setText("Title Text")
|
||||
window.label_textColor.setText("Text:")
|
||||
window.label_visColor.setText("Visualizer:")
|
||||
window.pushButton_createVideo.setText("Create Video")
|
||||
window.groupBox_create.setTitle("Create")
|
||||
window.groupBox_settings.setTitle("Settings")
|
||||
window.groupBox_preview.setTitle("Preview")
|
||||
|
||||
window.alignmentComboBox.addItem("Left")
|
||||
window.alignmentComboBox.addItem("Middle")
|
||||
window.alignmentComboBox.addItem("Right")
|
||||
window.fontsizeSpinBox.setValue(35)
|
||||
window.textXSpinBox.setValue(70)
|
||||
window.textYSpinBox.setValue(375)
|
||||
window.lineEdit_textColor.setText('%s,%s,%s' % self.textColor)
|
||||
window.lineEdit_visColor.setText('%s,%s,%s' % self.visColor)
|
||||
window.pushButton_textColor.clicked.connect(lambda: self.pickColor('text'))
|
||||
window.pushButton_visColor.clicked.connect(lambda: self.pickColor('vis'))
|
||||
btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*self.textColor).name()
|
||||
window.pushButton_textColor.setStyleSheet(btnStyle)
|
||||
btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*self.visColor).name()
|
||||
window.pushButton_visColor.setStyleSheet(btnStyle)
|
||||
|
||||
titleFont = self.settings.value("titleFont")
|
||||
if not titleFont == None:
|
||||
window.fontComboBox.setCurrentFont(QFont(titleFont))
|
||||
|
||||
alignment = self.settings.value("alignment")
|
||||
if not alignment == None:
|
||||
window.alignmentComboBox.setCurrentIndex(int(alignment))
|
||||
fontSize = self.settings.value("fontSize")
|
||||
if not fontSize == None:
|
||||
window.fontsizeSpinBox.setValue(int(fontSize))
|
||||
xPosition = self.settings.value("xPosition")
|
||||
if not xPosition == None:
|
||||
window.textXSpinBox.setValue(int(xPosition))
|
||||
yPosition = self.settings.value("yPosition")
|
||||
if not yPosition == None:
|
||||
window.textYSpinBox.setValue(int(yPosition))
|
||||
|
||||
window.fontComboBox.currentFontChanged.connect(self.drawPreview)
|
||||
window.lineEdit_title.textChanged.connect(self.drawPreview)
|
||||
window.alignmentComboBox.currentIndexChanged.connect(self.drawPreview)
|
||||
window.textXSpinBox.valueChanged.connect(self.drawPreview)
|
||||
window.textYSpinBox.valueChanged.connect(self.drawPreview)
|
||||
window.fontsizeSpinBox.valueChanged.connect(self.drawPreview)
|
||||
window.lineEdit_textColor.textChanged.connect(self.drawPreview)
|
||||
window.lineEdit_visColor.textChanged.connect(self.drawPreview)
|
||||
|
||||
self.drawPreview()
|
||||
|
||||
window.show()
|
||||
|
||||
def cleanUp(self):
|
||||
self.timer.stop()
|
||||
self.previewThread.quit()
|
||||
self.previewThread.wait()
|
||||
|
||||
self.settings.setValue("titleFont", self.window.fontComboBox.currentFont().toString())
|
||||
self.settings.setValue("alignment", str(self.window.alignmentComboBox.currentIndex()))
|
||||
self.settings.setValue("fontSize", str(self.window.fontsizeSpinBox.value()))
|
||||
self.settings.setValue("xPosition", str(self.window.textXSpinBox.value()))
|
||||
self.settings.setValue("yPosition", str(self.window.textYSpinBox.value()))
|
||||
self.settings.setValue("visColor", self.window.lineEdit_visColor.text())
|
||||
self.settings.setValue("textColor", self.window.lineEdit_textColor.text())
|
||||
|
||||
def openInputFileDialog(self):
|
||||
inputDir = self.settings.value("inputDir", expanduser("~"))
|
||||
|
||||
fileName = QtGui.QFileDialog.getOpenFileName(self.window,
|
||||
"Open Music File", inputDir, "Music Files (*.mp3 *.wav *.ogg *.flac)");
|
||||
|
||||
if not fileName == "":
|
||||
self.settings.setValue("inputDir", os.path.dirname(fileName))
|
||||
self.window.label_input.setText(fileName)
|
||||
|
||||
def openOutputFileDialog(self):
|
||||
outputDir = self.settings.value("outputDir", expanduser("~"))
|
||||
|
||||
fileName = QtGui.QFileDialog.getSaveFileName(self.window,
|
||||
"Set Output Video File", outputDir, "Video Files (*.mkv)");
|
||||
|
||||
if not fileName == "":
|
||||
self.settings.setValue("outputDir", os.path.dirname(fileName))
|
||||
self.window.label_output.setText(fileName)
|
||||
|
||||
def openBackgroundFileDialog(self):
|
||||
backgroundDir = self.settings.value("backgroundDir", expanduser("~"))
|
||||
|
||||
fileName = QtGui.QFileDialog.getOpenFileName(self.window,
|
||||
"Open Background Image", backgroundDir, "Image Files (*.jpg *.png);; Video Files (*.mp4)");
|
||||
|
||||
if not fileName == "":
|
||||
self.settings.setValue("backgroundDir", os.path.dirname(fileName))
|
||||
self.window.label_background.setText(fileName)
|
||||
self.drawPreview()
|
||||
|
||||
def createAudioVisualisation(self):
|
||||
ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~"))
|
||||
|
||||
self.videoThread = QtCore.QThread(self)
|
||||
self.videoWorker = video_thread.Worker(self)
|
||||
|
||||
self.videoWorker.moveToThread(self.videoThread)
|
||||
self.videoWorker.videoCreated.connect(self.videoCreated)
|
||||
self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated)
|
||||
self.videoWorker.progressBarSetText.connect(self.progressBarSetText)
|
||||
|
||||
self.videoThread.start()
|
||||
self.videoTask.emit(self.window.label_background.text(),
|
||||
self.window.lineEdit_title.text(),
|
||||
self.window.fontComboBox.currentFont(),
|
||||
self.window.fontsizeSpinBox.value(),
|
||||
self.window.alignmentComboBox.currentIndex(),
|
||||
self.window.textXSpinBox.value(),
|
||||
self.window.textYSpinBox.value(),
|
||||
core.Core.RGBFromString(self.window.lineEdit_textColor.text()),
|
||||
core.Core.RGBFromString(self.window.lineEdit_visColor.text()),
|
||||
self.window.label_input.text(),
|
||||
self.window.label_output.text())
|
||||
|
||||
|
||||
def progressBarUpdated(self, value):
|
||||
self.window.progressBar_create.setValue(value)
|
||||
|
||||
def progressBarSetText(self, value):
|
||||
self.window.progressBar_create.setFormat(value)
|
||||
|
||||
def videoCreated(self):
|
||||
self.videoThread.quit()
|
||||
self.videoThread.wait()
|
||||
|
||||
def drawPreview(self):
|
||||
self.newTask.emit(self.window.label_background.text(),
|
||||
self.window.lineEdit_title.text(),
|
||||
self.window.fontComboBox.currentFont(),
|
||||
self.window.fontsizeSpinBox.value(),
|
||||
self.window.alignmentComboBox.currentIndex(),
|
||||
self.window.textXSpinBox.value(),
|
||||
self.window.textYSpinBox.value(),
|
||||
core.Core.RGBFromString(self.window.lineEdit_textColor.text()),
|
||||
core.Core.RGBFromString(self.window.lineEdit_visColor.text()))
|
||||
# self.processTask.emit()
|
||||
|
||||
def showPreviewImage(self, image):
|
||||
self._scaledPreviewImage = image
|
||||
self._previewPixmap = QtGui.QPixmap.fromImage(self._scaledPreviewImage)
|
||||
|
||||
self.window.label_preview.setPixmap(self._previewPixmap)
|
||||
|
||||
def pickColor(self, colorTarget):
|
||||
color = QtGui.QColorDialog.getColor()
|
||||
if color.isValid():
|
||||
RGBstring = '%s,%s,%s' % (str(color.red()), str(color.green()), str(color.blue()))
|
||||
btnStyle = "QPushButton { background-color : %s; outline: none; }" % color.name()
|
||||
if colorTarget == 'text':
|
||||
self.window.lineEdit_textColor.setText(RGBstring)
|
||||
window.pushButton_textColor.setStyleSheet(btnStyle)
|
||||
elif colorTarget == 'vis':
|
||||
self.window.lineEdit_visColor.setText(RGBstring)
|
||||
window.pushButton_visColor.setStyleSheet(btnStyle)
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
# command line mode
|
||||
app = QtGui.QApplication(sys.argv, False)
|
||||
command = Command()
|
||||
signal.signal(signal.SIGINT, command.cleanUp)
|
||||
sys.exit(app.exec_())
|
||||
else:
|
||||
# gui mode
|
||||
if __name__ == "__main__":
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
window = uic.loadUi("main.ui")
|
||||
# window.adjustSize()
|
||||
desc = QtGui.QDesktopWidget()
|
||||
dpi = desc.physicalDpiX()
|
||||
topMargin = 0 if (dpi == 96) else int(10 * (dpi / 96))
|
||||
|
||||
window.resize(window.width() * (dpi / 96), window.height() * (dpi / 96))
|
||||
window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0)
|
||||
|
||||
main = Main(window)
|
||||
|
||||
signal.signal(signal.SIGINT, main.cleanUp)
|
||||
atexit.register(main.cleanUp)
|
||||
|
||||
sys.exit(app.exec_())
|
602
main.ui
602
main.ui
|
@ -1,602 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>635</width>
|
||||
<height>600</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>635</width>
|
||||
<height>600</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_settings">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>200</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>GroupBox</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_selectInput">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>PushButton</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_input">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>2</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Box</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_selectOutput">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>PushButton</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_output">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>2</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Box</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_selectBackground">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>PushButton</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_background">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>2</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Box</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_font">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFontComboBox" name="fontComboBox"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_alignment">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QComboBox" name="alignmentComboBox"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_fontsize">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="fontsizeSpinBox">
|
||||
<property name="maximum">
|
||||
<number>999</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_textX">
|
||||
<property name="layoutDirection">
|
||||
<enum>Qt::LeftToRight</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>X</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="textXSpinBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-99999</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>99999</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_textY">
|
||||
<property name="text">
|
||||
<string>Y</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="textYSpinBox">
|
||||
<property name="minimum">
|
||||
<number>-99999</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>99999</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_colorOptions">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_textColor">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_textColor">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="MaximumSize" stdset="0">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_textColor"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_visColor">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_visColor">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="MaximumSize" stdset="0">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_visColor"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_title">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_title"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_preview">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>220</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>220</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>GroupBox</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_preview">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>320</width>
|
||||
<height>180</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>320</width>
|
||||
<height>180</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Box</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_create">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>GroupBox</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar_create">
|
||||
<property name="value">
|
||||
<number>24</number>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="textVisible">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_createVideo">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>PushButton</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -1,79 +0,0 @@
|
|||
from PyQt4 import QtCore, QtGui, uic
|
||||
from PyQt4.QtCore import pyqtSignal, pyqtSlot
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from PIL.ImageQt import ImageQt
|
||||
import core
|
||||
import time
|
||||
from queue import Queue, Empty
|
||||
import numpy
|
||||
|
||||
class Worker(QtCore.QObject):
|
||||
|
||||
imageCreated = pyqtSignal(['QImage'])
|
||||
|
||||
def __init__(self, parent=None, queue=None):
|
||||
QtCore.QObject.__init__(self)
|
||||
parent.newTask.connect(self.createPreviewImage)
|
||||
parent.processTask.connect(self.process)
|
||||
self.core = core.Core()
|
||||
self.queue = queue
|
||||
|
||||
|
||||
@pyqtSlot(str, str, QtGui.QFont, int, int, int, int, tuple, tuple)
|
||||
def createPreviewImage(self, backgroundImage, titleText, titleFont, fontSize,\
|
||||
alignment, xOffset, yOffset, textColor, visColor):
|
||||
# print('worker thread id: {}'.format(QtCore.QThread.currentThreadId()))
|
||||
dic = {
|
||||
"backgroundImage": backgroundImage,
|
||||
"titleText": titleText,
|
||||
"titleFont": titleFont,
|
||||
"fontSize": fontSize,
|
||||
"alignment": alignment,
|
||||
"xoffset": xOffset,
|
||||
"yoffset": yOffset,
|
||||
"textColor" : textColor,
|
||||
"visColor" : visColor
|
||||
}
|
||||
self.queue.put(dic)
|
||||
|
||||
@pyqtSlot()
|
||||
def process(self):
|
||||
try:
|
||||
nextPreviewInformation = self.queue.get(block=False)
|
||||
while self.queue.qsize() >= 2:
|
||||
try:
|
||||
self.queue.get(block=False)
|
||||
except Empty:
|
||||
continue
|
||||
|
||||
bgImage = self.core.parseBaseImage(\
|
||||
nextPreviewInformation["backgroundImage"],
|
||||
preview=True
|
||||
)
|
||||
if bgImage == []:
|
||||
bgImage = ''
|
||||
else:
|
||||
bgImage = bgImage[0]
|
||||
|
||||
im = self.core.drawBaseImage(
|
||||
bgImage,
|
||||
nextPreviewInformation["titleText"],
|
||||
nextPreviewInformation["titleFont"],
|
||||
nextPreviewInformation["fontSize"],
|
||||
nextPreviewInformation["alignment"],
|
||||
nextPreviewInformation["xoffset"],
|
||||
nextPreviewInformation["yoffset"],
|
||||
nextPreviewInformation["textColor"],
|
||||
nextPreviewInformation["visColor"])
|
||||
spectrum = numpy.fromfunction(lambda x: 0.008*(x-128)**2, (255,), dtype="int16")
|
||||
|
||||
im = self.core.drawBars(spectrum, im, nextPreviewInformation["visColor"])
|
||||
|
||||
self._image = ImageQt(im)
|
||||
self._previewImage = QtGui.QImage(self._image)
|
||||
|
||||
self._scaledPreviewImage = self._previewImage.scaled(320, 180, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation)
|
||||
|
||||
self.imageCreated.emit(self._scaledPreviewImage)
|
||||
except Empty:
|
||||
True
|
75
setup.py
75
setup.py
|
@ -1,30 +1,53 @@
|
|||
from cx_Freeze import setup, Executable
|
||||
from setuptools import setup
|
||||
import os
|
||||
|
||||
# Dependencies are automatically detected, but it might need
|
||||
# fine tuning.
|
||||
buildOptions = dict(packages = [], excludes = [
|
||||
"apport",
|
||||
"apt",
|
||||
"ctypes",
|
||||
"curses",
|
||||
"distutils",
|
||||
"email",
|
||||
"html",
|
||||
"http",
|
||||
"json",
|
||||
"xmlrpc",
|
||||
"nose"
|
||||
], include_files = ["main.ui"])
|
||||
|
||||
import sys
|
||||
base = 'Win32GUI' if sys.platform=='win32' else None
|
||||
__version__ = '2.0.0.rc3'
|
||||
|
||||
executables = [
|
||||
Executable('main.py', base=base, targetName = 'audio-visualizer-python')
|
||||
]
|
||||
|
||||
setup(name='audio-visualizer-python',
|
||||
version = '1.0',
|
||||
description = 'a little GUI tool to render visualization videos of audio files',
|
||||
options = dict(build_exe = buildOptions),
|
||||
executables = executables)
|
||||
def package_files(directory):
|
||||
paths = []
|
||||
for (path, directories, filenames) in os.walk(directory):
|
||||
for filename in filenames:
|
||||
paths.append(os.path.join('..', path, filename))
|
||||
return paths
|
||||
|
||||
|
||||
setup(
|
||||
name='audio_visualizer_python',
|
||||
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',
|
||||
long_description="Create customized audio visualization videos and save "
|
||||
"them as Projects to continue editing later. Different components can "
|
||||
"be added and layered to add visualizers, images, videos, gradients, "
|
||||
"text, etc. Use Projects created in the GUI with commandline mode to "
|
||||
"automate your video production workflow without any complex syntax.",
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'Topic :: Multimedia :: Video :: Non-Linear Editor',
|
||||
],
|
||||
keywords=[
|
||||
'visualizer', 'visualization', 'commandline video',
|
||||
'video editor', 'ffmpeg', 'podcast'
|
||||
],
|
||||
packages=[
|
||||
'avpython',
|
||||
'avpython.toolkit',
|
||||
'avpython.components'
|
||||
],
|
||||
package_dir={'avpython': 'src'},
|
||||
package_data={
|
||||
'avpython': package_files('src'),
|
||||
},
|
||||
install_requires=['Pillow-SIMD', 'PyQt5', 'numpy'],
|
||||
entry_points={
|
||||
'gui_scripts': [
|
||||
'avp = avpython.main:main'
|
||||
],
|
||||
}
|
||||
)
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import sys
|
||||
import os
|
||||
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
# frozen
|
||||
wd = os.path.dirname(sys.executable)
|
||||
else:
|
||||
# unfrozen
|
||||
wd = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
# make relative imports work when using /src as a package
|
||||
sys.path.insert(0, wd)
|
|
@ -0,0 +1,5 @@
|
|||
# Allows for launching with python3 -m avpython
|
||||
|
||||
from avpython.main import main
|
||||
|
||||
main()
|
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
|
@ -0,0 +1,180 @@
|
|||
'''
|
||||
When using commandline mode, this module's object handles interpreting
|
||||
the arguments and giving them to Core, which tracks the main program state.
|
||||
Then it immediately exports a video.
|
||||
'''
|
||||
from PyQt5 import QtCore
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from core import Core
|
||||
|
||||
|
||||
class Command(QtCore.QObject):
|
||||
|
||||
createVideo = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
QtCore.QObject.__init__(self)
|
||||
self.core = Core()
|
||||
self.dataDir = self.core.dataDir
|
||||
self.canceled = False
|
||||
|
||||
self.parser = argparse.ArgumentParser(
|
||||
description='Create a visualization for an audio file',
|
||||
epilog='EXAMPLE COMMAND: main.py myvideotemplate.avp '
|
||||
'-i ~/Music/song.mp3 -o ~/video.mp4 '
|
||||
'-c 0 image path=~/Pictures/thisWeeksPicture.jpg '
|
||||
'-c 1 video "preset=My Logo" -c 2 vis layout=classic'
|
||||
)
|
||||
self.parser.add_argument(
|
||||
'-i', '--input', metavar='SOUND',
|
||||
help='input audio file'
|
||||
)
|
||||
self.parser.add_argument(
|
||||
'-o', '--output', metavar='OUTPUT',
|
||||
help='output video file'
|
||||
)
|
||||
self.parser.add_argument(
|
||||
'-e', '--export', action='store_true',
|
||||
help='use input and output files from project file'
|
||||
)
|
||||
|
||||
# optional arguments
|
||||
self.parser.add_argument(
|
||||
'projpath', metavar='path-to-project',
|
||||
help='open a project file (.avp)', nargs='?')
|
||||
self.parser.add_argument(
|
||||
'-c', '--comp', metavar=('LAYER', 'ARG'),
|
||||
help='first arg must be component NAME to insert at LAYER.'
|
||||
'"help" for information about possible args for a component.',
|
||||
nargs='*', action='append')
|
||||
|
||||
self.args = self.parser.parse_args()
|
||||
self.settings = Core.settings
|
||||
|
||||
if self.args.projpath:
|
||||
projPath = self.args.projpath
|
||||
if not os.path.dirname(projPath):
|
||||
projPath = os.path.join(
|
||||
self.settings.value("projectDir"),
|
||||
projPath
|
||||
)
|
||||
if not projPath.endswith('.avp'):
|
||||
projPath += '.avp'
|
||||
success = self.core.openProject(self, projPath)
|
||||
if not success:
|
||||
quit(1)
|
||||
self.core.selectedComponents = list(
|
||||
reversed(self.core.selectedComponents))
|
||||
self.core.componentListChanged()
|
||||
|
||||
if self.args.comp:
|
||||
for comp in self.args.comp:
|
||||
pos = comp[0]
|
||||
name = comp[1]
|
||||
args = comp[2:]
|
||||
try:
|
||||
pos = int(pos)
|
||||
except ValueError:
|
||||
print(pos, 'is not a layer number.')
|
||||
quit(1)
|
||||
realName = self.parseCompName(name)
|
||||
if not realName:
|
||||
print(name, 'is not a valid component name.')
|
||||
quit(1)
|
||||
modI = self.core.moduleIndexFor(realName)
|
||||
i = self.core.insertComponent(pos, modI, self)
|
||||
for arg in args:
|
||||
self.core.selectedComponents[i].command(arg)
|
||||
|
||||
if self.args.export and self.args.projpath:
|
||||
errcode, data = self.core.parseAvFile(projPath)
|
||||
for key, value in data['WindowFields']:
|
||||
if 'outputFile' in key:
|
||||
output = value
|
||||
if not os.path.dirname(value):
|
||||
output = os.path.join(
|
||||
os.path.expanduser('~'),
|
||||
output
|
||||
)
|
||||
if 'audioFile' in key:
|
||||
input = value
|
||||
self.createAudioVisualisation(input, output)
|
||||
|
||||
elif self.args.input and self.args.output:
|
||||
self.createAudioVisualisation(self.args.input, self.args.output)
|
||||
|
||||
elif 'help' not in sys.argv:
|
||||
self.parser.print_help()
|
||||
quit(1)
|
||||
|
||||
def createAudioVisualisation(self, input, output):
|
||||
self.core.selectedComponents = list(
|
||||
reversed(self.core.selectedComponents))
|
||||
self.core.componentListChanged()
|
||||
self.worker = self.core.newVideoWorker(
|
||||
self, input, output
|
||||
)
|
||||
self.worker.videoCreated.connect(self.videoCreated)
|
||||
self.lastProgressUpdate = time.time()
|
||||
self.worker.progressBarSetText.connect(self.progressBarSetText)
|
||||
self.createVideo.emit()
|
||||
|
||||
@QtCore.pyqtSlot(str)
|
||||
def progressBarSetText(self, value):
|
||||
if 'Export ' in value:
|
||||
# Don't duplicate completion/failure messages
|
||||
return
|
||||
if not value.startswith('Exporting') \
|
||||
and time.time() - self.lastProgressUpdate >= 0.05:
|
||||
# Show most messages very often
|
||||
print(value)
|
||||
elif time.time() - self.lastProgressUpdate >= 2.0:
|
||||
# Give user time to read ffmpeg's output during the export
|
||||
print('##### %s' % value)
|
||||
else:
|
||||
return
|
||||
self.lastProgressUpdate = time.time()
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def videoCreated(self):
|
||||
quit(0)
|
||||
|
||||
def showMessage(self, **kwargs):
|
||||
print(kwargs['msg'])
|
||||
if 'detail' in kwargs:
|
||||
print(kwargs['detail'])
|
||||
|
||||
@QtCore.pyqtSlot(str, str)
|
||||
def videoThreadError(self, msg, detail):
|
||||
print(msg)
|
||||
print(detail)
|
||||
quit(1)
|
||||
|
||||
def drawPreview(self, *args):
|
||||
pass
|
||||
|
||||
def parseCompName(self, name):
|
||||
'''Deduces a proper component name out of a commandline arg'''
|
||||
|
||||
if name.title() in self.core.compNames:
|
||||
return name.title()
|
||||
for compName in self.core.compNames:
|
||||
if name.capitalize() in compName:
|
||||
return compName
|
||||
|
||||
compFileNames = [
|
||||
os.path.splitext(
|
||||
os.path.basename(mod.__file__)
|
||||
)[0]
|
||||
for mod in self.core.modules
|
||||
]
|
||||
for i, compFileName in enumerate(compFileNames):
|
||||
if name.lower() in compFileName:
|
||||
return self.core.compNames[i]
|
||||
return
|
||||
|
||||
return None
|
|
@ -0,0 +1,618 @@
|
|||
'''
|
||||
Base classes for components to import. Read comments for some documentation
|
||||
on making a valid component.
|
||||
'''
|
||||
from PyQt5 import uic, QtCore, QtWidgets
|
||||
from PyQt5.QtGui import QColor
|
||||
import os
|
||||
import sys
|
||||
import math
|
||||
import time
|
||||
|
||||
from toolkit.frame import BlankFrame
|
||||
from toolkit import (
|
||||
getWidgetValue, setWidgetValue, connectWidget, rgbFromString
|
||||
)
|
||||
|
||||
|
||||
class ComponentMetaclass(type(QtCore.QObject)):
|
||||
'''
|
||||
Checks the validity of each Component class and mutates some attrs.
|
||||
E.g., takes only major version from version string & decorates methods
|
||||
'''
|
||||
|
||||
def initializationWrapper(func):
|
||||
def initializationWrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
except Exception:
|
||||
try:
|
||||
raise ComponentError(self, 'initialization process')
|
||||
except ComponentError:
|
||||
return
|
||||
return initializationWrapper
|
||||
|
||||
def renderWrapper(func):
|
||||
def renderWrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
except Exception as e:
|
||||
try:
|
||||
if e.__class__.__name__.startswith('Component'):
|
||||
raise
|
||||
else:
|
||||
raise ComponentError(self, 'renderer')
|
||||
except ComponentError:
|
||||
return BlankFrame()
|
||||
return renderWrapper
|
||||
|
||||
def commandWrapper(func):
|
||||
'''Intercepts the command() method to check for global args'''
|
||||
def commandWrapper(self, arg):
|
||||
if arg.startswith('preset='):
|
||||
from presetmanager import getPresetDir
|
||||
_, preset = arg.split('=', 1)
|
||||
path = os.path.join(getPresetDir(self), preset)
|
||||
if not os.path.exists(path):
|
||||
print('Couldn\'t locate preset "%s"' % preset)
|
||||
quit(1)
|
||||
else:
|
||||
print('Opening "%s" preset on layer %s' % (
|
||||
preset, self.compPos)
|
||||
)
|
||||
self.core.openPreset(path, self.compPos, preset)
|
||||
# Don't call the component's command() method
|
||||
return
|
||||
else:
|
||||
return func(self, arg)
|
||||
return commandWrapper
|
||||
|
||||
def propertiesWrapper(func):
|
||||
'''Intercepts the usual properties if the properties are locked.'''
|
||||
def propertiesWrapper(self):
|
||||
if self._lockedProperties is not None:
|
||||
return self._lockedProperties
|
||||
else:
|
||||
try:
|
||||
return func(self)
|
||||
except Exception:
|
||||
try:
|
||||
raise ComponentError(self, 'properties')
|
||||
except ComponentError:
|
||||
return []
|
||||
return propertiesWrapper
|
||||
|
||||
def errorWrapper(func):
|
||||
'''Intercepts the usual error message if it is locked.'''
|
||||
def errorWrapper(self):
|
||||
if self._lockedError is not None:
|
||||
return self._lockedError
|
||||
else:
|
||||
return func(self)
|
||||
return errorWrapper
|
||||
|
||||
def __new__(cls, name, parents, attrs):
|
||||
if 'ui' not in attrs:
|
||||
# Use module name as ui filename by default
|
||||
attrs['ui'] = '%s.ui' % os.path.splitext(
|
||||
attrs['__module__'].split('.')[-1]
|
||||
)[0]
|
||||
|
||||
# if parents[0] == QtCore.QObject: else:
|
||||
decorate = (
|
||||
'names', # Class methods
|
||||
'error', 'audio', 'properties', # Properties
|
||||
'preFrameRender', 'previewRender',
|
||||
'frameRender', 'command',
|
||||
)
|
||||
|
||||
# Auto-decorate methods
|
||||
for key in decorate:
|
||||
if key not in attrs:
|
||||
continue
|
||||
|
||||
if key in ('names'):
|
||||
attrs[key] = classmethod(attrs[key])
|
||||
|
||||
if key in ('audio'):
|
||||
attrs[key] = property(attrs[key])
|
||||
|
||||
if key == 'command':
|
||||
attrs[key] = cls.commandWrapper(attrs[key])
|
||||
|
||||
if key in ('previewRender', 'frameRender'):
|
||||
attrs[key] = cls.renderWrapper(attrs[key])
|
||||
|
||||
if key == 'preFrameRender':
|
||||
attrs[key] = cls.initializationWrapper(attrs[key])
|
||||
|
||||
if key == 'properties':
|
||||
attrs[key] = cls.propertiesWrapper(attrs[key])
|
||||
|
||||
if key == 'error':
|
||||
attrs[key] = cls.errorWrapper(attrs[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.
|
||||
'''
|
||||
|
||||
name = 'Component'
|
||||
# ui = 'name_Of_Non_Default_Ui_File'
|
||||
|
||||
version = '1.0.0'
|
||||
# The major version (before the first dot) is used to determine
|
||||
# preset compatibility; the rest is ignored so it can be non-numeric.
|
||||
|
||||
modified = QtCore.pyqtSignal(int, dict)
|
||||
_error = QtCore.pyqtSignal(str, str)
|
||||
|
||||
def __init__(self, moduleIndex, compPos, core):
|
||||
super().__init__()
|
||||
self.moduleIndex = moduleIndex
|
||||
self.compPos = compPos
|
||||
self.core = core
|
||||
self.currentPreset = None
|
||||
|
||||
self._trackedWidgets = {}
|
||||
self._presetNames = {}
|
||||
self._commandArgs = {}
|
||||
self._colorWidgets = {}
|
||||
self._colorFuncs = {}
|
||||
self._relativeWidgets = {}
|
||||
# pixel values stored as floats
|
||||
self._relativeValues = {}
|
||||
# maximum values of spinBoxes at 1080p (Core.resolutions[0])
|
||||
self._relativeMaximums = {}
|
||||
|
||||
self._lockedProperties = None
|
||||
self._lockedError = None
|
||||
self._lockedSize = None
|
||||
|
||||
# Stop lengthy processes in response to this variable
|
||||
self.canceled = False
|
||||
|
||||
def __str__(self):
|
||||
return self.__class__.name
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
preset = self.savePreset()
|
||||
except Exception as e:
|
||||
preset = '%s occured while saving preset' % str(e)
|
||||
return '%s\n%s\n%s' % (
|
||||
self.__class__.name, str(self.__class__.version), preset
|
||||
)
|
||||
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
# Render Methods
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
|
||||
def previewRender(self):
|
||||
image = BlankFrame(self.width, self.height)
|
||||
return image
|
||||
|
||||
def preFrameRender(self, **kwargs):
|
||||
'''
|
||||
Must call super() when subclassing
|
||||
Triggered only before a video is exported (video_thread.py)
|
||||
self.audioFile = filepath to the main input audio file
|
||||
self.completeAudioArray = a list of audio samples
|
||||
self.sampleSize = number of audio samples per video frame
|
||||
self.progressBarUpdate = signal to set progress bar number
|
||||
self.progressBarSetText = signal to set progress bar text
|
||||
Use the latter two signals to update the MainWindow if needed
|
||||
for a long initialization procedure (i.e., for a visualizer)
|
||||
'''
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def frameRender(self, frameNo):
|
||||
audioArrayIndex = frameNo * self.sampleSize
|
||||
image = BlankFrame(self.width, self.height)
|
||||
return image
|
||||
|
||||
def postFrameRender(self):
|
||||
pass
|
||||
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
# Properties
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
|
||||
def properties(self):
|
||||
'''
|
||||
Return a list of properties to signify if your component is
|
||||
non-animated ('static'), returns sound ('audio'), or has
|
||||
encountered an error in configuration ('error').
|
||||
'''
|
||||
return []
|
||||
|
||||
def error(self):
|
||||
'''
|
||||
Return a string containing an error message, or None for a default.
|
||||
Or tuple of two strings for a message with details.
|
||||
Alternatively use lockError(msgString) within properties()
|
||||
to skip this method entirely.
|
||||
'''
|
||||
return
|
||||
|
||||
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
|
||||
'''
|
||||
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
# Idle Methods
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
|
||||
def widget(self, parent):
|
||||
'''
|
||||
Call super().widget(*args) to create the component widget
|
||||
which also auto-connects any common widgets (e.g., checkBoxes)
|
||||
to self.update(). Then in a subclass connect special actions
|
||||
(e.g., pushButtons to select a file/colour) and initialize
|
||||
'''
|
||||
self.parent = parent
|
||||
self.settings = parent.settings
|
||||
self.page = self.loadUi(self.__class__.ui)
|
||||
|
||||
# Connect widget signals
|
||||
widgets = {
|
||||
'lineEdit': self.page.findChildren(QtWidgets.QLineEdit),
|
||||
'checkBox': self.page.findChildren(QtWidgets.QCheckBox),
|
||||
'spinBox': self.page.findChildren(QtWidgets.QSpinBox),
|
||||
'comboBox': self.page.findChildren(QtWidgets.QComboBox),
|
||||
}
|
||||
widgets['spinBox'].extend(
|
||||
self.page.findChildren(QtWidgets.QDoubleSpinBox)
|
||||
)
|
||||
for widgetList in widgets.values():
|
||||
for widget in widgetList:
|
||||
connectWidget(widget, self.update)
|
||||
|
||||
def update(self):
|
||||
'''
|
||||
Reads all tracked widget values into instance attributes
|
||||
and tells the MainWindow that the component was modified.
|
||||
Call super() at the END if you need to subclass this.
|
||||
'''
|
||||
for attr, widget in self._trackedWidgets.items():
|
||||
if attr in self._colorWidgets:
|
||||
# Color Widgets: text stored as tuple & update the button color
|
||||
rgbTuple = rgbFromString(widget.text())
|
||||
btnStyle = (
|
||||
"QPushButton { background-color : %s; outline: none; }"
|
||||
% QColor(*rgbTuple).name())
|
||||
self._colorWidgets[attr].setStyleSheet(btnStyle)
|
||||
setattr(self, attr, rgbTuple)
|
||||
|
||||
elif attr in self._relativeWidgets:
|
||||
# Relative widgets: number scales to fit export resolution
|
||||
self.updateRelativeWidget(attr)
|
||||
setattr(self, attr, self._trackedWidgets[attr].value())
|
||||
|
||||
else:
|
||||
# Normal tracked widget
|
||||
setattr(self, attr, getWidgetValue(widget))
|
||||
|
||||
if not self.core.openingProject:
|
||||
self.parent.drawPreview()
|
||||
saveValueStore = self.savePreset()
|
||||
saveValueStore['preset'] = self.currentPreset
|
||||
self.modified.emit(self.compPos, saveValueStore)
|
||||
|
||||
def loadPreset(self, presetDict, presetName=None):
|
||||
'''
|
||||
Subclasses should take (presetDict, *args) as args.
|
||||
Must use super().loadPreset(presetDict, *args) first,
|
||||
then update self.page widgets using the preset dict.
|
||||
'''
|
||||
self.currentPreset = presetName \
|
||||
if presetName is not None else presetDict['preset']
|
||||
for attr, widget in self._trackedWidgets.items():
|
||||
key = attr if attr not in self._presetNames \
|
||||
else self._presetNames[attr]
|
||||
val = presetDict[key]
|
||||
|
||||
if attr in self._colorWidgets:
|
||||
widget.setText('%s,%s,%s' % val)
|
||||
btnStyle = (
|
||||
"QPushButton { background-color : %s; outline: none; }"
|
||||
% QColor(*val).name()
|
||||
)
|
||||
self._colorWidgets[attr].setStyleSheet(btnStyle)
|
||||
elif attr in self._relativeWidgets:
|
||||
self._relativeValues[attr] = val
|
||||
pixelVal = self.pixelValForAttr(attr, val)
|
||||
setWidgetValue(widget, pixelVal)
|
||||
else:
|
||||
setWidgetValue(widget, val)
|
||||
|
||||
def savePreset(self):
|
||||
saveValueStore = {}
|
||||
for attr, widget in self._trackedWidgets.items():
|
||||
presetAttrName = (
|
||||
attr if attr not in self._presetNames
|
||||
else self._presetNames[attr]
|
||||
)
|
||||
if attr in self._relativeWidgets:
|
||||
try:
|
||||
val = self._relativeValues[attr]
|
||||
except AttributeError:
|
||||
val = self.floatValForAttr(attr)
|
||||
else:
|
||||
val = getattr(self, attr)
|
||||
|
||||
saveValueStore[presetAttrName] = val
|
||||
return saveValueStore
|
||||
|
||||
def commandHelp(self):
|
||||
'''Help text as string for this component's commandline arguments'''
|
||||
|
||||
def command(self, arg=''):
|
||||
'''
|
||||
Configure a component using an arg from the commandline. This is
|
||||
never called if global args like 'preset=' are found in the arg.
|
||||
So simply check for any non-global args in your component and
|
||||
call super().command() at the end to get a Help message.
|
||||
'''
|
||||
print(
|
||||
self.__class__.name, 'Usage:\n'
|
||||
'Open a preset for this component:\n'
|
||||
' "preset=Preset Name"'
|
||||
)
|
||||
self.commandHelp()
|
||||
quit(0)
|
||||
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
# "Private" Methods
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
|
||||
def trackWidgets(self, trackDict, **kwargs):
|
||||
'''
|
||||
Name widgets to track in update(), savePreset(), loadPreset(), and
|
||||
command(). Requires a dict of attr names as keys, widgets as values
|
||||
|
||||
Optional args:
|
||||
'presetNames': preset variable names to replace attr names
|
||||
'commandArgs': arg keywords that differ from attr names
|
||||
|
||||
NOTE: Any kwarg key set to None will selectively disable tracking.
|
||||
'''
|
||||
self._trackedWidgets = trackDict
|
||||
for kwarg in kwargs:
|
||||
try:
|
||||
if kwarg in (
|
||||
'presetNames',
|
||||
'commandArgs',
|
||||
'colorWidgets',
|
||||
'relativeWidgets',
|
||||
):
|
||||
setattr(self, '_%s' % kwarg, kwargs[kwarg])
|
||||
else:
|
||||
raise ComponentError(
|
||||
self, 'Nonsensical keywords to trackWidgets.')
|
||||
except ComponentError:
|
||||
continue
|
||||
|
||||
if kwarg == 'colorWidgets':
|
||||
def makeColorFunc(attr):
|
||||
def pickColor_():
|
||||
self.pickColor(
|
||||
self._trackedWidgets[attr],
|
||||
self._colorWidgets[attr]
|
||||
)
|
||||
return pickColor_
|
||||
self._colorFuncs = {
|
||||
attr: makeColorFunc(attr) for attr in kwargs[kwarg]
|
||||
}
|
||||
for attr, func in self._colorFuncs.items():
|
||||
self._colorWidgets[attr].clicked.connect(func)
|
||||
self._colorWidgets[attr].setStyleSheet(
|
||||
"QPushButton {"
|
||||
"background-color : #FFFFFF; outline: none; }"
|
||||
)
|
||||
|
||||
if kwarg == 'relativeWidgets':
|
||||
# store maximum values of spinBoxes to be scaled appropriately
|
||||
for attr in kwargs[kwarg]:
|
||||
self._relativeMaximums[attr] = \
|
||||
self._trackedWidgets[attr].maximum()
|
||||
self.updateRelativeWidgetMaximum(attr)
|
||||
|
||||
def pickColor(self, textWidget, button):
|
||||
'''Use color picker to get color input from the user.'''
|
||||
dialog = QtWidgets.QColorDialog()
|
||||
dialog.setOption(QtWidgets.QColorDialog.ShowAlphaChannel, True)
|
||||
color = dialog.getColor()
|
||||
if color.isValid():
|
||||
RGBstring = '%s,%s,%s' % (
|
||||
str(color.red()), str(color.green()), str(color.blue()))
|
||||
btnStyle = "QPushButton{background-color: %s; outline: none;}" \
|
||||
% color.name()
|
||||
textWidget.setText(RGBstring)
|
||||
button.setStyleSheet(btnStyle)
|
||||
|
||||
def lockProperties(self, propList):
|
||||
self._lockedProperties = propList
|
||||
|
||||
def lockError(self, msg):
|
||||
self._lockedError = msg
|
||||
|
||||
def lockSize(self, w, h):
|
||||
self._lockedSize = (w, h)
|
||||
|
||||
def unlockProperties(self):
|
||||
self._lockedProperties = None
|
||||
|
||||
def unlockError(self):
|
||||
self._lockedError = None
|
||||
|
||||
def unlockSize(self):
|
||||
self._lockedSize = None
|
||||
|
||||
def loadUi(self, filename):
|
||||
'''Load a Qt Designer ui file to use for this component's widget'''
|
||||
return uic.loadUi(os.path.join(self.core.componentsPath, filename))
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
if self._lockedSize is None:
|
||||
return int(self.settings.value('outputWidth'))
|
||||
else:
|
||||
return self._lockedSize[0]
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
if self._lockedSize is None:
|
||||
return int(self.settings.value('outputHeight'))
|
||||
else:
|
||||
return self._lockedSize[1]
|
||||
|
||||
def cancel(self):
|
||||
'''Stop any lengthy process in response to this variable.'''
|
||||
self.canceled = True
|
||||
|
||||
def reset(self):
|
||||
self.canceled = False
|
||||
self.unlockProperties()
|
||||
self.unlockError()
|
||||
|
||||
def relativeWidgetAxis(func):
|
||||
def relativeWidgetAxis(self, attr, *args, **kwargs):
|
||||
if 'axis' not in kwargs:
|
||||
axis = self.width
|
||||
if 'height' in attr.lower() \
|
||||
or 'ypos' in attr.lower() or attr == 'y':
|
||||
axis = self.height
|
||||
kwargs['axis'] = axis
|
||||
return func(self, attr, *args, **kwargs)
|
||||
return relativeWidgetAxis
|
||||
|
||||
@relativeWidgetAxis
|
||||
def pixelValForAttr(self, attr, val=None, **kwargs):
|
||||
if val is None:
|
||||
val = self._relativeValues[attr]
|
||||
return math.ceil(kwargs['axis'] * val)
|
||||
|
||||
@relativeWidgetAxis
|
||||
def floatValForAttr(self, attr, val=None, **kwargs):
|
||||
if val is None:
|
||||
val = self._trackedWidgets[attr].value()
|
||||
return val / kwargs['axis']
|
||||
|
||||
def setRelativeWidget(self, attr, floatVal):
|
||||
'''Set a relative widget using a float'''
|
||||
pixelVal = self.pixelValForAttr(attr, floatVal)
|
||||
self._trackedWidgets[attr].setValue(pixelVal)
|
||||
|
||||
|
||||
def updateRelativeWidget(self, attr):
|
||||
try:
|
||||
oldUserValue = getattr(self, attr)
|
||||
except AttributeError:
|
||||
oldUserValue = self._trackedWidgets[attr].value()
|
||||
newUserValue = self._trackedWidgets[attr].value()
|
||||
newRelativeVal = self.floatValForAttr(attr, newUserValue)
|
||||
|
||||
if attr in self._relativeValues:
|
||||
oldRelativeVal = self._relativeValues[attr]
|
||||
if oldUserValue == newUserValue \
|
||||
and oldRelativeVal != newRelativeVal:
|
||||
# Float changed without pixel value changing, which
|
||||
# means the pixel value needs to be updated
|
||||
self._trackedWidgets[attr].blockSignals(True)
|
||||
self.updateRelativeWidgetMaximum(attr)
|
||||
pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
|
||||
self._trackedWidgets[attr].setValue(pixelVal)
|
||||
self._trackedWidgets[attr].blockSignals(False)
|
||||
|
||||
if attr not in self._relativeValues \
|
||||
or oldUserValue != newUserValue:
|
||||
self._relativeValues[attr] = newRelativeVal
|
||||
|
||||
def updateRelativeWidgetMaximum(self, attr):
|
||||
maxRes = int(self.core.resolutions[0].split('x')[0])
|
||||
newMaximumValue = self.width * (
|
||||
self._relativeMaximums[attr] /
|
||||
maxRes
|
||||
)
|
||||
self._trackedWidgets[attr].setMaximum(int(newMaximumValue))
|
||||
|
||||
|
||||
class ComponentError(RuntimeError):
|
||||
'''Gives the MainWindow a traceback to display, and cancels the export.'''
|
||||
|
||||
prevErrors = []
|
||||
lastTime = time.time()
|
||||
|
||||
def __init__(self, caller, name, msg=None):
|
||||
if msg is None and sys.exc_info()[0] is not None:
|
||||
msg = str(sys.exc_info()[1])
|
||||
else:
|
||||
msg = 'Unknown error.'
|
||||
print("##### ComponentError by %s's %s: %s" % (
|
||||
caller.name, name, msg))
|
||||
|
||||
# Don't create multiple windows for quickly repeated messages
|
||||
if len(ComponentError.prevErrors) > 1:
|
||||
ComponentError.prevErrors.pop()
|
||||
ComponentError.prevErrors.insert(0, name)
|
||||
curTime = time.time()
|
||||
if name in ComponentError.prevErrors[1:] \
|
||||
and curTime - ComponentError.lastTime < 1.0:
|
||||
return
|
||||
ComponentError.lastTime = time.time()
|
||||
|
||||
from toolkit import formatTraceback
|
||||
if sys.exc_info()[0] is not None:
|
||||
string = (
|
||||
"%s component (#%s): %s encountered %s %s: %s" % (
|
||||
caller.__class__.name,
|
||||
str(caller.compPos),
|
||||
name,
|
||||
'an' if any([
|
||||
sys.exc_info()[0].__name__.startswith(vowel)
|
||||
for vowel in ('A', 'I', 'U', 'O', 'E')
|
||||
]) else 'a',
|
||||
sys.exc_info()[0].__name__,
|
||||
str(sys.exc_info()[1])
|
||||
)
|
||||
)
|
||||
detail = formatTraceback(sys.exc_info()[2])
|
||||
else:
|
||||
string = name
|
||||
detail = "Attributes:\n%s" % (
|
||||
"\n".join(
|
||||
[m for m in dir(caller) if not m.startswith('_')]
|
||||
)
|
||||
)
|
||||
|
||||
super().__init__(string)
|
||||
caller.lockError(string)
|
||||
caller._error.emit(string, detail)
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
from PIL import Image, ImageDraw
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
from PyQt5.QtGui import QColor
|
||||
from PIL.ImageQt import ImageQt
|
||||
import os
|
||||
|
||||
from component import Component
|
||||
from toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor
|
||||
|
||||
|
||||
class Component(Component):
|
||||
name = 'Color'
|
||||
version = '1.0.0'
|
||||
|
||||
def widget(self, *args):
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
super().widget(*args)
|
||||
|
||||
self.page.lineEdit_color1.setText('0,0,0')
|
||||
self.page.lineEdit_color2.setText('133,133,133')
|
||||
|
||||
# disable color #2 until non-default 'fill' option gets changed
|
||||
self.page.lineEdit_color2.setDisabled(True)
|
||||
self.page.pushButton_color2.setDisabled(True)
|
||||
self.page.spinBox_width.setValue(
|
||||
int(self.settings.value("outputWidth")))
|
||||
self.page.spinBox_height.setValue(
|
||||
int(self.settings.value("outputHeight")))
|
||||
|
||||
self.fillLabels = [
|
||||
'Solid',
|
||||
'Linear Gradient',
|
||||
'Radial Gradient',
|
||||
]
|
||||
for label in self.fillLabels:
|
||||
self.page.comboBox_fill.addItem(label)
|
||||
self.page.comboBox_fill.setCurrentIndex(0)
|
||||
|
||||
self.trackWidgets({
|
||||
'x': self.page.spinBox_x,
|
||||
'y': self.page.spinBox_y,
|
||||
'sizeWidth': self.page.spinBox_width,
|
||||
'sizeHeight': self.page.spinBox_height,
|
||||
'trans': self.page.checkBox_trans,
|
||||
'spread': self.page.comboBox_spread,
|
||||
'stretch': self.page.checkBox_stretch,
|
||||
'RG_start': self.page.spinBox_radialGradient_start,
|
||||
'LG_start': self.page.spinBox_linearGradient_start,
|
||||
'RG_end': self.page.spinBox_radialGradient_end,
|
||||
'LG_end': self.page.spinBox_linearGradient_end,
|
||||
'RG_centre': self.page.spinBox_radialGradient_spread,
|
||||
'fillType': self.page.comboBox_fill,
|
||||
'color1': self.page.lineEdit_color1,
|
||||
'color2': self.page.lineEdit_color2,
|
||||
}, presetNames={
|
||||
'sizeWidth': 'width',
|
||||
'sizeHeight': 'height',
|
||||
}, colorWidgets={
|
||||
'color1': self.page.pushButton_color1,
|
||||
'color2': self.page.pushButton_color2,
|
||||
}, relativeWidgets=[
|
||||
'x', 'y',
|
||||
'sizeWidth', 'sizeHeight',
|
||||
'LG_start', 'LG_end',
|
||||
'RG_start', 'RG_end', 'RG_centre',
|
||||
])
|
||||
|
||||
def update(self):
|
||||
fillType = self.page.comboBox_fill.currentIndex()
|
||||
if fillType == 0:
|
||||
self.page.lineEdit_color2.setEnabled(False)
|
||||
self.page.pushButton_color2.setEnabled(False)
|
||||
self.page.checkBox_trans.setEnabled(False)
|
||||
self.page.checkBox_stretch.setEnabled(False)
|
||||
self.page.comboBox_spread.setEnabled(False)
|
||||
else:
|
||||
self.page.lineEdit_color2.setEnabled(True)
|
||||
self.page.pushButton_color2.setEnabled(True)
|
||||
self.page.checkBox_trans.setEnabled(True)
|
||||
self.page.checkBox_stretch.setEnabled(True)
|
||||
self.page.comboBox_spread.setEnabled(True)
|
||||
if self.page.checkBox_trans.isChecked():
|
||||
self.page.lineEdit_color2.setEnabled(False)
|
||||
self.page.pushButton_color2.setEnabled(False)
|
||||
self.page.fillWidget.setCurrentIndex(fillType)
|
||||
|
||||
super().update()
|
||||
|
||||
def previewRender(self):
|
||||
return self.drawFrame(self.width, self.height)
|
||||
|
||||
def properties(self):
|
||||
return ['static']
|
||||
|
||||
def frameRender(self, frameNo):
|
||||
return self.drawFrame(self.width, self.height)
|
||||
|
||||
def drawFrame(self, width, height):
|
||||
r, g, b = self.color1
|
||||
shapeSize = (self.sizeWidth, self.sizeHeight)
|
||||
# in default state, skip all this logic and return a plain fill
|
||||
if self.fillType == 0 and shapeSize == (width, height) \
|
||||
and self.x == 0 and self.y == 0:
|
||||
return FloodFrame(width, height, (r, g, b, 255))
|
||||
|
||||
# Return a solid image at x, y
|
||||
if self.fillType == 0:
|
||||
frame = BlankFrame(width, height)
|
||||
image = Image.new("RGBA", shapeSize, (r, g, b, 255))
|
||||
frame.paste(image, box=(self.x, self.y))
|
||||
return frame
|
||||
|
||||
# Now fills that require using Qt...
|
||||
elif self.fillType > 0:
|
||||
image = FramePainter(width, height)
|
||||
|
||||
if self.stretch:
|
||||
w = width
|
||||
h = height
|
||||
else:
|
||||
w = self.sizeWidth
|
||||
h = self.sizeWidth
|
||||
|
||||
if self.fillType == 1: # Linear Gradient
|
||||
brush = QtGui.QLinearGradient(
|
||||
self.LG_start,
|
||||
self.LG_start,
|
||||
self.LG_end+width/3,
|
||||
self.LG_end)
|
||||
|
||||
elif self.fillType == 2: # Radial Gradient
|
||||
brush = QtGui.QRadialGradient(
|
||||
self.RG_start,
|
||||
self.RG_end,
|
||||
w, h,
|
||||
self.RG_centre)
|
||||
|
||||
brush.setSpread(self.spread)
|
||||
brush.setColorAt(0.0, PaintColor(*self.color1))
|
||||
if self.trans:
|
||||
brush.setColorAt(1.0, PaintColor(0, 0, 0, 0))
|
||||
elif self.fillType == 1 and self.stretch:
|
||||
brush.setColorAt(0.2, PaintColor(*self.color2))
|
||||
else:
|
||||
brush.setColorAt(1.0, PaintColor(*self.color2))
|
||||
image.setBrush(brush)
|
||||
image.drawRect(
|
||||
self.x, self.y,
|
||||
self.sizeWidth, self.sizeHeight
|
||||
)
|
||||
|
||||
return image.finalize()
|
||||
|
||||
def commandHelp(self):
|
||||
print('Specify a color:\n color=255,255,255')
|
||||
|
||||
def command(self, arg):
|
||||
if '=' in arg:
|
||||
key, arg = arg.split('=', 1)
|
||||
if key == 'color':
|
||||
self.page.lineEdit_color1.setText(arg)
|
||||
return
|
||||
super().command(arg)
|
|
@ -0,0 +1,660 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>586</width>
|
||||
<height>197</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_textColor">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>31</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Color #1</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_color1">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="MaximumSize" stdset="0">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_color1">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>1</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maxLength">
|
||||
<number>12</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_9">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_textColor_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>31</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Color #2</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_color2">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="MaximumSize" stdset="0">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_color2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>1</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maxLength">
|
||||
<number>12</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_xTitleAlign_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Width</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_width">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>999999999</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_yTitleAlign_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Height</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_height">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>999999999</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_7">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_xTitleAlign">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>X</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_x">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-10000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_yTitleAlign">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Y</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_y">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-10000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_9">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_textLayout">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Fill </string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_fill">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>-1</number>
|
||||
</property>
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QComboBox::AdjustToContentsOnFirstShow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_trans">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Transparent</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_stretch">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Stretch</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_spread">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Pad</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Reflect</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Repeat</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Minimum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="fillWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="blank"/>
|
||||
<widget class="QWidget" name="linearGradient">
|
||||
<widget class="QWidget" name="horizontalLayoutWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>-1</x>
|
||||
<y>0</y>
|
||||
<width>561</width>
|
||||
<height>31</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_xTitleAlign_4">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Start</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_linearGradient_start">
|
||||
<property name="minimum">
|
||||
<number>-10000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>End</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_linearGradient_end">
|
||||
<property name="minimum">
|
||||
<number>-10000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="radialGradient">
|
||||
<widget class="QWidget" name="horizontalLayoutWidget_3">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
<width>561</width>
|
||||
<height>31</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_xTitleAlign_6">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Start</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_radialGradient_start">
|
||||
<property name="minimum">
|
||||
<number>-10000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>End</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_radialGradient_end">
|
||||
<property name="minimum">
|
||||
<number>-10000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Centre</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_radialGradient_spread">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::PlusMinus</enum>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-10000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>3</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -0,0 +1,102 @@
|
|||
from PIL import Image, ImageDraw, ImageEnhance
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
import os
|
||||
|
||||
from component import Component
|
||||
from toolkit.frame import BlankFrame
|
||||
|
||||
|
||||
class Component(Component):
|
||||
name = 'Image'
|
||||
version = '1.0.0'
|
||||
|
||||
def widget(self, *args):
|
||||
super().widget(*args)
|
||||
self.page.pushButton_image.clicked.connect(self.pickImage)
|
||||
self.trackWidgets({
|
||||
'imagePath': self.page.lineEdit_image,
|
||||
'scale': self.page.spinBox_scale,
|
||||
'rotate': self.page.spinBox_rotate,
|
||||
'color': self.page.spinBox_color,
|
||||
'xPosition': self.page.spinBox_x,
|
||||
'yPosition': self.page.spinBox_y,
|
||||
'stretched': self.page.checkBox_stretch,
|
||||
'mirror': self.page.checkBox_mirror,
|
||||
}, presetNames={
|
||||
'imagePath': 'image',
|
||||
'xPosition': 'x',
|
||||
'yPosition': 'y',
|
||||
}, relativeWidgets=[
|
||||
'xPosition', 'yPosition', 'scale'
|
||||
])
|
||||
|
||||
def previewRender(self):
|
||||
return self.drawFrame(self.width, self.height)
|
||||
|
||||
def properties(self):
|
||||
props = ['static']
|
||||
if not os.path.exists(self.imagePath):
|
||||
props.append('error')
|
||||
return props
|
||||
|
||||
def error(self):
|
||||
if not self.imagePath:
|
||||
return "There is no image selected."
|
||||
if not os.path.exists(self.imagePath):
|
||||
return "The image selected does not exist!"
|
||||
|
||||
def frameRender(self, frameNo):
|
||||
return self.drawFrame(self.width, self.height)
|
||||
|
||||
def drawFrame(self, width, height):
|
||||
frame = BlankFrame(width, height)
|
||||
if self.imagePath and os.path.exists(self.imagePath):
|
||||
image = Image.open(self.imagePath)
|
||||
|
||||
# Modify image's appearance
|
||||
if self.color != 100:
|
||||
image = ImageEnhance.Color(image).enhance(
|
||||
float(self.color / 100)
|
||||
)
|
||||
if self.mirror:
|
||||
image = image.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
if self.stretched and image.size != (width, height):
|
||||
image = image.resize((width, height), Image.ANTIALIAS)
|
||||
if self.scale != 100:
|
||||
newHeight = int((image.height / 100) * self.scale)
|
||||
newWidth = int((image.width / 100) * self.scale)
|
||||
image = image.resize((newWidth, newHeight), Image.ANTIALIAS)
|
||||
|
||||
# Paste image at correct position
|
||||
frame.paste(image, box=(self.xPosition, self.yPosition))
|
||||
if self.rotate != 0:
|
||||
frame = frame.rotate(self.rotate)
|
||||
|
||||
return frame
|
||||
|
||||
def pickImage(self):
|
||||
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
|
||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self.page, "Choose Image", imgDir,
|
||||
"Image Files (%s)" % " ".join(self.core.imageFormats))
|
||||
if filename:
|
||||
self.settings.setValue("componentDir", os.path.dirname(filename))
|
||||
self.page.lineEdit_image.setText(filename)
|
||||
self.update()
|
||||
|
||||
def command(self, arg):
|
||||
if '=' in arg:
|
||||
key, arg = arg.split('=', 1)
|
||||
if key == 'path' and os.path.exists(arg):
|
||||
try:
|
||||
Image.open(arg)
|
||||
self.page.lineEdit_image.setText(arg)
|
||||
self.page.checkBox_stretch.setChecked(True)
|
||||
return
|
||||
except OSError as e:
|
||||
print("Not a supported image format")
|
||||
quit(1)
|
||||
super().command(arg)
|
||||
|
||||
def commandHelp(self):
|
||||
print('Load an image:\n path=/filepath/to/image.png')
|
|
@ -0,0 +1,372 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>586</width>
|
||||
<height>197</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_textColor">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>31</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Image</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_image">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>1</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_image">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>1</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="MaximumSize" stdset="0">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_9">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_xTitleAlign">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>X</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_x">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-10000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_yTitleAlign">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Y</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_y">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-1000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>1000</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_9">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_stretch">
|
||||
<property name="text">
|
||||
<string>Stretch</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_10">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_mirror">
|
||||
<property name="text">
|
||||
<string>Mirror</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Rotate</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_rotate">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::UpDownArrows</enum>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string notr="true">°</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>359</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Scale</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_scale">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::UpDownArrows</enum>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string>%</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>400</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>100</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Color</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_color">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::UpDownArrows</enum>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string>%</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>999</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>100</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -0,0 +1,203 @@
|
|||
import numpy
|
||||
from PIL import Image, ImageDraw
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
from PyQt5.QtGui import QColor
|
||||
import os
|
||||
import time
|
||||
from copy import copy
|
||||
|
||||
from component import Component
|
||||
from toolkit.frame import BlankFrame
|
||||
|
||||
|
||||
class Component(Component):
|
||||
name = 'Classic Visualizer'
|
||||
version = '1.0.0'
|
||||
|
||||
def names(*args):
|
||||
return ['Original Audio Visualization']
|
||||
|
||||
def properties(self):
|
||||
return ['pcm']
|
||||
|
||||
def widget(self, *args):
|
||||
self.scale = 20
|
||||
self.y = 0
|
||||
super().widget(*args)
|
||||
|
||||
self.page.comboBox_visLayout.addItem("Classic")
|
||||
self.page.comboBox_visLayout.addItem("Split")
|
||||
self.page.comboBox_visLayout.addItem("Bottom")
|
||||
self.page.comboBox_visLayout.addItem("Top")
|
||||
self.page.comboBox_visLayout.setCurrentIndex(0)
|
||||
|
||||
self.page.lineEdit_visColor.setText('255,255,255')
|
||||
|
||||
self.trackWidgets({
|
||||
'visColor': self.page.lineEdit_visColor,
|
||||
'layout': self.page.comboBox_visLayout,
|
||||
'scale': self.page.spinBox_scale,
|
||||
'y': self.page.spinBox_y,
|
||||
}, colorWidgets={
|
||||
'visColor': self.page.pushButton_visColor,
|
||||
}, relativeWidgets=[
|
||||
'y',
|
||||
])
|
||||
|
||||
def previewRender(self):
|
||||
spectrum = numpy.fromfunction(
|
||||
lambda x: float(self.scale)/2500*(x-128)**2, (255,), dtype="int16")
|
||||
return self.drawBars(
|
||||
self.width, self.height, spectrum, self.visColor, self.layout
|
||||
)
|
||||
|
||||
def preFrameRender(self, **kwargs):
|
||||
super().preFrameRender(**kwargs)
|
||||
self.smoothConstantDown = 0.08
|
||||
self.smoothConstantUp = 0.8
|
||||
self.lastSpectrum = None
|
||||
self.spectrumArray = {}
|
||||
|
||||
for i in range(0, len(self.completeAudioArray), self.sampleSize):
|
||||
if self.canceled:
|
||||
break
|
||||
self.lastSpectrum = self.transformData(
|
||||
i, self.completeAudioArray, self.sampleSize,
|
||||
self.smoothConstantDown, self.smoothConstantUp,
|
||||
self.lastSpectrum)
|
||||
self.spectrumArray[i] = copy(self.lastSpectrum)
|
||||
|
||||
progress = int(100*(i/len(self.completeAudioArray)))
|
||||
if progress >= 100:
|
||||
progress = 100
|
||||
pStr = "Analyzing audio: "+str(progress)+'%'
|
||||
self.progressBarSetText.emit(pStr)
|
||||
self.progressBarUpdate.emit(int(progress))
|
||||
|
||||
def frameRender(self, frameNo):
|
||||
arrayNo = frameNo * self.sampleSize
|
||||
return self.drawBars(
|
||||
self.width, self.height,
|
||||
self.spectrumArray[arrayNo],
|
||||
self.visColor, self.layout)
|
||||
|
||||
def transformData(
|
||||
self, i, completeAudioArray, sampleSize,
|
||||
smoothConstantDown, smoothConstantUp, lastSpectrum):
|
||||
if len(completeAudioArray) < (i + sampleSize):
|
||||
sampleSize = len(completeAudioArray) - i
|
||||
|
||||
window = numpy.hanning(sampleSize)
|
||||
data = completeAudioArray[i:i+sampleSize][::1] * window
|
||||
paddedSampleSize = 2048
|
||||
paddedData = numpy.pad(
|
||||
data, (0, paddedSampleSize - sampleSize), 'constant')
|
||||
spectrum = numpy.fft.fft(paddedData)
|
||||
sample_rate = 44100
|
||||
frequencies = numpy.fft.fftfreq(len(spectrum), 1./sample_rate)
|
||||
|
||||
y = abs(spectrum[0:int(paddedSampleSize/2) - 1])
|
||||
|
||||
# filter the noise away
|
||||
# y[y<80] = 0
|
||||
|
||||
y = self.scale * numpy.log10(y)
|
||||
y[numpy.isinf(y)] = 0
|
||||
|
||||
if lastSpectrum is not None:
|
||||
lastSpectrum[y < lastSpectrum] = \
|
||||
y[y < lastSpectrum] * smoothConstantDown + \
|
||||
lastSpectrum[y < lastSpectrum] * (1 - smoothConstantDown)
|
||||
|
||||
lastSpectrum[y >= lastSpectrum] = \
|
||||
y[y >= lastSpectrum] * smoothConstantUp + \
|
||||
lastSpectrum[y >= lastSpectrum] * (1 - smoothConstantUp)
|
||||
else:
|
||||
lastSpectrum = y
|
||||
|
||||
x = frequencies[0:int(paddedSampleSize/2) - 1]
|
||||
|
||||
return lastSpectrum
|
||||
|
||||
def drawBars(self, width, height, spectrum, color, layout):
|
||||
vH = height-height/8
|
||||
bF = width / 64
|
||||
bH = bF / 2
|
||||
bQ = bF / 4
|
||||
imTop = BlankFrame(width, height)
|
||||
draw = ImageDraw.Draw(imTop)
|
||||
r, g, b = color
|
||||
color2 = (r, g, b, 125)
|
||||
|
||||
bP = height / 1200
|
||||
|
||||
for j in range(0, 63):
|
||||
draw.rectangle((
|
||||
bH + j * bF, vH+bQ, bH + j * bF + bF, vH + bQ -
|
||||
spectrum[j * 4] * bP - bH), fill=color2)
|
||||
|
||||
draw.rectangle((
|
||||
bH + bQ + j * bF, vH, bH + bQ + j * bF + bH, vH -
|
||||
spectrum[j * 4] * bP), fill=color)
|
||||
|
||||
imBottom = imTop.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
|
||||
im = BlankFrame(width, height)
|
||||
|
||||
if layout == 0: # Classic
|
||||
y = self.y - int(height/100*43)
|
||||
im.paste(imTop, (0, y), mask=imTop)
|
||||
y = self.y + int(height/100*43)
|
||||
im.paste(imBottom, (0, y), mask=imBottom)
|
||||
|
||||
if layout == 1: # Split
|
||||
y = self.y + int(height/100*10)
|
||||
im.paste(imTop, (0, y), mask=imTop)
|
||||
y = self.y - int(height/100*10)
|
||||
im.paste(imBottom, (0, y), mask=imBottom)
|
||||
|
||||
if layout == 2: # Bottom
|
||||
y = self.y + int(height/100*10)
|
||||
im.paste(imTop, (0, y), mask=imTop)
|
||||
|
||||
if layout == 3: # Top
|
||||
y = self.y - int(height/100*10)
|
||||
im.paste(imBottom, (0, y), mask=imBottom)
|
||||
|
||||
return im
|
||||
|
||||
def command(self, arg):
|
||||
if '=' in arg:
|
||||
key, arg = arg.split('=', 1)
|
||||
try:
|
||||
if key == 'color':
|
||||
self.page.lineEdit_visColor.setText(arg)
|
||||
return
|
||||
elif key == 'layout':
|
||||
if arg == 'classic':
|
||||
self.page.comboBox_visLayout.setCurrentIndex(0)
|
||||
elif arg == 'split':
|
||||
self.page.comboBox_visLayout.setCurrentIndex(1)
|
||||
elif arg == 'bottom':
|
||||
self.page.comboBox_visLayout.setCurrentIndex(2)
|
||||
elif arg == 'top':
|
||||
self.page.comboBox_visLayout.setCurrentIndex(3)
|
||||
return
|
||||
elif key == 'scale':
|
||||
arg = int(arg)
|
||||
self.page.spinBox_scale.setValue(arg)
|
||||
return
|
||||
elif key == 'y':
|
||||
arg = int(arg)
|
||||
self.page.spinBox_y.setValue(arg)
|
||||
return
|
||||
except ValueError:
|
||||
print('You must enter a number.')
|
||||
quit(1)
|
||||
super().command(arg)
|
||||
|
||||
def commandHelp(self):
|
||||
print('Give a layout name:\n layout=[classic/split/bottom/top]')
|
||||
print('Specify a color:\n color=255,255,255')
|
||||
print('Visualizer scale (20 is default):\n scale=number')
|
||||
print('Y position:\n y=number')
|
|
@ -0,0 +1,193 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>633</width>
|
||||
<height>178</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>180</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_9">
|
||||
<property name="leftMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_visLayout">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Layout</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_visLayout"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Scale</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_scale">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::PlusMinus</enum>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>20</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_visColor">
|
||||
<property name="text">
|
||||
<string>Color</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_visColor">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="MaximumSize" stdset="0">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_visColor"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_10">
|
||||
<property name="leftMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Y</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_y">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::UpDownArrows</enum>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-5000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>5000</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Expanding</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -0,0 +1,72 @@
|
|||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
import os
|
||||
|
||||
from component import Component
|
||||
from toolkit.frame import BlankFrame
|
||||
|
||||
|
||||
class Component(Component):
|
||||
name = 'Sound'
|
||||
version = '1.0.0'
|
||||
|
||||
def widget(self, *args):
|
||||
super().widget(*args)
|
||||
self.page.pushButton_sound.clicked.connect(self.pickSound)
|
||||
self.trackWidgets({
|
||||
'sound': self.page.lineEdit_sound,
|
||||
'chorus': self.page.checkBox_chorus,
|
||||
'delay': self.page.spinBox_delay,
|
||||
'volume': self.page.spinBox_volume,
|
||||
}, commandArgs={
|
||||
'sound': None,
|
||||
})
|
||||
|
||||
def properties(self):
|
||||
props = ['static', 'audio']
|
||||
if not os.path.exists(self.sound):
|
||||
props.append('error')
|
||||
return props
|
||||
|
||||
def error(self):
|
||||
if not self.sound:
|
||||
return "No audio file selected."
|
||||
if not os.path.exists(self.sound):
|
||||
return "The audio file selected no longer exists!"
|
||||
|
||||
def audio(self):
|
||||
params = {}
|
||||
if self.delay != 0.0:
|
||||
params['adelay'] = '=%s' % str(int(self.delay * 1000.00))
|
||||
if self.chorus:
|
||||
params['chorus'] = \
|
||||
'=0.5:0.9:50|60|40:0.4|0.32|0.3:0.25|0.4|0.3:2|2.3|1.3'
|
||||
if self.volume != 1.0:
|
||||
params['volume'] = '=%s:replaygain_noclip=0' % str(self.volume)
|
||||
|
||||
return (self.sound, params)
|
||||
|
||||
def pickSound(self):
|
||||
sndDir = self.settings.value("componentDir", os.path.expanduser("~"))
|
||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self.page, "Choose Sound", sndDir,
|
||||
"Audio Files (%s)" % " ".join(self.core.audioFormats))
|
||||
if filename:
|
||||
self.settings.setValue("componentDir", os.path.dirname(filename))
|
||||
self.page.lineEdit_sound.setText(filename)
|
||||
self.update()
|
||||
|
||||
def commandHelp(self):
|
||||
print('Path to audio file:\n path=/filepath/to/sound.ogg')
|
||||
|
||||
def command(self, arg):
|
||||
if '=' in arg:
|
||||
key, arg = arg.split('=', 1)
|
||||
if key == 'path':
|
||||
if '*%s' % os.path.splitext(arg)[1] \
|
||||
not in self.core.audioFormats:
|
||||
print("Not a supported audio format")
|
||||
quit(1)
|
||||
self.page.lineEdit_sound.setText(arg)
|
||||
return
|
||||
|
||||
super().command(arg)
|
|
@ -0,0 +1,172 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>586</width>
|
||||
<height>197</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_textColor">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>31</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Audio File</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_sound">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>1</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_sound">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>1</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="MaximumSize" stdset="0">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Volume</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="spinBox_volume">
|
||||
<property name="suffix">
|
||||
<string>x</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>10.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Delay</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="spinBox_delay">
|
||||
<property name="suffix">
|
||||
<string>s</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999999.990000000223517</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.500000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_chorus">
|
||||
<property name="text">
|
||||
<string>Chorus</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -0,0 +1,284 @@
|
|||
from PIL import Image
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
import os
|
||||
import math
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from component import Component
|
||||
from toolkit.frame import BlankFrame, scale
|
||||
from toolkit import checkOutput, connectWidget
|
||||
from toolkit.ffmpeg import (
|
||||
openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound
|
||||
)
|
||||
|
||||
|
||||
class Component(Component):
|
||||
name = 'Spectrum'
|
||||
version = '1.0.0'
|
||||
|
||||
def widget(self, *args):
|
||||
self.previewFrame = None
|
||||
super().widget(*args)
|
||||
self._image = BlankFrame(self.width, self.height)
|
||||
self.chunkSize = 4 * self.width * self.height
|
||||
self.changedOptions = True
|
||||
|
||||
if hasattr(self.parent, 'window'):
|
||||
# update preview when audio file changes (if genericPreview is off)
|
||||
self.parent.window.lineEdit_audioFile.textChanged.connect(
|
||||
self.update
|
||||
)
|
||||
|
||||
self.trackWidgets({
|
||||
'filterType': self.page.comboBox_filterType,
|
||||
'window': self.page.comboBox_window,
|
||||
'mode': self.page.comboBox_mode,
|
||||
'amplitude': self.page.comboBox_amplitude0,
|
||||
'amplitude1': self.page.comboBox_amplitude1,
|
||||
'amplitude2': self.page.comboBox_amplitude2,
|
||||
'display': self.page.comboBox_display,
|
||||
'zoom': self.page.spinBox_zoom,
|
||||
'tc': self.page.spinBox_tc,
|
||||
'x': self.page.spinBox_x,
|
||||
'y': self.page.spinBox_y,
|
||||
'mirror': self.page.checkBox_mirror,
|
||||
'draw': self.page.checkBox_draw,
|
||||
'scale': self.page.spinBox_scale,
|
||||
'color': self.page.comboBox_color,
|
||||
'compress': self.page.checkBox_compress,
|
||||
'mono': self.page.checkBox_mono,
|
||||
'hue': self.page.spinBox_hue,
|
||||
}, relativeWidgets=[
|
||||
'x', 'y',
|
||||
])
|
||||
for widget in self._trackedWidgets.values():
|
||||
connectWidget(widget, lambda: self.changed())
|
||||
|
||||
def changed(self):
|
||||
self.changedOptions = True
|
||||
|
||||
def update(self):
|
||||
self.page.stackedWidget.setCurrentIndex(
|
||||
self.page.comboBox_filterType.currentIndex())
|
||||
super().update()
|
||||
|
||||
def previewRender(self):
|
||||
changedSize = self.updateChunksize()
|
||||
if not changedSize \
|
||||
and not self.changedOptions \
|
||||
and self.previewFrame is not None:
|
||||
return self.previewFrame
|
||||
|
||||
frame = self.getPreviewFrame()
|
||||
self.changedOptions = False
|
||||
if not frame:
|
||||
self.previewFrame = None
|
||||
return BlankFrame(self.width, self.height)
|
||||
else:
|
||||
self.previewFrame = frame
|
||||
return frame
|
||||
|
||||
def preFrameRender(self, **kwargs):
|
||||
super().preFrameRender(**kwargs)
|
||||
self.updateChunksize()
|
||||
w, h = scale(self.scale, self.width, self.height, str)
|
||||
self.video = FfmpegVideo(
|
||||
inputPath=self.audioFile,
|
||||
filter_=self.makeFfmpegFilter(),
|
||||
width=w, height=h,
|
||||
chunkSize=self.chunkSize,
|
||||
frameRate=int(self.settings.value("outputFrameRate")),
|
||||
parent=self.parent, component=self,
|
||||
)
|
||||
|
||||
def frameRender(self, frameNo):
|
||||
if FfmpegVideo.threadError is not None:
|
||||
raise FfmpegVideo.threadError
|
||||
return self.finalizeFrame(self.video.frame(frameNo))
|
||||
|
||||
def postFrameRender(self):
|
||||
closePipe(self.video.pipe)
|
||||
|
||||
def getPreviewFrame(self):
|
||||
genericPreview = self.settings.value("pref_genericPreview")
|
||||
startPt = 0
|
||||
if not genericPreview:
|
||||
inputFile = self.parent.window.lineEdit_audioFile.text()
|
||||
if not inputFile or not os.path.exists(inputFile):
|
||||
return
|
||||
duration = getAudioDuration(inputFile)
|
||||
if not duration:
|
||||
return
|
||||
startPt = duration / 3
|
||||
|
||||
command = [
|
||||
self.core.FFMPEG_BIN,
|
||||
'-thread_queue_size', '512',
|
||||
'-r', self.settings.value("outputFrameRate"),
|
||||
'-ss', "{0:.3f}".format(startPt),
|
||||
'-i',
|
||||
os.path.join(self.core.wd, 'background.png')
|
||||
if genericPreview else inputFile,
|
||||
'-f', 'image2pipe',
|
||||
'-pix_fmt', 'rgba',
|
||||
]
|
||||
command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt))
|
||||
command.extend([
|
||||
'-an',
|
||||
'-s:v', '%sx%s' % scale(self.scale, self.width, self.height, str),
|
||||
'-codec:v', 'rawvideo', '-',
|
||||
'-frames:v', '1',
|
||||
])
|
||||
logFilename = os.path.join(
|
||||
self.core.dataDir, 'preview_%s.log' % str(self.compPos))
|
||||
with open(logFilename, 'w') as log:
|
||||
log.write(" ".join(command) + '\n\n')
|
||||
with open(logFilename, 'a') as log:
|
||||
pipe = openPipe(
|
||||
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=log, bufsize=10**8
|
||||
)
|
||||
byteFrame = pipe.stdout.read(self.chunkSize)
|
||||
closePipe(pipe)
|
||||
|
||||
frame = self.finalizeFrame(byteFrame)
|
||||
return frame
|
||||
|
||||
def makeFfmpegFilter(self, preview=False, startPt=0):
|
||||
w, h = scale(self.scale, self.width, self.height, str)
|
||||
color = self.page.comboBox_color.currentText().lower()
|
||||
genericPreview = self.settings.value("pref_genericPreview")
|
||||
|
||||
if self.filterType == 0: # Spectrum
|
||||
if self.amplitude == 0:
|
||||
amplitude = 'sqrt'
|
||||
elif self.amplitude == 1:
|
||||
amplitude = 'cbrt'
|
||||
elif self.amplitude == 2:
|
||||
amplitude = '4thrt'
|
||||
elif self.amplitude == 3:
|
||||
amplitude = '5thrt'
|
||||
elif self.amplitude == 4:
|
||||
amplitude = 'lin'
|
||||
elif self.amplitude == 5:
|
||||
amplitude = 'log'
|
||||
filter_ = (
|
||||
'showspectrum=s=%sx%s:slide=scroll:win_func=%s:'
|
||||
'color=%s:scale=%s,'
|
||||
'colorkey=color=black:similarity=0.1:blend=0.5' % (
|
||||
self.settings.value("outputWidth"),
|
||||
self.settings.value("outputHeight"),
|
||||
self.page.comboBox_window.currentText(),
|
||||
color, amplitude,
|
||||
)
|
||||
)
|
||||
elif self.filterType == 1: # Histogram
|
||||
if self.amplitude1 == 0:
|
||||
amplitude = 'log'
|
||||
elif self.amplitude1 == 1:
|
||||
amplitude = 'lin'
|
||||
if self.display == 0:
|
||||
display = 'log'
|
||||
elif self.display == 1:
|
||||
display = 'sqrt'
|
||||
elif self.display == 2:
|
||||
display = 'cbrt'
|
||||
elif self.display == 3:
|
||||
display = 'lin'
|
||||
elif self.display == 4:
|
||||
display = 'rlog'
|
||||
filter_ = (
|
||||
'ahistogram=r=%s:s=%sx%s:dmode=separate:ascale=%s:scale=%s' % (
|
||||
self.settings.value("outputFrameRate"),
|
||||
self.settings.value("outputWidth"),
|
||||
self.settings.value("outputHeight"),
|
||||
amplitude, display
|
||||
)
|
||||
)
|
||||
elif self.filterType == 2: # Vector Scope
|
||||
if self.amplitude2 == 0:
|
||||
amplitude = 'log'
|
||||
elif self.amplitude2 == 1:
|
||||
amplitude = 'sqrt'
|
||||
elif self.amplitude2 == 2:
|
||||
amplitude = 'cbrt'
|
||||
elif self.amplitude2 == 3:
|
||||
amplitude = 'lin'
|
||||
m = self.page.comboBox_mode.currentText()
|
||||
filter_ = (
|
||||
'avectorscope=s=%sx%s:draw=%s:m=%s:scale=%s:zoom=%s' % (
|
||||
self.settings.value("outputWidth"),
|
||||
self.settings.value("outputHeight"),
|
||||
'line'if self.draw else 'dot',
|
||||
m, amplitude, str(self.zoom),
|
||||
)
|
||||
)
|
||||
elif self.filterType == 3: # Musical Scale
|
||||
filter_ = (
|
||||
'showcqt=r=%s:s=%sx%s:count=30:text=0:tc=%s,'
|
||||
'colorkey=color=black:similarity=0.1:blend=0.5 ' % (
|
||||
self.settings.value("outputFrameRate"),
|
||||
self.settings.value("outputWidth"),
|
||||
self.settings.value("outputHeight"),
|
||||
str(self.tc),
|
||||
)
|
||||
)
|
||||
elif self.filterType == 4: # Phase
|
||||
filter_ = (
|
||||
'aphasemeter=r=%s:s=%sx%s:video=1 [atrash][vtmp1]; '
|
||||
'[atrash] anullsink; '
|
||||
'[vtmp1] colorkey=color=black:similarity=0.1:blend=0.5, '
|
||||
'crop=in_w/8:in_h:(in_w/8)*7:0 '% (
|
||||
self.settings.value("outputFrameRate"),
|
||||
self.settings.value("outputWidth"),
|
||||
self.settings.value("outputHeight"),
|
||||
)
|
||||
)
|
||||
|
||||
return [
|
||||
'-filter_complex',
|
||||
'%s%s%s%s [v1]; '
|
||||
'[v1] %sscale=%s:%s%s%s%s [v]' % (
|
||||
exampleSound() if preview and genericPreview else '[0:a] ',
|
||||
'compand=gain=4,' if self.compress else '',
|
||||
'aformat=channel_layouts=mono,' if self.mono else '',
|
||||
filter_,
|
||||
'hflip, ' if self.mirror else '',
|
||||
w, h,
|
||||
', hue=h=%s:s=10' % str(self.hue) if self.hue > 0 else '',
|
||||
', trim=start=%s:end=%s' % (
|
||||
"{0:.3f}".format(startPt + 12),
|
||||
"{0:.3f}".format(startPt + 12.5)
|
||||
) if preview else '',
|
||||
', convolution=-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2:-2 '
|
||||
'-1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2'
|
||||
if self.filterType == 3 else ''
|
||||
),
|
||||
'-map', '[v]',
|
||||
]
|
||||
|
||||
def updateChunksize(self):
|
||||
width, height = scale(self.scale, self.width, self.height, int)
|
||||
oldChunkSize = int(self.chunkSize)
|
||||
self.chunkSize = 4 * width * height
|
||||
changed = self.chunkSize != oldChunkSize
|
||||
return changed
|
||||
|
||||
def finalizeFrame(self, imageData):
|
||||
try:
|
||||
image = Image.frombytes(
|
||||
'RGBA',
|
||||
scale(self.scale, self.width, self.height, int),
|
||||
imageData
|
||||
)
|
||||
self._image = image
|
||||
except ValueError:
|
||||
image = self._image
|
||||
if self.scale != 100 \
|
||||
or self.x != 0 or self.y != 0:
|
||||
frame = BlankFrame(self.width, self.height)
|
||||
frame.paste(image, box=(self.x, self.y))
|
||||
else:
|
||||
frame = image
|
||||
return frame
|
|
@ -0,0 +1,946 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>586</width>
|
||||
<height>197</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>197</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Type</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_filterType">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Spectrum</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Histogram</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Vector Scope</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Musical Scale</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Phase</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_9">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_xTitleAlign">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>X</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_x">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-10000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_yTitleAlign">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Y</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_y">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-10000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_10">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_compress">
|
||||
<property name="text">
|
||||
<string>Compress</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_mono">
|
||||
<property name="text">
|
||||
<string>Mono</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_mirror">
|
||||
<property name="text">
|
||||
<string>Mirror</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_11">
|
||||
<property name="text">
|
||||
<string>Hue</string>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_hue">
|
||||
<property name="suffix">
|
||||
<string>° </string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>359</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Scale</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_scale">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::UpDownArrows</enum>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string>%</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>400</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>100</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="stackedWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="autoFillBackground">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Plain</enum>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="page">
|
||||
<widget class="QWidget" name="verticalLayoutWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>561</width>
|
||||
<height>66</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMaximumSize</enum>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_9">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_textColor">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>31</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Window</string>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_window">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>hann</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>gauss</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>tukey</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>dolph</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>cauchy</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>parzen</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>poisson</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>rect</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>bartlett</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>hanning</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>hamming</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>blackman</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>welch</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>flattop</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>bharris</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>bnuttall</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>lanczos</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Amplitude</string>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_amplitude0">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Square root</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Cubic root</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>4thrt</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>5thrt</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Linear</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Logarithmic</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::MinimumExpanding</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Color </string>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_color">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Channel</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Intensity</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Rainbow</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Moreland</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Nebulae</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Fire</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Fiery</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Fruit</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Cool</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::MinimumExpanding</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_2">
|
||||
<widget class="QWidget" name="verticalLayoutWidget_2">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
<width>561</width>
|
||||
<height>31</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Display Scale</string>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_display">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Logarithmic</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Square root</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Cubic root</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Linear</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Reverse Log</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Amplitude</string>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_amplitude1">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Logarithmic</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Linear</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Minimum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_3">
|
||||
<widget class="QWidget" name="verticalLayoutWidget_3">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
<width>585</width>
|
||||
<height>64</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>Mode</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_mode">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>lissajous</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>lissajous_xy</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>polar</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Amplitude</string>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_amplitude2">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Linear</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Square root</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Cubic root</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Logarithmic</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Zoom</string>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_zoom">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_draw">
|
||||
<property name="text">
|
||||
<string>Line</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_6">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_4">
|
||||
<widget class="QWidget" name="verticalLayoutWidget_4">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>561</width>
|
||||
<height>31</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_10">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Timeclamp</string>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="spinBox_tc">
|
||||
<property name="suffix">
|
||||
<string>s</string>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.002000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.010000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>0.017000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_7">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_5">
|
||||
<widget class="QWidget" name="verticalLayoutWidget_5">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>551</width>
|
||||
<height>31</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_11"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -0,0 +1,137 @@
|
|||
from PIL import Image, ImageDraw
|
||||
from PyQt5.QtGui import QColor, QFont
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
import os
|
||||
|
||||
from component import Component
|
||||
from toolkit.frame import FramePainter
|
||||
|
||||
|
||||
class Component(Component):
|
||||
name = 'Title Text'
|
||||
version = '1.0.1'
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
self.titleFont = QFont()
|
||||
|
||||
def widget(self, *args):
|
||||
super().widget(*args)
|
||||
self.textColor = (255, 255, 255)
|
||||
self.title = 'Text'
|
||||
self.alignment = 1
|
||||
self.fontSize = self.height / 13.5
|
||||
|
||||
self.page.comboBox_textAlign.addItem("Left")
|
||||
self.page.comboBox_textAlign.addItem("Middle")
|
||||
self.page.comboBox_textAlign.addItem("Right")
|
||||
self.page.comboBox_textAlign.setCurrentIndex(int(self.alignment))
|
||||
|
||||
self.page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor)
|
||||
self.page.spinBox_fontSize.setValue(int(self.fontSize))
|
||||
self.page.lineEdit_title.setText(self.title)
|
||||
|
||||
self.page.pushButton_center.clicked.connect(self.centerXY)
|
||||
self.page.fontComboBox_titleFont.currentFontChanged.connect(
|
||||
self.update
|
||||
)
|
||||
|
||||
self.trackWidgets({
|
||||
'textColor': self.page.lineEdit_textColor,
|
||||
'title': self.page.lineEdit_title,
|
||||
'alignment': self.page.comboBox_textAlign,
|
||||
'fontSize': self.page.spinBox_fontSize,
|
||||
'xPosition': self.page.spinBox_xTextAlign,
|
||||
'yPosition': self.page.spinBox_yTextAlign,
|
||||
}, colorWidgets={
|
||||
'textColor': self.page.pushButton_textColor,
|
||||
}, relativeWidgets=[
|
||||
'xPosition', 'yPosition', 'fontSize',
|
||||
])
|
||||
self.centerXY()
|
||||
|
||||
def update(self):
|
||||
self.titleFont = self.page.fontComboBox_titleFont.currentFont()
|
||||
super().update()
|
||||
|
||||
def centerXY(self):
|
||||
self.setRelativeWidget('xPosition', 0.5)
|
||||
self.setRelativeWidget('yPosition', 0.5)
|
||||
|
||||
def getXY(self):
|
||||
'''Returns true x, y after considering alignment settings'''
|
||||
fm = QtGui.QFontMetrics(self.titleFont)
|
||||
if self.alignment == 0: # Left
|
||||
x = int(self.xPosition)
|
||||
|
||||
if self.alignment == 1: # Middle
|
||||
offset = int(fm.width(self.title)/2)
|
||||
x = self.xPosition - offset
|
||||
|
||||
if self.alignment == 2: # Right
|
||||
offset = fm.width(self.title)
|
||||
x = self.xPosition - offset
|
||||
return x, self.yPosition
|
||||
|
||||
def loadPreset(self, pr, *args):
|
||||
super().loadPreset(pr, *args)
|
||||
|
||||
font = QFont()
|
||||
font.fromString(pr['titleFont'])
|
||||
self.page.fontComboBox_titleFont.setCurrentFont(font)
|
||||
|
||||
def savePreset(self):
|
||||
saveValueStore = super().savePreset()
|
||||
saveValueStore['titleFont'] = self.titleFont.toString()
|
||||
return saveValueStore
|
||||
|
||||
def previewRender(self):
|
||||
return self.addText(self.width, self.height)
|
||||
|
||||
def properties(self):
|
||||
props = ['static']
|
||||
if not self.title:
|
||||
props.append('error')
|
||||
return props
|
||||
|
||||
def error(self):
|
||||
return "No text provided."
|
||||
|
||||
def frameRender(self, frameNo):
|
||||
return self.addText(self.width, self.height)
|
||||
|
||||
def addText(self, width, height):
|
||||
image = FramePainter(width, height)
|
||||
self.titleFont.setPixelSize(self.fontSize)
|
||||
image.setFont(self.titleFont)
|
||||
image.setPen(self.textColor)
|
||||
x, y = self.getXY()
|
||||
image.drawText(x, y, self.title)
|
||||
|
||||
return image.finalize()
|
||||
|
||||
def commandHelp(self):
|
||||
print('Enter a string to use as centred white text:')
|
||||
print(' "title=User Error"')
|
||||
print('Specify a text color:\n color=255,255,255')
|
||||
print('Set custom x, y position:\n x=500 y=500')
|
||||
|
||||
def command(self, arg):
|
||||
if '=' in arg:
|
||||
key, arg = arg.split('=', 1)
|
||||
if key == 'color':
|
||||
self.page.lineEdit_textColor.setText(arg)
|
||||
return
|
||||
elif key == 'size':
|
||||
self.page.spinBox_fontSize.setValue(int(arg))
|
||||
return
|
||||
elif key == 'x':
|
||||
self.page.spinBox_xTextAlign.setValue(int(arg))
|
||||
return
|
||||
elif key == 'y':
|
||||
self.page.spinBox_yTextAlign.setValue(int(arg))
|
||||
return
|
||||
elif key == 'title':
|
||||
self.page.lineEdit_title.setText(arg)
|
||||
return
|
||||
super().command(arg)
|
|
@ -0,0 +1,343 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>586</width>
|
||||
<height>197</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_title">
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_title">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Testing New GUI</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Font</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFontComboBox" name="fontComboBox_titleFont">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_8">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_fontSize">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Font Size</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_fontSize">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>500</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_12">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_textColor">
|
||||
<property name="text">
|
||||
<string>Text Color</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_textColor"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_textColor">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="MaximumSize" stdset="0">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_textLayout">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Text Layout</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_textAlign"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_center">
|
||||
<property name="text">
|
||||
<string>Center</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_6">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_xTitleAlign">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>X</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_xTextAlign">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>999999999</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_7">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_yTitleAlign">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Y</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_yTextAlign">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>999999999</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -0,0 +1,208 @@
|
|||
from PIL import Image
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
import os
|
||||
import math
|
||||
import subprocess
|
||||
|
||||
from component import Component
|
||||
from toolkit.frame import BlankFrame, scale
|
||||
from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo
|
||||
from toolkit import checkOutput
|
||||
|
||||
|
||||
class Component(Component):
|
||||
name = 'Video'
|
||||
version = '1.0.0'
|
||||
|
||||
def widget(self, *args):
|
||||
self.videoPath = ''
|
||||
self.badAudio = False
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.loopVideo = False
|
||||
super().widget(*args)
|
||||
self._image = BlankFrame(self.width, self.height)
|
||||
self.page.pushButton_video.clicked.connect(self.pickVideo)
|
||||
self.trackWidgets({
|
||||
'videoPath': self.page.lineEdit_video,
|
||||
'loopVideo': self.page.checkBox_loop,
|
||||
'useAudio': self.page.checkBox_useAudio,
|
||||
'distort': self.page.checkBox_distort,
|
||||
'scale': self.page.spinBox_scale,
|
||||
'volume': self.page.spinBox_volume,
|
||||
'xPosition': self.page.spinBox_x,
|
||||
'yPosition': self.page.spinBox_y,
|
||||
}, presetNames={
|
||||
'videoPath': 'video',
|
||||
'loopVideo': 'loop',
|
||||
'xPosition': 'x',
|
||||
'yPosition': 'y',
|
||||
}, relativeWidgets=[
|
||||
'xPosition', 'yPosition',
|
||||
])
|
||||
|
||||
def update(self):
|
||||
if self.page.checkBox_useAudio.isChecked():
|
||||
self.page.label_volume.setEnabled(True)
|
||||
self.page.spinBox_volume.setEnabled(True)
|
||||
else:
|
||||
self.page.label_volume.setEnabled(False)
|
||||
self.page.spinBox_volume.setEnabled(False)
|
||||
super().update()
|
||||
|
||||
def previewRender(self):
|
||||
self.updateChunksize()
|
||||
frame = self.getPreviewFrame(self.width, self.height)
|
||||
if not frame:
|
||||
return BlankFrame(self.width, self.height)
|
||||
else:
|
||||
return frame
|
||||
|
||||
def properties(self):
|
||||
props = []
|
||||
if hasattr(self.parent, 'window'):
|
||||
outputFile = self.parent.window.lineEdit_outputFile.text()
|
||||
else:
|
||||
outputFile = str(self.parent.args.output)
|
||||
|
||||
if not self.videoPath:
|
||||
self.lockError("There is no video selected.")
|
||||
elif not os.path.exists(self.videoPath):
|
||||
self.lockError("The video selected does not exist!")
|
||||
elif os.path.realpath(self.videoPath) == os.path.realpath(outputFile):
|
||||
self.lockError("Input and output paths match.")
|
||||
|
||||
if self.useAudio:
|
||||
props.append('audio')
|
||||
if not testAudioStream(self.videoPath) \
|
||||
and self.error() is None:
|
||||
self.lockError(
|
||||
"Could not identify an audio stream in this video.")
|
||||
|
||||
return props
|
||||
|
||||
def audio(self):
|
||||
params = {}
|
||||
if self.volume != 1.0:
|
||||
params['volume'] = '=%s:replaygain_noclip=0' % str(self.volume)
|
||||
return (self.videoPath, params)
|
||||
|
||||
def preFrameRender(self, **kwargs):
|
||||
super().preFrameRender(**kwargs)
|
||||
self.updateChunksize()
|
||||
self.video = FfmpegVideo(
|
||||
inputPath=self.videoPath, filter_=self.makeFfmpegFilter(),
|
||||
width=self.width, height=self.height, chunkSize=self.chunkSize,
|
||||
frameRate=int(self.settings.value("outputFrameRate")),
|
||||
parent=self.parent, loopVideo=self.loopVideo,
|
||||
component=self
|
||||
) if os.path.exists(self.videoPath) else None
|
||||
|
||||
def frameRender(self, frameNo):
|
||||
if FfmpegVideo.threadError is not None:
|
||||
raise FfmpegVideo.threadError
|
||||
return self.finalizeFrame(self.video.frame(frameNo))
|
||||
|
||||
def postFrameRender(self):
|
||||
closePipe(self.video.pipe)
|
||||
|
||||
def pickVideo(self):
|
||||
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
|
||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self.page, "Choose Video",
|
||||
imgDir, "Video Files (%s)" % " ".join(self.core.videoFormats)
|
||||
)
|
||||
if filename:
|
||||
self.settings.setValue("componentDir", os.path.dirname(filename))
|
||||
self.page.lineEdit_video.setText(filename)
|
||||
self.update()
|
||||
|
||||
def getPreviewFrame(self, width, height):
|
||||
if not self.videoPath or not os.path.exists(self.videoPath):
|
||||
return
|
||||
|
||||
command = [
|
||||
self.core.FFMPEG_BIN,
|
||||
'-thread_queue_size', '512',
|
||||
'-i', self.videoPath,
|
||||
'-f', 'image2pipe',
|
||||
'-pix_fmt', 'rgba',
|
||||
]
|
||||
command.extend(self.makeFfmpegFilter())
|
||||
command.extend([
|
||||
'-codec:v', 'rawvideo', '-',
|
||||
'-ss', '90',
|
||||
'-frames:v', '1',
|
||||
])
|
||||
pipe = openPipe(
|
||||
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, bufsize=10**8
|
||||
)
|
||||
byteFrame = pipe.stdout.read(self.chunkSize)
|
||||
closePipe(pipe)
|
||||
|
||||
frame = self.finalizeFrame(byteFrame)
|
||||
return frame
|
||||
|
||||
def makeFfmpegFilter(self):
|
||||
return [
|
||||
'-filter_complex',
|
||||
'[0:v] scale=%s:%s' % scale(
|
||||
self.scale, self.width, self.height, str),
|
||||
]
|
||||
|
||||
def updateChunksize(self):
|
||||
if self.scale != 100 and not self.distort:
|
||||
width, height = scale(self.scale, self.width, self.height, int)
|
||||
else:
|
||||
width, height = self.width, self.height
|
||||
self.chunkSize = 4 * width * height
|
||||
|
||||
def command(self, arg):
|
||||
if '=' in arg:
|
||||
key, arg = arg.split('=', 1)
|
||||
if key == 'path' and os.path.exists(arg):
|
||||
if '*%s' % os.path.splitext(arg)[1] in self.core.videoFormats:
|
||||
self.page.lineEdit_video.setText(arg)
|
||||
self.page.spinBox_scale.setValue(100)
|
||||
self.page.checkBox_loop.setChecked(True)
|
||||
return
|
||||
else:
|
||||
print("Not a supported video format")
|
||||
quit(1)
|
||||
elif arg == 'audio':
|
||||
if not self.page.lineEdit_video.text():
|
||||
print("'audio' option must follow a video selection")
|
||||
quit(1)
|
||||
self.page.checkBox_useAudio.setChecked(True)
|
||||
return
|
||||
super().command(arg)
|
||||
|
||||
def commandHelp(self):
|
||||
print('Load a video:\n path=/filepath/to/video.mp4')
|
||||
print('Using audio:\n path=/filepath/to/video.mp4 audio')
|
||||
|
||||
def finalizeFrame(self, imageData):
|
||||
try:
|
||||
if self.distort:
|
||||
image = Image.frombytes(
|
||||
'RGBA',
|
||||
(self.width, self.height),
|
||||
imageData)
|
||||
else:
|
||||
image = Image.frombytes(
|
||||
'RGBA',
|
||||
scale(self.scale, self.width, self.height, int),
|
||||
imageData)
|
||||
self._image = image
|
||||
except ValueError:
|
||||
# use last good frame
|
||||
image = self._image
|
||||
|
||||
if self.scale != 100 \
|
||||
or self.xPosition != 0 or self.yPosition != 0:
|
||||
frame = BlankFrame(self.width, self.height)
|
||||
frame.paste(image, box=(self.xPosition, self.yPosition))
|
||||
else:
|
||||
frame = image
|
||||
return frame
|
|
@ -0,0 +1,328 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>586</width>
|
||||
<height>197</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>197</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_textColor">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>31</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Video</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_video">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>1</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_video">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>1</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="MaximumSize" stdset="0">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_9">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_xTitleAlign">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>X</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_x">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-10000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_yTitleAlign">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Y</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_y">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-10000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_9">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_loop">
|
||||
<property name="text">
|
||||
<string>Loop</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_distort">
|
||||
<property name="text">
|
||||
<string>Distort by scale</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Scale</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_scale">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::UpDownArrows</enum>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string>%</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>400</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>100</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_10">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_useAudio">
|
||||
<property name="text">
|
||||
<string>Use Audio</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_volume">
|
||||
<property name="text">
|
||||
<string>Volume</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="spinBox_volume">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string>x</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>10.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -0,0 +1,194 @@
|
|||
from PIL import Image
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
from PyQt5.QtGui import QColor
|
||||
import os
|
||||
import math
|
||||
import subprocess
|
||||
|
||||
from component import Component
|
||||
from toolkit.frame import BlankFrame, scale
|
||||
from toolkit import checkOutput
|
||||
from toolkit.ffmpeg import (
|
||||
openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound
|
||||
)
|
||||
|
||||
|
||||
class Component(Component):
|
||||
name = 'Waveform'
|
||||
version = '1.0.0'
|
||||
|
||||
def widget(self, *args):
|
||||
super().widget(*args)
|
||||
self._image = BlankFrame(self.width, self.height)
|
||||
|
||||
self.page.lineEdit_color.setText('255,255,255')
|
||||
|
||||
if hasattr(self.parent, 'window'):
|
||||
self.parent.window.lineEdit_audioFile.textChanged.connect(
|
||||
self.update
|
||||
)
|
||||
|
||||
self.trackWidgets({
|
||||
'color': self.page.lineEdit_color,
|
||||
'mode': self.page.comboBox_mode,
|
||||
'amplitude': self.page.comboBox_amplitude,
|
||||
'x': self.page.spinBox_x,
|
||||
'y': self.page.spinBox_y,
|
||||
'mirror': self.page.checkBox_mirror,
|
||||
'scale': self.page.spinBox_scale,
|
||||
'opacity': self.page.spinBox_opacity,
|
||||
'compress': self.page.checkBox_compress,
|
||||
'mono': self.page.checkBox_mono,
|
||||
}, colorWidgets={
|
||||
'color': self.page.pushButton_color,
|
||||
}, relativeWidgets=[
|
||||
'x', 'y',
|
||||
])
|
||||
|
||||
def previewRender(self):
|
||||
self.updateChunksize()
|
||||
frame = self.getPreviewFrame(self.width, self.height)
|
||||
if not frame:
|
||||
return BlankFrame(self.width, self.height)
|
||||
else:
|
||||
return frame
|
||||
|
||||
def preFrameRender(self, **kwargs):
|
||||
super().preFrameRender(**kwargs)
|
||||
self.updateChunksize()
|
||||
w, h = scale(self.scale, self.width, self.height, str)
|
||||
self.video = FfmpegVideo(
|
||||
inputPath=self.audioFile,
|
||||
filter_=self.makeFfmpegFilter(),
|
||||
width=w, height=h,
|
||||
chunkSize=self.chunkSize,
|
||||
frameRate=int(self.settings.value("outputFrameRate")),
|
||||
parent=self.parent, component=self, debug=True,
|
||||
)
|
||||
|
||||
def frameRender(self, frameNo):
|
||||
if FfmpegVideo.threadError is not None:
|
||||
raise FfmpegVideo.threadError
|
||||
return self.finalizeFrame(self.video.frame(frameNo))
|
||||
|
||||
def postFrameRender(self):
|
||||
closePipe(self.video.pipe)
|
||||
|
||||
def getPreviewFrame(self, width, height):
|
||||
genericPreview = self.settings.value("pref_genericPreview")
|
||||
startPt = 0
|
||||
if not genericPreview:
|
||||
inputFile = self.parent.window.lineEdit_audioFile.text()
|
||||
if not inputFile or not os.path.exists(inputFile):
|
||||
return
|
||||
duration = getAudioDuration(inputFile)
|
||||
if not duration:
|
||||
return
|
||||
startPt = duration / 3
|
||||
if startPt + 3 > duration:
|
||||
startPt += startPt - 3
|
||||
|
||||
command = [
|
||||
self.core.FFMPEG_BIN,
|
||||
'-thread_queue_size', '512',
|
||||
'-r', self.settings.value("outputFrameRate"),
|
||||
'-ss', "{0:.3f}".format(startPt),
|
||||
'-i',
|
||||
os.path.join(self.core.wd, 'background.png')
|
||||
if genericPreview else inputFile,
|
||||
'-f', 'image2pipe',
|
||||
'-pix_fmt', 'rgba',
|
||||
]
|
||||
command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt))
|
||||
command.extend([
|
||||
'-an',
|
||||
'-s:v', '%sx%s' % scale(self.scale, self.width, self.height, str),
|
||||
'-codec:v', 'rawvideo', '-',
|
||||
'-frames:v', '1',
|
||||
])
|
||||
pipe = openPipe(
|
||||
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, bufsize=10**8
|
||||
)
|
||||
byteFrame = pipe.stdout.read(self.chunkSize)
|
||||
closePipe(pipe)
|
||||
|
||||
frame = self.finalizeFrame(byteFrame)
|
||||
return frame
|
||||
|
||||
def makeFfmpegFilter(self, preview=False, startPt=0):
|
||||
w, h = scale(self.scale, self.width, self.height, str)
|
||||
if self.amplitude == 0:
|
||||
amplitude = 'lin'
|
||||
elif self.amplitude == 1:
|
||||
amplitude = 'log'
|
||||
elif self.amplitude == 2:
|
||||
amplitude = 'sqrt'
|
||||
elif self.amplitude == 3:
|
||||
amplitude = 'cbrt'
|
||||
hexcolor = QColor(*self.color).name()
|
||||
opacity = "{0:.1f}".format(self.opacity / 100)
|
||||
genericPreview = self.settings.value("pref_genericPreview")
|
||||
if self.mode < 3:
|
||||
filter_ = 'showwaves=r=%s:s=%sx%s:mode=%s:colors=%s@%s:scale=%s' % (
|
||||
self.settings.value("outputFrameRate"),
|
||||
self.settings.value("outputWidth"),
|
||||
self.settings.value("outputHeight"),
|
||||
self.page.comboBox_mode.currentText().lower()
|
||||
if self.mode != 3 else 'p2p',
|
||||
hexcolor, opacity, amplitude,
|
||||
)
|
||||
elif self.mode > 2:
|
||||
filter_ = (
|
||||
'showfreqs=s=%sx%s:mode=%s:colors=%s@%s'
|
||||
':ascale=%s:fscale=%s' % (
|
||||
self.settings.value("outputWidth"),
|
||||
self.settings.value("outputHeight"),
|
||||
'line' if self.mode == 4 else 'bar',
|
||||
hexcolor, opacity, amplitude,
|
||||
'log' if self.mono else 'lin'
|
||||
)
|
||||
)
|
||||
|
||||
return [
|
||||
'-filter_complex',
|
||||
'%s%s%s'
|
||||
'%s%s%s [v1]; '
|
||||
'[v1] scale=%s:%s%s [v]' % (
|
||||
exampleSound() if preview and genericPreview else '[0:a] ',
|
||||
'compand=gain=4,' if self.compress else '',
|
||||
'aformat=channel_layouts=mono,'
|
||||
if self.mono and self.mode < 3 else '',
|
||||
filter_,
|
||||
', drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=4:color=%s@%s' % (
|
||||
hexcolor, opacity
|
||||
) if self.mode < 2 else '',
|
||||
', hflip' if self.mirror else'',
|
||||
w, h,
|
||||
', trim=duration=%s' % "{0:.3f}".format(startPt + 3)
|
||||
if preview else '',
|
||||
),
|
||||
'-map', '[v]',
|
||||
]
|
||||
|
||||
def updateChunksize(self):
|
||||
width, height = scale(self.scale, self.width, self.height, int)
|
||||
self.chunkSize = 4 * width * height
|
||||
|
||||
def finalizeFrame(self, imageData):
|
||||
try:
|
||||
image = Image.frombytes(
|
||||
'RGBA',
|
||||
scale(self.scale, self.width, self.height, int),
|
||||
imageData
|
||||
)
|
||||
self._image = image
|
||||
except ValueError:
|
||||
image = self._image
|
||||
if self.scale != 100 \
|
||||
or self.x != 0 or self.y != 0:
|
||||
frame = BlankFrame(self.width, self.height)
|
||||
frame.paste(image, box=(self.x, self.y))
|
||||
else:
|
||||
frame = image
|
||||
return frame
|
|
@ -0,0 +1,383 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>586</width>
|
||||
<height>197</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>197</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_textColor">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>31</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Mode</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_mode">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Cline</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Line</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Point</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Frequency Bar</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Frequency Line</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_9">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_xTitleAlign">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>X</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_x">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-10000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_yTitleAlign">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Y</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_y">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-10000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_9">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Color</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_color">
|
||||
<property name="inputMethodHints">
|
||||
<set>Qt::ImhNone</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_color">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="flat">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Opacity</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_opacity">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::UpDownArrows</enum>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string>%</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>100</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Scale</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_scale">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::UpDownArrows</enum>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string>%</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>400</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>100</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_10">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_compress">
|
||||
<property name="text">
|
||||
<string>Compress</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_mono">
|
||||
<property name="text">
|
||||
<string>Mono</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_mirror">
|
||||
<property name="text">
|
||||
<string>Mirror</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Amplitude</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_amplitude">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Linear</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Logarithmic</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Square root</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Cubic root</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -0,0 +1,527 @@
|
|||
'''
|
||||
Home to the Core class which tracks program state. Used by GUI & commandline
|
||||
to create a list of components and create a video thread to export.
|
||||
'''
|
||||
from PyQt5 import QtCore, QtGui, uic
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from importlib import import_module
|
||||
|
||||
import toolkit
|
||||
import video_thread
|
||||
|
||||
|
||||
class Core:
|
||||
'''
|
||||
MainWindow and Command module both use an instance of this class
|
||||
to store the core program state. This object tracks the components,
|
||||
talks to the components, handles opening/creating project files
|
||||
and presets, and creates the video thread to export.
|
||||
This class also stores constants as class variables.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
self.importComponents()
|
||||
self.selectedComponents = []
|
||||
self.savedPresets = {} # copies of presets to detect modification
|
||||
self.openingProject = False
|
||||
|
||||
def importComponents(self):
|
||||
def findComponents():
|
||||
for f in os.listdir(Core.componentsPath):
|
||||
name, ext = os.path.splitext(f)
|
||||
if name.startswith("__"):
|
||||
continue
|
||||
elif ext == '.py':
|
||||
yield name
|
||||
self.modules = [
|
||||
import_module('components.%s' % name)
|
||||
for name in findComponents()
|
||||
]
|
||||
# store canonical module names and indexes
|
||||
self.moduleIndexes = [i for i in range(len(self.modules))]
|
||||
self.compNames = [mod.Component.name for mod in self.modules]
|
||||
# alphabetize modules by Component name
|
||||
sortedModules = sorted(zip(self.compNames, self.modules))
|
||||
self.compNames = [y[0] for y in sortedModules]
|
||||
self.modules = [y[1] for y in sortedModules]
|
||||
|
||||
# store alternative names for modules
|
||||
self.altCompNames = []
|
||||
for i, mod in enumerate(self.modules):
|
||||
if hasattr(mod.Component, 'names'):
|
||||
for name in mod.Component.names():
|
||||
self.altCompNames.append((name, i))
|
||||
|
||||
def componentListChanged(self):
|
||||
for i, component in enumerate(self.selectedComponents):
|
||||
component.compPos = i
|
||||
|
||||
def insertComponent(self, compPos, moduleIndex, loader):
|
||||
'''
|
||||
Creates a new component using these args:
|
||||
(compPos, moduleIndex in self.modules, MWindow/Command/Core obj)
|
||||
'''
|
||||
if compPos < 0 or compPos > len(self.selectedComponents):
|
||||
compPos = len(self.selectedComponents)
|
||||
if len(self.selectedComponents) > 50:
|
||||
return None
|
||||
|
||||
component = self.modules[moduleIndex].Component(
|
||||
moduleIndex, compPos, self
|
||||
)
|
||||
self.selectedComponents.insert(
|
||||
compPos,
|
||||
component
|
||||
)
|
||||
self.componentListChanged()
|
||||
self.selectedComponents[compPos]._error.connect(
|
||||
loader.videoThreadError
|
||||
)
|
||||
|
||||
# init component's widget for loading/saving presets
|
||||
self.selectedComponents[compPos].widget(loader)
|
||||
self.updateComponent(compPos)
|
||||
|
||||
if hasattr(loader, 'insertComponent'):
|
||||
loader.insertComponent(compPos)
|
||||
return compPos
|
||||
|
||||
def moveComponent(self, startI, endI):
|
||||
comp = self.selectedComponents.pop(startI)
|
||||
self.selectedComponents.insert(endI, comp)
|
||||
|
||||
self.componentListChanged()
|
||||
return endI
|
||||
|
||||
def removeComponent(self, i):
|
||||
self.selectedComponents.pop(i)
|
||||
self.componentListChanged()
|
||||
|
||||
def clearComponents(self):
|
||||
self.selectedComponents = list()
|
||||
self.componentListChanged()
|
||||
|
||||
def updateComponent(self, i):
|
||||
# print('updating %s' % self.selectedComponents[i])
|
||||
self.selectedComponents[i].update()
|
||||
|
||||
def moduleIndexFor(self, compName):
|
||||
try:
|
||||
index = self.compNames.index(compName)
|
||||
return self.moduleIndexes[index]
|
||||
except ValueError:
|
||||
for altName, modI in self.altCompNames:
|
||||
if altName == compName:
|
||||
return self.moduleIndexes[modI]
|
||||
|
||||
def clearPreset(self, compIndex):
|
||||
self.selectedComponents[compIndex].currentPreset = None
|
||||
|
||||
def openPreset(self, filepath, compIndex, presetName):
|
||||
'''Applies a preset to a specific component'''
|
||||
saveValueStore = self.getPreset(filepath)
|
||||
if not saveValueStore:
|
||||
return False
|
||||
try:
|
||||
self.selectedComponents[compIndex].loadPreset(
|
||||
saveValueStore,
|
||||
presetName
|
||||
)
|
||||
except KeyError as e:
|
||||
print('preset missing value: %s' % e)
|
||||
|
||||
self.savedPresets[presetName] = dict(saveValueStore)
|
||||
return True
|
||||
|
||||
def getPreset(self, filepath):
|
||||
'''Returns the preset dict stored at this filepath'''
|
||||
if not os.path.exists(filepath):
|
||||
return False
|
||||
with open(filepath, 'r') as f:
|
||||
for line in f:
|
||||
saveValueStore = toolkit.presetFromString(line.strip())
|
||||
break
|
||||
return saveValueStore
|
||||
|
||||
def openProject(self, loader, filepath):
|
||||
''' loader is the object calling this method which must have
|
||||
its own showMessage(**kwargs) method for displaying errors.
|
||||
'''
|
||||
if not os.path.exists(filepath):
|
||||
loader.showMessage(msg='Project file not found.')
|
||||
return
|
||||
|
||||
errcode, data = self.parseAvFile(filepath)
|
||||
if errcode == 0:
|
||||
self.openingProject = True
|
||||
try:
|
||||
if hasattr(loader, 'window'):
|
||||
for widget, value in data['WindowFields']:
|
||||
widget = eval('loader.window.%s' % widget)
|
||||
widget.blockSignals(True)
|
||||
toolkit.setWidgetValue(widget, value)
|
||||
widget.blockSignals(False)
|
||||
|
||||
for key, value in data['Settings']:
|
||||
Core.settings.setValue(key, value)
|
||||
|
||||
for tup in data['Components']:
|
||||
name, vers, preset = tup
|
||||
clearThis = False
|
||||
modified = False
|
||||
|
||||
# add loaded named presets to savedPresets dict
|
||||
if 'preset' in preset and preset['preset'] is not None:
|
||||
nam = preset['preset']
|
||||
filepath2 = os.path.join(
|
||||
Core.presetDir, name, str(vers), nam)
|
||||
origSaveValueStore = self.getPreset(filepath2)
|
||||
if origSaveValueStore:
|
||||
self.savedPresets[nam] = dict(origSaveValueStore)
|
||||
modified = not origSaveValueStore == preset
|
||||
else:
|
||||
# saved preset was renamed or deleted
|
||||
clearThis = True
|
||||
|
||||
# create the actual component object & get its index
|
||||
i = self.insertComponent(
|
||||
-1,
|
||||
self.moduleIndexFor(name),
|
||||
loader
|
||||
)
|
||||
if i is None:
|
||||
loader.showMessage(msg="Too many components!")
|
||||
break
|
||||
|
||||
try:
|
||||
if 'preset' in preset and preset['preset'] is not None:
|
||||
self.selectedComponents[i].loadPreset(
|
||||
preset
|
||||
)
|
||||
else:
|
||||
self.selectedComponents[i].loadPreset(
|
||||
preset,
|
||||
preset['preset']
|
||||
)
|
||||
except KeyError as e:
|
||||
print('%s missing value: %s' % (
|
||||
self.selectedComponents[i], e)
|
||||
)
|
||||
|
||||
if clearThis:
|
||||
self.clearPreset(i)
|
||||
if hasattr(loader, 'updateComponentTitle'):
|
||||
loader.updateComponentTitle(i, modified)
|
||||
self.openingProject = False
|
||||
return True
|
||||
except Exception:
|
||||
errcode = 1
|
||||
data = sys.exc_info()
|
||||
|
||||
if errcode == 1:
|
||||
typ, value, tb = data
|
||||
if typ.__name__ == 'KeyError':
|
||||
# probably just an old version, still loadable
|
||||
print('file missing value: %s' % value)
|
||||
return
|
||||
if hasattr(loader, 'createNewProject'):
|
||||
loader.createNewProject(prompt=False)
|
||||
msg = '%s: %s\n\n' % (typ.__name__, value)
|
||||
msg += toolkit.formatTraceback(tb)
|
||||
loader.showMessage(
|
||||
msg="Project file '%s' is corrupted." % filepath,
|
||||
showCancel=False,
|
||||
icon='Warning',
|
||||
detail=msg)
|
||||
self.openingProject = False
|
||||
return False
|
||||
|
||||
def parseAvFile(self, filepath):
|
||||
'''
|
||||
Parses an avp (project) or avl (preset package) file.
|
||||
Returns dictionary with section names as the keys, each one
|
||||
contains a list of tuples: (compName, version, compPresetDict)
|
||||
'''
|
||||
validSections = (
|
||||
'Components',
|
||||
'Settings',
|
||||
'WindowFields'
|
||||
)
|
||||
data = {sect: [] for sect in validSections}
|
||||
try:
|
||||
with open(filepath, 'r') as f:
|
||||
def parseLine(line):
|
||||
'''Decides if a file line is a section header'''
|
||||
line = line.strip()
|
||||
newSection = ''
|
||||
|
||||
if line.startswith('[') and line.endswith(']') \
|
||||
and line[1:-1] in validSections:
|
||||
newSection = line[1:-1]
|
||||
|
||||
return line, newSection
|
||||
|
||||
section = ''
|
||||
i = 0
|
||||
for line in f:
|
||||
line, newSection = parseLine(line)
|
||||
if newSection:
|
||||
section = str(newSection)
|
||||
continue
|
||||
if line and section == 'Components':
|
||||
if i == 0:
|
||||
lastCompName = str(line)
|
||||
i += 1
|
||||
elif i == 1:
|
||||
lastCompVers = str(line)
|
||||
i += 1
|
||||
elif i == 2:
|
||||
lastCompPreset = toolkit.presetFromString(line)
|
||||
data[section].append((
|
||||
lastCompName,
|
||||
lastCompVers,
|
||||
lastCompPreset
|
||||
))
|
||||
i = 0
|
||||
elif line and section:
|
||||
key, value = line.split('=', 1)
|
||||
data[section].append((key, value.strip()))
|
||||
|
||||
return 0, data
|
||||
except Exception:
|
||||
return 1, sys.exc_info()
|
||||
|
||||
def importPreset(self, filepath):
|
||||
errcode, data = self.parseAvFile(filepath)
|
||||
returnList = []
|
||||
if errcode == 0:
|
||||
name, vers, preset = data['Components'][0]
|
||||
presetName = preset['preset'] \
|
||||
if preset['preset'] else os.path.basename(filepath)[:-4]
|
||||
newPath = os.path.join(
|
||||
Core.presetDir,
|
||||
name,
|
||||
vers,
|
||||
presetName
|
||||
)
|
||||
if os.path.exists(newPath):
|
||||
return False, newPath
|
||||
preset['preset'] = presetName
|
||||
self.createPresetFile(
|
||||
name, vers, presetName, preset
|
||||
)
|
||||
return True, presetName
|
||||
elif errcode == 1:
|
||||
# TODO: an error message
|
||||
return False, ''
|
||||
|
||||
def exportPreset(self, exportPath, compName, vers, origName):
|
||||
internalPath = os.path.join(
|
||||
Core.presetDir, compName, str(vers), origName
|
||||
)
|
||||
if not os.path.exists(internalPath):
|
||||
return
|
||||
if os.path.exists(exportPath):
|
||||
os.remove(exportPath)
|
||||
with open(internalPath, 'r') as f:
|
||||
internalData = [line for line in f]
|
||||
try:
|
||||
saveValueStore = toolkit.presetFromString(internalData[0].strip())
|
||||
self.createPresetFile(
|
||||
compName, vers,
|
||||
origName, saveValueStore,
|
||||
exportPath
|
||||
)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def createPresetFile(
|
||||
self, compName, vers, presetName, saveValueStore, filepath=''):
|
||||
'''Create a preset file (.avl) at filepath using args.
|
||||
Or if filepath is empty, create an internal preset using args'''
|
||||
if not filepath:
|
||||
dirname = os.path.join(Core.presetDir, compName, str(vers))
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
filepath = os.path.join(dirname, presetName)
|
||||
internal = True
|
||||
else:
|
||||
if not filepath.endswith('.avl'):
|
||||
filepath += '.avl'
|
||||
internal = False
|
||||
|
||||
with open(filepath, 'w') as f:
|
||||
if not internal:
|
||||
f.write('[Components]\n')
|
||||
f.write('%s\n' % compName)
|
||||
f.write('%s\n' % str(vers))
|
||||
f.write(toolkit.presetToString(saveValueStore))
|
||||
|
||||
def createProjectFile(self, filepath, window=None):
|
||||
'''Create a project file (.avp) using the current program state'''
|
||||
settingsKeys = [
|
||||
'componentDir',
|
||||
'inputDir',
|
||||
'outputDir',
|
||||
'presetDir',
|
||||
'projectDir',
|
||||
]
|
||||
try:
|
||||
if not filepath.endswith(".avp"):
|
||||
filepath += '.avp'
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
with open(filepath, 'w') as f:
|
||||
print('creating %s' % filepath)
|
||||
|
||||
f.write('[Components]\n')
|
||||
for comp in self.selectedComponents:
|
||||
saveValueStore = comp.savePreset()
|
||||
saveValueStore['preset'] = comp.currentPreset
|
||||
f.write('%s\n' % str(comp))
|
||||
f.write('%s\n' % str(comp.version))
|
||||
f.write('%s\n' % toolkit.presetToString(saveValueStore))
|
||||
|
||||
f.write('\n[Settings]\n')
|
||||
for key in Core.settings.allKeys():
|
||||
if key in settingsKeys:
|
||||
f.write('%s=%s\n' % (key, Core.settings.value(key)))
|
||||
|
||||
if window:
|
||||
f.write('\n[WindowFields]\n')
|
||||
f.write(
|
||||
'lineEdit_audioFile=%s\n'
|
||||
'lineEdit_outputFile=%s\n' % (
|
||||
window.lineEdit_audioFile.text(),
|
||||
window.lineEdit_outputFile.text()
|
||||
)
|
||||
)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
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
|
||||
)
|
||||
videoWorker.moveToThread(self.videoThread)
|
||||
videoWorker.videoCreated.connect(self.videoCreated)
|
||||
|
||||
self.videoThread.start()
|
||||
return videoWorker
|
||||
|
||||
def videoCreated(self):
|
||||
self.videoThread.quit()
|
||||
self.videoThread.wait()
|
||||
|
||||
def cancel(self):
|
||||
Core.canceled = True
|
||||
|
||||
def reset(self):
|
||||
Core.canceled = False
|
||||
|
||||
@classmethod
|
||||
def storeSettings(cls):
|
||||
'''Store settings/paths to directories as class variables'''
|
||||
from __init__ import wd
|
||||
from toolkit.ffmpeg import findFfmpeg
|
||||
|
||||
cls.wd = wd
|
||||
dataDir = QtCore.QStandardPaths.writableLocation(
|
||||
QtCore.QStandardPaths.AppConfigLocation
|
||||
)
|
||||
with open(os.path.join(wd, 'encoder-options.json')) as json_file:
|
||||
encoderOptions = json.load(json_file)
|
||||
|
||||
settings = {
|
||||
'dataDir': dataDir,
|
||||
'settings': QtCore.QSettings(
|
||||
os.path.join(dataDir, 'settings.ini'),
|
||||
QtCore.QSettings.IniFormat),
|
||||
'presetDir': os.path.join(dataDir, 'presets'),
|
||||
'componentsPath': os.path.join(wd, 'components'),
|
||||
'encoderOptions': encoderOptions,
|
||||
'resolutions': [
|
||||
'1920x1080',
|
||||
'1280x720',
|
||||
'854x480',
|
||||
],
|
||||
'FFMPEG_BIN': findFfmpeg(),
|
||||
'windowHasFocus': False,
|
||||
'canceled': False,
|
||||
}
|
||||
|
||||
settings['videoFormats'] = toolkit.appendUppercase([
|
||||
'*.mp4',
|
||||
'*.mov',
|
||||
'*.mkv',
|
||||
'*.avi',
|
||||
'*.webm',
|
||||
'*.flv',
|
||||
])
|
||||
settings['audioFormats'] = toolkit.appendUppercase([
|
||||
'*.mp3',
|
||||
'*.wav',
|
||||
'*.ogg',
|
||||
'*.fla',
|
||||
'*.flac',
|
||||
'*.aac',
|
||||
])
|
||||
settings['imageFormats'] = toolkit.appendUppercase([
|
||||
'*.png',
|
||||
'*.jpg',
|
||||
'*.tif',
|
||||
'*.tiff',
|
||||
'*.gif',
|
||||
'*.bmp',
|
||||
'*.ico',
|
||||
'*.xbm',
|
||||
'*.xpm',
|
||||
])
|
||||
|
||||
# Register all settings as class variables
|
||||
for classvar, val in settings.items():
|
||||
setattr(cls, classvar, val)
|
||||
|
||||
cls.loadDefaultSettings()
|
||||
|
||||
@classmethod
|
||||
def loadDefaultSettings(cls):
|
||||
cls.defaultSettings = {
|
||||
"outputWidth": 1280,
|
||||
"outputHeight": 720,
|
||||
"outputFrameRate": 30,
|
||||
"outputAudioCodec": "AAC",
|
||||
"outputAudioBitrate": "192",
|
||||
"outputVideoCodec": "H264",
|
||||
"outputVideoBitrate": "2500",
|
||||
"outputVideoFormat": "yuv420p",
|
||||
"outputPreset": "medium",
|
||||
"outputFormat": "mp4",
|
||||
"outputContainer": "MP4",
|
||||
"projectDir": os.path.join(cls.dataDir, 'projects'),
|
||||
"pref_insertCompAtTop": True,
|
||||
"pref_genericPreview": True,
|
||||
}
|
||||
|
||||
for parm, value in cls.defaultSettings.items():
|
||||
if cls.settings.value(parm) is None:
|
||||
cls.settings.setValue(parm, value)
|
||||
|
||||
# Allow manual editing of prefs. (Surprisingly necessary as Qt seems to
|
||||
# store True as 'true' but interprets a manually-added 'true' as str.)
|
||||
for key in cls.settings.allKeys():
|
||||
if not key.startswith('pref_'):
|
||||
continue
|
||||
val = cls.settings.value(key)
|
||||
if val in ('true', 'false'):
|
||||
cls.settings.setValue(key, True if val == 'true' else False)
|
||||
|
||||
|
||||
# always store settings in class variables even if a Core object is not created
|
||||
Core.storeSettings()
|
|
@ -0,0 +1,130 @@
|
|||
{
|
||||
"containers":[
|
||||
{
|
||||
"name": "MP4",
|
||||
"container": "mp4",
|
||||
"default-vcodec": "H264",
|
||||
"default-acodec": "AAC",
|
||||
"video-codecs": [
|
||||
"H264",
|
||||
"H264 (nvenc)",
|
||||
"MPEG4"
|
||||
],
|
||||
"audio-codecs": [
|
||||
"AAC",
|
||||
"AC3",
|
||||
"MP3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MOV",
|
||||
"container": "mov",
|
||||
"default-vcodec": "H264",
|
||||
"default-acodec": "AAC",
|
||||
"video-codecs": [
|
||||
"H264",
|
||||
"H264 (nvenc)",
|
||||
"MPEG4",
|
||||
"XVID"
|
||||
],
|
||||
"audio-codecs": [
|
||||
"AAC",
|
||||
"AC3",
|
||||
"MP3",
|
||||
"PCM s16 LE"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MKV",
|
||||
"container": "matroska",
|
||||
"default-vcodec": "H264",
|
||||
"default-acodec": "AAC",
|
||||
"video-codecs": [
|
||||
"H264",
|
||||
"H264 (nvenc)",
|
||||
"MPEG4",
|
||||
"MPEG2",
|
||||
"DV",
|
||||
"WMV"
|
||||
],
|
||||
"audio-codecs": [
|
||||
"AAC",
|
||||
"AC3",
|
||||
"MP3",
|
||||
"PCM s16 LE",
|
||||
"WMA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AVI",
|
||||
"container": "avi",
|
||||
"default-vcodec": "H264",
|
||||
"default-acodec": "AAC",
|
||||
"video-codecs": [
|
||||
"H264",
|
||||
"H264 (nvenc)",
|
||||
"MPEG4",
|
||||
"MPEG2",
|
||||
"DV",
|
||||
"WMV"
|
||||
],
|
||||
"audio-codecs": [
|
||||
"AAC",
|
||||
"AC3",
|
||||
"MP3",
|
||||
"PCM s16 LE",
|
||||
"WMA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "WEBM",
|
||||
"container": "webm",
|
||||
"default-vcodec": "VP9",
|
||||
"default-acodec": "Vorbis",
|
||||
"video-codecs": [
|
||||
"VP9",
|
||||
"VP8"
|
||||
],
|
||||
"audio-codecs": [
|
||||
"Vorbis"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FLV",
|
||||
"container": "flv",
|
||||
"default-vcodec": "FLV",
|
||||
"default-acodec": "Vorbis",
|
||||
"video-codecs": [
|
||||
"Sorenson (flv)",
|
||||
"H264",
|
||||
"H264 (nvenc)",
|
||||
"MPEG4"
|
||||
],
|
||||
"audio-codecs": [
|
||||
"MP3",
|
||||
"PCM s16 LE",
|
||||
"Vorbis"
|
||||
]
|
||||
}
|
||||
],
|
||||
"video-codecs":{
|
||||
"H264": ["libx264"],
|
||||
"H264 (nvenc)": ["h264_nvenc", "nvenc_h264"],
|
||||
"MPEG4": ["mpeg4"],
|
||||
"VP9": ["libvpx-vp9"],
|
||||
"VP8": ["libvpx"],
|
||||
"XVID": ["libxvid"],
|
||||
"Sorenson (flv)": ["flv"],
|
||||
"MPEG2": ["mp2video"],
|
||||
"DV": ["dvvideo"],
|
||||
"WMV": ["wmv2"]
|
||||
},
|
||||
"audio-codecs": {
|
||||
"AAC": ["libfdk_aac", "aac"],
|
||||
"AC3": ["ac3"],
|
||||
"MP3": ["libmp3lame"],
|
||||
"PCM s16 LE": ["pcm_s16le"],
|
||||
"WMA": ["wmav2"],
|
||||
"Vorbis": ["libvorbis"]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
from PyQt5 import uic, QtWidgets
|
||||
import sys
|
||||
import os
|
||||
|
||||
from __init__ import wd
|
||||
|
||||
|
||||
def main():
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
app.setApplicationName("audio-visualizer")
|
||||
|
||||
# Determine mode
|
||||
mode = 'GUI'
|
||||
if len(sys.argv) > 2:
|
||||
mode = 'commandline'
|
||||
elif len(sys.argv) == 2:
|
||||
if sys.argv[1].startswith('-'):
|
||||
mode = 'commandline'
|
||||
else:
|
||||
# opening a project file with gui
|
||||
proj = sys.argv[1]
|
||||
else:
|
||||
# normal gui launch
|
||||
proj = None
|
||||
|
||||
# Launch program
|
||||
if mode == 'commandline':
|
||||
from command import Command
|
||||
|
||||
main = Command()
|
||||
|
||||
elif mode == 'GUI':
|
||||
from mainwindow import MainWindow
|
||||
import atexit
|
||||
import signal
|
||||
|
||||
window = uic.loadUi(os.path.join(wd, "mainwindow.ui"))
|
||||
# window.adjustSize()
|
||||
desc = QtWidgets.QDesktopWidget()
|
||||
dpi = desc.physicalDpiX()
|
||||
|
||||
topMargin = 0 if (dpi == 96) else int(10 * (dpi / 96))
|
||||
window.resize(
|
||||
window.width() *
|
||||
(dpi / 96), window.height() *
|
||||
(dpi / 96)
|
||||
)
|
||||
# window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0)
|
||||
|
||||
main = MainWindow(window, proj)
|
||||
window.raise_()
|
||||
|
||||
signal.signal(signal.SIGINT, main.cleanUp)
|
||||
atexit.register(main.cleanUp)
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,975 @@
|
|||
'''
|
||||
When using GUI mode, this module's object (the main window) takes
|
||||
user input to construct a program state (stored in the Core object).
|
||||
This shows a preview of the video being created and allows for saving
|
||||
projects and exporting the video at a later time.
|
||||
'''
|
||||
from PyQt5 import QtCore, QtGui, uic, QtWidgets
|
||||
from PyQt5.QtWidgets import QMenu, QShortcut
|
||||
from PIL import Image
|
||||
from queue import Queue
|
||||
import sys
|
||||
import os
|
||||
import signal
|
||||
import filecmp
|
||||
import time
|
||||
|
||||
from core import Core
|
||||
import preview_thread
|
||||
from presetmanager import PresetManager
|
||||
from toolkit import disableWhenEncoding, disableWhenOpeningProject, 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
|
||||
self.setFrameStyle(QtWidgets.QFrame.StyledPanel)
|
||||
self.pixmap = QtGui.QPixmap(img)
|
||||
self.isFlickering = 0
|
||||
|
||||
def paintEvent(self, event):
|
||||
size = self.size()
|
||||
painter = QtGui.QPainter(self)
|
||||
point = QtCore.QPoint(0, 0)
|
||||
scaledPix = self.pixmap.scaled(
|
||||
size,
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
transformMode=QtCore.Qt.SmoothTransformation)
|
||||
|
||||
# start painting the label from left upper corner
|
||||
point.setX((size.width() - scaledPix.width())/2)
|
||||
point.setY((size.height() - scaledPix.height())/2)
|
||||
painter.drawPixmap(point, scaledPix)
|
||||
|
||||
def changePixmap(self, img):
|
||||
if self.parent.encoding:
|
||||
# track time to determine if the preview is flickering due to lag
|
||||
bTime = time.time()
|
||||
elif self.isFlickering > 0:
|
||||
# no longer exporting video so reset the paint area to normal
|
||||
self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, False)
|
||||
self.isFlickering = 0
|
||||
|
||||
self.pixmap = QtGui.QPixmap(img)
|
||||
self.repaint()
|
||||
|
||||
if self.parent.encoding and time.time() - bTime > 1.1:
|
||||
self.isFlickering += 1
|
||||
if self.isFlickering == 8:
|
||||
# if export is lagging, temporarily disable clearing the paint area
|
||||
self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, True)
|
||||
|
||||
@QtCore.pyqtSlot(str)
|
||||
def threadError(self, msg):
|
||||
self.parent.showMessage(
|
||||
msg=msg,
|
||||
icon='Critical',
|
||||
parent=self
|
||||
)
|
||||
|
||||
|
||||
class MainWindow(QtWidgets.QMainWindow):
|
||||
'''
|
||||
The MainWindow wraps many Core methods in order to update the GUI
|
||||
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
|
||||
processTask = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, window, project):
|
||||
QtWidgets.QMainWindow.__init__(self)
|
||||
# print('main thread id: {}'.format(QtCore.QThread.currentThreadId()))
|
||||
self.window = window
|
||||
self.core = Core()
|
||||
|
||||
# widgets of component settings
|
||||
self.pages = []
|
||||
self.lastAutosave = time.time()
|
||||
# list of previous five autosave times, used to reduce update spam
|
||||
self.autosaveTimes = []
|
||||
self.autosaveCooldown = 0.2
|
||||
self.encoding = False
|
||||
|
||||
# Create data directory, load/create settings
|
||||
self.dataDir = Core.dataDir
|
||||
self.presetDir = Core.presetDir
|
||||
self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
|
||||
self.settings = Core.settings
|
||||
self.presetManager = PresetManager(
|
||||
uic.loadUi(
|
||||
os.path.join(Core.wd, 'presetmanager.ui')), self)
|
||||
|
||||
if not os.path.exists(self.dataDir):
|
||||
os.makedirs(self.dataDir)
|
||||
for neededDirectory in (
|
||||
self.presetDir, self.settings.value("projectDir")):
|
||||
if not os.path.exists(neededDirectory):
|
||||
os.mkdir(neededDirectory)
|
||||
|
||||
# Create the preview window and its thread, queues, and timers
|
||||
self.previewWindow = PreviewWindow(self, os.path.join(
|
||||
Core.wd, "background.png"))
|
||||
window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
|
||||
|
||||
self.previewQueue = Queue()
|
||||
self.previewThread = QtCore.QThread(self)
|
||||
self.previewWorker = preview_thread.Worker(self, self.previewQueue)
|
||||
self.previewWorker.error.connect(self.previewWindow.threadError)
|
||||
self.previewWorker.moveToThread(self.previewThread)
|
||||
self.previewWorker.imageCreated.connect(self.showPreviewImage)
|
||||
self.previewThread.start()
|
||||
|
||||
self.timer = QtCore.QTimer(self)
|
||||
self.timer.timeout.connect(self.processTask.emit)
|
||||
self.timer.start(500)
|
||||
|
||||
# Begin decorating the window and connecting events
|
||||
self.window.installEventFilter(self)
|
||||
componentList = self.window.listWidget_componentList
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
window.progressBar_createVideo.setTextVisible(False)
|
||||
else:
|
||||
window.progressLabel.setHidden(True)
|
||||
|
||||
window.toolButton_selectAudioFile.clicked.connect(
|
||||
self.openInputFileDialog)
|
||||
|
||||
window.toolButton_selectOutputFile.clicked.connect(
|
||||
self.openOutputFileDialog)
|
||||
|
||||
def changedField():
|
||||
self.autosave()
|
||||
self.updateWindowTitle()
|
||||
|
||||
window.lineEdit_audioFile.textChanged.connect(changedField)
|
||||
window.lineEdit_outputFile.textChanged.connect(changedField)
|
||||
|
||||
window.progressBar_createVideo.setValue(0)
|
||||
|
||||
window.pushButton_createVideo.clicked.connect(
|
||||
self.createAudioVisualisation)
|
||||
|
||||
window.pushButton_Cancel.clicked.connect(self.stopVideo)
|
||||
|
||||
for i, container in enumerate(Core.encoderOptions['containers']):
|
||||
window.comboBox_videoContainer.addItem(container['name'])
|
||||
if container['name'] == self.settings.value('outputContainer'):
|
||||
selectedContainer = i
|
||||
|
||||
window.comboBox_videoContainer.setCurrentIndex(selectedContainer)
|
||||
window.comboBox_videoContainer.currentIndexChanged.connect(
|
||||
self.updateCodecs
|
||||
)
|
||||
|
||||
self.updateCodecs()
|
||||
|
||||
for i in range(window.comboBox_videoCodec.count()):
|
||||
codec = window.comboBox_videoCodec.itemText(i)
|
||||
if codec == self.settings.value('outputVideoCodec'):
|
||||
window.comboBox_videoCodec.setCurrentIndex(i)
|
||||
|
||||
for i in range(window.comboBox_audioCodec.count()):
|
||||
codec = window.comboBox_audioCodec.itemText(i)
|
||||
if codec == self.settings.value('outputAudioCodec'):
|
||||
window.comboBox_audioCodec.setCurrentIndex(i)
|
||||
|
||||
window.comboBox_videoCodec.currentIndexChanged.connect(
|
||||
self.updateCodecSettings
|
||||
)
|
||||
|
||||
window.comboBox_audioCodec.currentIndexChanged.connect(
|
||||
self.updateCodecSettings
|
||||
)
|
||||
|
||||
vBitrate = int(self.settings.value('outputVideoBitrate'))
|
||||
aBitrate = int(self.settings.value('outputAudioBitrate'))
|
||||
|
||||
window.spinBox_vBitrate.setValue(vBitrate)
|
||||
window.spinBox_aBitrate.setValue(aBitrate)
|
||||
window.spinBox_vBitrate.valueChanged.connect(self.updateCodecSettings)
|
||||
window.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings)
|
||||
|
||||
# Make component buttons
|
||||
self.compMenu = QMenu()
|
||||
for i, comp in enumerate(self.core.modules):
|
||||
action = self.compMenu.addAction(comp.Component.name)
|
||||
action.triggered.connect(
|
||||
lambda _, item=i: self.core.insertComponent(0, item, self)
|
||||
)
|
||||
|
||||
self.window.pushButton_addComponent.setMenu(self.compMenu)
|
||||
|
||||
componentList.dropEvent = self.dragComponent
|
||||
componentList.itemSelectionChanged.connect(
|
||||
self.changeComponentWidget
|
||||
)
|
||||
componentList.itemSelectionChanged.connect(
|
||||
self.presetManager.clearPresetListSelection
|
||||
)
|
||||
self.window.pushButton_removeComponent.clicked.connect(
|
||||
lambda: self.removeComponent()
|
||||
)
|
||||
|
||||
componentList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
componentList.customContextMenuRequested.connect(
|
||||
self.componentContextMenu
|
||||
)
|
||||
|
||||
currentRes = str(self.settings.value('outputWidth'))+'x' + \
|
||||
str(self.settings.value('outputHeight'))
|
||||
for i, res in enumerate(Core.resolutions):
|
||||
window.comboBox_resolution.addItem(res)
|
||||
if res == currentRes:
|
||||
currentRes = i
|
||||
window.comboBox_resolution.setCurrentIndex(currentRes)
|
||||
window.comboBox_resolution.currentIndexChanged.connect(
|
||||
self.updateResolution
|
||||
)
|
||||
|
||||
self.window.pushButton_listMoveUp.clicked.connect(
|
||||
lambda: self.moveComponent(-1)
|
||||
)
|
||||
self.window.pushButton_listMoveDown.clicked.connect(
|
||||
lambda: self.moveComponent(1)
|
||||
)
|
||||
|
||||
# Configure the Projects Menu
|
||||
self.projectMenu = QMenu()
|
||||
self.window.menuButton_newProject = self.projectMenu.addAction(
|
||||
"New Project"
|
||||
)
|
||||
self.window.menuButton_newProject.triggered.connect(
|
||||
lambda: self.createNewProject()
|
||||
)
|
||||
self.window.menuButton_openProject = self.projectMenu.addAction(
|
||||
"Open Project"
|
||||
)
|
||||
self.window.menuButton_openProject.triggered.connect(
|
||||
lambda: self.openOpenProjectDialog()
|
||||
)
|
||||
|
||||
action = self.projectMenu.addAction("Save Project")
|
||||
action.triggered.connect(self.saveCurrentProject)
|
||||
|
||||
action = self.projectMenu.addAction("Save Project As")
|
||||
action.triggered.connect(self.openSaveProjectDialog)
|
||||
|
||||
self.window.pushButton_projects.setMenu(self.projectMenu)
|
||||
|
||||
# Configure the Presets Button
|
||||
self.window.pushButton_presets.clicked.connect(
|
||||
self.openPresetManager
|
||||
)
|
||||
|
||||
self.updateWindowTitle()
|
||||
window.show()
|
||||
|
||||
if project and project != self.autosavePath:
|
||||
if not project.endswith('.avp'):
|
||||
project += '.avp'
|
||||
# open a project from the commandline
|
||||
if not os.path.dirname(project):
|
||||
project = os.path.join(
|
||||
self.settings.value("projectDir"), project
|
||||
)
|
||||
self.currentProject = project
|
||||
self.settings.setValue("currentProject", project)
|
||||
if os.path.exists(self.autosavePath):
|
||||
os.remove(self.autosavePath)
|
||||
else:
|
||||
# open the last currentProject from settings
|
||||
self.currentProject = self.settings.value("currentProject")
|
||||
|
||||
# delete autosave if it's identical to this project
|
||||
if self.autosaveExists(identical=True):
|
||||
os.remove(self.autosavePath)
|
||||
|
||||
if self.currentProject and os.path.exists(self.autosavePath):
|
||||
ch = self.showMessage(
|
||||
msg="Restore unsaved changes in project '%s'?"
|
||||
% os.path.basename(self.currentProject)[:-4],
|
||||
showCancel=True)
|
||||
if ch:
|
||||
self.saveProjectChanges()
|
||||
else:
|
||||
os.remove(self.autosavePath)
|
||||
|
||||
self.openProject(self.currentProject, prompt=False)
|
||||
self.drawPreview(True)
|
||||
|
||||
# verify Pillow version
|
||||
if not self.settings.value("pilMsgShown") \
|
||||
and 'post' not in Image.PILLOW_VERSION:
|
||||
self.showMessage(
|
||||
msg="You are using the standard version of the "
|
||||
"Python imaging library (Pillow %s). Upgrade "
|
||||
"to the Pillow-SIMD fork to enable hardware accelerations "
|
||||
"and export videos faster." % Image.PILLOW_VERSION
|
||||
)
|
||||
self.settings.setValue("pilMsgShown", True)
|
||||
|
||||
# verify Ffmpeg version
|
||||
if not self.settings.value("ffmpegMsgShown"):
|
||||
try:
|
||||
with open(os.devnull, "w") as f:
|
||||
ffmpegVers = checkOutput(
|
||||
['ffmpeg', '-version'], stderr=f
|
||||
)
|
||||
goodVersion = str(ffmpegVers).split()[2].startswith('3')
|
||||
except Exception:
|
||||
goodVersion = False
|
||||
else:
|
||||
goodVersion = True
|
||||
|
||||
if not goodVersion:
|
||||
self.showMessage(
|
||||
msg="You're using an old version of Ffmpeg. "
|
||||
"Some features may not work as expected."
|
||||
)
|
||||
self.settings.setValue("ffmpegMsgShown", True)
|
||||
|
||||
# Hotkeys for projects
|
||||
QtWidgets.QShortcut("Ctrl+S", self.window, self.saveCurrentProject)
|
||||
QtWidgets.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog)
|
||||
QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog)
|
||||
QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject)
|
||||
|
||||
# Hotkeys for component list
|
||||
for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert):
|
||||
QtWidgets.QShortcut(
|
||||
inskey, self.window,
|
||||
activated=lambda: self.window.pushButton_addComponent.click()
|
||||
)
|
||||
for delkey in ("Ctrl+R", QtCore.Qt.Key_Delete):
|
||||
QtWidgets.QShortcut(
|
||||
delkey, self.window.listWidget_componentList,
|
||||
self.removeComponent
|
||||
)
|
||||
QtWidgets.QShortcut(
|
||||
"Ctrl+Space", self.window,
|
||||
activated=lambda: self.window.listWidget_componentList.setFocus()
|
||||
)
|
||||
QtWidgets.QShortcut(
|
||||
"Ctrl+Shift+S", self.window,
|
||||
self.presetManager.openSavePresetDialog
|
||||
)
|
||||
QtWidgets.QShortcut(
|
||||
"Ctrl+Shift+C", self.window, self.presetManager.clearPreset
|
||||
)
|
||||
|
||||
QtWidgets.QShortcut(
|
||||
"Ctrl+Up", self.window.listWidget_componentList,
|
||||
activated=lambda: self.moveComponent(-1)
|
||||
)
|
||||
QtWidgets.QShortcut(
|
||||
"Ctrl+Down", self.window.listWidget_componentList,
|
||||
activated=lambda: self.moveComponent(1)
|
||||
)
|
||||
QtWidgets.QShortcut(
|
||||
"Ctrl+Home", self.window.listWidget_componentList,
|
||||
activated=lambda: self.moveComponent('top')
|
||||
)
|
||||
QtWidgets.QShortcut(
|
||||
"Ctrl+End", self.window.listWidget_componentList,
|
||||
activated=lambda: self.moveComponent('bottom')
|
||||
)
|
||||
|
||||
# Debug Hotkeys
|
||||
QtWidgets.QShortcut(
|
||||
"Ctrl+Alt+Shift+R", self.window, self.drawPreview
|
||||
)
|
||||
QtWidgets.QShortcut(
|
||||
"Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand
|
||||
)
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def cleanUp(self, *args):
|
||||
self.timer.stop()
|
||||
self.previewThread.quit()
|
||||
self.previewThread.wait()
|
||||
|
||||
@disableWhenOpeningProject
|
||||
def updateWindowTitle(self):
|
||||
appName = 'Audio Visualizer'
|
||||
try:
|
||||
if self.currentProject:
|
||||
appName += ' - %s' % \
|
||||
os.path.splitext(
|
||||
os.path.basename(self.currentProject))[0]
|
||||
if self.autosaveExists(identical=False):
|
||||
appName += '*'
|
||||
except AttributeError:
|
||||
pass
|
||||
self.window.setWindowTitle(appName)
|
||||
|
||||
@QtCore.pyqtSlot(int, dict)
|
||||
def updateComponentTitle(self, pos, presetStore=False):
|
||||
if type(presetStore) == dict:
|
||||
name = presetStore['preset']
|
||||
if name is None or name not in self.core.savedPresets:
|
||||
modified = False
|
||||
else:
|
||||
modified = (presetStore != self.core.savedPresets[name])
|
||||
else:
|
||||
modified = bool(presetStore)
|
||||
if pos < 0:
|
||||
pos = len(self.core.selectedComponents)-1
|
||||
title = str(self.core.selectedComponents[pos])
|
||||
if self.core.selectedComponents[pos].currentPreset:
|
||||
title += ' - %s' % self.core.selectedComponents[pos].currentPreset
|
||||
if modified:
|
||||
title += '*'
|
||||
self.window.listWidget_componentList.item(pos).setText(title)
|
||||
|
||||
def updateCodecs(self):
|
||||
containerWidget = self.window.comboBox_videoContainer
|
||||
vCodecWidget = self.window.comboBox_videoCodec
|
||||
aCodecWidget = self.window.comboBox_audioCodec
|
||||
index = containerWidget.currentIndex()
|
||||
name = containerWidget.itemText(index)
|
||||
self.settings.setValue('outputContainer', name)
|
||||
|
||||
vCodecWidget.clear()
|
||||
aCodecWidget.clear()
|
||||
|
||||
for container in Core.encoderOptions['containers']:
|
||||
if container['name'] == name:
|
||||
for vCodec in container['video-codecs']:
|
||||
vCodecWidget.addItem(vCodec)
|
||||
for aCodec in container['audio-codecs']:
|
||||
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
|
||||
aCodecWidget = self.window.comboBox_audioCodec
|
||||
currentVideoCodec = vCodecWidget.currentIndex()
|
||||
currentVideoCodec = vCodecWidget.itemText(currentVideoCodec)
|
||||
currentVideoBitrate = vBitrateWidget.value()
|
||||
currentAudioCodec = aCodecWidget.currentIndex()
|
||||
currentAudioCodec = aCodecWidget.itemText(currentAudioCodec)
|
||||
currentAudioBitrate = aBitrateWidget.value()
|
||||
self.settings.setValue('outputVideoCodec', currentVideoCodec)
|
||||
self.settings.setValue('outputAudioCodec', currentAudioCodec)
|
||||
self.settings.setValue('outputVideoBitrate', currentVideoBitrate)
|
||||
self.settings.setValue('outputAudioBitrate', currentAudioBitrate)
|
||||
|
||||
@disableWhenOpeningProject
|
||||
def autosave(self, force=False):
|
||||
if not self.currentProject:
|
||||
if os.path.exists(self.autosavePath):
|
||||
os.remove(self.autosavePath)
|
||||
elif force or time.time() - self.lastAutosave >= self.autosaveCooldown:
|
||||
self.core.createProjectFile(self.autosavePath, self.window)
|
||||
self.lastAutosave = time.time()
|
||||
if len(self.autosaveTimes) >= 5:
|
||||
# Do some math to reduce autosave spam. This gives a smooth
|
||||
# curve up to 5 seconds cooldown and maintains that for 30 secs
|
||||
# if a component is continuously updated
|
||||
timeDiff = self.lastAutosave - self.autosaveTimes.pop()
|
||||
if not force and timeDiff >= 1.0 \
|
||||
and timeDiff <= 10.0:
|
||||
if self.autosaveCooldown / 4.0 < 0.5:
|
||||
self.autosaveCooldown += 1.0
|
||||
self.autosaveCooldown = (
|
||||
5.0 * (self.autosaveCooldown / 5.0)
|
||||
) + (self.autosaveCooldown / 5.0) * 2
|
||||
elif force or timeDiff >= self.autosaveCooldown * 5:
|
||||
self.autosaveCooldown = 0.2
|
||||
self.autosaveTimes.insert(0, self.lastAutosave)
|
||||
|
||||
def autosaveExists(self, identical=True):
|
||||
'''Determines if creating the autosave should be blocked.'''
|
||||
try:
|
||||
if self.currentProject and os.path.exists(self.autosavePath) \
|
||||
and filecmp.cmp(
|
||||
self.autosavePath, self.currentProject) == identical:
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
print('project file couldn\'t be located:', self.currentProject)
|
||||
return identical
|
||||
return False
|
||||
|
||||
def saveProjectChanges(self):
|
||||
'''Overwrites project file with autosave file'''
|
||||
try:
|
||||
os.remove(self.currentProject)
|
||||
os.rename(self.autosavePath, self.currentProject)
|
||||
return True
|
||||
except (FileNotFoundError, IsADirectoryError) as e:
|
||||
self.showMessage(
|
||||
msg='Project file couldn\'t be saved.',
|
||||
detail=str(e))
|
||||
return False
|
||||
|
||||
def openInputFileDialog(self):
|
||||
inputDir = self.settings.value("inputDir", os.path.expanduser("~"))
|
||||
|
||||
fileName, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self.window, "Open Audio File",
|
||||
inputDir, "Audio Files (%s)" % " ".join(Core.audioFormats))
|
||||
|
||||
if fileName:
|
||||
self.settings.setValue("inputDir", os.path.dirname(fileName))
|
||||
self.window.lineEdit_audioFile.setText(fileName)
|
||||
|
||||
def openOutputFileDialog(self):
|
||||
outputDir = self.settings.value("outputDir", os.path.expanduser("~"))
|
||||
|
||||
fileName, _ = QtWidgets.QFileDialog.getSaveFileName(
|
||||
self.window, "Set Output Video File",
|
||||
outputDir,
|
||||
"Video Files (%s);; All Files (*)" % " ".join(
|
||||
Core.videoFormats))
|
||||
|
||||
if fileName:
|
||||
self.settings.setValue("outputDir", os.path.dirname(fileName))
|
||||
self.window.lineEdit_outputFile.setText(fileName)
|
||||
|
||||
def stopVideo(self):
|
||||
print('stop')
|
||||
self.videoWorker.cancel()
|
||||
self.canceled = True
|
||||
|
||||
def createAudioVisualisation(self):
|
||||
# create output video if mandatory settings are filled in
|
||||
audioFile = self.window.lineEdit_audioFile.text()
|
||||
outputPath = self.window.lineEdit_outputFile.text()
|
||||
|
||||
if audioFile and outputPath and self.core.selectedComponents:
|
||||
if not os.path.dirname(outputPath):
|
||||
outputPath = os.path.join(
|
||||
os.path.expanduser("~"), outputPath)
|
||||
if outputPath and os.path.isdir(outputPath):
|
||||
self.showMessage(
|
||||
msg='Chosen filename matches a directory, which '
|
||||
'cannot be overwritten. Please choose a different '
|
||||
'filename or move the directory.',
|
||||
icon='Warning',
|
||||
)
|
||||
return
|
||||
else:
|
||||
if not audioFile or not outputPath:
|
||||
self.showMessage(
|
||||
msg="You must select an audio file and output filename."
|
||||
)
|
||||
elif not self.core.selectedComponents:
|
||||
self.showMessage(
|
||||
msg="Not enough components."
|
||||
)
|
||||
return
|
||||
|
||||
self.canceled = False
|
||||
self.progressBarUpdated(-1)
|
||||
self.videoWorker = self.core.newVideoWorker(
|
||||
self, audioFile, outputPath
|
||||
)
|
||||
self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated)
|
||||
self.videoWorker.progressBarSetText.connect(
|
||||
self.progressBarSetText)
|
||||
self.videoWorker.imageCreated.connect(self.showPreviewImage)
|
||||
self.videoWorker.encoding.connect(self.changeEncodingStatus)
|
||||
self.createVideo.emit()
|
||||
|
||||
@QtCore.pyqtSlot(str, str)
|
||||
def videoThreadError(self, msg, detail):
|
||||
try:
|
||||
self.stopVideo()
|
||||
except AttributeError as e:
|
||||
if 'videoWorker' not in str(e):
|
||||
raise
|
||||
self.showMessage(
|
||||
msg=msg,
|
||||
detail=detail,
|
||||
icon='Critical',
|
||||
)
|
||||
|
||||
def changeEncodingStatus(self, status):
|
||||
self.encoding = status
|
||||
if status:
|
||||
self.window.pushButton_createVideo.setEnabled(False)
|
||||
self.window.pushButton_Cancel.setEnabled(True)
|
||||
self.window.comboBox_resolution.setEnabled(False)
|
||||
self.window.stackedWidget.setEnabled(False)
|
||||
self.window.tab_encoderSettings.setEnabled(False)
|
||||
self.window.label_audioFile.setEnabled(False)
|
||||
self.window.toolButton_selectAudioFile.setEnabled(False)
|
||||
self.window.label_outputFile.setEnabled(False)
|
||||
self.window.toolButton_selectOutputFile.setEnabled(False)
|
||||
self.window.lineEdit_audioFile.setEnabled(False)
|
||||
self.window.lineEdit_outputFile.setEnabled(False)
|
||||
self.window.pushButton_addComponent.setEnabled(False)
|
||||
self.window.pushButton_removeComponent.setEnabled(False)
|
||||
self.window.pushButton_listMoveDown.setEnabled(False)
|
||||
self.window.pushButton_listMoveUp.setEnabled(False)
|
||||
self.window.menuButton_newProject.setEnabled(False)
|
||||
self.window.menuButton_openProject.setEnabled(False)
|
||||
if sys.platform == 'darwin':
|
||||
self.window.progressLabel.setHidden(False)
|
||||
else:
|
||||
self.window.listWidget_componentList.setEnabled(False)
|
||||
else:
|
||||
self.window.pushButton_createVideo.setEnabled(True)
|
||||
self.window.pushButton_Cancel.setEnabled(False)
|
||||
self.window.comboBox_resolution.setEnabled(True)
|
||||
self.window.stackedWidget.setEnabled(True)
|
||||
self.window.tab_encoderSettings.setEnabled(True)
|
||||
self.window.label_audioFile.setEnabled(True)
|
||||
self.window.toolButton_selectAudioFile.setEnabled(True)
|
||||
self.window.lineEdit_audioFile.setEnabled(True)
|
||||
self.window.label_outputFile.setEnabled(True)
|
||||
self.window.toolButton_selectOutputFile.setEnabled(True)
|
||||
self.window.lineEdit_outputFile.setEnabled(True)
|
||||
self.window.pushButton_addComponent.setEnabled(True)
|
||||
self.window.pushButton_removeComponent.setEnabled(True)
|
||||
self.window.pushButton_listMoveDown.setEnabled(True)
|
||||
self.window.pushButton_listMoveUp.setEnabled(True)
|
||||
self.window.menuButton_newProject.setEnabled(True)
|
||||
self.window.menuButton_openProject.setEnabled(True)
|
||||
self.window.listWidget_componentList.setEnabled(True)
|
||||
self.window.progressLabel.setHidden(True)
|
||||
self.drawPreview(True)
|
||||
|
||||
@QtCore.pyqtSlot(int)
|
||||
def progressBarUpdated(self, value):
|
||||
self.window.progressBar_createVideo.setValue(value)
|
||||
|
||||
@QtCore.pyqtSlot(str)
|
||||
def progressBarSetText(self, value):
|
||||
if sys.platform == 'darwin':
|
||||
self.window.progressLabel.setText(value)
|
||||
else:
|
||||
self.window.progressBar_createVideo.setFormat(value)
|
||||
|
||||
def updateResolution(self):
|
||||
resIndex = int(self.window.comboBox_resolution.currentIndex())
|
||||
res = Core.resolutions[resIndex].split('x')
|
||||
changed = res[0] != self.settings.value("outputWidth")
|
||||
self.settings.setValue('outputWidth', res[0])
|
||||
self.settings.setValue('outputHeight', res[1])
|
||||
if changed:
|
||||
for i in range(len(self.core.selectedComponents)):
|
||||
self.core.updateComponent(i)
|
||||
|
||||
def drawPreview(self, force=False, **kwargs):
|
||||
'''Use autosave keyword arg to force saving or not saving if needed'''
|
||||
self.newTask.emit(self.core.selectedComponents)
|
||||
# self.processTask.emit()
|
||||
if force or 'autosave' in kwargs:
|
||||
if force or kwargs['autosave']:
|
||||
self.autosave(True)
|
||||
else:
|
||||
self.autosave()
|
||||
self.updateWindowTitle()
|
||||
|
||||
@QtCore.pyqtSlot(QtGui.QImage)
|
||||
def showPreviewImage(self, image):
|
||||
self.previewWindow.changePixmap(image)
|
||||
|
||||
def showFfmpegCommand(self):
|
||||
from textwrap import wrap
|
||||
from toolkit.ffmpeg import createFfmpegCommand
|
||||
command = createFfmpegCommand(
|
||||
self.window.lineEdit_audioFile.text(),
|
||||
self.window.lineEdit_outputFile.text(),
|
||||
self.core.selectedComponents
|
||||
)
|
||||
lines = wrap(" ".join(command), 49)
|
||||
self.showMessage(
|
||||
msg="Current FFmpeg command:\n\n %s" % " ".join(lines)
|
||||
)
|
||||
|
||||
def insertComponent(self, index):
|
||||
componentList = self.window.listWidget_componentList
|
||||
stackedWidget = self.window.stackedWidget
|
||||
|
||||
componentList.insertItem(
|
||||
index,
|
||||
self.core.selectedComponents[index].name)
|
||||
componentList.setCurrentRow(index)
|
||||
|
||||
# connect to signal that adds an asterisk when modified
|
||||
self.core.selectedComponents[index].modified.connect(
|
||||
self.updateComponentTitle)
|
||||
|
||||
self.pages.insert(index, self.core.selectedComponents[index].page)
|
||||
stackedWidget.insertWidget(index, self.pages[index])
|
||||
stackedWidget.setCurrentIndex(index)
|
||||
|
||||
return index
|
||||
|
||||
def removeComponent(self):
|
||||
componentList = self.window.listWidget_componentList
|
||||
|
||||
for selected in componentList.selectedItems():
|
||||
index = componentList.row(selected)
|
||||
self.window.stackedWidget.removeWidget(self.pages[index])
|
||||
componentList.takeItem(index)
|
||||
self.core.removeComponent(index)
|
||||
self.pages.pop(index)
|
||||
self.changeComponentWidget()
|
||||
self.drawPreview()
|
||||
|
||||
@disableWhenEncoding
|
||||
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()
|
||||
newRow = row + change
|
||||
if newRow > -1 and newRow < componentList.count():
|
||||
self.core.moveComponent(row, newRow)
|
||||
|
||||
# update widgets
|
||||
page = self.pages.pop(row)
|
||||
self.pages.insert(newRow, page)
|
||||
item = componentList.takeItem(row)
|
||||
newItem = componentList.insertItem(newRow, item)
|
||||
widget = stackedWidget.removeWidget(page)
|
||||
stackedWidget.insertWidget(newRow, page)
|
||||
componentList.setCurrentRow(newRow)
|
||||
stackedWidget.setCurrentIndex(newRow)
|
||||
self.drawPreview(True)
|
||||
|
||||
def getComponentListMousePos(self, position):
|
||||
'''
|
||||
Given a QPos, returns the component index under the mouse cursor
|
||||
or -1 if no component is there.
|
||||
'''
|
||||
componentList = self.window.listWidget_componentList
|
||||
|
||||
modelIndexes = [
|
||||
componentList.model().index(i)
|
||||
for i in range(componentList.count())
|
||||
]
|
||||
rects = [
|
||||
componentList.visualRect(modelIndex)
|
||||
for modelIndex in modelIndexes
|
||||
]
|
||||
mousePos = [rect.contains(position) for rect in rects]
|
||||
if not any(mousePos):
|
||||
# Not clicking a component
|
||||
mousePos = -1
|
||||
else:
|
||||
mousePos = mousePos.index(True)
|
||||
return mousePos
|
||||
|
||||
@disableWhenEncoding
|
||||
def dragComponent(self, event):
|
||||
'''Used as Qt drop event for the component listwidget'''
|
||||
componentList = self.window.listWidget_componentList
|
||||
mousePos = self.getComponentListMousePos(event.pos())
|
||||
if mousePos > -1:
|
||||
change = (componentList.currentRow() - mousePos) * -1
|
||||
else:
|
||||
change = (componentList.count() - componentList.currentRow() - 1)
|
||||
self.moveComponent(change)
|
||||
|
||||
def changeComponentWidget(self):
|
||||
selected = self.window.listWidget_componentList.selectedItems()
|
||||
if selected:
|
||||
index = self.window.listWidget_componentList.row(selected[0])
|
||||
self.window.stackedWidget.setCurrentIndex(index)
|
||||
|
||||
def openPresetManager(self):
|
||||
'''Preset manager for importing, exporting, renaming, deleting'''
|
||||
self.presetManager.show()
|
||||
|
||||
def clear(self):
|
||||
'''Get a blank slate'''
|
||||
self.core.clearComponents()
|
||||
self.window.listWidget_componentList.clear()
|
||||
for widget in self.pages:
|
||||
self.window.stackedWidget.removeWidget(widget)
|
||||
self.pages = []
|
||||
for field in (
|
||||
self.window.lineEdit_audioFile,
|
||||
self.window.lineEdit_outputFile
|
||||
):
|
||||
field.blockSignals(True)
|
||||
field.setText('')
|
||||
field.blockSignals(False)
|
||||
self.progressBarUpdated(0)
|
||||
self.progressBarSetText('')
|
||||
|
||||
@disableWhenEncoding
|
||||
def createNewProject(self, prompt=True):
|
||||
if prompt:
|
||||
self.openSaveChangesDialog('starting a new project')
|
||||
|
||||
self.clear()
|
||||
self.currentProject = None
|
||||
self.settings.setValue("currentProject", None)
|
||||
self.drawPreview(True)
|
||||
|
||||
def saveCurrentProject(self):
|
||||
if self.currentProject:
|
||||
self.core.createProjectFile(self.currentProject, self.window)
|
||||
try:
|
||||
os.remove(self.autosavePath)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
self.updateWindowTitle()
|
||||
else:
|
||||
self.openSaveProjectDialog()
|
||||
|
||||
def openSaveChangesDialog(self, phrase):
|
||||
success = True
|
||||
if self.autosaveExists(identical=False):
|
||||
ch = self.showMessage(
|
||||
msg="You have unsaved changes in project '%s'. "
|
||||
"Save before %s?" % (
|
||||
os.path.basename(self.currentProject)[:-4],
|
||||
phrase
|
||||
),
|
||||
showCancel=True)
|
||||
if ch:
|
||||
success = self.saveProjectChanges()
|
||||
|
||||
if success and os.path.exists(self.autosavePath):
|
||||
os.remove(self.autosavePath)
|
||||
|
||||
def openSaveProjectDialog(self):
|
||||
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
|
||||
self.window, "Create Project File",
|
||||
self.settings.value("projectDir"),
|
||||
"Project Files (*.avp)")
|
||||
if not filename:
|
||||
return
|
||||
if not filename.endswith(".avp"):
|
||||
filename += '.avp'
|
||||
self.settings.setValue("projectDir", os.path.dirname(filename))
|
||||
self.settings.setValue("currentProject", filename)
|
||||
self.currentProject = filename
|
||||
self.core.createProjectFile(filename, self.window)
|
||||
self.updateWindowTitle()
|
||||
|
||||
@disableWhenEncoding
|
||||
def openOpenProjectDialog(self):
|
||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self.window, "Open Project File",
|
||||
self.settings.value("projectDir"),
|
||||
"Project Files (*.avp)")
|
||||
self.openProject(filename)
|
||||
|
||||
def openProject(self, filepath, prompt=True):
|
||||
if not filepath or not os.path.exists(filepath) \
|
||||
or not filepath.endswith('.avp'):
|
||||
return
|
||||
|
||||
self.clear()
|
||||
# ask to save any changes that are about to get deleted
|
||||
if prompt:
|
||||
self.openSaveChangesDialog('opening another project')
|
||||
|
||||
self.currentProject = filepath
|
||||
self.settings.setValue("currentProject", filepath)
|
||||
self.settings.setValue("projectDir", os.path.dirname(filepath))
|
||||
# actually load the project using core method
|
||||
self.core.openProject(self, filepath)
|
||||
self.drawPreview(autosave=False)
|
||||
self.updateWindowTitle()
|
||||
|
||||
def showMessage(self, **kwargs):
|
||||
parent = kwargs['parent'] if 'parent' in kwargs else self.window
|
||||
msg = QtWidgets.QMessageBox(parent)
|
||||
msg.setModal(True)
|
||||
msg.setText(kwargs['msg'])
|
||||
msg.setIcon(
|
||||
eval('QtWidgets.QMessageBox.%s' % kwargs['icon'])
|
||||
if 'icon' in kwargs else QtWidgets.QMessageBox.Information
|
||||
)
|
||||
msg.setDetailedText(kwargs['detail'] if 'detail' in kwargs else None)
|
||||
if 'showCancel'in kwargs and kwargs['showCancel']:
|
||||
msg.setStandardButtons(
|
||||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
|
||||
else:
|
||||
msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
|
||||
ch = msg.exec_()
|
||||
if ch == 1024:
|
||||
return True
|
||||
return False
|
||||
|
||||
@disableWhenEncoding
|
||||
def componentContextMenu(self, QPos):
|
||||
'''Appears when right-clicking the component list'''
|
||||
componentList = self.window.listWidget_componentList
|
||||
self.menu = QMenu()
|
||||
parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0))
|
||||
|
||||
index = self.getComponentListMousePos(QPos)
|
||||
if index > -1:
|
||||
# Show preset menu if clicking a component
|
||||
self.presetManager.findPresets()
|
||||
menuItem = self.menu.addAction("Save Preset")
|
||||
menuItem.triggered.connect(
|
||||
self.presetManager.openSavePresetDialog
|
||||
)
|
||||
|
||||
# submenu for opening presets
|
||||
try:
|
||||
presets = self.presetManager.presets[
|
||||
str(self.core.selectedComponents[index])
|
||||
]
|
||||
self.presetSubmenu = QMenu("Open Preset")
|
||||
self.menu.addMenu(self.presetSubmenu)
|
||||
|
||||
for version, presetName in presets:
|
||||
menuItem = self.presetSubmenu.addAction(presetName)
|
||||
menuItem.triggered.connect(
|
||||
lambda _, presetName=presetName:
|
||||
self.presetManager.openPreset(presetName)
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if self.core.selectedComponents[index].currentPreset:
|
||||
menuItem = self.menu.addAction("Clear Preset")
|
||||
menuItem.triggered.connect(
|
||||
self.presetManager.clearPreset
|
||||
)
|
||||
self.menu.addSeparator()
|
||||
|
||||
# "Add Component" submenu
|
||||
self.submenu = QMenu("Add")
|
||||
self.menu.addMenu(self.submenu)
|
||||
insertCompAtTop = self.settings.value("pref_insertCompAtTop")
|
||||
for i, comp in enumerate(self.core.modules):
|
||||
menuItem = self.submenu.addAction(comp.Component.name)
|
||||
menuItem.triggered.connect(
|
||||
lambda _, item=i: self.core.insertComponent(
|
||||
0 if insertCompAtTop else index, item, self
|
||||
)
|
||||
)
|
||||
|
||||
self.menu.move(parentPosition + QPos)
|
||||
self.menu.show()
|
||||
|
||||
def eventFilter(self, object, event):
|
||||
if event.type() == QtCore.QEvent.WindowActivate \
|
||||
or event.type() == QtCore.QEvent.FocusIn:
|
||||
Core.windowHasFocus = True
|
||||
elif event.type() == QtCore.QEvent.WindowDeactivate \
|
||||
or event.type() == QtCore.QEvent.FocusOut:
|
||||
Core.windowHasFocus = False
|
||||
return False
|
|
@ -0,0 +1,828 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1008</width>
|
||||
<height>575</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::StrongFocus</enum>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="autoFillBackground">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>9</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::MinimumExpanding</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>360</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_previewWrapper">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_previewSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::MinimumExpanding</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>420</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinimumSize</enum>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinimumSize</enum>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_16">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinimumSize</enum>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_6">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>140</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_projects">
|
||||
<property name="text">
|
||||
<string>Projects</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_presets">
|
||||
<property name="text">
|
||||
<string>Presets</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Minimum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>2</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="listWidget_componentList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="tabKeyNavigation">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropOverwriteMode">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::InternalMove</enum>
|
||||
</property>
|
||||
<property name="defaultDropAction">
|
||||
<enum>Qt::MoveAction</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_14">
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_addComponent">
|
||||
<property name="text">
|
||||
<string>Add</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_removeComponent">
|
||||
<property name="text">
|
||||
<string>Remove</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_listMoveUp">
|
||||
<property name="text">
|
||||
<string>Up</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_listMoveDown">
|
||||
<property name="text">
|
||||
<string>Down</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<property name="leftMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetFixedSize</enum>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>500</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>180</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="tabPosition">
|
||||
<enum>QTabWidget::North</enum>
|
||||
</property>
|
||||
<property name="tabShape">
|
||||
<enum>QTabWidget::Rounded</enum>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tab_exportVideo">
|
||||
<attribute name="title">
|
||||
<string>Export Video</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_10">
|
||||
<property name="margin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_audioFile">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>85</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Audio File</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_audioFile">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>28</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>28</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="toolButton_selectAudioFile">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>28</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>28</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_11">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_outputFile">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>85</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Output File</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_outputFile">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>28</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>28</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="toolButton_selectOutputFile">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>28</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>28</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar_createVideo">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>24</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Minimum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_createVideo">
|
||||
<property name="text">
|
||||
<string>Create Video</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_Cancel">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="progressLabel">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>-1</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<zorder></zorder>
|
||||
<zorder></zorder>
|
||||
<zorder>progressLabel</zorder>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_encoderSettings">
|
||||
<attribute name="title">
|
||||
<string>Encoder Settings</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_9">
|
||||
<property name="margin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_13">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_videoFormat">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>85</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Container</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_videoContainer">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Minimum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>5</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_videoPreset">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Resolution</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_resolution">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_10">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_videoCodec">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>85</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Video Codec</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_videoCodec">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>5</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_resolution">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Video Bitrate (Kbps)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_vBitrate">
|
||||
<property name="maximum">
|
||||
<number>99999</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_11">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_audioCodec">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>85</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Audio Codec</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_audioCodec">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_audioBitrate">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Audio Bitrate (Kbps)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_aBitrate">
|
||||
<property name="maximum">
|
||||
<number>9999</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::MinimumExpanding</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>500</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="stackedWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>180</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>180</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>-1</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -0,0 +1,358 @@
|
|||
'''
|
||||
Preset manager object handles all interactions with presets, including
|
||||
the context menu accessed from MainWindow.
|
||||
'''
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
import string
|
||||
import os
|
||||
|
||||
from toolkit import badName
|
||||
from core import Core
|
||||
|
||||
|
||||
class PresetManager(QtWidgets.QDialog):
|
||||
def __init__(self, window, parent):
|
||||
super().__init__(parent.window)
|
||||
self.parent = parent
|
||||
self.core = parent.core
|
||||
self.settings = parent.settings
|
||||
self.presetDir = parent.presetDir
|
||||
if not self.settings.value('presetDir'):
|
||||
self.settings.setValue(
|
||||
"presetDir",
|
||||
os.path.join(parent.dataDir, 'projects'))
|
||||
|
||||
self.findPresets()
|
||||
|
||||
# window
|
||||
self.lastFilter = '*'
|
||||
self.presetRows = [] # list of (comp, vers, name) tuples
|
||||
self.window = window
|
||||
self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
|
||||
|
||||
# connect button signals
|
||||
self.window.pushButton_delete.clicked.connect(
|
||||
self.openDeletePresetDialog
|
||||
)
|
||||
self.window.pushButton_rename.clicked.connect(
|
||||
self.openRenamePresetDialog
|
||||
)
|
||||
self.window.pushButton_import.clicked.connect(
|
||||
self.openImportDialog
|
||||
)
|
||||
self.window.pushButton_export.clicked.connect(
|
||||
self.openExportDialog
|
||||
)
|
||||
self.window.pushButton_close.clicked.connect(
|
||||
self.window.close
|
||||
)
|
||||
|
||||
# create filter box and preset list
|
||||
self.drawFilterList()
|
||||
self.window.comboBox_filter.currentIndexChanged.connect(
|
||||
lambda: self.drawPresetList(
|
||||
self.window.comboBox_filter.currentText(),
|
||||
self.window.lineEdit_search.text()
|
||||
)
|
||||
)
|
||||
|
||||
# make auto-completion for search bar
|
||||
self.autocomplete = QtCore.QStringListModel()
|
||||
completer = QtWidgets.QCompleter()
|
||||
completer.setModel(self.autocomplete)
|
||||
self.window.lineEdit_search.setCompleter(completer)
|
||||
self.window.lineEdit_search.textChanged.connect(
|
||||
lambda: self.drawPresetList(
|
||||
self.window.comboBox_filter.currentText(),
|
||||
self.window.lineEdit_search.text()
|
||||
)
|
||||
)
|
||||
self.drawPresetList('*')
|
||||
|
||||
def show(self):
|
||||
'''Open a new preset manager window from the mainwindow'''
|
||||
self.findPresets()
|
||||
self.drawFilterList()
|
||||
self.drawPresetList('*')
|
||||
self.window.show()
|
||||
|
||||
def findPresets(self):
|
||||
parseList = []
|
||||
for dirpath, dirnames, filenames in os.walk(self.presetDir):
|
||||
# anything without a subdirectory must be a preset folder
|
||||
if dirnames:
|
||||
continue
|
||||
for preset in filenames:
|
||||
compName = os.path.basename(os.path.dirname(dirpath))
|
||||
if compName not in self.core.compNames:
|
||||
continue
|
||||
compVers = os.path.basename(dirpath)
|
||||
try:
|
||||
parseList.append((compName, int(compVers), preset))
|
||||
except ValueError:
|
||||
continue
|
||||
self.presets = {
|
||||
compName: [
|
||||
(vers, preset)
|
||||
for name, vers, preset in parseList
|
||||
if name == compName
|
||||
]
|
||||
for compName, _, __ in parseList
|
||||
}
|
||||
|
||||
def drawPresetList(self, compFilter=None, presetFilter=''):
|
||||
self.window.listWidget_presets.clear()
|
||||
if compFilter:
|
||||
self.lastFilter = str(compFilter)
|
||||
else:
|
||||
compFilter = str(self.lastFilter)
|
||||
self.presetRows = []
|
||||
presetNames = []
|
||||
for component, presets in self.presets.items():
|
||||
if compFilter != '*' and component != compFilter:
|
||||
continue
|
||||
for vers, preset in presets:
|
||||
if not presetFilter or presetFilter in preset:
|
||||
self.window.listWidget_presets.addItem(
|
||||
'%s: %s' % (component, preset)
|
||||
)
|
||||
self.presetRows.append((component, vers, preset))
|
||||
if preset not in presetNames:
|
||||
presetNames.append(preset)
|
||||
self.autocomplete.setStringList(presetNames)
|
||||
|
||||
def drawFilterList(self):
|
||||
self.window.comboBox_filter.clear()
|
||||
self.window.comboBox_filter.addItem('*')
|
||||
for component in self.presets:
|
||||
self.window.comboBox_filter.addItem(component)
|
||||
|
||||
def clearPreset(self, compI=None):
|
||||
'''Functions on mainwindow level from the context menu'''
|
||||
compI = self.parent.window.listWidget_componentList.currentRow()
|
||||
self.core.clearPreset(compI)
|
||||
self.parent.updateComponentTitle(compI, False)
|
||||
|
||||
def openSavePresetDialog(self):
|
||||
'''Functions on mainwindow level from the context menu'''
|
||||
window = self.parent.window
|
||||
selectedComponents = self.core.selectedComponents
|
||||
componentList = self.parent.window.listWidget_componentList
|
||||
|
||||
if componentList.currentRow() == -1:
|
||||
return
|
||||
while True:
|
||||
index = componentList.currentRow()
|
||||
currentPreset = selectedComponents[index].currentPreset
|
||||
newName, OK = QtWidgets.QInputDialog.getText(
|
||||
self.parent.window,
|
||||
'Audio Visualizer',
|
||||
'New Preset Name:',
|
||||
QtWidgets.QLineEdit.Normal,
|
||||
currentPreset
|
||||
)
|
||||
if OK:
|
||||
if badName(newName):
|
||||
self.warnMessage(self.parent.window)
|
||||
continue
|
||||
if newName:
|
||||
if index != -1:
|
||||
selectedComponents[index].currentPreset = newName
|
||||
saveValueStore = \
|
||||
selectedComponents[index].savePreset()
|
||||
saveValueStore['preset'] = newName
|
||||
componentName = str(selectedComponents[index]).strip()
|
||||
vers = selectedComponents[index].version
|
||||
self.createNewPreset(
|
||||
componentName, vers, newName,
|
||||
saveValueStore, window=self.parent.window)
|
||||
self.findPresets()
|
||||
self.drawPresetList()
|
||||
self.openPreset(newName, index)
|
||||
break
|
||||
|
||||
def createNewPreset(
|
||||
self, compName, vers, filename, saveValueStore, **kwargs):
|
||||
path = os.path.join(self.presetDir, compName, str(vers), filename)
|
||||
if self.presetExists(path, **kwargs):
|
||||
return
|
||||
self.core.createPresetFile(compName, vers, filename, saveValueStore)
|
||||
|
||||
def presetExists(self, path, **kwargs):
|
||||
if os.path.exists(path):
|
||||
window = self.window \
|
||||
if 'window' not in kwargs else kwargs['window']
|
||||
ch = self.parent.showMessage(
|
||||
msg="%s already exists! Overwrite it?" %
|
||||
os.path.basename(path),
|
||||
showCancel=True,
|
||||
icon='Warning',
|
||||
parent=window)
|
||||
if not ch:
|
||||
# user clicked cancel
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def openPreset(self, presetName, compPos=None):
|
||||
componentList = self.parent.window.listWidget_componentList
|
||||
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
|
||||
dirname = os.path.join(self.presetDir, componentName, str(version))
|
||||
filepath = os.path.join(dirname, presetName)
|
||||
self.core.openPreset(filepath, index, presetName)
|
||||
|
||||
self.parent.updateComponentTitle(index)
|
||||
self.parent.drawPreview()
|
||||
|
||||
def openDeletePresetDialog(self):
|
||||
row = self.getPresetRow()
|
||||
if row == -1:
|
||||
return
|
||||
comp, vers, name = self.presetRows[row]
|
||||
ch = self.parent.showMessage(
|
||||
msg='Really delete %s?' % name,
|
||||
showCancel=True,
|
||||
icon='Warning',
|
||||
parent=self.window
|
||||
)
|
||||
if not ch:
|
||||
return
|
||||
self.deletePreset(comp, vers, name)
|
||||
self.findPresets()
|
||||
self.drawPresetList()
|
||||
|
||||
for i, comp in enumerate(self.core.selectedComponents):
|
||||
if comp.currentPreset == name:
|
||||
self.clearPreset(i)
|
||||
|
||||
def deletePreset(self, comp, vers, name):
|
||||
filepath = os.path.join(self.presetDir, comp, str(vers), name)
|
||||
os.remove(filepath)
|
||||
|
||||
def warnMessage(self, window=None):
|
||||
self.parent.showMessage(
|
||||
msg='Preset names must contain only letters, '
|
||||
'numbers, and spaces.',
|
||||
parent=window if window else self.window)
|
||||
|
||||
def getPresetRow(self):
|
||||
row = self.window.listWidget_presets.currentRow()
|
||||
if row > -1:
|
||||
return row
|
||||
|
||||
# check if component selected in MainWindow has preset loaded
|
||||
componentList = self.parent.window.listWidget_componentList
|
||||
compIndex = componentList.currentRow()
|
||||
if compIndex == -1:
|
||||
return compIndex
|
||||
|
||||
preset = self.core.selectedComponents[compIndex].currentPreset
|
||||
if preset is None:
|
||||
return -1
|
||||
else:
|
||||
rowTuple = (
|
||||
self.core.selectedComponents[compIndex].name,
|
||||
self.core.selectedComponents[compIndex].version,
|
||||
preset
|
||||
)
|
||||
for i, tup in enumerate(self.presetRows):
|
||||
if rowTuple == tup:
|
||||
index = i
|
||||
break
|
||||
else:
|
||||
return -1
|
||||
return index
|
||||
|
||||
def openRenamePresetDialog(self):
|
||||
# TODO: maintain consistency by changing this to call createNewPreset()
|
||||
presetList = self.window.listWidget_presets
|
||||
index = self.getPresetRow()
|
||||
if index == -1:
|
||||
return
|
||||
|
||||
while True:
|
||||
newName, OK = QtWidgets.QInputDialog.getText(
|
||||
self.window,
|
||||
'Preset Manager',
|
||||
'Rename Preset:',
|
||||
QtWidgets.QLineEdit.Normal,
|
||||
self.presetRows[index][2]
|
||||
)
|
||||
if OK:
|
||||
if badName(newName):
|
||||
self.warnMessage()
|
||||
continue
|
||||
if newName:
|
||||
comp, vers, oldName = self.presetRows[index]
|
||||
path = os.path.join(
|
||||
self.presetDir, comp, str(vers))
|
||||
newPath = os.path.join(path, newName)
|
||||
oldPath = os.path.join(path, oldName)
|
||||
if self.presetExists(newPath):
|
||||
return
|
||||
if os.path.exists(newPath):
|
||||
os.remove(newPath)
|
||||
os.rename(oldPath, newPath)
|
||||
self.findPresets()
|
||||
self.drawPresetList()
|
||||
for i, comp in enumerate(self.core.selectedComponents):
|
||||
if 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):
|
||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self.window, "Import Preset File",
|
||||
self.settings.value("presetDir"),
|
||||
"Preset Files (*.avl)")
|
||||
if filename:
|
||||
# get installed path & ask user to overwrite if needed
|
||||
path = ''
|
||||
while True:
|
||||
if path:
|
||||
if self.presetExists(path):
|
||||
break
|
||||
else:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
success, path = self.core.importPreset(filename)
|
||||
if success:
|
||||
break
|
||||
|
||||
self.findPresets()
|
||||
self.drawPresetList()
|
||||
self.settings.setValue("presetDir", os.path.dirname(filename))
|
||||
|
||||
def openExportDialog(self):
|
||||
index = self.getPresetRow()
|
||||
if index == -1:
|
||||
return
|
||||
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
|
||||
self.window, "Export Preset",
|
||||
self.settings.value("presetDir"),
|
||||
"Preset Files (*.avl)")
|
||||
if filename:
|
||||
comp, vers, name = self.presetRows[index]
|
||||
if not self.core.exportPreset(filename, comp, vers, name):
|
||||
self.parent.showMessage(
|
||||
msg='Couldn\'t export %s.' % filename,
|
||||
parent=self.window
|
||||
)
|
||||
self.settings.setValue("presetDir", os.path.dirname(filename))
|
||||
|
||||
def clearPresetListSelection(self):
|
||||
self.window.listWidget_presets.setCurrentRow(-1)
|
||||
|
||||
|
||||
def getPresetDir(comp):
|
||||
'''Get the preset subdir for a particular version of a component'''
|
||||
return os.path.join(Core.presetDir, str(comp), str(comp.version))
|
|
@ -0,0 +1,150 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>presetmanager</class>
|
||||
<widget class="QWidget" name="presetmanager">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::NonModal</enum>
|
||||
</property>
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>497</width>
|
||||
<height>377</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Preset Manager</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_search">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>Filter by name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_filter">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QListWidget" name="listWidget_presets">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="tabKeyNavigation">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinimumSize</enum>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_import">
|
||||
<property name="text">
|
||||
<string>Import</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_export">
|
||||
<property name="text">
|
||||
<string>Export</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_rename">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Rename</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_delete">
|
||||
<property name="text">
|
||||
<string>Delete</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item alignment="Qt::AlignRight">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string><html><head/><body><p><span style=" font-size:10pt; font-style:italic;">Right-click components in the main window to create presets</span></p></body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_close">
|
||||
<property name="text">
|
||||
<string>Close</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -0,0 +1,85 @@
|
|||
'''
|
||||
Thread that runs to create QImages for MainWindow's preview label.
|
||||
Processes a queue of component lists.
|
||||
'''
|
||||
from PyQt5 import QtCore, QtGui, uic
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot
|
||||
from PIL import Image
|
||||
from PIL.ImageQt import ImageQt
|
||||
from queue import Queue, Empty
|
||||
import os
|
||||
|
||||
from toolkit.frame import Checkerboard
|
||||
from toolkit import disableWhenOpeningProject
|
||||
|
||||
|
||||
class Worker(QtCore.QObject):
|
||||
|
||||
imageCreated = pyqtSignal(QtGui.QImage)
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None, queue=None):
|
||||
QtCore.QObject.__init__(self)
|
||||
parent.newTask.connect(self.createPreviewImage)
|
||||
parent.processTask.connect(self.process)
|
||||
self.parent = parent
|
||||
self.core = parent.core
|
||||
self.settings = parent.settings
|
||||
self.queue = queue
|
||||
|
||||
width = int(self.settings.value('outputWidth'))
|
||||
height = int(self.settings.value('outputHeight'))
|
||||
self.background = Checkerboard(width, height)
|
||||
|
||||
@disableWhenOpeningProject
|
||||
@pyqtSlot(list)
|
||||
def createPreviewImage(self, components):
|
||||
dic = {
|
||||
"components": components,
|
||||
}
|
||||
self.queue.put(dic)
|
||||
|
||||
@pyqtSlot()
|
||||
def process(self):
|
||||
width = int(self.settings.value('outputWidth'))
|
||||
height = int(self.settings.value('outputHeight'))
|
||||
try:
|
||||
nextPreviewInformation = self.queue.get(block=False)
|
||||
while self.queue.qsize() >= 2:
|
||||
try:
|
||||
self.queue.get(block=False)
|
||||
except Empty:
|
||||
continue
|
||||
if self.background.width != width \
|
||||
or self.background.height != height:
|
||||
self.background = Checkerboard(width, height)
|
||||
|
||||
frame = self.background.copy()
|
||||
|
||||
components = nextPreviewInformation["components"]
|
||||
for component in reversed(components):
|
||||
try:
|
||||
component.lockSize(width, height)
|
||||
newFrame = component.previewRender()
|
||||
component.unlockSize()
|
||||
frame = Image.alpha_composite(
|
||||
frame, newFrame
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
errMsg = "Bad frame returned by %s's preview renderer. " \
|
||||
"%s. New frame size was %s*%s; should be %s*%s." % (
|
||||
str(component), str(e).capitalize(),
|
||||
newFrame.width, newFrame.height,
|
||||
width, height
|
||||
)
|
||||
self.error.emit(errMsg)
|
||||
break
|
||||
except RuntimeError as e:
|
||||
print(e)
|
||||
else:
|
||||
self.frame = ImageQt(frame)
|
||||
self.imageCreated.emit(QtGui.QImage(self.frame))
|
||||
|
||||
except Empty:
|
||||
True
|
|
@ -0,0 +1 @@
|
|||
from toolkit.common import *
|
|
@ -0,0 +1,139 @@
|
|||
'''
|
||||
Common functions
|
||||
'''
|
||||
from PyQt5 import QtWidgets
|
||||
import string
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
def badName(name):
|
||||
'''Returns whether a name contains non-alphanumeric chars'''
|
||||
return any([letter in string.punctuation for letter in name])
|
||||
|
||||
|
||||
def alphabetizeDict(dictionary):
|
||||
'''Alphabetizes a dict into OrderedDict '''
|
||||
return OrderedDict(sorted(dictionary.items(), key=lambda t: t[0]))
|
||||
|
||||
|
||||
def presetToString(dictionary):
|
||||
'''Returns string repr of a preset'''
|
||||
return repr(alphabetizeDict(dictionary))
|
||||
|
||||
|
||||
def presetFromString(string):
|
||||
'''Turns a string repr of OrderedDict into a regular dict'''
|
||||
return dict(eval(string))
|
||||
|
||||
|
||||
def appendUppercase(lst):
|
||||
for form, i in zip(lst, range(len(lst))):
|
||||
lst.append(form.upper())
|
||||
return lst
|
||||
|
||||
def pipeWrapper(func):
|
||||
'''A decorator to insert proper kwargs into Popen objects.'''
|
||||
def pipeWrapper(commandList, **kwargs):
|
||||
if sys.platform == 'win32':
|
||||
# Stop CMD window from appearing on Windows
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
kwargs['startupinfo'] = startupinfo
|
||||
|
||||
if 'bufsize' not in kwargs:
|
||||
kwargs['bufsize'] = 10**8
|
||||
if 'stdin' not in kwargs:
|
||||
kwargs['stdin'] = subprocess.DEVNULL
|
||||
return func(commandList, **kwargs)
|
||||
return pipeWrapper
|
||||
|
||||
|
||||
@pipeWrapper
|
||||
def checkOutput(commandList, **kwargs):
|
||||
return subprocess.check_output(commandList, **kwargs)
|
||||
|
||||
|
||||
def disableWhenEncoding(func):
|
||||
def decorator(self, *args, **kwargs):
|
||||
if self.encoding:
|
||||
return
|
||||
else:
|
||||
return func(self, *args, **kwargs)
|
||||
return decorator
|
||||
|
||||
|
||||
def disableWhenOpeningProject(func):
|
||||
def decorator(self, *args, **kwargs):
|
||||
if self.core.openingProject:
|
||||
return
|
||||
else:
|
||||
return func(self, *args, **kwargs)
|
||||
return decorator
|
||||
|
||||
|
||||
def rgbFromString(string):
|
||||
'''Turns an RGB string like "255, 255, 255" into a tuple'''
|
||||
try:
|
||||
tup = tuple([int(i) for i in string.split(',')])
|
||||
if len(tup) != 3:
|
||||
raise ValueError
|
||||
for i in tup:
|
||||
if i > 255 or i < 0:
|
||||
raise ValueError
|
||||
return tup
|
||||
except:
|
||||
return (255, 255, 255)
|
||||
|
||||
|
||||
def formatTraceback(tb=None):
|
||||
import traceback
|
||||
if tb is None:
|
||||
import sys
|
||||
tb = sys.exc_info()[2]
|
||||
return 'Traceback:\n%s' % "\n".join(traceback.format_tb(tb))
|
||||
|
||||
|
||||
def connectWidget(widget, func):
|
||||
if type(widget) == QtWidgets.QLineEdit:
|
||||
widget.textChanged.connect(func)
|
||||
elif type(widget) == QtWidgets.QSpinBox \
|
||||
or type(widget) == QtWidgets.QDoubleSpinBox:
|
||||
widget.valueChanged.connect(func)
|
||||
elif type(widget) == QtWidgets.QCheckBox:
|
||||
widget.stateChanged.connect(func)
|
||||
elif type(widget) == QtWidgets.QComboBox:
|
||||
widget.currentIndexChanged.connect(func)
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def setWidgetValue(widget, val):
|
||||
'''Generic setValue method for use with any typical QtWidget'''
|
||||
if type(widget) == QtWidgets.QLineEdit:
|
||||
widget.setText(val)
|
||||
elif type(widget) == QtWidgets.QSpinBox \
|
||||
or type(widget) == QtWidgets.QDoubleSpinBox:
|
||||
widget.setValue(val)
|
||||
elif type(widget) == QtWidgets.QCheckBox:
|
||||
widget.setChecked(val)
|
||||
elif type(widget) == QtWidgets.QComboBox:
|
||||
widget.setCurrentIndex(val)
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def getWidgetValue(widget):
|
||||
if type(widget) == QtWidgets.QLineEdit:
|
||||
return widget.text()
|
||||
elif type(widget) == QtWidgets.QSpinBox \
|
||||
or type(widget) == QtWidgets.QDoubleSpinBox:
|
||||
return widget.value()
|
||||
elif type(widget) == QtWidgets.QCheckBox:
|
||||
return widget.isChecked()
|
||||
elif type(widget) == QtWidgets.QComboBox:
|
||||
return widget.currentIndex()
|
|
@ -0,0 +1,459 @@
|
|||
'''
|
||||
Tools for using ffmpeg
|
||||
'''
|
||||
import numpy
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import signal
|
||||
from queue import PriorityQueue
|
||||
|
||||
import core
|
||||
from toolkit.common import checkOutput, pipeWrapper
|
||||
from component import ComponentError
|
||||
|
||||
|
||||
class FfmpegVideo:
|
||||
'''Opens a pipe to ffmpeg and stores a buffer of raw video frames.'''
|
||||
|
||||
# error from the thread used to fill the buffer
|
||||
threadError = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
mandatoryArgs = [
|
||||
'inputPath',
|
||||
'filter_',
|
||||
'width',
|
||||
'height',
|
||||
'frameRate', # frames per second
|
||||
'chunkSize', # number of bytes in one frame
|
||||
'parent', # mainwindow object
|
||||
'component', # component object
|
||||
]
|
||||
for arg in mandatoryArgs:
|
||||
setattr(self, arg, kwargs[arg])
|
||||
|
||||
self.frameNo = -1
|
||||
self.currentFrame = 'None'
|
||||
self.map_ = None
|
||||
|
||||
if 'loopVideo' in kwargs and kwargs['loopVideo']:
|
||||
self.loopValue = '-1'
|
||||
else:
|
||||
self.loopValue = '0'
|
||||
if 'filter_' in kwargs:
|
||||
if kwargs['filter_'][0] != '-filter_complex':
|
||||
kwargs['filter_'].insert(0, '-filter_complex')
|
||||
else:
|
||||
kwargs['filter_'] = None
|
||||
|
||||
self.command = [
|
||||
core.Core.FFMPEG_BIN,
|
||||
'-thread_queue_size', '512',
|
||||
'-r', str(self.frameRate),
|
||||
'-stream_loop', self.loopValue,
|
||||
'-i', self.inputPath,
|
||||
'-f', 'image2pipe',
|
||||
'-pix_fmt', 'rgba',
|
||||
]
|
||||
if type(kwargs['filter_']) is list:
|
||||
self.command.extend(
|
||||
kwargs['filter_']
|
||||
)
|
||||
self.command.extend([
|
||||
'-codec:v', 'rawvideo', '-',
|
||||
])
|
||||
|
||||
self.frameBuffer = PriorityQueue()
|
||||
self.frameBuffer.maxsize = self.frameRate
|
||||
self.finishedFrames = {}
|
||||
|
||||
self.thread = threading.Thread(
|
||||
target=self.fillBuffer,
|
||||
name='FFmpeg Frame-Fetcher'
|
||||
)
|
||||
self.thread.daemon = True
|
||||
self.thread.start()
|
||||
|
||||
def frame(self, num):
|
||||
while True:
|
||||
if num in self.finishedFrames:
|
||||
image = self.finishedFrames.pop(num)
|
||||
return image
|
||||
|
||||
i, image = self.frameBuffer.get()
|
||||
self.finishedFrames[i] = image
|
||||
self.frameBuffer.task_done()
|
||||
|
||||
def fillBuffer(self):
|
||||
logFilename = os.path.join(
|
||||
core.Core.dataDir, 'extra_%s.log' % str(self.component.compPos))
|
||||
with open(logFilename, 'w') as log:
|
||||
log.write(" ".join(self.command) + '\n\n')
|
||||
with open(logFilename, 'a') as log:
|
||||
self.pipe = openPipe(
|
||||
self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=log, bufsize=10**8
|
||||
)
|
||||
while True:
|
||||
if self.parent.canceled:
|
||||
break
|
||||
self.frameNo += 1
|
||||
|
||||
# If we run out of frames, use the last good frame and loop.
|
||||
try:
|
||||
if len(self.currentFrame) == 0:
|
||||
self.frameBuffer.put((self.frameNo-1, self.lastFrame))
|
||||
continue
|
||||
except AttributeError:
|
||||
FfmpegVideo.threadError = ComponentError(
|
||||
self.component, 'video',
|
||||
"Video seemed playable but wasn't."
|
||||
)
|
||||
break
|
||||
|
||||
try:
|
||||
self.currentFrame = self.pipe.stdout.read(self.chunkSize)
|
||||
except ValueError:
|
||||
FfmpegVideo.threadError = ComponentError(
|
||||
self.component, 'video')
|
||||
|
||||
if len(self.currentFrame) != 0:
|
||||
self.frameBuffer.put((self.frameNo, self.currentFrame))
|
||||
self.lastFrame = self.currentFrame
|
||||
|
||||
|
||||
@pipeWrapper
|
||||
def openPipe(commandList, **kwargs):
|
||||
return subprocess.Popen(commandList, **kwargs)
|
||||
|
||||
|
||||
def closePipe(pipe):
|
||||
pipe.stdout.close()
|
||||
pipe.send_signal(signal.SIGINT)
|
||||
|
||||
|
||||
def findFfmpeg():
|
||||
if getattr(sys, 'frozen', False):
|
||||
# The application is frozen
|
||||
if sys.platform == "win32":
|
||||
return os.path.join(core.Core.wd, 'ffmpeg.exe')
|
||||
else:
|
||||
return os.path.join(core.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 subprocess.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
|
||||
Core = core.Core
|
||||
|
||||
# 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
|
||||
]
|
||||
|
||||
extraAudio = [
|
||||
comp.audio for comp in components
|
||||
if 'audio' in comp.properties()
|
||||
]
|
||||
segment = createAudioFilterCommand(extraAudio, safeDuration)
|
||||
ffmpegCommand.extend(segment)
|
||||
if segment:
|
||||
# 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 createAudioFilterCommand(extraAudio, duration):
|
||||
'''Add extra inputs and any needed filters to the main ffmpeg command.'''
|
||||
# NOTE: Global filters are currently hard-coded here for debugging use
|
||||
globalFilters = 0 # increase to add global filters
|
||||
|
||||
if not extraAudio and not globalFilters:
|
||||
return []
|
||||
|
||||
ffmpegCommand = []
|
||||
# Add -i options for extra input files
|
||||
extraFilters = {}
|
||||
for streamNo, params in enumerate(reversed(extraAudio)):
|
||||
extraInputFile, params = params
|
||||
ffmpegCommand.extend([
|
||||
'-t', duration,
|
||||
# Tell ffmpeg about shorter clips (seemingly not needed)
|
||||
# streamDuration = getAudioDuration(extraInputFile)
|
||||
# if streamDuration and 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)
|
||||
),
|
||||
])
|
||||
return ffmpegCommand
|
||||
|
||||
|
||||
def testAudioStream(filename):
|
||||
'''Test if an audio stream definitely exists'''
|
||||
audioTestCommand = [
|
||||
core.Core.FFMPEG_BIN,
|
||||
'-i', filename,
|
||||
'-vn', '-f', 'null', '-'
|
||||
]
|
||||
try:
|
||||
checkOutput(audioTestCommand, stderr=subprocess.DEVNULL)
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def getAudioDuration(filename):
|
||||
'''Try to get duration of audio file as float, or False if not possible'''
|
||||
command = [core.Core.FFMPEG_BIN, '-i', filename]
|
||||
|
||||
try:
|
||||
fileInfo = checkOutput(command, stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as ex:
|
||||
fileInfo = ex.output
|
||||
|
||||
try:
|
||||
info = fileInfo.decode("utf-8").split('\n')
|
||||
except UnicodeDecodeError as e:
|
||||
print('Unicode error:', str(e))
|
||||
return False
|
||||
|
||||
for line in info:
|
||||
if 'Duration' in line:
|
||||
d = line.split(',')[0]
|
||||
d = d.split(' ')[3]
|
||||
d = d.split(':')
|
||||
duration = float(d[0])*3600 + float(d[1])*60 + float(d[2])
|
||||
break
|
||||
else:
|
||||
# String not found in output
|
||||
return False
|
||||
return duration
|
||||
|
||||
|
||||
def readAudioFile(filename, videoWorker):
|
||||
'''
|
||||
Creates the completeAudioArray given to components
|
||||
and used to draw the classic visualizer.
|
||||
'''
|
||||
duration = getAudioDuration(filename)
|
||||
if not duration:
|
||||
print('Audio file doesn\'t exist or unreadable.')
|
||||
return
|
||||
|
||||
command = [
|
||||
core.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=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8
|
||||
)
|
||||
|
||||
completeAudioArray = numpy.empty(0, dtype="int16")
|
||||
|
||||
progress = 0
|
||||
lastPercent = None
|
||||
while True:
|
||||
if core.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)+'%'
|
||||
videoWorker.progressBarSetText.emit(string)
|
||||
videoWorker.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 exampleSound():
|
||||
return (
|
||||
'aevalsrc=tan(random(1)*PI*t)*sin(random(0)*2*PI*t),'
|
||||
'apulsator=offset_l=0.5:offset_r=0.5,'
|
||||
)
|
|
@ -0,0 +1,94 @@
|
|||
'''
|
||||
Common tools for drawing compatible frames in a Component's frameRender()
|
||||
'''
|
||||
from PyQt5 import QtGui
|
||||
from PIL import Image
|
||||
from PIL.ImageQt import ImageQt
|
||||
import sys
|
||||
import os
|
||||
import math
|
||||
|
||||
import core
|
||||
|
||||
|
||||
class FramePainter(QtGui.QPainter):
|
||||
'''
|
||||
A QPainter for a blank frame, which can be converted into a
|
||||
Pillow image with finalize()
|
||||
'''
|
||||
def __init__(self, width, height):
|
||||
image = BlankFrame(width, height)
|
||||
self.image = QtGui.QImage(ImageQt(image))
|
||||
super().__init__(self.image)
|
||||
|
||||
def setPen(self, RgbTuple):
|
||||
super().setPen(PaintColor(*RgbTuple))
|
||||
|
||||
def finalize(self):
|
||||
self.end()
|
||||
imBytes = self.image.bits().asstring(self.image.byteCount())
|
||||
|
||||
return Image.frombytes(
|
||||
'RGBA', (self.image.width(), self.image.height()), imBytes
|
||||
)
|
||||
|
||||
|
||||
class PaintColor(QtGui.QColor):
|
||||
'''Reverse the painter colour if the hardware stores RGB values backward'''
|
||||
def __init__(self, r, g, b, a=255):
|
||||
if sys.byteorder == 'big':
|
||||
super().__init__(r, g, b, a)
|
||||
else:
|
||||
super().__init__(b, g, r, a)
|
||||
|
||||
|
||||
def scale(scalePercent, width, height, returntype=None):
|
||||
width = (float(width) / 100.0) * float(scalePercent)
|
||||
height = (float(height) / 100.0) * float(scalePercent)
|
||||
if returntype == str:
|
||||
return (str(math.ceil(width)), str(math.ceil(height)))
|
||||
elif returntype == int:
|
||||
return (math.ceil(width), math.ceil(height))
|
||||
else:
|
||||
return (width, height)
|
||||
|
||||
|
||||
def defaultSize(framefunc):
|
||||
'''Makes width/height arguments optional'''
|
||||
def decorator(*args):
|
||||
if len(args) < 2:
|
||||
newArgs = list(args)
|
||||
if len(args) == 0 or len(args) == 1:
|
||||
height = int(core.Core.settings.value("outputHeight"))
|
||||
newArgs.append(height)
|
||||
if len(args) == 0:
|
||||
width = int(core.Core.settings.value("outputWidth"))
|
||||
newArgs.insert(0, width)
|
||||
args = tuple(newArgs)
|
||||
return framefunc(*args)
|
||||
return decorator
|
||||
|
||||
|
||||
def FloodFrame(width, height, RgbaTuple):
|
||||
return Image.new("RGBA", (width, height), RgbaTuple)
|
||||
|
||||
|
||||
@defaultSize
|
||||
def BlankFrame(width, height):
|
||||
'''The base frame used by each component to start drawing.'''
|
||||
return FloodFrame(width, height, (0, 0, 0, 0))
|
||||
|
||||
|
||||
@defaultSize
|
||||
def Checkerboard(width, height):
|
||||
'''
|
||||
A checkerboard to represent transparency to the user.
|
||||
TODO: Would be cool to generate this image with numpy instead.
|
||||
'''
|
||||
image = FloodFrame(1920, 1080, (0, 0, 0, 0))
|
||||
image.paste(Image.open(
|
||||
os.path.join(core.Core.wd, "background.png")),
|
||||
(0, 0)
|
||||
)
|
||||
image = image.resize((width, height))
|
||||
return image
|
|
@ -0,0 +1,362 @@
|
|||
'''
|
||||
Thread created to export a video. It has a slot to begin export using
|
||||
an input file, output path, and component list. During export multiple
|
||||
threads are created to render the video as quickly as possible. Signals
|
||||
are emitted to update MainWindow's progress bar, detail text, and preview.
|
||||
Export can be cancelled with cancel()
|
||||
'''
|
||||
from PyQt5 import QtCore, QtGui
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot
|
||||
from PIL import Image
|
||||
from PIL.ImageQt import ImageQt
|
||||
import numpy
|
||||
import subprocess as sp
|
||||
import sys
|
||||
import os
|
||||
from queue import Queue, PriorityQueue
|
||||
from threading import Thread, Event
|
||||
import time
|
||||
import signal
|
||||
|
||||
from component import ComponentError
|
||||
from toolkit.frame import Checkerboard
|
||||
from toolkit.ffmpeg import (
|
||||
openPipe, readAudioFile,
|
||||
getAudioDuration, createFfmpegCommand
|
||||
)
|
||||
|
||||
|
||||
class Worker(QtCore.QObject):
|
||||
|
||||
imageCreated = pyqtSignal(['QImage'])
|
||||
videoCreated = pyqtSignal()
|
||||
progressBarUpdate = pyqtSignal(int)
|
||||
progressBarSetText = pyqtSignal(str)
|
||||
encoding = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, parent, inputFile, outputFile, components):
|
||||
QtCore.QObject.__init__(self)
|
||||
self.core = parent.core
|
||||
self.settings = parent.settings
|
||||
self.modules = parent.core.modules
|
||||
parent.createVideo.connect(self.createVideo)
|
||||
|
||||
self.parent = parent
|
||||
self.components = components
|
||||
self.outputFile = outputFile
|
||||
self.inputFile = inputFile
|
||||
|
||||
self.sampleSize = 1470 # 44100 / 30 = 1470
|
||||
self.canceled = False
|
||||
self.error = False
|
||||
self.stopped = False
|
||||
|
||||
def renderNode(self):
|
||||
'''
|
||||
Grabs audio data indices at frames to export, from compositeQueue.
|
||||
Sends it to the components' frameRender methods in layer order
|
||||
to create subframes & composite them into the final frame.
|
||||
The resulting frames are collected in the renderQueue
|
||||
'''
|
||||
while not self.stopped:
|
||||
audioI = self.compositeQueue.get()
|
||||
bgI = int(audioI / self.sampleSize)
|
||||
frame = None
|
||||
for layerNo, comp in enumerate(reversed((self.components))):
|
||||
if layerNo in self.staticComponents:
|
||||
if self.staticComponents[layerNo] is None:
|
||||
# this layer was merged into a following layer
|
||||
continue
|
||||
# static component
|
||||
if frame is None: # bottom-most layer
|
||||
frame = self.staticComponents[layerNo]
|
||||
else:
|
||||
frame = Image.alpha_composite(
|
||||
frame, self.staticComponents[layerNo]
|
||||
)
|
||||
else:
|
||||
# animated component
|
||||
if frame is None: # bottom-most layer
|
||||
frame = comp.frameRender(bgI)
|
||||
else:
|
||||
frame = Image.alpha_composite(
|
||||
frame, comp.frameRender(bgI)
|
||||
)
|
||||
|
||||
self.renderQueue.put([audioI, frame])
|
||||
self.compositeQueue.task_done()
|
||||
|
||||
def renderDispatch(self):
|
||||
'''
|
||||
Places audio data indices in the compositeQueue, to be used
|
||||
by a renderNode later. All indices are multiples of self.sampleSize
|
||||
sampleSize * frameNo = audioI, AKA audio data starting at frameNo
|
||||
'''
|
||||
print('Dispatching Frames for Compositing...')
|
||||
|
||||
for audioI in range(0, len(self.completeAudioArray), self.sampleSize):
|
||||
self.compositeQueue.put(audioI)
|
||||
|
||||
def previewDispatch(self):
|
||||
'''
|
||||
Grabs frames from the previewQueue, adds them to the checkerboard
|
||||
and emits a final QImage to the MainWindow for the live preview
|
||||
'''
|
||||
background = Checkerboard(self.width, self.height)
|
||||
|
||||
while not self.stopped:
|
||||
audioI, frame = self.previewQueue.get()
|
||||
if time.time() - self.lastPreview >= 0.06 or audioI == 0:
|
||||
image = Image.alpha_composite(background.copy(), frame)
|
||||
self.imageCreated.emit(QtGui.QImage(ImageQt(image)))
|
||||
self.lastPreview = time.time()
|
||||
|
||||
self.previewQueue.task_done()
|
||||
|
||||
@pyqtSlot()
|
||||
def createVideo(self):
|
||||
numpy.seterr(divide='ignore')
|
||||
self.encoding.emit(True)
|
||||
self.extraAudio = []
|
||||
self.width = int(self.settings.value('outputWidth'))
|
||||
self.height = int(self.settings.value('outputHeight'))
|
||||
|
||||
self.compositeQueue = Queue()
|
||||
self.compositeQueue.maxsize = 20
|
||||
self.renderQueue = PriorityQueue()
|
||||
self.renderQueue.maxsize = 20
|
||||
self.previewQueue = PriorityQueue()
|
||||
|
||||
self.reset()
|
||||
progressBarValue = 0
|
||||
self.progressBarUpdate.emit(progressBarValue)
|
||||
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
# READ AUDIO, INITIALIZE COMPONENTS, OPEN A PIPE TO FFMPEG
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
if any([
|
||||
True if 'pcm' in comp.properties() else False
|
||||
for comp in self.components
|
||||
]):
|
||||
self.progressBarSetText.emit("Loading audio file...")
|
||||
audioFileTraits = readAudioFile(
|
||||
self.inputFile, self
|
||||
)
|
||||
if audioFileTraits is None:
|
||||
self.cancelExport()
|
||||
return
|
||||
self.completeAudioArray, duration = audioFileTraits
|
||||
else:
|
||||
duration = getAudioDuration(self.inputFile)
|
||||
class FakeList:
|
||||
def __len__(self):
|
||||
return int((duration * 44100) + 44100) - 1470
|
||||
self.completeAudioArray = FakeList()
|
||||
|
||||
self.progressBarUpdate.emit(0)
|
||||
self.progressBarSetText.emit("Starting components...")
|
||||
canceledByComponent = False
|
||||
print('Loaded Components:', ", ".join([
|
||||
"%s) %s" % (num, str(component))
|
||||
for num, component in enumerate(reversed(self.components))
|
||||
]))
|
||||
self.staticComponents = {}
|
||||
for compNo, comp in enumerate(reversed(self.components)):
|
||||
try:
|
||||
comp.preFrameRender(
|
||||
audioFile=self.inputFile,
|
||||
completeAudioArray=self.completeAudioArray,
|
||||
sampleSize=self.sampleSize,
|
||||
progressBarUpdate=self.progressBarUpdate,
|
||||
progressBarSetText=self.progressBarSetText
|
||||
)
|
||||
except ComponentError:
|
||||
pass
|
||||
|
||||
compProps = comp.properties()
|
||||
if 'error' in compProps or comp._lockedError is not None:
|
||||
self.cancel()
|
||||
self.canceled = True
|
||||
canceledByComponent = True
|
||||
compError = comp.error() \
|
||||
if type(comp.error()) is tuple else (comp.error(), '')
|
||||
errMsg = (
|
||||
"Component #%s (%s) encountered an error!" % (
|
||||
str(compNo), comp.name
|
||||
)
|
||||
if comp.error() is None else
|
||||
'Export cancelled by component #%s (%s): %s' % (
|
||||
str(compNo),
|
||||
comp.name,
|
||||
compError[0]
|
||||
)
|
||||
)
|
||||
comp._error.emit(errMsg, compError[1])
|
||||
break
|
||||
if 'static' in compProps:
|
||||
self.staticComponents[compNo] = \
|
||||
comp.frameRender(0).copy()
|
||||
|
||||
if self.canceled:
|
||||
if canceledByComponent:
|
||||
print('Export cancelled by component #%s (%s): %s' % (
|
||||
compNo,
|
||||
comp.name,
|
||||
'No message.' if comp.error() is None else (
|
||||
comp.error() if type(comp.error()) is str
|
||||
else comp.error()[0])
|
||||
)
|
||||
)
|
||||
self.cancelExport()
|
||||
return
|
||||
|
||||
# Merge consecutive static component frames together
|
||||
for compNo in range(len(self.components)):
|
||||
if compNo not in self.staticComponents \
|
||||
or compNo + 1 not in self.staticComponents:
|
||||
continue
|
||||
self.staticComponents[compNo + 1] = Image.alpha_composite(
|
||||
self.staticComponents.pop(compNo),
|
||||
self.staticComponents[compNo + 1]
|
||||
)
|
||||
self.staticComponents[compNo] = None
|
||||
|
||||
ffmpegCommand = createFfmpegCommand(
|
||||
self.inputFile, self.outputFile, self.components, duration
|
||||
)
|
||||
print('###### FFMPEG COMMAND ######\n%s' % " ".join(ffmpegCommand))
|
||||
print('############################')
|
||||
self.out_pipe = openPipe(
|
||||
ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
|
||||
)
|
||||
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
# START CREATING THE VIDEO
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
|
||||
# Make 2 or 3 renderNodes in new threads to create the frames
|
||||
self.renderThreads = []
|
||||
try:
|
||||
numCpus = len(os.sched_getaffinity(0))
|
||||
except Exception:
|
||||
numCpus = os.cpu_count()
|
||||
|
||||
for i in range(2 if numCpus <= 2 else 3):
|
||||
self.renderThreads.append(
|
||||
Thread(target=self.renderNode, name="Render Thread"))
|
||||
self.renderThreads[i].daemon = True
|
||||
self.renderThreads[i].start()
|
||||
|
||||
self.dispatchThread = Thread(
|
||||
target=self.renderDispatch, name="Render Dispatch Thread")
|
||||
self.dispatchThread.daemon = True
|
||||
self.dispatchThread.start()
|
||||
|
||||
self.lastPreview = 0.0
|
||||
self.previewDispatch = Thread(
|
||||
target=self.previewDispatch, name="Render Dispatch Thread"
|
||||
)
|
||||
self.previewDispatch.daemon = True
|
||||
self.previewDispatch.start()
|
||||
|
||||
# Begin piping into ffmpeg!
|
||||
frameBuffer = {}
|
||||
progressBarValue = 0
|
||||
self.progressBarUpdate.emit(progressBarValue)
|
||||
self.progressBarSetText.emit("Exporting video...")
|
||||
if not self.canceled:
|
||||
for audioI in range(
|
||||
0, len(self.completeAudioArray), self.sampleSize):
|
||||
while True:
|
||||
if audioI in frameBuffer or self.canceled:
|
||||
# if frame's in buffer, pipe it to ffmpeg
|
||||
break
|
||||
# else fetch the next frame & add to the buffer
|
||||
audioI_, frame = self.renderQueue.get()
|
||||
frameBuffer[audioI_] = frame
|
||||
self.renderQueue.task_done()
|
||||
if self.canceled:
|
||||
break
|
||||
|
||||
try:
|
||||
self.out_pipe.stdin.write(frameBuffer[audioI].tobytes())
|
||||
self.previewQueue.put([audioI, frameBuffer.pop(audioI)])
|
||||
except Exception:
|
||||
break
|
||||
|
||||
# increase progress bar value
|
||||
completion = (audioI / len(self.completeAudioArray)) * 100
|
||||
if progressBarValue + 1 <= completion:
|
||||
progressBarValue = numpy.floor(completion)
|
||||
self.progressBarUpdate.emit(progressBarValue)
|
||||
self.progressBarSetText.emit(
|
||||
"Exporting video: %s%%" % str(int(progressBarValue))
|
||||
)
|
||||
|
||||
numpy.seterr(all='print')
|
||||
|
||||
try:
|
||||
self.out_pipe.stdin.close()
|
||||
except BrokenPipeError:
|
||||
print('Broken pipe to ffmpeg!')
|
||||
if self.out_pipe.stderr is not None:
|
||||
print(self.out_pipe.stderr.read())
|
||||
self.out_pipe.stderr.close()
|
||||
self.error = True
|
||||
self.out_pipe.wait()
|
||||
|
||||
for comp in reversed(self.components):
|
||||
comp.postFrameRender()
|
||||
|
||||
if self.canceled:
|
||||
print("Export Canceled")
|
||||
try:
|
||||
os.remove(self.outputFile)
|
||||
except Exception:
|
||||
pass
|
||||
self.progressBarUpdate.emit(0)
|
||||
self.progressBarSetText.emit('Export Canceled')
|
||||
else:
|
||||
if self.error:
|
||||
print("Export Failed")
|
||||
self.progressBarUpdate.emit(0)
|
||||
self.progressBarSetText.emit('Export Failed')
|
||||
else:
|
||||
print("Export Complete")
|
||||
self.progressBarUpdate.emit(100)
|
||||
self.progressBarSetText.emit('Export Complete')
|
||||
|
||||
self.error = False
|
||||
self.canceled = False
|
||||
self.stopped = True
|
||||
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)
|
||||
|
||||
def cancel(self):
|
||||
self.canceled = True
|
||||
self.stopped = True
|
||||
self.core.cancel()
|
||||
|
||||
for comp in self.components:
|
||||
comp.cancel()
|
||||
|
||||
try:
|
||||
self.out_pipe.send_signal(signal.SIGINT)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def reset(self):
|
||||
self.core.reset()
|
||||
self.canceled = False
|
||||
for comp in self.components:
|
||||
comp.reset()
|
133
video_thread.py
133
video_thread.py
|
@ -1,133 +0,0 @@
|
|||
from PyQt4 import QtCore, QtGui, uic
|
||||
from PyQt4.QtCore import pyqtSignal, pyqtSlot
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from PIL.ImageQt import ImageQt
|
||||
import core
|
||||
import numpy
|
||||
import subprocess as sp
|
||||
import sys
|
||||
|
||||
class Worker(QtCore.QObject):
|
||||
|
||||
videoCreated = pyqtSignal()
|
||||
progressBarUpdate = pyqtSignal(int)
|
||||
progressBarSetText = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QtCore.QObject.__init__(self)
|
||||
parent.videoTask.connect(self.createVideo)
|
||||
self.core = core.Core()
|
||||
|
||||
|
||||
@pyqtSlot(str, str, QtGui.QFont, int, int, int, int, tuple, tuple, str, str)
|
||||
def createVideo(self, backgroundImage, titleText, titleFont, fontSize, alignment,\
|
||||
xOffset, yOffset, textColor, visColor, inputFile, outputFile):
|
||||
# print('worker thread id: {}'.format(QtCore.QThread.currentThreadId()))
|
||||
def getBackgroundAtIndex(i):
|
||||
return self.core.drawBaseImage(
|
||||
backgroundFrames[i],
|
||||
titleText,
|
||||
titleFont,
|
||||
fontSize,
|
||||
alignment,
|
||||
xOffset,
|
||||
yOffset,
|
||||
textColor,
|
||||
visColor)
|
||||
|
||||
progressBarValue = 0
|
||||
self.progressBarUpdate.emit(progressBarValue)
|
||||
self.progressBarSetText.emit('Loading background image…')
|
||||
|
||||
backgroundFrames = self.core.parseBaseImage(backgroundImage)
|
||||
if len(backgroundFrames) < 2:
|
||||
# the base image is not a video so we can draw it now
|
||||
imBackground = getBackgroundAtIndex(0)
|
||||
else:
|
||||
# base images will be drawn while drawing the audio bars
|
||||
imBackground = None
|
||||
|
||||
self.progressBarSetText.emit('Loading audio file…')
|
||||
completeAudioArray = self.core.readAudioFile(inputFile)
|
||||
|
||||
# test if user has libfdk_aac
|
||||
encoders = sp.check_output(self.core.FFMPEG_BIN + " -encoders -hide_banner", shell=True)
|
||||
if b'libfdk_aac' in encoders:
|
||||
acodec = 'libfdk_aac'
|
||||
else:
|
||||
acodec = 'aac'
|
||||
|
||||
ffmpegCommand = [ self.core.FFMPEG_BIN,
|
||||
'-y', # (optional) means overwrite the output file if it already exists.
|
||||
'-f', 'rawvideo',
|
||||
'-vcodec', 'rawvideo',
|
||||
'-s', '1280x720', # size of one frame
|
||||
'-pix_fmt', 'rgb24',
|
||||
'-r', '30', # frames per second
|
||||
'-i', '-', # The input comes from a pipe
|
||||
'-an',
|
||||
'-i', inputFile,
|
||||
'-acodec', acodec, # output audio codec
|
||||
'-b:a', "192k",
|
||||
'-vcodec', "libx264",
|
||||
'-pix_fmt', "yuv420p",
|
||||
'-preset', "medium",
|
||||
'-f', "mp4"]
|
||||
|
||||
if acodec == 'aac':
|
||||
ffmpegCommand.append('-strict')
|
||||
ffmpegCommand.append('-2')
|
||||
|
||||
ffmpegCommand.append(outputFile)
|
||||
|
||||
out_pipe = sp.Popen(ffmpegCommand,
|
||||
stdin=sp.PIPE,stdout=sys.stdout, stderr=sys.stdout)
|
||||
|
||||
smoothConstantDown = 0.08
|
||||
smoothConstantUp = 0.8
|
||||
lastSpectrum = None
|
||||
sampleSize = 1470
|
||||
|
||||
numpy.seterr(divide='ignore')
|
||||
bgI = 0
|
||||
for i in range(0, len(completeAudioArray), sampleSize):
|
||||
# create video for output
|
||||
lastSpectrum = self.core.transformData(
|
||||
i,
|
||||
completeAudioArray,
|
||||
sampleSize,
|
||||
smoothConstantDown,
|
||||
smoothConstantUp,
|
||||
lastSpectrum)
|
||||
if imBackground != None:
|
||||
im = self.core.drawBars(lastSpectrum, imBackground, visColor)
|
||||
else:
|
||||
im = self.core.drawBars(lastSpectrum, getBackgroundAtIndex(bgI), visColor)
|
||||
if bgI < len(backgroundFrames)-1:
|
||||
bgI += 1
|
||||
|
||||
# write to out_pipe
|
||||
try:
|
||||
out_pipe.stdin.write(im.tobytes())
|
||||
finally:
|
||||
True
|
||||
|
||||
# increase progress bar value
|
||||
if progressBarValue + 1 <= (i / len(completeAudioArray)) * 100:
|
||||
progressBarValue = numpy.floor((i / len(completeAudioArray)) * 100)
|
||||
self.progressBarUpdate.emit(progressBarValue)
|
||||
self.progressBarSetText.emit('%s%%' % str(int(progressBarValue)))
|
||||
|
||||
numpy.seterr(all='print')
|
||||
|
||||
out_pipe.stdin.close()
|
||||
if out_pipe.stderr is not None:
|
||||
print(out_pipe.stderr.read())
|
||||
out_pipe.stderr.close()
|
||||
# out_pipe.terminate() # don't terminate ffmpeg too early
|
||||
out_pipe.wait()
|
||||
print("Video file created")
|
||||
self.core.deleteTempDir()
|
||||
self.progressBarUpdate.emit(100)
|
||||
self.progressBarSetText.emit('100%')
|
||||
self.videoCreated.emit()
|
Reference in New Issue