diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index ea823a2..2b27f86 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -12,47 +12,6 @@ const API_AUDIO = `${API_BASE_URL}/api/v1/audio`; const API_MULTITRACK = `${API_BASE_URL}/api/v1/multitrack`; const API_TASKS = `${API_BASE_URL}/api/v1/audio/tasks`; -// ── DEBUG diag (tạm thời) ── -window.__sfDiag = (function () { - var arr = []; - try { var s = localStorage.getItem('__sf_diag'); if (s) arr = JSON.parse(s) || []; } catch (e) {} - return function (msg) { - try { - arr.push(new Date().toISOString().slice(11, 23) + ' ' + msg); - if (arr.length > 2000) arr = arr.slice(-2000); - localStorage.setItem('__sf_diag', JSON.stringify(arr)); - } catch (e) {} - }; -})(); -window.__sfDiagDump = function () { - try { - var s = localStorage.getItem('__sf_diag'); - var arr = s ? (JSON.parse(s) || []) : []; - var idx = -1; - for (var i = arr.length - 1; i >= 0; i--) { if (arr[i].indexOf('BOUNCE_START') !== -1) { idx = i; break; } } - return (idx >= 0 ? arr.slice(idx) : arr.slice(-50)).join('\n'); - } catch (e) { return String(e); } -}; -window.__sfDiagShow = function () { - try { if (window.__sfDiagDump) { var ov = document.createElement('div'); ov.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,.65);display:flex;align-items:center;justify-content:center;font-family:monospace'; var box = document.createElement('div'); box.style.cssText = 'background:#fff;color:#111;border-radius:8px;padding:12px;width:min(92vw,900px);max-height:85vh;display:flex;flex-direction:column;box-shadow:0 4px 24px rgba(0,0,0,.5)'; var ta = document.createElement('textarea'); ta.readOnly = true; ta.value = window.__sfDiagDump(); ta.style.cssText = 'flex:1;min-height:55vh;font-family:monospace;font-size:11px;white-space:pre;resize:none'; var bar = document.createElement('div'); bar.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;margin-top:8px'; var bCopy = document.createElement('button'); bCopy.textContent = 'Copy'; bCopy.onclick = function () { try { ta.focus(); ta.select(); document.execCommand('copy'); bCopy.textContent = 'Đã copy!'; } catch (e) { bCopy.textContent = 'Copy thủ công (Ctrl+C)'; } }; var bClose = document.createElement('button'); bClose.textContent = 'Đóng'; bClose.onclick = function () { document.body.removeChild(ov); }; bar.appendChild(bCopy); bar.appendChild(bClose); box.appendChild(ta); box.appendChild(bar); ov.appendChild(box); document.body.appendChild(ov); try { ta.focus(); ta.select(); } catch (e) {} return; } } catch (e) { window.alert('DIAG:\n' + (window.__sfDiagDump ? window.__sfDiagDump() : '')); } -}; -// HB: main-thread heartbeat — khoảng cách giữa các HB chứng minh main thread -// bị block (timer không fire trong lúc block). Chạy vĩnh viễn; dump chỉ lấy -// từ BOUNCE_START. -window.__sfHBCount = 0; -setInterval(function () { window.__sfHBCount++; if (window.__sfDiag) window.__sfDiag('HB:' + window.__sfHBCount); }, 250); -// WI_QUERY: bridge write_index liveness via proven invoke path (event-free). -window.__sfWICount = 0; -setInterval(function () { - window.__sfWICount++; - if (!window.__TAURI__ || !window.__TAURI__.core) return; - window.__TAURI__.core.invoke('bridge_status').then(function (s) { - if (window.__sfDiag) window.__sfDiag('WI_QUERY#' + window.__sfWICount + ' idx=' + (s && s.write_index) + ' conn=' + (s && s.connected ? 1 : 0)); - }).catch(function (e) { - if (window.__sfDiag) window.__sfDiag('WI_QUERY#' + window.__sfWICount + ' err=' + String(e).slice(0, 80)); - }); -}, 2000); - // ── Dedicated per-track MIDI channel allocation ── // FluidSynth has 16 channels; if two tracks share a channel, arming one track // re-selects the other track's program and its instrument changes. Every track @@ -372,7 +331,6 @@ const _renderNativeTrack = (track, bpm, baseOffsetSec, notesOverride) => { try { const eng = track && track.synth_engine; const data = _buildNativeTrackNotes(track, bpm, baseOffsetSec, notesOverride); - if (window.__sfDiag) window.__sfDiag('renderNative track=' + (track && track.id) + ' sfId=' + (eng && eng.soundfont_id) + ' engType=' + (eng && eng.type) + ' notes=' + data.absNotes.length); if (!data.absNotes.length) return Promise.resolve(null); if (isSfTrackEngine(eng)) { const sfId = eng && eng.soundfont_id; @@ -2188,10 +2146,9 @@ function getAudioContext() { window.BridgeAudioNode.init(audioCtx); window.NativeBridgeService.onAudio(function (l, r) { try { - if (!window.__audioCbEnterAt || Date.now() - window.__audioCbEnterAt >= 1000) { window.__audioCbEnterAt = Date.now(); if (window.__sfDiag) window.__sfDiag('AUDIO_CB_ENTER'); } window.BridgeAudioNode.onAudio(l, r); } catch (e) { - if (window.__sfDiag) window.__sfDiag('AUDIO_CB_ERR ' + String(e && e.message || e).slice(0, 150)); + console.warn('[Bridge] audio callback error:', e && e.message || e); } }); // A14: plugin MIDI output -> route notes to the selected track @@ -24614,7 +24571,6 @@ const App = () => { // bridge tự render note-off sau duration. A11 (sample-accurate) sẽ thay // setTimeout bằng sampleOffset đẩy thẳng vào SHM. const scheduleMidiNoteDispatch = (track, pitch, velocity, durMs, startTime, program, destNode, ch, synthEngine) => { - if (window.__sfDiag) window.__sfDiag('schedMidi track=' + (track && track.id) + ' p=' + pitch + ' ch=' + ch + ' prog=' + program + ' bridgeActive=' + !!(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())); if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) { const ctx = getAudioContext(); const startAt = startTime || ctx.currentTime; // undefined/null = phát ngay @@ -24632,13 +24588,10 @@ const App = () => { // V11: epoch guard — hủy timer cũ khi stop/loop restart (xem ref). const epoch = bridgeNoteEpochRef.current; setTimeout(function () { - if (window.__sfDiag) window.__sfDiag('TMR_FIRE track=' + trkId + ' p=' + (pitch || 60) + ' epOk=' + (epoch === bridgeNoteEpochRef.current) + ' guard=' + (guardPlay() ? 1 : 0)); if (epoch !== bridgeNoteEpochRef.current) return; if (!guardPlay()) return; - if (window.__sfDiag) window.__sfDiag('DISPATCH_ON track=' + trkId + ' p=' + (pitch || 60)); var _rt = window.SonicMidiRouter; - if (window.__sfDiag) window.__sfDiag('PUSH_CALL router=' + (typeof _rt) + ' push=' + (_rt ? typeof _rt.pushEvent : 'none') + ' ver=' + (_rt && _rt.pushEvent ? (_rt.pushEvent.toString().indexOf('PUSH_ROUTE') >= 0 ? 'new' : 'old') : 'na') + ' brAct=' + (_rt ? _rt.isBridgeActive() : 'na')); - try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: trkId, pitch: pitch || 60, velocity: velocity || 0.8, percussion: isPerc }); } catch (e) { if (window.__sfDiag) window.__sfDiag('PUSH_EXC ' + (e && e.message)); } + 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; @@ -24658,7 +24611,6 @@ const App = () => { const startTrackPlayback = offsetTime => { const context = getAudioContext(); - if (window.__sfDiag) window.__sfDiag('startPlay offset=' + offsetTime + ' isBridgeConn=' + !!(window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) + ' routerActive=' + !!(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) + ' sonicSF=' + !!window.SonicSF + ' standalone=' + isStandaloneSf()); _nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade (play trong cửa sổ 80ms) restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain // Realtime VST FX (master chain qua SHM bridge): bật khi play + chain active. @@ -24694,7 +24646,6 @@ const App = () => { allPlayTracks.forEach(track => { const isPlayable = hasSolo ? track.solo : !track.muted; if (!isPlayable) return; - if (window.__sfDiag) window.__sfDiag('playTrk id=' + track.id + ' name=' + track.name + ' type=' + track.type + ' midiItems=' + (track.midiItems || []).length + ' items=' + ((track.items || []).length) + ' sfEng=' + isSfTrackEngine(track.synth_engine) + ' vstEng=' + isVstTrackEngine(track.synth_engine) + ' routerAct=' + !!(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())); const gainNode = getOrCreateTrackNode(track, context); // Realtime VST FX per-track (SHM bridge): mọi luồng track qua bridge — @@ -28305,9 +28256,7 @@ const App = () => { // (audio + MIDI + sections qua FX racks + mastering chain, đúng như nghe) và // capture PCM từ master bus, sau đó encode WAV trung thực. const triggerBounceExport = async () => { - if (!masterBus) { showToast("Hãy phát thử một lần để khởi tạo audio engine trước.", "warning"); return; } if (isExporting) return; - if (window.__sfDiag) window.__sfDiag('BOUNCE_START'); isExportingRef.current = true; // guardPlay: bounce = đang phát thật setIsExporting(true); setShowExportPanel(false); @@ -28327,6 +28276,45 @@ const App = () => { } let durationLimit = loopDur || (Math.max(1.0, computeMainSessionEndTime(allTracks)) + 1.2); // FX/mastering tail (chỉ khi không loop) + // ── OFFLINE render (ưu tiên): engine render full project qua native + // bridge --render (MIDI sample-accurate, audio items, track FX, mastering + // chain) — không gap, nhanh hơn realtime, đúng timeline hiển thị. + // Chỉ khi server connected; loop vẫn dùng realtime (cần cắt selection). + if (!loopDur && serverStatus === 'connected' && window.SonicAPI && window.SonicAPI.renderProject) { + try { + const schema = serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume, { volL: masterVolL, volR: masterVolR, link: masterLink, muted: masterMuted }); + const isFloat = exportSettings.bitDepth === 'float'; + const outExt = exportSettings.format === 'ogg' ? 'ogg' : 'wav'; + const res = await window.SonicAPI.renderProject(schema, 'bounce_' + Date.now() + '.' + outExt, { + bit_depth: isFloat ? 32 : parseInt(exportSettings.bitDepth) || 16, + sample_rate: parseInt(exportSettings.sampleRate) || 44100, + dither: !!exportSettings.dither, + normalize: !!exportSettings.normalize, + normalize_target: exportSettings.normalizeTarget || '-6dbtp', + mono_check: !!exportSettings.monoCheck, + metadata: { + title: exportSettings.metaTitle || '', + artist: exportSettings.metaArtist || '', + isrc: exportSettings.metaIsrc || '' + } + }); + const lufsTxt = (res.lufs !== null && res.lufs !== undefined) ? res.lufs + ' LUFS' : '-- LUFS'; + const tpTxt = (res.true_peak_db !== null && res.true_peak_db !== undefined) ? res.true_peak_db + ' dBTP' : '-- dBTP'; + const br = await fetch(res.url); + if (!br.ok) throw new Error('HTTP ' + br.status); + const b = await br.blob(); + const sp = await saveExportViaDialog(`sonicforge-bounce-${parseInt(exportSettings.sampleRate) || 44100}hz-${isFloat ? 32 : parseInt(exportSettings.bitDepth) || 16}bit.${outExt}`, b); + if (sp) showToast(`Bounce hoàn tất → ${sp} • ${lufsTxt} / ${tpTxt}`, 'success'); + else showToast(`Bounce hoàn tất — ${lufsTxt} / ${tpTxt}`, 'success'); + return; + } catch (err) { + console.warn('[Bounce] offline render fail — fallback realtime:', err); + } + } + + // ── Realtime fallback (server offline / render fail): chạy lại project + // qua WebAudio, capture master bus post-FX + post-mastering. ── + if (!masterBus) { showToast("Hãy phát thử một lần để khởi tạo audio engine trước.", "warning"); return; } const captureRate = audioCtx.sampleRate || 44100; const isFloatBit = exportSettings.bitDepth === 'float'; const bitDepth = isFloatBit ? 32 : parseInt(exportSettings.bitDepth) || 16; @@ -28406,8 +28394,6 @@ const App = () => { } const blob = new Blob([view], { type: 'audio/wav' }); await downloadBlobViaServer(blob, `sonicforge-bounce-${captureRate}hz-${bitDepth}bit.wav`).catch(() => {}); - if (window.__sfDiag) window.__sfDiag('BOUNCE_END'); - try { window.__sfDiagShow(); } catch (e) {} showToast("Bounce hoàn tất — file gồm MIDI + FX Rack + Mastering Chain!", "success"); } catch (err) { showToast("Lỗi bounce: " + err.message, "error"); @@ -28417,6 +28403,7 @@ const App = () => { setShowExportProgress(false); } }; + const handleSaveLocalProject = (name) => { const finalName = name || projectName || 'Dự án mới'; const localId = currentProjectId && currentProjectId.startsWith('local_') ? currentProjectId : 'local_' + Date.now(); diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 0aaa5cf..b2b8e26 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -1,10 +1,5 @@ const{useState,useRef,useEffect,useMemo,useCallback}=React;// ── FastAPI Backend Configuration ── -const API_BASE_URL=(window.API_BASE_URL||window.location.origin).replace(/\/+$/,'');const API_AUDIO=`${API_BASE_URL}/api/v1/audio`;const API_MULTITRACK=`${API_BASE_URL}/api/v1/multitrack`;const API_TASKS=`${API_BASE_URL}/api/v1/audio/tasks`;// ── DEBUG diag (tạm thời) ── -window.__sfDiag=function(){var arr=[];try{var s=localStorage.getItem('__sf_diag');if(s)arr=JSON.parse(s)||[];}catch(e){}return function(msg){try{arr.push(new Date().toISOString().slice(11,23)+' '+msg);if(arr.length>2000)arr=arr.slice(-2000);localStorage.setItem('__sf_diag',JSON.stringify(arr));}catch(e){}};}();window.__sfDiagDump=function(){try{var s=localStorage.getItem('__sf_diag');var arr=s?JSON.parse(s)||[]:[];var idx=-1;for(var i=arr.length-1;i>=0;i--){if(arr[i].indexOf('BOUNCE_START')!==-1){idx=i;break;}}return(idx>=0?arr.slice(idx):arr.slice(-50)).join('\n');}catch(e){return String(e);}};window.__sfDiagShow=function(){try{if(window.__sfDiagDump){var ov=document.createElement('div');ov.style.cssText='position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,.65);display:flex;align-items:center;justify-content:center;font-family:monospace';var box=document.createElement('div');box.style.cssText='background:#fff;color:#111;border-radius:8px;padding:12px;width:min(92vw,900px);max-height:85vh;display:flex;flex-direction:column;box-shadow:0 4px 24px rgba(0,0,0,.5)';var ta=document.createElement('textarea');ta.readOnly=true;ta.value=window.__sfDiagDump();ta.style.cssText='flex:1;min-height:55vh;font-family:monospace;font-size:11px;white-space:pre;resize:none';var bar=document.createElement('div');bar.style.cssText='display:flex;gap:8px;justify-content:flex-end;margin-top:8px';var bCopy=document.createElement('button');bCopy.textContent='Copy';bCopy.onclick=function(){try{ta.focus();ta.select();document.execCommand('copy');bCopy.textContent='Đã copy!';}catch(e){bCopy.textContent='Copy thủ công (Ctrl+C)';}};var bClose=document.createElement('button');bClose.textContent='Đóng';bClose.onclick=function(){document.body.removeChild(ov);};bar.appendChild(bCopy);bar.appendChild(bClose);box.appendChild(ta);box.appendChild(bar);ov.appendChild(box);document.body.appendChild(ov);try{ta.focus();ta.select();}catch(e){}return;}}catch(e){window.alert('DIAG:\n'+(window.__sfDiagDump?window.__sfDiagDump():''));}};// HB: main-thread heartbeat — khoảng cách giữa các HB chứng minh main thread -// bị block (timer không fire trong lúc block). Chạy vĩnh viễn; dump chỉ lấy -// từ BOUNCE_START. -window.__sfHBCount=0;setInterval(function(){window.__sfHBCount++;if(window.__sfDiag)window.__sfDiag('HB:'+window.__sfHBCount);},250);// WI_QUERY: bridge write_index liveness via proven invoke path (event-free). -window.__sfWICount=0;setInterval(function(){window.__sfWICount++;if(!window.__TAURI__||!window.__TAURI__.core)return;window.__TAURI__.core.invoke('bridge_status').then(function(s){if(window.__sfDiag)window.__sfDiag('WI_QUERY#'+window.__sfWICount+' idx='+(s&&s.write_index)+' conn='+(s&&s.connected?1:0));}).catch(function(e){if(window.__sfDiag)window.__sfDiag('WI_QUERY#'+window.__sfWICount+' err='+String(e).slice(0,80));});},2000);// ── Dedicated per-track MIDI channel allocation ── +const API_BASE_URL=(window.API_BASE_URL||window.location.origin).replace(/\/+$/,'');const API_AUDIO=`${API_BASE_URL}/api/v1/audio`;const API_MULTITRACK=`${API_BASE_URL}/api/v1/multitrack`;const API_TASKS=`${API_BASE_URL}/api/v1/audio/tasks`;// ── Dedicated per-track MIDI channel allocation ── // FluidSynth has 16 channels; if two tracks share a channel, arming one track // re-selects the other track's program and its instrument changes. Every track // gets its own stable, unique channel (0-15, skipping 9 which is the classic @@ -69,7 +64,7 @@ let _pendingFadeSession=null;const restorePendingFadeSession=()=>{if(!_pendingFa const _cleanNativeNotes=notes=>{const out=[];notes.forEach(function(n){const pitch=n.pitch||60;const start=n.start_beat||0;const dup=out.find(function(o){return o.pitch===pitch&&o.start_beat===start;});if(dup){dup.velocity=Math.max(dup.velocity,n.velocity!=null?n.velocity:0.8);dup.duration_beats=Math.max(dup.duration_beats,n.duration_beats||1);return;}out.push({pitch:pitch,start_beat:start,duration_beats:n.duration_beats||1,velocity:n.velocity!=null?n.velocity:0.8});});out.forEach(function(n){let nextStart=Infinity;out.forEach(function(m){if(m===n||m.pitch!==n.pitch||m.start_beat<=n.start_beat)return;if(m.start_beat{notes.forEach(function(n){const nStart=n.start_beat||0;let active=0;notes.forEach(function(m){const mEnd=(m.start_beat||0)+(m.duration_beats||1);if((m.start_beat||0)<=nStart&&nStart1){const g=1/Math.sqrt(active);n.velocity=(n.velocity!=null?n.velocity:0.8)*g;}});return notes;};const _buildNativeTrackNotes=(track,bpm,baseOffsetSec,notesOverride)=>{const secPerBeat=60.0/(parseInt(bpm)||120);const items=notesOverride?[{startTime:0,notes:notesOverride}]:track&&track.midiItems||[];let trackStartAbs=Infinity;const absNotes=[];items.forEach(function(item){const itemStartAbs=(baseOffsetSec||0)+(item.startTime||0);if(itemStartAbs{try{const eng=track&&track.synth_engine;const instrumentId=eng&&eng.plugin_id||track&&track.instrumentId||'';const sfId=eng&&eng.soundfont_id||'';const data=_buildNativeTrackNotes(track,bpm,baseOffsetSec,notesOverride);const nk=data.absNotes.map(function(n){return n.pitch+':'+n.start_beat+':'+n.duration_beats+':'+n.velocity;}).join(',');return[track&&track.id?track.id:'',instrumentId,sfId,eng&&eng.preset_id||'',eng&&eng.preset_path||'',parseFloat(bpm)||120,baseOffsetSec||0,nk].join('|');}catch(e){return'';}};const _renderNativeTrack=(track,bpm,baseOffsetSec,notesOverride)=>{try{const eng=track&&track.synth_engine;const data=_buildNativeTrackNotes(track,bpm,baseOffsetSec,notesOverride);if(window.__sfDiag)window.__sfDiag('renderNative track='+(track&&track.id)+' sfId='+(eng&&eng.soundfont_id)+' engType='+(eng&&eng.type)+' notes='+data.absNotes.length);if(!data.absNotes.length)return Promise.resolve(null);if(isSfTrackEngine(eng)){const sfId=eng&&eng.soundfont_id;if(!sfId)return Promise.resolve(null);return window.SonicAPI.soundfontRender({soundfont_id:sfId,bank:eng&&eng.soundfont_bank||0,program:eng&&eng.soundfont_program||0,bpm:parseFloat(bpm)||120,notes:data.absNotes}).then(function(res){if(!res||!res.success||!res.url)return null;return API_BASE_URL+res.url;}).catch(function(){return null;});}const instrumentId=eng&&eng.plugin_id||track&&track.instrumentId;if(!instrumentId||!isVstTrackEngine(eng))return Promise.resolve(null);return window.SonicAPI.midiRender({instrument_id:instrumentId,notes:data.absNotes,bpm:parseFloat(bpm)||120,preset_id:eng&&eng.preset_id||undefined,preset_path:eng&&eng.preset_path||undefined,preset_data:eng&&eng.preset_data||undefined}).then(function(res){if(!res||!res.success||!res.url)return null;return API_BASE_URL+res.url;}).catch(function(){return null;});}catch(e){return Promise.resolve(null);}};// Dedupe in-flight render + cache WAV: play lặp/re-schedule trong lúc render +const _scaleNativePolyphony=notes=>{notes.forEach(function(n){const nStart=n.start_beat||0;let active=0;notes.forEach(function(m){const mEnd=(m.start_beat||0)+(m.duration_beats||1);if((m.start_beat||0)<=nStart&&nStart1){const g=1/Math.sqrt(active);n.velocity=(n.velocity!=null?n.velocity:0.8)*g;}});return notes;};const _buildNativeTrackNotes=(track,bpm,baseOffsetSec,notesOverride)=>{const secPerBeat=60.0/(parseInt(bpm)||120);const items=notesOverride?[{startTime:0,notes:notesOverride}]:track&&track.midiItems||[];let trackStartAbs=Infinity;const absNotes=[];items.forEach(function(item){const itemStartAbs=(baseOffsetSec||0)+(item.startTime||0);if(itemStartAbs{try{const eng=track&&track.synth_engine;const instrumentId=eng&&eng.plugin_id||track&&track.instrumentId||'';const sfId=eng&&eng.soundfont_id||'';const data=_buildNativeTrackNotes(track,bpm,baseOffsetSec,notesOverride);const nk=data.absNotes.map(function(n){return n.pitch+':'+n.start_beat+':'+n.duration_beats+':'+n.velocity;}).join(',');return[track&&track.id?track.id:'',instrumentId,sfId,eng&&eng.preset_id||'',eng&&eng.preset_path||'',parseFloat(bpm)||120,baseOffsetSec||0,nk].join('|');}catch(e){return'';}};const _renderNativeTrack=(track,bpm,baseOffsetSec,notesOverride)=>{try{const eng=track&&track.synth_engine;const data=_buildNativeTrackNotes(track,bpm,baseOffsetSec,notesOverride);if(!data.absNotes.length)return Promise.resolve(null);if(isSfTrackEngine(eng)){const sfId=eng&&eng.soundfont_id;if(!sfId)return Promise.resolve(null);return window.SonicAPI.soundfontRender({soundfont_id:sfId,bank:eng&&eng.soundfont_bank||0,program:eng&&eng.soundfont_program||0,bpm:parseFloat(bpm)||120,notes:data.absNotes}).then(function(res){if(!res||!res.success||!res.url)return null;return API_BASE_URL+res.url;}).catch(function(){return null;});}const instrumentId=eng&&eng.plugin_id||track&&track.instrumentId;if(!instrumentId||!isVstTrackEngine(eng))return Promise.resolve(null);return window.SonicAPI.midiRender({instrument_id:instrumentId,notes:data.absNotes,bpm:parseFloat(bpm)||120,preset_id:eng&&eng.preset_id||undefined,preset_path:eng&&eng.preset_path||undefined,preset_data:eng&&eng.preset_data||undefined}).then(function(res){if(!res||!res.success||!res.url)return null;return API_BASE_URL+res.url;}).catch(function(){return null;});}catch(e){return Promise.resolve(null);}};// Dedupe in-flight render + cache WAV: play lặp/re-schedule trong lúc render // đang chạy → chờ cùng promise, không bắn duplicate request (tránh queue sau // RENDER_LOCK + render 2 lần cùng nội dung). const ensureRenderNativeTrack=(track,bpm,baseOffsetSec,notesOverride)=>{const key=_nativeTrackKey(track,bpm,baseOffsetSec,notesOverride);if(!key)return _renderNativeTrack(track,bpm,baseOffsetSec,notesOverride);if(_nativeTrackUrls[key])return Promise.resolve(_nativeTrackUrls[key]);if(_nativeTrackPending[key])return _nativeTrackPending[key];const p=_renderNativeTrack(track,bpm,baseOffsetSec,notesOverride).then(function(url){if(url)_nativeTrackUrls[key]=url;return url;}).then(function(url){delete _nativeTrackPending[key];return url;});_nativeTrackPending[key]=p;return p;};// Giới hạn decodeAudioData song song (max 3) — nhiều WAV fetch về cùng lúc → @@ -317,7 +312,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=202608250140`);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://';const tok=localStorage.getItem('sonic_token')||'';const wsUrl=`${wsProto}${u.host}${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.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 +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=202608250700`);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://';const tok=localStorage.getItem('sonic_token')||'';const wsUrl=`${wsProto}${u.host}${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.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 try{fxRtPushAllParams();}catch(e){}};ws.onmessage=ev=>{// Gap 8 (PDC realtime): text frame {cmd:'latency', total} tu server -> // cap nhat delay dry-bypass path (bu latency chain VST3 realtime). if(typeof ev.data==='string'){try{const d=JSON.parse(ev.data);if(d&&d.cmd==='latency'&&typeof d.total==='number'){fxRt.latencyTotal=d.total;// Chi delay dry path khi crossfade da xong (dry silent) — ap som @@ -403,7 +398,7 @@ try{masterBus.inputAnalyser.disconnect();masterBus.maximizerCompressor.disconnec // → global silence. The React effect applies it once per settings change. if(window.SonicSF&&window.SonicSF.init){window.SonicSF.init(audioCtx);}// C6: bootstrap native bridge 1 lần — query status, nối audio sink + router. if(window.NativeBridgeService&&window.SonicMidiRouter&&!window.__bridgeBootstrapped){window.__bridgeBootstrapped=true;window.SonicMidiRouter.onFallback=function(cmd,ch,pitch,vel){if(!window.SonicSF)return;if(cmd==='PANIC'){if(window.SonicSF.stopAll)window.SonicSF.stopAll();return;}if(cmd==='NOTE_ON')window.SonicSF.playNote(pitch,vel,undefined,undefined,undefined,undefined,ch);else window.SonicSF.stopNote(ch,pitch);};(async function(){try{var st=await window.NativeBridgeService.queryStatus();var connected=!!(st&&st.connected);window.SonicMidiRouter.setBridgeConnected(connected);console.log('[Bridge] bootstrap connected=',connected);if(connected){try{console.log('[Bridge] Requesting restart to match sampleRate:',audioCtx.sampleRate);await window.__TAURI__.core.invoke('restart_bridge_with_sample_rate',{sampleRate:audioCtx.sampleRate});// Bridge restart → mất hết instrument → reset map để reconnect reload. -window.__bridgeLoadedChannels={};window.__bridgeLoadedPaths={};for(var retry=0;retry<10;retry++){await new Promise(r=>setTimeout(r,100));var st2=await window.NativeBridgeService.queryStatus();if(st2&&st2.connected){console.log('[Bridge] Reconnected at sampleRate:',audioCtx.sampleRate);break;}}}catch(err){console.warn('[Bridge] restart error:',err);}window.BridgeAudioNode.init(audioCtx);window.NativeBridgeService.onAudio(function(l,r){try{if(!window.__audioCbEnterAt||Date.now()-window.__audioCbEnterAt>=1000){window.__audioCbEnterAt=Date.now();if(window.__sfDiag)window.__sfDiag('AUDIO_CB_ENTER');}window.BridgeAudioNode.onAudio(l,r);}catch(e){if(window.__sfDiag)window.__sfDiag('AUDIO_CB_ERR '+String(e&&e.message||e).slice(0,150));}});// A14: plugin MIDI output -> route notes to the selected track +window.__bridgeLoadedChannels={};window.__bridgeLoadedPaths={};for(var retry=0;retry<10;retry++){await new Promise(r=>setTimeout(r,100));var st2=await window.NativeBridgeService.queryStatus();if(st2&&st2.connected){console.log('[Bridge] Reconnected at sampleRate:',audioCtx.sampleRate);break;}}}catch(err){console.warn('[Bridge] restart error:',err);}window.BridgeAudioNode.init(audioCtx);window.NativeBridgeService.onAudio(function(l,r){try{window.BridgeAudioNode.onAudio(l,r);}catch(e){console.warn('[Bridge] audio callback error:',e&&e.message||e);}});// A14: plugin MIDI output -> route notes to the selected track // (Scaler2/Instacomposer chords play the selected track's // instrument). Feedback guard: skip events whose source channel // is the selected track's own channel (would loop generator into @@ -1772,7 +1767,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};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608250140`);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://';const tok=localStorage.getItem('sonic_token')||'';const ws=new WebSocket(`${wsProto}${u.host}${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 +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};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608250700`);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://';const tok=localStorage.getItem('sonic_token')||'';const ws=new WebSocket(`${wsProto}${u.host}${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(!(ev.data instanceof ArrayBuffer)||!entry.node)return;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==='set_param'){try{entry.ws.send(JSON.stringify({cmd:'set_param',slot:d.slot,key:d.key,value:d.value}));}catch(e){}return;}if(d.type!=='in')return;// Burst 4 block (1024 mẫu) → tách 4 WS frame 512 floats → bridge batch 4. // Pace 1 burst/21ms (giống master) — tránh dồn frame sau stall làm tràn ring. entry.inQueue.push(d);const pump=()=>{if(!entry.inQueue.length)return;const now=Date.now();while(entry.inQueue.length&&now-entry.lastInSent>=FXRT_BURST_MS){entry.lastInSent=now;const dd=entry.inQueue.shift();if(entry.ws&&entry.ws.readyState===1){try{const f=new Float32Array(dd.data);if(f.length>512){for(let i=0;i{const entry=fxRtTracks[trackId+':'+path];if(!entry||!entry.spliced||!entry.node)return;try{entry.node.disconnect();try{entry.spliced.tail.connect(entry.spliced.dest);}catch(e){}entry.spliced=null;}catch(e){console.warn('[FX RT track] unsplice fail',trackId,path,e);}};// Chain đổi trên session ĐANG CHẠY — set_chain KHÔNG restart (bridge swap @@ -1884,15 +1879,15 @@ if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){const isPerc // SonicSF (FluidSynth WASM). Giữ setTimeout scheduling (như SonicSF cũ); // bridge tự render note-off sau duration. A11 (sample-accurate) sẽ thay // setTimeout bằng sampleOffset đẩy thẳng vào SHM. -const scheduleMidiNoteDispatch=(track,pitch,velocity,durMs,startTime,program,destNode,ch,synthEngine)=>{if(window.__sfDiag)window.__sfDiag('schedMidi track='+(track&&track.id)+' p='+pitch+' ch='+ch+' prog='+program+' bridgeActive='+!!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()));if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){const ctx=getAudioContext();const startAt=startTime||ctx.currentTime;// undefined/null = phát ngay +const scheduleMidiNoteDispatch=(track,pitch,velocity,durMs,startTime,program,destNode,ch,synthEngine)=>{if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){const ctx=getAudioContext();const startAt=startTime||ctx.currentTime;// undefined/null = phát ngay const delayMs=Math.max(0,(startAt-ctx.currentTime)*1000);// Guard chống note-on trễ sau Stop: CHỈ khi là timeline (startTime thật). const guardPlay=startTime!=null?function(){return isPlayingRef.current||subTabsRef.current&&subTabsRef.current.some(s=>s.isPlaying)||isExportingRef.current;}:function(){return true;};const trkId=track?track.id:0;// Percussion (bank 128) → router allocateChannel ch 9; melodic → round-robin. const isPerc=!!(synthEngine&&synthEngine.soundfont_bank===128);// D8: track đổi instrument → gửi CC0/CC32 (bank THẬT từ synthEngine) + // program change qua bridge (A12) — helper dedupe theo track+bank+program. 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(window.__sfDiag)window.__sfDiag('TMR_FIRE track='+trkId+' p='+(pitch||60)+' epOk='+(epoch===bridgeNoteEpochRef.current)+' guard='+(guardPlay()?1:0));if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;if(window.__sfDiag)window.__sfDiag('DISPATCH_ON track='+trkId+' p='+(pitch||60));var _rt=window.SonicMidiRouter;if(window.__sfDiag)window.__sfDiag('PUSH_CALL router='+typeof _rt+' push='+(_rt?typeof _rt.pushEvent:'none')+' ver='+(_rt&&_rt.pushEvent?_rt.pushEvent.toString().indexOf('PUSH_ROUTE')>=0?'new':'old':'na')+' brAct='+(_rt?_rt.isBridgeActive():'na'));try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:trkId,pitch:pitch||60,velocity:velocity||0.8,percussion:isPerc});}catch(e){if(window.__sfDiag)window.__sfDiag('PUSH_EXC '+(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){}},delayMs,durMs||1000);return;}// Native VSTi (standalone browser, bridge inactive) → midi-render thay vì +const epoch=bridgeNoteEpochRef.current;setTimeout(function(){if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;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){}},delayMs,durMs||1000);return;}// Native VSTi (standalone browser, bridge inactive) → midi-render thay vì // FluidSynth GM sai âm — safety net cho mọi caller (ghost notes, per-note). -if(isStandaloneSf()&&isVstTrackEngine(synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeVstNote(track,pitch,velocity,durMs,startTime,track?track.id:'global');return;}window.SonicSF.playNote(pitch,velocity,durMs,startTime,program,destNode,ch,synthEngine);};const startTrackPlayback=offsetTime=>{const context=getAudioContext();if(window.__sfDiag)window.__sfDiag('startPlay offset='+offsetTime+' isBridgeConn='+!!(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected)+' routerActive='+!!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())+' sonicSF='+!!window.SonicSF+' standalone='+isStandaloneSf());_nativePlayEpoch++;// new play — vô hiệu doHardStop đang fade (play trong cửa sổ 80ms) +if(isStandaloneSf()&&isVstTrackEngine(synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeVstNote(track,pitch,velocity,durMs,startTime,track?track.id:'global');return;}window.SonicSF.playNote(pitch,velocity,durMs,startTime,program,destNode,ch,synthEngine);};const startTrackPlayback=offsetTime=>{const context=getAudioContext();_nativePlayEpoch++;// new play — vô hiệu doHardStop đang fade (play trong cửa sổ 80ms) restorePendingFadeSession();// V17: huy fade dang treo — replay khong bi dip gain // Realtime VST FX (master chain qua SHM bridge): bật khi play + chain active. try{fxRtStart();}catch(e){console.warn('[FX RT] start error:',e);}// D2: bridge active -> báo transport PLAY (bridge flush note-off cũ + đồng @@ -1909,7 +1904,7 @@ try{applyMasterAutomationSchedule(offsetTime||0);}catch(e){console.warn('[Auto] // 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 // signature này để phát hiện items đổi vị trí giữa lúc play (re-schedule). -scheduledItemsSigRef.current=buildItemsSignature(allPlayTracks,sessionTabs);console.log('[Play] offset='+offsetTime+' tracks='+allPlayTracks.length+' masterBus='+!!masterBus+' dest='+(context.destination?'ok':'MISSING')+' projectEnd='+(projectEndRef.current||0).toFixed(2));const hasSolo=allPlayTracks.some(t=>t.solo);allPlayTracks.forEach(track=>{const isPlayable=hasSolo?track.solo:!track.muted;if(!isPlayable)return;if(window.__sfDiag)window.__sfDiag('playTrk id='+track.id+' name='+track.name+' type='+track.type+' midiItems='+(track.midiItems||[]).length+' items='+(track.items||[]).length+' sfEng='+isSfTrackEngine(track.synth_engine)+' vstEng='+isVstTrackEngine(track.synth_engine)+' routerAct='+!!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()));const gainNode=getOrCreateTrackNode(track,context);// Realtime VST FX per-track (SHM bridge): mọi luồng track qua bridge — +scheduledItemsSigRef.current=buildItemsSignature(allPlayTracks,sessionTabs);console.log('[Play] offset='+offsetTime+' tracks='+allPlayTracks.length+' masterBus='+!!masterBus+' dest='+(context.destination?'ok':'MISSING')+' projectEnd='+(projectEndRef.current||0).toFixed(2));const hasSolo=allPlayTracks.some(t=>t.solo);allPlayTracks.forEach(track=>{const isPlayable=hasSolo?track.solo:!track.muted;if(!isPlayable)return;const gainNode=getOrCreateTrackNode(track,context);// Realtime VST FX per-track (SHM bridge): mọi luồng track qua bridge — // session/worklet cho TỪNG path có âm (audio-clips + MIDI/SF); splice // vào graph khi WS ready. try{const rtPaths=fxRtTrackPaths(track);if(rtPaths.indexOf('audio')>=0)fxRtTrackStart(track.id,track,'audio');if(rtPaths.indexOf('sf')>=0)fxRtTrackStart(track.id,track,'sf');}catch(e){console.warn('[FX RT track] start error:',e);}const pannerNode=activeTrackNodesRef.current[track.id].pannerNode;// Re-apply route gains theo content-type NGAY tại thời điểm play — node @@ -2157,13 +2152,19 @@ try{const br=await fetch(res.url);if(br.ok){const b=await br.blob();const sp=awa // trong OfflineAudioContext. Vì vậy bounce REALTIME: chạy lại toàn bộ project // (audio + MIDI + sections qua FX racks + mastering chain, đúng như nghe) và // capture PCM từ master bus, sau đó encode WAV trung thực. -const triggerBounceExport=async()=>{if(!masterBus){showToast("Hãy phát thử một lần để khởi tạo audio engine trước.","warning");return;}if(isExporting)return;if(window.__sfDiag)window.__sfDiag('BOUNCE_START');isExportingRef.current=true;// guardPlay: bounce = đang phát thật +const triggerBounceExport=async()=>{if(isExporting)return;isExportingRef.current=true;// guardPlay: bounce = đang phát thật setIsExporting(true);setShowExportPanel(false);setShowExportProgress(true);const audioCtx=getAudioContext();try{const allTracks=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;// MAIN SESSION end time (items trên track main — sections tính theo bounds, // không kéo dài theo nội dung SECTION-TAB) — bounce không cắt sớm/cắt thiếu. // LOOP export: độ dài = loopBars*secondsPerBar, bắt đầu từ selection/playhead, // cắt cứng biên — không fade, không kéo tail. let loopStart=0,loopDur=0;if(exportSettings.loop){const loopSecPerBar=60.0/(parseFloat(bpm)||120)*4;loopStart=selectionStart!==null&&selectionEnd!==null&&selectionEnd>selectionStart?selectionStart:currentTime;loopDur=Math.max(0.1,(exportSettings.loopBars||4)*loopSecPerBar);}let durationLimit=loopDur||Math.max(1.0,computeMainSessionEndTime(allTracks))+1.2;// FX/mastering tail (chỉ khi không loop) -const captureRate=audioCtx.sampleRate||44100;const isFloatBit=exportSettings.bitDepth==='float';const bitDepth=isFloatBit?32:parseInt(exportSettings.bitDepth)||16;const outChannels=exportSettings.channels==='mono'?1:2;const fmtTag=isFloatBit?3:1;// IEEE float vs PCM +// ── OFFLINE render (ưu tiên): engine render full project qua native +// bridge --render (MIDI sample-accurate, audio items, track FX, mastering +// chain) — không gap, nhanh hơn realtime, đúng timeline hiển thị. +// Chỉ khi server connected; loop vẫn dùng realtime (cần cắt selection). +if(!loopDur&&serverStatus==='connected'&&window.SonicAPI&&window.SonicAPI.renderProject){try{const schema=serializeProjectToSchema(currentProjectId||'temp_project',projectName||'Dự án tạm chưa lưu',bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});const isFloat=exportSettings.bitDepth==='float';const outExt=exportSettings.format==='ogg'?'ogg':'wav';const res=await window.SonicAPI.renderProject(schema,'bounce_'+Date.now()+'.'+outExt,{bit_depth:isFloat?32:parseInt(exportSettings.bitDepth)||16,sample_rate:parseInt(exportSettings.sampleRate)||44100,dither:!!exportSettings.dither,normalize:!!exportSettings.normalize,normalize_target:exportSettings.normalizeTarget||'-6dbtp',mono_check:!!exportSettings.monoCheck,metadata:{title:exportSettings.metaTitle||'',artist:exportSettings.metaArtist||'',isrc:exportSettings.metaIsrc||''}});const lufsTxt=res.lufs!==null&&res.lufs!==undefined?res.lufs+' LUFS':'-- LUFS';const tpTxt=res.true_peak_db!==null&&res.true_peak_db!==undefined?res.true_peak_db+' dBTP':'-- dBTP';const br=await fetch(res.url);if(!br.ok)throw new Error('HTTP '+br.status);const b=await br.blob();const sp=await saveExportViaDialog(`sonicforge-bounce-${parseInt(exportSettings.sampleRate)||44100}hz-${isFloat?32:parseInt(exportSettings.bitDepth)||16}bit.${outExt}`,b);if(sp)showToast(`Bounce hoàn tất → ${sp} • ${lufsTxt} / ${tpTxt}`,'success');else showToast(`Bounce hoàn tất — ${lufsTxt} / ${tpTxt}`,'success');return;}catch(err){console.warn('[Bounce] offline render fail — fallback realtime:',err);}}// ── Realtime fallback (server offline / render fail): chạy lại project +// qua WebAudio, capture master bus post-FX + post-mastering. ── +if(!masterBus){showToast("Hãy phát thử một lần để khởi tạo audio engine trước.","warning");return;}const captureRate=audioCtx.sampleRate||44100;const isFloatBit=exportSettings.bitDepth==='float';const bitDepth=isFloatBit?32:parseInt(exportSettings.bitDepth)||16;const outChannels=exportSettings.channels==='mono'?1:2;const fmtTag=isFloatBit?3:1;// IEEE float vs PCM showToast(`Bounce realtime ~${Math.round(durationLimit)}s — giữ nguyên âm thanh đang phát...`,"info");// Capture tap on the master bus output (post-FX + post-mastering) const chunks=[];const script=audioCtx.createScriptProcessor(4096,2,2);script.onaudioprocess=e=>{const L=e.inputBuffer.getChannelData(0);const R=e.inputBuffer.getChannelData(1);const n=L.length;const chunk=new Float32Array(n*2);for(let i=0;ia+c.length/2,0);let stereoBuf=new Float32 if(loopDur>0){const sf=Math.max(0,Math.floor(loopStart*captureRate))*2;const ef=Math.min(stereoBuf.length,Math.floor((loopStart+loopDur)*captureRate)*2);if(ef>sf)stereoBuf=stereoBuf.subarray(sf,ef);}const frames=stereoBuf.length/2;const bytesPerSample=isFloatBit?4:bitDepth/8;const headerSize=44;const fileSizeBytes=headerSize+frames*bytesPerSample*outChannels;const view=new DataView(new ArrayBuffer(fileSizeBytes));const writeString=(o,s)=>{for(let i=0;i>8&0xFF);view.setUint8(o+2,v>>16&0xFF);}else if(bitDepth===16)view.setInt16(o,Math.floor(s<0?s*0x8000:s*0x7FFF),true);else view.setUint8(o,Math.floor((s+1)*127.5),true);}o+=bytesPerSample;}}const blob=new Blob([view],{type:'audio/wav'});await downloadBlobViaServer(blob,`sonicforge-bounce-${captureRate}hz-${bitDepth}bit.wav`).catch(()=>{});if(window.__sfDiag)window.__sfDiag('BOUNCE_END');try{window.__sfDiagShow();}catch(e){}showToast("Bounce hoàn tất — file gồm MIDI + FX Rack + Mastering Chain!","success");}catch(err){showToast("Lỗi bounce: "+err.message,"error");}finally{isExportingRef.current=false;setIsExporting(false);setShowExportProgress(false);}};const handleSaveLocalProject=name=>{const finalName=name||projectName||'Dự án mới';const localId=currentProjectId&¤tProjectId.startsWith('local_')?currentProjectId:'local_'+Date.now();const projectSchemaObj=serializeProjectToSchema(localId,finalName,bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});const dataStr=JSON.stringify(projectSchemaObj);localStorage.setItem('sonic_local_project_data',dataStr);localStorage.setItem('sonic_project_id',localId);localStorage.setItem('sonic_project_name',finalName);setCurrentProjectId(localId);setProjectName(finalName);window.SonicStorage.exportProjectToSFS(projectSchemaObj);showToast(`Đã lưu dự án local "${finalName}" thành công!`,"success");// Đã đăng nhập → đồng bộ lên Cloud (fire-and-forget): project mở được từ +const s=Math.max(-1,Math.min(1,tpdfDither(raw,lsb)));if(bitDepth===24){const v=Math.floor(s<0?s*0x800000:s*0x7FFFFF)&0xFFFFFF;view.setUint8(o,v&0xFF);view.setUint8(o+1,v>>8&0xFF);view.setUint8(o+2,v>>16&0xFF);}else if(bitDepth===16)view.setInt16(o,Math.floor(s<0?s*0x8000:s*0x7FFF),true);else view.setUint8(o,Math.floor((s+1)*127.5),true);}o+=bytesPerSample;}}const blob=new Blob([view],{type:'audio/wav'});await downloadBlobViaServer(blob,`sonicforge-bounce-${captureRate}hz-${bitDepth}bit.wav`).catch(()=>{});showToast("Bounce hoàn tất — file gồm MIDI + FX Rack + Mastering Chain!","success");}catch(err){showToast("Lỗi bounce: "+err.message,"error");}finally{isExportingRef.current=false;setIsExporting(false);setShowExportProgress(false);}};const handleSaveLocalProject=name=>{const finalName=name||projectName||'Dự án mới';const localId=currentProjectId&¤tProjectId.startsWith('local_')?currentProjectId:'local_'+Date.now();const projectSchemaObj=serializeProjectToSchema(localId,finalName,bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});const dataStr=JSON.stringify(projectSchemaObj);localStorage.setItem('sonic_local_project_data',dataStr);localStorage.setItem('sonic_project_id',localId);localStorage.setItem('sonic_project_name',finalName);setCurrentProjectId(localId);setProjectName(finalName);window.SonicStorage.exportProjectToSFS(projectSchemaObj);showToast(`Đã lưu dự án local "${finalName}" thành công!`,"success");// Đã đăng nhập → đồng bộ lên Cloud (fire-and-forget): project mở được từ // máy khác / browser ẩn danh khi đăng nhập cùng tài khoản. if(currentUser&&window.SonicAPI&&window.SonicAPI.saveCloudProject){window.SonicAPI.saveCloudProject(finalName,dataStr).then(function(res){if(res&&res.project_id){console.log('[Cloud] local project synced:',res.project_id);}}).catch(function(e){console.warn('Cloud sync local project failed:',e);});}};const handleSaveCloudProject=async(name,existingProjectId)=>{const finalName=name||projectName||'Dự án mới';var useProjectId=existingProjectId||currentProjectId;const projectSchemaObj=serializeProjectToSchema(useProjectId||'project_'+Date.now(),finalName,bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});// Diagnostic: count source vs serialized items var srcMidi=0,srcClip=0,srcSec=0;tracks.forEach(function(t){srcMidi+=(t.midiItems||[]).length;srcClip+=(t.clips||[]).length;srcSec+=(t.sections||[]).length;});var serItems=0;if(projectSchemaObj.main_session&&projectSchemaObj.main_session.tracks){projectSchemaObj.main_session.tracks.forEach(function(st){if(st.items)serItems+=st.items.length;});}Object.keys(projectSchemaObj.section_store||{}).forEach(function(sk){(projectSchemaObj.section_store[sk].tracks||[]).forEach(function(st){if(st.items)serItems+=st.items.length;});});const dataJson=JSON.stringify(projectSchemaObj);try{if(existingProjectId){await window.SonicAPI.updateCloudProject(existingProjectId,finalName,dataJson);setProjectName(finalName);setCurrentProjectId(existingProjectId);localStorage.setItem('sonic_project_name',finalName);localStorage.setItem('sonic_project_id',existingProjectId);showToast('Đã ghi đè Cloud "'+finalName+'" (src midi='+srcMidi+' clip='+srcClip+' sec='+srcSec+' | ser='+serItems+').','success');}else if(currentProjectId&&!currentProjectId.startsWith('local_')){await window.SonicAPI.updateCloudProject(currentProjectId,finalName,dataJson);setProjectName(finalName);localStorage.setItem('sonic_project_name',finalName);localStorage.setItem('sonic_project_id',currentProjectId);showToast('Đã lưu Cloud "'+finalName+'" (src midi='+srcMidi+' clip='+srcClip+' sec='+srcSec+' | ser='+serItems+' id='+currentProjectId+').',serItems===0?'warning':'success');}else{const res=await window.SonicAPI.saveCloudProject(finalName,dataJson);const newProjId=res.project_id;setProjectName(finalName);localStorage.setItem('sonic_project_name',finalName);if(newProjId){setCurrentProjectId(newProjId);localStorage.setItem('sonic_project_id',newProjId);}showToast('Đã lưu Cloud mới "'+finalName+'" (src midi='+srcMidi+' clip='+srcClip+' sec='+srcSec+' | ser='+serItems+').',serItems===0?'warning':'success');}}catch(err){showToast(err.message||"Lỗi lưu dự án lên Cloud","error");}};const handleSaveProject=async()=>{if(!currentProjectId){setSaveProjectModalOpen(true);return;}if(currentProjectId.startsWith('local_')){handleSaveLocalProject(projectName);}else{if(!currentUser){showToast('Cần đăng nhập để lưu Cloud. currentUser='+(currentUser?'OK':'NULL')+' token='+(localStorage.getItem('sonic_token')?'exists':'missing'),'warning');return;}handleSaveCloudProject(projectName);}};const handleSaveProjectRef=useRef(handleSaveProject);handleSaveProjectRef.current=handleSaveProject;// ── Ctrl-S (Save) toàn cục ── diff --git a/app/static/js/services/audioRoutingEngine.js b/app/static/js/services/audioRoutingEngine.js index d4e942b..612a7e3 100644 --- a/app/static/js/services/audioRoutingEngine.js +++ b/app/static/js/services/audioRoutingEngine.js @@ -24,7 +24,6 @@ // Idempotent: updateSfRouting/ensureMasteringRouting chay tren MOI keydown/ // note-on — neu da noi dung dest thi KHONG disconnect (disconnect flush ring // -> am phim dang vang bi cat). Chi doi khi dest/track thuc su khac. - if (window.__sfDiag) window.__sfDiag("ROUTE to=" + (trackId || "masterBus") + " prev=" + (this._trackId || "masterBus") + " bridgeReady=" + bridgeNode.isReady() + " conn=" + this._connected); if (this._connected && this._dest === dest && this._trackId === (trackId || null)) return true; if (!bridgeNode.isReady()) bridgeNode.init(); bridgeNode.connect(dest); diff --git a/app/static/js/services/bridgeAudioNode.js b/app/static/js/services/bridgeAudioNode.js index f4fa443..af6fa6b 100644 --- a/app/static/js/services/bridgeAudioNode.js +++ b/app/static/js/services/bridgeAudioNode.js @@ -15,9 +15,6 @@ var _workletNode = null; var _ctx = null; var _initialized = false; - var _frames = 0; - var _peak = 0; - var _lastDiag = 0; var _useWorklet = false; var _dest = null; // destination gain node da noi (chong connect trung) @@ -103,18 +100,7 @@ function onAudio(l, r) { if (!_initialized) init(); - if (!window.__baeAt || Date.now() - window.__baeAt >= 1000) { window.__baeAt = Date.now(); if (window.__sfDiag) window.__sfDiag('BRIDGE_AUDIO_ENTER len=' + (l ? l.length : -1)); } var blk = { l: new Float32Array(l), r: new Float32Array(r) }; - _frames = (_frames || 0) + blk.l.length; - for (var _i = 0; _i < blk.l.length; _i++) { - var _a = Math.abs(blk.l[_i]); if (_a > (_peak || 0)) _peak = _a; - var _b = Math.abs(blk.r[_i]); if (_b > (_peak || 0)) _peak = _b; - } - if (!_lastDiag || Date.now() - _lastDiag >= 1000) { - _lastDiag = Date.now(); - if (window.__sfDiag) window.__sfDiag("BRIDGE_AUDIO frames=" + _frames + " peak=" + (_peak ? (20 * Math.log10(_peak)).toFixed(1) : "-inf") + "dB path=" + (_useWorklet ? "worklet" : (_preBuffer.length ? "preBuffer" : "chunks"))); - _peak = 0; - } if (_useWorklet && _workletNode) { _workletNode.port.postMessage({ type: 'audio', l: blk.l, r: blk.r }); return; diff --git a/app/static/js/services/nativeBridgeService.js b/app/static/js/services/nativeBridgeService.js index b1e6f54..968ff05 100644 --- a/app/static/js/services/nativeBridgeService.js +++ b/app/static/js/services/nativeBridgeService.js @@ -54,7 +54,6 @@ var self = this; try { window.__TAURI__.event.listen('bridge-audio', function (e) { - if (!self._evtDiagAt || Date.now() - self._evtDiagAt >= 1000) { self._evtDiagAt = Date.now(); if (window.__sfDiag) window.__sfDiag('BRIDGE_EVT_RX cb=' + (self._audioCb ? 'yes' : 'no') + ' len=' + (e.payload && e.payload.l ? e.payload.l.length : 0)); } // Bridge recovered after bridge-down (or never bootstrapped): // audio frames flow again -> re-establish routing (Fix C). if (!self.isBridgeConnected) self._reconnectIfNeeded(); @@ -72,7 +71,6 @@ if (window.AudioRoutingEngine) window.AudioRoutingEngine.disconnect(); console.warn('[BridgeService] bridge-down event received.'); }); - if (window.__sfDiag) window.__sfDiag('EVT_REG ok'); } catch (e) { console.warn('[BridgeService] event listen failed:', e); } @@ -126,8 +124,6 @@ * data2/data3 (A12): CC value / program / PB LSB|MSB. */ dispatchMidiEvent: function (cmd, channel, pitch, velocity, sampleOffset, data2, data3, live) { - this._pushEnter = (this._pushEnter || 0) + 1; - if (this._pushEnter % 100 === 0 && window.__sfDiag) window.__sfDiag('PUSH_ENTER cmd=' + cmd + ' ch=' + channel + ' tauri=' + !!this._tauri()); if (!this._tauri()) return false; var safeVelocity = Math.floor(Math.min(1.0, Math.max(0.0, velocity)) * 127); var byteCmd; @@ -137,8 +133,6 @@ case 'PITCH_BEND': byteCmd = 0xE; break; default: byteCmd = cmd === 'NOTE_ON' ? 0x9 : 0x8; break; } - this._pushSent = (this._pushSent || 0) + 1; - if (this._pushSent % 100 === 0 && window.__sfDiag) window.__sfDiag('PUSH_SENT ' + this._pushSent); // DIAG: Promise.race 800ms — invoke gửi đi nhưng không settle -> timeout // (phan biet main-thread block vs Rust hang). Promise.race([ @@ -153,12 +147,7 @@ live: !!live }), new Promise(function (_, rej) { setTimeout(function () { rej(new Error('timeout-800ms')); }, 800); }) - ]).then(function () { - this._pushOk = (this._pushOk || 0) + 1; - if ((_pushOk + (_pushFail || 0)) % 100 === 0 && window.__sfDiag) window.__sfDiag('PUSH_MIDI ok=' + _pushOk + ' fail=' + (_pushFail || 0)); - }).catch(function (e) { - this._pushFail = (this._pushFail || 0) + 1; - if (_lastPushFail !== String(e)) { _lastPushFail = String(e); if (window.__sfDiag) window.__sfDiag('PUSH_MIDI_FAIL ' + e); } + ]).catch(function (e) { console.warn('[BridgeService] push_midi_event:', e); }); return true; diff --git a/app/static/js/services/unifiedMidiRouter.js b/app/static/js/services/unifiedMidiRouter.js index 5fcc6ab..46342c1 100644 --- a/app/static/js/services/unifiedMidiRouter.js +++ b/app/static/js/services/unifiedMidiRouter.js @@ -56,13 +56,10 @@ var sampleOffset = opts.sampleOffset || 0; if (typeof ch === 'string') ch = this.allocateChannel(ch, opts.percussion); if (ch === undefined || ch === null) ch = 0; - this._pushEvt = (this._pushEvt || 0) + 1; - if (this._pushEvt % 100 === 0 && window.__sfDiag) window.__sfDiag('PUSH_ROUTE cmd=' + cmd + ' ch=' + ch + ' brAct=' + this.isBridgeActive() + ' nbs=' + !!window.NativeBridgeService + ' tauri=' + !!(window.__TAURI__)); if (this.isBridgeActive()) { window.NativeBridgeService.dispatchMidiEvent(cmd, ch, pitch, vel, sampleOffset, opts.data2, opts.data3, opts.live); return; } - if (this._pushEvt % 100 === 0 && window.__sfDiag) window.__sfDiag('PUSH_FALLBACK cmd=' + cmd + ' ch=' + ch); if (this.onFallback) this.onFallback(cmd, ch, pitch, vel); }, diff --git a/app/templates/index.html b/app/templates/index.html index 35eb0a0..eb1b752 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -39,10 +39,10 @@ - - - - + + + + @@ -51,7 +51,7 @@ - +