|
|
|
@@ -7307,6 +7307,7 @@ const loadVstToBridge = async (track) => {
|
|
|
|
|
if (!window.__bridgeLoadedPaths) window.__bridgeLoadedPaths = {};
|
|
|
|
|
const tId = track.id || track.trackId;
|
|
|
|
|
const bch = window.SonicMidiRouter.allocateChannel(tId, false);
|
|
|
|
|
markTrackLoad(tId, { state: 'loading', channel: bch });
|
|
|
|
|
try {
|
|
|
|
|
// V8 bug 5: engine _resolve_bridge_asset chi match FILE theo filename —
|
|
|
|
|
// KHONG walk folder VST3 ("X.vst3/X.vst3.dll") → 404. Dung path da luu
|
|
|
|
@@ -7331,11 +7332,12 @@ const loadVstToBridge = async (track) => {
|
|
|
|
|
}
|
|
|
|
|
// Đã load ĐÚNG path vào channel này rồi → skip (không LOAD trùng). Bỏ
|
|
|
|
|
// early-return mù theo flag — flag sai khi C++ restore instrument khác.
|
|
|
|
|
if (knownPath && window.__bridgeLoadedPaths[bch] === knownPath) return true;
|
|
|
|
|
if (knownPath && window.__bridgeLoadedPaths[bch] === knownPath) { markTrackLoad(tId, { state: 'done' }); return true; }
|
|
|
|
|
// Có path đáng tin → load thẳng qua Tauri invoke (không cần POST/auth/CORS).
|
|
|
|
|
if (knownPath) {
|
|
|
|
|
const ok = await window.NativeBridgeService.loadInstrument(knownPath, btype, bch);
|
|
|
|
|
if (ok) { window.__bridgeLoadedChannels[bch] = true; window.__bridgeLoadedPaths[bch] = knownPath; if (!window.__bridgePluginPaths) window.__bridgePluginPaths = {}; window.__bridgePluginPaths[instrumentId] = knownPath; }
|
|
|
|
|
if (!ok) markTrackLoad(tId, { state: 'failed' });
|
|
|
|
|
console.log('[Bridge] auto-load track', track.id, '->', btype, 'ch', bch, ok ? 'OK' : 'FAIL', knownPath);
|
|
|
|
|
return !!ok;
|
|
|
|
|
}
|
|
|
|
@@ -7355,12 +7357,13 @@ const loadVstToBridge = async (track) => {
|
|
|
|
|
}
|
|
|
|
|
} catch (e2) { console.warn('[Bridge] listPlugins fallback fail:', e2); }
|
|
|
|
|
}
|
|
|
|
|
if (!p) return false;
|
|
|
|
|
if (!p) { markTrackLoad(tId, { state: 'failed' }); return false; }
|
|
|
|
|
const ok = await window.NativeBridgeService.loadInstrument(p, btype, bch);
|
|
|
|
|
if (ok) { window.__bridgeLoadedChannels[bch] = true; window.__bridgeLoadedPaths[bch] = p; }
|
|
|
|
|
if (!ok) markTrackLoad(tId, { state: 'failed' });
|
|
|
|
|
console.log('[Bridge] auto-load track', track.id, '->', btype, 'ch', bch, ok ? 'OK' : 'FAIL', p);
|
|
|
|
|
return !!ok;
|
|
|
|
|
} catch (e) { console.warn('[Bridge] loadVstToBridge fail:', track.id, e); return false; }
|
|
|
|
|
} catch (e) { console.warn('[Bridge] loadVstToBridge fail:', track.id, e); markTrackLoad(tId, { state: 'failed' }); return false; }
|
|
|
|
|
};
|
|
|
|
|
// GM font dự phòng khi soundfont track không có trong thư viện (VD id
|
|
|
|
|
// sf_generaluser_gs do AI sinh nhưng user chưa tải font) — ưu tiên font GM
|
|
|
|
@@ -7385,6 +7388,7 @@ const loadAnyTrackToBridge = async (track) => {
|
|
|
|
|
const se = track.synth_engine || {};
|
|
|
|
|
const bank = se.soundfont_bank !== undefined ? se.soundfont_bank : (track.soundfont_bank !== undefined ? track.soundfont_bank : 0);
|
|
|
|
|
const bch = window.SonicMidiRouter.allocateChannel(track.id || track.trackId, bank === 128);
|
|
|
|
|
markTrackLoad(track.id || track.trackId, { state: 'loading', channel: bch });
|
|
|
|
|
let pl = null;
|
|
|
|
|
try {
|
|
|
|
|
const sfId = inst.replace('sf_', '');
|
|
|
|
@@ -7408,15 +7412,37 @@ const loadAnyTrackToBridge = async (track) => {
|
|
|
|
|
p = r2 && r2.path ? r2.path : null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!p) { console.warn('[Bridge] SF2 resolve fail:', track.id, inst); return false; }
|
|
|
|
|
if (!p) { console.warn('[Bridge] SF2 resolve fail:', track.id, inst); markTrackLoad(track.id || track.trackId, { state: 'failed' }); return false; }
|
|
|
|
|
const ok = await window.NativeBridgeService.loadInstrument(p, 'SF2', bch);
|
|
|
|
|
if (ok) { if (!window.__bridgeLoadedChannels) window.__bridgeLoadedChannels = {}; window.__bridgeLoadedChannels[bch] = true; if (!window.__bridgeLoadedPaths) window.__bridgeLoadedPaths = {}; window.__bridgeLoadedPaths[bch] = p; }
|
|
|
|
|
if (!ok) markTrackLoad(track.id || track.trackId, { state: 'failed' });
|
|
|
|
|
console.log('[Bridge] preload SF2 track', track.id, '-> ch', bch, ok ? 'OK' : 'FAIL', p);
|
|
|
|
|
return !!ok;
|
|
|
|
|
} catch (e) { console.warn('[Bridge] preload SF2 fail:', track.id, e); return false; }
|
|
|
|
|
} catch (e) { console.warn('[Bridge] preload SF2 fail:', track.id, e); markTrackLoad(track.id || track.trackId, { state: 'failed' }); return false; }
|
|
|
|
|
}
|
|
|
|
|
try { return await loadVstToBridge(track); } catch (e) { return false; }
|
|
|
|
|
};
|
|
|
|
|
// D16: per-track loading ruler (TCP). window.__trackLoadState[trackId] =
|
|
|
|
|
// { state:'pending'|'loading'|'done'|'failed', channel, startedTs, doneTs }.
|
|
|
|
|
// Poller trong App (loadTick) chuyển loading→done khi channel xuất hiện trong
|
|
|
|
|
// loaded_channels (queryStatus) — đúng cho cả C++ restore (bridge_state.json,
|
|
|
|
|
// chạy từ spawn) lẫn JS load — không đánh dấu done sớm khi C++ guard skip.
|
|
|
|
|
const markTrackLoad = (trackId, patch) => {
|
|
|
|
|
if (!trackId) return;
|
|
|
|
|
if (!window.__trackLoadState) window.__trackLoadState = {};
|
|
|
|
|
const cur = window.__trackLoadState[trackId] || { state: 'pending', channel: null, startedTs: 0, doneTs: 0 };
|
|
|
|
|
const next = Object.assign({}, cur, patch);
|
|
|
|
|
if (patch.state === 'loading' && cur.state !== 'loading') next.startedTs = Date.now();
|
|
|
|
|
if (patch.state === 'done' && !patch.doneTs) next.doneTs = Date.now();
|
|
|
|
|
window.__trackLoadState[trackId] = next;
|
|
|
|
|
if (window.__onTrackLoadState) { try { window.__onTrackLoadState(trackId); } catch (e) {} }
|
|
|
|
|
};
|
|
|
|
|
if (typeof document !== 'undefined' && !document.getElementById('sf-ruler-style')) {
|
|
|
|
|
const _st = document.createElement('style');
|
|
|
|
|
_st.id = 'sf-ruler-style';
|
|
|
|
|
_st.textContent = '@keyframes sf-ruler { 0% { transform: translateX(-110%); } 100% { transform: translateX(340%); } } .sf-ruler-bar { animation: sf-ruler 1.1s ease-in-out infinite; } @keyframes sf-ruler-done { 0% { opacity: 1; } 100% { opacity: 0; } } .sf-ruler-done { animation: sf-ruler-done 1.4s ease-out forwards; }';
|
|
|
|
|
document.head.appendChild(_st);
|
|
|
|
|
}
|
|
|
|
|
const ensureAndOpenVstGui = async (track) => {
|
|
|
|
|
const trackId = track.trackId || track.id;
|
|
|
|
|
const instrumentId = track.instrumentId || (track.synth_engine && track.synth_engine.plugin_id);
|
|
|
|
@@ -7522,6 +7548,28 @@ const wheelSteps = (key, deltaY, deltaMode, pxPerStep = 100) => {
|
|
|
|
|
// the soundfonts; the per-track channel re-selection happens at note time.
|
|
|
|
|
const preloadTrackInstruments = async (tracks) => {
|
|
|
|
|
const list = tracks || [];
|
|
|
|
|
// D13: bridge load TRƯỚC vòng WASM — vòng selectInstrument (parse font WASM
|
|
|
|
|
// từng track, có thể ~20s) trước đây chặn vòng bridge → user bấm play khi
|
|
|
|
|
// app vừa load: NOTE_ON vào channel bridge chưa có instrument → im lặng tới
|
|
|
|
|
// khi bridge load xong (~27s từ bootstrap — dbg.log play_first_audible).
|
|
|
|
|
if (window.NativeBridgeService && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
|
|
|
|
// D16b: pre-mark TẤT CẢ track có instrument thành loading TRƯỚC loop — loop
|
|
|
|
|
// nối tiếp (C++ g_loadMutex) khiến track 2-9 không có ruler tới khi track 1
|
|
|
|
|
// xong → UI lâu không có loading item. markTrackLoad channel=null hiện ruler
|
|
|
|
|
// ngay; loadAnyTrackToBridge set channel khi tới lượt; poller chuyển done.
|
|
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
|
|
const t = list[i];
|
|
|
|
|
const inst = t.instrumentId || (t.synth_engine && t.synth_engine.plugin_id);
|
|
|
|
|
if (!inst) continue;
|
|
|
|
|
markTrackLoad(t.id || t.trackId, { state: 'loading', channel: null });
|
|
|
|
|
}
|
|
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
|
|
const t = list[i];
|
|
|
|
|
const inst = t.instrumentId || (t.synth_engine && t.synth_engine.plugin_id);
|
|
|
|
|
if (!inst) continue;
|
|
|
|
|
try { await loadAnyTrackToBridge(t); } catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (window.SonicSF && window.SonicSF.selectInstrument) {
|
|
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
|
|
const t = list[i];
|
|
|
|
@@ -7536,17 +7584,7 @@ const preloadTrackInstruments = async (tracks) => {
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Lỗi 3: mở lại project → VST3 đã lưu phải load lại vào bridge (không mở GUI).
|
|
|
|
|
if (window.NativeBridgeService && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
|
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
|
|
const t = list[i];
|
|
|
|
|
const inst = t.instrumentId || (t.synth_engine && t.synth_engine.plugin_id);
|
|
|
|
|
if (!inst) continue;
|
|
|
|
|
try { await loadAnyTrackToBridge(t); } catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ProfileModal = ({
|
|
|
|
|
isOpen,
|
|
|
|
|
onClose,
|
|
|
|
@@ -18741,6 +18779,8 @@ const App = () => {
|
|
|
|
|
const activeAudioRecordersRef = useRef({});
|
|
|
|
|
const pianoRollRecorderRef = useRef(null);
|
|
|
|
|
const activeMidiPitchesRef = useRef(new Set());
|
|
|
|
|
const timelinePitchesRef = useRef({}); // D18c: per-track Set timeline MIDI pitches dang keu (NOTE_ON fired)
|
|
|
|
|
const tracksRef = useRef([]); // D18c: tracks state moi nhat (guard mute/solo fire-time)
|
|
|
|
|
const [activeMidiPitches, setActiveMidiPitches] = useState(new Set());
|
|
|
|
|
const recordingPCMDataRef = useRef({});
|
|
|
|
|
const recordingStartTimeRef = useRef(0);
|
|
|
|
@@ -18922,6 +18962,32 @@ const App = () => {
|
|
|
|
|
if (isMidiSoundTrack(t)) {
|
|
|
|
|
try { sendMidiVolumeCC7(t, effective, audible); } catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
// D18: track vừa MẤT tiếng (mute/solo) — dừng note đang giữ ngay. CC7=0
|
|
|
|
|
// (sendMidiVolumeCC7) không ăn với VST như Qin (bỏ qua channel volume)
|
|
|
|
|
// → note kêu tiếp (tremolo) khi solo track khác. NOTE_OFF từng pitch đang
|
|
|
|
|
// giữ + CC123/CC120 all-notes-off trên channel track đó.
|
|
|
|
|
if (!audible && wasAudible && isMidiSoundTrack(t)) {
|
|
|
|
|
try {
|
|
|
|
|
const _isPerc = !!(t.synth_engine && t.synth_engine.soundfont_bank === 128) || !!t.is_percussion;
|
|
|
|
|
const _held = activeMidiPitchesRef.current || new Set();
|
|
|
|
|
// D18c: timeline MIDI notes (per-track) dang keu - NOTE_OFF tung pitch;
|
|
|
|
|
// truoc day chi kill preview keyboard (activeMidiPitchesRef) -> note
|
|
|
|
|
// timeline (Qin VST2) cu ngan/loop (tremolo) khi solo track khac.
|
|
|
|
|
const _tSet = (timelinePitchesRef.current || {})[t.id];
|
|
|
|
|
if (_tSet && _tSet.size) {
|
|
|
|
|
_tSet.forEach(function (_p) {
|
|
|
|
|
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: t.id, pitch: _p, velocity: 0, percussion: _isPerc, live: true });
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
_held.forEach(function (_p) {
|
|
|
|
|
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: t.id, pitch: _p, velocity: 0, percussion: _isPerc, live: true });
|
|
|
|
|
});
|
|
|
|
|
window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: t.id, pitch: 123, velocity: 0, data2: 0, percussion: _isPerc });
|
|
|
|
|
window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: t.id, pitch: 120, velocity: 0, data2: 0, percussion: _isPerc });
|
|
|
|
|
if (heldMidiNotesRef.current) delete heldMidiNotesRef.current[t.id];
|
|
|
|
|
if (timelinePitchesRef.current) delete timelinePitchesRef.current[t.id];
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
if (audible && !wasAudible) becameAudible = true;
|
|
|
|
|
trackAudibleRef.current[t.id] = audible;
|
|
|
|
|
});
|
|
|
|
@@ -19114,6 +19180,31 @@ const App = () => {
|
|
|
|
|
const [editingTrackName, setEditingTrackName] = useState(null); // trackId being edited
|
|
|
|
|
const [editingClipName, setEditingClipName] = useState(null); // { trackId, clipId }
|
|
|
|
|
const [editNameInput, setEditNameInput] = useState('');
|
|
|
|
|
// D16: per-track loading ruler — ticker buộc re-render khi có track đang
|
|
|
|
|
// loading; poller queryStatus chuyển loading→done khi channel sẵn sàng
|
|
|
|
|
// (đúng cho cả C++ restore từ spawn lẫn JS load — không phụ thuộc thứ tự).
|
|
|
|
|
const [loadTick, setLoadTick] = useState(0);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
window.__onTrackLoadState = () => setLoadTick(t => t + 1);
|
|
|
|
|
const iv = setInterval(async () => {
|
|
|
|
|
try {
|
|
|
|
|
if (!window.NativeBridgeService || !window.SonicMidiRouter || !window.SonicMidiRouter.isBridgeActive()) return;
|
|
|
|
|
const m = window.__trackLoadState || {};
|
|
|
|
|
const loadingKeys = Object.keys(m).filter(k => m[k].state === 'loading');
|
|
|
|
|
if (!loadingKeys.length) return;
|
|
|
|
|
const st = await window.NativeBridgeService.queryStatus();
|
|
|
|
|
const lc = (st && Array.isArray(st.loaded_channels)) ? st.loaded_channels : null;
|
|
|
|
|
let changed = false;
|
|
|
|
|
loadingKeys.forEach(k => {
|
|
|
|
|
const s = m[k];
|
|
|
|
|
if (lc && s.channel != null && lc.some(x => x.channel === s.channel)) { m[k] = Object.assign({}, s, { state: 'done', doneTs: Date.now() }); changed = true; }
|
|
|
|
|
else if (!s.startedTs || Date.now() - s.startedTs > 90000) { m[k] = Object.assign({}, s, { state: 'failed' }); changed = true; }
|
|
|
|
|
});
|
|
|
|
|
if (changed) setLoadTick(t => t + 1);
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}, 400);
|
|
|
|
|
return () => { delete window.__onTrackLoadState; clearInterval(iv); };
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// ── Context Menu & Clipboard ──
|
|
|
|
|
const [contextMenu, setContextMenu] = useState(null); // { x, y, trackId }
|
|
|
|
@@ -19582,6 +19673,7 @@ const App = () => {
|
|
|
|
|
}, [activeTab, sessionTabs, subTabs, tracks]);
|
|
|
|
|
const activeTracksRef = useRef([]);
|
|
|
|
|
activeTracksRef.current = activeTracks;
|
|
|
|
|
tracksRef.current = tracks; // D18c: sync tracks state moi nhat (guard fire-time)
|
|
|
|
|
const sessionTabsRef = useRef(sessionTabs);
|
|
|
|
|
sessionTabsRef.current = sessionTabs;
|
|
|
|
|
|
|
|
|
@@ -23883,12 +23975,14 @@ const App = () => {
|
|
|
|
|
// để master FX bị teardown giữa lúc section đang phát).
|
|
|
|
|
if (fxRtWarmIdleTimer) { clearTimeout(fxRtWarmIdleTimer); fxRtWarmIdleTimer = null; }
|
|
|
|
|
restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain
|
|
|
|
|
// V30: flush ring audio cu (engine idle blocks tu truoc) — xoa backlog truoc
|
|
|
|
|
// khi play; neu khong block dau cua play cho ring cu chay het (~341ms cap cu).
|
|
|
|
|
try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {}
|
|
|
|
|
// D18: loop WRAP (fxRtWrapRestart) — KHONG flush ring bridge: ring dang chua
|
|
|
|
|
// tail pass cu + block moi (bridge render lien tuc); flush tai seam = gap
|
|
|
|
|
// trong → giat beat 0:0. Play that van flush (V30) de bo block idle cu.
|
|
|
|
|
const _wrapRestart = fxRtWrapRestart;
|
|
|
|
|
const _wrapMode = _wrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38
|
|
|
|
|
try { if (!_wrapRestart && window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {}
|
|
|
|
|
// V31: xoa backlog outQueue master FX (block idle ~341ms) — play moi khong
|
|
|
|
|
// cho phia sau; flush ring bridge (V30) khong chaym queue nay.
|
|
|
|
|
const _wrapMode = fxRtWrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38
|
|
|
|
|
try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: _wrapMode }); } catch (e) {}
|
|
|
|
|
if (!st.buffer) {
|
|
|
|
|
st.buffer = context.createBuffer(1, 128, context.sampleRate);
|
|
|
|
@@ -24349,6 +24443,7 @@ const App = () => {
|
|
|
|
|
const end = Math.max(st.selectionStart, st.selectionEnd);
|
|
|
|
|
if (bufferPos >= end) {
|
|
|
|
|
fxRtWrapRestart = true; // V38: loop wrap restart
|
|
|
|
|
try { _dbgLog('wrap_info', 'solo=' + (hasAnySolo ? 1 : 0) + ' local=' + (selectionMode === 'local' ? 1 : 0) + ' tracks=' + (tracks ? tracks.length : -1) + ' nl=' + nativeLoopSrcsRef.current.size); } catch (e) {}
|
|
|
|
|
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
|
|
|
|
|
stopAllPlayback();
|
|
|
|
|
setSubTabs(prev => prev.map(s => s.id === activeTabRef.current ? {
|
|
|
|
@@ -24377,6 +24472,7 @@ const App = () => {
|
|
|
|
|
})() : st.buffer.duration;
|
|
|
|
|
if (bufferPos >= effectiveDuration) {
|
|
|
|
|
fxRtWrapRestart = true; // V38: loop wrap restart
|
|
|
|
|
try { _dbgLog('wrap_info', 'solo=' + (hasAnySolo ? 1 : 0) + ' local=' + (selectionMode === 'local' ? 1 : 0) + ' tracks=' + (tracks ? tracks.length : -1) + ' nl=' + nativeLoopSrcsRef.current.size); } catch (e) {}
|
|
|
|
|
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
|
|
|
|
|
stopAllPlayback();
|
|
|
|
|
if (st.isLooping) {
|
|
|
|
@@ -24423,6 +24519,7 @@ const App = () => {
|
|
|
|
|
if (selRight > selLeft && updatedTime >= selRight) {
|
|
|
|
|
if (hasAnySolo || selectionMode === 'local') {
|
|
|
|
|
fxRtWrapRestart = true; // V38: loop wrap restart
|
|
|
|
|
try { _dbgLog('wrap_info', 'solo=' + (hasAnySolo ? 1 : 0) + ' local=' + (selectionMode === 'local' ? 1 : 0) + ' tracks=' + (tracks ? tracks.length : -1) + ' nl=' + nativeLoopSrcsRef.current.size); } catch (e) {}
|
|
|
|
|
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
|
|
|
|
|
stopAllPlayback();
|
|
|
|
|
startOffsetTimeRef.current = selLeft;
|
|
|
|
@@ -24437,6 +24534,7 @@ const App = () => {
|
|
|
|
|
setIsPlaying(true);
|
|
|
|
|
} else {
|
|
|
|
|
fxRtWrapRestart = true; // V38: loop wrap restart
|
|
|
|
|
try { _dbgLog('wrap_info', 'solo=' + (hasAnySolo ? 1 : 0) + ' local=' + (selectionMode === 'local' ? 1 : 0) + ' tracks=' + (tracks ? tracks.length : -1) + ' nl=' + nativeLoopSrcsRef.current.size); } catch (e) {}
|
|
|
|
|
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
|
|
|
|
|
stopAllPlayback();
|
|
|
|
|
startOffsetTimeRef.current = selLeft;
|
|
|
|
@@ -24460,6 +24558,7 @@ const App = () => {
|
|
|
|
|
}
|
|
|
|
|
if (isLoopingSelection) {
|
|
|
|
|
fxRtWrapRestart = true; // V38: loop wrap restart
|
|
|
|
|
try { _dbgLog('wrap_info', 'solo=' + (hasAnySolo ? 1 : 0) + ' local=' + (selectionMode === 'local' ? 1 : 0) + ' tracks=' + (tracks ? tracks.length : -1) + ' nl=' + nativeLoopSrcsRef.current.size); } catch (e) {}
|
|
|
|
|
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
|
|
|
|
|
stopAllPlayback();
|
|
|
|
|
startOffsetTimeRef.current = 0;
|
|
|
|
@@ -25321,6 +25420,14 @@ const App = () => {
|
|
|
|
|
setTimeout(function () {
|
|
|
|
|
if (epoch !== bridgeNoteEpochRef.current) return;
|
|
|
|
|
if (!guardPlay()) return;
|
|
|
|
|
// D18b: mute/solo guard tai thoi diem fire — note-on dang cho cua track
|
|
|
|
|
// vua tro nen inaudible (solo track khac / mute) khong duoc phat ra.
|
|
|
|
|
const _list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : (tracksRef.current && tracksRef.current.length ? tracksRef.current : activeTracks);
|
|
|
|
|
const _trk = _list.find(t => t.id === trkId) || track;
|
|
|
|
|
if (!_trk || computeTrackAudibleGain(_list, _trk) <= 0) return;
|
|
|
|
|
// D18c: track pitch dang keu (timeline) - de applyAllTrackMuteSolo kill
|
|
|
|
|
// dung note khi track thanh inaudible giua chung (Qin VST2 tremolo).
|
|
|
|
|
try { var _tps = timelinePitchesRef.current; (_tps[trkId] || (_tps[trkId] = new Set())).add(pitch || 60); } catch (e) {}
|
|
|
|
|
var _rt = window.SonicMidiRouter;
|
|
|
|
|
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: trkId, pitch: pitch || 60, velocity: velocity || 0.8, percussion: isPerc }); } catch (e) { console.warn('[Midi] pushEvent error:', e && e.message); }
|
|
|
|
|
}, delayMs);
|
|
|
|
@@ -25328,6 +25435,8 @@ const App = () => {
|
|
|
|
|
if (epoch !== bridgeNoteEpochRef.current) return;
|
|
|
|
|
if (!guardPlay()) return;
|
|
|
|
|
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: trkId, pitch: pitch || 60, velocity: 0, percussion: isPerc }); } catch (e) {}
|
|
|
|
|
// D18c: xoa khoi per-track set (note da tat - khong con can kill).
|
|
|
|
|
try { var _tps2 = timelinePitchesRef.current; if (_tps2[trkId]) _tps2[trkId].delete(pitch || 60); } catch (e) {}
|
|
|
|
|
}, delayMs, durMs || 1000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@@ -25344,12 +25453,14 @@ const App = () => {
|
|
|
|
|
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
|
|
|
|
|
// V30: flush ring audio cu (engine idle blocks tu truoc) — xoa backlog truoc
|
|
|
|
|
// khi play; neu khong block dau cua play cho ring cu chay het (~341ms cap cu).
|
|
|
|
|
try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {}
|
|
|
|
|
// D18: loop WRAP (fxRtWrapRestart) — KHONG flush ring bridge: ring dang chua
|
|
|
|
|
// tail pass cu + block moi (bridge render lien tuc); flush tai seam = gap
|
|
|
|
|
// trong → giat beat 0:0. Play that van flush (V30) de bo block idle cu.
|
|
|
|
|
const _wrapRestart = fxRtWrapRestart;
|
|
|
|
|
const _wrapMode = _wrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38
|
|
|
|
|
try { if (!_wrapRestart && window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {}
|
|
|
|
|
// V31: xoa backlog outQueue master FX (block idle ~341ms) — play moi khong
|
|
|
|
|
// cho phia sau; flush ring bridge (V30) khong chaym queue nay.
|
|
|
|
|
const _wrapMode = fxRtWrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38
|
|
|
|
|
try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: _wrapMode }); } catch (e) {}
|
|
|
|
|
_dbgPlayStartMs = performance.now();
|
|
|
|
|
_dbgFirstAudioPending = true;
|
|
|
|
@@ -25852,12 +25963,12 @@ const App = () => {
|
|
|
|
|
const context = getAudioContext();
|
|
|
|
|
_nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade
|
|
|
|
|
restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain
|
|
|
|
|
// V30: flush ring audio cu (engine idle blocks tu truoc) — xoa backlog truoc
|
|
|
|
|
// khi play; neu khong block dau cua play cho ring cu chay het (~341ms cap cu).
|
|
|
|
|
try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {}
|
|
|
|
|
// V31: xoa backlog outQueue master FX (block idle ~341ms) — play moi khong
|
|
|
|
|
// cho phia sau; flush ring bridge (V30) khong chaym queue nay.
|
|
|
|
|
try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: 'play' }); } catch (e) {}
|
|
|
|
|
// D18: piano-roll wrap (fxRtWrapRestart set o loop restart) — KHONG flush
|
|
|
|
|
// ring + clear_queue mode 'loop' (giu queue + wrapFade) nhu main path; truoc
|
|
|
|
|
// day luon mode 'play' → wipe + re-buffer moi pass = giat. KHONG reset flag
|
|
|
|
|
// (startSubTabPlayback goi sau van can doc de clear dung mode).
|
|
|
|
|
try { if (!fxRtWrapRestart && window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {}
|
|
|
|
|
try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: fxRtWrapRestart ? 'loop' : 'play' }); } catch (e) {}
|
|
|
|
|
// V10: piano roll loop restart cũng gọi stopAllPlayback() trước (STOP#1 →
|
|
|
|
|
// bridge transportStopped=true) → mọi note-on bị drop nếu không PLAY lại.
|
|
|
|
|
// Hủy STOP#2 (150ms) + báo PLAY để bridge nhận note mới (loop liên tục).
|
|
|
|
@@ -26057,18 +26168,11 @@ const App = () => {
|
|
|
|
|
};
|
|
|
|
|
const stopAllPlayback = (fade) => {
|
|
|
|
|
try {
|
|
|
|
|
if (fxRtWrapRestart && fxRtChain().length === 0 && nativeLoopSrcsRef.current.size === 0 && masterBus && masterBus.muteGain && masterBus.muteGain.gain) {
|
|
|
|
|
// V41: dip CHỈ khi bypass (chain rỗng). FX active → seam thật xảy ra sau
|
|
|
|
|
// latency (~80ms) tại _now+latency; dip tại _now chỉ tạo dropout 14ms giữa
|
|
|
|
|
// đuôi pass cũ (audible) mà không che được seam click.
|
|
|
|
|
// V42: rút ngắn dip 14ms → 5ms (2ms xuống + 3ms lên) — đủ che click seam
|
|
|
|
|
// nhưng dropout quá ngắn để nghe thấy (V39 14ms tạo dropout rõ).
|
|
|
|
|
const _actx = getAudioContext(); const _now = _actx.currentTime;
|
|
|
|
|
masterBus.muteGain.gain.cancelScheduledValues(_now);
|
|
|
|
|
masterBus.muteGain.gain.setValueAtTime(masterBus.muteGain.gain.value || 1, _now);
|
|
|
|
|
masterBus.muteGain.gain.linearRampToValueAtTime(0.0001, _now + 0.002);
|
|
|
|
|
masterBus.muteGain.gain.linearRampToValueAtTime(1, _now + 0.005);
|
|
|
|
|
} // V39: dip input truoc hard-stop
|
|
|
|
|
// D14: bỏ dip muteGain tại wrap (loop) — 5ms master→0.0001 mỗi vòng =
|
|
|
|
|
// âm beat 0:0 nhỏ hơn hẳn âm cuối pass (bug seam volume). Click seam giờ
|
|
|
|
|
// xử bằng release mượt (CC123, không CC120 hard-cut — D15) + worklet
|
|
|
|
|
// WRAP_FADE 256 (FX path) → không cần che bằng dropout master.
|
|
|
|
|
|
|
|
|
|
_dbgLog('stop_start', 'fade=' + !!fade + ' ' + _dbgBridgeGain());
|
|
|
|
|
isPlayingRef.current = false;
|
|
|
|
|
// Gap 7: xoa automation schedule — tra gain master ve 1.0 (fader rieng).
|
|
|
|
@@ -26100,6 +26204,7 @@ const App = () => {
|
|
|
|
|
// cũ bắn vào vòng mới = NOTE_OFF cắt note đang kêu → crack (xem ref).
|
|
|
|
|
bridgeNoteEpochRef.current++;
|
|
|
|
|
try { clearAllPendingNoteOffs(); } catch (e) {}
|
|
|
|
|
try { timelinePitchesRef.current = {}; } catch (e) {} // D18c: moi note timeline da tat theo transport stop
|
|
|
|
|
_cc11Epoch++; // CC11 timers lan play cu vo hieu (da clear o tren)
|
|
|
|
|
try { clearAllCC11Timers(); } catch (e) {}
|
|
|
|
|
try { if (subTabsRef.current) subTabsRef.current = subTabsRef.current.map(s => ({ ...s, isPlaying: false })); } catch (e) {}
|
|
|
|
@@ -26189,22 +26294,36 @@ const App = () => {
|
|
|
|
|
stopAllNativeSfNotes(); stopAllNativeVstNotes();
|
|
|
|
|
// D2/D6: bridge active -> transport STOP (bridge flush toàn pitch, hết
|
|
|
|
|
// âm ngân tức thì; A13 C++ flush note-off).
|
|
|
|
|
// D17: wrap (loop restart) — BỎ transport STOP. C++ STOP làm allNotesOff
|
|
|
|
|
// + CC64=0 trên cả 16 channel → cắt mọi note (kể cả note ngân qua seam)
|
|
|
|
|
// + có thể stall audio loop ~20ms (dbg.log: wrap play_start→first_audio
|
|
|
|
|
// δ17-32ms vs restart thường δ3-5ms) = "reload" tại beat 0:0. CC123 (D15)
|
|
|
|
|
// đã lo release JS-side; PLAY vẫn reset playhead (playheadSamples chỉ đổi
|
|
|
|
|
// qua transport); transportStopped giữ false → note-on pass mới không bị
|
|
|
|
|
// drop (V8 guard). Stop/seek thật vẫn gửi STOP.
|
|
|
|
|
const _wrapNoStop = !!(fxRtWrapRestart && !stale);
|
|
|
|
|
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) {
|
|
|
|
|
try { window.NativeBridgeService.transport('stop'); } catch (e) {}
|
|
|
|
|
_dbgLog('stop_transport', 'epoch=' + stopEpoch);
|
|
|
|
|
if (!_wrapNoStop) { try { window.NativeBridgeService.transport('stop'); } catch (e) {} }
|
|
|
|
|
_dbgLog('stop_transport', 'epoch=' + stopEpoch + (_wrapNoStop ? ' wrap-skip' : ''));
|
|
|
|
|
// V39: MIDI panic CC123/CC120 tai wrap — VSTi note treo (khong nhan note-off) phai tat ngay.
|
|
|
|
|
// D15: wrap (loop) → chỉ CC123 (all-notes-off, release mượt); bỏ CC120
|
|
|
|
|
// (all-sound-off, hard cut → click + âm beat 0:0 tụt). Release chồng
|
|
|
|
|
// lên re-noteon pass mới → seam như cùng khuôn nhạc. Stop thật giữ CC120.
|
|
|
|
|
try {
|
|
|
|
|
const _wrapRelease = !!(fxRtWrapRestart && !stale);
|
|
|
|
|
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive && window.SonicMidiRouter.isBridgeActive()) {
|
|
|
|
|
Object.keys(trackMidiChannelsRef.current || {}).forEach(function (_tid) {
|
|
|
|
|
var _ch = trackMidiChannelsRef.current[_tid];
|
|
|
|
|
window.NativeBridgeService.dispatchMidiEvent('CC', _ch, 123, 0, 0, 0, 0, false);
|
|
|
|
|
window.NativeBridgeService.dispatchMidiEvent('CC', _ch, 120, 0, 0, 0, 0, false);
|
|
|
|
|
if (!_wrapRelease) window.NativeBridgeService.dispatchMidiEvent('CC', _ch, 120, 0, 0, 0, 0, false);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (e) { console.warn('[Stop] midi panic error:', e); }
|
|
|
|
|
// V27: xóa audio còn buffer trong pipeline (preBuffer/chunks + worklet
|
|
|
|
|
// ring) — nếu không, âm trước stop còn trong ring phát tiếp sau fade.
|
|
|
|
|
try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) { console.warn('[Stop] bridge flush error:', e); }
|
|
|
|
|
// D18: wrap (_wrapNoStop) — KHÔNG flush (giữ dòng liên tục qua seam);
|
|
|
|
|
// flush ở đây + ở startTrackPlayback = ring rỗng → gap giật beat 0:0.
|
|
|
|
|
if (!_wrapNoStop) { try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) { console.warn('[Stop] bridge flush error:', e); } }
|
|
|
|
|
// V8 bug 3: transport stop thu 2 sau 150ms — note-on timer guard sync
|
|
|
|
|
// qua React effect (cham) van bay toi bridge sau STOP → retrigger am
|
|
|
|
|
// treo. V10: timer lưu ref — startTrackPlayback (loop restart) hủy
|
|
|
|
@@ -34001,7 +34120,38 @@ STRICT CONSTRAINTS:
|
|
|
|
|
}, /*#__PURE__*/React.createElement("i", {
|
|
|
|
|
"data-lucide": "trash-2",
|
|
|
|
|
className: "w-3 h-3"
|
|
|
|
|
}))))), /*#__PURE__*/React.createElement("div", {
|
|
|
|
|
}))))), (function () {
|
|
|
|
|
const _ls = window.__trackLoadState && window.__trackLoadState[track.id];
|
|
|
|
|
if (_ls && _ls.state === 'loading') {
|
|
|
|
|
return /*#__PURE__*/React.createElement("div", {
|
|
|
|
|
className: "flex items-center gap-1.5 mt-0.5",
|
|
|
|
|
title: "Đang nạp instrument vào bridge — track sẽ phát sau khi load xong"
|
|
|
|
|
}, /*#__PURE__*/React.createElement("div", {
|
|
|
|
|
className: "flex-1 h-1 rounded bg-zinc-800 overflow-hidden"
|
|
|
|
|
}, /*#__PURE__*/React.createElement("div", {
|
|
|
|
|
className: "h-full rounded sf-ruler-bar",
|
|
|
|
|
style: {
|
|
|
|
|
background: '#06b6d4',
|
|
|
|
|
width: '45%'
|
|
|
|
|
}
|
|
|
|
|
})), /*#__PURE__*/React.createElement("span", {
|
|
|
|
|
className: "text-[9px] text-cyan-400 font-mono shrink-0"
|
|
|
|
|
}, "Loading…"));
|
|
|
|
|
}
|
|
|
|
|
if (_ls && _ls.state === 'failed') {
|
|
|
|
|
return /*#__PURE__*/React.createElement("div", {
|
|
|
|
|
className: "mt-0.5 text-[9px] text-red-500 font-mono"
|
|
|
|
|
}, "Load failed");
|
|
|
|
|
}
|
|
|
|
|
if (_ls && _ls.state === 'done' && _ls.doneTs && Date.now() - _ls.doneTs < 2500) {
|
|
|
|
|
return /*#__PURE__*/React.createElement("div", {
|
|
|
|
|
className: "mt-0.5 h-1 rounded bg-green-600/30 overflow-hidden"
|
|
|
|
|
}, /*#__PURE__*/React.createElement("div", {
|
|
|
|
|
className: "h-full bg-green-500 sf-ruler-done"
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
})(), /*#__PURE__*/React.createElement("div", {
|
|
|
|
|
className: "flex flex-col gap-0.5 text-xs",
|
|
|
|
|
onClick: e => e.stopPropagation()
|
|
|
|
|
}, /*#__PURE__*/React.createElement("div", {
|
|
|
|
|