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:
+68
-19
@@ -4543,11 +4543,14 @@ const AIPresetModal = ({ isOpen, onClose }) => {
|
||||
}, "Đó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 [snapVal, setSnapVal] = React.useState('1/16');
|
||||
const [ccMode, setCcMode] = React.useState('velocity');
|
||||
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 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", {
|
||||
key: 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"
|
||||
}, ['velocity', 'pan'].map(mode => /*#__PURE__*/React.createElement("button", {
|
||||
key: mode,
|
||||
@@ -5608,10 +5647,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
||||
}, /*#__PURE__*/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"
|
||||
}, /*#__PURE__*/React.createElement("i", {
|
||||
"data-lucide": "save",
|
||||
className: "w-3 h-3"
|
||||
}), "Lưu"), /*#__PURE__*/React.createElement("button", {
|
||||
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "save", className: "w-3 h-3" }), "Lưu"), /*#__PURE__*/React.createElement("button", {
|
||||
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"
|
||||
}, /*#__PURE__*/React.createElement("i", {
|
||||
@@ -6206,6 +6242,13 @@ const App = () => {
|
||||
const [toastMessage, setToastMessage] = useState(null);
|
||||
const [audioDevices, setAudioDevices] = useState([]);
|
||||
const [midiDevices, setMidiDevices] = useState([]);
|
||||
const [selectedMidiInputId, setSelectedMidiInputId] = useState('');
|
||||
const handleMidiInputSelect = (id) => {
|
||||
setSelectedMidiInputId(id);
|
||||
if (window.SonicRecorderManager) {
|
||||
window.SonicRecorderManager.setSelectedMidiInputId(id);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
|
||||
@@ -15261,19 +15304,25 @@ const App = () => {
|
||||
if (!st) return null;
|
||||
if (st.type === 'PIANO_ROLL') {
|
||||
return /*#__PURE__*/React.createElement(PianoRollTabEditor, {
|
||||
st: st,
|
||||
zoom: zoom,
|
||||
bpm: bpm,
|
||||
viewportWidth: viewportWidth,
|
||||
onClose: () => closeSubTab(st.id),
|
||||
onUpdateNotes: handleUpdateMidiNotes,
|
||||
onSaveNotes: handleSaveMidiNotes,
|
||||
setSubTabs: setSubTabs,
|
||||
onPlayPause: handlePlayPause,
|
||||
onStop: stopAllPlayback,
|
||||
isPlaying: isPlaying,
|
||||
playPreviewNote: playMidiPreviewNote
|
||||
});
|
||||
st: st,
|
||||
zoom: zoom,
|
||||
bpm: bpm,
|
||||
viewportWidth: viewportWidth,
|
||||
onClose: () => closeSubTab(st.id),
|
||||
onUpdateNotes: handleUpdateMidiNotes,
|
||||
onSaveNotes: handleSaveMidiNotes,
|
||||
setSubTabs: setSubTabs,
|
||||
onPlayPause: handlePlayPause,
|
||||
onStop: stopAllPlayback,
|
||||
isPlaying: isPlaying,
|
||||
playPreviewNote: playMidiPreviewNote,
|
||||
showToast: showToast,
|
||||
midiDevices: midiDevices,
|
||||
recordingState: recordingState,
|
||||
onRecord: handleRecordClick,
|
||||
selectedMidiInputId: selectedMidiInputId,
|
||||
onMidiInputSelect: handleMidiInputSelect
|
||||
});
|
||||
}
|
||||
const subTrack = tracks.find(t => t.id === st.trackId);
|
||||
const vTrack = subTrack ? {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user