diff options
| author | martin | 2022-05-01 22:41:20 +0200 |
|---|---|---|
| committer | GitHub | 2022-05-01 22:41:20 +0200 |
| commit | 4c5aa37aa6f41d909153a2b7d522db6d7582659a (patch) | |
| tree | 326aa67921439defcb8c25ea5f770feb63e878a4 /src/components | |
| parent | 4a3ff8bfce622de0e5affa312d50557b5d336371 (diff) | |
| parent | 820358a79a87b214139eb7693ce80e96be79e3d8 (diff) | |
Merge pull request #69 from djfun/feature-newgui
GUI Redesign with Component System
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/__init__.py | 1 | ||||
| -rw-r--r-- | src/components/__template__.ui | 119 | ||||
| -rw-r--r-- | src/components/color.py | 160 | ||||
| -rw-r--r-- | src/components/color.ui | 666 | ||||
| -rw-r--r-- | src/components/image.py | 126 | ||||
| -rw-r--r-- | src/components/image.ui | 388 | ||||
| -rw-r--r-- | src/components/life.py | 475 | ||||
| -rw-r--r-- | src/components/life.ui | 405 | ||||
| -rw-r--r-- | src/components/original.py | 203 | ||||
| -rw-r--r-- | src/components/original.ui | 193 | ||||
| -rw-r--r-- | src/components/sound.py | 73 | ||||
| -rw-r--r-- | src/components/sound.ui | 172 | ||||
| -rw-r--r-- | src/components/spectrum.py | 316 | ||||
| -rw-r--r-- | src/components/spectrum.ui | 946 | ||||
| -rw-r--r-- | src/components/text.py | 203 | ||||
| -rw-r--r-- | src/components/text.ui | 671 | ||||
| -rw-r--r-- | src/components/video.py | 226 | ||||
| -rw-r--r-- | src/components/video.ui | 328 | ||||
| -rw-r--r-- | src/components/waveform.py | 215 | ||||
| -rw-r--r-- | src/components/waveform.ui | 383 |
20 files changed, 6269 insertions, 0 deletions
diff --git a/src/components/__init__.py b/src/components/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/components/__init__.py @@ -0,0 +1 @@ + diff --git a/src/components/__template__.ui b/src/components/__template__.ui new file mode 100644 index 0000000..301a2b7 --- /dev/null +++ b/src/components/__template__.ui @@ -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> diff --git a/src/components/color.py b/src/components/color.py new file mode 100644 index 0000000..8d0edd2 --- /dev/null +++ b/src/components/color.py @@ -0,0 +1,160 @@ +from PyQt5 import QtGui +import logging + +from ..component import Component +from ..toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor + + +log = logging.getLogger('AVP.Components.Color') + + +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): + log.debug("Color component is drawing frame #%s", 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) diff --git a/src/components/color.ui b/src/components/color.ui new file mode 100644 index 0000000..c1713fb --- /dev/null +++ b/src/components/color.ui @@ -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> diff --git a/src/components/image.py b/src/components/image.py new file mode 100644 index 0000000..42f9564 --- /dev/null +++ b/src/components/image.py @@ -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) diff --git a/src/components/image.ui b/src/components/image.ui new file mode 100644 index 0000000..2dad127 --- /dev/null +++ b/src/components/image.ui @@ -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> diff --git a/src/components/life.py b/src/components/life.py new file mode 100644 index 0000000..94704bc --- /dev/null +++ b/src/components/life.py @@ -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() diff --git a/src/components/life.ui b/src/components/life.ui new file mode 100644 index 0000000..85b2926 --- /dev/null +++ b/src/components/life.ui @@ -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> diff --git a/src/components/original.py b/src/components/original.py new file mode 100644 index 0000000..80228fe --- /dev/null +++ b/src/components/original.py @@ -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') diff --git a/src/components/original.ui b/src/components/original.ui new file mode 100644 index 0000000..a4d5119 --- /dev/null +++ b/src/components/original.ui @@ -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> diff --git a/src/components/sound.py b/src/components/sound.py new file mode 100644 index 0000000..118ea23 --- /dev/null +++ b/src/components/sound.py @@ -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) diff --git a/src/components/sound.ui b/src/components/sound.ui new file mode 100644 index 0000000..4c11332 --- /dev/null +++ b/src/components/sound.ui @@ -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> diff --git a/src/components/spectrum.py b/src/components/spectrum.py new file mode 100644 index 0000000..91f2afb --- /dev/null +++ b/src/components/spectrum.py @@ -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, 'lineEdit_audioFile'): + # update preview when audio file changes (if genericPreview is off) + self.parent.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.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 diff --git a/src/components/spectrum.ui b/src/components/spectrum.ui new file mode 100644 index 0000000..c6a8a15 --- /dev/null +++ b/src/components/spectrum.ui @@ -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> diff --git a/src/components/text.py b/src/components/text.py new file mode 100644 index 0000000..e8c5a9c --- /dev/null +++ b/src/components/text.py @@ -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) diff --git a/src/components/text.ui b/src/components/text.ui new file mode 100644 index 0000000..b62e0ed --- /dev/null +++ b/src/components/text.ui @@ -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> diff --git a/src/components/video.py b/src/components/video.py new file mode 100644 index 0000000..9fffc26 --- /dev/null +++ b/src/components/video.py @@ -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, 'lineEdit_outputFile'): + outputFile = self.parent.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 diff --git a/src/components/video.ui b/src/components/video.ui new file mode 100644 index 0000000..08d15d3 --- /dev/null +++ b/src/components/video.ui @@ -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> diff --git a/src/components/waveform.py b/src/components/waveform.py new file mode 100644 index 0000000..227f711 --- /dev/null +++ b/src/components/waveform.py @@ -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, 'lineEdit_audioFile'): + self.parent.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.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 diff --git a/src/components/waveform.ui b/src/components/waveform.ui new file mode 100644 index 0000000..5473f33 --- /dev/null +++ b/src/components/waveform.ui @@ -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> |
