diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index be2239d..f61af79 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -1778,6 +1778,7 @@ const fxRt = { active: false, starting: false, startToken: 0, sessionId: null, w // new-project / export-end → teardown bình thường. let fxRtKeepForRestart = false; let fxRtWrapRestart = false; // V38: loop wrap restart -- clear_queue mode 'loop' (always-wipe + RESUME_FILL) +let fxRtSeekRestartFlag = false; // V45.18: seek khi play master FX active -- clear_queue mode 'seek' (wipe queue cu + resume nhanh 8 block ~43ms) // V21: user stop (fade=true) giữ master FX session warm — play kế tiếp có FX // ngay, không chờ reload VST ~4s. Idle timeout: ngừng chơi lâu → teardown // giải phóng CPU/VST (chấp nhận ~4s delay ở play sau khi idle lâu). @@ -11084,6 +11085,20 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos // keybed PHẢI qua mastering FX của main out khi chain đang bật // (trước đây phải bật/tắt power mới có tác dụng). try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {} + // DIAG Bug 2 (keybed ARM khong am): routing + ARM state tai note-on. + try { + let _pdDest = 'none'; + if (window.AudioRoutingEngine && window.AudioRoutingEngine._connected) { + _pdDest = (window.AudioRoutingEngine._dest && window.AudioRoutingEngine._dest.constructor) ? window.AudioRoutingEngine._dest.constructor.name : 'null'; + _pdDest += '/' + window.AudioRoutingEngine._trackId; + } else { _pdDest = 'NOT_CONNECTED'; } + const _pdD = window.BridgeAudioNode ? window.BridgeAudioNode.getDiag() : null; + const _pdTab = activeTabRef.current; + const _pdSub = subTabsRef.current ? (subTabsRef.current.find(s => s.id === _pdTab) || {}).type : null; + const _kbArmedTab = !!(subTabsRef.current || []).some(function (s) { return s.type === 'PIANO_ROLL' && s.isArmed && s.trackId === st.trackId; }); + _dbgLog('preview_diag', 'src=keybed trk=' + st.trackId + ' kbTrk=' + (kbTrk ? kbTrk.id : 'UNDEF') + ' kbArmedTab=' + (_kbArmedTab ? 1 : 0) + ' armedTrk=' + (kbTrk && kbTrk.isArmed ? 1 : 0) + ' tab=' + _pdTab + ' sub=' + (_pdSub || 'none') + ' play=' + (isPlayingRef.current ? 1 : 0) + ' br=' + (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive() ? 1 : 0) + ' fxRt=' + (fxRt.active ? 1 : 0) + ' dest=' + _pdDest + ' bd=' + (_pdD ? ('i=' + _pdD.initialized + ' wl=' + _pdD.useWorklet + ' nd=' + _pdD.hasWorkletNode + ' pre=' + _pdD.preBufferLen + ' ch=' + _pdD.chunksLen + ' g=' + _pdD.gain) : 'MISSING') + ' mb=' + (!!masterBus)); + } catch (e) {} + try { window.__dbgPreviewAudioArmed = { count: 0, nz: 0, t0: performance.now() }; } catch (e) {} if (window.triggerMidiVuActivity) { window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100); } @@ -19410,6 +19425,7 @@ const App = () => { startOffsetTimeRef.current = time; setCurrentTime(time); fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime + fxRtSeekRestartFlag = true; // V45.18: seek khi play — startTrackPlayback clear_queue mode 'seek' (wipe queue cu + resume nhanh) stopAllPlayback(); try { if (window.__fxRtPlayheadFreeze) window.__fxRtPlayheadFreeze(true); } catch (e) { } setTimeout(() => { @@ -25559,7 +25575,7 @@ const App = () => { // tail pass cu + block moi (bridge render lien tuc); flush tai seam = gap // trong → giat beat 0:0. Play that van flush (V30) de bo block idle cu. const _wrapRestart = fxRtWrapRestart; - const _wrapMode = _wrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38 + const _wrapMode = _wrapRestart ? 'loop' : (fxRtSeekRestartFlag ? 'seek' : 'play'); fxRtWrapRestart = false; fxRtSeekRestartFlag = false; // V38/V45.18 try { if (!_wrapRestart && window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {} // V31: xoa backlog outQueue master FX (block idle ~341ms) — play moi khong // cho phia sau; flush ring bridge (V30) khong chaym queue nay. diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 844f898..581dd63 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -307,6 +307,7 @@ const fxRt={active:false,starting:false,startToken:0,sessionId:null,ws:null,node // stop thật (fade=true) giữ master FX warm + idle timeout (V21); bridge-down / // new-project / export-end → teardown bình thường. let fxRtKeepForRestart=false;let fxRtWrapRestart=false;// V38: loop wrap restart -- clear_queue mode 'loop' (always-wipe + RESUME_FILL) +let fxRtSeekRestartFlag=false;// V45.18: seek khi play master FX active -- clear_queue mode 'seek' (wipe queue cu + resume nhanh 8 block ~43ms) // V21: user stop (fade=true) giữ master FX session warm — play kế tiếp có FX // ngay, không chờ reload VST ~4s. Idle timeout: ngừng chơi lâu → teardown // giải phóng CPU/VST (chấp nhận ~4s delay ở play sau khi idle lâu). @@ -339,7 +340,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=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 +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=202608311222`);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 @@ -892,7 +893,8 @@ let minR=0,maxR=1;for(let i=0;i!painted.includes(ci));if(unpainted.length>0){const updatedNotes=notes.map((n,i)=>{if(!unpainted.includes(i))return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);drag.lastPainted=[...painted,...unpainted];}};const renderKeybed=()=>{var kbTrk=activeTracks.find(function(t){return t.id===st.trackId;});var kbCtx=resolveTrackInstrumentCtx(kbTrk,activeTracks);ensureSonicInstrument(kbCtx);const keys=[];for(let pitch=127;pitch>=PITCH_START;pitch--){const isBlack=[1,3,6,8,10].includes(pitch%12);const notesArray=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const octave=Math.floor(pitch/12)-1;const label=`${notesArray[pitch%12]}${octave}`;const showLabel=pitch%12===0;keys.push(/*#__PURE__*/React.createElement("div",{key:pitch,style:{height:`${NoteHeight}px`},className:`w-[60px] shrink-0 border-b border-zinc-800 text-[9px] font-bold flex items-center justify-end pr-2 transition cursor-pointer ${activeMidiPitches&&activeMidiPitches.has(pitch)?'bg-emerald-500 text-white border-emerald-400':isBlack?'bg-zinc-950 text-slate-500 border-zinc-900 hover:bg-zinc-800':'bg-white text-zinc-800 border-r border-zinc-400 hover:bg-amber-100'}`,onMouseDown:e=>{e.stopPropagation();keybedMouseDownRef.current=true;try{// Đồng bộ mastering + routing SF NGAY trước khi preview — âm // keybed PHẢI qua mastering FX của main out khi chain đang bật // (trước đây phải bật/tắt power mới có tác dụng). -try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(er){}if(window.triggerMidiVuActivity){window.triggerMidiVuActivity((st.parent_tab_id&&st.parent_tab_id.startsWith('session_')?'_sess_':'')+st.trackId,100);}const kbNative=isStandaloneSf()&&isSfTrackEngine(kbCtx.synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive());const kbNativeVst=isStandaloneSf()&&isVstTrackEngine(kbCtx.synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive());if(kbNative){playNativeSfNote(kbTrk,pitch,100/127,5000,undefined,'kb_'+st.trackId+'_'+pitch);}else if(kbNativeVst){playNativeVstNote(kbTrk,pitch,100/127,5000,undefined,'kb_'+st.trackId+'_'+pitch);}if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(kbTrk?kbTrk.id:0,kbCtx.program,kbCtx.synthEngine);}catch(er){}window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:kbTrk?kbTrk.id:0,pitch:pitch,velocity:100/127,percussion:!!(kbCtx&&kbCtx.synthEngine&&kbCtx.synthEngine.soundfont_bank===128),live:true});}else if(window.SonicSF&&!kbNative&&!kbNativeVst){// ⚠️ FIX: giữ note theo thời gian bấm phím — durationMs lớn (5s) +try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(er){}// DIAG Bug 2 (keybed ARM khong am): routing + ARM state tai note-on. +try{let _pdDest='none';if(window.AudioRoutingEngine&&window.AudioRoutingEngine._connected){_pdDest=window.AudioRoutingEngine._dest&&window.AudioRoutingEngine._dest.constructor?window.AudioRoutingEngine._dest.constructor.name:'null';_pdDest+='/'+window.AudioRoutingEngine._trackId;}else{_pdDest='NOT_CONNECTED';}const _pdD=window.BridgeAudioNode?window.BridgeAudioNode.getDiag():null;const _pdTab=activeTabRef.current;const _pdSub=subTabsRef.current?(subTabsRef.current.find(s=>s.id===_pdTab)||{}).type:null;const _kbArmedTab=!!(subTabsRef.current||[]).some(function(s){return s.type==='PIANO_ROLL'&&s.isArmed&&s.trackId===st.trackId;});_dbgLog('preview_diag','src=keybed trk='+st.trackId+' kbTrk='+(kbTrk?kbTrk.id:'UNDEF')+' kbArmedTab='+(_kbArmedTab?1:0)+' armedTrk='+(kbTrk&&kbTrk.isArmed?1:0)+' tab='+_pdTab+' sub='+(_pdSub||'none')+' play='+(isPlayingRef.current?1:0)+' br='+(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()?1:0)+' fxRt='+(fxRt.active?1:0)+' dest='+_pdDest+' bd='+(_pdD?'i='+_pdD.initialized+' wl='+_pdD.useWorklet+' nd='+_pdD.hasWorkletNode+' pre='+_pdD.preBufferLen+' ch='+_pdD.chunksLen+' g='+_pdD.gain:'MISSING')+' mb='+!!masterBus);}catch(e){}try{window.__dbgPreviewAudioArmed={count:0,nz:0,t0:performance.now()};}catch(e){}if(window.triggerMidiVuActivity){window.triggerMidiVuActivity((st.parent_tab_id&&st.parent_tab_id.startsWith('session_')?'_sess_':'')+st.trackId,100);}const kbNative=isStandaloneSf()&&isSfTrackEngine(kbCtx.synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive());const kbNativeVst=isStandaloneSf()&&isVstTrackEngine(kbCtx.synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive());if(kbNative){playNativeSfNote(kbTrk,pitch,100/127,5000,undefined,'kb_'+st.trackId+'_'+pitch);}else if(kbNativeVst){playNativeVstNote(kbTrk,pitch,100/127,5000,undefined,'kb_'+st.trackId+'_'+pitch);}if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(kbTrk?kbTrk.id:0,kbCtx.program,kbCtx.synthEngine);}catch(er){}window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:kbTrk?kbTrk.id:0,pitch:pitch,velocity:100/127,percussion:!!(kbCtx&&kbCtx.synthEngine&&kbCtx.synthEngine.soundfont_bank===128),live:true});}else if(window.SonicSF&&!kbNative&&!kbNativeVst){// ⚠️ FIX: giữ note theo thời gian bấm phím — durationMs lớn (5s) // chỉ là auto-off phòng hờ; mouseup/mouseleave gọi stopNote dừng // NGAY (trước đây 500ms → note tự tắt giữa chừng khi giữ phím). window.SonicSF.playNote(pitch,100,5000,undefined,kbCtx.program,null,kbCtx.ch,kbCtx.synthEngine);}}catch(err){console.error('playNote error:',err);}},onMouseEnter:e=>{if(keybedMouseDownRef.current){try{if(window.triggerMidiVuActivity){window.triggerMidiVuActivity((st.parent_tab_id&&st.parent_tab_id.startsWith('session_')?'_sess_':'')+st.trackId,100);}if(isStandaloneSf()&&isSfTrackEngine(kbCtx.synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeSfNote(kbTrk,pitch,100/127,5000,undefined,'kb_'+st.trackId+'_'+pitch);}else if(isStandaloneSf()&&isVstTrackEngine(kbCtx.synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeVstNote(kbTrk,pitch,100/127,5000,undefined,'kb_'+st.trackId+'_'+pitch);}if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(kbTrk?kbTrk.id:0,kbCtx.program,kbCtx.synthEngine);}catch(er){}window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:kbTrk?kbTrk.id:0,pitch:pitch,velocity:100/127,percussion:!!(kbCtx&&kbCtx.synthEngine&&kbCtx.synthEngine.soundfont_bank===128),live:true});}else if(window.SonicSF&&!(isStandaloneSf()&&(isSfTrackEngine(kbCtx.synthEngine)||isVstTrackEngine(kbCtx.synthEngine)))){// giữ note khi kéo qua phím (mouse enter) — dừng bằng mouseup/leave @@ -1389,6 +1391,7 @@ const[prHint,setPrHint]=useState(null);React.useEffect(()=>{window.__setPrHint=h 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 +fxRtSeekRestartFlag=true;// V45.18: seek khi play — startTrackPlayback clear_queue mode 'seek' (wipe queue cu + resume nhanh) 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. @@ -1960,7 +1963,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=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 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=202608311222`);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 @@ -2098,7 +2101,7 @@ restorePendingFadeSession();// V17: huy fade dang treo — replay khong bi dip g // D18: loop WRAP (fxRtWrapRestart) — KHONG flush ring bridge: ring dang chua // tail pass cu + block moi (bridge render lien tuc); flush tai seam = gap // trong → giat beat 0:0. Play that van flush (V30) de bo block idle cu. -const _wrapRestart=fxRtWrapRestart;const _wrapMode=_wrapRestart?'loop':'play';fxRtWrapRestart=false;// V38 +const _wrapRestart=fxRtWrapRestart;const _wrapMode=_wrapRestart?'loop':fxRtSeekRestartFlag?'seek':'play';fxRtWrapRestart=false;fxRtSeekRestartFlag=false;// V38/V45.18 try{if(!_wrapRestart&&window.BridgeAudioNode&&window.BridgeAudioNode.flush)window.BridgeAudioNode.flush();}catch(e){}// V31: xoa backlog outQueue master FX (block idle ~341ms) — play moi khong // cho phia sau; flush ring bridge (V30) khong chaym queue nay. try{if(fxRt&&fxRt.node&&fxRt.node.port)fxRt.node.port.postMessage({type:'clear_queue',mode:_wrapMode});}catch(e){}_dbgPlayStartMs=performance.now();_dbgFirstAudioPending=true;_dbgFirstAudiblePending=true;_dbgLog('play_start','offset='+offsetTime+' epoch='+_nativePlayEpoch+' '+_dbgBridgeGain()+' '+_dbgWorkletState()+' fxrt='+(fxRt.active?1:0)+' node='+(fxRt.node?1:0)+' q='+(fxRt.stats?fxRt.stats.queueLen:'n/a'));// Realtime VST FX (master chain qua SHM bridge): bật khi play + chain active. diff --git a/app/static/js/sf-fx-realtime.js b/app/static/js/sf-fx-realtime.js index bd3697f..17a1899 100644 --- a/app/static/js/sf-fx-realtime.js +++ b/app/static/js/sf-fx-realtime.js @@ -138,6 +138,25 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor { this.wrapFadeBlocks = this.outQueue.length + Math.ceil((this.latencySamples || 0) / this.BLOCK); return; } + if (d.mode === 'seek') { + // V45.18: seek khi play master FX active — wipe queue cu (audio cu + // choi tiep 0-50ms sau click) + resume nhanh RESUME_FILL 8 (~43ms) + // thay vi FILL 24 (~128ms) — het "am tre 1 beat" (hit dau roi vao + // vung cam 128ms). Giong resume wrap V45.4 (proven khong gap). + this.outQueue.length = 0; + this.outPos = 0; + this.playing = false; + this.wrapFadeBlocks = -1; + this.wrapMode = false; + this.resumeFadeIn = true; + this.resumeFill = this.RESUME_FILL; + this.silenced = false; + this.fadeFromL = this.lastOutL; + this.fadeFromR = this.lastOutR; + this.holdFadeLeft = this.HOLD_FADE * this.BLOCK; + this.port.postMessage({ type: 'dbg', tag: 'fx_seek_clear', t: Math.round(currentTime * 1000), q: 0 }); + return; + } if (d.mode !== 'play' && this.outQueue.length < this.FILL) return; // V31: play/ARM live note — main thread bao xoa backlog block idle // (engine render lien tuc khi STOP -> outQueue day ~32 block ~170ms diff --git a/app/templates/index.html b/app/templates/index.html index 2f8d76e..57d217f 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -51,7 +51,7 @@ - +