v0.54.0: Kaleidostrobe at 60 fps, its big window on the GPU, two-tone gray mode, sync can go sooner

The plugin still steps at its own 25 fps, frame for frame; between frames
the turn continues, textures crossfade, and the cone follows a preview of
the newest audio, so beats land a refresh sooner. The big window replays
the original's GL calls through OpenGL (QPainter took ~40 ms a frame at
1080p); without GL it falls back. Gray mode is only the gray-mode colour on
the panel background. The sync slider goes to -50 ms (sooner).
This commit is contained in:
2026-10-14 12:26:38 -07:00
parent e38c751e39
commit 7ae55e757a
11 changed files with 514 additions and 121 deletions
+102
View File
@@ -0,0 +1,102 @@
"""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, crossfaded textures,
the cone driven by a preview of the newest audio — which must never
change the original's own frames.
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 GOLDEN, _digest, _frame, _paint, _FakePlayer, _LastFm
class TestBetweenFrames:
def test_previews_between_frames_change_nothing(self):
"""The renderer asks for a preview every screen refresh; the frames
must still be the original's."""
engine = ks.Kaleidostrobe(seed=1000, ticks=1000)
for f, (spectrum, ticks) in enumerate(zip(GOLDEN["spectra"], GOLDEN["ticks"])):
engine.preview(spectrum, int(ticks) + 1)
engine.preview(GOLDEN["spectra"][-1 - f], int(ticks) + 2)
frame = engine.step(spectrum, int(ticks))
assert _digest(frame.textures[0]) == GOLDEN["tex_digest"][f][0], f"frame {f}"
assert _digest(frame.textures[1]) == GOLDEN["tex_digest"][f][1], f"frame {f}"
def test_gray_mode_is_two_tone(self, qapp):
"""Only the gray-mode colour and the panel's background, blended."""
r = KaleidostrobeRenderer()
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.3 # 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()