@@ -160,32 +160,13 @@ fn sdTriangle(p: vec2<f32>, r: f32) -> f32 {
160160 return - length (p2 ) * sign (p2 . y );
161161}
162162
163- fn toUpperAscii (code : u32 ) -> u32 {
164- return select (code , code - 32u , (code >= 97u ) & (code <= 122u ));
165- }
166-
163+ // Correct pitch class from raw OpenMPT note value (1–120).
164+ // OpenMPT: note 1 = C-0, note 2 = C#0 … note 12 = B-0, note 13 = C-1, …
165+ // (note - 1) % 12 → 0=C, 1=C#, 2=D, 3=D#, 4=E, 5=F, 6=F#, 7=G, 8=G#, 9=A, 10=A#, 11=B
167166fn pitchClassFromPacked (packed : u32 ) -> f32 {
168- let c0 = toUpperAscii ((packed >> 24 ) & 255u );
169- var semitone : i32 = 0 ;
170- var valid = true ;
171- switch (c0 ) {
172- case 65u : { semitone = 9 ; }
173- case 66u : { semitone = 11 ; }
174- case 67u : { semitone = 0 ; }
175- case 68u : { semitone = 2 ; }
176- case 69u : { semitone = 4 ; }
177- case 70u : { semitone = 5 ; }
178- case 71u : { semitone = 7 ; }
179- default : { valid = false ; }
180- }
181- if (! valid ) { return 0 .0 ; }
182- let c1 = toUpperAscii ((packed >> 16 ) & 255u );
183- if ((c1 == 35u ) || (c1 == 43u )) {
184- semitone = (semitone + 1 ) % 12 ;
185- } else if (c1 == 66u ) {
186- semitone = (semitone + 11 ) % 12 ;
187- }
188- return f32 (semitone ) / 12 .0 ;
167+ let note = (packed >> 24 ) & 255u ;
168+ if (note == 0u || note > 120u ) { return 0 .0 ; }
169+ return f32 ((note - 1u ) % 12u ) / 12 .0 ;
189170}
190171
191172@fragment
@@ -233,71 +214,114 @@ fn fs(in: VertexOut) -> @location(0) vec4<f32> {
233214 return vec4 <f32 >(col * dimFactor , 1 .0 );
234215 }
235216
236- // Frosted Cap Shape
217+ // Frosted Cap Shape — use for alpha masking, not early discard
237218 let dBox = sdRoundedBox (uv - 0 .5 , vec2 <f32 >(0 .42 ), 0 .1 );
238219 let isCap = dBox < 0 .0 ;
239-
240- if (! isCap ) {
241- discard ;
242- }
243-
220+ let capEdge = smoothstep (0 .0 , 0 .08 , - dBox );
221+
244222 var capColor = vec3 <f32 >(0 .15 , 0 .16 , 0 .18 ); // Inactive plastic
245223 var glow = 0 .0 ;
246-
247- let noteChar = (in . packedA >> 24 ) & 255u ;
248- let hasNote = (noteChar >= 65u && noteChar <= 122u ); // Simple check
249-
250- let playheadStep = uniforms . playheadRow - floor (uniforms . playheadRow / 64 .0 ) * 64 .0 ;
251- let rowDistRaw = abs (f32 (in . row % 64u ) - playheadStep );
252- let rowDist = min (rowDistRaw , 64 .0 - rowDistRaw );
224+ let p = uv - 0 .5 ;
225+
226+ // Unpack all fields (OpenMPT numeric encoding)
227+ let note = (in . packedA >> 24 ) & 255u ;
228+ let volCmd = (in . packedA >> 8 ) & 255u ;
229+ let effCmd = (in . packedB >> 8 ) & 255u ;
230+
231+ let hasNote = (note > 0u ) && (note <= 120u ); // regular note
232+ let isNoteOff = (note == 121u ); // --- note-off
233+ let isNoteCut = (note == 122u ) || (note == 123u ); // === note-cut/fade
234+ let hasVol = (volCmd > 0u );
235+ let hasEffect = (effCmd > 0u );
236+
237+ let playheadStep = uniforms . playheadRow - floor (uniforms . playheadRow / 64 .0 ) * 64 .0 ;
238+ let rowDistRaw = abs (f32 (in . row % 64u ) - playheadStep );
239+ let rowDist = min (rowDistRaw , 64 .0 - rowDistRaw );
253240 let playheadActivation = 1 .0 - smoothstep (0 .0 , 1 .5 , rowDist );
241+ let onPlayhead = rowDist < 1 .5 ;
242+
243+ // Channel live state
244+ var chTrigger = false ;
245+ if (in . channel < arrayLength (& channels )) {
246+ let ch = channels [in . channel ];
247+ chTrigger = (ch . trigger > 0u ) && onPlayhead ;
248+ }
249+
254250 if (hasNote ) {
255251 let pitchHue = pitchClassFromPacked (in . packedA );
256- let baseCol = neonPalette (pitchHue );
257- capColor = mix (capColor , baseCol , 0 .4 );
258-
259- // Highlight if active row
252+ let baseCol = neonPalette (pitchHue );
253+ capColor = mix (capColor , baseCol * 0 .55 , 0 .5 );
254+
260255 if (playheadActivation > 0 .0 ) {
261- glow = playheadActivation ;
262- capColor = mix (capColor , vec3 < f32 >( 1 .0 ), 0 .5 );
256+ glow = playheadActivation ;
257+ capColor = mix (capColor , baseCol , playheadActivation * 0 .6 );
263258 }
264-
265- // Trigger flash
266- let ch = channels [in . channel ];
267- if (ch . trigger > 0u && playheadActivation > 0 .5 ) {
268- glow += 1 .0 ;
269- capColor += vec3 <f32 >(0 .5 );
259+ if (chTrigger ) {
260+ glow += 1 .0 ;
261+ capColor = mix (capColor , baseCol * 1 .4 + 0 .3 , 0 .5 );
270262 }
263+
264+ // ── Blue note-on indicator (top-left micro-LED) ──────────────
265+ let ledNoteOn = vec2 <f32 >(- 0 .30 , - 0 .30 );
266+ let dLedOn = length (p - ledNoteOn ) - 0 .07 ;
267+ if (dLedOn < 0 .0 ) {
268+ let ledCol = select (
269+ vec3 <f32 >(0 .05 , 0 .10 , 0 .22 ),
270+ vec3 <f32 >(0 .25 , 0 .55 , 1 .00 ),
271+ chTrigger || (playheadActivation > 0 .5 )
272+ );
273+ return vec4 <f32 >(ledCol * dimFactor , 1 .0 );
274+ }
275+ } else if (isNoteOff ) {
276+ capColor = mix (capColor , vec3 <f32 >(0 .45 , 0 .05 , 0 .05 ), 0 .55 );
277+ } else if (isNoteCut ) {
278+ capColor = mix (capColor , vec3 <f32 >(0 .60 , 0 .20 , 0 .02 ), 0 .45 );
271279 }
272-
273- // Playhead Highlight Line
280+
281+ // ── Amber volume indicator (top-right micro-LED) ─────────────────
282+ let ledVol = vec2 <f32 >(0 .30 , - 0 .30 );
283+ let dLedVol = length (p - ledVol ) - 0 .07 ;
284+ if (dLedVol < 0 .0 ) {
285+ let ledCol = select (
286+ vec3 <f32 >(0 .18 , 0 .12 , 0 .02 ),
287+ vec3 <f32 >(1 .00 , 0 .65 , 0 .10 ),
288+ hasVol
289+ );
290+ let ledGlow = select (0 .0 , bloom * 0 .8 , hasVol && onPlayhead );
291+ return vec4 <f32 >((ledCol + ledGlow ) * dimFactor , 1 .0 );
292+ }
293+
294+ // ── Effect indicator (bottom-centre micro-LED) ────────────────────
295+ let ledEff = vec2 <f32 >(0 .0 , 0 .30 );
296+ let dLedEff = length (p - ledEff ) - 0 .06 ;
297+ if (dLedEff < 0 .0 ) {
298+ let ledCol = select (
299+ vec3 <f32 >(0 .08 , 0 .12 , 0 .08 ),
300+ vec3 <f32 >(0 .30 , 0 .95 , 0 .40 ),
301+ hasEffect
302+ );
303+ return vec4 <f32 >(ledCol * dimFactor , 1 .0 );
304+ }
305+
306+ // Playhead highlight
274307 if (playheadActivation > 0 .0 ) {
275- capColor += vec3 <f32 >(0 .1 , 0 .1 , 0 .15 ) * playheadActivation ;
276- if (isPlaying && playheadActivation > 0 .5 ) {
277- glow += 0 .2 ;
278- }
308+ capColor += vec3 <f32 >(0 .08 , 0 .08 , 0 .12 ) * playheadActivation ;
309+ if (isPlaying && playheadActivation > 0 .5 ) { glow += 0 .2 ; }
279310 }
280-
281- // Frosted Effect
282- let edge = smoothstep (0 .0 , 0 .1 , - dBox );
283- let light = vec3 <f32 >(0 .5 , - 0 .8 , 1 .0 );
284- let n = normalize (vec3 <f32 >((uv . x - 0 .5 ), (uv . y - 0 .5 ), 0 .5 ));
285- let diff = max (0 .0 , dot (n , normalize (light )));
286-
311+
312+ // Frosted surface shading
313+ let edge = smoothstep (0 .0 , 0 .1 , - dBox );
314+ let nrm = normalize (vec3 <f32 >(p . x , p . y , 0 .5 ));
315+ let light = normalize (vec3 <f32 >(0 .5 , - 0 .8 , 1 .0 ));
316+ let diff = max (0 .0 , dot (nrm , light ));
287317 capColor *= (0 .5 + 0 .5 * diff );
288318 capColor += vec3 <f32 >(glow * 0 .5 );
289-
290- // Bloom boost
291- if (glow > 0 .0 ) {
292- capColor *= (1 .0 + bloom );
293- }
319+ if (glow > 0 .0 ) { capColor *= (1 .0 + bloom ); }
294320
295321 // Kick reactive glow
296- let p = in . uv - 0 .5 ;
297322 let kickPulse = uniforms . kickTrigger * exp (- length (p ) * 3 .0 ) * 0 .3 ;
298323 capColor += vec3 <f32 >(0 .9 , 0 .2 , 0 .4 ) * kickPulse * uniforms . bloomIntensity ;
299- // Dithering for night mode
300- let noise = fract (sin (dot (in . uv * uniforms . timeSec , vec2 <f32 >(12 .9898 , 78 .233 ))) * 43758 .5453 );
324+ let noise = fract (sin (dot (uv * uniforms . timeSec , vec2 <f32 >(12 .9898 , 78 .233 ))) * 43758 .5453 );
301325 capColor += (noise - 0 .5 ) * 0 .01 ;
302326
303327 return vec4 <f32 >(capColor * dimFactor , edge );
0 commit comments