IMPROVE: thay đổi giao diện của PIANO ROLL TAB
This commit is contained in:
+88
-89
@@ -7115,24 +7115,26 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
// Shift+C chord stamp, Shift+S lock-scale toggle
|
// Shift+C chord stamp, Shift+S lock-scale toggle
|
||||||
const inField = e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable);
|
const inField = e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable);
|
||||||
// Ctrl+C/X/V — copy/paste/cut NOTES (thay 2 nút Copy/Paste đã bỏ —
|
// Ctrl+C/X/V — copy/paste/cut NOTES (thay 2 nút Copy/Paste đã bỏ —
|
||||||
// user 08:40)
|
// user 08:40) — CHỈ tác động NOTES ĐƯỢC CHỌN (user 09:10)
|
||||||
if ((e.ctrlKey || e.metaKey) && !e.altKey && !inField) {
|
if ((e.ctrlKey || e.metaKey) && !e.altKey && !inField) {
|
||||||
if (e.key === 'c') {
|
if (e.key === 'c') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (notes && notes.length) {
|
const sel = notes.filter(n => selectedNoteIds.includes(n.id));
|
||||||
if (onCopyNotes) onCopyNotes(notes);
|
if (sel.length) {
|
||||||
showToast('Đã copy ' + notes.length + ' nốt vào clipboard.', 'success');
|
if (onCopyNotes) onCopyNotes(sel);
|
||||||
} else { showToast('Không có nốt để copy.', 'warning'); }
|
showToast('Đã copy ' + sel.length + ' nốt được chọn.', 'success');
|
||||||
|
} else { showToast('Chọn notes trước khi copy (Ctrl+C).', 'warning'); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === 'x') {
|
if (e.key === 'x') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (notes && notes.length) {
|
const sel = notes.filter(n => selectedNoteIds.includes(n.id));
|
||||||
if (onCopyNotes) onCopyNotes(notes);
|
if (sel.length) {
|
||||||
|
if (onCopyNotes) onCopyNotes(sel);
|
||||||
pushToUndo(notes);
|
pushToUndo(notes);
|
||||||
setNotes([]);
|
setNotes(prev => (prev || []).filter(n => !selectedNoteIds.includes(n.id)));
|
||||||
showToast('Đã cắt ' + notes.length + ' nốt.', 'success');
|
showToast('Đã cắt ' + sel.length + ' nốt được chọn.', 'success');
|
||||||
} else { showToast('Không có nốt để cắt.', 'warning'); }
|
} else { showToast('Chọn notes trước khi cắt (Ctrl+X).', 'warning'); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === 'v') {
|
if (e.key === 'v') {
|
||||||
@@ -7616,6 +7618,11 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
}
|
}
|
||||||
}, [notes, snapValue, rollZoom, selectedNoteIds, selectionMarquee, st.currentTime, bpm, viewWidth, viewBeats, recordingState, recTempMidiNotes, showGhostNotes, sessionSyncMode, ghostLayers, renderBeatOffset, renderTick, activeTracks, focusItemId]);
|
}, [notes, snapValue, rollZoom, selectedNoteIds, selectionMarquee, st.currentTime, bpm, viewWidth, viewBeats, recordingState, recTempMidiNotes, showGhostNotes, sessionSyncMode, ghostLayers, renderBeatOffset, renderTick, activeTracks, focusItemId]);
|
||||||
|
|
||||||
|
// showCC/ccHeight khai báo TRƯỚC useLayoutEffect vẽ CC (deps tham chiếu —
|
||||||
|
// khai báo sau → TDZ error — user 08:50)
|
||||||
|
const [showCC, setShowCC] = React.useState(true);
|
||||||
|
const [ccHeight, setCcHeight] = React.useState(80);
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
const canvas = ccCanvasRef.current;
|
const canvas = ccCanvasRef.current;
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
@@ -7659,7 +7666,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
ctx.arc(x, y, 3.5, 0, 2 * Math.PI);
|
ctx.arc(x, y, 3.5, 0, 2 * Math.PI);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
});
|
});
|
||||||
}, [notes, ccMode, rollZoom, viewWidth, selectedNoteIds, renderBeatOffset]);
|
}, [notes, ccMode, rollZoom, viewWidth, selectedNoteIds, renderBeatOffset, ccHeight, showCC]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const scrollToC3 = () => {
|
const scrollToC3 = () => {
|
||||||
@@ -8539,8 +8546,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
snapToScaleRef.current = st.snapToScale !== undefined ? st.snapToScale : true;
|
snapToScaleRef.current = st.snapToScale !== undefined ? st.snapToScale : true;
|
||||||
const [scaleMenuPos, setScaleMenuPos] = React.useState(null);
|
const [scaleMenuPos, setScaleMenuPos] = React.useState(null);
|
||||||
const scaleMenuOriginRef = React.useRef(null);
|
const scaleMenuOriginRef = React.useRef(null);
|
||||||
const [showCC, setShowCC] = React.useState(true);
|
|
||||||
const [ccHeight, setCcHeight] = React.useState(80);
|
|
||||||
|
|
||||||
const snapPitchToScale = (pitch, scale) => {
|
const snapPitchToScale = (pitch, scale) => {
|
||||||
if (!scale) return pitch;
|
if (!scale) return pitch;
|
||||||
@@ -8901,13 +8906,61 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
|
|||||||
return showGhostNotes ? base + 'bg-purple-900/60 text-purple-300 border border-purple-700' : base + 'text-zinc-500 hover:text-zinc-300';
|
return showGhostNotes ? base + 'bg-purple-900/60 text-purple-300 border border-purple-700' : base + 'text-zinc-500 hover:text-zinc-300';
|
||||||
}(),
|
}(),
|
||||||
title: "Toggle ghost notes visibility"
|
title: "Toggle ghost notes visibility"
|
||||||
}, "👻 MIDI ghost notes"), React.createElement("button", {
|
}, "👻 MIDI ghost notes"), React.createElement("div", {
|
||||||
onClick: onClose,
|
className: "flex items-center gap-1 ml-auto"
|
||||||
className: "px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs flex items-center gap-1 transition ml-auto"
|
}, React.createElement("button", {
|
||||||
}, React.createElement("i", {
|
onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes), className: "px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"
|
||||||
"data-lucide": "x",
|
}, React.createElement("i", { "data-lucide": "save", className: "w-3 h-3" }), "L\u01B0u"), React.createElement("button", {
|
||||||
className: "w-3 h-3"
|
onClick: () => {
|
||||||
}), "Đóng"), React.createElement("div", {
|
const ppq = 480;
|
||||||
|
const bpmNum = parseInt(bpm) || 120;
|
||||||
|
const ticksPerBeat = ppq;
|
||||||
|
const events = [];
|
||||||
|
(notes || []).forEach(n => {
|
||||||
|
const startTick = Math.round((n.start_beat || 0) * ticksPerBeat);
|
||||||
|
const durTick = Math.round((n.duration_beats || 1) * ticksPerBeat);
|
||||||
|
const pitch = n.pitch || 60;
|
||||||
|
const vel = Math.round((n.velocity || 0.8) * 127);
|
||||||
|
events.push({ tick: startTick, type: 'note_on', pitch, velocity: vel });
|
||||||
|
events.push({ tick: startTick + durTick, type: 'note_off', pitch, velocity: 0 });
|
||||||
|
});
|
||||||
|
events.sort((a, b) => a.tick - b.tick || (a.type === 'note_off' ? -1 : 1));
|
||||||
|
const writeVLQ = (bytes, v) => {
|
||||||
|
let val = Math.max(0, v);
|
||||||
|
const buf = [];
|
||||||
|
buf.push(val & 0x7F);
|
||||||
|
while (val > 0x7F) { val >>= 7; buf.push(0x80 | (val & 0x7F)); }
|
||||||
|
for (let i = buf.length - 1; i >= 0; i--) bytes.push(buf[i]);
|
||||||
|
};
|
||||||
|
const trackBytes = [];
|
||||||
|
let lastTick = 0;
|
||||||
|
events.forEach(ev => {
|
||||||
|
const delta = Math.max(0, ev.tick - lastTick);
|
||||||
|
writeVLQ(trackBytes, delta);
|
||||||
|
trackBytes.push(ev.type === 'note_on' ? 0x90 : 0x80, ev.pitch, ev.velocity);
|
||||||
|
lastTick = ev.tick;
|
||||||
|
});
|
||||||
|
writeVLQ(trackBytes, 0);
|
||||||
|
trackBytes.push(0xFF, 0x2F, 0x00);
|
||||||
|
const trackData = [0x4D, 0x54, 0x72, 0x6B];
|
||||||
|
const len = trackBytes.length;
|
||||||
|
trackData.push((len >> 24) & 0xFF, (len >> 16) & 0xFF, (len >> 8) & 0xFF, len & 0xFF);
|
||||||
|
trackData.push(...trackBytes);
|
||||||
|
const header = [0x4D, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x01, (ppq >> 8) & 0xFF, ppq & 0xFF];
|
||||||
|
const all = header.concat(trackData);
|
||||||
|
const blob = new Blob([new Uint8Array(all)], { type: 'audio/midi' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = (st.label || 'midi') + '.mid';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
showToast('Đã xuất file MIDI!', 'success');
|
||||||
|
},
|
||||||
|
className: "px-2.5 py-1 bg-amber-700 hover:bg-amber-600 text-white rounded text-xs flex items-center gap-1 transition font-semibold"
|
||||||
|
}, React.createElement("i", { "data-lucide": "file-down", className: "w-3 h-3" }), "Export MIDI")), React.createElement("div", {
|
||||||
style: { flexBasis: "100%", height: 0 }
|
style: { flexBasis: "100%", height: 0 }
|
||||||
}), React.createElement("button", {
|
}), React.createElement("button", {
|
||||||
onClick: () => setArpModal({ pattern: 'UP', rate: '1/16', octaves: 2, gate: 80, triplet: false, dotted: false }),
|
onClick: () => setArpModal({ pattern: 'UP', rate: '1/16', octaves: 2, gate: 80, triplet: false, dotted: false }),
|
||||||
@@ -8964,7 +9017,7 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
|
|||||||
}, React.createElement("div", {
|
}, React.createElement("div", {
|
||||||
className: `w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'translate-x-3.5' : 'translate-x-0.5'}`
|
className: `w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'translate-x-3.5' : 'translate-x-0.5'}`
|
||||||
}))), React.createElement("div", {
|
}))), React.createElement("div", {
|
||||||
className: "flex items-center gap-1 text-xs"
|
className: "flex items-center gap-1 text-sm"
|
||||||
}, React.createElement("span", {
|
}, React.createElement("span", {
|
||||||
className: "text-zinc-500 font-semibold"
|
className: "text-zinc-500 font-semibold"
|
||||||
}, "Scale:"), React.createElement("select", {
|
}, "Scale:"), React.createElement("select", {
|
||||||
@@ -8974,7 +9027,7 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
|
|||||||
setSelectedScale(v === 'null' ? null : JSON.parse(v));
|
setSelectedScale(v === 'null' ? null : JSON.parse(v));
|
||||||
setRenderTick(t => t + 1);
|
setRenderTick(t => t + 1);
|
||||||
},
|
},
|
||||||
className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500 max-w-[110px]"
|
className: "bg-zinc-800 border border-zinc-700 text-sm text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500 max-w-[110px]"
|
||||||
}, React.createElement("option", { value: "null" }, "None"), (function() {
|
}, React.createElement("option", { value: "null" }, "None"), (function() {
|
||||||
const opts = [];
|
const opts = [];
|
||||||
const pushKey = (label, val) => opts.push(React.createElement("option", { key: label, value: JSON.stringify(val) }, label));
|
const pushKey = (label, val) => opts.push(React.createElement("option", { key: label, value: JSON.stringify(val) }, label));
|
||||||
@@ -8988,105 +9041,51 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
|
|||||||
}())), React.createElement("select", {
|
}())), React.createElement("select", {
|
||||||
value: scaleRoot,
|
value: scaleRoot,
|
||||||
onChange: e => { setScaleRoot(parseInt(e.target.value) || 0); setRenderTick(t => t + 1); },
|
onChange: e => { setScaleRoot(parseInt(e.target.value) || 0); setRenderTick(t => t + 1); },
|
||||||
className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"
|
className: "bg-zinc-800 border border-zinc-700 text-sm text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"
|
||||||
}, ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'].map((r, i) => React.createElement("option", { key: r, value: i }, r))), React.createElement("button", {
|
}, ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'].map((r, i) => React.createElement("option", { key: r, value: i }, r))), React.createElement("button", {
|
||||||
onClick: () => applyForceToScale(),
|
onClick: () => applyForceToScale(),
|
||||||
title: "Force selected notes to scale",
|
title: "Force selected notes to scale",
|
||||||
className: "px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-amber-400 border border-zinc-700 hover:border-amber-600"
|
className: "px-1.5 py-0.5 rounded text-sm bg-zinc-800 text-amber-400 border border-zinc-700 hover:border-amber-600"
|
||||||
}, "Force")), React.createElement("div", {
|
}, "Force")), React.createElement("div", {
|
||||||
className: "flex items-center gap-1 text-xs"
|
className: "flex items-center gap-1 text-sm"
|
||||||
}, React.createElement("span", {
|
}, React.createElement("span", {
|
||||||
className: "text-zinc-500 font-semibold ml-1"
|
className: "text-zinc-500 font-semibold ml-1"
|
||||||
}, "Chord:"), React.createElement("select", {
|
}, "Chord:"), React.createElement("select", {
|
||||||
value: chordType,
|
value: chordType,
|
||||||
onChange: e => setChordType(e.target.value),
|
onChange: e => setChordType(e.target.value),
|
||||||
className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-[10px] outline-none"
|
className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-sm outline-none"
|
||||||
}, React.createElement("option", { value: "triad" }, "Triad"), React.createElement("option", { value: "min7" }, "Min7"), React.createElement("option", { value: "sus4" }, "Sus4"), React.createElement("option", { value: "add9" }, "Add9")), React.createElement("button", {
|
}, React.createElement("option", { value: "triad" }, "Triad"), React.createElement("option", { value: "min7" }, "Min7"), React.createElement("option", { value: "sus4" }, "Sus4"), React.createElement("option", { value: "add9" }, "Add9")), React.createElement("button", {
|
||||||
onClick: () => setChordStampMode(m => !m),
|
onClick: () => setChordStampMode(m => !m),
|
||||||
title: "Chord stamp mode — click canvas to stamp chord (Shift+C)",
|
title: "Chord stamp mode — click canvas to stamp chord (Shift+C)",
|
||||||
className: `px-1.5 py-0.5 rounded text-[10px] border ${chordStampMode ? 'bg-amber-800/60 text-amber-300 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-zinc-700 hover:border-amber-600'}`
|
className: `px-1.5 py-0.5 rounded text-sm border ${chordStampMode ? 'bg-amber-800/60 text-amber-300 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-zinc-700 hover:border-amber-600'}`
|
||||||
}, "Stamp"), React.createElement("button", {
|
}, "Stamp"), React.createElement("button", {
|
||||||
onClick: () => applyHarmonize(3),
|
onClick: () => applyHarmonize(3),
|
||||||
title: "Harmonize +3rd",
|
title: "Harmonize +3rd",
|
||||||
className: "px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-emerald-600"
|
className: "px-1.5 py-0.5 rounded text-sm bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-emerald-600"
|
||||||
}, "+3"), React.createElement("button", {
|
}, "+3"), React.createElement("button", {
|
||||||
onClick: () => applyHarmonize(5),
|
onClick: () => applyHarmonize(5),
|
||||||
title: "Harmonize +5th",
|
title: "Harmonize +5th",
|
||||||
className: "px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-emerald-600"
|
className: "px-1.5 py-0.5 rounded text-sm bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-emerald-600"
|
||||||
}, "+5"), React.createElement("button", {
|
}, "+5"), React.createElement("button", {
|
||||||
onClick: () => applyHarmonize(7),
|
onClick: () => applyHarmonize(7),
|
||||||
title: "Harmonize +7th",
|
title: "Harmonize +7th",
|
||||||
className: "px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-emerald-600"
|
className: "px-1.5 py-0.5 rounded text-sm bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-emerald-600"
|
||||||
}, "+7")), React.createElement("div", {
|
}, "+7")), React.createElement("div", {
|
||||||
className: "flex items-center gap-1 text-xs"
|
className: "flex items-center gap-1 text-sm"
|
||||||
}, React.createElement("span", {
|
}, React.createElement("span", {
|
||||||
className: "text-zinc-500 font-semibold"
|
className: "text-zinc-500 font-semibold"
|
||||||
}, "Vel:"), React.createElement("button", {
|
}, "Vel:"), React.createElement("button", {
|
||||||
onClick: () => applyVelocityCompress(),
|
onClick: () => applyVelocityCompress(),
|
||||||
title: "Compress velocity toward target",
|
title: "Compress velocity toward target",
|
||||||
className: "px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-purple-600"
|
className: "px-1.5 py-0.5 rounded text-sm bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-purple-600"
|
||||||
}, "Comp"), React.createElement("input", {
|
}, "Comp"), React.createElement("input", {
|
||||||
type: "number", value: velocityTarget, onChange: e => setVelocityTarget(parseInt(e.target.value) || 80),
|
type: "number", value: velocityTarget, onChange: e => setVelocityTarget(parseInt(e.target.value) || 80),
|
||||||
className: "w-9 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-[10px] text-center"
|
className: "w-14 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-sm text-center"
|
||||||
}), React.createElement("button", {
|
}), React.createElement("button", {
|
||||||
onClick: () => applyVelocityNormalize(),
|
onClick: () => applyVelocityNormalize(),
|
||||||
title: "Normalize (max → 127)",
|
title: "Normalize (max → 127)",
|
||||||
className: "px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-purple-600"
|
className: "px-1.5 py-0.5 rounded text-sm bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-purple-600"
|
||||||
}, "Norm")), React.createElement("div", {
|
}, "Norm"))),
|
||||||
className: "flex items-center gap-1 ml-auto"
|
|
||||||
}, React.createElement("button", {
|
|
||||||
onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes), className: "px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"
|
|
||||||
}, React.createElement("i", { "data-lucide": "save", className: "w-3 h-3" }), "L\u01B0u"), React.createElement("button", {
|
|
||||||
onClick: () => {
|
|
||||||
const ppq = 480;
|
|
||||||
const bpmNum = parseInt(bpm) || 120;
|
|
||||||
const ticksPerBeat = ppq;
|
|
||||||
const events = [];
|
|
||||||
(notes || []).forEach(n => {
|
|
||||||
const startTick = Math.round((n.start_beat || 0) * ticksPerBeat);
|
|
||||||
const durTick = Math.round((n.duration_beats || 1) * ticksPerBeat);
|
|
||||||
const pitch = n.pitch || 60;
|
|
||||||
const vel = Math.round((n.velocity || 0.8) * 127);
|
|
||||||
events.push({ tick: startTick, type: 'note_on', pitch, velocity: vel });
|
|
||||||
events.push({ tick: startTick + durTick, type: 'note_off', pitch, velocity: 0 });
|
|
||||||
});
|
|
||||||
events.sort((a, b) => a.tick - b.tick || (a.type === 'note_off' ? -1 : 1));
|
|
||||||
const writeVLQ = (bytes, v) => {
|
|
||||||
let val = Math.max(0, v);
|
|
||||||
const buf = [];
|
|
||||||
buf.push(val & 0x7F);
|
|
||||||
while (val > 0x7F) { val >>= 7; buf.push(0x80 | (val & 0x7F)); }
|
|
||||||
for (let i = buf.length - 1; i >= 0; i--) bytes.push(buf[i]);
|
|
||||||
};
|
|
||||||
const trackBytes = [];
|
|
||||||
let lastTick = 0;
|
|
||||||
events.forEach(ev => {
|
|
||||||
const delta = Math.max(0, ev.tick - lastTick);
|
|
||||||
writeVLQ(trackBytes, delta);
|
|
||||||
trackBytes.push(ev.type === 'note_on' ? 0x90 : 0x80, ev.pitch, ev.velocity);
|
|
||||||
lastTick = ev.tick;
|
|
||||||
});
|
|
||||||
writeVLQ(trackBytes, 0);
|
|
||||||
trackBytes.push(0xFF, 0x2F, 0x00);
|
|
||||||
const trackData = [0x4D, 0x54, 0x72, 0x6B];
|
|
||||||
const len = trackBytes.length;
|
|
||||||
trackData.push((len >> 24) & 0xFF, (len >> 16) & 0xFF, (len >> 8) & 0xFF, len & 0xFF);
|
|
||||||
trackData.push(...trackBytes);
|
|
||||||
const header = [0x4D, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x01, (ppq >> 8) & 0xFF, ppq & 0xFF];
|
|
||||||
const all = header.concat(trackData);
|
|
||||||
const blob = new Blob([new Uint8Array(all)], { type: 'audio/midi' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = (st.label || 'midi') + '.mid';
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
showToast('Đã xuất file MIDI!', 'success');
|
|
||||||
},
|
|
||||||
className: "px-2.5 py-1 bg-amber-700 hover:bg-amber-600 text-white rounded text-xs flex items-center gap-1 transition font-semibold"
|
|
||||||
}, React.createElement("i", { "data-lucide": "file-down", className: "w-3 h-3" }), "Export MIDI"))),
|
|
||||||
|
|
||||||
/* 2. BAR RULER */
|
/* 2. BAR RULER */
|
||||||
React.createElement("div", {
|
React.createElement("div", {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -24,7 +24,7 @@
|
|||||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||||
<script src="/static/js/app.precompiled.js?v=202608070840" defer></script>
|
<script src="/static/js/app.precompiled.js?v=202608070910" defer></script>
|
||||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -2822,3 +2822,36 @@
|
|||||||
(3) Ctrl+C/X/V trong PIANO ROLL TAB (keydown 7106): C = copy notes (onCopyNotes + toast), X = copy + xóa hết notes, V = paste clipboardNotes (append — như nút cũ) — deps thêm clipboardNotes (handler re-create khi clipboard đổi).
|
(3) Ctrl+C/X/V trong PIANO ROLL TAB (keydown 7106): C = copy notes (onCopyNotes + toast), X = copy + xóa hết notes, V = paste clipboardNotes (append — như nút cũ) — deps thêm clipboardNotes (handler re-create khi clipboard đổi).
|
||||||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070840), `wiki.md`. Rebuild precompiled (build PASS).
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070840), `wiki.md`. Rebuild precompiled (build PASS).
|
||||||
- **Ghi chú/Test:** hard refresh → mở PIANO ROLL → toolbar 1 hàng (không còn Copy/Paste); ARP/STRUM/HUMANIZE + mức humanize hoạt động (mở modal); Ctrl+C → Ctrl+V paste notes; Ctrl+X cắt notes; Alt+A/S/R mở tool.
|
- **Ghi chú/Test:** hard refresh → mở PIANO ROLL → toolbar 1 hàng (không còn Copy/Paste); ARP/STRUM/HUMANIZE + mức humanize hoạt động (mở modal); Ctrl+C → Ctrl+V paste notes; Ctrl+X cắt notes; Alt+A/S/R mở tool.
|
||||||
|
|
||||||
|
### [2026-08-07 08:45] Task: (1) Lưu + Export MIDI lên trước vị trí nút Đóng, bỏ Đóng (2) Velocity không cập nhật realtime khi đổi độ rộng/bật tắt thanh velocity
|
||||||
|
- **Yêu cầu user:** (1) mang 2 nút Lưu + Export MIDI lên trước nút Đóng + remove nút Đóng; (2) velocity note không cập nhật realtime khi thay đổi ccHeight (độ rộng) hoặc showCC (bật/tắt thanh velocity).
|
||||||
|
- **FIX (app.jsx PianoRollTabEditor):**
|
||||||
|
(1) Xóa nút "Đóng" (onClose) — chèn div ml-auto [Lưu + Export MIDI] (di chuyển từ cuối toolbar — xóa bản cũ); sửa cân bằng ngoặc (bản cũ `"Export MIDI")))` đóng cả toolbar div — thêm `)` tại `"Norm")))`).
|
||||||
|
(2) Effect vẽ CC canvas — thêm `ccHeight, showCC` vào deps (`[notes, ccMode, rollZoom, viewWidth, selectedNoteIds, renderBeatOffset, ccHeight, showCC]`) — đổi độ rộng/bật tắt → vẽ lại realtime.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070845), `wiki.md`. Rebuild precompiled (build PASS).
|
||||||
|
- **Ghi chú/Test:** hard refresh → PIANO ROLL toolbar: không còn nút Đóng; Lưu/Export ở cuối (trước hàng tool Scale/Chord/Vel); kéo đổi độ rộng thanh velocity / bấm Vel toggle → velocity bars cập nhật ngay.
|
||||||
|
|
||||||
|
### [2026-08-07 08:50] Task: FIX TDZ — ccHeight/showCC khai báo SAU effect vẽ CC
|
||||||
|
- **Lỗi user:** `Uncaught ReferenceError: Cannot access 'ccHeight' before initialization`.
|
||||||
|
- **Nguyên nhân:** 08:45 thêm ccHeight/showCC vào deps useLayoutEffect vẽ CC (7620) — NHƯNG 2 state khai báo ở 8542 (SAU effect) → deps array tham chiếu biến trước khai báo → TDZ error khi render.
|
||||||
|
- **FIX (app.jsx):** di chuyển `const [showCC, setShowCC]` + `const [ccHeight, setCcHeight]` LÊN TRƯỚC useLayoutEffect vẽ CC (7619) — xóa bản trùng cũ (8542).
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070850), `wiki.md`. Rebuild precompiled (build PASS).
|
||||||
|
- **Ghi chú/Test:** hard refresh → mở PIANO ROLL → KHÔNG còn lỗi TDZ; đổi độ rộng thanh velocity / toggle Vel → vẽ lại realtime.
|
||||||
|
|
||||||
|
### [2026-08-07 08:55] Task: Tăng size tool từ "Snap to Scale" đến cuối hàng
|
||||||
|
- **Yêu cầu user:** tăng size các tool từ Snap to Scale đến cuối hàng (Snap to Scale toggle, Scale ▾, Root ▾, Force, Chord ▾, Stamp, +3/+5/+7, Vel: Comp/Norm).
|
||||||
|
- **FIX (app.jsx):** trong khối (Snap to Scale → Norm) — `text-[10px]` → `text-xs` (12px) — 9 chỗ (Force/Stamp/+3/+5/+7/Comp/Norm buttons + Chord select...).
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070855), `wiki.md`. Rebuild precompiled (build PASS).
|
||||||
|
- **Ghi chú/Test:** hard refresh → PIANO ROLL toolbar → các tool cuối hàng to hơn (text-xs).
|
||||||
|
|
||||||
|
### [2026-08-07 09:00] Task: Đồng bộ font hàng tool (Snap to Scale → Norm) lên text-sm
|
||||||
|
- **Yêu cầu user:** tăng kích thước font chữ trong hàng đó cho ĐỒNG BỘ.
|
||||||
|
- **FIX (app.jsx):** khối Snap to Scale → Norm — `text-xs` → `text-sm` (12 chỗ: 5 container div + buttons + Chord select + nhãn) + thêm `text-sm` vào 2 select Scale/Root (mặc định browser) — toàn hàng đồng bộ 14px.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070900), `wiki.md`. Rebuild precompiled (build PASS).
|
||||||
|
- **Ghi chú/Test:** hard refresh → PIANO ROLL toolbar hàng tool — font đồng bộ text-sm (14px).
|
||||||
|
|
||||||
|
### [2026-08-07 09:05] Task: Tăng chiều rộng input target velocity (Vel Comp)
|
||||||
|
- **Yêu cầu user:** tăng chiều rộng scrollbox/input của Vel Comp để chứa đủ số velocity (127).
|
||||||
|
- **FIX (app.jsx):** input `velocityTarget` (bên cạnh nút Comp) — `w-9` → `w-14` (đủ 3 chữ số 0-127).
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070905), `wiki.md`. Rebuild precompiled (build PASS).
|
||||||
|
- **Ghi chú/Test:** hard refresh → PIANO ROLL → input target (cạnh Comp) rộng hơn, hiện đủ "127".
|
||||||
|
|||||||
Reference in New Issue
Block a user