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:
@@ -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;
|
||||
|
||||
@@ -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' } },
|
||||
|
||||
Reference in New Issue
Block a user