From 7132b0918c974dcc22b7cf6ff082abf98dcc7d0e Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sat, 11 Apr 2026 16:26:37 -0700 Subject: [PATCH] fix(perc): animate drum pad notation + subtle note-based pitch harmony MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- fedac/native/pieces/notepat.mjs | 38 +++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/fedac/native/pieces/notepat.mjs b/fedac/native/pieces/notepat.mjs index 9ceb58d856..c3bf459d0a 100644 --- a/fedac/native/pieces/notepat.mjs +++ b/fedac/native/pieces/notepat.mjs @@ -1857,7 +1857,13 @@ function act({ event: e, sound, wifi, system }) { const accent = shiftHeld ? 1.5 : 1.0; const baseDrumVol = (1.0 + velocity * 0.8) * accent; const drumVol = baseDrumVol * master; - const drumPitch = Math.pow(2, effectivePitchShift()); + // Subtle note-based pitch harmonization: each semitone nudges the + // drum's fundamental by 1/36 of an octave (one full keyboard + // octave = a minor third of pitch shift). A kick on C3 sounds + // noticeably lower than a kick on C5, and a snare on D harmonizes + // just a hair above a kick on C. Combined with the trackpad + // pitch shift via effectivePitchShift(). + const drumPitch = Math.pow(2, effectivePitchShift() + semitones / 36); // Drums get their own pan (kit geometry + grid bias), not the // QWERTY physical key position → pan. Left keys pan left, right pan right. const drumPan = drumPanFor(letter, offset, key); @@ -2179,7 +2185,9 @@ function act({ event: e, sound, wifi, system }) { // Percussion pad tap: fire drum (or drum sample) and flash trail. const touchDrum = percussionDrumFor(hitNote.letter, hitNote.gridOffset); if (touchDrum) { - const touchDrumPitch = Math.pow(2, effectivePitchShift()); + // Subtle note-based pitch harmonization — see keyboard drum + // path for rationale. Each semitone = 1/36 octave of drum tuning. + const touchDrumPitch = Math.pow(2, effectivePitchShift() + semitones / 36); // QWERTY physical key position → pan (matches where finger lands). const touchDrumPan = drumPanFor(hitNote.letter, hitNote.gridOffset, hitNote.key); const touchBaseVol = 1.8; @@ -2306,7 +2314,8 @@ function act({ event: e, sound, wifi, system }) { // Drag-to-drum: drum pads fire as one-shots on rollover too. const rollDrum = percussionDrumFor(hitNote.letter, hitNote.gridOffset); if (rollDrum) { - const rollDrumPitch = Math.pow(2, effectivePitchShift()); + // Subtle note-based pitch harmonization (see keyboard path). + const rollDrumPitch = Math.pow(2, effectivePitchShift() + semitones / 36); // QWERTY physical key position → pan (matches where finger lands). const rollDrumPan = drumPanFor(hitNote.letter, hitNote.gridOffset, hitNote.key); const rollBaseVol = 1.8; @@ -3479,13 +3488,24 @@ function paint({ wipe, ink, box, line, write, screen, sound, system, trackpad, p const plotH = pyMax - pyMin; const logLo = Math.log(30); const logHi = Math.log(10000); - // Use a key-stable random seed so the jitter across pads - // differs without being globally synchronized. - const seed = (frame * 9301 + (key ? key.charCodeAt(0) : 0) * 49297) % 233280; + // Per-pad PRNG — produces a visibly different scatter each + // frame. Previous impl used `seed + n * 1103515245`, but + // seed was only 0..233279 while the n term dominated by a + // factor of ~10k, so frame changes only moved the output + // by a few parts per million — looked static. + // + // New impl: xorshift32 with seed XORed into the state on + // every call. `seed` now uses the raw frame + key char so + // adjacent frames land in completely different LCG + // orbits. Pads offset by key char so they don't flicker + // in sync. + const seed = ((frame * 2654435769) ^ ((key ? key.charCodeAt(0) : 0) * 374761393)) >>> 0; const prng = (n) => { - // Tiny LCG derived from seed + n - const s = (seed + n * 1103515245 + 12345) & 0x7fffffff; - return (s / 2147483648); + let s = (seed ^ (n * 2246822519)) >>> 0; + s ^= s << 13; s >>>= 0; + s ^= s >>> 17; + s ^= s << 5; s >>>= 0; + return (s >>> 0) / 4294967296; }; for (let i = 0; i < sig.length; i++) { const [t, f, vol] = sig[i]; -- 2.54.0