Skip to content

Commit f03a3a6

Browse files
authored
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
1 parent 48a9105 commit f03a3a6

43 files changed

Lines changed: 974 additions & 676 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ prof/
1010
.env/
1111
.vscode/
1212
tests/data/config/log/
13+
tests/data/config/presets/
1314
tests/data/config/settings.ini
1415
tests/data/config/autosave.avp
1516
*.mkv

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "uv_build"
66
name = "audio-visualizer-python"
77
description = "Create audio visualization videos from a GUI or commandline"
88
readme = "README.md"
9-
version = "2.2.3"
9+
version = "2.2.4"
1010
requires-python = ">= 3.12"
1111
license = "MIT"
1212
classifiers=[

src/avp/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44

55

6-
__version__ = "2.2.3"
6+
__version__ = "2.2.4"
77

88

99
class Logger(logging.getLoggerClass()):

src/avp/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,8 @@ def main() -> int:
4040
screen = app.primaryScreen()
4141
if screen is None:
4242
dpi = None
43-
log.error("Could not detect DPI")
4443
else:
4544
dpi = screen.physicalDotsPerInchX()
46-
log.info("Detected screen DPI: %s", dpi)
4745

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

54+
log.info(f"QApplication Platform: {QApplication.platformName()}")
55+
log.info(f"Detected screen DPI: {dpi}")
5656
# Both branches here may occur in one execution:
5757
# Commandline parsing could change mode back to GUI
5858
if mode == "GUI":

src/avp/command.py

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
import shutil
1515
import logging
1616

17-
from . import core, __version__
17+
from . import __version__
18+
from .core import Core
1819

1920

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

3031
def __init__(self):
3132
super().__init__()
32-
self.core = core.Core()
33-
core.Core.mode = "commandline"
33+
self.core = Core()
34+
Core.mode = "commandline"
3435
self.dataDir = self.core.dataDir
3536
self.canceled = False
36-
self.settings = core.Core.settings
37+
self.settings = Core.settings
3738

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

7981
# project/GUI options
@@ -101,8 +103,8 @@ def parseArgs(self):
101103
args = parser.parse_args()
102104

103105
if args.verbose:
104-
core.STDOUT_LOGLVL = logging.DEBUG
105-
core.Core.makeLogger(deleteOldLogs=False, fileLogLvl=logging.DEBUG)
106+
Core.stdoutLogLvl = logging.DEBUG
107+
Core.makeLogger(deleteOldLogs=False, fileLogLvl=logging.DEBUG)
106108

107109
if args.log:
108110
self.createLogFile()
@@ -168,7 +170,7 @@ def parseArgs(self):
168170
return "commandline"
169171

170172
elif args.no_preview:
171-
core.Core.previewEnabled = False
173+
Core.previewEnabled = False
172174

173175
elif (
174176
args.projpath is None
@@ -203,27 +205,26 @@ def stopVideo(self, *args):
203205

204206
@QtCore.pyqtSlot(str)
205207
def progressBarSetText(self, value):
206-
if "Export " in value:
207-
# Don't duplicate completion/failure messages
208+
if "Export " in value or time.time() - self.lastProgressUpdate < 0.1:
209+
# Don't duplicate completion/failure messages or send too many messages
208210
return
209-
if (
210-
not value.startswith("Exporting")
211-
and time.time() - self.lastProgressUpdate >= 0.05
212-
):
211+
212+
if not value.endswith("%"):
213213
# Show most messages very often
214214
print(value)
215-
elif time.time() - self.lastProgressUpdate >= 2.0:
216-
# Give user time to read ffmpeg's output during the export
217-
print("##### %s" % value)
218-
else:
219-
return
215+
elif log.getEffectiveLevel() > logging.INFO:
216+
# if ffmpeg isn't printing export progress for us,
217+
# then overwrite previous message with the next one
218+
# if this text is our main export progress
219+
print(f"{value}\r", end="")
220220
self.lastProgressUpdate = time.time()
221221

222222
@QtCore.pyqtSlot()
223223
def videoCreated(self):
224224
self.quit(0)
225225

226226
def quit(self, code):
227+
print()
227228
quit(code)
228229

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

243244
def parseCompName(self, name):
244245
"""Deduces a proper component name out of a commandline arg"""
245-
246246
if name.title() in self.core.compNames:
247247
return name.title()
248248
for compName in self.core.compNames:
249249
if name.capitalize() in compName:
250250
return compName
251+
for altName, moduleIndex in self.core.altCompNames:
252+
if name.title() in altName:
253+
return self.core.compNames[moduleIndex]
251254

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

291295
def concatenateLogs(logPattern):
292296
nonlocal filename
293-
renderLogs = glob.glob(os.path.join(core.Core.logDir, logPattern))
297+
renderLogs = glob.glob(os.path.join(Core.logDir, logPattern))
294298
with open(filename, "a") as fw:
295299
for renderLog in renderLogs:
296300
with open(renderLog, "r") as fr:
Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import numpy
22
from PIL import Image, ImageDraw
3-
from copy import copy
43

5-
from ..component import Component
6-
from ..toolkit.frame import BlankFrame
4+
from ..libcomponent import BaseComponent
5+
from ..toolkit.frame import BlankFrame, FloodFrame
76
from ..toolkit.visualizer import createSpectrumArray
87

98

10-
class Component(Component):
9+
class Component(BaseComponent):
1110
name = "Classic Visualizer"
12-
version = "1.1.0"
11+
version = "1.2.0"
1312

1413
def names(*args):
15-
return ["Original Audio Visualization"]
14+
return ["Original"]
1615

1716
def properties(self):
18-
return ["pcm"]
17+
props = ["pcm"]
18+
if self.invert:
19+
props.append("composite")
20+
return props
1921

2022
def widget(self, *args):
2123
self.scale = 20
@@ -37,6 +39,7 @@ def widget(self, *args):
3739
"y": self.page.spinBox_y,
3840
"smooth": self.page.spinBox_sensitivity,
3941
"bars": self.page.spinBox_bars,
42+
"invert": self.page.checkBox_invert,
4043
},
4144
colorWidgets={
4245
"visColor": self.page.pushButton_visColor,
@@ -46,14 +49,19 @@ def widget(self, *args):
4649
],
4750
)
4851

