fix: wire positions on zoom, MIDI import, envelope release min

- Fix wires not recalculating positions on zoom until panning
- Add MIDI file import button to Piano Roll (parses .mid files)
- Allow envelope release to go to 0 (was clamped at 0.001)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-21 01:38:12 +01:00
parent 65a89e2b59
commit 48d4a24c1b
5 changed files with 175 additions and 3 deletions

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react';
import * as Tone from 'tone';
import { state, updateModuleParam, emit } from '../engine/state.js';
import { setSequencerSignals } from '../engine/audioEngine.js';
import { parseMidi } from '../utils/midiParser.js';
const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const BLACK_KEYS = [1, 3, 6, 8, 10];
@@ -65,6 +66,7 @@ export default function PianoRollWidget({ moduleId }) {
const [tool, setTool] = useState('draw'); // 'draw' | 'erase'
const drawingRef = useRef(null);
const rafRef = useRef(null);
const midiInputRef = useRef(null);
const bpm = mod?.params?.bpm ?? 140;
const bars = parseInt(mod?.params?.bars || '4');
@@ -321,6 +323,37 @@ export default function PianoRollWidget({ moduleId }) {
}
}, [tool, notes, beatW, totalBeats, mod]);
const handleMidiImport = useCallback(async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const arrayBuffer = await file.arrayBuffer();
const midi = parseMidi(arrayBuffer);
if (midi.notes.length === 0) return;
// Update BPM if detected
if (midi.bpm && mod) {
mod.params.bpm = midi.bpm;
}
// Auto-fit bars to cover all notes
const maxBeat = Math.max(...midi.notes.map(n => n.start + n.duration));
const neededBars = Math.ceil(maxBeat / 4);
const fitBars = [1, 2, 4, 8].find(b => b >= neededBars) || 8;
if (mod) mod.params.bars = String(fitBars);
// Set notes
if (mod) {
mod.params._notes = midi.notes;
notesRef.current = midi.notes;
emit();
}
} catch (err) {
console.error('[PianoRoll] MIDI import failed:', err);
}
e.target.value = '';
}, [mod]);
return (
<div style={{ width: ROLL_W }}>
{/* Mini toolbar */}
@@ -345,6 +378,15 @@ export default function PianoRollWidget({ moduleId }) {
}}
onClick={() => setTool('erase')}
> Erase</button>
<button
style={{
padding: '1px 6px', fontSize: 9, border: '1px solid #333',
background: '#111', color: '#888',
borderRadius: 3, cursor: 'pointer', fontFamily: 'inherit',
}}
onClick={() => midiInputRef.current?.click()}
>🎵 MIDI</button>
<input ref={midiInputRef} type="file" accept=".mid,.midi" style={{ display: 'none' }} onChange={handleMidiImport} />
<span style={{ fontSize: 8, color: '#555', marginLeft: 'auto', alignSelf: 'center' }}>
{notes.length} notes · {bars} bars
</span>