Files
lintunes/scripts/kaleidostrobe/compare.py
T
trav e38c751e39 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.
2026-10-14 12:02:22 -07:00

108 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)