From f66eb99465c61232a7f649e66bee59504bb0e52c Mon Sep 17 00:00:00 2001 From: Brianna Rainey Date: Wed, 28 Jan 2026 17:49:58 -0500 Subject: v2.2.1 - fix #74, fix #92, add optional 64th bar to Classic Visualizer, improve Conway default (#93) * update gitignore ignore profiling and coverage data * F1 opens help window, create appName variable, move undostack class * fix kaleidoscope effect, increase default Y values by +4 the increased y values allow the cells to continue animating for more than 60 minutes instead of 30 (at default 60f/t) * update version number * add minimumWidth to undo history window * Classic Visualizer: option to include 64th bar * Waveform component: fix #74 - new animation speed option * move shared visualizer code into toolkit * Waveform component: compress audio by default * Waveform component: fix 100% animation speed * new components receive random color * update to Qt 6 * fix pushbutton stylesheet * fix #92: replace ok/cancel with save/discard/cancel * remove obsolete PaintColor subclass * mv common shadow code into addShadow func * add 3rd option of ok/cancel back to showMessage the 3 options are: - ok - ok/cancel - save/discard/cancel * Image component: add shadow option * small test of rgbFromString * fix color tuple string * test another way to get comp names from CLI * rename component tests, add some more * Image component: scale shadow based on resolution * catch AttributeError if previewRender returns None * Text component: fix blur radius only able to increase the relativeWidgets system causes QDoubleSpinbox to only allow increases, because it really only works with integeres, so I changed the blur radius into a normal QSpinBox. I noted where the problem exists within component.py for future reference. This commit also removes an unneeded VerticalLayout from the ui file * remove unnecessary QVBoxLayout * paste shadow at x,y instead of using offset method * fix tests due to shadow change * don't print warning in connectWidget due to QFontComboBox--- src/avp/toolkit/common.py | 18 ++++++++- src/avp/toolkit/frame.py | 26 ++++--------- src/avp/toolkit/visualizer.py | 87 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 20 deletions(-) create mode 100644 src/avp/toolkit/visualizer.py (limited to 'src/avp/toolkit') diff --git a/src/avp/toolkit/common.py b/src/avp/toolkit/common.py index e35aba2..a6195ed 100644 --- a/src/avp/toolkit/common.py +++ b/src/avp/toolkit/common.py @@ -4,7 +4,7 @@ Common functions from PyQt6 import QtWidgets import string -import os +import random import sys import subprocess import logging @@ -135,7 +135,12 @@ def rgbFromString(string): if i > 255 or i < 0: raise ValueError return tup - except: + except Exception as e: + log.warning( + "Could not parse '%s' as a color (encountered %s).", + string, + type(e).__name__, + ) return (255, 255, 255) @@ -150,6 +155,7 @@ def formatTraceback(tb=None): def connectWidget(widget, func): + unsupportedWidgets = ["QtWidgets.QFontComboBox"] if type(widget) == QtWidgets.QLineEdit: widget.textChanged.connect(func) elif type(widget) == QtWidgets.QSpinBox or type(widget) == QtWidgets.QDoubleSpinBox: @@ -158,6 +164,10 @@ def connectWidget(widget, func): widget.stateChanged.connect(func) elif type(widget) == QtWidgets.QComboBox: widget.currentIndexChanged.connect(func) + elif type(widget) in unsupportedWidgets: + log.info( + "Could not connect %s using connectWidget()", str(widget.__class__.__name__) + ) else: log.warning("Failed to connect %s ", str(widget.__class__.__name__)) return False @@ -190,3 +200,7 @@ def getWidgetValue(widget): return widget.isChecked() elif type(widget) == QtWidgets.QComboBox: return widget.currentIndex() + + +def randomColor(): + return (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) diff --git a/src/avp/toolkit/frame.py b/src/avp/toolkit/frame.py index 94537a6..829b05b 100644 --- a/src/avp/toolkit/frame.py +++ b/src/avp/toolkit/frame.py @@ -3,7 +3,7 @@ Common tools for drawing compatible frames in a Component's frameRender() """ from PyQt6 import QtGui -from PIL import Image +from PIL import Image, ImageEnhance, ImageChops, ImageFilter from PIL.ImageQt import ImageQt from PyQt6 import QtCore import sys @@ -30,7 +30,7 @@ class FramePainter(QtGui.QPainter): def setPen(self, penStyle): if type(penStyle) is tuple: - super().setPen(PaintColor(*penStyle)) + super().setPen(QtGui.QColor(*penStyle)) else: super().setPen(penStyle) @@ -45,24 +45,14 @@ class FramePainter(QtGui.QPainter): buffer.close() self.end() return frame - imBytes = self.image.bits().asstring(self.image.byteCount()) - frame = Image.frombytes( - "RGBA", (self.image.width(), self.image.height()), imBytes - ) - self.end() - return frame - -class PaintColor(QtGui.QColor): - """ - Subclass of QtGui.QColor with an added scale() method - Previously this class reversed the painter colour to solve - hardware issues related to endianness, - but Qt appears to deal with this itself nowadays - """ - def __init__(self, r, g, b, a=255): - super().__init__(r, g, b, a) +def addShadow(frame, blurRadius, blurOffsetX, blurOffsetY): + shadImg = ImageEnhance.Contrast(frame).enhance(0.0) + shadImg = shadImg.filter(ImageFilter.GaussianBlur(blurRadius)) + frame = shadImg.paste(frame, box=(-blurOffsetX, -blurOffsetY), mask=frame) + frame = shadImg + return frame def scale(scalePercent, width, height, returntype=None): diff --git a/src/avp/toolkit/visualizer.py b/src/avp/toolkit/visualizer.py new file mode 100644 index 0000000..c55a3f3 --- /dev/null +++ b/src/avp/toolkit/visualizer.py @@ -0,0 +1,87 @@ +"""Functions used to transform and manipulate audio for use by visualizers""" + +from copy import copy +import numpy + + +def createSpectrumArray( + component, + completeAudioArray, + sampleSize, + smoothConstantDown, + smoothConstantUp, + scale, + progressBarUpdate, + progressBarSetText, +): + lastSpectrum = None + spectrumArray = {} + for i in range(0, len(completeAudioArray), sampleSize): + if component.canceled: + break + lastSpectrum = transformData( + i, + completeAudioArray, + sampleSize, + smoothConstantDown, + smoothConstantUp, + lastSpectrum, + scale, + ) + spectrumArray[i] = copy(lastSpectrum) + + progress = int(100 * (i / len(completeAudioArray))) + if progress >= 100: + progress = 100 + progressText = f"Analyzing audio: {str(progress)}%" + progressBarSetText.emit(progressText) + progressBarUpdate.emit(int(progress)) + return spectrumArray + + +def transformData( + i, + completeAudioArray, + sampleSize, + smoothConstantDown, + smoothConstantUp, + lastSpectrum, + scale, +): + 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 + + with numpy.errstate(divide="ignore"): + y = 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 -- cgit v1.2.3