Holding Erase across the loop boundary deletes roughly a whole bar of the held pad's notes instead of the few ticks under the playhead — and leaves the notes actually being played untouched.
The defect
collectErase in src/core/sequencer/schedulerCore.ts:
const seqFrom = sequenceTickAt(from, this.loop);
const seqTo = sequenceTickAt(to, this.loop);
const lo = Math.min(seqFrom, seqTo);
const hi = Math.max(seqFrom, seqTo);
...
if (event.tickStart >= lo && event.tickStart < hi) ids.push(event.id);
from and to are linear ticks; sequenceTickAt folds them onto the loop. When the lookahead window straddles the loop end, seqTo < seqFrom, and Math.min/Math.max silently yield the complement of the intended window.
Concrete failure
One-bar loop [0, 3840) at 120 bpm, window from = 3800, to = 3900:
seqFrom = 3800, seqTo = 3900 % 3840 = 60
lo = 60, hi = 3800
- Every event of the held pad in ticks 60…3799 is erased in a single scheduler wake
- The ticks genuinely being played —
[3800, 3840) and [0, 60) — are not erased
So the feature does close to the opposite of its purpose at the wrap point, and the erasure is committed to the store via result.erased, making it persistent, user-visible data loss during a live take.
Why the tests miss it
schedulerCore.test.ts has one live-erase test, which runs a 1.0-second step against a 2-second loop, so the window never wraps.
Suggested fix
The correct primitive is already used one line above in scheduleSequence:
for (const windowed of eventsInWindow(track.events, (e) => e.tickStart, from, to, this.loop)) {
collectErase should iterate the same wrap-aware window rather than folding and taking min/max. Add a regression test with a window that straddles the loop end.
Spec breached
§7.7 — "While holding a pad + touching Erase, that pad's events are removed as the loop passes".
Holding Erase across the loop boundary deletes roughly a whole bar of the held pad's notes instead of the few ticks under the playhead — and leaves the notes actually being played untouched.
The defect
collectEraseinsrc/core/sequencer/schedulerCore.ts:fromandtoare linear ticks;sequenceTickAtfolds them onto the loop. When the lookahead window straddles the loop end,seqTo < seqFrom, andMath.min/Math.maxsilently yield the complement of the intended window.Concrete failure
One-bar loop
[0, 3840)at 120 bpm, windowfrom = 3800,to = 3900:seqFrom = 3800,seqTo = 3900 % 3840 = 60lo = 60,hi = 3800[3800, 3840)and[0, 60)— are not erasedSo the feature does close to the opposite of its purpose at the wrap point, and the erasure is committed to the store via
result.erased, making it persistent, user-visible data loss during a live take.Why the tests miss it
schedulerCore.test.tshas one live-erase test, which runs a 1.0-second step against a 2-second loop, so the window never wraps.Suggested fix
The correct primitive is already used one line above in
scheduleSequence:collectEraseshould iterate the same wrap-aware window rather than folding and taking min/max. Add a regression test with a window that straddles the loop end.Spec breached
§7.7 — "While holding a pad + touching Erase, that pad's events are removed as the loop passes".