diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index f101889..be2239d 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -19405,11 +19405,14 @@ const App = () => { const applyPlayheadDirect = (time) => { localSelectionAnchorRef.current = time; if (isPlaying) { + // V45.17: seek khi đang play — startOffset + freeze NGAY: playhead nhảy + // tới click + đứng tại đó (không chạy theo processed anchor cũ 50ms). + startOffsetTimeRef.current = time; setCurrentTime(time); fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime stopAllPlayback(); + try { if (window.__fxRtPlayheadFreeze) window.__fxRtPlayheadFreeze(true); } catch (e) { } setTimeout(() => { - startOffsetTimeRef.current = time; startAudioTimeRef.current = getAudioContext().currentTime; startTrackPlayback(time); setIsPlaying(true); diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index edcdc53..844f898 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -339,7 +339,7 @@ else if(fxRt.chainKey!==ck){fxRtSetChain(chain);fxRt.chainKey=ck;return;}else re // node → bypass vô hiệu. Mỗi fxRtStop() bump startToken → mọi checkpoint // dưới đây thấy mismatch → hủy session vừa tạo, không nối node. const token=++fxRt.startToken;fxRt.starting=true;const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet||!masterBus||!masterBus.output||!masterBus.analyser||!masterBus.masterFxGain||!masterBus.masterFxDryGain)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(window.currentMasteringSettings||{}).vstBypass}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT] start fail:',e);showToast('Không khởi động VST FX realtime: '+(e.message||e),'warning');fxRt.starting=false;return;}if(token!==fxRt.startToken){// Bypass/stop giữa lúc engine đang load chain — hủy session vừa tạo. -console.warn('[FX RT] start cancelled mid-flight — session stopped');try{window.SonicAPI.fxRealtimeStop({session_id:st.session_id}).catch(()=>{});}catch(e2){}fxRt.starting=false;return;}fxRt.sessionId=st.session_id;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608311143`);if(token!==fxRt.startToken){fxRt.starting=false;return;}fxRt.node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms: Chromium thu ::1 truoc (Windows khong +console.warn('[FX RT] start cancelled mid-flight — session stopped');try{window.SonicAPI.fxRealtimeStop({session_id:st.session_id}).catch(()=>{});}catch(e2){}fxRt.starting=false;return;}fxRt.sessionId=st.session_id;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608311151`);if(token!==fxRt.startToken){fxRt.starting=false;return;}fxRt.node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms: Chromium thu ::1 truoc (Windows khong // co listener IPv6 -> SYN hang ~2s) roi moi fallback 127.0.0.1. Dung // thang 127.0.0.1 (loopback) -> WS open ~5ms. Server chap nhan loopback. const wsHost=u.hostname==='localhost'||u.hostname==='::1'?'127.0.0.1'+(u.port?':'+u.port:''):u.host;const tok=localStorage.getItem('sonic_token')||'';const wsUrl=`${wsProto}${wsHost}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`;const ws=new WebSocket(wsUrl);fxRt.ws=ws;ws.binaryType='arraybuffer';ws.onopen=()=>{if(token!==fxRt.startToken){try{ws.close();}catch(e){}fxRt.starting=false;return;}fxRt.active=true;fxRt.flushing=false;fxRt.starting=false;fxRt.inQueue=[];// bỏ audio stale sản xuất trước khi WS open (~4s load VST) — hết lag khởi đầu @@ -1386,8 +1386,10 @@ const[contextMenu,setContextMenu]=useState(null);// { x, y, trackId } // window.__setPrHint (mouse in/out + Shift/Ctrl state) const[prHint,setPrHint]=useState(null);React.useEffect(()=>{window.__setPrHint=h=>setPrHint(h||null);return()=>{delete window.__setPrHint;};},[]);const clipboardRef=useRef(null);// { buffer, name, volume, color } for copy/paste // ── Undo/Redo Engine (LOOP_EDITOR.md §4 + Global Extension) ── -const[undoStack,setUndoStack]=useState([]);const[redoStack,setRedoStack]=useState([]);const MAX_UNDO=30;const pushAction=(actionType,trackId,beforeState,afterState)=>{const node={action_type:actionType,track_id:trackId,timestamp:Date.now(),before_state:beforeState,after_state:afterState};setUndoStack(prev=>{const next=[...prev,node];if(next.length>MAX_UNDO)next.shift();return next;});setRedoStack([]);};const handleUndo=()=>{if(window.UndoRedoEngine&&window.UndoRedoEngine.canUndo()){const entry=window.UndoRedoEngine.undo();if(entry){if(entry.undo&&typeof entry.undo==='function')entry.undo(entry);showToast(`Undo: ${entry.label||entry.type}`,'info');return;}}if(undoStack.length===0)return;const last=undoStack[undoStack.length-1];setUndoStack(prev=>prev.slice(0,-1));setRedoStack(prev=>[...prev,last]);applyTrackState(last.track_id,last.before_state);showToast(`Undo: ${last.action_type}`,'info');};const handleRedo=()=>{if(window.UndoRedoEngine&&window.UndoRedoEngine.canRedo()){const entry=window.UndoRedoEngine.redo();if(entry){if(entry.redo&&typeof entry.redo==='function')entry.redo(entry);showToast(`Redo: ${entry.label||entry.type}`,'info');return;}}if(redoStack.length===0)return;const last=redoStack[redoStack.length-1];setRedoStack(prev=>prev.slice(0,-1));setUndoStack(prev=>[...prev,last]);applyTrackState(last.track_id,last.after_state);showToast(`Redo: ${last.action_type}`,'info');};const applyTrackState=(trackId,state)=>{if(trackId==='ALL_TRACKS'){state.forEach(entry=>{applyTrackState(entry.trackId,entry.state);});return;}updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;var updated={...t,...state};if(state.sections!==undefined){updated.sections=state.sections;}if(state.midiItems!==undefined){updated.midiItems=state.midiItems;}if(state.clips!==undefined){updated.clips=state.clips;}return updated;}));};const getSelectedMidiItemInfo=()=>{if(!selectedItemIds||selectedItemIds.size!==1)return null;const selId=selectedItemIds.values().next().value;const tlist=activeTracks||tracks||[];for(const t of tlist){const found=(t.midiItems||[]).find(m=>m.id===selId);if(found)return{itemName:found.name,trackName:t.name,trackId:t.id,itemId:selId,notes:found.notes||[],startTime:found.startTime||0,duration:found.duration||4};}return null;};const captureTrackSnapshot=trackId=>{const track=activeTracks.find(t=>t.id===trackId);if(!track)return null;return{volumeDb:track.volumeDb,pan:track.pan,muted:track.muted,name:track.name,markers:JSON.parse(JSON.stringify(track.markers||[])),buffer:track.buffer,startTime:track.startTime||0,clips:track.clips?track.clips.map(c=>({id:c.id,buffer:c.buffer,startTime:c.startTime,name:c.name})):null,sections:track.sections?track.sections.map(function(s){return{id:s.id,start:s.start,duration:s.duration,name:s.name,color:s.color,notes:s.notes?s.notes.map(function(n){return{id:n.id,pitch:n.pitch,start_beat:n.start_beat,duration_beats:n.duration_beats,velocity:n.velocity};}):null,tracks:s.tracks?s.tracks.map(function(st){return{id:st.id,name:st.name,color:st.color,clips:st.clips?st.clips.map(function(c){return{id:c.id,startTime:c.startTime,name:c.name,speed:c.speed,buffer:c.buffer};}):null,midiItems:st.midiItems?st.midiItems.map(function(m){return{id:m.id,startTime:m.startTime,duration:m.duration,name:m.name,notes:m.notes?m.notes.map(function(n){return{id:n.id,pitch:n.pitch,start_beat:n.start_beat,duration_beats:n.duration_beats,velocity:n.velocity};}):null};}):null};}):null};}):null,midiItems:track.midiItems?track.midiItems.map(function(m){return{id:m.id,startTime:m.startTime,duration:m.duration,name:m.name,notes:m.notes?m.notes.map(function(n){return{id:n.id,pitch:n.pitch,start_beat:n.start_beat,duration_beats:n.duration_beats,velocity:n.velocity};}):null};}):null};};const captureAllTracksSnapshot=()=>{const curTracks=activeTracksRef.current||activeTracks||[];return curTracks.map(t=>({trackId:t.id,state:captureTrackSnapshot(t.id)}));};const setBpmWithUndo=newBpm=>{const oldBpm=bpmRef.current;if(String(oldBpm)===String(newBpm))return;const entry={type:'SET_BPM',scope:'global',label:`BPM ${oldBpm} → ${newBpm}`,before:oldBpm,after:newBpm,undo:e=>{setBpm(e.before);showToast(`Undo: BPM → ${e.before}`,'info');},redo:e=>{setBpm(e.after);showToast(`Redo: BPM → ${e.after}`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setBpm(String(newBpm));};const setPlayheadWithUndo=newTime=>{const oldTime=currentTime;if(Math.abs(oldTime-newTime)<0.001)return;const entry={type:'SET_PLAYHEAD',scope:'global',label:`Playhead ${formatTimeSimple(oldTime)} → ${formatTimeSimple(newTime)}`,before:oldTime,after:newTime,undo:e=>{applyPlayheadDirect(e.before);showToast(`Undo: Playhead`,'info');},redo:e=>{applyPlayheadDirect(e.after);showToast(`Redo: Playhead`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);applyPlayheadDirect(newTime);};const applyPlayheadDirect=time=>{localSelectionAnchorRef.current=time;if(isPlaying){setCurrentTime(time);fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime -stopAllPlayback();setTimeout(()=>{startOffsetTimeRef.current=time;startAudioTimeRef.current=getAudioContext().currentTime;startTrackPlayback(time);setIsPlaying(true);},50);}else{setCurrentTime(time);}};const setSelectionWithUndo=(newStart,newEnd,mode)=>{const oldStart=selectionRef.current.start;const oldEnd=selectionRef.current.end;const oldMode=selectionMode;if(oldStart===newStart&&oldEnd===newEnd&&oldMode===mode)return;const entry={type:'SET_SELECTION',scope:'global',label:`Selection`,before:{start:oldStart,end:oldEnd,mode:oldMode},after:{start:newStart,end:newEnd,mode:mode},undo:e=>{setSelectionStart(e.before.start);setSelectionEnd(e.before.end);setSelectionMode(e.before.mode);showToast(`Undo: Selection`,'info');},redo:e=>{setSelectionStart(e.after.start);setSelectionEnd(e.after.end);setSelectionMode(e.after.mode);showToast(`Redo: Selection`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setSelectionStart(newStart);setSelectionEnd(newEnd);if(mode!==undefined)setSelectionMode(mode);};const setSelectedItemsWithUndo=newSet=>{const oldSet=selectedItemIdsRef.current;if(oldSet&&newSet&&oldSet.size===newSet.size&&[...oldSet].every(x=>newSet.has(x)))return;const entry={type:'SELECT_ITEMS',scope:'global',label:`Selection`,before:[...oldSet],after:[...newSet],undo:e=>{setSelectedItemIds(new Set(e.before));showToast(`Undo: Selection`,'info');},redo:e=>{setSelectedItemIds(new Set(e.after));showToast(`Redo: Selection`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setSelectedItemIds(newSet);};const setAiPromptWithUndo=newText=>{const oldText=aiPrompt;if(oldText===newText)return;const entry={type:'SET_AI_PROMPT',scope:'global',label:`AI Prompt`,before:oldText,after:newText,undo:e=>{setAiPrompt(e.before);showToast(`Undo: AI Prompt`,'info');},redo:e=>{setAiPrompt(e.after);showToast(`Redo: AI Prompt`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setAiPrompt(newText);};// Gán preset VST3 (.vstpreset từ thư viện storage/presets) vào track: +const[undoStack,setUndoStack]=useState([]);const[redoStack,setRedoStack]=useState([]);const MAX_UNDO=30;const pushAction=(actionType,trackId,beforeState,afterState)=>{const node={action_type:actionType,track_id:trackId,timestamp:Date.now(),before_state:beforeState,after_state:afterState};setUndoStack(prev=>{const next=[...prev,node];if(next.length>MAX_UNDO)next.shift();return next;});setRedoStack([]);};const handleUndo=()=>{if(window.UndoRedoEngine&&window.UndoRedoEngine.canUndo()){const entry=window.UndoRedoEngine.undo();if(entry){if(entry.undo&&typeof entry.undo==='function')entry.undo(entry);showToast(`Undo: ${entry.label||entry.type}`,'info');return;}}if(undoStack.length===0)return;const last=undoStack[undoStack.length-1];setUndoStack(prev=>prev.slice(0,-1));setRedoStack(prev=>[...prev,last]);applyTrackState(last.track_id,last.before_state);showToast(`Undo: ${last.action_type}`,'info');};const handleRedo=()=>{if(window.UndoRedoEngine&&window.UndoRedoEngine.canRedo()){const entry=window.UndoRedoEngine.redo();if(entry){if(entry.redo&&typeof entry.redo==='function')entry.redo(entry);showToast(`Redo: ${entry.label||entry.type}`,'info');return;}}if(redoStack.length===0)return;const last=redoStack[redoStack.length-1];setRedoStack(prev=>prev.slice(0,-1));setUndoStack(prev=>[...prev,last]);applyTrackState(last.track_id,last.after_state);showToast(`Redo: ${last.action_type}`,'info');};const applyTrackState=(trackId,state)=>{if(trackId==='ALL_TRACKS'){state.forEach(entry=>{applyTrackState(entry.trackId,entry.state);});return;}updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;var updated={...t,...state};if(state.sections!==undefined){updated.sections=state.sections;}if(state.midiItems!==undefined){updated.midiItems=state.midiItems;}if(state.clips!==undefined){updated.clips=state.clips;}return updated;}));};const getSelectedMidiItemInfo=()=>{if(!selectedItemIds||selectedItemIds.size!==1)return null;const selId=selectedItemIds.values().next().value;const tlist=activeTracks||tracks||[];for(const t of tlist){const found=(t.midiItems||[]).find(m=>m.id===selId);if(found)return{itemName:found.name,trackName:t.name,trackId:t.id,itemId:selId,notes:found.notes||[],startTime:found.startTime||0,duration:found.duration||4};}return null;};const captureTrackSnapshot=trackId=>{const track=activeTracks.find(t=>t.id===trackId);if(!track)return null;return{volumeDb:track.volumeDb,pan:track.pan,muted:track.muted,name:track.name,markers:JSON.parse(JSON.stringify(track.markers||[])),buffer:track.buffer,startTime:track.startTime||0,clips:track.clips?track.clips.map(c=>({id:c.id,buffer:c.buffer,startTime:c.startTime,name:c.name})):null,sections:track.sections?track.sections.map(function(s){return{id:s.id,start:s.start,duration:s.duration,name:s.name,color:s.color,notes:s.notes?s.notes.map(function(n){return{id:n.id,pitch:n.pitch,start_beat:n.start_beat,duration_beats:n.duration_beats,velocity:n.velocity};}):null,tracks:s.tracks?s.tracks.map(function(st){return{id:st.id,name:st.name,color:st.color,clips:st.clips?st.clips.map(function(c){return{id:c.id,startTime:c.startTime,name:c.name,speed:c.speed,buffer:c.buffer};}):null,midiItems:st.midiItems?st.midiItems.map(function(m){return{id:m.id,startTime:m.startTime,duration:m.duration,name:m.name,notes:m.notes?m.notes.map(function(n){return{id:n.id,pitch:n.pitch,start_beat:n.start_beat,duration_beats:n.duration_beats,velocity:n.velocity};}):null};}):null};}):null};}):null,midiItems:track.midiItems?track.midiItems.map(function(m){return{id:m.id,startTime:m.startTime,duration:m.duration,name:m.name,notes:m.notes?m.notes.map(function(n){return{id:n.id,pitch:n.pitch,start_beat:n.start_beat,duration_beats:n.duration_beats,velocity:n.velocity};}):null};}):null};};const captureAllTracksSnapshot=()=>{const curTracks=activeTracksRef.current||activeTracks||[];return curTracks.map(t=>({trackId:t.id,state:captureTrackSnapshot(t.id)}));};const setBpmWithUndo=newBpm=>{const oldBpm=bpmRef.current;if(String(oldBpm)===String(newBpm))return;const entry={type:'SET_BPM',scope:'global',label:`BPM ${oldBpm} → ${newBpm}`,before:oldBpm,after:newBpm,undo:e=>{setBpm(e.before);showToast(`Undo: BPM → ${e.before}`,'info');},redo:e=>{setBpm(e.after);showToast(`Redo: BPM → ${e.after}`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setBpm(String(newBpm));};const setPlayheadWithUndo=newTime=>{const oldTime=currentTime;if(Math.abs(oldTime-newTime)<0.001)return;const entry={type:'SET_PLAYHEAD',scope:'global',label:`Playhead ${formatTimeSimple(oldTime)} → ${formatTimeSimple(newTime)}`,before:oldTime,after:newTime,undo:e=>{applyPlayheadDirect(e.before);showToast(`Undo: Playhead`,'info');},redo:e=>{applyPlayheadDirect(e.after);showToast(`Redo: Playhead`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);applyPlayheadDirect(newTime);};const applyPlayheadDirect=time=>{localSelectionAnchorRef.current=time;if(isPlaying){// V45.17: seek khi đang play — startOffset + freeze NGAY: playhead nhảy +// tới click + đứng tại đó (không chạy theo processed anchor cũ 50ms). +startOffsetTimeRef.current=time;setCurrentTime(time);fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime +stopAllPlayback();try{if(window.__fxRtPlayheadFreeze)window.__fxRtPlayheadFreeze(true);}catch(e){}setTimeout(()=>{startAudioTimeRef.current=getAudioContext().currentTime;startTrackPlayback(time);setIsPlaying(true);},50);}else{setCurrentTime(time);}};const setSelectionWithUndo=(newStart,newEnd,mode)=>{const oldStart=selectionRef.current.start;const oldEnd=selectionRef.current.end;const oldMode=selectionMode;if(oldStart===newStart&&oldEnd===newEnd&&oldMode===mode)return;const entry={type:'SET_SELECTION',scope:'global',label:`Selection`,before:{start:oldStart,end:oldEnd,mode:oldMode},after:{start:newStart,end:newEnd,mode:mode},undo:e=>{setSelectionStart(e.before.start);setSelectionEnd(e.before.end);setSelectionMode(e.before.mode);showToast(`Undo: Selection`,'info');},redo:e=>{setSelectionStart(e.after.start);setSelectionEnd(e.after.end);setSelectionMode(e.after.mode);showToast(`Redo: Selection`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setSelectionStart(newStart);setSelectionEnd(newEnd);if(mode!==undefined)setSelectionMode(mode);};const setSelectedItemsWithUndo=newSet=>{const oldSet=selectedItemIdsRef.current;if(oldSet&&newSet&&oldSet.size===newSet.size&&[...oldSet].every(x=>newSet.has(x)))return;const entry={type:'SELECT_ITEMS',scope:'global',label:`Selection`,before:[...oldSet],after:[...newSet],undo:e=>{setSelectedItemIds(new Set(e.before));showToast(`Undo: Selection`,'info');},redo:e=>{setSelectedItemIds(new Set(e.after));showToast(`Redo: Selection`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setSelectedItemIds(newSet);};const setAiPromptWithUndo=newText=>{const oldText=aiPrompt;if(oldText===newText)return;const entry={type:'SET_AI_PROMPT',scope:'global',label:`AI Prompt`,before:oldText,after:newText,undo:e=>{setAiPrompt(e.before);showToast(`Undo: AI Prompt`,'info');},redo:e=>{setAiPrompt(e.after);showToast(`Redo: AI Prompt`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setAiPrompt(newText);};// Gán preset VST3 (.vstpreset từ thư viện storage/presets) vào track: // preset_id nằm trong synth_engine → render engine tải qua load_preset khi // render → âm render = âm đã chỉnh trong preset. const setTrackPreset=(trackId,presetId)=>{if(!trackId||!presetId)return;var mt=activeTracksRef.current||tracks;var cur=null;for(var ci=0;ciprev.map(t=>t.id===trackId?{...t,synth_engine:{...(t.synth_engine||{}),preset_id:presetId}}:t));setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);var p=(window.SonicRuntime&&window.SonicRuntime.presets||[]).find(function(x){return x.id===presetId;});showToast('Đã gán preset: '+(p&&p.name||presetId),'success');};const setTrackInstrumentWithUndo=(trackId,instrumentId,displayName,bankNumber,programNumber)=>{let track=activeTracks.find(t=>t.id===trackId);if(!track&&sessionTabsRef&&sessionTabsRef.current){for(let _ti=0;_tit.id===trackId);}}if(!track)return;const oldInstrumentId=track.instrumentId;const oldInstrumentName=track.instrumentName;if(oldInstrumentId===instrumentId&&oldInstrumentName===displayName)return;const entry={type:'SET_INSTRUMENT',scope:'track:'+trackId,label:`Instrument ${track.name}`,before:{instrumentId:oldInstrumentId,instrumentName:oldInstrumentName,bankNumber:track.soundfont_bank,programNumber:track.instrumentProgram},after:{instrumentId,instrumentName:displayName,bankNumber,programNumber},undo:e=>{setTrackInstrumentWithProgram(trackId,e.before.instrumentId,e.before.programNumber,e.before.instrumentName,e.before.bankNumber);showToast(`Undo: Instrument`,'info');},redo:e=>{setTrackInstrumentWithProgram(trackId,e.after.instrumentId,e.after.programNumber,e.after.instrumentName,e.after.bankNumber);showToast(`Redo: Instrument`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);setTrackInstrumentWithProgram(trackId,instrumentId,programNumber,displayName,bankNumber);};const createSectionWithUndo=(trackId,section)=>{const entry={type:'CREATE_SECTION',scope:'track:'+trackId,label:`Create Section`,before:null,after:section,undo:e=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,sections:(t.sections||[]).filter(s=>s.id!==e.after.id)}:t));showToast(`Undo: Create Section`,'info');},redo:e=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,sections:[...(t.sections||[]),e.after]}:t));showToast(`Redo: Create Section`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);};const createMidiWithUndo=(trackId,midiItem)=>{const entry={type:'CREATE_MIDI',scope:'track:'+trackId,label:`Create MIDI Item`,before:null,after:midiItem,undo:e=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,midiItems:(t.midiItems||[]).filter(m=>m.id!==e.after.id)}:t));showToast(`Undo: Create MIDI`,'info');},redo:e=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,midiItems:[...(t.midiItems||[]),e.after]}:t));showToast(`Redo: Create MIDI`,'info');}};if(window.UndoRedoEngine)window.UndoRedoEngine.execute(entry);};// Export MIDI notes → audio bằng VSTi: midi-render (pedalboard) → chèn clip @@ -1958,7 +1960,7 @@ fxRtTrackReconcile(trackId,node,track);}catch(e){console.warn('rebuildTrackFxGra // Cần activeTrackNodesRef/isPlayingRef. Session giống master (fxRtStart): // fetch start → AudioWorkletNode 'sf-fx-realtime' → WS; WS open → splice // worklet vào chain hiện tại (đọc mods MỚI NHẤT — an toàn với rebuild). -const fxRtTrackStart=async(trackId,track,path)=>{const key=trackId+':'+path;const chain=fxRtTrackChain(track);if(!chain.length)return;if(fxRtTracks[key]){if(!fxRtTracks[key].node||fxRtTracks[key].node.context!==getAudioContext())fxRtTrackStop(trackId,path,true);else return;}const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(track&&track.vstFxBypass)}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT track] start fail',trackId,path,e);return;}const entry={sessionId:st.session_id,ws:null,node:null,ready:false,path,spliced:null,chainKey:fxRtTrackChainKey(chain)+'|vb:'+(track&&track.vstFxBypass?'1':'0'),paramsBySlot:chain.map(s=>fxRtEntryParams(s,null)),inQueue:[],pumpTimer:null,lastInSent:0,flushing:false,flushTimer:null};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608311143`);const node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});entry.node=node;const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms (Chromium thu ::1 truoc roi fallback) - dung 127.0.0.1 +const fxRtTrackStart=async(trackId,track,path)=>{const key=trackId+':'+path;const chain=fxRtTrackChain(track);if(!chain.length)return;if(fxRtTracks[key]){if(!fxRtTracks[key].node||fxRtTracks[key].node.context!==getAudioContext())fxRtTrackStop(trackId,path,true);else return;}const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(track&&track.vstFxBypass)}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT track] start fail',trackId,path,e);return;}const entry={sessionId:st.session_id,ws:null,node:null,ready:false,path,spliced:null,chainKey:fxRtTrackChainKey(chain)+'|vb:'+(track&&track.vstFxBypass?'1':'0'),paramsBySlot:chain.map(s=>fxRtEntryParams(s,null)),inQueue:[],pumpTimer:null,lastInSent:0,flushing:false,flushTimer:null};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608311151`);const node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});entry.node=node;const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms (Chromium thu ::1 truoc roi fallback) - dung 127.0.0.1 const wsHost=u.hostname==='localhost'||u.hostname==='::1'?'127.0.0.1'+(u.port?':'+u.port:''):u.host;const tok=localStorage.getItem('sonic_token')||'';const ws=new WebSocket(`${wsProto}${wsHost}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`);entry.ws=ws;ws.binaryType='arraybuffer';ws.onopen=()=>{entry.ready=true;entry.inQueue=[];// bỏ audio stale trước khi WS open — hết lag khởi đầu fxRtTrackSplice(trackId,path);try{(entry.paramsBySlot||[]).forEach((pl,slot)=>pl.forEach(p=>ws.send(JSON.stringify({cmd:'set_param',slot,key:p.key,value:p.value}))));}catch(e){}};ws.onmessage=ev=>{if(typeof ev.data==='string'){try{const d=JSON.parse(ev.data);if(d&&d.cmd==='flush_done'){entry.flushing=false;if(entry.flushTimer){clearTimeout(entry.flushTimer);entry.flushTimer=null;}if(entry.node)entry.node.port.postMessage({type:'flush_done'});}}catch(e){}return;}if(!(ev.data instanceof ArrayBuffer)||!entry.node)return;if(entry.flushing)return;// V24: output stale trong luc flush — drop const f=new Float32Array(ev.data);const half=f.length/2;const l=new Float32Array(half);const r=new Float32Array(half);for(let i=0;ifxRtTrackStop(trackId,path,true);ws.onerror=()=>{try{ws.close();}catch(e){}};entry.node.port.onmessage=ev=>{const d=ev.data;if(!d||!entry.ws)return;if(d.type==='flush'){// V24: nhu master — worklet underrun-resume → flush engine ring diff --git a/app/templates/index.html b/app/templates/index.html index 72ac1fc..2f8d76e 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -51,7 +51,7 @@ - +