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
+107
View File
@@ -0,0 +1,107 @@
"""Run the emulated original and the Python port side by side."""
import math, sys, random
sys.path.insert(0, __file__.rsplit('/', 1)[0])
sys.path.insert(0, str(__import__('pathlib').Path(__file__).resolve().parents[2]))
import numpy as np
from plugin import Plugin
from lintunes.kaleidostrobe.engine import Kaleidostrobe
def rotz(a):
r = math.radians(a); c, s = math.cos(r), math.sin(r)
m = np.eye(4); m[0, 0] = c; m[0, 1] = -s; m[1, 0] = s; m[1, 1] = c
return m
def trans(x, y, z):
m = np.eye(4); m[:3, 3] = (x, y, z); return m
def gl_triangles(calls):
"""(texture, alpha, [(st×3, eye-xyz×3)]) per glBegin batch, plus textures."""
stack = [np.eye(4)]; tex = None; alpha = 1.0; st = (0, 0)
out = []; cur = None; mode = None
for name, a in calls:
M = stack[-1]
if name == '_glLoadIdentity': stack[-1] = np.eye(4)
elif name == '_gluLookAt': stack[-1] = stack[-1] @ trans(0, 0, -15)
elif name == '_glPushMatrix': stack.append(stack[-1].copy())
elif name == '_glPopMatrix': stack.pop()
elif name == '_glRotatef': stack[-1] = stack[-1] @ rotz(a[0])
elif name == '_glTranslatef': stack[-1] = stack[-1] @ trans(*a[:3])
elif name == '_glBindTexture': tex = a[1]
elif name == '_glColor4f': alpha = a[3]
elif name == '_glBegin': cur = []; mode = a[0]
elif name == '_glTexCoord2f': st = a[:2]
elif name in ('_glVertex2f', '_glVertex3f'):
v = (a[0], a[1], a[2] if name == '_glVertex3f' else 0.0, 1.0)
cur.append((st, (M @ np.array(v))[:3]))
elif name == '_glEnd':
tris = []
if mode == 4:
tris = [cur[i:i + 3] for i in range(0, len(cur), 3)]
elif mode == 6:
tris = [[cur[0], cur[i], cur[i + 1]] for i in range(1, len(cur) - 1)]
for t in tris:
out.append((tex, alpha, t))
return out
def engine_triangles(frame):
out = []
for layer in frame.layers:
base = trans(0, 0, -15) @ trans(0, 0, layer.lift) @ rotz(layer.turn)
for t in layer.triangles:
M = base @ rotz(t.turn)
out.append((layer.texture + 1, layer.alpha,
[(t.st[i], (M @ np.array((*t.xyz[i], 1.0)))[:3]) for i in range(3)]))
return out
def spectra(frame, rnd):
"""Fake iTunes spectra: a kick every ~13 frames, noise elsewhere."""
kick = (frame % 13) < 2
row = [rnd.randrange(0, 40) for _ in range(512)]
for b in range(4):
row[b] = rnd.randrange(180, 256) if kick else rnd.randrange(20, 120)
for b in range(4, 32):
row[b] = rnd.randrange(60, 200)
return [row, row[:]]
def main(frames=200, seed_ticks=1000, w=640, h=480):
p = Plugin(rect=(0, 0, h, w), ticks=seed_ticks)
e = Kaleidostrobe(seed=seed_ticks, ticks=seed_ticks)
e.set_size(w, h)
rnd = random.Random(7)
tex_bad = geo_bad = 0
t0 = p.k.micro
for f in range(frames):
spec = spectra(f, rnd)
start = len(p.k.gl)
p.k.micro += 40001
ticks = seed_ticks + (p.k.micro - t0) * 60 // 1_000_000
calls = p.frame(None, spec, dt_us=0, dticks=ticks - p.k.ticks)
ours = e.step(np.array(spec, np.uint8), ticks)
# textures: the two glTexImage2D uploads of this frame
idx = [i for i in range(start, len(p.k.gl)) if p.k.gl[i][0] == '_glTexImage2D']
theirs = [np.frombuffer(p.k.textures[i], np.uint8).reshape(128, 128, 4) for i in idx]
for t in range(2):
d = int((theirs[t] != ours.textures[t]).any(-1).sum())
if d:
tex_bad += 1
if tex_bad <= 5:
print(f'frame {f} tex {t}: {d} texels differ')
a, b = gl_triangles(calls), engine_triangles(ours)
ok = len(a) == len(b) and all(
x[0] == y[0] and abs(x[1] - y[1]) < 1e-6 and
all(np.allclose(x[2][i][0], y[2][i][0], atol=1e-4) and
np.allclose(x[2][i][1], y[2][i][1], atol=1e-3) for i in range(3))
for x, y in zip(a, b))
if not ok:
geo_bad += 1
if geo_bad <= 3:
print(f'frame {f}: geometry differs ({len(a)} vs {len(b)} triangles)')
for x, y in list(zip(a, b))[:2]:
print(' orig', x); print(' port', y)
if f % 50 == 0:
print(f'frame {f}: beat={ours.beat} beats={e.beat_count} objects={e.bottom_object},{e.top_object} '
f'map={e.cur_map} wave={e.cur_wave} delta={e.cur_delta}')
print(f'{frames} frames: texture mismatches {tex_bad}, geometry mismatches {geo_bad}')
if __name__ == '__main__':
main(int(sys.argv[1]) if len(sys.argv) > 1 else 200)