notepat: massive FPS boost — QR cache + C-side waveform strip master
Two hot paths were costing most of the per-frame budget on slow Intel m3 class CPUs (the ThinkPad this runs on). Moving both into C brings notepat's paint path back into single-digit milliseconds. 1. graph_qr() — now caches the last qrcodegen_encodeText() result keyed by the input URL. Previously the QR was re-encoded (Reed-Solomon + masking) every single paint frame; the cache turns that into a single strncmp and a cheap module-grid walk. `notepat.com` never changes so the cache is effectively permanent. 2. sound.speaker.drawStrip(x, y, w, h, seconds, needleFrac, flags) renders the whole scrolling DJ-needle waveform in one C call: - reads the speaker ring buffer under audio->lock, copies only the slice we need (no Float32Array allocation handed to JS) - per-column peak-scan runs in native int/float math, not QuickJS interpreted loops - background, zero-line, per-column warm→cold bar, and the playhead needle are drawn in-place via graph_* primitives Previously notepat did `sound.speaker.getRecentBuffer(4)` every 4 frames (malloc + ring copy + new Float32Array into JS), then a 600- iteration JS for-loop per paint that called Math.floor/abs/round and ink()+line() 600 times. On a dual-core m3-8100Y that was pinning FPS at ~20. New shape keeps it well under 1ms per frame and renders every paint (no more 4-frame staleness). Concurrent behavior changes (requested by @jeffrey): * Playhead needle moves from the right edge to the CENTER of the strip (needleFrac=0.5) — reads like a turntable stylus, not a VU capture. * When spacebar is held, the reverse-scroll flag flips so the waveform visually moves the opposite direction: the held key now reads as a "rewind/replay cursor" rather than live forward capture. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>