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:
2026-08-10 18:46:54 +07:00
parent 67ad2b8e4b
commit 61cb4b846f
33 changed files with 1097 additions and 103 deletions
+113 -2
View File
@@ -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 dng cái cũ khi play cái mi
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 vi â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 bng 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) {