Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
219bf25
more tests of actions, waveform, spectrum, projects
tassaron Feb 3, 2026
a4b3142
test classic comp commandline, presets
tassaron Feb 4, 2026
7a6f605
test comp gradient
tassaron Feb 4, 2026
55587cf
Color component: add tooltip to color2 picker
tassaron Feb 4, 2026
afd6c6e
ignore test presets
tassaron Feb 4, 2026
eeacce8
do not change presetDir to "projects"
tassaron Feb 4, 2026
9a509b7
change noisiness of terminal output
tassaron Feb 4, 2026
3b5309b
fix ascii art in comments
tassaron Feb 4, 2026
2385439
change tests to work with pytest-xdist
tassaron Feb 4, 2026
c36932a
style change: rename core.Core to Core
tassaron Feb 4, 2026
fa517bc
check alt comp names when parsing cmdline
tassaron Feb 4, 2026
e1e5ce3
rename `original.py` to `classic.py`
tassaron Feb 4, 2026
687d188
move `component.py` into subpackage
tassaron Feb 4, 2026
cd20149
rename comp_original to comp_classic
tassaron Feb 4, 2026
dc879d8
clean-up unused imports
tassaron Feb 4, 2026
fe0775a
traceback if renderFrame() raises exception
tassaron Feb 4, 2026
3ad9909
do not try to insert non-existent components
tassaron Feb 4, 2026
795fa3e
add "composite" property for components
tassaron Feb 5, 2026
bd0babd
Classic Visualizer: add invert option
tassaron Feb 5, 2026
0c01325
Image component: fix path commandline option
tassaron Feb 7, 2026
e595647
document color difference on X11 vs Wayland
tassaron Feb 10, 2026
5b0e123
Image component: restrict file formats in CLI to match GUI
tassaron Feb 11, 2026
044b1f3
move ffmpeg version check into function
tassaron Feb 12, 2026
86f7a75
move log messages to after CLI parsing
tassaron Feb 12, 2026
4cf229e
Waveform test: replace e595647 with actual fix
tassaron Feb 12, 2026
dbe892b
test image component path argument
tassaron Feb 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ prof/
.env/
.vscode/
tests/data/config/log/
tests/data/config/presets/
tests/data/config/settings.ini
tests/data/config/autosave.avp
*.mkv
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.3"
version = "2.2.4"
requires-python = ">= 3.12"
license = "MIT"
classifiers=[
Expand Down
2 changes: 1 addition & 1 deletion src/avp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging


__version__ = "2.2.3"
__version__ = "2.2.4"


class Logger(logging.getLoggerClass()):
Expand Down
4 changes: 2 additions & 2 deletions src/avp/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,8 @@ def main() -> int:
screen = app.primaryScreen()
if screen is None:
dpi = None
log.error("Could not detect DPI")
else:
dpi = screen.physicalDotsPerInchX()
log.info("Detected screen DPI: %s", dpi)

# Launch program
if mode == "commandline":
Expand All @@ -53,6 +51,8 @@ def main() -> int:
mode = main.parseArgs()
log.debug("Finished creating command object")

log.info(f"QApplication Platform: {QApplication.platformName()}")
log.info(f"Detected screen DPI: {dpi}")
# Both branches here may occur in one execution:
# Commandline parsing could change mode back to GUI
if mode == "GUI":
Expand Down
50 changes: 27 additions & 23 deletions src/avp/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
import shutil
import logging

from . import core, __version__
from . import __version__
from .core import Core


log = logging.getLogger("AVP.Commandline")
Expand All @@ -29,11 +30,11 @@ class Command(QtCore.QObject):

def __init__(self):
super().__init__()
self.core = core.Core()
core.Core.mode = "commandline"
self.core = Core()
Core.mode = "commandline"
self.dataDir = self.core.dataDir
self.canceled = False
self.settings = core.Core.settings
self.settings = Core.settings

# ctrl-c stops the export thread
signal.signal(signal.SIGINT, self.stopVideo)
Expand Down Expand Up @@ -71,9 +72,10 @@ def parseArgs(self):
help="copy and shorten recent log files into ~/avp_log.txt",
)
debugCommands.add_argument(
"--verbose", "-v",
"--verbose",
"-v",
action="store_true",
help="create bigger logfiles while program is running",
help="send log messages and ffmpeg output to stdout, and create more verbose log files (good to use before --log)",
)

# project/GUI options
Expand Down Expand Up @@ -101,8 +103,8 @@ def parseArgs(self):
args = parser.parse_args()

if args.verbose:
core.STDOUT_LOGLVL = logging.DEBUG
core.Core.makeLogger(deleteOldLogs=False, fileLogLvl=logging.DEBUG)
Core.stdoutLogLvl = logging.DEBUG
Core.makeLogger(deleteOldLogs=False, fileLogLvl=logging.DEBUG)

if args.log:
self.createLogFile()
Expand Down Expand Up @@ -168,7 +170,7 @@ def parseArgs(self):
return "commandline"

elif args.no_preview:
core.Core.previewEnabled = False
Core.previewEnabled = False

