Compare commits
258 Commits
master
...
feature-ne
Author | SHA1 | Date |
---|---|---|
Brianna | 22978a0635 | |
tassaron | 8411857030 | |
tassaron | ad6dd9f532 | |
tassaron | 4a310ffb28 | |
tassaron | e8a7b18293 | |
tassaron | 85d3b779d0 | |
tassaron | 62e2ef18a3 | |
tassaron | 9d9c4076ac | |
tassaron | 6bf8a553d6 | |
tassaron | be9eb9077b | |
tassaron | d4b63e4d46 | |
tassaron | c07f2426ce | |
tassaron | 87e762a8aa | |
tassaron | 43ea3bfd73 | |
tassaron | c06ca5cdcb | |
tassaron | f66ec40ba6 | |
tassaron | ddb04f3a2f | |
tassaron | f65ced2853 | |
tassaron | a1d7cbb984 | |
tassaron | 733c005eea | |
tassaron | a327bec4e4 | |
tassaron | 39d6a4e5af | |
tassaron | ea1a422cc5 | |
tassaron | bed07479f1 | |
tassaron | 9c8792df9b | |
tassaron | a233d36ce2 | |
tassaron | 3f2834529f | |
tassaron | d6b6083f80 | |
tassaron | 282f1c4b12 | |
tassaron | 64da6f14ce | |
tassaron | c3f128806b | |
tassaron | bdb006f25d | |
Brianna | c5a0c9b364 | |
Brianna | 2603c63925 | |
tassaron | 1c4afc96d6 | |
tassaron | 8baa24e878 | |
tassaron | 9732f3bdeb | |
tassaron | cacab464c7 | |
Brianna | 8da72ab3cb | |
tassaron | 8b253717f7 | |
tassaron | 3ed84e1c3e | |
tassaron | 4d0daa4336 | |
tassaron | 354637d34c | |
tassaron | 060a7dc2d2 | |
tassaron | 998f741495 | |
Brianna | 20905230fe | |
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,20 @@
|
|||
__pycache__
|
||||
settings.ini
|
||||
build/*
|
||||
*.py[cod]
|
||||
build/*
|
||||
dist/*
|
||||
env/*
|
||||
.vscode/*
|
||||
*.mkv
|
||||
*.mp4
|
||||
*.wav
|
||||
*.mp3
|
||||
*.aif
|
||||
*.ac3
|
||||
*.zip
|
||||
*.tar
|
||||
*.tar.*
|
||||
*.exe
|
||||
ffmpeg
|
||||
*.bak
|
||||
*~
|
||||
*.goutput*
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
Original version created by Martin Kaistra <djfun>
|
||||
Version 2 created by tassaron and DH4
|
||||
|
||||
Contributors:
|
||||
* Martin Kaistra <djfun>
|
||||
* Brianna Rainey <tassaron>
|
||||
* DH4
|
||||
|
||||
Pull Requests By:
|
||||
* HunterwolfAT
|
||||
* rikai
|
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.0rc5'
|
||||
|
||||
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,36 @@
|
|||
import sys
|
||||
import os
|
||||
import logging
|
||||
|
||||
|
||||
class Logger(logging.getLoggerClass()):
|
||||
'''
|
||||
Custom Logger class to handle custom VERBOSE log level.
|
||||
Levels used in this program are as follows:
|
||||
VERBOSE Annoyingly frequent debug messages (e.g, in loops)
|
||||
DEBUG Ordinary debug information
|
||||
INFO Expected events that are expensive or irreversible
|
||||
WARNING A non-fatal error or suspicious behaviour
|
||||
ERROR Any error that would interrupt the user
|
||||
CRITICAL Things that really shouldn't happen at all
|
||||
'''
|
||||
def __init__(self, name, level=logging.NOTSET):
|
||||
super().__init__(name, level)
|
||||
logging.addLevelName(5, "VERBOSE")
|
||||
|
||||
def verbose(self, msg, *args, **kwargs):
|
||||
if self.isEnabledFor(5):
|
||||
self._log(5, msg, args, **kwargs)
|
||||
logging.setLoggerClass(Logger)
|
||||
logging.VERBOSE = 5
|
||||
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 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()
|
|
@ -0,0 +1,190 @@
|
|||
'''
|
||||
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
|
||||
import signal
|
||||
|
||||
from core import Core
|
||||
|
||||
|
||||
class Command(QtCore.QObject):
|
||||
|
||||
createVideo = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
QtCore.QObject.__init__(self)
|
||||
self.core = Core()
|
||||
Core.mode = 'commandline'
|
||||
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)
|
||||
|
||||
# ctrl-c stops the export thread
|
||||
signal.signal(signal.SIGINT, self.stopVideo)
|
||||
|
||||
if self.args.export and self.args.projpath:
|
||||
errcode, data = self.core.parseAvFile(projPath)
|
||||
for key, value in data['WindowFields']:
|
||||
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()
|
||||
|
||||
def stopVideo(self, *args):
|
||||
self.worker.error = True
|
||||
self.worker.cancelExport()
|
||||
self.worker.cancel()
|
||||
|
||||
@QtCore.pyqtSlot(str)
|
||||
def progressBarSetText(self, value):
|
||||
if 'Export ' in value:
|
||||
# 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,924 @@
|
|||
'''
|
||||
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
|
||||
import logging
|
||||
from copy import copy
|
||||
|
||||
from toolkit.frame import BlankFrame
|
||||
from toolkit import (
|
||||
getWidgetValue, setWidgetValue, connectWidget, rgbFromString, blockSignals
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger('AVP.ComponentHandler')
|
||||
|
||||
|
||||
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:
|
||||
log.verbose(
|
||||
'### %s #%s renders a preview frame ###',
|
||||
self.__class__.name, str(self.compPos),
|
||||
)
|
||||
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='):
|
||||
_, preset = arg.split('=', 1)
|
||||
path = os.path.join(self.core.getPresetDir(self), preset)
|
||||
if not os.path.exists(path):
|
||||
print('Couldn\'t locate preset "%s"' % preset)
|
||||
quit(1)
|
||||
else:
|
||||
print('Opening "%s" preset on layer %s' % (
|
||||
preset, self.compPos)
|
||||
)
|
||||
self.core.openPreset(path, self.compPos, preset)
|
||||
# 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 loadPresetWrapper(func):
|
||||
'''Wraps loadPreset to handle the self.openingPreset boolean'''
|
||||
class openingPreset:
|
||||
def __init__(self, comp):
|
||||
self.comp = comp
|
||||
|
||||
def __enter__(self):
|
||||
self.comp.openingPreset = True
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.comp.openingPreset = False
|
||||
|
||||
def presetWrapper(self, *args):
|
||||
with openingPreset(self):
|
||||
try:
|
||||
return func(self, *args)
|
||||
except Exception:
|
||||
try:
|
||||
raise ComponentError(self, 'preset loader')
|
||||
except ComponentError:
|
||||
return
|
||||
return presetWrapper
|
||||
|
||||
def updateWrapper(func):
|
||||
'''
|
||||
Calls _preUpdate before every subclass update().
|
||||
Afterwards, for non-user updates, calls _autoUpdate().
|
||||
For undoable updates triggered by the user, calls _userUpdate()
|
||||
'''
|
||||
class wrap:
|
||||
def __init__(self, comp, auto):
|
||||
self.comp = comp
|
||||
self.auto = auto
|
||||
|
||||
def __enter__(self):
|
||||
self.comp._preUpdate()
|
||||
|
||||
def __exit__(self, *args):
|
||||
if self.auto or self.comp.openingPreset \
|
||||
or not hasattr(self.comp.parent, 'undoStack'):
|
||||
log.verbose('Automatic update')
|
||||
self.comp._autoUpdate()
|
||||
else:
|
||||
log.verbose('User update')
|
||||
self.comp._userUpdate()
|
||||
|
||||
def updateWrapper(self, **kwargs):
|
||||
auto = kwargs['auto'] if 'auto' in kwargs else False
|
||||
with wrap(self, auto):
|
||||
try:
|
||||
return func(self)
|
||||
except Exception:
|
||||
try:
|
||||
raise ComponentError(self, 'update method')
|
||||
except ComponentError:
|
||||
return
|
||||
return updateWrapper
|
||||
|
||||
def widgetWrapper(func):
|
||||
'''Connects all widgets to update method after the subclass's method'''
|
||||
class wrap:
|
||||
def __init__(self, comp):
|
||||
self.comp = comp
|
||||
|
||||
def __enter__(self):
|
||||
pass
|
||||
|
||||
def __exit__(self, *args):
|
||||
for widgetList in self.comp._allWidgets.values():
|
||||
for widget in widgetList:
|
||||
log.verbose('Connecting %s', str(
|
||||
widget.__class__.__name__))
|
||||
connectWidget(widget, self.comp.update)
|
||||
|
||||
def widgetWrapper(self, *args, **kwargs):
|
||||
auto = kwargs['auto'] if 'auto' in kwargs else False
|
||||
with wrap(self):
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
except Exception:
|
||||
try:
|
||||
raise ComponentError(self, 'widget creation')
|
||||
except ComponentError:
|
||||
return
|
||||
return widgetWrapper
|
||||
|
||||
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]
|
||||
|
||||
decorate = (
|
||||
'names', # Class methods
|
||||
'error', 'audio', 'properties', # Properties
|
||||
'preFrameRender', 'previewRender',
|
||||
'loadPreset', 'command',
|
||||
'update', 'widget',
|
||||
)
|
||||
|
||||
# Auto-decorate methods
|
||||
for key in decorate:
|
||||
if key not in attrs:
|
||||
continue
|
||||
if key in ('names'):
|
||||
attrs[key] = classmethod(attrs[key])
|
||||
elif key in ('audio'):
|
||||
attrs[key] = property(attrs[key])
|
||||
elif key == 'command':
|
||||
attrs[key] = cls.commandWrapper(attrs[key])
|
||||
elif key == 'previewRender':
|
||||
attrs[key] = cls.renderWrapper(attrs[key])
|
||||
elif key == 'preFrameRender':
|
||||
attrs[key] = cls.initializationWrapper(attrs[key])
|
||||
elif key == 'properties':
|
||||
attrs[key] = cls.propertiesWrapper(attrs[key])
|
||||
elif key == 'error':
|
||||
attrs[key] = cls.errorWrapper(attrs[key])
|
||||
elif key == 'loadPreset':
|
||||
attrs[key] = cls.loadPresetWrapper(attrs[key])
|
||||
elif key == 'update':
|
||||
attrs[key] = cls.updateWrapper(attrs[key])
|
||||
elif key == 'widget' and parents[0] != QtCore.QObject:
|
||||
attrs[key] = cls.widgetWrapper(attrs[key])
|
||||
|
||||
# Turn version string into a number
|
||||
try:
|
||||
if 'version' not in attrs:
|
||||
log.error(
|
||||
'No version attribute in %s. Defaulting to 1',
|
||||
attrs['name'])
|
||||
attrs['version'] = 1
|
||||
else:
|
||||
attrs['version'] = int(attrs['version'].split('.')[0])
|
||||
except ValueError:
|
||||
log.critical(
|
||||
'%s component has an invalid version string:\n%s',
|
||||
attrs['name'], str(attrs['version'])
|
||||
)
|
||||
except KeyError:
|
||||
log.critical('%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
|
||||
|
||||
# STATUS VARIABLES
|
||||
self.currentPreset = None
|
||||
self._allWidgets = {}
|
||||
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 = {}
|
||||
|
||||
# LOCKING VARIABLES
|
||||
self.openingPreset = False
|
||||
self.mergeUndo = True
|
||||
self._lockedProperties = None
|
||||
self._lockedError = None
|
||||
self._lockedSize = None
|
||||
# If set to a dict, values are used as basis to update relative widgets
|
||||
self.oldAttrs = None
|
||||
# Stop lengthy processes in response to this variable
|
||||
self.canceled = False
|
||||
|
||||
def __str__(self):
|
||||
return self.__class__.name
|
||||
|
||||
def __repr__(self):
|
||||
import pprint
|
||||
try:
|
||||
preset = self.savePreset()
|
||||
except Exception as e:
|
||||
preset = '%s occurred while saving preset' % str(e)
|
||||
|
||||
return (
|
||||
'Component(module %s, pos %s) (%s)\n'
|
||||
'Name: %s v%s\nPreset: %s' % (
|
||||
self.moduleIndex, self.compPos,
|
||||
object.__repr__(self),
|
||||
self.__class__.name, str(self.__class__.version),
|
||||
pprint.pformat(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) and initialize
|
||||
'''
|
||||
self.parent = parent
|
||||
self.settings = parent.settings
|
||||
log.verbose(
|
||||
'Creating UI for %s #%s\'s widget',
|
||||
self.__class__.name, self.compPos
|
||||
)
|
||||
self.page = self.loadUi(self.__class__.ui)
|
||||
|
||||
# Find all normal widgets which will be connected after subclass method
|
||||
self._allWidgets = {
|
||||
'lineEdit': self.page.findChildren(QtWidgets.QLineEdit),
|
||||
'checkBox': self.page.findChildren(QtWidgets.QCheckBox),
|
||||
'spinBox': self.page.findChildren(QtWidgets.QSpinBox),
|
||||
'comboBox': self.page.findChildren(QtWidgets.QComboBox),
|
||||
}
|
||||
self._allWidgets['spinBox'].extend(
|
||||
self.page.findChildren(QtWidgets.QDoubleSpinBox)
|
||||
)
|
||||
|
||||
def update(self):
|
||||
'''
|
||||
Starting point for a component update. A subclass should override
|
||||
this method, and the base class will then magically insert a call
|
||||
to either _autoUpdate() or _userUpdate() at the end.
|
||||
'''
|
||||
|
||||
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]
|
||||
try:
|
||||
val = presetDict[key]
|
||||
except KeyError as e:
|
||||
log.info(
|
||||
'%s missing value %s. Outdated preset?',
|
||||
self.currentPreset, str(e)
|
||||
)
|
||||
val = getattr(self, 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 _preUpdate(self):
|
||||
'''Happens before subclass update()'''
|
||||
for attr in self._relativeWidgets:
|
||||
self.updateRelativeWidget(attr)
|
||||
|
||||
def _userUpdate(self):
|
||||
'''Happens after subclass update() for an undoable update by user.'''
|
||||
oldWidgetVals = {
|
||||
attr: copy(getattr(self, attr))
|
||||
for attr in self._trackedWidgets
|
||||
}
|
||||
newWidgetVals = {
|
||||
attr: getWidgetValue(widget)
|
||||
if attr not in self._colorWidgets else rgbFromString(widget.text())
|
||||
for attr, widget in self._trackedWidgets.items()
|
||||
}
|
||||
modifiedWidgets = {
|
||||
attr: val
|
||||
for attr, val in newWidgetVals.items()
|
||||
if val != oldWidgetVals[attr]
|
||||
}
|
||||
if modifiedWidgets:
|
||||
action = ComponentUpdate(self, oldWidgetVals, modifiedWidgets)
|
||||
self.parent.undoStack.push(action)
|
||||
|
||||
def _autoUpdate(self):
|
||||
'''Happens after subclass update() for an internal component update.'''
|
||||
newWidgetVals = {
|
||||
attr: getWidgetValue(widget)
|
||||
for attr, widget in self._trackedWidgets.items()
|
||||
}
|
||||
self.setAttrs(newWidgetVals)
|
||||
self._sendUpdateSignal()
|
||||
|
||||
def setAttrs(self, attrDict):
|
||||
'''
|
||||
Sets attrs (linked to trackedWidgets) in this component to
|
||||
the values in the attrDict. Mutates certain widget values if needed
|
||||
'''
|
||||
for attr, val in attrDict.items():
|
||||
if attr in self._colorWidgets:
|
||||
# Color Widgets must have a tuple & have a button to update
|
||||
if type(val) is tuple:
|
||||
rgbTuple = val
|
||||
else:
|
||||
rgbTuple = rgbFromString(val)
|
||||
btnStyle = (
|
||||
"QPushButton { background-color : %s; outline: none; }"
|
||||
% QColor(*rgbTuple).name())
|
||||
self._colorWidgets[attr].setStyleSheet(btnStyle)
|
||||
setattr(self, attr, rgbTuple)
|
||||
|
||||
else:
|
||||
# Normal tracked widget
|
||||
setattr(self, attr, val)
|
||||
log.verbose('Setting %s self.%s to %s' % (
|
||||
self.__class__.name, attr, val))
|
||||
|
||||
def setWidgetValues(self, attrDict):
|
||||
'''
|
||||
Sets widgets defined by keys in trackedWidgets in this preset to
|
||||
the values in the attrDict.
|
||||
'''
|
||||
affectedWidgets = [
|
||||
self._trackedWidgets[attr] for attr in attrDict
|
||||
]
|
||||
with blockSignals(affectedWidgets):
|
||||
for attr, val in attrDict.items():
|
||||
widget = self._trackedWidgets[attr]
|
||||
if attr in self._colorWidgets:
|
||||
val = '%s,%s,%s' % val
|
||||
setWidgetValue(widget, val)
|
||||
|
||||
def _sendUpdateSignal(self):
|
||||
if not self.core.openingProject:
|
||||
self.parent.drawPreview()
|
||||
saveValueStore = self.savePreset()
|
||||
saveValueStore['preset'] = self.currentPreset
|
||||
self.modified.emit(self.compPos, saveValueStore)
|
||||
|
||||
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
|
||||
'colorWidgets': identify attr as RGB tuple & update button CSS
|
||||
'relativeWidgets': change value proportionally to resolution
|
||||
|
||||
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, '_{}'.format(kwarg), kwargs[kwarg])
|
||||
else:
|
||||
raise ComponentError(
|
||||
self, 'Nonsensical keywords to trackWidgets.')
|
||||
except ComponentError:
|
||||
continue
|
||||
|
||||
if kwarg == 'colorWidgets':
|
||||
def makeColorFunc(attr):
|
||||
def pickColor_():
|
||||
self.mergeUndo = False
|
||||
self.pickColor(
|
||||
self._trackedWidgets[attr],
|
||||
self._colorWidgets[attr]
|
||||
)
|
||||
self.mergeUndo = True
|
||||
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)
|
||||
setattr(
|
||||
self, attr, getWidgetValue(self._trackedWidgets[attr])
|
||||
)
|
||||
|
||||
self._preUpdate()
|
||||
self._autoUpdate()
|
||||
|
||||
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):
|
||||
hasVerticalWords = (
|
||||
lambda attr:
|
||||
'height' in attr.lower() or
|
||||
'ypos' in attr.lower() or
|
||||
attr == 'y'
|
||||
)
|
||||
if 'axis' not in kwargs:
|
||||
axis = self.width
|
||||
if hasVerticalWords(attr):
|
||||
axis = self.height
|
||||
kwargs['axis'] = axis
|
||||
if 'axis' in kwargs and type(kwargs['axis']) is tuple:
|
||||
axis = kwargs['axis'][0]
|
||||
if hasVerticalWords(attr):
|
||||
axis = kwargs['axis'][1]
|
||||
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]
|
||||
if val > 50.0:
|
||||
log.warning(
|
||||
'%s #%s attempted to set %s to dangerously high number %s',
|
||||
self.__class__.name, self.compPos, attr, val
|
||||
)
|
||||
val = 50.0
|
||||
result = math.ceil(kwargs['axis'] * val)
|
||||
log.verbose(
|
||||
'Converting %s: f%s to px%s using axis %s',
|
||||
attr, val, result, kwargs['axis']
|
||||
)
|
||||
return result
|
||||
|
||||
@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)
|
||||
with blockSignals(self._trackedWidgets[attr]):
|
||||
self._trackedWidgets[attr].setValue(pixelVal)
|
||||
self.update(auto=True)
|
||||
|
||||
def getOldAttr(self, attr):
|
||||
'''
|
||||
Returns previous state of this attr. Used to determine whether
|
||||
a relative widget must be updated. Required because undoing/redoing
|
||||
can make determining the 'previous' value tricky.
|
||||
'''
|
||||
if self.oldAttrs is not None:
|
||||
return self.oldAttrs[attr]
|
||||
else:
|
||||
try:
|
||||
return getattr(self, attr)
|
||||
except AttributeError:
|
||||
log.error('Using visible values instead of oldAttrs')
|
||||
return self._trackedWidgets[attr].value()
|
||||
|
||||
def updateRelativeWidget(self, attr):
|
||||
'''Called by _preUpdate() for each relativeWidget before each update'''
|
||||
oldUserValue = self.getOldAttr(attr)
|
||||
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
|
||||
log.debug(
|
||||
'Updating %s #%s\'s relative widget: %s',
|
||||
self.__class__.name, self.compPos, attr)
|
||||
with blockSignals(self._trackedWidgets[attr]):
|
||||
self.updateRelativeWidgetMaximum(attr)
|
||||
pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
|
||||
self._trackedWidgets[attr].setValue(pixelVal)
|
||||
|
||||
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.'
|
||||
log.error("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)
|
||||
|
||||
|
||||
class ComponentUpdate(QtWidgets.QUndoCommand):
|
||||
'''Command object for making a component action undoable'''
|
||||
def __init__(self, parent, oldWidgetVals, modifiedVals):
|
||||
super().__init__(
|
||||
'change %s component #%s' % (
|
||||
parent.name, parent.compPos
|
||||
)
|
||||
)
|
||||
self.undone = False
|
||||
self.res = (int(parent.width), int(parent.height))
|
||||
self.parent = parent
|
||||
self.oldWidgetVals = {
|
||||
attr: copy(val)
|
||||
if attr not in self.parent._relativeWidgets
|
||||
else self.parent.floatValForAttr(attr, val, axis=self.res)
|
||||
for attr, val in oldWidgetVals.items()
|
||||
if attr in modifiedVals
|
||||
}
|
||||
self.modifiedVals = {
|
||||
attr: val
|
||||
if attr not in self.parent._relativeWidgets
|
||||
else self.parent.floatValForAttr(attr, val, axis=self.res)
|
||||
for attr, val in modifiedVals.items()
|
||||
}
|
||||
|
||||
# Because relative widgets change themselves every update based on
|
||||
# their previous value, we must store ALL their values in case of undo
|
||||
self.relativeWidgetValsAfterUndo = {
|
||||
attr: copy(getattr(self.parent, attr))
|
||||
for attr in self.parent._relativeWidgets
|
||||
}
|
||||
|
||||
# Determine if this update is mergeable
|
||||
self.id_ = -1
|
||||
if len(self.modifiedVals) == 1 and self.parent.mergeUndo:
|
||||
attr, val = self.modifiedVals.popitem()
|
||||
self.id_ = sum([ord(letter) for letter in attr[-14:]])
|
||||
self.modifiedVals[attr] = val
|
||||
else:
|
||||
log.warning(
|
||||
'%s component settings changed at once. (%s)',
|
||||
len(self.modifiedVals), repr(self.modifiedVals)
|
||||
)
|
||||
|
||||
def id(self):
|
||||
'''If 2 consecutive updates have same id, Qt will call mergeWith()'''
|
||||
return self.id_
|
||||
|
||||
def mergeWith(self, other):
|
||||
self.modifiedVals.update(other.modifiedVals)
|
||||
return True
|
||||
|
||||
def setWidgetValues(self, attrDict):
|
||||
'''
|
||||
Mask the component's usual method to handle our
|
||||
relative widgets in case the resolution has changed.
|
||||
'''
|
||||
newAttrDict = {
|
||||
attr: val if attr not in self.parent._relativeWidgets
|
||||
else self.parent.pixelValForAttr(attr, val)
|
||||
for attr, val in attrDict.items()
|
||||
}
|
||||
self.parent.setWidgetValues(newAttrDict)
|
||||
|
||||
def redo(self):
|
||||
if self.undone:
|
||||
log.info('Redoing component update')
|
||||
self.parent.oldAttrs = self.relativeWidgetValsAfterUndo
|
||||
self.setWidgetValues(self.modifiedVals)
|
||||
self.parent.update(auto=True)
|
||||
self.parent.oldAttrs = None
|
||||
if not self.undone:
|
||||
self.relativeWidgetValsAfterRedo = {
|
||||
attr: copy(getattr(self.parent, attr))
|
||||
for attr in self.parent._relativeWidgets
|
||||
}
|
||||
self.parent._sendUpdateSignal()
|
||||
|
||||
def undo(self):
|
||||
log.info('Undoing component update')
|
||||
self.undone = True
|
||||
self.parent.oldAttrs = self.relativeWidgetValsAfterRedo
|
||||
self.setWidgetValues(self.oldWidgetVals)
|
||||
self.parent.update(auto=True)
|
||||
self.parent.oldAttrs = None
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
<?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="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>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<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>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<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>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<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_4">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<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,159 @@
|
|||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
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 = FloodFrame(self.sizeWidth, self.sizeHeight, (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,666 @@
|
|||
<?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="text">
|
||||
<string>0,0,0</string>
|
||||
</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="text">
|
||||
<string>133,133,133</string>
|
||||
</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>19200</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>10800</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,126 @@
|
|||
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.1'
|
||||
|
||||
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,
|
||||
'stretchScale': self.page.spinBox_scale_stretch,
|
||||
'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):
|
||||
scale = self.scale if not self.stretched else self.stretchScale
|
||||
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 scale != 100:
|
||||
newHeight = int((image.height / 100) * scale)
|
||||
newWidth = int((image.width / 100) * 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.mergeUndo = False
|
||||
self.page.lineEdit_image.setText(filename)
|
||||
self.mergeUndo = True
|
||||
|
||||
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')
|
||||
|
||||
def savePreset(self):
|
||||
# Maintain the illusion that the scale spinbox is one widget
|
||||
scaleBox = self.page.spinBox_scale
|
||||
stretchScaleBox = self.page.spinBox_scale_stretch
|
||||
if self.page.checkBox_stretch.isChecked():
|
||||
scaleBox.setValue(stretchScaleBox.value())
|
||||
else:
|
||||
stretchScaleBox.setValue(scaleBox.value())
|
||||
return super().savePreset()
|
||||
|
||||
def update(self):
|
||||
# Maintain the illusion that the scale spinbox is one widget
|
||||
scaleBox = self.page.spinBox_scale
|
||||
stretchScaleBox = self.page.spinBox_scale_stretch
|
||||
if self.page.checkBox_stretch.isChecked():
|
||||
scaleBox.setVisible(False)
|
||||
stretchScaleBox.setVisible(True)
|
||||
else:
|
||||
scaleBox.setVisible(True)
|
||||
stretchScaleBox.setVisible(False)
|
|
@ -0,0 +1,388 @@
|
|||
<?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>
|
||||
<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>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_scale_stretch">
|
||||
<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>
|
||||
</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,475 @@
|
|||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
from PyQt5.QtWidgets import QUndoCommand
|
||||
from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter
|
||||
import os
|
||||
import math
|
||||
|
||||
from component import Component
|
||||
from toolkit.frame import BlankFrame, scale
|
||||
|
||||
|
||||
class Component(Component):
|
||||
name = 'Conway\'s Game of Life'
|
||||
version = '1.0.0'
|
||||
|
||||
def widget(self, *args):
|
||||
super().widget(*args)
|
||||
self.scale = 32
|
||||
self.updateGridSize()
|
||||
self.startingGrid = set()
|
||||
self.page.pushButton_pickImage.clicked.connect(self.pickImage)
|
||||
self.trackWidgets({
|
||||
'tickRate': self.page.spinBox_tickRate,
|
||||
'scale': self.page.spinBox_scale,
|
||||
'color': self.page.lineEdit_color,
|
||||
'shapeType': self.page.comboBox_shapeType,
|
||||
'shadow': self.page.checkBox_shadow,
|
||||
'customImg': self.page.checkBox_customImg,
|
||||
'showGrid': self.page.checkBox_showGrid,
|
||||
'image': self.page.lineEdit_image,
|
||||
}, colorWidgets={
|
||||
'color': self.page.pushButton_color,
|
||||
})
|
||||
self.shiftButtons = (
|
||||
self.page.toolButton_up,
|
||||
self.page.toolButton_down,
|
||||
self.page.toolButton_left,
|
||||
self.page.toolButton_right,
|
||||
)
|
||||
|
||||
def shiftFunc(i):
|
||||
def shift():
|
||||
self.shiftGrid(i)
|
||||
return shift
|
||||
shiftFuncs = [shiftFunc(i) for i in range(len(self.shiftButtons))]
|
||||
for i, widget in enumerate(self.shiftButtons):
|
||||
widget.clicked.connect(shiftFuncs[i])
|
||||
self.page.spinBox_scale.setValue(self.scale)
|
||||
self.page.spinBox_scale.valueChanged.connect(self.updateGridSize)
|
||||
|
||||
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.mergeUndo = False
|
||||
self.page.lineEdit_image.setText(filename)
|
||||
self.mergeUndo = True
|
||||
|
||||
def shiftGrid(self, d):
|
||||
action = ShiftGrid(self, d)
|
||||
self.parent.undoStack.push(action)
|
||||
|
||||
def update(self):
|
||||
self.updateGridSize()
|
||||
if self.page.checkBox_customImg.isChecked():
|
||||
self.page.label_color.setVisible(False)
|
||||
self.page.lineEdit_color.setVisible(False)
|
||||
self.page.pushButton_color.setVisible(False)
|
||||
self.page.label_shape.setVisible(False)
|
||||
self.page.comboBox_shapeType.setVisible(False)
|
||||
self.page.label_image.setVisible(True)
|
||||
self.page.lineEdit_image.setVisible(True)
|
||||
self.page.pushButton_pickImage.setVisible(True)
|
||||
else:
|
||||
self.page.label_color.setVisible(True)
|
||||
self.page.lineEdit_color.setVisible(True)
|
||||
self.page.pushButton_color.setVisible(True)
|
||||
self.page.label_shape.setVisible(True)
|
||||
self.page.comboBox_shapeType.setVisible(True)
|
||||
self.page.label_image.setVisible(False)
|
||||
self.page.lineEdit_image.setVisible(False)
|
||||
self.page.pushButton_pickImage.setVisible(False)
|
||||
enabled = (len(self.startingGrid) > 0)
|
||||
for widget in self.shiftButtons:
|
||||
widget.setEnabled(enabled)
|
||||
|
||||
def previewClickEvent(self, pos, size, button):
|
||||
pos = (
|
||||
math.ceil((pos[0] / size[0]) * self.gridWidth) - 1,
|
||||
math.ceil((pos[1] / size[1]) * self.gridHeight) - 1
|
||||
)
|
||||
action = ClickGrid(self, pos, button)
|
||||
self.parent.undoStack.push(action)
|
||||
|
||||
def updateGridSize(self):
|
||||
w, h = self.core.resolutions[-1].split('x')
|
||||
self.gridWidth = int(int(w) / self.scale)
|
||||
self.gridHeight = int(int(h) / self.scale)
|
||||
self.pxWidth = math.ceil(self.width / self.gridWidth)
|
||||
self.pxHeight = math.ceil(self.height / self.gridHeight)
|
||||
|
||||
def previewRender(self):
|
||||
return self.drawGrid(self.startingGrid)
|
||||
|
||||
def preFrameRender(self, *args, **kwargs):
|
||||
super().preFrameRender(*args, **kwargs)
|
||||
self.progressBarSetText.emit("Computing evolution...")
|
||||
self.tickGrids = {0: self.startingGrid}
|
||||
tick = 0
|
||||
for frameNo in range(
|
||||
self.tickRate, self.audioArrayLen, self.sampleSize
|
||||
):
|
||||
if self.parent.canceled:
|
||||
break
|
||||
if frameNo % self.tickRate == 0:
|
||||
tick += 1
|
||||
self.tickGrids[tick] = self.gridForTick(tick)
|
||||
|
||||
# update progress bar
|
||||
progress = int(100*(frameNo/self.audioArrayLen))
|
||||
if progress >= 100:
|
||||
progress = 100
|
||||
pStr = "Computing evolution: "+str(progress)+'%'
|
||||
self.progressBarSetText.emit(pStr)
|
||||
self.progressBarUpdate.emit(int(progress))
|
||||
|
||||
def properties(self):
|
||||
if self.customImg and (
|
||||
not self.image or not os.path.exists(self.image)
|
||||
):
|
||||
return ['error']
|
||||
return []
|
||||
|
||||
def error(self):
|
||||
return "No image selected to represent life."
|
||||
|
||||
def frameRender(self, frameNo):
|
||||
tick = math.floor(frameNo / self.tickRate)
|
||||
grid = self.tickGrids[tick]
|
||||
return self.drawGrid(grid)
|
||||
|
||||
def drawGrid(self, grid):
|
||||
frame = BlankFrame(self.width, self.height)
|
||||
|
||||
def drawCustomImg():
|
||||
try:
|
||||
img = Image.open(self.image)
|
||||
except Exception:
|
||||
return
|
||||
img = img.resize((self.pxWidth, self.pxHeight), Image.ANTIALIAS)
|
||||
frame.paste(img, box=(drawPtX, drawPtY))
|
||||
|
||||
def drawShape():
|
||||
drawer = ImageDraw.Draw(frame)
|
||||
rect = (
|
||||
(drawPtX, drawPtY),
|
||||
(drawPtX + self.pxWidth, drawPtY + self.pxHeight)
|
||||
)
|
||||
shape = self.page.comboBox_shapeType.currentText().lower()
|
||||
|
||||
# Rectangle
|
||||
if shape == 'rectangle':
|
||||
drawer.rectangle(rect, fill=self.color)
|
||||
|
||||
# Elliptical
|
||||
elif shape == 'elliptical':
|
||||
drawer.ellipse(rect, fill=self.color)
|
||||
|
||||
tenthX, tenthY = scale(10, self.pxWidth, self.pxHeight, int)
|
||||
smallerShape = (
|
||||
(drawPtX + tenthX + int(tenthX / 4),
|
||||
drawPtY + tenthY + int(tenthY / 2)),
|
||||
(drawPtX + self.pxWidth - tenthX - int(tenthX / 4),
|
||||
drawPtY + self.pxHeight - (tenthY + int(tenthY / 2)))
|
||||
)
|
||||
outlineShape = (
|
||||
(drawPtX + int(tenthX / 4),
|
||||
drawPtY + int(tenthY / 2)),
|
||||
(drawPtX + self.pxWidth - int(tenthX / 4),
|
||||
drawPtY + self.pxHeight - int(tenthY / 2))
|
||||
)
|
||||
# Circle
|
||||
if shape == 'circle':
|
||||
drawer.ellipse(outlineShape, fill=self.color)
|
||||
drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
|
||||
|
||||
# Lilypad
|
||||
elif shape == 'lilypad':
|
||||
drawer.pieslice(smallerShape, 290, 250, fill=self.color)
|
||||
|
||||
# Pac-Man
|
||||
elif shape == 'pac-man':
|
||||
drawer.pieslice(outlineShape, 35, 320, fill=self.color)
|
||||
|
||||
hX, hY = scale(50, self.pxWidth, self.pxHeight, int) # halfline
|
||||
tX, tY = scale(33, self.pxWidth, self.pxHeight, int) # thirdline
|
||||
qX, qY = scale(20, self.pxWidth, self.pxHeight, int) # quarterline
|
||||
|
||||
# Path
|
||||
if shape == 'path':
|
||||
drawer.ellipse(rect, fill=self.color)
|
||||
rects = {
|
||||
direction: False
|
||||
for direction in (
|
||||
'up', 'down', 'left', 'right',
|
||||
)
|
||||
}
|
||||
for cell in self.nearbyCoords(x, y):
|
||||
if cell not in grid:
|
||||
continue
|
||||
if cell[0] == x:
|
||||
if cell[1] < y:
|
||||
rects['up'] = True
|
||||
if cell[1] > y:
|
||||
rects['down'] = True
|
||||
if cell[1] == y:
|
||||
if cell[0] < x:
|
||||
rects['left'] = True
|
||||
if cell[0] > x:
|
||||
rects['right'] = True
|
||||
|
||||
for direction, rect in rects.items():
|
||||
if rect:
|
||||
if direction == 'up':
|
||||
sect = (
|
||||
(drawPtX, drawPtY),
|
||||
(drawPtX + self.pxWidth, drawPtY + hY)
|
||||
)
|
||||
elif direction == 'down':
|
||||
sect = (
|
||||
(drawPtX, drawPtY + hY),
|
||||
(drawPtX + self.pxWidth,
|
||||
drawPtY + self.pxHeight)
|
||||
)
|
||||
elif direction == 'left':
|
||||
sect = (
|
||||
(drawPtX, drawPtY),
|
||||
(drawPtX + hX,
|
||||
drawPtY + self.pxHeight)
|
||||
)
|
||||
elif direction == 'right':
|
||||
sect = (
|
||||
(drawPtX + hX, drawPtY),
|
||||
(drawPtX + self.pxWidth,
|
||||
drawPtY + self.pxHeight)
|
||||
)
|
||||
drawer.rectangle(sect, fill=self.color)
|
||||
|
||||
# Duck
|
||||
elif shape == 'duck':
|
||||
duckHead = (
|
||||
(drawPtX + qX, drawPtY + qY),
|
||||
(drawPtX + int(qX * 3), drawPtY + int(tY * 2))
|
||||
)
|
||||
duckBeak = (
|
||||
(drawPtX + hX, drawPtY + qY),
|
||||
(drawPtX + self.pxWidth + qX,
|
||||
drawPtY + int(qY * 3))
|
||||
)
|
||||
duckWing = (
|
||||
(drawPtX, drawPtY + hY),
|
||||
rect[1]
|
||||
)
|
||||
duckBody = (
|
||||
(drawPtX + int(qX / 4), drawPtY + int(qY * 3)),
|
||||
(drawPtX + int(tX * 2), drawPtY + self.pxHeight)
|
||||
)
|
||||
drawer.ellipse(duckBody, fill=self.color)
|
||||
drawer.ellipse(duckHead, fill=self.color)
|
||||
drawer.pieslice(duckWing, 130, 200, fill=self.color)
|
||||
drawer.pieslice(duckBeak, 145, 200, fill=self.color)
|
||||
|
||||
# Peace
|
||||
elif shape == 'peace':
|
||||
line = ((
|
||||
drawPtX + hX - int(tenthX / 2), drawPtY + int(tenthY / 2)),
|
||||
(drawPtX + hX + int(tenthX / 2),
|
||||
drawPtY + self.pxHeight - int(tenthY / 2))
|
||||
)
|
||||
drawer.ellipse(outlineShape, fill=self.color)
|
||||
drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
|
||||
drawer.rectangle(line, fill=self.color)
|
||||
|
||||
def slantLine(difference):
|
||||
return (
|
||||
(drawPtX + difference),
|
||||
(drawPtY + self.pxHeight - qY)
|
||||
),
|
||||
(
|
||||
(drawPtX + hX),
|
||||
(drawPtY + hY)
|
||||
)
|
||||
|
||||
drawer.line(
|
||||
slantLine(qX),
|
||||
fill=self.color,
|
||||
width=tenthX
|
||||
)
|
||||
drawer.line(
|
||||
slantLine(self.pxWidth - qX),
|
||||
fill=self.color,
|
||||
width=tenthX
|
||||
)
|
||||
|
||||
for x, y in grid:
|
||||
drawPtX = x * self.pxWidth
|
||||
if drawPtX > self.width:
|
||||
continue
|
||||
drawPtY = y * self.pxHeight
|
||||
if drawPtY > self.height:
|
||||
continue
|
||||
|
||||
if self.customImg:
|
||||
drawCustomImg()
|
||||
else:
|
||||
drawShape()
|
||||
|
||||
if self.shadow:
|
||||
shadImg = ImageEnhance.Contrast(frame).enhance(0.0)
|
||||
shadImg = shadImg.filter(ImageFilter.GaussianBlur(5.00))
|
||||
shadImg = ImageChops.offset(shadImg, -2, 2)
|
||||
shadImg.paste(frame, box=(0, 0), mask=frame)
|
||||
frame = shadImg
|
||||
if self.showGrid:
|
||||
drawer = ImageDraw.Draw(frame)
|
||||
w, h = scale(0.05, self.width, self.height, int)
|
||||
for x in range(self.pxWidth, self.width, self.pxWidth):
|
||||
drawer.rectangle(
|
||||
((x, 0),
|
||||
(x + w, self.height)),
|
||||
fill=self.color,
|
||||
)
|
||||
for y in range(self.pxHeight, self.height, self.pxHeight):
|
||||
drawer.rectangle(
|
||||
((0, y),
|
||||
(self.width, y + h)),
|
||||
fill=self.color,
|
||||
)
|
||||
|
||||
return frame
|
||||
|
||||
def gridForTick(self, tick):
|
||||
'''Given a tick number over 0, returns a new grid set of tuples'''
|
||||
lastGrid = self.tickGrids[tick - 1]
|
||||
|
||||
def neighbours(x, y):
|
||||
return {
|
||||
cell for cell in self.nearbyCoords(x, y)
|
||||
if cell in lastGrid
|
||||
}
|
||||
|
||||
newGrid = set()
|
||||
for x, y in lastGrid:
|
||||
surrounding = len(neighbours(x, y))
|
||||
if surrounding == 2 or surrounding == 3:
|
||||
newGrid.add((x, y))
|
||||
potentialNewCells = {
|
||||
coordTup for origin in lastGrid
|
||||
for coordTup in list(self.nearbyCoords(*origin))
|
||||
}
|
||||
for x, y in potentialNewCells:
|
||||
if (x, y) in newGrid:
|
||||
continue
|
||||
surrounding = len(neighbours(x, y))
|
||||
if surrounding == 3:
|
||||
newGrid.add((x, y))
|
||||
|
||||
return newGrid
|
||||
|
||||
def savePreset(self):
|
||||
pr = super().savePreset()
|
||||
pr['GRID'] = sorted(self.startingGrid)
|
||||
return pr
|
||||
|
||||
def loadPreset(self, pr, *args):
|
||||
self.startingGrid = set(pr['GRID'])
|
||||
if self.startingGrid:
|
||||
for widget in self.shiftButtons:
|
||||
widget.setEnabled(True)
|
||||
super().loadPreset(pr, *args)
|
||||
|
||||
def nearbyCoords(self, x, y):
|
||||
yield x + 1, y + 1
|
||||
yield x + 1, y - 1
|
||||
yield x - 1, y + 1
|
||||
yield x - 1, y - 1
|
||||
yield x, y + 1
|
||||
yield x, y - 1
|
||||
yield x + 1, y
|
||||
yield x - 1, y
|
||||
|
||||
|
||||
class ClickGrid(QUndoCommand):
|
||||
def __init__(self, comp, pos, id_):
|
||||
super().__init__(
|
||||
"click %s component #%s" % (comp.name, comp.compPos))
|
||||
self.comp = comp
|
||||
self.pos = [pos]
|
||||
self.id_ = id_
|
||||
|
||||
def id(self):
|
||||
return self.id_
|
||||
|
||||
def mergeWith(self, other):
|
||||
self.pos.extend(other.pos)
|
||||
return True
|
||||
|
||||
def add(self):
|
||||
for pos in self.pos[:]:
|
||||
self.comp.startingGrid.add(pos)
|
||||
self.comp.update(auto=True)
|
||||
|
||||
def remove(self):
|
||||
for pos in self.pos[:]:
|
||||
self.comp.startingGrid.discard(pos)
|
||||
self.comp.update(auto=True)
|
||||
|
||||
def redo(self):
|
||||
if self.id_ == 1: # Left-click
|
||||
self.add()
|
||||
elif self.id_ == 2: # Right-click
|
||||
self.remove()
|
||||
|
||||
def undo(self):
|
||||
if self.id_ == 1: # Left-click
|
||||
self.remove()
|
||||
elif self.id_ == 2: # Right-click
|
||||
self.add()
|
||||
|
||||
class ShiftGrid(QUndoCommand):
|
||||
def __init__(self, comp, direction):
|
||||
super().__init__(
|
||||
"change %s component #%s" % (comp.name, comp.compPos))
|
||||
self.comp = comp
|
||||
self.direction = direction
|
||||
self.distance = 1
|
||||
|
||||
def id(self):
|
||||
return self.direction
|
||||
|
||||
def mergeWith(self, other):
|
||||
self.distance += other.distance
|
||||
return True
|
||||
|
||||
def newGrid(self, Xchange, Ychange):
|
||||
return {
|
||||
(x + Xchange, y + Ychange)
|
||||
for x, y in self.comp.startingGrid
|
||||
}
|
||||
|
||||
def redo(self):
|
||||
if self.direction == 0:
|
||||
newGrid = self.newGrid(0, -self.distance)
|
||||
elif self.direction == 1:
|
||||
newGrid = self.newGrid(0, self.distance)
|
||||
elif self.direction == 2:
|
||||
newGrid = self.newGrid(-self.distance, 0)
|
||||
elif self.direction == 3:
|
||||
newGrid = self.newGrid(self.distance, 0)
|
||||
self.comp.startingGrid = newGrid
|
||||
self.comp._sendUpdateSignal()
|
||||
|
||||
def undo(self):
|
||||
if self.direction == 0:
|
||||
newGrid = self.newGrid(0, self.distance)
|
||||
elif self.direction == 1:
|
||||
newGrid = self.newGrid(0, -self.distance)
|
||||
elif self.direction == 2:
|
||||
newGrid = self.newGrid(self.distance, 0)
|
||||
elif self.direction == 3:
|
||||
newGrid = self.newGrid(-self.distance, 0)
|
||||
self.comp.startingGrid = newGrid
|
||||
self.comp._sendUpdateSignal()
|
|
@ -0,0 +1,405 @@
|
|||
<?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="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Simulation Speed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_tickRate">
|
||||
<property name="suffix">
|
||||
<string> frames per tick</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>30</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>5</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>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_color">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>0,0,0</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Grid Scale</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_scale">
|
||||
<property name="minimum">
|
||||
<number>22</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>128</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>32</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_customImg">
|
||||
<property name="text">
|
||||
<string>Custom Image</string>
|
||||
</property>
|
||||
</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_8">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_image">
|
||||
<property name="text">
|
||||
<string>Image</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_image"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_pickImage">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" 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>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_color">
|
||||
<property name="text">
|
||||
<string>Color</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_color_3">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>0,0,0</string>
|
||||
</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_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_shape">
|
||||
<property name="text">
|
||||
<string>Shape</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_shapeType">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Path</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Rectangle</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Elliptical</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Circle</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Lilypad</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Pac-Man</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Duck</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Peace</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_8">
|
||||
<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="QCheckBox" name="checkBox_shadow">
|
||||
<property name="text">
|
||||
<string>Shadow</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_showGrid">
|
||||
<property name="text">
|
||||
<string>Show Grid</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>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_9">
|
||||
<item>
|
||||
<widget class="QToolButton" name="toolButton_up">
|
||||
<property name="text">
|
||||
<string>Up</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::UpArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="toolButton_down">
|
||||
<property name="text">
|
||||
<string>Down</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::DownArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="toolButton_left">
|
||||
<property name="text">
|
||||
<string>Left</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::LeftArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="toolButton_right">
|
||||
<property name="text">
|
||||
<string>Right</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::RightArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_9">
|
||||
<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>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="textBrowser">
|
||||
<property name="html">
|
||||
<string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
|
||||
p, li { white-space: pre-wrap; }
|
||||
</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;">
|
||||
<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Click the preview window to place a cell. Right-click to remove.</span></p>
|
||||
<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- A cell with less than 2 neighbours will die from underpopulation</p>
|
||||
<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- A cell with more than 3 neighbours will die from overpopulation.</p>
|
||||
<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- An empty space surrounded by 3 live cells will cause reproduction.</p></body></html></string>
|
||||
</property>
|
||||
<property name="tabStopWidth">
|
||||
<number>80</number>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::NoTextInteraction</set>
|
||||
</property>
|
||||
<property name="openLinks">
|
||||
<bool>false</bool>
|
||||
</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>586</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,73 @@
|
|||
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.mergeUndo = False
|
||||
self.page.lineEdit_sound.setText(filename)
|
||||
self.mergeUndo = True
|
||||
|
||||
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,316 @@
|
|||
from PIL import Image
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
import os
|
||||
import math
|
||||
import subprocess
|
||||
import time
|
||||
import logging
|
||||
|
||||
from component import Component
|
||||
from toolkit.frame import BlankFrame, scale
|
||||
from toolkit import checkOutput, connectWidget
|
||||
from toolkit.ffmpeg import (
|
||||
openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger('AVP.Components.Spectrum')
|
||||
|
||||
|
||||
class Component(Component):
|
||||
name = 'Spectrum'
|
||||
version = '1.0.1'
|
||||
|
||||
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
|
||||
self.previewSize = (214, 120)
|
||||
self.previewPipe = None
|
||||
|
||||
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):
|
||||
filterType = self.page.comboBox_filterType.currentIndex()
|
||||
self.page.stackedWidget.setCurrentIndex(filterType)
|
||||
if filterType == 3:
|
||||
self.page.spinBox_hue.setEnabled(False)
|
||||
else:
|
||||
self.page.spinBox_hue.setEnabled(True)
|
||||
if filterType == 2 or filterType == 4:
|
||||
self.page.checkBox_mono.setEnabled(False)
|
||||
else:
|
||||
self.page.checkBox_mono.setEnabled(True)
|
||||
|
||||
def previewRender(self):
|
||||
changedSize = self.updateChunksize()
|
||||
if not changedSize \
|
||||
and not self.changedOptions \
|
||||
and self.previewFrame is not None:
|
||||
log.debug(
|
||||
'Spectrum #%s is reusing old preview frame' % self.compPos)
|
||||
return self.previewFrame
|
||||
|
||||
frame = self.getPreviewFrame()
|
||||
self.changedOptions = False
|
||||
if not frame:
|
||||
log.warning(
|
||||
'Spectrum #%s failed to create a preview frame' % self.compPos)
|
||||
self.previewFrame = None
|
||||
return BlankFrame(self.width, self.height)
|
||||
else:
|
||||
self.previewFrame = frame
|
||||
return frame
|
||||
|
||||
def preFrameRender(self, **kwargs):
|
||||
super().preFrameRender(**kwargs)
|
||||
if self.previewPipe is not None:
|
||||
self.previewPipe.wait()
|
||||
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',
|
||||
self.core.junkStream
|
||||
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',
|
||||
])
|
||||
|
||||
if self.core.logEnabled:
|
||||
logFilename = os.path.join(
|
||||
self.core.logDir, 'preview_%s.log' % str(self.compPos))
|
||||
log.debug('Creating ffmpeg process (log at %s)' % logFilename)
|
||||
with open(logFilename, 'w') as logf:
|
||||
logf.write(" ".join(command) + '\n\n')
|
||||
with open(logFilename, 'a') as logf:
|
||||
self.previewPipe = openPipe(
|
||||
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=logf, bufsize=10**8
|
||||
)
|
||||
else:
|
||||
self.previewPipe = openPipe(
|
||||
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, bufsize=10**8
|
||||
)
|
||||
byteFrame = self.previewPipe.stdout.read(self.chunkSize)
|
||||
closePipe(self.previewPipe)
|
||||
|
||||
frame = self.finalizeFrame(byteFrame)
|
||||
return frame
|
||||
|
||||
def makeFfmpegFilter(self, preview=False, startPt=0):
|
||||
if preview:
|
||||
w, h = self.previewSize
|
||||
else:
|
||||
w, h = (self.width, self.height)
|
||||
color = self.page.comboBox_color.currentText().lower()
|
||||
genericPreview = self.settings.value("pref_genericPreview")
|
||||
|
||||
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' % (
|
||||
w, h,
|
||||
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"),
|
||||
w, h,
|
||||
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' % (
|
||||
w, h,
|
||||
'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"),
|
||||
w, h,
|
||||
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"),
|
||||
w, h,
|
||||
)
|
||||
)
|
||||
|
||||
if self.filterType < 2:
|
||||
exampleSnd = exampleSound('freq')
|
||||
elif self.filterType == 2 or self.filterType == 4:
|
||||
exampleSnd = exampleSound('stereo')
|
||||
elif self.filterType == 3:
|
||||
exampleSnd = exampleSound('white')
|
||||
|
||||
return [
|
||||
'-filter_complex',
|
||||
'%s%s%s%s [v1]; '
|
||||
'[v1] %s%s%s%s%s [v]' % (
|
||||
exampleSnd if preview and genericPreview else '[0:a] ',
|
||||
'compand=gain=4,' if self.compress else '',
|
||||
'aformat=channel_layouts=mono,'
|
||||
if self.mono and self.filterType not in (2, 4) else '',
|
||||
filter_,
|
||||
'hflip, ' if self.mirror else '',
|
||||
'trim=start=%s:end=%s, ' % (
|
||||
"{0:.3f}".format(startPt + 12),
|
||||
"{0:.3f}".format(startPt + 12.5)
|
||||
) if preview else '',
|
||||
'scale=%sx%s' % scale(
|
||||
self.scale, self.width, self.height, str),
|
||||
', hue=h=%s:s=10' % str(self.hue)
|
||||
if self.hue > 0 and self.filterType != 3 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
|
||||
|
||||
frame = BlankFrame(self.width, self.height)
|
||||
frame.paste(image, box=(self.x, self.y))
|
||||
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,203 @@
|
|||
from PIL import ImageEnhance, ImageFilter, ImageChops
|
||||
from PyQt5.QtGui import QColor, QFont
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
import os
|
||||
import logging
|
||||
|
||||
from component import Component
|
||||
from toolkit.frame import FramePainter, PaintColor
|
||||
|
||||
log = logging.getLogger('AVP.Components.Text')
|
||||
|
||||
|
||||
class Component(Component):
|
||||
name = 'Title Text'
|
||||
version = '1.0.1'
|
||||
|
||||
def widget(self, *args):
|
||||
super().widget(*args)
|
||||
self.title = 'Text'
|
||||
self.alignment = 1
|
||||
self.titleFont = QFont()
|
||||
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.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,
|
||||
'fontStyle': self.page.comboBox_fontStyle,
|
||||
'stroke': self.page.spinBox_stroke,
|
||||
'strokeColor': self.page.lineEdit_strokeColor,
|
||||
'shadow': self.page.checkBox_shadow,
|
||||
'shadX': self.page.spinBox_shadX,
|
||||
'shadY': self.page.spinBox_shadY,
|
||||
'shadBlur': self.page.spinBox_shadBlur,
|
||||
}, colorWidgets={
|
||||
'textColor': self.page.pushButton_textColor,
|
||||
'strokeColor': self.page.pushButton_strokeColor,
|
||||
}, relativeWidgets=[
|
||||
'xPosition', 'yPosition', 'fontSize',
|
||||
'stroke', 'shadX', 'shadY', 'shadBlur'
|
||||
])
|
||||
self.centerXY()
|
||||
|
||||
def update(self):
|
||||
self.titleFont = self.page.fontComboBox_titleFont.currentFont()
|
||||
if self.page.checkBox_shadow.isChecked():
|
||||
self.page.label_shadX.setHidden(False)
|
||||
self.page.spinBox_shadX.setHidden(False)
|
||||
self.page.spinBox_shadY.setHidden(False)
|
||||
self.page.label_shadBlur.setHidden(False)
|
||||
self.page.spinBox_shadBlur.setHidden(False)
|
||||
else:
|
||||
self.page.label_shadX.setHidden(True)
|
||||
self.page.spinBox_shadX.setHidden(True)
|
||||
self.page.spinBox_shadY.setHidden(True)
|
||||
self.page.label_shadBlur.setHidden(True)
|
||||
self.page.spinBox_shadBlur.setHidden(True)
|
||||
|
||||
def centerXY(self):
|
||||
self.setRelativeWidget('xPosition', 0.5)
|
||||
self.setRelativeWidget('yPosition', 0.521)
|
||||
|
||||
def getXY(self):
|
||||
'''Returns true x, y after considering alignment settings'''
|
||||
fm = QtGui.QFontMetrics(self.titleFont)
|
||||
x = self.pixelValForAttr('xPosition')
|
||||
|
||||
if self.alignment == 1: # Middle
|
||||
offset = int(fm.width(self.title)/2)
|
||||
x -= offset
|
||||
if self.alignment == 2: # Right
|
||||
offset = fm.width(self.title)
|
||||
x -= 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):
|
||||
font = self.titleFont
|
||||
font.setPixelSize(self.fontSize)
|
||||
font.setStyle(QFont.StyleNormal)
|
||||
font.setWeight(QFont.Normal)
|
||||
font.setCapitalization(QFont.MixedCase)
|
||||
if self.fontStyle == 1:
|
||||
font.setWeight(QFont.DemiBold)
|
||||
if self.fontStyle == 2:
|
||||
font.setWeight(QFont.Bold)
|
||||
elif self.fontStyle == 3:
|
||||
font.setStyle(QFont.StyleItalic)
|
||||
elif self.fontStyle == 4:
|
||||
font.setWeight(QFont.Bold)
|
||||
font.setStyle(QFont.StyleItalic)
|
||||
elif self.fontStyle == 5:
|
||||
font.setStyle(QFont.StyleOblique)
|
||||
elif self.fontStyle == 6:
|
||||
font.setCapitalization(QFont.SmallCaps)
|
||||
|
||||
image = FramePainter(width, height)
|
||||
x, y = self.getXY()
|
||||
log.debug('Text position translates to %s, %s', x, y)
|
||||
if self.stroke > 0:
|
||||
outliner = QtGui.QPainterPathStroker()
|
||||
outliner.setWidth(self.stroke)
|
||||
path = QtGui.QPainterPath()
|
||||
if self.fontStyle == 6:
|
||||
# PathStroker ignores smallcaps so we need this weird hack
|
||||
path.addText(x, y, font, self.title[0])
|
||||
fm = QtGui.QFontMetrics(font)
|
||||
newX = x + fm.width(self.title[0])
|
||||
strokeFont = self.page.fontComboBox_titleFont.currentFont()
|
||||
strokeFont.setCapitalization(QFont.SmallCaps)
|
||||
strokeFont.setPixelSize(int((self.fontSize / 7) * 5))
|
||||
strokeFont.setLetterSpacing(QFont.PercentageSpacing, 139)
|
||||
path.addText(newX, y, strokeFont, self.title[1:])
|
||||
else:
|
||||
path.addText(x, y, font, self.title)
|
||||
path = outliner.createStroke(path)
|
||||
image.setPen(QtCore.Qt.NoPen)
|
||||
image.setBrush(PaintColor(*self.strokeColor))
|
||||
image.drawPath(path)
|
||||
|
||||
image.setFont(font)
|
||||
image.setPen(self.textColor)
|
||||
image.drawText(x, y, self.title)
|
||||
|
||||
# turn QImage into Pillow frame
|
||||
frame = image.finalize()
|
||||
if self.shadow:
|
||||
shadImg = ImageEnhance.Contrast(frame).enhance(0.0)
|
||||
shadImg = shadImg.filter(ImageFilter.GaussianBlur(self.shadBlur))
|
||||
shadImg = ImageChops.offset(shadImg, self.shadX, self.shadY)
|
||||
shadImg.paste(frame, box=(0, 0), mask=frame)
|
||||
frame = shadImg
|
||||
|
||||
return frame
|
||||
|
||||
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,671 @@
|
|||
<?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="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<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="Expanding" 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>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" 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="Preferred" 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_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">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</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="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Center Text</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="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>50</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">
|
||||
<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="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>50</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>999999999</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<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="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Text Color</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_textColor">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" 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="MaximumSize" stdset="0">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</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="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="prefix">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>500</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_3">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Font Style</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_fontStyle">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Normal</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Semi-Bold</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Bold</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Italic</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Bold Italic</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Faux Italic</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Small Caps</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_12">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_textColor">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::NoFocus</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>255,255,255</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<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>Stroke</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_stroke">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string>px</string>
|
||||
</property>
|
||||
</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>Stroke Color</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_strokeColor">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::NoFocus</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>0,0,0</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_strokeColor">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" 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="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_2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_shadow">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Shadow</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_shadX">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Shadow Offset</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_shadX">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-1000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>1000</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>-4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_shadY">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-1000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>1000</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>8</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_shadBlur">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Shadow Blur</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="spinBox_shadBlur">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>99.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>5.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<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>
|
||||
</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,226 @@
|
|||
from PIL import Image
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
import os
|
||||
import math
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
from component import Component
|
||||
from toolkit.frame import BlankFrame, scale
|
||||
from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo
|
||||
from toolkit import checkOutput
|
||||
|
||||
|
||||
log = logging.getLogger('AVP.Components.Video')
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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.mergeUndo = False
|
||||
self.page.lineEdit_video.setText(filename)
|
||||
self.mergeUndo = True
|
||||
|
||||
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',
|
||||
])
|
||||
|
||||
if self.core.logEnabled:
|
||||
logFilename = os.path.join(
|
||||
self.core.logDir, 'preview_%s.log' % str(self.compPos))
|
||||
log.debug('Creating ffmpeg process (log at %s)' % logFilename)
|
||||
with open(logFilename, 'w') as logf:
|
||||
logf.write(" ".join(command) + '\n\n')
|
||||
with open(logFilename, 'a') as logf:
|
||||
pipe = openPipe(
|
||||
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=logf, bufsize=10**8
|
||||
)
|
||||
else:
|
||||
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,215 @@
|
|||
from PIL import Image
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
from PyQt5.QtGui import QColor
|
||||
import os
|
||||
import math
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
from component import Component
|
||||
from toolkit.frame import BlankFrame, scale
|
||||
from toolkit import checkOutput
|
||||
from toolkit.ffmpeg import (
|
||||
openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger('AVP.Components.Waveform')
|
||||
|
||||
|
||||
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',
|
||||
self.core.junkStream
|
||||
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',
|
||||
])
|
||||
if self.core.logEnabled:
|
||||
logFilename = os.path.join(
|
||||
self.core.logDir, 'preview_%s.log' % str(self.compPos))
|
||||
log.debug('Creating ffmpeg log at %s', logFilename)
|
||||
with open(logFilename, 'w') as logf:
|
||||
logf.write(" ".join(command) + '\n\n')
|
||||
with open(logFilename, 'a') as logf:
|
||||
pipe = openPipe(
|
||||
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=logf, bufsize=10**8
|
||||
)
|
||||
else:
|
||||
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'
|
||||
)
|
||||
)
|
||||
|
||||
baselineHeight = int(self.height * (4 / 1080))
|
||||
return [
|
||||
'-filter_complex',
|
||||
'%s%s%s'
|
||||
'%s%s%s [v1]; '
|
||||
'[v1] scale=%s:%s%s [v]' % (
|
||||
exampleSound('wave', extra='')
|
||||
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=%s:color=%s@%s' % (
|
||||
baselineHeight, 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,606 @@
|
|||
'''
|
||||
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 logging
|
||||
|
||||
import toolkit
|
||||
|
||||
|
||||
log = logging.getLogger('AVP.Core')
|
||||
STDOUT_LOGLVL = logging.WARNING
|
||||
FILE_LOGLVL = None
|
||||
|
||||
|
||||
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 __repr__(self):
|
||||
return "\n=~=~=~=\n".join(
|
||||
[repr(comp) for comp in self.selectedComponents]
|
||||
)
|
||||
|
||||
def importComponents(self):
|
||||
def findComponents():
|
||||
for f in os.listdir(Core.componentsPath):
|
||||
name, ext = os.path.splitext(f)
|
||||
if name.startswith("__"):
|
||||
continue
|
||||
elif ext == '.py':
|
||||
yield name
|
||||
log.debug('Importing component modules')
|
||||
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, component, loader):
|
||||
'''
|
||||
Creates a new component using these args:
|
||||
(compPos, component obj or moduleIndex, MWindow/Command/Core obj)
|
||||
'''
|
||||
if compPos < 0 or compPos > len(self.selectedComponents):
|
||||
compPos = len(self.selectedComponents)
|
||||
if len(self.selectedComponents) > 50:
|
||||
return -1
|
||||
if type(component) is int:
|
||||
# create component using module index in self.modules
|
||||
moduleIndex = int(component)
|
||||
log.debug(
|
||||
'Creating new component from module #%s', str(moduleIndex))
|
||||
component = self.modules[moduleIndex].Component(
|
||||
moduleIndex, compPos, self
|
||||
)
|
||||
component.widget(loader)
|
||||
else:
|
||||
moduleIndex = -1
|
||||
log.debug(
|
||||
'Inserting previously-created %s component', component.name)
|
||||
|
||||
component._error.connect(
|
||||
loader.videoThreadError
|
||||
)
|
||||
self.selectedComponents.insert(
|
||||
compPos,
|
||||
component
|
||||
)
|
||||
if hasattr(loader, 'insertComponent'):
|
||||
loader.insertComponent(compPos)
|
||||
|
||||
self.componentListChanged()
|
||||
self.updateComponent(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):
|
||||
log.debug(
|
||||
'Auto-updating %s #%s',
|
||||
self.selectedComponents[i], str(i))
|
||||
self.selectedComponents[i].update(auto=True)
|
||||
|
||||
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
|
||||
comp = self.selectedComponents[compIndex]
|
||||
comp.loadPreset(
|
||||
saveValueStore,
|
||||
presetName
|
||||
)
|
||||
|
||||
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 getPresetDir(self, comp):
|
||||
'''Get the preset subdir for a particular version of a component'''
|
||||
return os.path.join(Core.presetDir, comp.name, str(comp.version))
|
||||
|
||||
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)
|
||||
with toolkit.blockSignals(widget):
|
||||
toolkit.setWidgetValue(widget, value)
|
||||
|
||||
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 == -1:
|
||||
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:
|
||||
log.warning('%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
|
||||
log.warning('Project 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)
|
||||
'''
|
||||
log.debug('Parsing av file: %s', filepath)
|
||||
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'''
|
||||
log.info('Creating %s', filepath)
|
||||
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:
|
||||
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'''
|
||||
import video_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
|
||||
)
|
||||
# Windows: C:/Users/<USER>/AppData/Local/audio-visualizer
|
||||
# macOS: ~/Library/Preferences/audio-visualizer
|
||||
# Linux: ~/.config/audio-visualizer
|
||||
with open(os.path.join(wd, 'encoder-options.json')) as json_file:
|
||||
encoderOptions = json.load(json_file)
|
||||
|
||||
settings = {
|
||||
'canceled': False,
|
||||
'FFMPEG_BIN': findFfmpeg(),
|
||||
'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'),
|
||||
'junkStream': os.path.join(wd, 'gui', 'background.png'),
|
||||
'encoderOptions': encoderOptions,
|
||||
'resolutions': [
|
||||
'1920x1080',
|
||||
'1280x720',
|
||||
'854x480',
|
||||
],
|
||||
'logDir': os.path.join(dataDir, 'log'),
|
||||
'logEnabled': 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()
|
||||
if not os.path.exists(cls.dataDir):
|
||||
os.makedirs(cls.dataDir)
|
||||
for neededDirectory in (
|
||||
cls.presetDir, cls.logDir, cls.settings.value("projectDir")):
|
||||
if not os.path.exists(neededDirectory):
|
||||
os.mkdir(neededDirectory)
|
||||
cls.makeLogger()
|
||||
|
||||
@classmethod
|
||||
def loadDefaultSettings(cls):
|
||||
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,
|
||||
"pref_undoLimit": 10,
|
||||
}
|
||||
|
||||
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)
|
||||
try:
|
||||
val = int(val)
|
||||
except ValueError:
|
||||
if val == 'true':
|
||||
val = True
|
||||
elif val == 'false':
|
||||
val = False
|
||||
cls.settings.setValue(key, val)
|
||||
|
||||
@staticmethod
|
||||
def makeLogger():
|
||||
# send critical log messages to stdout
|
||||
logStream = logging.StreamHandler()
|
||||
logStream.setLevel(STDOUT_LOGLVL)
|
||||
streamFormatter = logging.Formatter(
|
||||
'<%(name)s> %(levelname)s: %(message)s'
|
||||
)
|
||||
logStream.setFormatter(streamFormatter)
|
||||
log = logging.getLogger('AVP')
|
||||
log.addHandler(logStream)
|
||||
|
||||
if FILE_LOGLVL is not None:
|
||||
# write log files as well!
|
||||
Core.logEnabled = True
|
||||
logFilename = os.path.join(Core.logDir, 'avp_debug.log')
|
||||
libLogFilename = os.path.join(Core.logDir, 'global_debug.log')
|
||||
# delete old logs
|
||||
for log_ in (logFilename, libLogFilename):
|
||||
if os.path.exists(log_):
|
||||
os.remove(log_)
|
||||
|
||||
logFile = logging.FileHandler(logFilename)
|
||||
logFile.setLevel(FILE_LOGLVL)
|
||||
libLogFile = logging.FileHandler(libLogFilename)
|
||||
libLogFile.setLevel(FILE_LOGLVL)
|
||||
fileFormatter = logging.Formatter(
|
||||
'[%(asctime)s] %(threadName)-10.10s %(name)-23.23s %(levelname)s: '
|
||||
'%(message)s'
|
||||
)
|
||||
logFile.setFormatter(fileFormatter)
|
||||
libLogFile.setFormatter(fileFormatter)
|
||||
|
||||
libLog = logging.getLogger()
|
||||
log.addHandler(logFile)
|
||||
libLog.addHandler(libLogFile)
|
||||
# lowest level must be explicitly set on the root Logger
|
||||
libLog.setLevel(0)
|
||||
|
||||
# always store settings in class variables even if a Core object is not created
|
||||
Core.storeSettings()
|
|
@ -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,191 @@
|
|||
'''
|
||||
QCommand classes for every undoable user action performed in the MainWindow
|
||||
'''
|
||||
from PyQt5.QtWidgets import QUndoCommand
|
||||
import os
|
||||
from copy import copy
|
||||
|
||||
from core import Core
|
||||
|
||||
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
# COMPONENT ACTIONS
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
|
||||
class AddComponent(QUndoCommand):
|
||||
def __init__(self, parent, compI, moduleI):
|
||||
super().__init__(
|
||||
"create new %s component" %
|
||||
parent.core.modules[moduleI].Component.name
|
||||
)
|
||||
self.parent = parent
|
||||
self.moduleI = moduleI
|
||||
self.compI = compI
|
||||
self.comp = None
|
||||
|
||||
def redo(self):
|
||||
if self.comp is None:
|
||||
self.parent.core.insertComponent(
|
||||
self.compI, self.moduleI, self.parent)
|
||||
else:
|
||||
# inserting previously-created component
|
||||
self.parent.core.insertComponent(
|
||||
self.compI, self.comp, self.parent)
|
||||
|
||||
def undo(self):
|
||||
self.comp = self.parent.core.selectedComponents[self.compI]
|
||||
self.parent._removeComponent(self.compI)
|
||||
|
||||
|
||||
class RemoveComponent(QUndoCommand):
|
||||
def __init__(self, parent, selectedRows):
|
||||
super().__init__('remove component')
|
||||
self.parent = parent
|
||||
componentList = self.parent.window.listWidget_componentList
|
||||
self.selectedRows = [
|
||||
componentList.row(selected) for selected in selectedRows
|
||||
]
|
||||
self.components = [
|
||||
parent.core.selectedComponents[i] for i in self.selectedRows
|
||||
]
|
||||
|
||||
def redo(self):
|
||||
self.parent._removeComponent(self.selectedRows[0])
|
||||
|
||||
def undo(self):
|
||||
componentList = self.parent.window.listWidget_componentList
|
||||
for index, comp in zip(self.selectedRows, self.components):
|
||||
self.parent.core.insertComponent(
|
||||
index, comp, self.parent
|
||||
)
|
||||
self.parent.drawPreview()
|
||||
|
||||
|
||||
class MoveComponent(QUndoCommand):
|
||||
def __init__(self, parent, row, newRow, tag):
|
||||
super().__init__("move component %s" % tag)
|
||||
self.parent = parent
|
||||
self.row = row
|
||||
self.newRow = newRow
|
||||
self.id_ = ord(tag[0])
|
||||
|
||||
def id(self):
|
||||
'''If 2 consecutive updates have same id, Qt will call mergeWith()'''
|
||||
return self.id_
|
||||
|
||||
def mergeWith(self, other):
|
||||
self.newRow = other.newRow
|
||||
return True
|
||||
|
||||
def do(self, rowa, rowb):
|
||||
componentList = self.parent.window.listWidget_componentList
|
||||
|
||||
page = self.parent.pages.pop(rowa)
|
||||
self.parent.pages.insert(rowb, page)
|
||||
|
||||
item = componentList.takeItem(rowa)
|
||||
componentList.insertItem(rowb, item)
|
||||
|
||||
stackedWidget = self.parent.window.stackedWidget
|
||||
widget = stackedWidget.removeWidget(page)
|
||||
stackedWidget.insertWidget(rowb, page)
|
||||
componentList.setCurrentRow(rowb)
|
||||
stackedWidget.setCurrentIndex(rowb)
|
||||
self.parent.core.moveComponent(rowa, rowb)
|
||||
self.parent.drawPreview(True)
|
||||
|
||||
def redo(self):
|
||||
self.do(self.row, self.newRow)
|
||||
|
||||
def undo(self):
|
||||
self.do(self.newRow, self.row)
|
||||
|
||||
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
# PRESET ACTIONS
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
|
||||
class ClearPreset(QUndoCommand):
|
||||
def __init__(self, parent, compI):
|
||||
super().__init__("clear preset")
|
||||
self.parent = parent
|
||||
self.compI = compI
|
||||
self.component = self.parent.core.selectedComponents[compI]
|
||||
self.store = self.component.savePreset()
|
||||
self.store['preset'] = self.component.currentPreset
|
||||
|
||||
def redo(self):
|
||||
self.parent.core.clearPreset(self.compI)
|
||||
self.parent.updateComponentTitle(self.compI, False)
|
||||
|
||||
def undo(self):
|
||||
self.parent.core.selectedComponents[self.compI].loadPreset(self.store)
|
||||
self.parent.updateComponentTitle(self.compI, self.store)
|
||||
|
||||
|
||||
class OpenPreset(QUndoCommand):
|
||||
def __init__(self, parent, presetName, compI):
|
||||
super().__init__("open %s preset" % presetName)
|
||||
self.parent = parent
|
||||
self.presetName = presetName
|
||||
self.compI = compI
|
||||
|
||||
comp = self.parent.core.selectedComponents[compI]
|
||||
self.store = comp.savePreset()
|
||||
self.store['preset'] = copy(comp.currentPreset)
|
||||
|
||||
def redo(self):
|
||||
self.parent._openPreset(self.presetName, self.compI)
|
||||
|
||||
def undo(self):
|
||||
self.parent.core.selectedComponents[self.compI].loadPreset(
|
||||
self.store)
|
||||
self.parent.parent.updateComponentTitle(self.compI, self.store)
|
||||
|
||||
|
||||
class RenamePreset(QUndoCommand):
|
||||
def __init__(self, parent, path, oldName, newName):
|
||||
super().__init__('rename preset')
|
||||
self.parent = parent
|
||||
self.path = path
|
||||
self.oldName = oldName
|
||||
self.newName = newName
|
||||
|
||||
def redo(self):
|
||||
self.parent.renamePreset(self.path, self.oldName, self.newName)
|
||||
|
||||
def undo(self):
|
||||
self.parent.renamePreset(self.path, self.newName, self.oldName)
|
||||
|
||||
|
||||
class DeletePreset(QUndoCommand):
|
||||
def __init__(self, parent, compName, vers, presetFile):
|
||||
self.parent = parent
|
||||
self.preset = (compName, vers, presetFile)
|
||||
self.path = os.path.join(
|
||||
Core.presetDir, compName, str(vers), presetFile
|
||||
)
|
||||
self.store = self.parent.core.getPreset(self.path)
|
||||
self.presetName = self.store['preset']
|
||||
super().__init__('delete %s preset (%s)' % (self.presetName, compName))
|
||||
self.loadedPresets = [
|
||||
i for i, comp in enumerate(self.parent.core.selectedComponents)
|
||||
if self.presetName == str(comp.currentPreset)
|
||||
]
|
||||
|
||||
def redo(self):
|
||||
os.remove(self.path)
|
||||
for i in self.loadedPresets:
|
||||
self.parent.core.clearPreset(i)
|
||||
self.parent.parent.updateComponentTitle(i, False)
|
||||
self.parent.findPresets()
|
||||
self.parent.drawPresetList()
|
||||
|
||||
def undo(self):
|
||||
self.parent.createNewPreset(*self.preset, self.store)
|
||||
selectedComponents = self.parent.core.selectedComponents
|
||||
for i in self.loadedPresets:
|
||||
selectedComponents[i].currentPreset = self.presetName
|
||||
self.parent.parent.updateComponentTitle(i)
|
||||
self.parent.findPresets()
|
||||
self.parent.drawPresetList()
|
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,835 @@
|
|||
<?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>
|
||||
<widget class="QPushButton" name="pushButton_undo">
|
||||
<property name="text">
|
||||
<string>Undo</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>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,361 @@
|
|||
'''
|
||||
Preset manager object handles all interactions with presets, including
|
||||
the context menu accessed from MainWindow.
|
||||
'''
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
import string
|
||||
import os
|
||||
import logging
|
||||
|
||||
from toolkit import badName
|
||||
from core import Core
|
||||
from gui.actions import *
|
||||
|
||||
|
||||
log = logging.getLogger('AVP.Gui.PresetManager')
|
||||
|
||||
|
||||
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()
|
||||
action = ClearPreset(self.parent, compI)
|
||||
self.parent.undoStack.push(action)
|
||||
|
||||
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
|
||||
index = compPos if compPos is not None else componentList.currentRow()
|
||||
if index == -1:
|
||||
return
|
||||
action = OpenPreset(self, presetName, index)
|
||||
self.parent.undoStack.push(action)
|
||||
|
||||
def _openPreset(self, presetName, index):
|
||||
selectedComponents = self.core.selectedComponents
|
||||
|
||||
componentName = selectedComponents[index].name.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)
|
||||
|
||||
def deletePreset(self, comp, vers, name):
|
||||
action = DeletePreset(self, comp, vers, name)
|
||||
self.parent.undoStack.push(action)
|
||||
|
||||
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):
|
||||
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)
|
||||
if self.presetExists(newPath):
|
||||
return
|
||||
action = RenamePreset(self, path, oldName, newName)
|
||||
self.parent.undoStack.push(action)
|
||||
break
|
||||
|
||||
def renamePreset(self, path, oldName, newName):
|
||||
oldPath = os.path.join(path, oldName)
|
||||
newPath = os.path.join(path, newName)
|
||||
if os.path.exists(newPath):
|
||||
os.remove(newPath)
|
||||
os.rename(oldPath, newPath)
|
||||
self.findPresets()
|
||||
self.drawPresetList()
|
||||
path = os.path.dirname(newPath)
|
||||
for i, comp in enumerate(self.core.selectedComponents):
|
||||
if self.core.getPresetDir(comp) == path \
|
||||
and comp.currentPreset == oldName:
|
||||
self.core.openPreset(newPath, i, newName)
|
||||
self.parent.updateComponentTitle(i, False)
|
||||
self.parent.drawPreview()
|
||||
|
||||
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)
|
|
@ -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,90 @@
|
|||
'''
|
||||
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
|
||||
import logging
|
||||
|
||||
from toolkit.frame import Checkerboard
|
||||
from toolkit import disableWhenOpeningProject
|
||||
|
||||
|
||||
log = logging.getLogger("AVP.Gui.PreviewThread")
|
||||
|
||||
|
||||
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):
|
||||
try:
|
||||
nextPreviewInformation = self.queue.get(block=False)
|
||||
while self.queue.qsize() >= 2:
|
||||
try:
|
||||
self.queue.get(block=False)
|
||||
except Empty:
|
||||
continue
|
||||
width = int(self.settings.value('outputWidth'))
|
||||
height = int(self.settings.value('outputHeight'))
|
||||
if self.background.width != width \
|
||||
or self.background.height != height:
|
||||
self.background = Checkerboard(width, height)
|
||||
|
||||
frame = self.background.copy()
|
||||
log.info('Creating new preview frame')
|
||||
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
|
||||
)
|
||||
log.critical(errMsg)
|
||||
self.error.emit(errMsg)
|
||||
break
|
||||
except RuntimeError as e:
|
||||
log.error(str(e))
|
||||
else:
|
||||
self.frame = ImageQt(frame)
|
||||
self.imageCreated.emit(QtGui.QImage(self.frame))
|
||||
|
||||
except Empty:
|
||||
True
|
|
@ -0,0 +1,61 @@
|
|||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
import logging
|
||||
|
||||
log = logging.getLogger('AVP.Gui.PreviewWindow')
|
||||
|
||||
|
||||
class PreviewWindow(QtWidgets.QLabel):
|
||||
'''
|
||||
Paints the preview QLabel in MainWindow 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)
|
||||
|
||||
def paintEvent(self, event):
|
||||
size = self.size()
|
||||
painter = QtGui.QPainter(self)
|
||||
point = QtCore.QPoint(0, 0)
|
||||
scaledPix = self.pixmap.scaled(
|
||||
size,
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
transformMode=QtCore.Qt.SmoothTransformation)
|
||||
|
||||
# start painting the label from left upper corner
|
||||
point.setX((size.width() - scaledPix.width())/2)
|
||||
point.setY((size.height() - scaledPix.height())/2)
|
||||
painter.drawPixmap(point, scaledPix)
|
||||
|
||||
def changePixmap(self, img):
|
||||
self.pixmap = QtGui.QPixmap(img)
|
||||
self.repaint()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if self.parent.encoding:
|
||||
return
|
||||
|
||||
i = self.parent.window.listWidget_componentList.currentRow()
|
||||
if i >= 0:
|
||||
component = self.parent.core.selectedComponents[i]
|
||||
if not hasattr(component, 'previewClickEvent'):
|
||||
return
|
||||
pos = (event.x(), event.y())
|
||||
size = (self.width(), self.height())
|
||||
butt = event.button()
|
||||
log.info('Click event for #%s: %s button %s' % (
|
||||
i, pos, butt))
|
||||
component.previewClickEvent(
|
||||
pos, size, butt
|
||||
)
|
||||
|
||||
@QtCore.pyqtSlot(str)
|
||||
def threadError(self, msg):
|
||||
self.parent.showMessage(
|
||||
msg=msg,
|
||||
icon='Critical',
|
||||
parent=self
|
||||
)
|
||||
log.info('%', repr(self.parent))
|
|
@ -0,0 +1,60 @@
|
|||
from PyQt5 import uic, QtWidgets
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
|
||||
from __init__ import wd
|
||||
|
||||
|
||||
log = logging.getLogger('AVP.Main')
|
||||
|
||||
|
||||
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()
|
||||
log.debug("Finished creating command object")
|
||||
|
||||
elif mode == 'GUI':
|
||||
from gui.mainwindow import MainWindow
|
||||
|
||||
window = uic.loadUi(os.path.join(wd, "gui", "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)
|
||||
log.debug("Finished creating main window")
|
||||
window.raise_()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1 @@
|
|||
from toolkit.common import *
|
|
@ -0,0 +1,193 @@
|
|||
'''
|
||||
Common functions
|
||||
'''
|
||||
from PyQt5 import QtWidgets
|
||||
import string
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import logging
|
||||
from copy import copy
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
log = logging.getLogger('AVP.Toolkit.Common')
|
||||
|
||||
|
||||
class blockSignals:
|
||||
'''
|
||||
Context manager to temporarily block list of QtWidgets from updating,
|
||||
and guarantee restoring the previous state afterwards.
|
||||
'''
|
||||
def __init__(self, widgets):
|
||||
if type(widgets) is dict:
|
||||
self.widgets = concatDictVals(widgets)
|
||||
else:
|
||||
self.widgets = (
|
||||
widgets if hasattr(widgets, '__iter__')
|
||||
else [widgets]
|
||||
)
|
||||
|
||||
def __enter__(self):
|
||||
log.verbose(
|
||||
'Blocking signals for %s',
|
||||
", ".join([
|
||||
str(w.__class__.__name__) for w in self.widgets
|
||||
])
|
||||
)
|
||||
self.oldStates = [w.signalsBlocked() for w in self.widgets]
|
||||
for w in self.widgets:
|
||||
w.blockSignals(True)
|
||||
|
||||
def __exit__(self, *args):
|
||||
log.verbose(
|
||||
'Resetting blockSignals to %s', str(bool(sum(self.oldStates))))
|
||||
for w, state in zip(self.widgets, self.oldStates):
|
||||
w.blockSignals(state)
|
||||
|
||||
|
||||
def concatDictVals(d):
|
||||
'''Concatenates all values in given dict into one list.'''
|
||||
key, value = d.popitem()
|
||||
d[key] = value
|
||||
final = copy(value)
|
||||
if type(final) is not list:
|
||||
final = [final]
|
||||
final.extend([val for val in d.values()])
|
||||
else:
|
||||
value.extend([item for val in d.values() for item in val])
|
||||
return final
|
||||
|
||||
|
||||
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:
|
||||
log.warning('Failed to connect %s ', str(widget.__class__.__name__))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def setWidgetValue(widget, val):
|
||||
'''Generic setValue method for use with any typical QtWidget'''
|
||||
log.verbose('Setting %s to %s' % (str(widget.__class__.__name__), val))
|
||||
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:
|
||||
log.warning('Failed to set %s ', str(widget.__class__.__name__))
|
||||
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,481 @@
|
|||
'''
|
||||
Tools for using ffmpeg
|
||||
'''
|
||||
import numpy
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import signal
|
||||
from queue import PriorityQueue
|
||||
import logging
|
||||
|
||||
import core
|
||||
from toolkit.common import checkOutput, pipeWrapper
|
||||
|
||||
|
||||
log = logging.getLogger('AVP.Toolkit.Ffmpeg')
|
||||
|
||||
|
||||
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):
|
||||
from component import ComponentError
|
||||
if core.Core.logEnabled:
|
||||
logFilename = os.path.join(
|
||||
core.Core.logDir, 'render_%s.log' % str(self.component.compPos)
|
||||
)
|
||||
log.debug('Creating ffmpeg process (log at %s)', logFilename)
|
||||
with open(logFilename, 'w') as logf:
|
||||
logf.write(" ".join(self.command) + '\n\n')
|
||||
with open(logFilename, 'a') as logf:
|
||||
self.pipe = openPipe(
|
||||
self.command, stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE, stderr=logf, bufsize=10**8
|
||||
)
|
||||
else:
|
||||
self.pipe = openPipe(
|
||||
self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, 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, FileNotFoundError):
|
||||
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:
|
||||
log.error('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:
|
||||
log.error('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(
|
||||
style='white', extra='apulsator=offset_l=0.35:offset_r=0.67'):
|
||||
'''Help generate an example sound for use in creating a preview'''
|
||||
|
||||
if style == 'white':
|
||||
src = '-2+random(0)'
|
||||
elif style == 'freq':
|
||||
src = 'sin(1000*t*PI*t)'
|
||||
elif style == 'wave':
|
||||
src = 'sin(random(0)*2*PI*t)*tan(random(0)*2*PI*t)'
|
||||
elif style == 'stereo':
|
||||
src = '0.1*sin(2*PI*(360-2.5/2)*t) : 0.1*sin(2*PI*(360+2.5/2)*t)'
|
||||
|
||||
return "aevalsrc='%s', %s%s" % (src, extra, ', ' if extra else '')
|
|
@ -0,0 +1,103 @@
|
|||
'''
|
||||
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 logging
|
||||
|
||||
import core
|
||||
|
||||
|
||||
log = logging.getLogger('AVP.Toolkit.Frame')
|
||||
|
||||
|
||||
class FramePainter(QtGui.QPainter):
|
||||
'''
|
||||
A QPainter for a blank frame, which can be converted into a
|
||||
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, penStyle):
|
||||
if type(penStyle) is tuple:
|
||||
super().setPen(PaintColor(*penStyle))
|
||||
else:
|
||||
super().setPen(penStyle)
|
||||
|
||||
def finalize(self):
|
||||
log.verbose("Finalizing FramePainter")
|
||||
imBytes = self.image.bits().asstring(self.image.byteCount())
|
||||
frame = Image.frombytes(
|
||||
'RGBA', (self.image.width(), self.image.height()), imBytes
|
||||
)
|
||||
self.end()
|
||||
return frame
|
||||
|
||||
|
||||
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.
|
||||
'''
|
||||
log.debug('Creating new %s*%s checkerboard' % (width, height))
|
||||
image = FloodFrame(1920, 1080, (0, 0, 0, 0))
|
||||
image.paste(Image.open(
|
||||
os.path.join(core.Core.wd, 'gui', "background.png")),
|
||||
(0, 0)
|
||||
)
|
||||
image = image.resize((width, height))
|
||||
return image
|
|
@ -0,0 +1,396 @@
|
|||
'''
|
||||
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
|
||||
import logging
|
||||
|
||||
from component import ComponentError
|
||||
from toolkit.frame import Checkerboard
|
||||
from toolkit.ffmpeg import (
|
||||
openPipe, readAudioFile,
|
||||
getAudioDuration, createFfmpegCommand
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger("AVP.VideoThread")
|
||||
|
||||
|
||||
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.hertz = 44100
|
||||
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
|
||||
'''
|
||||
def err():
|
||||
self.closePipe()
|
||||
self.cancelExport()
|
||||
self.error = True
|
||||
msg = 'A render node failed critically.'
|
||||
log.critical(msg)
|
||||
comp._error.emit(msg, str(e))
|
||||
|
||||
while not self.stopped:
|
||||
audioI = self.compositeQueue.get()
|
||||
bgI = int(audioI / self.sampleSize)
|
||||
frame = None
|
||||
for layerNo, comp in enumerate(reversed((self.components))):
|
||||
try:
|
||||
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)
|
||||
)
|
||||
except Exception as e:
|
||||
err()
|
||||
|
||||
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
|
||||
'''
|
||||
log.debug('Dispatching Frames for Compositing...')
|
||||
|
||||
for audioI in range(0, self.audioArrayLen, 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
|
||||
self.audioArrayLen = len(self.completeAudioArray)
|
||||
else:
|
||||
duration = getAudioDuration(self.inputFile)
|
||||
self.completeAudioArray = []
|
||||
self.audioArrayLen = int(
|
||||
((duration * self.hertz) +
|
||||
self.hertz) - self.sampleSize)
|
||||
|
||||
self.progressBarUpdate.emit(0)
|
||||
self.progressBarSetText.emit("Starting components...")
|
||||
canceledByComponent = False
|
||||
initText = ", ".join([
|
||||
"%s) %s" % (num, str(component))
|
||||
for num, component in enumerate(reversed(self.components))
|
||||
])
|
||||
print('Loaded Components:', initText)
|
||||
log.info('Calling preFrameRender for %s', initText)
|
||||
self.staticComponents = {}
|
||||
for compNo, comp in enumerate(reversed(self.components)):
|
||||
try:
|
||||
comp.preFrameRender(
|
||||
audioFile=self.inputFile,
|
||||
completeAudioArray=self.completeAudioArray,
|
||||
audioArrayLen=self.audioArrayLen,
|
||||
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]
|
||||
)
|
||||
)
|
||||
log.error(errMsg)
|
||||
comp._error.emit(errMsg, compError[1])
|
||||
break
|
||||
if 'static' in compProps:
|
||||
self.staticComponents[compNo] = \
|
||||
comp.frameRender(0).copy()
|
||||
|
||||
if self.canceled:
|
||||
if canceledByComponent:
|
||||
log.error(
|
||||
'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
|
||||
)
|
||||
cmd = " ".join(ffmpegCommand)
|
||||
print('###### FFMPEG COMMAND ######\n%s' % cmd)
|
||||
print('############################')
|
||||
log.info('Opening pipe to ffmpeg')
|
||||
log.info(cmd)
|
||||
try:
|
||||
self.out_pipe = openPipe(
|
||||
ffmpegCommand,
|
||||
stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
|
||||
)
|
||||
except sp.CalledProcessError:
|
||||
log.critical('Ffmpeg pipe couldn\'t be created!')
|
||||
raise
|
||||
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
# START CREATING THE VIDEO
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
|
||||
# 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, self.audioArrayLen, 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 / self.audioArrayLen) * 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')
|
||||
|
||||
self.closePipe()
|
||||
|
||||
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 closePipe(self):
|
||||
try:
|
||||
self.out_pipe.stdin.close()
|
||||
except BrokenPipeError:
|
||||
log.error('Broken pipe to ffmpeg!')
|
||||
if self.out_pipe.stderr is not None:
|
||||
log.error(self.out_pipe.stderr.read())
|
||||
self.out_pipe.stderr.close()
|
||||
self.error = True
|
||||
self.out_pipe.wait()
|
||||
|
||||
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