fix(bounce): offline render qua native bridge — bỏ gap realtime, thêm headroom, strip DIAG

This commit is contained in:
2026-08-25 09:11:29 +07:00
parent 4037dbf40d
commit dd17f87e4e
7 changed files with 66 additions and 107 deletions
+42 -55
View File
@@ -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();
+18 -17
View File
@@ -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<nextStart)nextStart=m.start_beat;});if(nextStart<n.start_beat+n.duration_beats){n.duration_beats=Math.max(0.01,nextStart-n.start_beat-0.01);}});return out;};// Polyphony gain compensation (spec §II.1): scale velocity theo số voice đồng
// thời tại thời điểm note bắt đầu — Equal Power G = 1/sqrt(N). Solo N=1 → giữ
// nguyên; chord 8 note → -9dB mỗi voice, tổng peak không vượt trần.
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&&nStart<mEnd)active++;});if(active>1){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<trackStartAbs)trackStartAbs=itemStartAbs;(item.notes||[]).forEach(function(n){absNotes.push({pitch:n.pitch||60,start_beat:itemStartAbs/secPerBeat+(n.start_beat||0),duration_beats:n.duration_beats||1,velocity:n.velocity!=null?n.velocity:0.8});});});return{absNotes:_scaleNativePolyphony(_cleanNativeNotes(absNotes)),trackStartAbs:trackStartAbs};};const _nativeTrackKey=(track,bpm,baseOffsetSec,notesOverride)=>{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&&nStart<mEnd)active++;});if(active>1){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<trackStartAbs)trackStartAbs=itemStartAbs;(item.notes||[]).forEach(function(n){absNotes.push({pitch:n.pitch||60,start_beat:itemStartAbs/secPerBeat+(n.start_beat||0),duration_beats:n.duration_beats||1,velocity:n.velocity!=null?n.velocity:0.8});});});return{absNotes:_scaleNativePolyphony(_cleanNativeNotes(absNotes)),trackStartAbs:trackStartAbs};};const _nativeTrackKey=(track,bpm,baseOffsetSec,notesOverride)=>{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;i<half;i++){l[i]=f[i*2];r[i]=f[i*2+1];}entry.node.port.postMessage({type:'out',l,r});};ws.onclose=()=>fxRtTrackStop(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<f.length;i+=512)entry.ws.send(f.slice(i,i+512).buffer);}else{entry.ws.send(dd.data);}}catch(e){}}}if(entry.inQueue.length){if(entry.pumpTimer)clearTimeout(entry.pumpTimer);entry.pumpTimer=setTimeout(pump,Math.max(1,FXRT_BURST_MS-(now-entry.lastInSent)));}else if(entry.pumpTimer){clearTimeout(entry.pumpTimer);entry.pumpTimer=null;}};pump();};}catch(e){console.warn('[FX RT track] setup fail',trackId,path,e);fxRtTrackStop(trackId,path,true);}};const fxRtTrackUnsplice=(trackId,path)=>{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;i<n;i++){chunk[i*2]=L[i];chunk[i*2+1]=R[i];}chunks.push(chunk);};masterBus.output.connect(script);script.connect(audioCtx.destination);// Rewind + play the FULL project from the start (FluidSynth + FX racks +
// mastering chain — exactly what you hear).
@@ -2174,7 +2175,7 @@ const totalFrames=chunks.reduce((a,c)=>a+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<s.length;i++)view.setUint8(o+i,s.charCodeAt(i));};writeString(0,'RIFF');view.setUint32(4,fileSizeBytes-8,true);writeString(8,'WAVE');writeString(12,'fmt ');view.setUint32(16,16,true);view.setUint16(20,fmtTag,true);view.setUint16(22,outChannels,true);view.setUint32(24,captureRate,true);view.setUint32(28,captureRate*bytesPerSample*outChannels,true);view.setUint16(32,bytesPerSample,true);view.setUint16(34,bitDepth,true);writeString(36,'data');view.setUint32(40,frames*bytesPerSample*outChannels,true);let o=44;const lsb=bitDepth===24?1/0x800000:bitDepth===8?1/128:1/0x8000;// 1 LSB in float
for(let i=0;i<frames;i++){for(let ch=0;ch<outChannels;ch++){const raw=stereoBuf[i*2+ch];if(isFloatBit){view.setFloat32(o,raw,true);// bit-exact, no clamp/quantize
}else{// Single-stage TPDF dither, then clamp, then quantize (final export only)
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(()=>{});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&&currentProjectId.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&&currentProjectId.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 ──
@@ -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);
-14
View File
@@ -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;
+1 -12
View File
@@ -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;
@@ -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);
},
+5 -5
View File
@@ -39,10 +39,10 @@
<script src="/static/js/services/storage.js?v=202608038200"></script>
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608101800"></script>
<script src="/static/js/services/nativeBridgeService.js?v=202608250200"></script>
<script src="/static/js/services/bridgeAudioNode.js?v=202608250500"></script>
<script src="/static/js/services/unifiedMidiRouter.js?v=202608250054"></script>
<script src="/static/js/services/audioRoutingEngine.js?v=202608250107"></script>
<script src="/static/js/services/nativeBridgeService.js?v=202608250700"></script>
<script src="/static/js/services/bridgeAudioNode.js?v=202608250700"></script>
<script src="/static/js/services/unifiedMidiRouter.js?v=202608250700"></script>
<script src="/static/js/services/audioRoutingEngine.js?v=202608250700"></script>
<script src="/static/js/services/aiGateway.js?v=202608037200"></script>
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>
@@ -51,7 +51,7 @@
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
<script src="/static/js/app.precompiled.js?v=202608250300" defer></script>
<script src="/static/js/app.precompiled.js?v=202608250700" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {