fix: piano roll click accuracy on zoom + Super Mario chiptune preset

- Fix mouse coordinate scaling in piano roll when canvas is zoomed
  (getBoundingClientRect returns visual size, now dividing by scale ratio)
- Replace Megaman melody with Super Mario Bros overworld theme
- Tune chiptune preset: faster BPM (200), snappier envelopes, brighter
  filter, less delay/distortion for cleaner NES sound

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-21 01:46:06 +01:00
parent 48d4a24c1b
commit 9d61adb064
2 changed files with 99 additions and 63 deletions

View File

@@ -10,44 +10,73 @@ const BLACK_KEYS = [1, 3, 6, 8, 10];
function midiToFreq(midi) { return 440 * Math.pow(2, (midi - 69) / 12); }
function isBlack(midi) { return BLACK_KEYS.includes(midi % 12); }
// Chiptune melody: Megaman-style theme in C minor
// Format: { note: MIDI, start: beats, duration: beats }
const MEGA_MELODY = [
// Bar 1: Opening riff
{ note: 72, start: 0, duration: 0.25 }, // C5
{ note: 72, start: 0.5, duration: 0.25 },
{ note: 75, start: 1, duration: 0.5 }, // Eb5
{ note: 72, start: 1.75, duration: 0.25 },
{ note: 70, start: 2, duration: 0.5 }, // Bb4
{ note: 67, start: 2.75, duration: 0.25 }, // G4
{ note: 65, start: 3, duration: 0.5 }, // F4
{ note: 63, start: 3.5, duration: 0.5 }, // Eb4
// Super Mario Bros - Overworld Theme (NES, 1985)
// BPM ~200, swing 8ths feel. Each beat = 1 quarter note.
// s = eighth note unit (0.5 beats)
const s = 0.5;
const MARIO_MELODY = [
// Bar 1: E5 E5 . E5 . C5 E5 .
{ note: 76, start: 0, duration: s }, // E5
{ note: 76, start: 1*s, duration: s }, // E5
// rest
{ note: 76, start: 3*s, duration: s }, // E5
// rest
{ note: 72, start: 5*s, duration: s }, // C5
{ note: 76, start: 6*s, duration: 2*s }, // E5
// Bar 2: Response phrase
{ note: 60, start: 4, duration: 0.5 }, // C4
{ note: 63, start: 4.5, duration: 0.25 }, // Eb4
{ note: 65, start: 5, duration: 0.25 }, // F4
{ note: 67, start: 5.5, duration: 0.75 }, // G4
{ note: 70, start: 6.5, duration: 0.5 }, // Bb4
{ note: 72, start: 7, duration: 1 }, // C5
// Bar 2: G5 . . . G4 . . .
{ note: 79, start: 8*s, duration: 2*s }, // G5
{ note: 67, start: 12*s, duration: 2*s }, // G4
// Bar 3: Climb up
{ note: 67, start: 8, duration: 0.25 }, // G4
{ note: 70, start: 8.5, duration: 0.25 }, // Bb4
{ note: 72, start: 9, duration: 0.25 }, // C5
{ note: 75, start: 9.5, duration: 0.5 }, // Eb5
{ note: 77, start: 10, duration: 0.5 }, // F5
{ note: 79, start: 10.5, duration: 0.5 }, // G5
{ note: 77, start: 11, duration: 0.25 }, // F5
{ note: 75, start: 11.5, duration: 0.5 }, // Eb5
// Bar 3: C5 . . G4 . . E4 . .
{ note: 72, start: 16*s, duration: s }, // C5
{ note: 67, start: 18*s, duration: s }, // G4
{ note: 64, start: 20*s, duration: s }, // E4
// Bar 4: Resolution
{ note: 72, start: 12, duration: 0.5 }, // C5
{ note: 70, start: 12.5, duration: 0.25 }, // Bb4
{ note: 67, start: 13, duration: 0.5 }, // G4
{ note: 63, start: 13.75, duration: 0.25 }, // Eb4
{ note: 60, start: 14, duration: 1 }, // C4
{ note: 60, start: 15.5, duration: 0.5 }, // C4 (pickup)
// Bar 4: . A4 . B4 . Bb4 A4 .
{ note: 69, start: 22*s, duration: s }, // A4
{ note: 71, start: 24*s, duration: s }, // B4
{ note: 70, start: 25*s, duration: s }, // Bb4
{ note: 69, start: 26*s, duration: s }, // A4
// Bar 5: G4 E5 G5 A5 . F5 G5 .
{ note: 67, start: 28*s, duration: 0.75 }, // G4 (triplet feel)
{ note: 76, start: 30*s, duration: 0.75 }, // E5
{ note: 79, start: 32*s, duration: s }, // G5
{ note: 81, start: 33*s, duration: s }, // A5
{ note: 77, start: 35*s, duration: s }, // F5
{ note: 79, start: 36*s, duration: s }, // G5
// Bar 6: . E5 . C5 D5 B4 . .
{ note: 76, start: 38*s, duration: s }, // E5
{ note: 72, start: 40*s, duration: s }, // C5
{ note: 74, start: 41*s, duration: s }, // D5
{ note: 71, start: 42*s, duration: s }, // B4
// Bar 7: . . C5 . . G4 . . E4
{ note: 72, start: 44*s, duration: s }, // C5
{ note: 67, start: 46*s, duration: s }, // G4
{ note: 64, start: 48*s, duration: s }, // E4
// Bar 8: . A4 . B4 . Bb4 A4 .
{ note: 69, start: 50*s, duration: s }, // A4
{ note: 71, start: 52*s, duration: s }, // B4
{ note: 70, start: 53*s, duration: s }, // Bb4
{ note: 69, start: 54*s, duration: s }, // A4
// Bar 9: G4 E5 G5 A5 . F5 G5 .
{ note: 67, start: 56*s, duration: 0.75 }, // G4
{ note: 76, start: 58*s, duration: 0.75 }, // E5
{ note: 79, start: 60*s, duration: s }, // G5
{ note: 81, start: 61*s, duration: s }, // A5
{ note: 77, start: 63*s, duration: s }, // F5
{ note: 79, start: 64*s, duration: s }, // G5
// Bar 10: . E5 . C5 D5 B4
{ note: 76, start: 66*s, duration: s }, // E5
{ note: 72, start: 68*s, duration: s }, // C5
{ note: 74, start: 69*s, duration: s }, // D5
{ note: 71, start: 70*s, duration: 2*s }, // B4
];
const ROLL_W = 500;
@@ -75,9 +104,9 @@ export default function PianoRollWidget({ moduleId }) {
// Init notes
if (!mod?.params?._notes) {
if (mod) mod.params._notes = [...MEGA_MELODY];
if (mod) mod.params._notes = [...MARIO_MELODY];
}
const notes = mod?.params?._notes || MEGA_MELODY;
const notes = mod?.params?._notes || MARIO_MELODY;
const notesRef = useRef(notes);
notesRef.current = notes;
@@ -258,8 +287,11 @@ export default function PianoRollWidget({ moduleId }) {
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
// Account for CSS transform scale (zoom) — rect is visual size, canvas is logical size
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
if (mx < KEY_W) return; // Clicked on piano keys
@@ -298,7 +330,7 @@ export default function PianoRollWidget({ moduleId }) {
const handleMove = (me) => {
if (!drawingRef.current) return;
const mmx = me.clientX - rect.left;
const mmx = (me.clientX - rect.left) * scaleX;
const endBeat = Math.max(drawingRef.current.start + 0.25,
Math.ceil(((mmx - KEY_W) / beatW) * 4) / 4);
drawingRef.current.duration = endBeat - drawingRef.current.start;