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:
@@ -14,6 +14,7 @@ wave-table entry. The port has to reproduce them exactly.
|
||||
import hashlib
|
||||
import math
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
@@ -238,7 +239,8 @@ class TestRenderer:
|
||||
for _ in range(frames):
|
||||
r.advance(_frame(dt=0.04))
|
||||
ours = _paint(r, 96, 60)
|
||||
gl = _gl_reference(r._frame, 96, 60)
|
||||
layers, textures = r._shown # the in-between picture, not the frame
|
||||
gl = _gl_reference(SimpleNamespace(layers=layers, textures=textures), 96, 60)
|
||||
assert np.abs(ours - gl).max() <= 3
|
||||
|
||||
def test_fills_any_shape(self, qapp):
|
||||
@@ -250,14 +252,6 @@ class TestRenderer:
|
||||
magenta = (img[..., 0] == 255) & (img[..., 1] == 0) & (img[..., 2] == 255)
|
||||
assert not magenta.any() # the cone reaches every corner
|
||||
|
||||
def test_dim_mode_is_gray(self, qapp):
|
||||
r = KaleidostrobeRenderer()
|
||||
for _ in range(5):
|
||||
r.advance(_frame(dt=0.04))
|
||||
r.dimmed = True # the widget then passes its gray
|
||||
img = _paint(r, 120, 80, color="gray")
|
||||
assert np.abs(img[..., 0] - img[..., 2]).max() <= 2 # gray tint
|
||||
|
||||
def test_pause_blanks_but_keeps_the_plugin(self, qapp):
|
||||
r = KaleidostrobeRenderer()
|
||||
for _ in range(5):
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user