elif (
args.projpath is None
Expand Down Expand Up @@ -203,27 +205,26 @@ def stopVideo(self, *args):

@QtCore.pyqtSlot(str)
def progressBarSetText(self, value):
if "Export " in value:
# Don't duplicate completion/failure messages
if "Export " in value or time.time() - self.lastProgressUpdate < 0.1:
# Don't duplicate completion/failure messages or send too many messages
return
if (
not value.startswith("Exporting")
and time.time() - self.lastProgressUpdate >= 0.05
):

if not value.endswith("%"):
# Show most messages very often
print(value)
elif time.time() - self.lastProgressUpdate >= 2.0:
# Give user time to read ffmpeg's output during the export
print("##### %s" % value)
else:
return
elif log.getEffectiveLevel() > logging.INFO:
# if ffmpeg isn't printing export progress for us,
# then overwrite previous message with the next one
# if this text is our main export progress
print(f"{value}\r", end="")
self.lastProgressUpdate = time.time()

@QtCore.pyqtSlot()
def videoCreated(self):
self.quit(0)

def quit(self, code):
print()
quit(code)

def showMessage(self, **kwargs):
Expand All @@ -242,12 +243,14 @@ def drawPreview(self, *args):

def parseCompName(self, name):
"""Deduces a proper component name out of a commandline arg"""

if name.title() in self.core.compNames:
return name.title()
for compName in self.core.compNames:
if name.capitalize() in compName:
return compName
for altName, moduleIndex in self.core.altCompNames:
if name.title() in altName:
return self.core.compNames[moduleIndex]

compFileNames = [
os.path.splitext(os.path.basename(mod.__file__))[0]
Expand Down Expand Up @@ -281,16 +284,17 @@ def getFilename():
print("Log file could not be created (too many exist).")
return
try:
shutil.copy(os.path.join(core.Core.logDir, "avp_debug.log"), filename)
shutil.copy(os.path.join(Core.logDir, "avp_debug.log"), filename)
with open(filename, "a") as f:
f.write(f"{'='*60} debug log ends {'='*60}\n")
except FileNotFoundError:
print("No debug log was found. Run `avp --verbose` before `avp --log`.")
with open(filename, "w") as f:
f.write(f"{'='*60} no debug log {'='*60}\n")

def concatenateLogs(logPattern):
nonlocal filename
renderLogs = glob.glob(os.path.join(core.Core.logDir, logPattern))
renderLogs = glob.glob(os.path.join(Core.logDir, logPattern))
with open(filename, "a") as fw:
for renderLog in renderLogs:
with open(renderLog, "r") as fr:
Expand Down
81 changes: 53 additions & 28 deletions src/avp/components/original.py → src/avp/components/classic.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import numpy
from PIL import Image, ImageDraw
from copy import copy

from ..component import Component
from ..toolkit.frame import BlankFrame
from ..libcomponent import BaseComponent
from ..toolkit.frame import BlankFrame, FloodFrame
from ..toolkit.visualizer import createSpectrumArray


class Component(Component):
class Component(BaseComponent):
name = "Classic Visualizer"
version = "1.1.0"
version = "1.2.0"

def names(*args):
return ["Original Audio Visualization"]
return ["Original"]

def properties(self):
return ["pcm"]
props = ["pcm"]
if self.invert:
props.append("composite")
return props

def widget(self, *args):
self.scale = 20
Expand All @@ -37,6 +39,7 @@ def widget(self, *args):
"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,
Expand All @@ -46,14 +49,19 @@ def widget(self, *args):
],
)

def previewRender(self):
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
self.width,
self.height,
spectrum,
self.visColor,
self.layout,
frame,
)

def preFrameRender(self, **kwargs):
Expand All @@ -71,17 +79,18 @@ def preFrameRender(self, **kwargs):
self.progressBarSetText,
)

def frameRender(self, frameNo):
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):
def drawBars(self, width, height, spectrum, color, layout, frame):
bigYCoord = height - height / 8
smallYCoord = height / 1200
bigXCoord = width / (self.bars + 1)
Expand All @@ -94,32 +103,44 @@ def drawBars(self, width, height, spectrum, color, layout):
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(
(
# 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,
),
fill=color2,
)
)
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(
(
x0,
y0 if y0 < y1 else y1,
x1 if x1 > x0 else x0,
y1 if y0 < y1 else y0,
),
fill=color,
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)
Expand All @@ -146,7 +167,11 @@ def drawBars(self, width, height, spectrum, color, layout):
y = self.y - int(height / 100 * 10)
im.paste(imBottom, (0, y), mask=imBottom)

return im
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
<item>
<widget class="QLineEdit" name="lineEdit_visColor">
<property name="text">
<string></string>
<string/>
</property>
</widget>
</item>
Expand Down Expand Up @@ -232,6 +232,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_invert">
<property name="text">
<string>Invert</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
Expand Down
4 changes: 2 additions & 2 deletions src/avp/components/color.py
Original file line number Diff line number Diff line change
@@ -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"

Expand Down
3 changes: 3 additions & 0 deletions src/avp/components/color.ui
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@
<height>32</height>
</size>
</property>
<property name="toolTip">
<string>End color of gradient. Disabled if fill is solid.</string>
</property>
<property name="text">
<string/>
</property>
Expand Down
Loading