From f975144f25d34f97329b2d4e52891061573cea08 Mon Sep 17 00:00:00 2001 From: Aeliton G. Silva Date: Mon, 12 Jan 2026 22:39:55 -0300 Subject: Use pyproject.toml + uv_build This replaces setup.py by a modern pyproject.toml using uv_build backend. Dependencies are being also managed by uv, so to install dependencies and run the project one can execute: ``` uv sync uv run pytest # optional python -m avp ``` To build the both source and binary (wheel) distribution package run: ``` uv build ``` Uv can be installed with `pip install uv`. The directory structure has been changed to reflect best practices. - src/* -> src/avp/ - src/tests -> ../tests --- src/components/__init__.py | 1 - src/components/__template__.ui | 119 ------ src/components/color.py | 176 -------- src/components/color.ui | 666 ----------------------------- src/components/image.py | 129 ------ src/components/image.ui | 388 ----------------- src/components/life.py | 520 ---------------------- src/components/life.ui | 405 ------------------ src/components/original.py | 243 ----------- src/components/original.ui | 243 ----------- src/components/sound.py | 77 ---- src/components/sound.ui | 172 -------- src/components/spectrum.py | 368 ---------------- src/components/spectrum.ui | 946 ----------------------------------------- src/components/text.py | 218 ---------- src/components/text.ui | 671 ----------------------------- src/components/video.py | 254 ----------- src/components/video.ui | 328 -------------- src/components/waveform.py | 230 ---------- src/components/waveform.ui | 383 ----------------- 20 files changed, 6537 deletions(-) delete mode 100644 src/components/__init__.py delete mode 100644 src/components/__template__.ui delete mode 100644 src/components/color.py delete mode 100644 src/components/color.ui delete mode 100644 src/components/image.py delete mode 100644 src/components/image.ui delete mode 100644 src/components/life.py delete mode 100644 src/components/life.ui delete mode 100644 src/components/original.py delete mode 100644 src/components/original.ui delete mode 100644 src/components/sound.py delete mode 100644 src/components/sound.ui delete mode 100644 src/components/spectrum.py delete mode 100644 src/components/spectrum.ui delete mode 100644 src/components/text.py delete mode 100644 src/components/text.ui delete mode 100644 src/components/video.py delete mode 100644 src/components/video.ui delete mode 100644 src/components/waveform.py delete mode 100644 src/components/waveform.ui (limited to 'src/components') diff --git a/src/components/__init__.py b/src/components/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/src/components/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/components/__template__.ui b/src/components/__template__.ui deleted file mode 100644 index 301a2b7..0000000 --- a/src/components/__template__.ui +++ /dev/null @@ -1,119 +0,0 @@ - - - Form - - - - 0 - 0 - 586 - 197 - - - - Form - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - diff --git a/src/components/color.py b/src/components/color.py deleted file mode 100644 index 1f32c23..0000000 --- a/src/components/color.py +++ /dev/null @@ -1,176 +0,0 @@ -from PyQt6 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( - float(self.LG_start), - float(self.LG_start), - float(self.LG_end + width / 3), - float(self.LG_end), - ) - - elif self.fillType == 2: # Radial Gradient - brush = QtGui.QRadialGradient( - float(self.RG_start), - float(self.RG_end), - float(w), - float(h), - float(self.RG_centre), - ) - spread = QtGui.QGradient.Spread.PadSpread - if self.spread == 1: - spread = QtGui.QGradient.Spread.ReflectSpread - elif self.spread == 2: - spread = QtGui.QGradient.Spread.RepeatSpread - brush.setSpread(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 deleted file mode 100644 index c1713fb..0000000 --- a/src/components/color.ui +++ /dev/null @@ -1,666 +0,0 @@ - - - Form - - - - 0 - 0 - 586 - 197 - - - - Form - - - - - - 4 - - - - - - - - 0 - 0 - - - - - 31 - 0 - - - - Color #1 - - - - - - - - 32 - 32 - - - - - - - - 32 - 32 - - - - - - - - - 0 - 0 - - - - - 1 - 0 - - - - 0,0,0 - - - 12 - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 20 - - - - - - - - - 0 - 0 - - - - - 31 - 0 - - - - Color #2 - - - - - - - - 32 - 32 - - - - - - - - 32 - 32 - - - - - - - - - 0 - 0 - - - - - 1 - 0 - - - - 133,133,133 - - - 12 - - - - - - - - - 0 - - - - - - 0 - 0 - - - - Width - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - 80 - 16777215 - - - - - 0 - 0 - - - - 0 - - - 19200 - - - 0 - - - - - - - - 0 - 0 - - - - Height - - - - - - - - 0 - 0 - - - - - 80 - 16777215 - - - - 10800 - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 20 - - - - - - - - - 0 - 0 - - - - X - - - - - - - - 0 - 0 - - - - - 80 - 16777215 - - - - - 0 - 0 - - - - -10000 - - - 10000 - - - 0 - - - - - - - - 0 - 0 - - - - Y - - - - - - - - 0 - 0 - - - - - 80 - 16777215 - - - - -10000 - - - 10000 - - - - - - - - - 0 - - - - - - 0 - 0 - - - - Fill - - - - - - - - 0 - 0 - - - - -1 - - - QComboBox::AdjustToContentsOnFirstShow - - - - - - - - 0 - 0 - - - - Transparent - - - - - - - - 0 - 0 - - - - Stretch - - - - - - - - Pad - - - - - Reflect - - - - - Repeat - - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 40 - 20 - - - - - - - - - - - - - 0 - 0 - - - - 0 - - - 2 - - - - - - - -1 - 0 - 561 - 31 - - - - - - - - 0 - 0 - - - - Start - - - - - - - -10000 - - - 10000 - - - 10 - - - - - - - - 0 - 0 - - - - End - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - -10000 - - - 10000 - - - 10 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - -1 - -1 - 561 - 31 - - - - - - - - 0 - 0 - - - - Start - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - -10000 - - - 10000 - - - 10 - - - - - - - - 0 - 0 - - - - End - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - -10000 - - - 10000 - - - 10 - - - - - - - - 0 - 0 - - - - Centre - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - QAbstractSpinBox::PlusMinus - - - -10000 - - - 10000 - - - 3 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - - - - - diff --git a/src/components/image.py b/src/components/image.py deleted file mode 100644 index 2393611..0000000 --- a/src/components/image.py +++ /dev/null @@ -1,129 +0,0 @@ -from PIL import Image, ImageDraw, ImageEnhance -from PyQt6 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.Transpose.FLIP_LEFT_RIGHT) - if self.stretched and image.size != (width, height): - image = image.resize((width, height), Image.Resampling.LANCZOS) - if scale != 100: - newHeight = int((image.height / 100) * scale) - newWidth = int((image.width / 100) * scale) - image = image.resize((newWidth, newHeight), Image.Resampling.LANCZOS) - - # 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 deleted file mode 100644 index 2dad127..0000000 --- a/src/components/image.ui +++ /dev/null @@ -1,388 +0,0 @@ - - - Form - - - - 0 - 0 - 586 - 197 - - - - Form - - - - - - 4 - - - - - - - - 0 - 0 - - - - - 31 - 0 - - - - Image - - - - - - - - 1 - 0 - - - - - - - - - 0 - 0 - - - - - 1 - 0 - - - - - 32 - 32 - - - - ... - - - - 32 - 32 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 20 - - - - - - - - - 0 - 0 - - - - X - - - - - - - - 0 - 0 - - - - - 80 - 16777215 - - - - -10000 - - - 10000 - - - - - - - - 0 - 0 - - - - Y - - - - - - - - 0 - 0 - - - - - 80 - 16777215 - - - - - 0 - 0 - - - - -1000 - - - 1000 - - - 0 - - - - - - - - - - - Stretch - - - false - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 20 - - - - - - - - Mirror - - - - - - - Rotate - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - QAbstractSpinBox::UpDownArrows - - - ° - - - 0 - - - 359 - - - 0 - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - - - - - - - - - 0 - 0 - - - - Scale - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - QAbstractSpinBox::UpDownArrows - - - % - - - 10 - - - 400 - - - 100 - - - - - - - % - - - 10 - - - 400 - - - 100 - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - Color - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - QAbstractSpinBox::UpDownArrows - - - % - - - 0 - - - 999 - - - 1 - - - 100 - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - diff --git a/src/components/life.py b/src/components/life.py deleted file mode 100644 index 5b719d1..0000000 --- a/src/components/life.py +++ /dev/null @@ -1,520 +0,0 @@ -from PyQt6 import QtGui, QtCore, QtWidgets -from PyQt6.QtGui import QUndoCommand -from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter -import os -import math -import logging - - -from ..component import Component -from ..toolkit.frame import BlankFrame, scale - - -log = logging.getLogger("AVP.Component.Life") - - -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() - # The initial grid: a "Queen Bee Shuttle" - # https://conwaylife.com/wiki/Queen_bee_shuttle - self.startingGrid = set( - [ - (3, 7), - (3, 8), - (4, 7), - (4, 8), - (8, 7), - (9, 6), - (9, 8), - (10, 5), - (10, 9), - (11, 6), - (11, 7), - (11, 8), - (12, 4), - (12, 5), - (12, 9), - (12, 10), - (23, 6), - (23, 7), - (24, 6), - (24, 7), - ] - ) - - # Amount of 'bleed' (off-canvas coordinates) on each side of the grid - self.bleedSize = 40 - - 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.tickGrids = {0: self.startingGrid} - - 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) - - # Compute grid evolution on this frame if it hasn't been computed yet - if tick not in self.tickGrids: - self.tickGrids[tick] = self.gridForTick(tick) - grid = self.tickGrids[tick] - - # Delete old evolution data which we shouldn't need anymore - if tick - 60 in self.tickGrids: - del self.tickGrids[tick - 60] - 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.Resampling.LANCZOS) - 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) - - # Pie - elif shape == "pie": - 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 (a set of tuples). - This must compute the previous ticks' grids if not already computed - """ - if tick - 1 not in self.tickGrids: - self.tickGrids[tick - 1] = self.gridForTick(tick - 1) - - lastGrid = self.tickGrids[tick - 1] - - def neighbours(x, y): - return {cell for cell in self.nearbyCoords(x, y) if cell in lastGrid} - - newGrid = set() - # Copy cells from the previous grid if they have 2 or 3 neighbouring cells - # and if they are within the grid or its bleed area (off-canvas area) - for x, y in lastGrid: - if ( - -self.bleedSize > x > self.gridWidth + self.bleedSize - or -self.bleedSize > y > self.gridHeight + self.bleedSize - ): - continue - surrounding = len(neighbours(x, y)) - if surrounding == 2 or surrounding == 3: - newGrid.add((x, y)) - - # Find positions around living cells which must be checked for reproduction - potentialNewCells = { - coordTup - for origin in lastGrid - for coordTup in list(self.nearbyCoords(*origin)) - } - # Check for reproduction - for x, y in potentialNewCells: - if (x, y) in newGrid: - # Ignore non-empty cell - 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, button): - super().__init__("click %s component #%s" % (comp.name, comp.compPos)) - self.comp = comp - self.pos = [pos] - if button == QtCore.Qt.MouseButton.RightButton: - self.button = 2 - else: - self.button = 1 - - def id(self): - return self.button - - 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.button == 1: # Left-click - self.add() - elif self.button == 2: # Right-click - self.remove() - - def undo(self): - if self.button == 1: # Left-click - self.remove() - elif self.button == 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 deleted file mode 100644 index 30cf9d0..0000000 --- a/src/components/life.ui +++ /dev/null @@ -1,405 +0,0 @@ - - - Form - - - - 0 - 0 - 586 - 197 - - - - Form - - - - - - - - - - - - Simulation Speed - - - - - - - frames per tick - - - 1 - - - 30 - - - 5 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 16777215 - - - - 255,255,255 - - - - - - - - - - - Grid Scale - - - - - - - 22 - - - 128 - - - 32 - - - - - - - Custom Image - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - Image - - - - - - - - - - - 0 - 0 - - - - - 32 - 32 - - - - ... - - - - - - - Color - - - - - - - - 0 - 16777215 - - - - 0,0,0 - - - - - - - - 0 - 0 - - - - - 32 - 32 - - - - - - - false - - - false - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Shape - - - - - - - - Path - - - - - Rectangle - - - - - Elliptical - - - - - Circle - - - - - Lilypad - - - - - Pie - - - - - Duck - - - - - Peace - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - Shadow - - - - - - - Show Grid - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - Up - - - Qt::UpArrow - - - - - - - Down - - - Qt::DownArrow - - - - - - - Left - - - Qt::LeftArrow - - - - - - - Right - - - Qt::RightArrow - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - <!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> - - - 80 - - - Qt::NoTextInteraction - - - false - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - diff --git a/src/components/original.py b/src/components/original.py deleted file mode 100644 index fad797b..0000000 --- a/src/components/original.py +++ /dev/null @@ -1,243 +0,0 @@ -import numpy -from PIL import Image, ImageDraw -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, - "smooth": self.page.spinBox_smooth, - }, - 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 + 0 if not self.smooth else self.smooth / 15 - self.smoothConstantUp = 0.8 - 0 if not self.smooth else self.smooth / 15 - 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.0 / 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): - x0 = bH + j * bF - y0 = vH + bQ - y1 = vH + bQ - spectrum[j * 4] * bP - bH - x1 = bH + j * bF + bF - draw.rectangle( - ( - x0, - y0 if y0 < y1 else y1, - x1 if x1 > x0 else x0, - y1 if y0 < y1 else y0, - ), - fill=color2, - ) - - x0 = bH + bQ + j * bF - y0 = vH - x1 = bH + bQ + j * bF + bH - y1 = vH - spectrum[j * 4] * bP - draw.rectangle( - ( - x0, - y0 if y0 < y1 else y1, - x1 if x1 > x0 else x0, - y1 if y0 < y1 else y0, - ), - fill=color, - ) - - imBottom = imTop.transpose(Image.Transpose.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 deleted file mode 100644 index c7b7e22..0000000 --- a/src/components/original.ui +++ /dev/null @@ -1,243 +0,0 @@ - - - Form - - - - 0 - 0 - 586 - 178 - - - - - 180 - 0 - - - - Form - - - - - - 4 - - - - - - 0 - 0 - - - - Layout - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 20 - - - - - - - - Color - - - - - - - - 32 - 32 - - - - - - - - 32 - 32 - - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 20 - - - - - - - - Y - - - - - - - QAbstractSpinBox::UpDownArrows - - - -5000 - - - 5000 - - - 10 - - - 0 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - 4 - - - - - Scale - - - - - - - QAbstractSpinBox::PlusMinus - - - 1 - - - 20 - - - - - - - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 40 - 20 - - - - - - - - - - QLayout::SetDefaultConstraint - - - 4 - - - - - Sensitivity - - - - - - - 5 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - diff --git a/src/components/sound.py b/src/components/sound.py deleted file mode 100644 index 2df8e38..0000000 --- a/src/components/sound.py +++ /dev/null @@ -1,77 +0,0 @@ -from PyQt6 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 deleted file mode 100644 index 4c11332..0000000 --- a/src/components/sound.ui +++ /dev/null @@ -1,172 +0,0 @@ - - - Form - - - - 0 - 0 - 586 - 197 - - - - Form - - - - - - 4 - - - - - - - - 0 - 0 - - - - - 31 - 0 - - - - Audio File - - - - - - - - 1 - 0 - - - - - - - - - 0 - 0 - - - - - 1 - 0 - - - - - 32 - 32 - - - - ... - - - - 32 - 32 - - - - - - - - - - - - - - Volume - - - - - - - x - - - 10.000000000000000 - - - 0.100000000000000 - - - 1.000000000000000 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Delay - - - - - - - s - - - 9999999.990000000223517 - - - 0.500000000000000 - - - - - - - Chorus - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - diff --git a/src/components/spectrum.py b/src/components/spectrum.py deleted file mode 100644 index 062ebc7..0000000 --- a/src/components/spectrum.py +++ /dev/null @@ -1,368 +0,0 @@ -from PIL import Image -from PyQt6 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", - str(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): - """Makes final FFmpeg filter command""" - - def getFilterComplexCommand(): - """Inner function that creates the final, complex part of the filter command""" - nonlocal self - genericPreview = self.settings.value("pref_genericPreview") - - def getFilterComplexCommandForType(): - """Determine portion of filter command that changes depending on selected type""" - nonlocal self - if preview: - w, h = self.previewSize - else: - w, h = (self.width, self.height) - color = self.page.comboBox_color.currentText().lower() - - 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_ = ( - f"showspectrum=s={w}x{h}:" - "slide=scroll:" - f"win_func={self.page.comboBox_window.currentText()}:" - f"color={color}:" - f"scale={amplitude}," - "colorkey=color=black:" - "similarity=0.1:blend=0.5" - ) - 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_ = ( - f'ahistogram=r={str(self.settings.value("outputFrameRate"))}:' - f"s={w}x{h}:" - "dmode=separate:" - f"ascale={amplitude}:" - f"scale={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_ = ( - f"avectorscope=s={w}x{h}:" - f'draw={"line" if self.draw else "dot"}:' - f"m={m}:" - f"scale={amplitude}:" - f"zoom={str(self.zoom)}" - ) - elif self.filterType == 3: # Musical Scale - filter_ = ( - f'showcqt=r={str(self.settings.value("outputFrameRate"))}:' - f"s={w}x{h}:" - "count=30:" - "text=0:" - f"tc={str(self.tc)}," - "colorkey=color=black:" - "similarity=0.1:blend=0.5" - ) - elif self.filterType == 4: # Phase - filter_ = ( - f'aphasemeter=r={str(self.settings.value("outputFrameRate"))}:' - f"s={w}x{h}:" - "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 " - ) - return filter_ - - 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") - compression = "compand=gain=4," if self.compress else "" - aformat = ( - "aformat=channel_layouts=mono," - if self.mono and self.filterType not in (2, 4) - else "" - ) - filter_ = getFilterComplexCommandForType() - hflip = "hflip, " if self.mirror else "" - trim = ( - "trim=start=%s:end=%s, " - % ( - "{0:.3f}".format(startPt + 12), - "{0:.3f}".format(startPt + 12.5), - ) - if preview - else "" - ) - scale_ = "scale=%sx%s" % scale(self.scale, self.width, self.height, str) - hue = ( - ", hue=h=%s:s=10" % str(self.hue) - if self.hue > 0 and self.filterType != 3 - else "" - ) - convolution = ( - ", 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 "" - ) - - return ( - f"{exampleSnd if preview and genericPreview else '[0:a] '}" - f"{compression}{aformat}{filter_} [v1]; " - f"[v1] {hflip}{trim}{scale_}{hue}{convolution} [v]" - ) - - return [ - "-filter_complex", - getFilterComplexCommand(), - "-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 deleted file mode 100644 index c6a8a15..0000000 --- a/src/components/spectrum.ui +++ /dev/null @@ -1,946 +0,0 @@ - - - Form - - - - 0 - 0 - 586 - 197 - - - - - 0 - 0 - - - - - 0 - 197 - - - - Form - - - - - - 4 - - - - - - - - - - - 0 - 0 - - - - Type - - - - - - - - Spectrum - - - - - Histogram - - - - - Vector Scope - - - - - Musical Scale - - - - - Phase - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 20 - - - - - - - - - 0 - 0 - - - - X - - - - - - - - 0 - 0 - - - - - 80 - 16777215 - - - - -10000 - - - 10000 - - - - - - - - 0 - 0 - - - - Y - - - - - - - - 0 - 0 - - - - - 80 - 16777215 - - - - - 0 - 0 - - - - -10000 - - - 10000 - - - 0 - - - - - - - - - - - Compress - - - - - - - Mono - - - - - - - Mirror - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Hue - - - 4 - - - - - - - ° - - - 359 - - - - - - - - 0 - 0 - - - - Scale - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - QAbstractSpinBox::UpDownArrows - - - % - - - 10 - - - 400 - - - 100 - - - - - - - - - - 0 - 0 - - - - false - - - QFrame::NoFrame - - - QFrame::Plain - - - 0 - - - - - - 0 - 0 - 561 - 66 - - - - - QLayout::SetMaximumSize - - - 0 - - - - - QLayout::SetDefaultConstraint - - - - - - 0 - 0 - - - - - 31 - 0 - - - - Window - - - 4 - - - - - - - - hann - - - - - gauss - - - - - tukey - - - - - dolph - - - - - cauchy - - - - - parzen - - - - - poisson - - - - - rect - - - - - bartlett - - - - - hanning - - - - - hamming - - - - - blackman - - - - - welch - - - - - flattop - - - - - bharris - - - - - bnuttall - - - - - lanczos - - - - - - - - - 0 - 0 - - - - Amplitude - - - 4 - - - - - - - - Square root - - - - - Cubic root - - - - - 4thrt - - - - - 5thrt - - - - - Linear - - - - - Logarithmic - - - - - - - - Qt::Horizontal - - - QSizePolicy::MinimumExpanding - - - - 10 - 20 - - - - - - - - - - - - - 0 - 0 - - - - Color - - - 4 - - - - - - - - Channel - - - - - Intensity - - - - - Rainbow - - - - - Moreland - - - - - Nebulae - - - - - Fire - - - - - Fiery - - - - - Fruit - - - - - Cool - - - - - - - - Qt::Horizontal - - - QSizePolicy::MinimumExpanding - - - - 10 - 20 - - - - - - - - - - - - - - -1 - -1 - 561 - 31 - - - - - - - - - - 0 - 0 - - - - Display Scale - - - 4 - - - - - - - - Logarithmic - - - - - Square root - - - - - Cubic root - - - - - Linear - - - - - Reverse Log - - - - - - - - - 0 - 0 - - - - Amplitude - - - 4 - - - - - - - - Logarithmic - - - - - Linear - - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 40 - 20 - - - - - - - - - - - - - - -1 - -1 - 585 - 64 - - - - - - - - - Mode - - - - - - - - lissajous - - - - - lissajous_xy - - - - - polar - - - - - - - - - 0 - 0 - - - - Amplitude - - - 4 - - - - - - - - Linear - - - - - Square root - - - - - Cubic root - - - - - Logarithmic - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - 0 - 0 - - - - Zoom - - - 4 - - - - - - - 1 - - - 10 - - - - - - - Line - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - 0 - 0 - 561 - 31 - - - - - - - - - - 0 - 0 - - - - Timeclamp - - - 4 - - - - - - - s - - - 3 - - - 0.002000000000000 - - - 1.000000000000000 - - - 0.010000000000000 - - - 0.017000000000000 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - 0 - 0 - 551 - 31 - - - - - - - - - - - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 10 - - - - - - - - - diff --git a/src/components/text.py b/src/components/text.py deleted file mode 100644 index 40c981a..0000000 --- a/src/components/text.py +++ /dev/null @@ -1,218 +0,0 @@ -from PIL import ImageEnhance, ImageFilter, ImageChops -from PyQt6.QtGui import QColor, QFont -from PyQt6 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._sendUpdateSignal - ) - # The QFontComboBox must be connected directly to the Qt Signal - # which triggers the preview to update. - # This unfortunately makes changing the font into a non-undoable action. - # Must be something broken in the conversion to a ComponentAction - - 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) - text_width = fm.boundingRect(self.title).width() - x = self.pixelValForAttr("xPosition") - - if self.alignment == 1: # Middle - offset = int(text_width / 2) - elif self.alignment == 2: # Right - offset = text_width - else: - raise ValueError(f"Alignment value {self.alignment} unknown") - - 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.Style.StyleNormal) - font.setWeight(QFont.Weight.Normal) - font.setCapitalization(QFont.Capitalization.MixedCase) - if self.fontStyle == 1: - font.setWeight(QFont.Weight.DemiBold) - if self.fontStyle == 2: - font.setWeight(QFont.Weight.Bold) - elif self.fontStyle == 3: - font.setStyle(QFont.Style.StyleItalic) - elif self.fontStyle == 4: - font.setWeight(QFont.Weight.Bold) - font.setStyle(QFont.Style.StyleItalic) - elif self.fontStyle == 5: - font.setStyle(QFont.Style.StyleOblique) - elif self.fontStyle == 6: - font.setCapitalization(QFont.Capitalization.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.boundingRect(self.title[0]).width() - strokeFont = self.page.fontComboBox_titleFont.currentFont() - strokeFont.setCapitalization(QFont.Capitalization.SmallCaps) - strokeFont.setPixelSize(int((self.fontSize / 7) * 5)) - strokeFont.setLetterSpacing(QFont.SpacingType.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.PenStyle.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 deleted file mode 100644 index b62e0ed..0000000 --- a/src/components/text.ui +++ /dev/null @@ -1,671 +0,0 @@ - - - Form - - - - 0 - 0 - 586 - 197 - - - - Form - - - - - - 6 - - - QLayout::SetDefaultConstraint - - - 4 - - - - - - - Title - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - Testing New GUI - - - - - - - - 0 - 0 - - - - Font - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - - - - - - 0 - - - - - - 0 - 0 - - - - Text Layout - - - - - - - - 0 - 0 - - - - - 100 - 16777215 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 20 - - - - - - - - - 0 - 0 - - - - Center Text - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 20 - - - - - - - - - 0 - 0 - - - - X - - - - - - - - 0 - 0 - - - - - 50 - 16777215 - - - - - 0 - 0 - - - - 0 - - - 999999999 - - - 0 - - - - - - - - 0 - 0 - - - - Y - - - - - - - - 0 - 0 - - - - - 50 - 16777215 - - - - 999999999 - - - - - - - - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Text Color - - - - - - - - 0 - 0 - - - - - 32 - 32 - - - - - - - - 32 - 32 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 20 - - - - - - - - - 0 - 0 - - - - Font Size - - - - - - - - 0 - 0 - - - - - - - - - - 1 - - - 500 - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 20 - - - - - - - - - 0 - 0 - - - - Font Style - - - - - - - - Normal - - - - - Semi-Bold - - - - - Bold - - - - - Italic - - - - - Bold Italic - - - - - Faux Italic - - - - - Small Caps - - - - - - - - - - - - - 0 - 0 - - - - - 0 - 16777215 - - - - Qt::NoFocus - - - 255,255,255 - - - - - - - - 0 - 0 - - - - Stroke - - - - - - - - 0 - 0 - - - - px - - - - - - - - 0 - 0 - - - - Stroke Color - - - - - - - - 0 - 0 - - - - - 0 - 16777215 - - - - Qt::NoFocus - - - 0,0,0 - - - - - - - - 0 - 0 - - - - - 32 - 32 - - - - - - - - 32 - 32 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - 0 - 0 - - - - Shadow - - - - - - - - 0 - 0 - - - - Shadow Offset - - - - - - - - 0 - 0 - - - - -1000 - - - 1000 - - - -4 - - - - - - - - 0 - 0 - - - - -1000 - - - 1000 - - - 8 - - - - - - - - 0 - 0 - - - - Shadow Blur - - - - - - - - 0 - 0 - - - - 99.000000000000000 - - - 0.100000000000000 - - - 5.000000000000000 - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 40 - 20 - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - diff --git a/src/components/video.py b/src/components/video.py deleted file mode 100644 index 65a05af..0000000 --- a/src/components/video.py +++ /dev/null @@ -1,254 +0,0 @@ -from PIL import Image -from PyQt6 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 = [] - outputFile = None - if hasattr(self.parent, "lineEdit_outputFile"): - # check only happens in GUI mode - outputFile = self.parent.lineEdit_outputFile.text() - - 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 outputFile and 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 deleted file mode 100644 index 08d15d3..0000000 --- a/src/components/video.ui +++ /dev/null @@ -1,328 +0,0 @@ - - - Form - - - - 0 - 0 - 586 - 197 - - - - - 0 - 0 - - - - - 0 - 197 - - - - Form - - - - - - 4 - - - - - - - - 0 - 0 - - - - - 31 - 0 - - - - Video - - - - - - - - 1 - 0 - - - - - - - - - 0 - 0 - - - - - 1 - 0 - - - - - 32 - 32 - - - - ... - - - - 32 - 32 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 20 - - - - - - - - - 0 - 0 - - - - X - - - - - - - - 0 - 0 - - - - - 80 - 16777215 - - - - -10000 - - - 10000 - - - - - - - - 0 - 0 - - - - Y - - - - - - - - 0 - 0 - - - - - 80 - 16777215 - - - - - 0 - 0 - - - - -10000 - - - 10000 - - - 0 - - - - - - - - - - - - - Loop - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Distort by scale - - - - - - - Scale - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - QAbstractSpinBox::UpDownArrows - - - % - - - 10 - - - 400 - - - 100 - - - - - - - - - - - Use Audio - - - - - - - Volume - - - - - - - - 0 - 0 - - - - x - - - 0.000000000000000 - - - 10.000000000000000 - - - 0.100000000000000 - - - 1.000000000000000 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - diff --git a/src/components/waveform.py b/src/components/waveform.py deleted file mode 100644 index 7dc0b99..0000000 --- a/src/components/waveform.py +++ /dev/null @@ -1,230 +0,0 @@ -from PIL import Image -from PyQt6 import QtGui, QtCore, QtWidgets -from PyQt6.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", - str(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=" - f'r={str(self.settings.value("outputFrameRate"))}:' - f's={self.settings.value("outputWidth")}x{self.settings.value("outputHeight")}:' - f'mode={self.page.comboBox_mode.currentText().lower() if self.mode != 3 else "p2p"}:' - f"colors={hexcolor}@{opacity}:scale={amplitude}" - ) - elif self.mode > 2: - filter_ = ( - f'showfreqs=s={str(self.settings.value("outputWidth"))}x{str(self.settings.value("outputHeight"))}:' - f'mode={"line" if self.mode == 4 else "bar"}:' - f"colors={hexcolor}@{opacity}" - f":ascale={amplitude}:fscale={'log' if self.mono else 'lin'}" - ) - - baselineHeight = int(self.height * (4 / 1080)) - return [ - "-filter_complex", - f"{exampleSound('wave', extra='') if preview and genericPreview else '[0:a] '}" - f"{'compand=gain=4,' if self.compress else ''}" - f"{'aformat=channel_layouts=mono,' if self.mono and self.mode < 3 else ''}" - f"{filter_}" - f"{', drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=%s:color=%s@%s' % (baselineHeight, hexcolor, opacity) if self.mode < 2 else ''}" - f"{', hflip' if self.mirror else''}" - " [v1]; " - "[v1] scale=%s:%s%s [v]" - % ( - 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 deleted file mode 100644 index 5473f33..0000000 --- a/src/components/waveform.ui +++ /dev/null @@ -1,383 +0,0 @@ - - - Form - - - - 0 - 0 - 586 - 197 - - - - - 0 - 0 - - - - - 0 - 197 - - - - Form - - - - - - 4 - - - - - - - - 0 - 0 - - - - - 31 - 0 - - - - Mode - - - - - - - - Cline - - - - - Line - - - - - Point - - - - - Frequency Bar - - - - - Frequency Line - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 20 - - - - - - - - - 0 - 0 - - - - X - - - - - - - - 0 - 0 - - - - - 80 - 16777215 - - - - -10000 - - - 10000 - - - - - - - - 0 - 0 - - - - Y - - - - - - - - 0 - 0 - - - - - 80 - 16777215 - - - - - 0 - 0 - - - - -10000 - - - 10000 - - - 0 - - - - - - - - - - - - - Color - - - - - - - Qt::ImhNone - - - - - - - - 0 - 0 - - - - - 32 - 32 - - - - - - - false - - - false - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Opacity - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - QAbstractSpinBox::UpDownArrows - - - % - - - 0 - - - 100 - - - 100 - - - - - - - Scale - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - QAbstractSpinBox::UpDownArrows - - - % - - - 10 - - - 400 - - - 100 - - - - - - - - - - - Compress - - - - - - - Mono - - - - - - - Mirror - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Amplitude - - - - - - - - Linear - - - - - Logarithmic - - - - - Square root - - - - - Cubic root - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - -- cgit v1.2.3