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 <noreply@anthropic.com>
This commit is contained in:
vadimwit
2026-06-16 11:00:43 +01:00
parent f98528733d
commit 4727645bb9
+9 -1
View File
@@ -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)))]