V45: D17 seamless loop wrap + D18 stop solo note-off (Qin) + D18b/c guard + engine VST load fixes

- D17: wrap_info/wrap-skip, fx wrap xfade, playhead-derived scheduling
- D18: solo stop -> NOTE_OFF all sounding timeline pitches + CC123/CC120
- D18b: track audible gain guard, tracksRef fallback
- D18c: timelinePitchesRef per-track Set, kill at applyAllTrackMuteSolo
- bridge ring _MAX 16, index v=202608290928
- plugins.py/vst_engine.py/fx_realtime.py/JuceFxEngine.cpp: VSTi load + Qin fixes (uncommitted 12:18)
This commit is contained in:
2026-08-29 16:39:02 +07:00
parent 2afa828c8f
commit c084ac8ba7
9 changed files with 434 additions and 104 deletions
+7 -3
View File
@@ -1,13 +1,10 @@
import os, sys, uuid, json, tempfile, subprocess, time as _time, threading, asyncio, logging
import numpy as np
import soundfile as sf
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Query, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse
from pydantic import BaseModel
from typing import Optional, Any
from app.config import settings
from app.core.vst_engine import PluginManager, HAS_PYFLUIDSYNTH, render_soundfont_midi_to_audio, RENDER_LOCK, _render_locked
from app.core.render_engine import PythonRenderEngine, _find_sf2_path, _find_default_sf2
from app.core.soundfont_inspector import SoundFontInspector
from app.core.soundfont_converter import SoundFontConverter
from app.core.soundfont_scanner import SoundFontAutoScanner
@@ -570,6 +567,7 @@ async def delete_soundfont(sf_id: str, current_user: dict = Depends(get_current_
@router.get("/soundfonts/download/{sf_id}")
async def download_soundfont_asset(sf_id: str):
from app.core.render_engine import _find_sf2_path, _find_default_sf2
clean_id = sf_id.replace("sf_", "") if sf_id.startswith("sf_") else sf_id
# Cũng tìm trong static/soundfonts (font bundled theo deployment) — trước
# đây chỉ UPLOAD + SYSTEM → font bundled 404 → incognito (IndexedDB rỗng)
@@ -963,6 +961,7 @@ class FxRenderRequest(BaseModel):
@router.post("/fx-render")
async def fx_render(req: FxRenderRequest, current_user: dict = Depends(get_current_user)):
"""Áp fx_chain lên WAV đầu vào → WAV mới trong PROCESSED_DIR."""
import soundfile as sf
enforce_password_changed(current_user)
src = (req.input_path or "").strip()
if not src:
@@ -1233,6 +1232,8 @@ async def soundfont_render(req: SoundfontRenderRequest, current_user: dict = Dep
Khác FluidSynthWASM (docker): âm render server-side bằng pyfluidsynth, trả
WAV để client play. Dùng cho: preview/keyboard, play MIDI item khi chạy
transport ở môi trường standalone."""
from app.core.render_engine import _find_sf2_path, _find_default_sf2
import soundfile as sf
enforce_password_changed(current_user)
if not req.notes:
raise HTTPException(status_code=400, detail="Chưa có nốt nhạc để render")
@@ -1288,6 +1289,9 @@ def render_project(
current_user: dict = Depends(get_current_user)
):
enforce_password_changed(current_user)
from app.core.render_engine import PythonRenderEngine
import soundfile as sf
import numpy as np
engine = PythonRenderEngine()
# Prevent path traversal: strip directory components; force .wav/.ogg.
safe_name = os.path.basename((req.output_filename or "render_output.wav").replace("\\", "/"))
+4 -3
View File
@@ -23,10 +23,7 @@ import threading
import time
import uuid
import numpy as np
from app.config import settings
from app.core.native_render import find_fx_bridge_exe, find_juce_fx_bridge_exe
FXRT_MAGIC = 0x46585254
FXRT_BLOCK = 256
@@ -91,6 +88,7 @@ class FxRealtimeSession:
"""Một session realtime: SHM + subprocess fx_vst_bridge + I/O block."""
def __init__(self, fx_chain, sample_rate, block_size=FXRT_BLOCK):
import numpy as np
from multiprocessing import shared_memory
self.fx_chain = fx_chain or []
self.sample_rate = int(sample_rate)
@@ -163,6 +161,7 @@ class FxRealtimeSession:
return out
def _spawn(self):
from app.core.native_render import find_fx_bridge_exe, find_juce_fx_bridge_exe
juce_exe = os.getenv("SF_JUCE_FX_BRIDGE_PATH", "").strip()
# G3: mặc định dùng JUCE bridge (latency graph thật + processed samples);
# fallback bridge cũ khi không có exe JUCE (regression an toàn).
@@ -265,6 +264,7 @@ class FxRealtimeSession:
"""Gom FXRT_IN_SLOTS frame vào pending, đủ 4 → publish atomic vào ring
(ghi data toàn bộ slot trước, sau đó in_write += n 1 lần). Ring đầy khi
publish → drop batch cũ nhất (giữ latency thấp)."""
import numpy as np
arr = np.frombuffer(interleaved, dtype=np.float32)
if arr.size != self.block_size * 2:
return
@@ -333,6 +333,7 @@ class FxRealtimeSession:
self.stats['flush'] += 1
def read_output(self):
import numpy as np
"""Drain output ring → list bytes (mỗi block 2048 bytes interleaved)."""
out = []
with self._lock:
+3 -1
View File
@@ -1,9 +1,9 @@
# SonicForge Studio VST / VSTi Engine Service
from __future__ import annotations
import os
import json
import re
import time as _time
import numpy as np
import functools
import threading
from ctypes import c_char_p
@@ -52,6 +52,7 @@ def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int =
Xử lí đúng note CHỒNG nhau + đuôi release (event-stream noteon/noteoff).
Trả stereo float32 (2, N). Raise RuntimeError nếu pyfluidsynth thiếu / SF
không load được."""
import numpy as np
import fluidsynth as _fs
settings = _fs.new_fluid_settings()
_fs.fluid_settings_setnum(settings, b'synth.sample-rate', float(sr))
@@ -112,6 +113,7 @@ def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int =
@_render_locked
def render_midi_events_to_audio(midi_events: list, sr: int = 44100, bpm: float = 120.0, instrument: str = 'synth') -> np.ndarray:
import numpy as np
beat_duration_sec = 60.0 / max(30.0, bpm)
max_duration_sec = 2.0
for event in midi_events:
+196 -46
View File
@@ -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", {
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -31,7 +31,7 @@
console.warn('[BridgeAudioNode] AudioWorklet unavailable — ScriptProcessor fallback');
return false;
}
var url = '/static/js/services/bridgeAudioWorklet.js?v=202608272301';
var url = '/static/js/services/bridgeAudioWorklet.js?v=202608291200';
_ctx.audioWorklet.addModule(url).then(function () {
if (!_initialized) return; // disconnect() happened while loading
if (_useWorklet) return; // already set up (init retry)
+3 -2
View File
@@ -1,6 +1,7 @@
// app/static/js/services/bridgeAudioWorklet.js
// VSTi crackle fix: audio sink chay tren audio thread (khong jank main thread).
// - Ring block 256 mau, toi da 8 block (~42ms @48k) — hap thu jitter pump
// - Ring block 256 mau, toi da 16 block (~85ms @48k) — hap thu jitter pump
// (option 2: 8->16 de dung tremolo crackle; backlog lon hon ~85ms)
// nhung gioi han backlog: 64 block (~371ms) tao tre ~0.3-0.4s khi play/
// keyboard preview (audio moi phai cho ring cu chay het).
// - Underrun: fade ve 0 thay vi click (hard silence).
@@ -10,7 +11,7 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
constructor() {
super();
this._ring = [];
this._MAX = 8;
this._MAX = 16;
this._gain = 1.0;
this._lastOutL = 0.0;
this._lastOutR = 0.0;
+2 -2
View File
@@ -40,7 +40,7 @@
<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=202608250700"></script>
<script src="/static/js/services/bridgeAudioNode.js?v=202608250700"></script>
<script src="/static/js/services/bridgeAudioNode.js?v=202608291200"></script>
<script src="/static/js/services/unifiedMidiRouter.js?v=202608271630"></script>
<script src="/static/js/services/audioRoutingEngine.js?v=202608250700"></script>
<script src="/static/js/services/aiGateway.js?v=202608037200"></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=202608281631" defer></script>
<script src="/static/js/app.precompiled.js?v=202608290928" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+130 -1
View File
@@ -9,6 +9,7 @@
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iostream>
@@ -72,6 +73,103 @@ bool jBool(const json_object_s* o, const char* key, bool def) {
return def;
}
int64_t jInt64(const json_object_s* o, const char* key, int64_t def) {
const json_object_element_s* m = jMember(o, key);
if (m && m->value && m->value->type == json_type_number) {
const auto* n = static_cast<const json_number_s*>(m->value->payload);
return (int64_t)std::strtoll(n->number, nullptr, 10);
}
return def;
}
uint32_t jUInt32(const json_object_s* o, const char* key, uint32_t def) {
const json_object_element_s* m = jMember(o, key);
if (m && m->value && m->value->type == json_type_number) {
const auto* n = static_cast<const json_number_s*>(m->value->payload);
return (uint32_t)std::strtoul(n->number, nullptr, 10);
}
return def;
}
std::string jsonEscape(const std::string& s) {
std::string out;
out.reserve(s.size());
for (char c : s) {
if (c == '\\' || c == '"') { out += '\\'; out += c; }
else out += c;
}
return out;
}
// --- VST3 description cache -------------------------------------------------
// findAllTypesForFile() slow path load DLL + quét factory + instantiate
// component (hàng trăm ms/plugin mỗi session). createInstanceFromDescription
// chỉ cần {fileOrIdentifier, name, uniqueId, deprecatedUid} —
// findClassMatchingDescription so name+uid trên class info, KHÔNG instantiate.
// Cache JSON theo path+mtime: hit → dựng desc trực tiếp, bỏ qua scan.
struct DescCacheEntry {
std::string path;
int64_t mtime = 0;
std::string name;
uint32_t uniqueId = 0;
uint32_t deprecatedUid = 0;
};
std::string descCachePath() {
return juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
.getChildFile("SonicForgeDAW")
.getChildFile("juce_vst3_desc_cache.json")
.getFullPathName()
.toStdString();
}
std::vector<DescCacheEntry> loadDescCache() {
std::vector<DescCacheEntry> out;
const juce::File f(descCachePath());
if (!f.existsAsFile()) return out;
const juce::String content = f.loadFileAsString();
json_value_s* root = json_parse(content.toRawUTF8(), content.getNumBytesAsUTF8());
if (root && root->type == json_type_array) {
const auto* arr = static_cast<const json_array_s*>(root->payload);
for (const json_array_element_s* el = arr->start; el; el = el->next) {
const json_value_s* v = el->value;
if (!v || v->type != json_type_object) continue;
const auto* o = static_cast<const json_object_s*>(v->payload);
DescCacheEntry e;
e.path = jString(o, "path", "");
e.mtime = jInt64(o, "mtime", 0);
e.name = jString(o, "name", "");
e.uniqueId = jUInt32(o, "unique_id", 0);
e.deprecatedUid = jUInt32(o, "deprecated_uid", 0);
if (!e.path.empty() && !e.name.empty())
out.push_back(std::move(e));
}
}
if (root) free(root);
return out;
}
void saveDescCache(const std::vector<DescCacheEntry>& cache) {
std::string json = "[\n";
for (size_t i = 0; i < cache.size(); ++i) {
const auto& e = cache[i];
if (i) json += ",\n";
json += " {\"path\":\"" + jsonEscape(e.path) +
"\",\"mtime\":" + std::to_string(e.mtime) +
",\"name\":\"" + jsonEscape(e.name) +
"\",\"unique_id\":" + std::to_string(e.uniqueId) +
",\"deprecated_uid\":" + std::to_string(e.deprecatedUid) + "}";
}
json += "\n]\n";
try {
const juce::File f(descCachePath());
f.getParentDirectory().createDirectory();
f.replaceWithText(juce::String(json));
} catch (...) {
// Cache lỗi không được làm hỏng prepare() — lần sau scan lại.
}
}
} // namespace
struct JuceFxEngine::Slot {
@@ -155,6 +253,8 @@ void JuceFxEngine::prepare(uint32_t sampleRate, uint32_t blockSize) {
// Slot bypass / type khác vst3 / load fail → passthrough (latency 0).
impl_->pluginNodes.assign(impl_->slots.size(), nullptr);
impl_->slotLatencies.assign(impl_->slots.size(), 0);
static std::vector<DescCacheEntry> descCache = loadDescCache();
bool cacheDirty = false;
juce::AudioProcessorGraph::Node::Ptr prev = inNode;
for (size_t i = 0; i < impl_->slots.size(); ++i) {
const Slot& s = impl_->slots[i];
@@ -162,7 +262,35 @@ void JuceFxEngine::prepare(uint32_t sampleRate, uint32_t blockSize) {
continue;
juce::VST3PluginFormat fmt;
juce::OwnedArray<juce::PluginDescription> descs;
fmt.findAllTypesForFile(descs, juce::File(s.path).getFullPathName());
const juce::File pluginFile(s.path);
const int64_t mtime = pluginFile.getLastModificationTime().toMilliseconds();
const DescCacheEntry* hit = nullptr;
for (const auto& e : descCache)
if (e.path == s.path && e.mtime == mtime) { hit = &e; break; }
if (hit) {
// Cache hit: dựng desc (name+uid) — không scan factory, không
// instantiate 2 lần như findAllTypesForFile slow path.
auto* d = new juce::PluginDescription();
d->fileOrIdentifier = juce::String(hit->path);
d->name = juce::String(hit->name);
d->uniqueId = hit->uniqueId;
d->deprecatedUid = hit->deprecatedUid;
d->pluginFormatName = "VST3";
descs.add(d);
} else {
fmt.findAllTypesForFile(descs, pluginFile.getFullPathName());
if (!descs.isEmpty()) {
const auto* d = descs.getFirst();
DescCacheEntry e;
e.path = s.path;
e.mtime = mtime;
e.name = d->name.toStdString();
e.uniqueId = d->uniqueId;
e.deprecatedUid = d->deprecatedUid;
descCache.push_back(std::move(e));
cacheDirty = true;
}
}
if (descs.isEmpty()) {
std::cerr << "[JuceFxEngine] slot " << i << " no VST3 desc: "
<< s.path << std::endl;
@@ -195,6 +323,7 @@ void JuceFxEngine::prepare(uint32_t sampleRate, uint32_t blockSize) {
}
prev = node;
}
if (cacheDirty) saveDescCache(descCache);
for (int ch = 0; ch < 2; ++ch) {
NCh src; src.nodeID = prev->nodeID; src.channelIndex = ch;
NCh dst; dst.nodeID = outNode->nodeID; dst.channelIndex = ch;