Follow-up from the shader playback audit (PR #268). The WebGL2 hybrid overlay and the
reference shader patternv0.50.wgsl apply octaveBrightness(note): the same pitch class shares
hue across octaves but brightens in higher octaves (0.65 at C-0 → 1.0 at B-9). The older LED
shaders patternv0.46/0.47/0.48 still color by pitch class only, so C-3 and C-5 render
identically and sustain tails don't inherit the octave-scaled tint. Active focus: WGSL
circular/horizontal LED note-color readability for a PUBLIC audience.
This is a VISUAL-ONLY back-port. It must reach exact parity with patternv0.50.wgsl's existing
behavior — same curve, same application points — not introduce a new look.
Design decision (read before implementing)
Keep the brightness math IN-SHADER (WGSL), matching v0.50. Do NOT move it to a CPU-side
color/pack encoding: this project packs the raw note byte ([Note|Instr|VolCmd|VolVal]), not
RGB, so CPU-side baking would require changing the packed format (forbidden here) and would
diverge from v0.50, defeating the parity goal. Do NOT retune the floor/gamma in this issue —
strict parity means copying v0.50's curve verbatim. (A separate follow-up can re-tune the
shared curve across the whole shader family, v0.50 included.)
Approach
- Copy the octaveBrightness helper VERBATIM from patternv0.50.wgsl into v0.46/0.47/0.48.
Do not retype the constants — read v0.50's actual implementation and match it exactly,
including its NOTE_MAX/note-limit constant name and value. Reference shape:
fn octaveBrightness(note: u32) -> f32 {
if (note == 0u || note > NOTE_MAX) { return 1.0; }
let oct = (note - 1u) / 12u; // 0..9
return 0.65 + 0.35 * f32(oct) / 9.0;
}
If a same-named helper already exists in v0.47/v0.48 under any name, reuse it instead of
adding a duplicate (compile error on redefinition).
- Multiply the helper into the VALUE/brightness of the note color, at every site where a
PITCHED note color is produced — including the sustain-tail path — BEFORE bloom/ACES.
- If the shader uses an HSV helper: keep hue/sat, scale value:
hsv2rgb(vec3(hue, sat, base_v * octaveBrightness(note)))
- If it produces RGB directly: base_color * octaveBrightness(note)
Apply pre-tone-map (where v0.50 applies it). Do not apply it after ACES.
- Sustain tails: tint = noteColor(last_note) * octaveBrightness(last_note) * SUSTAIN_DIM.
The tail must use the SUSTAINING note's integer (last_note), dimmed but NOT re-hued. Match
v0.50's existing SUSTAIN_DIM factor exactly; do not invent a per-shader value.
- Run
npm run sync:shaders to regenerate public/shaders/ copies, then npm run typecheck
and npm run build. Commit the regenerated public/shaders/ files.
Hard constraints (gating — must hold)
- octaveBrightness is GATED strictly behind "this cell is pitched note data". It must NEVER
scale expression/effect/amber colors (volCmd/effCmd paths) or empty cells. Applying it to a
non-note column would dim those columns by whatever integer sits in the note slot.
- Note-Off / note-cut codes must hit the early-return guard (return 1.0) and must NOT render as
a pitched, octave-scaled LED. Confirm the note-off integer each format uses is > NOTE_MAX (or
is already handled by the existing note-off branch before color is computed).
- No change to GPU packing, struct layout, or uniform bindings. This is visual-only.
Acceptance criteria
Verification
Static (no GPU): add/keep a tiny node check asserting the helper math:
- note 1 (C-0): oct 0 → 0.650
- note 13 (C-1): oct 1 → ~0.689
- note 109 (C-9): oct 9 → 1.000
Visual: load a module spanning low→high registers (e.g. 4-mat_-_space_debris.mod), select each
of v0.46/0.47/0.48, and confirm (a) octave brightness gradient on active notes, (b) sustain
tails carry the dimmed pitch tint, (c) effect-only/amber rows look identical to before.
Cross-check the same module/notes against v0.50 for parity.
Out of scope (do NOT do here)
- Raising the 0.65 floor or adding a perceptual gamma curve (separate cross-shader issue).
- Adding v0.49 or any other shader.
- Any TypeScript, pack-step, renderer, uniform-layout, or deploy changes.
Open questions to resolve during implementation
- Confirm v0.50's exact NOTE_MAX constant name + value (96 vs 119) and copy it; do not guess.
- Confirm the sustain-tail path in v0.46/0.47/0.48 already has access to the sustaining note's
integer (last_note). The DURA/high-precision packing copies the note into tail cells — verify
that's true for these three shaders. If a tail row does NOT carry last_note, STOP and flag it:
octave-scaling the tail would otherwise require a packing change, which is out of scope.
- Confirm v0.47/v0.48 don't already define a brightness helper under a different name.
Related
Follow-up from the shader playback audit (PR #268). The WebGL2 hybrid overlay and the
reference shader patternv0.50.wgsl apply octaveBrightness(note): the same pitch class shares
hue across octaves but brightens in higher octaves (0.65 at C-0 → 1.0 at B-9). The older LED
shaders patternv0.46/0.47/0.48 still color by pitch class only, so C-3 and C-5 render
identically and sustain tails don't inherit the octave-scaled tint. Active focus: WGSL
circular/horizontal LED note-color readability for a PUBLIC audience.
This is a VISUAL-ONLY back-port. It must reach exact parity with patternv0.50.wgsl's existing
behavior — same curve, same application points — not introduce a new look.
Design decision (read before implementing)
Keep the brightness math IN-SHADER (WGSL), matching v0.50. Do NOT move it to a CPU-side
color/pack encoding: this project packs the raw note byte ([Note|Instr|VolCmd|VolVal]), not
RGB, so CPU-side baking would require changing the packed format (forbidden here) and would
diverge from v0.50, defeating the parity goal. Do NOT retune the floor/gamma in this issue —
strict parity means copying v0.50's curve verbatim. (A separate follow-up can re-tune the
shared curve across the whole shader family, v0.50 included.)
Approach
Do not retype the constants — read v0.50's actual implementation and match it exactly,
including its NOTE_MAX/note-limit constant name and value. Reference shape:
fn octaveBrightness(note: u32) -> f32 {
if (note == 0u || note > NOTE_MAX) { return 1.0; }
let oct = (note - 1u) / 12u; // 0..9
return 0.65 + 0.35 * f32(oct) / 9.0;
}
If a same-named helper already exists in v0.47/v0.48 under any name, reuse it instead of
adding a duplicate (compile error on redefinition).
PITCHED note color is produced — including the sustain-tail path — BEFORE bloom/ACES.
hsv2rgb(vec3(hue, sat, base_v * octaveBrightness(note)))
Apply pre-tone-map (where v0.50 applies it). Do not apply it after ACES.
The tail must use the SUSTAINING note's integer (last_note), dimmed but NOT re-hued. Match
v0.50's existing SUSTAIN_DIM factor exactly; do not invent a per-shader value.
npm run sync:shadersto regenerate public/shaders/ copies, thennpm run typecheckand
npm run build. Commit the regenerated public/shaders/ files.Hard constraints (gating — must hold)
scale expression/effect/amber colors (volCmd/effCmd paths) or empty cells. Applying it to a
non-note column would dim those columns by whatever integer sits in the note slot.
a pitched, octave-scaled LED. Confirm the note-off integer each format uses is > NOTE_MAX (or
is already handled by the existing note-off branch before color is computed).
Acceptance criteria
npm run typecheckandnpm run buildgreen.npm run sync:shadersand committed.Verification
Static (no GPU): add/keep a tiny node check asserting the helper math:
Visual: load a module spanning low→high registers (e.g. 4-mat_-_space_debris.mod), select each
of v0.46/0.47/0.48, and confirm (a) octave brightness gradient on active notes, (b) sustain
tails carry the dimmed pitch tint, (c) effect-only/amber rows look identical to before.
Cross-check the same module/notes against v0.50 for parity.
Out of scope (do NOT do here)
Open questions to resolve during implementation
integer (last_note). The DURA/high-precision packing copies the note into tail cells — verify
that's true for these three shaders. If a tail row does NOT carry last_note, STOP and flag it:
octave-scaling the tail would otherwise require a packing change, which is out of scope.
Related