From f03a3a686c7304588dd434322c73506531e53595 Mon Sep 17 00:00:00 2001 From: Brianna Rainey Date: Thu, 12 Feb 2026 15:38:54 -0500 Subject: v2.2.4 - Quiet FFmpeg; add "invert" option to Classic Vis; fix CLI parsing for Image component (#96) * change noisiness of terminal output ffmpeg no longer prints everything into the terminal unless we're in `--verbose` mode. percentage progress text stays on one line while not in verbose mode. * Added hint to run `avp --verbose` if `avp --log` is run with no avp_debug.log file present * Classic Visualizer: add invert option * Image component: fix path commandline option * Image component: restrict file formats in CLI to match GUI * Color component: add tooltip to color2 picker (second color of gradients) * change tests to work with pytest-xdist avp core stores its config (location of `settings.ini`) in temp directories if using multiple workers to run tests, so they don't interfere with each other. when using a single worker, the `tests/data/config` directory is still used * check alt comp names when parsing cmdline * rename `original.py` to `classic.py` * move `component.py` into subpackage * rename comp_original to comp_classic * show traceback if renderFrame() raises exception * do not try to insert non-existent components from project files * add "composite" property for components if a component returns "composite" then it will receive a frame to draw on during calls to previewRender and frameRender * more tests of projects, actions, waveform, spectrum, image, color, classic * do not change presetDir to "projects" within PresetManager--- src/avp/components/classic.py | 210 +++++++++++++++++++++++++++++++ src/avp/components/classic.ui | 274 +++++++++++++++++++++++++++++++++++++++++ src/avp/components/color.py | 4 +- src/avp/components/color.ui | 3 + src/avp/components/image.py | 16 ++- src/avp/components/life.py | 7 +- src/avp/components/original.py | 185 ---------------------------- src/avp/components/original.ui | 267 --------------------------------------- src/avp/components/sound.py | 6 +- src/avp/components/spectrum.py | 9 +- src/avp/components/text.py | 10 +- src/avp/components/video.py | 8 +- src/avp/components/waveform.py | 8 +- 13 files changed, 518 insertions(+), 489 deletions(-) create mode 100644 src/avp/components/classic.py create mode 100644 src/avp/components/classic.ui delete mode 100644 src/avp/components/original.py delete mode 100644 src/avp/components/original.ui (limited to 'src/avp/components') diff --git a/src/avp/components/classic.py b/src/avp/components/classic.py new file mode 100644 index 0000000..72089af --- /dev/null +++ b/src/avp/components/classic.py @@ -0,0 +1,210 @@ +import numpy +from PIL import Image, ImageDraw + +from ..libcomponent import BaseComponent +from ..toolkit.frame import BlankFrame, FloodFrame +from ..toolkit.visualizer import createSpectrumArray + + +class Component(BaseComponent): + name = "Classic Visualizer" + version = "1.2.0" + + def names(*args): + return ["Original"] + + def properties(self): + props = ["pcm"] + if self.invert: + props.append("composite") + return props + + def widget(self, *args): + self.scale = 20 + self.bars = 63 + 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.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_sensitivity, + "bars": self.page.spinBox_bars, + "invert": self.page.checkBox_invert, + }, + colorWidgets={ + "visColor": self.page.pushButton_visColor, + }, + relativeWidgets=[ + "y", + ], + ) + + def previewRender(self, frame=None): + 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, + frame, + ) + + def preFrameRender(self, **kwargs): + super().preFrameRender(**kwargs) + smoothConstantDown = 0.08 if not self.smooth else self.smooth / 15 + smoothConstantUp = 0.8 if not self.smooth else self.smooth / 15 + self.spectrumArray = createSpectrumArray( + self, + self.completeAudioArray, + self.sampleSize, + smoothConstantDown, + smoothConstantUp, + self.scale, + self.progressBarUpdate, + self.progressBarSetText, + ) + + def frameRender(self, frameNo, frame=None): + arrayNo = frameNo * self.sampleSize + return self.drawBars( + self.width, + self.height, + self.spectrumArray[arrayNo], + self.visColor, + self.layout, + frame, + ) + + def drawBars(self, width, height, spectrum, color, layout, frame): + bigYCoord = height - height / 8 + smallYCoord = height / 1200 + bigXCoord = width / (self.bars + 1) + middleXCoord = bigXCoord / 2 + smallXCoord = bigXCoord / 4 + + imTop = BlankFrame(width, height) + draw = ImageDraw.Draw(imTop) + r, g, b = color + color2 = (r, g, b, 125) + + for i in range(self.bars): + # draw outline behind rectangles if not inverted + if frame is None: + x0 = middleXCoord + i * bigXCoord + y0 = bigYCoord + smallXCoord + x1 = middleXCoord + i * bigXCoord + bigXCoord + y1 = ( + bigYCoord + + smallXCoord + - spectrum[i * 4] * smallYCoord + - middleXCoord + ) + selection = ( + x0, + y0 if y0 < y1 else y1, + x1 if x1 > x0 else x0, + y1 if y0 < y1 else y0, + ) + draw.rectangle( + selection, + fill=color2, + ) + + x0 = middleXCoord + smallXCoord + i * bigXCoord + y0 = bigYCoord + x1 = middleXCoord + smallXCoord + i * bigXCoord + middleXCoord + y1 = bigYCoord - spectrum[i * 4] * smallYCoord + selection = ( + x0, + y0 if y0 < y1 else y1, + x1 if x1 > x0 else x0, + y1 if y0 < y1 else y0, + ) + # fill rectangle if not inverted + draw.rectangle( + selection, + fill=color if frame is None else (0, 0, 0, 0), + outline=color, + width=int(x1 - x0), + ) + + 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) + + if frame is None: + return im + f = FloodFrame(width, height, color) + f.paste(frame, (0, 0), mask=im) + return f + + 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/avp/components/classic.ui b/src/avp/components/classic.ui new file mode 100644 index 0000000..1ae7faa --- /dev/null +++ b/src/avp/components/classic.ui @@ -0,0 +1,274 @@ + + + Form + + + + 0 + 0 + 586 + 178 + + + + + 180 + 0 + + + + Form + + + + + + 4 + + + + + + 0 + 0 + + + + Layout + + + + + + + + + + Qt::Orientation::Horizontal + + + QSizePolicy::Policy::Fixed + + + + 5 + 20 + + + + + + + + Color + + + + + + + + 32 + 32 + + + + + + + + 32 + 32 + + + + + + + + + + + + + + + Qt::Orientation::Horizontal + + + QSizePolicy::Policy::Fixed + + + + 5 + 20 + + + + + + + + Y + + + + + + + QAbstractSpinBox::ButtonSymbols::UpDownArrows + + + -5000 + + + 5000 + + + 10 + + + 0 + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + 4 + + + + + Scale + + + + + + + QAbstractSpinBox::ButtonSymbols::PlusMinus + + + 1 + + + 20 + + + + + + + Sensitivity + + + + + + + 5 + + + + + + + Qt::Orientation::Horizontal + + + QSizePolicy::Policy::Expanding + + + + 40 + 20 + + + + + + + + + + QLayout::SizeConstraint::SetDefaultConstraint + + + 4 + + + + + Bars + + + + + + + 63 + + + 64 + + + 63 + + + + + + + Invert + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/avp/components/color.py b/src/avp/components/color.py index cb0960a..826f37f 100644 --- a/src/avp/components/color.py +++ b/src/avp/components/color.py @@ -1,14 +1,14 @@ from PyQt6 import QtGui import logging -from ..component import Component +from ..libcomponent import BaseComponent from ..toolkit.frame import BlankFrame, FloodFrame, FramePainter log = logging.getLogger("AVP.Components.Color") -class Component(Component): +class Component(BaseComponent): name = "Color" version = "1.0.0" diff --git a/src/avp/components/color.ui b/src/avp/components/color.ui index c36bdd8..788adb9 100644 --- a/src/avp/components/color.ui +++ b/src/avp/components/color.ui @@ -124,6 +124,9 @@ 32 + + End color of gradient. Disabled if fill is solid. + diff --git a/src/avp/components/image.py b/src/avp/components/image.py index e012cec..a082092 100644 --- a/src/avp/components/image.py +++ b/src/avp/components/image.py @@ -1,14 +1,13 @@ from PIL import Image, ImageOps, ImageEnhance from PyQt6 import QtWidgets import os -from copy import copy -from ..component import Component +from ..libcomponent import BaseComponent from ..toolkit.frame import BlankFrame, addShadow from ..toolkit.visualizer import createSpectrumArray -class Component(Component): +class Component(BaseComponent): name = "Image" version = "2.1.0" @@ -177,17 +176,22 @@ class Component(Component): self.mergeUndo = True def command(self, arg): + def fail(): + print("Not a supported image format") + quit(1) + if "=" in arg: key, arg = arg.split("=", 1) if key == "path" and os.path.exists(arg): + if f"*{os.path.splitext(arg)[1]}" not in self.core.imageFormats: + fail() try: Image.open(arg) self.page.lineEdit_image.setText(arg) - self.page.checkBox_stretch.setChecked(True) + self.page.comboBox_resizeMode.setCurrentIndex(2) return except OSError as e: - print("Not a supported image format") - quit(1) + fail() super().command(arg) def commandHelp(self): diff --git a/src/avp/components/life.py b/src/avp/components/life.py index a062617..374b299 100644 --- a/src/avp/components/life.py +++ b/src/avp/components/life.py @@ -1,13 +1,12 @@ from PyQt6 import QtCore, QtWidgets from PyQt6.QtGui import QUndoCommand -from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter, ImageOps +from PIL import Image, ImageDraw import os -from copy import copy import math import logging -from ..component import Component +from ..libcomponent import BaseComponent from ..toolkit.frame import BlankFrame, scale, addShadow from ..toolkit.visualizer import createSpectrumArray @@ -15,7 +14,7 @@ from ..toolkit.visualizer import createSpectrumArray log = logging.getLogger("AVP.Component.Life") -class Component(Component): +class Component(BaseComponent): name = "Conway's Game of Life" version = "2.0.1" diff --git a/src/avp/components/original.py b/src/avp/components/original.py deleted file mode 100644 index 0da78dc..0000000 --- a/src/avp/components/original.py +++ /dev/null @@ -1,185 +0,0 @@ -import numpy -from PIL import Image, ImageDraw -from copy import copy - -from ..component import Component -from ..toolkit.frame import BlankFrame -from ..toolkit.visualizer import createSpectrumArray - - -class Component(Component): - name = "Classic Visualizer" - version = "1.1.0" - - def names(*args): - return ["Original Audio Visualization"] - - def properties(self): - return ["pcm"] - - def widget(self, *args): - self.scale = 20 - self.bars = 63 - 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.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_sensitivity, - "bars": self.page.spinBox_bars, - }, - 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) - smoothConstantDown = 0.08 if not self.smooth else self.smooth / 15 - smoothConstantUp = 0.8 if not self.smooth else self.smooth / 15 - self.spectrumArray = createSpectrumArray( - self, - self.completeAudioArray, - self.sampleSize, - smoothConstantDown, - smoothConstantUp, - self.scale, - self.progressBarUpdate, - self.progressBarSetText, - ) - - def frameRender(self, frameNo): - arrayNo = frameNo * self.sampleSize - return self.drawBars( - self.width, - self.height, - self.spectrumArray[arrayNo], - self.visColor, - self.layout, - ) - - def drawBars(self, width, height, spectrum, color, layout): - bigYCoord = height - height / 8 - smallYCoord = height / 1200 - bigXCoord = width / (self.bars + 1) - middleXCoord = bigXCoord / 2 - smallXCoord = bigXCoord / 4 - - imTop = BlankFrame(width, height) - draw = ImageDraw.Draw(imTop) - r, g, b = color - color2 = (r, g, b, 125) - - for i in range(self.bars): - x0 = middleXCoord + i * bigXCoord - y0 = bigYCoord + smallXCoord - y1 = bigYCoord + smallXCoord - spectrum[i * 4] * smallYCoord - middleXCoord - x1 = middleXCoord + i * bigXCoord + bigXCoord - draw.rectangle( - ( - x0, - y0 if y0 < y1 else y1, - x1 if x1 > x0 else x0, - y1 if y0 < y1 else y0, - ), - fill=color2, - ) - - x0 = middleXCoord + smallXCoord + i * bigXCoord - y0 = bigYCoord - x1 = middleXCoord + smallXCoord + i * bigXCoord + middleXCoord - y1 = bigYCoord - spectrum[i * 4] * smallYCoord - 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/avp/components/original.ui b/src/avp/components/original.ui deleted file mode 100644 index 8dbdaa2..0000000 --- a/src/avp/components/original.ui +++ /dev/null @@ -1,267 +0,0 @@ - - - Form - - - - 0 - 0 - 586 - 178 - - - - - 180 - 0 - - - - Form - - - - - - 4 - - - - - - 0 - 0 - - - - Layout - - - - - - - - - - Qt::Orientation::Horizontal - - - QSizePolicy::Policy::Fixed - - - - 5 - 20 - - - - - - - - Color - - - - - - - - 32 - 32 - - - - - - - - 32 - 32 - - - - - - - - - - - - - - - Qt::Orientation::Horizontal - - - QSizePolicy::Policy::Fixed - - - - 5 - 20 - - - - - - - - Y - - - - - - - QAbstractSpinBox::ButtonSymbols::UpDownArrows - - - -5000 - - - 5000 - - - 10 - - - 0 - - - - - - - Qt::Orientation::Horizontal - - - - 40 - 20 - - - - - - - - - - 4 - - - - - Scale - - - - - - - QAbstractSpinBox::ButtonSymbols::PlusMinus - - - 1 - - - 20 - - - - - - - Sensitivity - - - - - - - 5 - - - - - - - Qt::Orientation::Horizontal - - - QSizePolicy::Policy::Expanding - - - - 40 - 20 - - - - - - - - - - QLayout::SizeConstraint::SetDefaultConstraint - - - 4 - - - - - Bars - - - - - - - 63 - - - 64 - - - 63 - - - - - - - Qt::Orientation::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::Orientation::Vertical - - - - 20 - 40 - - - - - - - - - diff --git a/src/avp/components/sound.py b/src/avp/components/sound.py index 2df8e38..c212870 100644 --- a/src/avp/components/sound.py +++ b/src/avp/components/sound.py @@ -1,11 +1,11 @@ -from PyQt6 import QtGui, QtCore, QtWidgets +from PyQt6 import QtWidgets import os -from ..component import Component +from ..libcomponent import BaseComponent from ..toolkit.frame import BlankFrame -class Component(Component): +class Component(BaseComponent): name = "Sound" version = "1.0.0" diff --git a/src/avp/components/spectrum.py b/src/avp/components/spectrum.py index 062ebc7..0446865 100644 --- a/src/avp/components/spectrum.py +++ b/src/avp/components/spectrum.py @@ -1,14 +1,11 @@ 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 ..libcomponent import BaseComponent from ..toolkit.frame import BlankFrame, scale -from ..toolkit import checkOutput, connectWidget +from ..toolkit import connectWidget from ..toolkit.ffmpeg import ( openPipe, closePipe, @@ -21,7 +18,7 @@ from ..toolkit.ffmpeg import ( log = logging.getLogger("AVP.Components.Spectrum") -class Component(Component): +class Component(BaseComponent): name = "Spectrum" version = "1.0.1" diff --git a/src/avp/components/text.py b/src/avp/components/text.py index bee117e..d248772 100644 --- a/src/avp/components/text.py +++ b/src/avp/components/text.py @@ -1,16 +1,14 @@ -from PIL import ImageEnhance, ImageFilter, ImageChops -from PyQt6.QtGui import QColor, QFont -from PyQt6 import QtGui, QtCore, QtWidgets -import os +from PyQt6.QtGui import QFont +from PyQt6 import QtGui, QtCore import logging -from ..component import Component +from ..libcomponent import BaseComponent from ..toolkit.frame import FramePainter, addShadow log = logging.getLogger("AVP.Components.Text") -class Component(Component): +class Component(BaseComponent): name = "Title Text" version = "1.0.1" diff --git a/src/avp/components/video.py b/src/avp/components/video.py index 65a05af..1e9b788 100644 --- a/src/avp/components/video.py +++ b/src/avp/components/video.py @@ -1,20 +1,18 @@ from PIL import Image -from PyQt6 import QtGui, QtCore, QtWidgets +from PyQt6 import QtWidgets import os -import math import subprocess import logging -from ..component import Component +from ..libcomponent import BaseComponent 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): +class Component(BaseComponent): name = "Video" version = "1.0.0" diff --git a/src/avp/components/waveform.py b/src/avp/components/waveform.py index e10dec2..bfebc30 100644 --- a/src/avp/components/waveform.py +++ b/src/avp/components/waveform.py @@ -3,12 +3,10 @@ from PyQt6.QtGui import QColor import os import subprocess import logging -from copy import copy -from ..component import Component -from ..toolkit.visualizer import transformData, createSpectrumArray +from ..libcomponent import BaseComponent +from ..toolkit.visualizer import createSpectrumArray from ..toolkit.frame import BlankFrame, scale -from ..toolkit import checkOutput from ..toolkit.ffmpeg import ( openPipe, closePipe, @@ -21,7 +19,7 @@ from ..toolkit.ffmpeg import ( log = logging.getLogger("AVP.Components.Waveform") -class Component(Component): +class Component(BaseComponent): name = "Waveform" version = "2.0.0" -- cgit v1.2.3