From 4727645bb9d23c23ea182fa9cda39cfbf34c1dca Mon Sep 17 00:00:00 2001 From: vadimwit Date: Tue, 16 Jun 2026 11:00:43 +0100 Subject: [PATCH] fix(piano): dedupe duplicate absolute notes in pianoVoicing (task L-13) A forced rootless sus2 collided the suspended-2nd and synthesized 9th onto one key (notes [26,26,31]). Dedupe at the single funnel point ([...new Set(placed)]); pcs/bass derived from the deduped array. No-op on default/chain path (L-11 render unaffected). Critic: Set merges only identical absolute integers (same key) so no needed note is lost. Co-Authored-By: Claude Fable 5 --- src/lib/piano.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/lib/piano.js b/src/lib/piano.js index 172c2c4..ef39bef 100644 --- a/src/lib/piano.js +++ b/src/lib/piano.js @@ -339,7 +339,15 @@ export function pianoVoicing(chord, opts = {}) { const style = BUILDERS[opts.style] ? opts.style : defaultStyle(quality) const base = BUILDERS[style](rootPc, quality) - const notes = placeVoicing(base, rootPc, opts.prev) + const placed = placeVoicing(base, rootPc, opts.prev) + + // Dedupe ABSOLUTE notes: two voices can collide on one key (e.g. a forced + // rootless sus2 lands the suspended-2nd "3rd-slot" tone and the synthesized + // 9th on the same absolute key, pc 2 → notes [26,26,31]). A duplicate value + // would stack two highlights on one key in the renderer, so we drop the + // redundant collided note here — the single point every style/quality flows + // through — keeping the first occurrence. A no-op when nothing collides. + const notes = [...new Set(placed)] // pcs in the voiced order (low→high), deduped — what keys light up. const pcs = [...new Set(notes.map((n) => mod12(n)))]