feat: piano roll transport, ARM/INPUT, AI bar range, MIDI recording

- Transport: play/stop/back/next/record buttons in piano roll toolbar
- ARM toggle + MIDI input device selector next to Snap to Scale
- AI bar range selector (bar X to bar Y) for Copilot MIDI generation
- handleAISend detects active piano roll tab and routes MIDI prompt
- Added selectedMidiInputId state + handleMidiInputSelect for MIDI input
This commit is contained in:
2026-07-25 17:22:24 +07:00
parent 1a97a4b33b
commit 1b711ef014
3 changed files with 79 additions and 24 deletions
+68 -19
View File
@@ -4543,11 +4543,14 @@ const AIPresetModal = ({ isOpen, onClose }) => {
}, "Đóng")))); }, "Đóng"))));
}; };
const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNotes, onSaveNotes, setSubTabs, onPlayPause, onStop, isPlaying, playPreviewNote, showToast }) => { const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNotes, onSaveNotes, setSubTabs, onPlayPause, onStop, isPlaying, playPreviewNote, showToast, midiDevices, recordingState, onRecord, selectedMidiInputId, onMidiInputSelect }) => {
const [activeRollTool, setActiveRollTool] = React.useState('select'); const [activeRollTool, setActiveRollTool] = React.useState('select');
const [snapVal, setSnapVal] = React.useState('1/16'); const [snapVal, setSnapVal] = React.useState('1/16');
const [ccMode, setCcMode] = React.useState('velocity'); const [ccMode, setCcMode] = React.useState('velocity');
const [rollZoom, setRollZoom] = React.useState(60); // local horizontal zoom factor const [rollZoom, setRollZoom] = React.useState(60); // local horizontal zoom factor
const [isArmed, setIsArmed] = React.useState(false);
const [aiBarStart, setAiBarStart] = React.useState(0);
const [aiBarEnd, setAiBarEnd] = React.useState(4);
const canvasRef = React.useRef(null); const canvasRef = React.useRef(null);
const ccCanvasRef = React.useRef(null); const ccCanvasRef = React.useRef(null);
@@ -5594,7 +5597,43 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
}, ['free', '4', '1', '1/2', '1/4', '1/8', '1/16', '1/32'].map(v => /*#__PURE__*/React.createElement("option", { }, ['free', '4', '1', '1/2', '1/4', '1/8', '1/16', '1/32'].map(v => /*#__PURE__*/React.createElement("option", {
key: v, key: v,
value: v value: v
}, v)))), /*#__PURE__*/React.createElement("div", { }, v)))), /*#__PURE__*/React.createElement("button", {
onClick: () => setIsArmed(!isArmed),
className: `px-2 py-1 rounded text-xs font-bold ${isArmed ? 'bg-red-600 text-white' : 'bg-zinc-800 text-zinc-400'}`
}, "ARM"), /*#__PURE__*/React.createElement("select", {
value: selectedMidiInputId || '',
onChange: e => onMidiInputSelect(e.target.value),
className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-xs outline-none max-w-[100px]"
}, /*#__PURE__*/React.createElement("option", { value: "" }, "Input"), (midiDevices || []).map(d => /*#__PURE__*/React.createElement("option", { key: d.id, value: d.id }, d.name || d.id))), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-0.5 ml-1"
}, /*#__PURE__*/React.createElement("button", {
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: Math.max(0, (st.currentTime || 0) - beatSec * 4) } : s)),
className: "w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",
title: "Back 1 bar"
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "step-back", className: "w-3 h-3" })), /*#__PURE__*/React.createElement("button", {
onClick: onPlayPause,
className: `w-6 h-6 flex items-center justify-center rounded border ${isPlaying ? 'bg-emerald-600 text-black' : 'bg-cyan-600 text-white'} border-cyan-500`,
title: isPlaying ? "Pause" : "Play"
}, /*#__PURE__*/React.createElement("i", { "data-lucide": isPlaying ? "pause" : "play", className: "w-3 h-3 fill-current" })), /*#__PURE__*/React.createElement("button", {
onClick: onStop,
className: "w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",
title: "Stop"
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "square", className: "w-3 h-3 fill-current" })), /*#__PURE__*/React.createElement("button", {
onClick: onRecord,
className: `w-6 h-6 flex items-center justify-center rounded border ${recordingState === 'RECORDING' ? 'bg-red-600 text-white border-red-500 animate-pulse' : 'bg-zinc-800 text-red-500 border-zinc-700'}`
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "circle", className: "w-3 h-3 fill-current" })), /*#__PURE__*/React.createElement("button", {
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: (st.currentTime || 0) + beatSec * 4 } : s)),
className: "w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",
title: "Forward 1 bar"
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "step-forward", className: "w-3 h-3" }))), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1 ml-1 text-xs"
}, /*#__PURE__*/React.createElement("span", { className: "text-zinc-500" }, "AI:"), /*#__PURE__*/React.createElement("input", {
type: "number", value: aiBarStart, onChange: e => setAiBarStart(parseInt(e.target.value) || 0),
className: "w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"
}), /*#__PURE__*/React.createElement("span", { className: "text-zinc-500" }, "-"), /*#__PURE__*/React.createElement("input", {
type: "number", value: aiBarEnd, onChange: e => setAiBarEnd(parseInt(e.target.value) || 1),
className: "w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"
}), /*#__PURE__*/React.createElement("span", { className: "text-zinc-500" }, "bar")), /*#__PURE__*/React.createElement("div", {
className: "flex bg-zinc-800 p-0.5 rounded border border-zinc-700 text-xs" className: "flex bg-zinc-800 p-0.5 rounded border border-zinc-700 text-xs"
}, ['velocity', 'pan'].map(mode => /*#__PURE__*/React.createElement("button", { }, ['velocity', 'pan'].map(mode => /*#__PURE__*/React.createElement("button", {
key: mode, key: mode,
@@ -5608,10 +5647,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
}, /*#__PURE__*/React.createElement("button", { }, /*#__PURE__*/React.createElement("button", {
onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes), 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" 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"
}, /*#__PURE__*/React.createElement("i", { }, /*#__PURE__*/React.createElement("i", { "data-lucide": "save", className: "w-3 h-3" }), "Lưu"), /*#__PURE__*/React.createElement("button", {
"data-lucide": "save",
className: "w-3 h-3"
}), "Lưu"), /*#__PURE__*/React.createElement("button", {
onClick: onClose, onClick: onClose,
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" 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"
}, /*#__PURE__*/React.createElement("i", { }, /*#__PURE__*/React.createElement("i", {
@@ -6206,6 +6242,13 @@ const App = () => {
const [toastMessage, setToastMessage] = useState(null); const [toastMessage, setToastMessage] = useState(null);
const [audioDevices, setAudioDevices] = useState([]); const [audioDevices, setAudioDevices] = useState([]);
const [midiDevices, setMidiDevices] = useState([]); const [midiDevices, setMidiDevices] = useState([]);
const [selectedMidiInputId, setSelectedMidiInputId] = useState('');
const handleMidiInputSelect = (id) => {
setSelectedMidiInputId(id);
if (window.SonicRecorderManager) {
window.SonicRecorderManager.setSelectedMidiInputId(id);
}
};
useEffect(() => { useEffect(() => {
if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) { if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
@@ -15261,19 +15304,25 @@ const App = () => {
if (!st) return null; if (!st) return null;
if (st.type === 'PIANO_ROLL') { if (st.type === 'PIANO_ROLL') {
return /*#__PURE__*/React.createElement(PianoRollTabEditor, { return /*#__PURE__*/React.createElement(PianoRollTabEditor, {
st: st, st: st,
zoom: zoom, zoom: zoom,
bpm: bpm, bpm: bpm,
viewportWidth: viewportWidth, viewportWidth: viewportWidth,
onClose: () => closeSubTab(st.id), onClose: () => closeSubTab(st.id),
onUpdateNotes: handleUpdateMidiNotes, onUpdateNotes: handleUpdateMidiNotes,
onSaveNotes: handleSaveMidiNotes, onSaveNotes: handleSaveMidiNotes,
setSubTabs: setSubTabs, setSubTabs: setSubTabs,
onPlayPause: handlePlayPause, onPlayPause: handlePlayPause,
onStop: stopAllPlayback, onStop: stopAllPlayback,
isPlaying: isPlaying, isPlaying: isPlaying,
playPreviewNote: playMidiPreviewNote playPreviewNote: playMidiPreviewNote,
}); showToast: showToast,
midiDevices: midiDevices,
recordingState: recordingState,
onRecord: handleRecordClick,
selectedMidiInputId: selectedMidiInputId,
onMidiInputSelect: handleMidiInputSelect
});
} }
const subTrack = tracks.find(t => t.id === st.trackId); const subTrack = tracks.find(t => t.id === st.trackId);
const vTrack = subTrack ? { const vTrack = subTrack ? {
File diff suppressed because one or more lines are too long
+6
View File
@@ -133,3 +133,9 @@
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
- **Ghi chú/Test (nếu có):** `npm run build` — build passes. - **Ghi chú/Test (nếu có):** `npm run build` — build passes.
--- ---
### [2026-07-25 17:22] Task: Piano roll toolbar - transport, ARM/INPUT, AI bar range
- **Tóm tắt thay đổi:** Thêm transport (play/stop/back/next/record), ARM+INPUT select MIDI, AI bar range (bar x-y) vào piano roll toolbar. handleAISend phát hiện piano roll active → MIDI prompt. Thêm selectedMidiInputId + handleMidiInputSelect.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
---