diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 1a14383..41025b0 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -18943,6 +18943,7 @@ const App = () => { // (setTimeout 60ms restart) thay trang thai PRE-solo -> Qin note van fire // (tremolo). Render ke tiep tu dong dong bo lai ref. activeTracksRef.current = effective; + try { if (window.__dbgLog) window.__dbgLog('solo_apply', 'track=' + trackId + ' patch=' + (patch ? JSON.stringify(patch) : '-') + ' n=' + effective.length + ' solo=[' + effective.filter(function (t) { return t.solo; }).map(function (t) { return t.id; }).join(',') + '] muted=[' + effective.filter(function (t) { return t.muted; }).map(function (t) { return t.id; }).join(',') + ']'); } catch (e) {} let becameAudible = false; effective.forEach(t => { const audible = computeTrackAudibleGain(effective, t) > 0; @@ -18975,6 +18976,7 @@ const App = () => { if (!audible && wasAudible && isMidiSoundTrack(t)) { try { const _isPerc = !!(t.synth_engine && t.synth_engine.soundfont_bank === 128) || !!t.is_percussion; + try { if (window.__dbgLog) window.__dbgLog('solo_kill', 'track=' + t.id + ' tset=' + (((timelinePitchesRef.current || {})[t.id] || new Set()).size) + ' held=' + ((activeMidiPitchesRef.current || new Set()).size)); } catch (e) {} const _held = activeMidiPitchesRef.current || new Set(); // D18c: timeline MIDI notes (per-track) dang keu - NOTE_OFF tung pitch; // truoc day chi kill preview keyboard (activeMidiPitchesRef) -> note @@ -25425,7 +25427,7 @@ const App = () => { // vua tro nen inaudible (solo track khac / mute) khong duoc phat ra. const _list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : (tracksRef.current && tracksRef.current.length ? tracksRef.current : activeTracks); const _trk = _list.find(t => t.id === trkId) || track; - if (!_trk || computeTrackAudibleGain(_list, _trk) <= 0) return; + if (!_trk || computeTrackAudibleGain(_list, _trk) <= 0) { try { if (window.__dbgLog) window.__dbgLog('solo_guard', 'block ch=' + trkId + ' p=' + (pitch || 60)); } catch (e) {} return; } // D18c: track pitch dang keu (timeline) - de applyAllTrackMuteSolo kill // dung note khi track thanh inaudible giua chung (Qin VST2 tremolo). try { var _tps = timelinePitchesRef.current; (_tps[trkId] || (_tps[trkId] = new Set())).add(pitch || 60); } catch (e) {} @@ -25497,6 +25499,7 @@ const App = () => { // FX (khi bật). try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {} const allPlayTracks = activeTracksRef.current && activeTracksRef.current.length ? activeTracksRef.current : activeTracks; + try { if (window.__dbgLog) window.__dbgLog('play_tracks', 'n=' + allPlayTracks.length + ' solo=' + (allPlayTracks.some(function (t) { return t.solo; }) ? 1 : 0) + ' ids=' + allPlayTracks.map(function (t) { return t.id + ':' + (t.solo ? 'S' : t.muted ? 'M' : 'A'); }).join(',')); } catch (e) {} // Capture items signature at schedule time — updatePlayhead so sánh với // signature này để phát hiện items đổi vị trí giữa lúc play (re-schedule). // V28: defer bookkeeping signature — tính đồng bộ chặn main thread trước @@ -28088,6 +28091,7 @@ const App = () => { // ── Track Controls ── const toggleTrackSoloEvaluate = trackId => { const wasPlaying = isPlaying; + try { if (window.__dbgLog) window.__dbgLog('solo_toggle', 'track=' + trackId + ' wasPlaying=' + (wasPlaying ? 1 : 0) + ' refSolo=' + ((activeTracksRef.current || []).find(function (t) { return t.id === trackId; }) || {}).solo + ' n=' + (activeTracksRef.current || []).length); } catch (e) {} const playingSubTab = subTabs.find(s => s.isPlaying && s.type === 'PIANO_ROLL'); fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime stopAllPlayback(); diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 9ac3c5d..d4a8fea 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=202608291752`);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=202608291806`);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 @@ -1326,7 +1326,7 @@ const applyAllTrackMuteSolo=(trackId,patch)=>{const list=activeTracksRef.current // audible -> D18b guard (25423) + scheduler (25498) + toggleTrackSoloEvaluate // (setTimeout 60ms restart) thay trang thai PRE-solo -> Qin note van fire // (tremolo). Render ke tiep tu dong dong bo lai ref. -activeTracksRef.current=effective;let becameAudible=false;effective.forEach(t=>{const audible=computeTrackAudibleGain(effective,t)>0;const wasAudible=trackAudibleRef.current[t.id]!==false;const node=activeTrackNodesRef.current[t.id];const audibleGain=computeTrackAudibleGain(effective,t);if(node)setTrackNodeGain(node,audibleGain);// Section sub-nodes (key: _sub_) — mute/solo của +activeTracksRef.current=effective;try{if(window.__dbgLog)window.__dbgLog('solo_apply','track='+trackId+' patch='+(patch?JSON.stringify(patch):'-')+' n='+effective.length+' solo=['+effective.filter(function(t){return t.solo;}).map(function(t){return t.id;}).join(',')+'] muted=['+effective.filter(function(t){return t.muted;}).map(function(t){return t.id;}).join(',')+']');}catch(e){}let becameAudible=false;effective.forEach(t=>{const audible=computeTrackAudibleGain(effective,t)>0;const wasAudible=trackAudibleRef.current[t.id]!==false;const node=activeTrackNodesRef.current[t.id];const audibleGain=computeTrackAudibleGain(effective,t);if(node)setTrackNodeGain(node,audibleGain);// Section sub-nodes (key: _sub_) — mute/solo của // SECTION-TAB phải áp gain cho node play section — CHỈ trong section // (activeTracksRef = tracks section), KHÔNG đụng track MAIN. Object.keys(activeTrackNodesRef.current).forEach(function(k){if(k.endsWith('_sub_'+t.id)){const sn=activeTrackNodesRef.current[k];if(sn&&sn.gainNode)setTrackNodeGain(sn,audibleGain);}});if(node&&node.sfEntry){try{node.sfEntry.gain.setTargetAtTime(audibleGain,getAudioContext().currentTime,0.02);}catch(e){}}// MIDI/bridge tracks: CC7 (channel volume) realtime — even for notes @@ -1336,7 +1336,7 @@ if(isMidiSoundTrack(t)){try{sendMidiVolumeCC7(t,effective,audible);}catch(e){}}/ // (sendMidiVolumeCC7) không ăn với VST như Qin (bỏ qua channel volume) // → note kêu tiếp (tremolo) khi solo track khác. NOTE_OFF từng pitch đang // giữ + CC123/CC120 all-notes-off trên channel track đó. -if(!audible&&wasAudible&&isMidiSoundTrack(t)){try{const _isPerc=!!(t.synth_engine&&t.synth_engine.soundfont_bank===128)||!!t.is_percussion;const _held=activeMidiPitchesRef.current||new Set();// D18c: timeline MIDI notes (per-track) dang keu - NOTE_OFF tung pitch; +if(!audible&&wasAudible&&isMidiSoundTrack(t)){try{const _isPerc=!!(t.synth_engine&&t.synth_engine.soundfont_bank===128)||!!t.is_percussion;try{if(window.__dbgLog)window.__dbgLog('solo_kill','track='+t.id+' tset='+((timelinePitchesRef.current||{})[t.id]||new Set()).size+' held='+(activeMidiPitchesRef.current||new Set()).size);}catch(e){}const _held=activeMidiPitchesRef.current||new Set();// D18c: timeline MIDI notes (per-track) dang keu - NOTE_OFF tung pitch; // truoc day chi kill preview keyboard (activeMidiPitchesRef) -> note // timeline (Qin VST2) cu ngan/loop (tremolo) khi solo track khac. const _tSet=(timelinePitchesRef.current||{})[t.id];if(_tSet&&_tSet.size){_tSet.forEach(function(_p){window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:t.id,pitch:_p,velocity:0,percussion:_isPerc,live:true});});}_held.forEach(function(_p){window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:t.id,pitch:_p,velocity:0,percussion:_isPerc,live:true});});window.SonicMidiRouter.pushEvent({cmd:'CC',channel:t.id,pitch:123,velocity:0,data2:0,percussion:_isPerc});window.SonicMidiRouter.pushEvent({cmd:'CC',channel:t.id,pitch:120,velocity:0,data2:0,percussion:_isPerc});if(heldMidiNotesRef.current)delete heldMidiNotesRef.current[t.id];if(timelinePitchesRef.current)delete timelinePitchesRef.current[t.id];}catch(e){}}if(audible&&!wasAudible)becameAudible=true;trackAudibleRef.current[t.id]=audible;});// A track that just became audible may have had its audio sources dropped @@ -1936,7 +1936,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=202608291752`);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=202608291806`);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 @@ -2060,7 +2060,7 @@ const isPerc=!!(synthEngine&&synthEngine.soundfont_bank===128);// D8: track đ if(program!==undefined&&program!==null){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(trkId,program,synthEngine);}catch(e){}}// V11: epoch guard — hủy timer cũ khi stop/loop restart (xem ref). const epoch=bridgeNoteEpochRef.current;setTimeout(function(){if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;// D18b: mute/solo guard tai thoi diem fire — note-on dang cho cua track // vua tro nen inaudible (solo track khac / mute) khong duoc phat ra. -const _list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracksRef.current&&tracksRef.current.length?tracksRef.current:activeTracks;const _trk=_list.find(t=>t.id===trkId)||track;if(!_trk||computeTrackAudibleGain(_list,_trk)<=0)return;// D18c: track pitch dang keu (timeline) - de applyAllTrackMuteSolo kill +const _list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracksRef.current&&tracksRef.current.length?tracksRef.current:activeTracks;const _trk=_list.find(t=>t.id===trkId)||track;if(!_trk||computeTrackAudibleGain(_list,_trk)<=0){try{if(window.__dbgLog)window.__dbgLog('solo_guard','block ch='+trkId+' p='+(pitch||60));}catch(e){}return;}// D18c: track pitch dang keu (timeline) - de applyAllTrackMuteSolo kill // dung note khi track thanh inaudible giua chung (Qin VST2 tremolo). try{var _tps=timelinePitchesRef.current;(_tps[trkId]||(_tps[trkId]=new Set())).add(pitch||60);}catch(e){}var _rt=window.SonicMidiRouter;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:trkId,pitch:pitch||60,velocity:velocity||0.8,percussion:isPerc});}catch(e){console.warn('[Midi] pushEvent error:',e&&e.message);}},delayMs);scheduleNoteOff('B:'+trkId+':'+(pitch||60),function(){if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:trkId,pitch:pitch||60,velocity:0,percussion:isPerc});}catch(e){}// D18c: xoa khoi per-track set (note da tat - khong con can kill). try{var _tps2=timelinePitchesRef.current;if(_tps2[trkId])_tps2[trkId].delete(pitch||60);}catch(e){}},delayMs,durMs||1000);return;}// Native VSTi (standalone browser, bridge inactive) → midi-render thay vì @@ -2089,7 +2089,7 @@ if(bridgeStopRetryTimerRef.current){clearTimeout(bridgeStopRetryTimerRef.current isPlayingRef.current=true;// Gap 7: master volume automation — schedule envelope tu playhead. try{applyMasterAutomationSchedule(offsetTime||0);}catch(e){console.warn('[Auto] schedule error:',e);}// ⚠️ FIX: đồng bộ mastering NGAY khi play — MIDI item phải qua mastering // FX (khi bật). -try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(e){}const allPlayTracks=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:activeTracks;// Capture items signature at schedule time — updatePlayhead so sánh với +try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(e){}const allPlayTracks=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:activeTracks;try{if(window.__dbgLog)window.__dbgLog('play_tracks','n='+allPlayTracks.length+' solo='+(allPlayTracks.some(function(t){return t.solo;})?1:0)+' ids='+allPlayTracks.map(function(t){return t.id+':'+(t.solo?'S':t.muted?'M':'A');}).join(','));}catch(e){}// Capture items signature at schedule time — updatePlayhead so sánh với // signature này để phát hiện items đổi vị trí giữa lúc play (re-schedule). // V28: defer bookkeeping signature — tính đồng bộ chặn main thread trước // first-audio (góp vào delay onset); signature chỉ so sánh ~10fps trong @@ -2325,7 +2325,7 @@ useEffect(()=>{const handleMouseMove=e=>{const resize=resizedSectionItemRef.curr if(activeTabRef.current&&activeTabRef.current.startsWith('session_')){const _tabId=activeTabRef.current;setTimeout(()=>{try{syncSectionTabToMain(_tabId);}catch(e){}},0);}};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);// ── Sweep Select mousemove/mouseup ── useEffect(()=>{const handleMouseMove=e=>{if(!isSweepingRef.current)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);setSweepSelect(prev=>{var updated=prev?{...prev,endTime:time}:null;sweepSelectRef.current=updated;return updated;});};const handleMouseUp=()=>{if(!isSweepingRef.current)return;isSweepingRef.current=false;sweepTrackIdRef.current=null;const sweep=sweepSelectRef.current;sweepSelectRef.current=null;captureSelectionUndo();if(sweep){const start=Math.min(sweep.startTime,sweep.endTime);const end=Math.max(sweep.startTime,sweep.endTime);if(Math.abs(end-start)<0.02){setSelectedItemIds(new Set());setSweepSelect(null);pushSelectionUndo();return;}const curTracks=activeTracksRef.current||[];const found=new Set();curTracks.forEach(t=>{(t.midiItems||[]).forEach(m=>{if(m.startTimestart){found.add(m.id);}});(t.sections||[]).forEach(s=>{if(s.startstart){found.add(s.id);}});(t.clips||[]).forEach(c=>{const dur=c.buffer?c.buffer.duration/(c.speed||1.0):4;if(c.startTimestart){const cid=c.id==='default'?'default_'+t.id:c.id;found.add(cid);}});});setSelectedItemIds(prev=>{const next=new Set(prev);found.forEach(id=>{if(next.has(id))next.delete(id);else next.add(id);});return next;});setSweepSelect(null);sweepTrackIdRef.current=null;pushSelectionUndo();}};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom]);const handleSelectRange=(start,end,reset)=>{captureSelectionUndo();const maxLen=maxDuration;const cleanStart=Math.max(0,Math.min(maxLen,start));const cleanEnd=Math.max(0,Math.min(maxLen,end));if(reset){setSelectionStart(cleanStart);setSelectionEnd(cleanEnd);}else{setSelectionEnd(cleanEnd);}setSelectionCleared(false);pushSelectionUndo();};const handleSelectionInputChange=(field,val)=>{captureSelectionUndo();const numericVal=Math.max(0,parseFloat(val)||0);if(selectionMode==='local'){if(field==='start'){setLocalSelectionStart(numericVal);}else{setLocalSelectionEnd(numericVal);}}else{if(field==='start'){setSelectionStart(numericVal);}else{setSelectionEnd(numericVal);}}pushSelectionUndo();};const selectionStats=useMemo(()=>{if(selLeft===null||selRight===null){return{start:0,end:0,length:0};}const s=Math.min(selLeft,selRight);const e=Math.max(selLeft,selRight);return{start:parseFloat(s.toFixed(3)),end:parseFloat(e.toFixed(3)),length:parseFloat((e-s).toFixed(3))};},[selLeft,selRight]);// ── Handle Drag (selection resize) ── const handleHandleDragStart=(e,side)=>{e.preventDefault();e.stopPropagation();const startX=e.clientX;const useLocal=selectionMode==='local';const currentStart=useLocal?localSelectionStart:selectionStart;const currentEnd=useLocal?localSelectionEnd:selectionEnd;const initialLeft=Math.min(currentStart,currentEnd);const initialRight=Math.max(currentStart,currentEnd);const setStart=useLocal?setLocalSelectionStart:setSelectionStart;const setEnd=useLocal?setLocalSelectionEnd:setSelectionEnd;const handleMouseMove=moveEvent=>{const deltaX=moveEvent.clientX-startX;const deltaSec=deltaX/zoom;if(side==='left'){const newLeft=Math.max(0,Math.min(initialRight-0.05,initialLeft+deltaSec));setStart(newLeft);setEnd(initialRight);}else{const newRight=Math.max(initialLeft+0.05,Math.min(maxDuration,initialRight+deltaSec));setStart(initialLeft);setEnd(newRight);}};const handleMouseUp=()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);};const handleSelectionBodyDragStart=e=>{e.preventDefault();e.stopPropagation();const startX=e.clientX;const useLocal=selectionMode==='local';const currentStart=useLocal?localSelectionStart:selectionStart;const currentEnd=useLocal?localSelectionEnd:selectionEnd;const initialLeft=Math.min(currentStart,currentEnd);const initialRight=Math.max(currentStart,currentEnd);const widthSec=initialRight-initialLeft;const setStart=useLocal?setLocalSelectionStart:setSelectionStart;const setEnd=useLocal?setLocalSelectionEnd:setSelectionEnd;const handleMouseMove=moveEvent=>{const deltaX=moveEvent.clientX-startX;const deltaSec=deltaX/zoom;let newLeft=initialLeft+deltaSec;let newRight=initialRight+deltaSec;if(newLeft<0){newLeft=0;newRight=widthSec;}if(newRight>maxDuration){newRight=maxDuration;newLeft=maxDuration-widthSec;}setStart(newLeft);setEnd(newRight);};const handleMouseUp=()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);};// ── Track Controls ── -const toggleTrackSoloEvaluate=trackId=>{const wasPlaying=isPlaying;const playingSubTab=subTabs.find(s=>s.isPlaying&&s.type==='PIANO_ROLL');fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime +const toggleTrackSoloEvaluate=trackId=>{const wasPlaying=isPlaying;try{if(window.__dbgLog)window.__dbgLog('solo_toggle','track='+trackId+' wasPlaying='+(wasPlaying?1:0)+' refSolo='+((activeTracksRef.current||[]).find(function(t){return t.id===trackId;})||{}).solo+' n='+(activeTracksRef.current||[]).length);}catch(e){}const playingSubTab=subTabs.find(s=>s.isPlaying&&s.type==='PIANO_ROLL');fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime stopAllPlayback();updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,solo:!t.solo,muted:false}:t));setTimeout(()=>{const soloCtx=getAudioContext();if(wasPlaying){startOffsetTimeRef.current=currentTime;startAudioTimeRef.current=soloCtx.currentTime;startBufferOffsetRef.current=currentTime;setIsPlaying(true);startTrackPlayback(currentTime);}if(playingSubTab){const prSt=subTabs.find(s=>s.id===playingSubTab.id);if(prSt){const prOffset=prSt.currentTime||0;startOffsetTimeRef.current=prOffset;startAudioTimeRef.current=soloCtx.currentTime;startBufferOffsetRef.current=prOffset*(prSt.speed||1.0);schedulePianoRollMidi(prSt,prOffset);startSubTabPlayback(prSt,prOffset);setSubTabs(prev=>prev.map(s=>s.id===prSt.id?Object.assign({},s,{isPlaying:true,currentTime:prOffset}):s));}}},60);setTimeout(()=>lucide.createIcons(),50);};const toggleTrackMute=trackId=>{const beforeSnap=captureTrackSnapshot(trackId);updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,muted:!t.muted}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'MUTE',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});setTimeout(()=>lucide.createIcons(),50);};const updateTrackProp=(trackId,props)=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,...props}:t));// Mixer fader dùng updateTrackProp({volumeDb}) — áp gain realtime cho node // thường + sub-node SECTION (key _sub_) if(props&&props.volumeDb!==undefined){const volLinear=props.volumeDb<=-50?0:Math.pow(10,props.volumeDb/20);const ctx=getAudioContext();const nodes=activeTrackNodesRef.current[trackId];if(nodes)nodes.gainNode.gain.setValueAtTime(volLinear,ctx.currentTime);Object.keys(activeTrackNodesRef.current).forEach(function(k){if(k.endsWith('_sub_'+trackId)){const sn=activeTrackNodesRef.current[k];if(sn&&sn.gainNode)sn.gainNode.gain.setValueAtTime(volLinear,ctx.currentTime);}});}setTimeout(()=>lucide.createIcons(),50);};const toggleTrackDrum=trackId=>{var mt=tracks||[];var tidx=mt.findIndex(function(tr){return tr.id===trackId;});if(tidx<0)tidx=0;setTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;const becomingPercussion=!t.is_percussion;let newBank=becomingPercussion?128:t._saved_sf_bank!==undefined?t._saved_sf_bank:0;let newProgram=becomingPercussion?0:t._saved_sf_program!==undefined?t._saved_sf_program:0;let newCh=becomingPercussion?9:t._saved_sf_channel!==undefined?t._saved_sf_channel:tidx%16;return{...t,is_percussion:becomingPercussion,soundfont_bank:newBank,instrumentProgram:newProgram,midiChannel:newCh,_saved_sf_bank:becomingPercussion?t.soundfont_bank:t._saved_sf_bank,_saved_sf_program:becomingPercussion?t.instrumentProgram:t._saved_sf_program,_saved_sf_channel:becomingPercussion?t.midiChannel:t._saved_sf_channel,synth_engine:t.synth_engine?{...t.synth_engine,soundfont_bank:newBank,soundfont_program:newProgram}:t.synth_engine};}));setTimeout(()=>lucide.createIcons(),50);};const updateTrackVolumeDb=(trackId,val)=>{const beforeSnap=captureTrackSnapshot(trackId);updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,volumeDb:val}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'VOLUME_CHANGE',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});// Real-time update during playback diff --git a/app/templates/index.html b/app/templates/index.html index f2b4590..730063b 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -51,7 +51,7 @@ - +