v0.53.0: Kaleidostrobe, the 2002 iTunes visualizer, as the fourth style

Reverse-engineered from its PowerPC binary and checked against the original
running under emulation: textures byte for byte and every triangle, over
3,600 frames. 25 fps like the plugin; its colour maps and wave images are
extracted from its own code; tests/kaleidostrobe_golden.npz was recorded
from the original.
This commit is contained in:
2026-10-14 12:02:22 -07:00
parent 6178f5d0f7
commit e38c751e39
21 changed files with 2109 additions and 3 deletions
Binary file not shown.
+320
View File
@@ -0,0 +1,320 @@
"""Round 84: Kaleidostrobe, the 2002 iTunes visualizer, as the fourth style.
``kaleidostrobe_golden.npz`` was recorded from the *original* PowerPC plugin,
run under emulation through its own VisualPluginHandler: every frame's two
uploaded textures (as digests) and every triangle it handed OpenGL, for a
fixed run of spectra; plus the output of each warp (calcDelta) and each
wave-table entry. The port has to reproduce them exactly.
1. The engine against the original: textures byte for byte, geometry.
2. The pieces: warps, wave shapes, transform's quirks, Darwin's rand().
3. The renderer: 25 fps, the GL picture, dim mode, pause.
4. The style in the app.
"""
import hashlib
import math
from pathlib import Path
import numpy as np
import pytest
from PyQt6.QtCore import QObject, QRectF, pyqtSignal
from PyQt6.QtGui import QColor, QImage, QPainter
from lintunes.kaleidostrobe import engine as ks
from lintunes.gui import visualizer_styles as vs
from lintunes.gui.visualizer_kaleido import KaleidostrobeRenderer, itunes_spectrum
from lintunes.preferences import Preferences
GOLDEN = np.load(Path(__file__).with_name("kaleidostrobe_golden.npz"))
RATE = 44100
def _digest(a):
return hashlib.sha256(np.ascontiguousarray(a).tobytes()).hexdigest()[:16]
def _pattern(seed):
y, x = np.mgrid[0:128, 0:128]
return ((x * (7 + seed) + y * (13 + 2 * seed)) ^ (x * y + seed)).astype(np.uint8)
def _rotz(deg):
r = math.radians(deg)
c, s = math.cos(r), math.sin(r)
return np.array([[c, -s, 0, 0], [s, c, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
def _lift(z):
m = np.eye(4)
m[2, 3] = z
return m
def _eye_space(frame):
"""Each triangle as (s, t, x, y, z) × 3 in eye space, the way the
golden file recorded the original's GL calls."""
out = []
for layer in frame.layers:
base = _lift(-ks.EYE_Z) @ _lift(layer.lift) @ _rotz(layer.turn)
for tri in layer.triangles:
m = base @ _rotz(tri.turn)
out.append([(*tri.st[i], *(m @ np.array((*tri.xyz[i], 1.0)))[:3])
for i in range(3)])
return np.array(out)
# --------------------------------------------------------------------------
# 1. The engine against the original
# --------------------------------------------------------------------------
class TestAgainstTheOriginal:
def test_every_frame_matches(self):
engine = ks.Kaleidostrobe(seed=1000, ticks=1000) # srand(TickCount())
engine.set_size(640, 480)
counts = GOLDEN["tri_counts"]
offsets = np.concatenate([[0], np.cumsum(counts)])
for f, (spectrum, ticks) in enumerate(zip(GOLDEN["spectra"], GOLDEN["ticks"])):
frame = engine.step(spectrum, int(ticks))
for tex in (0, 1):
assert _digest(frame.textures[tex]) == GOLDEN["tex_digest"][f][tex], \
f"frame {f}, texture {tex}"
ours = _eye_space(frame)
theirs = GOLDEN["tris"][offsets[f]:offsets[f + 1]]
assert ours.shape == theirs.shape, f"frame {f}"
np.testing.assert_allclose(ours[..., :2], theirs[..., :2], atol=1e-4)
np.testing.assert_allclose(ours[..., 2:], theirs[..., 2:], atol=1e-3)
def test_the_run_saw_beats_and_switches(self):
"""The recorded run must exercise more than the opening state."""
engine = ks.Kaleidostrobe(seed=1000, ticks=1000)
deltas = set()
for spectrum, ticks in zip(GOLDEN["spectra"], GOLDEN["ticks"]):
engine.step(spectrum, int(ticks))
deltas.add(tuple(engine.cur_delta))
assert engine.beat_count >= 8
assert len(deltas) > 1
# --------------------------------------------------------------------------
# 2. The pieces
# --------------------------------------------------------------------------
class TestPieces:
@pytest.mark.parametrize("i, n", list(enumerate(ks.DELTA_TABLE)))
def test_every_warp_matches(self, i, n):
state = {}
for it in range(3):
out = ks.delta(n, _pattern(n * 3 + it), state)
assert _digest(out) == GOLDEN["delta_digest"][i][it], f"iteration {it}"
@pytest.mark.parametrize("idx", range(len(ks.WAVE_TABLE)))
def test_every_wave_matches(self, idx):
engine = ks.Kaleidostrobe(seed=1, ticks=1000)
engine.rng = ks.ParkMiller(4321)
engine.cur_wave[0] = idx
engine._init_wave(0)
assert _digest(engine.buffers[0, 0]) == GOLDEN["wave_digest"][idx]
def test_rand_is_darwins(self):
rng = ks.ParkMiller(1)
assert [rng.rand() for _ in range(3)] == [16807, 282475249, 1622650073]
def test_flip_diag_keeps_its_off_by_one(self):
img = np.arange(128 * 128, dtype=np.int64).reshape(128, 128) % 251
img = img.astype(np.uint8)
before = img.copy()
ks.flip_diag(img)
assert img[10, 3] == before[3, 10] # transposed…
assert img[10, 9] == before[10, 9] # …except beside the diagonal
assert img[9, 10] == before[9, 10]
def test_inside_out_flips_each_half(self):
img = np.repeat(np.arange(128, dtype=np.uint8)[:, None], 128, 1)
ks.inside_out(img)
assert img[0, 0] == 63 and img[63, 0] == 0
assert img[64, 0] == 127 and img[127, 0] == 64
def test_a_reselected_warp_resumes_its_own_counter(self):
engine = ks.Kaleidostrobe(seed=3, ticks=1000)
engine.cur_delta = [0, 0] # calcDelta000 on both layers
for _ in range(5):
engine._update_textures()
engine.cur_delta[0] = 1 # another warp: its own counter, 0
engine._update_textures()
assert engine.delta_state[0][1] == {'h': 1, 'v': 1}
assert engine.delta_state[0][0] == {'h': 5}
# --------------------------------------------------------------------------
# 3. The renderer
# --------------------------------------------------------------------------
def _frame(dt=0.016, seconds=4096 / RATE, hz=60.0):
t = np.arange(int(seconds * RATE)) / RATE
samples = (0.4 * np.sin(2 * np.pi * hz * t)).astype(np.float32)
return vs.Frame(samples, RATE, np.zeros(0, np.float32), dt)
def _paint(renderer, w, h, background="white", color="blue"):
img = QImage(w, h, QImage.Format.Format_RGB32)
img.fill(QColor(background))
painter = QPainter(img)
renderer.paint(painter, QRectF(0, 0, w, h), QColor(color), QColor("gray"))
painter.end()
a = np.frombuffer(img.constBits().asstring(img.sizeInBytes()), np.uint8)
return a.reshape(h, w, 4)[..., [2, 1, 0]].astype(float)
def _bilinear(tex, s, t):
u, v = s * 128 - 0.5, t * 128 - 0.5
u0, v0 = np.floor(u).astype(int), np.floor(v).astype(int)
fu, fv = (u - u0)[..., None], (v - v0)[..., None]
T = tex.astype(float)
def at(y, x):
return T[y % 128, x % 128]
return (at(v0, u0) * (1 - fu) * (1 - fv) + at(v0, u0 + 1) * fu * (1 - fv)
+ at(v0 + 1, u0) * (1 - fu) * fv + at(v0 + 1, u0 + 1) * fu * fv)
def _gl_reference(frame, w, h):
"""What OpenGL drew: cast a ray through each pixel centre, take the
nearest triangle, interpolate (s, t), GL_LINEAR + GL_REPEAT, blend."""
f = 1 / math.tan(math.radians(ks.FOVY) / 2)
px, py = np.meshgrid(np.arange(w) + 0.5, np.arange(h) + 0.5)
d = np.stack([(2 * px / w - 1) * (w / h) / f, (1 - 2 * py / h) / f,
-np.ones_like(px)], -1)
out = np.zeros((h, w, 3))
for layer in frame.layers:
depth = np.full((h, w), -np.inf)
col = np.zeros((h, w, 3))
hit = np.zeros((h, w), bool)
base = _lift(-ks.EYE_Z) @ _lift(layer.lift) @ _rotz(layer.turn)
for tri in layer.triangles:
m = base @ _rotz(tri.turn)
p = np.array([(m @ np.array((*v, 1.0)))[:3] for v in tri.xyz])
e1, e2 = p[1] - p[0], p[2] - p[0]
n = np.cross(e1, e2)
with np.errstate(divide='ignore', invalid='ignore'):
x = d * ((p[0] @ n) / (d @ n))[..., None]
d00, d01, d11 = e1 @ e1, e1 @ e2, e2 @ e2
v2 = x - p[0]
den = d00 * d11 - d01 * d01
b1 = (d11 * (v2 @ e1) - d01 * (v2 @ e2)) / den
b2 = (d00 * (v2 @ e2) - d01 * (v2 @ e1)) / den
b0 = 1 - b1 - b2
take = (b0 >= 0) & (b1 >= 0) & (b2 >= 0) & (x[..., 2] > depth)
st = np.array(tri.st)
s = b0 * st[0, 0] + b1 * st[1, 0] + b2 * st[2, 0]
t = b0 * st[0, 1] + b1 * st[1, 1] + b2 * st[2, 1]
c = _bilinear(frame.textures[layer.texture][..., :3], s, t)
col[take] = c[take]
depth[take] = x[..., 2][take]
hit |= take
out[hit] = out[hit] * (1 - layer.alpha) + col[hit] * layer.alpha
return out
class TestRenderer:
def test_runs_at_the_plugins_25_fps(self, qapp):
r = KaleidostrobeRenderer()
for _ in range(125): # 2 s of 16 ms widget ticks
r.advance(_frame())
assert r._engine.frame_count == 50
def test_a_stall_is_not_replayed(self, qapp):
r = KaleidostrobeRenderer()
r.advance(_frame())
r.advance(_frame(dt=5.0)) # the GUI thread froze for 5 s
assert r._engine.frame_count <= 1 + 3
@pytest.mark.parametrize("frames", [3, 30, 90])
def test_paints_what_opengl_drew(self, qapp, frames):
r = KaleidostrobeRenderer()
r.advance(_frame())
r._engine.set_size(96, 60)
for _ in range(frames):
r.advance(_frame(dt=0.04))
ours = _paint(r, 96, 60)
gl = _gl_reference(r._frame, 96, 60)
assert np.abs(ours - gl).max() <= 3
def test_fills_any_shape(self, qapp):
r = KaleidostrobeRenderer()
for _ in range(5):
r.advance(_frame(dt=0.04))
for w, h in ((140, 36), (300, 900), (4, 4)):
img = _paint(r, w, h, background="magenta")
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):
r.advance(_frame(dt=0.04))
engine, count = r._engine, r._engine.frame_count
r.clear()
img = _paint(r, 40, 30, background="white")
assert (img == 255).all() # nothing drawn
r.advance(_frame(dt=0.04))
assert r._engine is engine and engine.frame_count == count + 1
@pytest.mark.parametrize("rate", [44100, 48000, 96000])
def test_spectrum_bins_are_itunes_43_hz(self, rate):
t = np.arange(4096) / rate
tone = (0.5 * np.sin(2 * np.pi * 1000 * t)).astype(np.float32)
spec = itunes_spectrum(tone, rate)
assert spec.shape == (2, 512) and spec.dtype == np.uint8
assert abs(int(spec[0].argmax()) - round(1000 / (44100 / 1024))) <= 1
assert (spec[0] == spec[1]).all()
def test_silence_is_zero(self):
assert not itunes_spectrum(np.zeros(4096, np.float32), RATE).any()
# --------------------------------------------------------------------------
# 4. In the app
# --------------------------------------------------------------------------
class _FakePlayer(QObject):
audio_buffer = pyqtSignal(object)
playing_changed = pyqtSignal(bool)
track_changed = pyqtSignal(object)
def is_playing(self):
return False
class _LastFm(QObject):
login_finished = pyqtSignal(bool, str)
status_message = pyqtSignal(str)
def is_logged_in(self):
return False
class TestInTheApp:
def test_is_a_style(self, qapp, tmp_path):
from lintunes.gui.visualizer import VisualizerWidget
prefs = Preferences(tmp_path)
prefs.set("visualizer_style", "kaleidostrobe")
vis = VisualizerWidget(_FakePlayer(), prefs)
assert isinstance(vis._renderer, KaleidostrobeRenderer)
def test_preferences_offers_it(self, qapp, tmp_path):
from lintunes.gui.preferences_dialog import PreferencesDialog
prefs = Preferences(tmp_path)
dialog = PreferencesDialog(prefs, _LastFm())
dialog._viz_radios["kaleidostrobe"].setChecked(True)
assert prefs.get("visualizer_style") == "kaleidostrobe"
assert dialog._viz_pages.currentIndex() == 3