Files
lintunes/tests/test_round85.py
T
trav 00aa5e7f2e v0.55.0: Kaleidostrobe smooth again; visualizer tile hover controls
The cones move only on the plugin's own 25 fps frames again: driving them
from fresh audio every refresh made bass-heavy songs twitch (a jump on 84%
of refreshes against 41%). The continuous turn, texture crossfade and the
GPU big window stay.

The tile: a click opens it big (and it stays blank while big); hovering
shows a colour wheel (colour / gray / off) and up/down arrows (previous /
next visualizer). No more double-click or click-to-cycle. The big album
art closes on a click or Esc, like the big visualizer.
2026-10-14 12:51:38 -07:00

96 lines
4.0 KiB
Python

"""Round 85: Kaleidostrobe at 60 fps, its big window on the GPU, gray mode
as two tones, and a sync slider that can also say "sooner".
1. Between the plugin's 25 fps frames: a smooth turn and crossfaded
textures. (The cone also followed a preview of the newest audio here;
Round 86 took that out — it made the picture twitch.)
2. Gray mode: the gray-mode colour on the panel background, nothing else.
3. The sync slider below zero.
4. The big window: an OpenGL canvas for Kaleidostrobe, QPainter without GL.
"""
import numpy as np
from PyQt6.QtGui import QColor
from lintunes.kaleidostrobe import engine as ks
from lintunes.gui import visualizer_styles as vs
from lintunes.gui.visualizer_kaleido import KaleidostrobeRenderer
from lintunes.preferences import Preferences
from test_round84 import _frame, _paint, _FakePlayer, _LastFm
SEED = 7
class TestBetweenFrames:
def test_gray_mode_is_two_tone(self, qapp):
"""Only the gray-mode colour and the panel's background, blended."""
r = KaleidostrobeRenderer()
r._engine = ks.Kaleidostrobe(seed=SEED, ticks=1000)
for _ in range(5):
r.advance(_frame(dt=0.04))
r.dimmed = True # the widget then passes its gray
r.panel = QColor(240, 236, 230)
img = _paint(r, 120, 80, color="#606060")
ground, ink = np.array([240, 236, 230.]), np.array([96, 96, 96.])
k = (img - ground) / (ink - ground) # 0 = ground, 1 = ink
assert np.ptp(k, axis=-1).max() < 0.03 # on the line between
assert k.min() > -0.02 and k.max() < 1.02
assert k.max() - k.min() > 0.15 # and still a picture
def test_draws_between_the_plugins_frames(self, qapp):
"""60 fps on screen: the turn moves every refresh, frames at 25."""
r = KaleidostrobeRenderer()
r.advance(_frame(dt=0.04))
turns, frames = [], []
for _ in range(12):
r.advance(_frame(dt=1 / 60))
turns.append(r._shown[0][0].turn)
frames.append(r._engine.frame_count)
assert len(set(turns)) == 12 # every refresh
assert np.all(np.diff(turns) > 0) # smoothly onward
assert frames[-1] - frames[0] == 4 # 12/60 s at 25 fps
class TestSyncSooner:
def test_negative_delay_is_kept(self):
assert vs.sync_delay_ms({"visualizer_sync_delay_ms": -30}) == -30
assert vs.sync_delay_ms({"visualizer_sync_delay_ms": -999}) == vs.SYNC_DELAY_LIMITS[0]
def test_slider_reads_sooner_and_later(self, qapp, tmp_path, monkeypatch):
from lintunes.gui import preferences_dialog as pd
monkeypatch.setattr(pd, "load_config", lambda: {})
dialog = pd.PreferencesDialog(Preferences(tmp_path), _LastFm())
assert dialog._sync_slider.minimum() < 0
assert pd._sync_text(-20) == "20 ms sooner"
assert pd._sync_text(40) == "40 ms later"
class TestBigWindow:
def _vis(self, tmp_path, style):
from lintunes.gui.visualizer import VisualizerWidget
prefs = Preferences(tmp_path)
prefs.set("visualizer_style", style)
return prefs, VisualizerWidget(_FakePlayer(), prefs)
def test_kaleidostrobe_gets_the_gl_canvas(self, qapp, tmp_path):
prefs, vis = self._vis(tmp_path, "kaleidostrobe")
vis.open_big()
try:
assert vis._big._canvas is not None and not vis._big._canvas.isHidden()
prefs.set("visualizer_style", "bars") # another style
vis._repaint()
assert vis._big._canvas.isHidden()
finally:
vis._big.close()
def test_without_opengl_it_paints_with_qpainter(self, qapp, tmp_path):
prefs, vis = self._vis(tmp_path, "kaleidostrobe")
vis.open_big()
try:
vis._big._canvas.unavailable.emit() # no GL driver
assert vis._big._canvas.isHidden()
assert not vis._big._wants_gl()
finally:
vis._big.close()