feat(notepat): space bar = instant-replay reverse loop pedal
User: "i was thinking space bar could play in reverse whatever I just
played... like holding space bar instantly starts playing the reverse
from when i pressed it... but i can still play other stuff on top of
course... so it basically plays the audio stream like instant replay
... and pressing space again right away would technically play the
reverse of the reverse, and in a loop... the length of holding space
to go backwards is a bit like setting the out point on the loop!"
Space is now an instant-replay loop pedal with bounce semantics.
MECHANICS
- Every note and drum hit gets appended to `playbackHistory` as
{ ts, kind, letter, octave, freq, vel, pan, wave, pitch }. Rolling
buffer: 256 events / 12 seconds max.
- On space key-down: startReversePlayback() snapshots the current
history and RESETS `playbackHistory` to empty — the new phase
begins now. The snapshot is translated into `reverseQueue`:
events sorted from most-recent to earliest, each with
`playAtMs = now + (latestTs - itsTs)`. The most recent original
event fires immediately, older events fire at their original
relative intervals.
- Each tick in sim() drains any `reverseQueue` entries whose
playAtMs has arrived. playReverseEvent() fires the voice via
sound.synth / playPercussion AND calls recordPlayback({...ev})
WITHOUT the original timestamp, so the reverse-fired event lands
in the new phase with its CURRENT timestamp. This is what makes
the loop work: the new phase captures events in the order they
reverse-played, and the next space press reverses THAT.
- On space key-up: stopReversePlayback() drains the queue. Whatever
got recorded during the press becomes the current phase. The
duration of the hold determines how much content the next loop
contains — "the length of holding space is a bit like setting the
out point on the loop."
- User-played notes DURING a reverse press layer on top normally
and also go into the new phase history, so layered riffs become
part of the next reverse.
FALLBACK: when history is empty (first press of the session), space
still plays the old kick-drum so it isn't silent.
HOOKED CALL SITES: recordPlayback is called from the keyboard drum
trigger path, keyboard melodic trigger path, touch drum pad tap
path, and drag-rollover drum path. Touch melodic paths aren't hooked
yet (they already flow through a different rememberSound shape) —
can follow up if needed. playReverseEvent is fed by the sim() tick
drain, not by setTimeout, so it doesn't rely on the polyfill and
timing resolution is one frame (~16 ms) which is plenty for percussive
replay.
BOUNCE EXAMPLE:
User taps A B C D across 500 ms → history = [A, B, C, D]
Press + hold space for ~500 ms → reverse plays D C B A; each gets
re-recorded. New history = [D, C, B, A] (new timestamps).
Release, press again → reverse plays A B C D. Bounce.
Loop indefinitely.