fix(bounce): offline render qua native bridge — bỏ gap realtime, thêm headroom, strip DIAG
This commit is contained in:
+42
-55
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user