diff --git a/.gitignore b/.gitignore
index 6fe019a..9800ef6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ __pycache__
build/
dist/
env/
+prof/
.venv/
.env/
.vscode/
@@ -24,3 +25,5 @@ ffmpeg
*.goutput*
*.kate-swp
*.code-workspace
+.coverage
+htmlcov/
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index a245e09..2f0647c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "uv_build"
name = "audio-visualizer-python"
description = "Create audio visualization videos from a GUI or commandline"
readme = "README.md"
-version = "2.2.0"
+version = "2.2.1"
requires-python = ">= 3.12"
license = "MIT"
classifiers=[
diff --git a/src/avp/__init__.py b/src/avp/__init__.py
index ea32f26..a88bf10 100644
--- a/src/avp/__init__.py
+++ b/src/avp/__init__.py
@@ -3,7 +3,7 @@
import logging
-__version__ = "2.2.0"
+__version__ = "2.2.1"
class Logger(logging.getLoggerClass()):
diff --git a/src/avp/component.py b/src/avp/component.py
index 6c5e381..5906ab1 100644
--- a/src/avp/component.py
+++ b/src/avp/component.py
@@ -18,6 +18,7 @@
setWidgetValue,
connectWidget,
rgbFromString,
+ randomColor,
blockSignals,
)
@@ -635,9 +636,17 @@ def pickColor_():
self._colorFuncs = {attr: makeColorFunc(attr) for attr in kwargs[kwarg]}
for attr, func in self._colorFuncs.items():
+ colorText = self._trackedWidgets[attr].text()
+ if colorText == "":
+ rndColor = randomColor()
+ self._trackedWidgets[attr].setText(str(rndColor)[1:-1])
self._colorWidgets[attr].clicked.connect(func)
self._colorWidgets[attr].setStyleSheet(
- "QPushButton {" "background-color : #FFFFFF; outline: none; }"
+ "QPushButton {"
+ "background-color : %s; outline: none; }"
+ % QColor(
+ *rgbFromString(colorText) if colorText else rndColor
+ ).name()
)
if kwarg == "relativeWidgets":
@@ -798,6 +807,7 @@ def updateRelativeWidget(self, attr):
if oldUserValue == newUserValue and oldRelativeVal != newRelativeVal:
# Float changed without pixel value changing, which
# means the pixel value needs to be updated
+ # TODO QDoubleSpinBox doesn't work with relativeWidgets because of this
log.debug(
"Updating %s #%s's relative widget: %s",
self.__class__.name,
diff --git a/src/avp/components/color.py b/src/avp/components/color.py
index 1f32c23..cb0960a 100644
--- a/src/avp/components/color.py
+++ b/src/avp/components/color.py
@@ -2,7 +2,7 @@
import logging
from ..component import Component
-from ..toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor
+from ..toolkit.frame import BlankFrame, FloodFrame, FramePainter
log = logging.getLogger("AVP.Components.Color")
@@ -152,13 +152,13 @@ def drawFrame(self, width, height):
elif self.spread == 2:
spread = QtGui.QGradient.Spread.RepeatSpread
brush.setSpread(spread)
- brush.setColorAt(0.0, PaintColor(*self.color1))
+ brush.setColorAt(0.0, QtGui.QColor(*self.color1))
if self.trans:
- brush.setColorAt(1.0, PaintColor(0, 0, 0, 0))
+ brush.setColorAt(1.0, QtGui.QColor(0, 0, 0, 0))
elif self.fillType == 1 and self.stretch:
- brush.setColorAt(0.2, PaintColor(*self.color2))
+ brush.setColorAt(0.2, QtGui.QColor(*self.color2))
else:
- brush.setColorAt(1.0, PaintColor(*self.color2))
+ brush.setColorAt(1.0, QtGui.QColor(*self.color2))
image.setBrush(brush)
image.drawRect(self.x, self.y, self.sizeWidth, self.sizeHeight)
diff --git a/src/avp/components/color.ui b/src/avp/components/color.ui
index c1713fb..c36bdd8 100644
--- a/src/avp/components/color.ui
+++ b/src/avp/components/color.ui
@@ -74,7 +74,7 @@
- 0,0,0
+
12
@@ -84,10 +84,10 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
- QSizePolicy::Fixed
+ QSizePolicy::Policy::Fixed
@@ -176,7 +176,7 @@
Width
- Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter
@@ -246,10 +246,10 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
- QSizePolicy::Fixed
+ QSizePolicy::Policy::Fixed
@@ -370,7 +370,7 @@
-1
- QComboBox::AdjustToContentsOnFirstShow
+ QComboBox::SizeAdjustPolicy::AdjustToContentsOnFirstShow
@@ -422,10 +422,10 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
- QSizePolicy::Minimum
+ QSizePolicy::Policy::Minimum
@@ -461,7 +461,7 @@
-1
0
561
- 31
+ 34
@@ -503,7 +503,7 @@
End
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
@@ -523,7 +523,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -543,7 +543,7 @@
-1
-1
561
- 31
+ 34
@@ -559,7 +559,7 @@
Start
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
@@ -588,7 +588,7 @@
End
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
@@ -617,14 +617,14 @@
Centre
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
-
- QAbstractSpinBox::PlusMinus
+ QAbstractSpinBox::ButtonSymbols::PlusMinus
-10000
@@ -640,7 +640,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
diff --git a/src/avp/components/image.py b/src/avp/components/image.py
index bada15f..e012cec 100644
--- a/src/avp/components/image.py
+++ b/src/avp/components/image.py
@@ -4,13 +4,13 @@
from copy import copy
from ..component import Component
-from ..toolkit.frame import BlankFrame
-from .original import Component as Visualizer
+from ..toolkit.frame import BlankFrame, addShadow
+from ..toolkit.visualizer import createSpectrumArray
class Component(Component):
name = "Image"
- version = "2.0.0"
+ version = "2.1.0"
def widget(self, *args):
super().widget(*args)
@@ -35,6 +35,7 @@ def widget(self, *args):
"mirror": self.page.checkBox_mirror,
"respondToAudio": self.page.checkBox_respondToAudio,
"sensitivity": self.page.spinBox_sensitivity,
+ "shadow": self.page.checkBox_shadow,
},
presetNames={
"imagePath": "image",
@@ -75,31 +76,16 @@ def preFrameRender(self, **kwargs):
# Trigger creation of new base image
self.existingImage = None
- smoothConstantDown = 0.08 + 0
- smoothConstantUp = 0.8 - 0
- self.lastSpectrum = None
- self.spectrumArray = {}
-
- for i in range(0, len(self.completeAudioArray), self.sampleSize):
- if self.canceled:
- break
- self.lastSpectrum = Visualizer.transformData(
- i,
- self.completeAudioArray,
- self.sampleSize,
- smoothConstantDown,
- smoothConstantUp,
- self.lastSpectrum,
- self.sensitivity,
- )
- 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))
+ self.spectrumArray = createSpectrumArray(
+ self,
+ self.completeAudioArray,
+ self.sampleSize,
+ 0.08,
+ 0.8,
+ self.sensitivity,
+ self.progressBarUpdate,
+ self.progressBarSetText,
+ )
def frameRender(self, frameNo):
return self.drawFrame(
@@ -139,9 +125,16 @@ def drawFrame(self, width, height, dynamicScale):
self.existingImage = image
# Respond to audio
+ resolutionFactor = height / 1080
+ shadX = int(resolutionFactor * 1)
+ shadY = int(resolutionFactor * -1)
+ shadBlur = resolutionFactor * 3.50
scale = 0
if dynamicScale is not None:
scale = dynamicScale[36 * 4] / 4
+ shadX += int((scale / 4) * resolutionFactor)
+ shadY += int((scale / 2) * resolutionFactor)
+ shadBlur += (scale / 8) * resolutionFactor
image = ImageOps.contain(
image,
(
@@ -161,6 +154,8 @@ def drawFrame(self, width, height, dynamicScale):
)
if self.rotate != 0:
frame = frame.rotate(self.rotate)
+ if self.shadow:
+ frame = addShadow(frame, shadBlur, shadX, shadY)
return frame
diff --git a/src/avp/components/image.ui b/src/avp/components/image.ui
index 72593a3..b53c4b0 100644
--- a/src/avp/components/image.ui
+++ b/src/avp/components/image.ui
@@ -306,6 +306,16 @@
+ -
+
+
+ Qt::LayoutDirection::RightToLeft
+
+
+ Shadow
+
+
+
-
diff --git a/src/avp/components/life.py b/src/avp/components/life.py
index 9e5e202..a062617 100644
--- a/src/avp/components/life.py
+++ b/src/avp/components/life.py
@@ -8,8 +8,8 @@
from ..component import Component
-from ..toolkit.frame import BlankFrame, scale
-from .original import Component as Visualizer
+from ..toolkit.frame import BlankFrame, scale, addShadow
+from ..toolkit.visualizer import createSpectrumArray
log = logging.getLogger("AVP.Component.Life")
@@ -17,7 +17,7 @@
class Component(Component):
name = "Conway's Game of Life"
- version = "2.0.0"
+ version = "2.0.1"
def widget(self, *args):
super().widget(*args)
@@ -27,26 +27,26 @@ def widget(self, *args):
# 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),
+ (3, 11),
+ (3, 12),
+ (4, 11),
+ (4, 12),
+ (8, 11),
+ (9, 10),
+ (9, 12),
(10, 9),
- (11, 6),
- (11, 7),
- (11, 8),
- (12, 4),
- (12, 5),
+ (10, 13),
+ (11, 10),
+ (11, 11),
+ (11, 12),
+ (12, 8),
(12, 9),
- (12, 10),
- (23, 6),
- (23, 7),
- (24, 6),
- (24, 7),
+ (12, 13),
+ (12, 14),
+ (23, 10),
+ (23, 11),
+ (24, 10),
+ (24, 11),
]
)
@@ -163,41 +163,27 @@ def updateGridSize(self):
def previewRender(self):
image = self.drawGrid(self.startingGrid, self.color)
image = self.addKaleidoscopeEffect(image)
- image = self.addShadow(image)
+ if self.shadow:
+ image = addShadow(image, 5.00, -2, 2)
image = self.addGridLines(image)
return image
def preFrameRender(self, *args, **kwargs):
super().preFrameRender(*args, **kwargs)
self.tickGrids = {0: self.startingGrid}
-
- smoothConstantDown = 0.08 + 0
- smoothConstantUp = 0.8 - 0
- self.lastSpectrum = None
- self.spectrumArray = {}
if self.sensitivity == 0:
return
- for i in range(0, len(self.completeAudioArray), self.sampleSize):
- if self.canceled:
- break
- self.lastSpectrum = Visualizer.transformData(
- i,
- self.completeAudioArray,
- self.sampleSize,
- smoothConstantDown,
- smoothConstantUp,
- self.lastSpectrum,
- self.sensitivity,
- )
- 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))
+ self.spectrumArray = createSpectrumArray(
+ self,
+ self.completeAudioArray,
+ self.sampleSize,
+ 0.08,
+ 0.8,
+ 20,
+ self.progressBarUpdate,
+ self.progressBarSetText,
+ )
def properties(self):
if self.customImg and (not self.image or not os.path.exists(self.image)):
@@ -256,21 +242,11 @@ def frameRender(self, frameNo):
if not self.customImg:
image = Image.alpha_composite(previousGridImage, image)
image = self.addKaleidoscopeEffect(image)
- image = self.addShadow(image)
+ if self.shadow:
+ image = addShadow(image, 5.00, -2, 2)
image = self.addGridLines(image)
return image
- def addShadow(self, frame):
- if not self.shadow:
- return frame
-
- 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
- return frame
-
def addGridLines(self, frame):
if not self.showGrid:
return frame
@@ -299,11 +275,6 @@ def addKaleidoscopeEffect(self, frame):
flippedImage = frame.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
frame.paste(flippedImage, (0, 0), mask=flippedImage)
- flippedImage = frame.transpose(Image.Transpose.ROTATE_90)
- frame.paste(flippedImage, (0, 0), mask=flippedImage)
-
- flippedImage = frame.transpose(Image.Transpose.ROTATE_270)
- frame.paste(flippedImage, (0, 0), mask=flippedImage)
return frame
def drawGrid(self, grid, color, spectrumData=None, didntChange=None):
@@ -506,10 +477,10 @@ def slantLine(difference):
for x, y in grid:
drawPtX = x * self.pxWidth
- if drawPtX > self.width:
+ if drawPtX > self.width or drawPtX + self.pxWidth < 0:
continue
drawPtY = y * self.pxHeight
- if drawPtY > self.height:
+ if drawPtY > self.height or drawPtY + self.pxHeight < 0:
continue
audioMorphWidth = (
diff --git a/src/avp/components/life.ui b/src/avp/components/life.ui
index a0c8999..0070fa4 100644
--- a/src/avp/components/life.ui
+++ b/src/avp/components/life.ui
@@ -68,7 +68,7 @@
- 255,255,255
+
diff --git a/src/avp/components/original.py b/src/avp/components/original.py
index 64eba4d..0da78dc 100644
--- a/src/avp/components/original.py
+++ b/src/avp/components/original.py
@@ -4,11 +4,12 @@
from ..component import Component
from ..toolkit.frame import BlankFrame
+from ..toolkit.visualizer import createSpectrumArray
class Component(Component):
name = "Classic Visualizer"
- version = "1.0.0"
+ version = "1.1.0"
def names(*args):
return ["Original Audio Visualization"]
@@ -18,6 +19,7 @@ def properties(self):
def widget(self, *args):
self.scale = 20
+ self.bars = 63
self.y = 0
super().widget(*args)
@@ -27,15 +29,14 @@ def widget(self, *args):
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,
+ "smooth": self.page.spinBox_sensitivity,
+ "bars": self.page.spinBox_bars,
},
colorWidgets={
"visColor": self.page.pushButton_visColor,
@@ -59,29 +60,16 @@ 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.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,
- smoothConstantDown,
- smoothConstantUp,
- self.lastSpectrum,
- self.scale,
- )
- 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))
+ self.spectrumArray = createSpectrumArray(
+ self,
+ self.completeAudioArray,
+ self.sampleSize,
+ smoothConstantDown,
+ smoothConstantUp,
+ self.scale,
+ self.progressBarUpdate,
+ self.progressBarSetText,
+ )
def frameRender(self, frameNo):
arrayNo = frameNo * self.sampleSize
@@ -93,60 +81,10 @@ def frameRender(self, frameNo):
self.layout,
)
- @staticmethod
- 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
-
def drawBars(self, width, height, spectrum, color, layout):
bigYCoord = height - height / 8
smallYCoord = height / 1200
- bigXCoord = width / 64
+ bigXCoord = width / (self.bars + 1)
middleXCoord = bigXCoord / 2
smallXCoord = bigXCoord / 4
@@ -155,7 +93,7 @@ def drawBars(self, width, height, spectrum, color, layout):
r, g, b = color
color2 = (r, g, b, 125)
- for i in range(0, 63):
+ for i in range(self.bars):
x0 = middleXCoord + i * bigXCoord
y0 = bigYCoord + smallXCoord
y1 = bigYCoord + smallXCoord - spectrum[i * 4] * smallYCoord - middleXCoord
diff --git a/src/avp/components/original.ui b/src/avp/components/original.ui
index c7b7e22..8dbdaa2 100644
--- a/src/avp/components/original.ui
+++ b/src/avp/components/original.ui
@@ -44,10 +44,10 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
- QSizePolicy::Fixed
+ QSizePolicy::Policy::Fixed
@@ -84,15 +84,19 @@
-
-
+
+
+
+
+
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
- QSizePolicy::Fixed
+ QSizePolicy::Policy::Fixed
@@ -112,7 +116,7 @@
-
- QAbstractSpinBox::UpDownArrows
+ QAbstractSpinBox::ButtonSymbols::UpDownArrows
-5000
@@ -131,7 +135,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -158,7 +162,7 @@
-
- QAbstractSpinBox::PlusMinus
+ QAbstractSpinBox::ButtonSymbols::PlusMinus
1
@@ -168,13 +172,27 @@
+ -
+
+
+ Sensitivity
+
+
+
+ -
+
+
+ 5
+
+
+
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
- QSizePolicy::Expanding
+ QSizePolicy::Policy::Expanding
@@ -189,29 +207,35 @@
-
- QLayout::SetDefaultConstraint
+ QLayout::SizeConstraint::SetDefaultConstraint
4
-
-
+
- Sensitivity
+ Bars
-
-
+
+
+ 63
+
- 5
+ 64
+
+
+ 63
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -226,7 +250,7 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
diff --git a/src/avp/components/text.py b/src/avp/components/text.py
index 40c981a..bee117e 100644
--- a/src/avp/components/text.py
+++ b/src/avp/components/text.py
@@ -5,7 +5,7 @@
import logging
from ..component import Component
-from ..toolkit.frame import FramePainter, PaintColor
+from ..toolkit.frame import FramePainter, addShadow
log = logging.getLogger("AVP.Components.Text")
@@ -26,7 +26,6 @@ def widget(self, *args):
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(
@@ -35,7 +34,7 @@ def widget(self, *args):
# 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
+ # Fix requires updating ComponentAction to handle fonts
self.trackWidgets(
{
@@ -173,7 +172,7 @@ def addText(self, width, height):
path.addText(x, y, font, self.title)
path = outliner.createStroke(path)
image.setPen(QtCore.Qt.PenStyle.NoPen)
- image.setBrush(PaintColor(*self.strokeColor))
+ image.setBrush(QtGui.QColor(*self.strokeColor))
image.drawPath(path)
image.setFont(font)
@@ -183,11 +182,7 @@ def addText(self, width, height):
# 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
+ frame = addShadow(frame, self.shadBlur / 10, self.shadX, self.shadY)
return frame
diff --git a/src/avp/components/text.ui b/src/avp/components/text.ui
index b62e0ed..ce961fe 100644
--- a/src/avp/components/text.ui
+++ b/src/avp/components/text.ui
@@ -15,646 +15,645 @@
-
-
-
- 6
-
-
- QLayout::SetDefaultConstraint
-
+
+
-
+
+
+ Title
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ Text
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Font
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ -
+
- 4
+ 0
-
-
-
-
-
-
- Title
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 0
- 0
-
-
-
- Testing New GUI
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Font
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 0
- 0
-
-
-
-
-
+
+
+
+ 0
+ 0
+
+
+
+ Text Layout
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+ QSizePolicy::Policy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Center Text
+
+
-
-
-
+
+
+ Qt::Orientation::Horizontal
+
+
+ QSizePolicy::Policy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ X
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 50
+ 16777215
+
+
+
+
+ 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
-
-
-
-
+
+ 999999999
+
+
+ 0
+
+
-
-
-
-
-
-
-
- 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
+
+
+
+ Y
+
+
-
-
-
-
-
-
-
- 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
+
+
+
+
+ 50
+ 16777215
+
+
+
+ 999999999
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ Text Color
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 32
+ 32
+
+
+
+
+
+
+
+ 32
+ 32
+
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+ QSizePolicy::Policy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Font Size
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+
+ 1
+
+
+ 500
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+ QSizePolicy::Policy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Font Style
+
+
-
-
+
-
-
-
-
- 0
- 0
-
-
-
- Shadow
-
-
+
+ Normal
+
-
-
-
-
- 0
- 0
-
-
-
- Shadow Offset
-
-
+
+ Semi-Bold
+
-
-
-
-
- 0
- 0
-
-
-
- -1000
-
-
- 1000
-
-
- -4
-
-
+
+ Bold
+
-
-
-
-
- 0
- 0
-
-
-
- -1000
-
-
- 1000
-
-
- 8
-
-
+
+ Italic
+
-
-
-
-
- 0
- 0
-
-
-
- Shadow Blur
-
-
+
+ Bold Italic
+
-
-
-
-
- 0
- 0
-
-
-
- 99.000000000000000
-
-
- 0.100000000000000
-
-
- 5.000000000000000
-
-
+
+ Faux Italic
+
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Minimum
-
-
-
- 40
- 20
-
-
-
+
+ Small Caps
+
-
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 16777215
+
+
+
+ Qt::FocusPolicy::NoFocus
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Stroke
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ px
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Stroke Color
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 16777215
+
+
+
+ Qt::FocusPolicy::NoFocus
+
+
+ 0,0,0
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 32
+ 32
+
+
+
+
+
+
+
+ 32
+ 32
+
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+ QSizePolicy::Policy::MinimumExpanding
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Shadow
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+ QSizePolicy::Policy::Preferred
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Shadow Offset
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ -1000
+
+
+ 1000
+
+
+ 2
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ -1000
+
+
+ 1000
+
+
+ -2
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Shadow Blur
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ QAbstractSpinBox::ButtonSymbols::PlusMinus
+
+
+ QAbstractSpinBox::CorrectionMode::CorrectToPreviousValue
+
+
+ 1000
+
+
+ QAbstractSpinBox::StepType::DefaultStepType
+
+
+ 35
+
+
+ 10
+
+
-
- Qt::Vertical
+ Qt::Orientation::Vertical
diff --git a/src/avp/components/video.ui b/src/avp/components/video.ui
index 08d15d3..72ecb0c 100644
--- a/src/avp/components/video.ui
+++ b/src/avp/components/video.ui
@@ -27,168 +27,161 @@
-
-
-
- 4
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 31
+ 0
+
+
+
+ Video
+
+
+
+ -
+
+
+
+ 1
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 1
+ 0
+
+
+
+
+ 32
+ 32
+
+
+
+ ...
+
+
+
+ 32
+ 32
+
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+ QSizePolicy::Policy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ X
+
+
+
-
-
-
-
-
-
-
- 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
-
-
-
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+ -10000
+
+
+ 10000
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Y
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+
+ 0
+ 0
+
+
+
+ -10000
+
+
+ 10000
+
+
+ 0
+
+
@@ -204,7 +197,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -227,14 +220,14 @@
Scale
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
-
- QAbstractSpinBox::UpDownArrows
+ QAbstractSpinBox::ButtonSymbols::UpDownArrows
%
@@ -296,7 +289,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -311,7 +304,7 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
diff --git a/src/avp/components/waveform.py b/src/avp/components/waveform.py
index 7dc0b99..e10dec2 100644
--- a/src/avp/components/waveform.py
+++ b/src/avp/components/waveform.py
@@ -1,12 +1,12 @@
-from PIL import Image
-from PyQt6 import QtGui, QtCore, QtWidgets
+from PIL import Image, ImageChops
from PyQt6.QtGui import QColor
import os
-import math
import subprocess
import logging
+from copy import copy
from ..component import Component
+from ..toolkit.visualizer import transformData, createSpectrumArray
from ..toolkit.frame import BlankFrame, scale
from ..toolkit import checkOutput
from ..toolkit.ffmpeg import (
@@ -23,14 +23,20 @@
class Component(Component):
name = "Waveform"
- version = "1.0.0"
+ version = "2.0.0"
+
+ @property
+ def updateInterval(self):
+ """How many frames from FFmpeg are ignored between each final frame"""
+ return 100 - self.speed
+
+ def properties(self):
+ return [] if self.speed == 100 else ["pcm"]
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)
@@ -46,6 +52,7 @@ def widget(self, *args):
"opacity": self.page.spinBox_opacity,
"compress": self.page.checkBox_compress,
"mono": self.page.checkBox_mono,
+ "speed": self.page.spinBox_speed,
},
colorWidgets={
"color": self.page.pushButton_color,
@@ -65,6 +72,10 @@ def previewRender(self):
return frame
def preFrameRender(self, **kwargs):
+ self._fadingImage = None
+ self._prevImage = None
+ self._currImage = None
+ self._lastUpdatedFrame = 0
super().preFrameRender(**kwargs)
self.updateChunksize()
w, h = scale(self.scale, self.width, self.height, str)
@@ -79,11 +90,64 @@ def preFrameRender(self, **kwargs):
component=self,
debug=True,
)
+ if self.speed == 100:
+ return
+ self.spectrumArray = createSpectrumArray(
+ self,
+ self.completeAudioArray,
+ self.sampleSize,
+ 0.08,
+ 0.8,
+ 20,
+ self.progressBarUpdate,
+ self.progressBarSetText,
+ )
def frameRender(self, frameNo):
if FfmpegVideo.threadError is not None:
raise FfmpegVideo.threadError
- return self.finalizeFrame(self.video.frame(frameNo))
+ newFrame = self.finalizeFrame(self.video.frame(frameNo))
+ if self.speed == 100:
+ return newFrame
+ frameDiff = 0 if frameNo == 0 else frameNo % self.updateInterval
+ peaks = [
+ self.spectrumArray[frameNo * self.sampleSize][i * 4] for i in range(64)
+ ]
+ peakValue = 70 - (max(*peaks) - min(*peaks))
+ isValidPeak = (
+ peakValue > 27
+ and frameNo - self._lastUpdatedFrame > self.updateInterval / 2
+ )
+ if frameDiff == 0 or isValidPeak:
+ self._lastUpdatedFrame = frameNo
+ self._fadingImage = self._prevImage
+ self._prevImage = self._image
+ self._currImage = newFrame
+ usualAlpha = 0.0 + (1 / self.updateInterval) * frameDiff
+ alpha = max(
+ 0.1
+ + (
+ 1
+ / max(
+ 10,
+ peakValue,
+ )
+ ),
+ usualAlpha,
+ )
+ baseImage = self._prevImage
+ if self._fadingImage is not None:
+ # fade away the old previous frame from ages ago
+ baseImage = ImageChops.blend(
+ self._prevImage, self._fadingImage, max(0.0, 0.9 - usualAlpha)
+ )
+ blendedImage = ImageChops.blend(
+ baseImage,
+ ImageChops.lighter(self._prevImage, newFrame),
+ alpha,
+ )
+ baseImage.paste(blendedImage, (0, 0), mask=blendedImage)
+ return Image.alpha_composite(self._currImage, baseImage)
def postFrameRender(self):
closePipe(self.video.pipe)
@@ -162,17 +226,17 @@ def getPreviewFrame(self, width, height):
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:
+ elif self.amplitude == 1:
amplitude = "sqrt"
- elif self.amplitude == 3:
+ elif self.amplitude == 2:
amplitude = "cbrt"
+ elif self.amplitude == 3:
+ amplitude = "lin"
hexcolor = QColor(*self.color).name()
opacity = "{0:.1f}".format(self.opacity / 100)
genericPreview = self.settings.value("pref_genericPreview")
- if self.mode < 3:
+ if self.mode > 1:
filter_ = (
"showwaves="
f'r={str(self.settings.value("outputFrameRate"))}:'
@@ -180,10 +244,10 @@ def makeFfmpegFilter(self, preview=False, startPt=0):
f'mode={self.page.comboBox_mode.currentText().lower() if self.mode != 3 else "p2p"}:'
f"colors={hexcolor}@{opacity}:scale={amplitude}"
)
- elif self.mode > 2:
+ 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'mode={"line" if self.mode == 0 else "bar"}:'
f"colors={hexcolor}@{opacity}"
f":ascale={amplitude}:fscale={'log' if self.mono else 'lin'}"
)
diff --git a/src/avp/components/waveform.ui b/src/avp/components/waveform.ui
index 5473f33..434ba62 100644
--- a/src/avp/components/waveform.ui
+++ b/src/avp/components/waveform.ui
@@ -27,156 +27,144 @@
-
-
-
- 4
-
+
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 31
- 0
-
-
-
- Mode
-
-
-
- -
-
-
-
-
- Cline
-
-
- -
-
- Line
-
-
- -
-
- Point
-
-
- -
-
- Frequency Bar
-
-
- -
-
- Frequency Line
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 5
- 20
-
-
-
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 31
+ 0
+
+
+
+ Mode
+
+
+
+ -
+
-
-
-
-
- 0
- 0
-
-
-
- X
-
-
+
+ Frequency Line
+
-
-
-
-
- 0
- 0
-
-
-
-
- 80
- 16777215
-
-
-
- -10000
-
-
- 10000
-
-
+
+ Frequency Bar
+
-
-
-
-
- 0
- 0
-
-
-
- Y
-
-
+
+ Cline
+
-
-
-
-
- 0
- 0
-
-
-
-
- 80
- 16777215
-
-
-
-
- 0
- 0
-
-
-
- -10000
-
-
- 10000
-
-
- 0
-
-
+
+ Line
+
-
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+ QSizePolicy::Policy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ X
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+ -10000
+
+
+ 10000
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Y
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+
+ 0
+ 0
+
+
+
+ -10000
+
+
+ 10000
+
+
+ 0
+
+
@@ -192,7 +180,7 @@
-
- Qt::ImhNone
+ Qt::InputMethodHint::ImhNone
@@ -224,7 +212,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -240,14 +228,14 @@
Opacity
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
-
- QAbstractSpinBox::UpDownArrows
+ QAbstractSpinBox::ButtonSymbols::UpDownArrows
%
@@ -269,14 +257,14 @@
Scale
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
-
- QAbstractSpinBox::UpDownArrows
+ QAbstractSpinBox::ButtonSymbols::UpDownArrows
%
@@ -301,6 +289,9 @@
Compress
+
+ true
+
-
@@ -320,7 +311,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -341,32 +332,75 @@
-
- Linear
+ Logarithmic
-
- Logarithmic
+ Square root
-
- Square root
+ Cubic root
-
- Cubic root
+ Linear
+ -
+
+
-
+
+
+ Animation Speed
+
+
+
+ -
+
+
+ %
+
+
+ 10
+
+
+ 100
+
+
+ 10
+
+
+ 50
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
-
- Qt::Vertical
+ Qt::Orientation::Vertical
diff --git a/src/avp/core.py b/src/avp/core.py
index 196cd7d..099b0b4 100644
--- a/src/avp/core.py
+++ b/src/avp/core.py
@@ -12,7 +12,7 @@
from . import toolkit
-
+appName = "Audio Visualizer Python"
log = logging.getLogger("AVP.Core")
STDOUT_LOGLVL = logging.WARNING
diff --git a/src/avp/gui/mainwindow.py b/src/avp/gui/mainwindow.py
index e7a5fe3..5a051fd 100644
--- a/src/avp/gui/mainwindow.py
+++ b/src/avp/gui/mainwindow.py
@@ -7,7 +7,7 @@
from PyQt6 import QtCore, QtWidgets, uic
import PyQt6.QtWidgets as QtWidgets
-from PyQt6.QtGui import QUndoStack, QShortcut
+from PyQt6.QtGui import QShortcut
from PIL import Image
from queue import Queue
import sys
@@ -16,12 +16,16 @@
import filecmp
import time
import logging
+from textwrap import wrap
-from ..core import Core
+from ..__init__ import __version__
+from ..core import Core, appName
+from .undostack import UndoStack
from . import preview_thread
from .preview_win import PreviewWindow
from .presetmanager import PresetManager
from .actions import *
+from ..toolkit.ffmpeg import createFfmpegCommand
from ..toolkit import (
disableWhenEncoding,
disableWhenOpeningProject,
@@ -30,25 +34,9 @@
)
-appName = "Audio Visualizer"
log = logging.getLogger("AVP.Gui.MainWindow")
-class MyQUndoStack(QUndoStack):
- # FIXME move this class
- @property
- def encoding(self):
- return self.parent().encoding
-
- @disableWhenEncoding
- def undo(self, *args, **kwargs):
- super().undo(*args, **kwargs)
-
- @disableWhenEncoding
- def redo(self, *args, **kwargs):
- super().redo(*args, **kwargs)
-
-
class MainWindow(QtWidgets.QMainWindow):
"""
The MainWindow wraps many Core methods in order to update the GUI
@@ -91,7 +79,7 @@ def __init__(self, project, dpi):
self.settings = Core.settings
# Create stack of undoable user actions
- self.undoStack = MyQUndoStack(self)
+ self.undoStack = UndoStack(self)
undoLimit = self.settings.value("pref_undoLimit")
self.undoStack.setUndoLimit(undoLimit)
@@ -102,6 +90,7 @@ def __init__(self, project, dpi):
layout = QtWidgets.QVBoxLayout()
layout.addWidget(undoView)
self.undoDialog.setLayout(layout)
+ self.undoDialog.setMinimumWidth(int(self.width() / 2))
# Create Preset Manager
self.presetManager = PresetManager(self)
@@ -325,7 +314,11 @@ def changedField():
self.drawPreview(True)
log.info("Pillow version %s", Image.__version__)
- log.info("PyQt version %s (Qt version %s)", QtCore.PYQT_VERSION_STR, QtCore.QT_VERSION_STR)
+ log.info(
+ "PyQt version %s (Qt version %s)",
+ QtCore.PYQT_VERSION_STR,
+ QtCore.QT_VERSION_STR,
+ )
# verify Ffmpeg version
if not self.core.FFMPEG_BIN:
@@ -408,6 +401,7 @@ def changedField():
activated=lambda: self.moveComponent("bottom"),
)
+ QShortcut("F1", self, self.showHelpWindow)
QShortcut("Ctrl+Shift+F", self, self.showFfmpegCommand)
QShortcut("Ctrl+Shift+U", self, self.showUndoStack)
@@ -422,6 +416,12 @@ def changedField():
if not self.core.selectedComponents:
self.core.insertComponent(0, 0, self)
self.core.insertComponent(1, 1, self)
+ # set colors to white and black to match classic appearance of program
+ self.core.selectedComponents[0].page.lineEdit_visColor.setText(
+ "255,255,255"
+ )
+ self.core.selectedComponents[1].page.lineEdit_color1.setText("0,0,0")
+ self.undoStack.clear()
def __repr__(self):
return (
@@ -762,10 +762,10 @@ def showPreviewImage(self, image):
def showUndoStack(self):
self.undoDialog.show()
- def showFfmpegCommand(self):
- from textwrap import wrap
- from ..toolkit.ffmpeg import createFfmpegCommand
+ def showHelpWindow(self):
+ self.showMessage(msg=f"{appName} v{__version__}")
+ def showFfmpegCommand(self):
command = createFfmpegCommand(
self.lineEdit_audioFile.text(),
self.lineEdit_outputFile.text(),
@@ -899,7 +899,9 @@ def clear(self):
@disableWhenEncoding
def createNewProject(self, prompt=True):
if prompt:
- self.openSaveChangesDialog("starting a new project")
+ ch = self.openSaveChangesDialog("starting a new project")
+ if ch is None:
+ return
self.clear()
self.currentProject = None
@@ -919,18 +921,19 @@ def saveCurrentProject(self):
def openSaveChangesDialog(self, phrase):
success = True
+ ch = True
if self.autosaveExists(identical=False):
ch = self.showMessage(
msg="You have unsaved changes in project '%s'. "
"Save before %s?"
% (os.path.basename(self.currentProject)[:-4], phrase),
- showCancel=True,
+ showDiscard=True,
)
if ch:
success = self.saveProjectChanges()
-
- if success and os.path.exists(self.autosavePath):
+ if ch is not None and success and os.path.exists(self.autosavePath):
os.remove(self.autosavePath)
+ return success and ch
def openSaveProjectDialog(self):
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
@@ -967,10 +970,12 @@ def openProject(self, filepath, prompt=True):
):
return
- self.clear()
# ask to save any changes that are about to get deleted
if prompt:
- self.openSaveChangesDialog("opening another project")
+ ch = self.openSaveChangesDialog("opening another project")
+ if ch is None:
+ return
+ self.clear()
self.currentProject = filepath
self.settings.setValue("currentProject", filepath)
@@ -992,7 +997,13 @@ def showMessage(self, **kwargs):
else QtWidgets.QMessageBox.Icon.Information
)
msg.setDetailedText(kwargs["detail"] if "detail" in kwargs else None)
- if "showCancel" in kwargs and kwargs["showCancel"]:
+ if "showDiscard" in kwargs and kwargs["showDiscard"]:
+ msg.setStandardButtons(
+ QtWidgets.QMessageBox.StandardButton.Save
+ | QtWidgets.QMessageBox.StandardButton.Discard
+ | QtWidgets.QMessageBox.StandardButton.Cancel
+ )
+ elif "showCancel" in kwargs and kwargs["showCancel"]:
msg.setStandardButtons(
QtWidgets.QMessageBox.StandardButton.Ok
| QtWidgets.QMessageBox.StandardButton.Cancel
@@ -1000,9 +1011,14 @@ def showMessage(self, **kwargs):
else:
msg.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok)
ch = msg.exec()
- if ch == 1024:
+ if ch == 1024 or ch == 2048:
+ # OK or Save
return True
- return False
+ elif ch > 8000000:
+ # Discard
+ return False
+ # Cancel
+ return None
@disableWhenEncoding
def componentContextMenu(self, QPos):
diff --git a/src/avp/gui/presetmanager.py b/src/avp/gui/presetmanager.py
index 980a969..ca0029d 100644
--- a/src/avp/gui/presetmanager.py
+++ b/src/avp/gui/presetmanager.py
@@ -9,7 +9,7 @@
import logging
from ..toolkit import badName
-from ..core import Core
+from ..core import Core, appName
from .actions import *
@@ -137,7 +137,7 @@ def openSavePresetDialog(self):
currentPreset = selectedComponents[index].currentPreset
newName, OK = QtWidgets.QInputDialog.getText(
self.parent,
- "Audio Visualizer",
+ appName,
"New Preset Name:",
QtWidgets.QLineEdit.EchoMode.Normal,
currentPreset,
diff --git a/src/avp/gui/preview_thread.py b/src/avp/gui/preview_thread.py
index 1d78516..a59652a 100644
--- a/src/avp/gui/preview_thread.py
+++ b/src/avp/gui/preview_thread.py
@@ -65,17 +65,18 @@ def process(self):
component.unlockSize()
frame = Image.alpha_composite(frame, newFrame)
- except ValueError as e:
+ except (AttributeError, ValueError) as e:
errMsg = (
"Bad frame returned by %s's preview renderer. "
- "%s. New frame size was %s*%s; should be %s*%s."
+ "%s. New frame %s."
% (
str(component),
str(e).capitalize(),
- newFrame.width,
- newFrame.height,
- width,
- height,
+ "is None" if newFrame is None else "size was %s*%s; should be %s*%s" % (
+ newFrame.width,
+ newFrame.height,
+ width,
+ height),
)
)
log.critical(errMsg)
diff --git a/src/avp/gui/undostack.py b/src/avp/gui/undostack.py
new file mode 100644
index 0000000..fd1a3e9
--- /dev/null
+++ b/src/avp/gui/undostack.py
@@ -0,0 +1,16 @@
+from PyQt6.QtGui import QUndoStack
+from ..toolkit.common import disableWhenEncoding
+
+
+class UndoStack(QUndoStack):
+ @property
+ def encoding(self):
+ return self.parent().encoding
+
+ @disableWhenEncoding
+ def undo(self, *args, **kwargs):
+ super().undo(*args, **kwargs)
+
+ @disableWhenEncoding
+ def redo(self, *args, **kwargs):
+ super().redo(*args, **kwargs)
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 @@
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 @@
"""
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 @@ def __init__(self, width, height):
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 @@ def finalize(self):
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
diff --git a/tests/test_commandline_parser.py b/tests/test_commandline_parser.py
index d092072..77836ce 100644
--- a/tests/test_commandline_parser.py
+++ b/tests/test_commandline_parser.py
@@ -51,6 +51,11 @@ def test_commandline_parses_classic_by_alias(qtbot):
assert command.parseCompName("original") == "Classic Visualizer"
-def test_commandline_parses_conway_by_name(qtbot):
+def test_commandline_parses_conway_by_short_name(qtbot):
command = Command()
assert command.parseCompName("conway") == "Conway's Game of Life"
+
+
+def test_commandline_parses_image_by_name(qtbot):
+ command = Command()
+ assert command.parseCompName("image") == "Image"
diff --git a/tests/test_comp_color.py b/tests/test_comp_color.py
new file mode 100644
index 0000000..6b82e4c
--- /dev/null
+++ b/tests/test_comp_color.py
@@ -0,0 +1,22 @@
+from avp.command import Command
+from pytestqt import qtbot
+from pytest import fixture
+from . import imageDataSum
+
+
+@fixture
+def coreWithColorComp(qtbot):
+ """Fixture providing a Command object with Color component added"""
+ command = Command()
+ command.settings.setValue("outputHeight", 1080)
+ command.settings.setValue("outputWidth", 1920)
+ command.core.insertComponent(0, command.core.moduleIndexFor("Color"), command)
+ yield command.core
+
+
+def test_comp_color_set_color(coreWithColorComp):
+ "Set imagePath of Image component"
+ comp = coreWithColorComp.selectedComponents[0]
+ comp.page.lineEdit_color1.setText("111,111,111")
+ image = comp.previewRender()
+ assert imageDataSum(image) == 1219276800
diff --git a/tests/test_image_comp.py b/tests/test_comp_image.py
similarity index 100%
rename from tests/test_image_comp.py
rename to tests/test_comp_image.py
diff --git a/tests/test_comp_life.py b/tests/test_comp_life.py
new file mode 100644
index 0000000..ad78e52
--- /dev/null
+++ b/tests/test_comp_life.py
@@ -0,0 +1,23 @@
+from avp.command import Command
+from pytestqt import qtbot
+from pytest import fixture
+from . import imageDataSum
+
+
+@fixture
+def coreWithLifeComp(qtbot):
+ """Fixture providing a Command object with Waveform component added"""
+ command = Command()
+ command.settings.setValue("outputHeight", 1080)
+ command.settings.setValue("outputWidth", 1920)
+ command.core.insertComponent(
+ 0, command.core.moduleIndexFor("Conway's Game of Life"), command
+ )
+ yield command.core
+
+
+def test_comp_life_previewRender(coreWithLifeComp):
+ comp = coreWithLifeComp.selectedComponents[0]
+ comp.page.lineEdit_color.setText("111,111,111")
+ image = comp.previewRender()
+ assert imageDataSum(image) == 339785512
diff --git a/tests/test_classic_visualizer.py b/tests/test_comp_original.py
similarity index 88%
rename from tests/test_classic_visualizer.py
rename to tests/test_comp_original.py
index e301263..6264644 100644
--- a/tests/test_classic_visualizer.py
+++ b/tests/test_comp_original.py
@@ -1,4 +1,5 @@
from avp.command import Command
+from avp.toolkit.visualizer import transformData
from pytestqt import qtbot
from pytest import fixture
from . import audioData, MockSignal, imageDataSum
@@ -31,13 +32,9 @@ def test_comp_classic_removed(coreWithClassicComp):
def test_comp_classic_drawBars(coreWithClassicComp, audioData):
"""Call drawBars after creating audio spectrum data manually."""
- spectrumArray = {
- 0: coreWithClassicComp.selectedComponents[0].transformData(
- 0, audioData[0], sampleSize, 0.08, 0.8, None, 20
- )
- }
+ spectrumArray = {0: transformData(0, audioData[0], sampleSize, 0.08, 0.8, None, 20)}
for i in range(sampleSize, len(audioData[0]), sampleSize):
- spectrumArray[i] = coreWithClassicComp.selectedComponents[0].transformData(
+ spectrumArray[i] = transformData(
i,
audioData[0],
sampleSize,
diff --git a/tests/test_comp_spectrum.py b/tests/test_comp_spectrum.py
new file mode 100644
index 0000000..44fb257
--- /dev/null
+++ b/tests/test_comp_spectrum.py
@@ -0,0 +1,20 @@
+from avp.command import Command
+from pytestqt import qtbot
+from pytest import fixture
+from . import imageDataSum
+
+
+@fixture
+def coreWithSpectrumComp(qtbot):
+ """Fixture providing a Command object with Spectrum component added"""
+ command = Command()
+ command.settings.setValue("outputHeight", 1080)
+ command.settings.setValue("outputWidth", 1920)
+ command.core.insertComponent(0, command.core.moduleIndexFor("Spectrum"), command)
+ yield command.core
+
+
+def test_comp_waveform_previewRender(coreWithSpectrumComp):
+ comp = coreWithSpectrumComp.selectedComponents[0]
+ image = comp.previewRender()
+ assert imageDataSum(image) == 71992628
diff --git a/tests/test_comp_text.py b/tests/test_comp_text.py
new file mode 100644
index 0000000..e389ff9
--- /dev/null
+++ b/tests/test_comp_text.py
@@ -0,0 +1,45 @@
+from avp.command import Command
+from PyQt6.QtGui import QFont
+from pytestqt import qtbot
+from pytest import fixture, mark
+from . import audioData, MockSignal, imageDataSum
+
+
+@fixture
+def coreWithTextComp(qtbot):
+ """Fixture providing a Command object with Title Text component added"""
+ command = Command()
+ command.core.insertComponent(0, command.core.moduleIndexFor("Title Text"), command)
+ yield command.core
+
+
+def setTextSettings(comp):
+ comp.page.spinBox_fontSize.setValue(40)
+ comp.page.checkBox_shadow.setChecked(True)
+ comp.page.spinBox_shadBlur.setValue(0)
+ comp.page.spinBox_shadX.setValue(2)
+ comp.page.spinBox_shadY.setValue(-2)
+ comp.page.fontComboBox_titleFont.setCurrentFont(QFont("Noto Sans"))
+ comp.page.lineEdit_textColor.setText("255,255,255")
+
+
+@mark.parametrize(
+ "width, height",
+ ((1920, 1080), (1280, 720)),
+)
+def test_comp_text_renderFrame(coreWithTextComp, width, height):
+ """Call renderFrame of Title Text component added to Command object."""
+ comp = coreWithTextComp.selectedComponents[0]
+ comp.parent.settings.setValue("outputWidth", width)
+ comp.parent.settings.setValue("outputHeight", height)
+ setTextSettings(comp)
+ comp.centerXY()
+ image = comp.frameRender(0)
+ assert comp.titleFont.family() == "Noto Sans"
+ assert comp.xPosition == width / 2
+ assert image.width == width
+ assert comp.fontSize == 40
+ assert comp.shadX == 2
+ assert comp.shadY == -2
+ assert comp.shadBlur == 0
+ assert imageDataSum(image) == 727403 or 738586
diff --git a/tests/test_comp_waveform.py b/tests/test_comp_waveform.py
new file mode 100644
index 0000000..a71040b
--- /dev/null
+++ b/tests/test_comp_waveform.py
@@ -0,0 +1,17 @@
+from avp.command import Command
+from pytestqt import qtbot
+from pytest import fixture
+
+
+@fixture
+def coreWithWaveformComp(qtbot):
+ """Fixture providing a Command object with Waveform component added"""
+ command = Command()
+ command.core.insertComponent(0, command.core.moduleIndexFor("Waveform"), command)
+ yield command.core
+
+
+def test_comp_waveform_setColor(coreWithWaveformComp):
+ comp = coreWithWaveformComp.selectedComponents[0]
+ comp.page.lineEdit_color.setText("255,255,255")
+ assert comp.color == (255, 255, 255)
diff --git a/tests/test_text_comp.py b/tests/test_text_comp.py
deleted file mode 100644
index 23dd1fd..0000000
--- a/tests/test_text_comp.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from avp.command import Command
-from PyQt6.QtGui import QFont
-from pytestqt import qtbot
-from pytest import fixture
-from . import audioData, MockSignal, imageDataSum
-
-
-@fixture
-def coreWithTextComp(qtbot):
- """Fixture providing a Command object with Title Text component added"""
- command = Command()
- command.core.insertComponent(0, command.core.moduleIndexFor("Title Text"), command)
- yield command.core
-
-
-def test_comp_text_renderFrame_resize(coreWithTextComp):
- """Call renderFrame of Title Text component added to Command object."""
- comp = coreWithTextComp.selectedComponents[0]
- comp.parent.settings.setValue("outputWidth", 1920)
- comp.parent.settings.setValue("outputHeight", 1080)
- comp.parent.core.updateComponent(0)
- comp.titleFont = QFont("Noto Sans")
- image = comp.frameRender(0)
- assert imageDataSum(image) == 2957069
-
-
-def test_comp_text_renderFrame(coreWithTextComp):
- """Call renderFrame of Title Text component added to Command object."""
- comp = coreWithTextComp.selectedComponents[0]
- comp.parent.settings.setValue("outputWidth", 1280)
- comp.parent.settings.setValue("outputHeight", 720)
- comp.parent.core.updateComponent(0)
- image = comp.frameRender(0)
- assert imageDataSum(image) == 1412293 or 1379298
diff --git a/tests/test_toolkit_common.py b/tests/test_toolkit_common.py
index d903842..8e9dca2 100644
--- a/tests/test_toolkit_common.py
+++ b/tests/test_toolkit_common.py
@@ -1,6 +1,27 @@
+from pytest import fixture
from pytestqt import qtbot
from avp.command import Command
-from avp.toolkit import blockSignals
+from avp.toolkit import blockSignals, rgbFromString
+
+
+@fixture
+def gotWarning():
+ """Check if a function called log.warning"""
+ import avp.toolkit.common as tk
+ warning = False
+ def gotWarning():
+ nonlocal warning
+ return warning
+ class log:
+ def warning(self, *args):
+ nonlocal warning
+ warning = True
+ oldLog = tk.log
+ tk.log = log()
+ try:
+ yield gotWarning
+ finally:
+ tk.log = oldLog
def test_blockSignals(qtbot):
@@ -11,3 +32,13 @@ def test_blockSignals(qtbot):
with blockSignals(comp.page.spinBox_scale):
assert comp.page.spinBox_scale.signalsBlocked() == True
assert comp.page.spinBox_scale.signalsBlocked() == False
+
+
+def test_rgbFromString(gotWarning):
+ assert rgbFromString("255,255,255") == (255, 255, 255)
+ assert not gotWarning()
+
+
+def test_rgbFromString_error(gotWarning):
+ assert rgbFromString("255,255,256") == (255, 255, 255)
+ assert gotWarning()
\ No newline at end of file
diff --git a/uv.lock b/uv.lock
index a3d6bfb..edc9406 100644
--- a/uv.lock
+++ b/uv.lock
@@ -4,7 +4,7 @@ requires-python = ">=3.12"
[[package]]
name = "audio-visualizer-python"
-version = "2.2.0"
+version = "2.2.1"
source = { editable = "." }
dependencies = [
{ name = "numpy" },