49-
def previewRender(self):
52+
def previewRender(self, frame=None):
5053
spectrum = numpy.fromfunction(
5154
lambda x: float(self.scale) / 2500 * (x - 128) ** 2,
5255
(255,),
5356
dtype="int16",
5457
)
5558
return self.drawBars(
56-
self.width, self.height, spectrum, self.visColor, self.layout
59+
self.width,
60+
self.height,
61+
spectrum,
62+
self.visColor,
63+
self.layout,
64+
frame,
5765
)
5866

5967
def preFrameRender(self, **kwargs):
@@ -71,17 +79,18 @@ def preFrameRender(self, **kwargs):
7179
self.progressBarSetText,
7280
)
7381

74-
def frameRender(self, frameNo):
82+
def frameRender(self, frameNo, frame=None):
7583
arrayNo = frameNo * self.sampleSize
7684
return self.drawBars(
7785
self.width,
7886
self.height,
7987
self.spectrumArray[arrayNo],
8088
self.visColor,
8189
self.layout,
90+
frame,
8291
)
8392

84-
def drawBars(self, width, height, spectrum, color, layout):
93+
def drawBars(self, width, height, spectrum, color, layout, frame):
8594
bigYCoord = height - height / 8
8695
smallYCoord = height / 1200
8796
bigXCoord = width / (self.bars + 1)
@@ -94,32 +103,44 @@ def drawBars(self, width, height, spectrum, color, layout):
94103
color2 = (r, g, b, 125)
95104

