#!/usr/bin/env python3 """Run the C64 Space Taxi music player and log every SID write per frame. The player lives in the TITLE memory image at $CB00-$CFFF (its own relocated copy; the $7500 copy is gameplay-side). Rather than hand-decode its byte code, drive the real routines under cpu6502.py -- the same trick that extracted the speech: $CB00 select song ($CB01 = song number) and init (hooks the IRQ vector) $CF52 the IRQ body: first frame calls start ($CC28), then ticks the three voices via $CC6C, then JMPs to the KERNAL IRQ ($EA31) which is what advances the jiffy clock $A2. We poke an RTS at $EA31 and bump $A2 ourselves, once per frame. A song is over when all three voice stream pointers' high bytes are zero (that is exactly what the game's play-and-wait loop at $44D5 spins on). """ import json, sys sys.path.insert(0, "/home/scott/claude/joeylib/stuff/spacetaxi") from cpu6502 import Cpu6502 RAM = bytearray(open(sys.argv[1], "rb").read()[:65536]) OUT = sys.argv[2] MAX_FRAMES = 4000 class SidTrap(Cpu6502): def __init__(self, ram): super().__init__(ram) self.log = [] self.frame = 0 def write8(self, addr, val): a = addr & 0xFFFF if 0xD400 <= a <= 0xD418: self.log.append((self.frame, a - 0xD400, val & 0xFF)) super().write8(addr, val) songs = {} for song in range(0, 9): ram = bytearray(RAM) ram[0xEA31] = 0x60 # KERNAL IRQ tail -> RTS cpu = SidTrap(ram) cpu.call(0xCC00) # full reset first ram[0xCB01] = song cpu.call(0xCB00) # select + arm for frame in range(MAX_FRAMES): cpu.frame = frame ram[0xA2] = (ram[0xA2] + 1) & 0xFF cpu.call(0xCF52) if ram[0xCB81] == 0 and ram[0xCB88] == 0 and ram[0xCB8F] == 0: break songs[song] = {"frames": frame + 1, "writes": cpu.log, "ptr": ram[0xCB40 + song * 2] | (ram[0xCB41 + song * 2] << 8)} print(f"song {song}: ptr=${songs[song]['ptr']:04X} frames={frame+1} " f"({(frame+1)/50:.1f}s PAL) sidWrites={len(cpu.log)}") json.dump(songs, open(OUT, "w"))