fix(perc): animate drum pad notation + subtle note-based pitch harmony master
Two related drum kit polish fixes. ### 1. Animated drum-pad notation was actually static The per-pad stochastic notation uses a frame-indexed PRNG to jitter voice positions + noise dot scatter every paint tick. The seed IS frame-derived: const seed = (frame * 9301 + keyChar * 49297) % 233280; But the inner RNG was: const s = (seed + n * 1103515245 + 12345) & 0x7fffffff; Here `seed` is at most 233279, while `n * 1103515245` dominates by a factor of ~10000x. Frame changes only moved the output by about 9301/2^31 ≈ 4 parts per million. Visually zero. The dots effectively froze into a function of `n` alone, which looked static. Fix: proper xorshift32 with the seed XOR'd into the state so frame changes actually propagate through the RNG: const seed = ((frame * 2654435769) ^ (keyChar * 374761393)) >>> 0; const prng = (n) => { let s = (seed ^ (n * 2246822519)) >>> 0; s ^= s << 13; s >>>= 0; s ^= s >>> 17; s ^= s << 5; s >>>= 0; return (s >>> 0) / 4294967296; }; Now every paint produces a genuinely different random sample per voice per dot, so the notation visibly shimmers as promised. ### 2. Drums now subtly harmonize to the note they're mapped to Previously, drumPitch only reflected the trackpad pitch shift. So the kick played on C3 and the kick played on C5 sounded identical — the octave dimension was ignored for percussion. User asked: "can the perc sounds all be subtley pitched according to what octave note they are mapped to... so all the perc harmonizes better". Implementation: factor in `semitones / 36` (1 semitone = 1/36 of an octave = a full keyboard octave maps to a minor third of drum pitch shift) on top of the trackpad pitch shift. Subtle enough that a kick still sounds like a kick, but noticeable enough that: - kick on c3 is clearly lower than kick on c5 - snare on d harmonizes a hair above kick on c - the cowbell bells shift tuning with octave without chipmunking Applied to all three drum trigger sites: - keyboard act() drum branch (existing `semitones` variable) - touch-tap drum pad (existing `semitones` variable) - drag-rollover drum pad (existing `semitones` variable) All three use the same formula: drumPitch = Math.pow(2, effectivePitchShift() + semitones / 36)