96105
for i in range(self.bars):
97-
x0 = middleXCoord + i * bigXCoord
98-
y0 = bigYCoord + smallXCoord
99-
y1 = bigYCoord + smallXCoord - spectrum[i * 4] * smallYCoord - middleXCoord
100-
x1 = middleXCoord + i * bigXCoord + bigXCoord
101-
draw.rectangle(
102-
(
106+
# draw outline behind rectangles if not inverted
107+
if frame is None:
108+
x0 = middleXCoord + i * bigXCoord
109+
y0 = bigYCoord + smallXCoord
110+
x1 = middleXCoord + i * bigXCoord + bigXCoord
111+
y1 = (
112+
bigYCoord
113+
+ smallXCoord
114+
- spectrum[i * 4] * smallYCoord
115+
- middleXCoord
116+
)
117+
selection = (
103118
x0,
104119
y0 if y0 < y1 else y1,
105120
x1 if x1 > x0 else x0,
106121
y1 if y0 < y1 else y0,
107-
),
108-
fill=color2,
109-
)
122+
)
123+
draw.rectangle(
124+
selection,
125+
fill=color2,
126+
)
110127

111128
x0 = middleXCoord + smallXCoord + i * bigXCoord
112129
y0 = bigYCoord
113130
x1 = middleXCoord + smallXCoord + i * bigXCoord + middleXCoord
114131
y1 = bigYCoord - spectrum[i * 4] * smallYCoord
132+
selection = (
133+
x0,
134+
y0 if y0 < y1 else y1,
135+
x1 if x1 > x0 else x0,
136+
y1 if y0 < y1 else y0,
137+
)
138+
# fill rectangle if not inverted
115139
draw.rectangle(
116-
(
117-
x0,
118-
y0 if y0 < y1 else y1,
119-
x1 if x1 > x0 else x0,
120-
y1 if y0 < y1 else y0,
121-
),
122-
fill=color,
140+
selection,
141+
fill=color if frame is None else (0, 0, 0, 0),
142+
outline=color,
143+
width=int(x1 - x0),
123144
)
124145

125146
imBottom = imTop.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
@@ -146,7 +167,11 @@ def drawBars(self, width, height, spectrum, color, layout):
146167
y = self.y - int(height / 100 * 10)
147168
im.paste(imBottom, (0, y), mask=imBottom)
148169

149-
return im
170+
if frame is None:
171+
return im
172+
f = FloodFrame(width, height, color)
173+
f.paste(frame, (0, 0), mask=im)
174+
return f
150175

151176
def command(self, arg):
152177
if "=" in arg:
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
<item>
8787
<widget class="QLineEdit" name="lineEdit_visColor">
8888
<property name="text">
89-
<string></string>
89+
<string/>
9090
</property>
9191
</widget>
9292
</item>
@@ -232,6 +232,13 @@
232232
</property>
233233
</widget>
234234
</item>
235+
<item>
236+
<widget class="QCheckBox" name="checkBox_invert">
237+
<property name="text">
238+
<string>Invert</string>
239+
</property>
240+
</widget>
241+
</item>
235242
<item>
236243
<spacer name="horizontalSpacer">
237244
<property name="orientation">

src/avp/components/color.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
from PyQt6 import QtGui
22
import logging
33

4-
from ..component import Component
4+
from ..libcomponent import BaseComponent
55
from ..toolkit.frame import BlankFrame, FloodFrame, FramePainter
66

77

88
log = logging.getLogger("AVP.Components.Color")
99

1010

11-
class Component(Component):
11+
class Component(BaseComponent):
1212
name = "Color"
1313
version = "1.0.0"
1414

src/avp/components/color.ui

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@
124124
<height>32</height>
125125
</size>
126126
</property>
127+
<property name="toolTip">
128+
<string>End color of gradient. Disabled if fill is solid.</string>
129+
</property>
127130
<property name="text">
128131
<string/>
129132
</property>

0 commit comments

Comments
 (0)