FIX: 11 bugs bảo mật/ổn định (static mount chặn dotfile, delete traversal, cleanup giữ clips serverFileId, upload whitelist, password strength, auth audio endpoints, pedalboard==0.9.19, vendor CDN local) + FEATURE: Carla bridge preview/export MIDI notes âm VSTi (POST /midi-render, /carla-play-notes, nút Preview VSTi/Export MIDI->Audio; pedalboard 0.9.19 raw MIDI bytes; SONICFORGE_STORAGE_DIR cô lập test storage)
This commit is contained in:
+113
-2
@@ -7544,7 +7544,7 @@ const AIPresetModal = ({ isOpen, onClose }) => {
|
||||
}))) : null);
|
||||
};
|
||||
|
||||
const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClose, onUpdateNotes, onSaveNotes, setSubTabs, onPlayPause, onStop, isPlaying, playPreviewNote, showToast, midiDevices, recordingState, recTempMidiNotes, onRecord, selectedMidiInputId, onMidiInputSelect, activeMidiPitches, onInstrumentSelect, onRescheduleMidi, onSeekPlayhead, snapValue, onSnapChange, onRealtimePlay, onCopyNotes, clipboardNotes }) => {
|
||||
const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClose, onUpdateNotes, onSaveNotes, setSubTabs, onPlayPause, onStop, isPlaying, playPreviewNote, showToast, midiDevices, recordingState, recTempMidiNotes, onRecord, selectedMidiInputId, onMidiInputSelect, activeMidiPitches, onInstrumentSelect, onRescheduleMidi, onSeekPlayhead, snapValue, onSnapChange, onRealtimePlay, onCopyNotes, clipboardNotes, onExportMidiAudio }) => {
|
||||
const [activeRollTool, setActiveRollTool] = React.useState('select');
|
||||
const [renderTick, setRenderTick] = React.useState(0);
|
||||
const [ccMode, setCcMode] = React.useState('velocity');
|
||||
@@ -7570,6 +7570,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
const keybedRef = React.useRef(null);
|
||||
const keybedMouseDownRef = React.useRef(false);
|
||||
const rulerScrollRef = React.useRef(null);
|
||||
// Audio element phát preview VSTi render — dừng cái cũ khi play cái mới
|
||||
const vstiPreviewAudioRef = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
const up = () => { keybedMouseDownRef.current = false; };
|
||||
window.addEventListener('mouseup', up);
|
||||
@@ -9695,7 +9697,60 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
|
||||
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", {
|
||||
}, React.createElement("i", { "data-lucide": "file-down", className: "w-3 h-3" }), "Export MIDI"), React.createElement("button", {
|
||||
onClick: async () => {
|
||||
// Preview/export MIDI notes với âm VSTi: midi-render (pedalboard) →
|
||||
// play wav ngay; nếu pedalboard không render được plugin (VST2/state
|
||||
// GUI) fallback carla-play-notes (realtime qua Carla bridge, OSC).
|
||||
const se = st.synth_engine || {};
|
||||
const instrumentId = se.plugin_id || st.instrumentId || null;
|
||||
if (!instrumentId || String(se.type || '').indexOf('vst') === -1) {
|
||||
showToast('Chọn VST instrument (nút Synth) trước khi Preview VSTi', 'warning');
|
||||
return;
|
||||
}
|
||||
if (!notes || !notes.length) { showToast('Chưa có nốt nhạc', 'warning'); return; }
|
||||
const payload = {
|
||||
instrument_id: instrumentId,
|
||||
notes: (notes || []).map(n => ({ pitch: n.pitch, start_beat: n.start_beat, duration_beats: n.duration_beats, velocity: n.velocity != null ? n.velocity : 0.8 })),
|
||||
bpm: parseFloat(bpm) || 120,
|
||||
preset_id: se.preset_id || undefined,
|
||||
preset_path: se.preset_path || undefined,
|
||||
preset_data: se.preset_data || undefined,
|
||||
};
|
||||
try {
|
||||
const res = await window.SonicAPI.midiRender(payload);
|
||||
if (vstiPreviewAudioRef.current) { try { vstiPreviewAudioRef.current.pause(); } catch (e) {} }
|
||||
const audio = new Audio(res.url);
|
||||
vstiPreviewAudioRef.current = audio;
|
||||
audio.play().catch(() => {});
|
||||
showToast(`Preview VSTi (${(res.duration_sec || 0).toFixed(1)}s) — ${se.plugin_id || instrumentId}`, 'success');
|
||||
} catch (err) {
|
||||
// midi-render fail (404 plugin không load / 501 pedalboard) → Carla
|
||||
try {
|
||||
await window.SonicAPI.carlaPlayNotes({ notes: payload.notes, bpm: payload.bpm, channel: se.midi_channel || 0 });
|
||||
showToast('Preview qua Carla (realtime) — nghe loa hệ thống', 'success');
|
||||
} catch (err2) {
|
||||
showToast('Preview VSTi thất bại: ' + ((err && err.message) || 'lỗi'), 'error');
|
||||
}
|
||||
}
|
||||
},
|
||||
className: "px-2.5 py-1 bg-violet-700 hover:bg-violet-600 text-white rounded text-xs flex items-center gap-1 transition font-semibold",
|
||||
title: "Render MIDI notes bằng VSTi (pedalboard) và phát — fallback qua Carla realtime"
|
||||
}, React.createElement("i", { "data-lucide": "play", className: "w-3 h-3" }), "Preview VSTi"), React.createElement("button", {
|
||||
onClick: () => {
|
||||
const se = st.synth_engine || {};
|
||||
const instrumentId = se.plugin_id || st.instrumentId || null;
|
||||
if (!instrumentId || String(se.type || '').indexOf('vst') === -1) {
|
||||
showToast('Chọn VST instrument (nút Synth) trước khi Export MIDI → Audio', 'warning');
|
||||
return;
|
||||
}
|
||||
if (!notes || !notes.length) { showToast('Chưa có nốt nhạc', 'warning'); return; }
|
||||
if (onExportMidiAudio) onExportMidiAudio(st, notes, bpm);
|
||||
else showToast('Không có handler export audio', 'error');
|
||||
},
|
||||
className: "px-2.5 py-1 bg-fuchsia-700 hover:bg-fuchsia-600 text-white rounded text-xs flex items-center gap-1 transition font-semibold",
|
||||
title: "Render MIDI notes bằng VSTi → WAV, chèn clip vào track và tải về"
|
||||
}, React.createElement("i", { "data-lucide": "audio-lines", className: "w-3 h-3" }), "Export MIDI → Audio")), React.createElement("div", {
|
||||
style: { flexBasis: "100%", height: 0 }
|
||||
}), React.createElement("button", {
|
||||
onClick: () => setArpModal({ pattern: 'UP', rate: '1/16', octaves: 2, gate: 80, triplet: false, dotted: false }),
|
||||
@@ -16038,6 +16093,61 @@ const App = () => {
|
||||
if (window.UndoRedoEngine) window.UndoRedoEngine.execute(entry);
|
||||
};
|
||||
|
||||
// Export MIDI notes → audio bằng VSTi: midi-render (pedalboard) → chèn clip
|
||||
// vào track (cùng flow insert sound clip, có undo) + download file wav.
|
||||
const handleExportMidiAudio = async (st, notes, bpm) => {
|
||||
const se = st.synth_engine || {};
|
||||
const instrumentId = se.plugin_id || st.instrumentId || null;
|
||||
if (!instrumentId || String(se.type || '').indexOf('vst') === -1) {
|
||||
showToast('Chọn VST instrument (nút Synth) trước khi Export MIDI → Audio', 'warning');
|
||||
return;
|
||||
}
|
||||
if (!notes || !notes.length) { showToast('Chưa có nốt nhạc', 'warning'); return; }
|
||||
const payload = {
|
||||
instrument_id: instrumentId,
|
||||
notes: (notes || []).map(n => ({ pitch: n.pitch, start_beat: n.start_beat, duration_beats: n.duration_beats, velocity: n.velocity != null ? n.velocity : 0.8 })),
|
||||
bpm: parseFloat(bpm) || 120,
|
||||
preset_id: se.preset_id || undefined,
|
||||
preset_path: se.preset_path || undefined,
|
||||
preset_data: se.preset_data || undefined,
|
||||
};
|
||||
try {
|
||||
const res = await window.SonicAPI.midiRender(payload);
|
||||
// Decode wav để chèn clip có buffer (phát được ngay trong track)
|
||||
const resp = await fetch(res.url);
|
||||
if (!resp.ok) throw new Error('Không tải được wav render');
|
||||
const blob = await resp.blob();
|
||||
const { audioBuffer, channelInfo } = await window.SonicAudio.decodeAudioFile(blob);
|
||||
const clipId = `clip_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
||||
const track = activeTracks.find(t => t.id === st.trackId);
|
||||
const newClip = {
|
||||
id: clipId,
|
||||
buffer: audioBuffer,
|
||||
channelInfo: channelInfo,
|
||||
startTime: currentTime || 0,
|
||||
name: `MIDI ${st.label || 'render'}`,
|
||||
speed: 1.0,
|
||||
serverFileId: res.file_id,
|
||||
};
|
||||
createClipWithUndo(st.trackId, newClip);
|
||||
updateActiveTracks(prev => prev.map(t => t.id === st.trackId ? {
|
||||
...t,
|
||||
clips: [...(t.clips || []), newClip],
|
||||
serverFileId: t.serverFileId || res.file_id,
|
||||
} : t));
|
||||
// Download wav (đã lưu trên server processed)
|
||||
const a = document.createElement('a');
|
||||
a.href = res.url;
|
||||
a.download = ((st.label || 'midi').replace(/[^a-z0-9-_]/gi, '_')) + '_vsti.wav';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
showToast(`Đã render MIDI → Audio (${(res.duration_sec || 0).toFixed(1)}s) + chèn clip`, 'success');
|
||||
} catch (err) {
|
||||
showToast('Export MIDI → Audio thất bại: ' + ((err && err.message) || 'lỗi'), 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const createClipWithUndo = (trackId, clip) => {
|
||||
const entry = {
|
||||
type: 'CREATE_CLIP',
|
||||
@@ -29309,6 +29419,7 @@ STRICT CONSTRAINTS:
|
||||
onRealtimePlay: handlePianoRollRealtimePlay,
|
||||
onCopyNotes: (n) => setPianoRollClipboard(JSON.parse(JSON.stringify(n || []))),
|
||||
clipboardNotes: pianoRollClipboard,
|
||||
onExportMidiAudio: handleExportMidiAudio,
|
||||
onRescheduleMidi: (updatedNotes) => {
|
||||
const playingSub = subTabs.find(s => s.id === activeTab && s.type === 'PIANO_ROLL' && s.isPlaying);
|
||||
if (playingSub) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -86,6 +86,12 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
|
||||
carlaMidi: (payload) => apiRequest('/api/v1/plugins/carla-midi', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Quick-render preview VSTi (âm thật = âm export, cùng code path)
|
||||
previewInstrument: (payload) => apiRequest('/api/v1/plugins/preview', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Render MIDI notes → WAV bằng VSTi (pedalboard, âm thật) — trả file_id
|
||||
// để UI preview / gán clip vào track / download
|
||||
midiRender: (payload) => apiRequest('/api/v1/plugins/midi-render', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Phát dãy MIDI notes realtime qua Carla bridge (OSC) — preview khi
|
||||
// pedalboard không render được plugin (VD VST2)
|
||||
carlaPlayNotes: (payload) => apiRequest('/api/v1/plugins/carla-play-notes', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Thư viện preset VST3 (.vstpreset) — cầu nối Carla → pedalboard
|
||||
listPresets: () => apiRequest('/api/v1/presets', { method: 'GET' }),
|
||||
uploadPreset: (file, pluginHint) => {
|
||||
|
||||
Reference in New Issue
Block a user