From 9d61adb06445e598ebe90901a6025548a720aa98 Mon Sep 17 00:00:00 2001 From: Jose Luis Date: Sat, 21 Mar 2026 01:46:06 +0100 Subject: [PATCH] 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 --- src/components/PianoRollWidget.jsx | 112 ++++++++++++++++++----------- src/presets/chiptune.js | 50 +++++++------ 2 files changed, 99 insertions(+), 63 deletions(-) diff --git a/src/components/PianoRollWidget.jsx b/src/components/PianoRollWidget.jsx index b275eb5..adebc94 100644 --- a/src/components/PianoRollWidget.jsx +++ b/src/components/PianoRollWidget.jsx @@ -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; diff --git a/src/presets/chiptune.js b/src/presets/chiptune.js index dd19c4b..7df0916 100644 --- a/src/presets/chiptune.js +++ b/src/presets/chiptune.js @@ -1,35 +1,40 @@ /** - * Chiptune Demo Preset + * Super Mario Bros Chiptune Demo Preset * * Signal flow: - * Piano Roll → [freq] Osc1 (square, melody) → VCA1 ← Envelope1 (short plucky) - * Piano Roll → [freq] Osc2 (square, -12 oct sub bass) → VCA2 ← Envelope2 (bass sustain) - * LFO (vibrato) → Osc1 detune - * VCA1 + VCA2 → Mixer → Filter (lowpass, slight resonance) → Delay → Distortion (light) → Output - * Mixer → Scope (visualization) + * Piano Roll → [freq] Osc1 (square, melody) → VCA1 ← Envelope1 (short staccato) + * Piano Roll → [freq] Osc2 (square, sub octave) → VCA2 ← Envelope2 (bass body) + * LFO (subtle vibrato) → Osc1 detune + * VCA1 + VCA2 → Mixer → Filter (bright lowpass) → Delay (subtle echo) → Output + * Mixer → Scope * - * Layout: left-to-right signal flow, neatly arranged + * Tuned for NES-style square wave chiptune at ~200 BPM */ export const CHIPTUNE_PRESET = { modules: [ - // Row 1: Piano Roll & Sources - { id: 1, type: 'pianoroll', x: 40, y: 40, params: { bpm: 140, loop: 'on', bars: '4' } }, + // Source: Piano Roll with Mario melody + { id: 1, type: 'pianoroll', x: 40, y: 40, params: { bpm: 200, loop: 'on', bars: '8' } }, + + // Oscillators: square waves for authentic NES sound { id: 2, type: 'oscillator', x: 600, y: 20, params: { waveform: 'square', frequency: 440, detune: 0 } }, { id: 3, type: 'oscillator', x: 600, y: 260, params: { waveform: 'square', frequency: 220, detune: 0 } }, - { id: 4, type: 'lfo', x: 40, y: 380, params: { waveform: 'sine', frequency: 5.5, amplitude: 0.3 } }, - // Row 2: Envelopes & VCAs - { id: 5, type: 'envelope', x: 820, y: 20, params: { attack: 0.005, decay: 0.15, sustain: 0.2, release: 0.1 } }, - { id: 6, type: 'envelope', x: 820, y: 240, params: { attack: 0.005, decay: 0.3, sustain: 0.6, release: 0.2 } }, + // Subtle vibrato LFO + { id: 4, type: 'lfo', x: 40, y: 380, params: { waveform: 'sine', frequency: 6, amplitude: 0.15 } }, + + // Envelopes: short and snappy for chiptune feel + { id: 5, type: 'envelope', x: 820, y: 20, params: { attack: 0.001, decay: 0.08, sustain: 0.6, release: 0.05 } }, + { id: 6, type: 'envelope', x: 820, y: 240, params: { attack: 0.001, decay: 0.1, sustain: 0.4, release: 0.08 } }, + + // VCAs { id: 7, type: 'vca', x: 1020, y: 20, params: { gain: 0.7 } }, - { id: 8, type: 'vca', x: 1020, y: 220, params: { gain: 0.5 } }, + { id: 8, type: 'vca', x: 1020, y: 220, params: { gain: 0.35 } }, - // Row 3: Mixer, processing, output - { id: 9, type: 'mixer', x: 1220, y: 60, params: { gain1: 0.8, gain2: 0.6, gain3: 0.0, gain4: 0.0 } }, - { id: 10, type: 'filter', x: 1420, y: 40, params: { type: 'lowpass', frequency: 3500, Q: 2.5 } }, - { id: 11, type: 'delay', x: 1620, y: 40, params: { delayTime: 0.18, feedback: 0.35, wet: 0.25 } }, - { id: 12, type: 'distortion', x: 1620, y: 280, params: { distortion: 0.15, wet: 0.3 } }, + // Mixer, processing, output + { id: 9, type: 'mixer', x: 1220, y: 60, params: { gain1: 0.8, gain2: 0.5, gain3: 0.0, gain4: 0.0 } }, + { id: 10, type: 'filter', x: 1420, y: 40, params: { type: 'lowpass', frequency: 6000, Q: 1.5 } }, + { id: 11, type: 'delay', x: 1620, y: 40, params: { delayTime: 0.15, feedback: 0.2, wet: 0.15 } }, { id: 13, type: 'output', x: 1840, y: 120, params: { volume: -8 } }, // Scope @@ -59,12 +64,11 @@ export const CHIPTUNE_PRESET = { { id: 10, from: { moduleId: 7, port: 'out' }, to: { moduleId: 9, port: 'in1' } }, { id: 11, from: { moduleId: 8, port: 'out' }, to: { moduleId: 9, port: 'in2' } }, - // Mixer → Filter → Delay → Distortion → Output + // Mixer → Filter → Delay → Output { id: 12, from: { moduleId: 9, port: 'out' }, to: { moduleId: 10, port: 'in' } }, { id: 13, from: { moduleId: 10, port: 'out' }, to: { moduleId: 11, port: 'in' } }, - { id: 14, from: { moduleId: 11, port: 'out' }, to: { moduleId: 12, port: 'in' } }, - { id: 15, from: { moduleId: 12, port: 'out' }, to: { moduleId: 13, port: 'left' } }, - { id: 16, from: { moduleId: 12, port: 'out' }, to: { moduleId: 13, port: 'right' } }, + { id: 15, from: { moduleId: 11, port: 'out' }, to: { moduleId: 13, port: 'left' } }, + { id: 16, from: { moduleId: 11, port: 'out' }, to: { moduleId: 13, port: 'right' } }, // Mixer → Scope { id: 17, from: { moduleId: 9, port: 'out' }, to: { moduleId: 14, port: 'in' } },