368 Commits

Author SHA1 Message Date
3dtours c32c77e4f4 fix(ui): master chain panel - xoa toolbar Delta Listen, GUI VST mo mac dinh spawn fresh moi lan mo panel, busy spinner + banner loi + nut Thu lai khi spawn GUI cham/fail 2026-09-02 20:32:20 +07:00
3dtours 8e4fed5f05 fix(api): /fx-gui/close dong TAT CA bridge trung plugin (khong chi proc dau) 2026-09-02 12:41:16 +07:00
3dtours 33c5cc290f fix(api,ui): chan double-spawn GUI embed master chain - register bridge ngay sau Popen + inflight guard + frontend busy ref (crash 0xc0000005 2 instance Ozone) 2026-09-02 12:34:46 +07:00
3dtours f793ebce9b fix(bridge): first-paint kick real click — iZotope GUI embed hien thi that 2026-09-02 11:44:16 +07:00
3dtours d3094a3ba5 fix(ui): add VST FX vao master chain tu dong mo GUI embed
MasteringModal co 2 duong add: addMasterVst (goi openMasterVstGui, ham
chet khong ai dung) va addVstToChain (picker dung, chi them vao chain
khong mo GUI) -> user phai bam vao slot moi thay GUI. Gop: addVstToChain
goi openMasterVstGui ngay sau khi them slot.

Verified: engine-from-source serve bundle co fix; app installed (installer
rebuilt -Frontend -Tauri): Ozone GUI OPEN 11.3s, frames 10/10 jpeg, spawn.log
sach 0x7E/Fatal.
2026-09-02 10:01:48 +07:00
3dtours d068927694 fix(bridge): Ozone FX GUI embed crash license check — sleep truoc attach + retry 5 spawn
Root cause (minidump x3, cung offset iZOzone11Core.dll+0x18f6b92): crash
deterministic 1 instruction - memcmp std::string [rdi+0x100] vs "iLok"
trong license check, doc heap ptr garbage (use-after-free/corrupt trong
iZotope Product Engine license path, flaky theo timing).

Fixes:
- RenderFxJob.cpp: fxGuiLoadAttach giu retry 4 attempt moi instance moi
  (SEH-guarded dispose); them Sleep(2s) giua load va attach cho plugin
  iZotope/Ozone - Product Engine service het race, attempt 1 open ngay
  (E2E: 60-70s -> 10-22s, 8/8 run OK).
- plugins.py: _spawn_fx_gui_embed process-level respawn 3->5 lan + delay
  2s giua respawn (service can thoi gian hoi phuc sau crash); di chuyen
  BELOW_NORMAL/CREATE_NO_WINDOW sang _fx_gui_creationflags(); gui stdout
  bridge qua thread de khong block event loop (asyncio.to_thread).
- main_fx.cpp: FxCrashDumpHandler log MOI exception (khong chi lan dau),
  minidump van 1 lan/process.

Verified: E2E engine-from-source 8/8 run Ozone embed OK; app installed
(thu cai moi installer NSIS): OPEN 49.5s, frames 10/10 jpeg, spawn.log
khong con 0x7E/Fatal Error.
2026-09-02 09:40:17 +07:00
3dtours 5b90bd43b5 bump sf-fx-realtime cache-bust version 2026-09-01 22:49:30 +07:00
3dtours 4da46d87c8 fix(bridge): skip toàn bộ iZotope trong scan + VST2 load — wrapper 0x7E/FastFail hết dialog 2026-09-01 22:26:40 +07:00
3dtours aaf58db56f fix(bridge): VST2 iZotope wrapper 0x7E — add Cores dirs to PATH; skip Ozone Pro fastfail 2026-09-01 22:07:10 +07:00
3dtours be5ecf0a48 fix(ui): Plugin Manager Scan circular JSON — onClick pas synthetic event lam dirsOverride (JSON.stringify event circular) 2026-09-01 21:47:52 +07:00
3dtours 31f19fa537 bridge: skip Ozone 11 modules (license fastfail c0000409 kills scan/render)
Ozone 11 (iZotope) tu __fastfail FATAL_APP_EXIT khi load — khong bi SEH chan,
giet ca bridge process. Scan -> mat ca dir -> FX list rong; render/realtime ->
crash. Khong load: scan emit UNKNOWN, load tra loi sach. Ozone Pro van OK.
ponytail: SandboxVst3Host (1 module 1 process) khi can Ozone 11.
2026-09-01 20:37:59 +07:00
3dtours de8abd12fa fix(bridge): NOMINMAX trong VstDllSearch.h — windows.h macro min/max pha std::min (C2589) 2026-09-01 18:10:08 +07:00
3dtours 1acb6f23f1 fix(bridge): 0x7E iZotope — append core DLL dirs (Program Files/iZotope/*/Cores) vao PATH truoc Module::create
iZotope VST3 single-file (Ozone 11.vst3) reference core DLL
(iZOzone11Core.dll) dat rieng o C:\Program Files\iZotope\<Product>\Cores,
khong canh .vst3. SetDllDirectoryW chi phu dir canh plugin -> LoadLibraryW
fail ERROR_MOD_NOT_FOUND (0x7E) -> Windows dialog khi app load plugin.
VstDllSearch.h quet cac dir Cores co dinh + append vao PATH (process-global,
bridge load 1 plugin 1 luc). Ap dung ca RenderFxJob (FX chain + scan) va
Vst3Instrument (VSTi).
2026-09-01 18:08:10 +07:00
3dtours a8a3038dd2 fix(engine): bundle bridge tu bridge-win/Release (MSBuild) khong phai build/Release (CMake cu)
engine.spec chi nhin native_bridge/build/Release -> bundle chua
fx_vst_bridge.exe cu/hong 282KB (--scan crash c0000409 ngay ca voi dir
khong ton tai) -> Plugin Manager scan chet. Fix: exe tu bridge-win/Release,
DLL tu build/Release, fallback ve build/Release cho checkout cu.
2026-09-01 17:31:41 +07:00
3dtours 67b8658015 V45.39b: engine_port.txt ghi port+engine_exe_path, get_engine_port doi chieu path engine cua chinh app (2 instance khong doc lan port cua nhau) 2026-09-01 17:02:23 +07:00
3dtours 2c4d6c358b V45.39: fix SERVER OFFLINE lau + UI load cham - engine ghi port file (engine_port.txt), UI bootstrap doc port engine cua chinh no (get_engine_port) + timeout/retry 60s, health poll self-heal reload, khong taskkill /IM daw_engine (giet engine instance khac), /api/v1/plugins/available chay scan trong threadpool (khong chan event loop -> /health het timeout) 2026-09-01 16:45:06 +07:00
3dtours 1e0314249d V45.38: bundle VC++ runtime app-local (MSVCP140/VCRUNTIME140) vao installer+portable - bridge C++ link /MD can runtime canh exe, khong phu thuoc vc_redist he thong (fix 0x7E ERROR_MOD_NOT_FOUND tren may khong cai redist) 2026-09-01 16:19:33 +07:00
3dtours c1b73db971 Merge main (V45.1-32 fixes: ReferenceError guard, crackle/solo/seek fixes) vao standalone-shm-bridge (giu FILL 96) 2026-09-01 16:03:28 +07:00
3dtours c8e2d0f6e2 V45.37: build_windows.ps1 full-build - dung path CMake build/bridge-win (vcxproj+Release), Continue+LASTEXITCODE (native stderr), kill engine theo path khong dong production 2026-09-01 12:26:04 +07:00
3dtours 3aed7e93ea V45.37: build_windows.ps1 full-build - dung path CMake build/bridge-win (vcxproj+Release), Continue+LASTEXITCODE (native stderr), kill engine theo path khong dong production 2026-09-01 12:25:43 +07:00
3dtours 855a753e26 V45.36: build_install.ps1 robust voi native stderr (Continue+LASTEXITCODE), kill theo path install/dist khong dong production 2026-09-01 12:12:18 +07:00
3dtours aad25f4049 V45.36: build_install.ps1 robust voi native stderr (Continue+LASTEXITCODE), kill theo path install/dist khong dong production 2026-09-01 12:12:14 +07:00
3dtours 400b5e5370 V45.35: fix build_install/build_windows copy layout PHANG - Copy-Item wildcard tran noi dung _internal ra root (exe treo luc chay); dung robocopy /MIR giu dung cau truc onedir exe+_internal 2026-09-01 11:44:06 +07:00
3dtours e691c43e62 V45.35: fix build_install/build_windows copy layout PHANG - Copy-Item wildcard tran noi dung _internal ra root (exe treo luc chay); dung robocopy /MIR giu dung cau truc onedir exe+_internal 2026-09-01 11:44:04 +07:00
3dtours 1932467539 V45.34: ban cai phai SACH - engine.spec loai dev config (DB/.secret_key/plugin_dirs.json/sf_scan_state.json) khoi bundle; build_install.ps1 + build_windows.ps1 fail-fast neu dist dinh config dev (reset ve mac dinh cho nguoi dung moi) 2026-09-01 11:31:13 +07:00
3dtours 4cd8fa780d V45.34: ban cai phai SACH - engine.spec loai dev config (DB/.secret_key/plugin_dirs.json/sf_scan_state.json) khoi bundle; build_install.ps1 + build_windows.ps1 fail-fast neu dist dinh config dev (reset ve mac dinh cho nguoi dung moi) 2026-09-01 11:31:10 +07:00
locpham a8082b27c4 V45.33: fix Plugin Manager VSTi liệt kê nhầm VST FX (Ozone Pro/Tonal Balance Control Pro) làm instrument - /available chặn entry UNKNOWN theo tên (bridge timeout introspect VST3) + _norm_path_stem 2026-09-01 10:37:19 +07:00
locpham 4713f8a8a1 V45.33: fix Plugin Manager VSTi liệt kê nhầm VST FX (Ozone Pro/Tonal Balance Control Pro) làm instrument - /available chặn entry UNKNOWN theo tên (bridge timeout introspect VST3) + _norm_path_stem 2026-09-01 10:37:12 +07:00
admin 5f8844637e V45.32: fix (1) latency preview MIDI keyboard - flush ring bridge sink (bridgeAudioWorklet _MAX 32 ~170ms audio cu render truoc) tai idle->active live note-on trong __clearMasterFxQueue (cung dieu kien V45.26: _liveHeldCount===0 && im lang >500ms) de note moi nghe ngay; guard _timelinePlaying (khong flush khi timeline dang play); (2) crackle VST2 tremolo/loop - overrun crossfade tich luy moi block drop cong 256 mau cap 1024 (~21ms) thay vi co dinh 64 mau (1.3ms qua ngan -> blip/wow khi drop loat block o chord nhieu voice) (tai lap tu V45.28, giu ring 32 de latency thap). v=202608311858 2026-08-31 19:00:24 +07:00
admin 21cc8421a5 V45.31: REVERT V45.28 (4ed1de6) ve dung trang thai b64343a (V45.27) de kiem tra latency cao - ring worklet 32 block (~170ms), worklet URL v=202608311809, bo overrun crossfade tich luy 256/cap1024 cua V45.28. v=202608311809 2026-08-31 18:48:08 +07:00
admin 833f3bd158 V45.30: REVERT V45.29 (a069773) - V45.29 ring worklet 80 (~426ms) gay latency cao khi preview midi keyboard (ring luon day, do tre = dung ring cap) va crackle khi Chord VST2; quay ve V45.28 (4ed1de6): ring 48 (~256ms), worklet URL v=202608311820, bo log worklet_stats dinh ky. v=202608311820 2026-08-31 18:44:12 +07:00
admin a069773d4a V45.29: fix wobble preview VST2 chord van con sau V45.28 - ring worklet bridgeAudioWorklet 48->80 block (~426ms) phu stall render loop C++ toi ~361ms do tu log (fxrt_bridge loopMax=361ms, fxrt_pump pump_gap 362ms, longtask d=346): ring 48 (256ms) van khong du -> underrun lap lai fade ve 0 = wobble; them worklet_stats log dinh ky moi ~2s vao dbg.log (preview_diag chi fire keybed/vkb, duong preview khac khong log ws= duoc) de xac minh ov/un sau fix. v=202608311832 2026-08-31 18:32:37 +07:00
admin 4ed1de638d V45.28: fix wobble preview VST2 loop/tremolo khi nhan chord - ring worklet bridgeAudioWorklet 32->48 block (~256ms) hap thu stall render loop C++ khi chord nhieu voice (stall >170ms -> underrun lap lai fade ve 0 nghe nhu wobble; 16->32 V45.27 da dung crackle, 32 chua du cho chord); overrun crossfade tich luy moi block drop cong 256 mau cap 1024 (~21ms) thay vi co dinh 64 mau (1.3ms qua ngan -> blip/wow khi drop loat block o chord nhieu voice). v=202608311820 2026-08-31 18:20:53 +07:00
admin b64343aac6 V45.27: fix (1) PR tab preview cam khi sub-tab PR khong ARM - ARM MAIN/SECTION cuc bo, khong leak sang PR (3 nhanh: vkbNoteOn prArmedIds + attachMidiHandler bridge bPrArmed + SonicSF arSubs them && isArmed); (2) crackle preview VST2 instrument loop/tremolo - ring worklet bridgeAudioWorklet 16->32 block (~170ms) hap thu stall render loop C++ khi instrument xu li loop/tremolo nang CPU (8->16 truoc da dung tremolo, 16 chua du); them stats overrun/underrun tu worklet -> getDiag/preview_diag (ws=ov/un) de xac minh con crackle khong. v=202608311809 2026-08-31 18:10:47 +07:00
admin 72ae2c8a9a V45.26: fix wobble/crackle khi preview MIDI keyboard nhan (1/8 lien tuc) o MAIN/SECTION/PR tab. Root cause: unifiedMidiRouter.pushEvent clear master FX outQueue (__clearMasterFxQueue, V31 fix lag idle backlog ~341ms) tren MOI live NOTE_ON -> rapid notes: moi note wipe queue dang chua tail note truoc + resume fade-in 43ms -> cat tail (crackle) + fade nhap nhay (wobble). Fix: chi clear khi that su idle->active (khong con note dang giu VA im lang >500ms) — dem _liveHeldCount + _lastLiveEventAt; note dang ngan / chuoi note nhanh giu queue. Timeline (live=false) khong doi. v=202608311756 2026-08-31 17:56:17 +07:00
admin fbefa7e864 V45.25: fix ARM leak trong PR tab - preview chi track dang edit. Root cause: vkbNoteOn + attachMidiHandler (bridge bPrArmed va SonicSF arSubs) gom ARM tu TAT CA sub-tab PR (forEach s.isArmed) + track MAIN isArmed -> ARM track 1 leak sang PR tab track 2 (khong ARM) van nghe nhac cu track 1. Fix: trong PIANO_ROLL tab, target = duy nhat sub-tab PR dang active (activeTabRef), bat ke isArmed; MAIN/SECTION giu nguyen (ARM moi, khong ARM -> cam + toast). v=202608311739 2026-08-31 17:39:40 +07:00
admin cdf71078f8 V45.24: dao y/c - solo/mute CHI cuc bo MAIN/SECTION, PIANO ROLL TAB KHONG bi cam. REVERT V45.23 (4 gate computeTrackAudibleGain o preview) + lam PR doc lap: (1) scheduleMidiNoteDispatch them param prNote=true cho 6 nhanh PR (play, ghost, click, draw, brush, wheel) -> bo qua solo_guard + goi ensureTrackPrAudible (CC7=vol theo volumeDb bo qua mute/solo + setTrackNodeGain) tai thoi diem fire; (2) preview keybed vkbNoteOn + hardware attachMidiHandler (bridge + SonicSF fallback) khi _isPrTabNow goi ensureTrackPrAudible de channel khong con bi CC7=0 tu toggle MAIN; (3) applyAllTrackMuteSolo + sync useEffect bo qua gain=0/CC7=0/kill note khi track co sub-tab PR dang PLAY. Timeline MAIN giu solo_guard. v=202608311725 2026-08-31 17:26:08 +07:00
admin 6cd4eb423a V45.23: fix Bug 4 (solo/mute khong tac dung o PIANO ROLL tab) - preview MIDI (keybed vkbNoteOn + hardware attachMidiHandler NOTE_ON bridge/SonicSF fallback) bo qua trang thai mute/solo cua MAIN/SECTION context: track muted hoac solo-out van phat am khi bam phim o PR tab. Them gate computeTrackAudibleGain vao 4 nhanh preview (bTracks, arSubs, armedTracks, vkb targets) - dung semantics nhu scheduleMidiNoteDispatch solo_guard (timeline da dung tu V45.21). vkbNoteOff/note-off bridge KHONG gate (phai kill note dang giu khi mute giua chuong). marker solo_guard block midi ch= p= cho dbg. v=202608311716 2026-08-31 17:16:49 +07:00
admin 2fe78ddd1b V45.22: fix Bug 3 (wobble loop wrap) - wrap restart skipRouting nhu seekPlaybackTo/applyPlayheadDirect: updateSfRouting (AudioRoutingEngine.connect/ disconnect bridge sink + setOutputDestination) chay o MOI wrap trong luc isPlayingRef=false -> stall main thread 122-161ms (pump_gap) > KEEP 16 (~85ms) -> outQueue can -> underrun -> wobble beat 0:0 + playhead desync. skipRouting bo routing khoi critical path wrap -> stall ve duoi KEEP -> het underrun. Them instrument: stop_start log stack caller (xac nhan mystery stop = user Space/Stop, khong phai code) + wrap_timing stop=/start= phan tach stall. v=202608311708 2026-08-31 17:09:28 +07:00
admin 7572d339de V45.21: fix Bug 1 (seek delay - seek branch resumeFill=8 thay vi RESUME_FILL 24 ~128ms -> 43ms nhu wrap V45.4; applyPlayheadDirect stopAllPlayback(false,true) skipRouting nhu seekPlaybackTo) + Bug 2 (attachMidiHandler guard _isPrTabNow nhu vkbNoteOn V45.19 - ARM piano roll khong leak ra MAIN/SESSION) v=202608311642 2026-08-31 16:43:32 +07:00
admin 75b1512779 V45.20: fix Bug 6 (piano roll trang - bo subTabsRef/activeTabRef ngoai scope trong renderKeybed -> const _kbSilent=false; renderKeybed chi chay o PR tab nen luon false) + Bug 1 debug (them pitch vao log midi_push) v=202608311305 2026-08-31 13:04:00 +07:00
admin 36f2cf79b1 V45.19: fix Bug 1b (seek#2 + wrap freeze) + Bug 2 (ARM scope theo active tab) v=202608311242
- Bug 1b: worklet clear_queue nho clearMode; dbgPlayFirst post fx_seek_first khi
  mode=seek -> main snap khong can guard 50ms (seek resume nhanh ~43ms < 50ms).
- Bug 1b: loop wrap KHONG freeze playhead nua (chi re-anchor processed) -> het
  playhead dung ~1.5s sau wrap (truoc day freeze 1500ms fail-safe).
- Bug 2: vkbNoteOn chi gom ARM piano roll khi DANG o PIANO_ROLL tab; MAIN/SECTION
  khong ARM -> targets rong -> preview cam. renderKeybed silent flag tuong tu.
- DIAG: preview_diag them trg/prTab (vkb) + sl (keybed).
2026-08-31 12:43:08 +07:00
admin 8a6a79c757 V45.18: fix seek khi play master FX active - clear_queue mode 'seek' (wipe queue cu + resume nhanh 8 block ~43ms thay vi FILL 24 ~128ms) het am tre 1 beat; DIAG preview_diag src=keybed cho Bug 2 ARM piano roll v=202608311222 2026-08-31 12:25:01 +07:00
admin 8ccf7e4b14 V45.17: fix seek khi play master FX active - set startOffsetTimeRef + __fxRtPlayheadFreeze NGAY tai click (het 50ms dau playhead chay theo processed anchor cu truoc khi dung) v=202608311151 2026-08-31 11:52:23 +07:00
admin 80b8c3e034 V45.16: freeze playhead khi seek master FX active (het playhead chay truoc am ~128ms) v=202608311143 2026-08-31 11:43:21 +07:00
admin 39f00c3c0b V45.15: fix ARM piano roll khong am - gop subTab isArmed (PIANO_ROLL) vao 2 path gui note (midi_push bridge branch + vkbNoteOn) - preview note toi track dang edit du track main khong ARM v=202608311122 2026-08-31 11:23:34 +07:00
admin 752db690b3 V45.14: diag preview_audio - dem bridge-audio frames 500ms sau note-on (tach engine render vs JS routing) v=202608311112 2026-08-31 11:11:47 +07:00
admin ad71ac1168 V45.13: diag Bug 3/4 preview khong am - preview_diag tai vkbNoteOn (routing state, BridgeAudioNode.getDiag, masterBus, fxRt, arm/tab) + sf_route tai updateSfRouting (nhanh PR_CONNECT/SINGLE_CONNECT/MASTER_FALLBACK) v=202608311101 2026-08-31 11:05:13 +07:00
admin 845823a9df V45.12: fix SF preset wrong sau reopen (race C++ restore async ghi de program JS vua gui) - retry FORCE CC0/CC32/PROGRAM sau 1.5s/4s/8s qua cua so restore, bo dedupe v=202608311035 2026-08-31 10:35:59 +07:00
admin f6104a43d5 V45.11: fix SF preset wrong after reopen - preload SF2 gui program change (C++ loadSoundFontFile reset bank/program 0 moi lan load, JS khong gui lai) v=202608300815 2026-08-30 08:15:51 +07:00
admin e57b9fade2 V45.10: fix seek lag solo track - skip sink re-point on seek restart v=202608300814 2026-08-30 08:14:42 +07:00
admin 6b18d25520 V45.9: fix solo/un-solo khi play -> soundfont delay/wobble/crackle - updateSfRouting re-point bridge sink (AudioRoutingEngine.connect -> disconnect/reconnect gainNode FluidSynth) giua main play khi midiAudible count doi (2->1) = glitch ngay; skip sink re-point khi isPlayingRef (main/session), mute/solo da xu ly bang CC7 + track gain, routing set lai khi STOP; v=202608300750 2026-08-30 07:51:18 +07:00
admin 4c0987733e V45.8: fix Qin VST2 loop wrap crackle - wrap release them NOTE_OFF per-pitch (timelinePitchesRef) vi CC123 khong an voi VST2 (Qin bo qua all-notes-off CC) -> note cu ring qua seam re-trigger cung pitch = giat; per-note OFF da chung minh an (panic 128 noteOff o STOP, D18c); v=202608292315 2026-08-29 22:42:02 +07:00
admin 85fdcb16e2 V45.7b: fix TCP loading loop — _notifyStatus chi fire cb khi connected doi (poller queryStatus 400ms tung reset map + reload track -> LOAD skip vo han) 2026-08-29 22:11:00 +07:00
admin c0fbf29d41 V45.7: fix SF2 load fail fallback — catch bridgeLoad 404 so GM fallback runs 2026-08-29 22:04:37 +07:00
admin 945e63918d V45.6 fix solo Q3: bo stopAllPlayback+restart 60ms khi solo main-session (track solo giu nguyen tieng, het wobble re-attack); GIU timelinePitchesRef qua stop - kill branch NOTE_OFF tung pitch (Qin im khi solo track khac, truoc day tset=0); C++ allNotesOff sandbox VST2 qua control 7 (child panic local CC64/CC123+128 noteOff) thay 128 note-off flood queue 64-cap (Qin het treo sau STOP); IPC toggleSolo tu applyAllTrackMuteSolo (ref patch + kill); v=202608292138 2026-08-29 21:39:59 +07:00
admin 72ae22f865 V45.5 instrument solo path (Q3 debug): dbg log solo_toggle/solo_apply/solo_kill/solo_guard/play_tracks - lan test toi thay ro trang thai ref + note bi block; C++ [Vst2Sandbox] ON/OFF/CC log trong SandboxVst2Host (Qin chay sandbox, truoc day midi vao Qin hoan toan mu trong bridge.log - chi VST3 log [midi]), v=202608291806 2026-08-29 18:09:16 +07:00
admin 82796417c1 V45.4 fix SF loop wrap gap: worklet KEEP 4->16 (85ms che stall 68-82ms, het underrun wrap dien hinh) + wrap underrun bo flush (D17 da skip engine flush; worklet flush pha gap seam) + resumeFill 8 (~43ms) thay FILL 24 (~128ms) - gap wrap ~110-235ms giam con vai chuc ms, v=202608291752 2026-08-29 17:54:36 +07:00
admin a7630f0e6d chore: bo target/ root (artifact lan) khoi index + gitignore 2026-08-29 17:50:45 +07:00
admin 788a371f72 V45.3 fix Qin tremolo khi solo track khac: activeTracksRef luon ghi state da patch (bo restore STALE) - D18b guard/scheduler thay solo-out ngay, v=202608291749 2026-08-29 17:50:12 +07:00
admin f3fd0109a7 V45.2 fix Qin silence (20s audio-thread stall) + SF loop downbeat delay
- SandboxVst2Host::loadSerializedState: queuePreset non-blocking (byte-identical
  echo never matched -> waitApply always burned 20s TIMEOUT on the audio thread;
  VST2 restore runs via runOnMain so every startup silenced ALL audio ~20s = Qin
  'failed that su'). Bridge now holds reloading + defers saveStateJob until the
  child republishes a different chunk (clobber protection, no audio block).
- sf-fx-realtime clear_queue('loop'): truncate old tail to KEEP=4 blocks
  (~21ms) instead of keeping FILL 24 (~128ms) -> wrap downbeat no longer ~110ms
  late; skipFade crossfade masks the truncation jump, seam crossfade recount.
- bump ?v= 202608291721 (app.jsx x2, index.html x2); deploy 3 _internal + dist.
2026-08-29 17:25:32 +07:00
admin 6b26d5db0e V45.1: fix load VSTi loi ReferenceError instrumentSelectorData (module scope) - guard typeof undefined, v=202608290945 2026-08-29 16:46:05 +07:00
admin c084ac8ba7 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)
2026-08-29 16:39:02 +07:00
admin 2b334c0812 V46: FILL 48->96 (cushion ~512ms) chong stall ~500ms (u2 deficit 723ms) + instrument 3 lop (wl_gap/main_in_gap/pump_hb) 2026-08-29 09:01:35 +07:00
admin 2a3e5bf08f V45: tang FILL 24->48 (cushion 256ms) chong stall OS ~200ms (PDC 14080) 2026-08-29 08:00:22 +07:00
admin 2afa828c8f V44: fix crackle - uu tien audio thread bridge (HIGHEST) + FILL 24 (cushion ~90ms) PDC 7936 2026-08-28 17:22:50 +07:00
admin 8990f04357 V43: tang FILL 8->16 chong underrun crackle (cushion 3-8 -> 11-16) + PDC 5888 2026-08-28 16:36:13 +07:00
admin bf74a0be4f G3 deploy: mac dinh dung JUCE bridge + bundle juce_fx_bridge vao engine 2026-08-28 16:21:12 +07:00
admin 6cbdbd8e9c G3: 1 clock - PDC latency that + playhead engine-derived 2026-08-28 15:51:19 +07:00
admin c72362e088 G2: JUCE VST3 hosting + latency that qua FxLatReport - probe pureComp 2108 2026-08-28 15:28:06 +07:00
admin 091abe5a91 G1: them G1_RESULTS.md 2026-08-28 12:35:59 +07:00
admin 9c0d35def4 G1: JUCE engine SHM chain rong POC - probe pass, khong lag them 2026-08-28 12:35:37 +07:00
admin d7dd626dd5 G0: dich chinh - khong co drift bridge (run2 drained=True, recv=sent=10000), rut de xuat bu drift 2026-08-28 12:15:27 +07:00
admin d78af9304f G0: baseline latency do xong - gate G2 mo, drift bridge -0.5% phat hien 2026-08-28 12:03:13 +07:00
admin ebe17d902b docs: plan JUCE - phan bien V2: xoa risk buffer toan track (Option B leftover), param queue lock-free SPSC, validate sampleRate/blockSize doi giua chung 2026-08-28 11:49:54 +07:00
admin a10f679c0d docs: plan JUCE - phan bien split-brain (1 master clock, Option A), param pump GUI->JUCE, do jitter sigma G0 2026-08-28 11:45:46 +07:00
admin 51bac2770d docs: JUCE latency plan - thay heuristic PDC bang AudioProcessorGraph (G0-G5) 2026-08-28 11:38:46 +07:00
admin fa61712ecc V42+V42A: fix seam click khi loop master FX — worklet wrap crossfade (tail->head) + native WebAudio loop cho bypass (het click beat 0); dip bypass 5ms; PDC playhead freeze/snap 2026-08-28 11:35:32 +07:00
admin 9ccc28d211 fix: use 127.0.0.1 for WebSocket when host is localhost/::1 to avoid IPv6 fallback delay (~300ms) 2026-08-26 22:45:56 +07:00
admin fa01ef02f7 V25: fix wobble khi play - re-buffer FILL sau flush (het loop underrun-flush) 2026-08-26 21:37:06 +07:00
admin f38e500326 V24: fix master-chain crackle, 1s lag, solo bypass
- worklet: FILL 48->24 (PDC 7936 dung), flush protocol khi resume sau interruption
  (clear queue + fade-in/crossfade chi voi block moi), drop block stale
- engine fx_realtime: _flush_input_locked NO-DROP (ring day -> giu pending, cap 2048),
  flush_all() dong bo in/out khi worklet resume
- app.jsx: WS flush_done + flushing guard, fallback timer 500ms
- plugins.py: xu ly {cmd:flush} -> flush_done
- index.html: bump v param
2026-08-26 20:15:13 +07:00
admin 28a06134f9 V23: fix crackle master FX - underrun fade-out tu mau cuoi + crossfade 64 mau o cap-drop/resume 2026-08-26 19:45:36 +07:00
admin 2e996af102 fix(cc11): unmute realtime dung effective list thay vi ref stale 2026-08-26 18:40:15 +07:00
admin b95c63e7db V21: keep master FX session warm on user stop - no 4s VST reload on replay
- stopAllPlayback(fade=true) giữ master FX session (không teardown) → play
  kế tiếp FX xử lý ngay, hết cửa sổ dry ~4s khi reload VST
- per-track FX vẫn teardown khi user stop (VSTi nhiều track nặng CPU)
- idle timeout 120s (FX_RT_WARM_IDLE_MS): ngừng chơi lâu → teardown giải
  phóng CPU/VST
- fxRtStart/fxRtStop/startSubTabPlayback hủy idle timer khi play
2026-08-26 18:31:25 +07:00
admin 3e6080603c v20: fix CC11 piano roll (epoch check _cc11Epoch - v19 con so _nativePlayEpoch) + SF program change khi play piano roll (bridge) 2026-08-26 18:18:20 +07:00
admin 1f78a834a8 v19: fix CC11 piano roll (epoch rieng cho CC11 timers) + Synth/GUI piano roll cho section subtrack 2026-08-26 18:07:26 +07:00
admin e53a31b525 v18: CC11 expression realtime khi play main timeline + section subTrack + local loop (dong bo voi export native_render) 2026-08-26 17:46:44 +07:00
admin 02ebf41e97 v17: Select Instrument modal highlight instrument dang load (SF preset + VST) 2026-08-26 17:17:21 +07:00
admin a0f5f98209 v16b: revert EXPRESSION_CC 7 -> 11 (test CC7 xong, ve CC11 expression) 2026-08-26 17:04:31 +07:00
admin 0e555114f8 v16: persist cc11_global_curve qua save/load (item + sub_tabs) + commitCC11Curve dong bo subTab st.notes (CC11 realtime khi Play) 2026-08-26 16:58:44 +07:00
admin ad3dfa5c47 v15b: rebuild plugin_host with CC7 fix (realtime sandbox VST3 path) 2026-08-26 16:46:00 +07:00
admin 24109f8bc4 v15b: fix CC7 to VSTi (legacy tag kHostMidiCC+cc + volume-param fallback), CC lane grid + 0/+- labels 2026-08-26 16:45:42 +07:00
admin e54dec4010 Fix: persist expression_curve qua project save/load; TEST CC7 thay CC11 2026-08-26 16:29:54 +07:00
admin f10f9873e9 Fix: carry expression_curve qua native track render + cache key 2026-08-26 16:20:53 +07:00
admin 5689536fb1 CC11: global curve from first to last note, manual node add/drag/remove 2026-08-26 16:15:47 +07:00
admin 56235623cd v13: CC11 default dashed lien tuc tu note dau den note cuoi (khong per-note); node tao tren dashed 2026-08-26 16:06:08 +07:00
admin e6b5e7f4d7 v12: CC11 default = flat bar at default automation pos (no auto nodes); click add node, drag move, right-click remove 2026-08-26 13:02:58 +07:00
admin fd50d99250 v11: fix CC11 lane sync - engine serve precompiled stale; click=add node, drag=move, right-click=remove; shared mode when no selection 2026-08-26 12:48:39 +07:00
admin b63e5b21d6 fix(cc11): replace auto-select (khong accumulate) + prefer selected note khi click lane (chord) + bump cache-buster v-param 2026-08-26 12:01:21 +07:00
admin 74dccaf54a feat(cc11): lane header CC dropdown + graph thread (2 fixed end nodes, preview-draw drag fix flicker) 2026-08-26 11:48:13 +07:00
admin a800834c89 fix(cc11): auto-select note on CC11 lane click so curve is visible
Draw branch renders expression curve only for selected notes, but
handleCC11MouseDown never selected the note -> anchors added but
invisible. Select the clicked note before adding the anchor.
2026-08-26 11:31:03 +07:00
admin 2c96a223af feat: CC11 expression automation (spec midi_note_cc11_expression)
- MIDINote schema + expression_curve (position_ratio 0..1, value 0..127)
- Piano roll CC lane mode Expression (CC11): draw/move/del anchors
- Offline: _expression_events grid 64 samples -> job.events -> RenderJob.cpp sample-accurate CC
- Realtime: scheduleNoteExpressionCC11 grid 16ms via bridge pushEvent / SonicSF fallback, epoch-guarded, cleared on stop
- tests/test_cc11_expression.py 6/6 (unit interp/grid/clamp + integration energy follows curve)
- rebuild daw_vst_bridge (narrowing fix) + sync binaries/dist, babel rebuild + sync 5 copies
2026-08-26 11:14:56 +07:00
admin c146d3eb14 fix(export): offline render VSTi qua engine khi chua play — fill midiCacheRef khong can play truoc 2026-08-26 07:51:08 +07:00
admin 26086cb331 fix(render): VSTi export lay preset dung bridge channel (midi_channel, khong phai track.id)
render_session_container da tinh midi_channel round-robin melodic giong
unifiedMidiRouter.allocateChannel, nhung VSTi branch truyen track.id vao
bridge_state_preset_b64 -> lay preset cua channel stale (VD track5 -> ch5
trong khi realtime choi ch2) -> export sai loai nhac cu. Dung midi_channel.
2026-08-25 21:33:33 +07:00
admin 57fa2e78fe fix(export): carry bridge-captured VSTi preset into offline render
Offline render tao bridge moi khong co preset -> plugin load init patch
-> am khac realtime (user tuong fallback). Gio doc presetBase64 tu
bridge_state.json (channel == track.id, plugin_path khop basename) va
truyen vao render_offline khi track khong nhung preset_data.
2026-08-25 21:14:48 +07:00
admin 014d02283d fix(engine): bundle daw_vst_bridge.exe vao frozen engine — VSTi export khong con fallback synth (native_bridge 501) 2026-08-25 20:11:50 +07:00
admin ae7299d097 fix(render): GM-only track (instrumentProgram, khong soundfont_id) render qua default SF2 thay vi oscillator synth 2026-08-25 19:48:07 +07:00
admin de4caefb50 fix(preview): download soundfont resolve plugin_dirs + fallback default GM (hết oscillator) 2026-08-25 17:54:39 +07:00
admin 6cee46d82a fix: SF render bridge-first (không fallback synth qua pyfluidsynth gain nhỏ) 2026-08-25 12:01:17 +07:00
admin ebf0828c55 fix(render): offline SF fallback tìm font GM có sẵn trong plugin_dirs user (Windows) 2026-08-25 09:45:08 +07:00
admin 51d948fdc9 fix(render): resolve VSTi offline qua plugin_dirs user + plugin_path schema
_resolve_instrument dùng PluginManager() chỉ scan VST_DIR mặc định -> VSTi
trong thư mục user (vd Steinberg\VSTPlugins\DUNE 3.dll) 404 -> render_engine
fallback oscillator synth -> bounce sai tiếng. Dùng get_plugin_manager()
(gộp plugin_dirs user) + resolve_plugin_path (lưới an toàn, chặn ext lạ).
render_offline nhận plugin_path từ schema synth_engine.plugin_path ưu tiên.
2026-08-25 09:24:55 +07:00
admin dd17f87e4e fix(bounce): offline render qua native bridge — bỏ gap realtime, thêm headroom, strip DIAG 2026-08-25 09:11:29 +07:00
admin 4037dbf40d fix(bounce): declare _lastDiag — second ReferenceError in BridgeAudioNode 2026-08-25 08:51:46 +07:00
admin ea3de8b53c fix(bounce): declare _frames/_peak in BridgeAudioNode — ReferenceError killed audio sink 2026-08-25 08:42:52 +07:00
admin cd63ae1901 debug(bounce): capture AUDIO_CB_ERR + BRIDGE_AUDIO_ENTER to find silent onAudio failure 2026-08-25 08:40:48 +07:00
admin 662383e538 debug(bounce): EVT_REG/BRIDGE_EVT_RX/WI_QUERY diag + fix PUSH_SENT typo 2026-08-25 08:11:02 +07:00
admin 8ef1935068 fix(midi): prefix _pushEnter/_pushSent/_pushOk/_pushFail with this. — ReferenceError blocked dispatchMidiEvent (v=202608250116) 2026-08-25 07:58:14 +07:00
admin 388da8b380 fix(midi): _pushEvt -> this._pushEvt — ReferenceError killed pushEvent before PUSH_ROUTE; ?v=202608250054 2026-08-25 07:55:11 +07:00
admin 109a8f5c5b debug(midi): PUSH_CALL fingerprint + PUSH_EXC + ?v=202608250051 — isolate stale-JS vs pushEvent break 2026-08-25 07:52:59 +07:00
admin 60d1eee107 debug(midi): diag PUSH_ROUTE/PUSH_ENTER — pin pushEvent->dispatchMidiEvent break point 2026-08-25 07:36:20 +07:00
admin a00c57d44f fix(ui): black screen — isExportingRef.current = isExporting dat truoc khai bao isExporting (TDZ ReferenceError moi render); bo sync, bounce set true/false tuong minh 2026-08-25 07:31:34 +07:00
admin fb59184dd2 fix(bounce): guardPlay drop het note-on — isPlayingRef bi render ghi de =false khi bounce setState; them isExportingRef vao guardPlay (bounce = dang phat that) + TMR_FIRE diag 2026-08-25 07:28:37 +07:00
admin 51891ac8b9 debug: HB heartbeat 250ms + DISPATCH_ON truoc pushEvent + PUSH_SENT/PUSH_MIDI_TIMEOUT (race 800ms) cho bounce silent — phan biet main-thread block vs Rust hang 2026-08-25 07:23:59 +07:00
admin f8a500c050 debug: diag PUSH_MIDI ok/fail cho push_midi_event (nghi midi queue full — bridge khong nhan note trong bounce) 2026-08-25 07:05:41 +07:00
admin f741e1e1f1 debug: DIAG bang overlay textarea co nut Copy thay cho window.alert (text khong copy duoc) 2026-08-25 07:01:38 +07:00
admin ba7c03ca5e fix: bounce silent — AudioRoutingEngine no longer full-tears-down sink on dest change
stopAllPlayback->updateSfRouting->startTrackPlayback reroutes bridge sink
twice during bounce; each dest change previously called this.disconnect()
= BridgeAudioNode teardown + async worklet reload, audio swallowed into
_preBuffer (bounded 64 blocks) forever if worklet never reactivates ->
silent WAV (only old reverb tail). Fix: rely on BridgeAudioNode.connect()
re-pointing _gainNode (already handles dest switch without teardown);
init() only when not ready. Add __sfDiag ROUTE + BRIDGE_AUDIO (frames/
peak/path per sec) to verify at bounce end.
2026-08-24 22:02:31 +07:00
admin 9cd6113086 debug: diag logs cho bounce silent — router/native path 2026-08-24 21:42:49 +07:00
admin bf98038572 fix(bridge): soundfont track khong co trong thu vien (sf_generaluser_gs) fallback sang GM font co san -> bounce ra du track, het cam 2026-08-24 21:13:53 +07:00
admin 74bd6d849b fix(export): offline render tu tai file ve exportDir sau khi hoan thanh
renderMasteringExport truoc day chi hien toast 'Tai ve' (phai click) ->
user tuong khong co file. Gio tu fetch res.url -> saveExportViaDialog
(ghi thang vao exportDir da chon hoac save-dialog), toast kem path.
2026-08-24 20:54:50 +07:00
admin 4b643883ff fix(export): offline render dong export panel + hien progress timeline
renderMasteringExport khong tu dong export panel/progress (chi bounce cu
lam). triggerWavExport: nhanh offline render -> setShowExportPanel(false),
setShowExportProgress(true), setExportProgress(-1) truoc khi goi, reset
progress trong finally.
2026-08-24 20:51:13 +07:00
admin 1061a36626 fix(export): MIDI project WAV export qua server offline render thay vi bounce realtime
Bounce REALTIME race (note-on drop sau STOP / channel reload / watchdog
recovery) ra file chi 1 note roi cam hoac cam han. Offline render server
deterministic da xac nhan ra nhac day du (42.67s RMS ~0.24 moi giay).

triggerWavExport: project co MIDI + source=project + format=wav -> goi
renderMasteringExport (server /api/v1/plugins/render) thay vi bounce.
Bounce chi la fallback khi server disconnected va cache MIDI khong du.
2026-08-24 20:32:19 +07:00
admin 141fa4a141 fix(export): scale TPDF dither to 1 LSB — full-scale noise made bounce exports sound like white noise 2026-08-24 20:17:24 +07:00
admin 2b7c7cef2c Fix export WAV khong mo duoc: default 32-bit float -> PCM 16 (client + server render + plugins.py) 2026-08-24 19:58:44 +07:00
admin 5b747606c7 Rebrand SonicForge Studio -> ScoreFX Studio: UI names, About logo, favicon + icons for all exe builds 2026-08-24 19:28:22 +07:00
admin 2bcc8369a5 fix(export): MIDI offline khong co am thanh - cache preview cam/khong du -> tu chuyen sang bounce REALTIME (capture masterBus.output) thay vi xuat file cam 2026-08-24 18:59:22 +07:00
admin 8639529ecd fix(export): offline export nhan MIDI-only tracks qua midi cache - filter khong loai track khong co buffer 2026-08-24 18:51:14 +07:00
admin 43b68200aa feat(export): modal tien trinh export rieng (khong dung statusbar) - dong modal settings ngay khi bam Export + bao loi that khi khong ghi duoc file 2026-08-24 18:43:50 +07:00
admin 8c972c3df2 fix(export): tu dong dong modal khi hoan tat + throttle setProgress 150ms (toc do export) + dong modal path server 2026-08-24 18:35:24 +07:00
admin 14f8340fdf fix(export): progress bar hien thi duoc - footer ngoai vung scroll + encode chunked yield + yield frame dau 2026-08-24 18:31:28 +07:00
admin fefe95d0a9 feat(export): stop transport khi export offline + check file export khong cam (peak < -60dB -> canh bao) 2026-08-24 18:26:52 +07:00
admin edbb477657 feat(export): thanh tien do offline export (render track VST -> encode -> save) trong ExportModal 2026-08-24 18:24:42 +07:00
admin 993afb9ee4 feat(export): modal chi 2 nut Export/Huy + option Realtime, Bounce MIDI chuyen vao menu File, bo auto-bounce offline 2026-08-24 18:19:11 +07:00
admin 26ca6e6ae0 fix(ui): VST GUI mo 1 click (khong kill bridge khi click lai) + File Export Mix mo modal + loading state embed 2026-08-24 18:10:48 +07:00
admin cea3b08999 fix(ui): move loop-selection effects after state init (TDZ crash -> black screen) 2026-08-24 18:02:09 +07:00
admin 51e62a1158 fix(ui): persist transport loop + panel mixer + redesign export modal (folder picker + default dir) 2026-08-24 17:47:40 +07:00
admin 09bc762218 fix(masterfx): xoa han panel Wave Observer khoi mastering modal - khong con hien sau khi dong GUI 2026-08-24 17:35:33 +07:00
admin 9c81d9c1fa fix(masterfx): Wave Observer scope khong tu chay (toggle default TAT) + preset VST master giu sau save/load 2026-08-24 17:26:36 +07:00
admin 6ec249ef13 fix(fxrt): mirror sidecar exe clean build, probe toggle do latency thuc (Ozone) - cache HIT xac nhan khong reload khi toggle 2026-08-24 17:05:13 +07:00
admin 59e4951912 fix(fxrt): toggle VST bypass khong reload plugin - cache instance trong bridge, chain giu slot bypassed, bypass state trong chainKey, poll 100ms + fade-in 512 samples che gap latency 2026-08-24 16:56:10 +07:00
admin 3a3f46b7d3 fix(fxrt): mo GUI VST khong gay crackle - GUI spawn BELOW_NORMAL, mien watchdog khi mo native GUI, realtime loop ABOVE_NORMAL 2026-08-24 16:38:52 +07:00
admin 9137e7e276 fix(fxrt): giu session realtime khi loop/seek/reschedule - het bypass+reload moi pass loop
- fxRtKeepForRestart flag: nhánh restart/resume (loop selection/project-end,
  sub-tab loop, reschedule items, seek, solo toggle, piano-roll seek, export
  rewind, unmute, playhead, tab-switch resume, ghost play) set flag truoc
  stopAllPlayback -> giu session FX (fxRtStart early-return) -> khong reload
  VST ~4s moi pass loop.
- User stop thật (fade=true), bridge-down, new project, recovery rebuild,
  export end, offline export: khong set flag -> teardown nhu cu.
- Rebuild app.precompiled.js tu source (babel) + giu lai IIFE overlay telemetry.
- Bump cache-buster 202608242200 -> 202608242300 (5 ban).
2026-08-24 15:54:13 +07:00
admin 348079ceae fix(fxrt): doc stats tu fxRt const global, khong phai window.fxRt 2026-08-24 15:40:49 +07:00
admin 69bb449958 feat(fxrt): hien thi telemetry stats tren UI (vi WebView2 khong mo devtools) 2026-08-24 15:35:16 +07:00
admin 559d52e3c5 fix(fxrt): bo pace setTimeout input, gui ngay theo dong ho audio worklet (het crackle sau 3-4s khi chain rong) 2026-08-24 15:28:41 +07:00
admin 5c7fc2c779 chore(probe): dong bo FILL/CAP 48/64 voi worklet 2026-08-24 15:20:26 +07:00
admin c10d2c2f14 chore(engine): gui sess.stats kem latency frame (chan doan drop/publish session that) 2026-08-24 15:20:26 +07:00
admin 272382f73c fix(fxrt): tang cushion worklet 128->256ms + telemetry underrun (giam crackle do main-thread stall) 2026-08-24 15:20:26 +07:00
admin 36aa15e2eb chore(engine): FXRT_BRIDGE_LOG diagnostic stderr bridge vao file 2026-08-24 12:25:14 +07:00
admin e12a8d754a fix(fxrt): chan out ring overflow trong bridge (backpressure, het pointer collision) 2026-08-24 12:25:14 +07:00
admin 09d8a386fb fix(fxrt): resume sau long stall phai fade-in (het click), khong hold khi da im lang 2026-08-24 12:25:13 +07:00
admin 86d5bbc372 chore(probe): them param_burst kich thich set_param qua ctrl ring (FXRT_PROBE_PARAMS, FXRT_PROBE_PARAM_IDS) 2026-08-24 12:03:08 +07:00
admin 4240f15e30 fix(engine): defer set_param ra worker thread, VST API khong goi tren audio thread 2026-08-24 12:03:05 +07:00
admin b907db3251 chore(probe): theo doi clipping output (FXRT_PROBE_AMP + flat-top stats) 2026-08-24 11:39:49 +07:00
admin 5ca2027966 fix(fxrt): underrun xu ly o worklet (hold+fade), bo fade engine, fix wet bus
- sf-fx-realtime.js: underrun -> hold block cuoi (HOLD) roi fade-out 4 block ~21ms
  gain 1->0 truoc khi im lang (het click cat cung khi main-thread stall chan ca
  2 huong - engine khong the gui fade toi kip); resume giua fade -> fade-in nhanh
- plugins.py: bo fade-out/fade-in engine (den tre vo dung + fade-in sau stall tao
  dip dinh ky = vibrato/wow&flutter), giu to_thread(time.sleep) poll 2ms
- app.jsx/precompiled/index.html: fix master wet bus (no dry lai) + latency const
  FX_RT_ROUNDTRIP_SAMPLES + bump worklet cache-buster v=202608241800
- fxrt_ws_probe.py: consumer stall (mo hinh dung app that), WorkletSim holdfade mode

Probe consumer-stall (app that): light 5/5 hard click 0.245-0.285 -> hard=0;
heavy: recv=sent (het fade thua), hard=0
2026-08-24 11:19:56 +07:00
admin 3c9f1c5809 chore(probe): them fxrt probe diagnostics
- fxrt_probe.py: in-process bridge loopMax/jitter do
- fxrt_ws_probe.py: pipeline that (uvicorn :8011 + WS + worklet sim),
  dem lost/silence/peak@silence, stalls heavy|light|none
2026-08-24 10:40:24 +07:00
admin 21130858d6 fix(engine): giam jitter bridge timer va fade-out khi transport stall
- RealtimeFxLoop: timeBeginPeriod(1) + sleep_until thay Sleep(1) -> loopMax 32.9->24.8ms
- plugins.py _pump_output: stall-fade 8 block gain 1->0 khi gap > 5.5*block_sec (~29ms),
  burst send, fade-in 0->1 khi resume; poll dung asyncio.to_thread(time.sleep) vi asyncio.sleep
  bi cap 15.6ms tren Win11 (IOCP) du timeBeginPeriod
- fx_realtime.py: stats dem publish/drop/stale/read cho diagnostics
- rebuild bridge binary
2026-08-24 10:40:21 +07:00
admin 708e7de32b fix(audio): offline mix limiter -3dBFS, 2-stage normalize (-14 LUFS + hard peak -1 dBTP), OGG export 2026-08-24 09:10:00 +07:00
admin 10997a65b9 fix(fx-rt): het crackle master FX - limiter attack smooth + crossfade khi bat/tat chain
- NativeInstrumentEngine: kAttack 0.9->0.1 (het zipper khi sum nhieu track)
- app.jsx: crossfade masterFxDryGain/masterFxGain thay hard-rewire graph luc
  activation/deactivation master chain FX; worklet bao 'started' -> ramp dung
  luc; PDC tre sau khi dry silent (tranh gap 80ms)
- sf-fx-realtime.js: fade-in 512 mau luc FILL xong / resume sau underrun
- index.html + addModule: cache-buster v=202608241200
- rebuild app.precompiled.js, copy bundle vao dist/ + src-tauri/resources/
- keo dai fix cac round truoc: FILL 16, FXRT_BURST_MS, REBUFFER, pacing,
  VST GUI click/focus, solo crackle
- them harness + plans/patch scripts
2026-08-24 07:46:09 +07:00
admin 373a96cf7b fix(fx-gui): bring plugin window to top before real click (anchored mode)
Real-input clicks were landing in whatever window sat above the anchored
plugin window (foreground Chrome in tests) — WindowFromPoint at the click
point returned the browser, so the editor never saw the input. Attach to
the foreground thread, SetForegroundWindow + BringWindowToTop before the
click (still non-TOPMOST — no floating regression from 80c663e).
2026-08-23 18:09:52 +07:00
admin 80c663e419 fix(fx-gui): re-enable real-input fallback (anchored, non-topmost) 2026-08-23 16:47:38 +07:00
admin 2ad833fbe7 fix(masterfx): merge captured preset vao realtime chain 2026-08-23 16:47:35 +07:00
admin 6176cf3f8e fix(auth): standalone auto-login qua IP LAN cua chinh may — _is_local_client (own interface IPs) 2026-08-23 15:40:49 +07:00
admin 6edfe1db32 fix(fx-rt): het crackle master FX - engine gui block ASAP + worklet FILL buffer 8
_pump_output bo pace realtime (truoc: moi block send cach nhau block_sec lam
client queue ~1 -> jitter bridge/WS/GC -> queue can -> zero-fill crackle).
Gio gui het burst ASAP; worklet sf-fx-realtime them FILL=8: chi phat khi
outQueue >= 8 block, underrun -> re-buffer (im lang cho du FILL) thay vi
zero-fill lien tuc. Cap 32 giu nguyen. PDC: FX_RT_ROUNDTRIP_SAMPLES 1792->3840
(RTT ~7 block + FILL 8 block = 15 block @48k) de dry-bypass path khong lech
pha voi mastered. Cache-buster v=202608212401 (precompiled + worklet).
Rebuild PyInstaller + copy 3 noi deploy. Verify: probe crackle3 25s Ozone
rebuffer_events=0 (zero_fill 12 block = startup fill), picker UI PASS (59 VST).
2026-08-23 12:50:11 +07:00
admin 9bb9a234b0 feat(rt+auth): batched input publish boi so 4 (het crackle) + auto-login loopback khong can mat khau
- fx_realtime: write_input gom FXRT_IN_SLOTS(4) frame vao _in_pending, flush
  ghi 1 lan + h.in_write += n (publish atomic -> bridge take=4 luon, het deficit)
- plugins pump: flush_input_stale(0.004) dau vong lap -> input le khong ket
- auth: _is_loopback(127.*/::1/localhost) -> get_current_user khong token = admin
  tu DB (must_change_password=False); POST /auth/local auto-login loopback, LAN 403
- ws fx-realtime: loopback khong token cho phep, remote thieu token -> close 4401
- UI: checkAuthStatus khong savedToken -> thu localLogin, thanh cong vao DAW,
  that bai -> modal login nhu cu; cache-buster v=202608212400
2026-08-23 12:00:05 +07:00
admin 0acc3ee7ee fix(fx-rt): burst 4-block input batch qua bridge — hết lag/giật/crackle master FX
- worklet gom 1024 mẫu (4 block) vào 1 frame interleaved, main thread tách 4 WS frame 512 floats
- bridge take=min(avail,4) xử lí 1 call process cho burst → overhead VST3 amortize 4 lần
- engine _pump_output pace theo deadline thay vì sleep cố định (outRead 131→187.5/s)
- FX_RT_ROUNDTRIP_SAMPLES 512→1792 (RTT batching ~7 block)
2026-08-23 11:44:54 +07:00
admin bec82a20bc Cache-buster app.precompiled.js -> v=202608212359 (fix deserialize vstBypass) 2026-08-23 10:44:33 +07:00
admin a105bdb959 Fix: master FX chain khong xu ly sau khi load project — deserialize composite master.bypass thanh vstBypass=true loai het VST3 khoi fxRtChain (chain=[] → fxRtStart early-return → masterBus pass-through, offline export cung tat). Deserializer gio hardcode vstBypass=false (khong co UI nao set flag nay cho master; chi ton tai tu bug deserialize). Build lai app.precompiled.js + copy 3 ban deploy + cache-buster moi. Verify UI: LOADED vstBypass=false, chain VST3 Ozone giu, fxRtChainLen=1; play → fxRt.active=true ws OPEN inQueue=64; DB save moi master.bypass=false. 2026-08-23 10:44:28 +07:00
admin 1d4ff1282b Gap 13: BWF/INFO metadata cho WAV export — wav_bwf.patch_bwf_metadata chen bext + LIST/INFO (INAM/IART/ISRC/ICMT) truoc data chunk, giu nguyen PCM bits; render_project(metadata=) + RenderRequest.metadata + cloud render task; ExportModal Title/Artist/ISRC; test_66 (178 passed) 2026-08-23 09:11:18 +07:00
admin 94a0bc7401 Gap 7: master volume automation lane — render_engine automation.volume_db (dB interp -30..+3 sau fader truoc normalize), schema master.automation, UI AUTOM panel MasterStripConsole + realtime scheduling tren masterBus.output.gain (play/seek/stop), serialize/parse, test_65 (177 passed) 2026-08-23 09:05:39 +07:00
admin 2b5f396133 Gap 12: soundfont render qua native bridge — render_soundfont_offline (FluidSynth C API) thay pyfluidsynth khi thiếu lib; render_engine soundfont branch fallback bridge (test_64, 176 passed) 2026-08-23 08:52:57 +07:00
admin 6b2c9a039b Gap 8: PDC realtime — WS latency push + dry-path delay compensation
Backend: /ws/fx-realtime pump drain get_latencies (SHM ring, bridge da bao REPORT_LATENCY) -> push {cmd:'latency',total,latencies} khi tong doi. Frontend: pdcDelay (DelayNode) giua dryOutput->output bu delay master chain (round-trip 2 block + latency VST3), fxRtApplyPdc cap nhat tu WS frame, reset khi chain off. Test test_63 fake session 1024 -> frame total 1024; full 175 passed.
2026-08-23 08:44:54 +07:00
admin db12698ba2 Gap 11: A/B reference track — /reference-analyze LUFS/dBTP/centroid + RefCompareModal A/B listen (5 test, 174 passed) 2026-08-23 08:34:09 +07:00
admin 3ad379f0ba Gap 10: multiband compressor + de-esser (LR4 crossover, UI, test_62) 2026-08-23 08:25:07 +07:00
admin c8c49b6b9c Gap 4: MaximizerFx oversample 4x soft-clip + ceiling — zero-stuff + FIR33 anti-alias + post-decimation clamp (giảm aliasing fold-back, sample peak <= ceiling); test_61: impulse delay 8 mẫu, unity transparent, alias 9.1k -117dBFS, ceiling -1dB (24 passed) 2026-08-23 07:59:33 +07:00
admin 5d37c0683b D3+D4: export stage PCM16/24 + TPDF dither + noise shaping, resample 44.1/48/96k, normalize -1dBTP/-14LUFS, mono side/mid check; Export modal mastering options + /plugins/render options (test 9 case, 23 passed) 2026-08-23 07:47:27 +07:00
admin 35076f680b C: realtime VST3 setParam live — Vst3Fx::setParam (title/shortTitle->ParamID, setParamNormalized + inputParameterChanges queue), RealtimeFxChain dispatch vst3, test_60 delay 1s->0.25s giua stream khong restart 2026-08-23 07:36:43 +07:00
admin 17e22bcf32 D2: LimiterFx brickwall — lookahead 1-5ms + oversample 4x + hard clip, UI toggle SOFT/BRICKWALL + LOOKAHEAD ms 2026-08-23 07:23:52 +07:00
admin fe82b19557 D1: LUFS EBU R128 + true peak sau render (app/core/loudness.py), API /plugins/render tra them lufs/true_peak_db, test 4 cases 2026-08-22 21:50:29 +07:00
admin 2a5e2af04a master strip: L/R volume + LINK + MUTE that, bo soloBtn 2026-08-22 21:41:54 +07:00
admin 4ae2f06c57 Review DAW-standard R1-R8: cài đặt thực tế qua render_project + API /render — track chain order (vst3 giữa), gain/pan sau chain, master chain trên mix, gain staging, mono/stereo, smoke export 2026-08-22 21:13:11 +07:00
admin 11e564abc5 Phase 5.8: tick hoàn tất checklist DAW 2026-08-22 21:03:00 +07:00
admin c3aba40ffe Phase 4+5: WebAudio I/O shim + verify DAW checklist — live=export, order, fader, PDC, smoke 2026-08-22 21:02:52 +07:00
admin e5c957fc87 Phase 3: engine render chain hop nhat — track/master doc mastering_settings.chain (imager/maximizer), PDC probe FX_LATENCIES, fix daw_vst_bridge link BuiltinFxChain, pytest 3.2/3.3 2026-08-22 20:17:59 +07:00
admin 1390ad545b Phase 2: BuiltinFxChain C++ (8 DSP) + control ring SET_PARAM/REPORT_LATENCY
- native_bridge/src/BuiltinFxChain.{h,cpp}: 8 builtin DSP port 1:1 từ
  Python _apply_builtin_fx_chain (eq/eqpro/compressor/limiter/exciter/rebalance)
  và JS MASTER_MODULE_IO (imager/maximizer). Biquad RBJ DF2T, block-wise
  stateful. createBuiltinFx trả nullptr cho gain/normalize (legacy).
- RenderFxJob: FxSlot thêm bfx, parse builtin 8 loại, runBuiltinSlotSafe SEH,
  realtimeRunChain nhánh builtin, chainGen()/entryLatencies(), setParam
  áp live lên bfx; Vst3Fx latencySamples từ getLatencySamples().
- RealtimeFxLoop: drain ctrl ring (SET_PARAM -> chain.setParam, drop cũ khi
  đầy) + REPORT_LATENCY khi gen đổi (ipc->lat[] ring, lastGen).
- FxRealtimeIPC.h: header 18 u32 (72B), ctrl/lat slots 8, FxCtrlCmd 24B,
  FxLatReport 8B, static_asserts.
- app/core/fx_realtime.py: mirror header 72B (ctrl_write/read, lat_write/read,
  slots), set_param ghi ctrl ring qua _ctrl_enqueue_locked (guard shm),
  get_latencies drain lat ring qua _lat_drain.
- tests/test_builtin_fx_golden.py: golden SNR C++ vs Python (8 cases, ngưỡng
  30dB) + order test eq/compressor xen kẽ.
- TASKS_DAW_A.md: tick 2.1-2.9.

Test: 136 passed (4 fx_realtime_chain + 8 builtin golden + regression).
2026-08-22 19:55:08 +07:00
admin fd790c1dc6 feat(fx): Phase 1 DAW chain hop nhat — fx_realtime chain JSON giu thu tu, SET_PARAM/REPORT_LATENCY qua WS text, native_render chain builtin+vst3 xen ke, pytest 2026-08-22 19:26:43 +07:00
admin 5ad2c681e7 Remove Carla native VSTi bridge entirely
Drop Carla integration across frontend and backend:
- plugins.py: remove /carla-midi, /carla-status, /carla-stop, /open-in-carla,
  /carla-play-notes endpoints and Carla process management
- runtime.py: remove find_carla / _carla_* helpers, detect and capabilities
- app.jsx: remove Carla bridge UI, SonicCarlaMidi routing, shouldRouteCarla
  guards, VST row Carla button and preset fallback
- system.py: remove /carla-path
- Keep audio path unchanged (tracks -> masterBus -> output -> analyser),
  directOut only for offline render

Build frontend, tests pass (124 passed).
2026-08-22 18:39:49 +07:00
admin 14265515aa fix(exit): taskkill rieng tung process — kill het plugin_host/fx_vst_bridge khi thoat app (truoc day /IM gop chi chay lenh dau); CREATE_NO_WINDOW cho moi subprocess engine (het terminal chớp chớp) 2026-08-22 18:06:26 +07:00
admin f143646ba7 fix(fx-gui): /show resize window+plugin view theo rect panel (layout khớp ô embed) — giữ offscreen, không topmost; sync sidecar mới vào bundle _internal 2026-08-22 17:52:16 +07:00
admin 4bb0db2b21 fix(fx-gui): meter VST chạy đúng — bỏ điều kiện inRead chặn peek + chống race lúc load + ẩn cửa sổ native floating 2026-08-22 17:36:31 +07:00
admin 8346bc3041 build(native): rebuild bridge + commit sidecar binary để feature theo push
- fix compile: performEdit dùng setParamNormalized (setParamFromEdit không tồn tại trong SDK này)
- rebuild fx_vst_bridge.exe/daw_vst_bridge.exe/plugin_host.exe (chứa /shm route + SHM audio feeder)
- src-tauri/binaries: hết ignore, commit binary + dll + vst3scan.json — máy khác clone về có ngay feature native
- xóa rác app.precompiled.js.bak + dọn bundle dirs (.bak/.new/_diag/.pytest_cache)
2026-08-22 17:12:18 +07:00
admin 6dfe390bda feat(fx-gui): đưa audio vào process GUI — meter VST nhảy theo âm thật qua SHM peek
- GUI process peek ring SHM đầu vào (không consume, không tranh chấp RealtimeFxLoop)
- gọi fx->processAudio() trên instance GUI để meter/UI plugin chuyển động theo âm thật
- performEdit forward sang controller -> meter điều khiển bởi param cũng cập nhật
- bridge /shm: frontend đẩy tên session live vào mỗi GUI đang mở (start/stop/open)
- tests: 127 passed
2026-08-22 17:06:37 +07:00
admin 6ba38286ad fix(master-fx): loại double-processing khi load VST FX vào master fx chain
- Bỏ push setFxChain xuống native bridge (T2.4) — SHM fxRt loop (fx_vst_bridge
  --realtime-fx) là nơi xử lý live master mix duy nhất; trước đây MIDI/VSTi
  instrument bị xử lý 2 lần (native bridge + SHM loop) → over-process,
  âm bể/crackle. setFxChain vẫn dùng khi Export/Offline render.
- closeMasterGui/closeFxGui: POST /close (kill process) thay vì /hide —
  không còn cửa sổ floating always-on-top lơ lửng, tắt được dứt điểm.
- openFxGui already_running kèm embed_url → mở lại embed panel thay vì chỉ toast.
- open_fx_gui (plugins.py): spawn fx-gui với SF_PARENT_PID=<engine pid>.
- RenderFxJob.cpp: watchdog thread trong run_open_fx_gui/run_fx_gui_server —
  engine chết → WM_CLOSE, GUI process tự thoát, không để lại cửa sổ mồ côi.
2026-08-22 16:53:20 +07:00
admin e22757d5be fix(mastering): master dry mac dinh — built-in modules tat khi khoi tao
- App default + 2 fallback (ProfileModal, load project): eqActive/
  imagerActive/maximizerActive = false (truoc day chain rong nhung cac
  flag van true -> Imager+Maximizer van xu ly am mac du MASTERING PANEL
  trong)
- applyMasteringSettings doc flag truc tiep, khong check chain — user
  phai them module trong MASTERING PANEL moi bat built-in processing
- bump cache-bust app.precompiled.js -> 202608212330
2026-08-22 16:27:57 +07:00
admin edc647ded6 fix(rt): restart fxRt khi chain VST doi trong luc realtime dang chay
- masteringSettings effect goi fxRtStart() khong dieu kien: early-return
  khi active + cung chainKey, stop+start lai khi chain doi (insert/remove/
  bypass/reorder VST trong MASTER FX CHAIN) — truoc day nhanh chainKey
  chet vi branch chi chay khi !fxRt.active
- test list_scan_all: path plugin phai ton tai tren dia (loc entry chet
  truoc cache); dung _scan_json helper voi file that trong tmp_path
- bump cache-bust app.precompiled.js -> 202608212300
2026-08-22 16:13:10 +07:00
3dtours eef8603f98 build: rebuild app.precompiled.js (mastering empty-chain fix) 2026-08-22 16:06:19 +07:00
3dtours d74ccdaa8d fix(mastering): empty chain by default, allow removing all modules
MASTERING PANEL auto-seeded 3 modules (Dynamic EQ/Imager/Maximizer) and
removeChainModule re-added them when the chain hit zero. Chain now starts
empty everywhere (init, restore fallback, migration, graph fallback) and
can be removed down to empty; activeModule guards empty chain (null).

Cache-bust precompiled.js ?v= bump.
2026-08-22 15:58:39 +07:00
3dtours 9c6e200708 fix(plugin-dirs): remove het dir - rescan khong con VST trong thu muc mac dinh (authoritative user dirs, khong fallback default khi da luu rong; list_scan_all/_scan_plugins chi scan dir user khai) 2026-08-22 15:29:31 +07:00
3dtours 681cab4f25 fix(plugin-manager): xoa/add dir luu dung dir moi (stale closure); loc plugin file da mat khoi disk; scan VST3 giam timeout module con lai 4s va subprocess 300s 2026-08-22 15:10:35 +07:00
3dtours c07b792bd4 debug(renderfx): log tung buoc load VST3 de xac dinh diem treo (Ozone Pro) 2026-08-22 11:54:48 +07:00
3dtours 068de2cb23 docs(wiki): ghi nhan DEPLOY tu dong Windows + ket qua verify cuoi (44 FX, 5 instrument) 2026-08-22 04:13:12 +00:00
3dtours 384827d1e7 fix(available): iZotope khong san xuat VST instrument — loai moi module iZ* trong path chua 'izotope' khoi nut Synth (iZOzonePro, iZRXPro*, iZTonalBalanceControlPro) 2026-08-22 04:10:43 +00:00
3dtours aeec85604f fix(available): loai iZotope VST2 legacy (Steinberg\VSTPlugins\iZotope\*.dll) trung ten VST3 FX da scan — block theo ten normalized (bo prefix iZ, lowercase alnum); giu VST2 that (DSK/Kong) 2026-08-22 04:07:19 +00:00
3dtours aaf97b1255 fix(available): bo qua .dll trong thu muc da co .vst3 (iZotope installs .vst3 + module .dll cung thu muc) — FX khong con lot vao nut Synth (3/3) 2026-08-22 03:57:14 +00:00
3dtours a09a30c79d fix(available): _scan_plugins bo qua file .dll ben trong folder .vst3 — FX Ozone/Neutron khong con lot vao nut Synth (2/2, dong bo _scan_vst_in_dirs) 2026-08-22 03:49:03 +00:00
3dtours ece5f313b3 fix(available): _scan_vst_in_dirs bo qua file .dll ben trong folder .vst3 (bundle) — FX Ozone/Neutron khong con lot vao nut Synth (path trung khong khop block set) 2026-08-22 03:46:13 +00:00
3dtours 442f1ff1f5 chore(deploy): them deploy_engine_windows.ps1 — rebuild + deploy daw_engine.exe 1 lenh (pull, build.mjs, pyinstaller, detect runDir, copy bundle, restart) 2026-08-22 03:35:23 +00:00
3dtours 34d43f5018 fix(fx-list): MASTERING PANEL khong hien thi VST FX da scan trong Plugin Manager (cache _fx_scan_all ket [] vinh vien)
- vst_engine: list_scan_all cache co TTL 300s (FX_SCAN_TTL) + chi cache khi
  find_fx_bridge_exe() co exe (thieu bridge -> tra [] NHUNG khong cache de
  lan sau thu lai) — truoc day ket [] vi mui gio, MASTERING PANEL rong.
- vst_engine: them invalidate_fx_scan() clear cache scan VST.
- plugins.py: POST /scan (Plugin Manager Scan) goi _invalidate_fx_scan_cache()
  -> MASTERING PANEL lan mo ke tiep quet lai voi dirs moi; response them
  fx_cache_invalidated.
- app.jsx: MasteringModal/FXRackModal load listFx() lai moi lan mo modal
  (deps [isOpen]/[track.id]) + loading state; VstPluginPicker nhan prop
  loading -> hien 'Dang quet VST FX...' thay vi 'Khong co VST FX'.
- precompiled rebuild (LF) + index.html ?v=202608220330; dist sync.
- tests: +4 test cache TTL/invalidate/no-bridge (TestFxScanCache).
2026-08-22 03:34:33 +00:00
3dtours ac5a171464 fix(build): setPluginSearchPath khong dung std::filesystem (block-scope alias gay C2371/C2088 tren MSVC) — dung GetFileAttributesA + find_last_of 2026-08-22 02:49:58 +00:00
3dtours 2a4ae86f5a fix(fx-bridge): SetDllDirectoryW truoc Module::create — plugin VST3 can DLL dep canh no bi LOI 0x7E (ERROR_MOD_NOT_FOUND)
- SDK module_win32 LoadLibraryW khong search thu muc plugin cho dependency DLLs
- setPluginSearchPath: bundle folder -> Contents/x86_64-win; file don -> parent dir
- ap dung ca render (vst3FxLoadInner) lan scan (scanModuleInner)
- test: ~30 plugin iZotope/RX load OK; Melodyne COM 0x80010106 la plugin dac thu (STA+license)
2026-08-22 02:45:45 +00:00
3dtours eecc4501bc perf(scan): timeout/module 30s->12s (Ozone treo vinh vien), subprocess 600->180s 2026-08-22 02:05:49 +00:00
3dtours 67245a6f61 fix(fx-list): UNKNOWN entries (Ozone treo scan) van vao list_fx de MASTERING PANEL khong mat plugin; nut Synth loai qua is_unknown 2026-08-22 02:00:48 +00:00
3dtours dd2a320b2a fix(scan): fx_vst_bridge --scan timeout per-module 30s + emit UNKNOWN thay vi is_instrument=true
- RenderFxJob: safeScanModule chay tren worker thread + TerminateThread neu
  Module::create treo >30s (Ozone 11 Clarity treo vinh vien, SEH khong bat
  hang) -> scan khong bao gio ket thuc -> list_fx() timeout -> fx_paths rong
  -> filter /available vo hieu -> nut Synth hien ca VST FX
- Module crash/hang -> emit is_fx=false,is_instrument=false (UNKNOWN) thay vi
  is_instrument=true (truoc day xep VST FX crash vao danh sach instrument)
- vst_engine: list_scan_all() tra toan bo (FX+instrument+unknown); list_fx()
  loc is_fx tu cache chung
- plugins.py /available: loai ca fx_paths lan unknown_paths khoi
  vst_instruments -> nut Synth chi hien VSTi that
2026-08-22 01:53:21 +00:00
3dtours 1a8b210063 feat(fx-rt): VST master chain realtime khi insert/bypass/reorder + VU meter đo sau VST node
- fxRtStart: so chainKey signature — insert/remove/bypass/reorder VST trong
  MASTER FX CHAIN giờ restart session SHM với chuỗi FX mới (trước đây
  early-return khi active → VST mới không bao giờ được áp dụng realtime)
- masterBus.splitter (leftAnalyser/rightAnalyser) chuyển sang sau fxRt.node
  khi realtime active → VU meter master strip + peak graph hiển thị audio
  data ĐÃ QUA VST; khôi phục output khi stop
- MasteringModal outPeak/outRms/LUFS đọc masterBus.analyser (post-VST) thay
  vì outputAnalyser (pre-VST)
2026-08-21 15:39:23 +00:00
admin bb6658e03d fix(fx): realtime VST ngắt quãng khi mở MASTERING PANEL + PWR master không ảnh hưởng âm; pacing WS input/output, gate fxRt theo masterConnected, worklet cap 32 2026-08-21 21:48:56 +07:00
admin 4e3f2fd08a Fix 3 bug standalone: save project lỗi schema, export MIDI/WAV không hỏi nơi lưu
- Bug 1: serializeTracksList bỏ section item không có sectionId (do insertSectionAtPlayhead tạo) — tránh source_data {} làm schema oneOf fail
- Bug 2: export MIDI qua save-dialog + write file, toast kèm đường dẫn
- Bug 3: export mix WAV/MP3/OGG qua save-dialog + write file, toast kèm đường dẫn
- Marker tauri_bridge_ready: xóa khi window destroy + guard SF_PARENT_PID — tránh standalone treo 120s
2026-08-21 17:13:28 +07:00
admin 9b14c99a7a Fix 3 bug standalone: VKB chiếm phím V, TCP FX button mở FX chain, VST GUI embed qua bridge
- VKB: stopImmediatePropagation thay stopPropagation (keydown+keyup) — chặn
  transport hotkey 'v' mở prompt Gain khi VKB đang mở (VKB đăng ký trước).
- TCP FX button: onClick -> window.__openFxRack(track.id, track.name), title
  'Mở Track FX Chain', giữ caret ▾ (title 'FX nhanh (cũ)') cho dropdown legacy.
- VST FX GUI embed: copy fx_vst_bridge.exe vào bundle (engine.spec binaries),
  find_bridge_exe tìm được trong _internal/ — hết lỗi 409 'Không tìm thấy bridge'.

Verified bằng Playwright: VKB không mở Gain prompt, FX chain rack mở + GUI VST
embed hiển thị (cả track FX rack lẫn Master FX Chain).
2026-08-21 16:17:33 +07:00
admin bcd8e37e86 feat(build): Phase 7 — verify 10 mục + fix export MIDI thiếu delta-time + fix WAV float32 2026-08-21 15:29:47 +07:00
admin a667d0354f feat(chords): thêm Chords Panel — chordTheory service + presets/custom/AI tab, insert progression vào timeline, Shift+K/menu/toolbar 2026-08-21 12:35:20 +07:00
admin ec85e7d1a9 Phase 5: Virtual MIDI Keyboard (F2/menu Tools) — QWERTY mapping spec §3, note routing theo attachMidiHandler (bridge/native SF-VST/SonicSF/Carla), live record forward, drag+persist localStorage, verify Playwright + 3 regression PASS 2026-08-21 12:16:03 +07:00
admin 5fc45a59f8 Phase 3: gộp builtin DSP + VST FX thành 1 FX CHAIN (track + master), đổi tên panel/nút/menu, fix lookup track cho nút FX TCP 2026-08-21 11:36:48 +07:00
admin 2c93c701ce feat(export): Export Mix loop không fade (checkbox + cắt buffer client) + server download thay blob (WebView2 chặn) 2026-08-21 11:25:49 +07:00
admin 96da8b5ac0 fix(export): MIDI export qua server endpoint thay blob download (WebView2 chặn) + velocity 1..127, bỏ rest-track 2026-08-21 11:10:32 +07:00
admin d7756b08ab fix(bridge): đồng đều âm lượng VSTi jbridge, GUI native lên top, modal instrument lọc FX
- calibrate: probe sandbox (jbridge) theo real-time pacing — child render qua SHM
  consumer-sync, probe µs-speed chỉ đo được silence/attack → makeup=1.0 mọi channel.
  Thêm virtual probeNeedsRealtime() (SandboxVst2Host → true), calibrate() Sleep
  ~1 block/5.8ms để child kịp nhận MIDI + render steady-state → makeup đo peak thật
  (3 channel Qin ≈ 2.6, trước đây = 1.0 → âm lượng đồng đều giữa các track/VSTi).
- OPEN_GUI: SetWindowPos(HWND_TOPMOST) + SetForegroundWindow + BringWindowToTop
  → GUI VSTi hiển thị ở top window, tương tác ngay.
- /api/v1/plugins/available: filter vst_instruments + extra theo is_fx của
  fx_vst_bridge --scan (trước merge entry) — modal Select Instrument chỉ còn
  soundfont + VSTi, không hiện VST FX (90 → 33).
2026-08-21 10:18:25 +07:00
admin da7ae02509 fix: VST2 qua jbridge bị tạch tạch — pace child theo consumer-sync + bridge copy full block
Root cause: child plugin_host render ~50x real-time (9323 blocks/s), bridge
đọc slot đang bị child ghi đè (torn read) và mỗi segment của 1 DAW block
đọc từ nhiều child block khác nhau -> discontinuity -> tạch thay vì âm thật.

- SandboxHostIPC: thêm bridgeConsumed[2] cuối struct (append-only, offsets
  cũ giữ nguyên) — child đếm publish/slot, bridge đếm consume/slot.
- plugin_host_main.cpp: sau khi publish 1 block, chờ bridge consume xong
  block đó mới render block kế (wait-sau-publish, không phải wait-trước-
  render — wait-trước vẫn cho phép publish 2 block liên tiếp giữa 2 lần
  consume -> bridge đọc latest sẽ bỏ block). Child giờ publish đúng 1
  block/DAW block = real-time; bridge không bao giờ stall (main loop DAW
  render + pace mọi iteration) nên không deadlock; nếu bridge chết,
  parent_alive break wait.
- SandboxVst2Host: copy full 256 mẫu vào local buffer khi slot đổi, serve
  mọi segment của 1 DAW block từ 1 child block nhất quán.

Verify: child pace 371 blocks/2s (real-time 375, trước 18647); domFreq của
waveform = 257.8Hz đúng nốt C4 (note 60) — âm thật, không phải noise;
correlation giữa các block liên tiếp âm đều ~-0.4 (waveform tuần hoàn);
GUI open path không deadlock (pace vẫn real-time, jBridge dialog + Qin_VST_
Window hiện đủ); GUI đóng -> child idle delta 0; silence_all master peak
0.000016.
2026-08-21 09:43:11 +07:00
admin 0f6235c9b9 Fix Qin GUI nền trắng khi mở lại + MIDI keyboard preview trong GUI bị câm
- plugin_host_main.cpp WM_CLOSE: destroy host window (sau effEditClose
  jBridge dialog cũ vẫn sống trong host; mở lại trên cùng window tạo
  jBridge dialog thứ 2 chồng lên -> nền trắng, editor không load). Mở lại
  tạo window mới sạch, đúng 1 jBridge dialog + Qin_VST_Window.
- plugin_host_main.cpp: bỏ idle-skip khi editor đang attach — preview phím
  MIDI trong GUI là âm nội bộ plugin, không qua host MIDI; vòng loop idle
  không gọi processAudioBlock nên voice nội bộ không bao giờ render (câm).
  GUI mở -> render liên tục; GUI đóng vẫn idle như cũ.
- Verify: mở/đóng/mở lại GUI ch1 -> host mới, đúng 1 editor, âm ch1 peak
  0.13 (GUI mở) / 0.10 (sau đóng); bridgeWrite tăng liên tục khi GUI mở.
2026-08-21 09:29:38 +07:00
admin e7e974b3e0 Fix GUI Qin không hiện lại sau hide + âm thanh lỗi sau khi đóng cửa sổ
- plugin_host_main.cpp: chặn WM_CLOSE (đóng GUI nhưng giữ window sống, hide
  thay vì destroy), WM_DESTROY reset hwnd; dùng g_hostHwnd toàn cục thay
  hwnd local để control OPEN_GUI/CLOSE_GUI tái sử dụng đúng window.
- main.cpp: bỏ qua dedupe hasAttachedView cho instrument child-own
  (SandboxVst2Host) — bridge guiOpen_ stale sau khi child tự đóng nên
  OPEN_GUI lặp lại phải forward control 4; child tự chặn double-open.
- Verify: WM_CLOSE -> window ẩn còn sống; OPEN_GUI lần 2 -> window hiện lại,
  âm ch1 vẫn kêu (peak 0.15), không câm.
2026-08-21 09:21:56 +07:00
admin a7b01d3484 fix(bridge): 3 bug âm thanh Qin/Nexus standalone
1) wavesynth tự phát khi mở app: probe calibrate() render vào live mix
   (renderAll process cùng instance) -> thêm calibratingCh_[16], renderAll
   bỏ qua channel đang calibrate; plugin_host panic (sustain off + all
   notes off + 128 noteOff) ngay sau preset apply để startup im lặng.
2) bấm GUI mở 2 cửa sổ Qin: sandbox VST2 child tự sở hữu top-level window
   -> ownsGuiWindow() + main.cpp forward OPEN_GUI control, không tạo native
   parent / không registry double-open.
3) play phát nhạc cụ sai: restore clobber — seed preset đè patch thật trong
   state file (ch3 63ba3667 -> 7f07473c) do saveStateJob chạy lúc child còn
   seed. Fix: queuePreset(waitApply=true) chờ child apply xong (presetOut
   memcmp), consume-before-apply (zero presetInLen trước khi apply), deferred
   double-load re-check trong main.cpp skip assign trùng path, watchdog
   respawn re-queue lastPreset_.
2026-08-21 08:50:28 +07:00
admin b91e6d3875 bridge: fix Qin VST2 silent playback via per-child SHM sandbox
- out-of-process VST2 sandbox: each Qin track spawns its own plugin_host.exe child
  with unique SHM (SonicForge_PluginHost2_<bridgePid>_<ch>_<seq>), watchdog <=3 respawns
- preset round-trip bridge<->child over SHM (presetIn/presetOut 64KB), stub rejection MIN_PRESET=100
- seed qin_default.b64 default preset into every fresh child; saved patch wins on restore
- forward MIDI on channel 0 (Qin_RV only listens on ch0); one instrument per child
- controls 4/5/6 = OPEN_GUI/CLOSE_GUI/QUIT
- gitignore runtime carla_projects
2026-08-20 19:16:43 +07:00
admin d6649e4474 bridge: auto-create jBridge Qin settings (RUN_IN_EXISTING_AUXHOST 0)
Makes the unlimited-Qin fix durable: bridge writes
%APPDATA%\jBridge\SettingsFilesFor{64,32}BitPlugins\Qin_RV.jBridge
at startup when missing, so every Qin instance runs in its own
auxhost on any machine. Verified: 4 Qin ch0/1/2/9 live, 4 auxhost.exe,
0 ML_RELEASE_ERROR, GUI attachView ok=1 on all.
2026-08-20 11:14:39 +07:00
admin c78d9f2911 bridge: drop 2-instance Qin cap (jBridge per-plugin settings fix)
Qin bridges via jBridge shared auxhost; 3rd live instance flooded
ML_RELEASE_ERROR (engine per-auxhost capacity, not our code). Fix:
write %APPDATA%\jBridge\SettingsFilesFor64BitPlugins\Qin_RV.jBridge
with RUN_IN_EXISTING_AUXHOST 0 -> each instance gets own auxhost,
unlimited tracks. Remove LOAD REJECT/restore SKIP guards. Verified:
4 Qin ch0/1/2/9 live, 4 auxhost.exe, 0 ML_RELEASE_ERROR, GUI all attach.
2026-08-20 11:11:27 +07:00
admin 588c0da32d native_bridge: recalibrate after preset restore, reject 3rd live VST2 instance 2026-08-20 10:50:11 +07:00
admin 4932bb88db fix(bridge): restore VST2 preset chunk on respawn + offline render (Qin silent fix) 2026-08-20 10:18:18 +07:00
admin 7c22ae1f02 refactor(bridge): split fx bridge, stabilize VST3 host chain, drop old install engines
- remove diagnostic canaries from audio hot path (crackle fix) and
  stuck C5 note raw-MIDI test (Vst2Instrument::init noteOff)
- split native bridge into daw_vst_bridge (VSTi) + fx_vst_bridge (VST FX)
- VST3: processData prepare before setActive, zero input buses, accept
  both .vstpreset and bridge-state blob in RenderJob presets
- native_render/vst_engine: bridge-exe discovery + render_fx_chain + /fx-gui
- delete old install/ engine binaries (install via sidecar+resources now)
- add BUILD_LAYOUT.md, build_selfcheck, tools/ diagnostics
2026-08-20 09:42:42 +07:00
admin 5df96aef1e fix(bridge): deploy ABI-fixed VST2 binary to install + open GUI for VST2 tracks
Qin_RV_x64.dll (Kong Audio) crashed with SEH 0xc0000005 in Bridger64.dll
using the stale install binary (pre-ABI-fix). Copy native_bridge build
Release binary into install/ so the packaged app runs the fixed host.

Also open the native GUI after loading a VST2 instrument (previously
VST3-only), so Qin_RV's editor (Qin_VST_Window via auxhost.exe) shows
automatically on track load.
2026-08-19 22:36:28 +07:00
admin 819257a1b3 Fix bypass JSON bool + load SF2 tracks into bridge on restore
- RenderFxJob: parse bypass as JSON bool (memberBool) — bypass:true
  previously ignored (memberInt parses numbers only), plugin kept
  processing. Verified: bypass RMS == plain, VST3 loaded once.
- preloadTrackInstruments / bridge reconnect: load SF2 instruments
  into bridge too (loadAnyTrackToBridge) — opened projects with SF2
  tracks sent MIDI to empty bridge channels (C++ silent), never
  reaching the native mastering chain.
- rebuild app.precompiled.js from app.jsx (was stale: missing A14).
2026-08-19 21:55:48 +07:00
admin 51f12113fa plan: quyet dinh Scaler=VSTi (scan that xac nhan), ket luan Ozone -Inf giu huong (c) 2026-08-19 21:23:14 +07:00
admin b10b0e77f4 Phase 1-2 (PLAN_MASTERBUS_FX_RACK_VST): tests cho render_fx_chain + API fx-render; plan HOÀN THÀNH Phase 0-4
- test_native_render: render_fx_chain builtin gain e2e (bridge --render-fx)
- test_plugin_api: POST /api/v1/plugins/fx-render builtin gain
- PLAN_MASTERBUS_FX_RACK_VST.md: cap nhat trang thai cac phase (0-4 da implement tu cac commit truoc + phan nay)
2026-08-19 21:21:33 +07:00
admin 45a7bdebfa Phase 0 (PLAN_MASTERBUS_FX_RACK_VST): banner ra stderr de stdout sach cho --scan JSON / --fx-gui port
Test /c/tmp/test_fx_phase0.py: 7/7 pass — gain0 bit-exact, gain -6dB RMS/2,
normalize peak<=0.25, empty chain rc=1, Ozone 11 Maximizer VST3 render khac
input (88172 samples), --scan phan loai dung (Ozone is_fx, Scaler 2 instrument).
2026-08-19 21:16:01 +07:00
admin 6a3b430da0 A14: MIDI-out từ plugin (VST2/VST3) → SHM → Rust emit → DAW track
- Vst2Instrument: audioMaster callback thu VstEvents, push SHM midiOutQueue
- Vst3Instrument: IEventList output bus (kAudioMasterWantMidi) — check
- SharedMemoryIPC.h: midiOutQueue[64] + midiOutCount (offset 27556/28324, sizeof 28328)
- shm.rs: MidiEventIPC Serialize, drain_midi_out() emit 'bridge-midi-out'
- lib.rs: pump emit midi-out events
- nativeBridgeService.js: onMidiOut API
- app.jsx: route MIDI-out về selected track qua SonicMidiRouter (feedback guard)
- TASKS.md: A14 ✅
2026-08-19 21:12:07 +07:00
admin 6efef85e47 fix(bridge): VST2 host dung ABI SDK — crash outs>2, NaN input, editOpen(NULL), opcodes; docs A14 MIDI-out + phan tich Ozone -Inf
- aeffect.h: bo sung opcodes 4-22, 53, 55 + audioMaster 15/41-44 + ERect (dung chuan VST2.4 SDK, DUNE load OK khong crash)
- Vst2Instrument: cap du pointer arrays theo numInputs/numOutputs (fix AV 0xC0000005 Qin outs=32), input fill 0.0f (het NaN poison effect), bo probe effEditOpen(NULL) (crash WaveObserver), GetDirectory + SizeWindow hook, canDo sendVstEvents/sendVstTimeInfo
- TASKS.md: A14 MIDI-out tu plugin (Scaler2/Instacomposer/MelodicFlow xuat MIDI khong phai audio)
- PLAN_MASTERBUS_FX_RACK_VST.md: phan tich Ozone -Inf — chain day du, nguyen nhan = audio DAW khong chay qua bridge master ring
2026-08-19 20:57:16 +07:00
admin 927597a822 fix(tcp): prevent panel scroll when wheeling volume ruler
React onWheel is passive so e.preventDefault() was a no-op, letting the
TCP panel scroll and the ruler slip away from the cursor (no snap).
Add native non-passive capture listener that preventDefault()s wheel
events starting on [data-sf-wheel] ruler containers.
2026-08-19 15:05:37 +07:00
admin 2f5c70ac29 Fix master volume + VST3FX GUI state restore on save/reload
- serializeProjectToSchema now writes master volume_db and mastering gui_open_path/name
- deserialize restores masterVolume + masteringSettings.gui_open_path/name
- track vst_gui_open_path/name fallback to live state (not just _persistFxGui*)
- MasteringModal/FXRackModal reopen effects react to isOpen + restored gui path
- openMasterVstGui/openVstGui store gui name for reopen
- bridge: /preset capture endpoint + RenderFxJob.captureState for GUI knob state
2026-08-19 14:44:40 +07:00
admin 8874e42596 fix(tcp): move vol/pan ruler wheel+dblclick handler to whole row
Scroll wheel giảm/tăng volume chỉ hoạt động trên input 4px; chuyển
onWheel+onDoubleClick lên row div (label+dB span cũng ăn), giữ bubbling
từ input. Áp dụng cho main TCP và sub-tab, cả app.jsx và precompiled.
2026-08-19 12:29:11 +07:00
admin dbbcb85ffe Fix fx-gui z-order: anchored window HWND_TOPMOST so real clicks reach plugin over webview; /hide demotes 2026-08-19 11:39:11 +07:00
admin 2bbe80efa3 fix(bridge): set ringMask=7 on Windows — ring collapsed to 1 slot (slot = writeIndex & 0), dropped audio blocks on GUI jank 2026-08-19 11:20:39 +07:00
admin 77360578fd fix(gui): embed VST GUI show on mount, health-check timeout/retry, send preset in fx chain (issues 1/3/4) 2026-08-19 10:56:22 +07:00
admin 466b2022f2 fix(fx): apply preset_b64 in realtime FX chain (issue 4) 2026-08-19 10:56:21 +07:00
admin 1dfba14114 fix(gui): persist embedded VST GUI state across panel close/reopen
Close/reopen of Mastering panel or FX Rack unmounted the modal, losing
masterGuiOpen/fxGuiOpen state. The native bridge window stayed alive but
hidden, so reopening the panel showed no GUI and no /show was re-posted —
user could not interact with the VST GUI. Persist the embed state at module
scope so remount re-anchors and re-shows the window automatically.
2026-08-19 10:34:52 +07:00
admin e5ce9ca90a fix(vst-gui): borderless fx-gui window + anchored re-show (mouse stable)
- RenderFxJob: offscreen fx-gui window now WS_POPUP (borderless) so
  GetWindowRect == client rect; anchored placement over the embed panel is
  exact. WS_OVERLAPPEDWINDOW put the client ~31px lower/right -> cursor
  jumped on hover and clicks hit the webview (relay rejected).
- AdjustWindowRect sizes to the window's own style (no caption for POPUP).
- FxGuiServer: mousemove only SetCursorPos while dragging (buttons!=0);
  hover no longer teleports the cursor.
- FxGuiServer /show: skip SetWindowPos/resize when rect unchanged (frontend
  now re-POSTs /show every ~500ms to re-anchor on scroll/resize).
- app.jsx VstGuiEmbed: drop shownRef one-shot; re-POST /show each refresh.
- Rebuilt bridge (6 locations) + Tauri release (embedded sidecar) + v=202608190002.
2026-08-19 10:22:35 +07:00
admin da0f8fa40e feat(native): realtime master FX chain + fx-gui fast start, hide/show, non-topmost
- FxGuiServer: HWND_TOP (no longer always-on-top), /hide + /show endpoints,
  onStarted callback prints SF_FXGUI_PORT before plugin load (fast connect)
- RenderFxJob: RealtimeFxChain (mutex+worker, SEH-guarded, bypass slots,
  deferred Vst3Fx dtor); --fx-gui split into parse/create/load-attach stages
- main: FreeConsole unless SF_KEEP_CONSOLE; control type 6 SET_FX_CHAIN feeds
  g_fxChain on the audio loop; shutdown before CoUninitialize
- plugins.py: CREATE_NO_WINDOW on both Popen paths
- lib.rs: set_fx_chain command -> push_control(6)
- app.jsx/nativeBridgeService: setFxChain sync from MasteringModal; panel close
  uses /hide + refs to fix closure bug; VstGuiEmbed rect refresh -> /show
2026-08-19 09:59:13 +07:00
admin c6de0720f4 fix(bridge): anchored fx-gui embed - dance dat window dung panel rect, khong park/restore cursor, resize view qua WM_FXGUI_RESIZE 2026-08-19 09:25:43 +07:00
admin 346ada6e47 fix(bridge): real-input dance dat window tai cursor thay vi goc work-area - het GUI floating ngoai cua so DAW khi click vao embed (iZotope/JUCE reject PostMessage) 2026-08-19 08:25:12 +07:00
admin 0334c2a5a3 fix(backend): find_bridge_exe frozen onedir thieu exe_dir candidate - bridge sidecar nam canh app root ngoai _MEIPASS lam list_fx tra rong (Mastering/FX Rack khong load VST duoc); mirror logic runtime._vst_render_available 2026-08-19 08:15:59 +07:00
admin 0cf771242f VST FX integration: plugin picker theo category + GUI embed fill viewport (FX Rack & Mastering) 2026-08-19 07:55:57 +07:00
admin 19904bcad8 Merge branch 'standalone-shm-bridge' of https://gitea.labz.io.vn/3dtours/SonicForgeStudio into standalone-shm-bridge 2026-08-19 07:14:07 +07:00
admin d355016fe7 rebuild exe release: bridge_status ghi loaded_channels (V10) — Rust binary cu thieu field lam JS fallback phai doan; kem schema json regenerate 2026-08-19 07:06:37 +07:00
admin ec8b57ecb3 V12: fix VSTi GUI khong mo lan 2 khi bridge_status thieu loaded_channels
bridge_status (Rust binary cu, chua co V10 field loaded_channels) -> queryStatus
fallback khong co loaded_channels -> lc undefined -> silent return 6626 lam GUI
chet im lang o click 2. Bo silent return: mo GUI truc tiep tren bch, an toan vi
C++ handleOpenGui DEFER khi channel chua co instrument.
2026-08-18 22:58:14 +07:00
admin 0cde007d26 V11 fix GUI silent-return: only skip load when path resolved; refresh bridge state via queryStatus and open GUI on any restored channel 2026-08-18 21:51:02 +07:00
admin d3a7a0f793 FX GUI embed: WS_EX_TOOLWINDOW + no minimize box for offscreen window (no stray taskbar button that cannot be restored) 2026-08-18 21:35:48 +07:00
admin d07f626a6c Fix FX GUI panel frozen: prefer PrintWindow live frame + real-input fallback for GPU-rendered editors 2026-08-18 21:24:33 +07:00
3dtours 77bbbe298d build(win): rebuild sonicforge-daw.exe - playback fade/stop fixes (be8af7a) 2026-08-18 20:27:13 +07:00
admin 7bc96eebbc fix(playback): het note chong am echo o bass - KHONG huy note-off cu cung pitch (Bug 2 huy mui lam note cu khong bao gio tat); giu neu note-off cu bam truoc note-on moi, doi ve ngay truoc note-on neu overlap; sync 6 ban JS + index.html ?v=202608181900 2026-08-18 20:13:53 +07:00
admin be8af7a6ee fix(playback): het fade-out dai dang khi stop - restore gain LUON (ke ca stale/play moi trong cua so fade) qua pending fade session, huy fade ngay khi play moi (het dip 200ms); sync 6 ban JS + index.html ?v=202608181840 2026-08-18 19:53:59 +07:00
admin 4ddaf1f26a fix(playback): het retrigger 'play lai roi tat' khi stop - stop ngay clip source chua phat (playbackState=scheduled) truoc fade, source dang keu giu lai ramp 200ms; sync 5 ban JS + index.html ?v=202608181835 2026-08-18 18:45:32 +07:00
admin eb5a1483ca fix(playback): stop transport fade-out that - handleStop goi stopAllPlayback(true) (bo sot v14) + FADE_MS 80->200ms de fade nghe ro, het cat dung; sync 5 ban JS + index.html ?v=202608181827 2026-08-18 18:45:32 +07:00
admin 1e16189da8 fix(playback): stop fade-out 80ms khi nhan Stop/Pause - ramp gain track + bridge gain xuong 0 truoc hard-stop, guard play-gen tranh STOP tre giet play moi; sync 5 ban JS + index.html ?v=202608181818 2026-08-18 18:45:32 +07:00
admin 45a15da55e fix(midi): playback khong cam loop pass 2+ va note cung pitch khong bi cat (bugs 1-2) - set isPlayingRef truoc bridge PLAY (startTrackPlayback/local/piano-roll), cancel STOP#2 retry khi loop restart; dedupe NOTE_OFF theo channel:pitch (pendingNoteOffMap + scheduleNoteOff) de NOTE_OFF tre (dur+30ms) khong cat note-on cung pitch ke tiep (bar lien nhau, piano roll note chong), clear map khi stop; sync 5 ban JS + index.html ?v=202608181807, rebuild exe release 2026-08-18 18:45:31 +07:00
3dtours 6b6328044c docs(win): mark all 6 Windows lossless-audio tasks complete (192.168.1.24) 2026-08-18 17:53:23 +07:00
3dtours df0fcbed8a build(win): rebuild sonicforge-daw.exe (Tauri bundle) — engine with lossless float32 pipeline 2026-08-18 17:52:22 +07:00
3dtours 161a380a60 build(win): bundle jpeg62.dll with bridge (FxGuiServer JPEG encode needs it at runtime) 2026-08-18 17:45:47 +07:00
3dtours 398ff21f0b build(win): rebuild daw_vst_bridge.exe - IEEE Float32 WAV render + header fix (lossless gate 9/9, offline render 9/9) 2026-08-18 17:37:46 +07:00
3dtours 50cfed3340 test(gate): accept fact/other chunks — validate data chunk ends at EOF
sf.write() float32 WAVs carry a fact chunk; strict 44+data_size check was
too narrow. Walk chunks instead; bridge header bug (half data_size) still
caught because its data chunk must run to EOF.
2026-08-18 17:36:56 +07:00
3dtours ba9ceeea90 fix(bridge): WAV header data_size for IEEE float32 (was 16-bit byte count)
- finishWav call sites accumulated n*4 (stereo 16-bit) but float32 writer
  emits n*8 bytes -> RIFF/data chunk sizes half the real value; players and
  soundfile truncated renders to half length
- gate test now asserts file size == 44 + data_size to catch this
2026-08-18 17:35:54 +07:00
admin 351ee56080 fix(bridge): load dung VSTi theo track (V9) - bo early-return mui theo flag, load qua Tauri invoke khi co path, guard path sai (WaveObserver vs Nexus), chi mo GUI khi load OK; sync 6 ban JS + index.html ?v; kem cac fix bridge truoc (bugs 1-4, GUI restore) 2026-08-18 16:56:05 +07:00
admin cbcc1f658c merge: Normalize Linux audio pipeline to lossless Float32 (a2f9313) 2026-08-18 12:10:24 +07:00
3dtours a2f93138be Normalize Linux audio pipeline to lossless Float32 (skill: lossless-audio-compliance)
- native bridge WAV writers: 16-bit PCM truncation -> IEEE Float32 (fmt 3, bit-exact)
- vendor sheredom_json.h (vst3sdk 3.8.1 lacks moduleinfo/json.h)
- Dockerfile: drop nonexistent sfizz tag v1.2.3
- Python sf.write -> subtype=FLOAT everywhere
- resample_poly -> kaiser(16) window (>=130dB stopband)
- audio_editor._write_wav_lossless(): FLOAT passthrough, TPDF dither only for
  8/16/24-bit final exports
- app.jsx WAV encoders: Float32 default, TPDF dither for integer depths, fix 24-bit
- tests: lossless compliance gate (null test < -140dBFS, float32 format) 9/9 pass
- TASKS_WINDOWS_AUDIO_NORMALIZE.md: Windows-side remaining tasks
2026-08-18 11:50:26 +07:00
admin 308857c4a7 fix: VST GUI embed chup frame trang (Ozone 11 GPU) - luon chup ca BitBlt va PrintWindow PW_RENDERFULLCONTENT, chon frame chi tiet hon; fix loi JPEG chi ghi hang dau tien 2026-08-18 09:56:53 +07:00
admin 4eadf897a4 feat(mastering): remove redundant VST FX MASTERBUS row from panel
VSTs are now added via the chain '+' button (addVstToChain mirrors into
vstFxChain for offline render), so the old bottom row (slot list + '+
Thêm VST FX...' select + offline preview) duplicated the chain and is
removed.
2026-08-18 09:43:30 +07:00
admin c9b35d60ff fix(mastering): VST chain edits no longer reverted by cross-device temp sync
Guard the 3s applyRemote sync so it does not clobber masteringSettings
while the Mastering panel is open (user actively editing chain). Another
connected session's temp snapshot (written every 30s) was reverting
chain add/remove/toggle of VST modules within ~3s.
2026-08-18 09:36:24 +07:00
admin 569be72110 fix(native_bridge): strip query string in fx-gui HTTP path + walkthrough notes
- readRequest now trims '?t=<ms>' cache-buster so /frame?t=... matches
  /frame route (frontend VstGuiEmbed polls with timestamp → was 404, no image)
- document: jpeg62.dll manual copy, add auto-opens GUI (already_running
  expected), reopen needs bridge exit wait
2026-08-18 08:27:35 +07:00
3dtours c382afe2a6 feat: embed native VST GUI in mastering + fx rack panels (bridge --fx-gui frame capture + input relay)
Linux parts (0L1-4L1 per TASKS_LINUX_WINDOWS.md):
- native_bridge: FxGuiServer.cpp HTTP server (Winsock) — /frame BitBlt +
  PrintWindow fallback -> JPEG (libjpeg-turbo q70), /input PostMessage relay
  (mouse/wheel/key, coords scaled), /close, /ping; RenderFxJob fxGuiSetup +
  run_fx_gui_server (offscreen); main.cpp --fx-gui dispatch; vcpkg +
  CMakeLists WIN32-only + JPEG.
- api: open_fx_gui embed flag spawn --fx-gui, parse SF_FXGUI_PORT -> embed_url,
  fx-gui/close endpoint; 4 unit tests.
- ui: VstGuiEmbed component (frame refresh 100ms, ping 2s, input relay,
  scale coords) shared by MasteringModal + FXRackModal; swap graph<->GUI,
  cleanup on unmount, dead-bridge toast fallback.
- docs: TASKS_LINUX_WINDOWS.md, WALKTHROUGH_WINDOWS.md, PLAN status.
Windows tasks 0W1-4W1 remain (build + E2E real VST).
2026-08-17 22:37:55 +07:00
admin 38f4cbd8b5 feat: open native VST FX GUI from mastering panel + FX rack (bridge --open-fx-gui) 2026-08-17 21:52:40 +07:00
admin b261eb0d7e feat: VST FX rack per-track + masterbus render via native_bridge 2026-08-17 21:31:57 +07:00
admin 8fbacd1d2b fix(tcp): open VSTi GUI via bridge API on LAN browser; scale vol/pan ruler wheel by deltaY 2026-08-17 20:44:44 +07:00
admin bbde74f1a7 fix: normalize peak audio render offline chống clip polyphony; TCP wheel điều chỉnh volume/pan 2026-08-17 20:04:34 +07:00
admin dc331f39d8 phase5: mặc định RENDER_ENGINE=bridge, gỡ pedalboard khỏi requirements/source/LICENSE
- config.py: SF_RENDER_ENGINE default 'bridge' (pedalboard GPL-3.0 đã gỡ)
- requirements.txt: bỏ pedalboard==0.9.19
- vst_engine.py: bỏ HAS_PEDALBOARD/check_pedalboard_safe/load_vst/import pedalboard;
  midi_events_to_messages giữ làm utility thuần (không gate pedalboard)
- render_engine.py: bỏ nhánh elif vst and HAS_PEDALBOARD + import thừa
- plugins.py: preview/midi-render chỉ qua native_bridge; guard SF_RENDER_ENGINE=bridge
- runtime.py: _vst_render_available() check bridge exe (SF_BRIDGE_PATH/install/) thay find_spec('pedalboard')
- LICENSE.md/THIRD_PARTY_LICENSES.md: bỏ cảnh báo GPL-3.0 pedalboard, đánh số lại
- tests: bỏ test golden so pedalboard + test requires_pedalboard; 114 passed 0 skip
2026-08-17 12:58:51 +07:00
admin e79dae5d5c phase4: Linux native_bridge build (Docker) — sfizz from source, --render SF2/SFZ
- Dockerfile: build sfizz tu native_bridge/sfizz-src (hoac git clone v1.2.3),
  sed fix sfizz.pc (-llibsfizz -> -lsfizz), cmake bridge voi 3 flag SMTG OFF
  (VSTGUI/plugin/hosting examples) — bridge headless chi can --render.
- CMakeLists: nhanh non-WIN32 build daw_vst_bridge tu main_linux.cpp +
  RenderJob.cpp + NativeInstrumentEngine.cpp (SF2/SF3 + SFZ; VST3/VST2
  hosting Windows-only, guard #ifdef _WIN32). gui_probe/plugin_host/
  seh_channel_test chi build tren Windows.
- RenderJob.cpp: JSON parser (sheredom json.h) khong con phu thuoc
  HAVE_VST3SDK — render SF2/SFZ chay duoc khong can VST3 SDK.
- NativeInstrumentEngine.cpp: guard windows.h/VST3/VST2/Sandbox/SEH.
- main.cpp: g_engine unconditional + guard disable_ime_contexts (_WIN32).
- engine.spec: bo pedalboard collect_dynamic_libs + hidden imports.
- vst_engine/render_engine: DecentSampler -> NotImplementedError + huong
  dan convert .dspreset -> .vstpreset; pianobook fallback synth.
- Verified: WSL Ubuntu 24.04 build OK, bridge Linux --render SF2 ra WAV
  44100 stereo peak 1575; Python tests 115 passed 1 skipped.
- Dockerfile build chua test duoc (may khong co Docker).
2026-08-17 11:46:24 +07:00
admin efe26e3277 phase3: render_engine export qua native_bridge, FX scipy fallback, bỏ pedalboard branch
- VSTi track: SF_RENDER_ENGINE=bridge → native_render.render_offline (velocity
  float 0..1 từ midi_events MIDI 0..127), fallback oscillator khi lỗi
- chorus/reverb/gain: luôn dùng scipy/numpy fallback, xóa nhánh HAS_PEDALBOARD
- bỏ top-level import pedalboard (Pedalboard/Gain/Chorus/Reverb)
- test mới: bridge branch fallback, FX chorus/reverb scipy
2026-08-17 11:10:43 +07:00
admin fb63f06292 plan: note phase2 velocity fix, bank/program parity, golden RMS test 2026-08-17 11:04:10 +07:00
admin 3d963eb8dd phase2: fix velocity double-scaling in bridge job, bank/program parity, golden RMS test
- render_offline: send velocity as float 0..1 (RenderJob.cpp scales *127);
  old int(0.8*127) was scaled again -> clamp 127, velocity ignored
- forward soundfont_bank/program into bridge job (presence-based, matches
  midi_events_to_messages)
- new tests: bridge branch dispatch (SF_RENDER_ENGINE=bridge -> 404),
  bridge vs pedalboard golden RMS comparison (plan section 6)
2026-08-17 11:04:01 +07:00
admin 235dee103d Phase 2: native_render Python client (bridge --render) + SF_RENDER_ENGINE flag cho preview/midi-render 2026-08-17 10:49:54 +07:00
admin bbb11f8066 Phase 1: import .vstpreset in offline render (loadSerializedState)
daw_vst_bridge --render now applies a VST3 preset when job has 'preset'
(base64 of a .vstpreset file). Parse the SDK binary chunk format with
PresetFile (vst3sdk), rewrap component/controller state into the bridge
blob [4B BE compLen][comp][4B BE ctrlLen][ctrl] and apply via
loadSerializedState before rendering. SF2/SFZ ignore preset (warning).
Bad base64/magic/chunks fail the job with rc 1 and no partial clip.

Tests: golden Nexus .vstpreset fixture; VST3 with/without preset,
bad preset -> rc 1, non-VST3 preset ignored. 20/20 pass.
2026-08-17 10:44:13 +07:00
admin 4d1a2ed54a docs(plan): mark Phase 0 done (bridge --render CLI verified) 2026-08-17 08:04:53 +07:00
admin 0cca84c5f9 feat(bridge): add --render offline CLI (Phase 0 of pedalboard replacement)
daw_vst_bridge --render <job.json> --out <clip.wav> renders MIDI notes
through the same instrument engines as realtime (SF2/SFZ/VST3/VST2) to a
16-bit stereo WAV, headless, no SHM. Mirrors plugins.py/vst_engine.py
note semantics (velocity int(vel*127), bank CC0 + program at t=0, beatSec
60/max(30,bpm), totalNeeded max(1024,endSample), no tail). Chunk clamped
to 256 to match VST3 host prepared buffers. Uses vendored sheredom json.h
(gated on HAVE_VST3SDK); rc: 0 ok, 1 arg/job error, 2 load fail, 3 crash.
Partial output removed on failure. Verified: SF2, SFZ, Nexus VST3 render
non-silent at exact duration; 9/9 tests pass.
2026-08-17 08:04:25 +07:00
admin 4111995a5c Add VST2 hosting to native bridge; fix CC7 data2 routing verified via SHM test
VST2 (Steinberg 2.4 ABI) host via Vst2Instrument: aeffect.h, dispatcher,
process events/replacing, kEffectStartProcess, headless GUI skip.
- create_instrument returns Vst2Instrument for InstrumentType::VST2
- 64B-aligned scratch buffers for SIMD plugins; SEH crash capture module+stack
- plugins.py: VST2 resolves .dll/.so; vst_engine.py reports VST2 type
- app.jsx: derive instrumentType from scan (VST3 vs VST2), not hardcoded
- Install exes refreshed from Release build

SF2 FluidSynth audio verified end-to-end through SHM ring (energy 1.33):
silence was a test-harness bug (CC7 value written to velocity field, bridge
reads data2) — app sends data2 correctly. All temporary diagnostics removed.
2026-08-17 07:55:46 +07:00
admin 9d9b4b3686 Merge remote-tracking branch 'origin/standalone-shm-bridge' into standalone-shm-bridge 2026-08-16 21:27:50 +07:00
3dtours 353244708f docs: plan replace pedalboard (GPL-3.0) with native_bridge offline render 2026-08-16 21:25:20 +07:00
admin 98574aa0ea fix: ensure all daw_engine exit on app close (abrupt kill + LAN client open)
- desktop_engine: watchdog parent check via WaitForSingleObject(SYNCHRONIZE)
  instead of OpenProcess — killed parents linger as zombie objects, OpenProcess
  succeeds forever and watchdog never fires; hard-exit after 5s if graceful
  shutdown hangs (open LAN connection)
- lib.rs: Destroyed handler sweeps any orphan daw_engine/daw_vst_bridge by
  image name before exit(0), covering manual-launch and stale engines
2026-08-16 21:18:21 +07:00
3dtours e2c0564982 docs(license): add LICENSE.md, audit third-party licenses (Python/native/tauri), flag pedalboard GPL-3.0 2026-08-16 21:08:13 +07:00
3dtours 4f6e2bc049 docs: design VST2 backward compatibility for standalone-shm-bridge 2026-08-16 21:04:34 +07:00
admin cead8c8c71 fix(vsti): crackle fix P1-P3 — SHM 8-slot ring + drain-delta pump + AudioWorklet sink
- SharedMemoryIPC.h/main.cpp: render into ring slot (idx & ringMask), 8 slots
- shm.rs: ring fields + read_ring_audio; lib.rs pump drains FULL delta, not just latest
- bridgeAudioNode.js: AudioWorklet primary (bridge-audio-sink), ScriptProcessor fallback
- bridgeAudioWorklet.js: ring 64 blocks, underrun fade, overrun drop + crossfade
- layout tests: sizeof 27560 (8-align via u64)
2026-08-16 20:43:50 +07:00
admin ed26e1706d fix(latency): threadpool renders + RENDER_LOCK + client pre-render cache 2026-08-16 20:21:48 +07:00
admin 90b5a63719 Bump frontend cache-buster to 202608141031 2026-08-16 19:11:15 +07:00
3dtours cc0a573ca9 docs: phân tích + hướng fix VSTi play crackle/noise (Linux review) 2026-08-16 18:54:56 +07:00
admin 781425588c fix(standalone): bundle pedalboard.midi_utils + pyfluidsynth dll path
- engine.spec hiddenimports: pedalboard.midi_utils (C++ ExternalPlugin.h
  import dong, PyInstaller khong phat hien duoc -> midi-render 500)
- engine.spec hiddenimports: fluidsynth (pyfluidsynth cffi) cho
  /soundfont-render native
- vst_engine.py: _ensure_fluidsynth_dll_on_path() - frozen app them
  exe_dir vao PATH de ctypes.util.find_library tim thay
  libfluidsynth-3.dll dat ke exe
2026-08-16 18:50:27 +07:00
admin 1d40cf27b7 feat(standalone): native VSTi midi-render cho browser client khi bridge inactive
- playNativeVstNote/scheduleNativeVstItem: render qua /api/v1/plugins/midi-render
  (pedalboard VST3 dung instrument_id/preset) -> play WAV, map token rieng
- Them nhanh nativeVst tai moi diem route nhanh cua: startTrackPlayback,
  section/subtrack, playMidiPreviewNote, piano roll preview (alt-scroll/click/
  brush/draw), keybed, MIDI hardware input
- Safety net trong scheduleMidiNoteDispatch fallthrough (ghost notes va caller khac)
- stopAllNativeVstNotes/stopNativeVstNote di kem cac diem stop hien co
2026-08-16 18:23:53 +07:00
admin b2470e1422 Show server port alongside IP in SERVER label 2026-08-16 17:59:47 +07:00
admin cdbff49ea2 Remove LAN access badge; show server IP in SERVER label 2026-08-16 17:56:55 +07:00
admin cca58c4893 feat(piano-roll): vào tab lúc main play -> chỉ play MIDI notes track đang edit, các track khác mute
useEffect [activeTab] mới: activeTab là PIANO_ROLL và main/section đang play ->
stopAllPlayback + restart play CHỈ track piano roll tại vị trí tương ứng trong
item (mainPos - item.startTime). mainResumeRef giữ để quay lại main/section
resume đúng vị trí (nhánh resume cũ không đổi).
2026-08-16 17:47:31 +07:00
admin e541d16132 merge: docs docker play VSTi (70da711) 2026-08-16 17:28:30 +07:00
admin 7bcaf19c28 feat(vst-gui): open VST3 GUI while playing (level 1)
- main.cpp: drop transportStopped defer in handleOpenGui (G0.1 legacy);
  G0.1b setReloading-before-job + single UiThread (G1.2) block the
  2-thread-1-DLL race. GUI opens immediately during PLAY, short mute
  (~0.1-0.3s first time, hasAttachedOnce_ false so reloadForGUI skips
  reload); notes already sounding are kept. Reopen after close still
  reloads (1-3s mute) - level 2 not feasible.
- app.jsx: remove !isPlayingRef.current guard before openNativeGUI in
  loadTrackInstrumentToBridge.
- rebuild app.precompiled.js + daw_vst_bridge.exe.
2026-08-16 17:27:38 +07:00
3dtours 70da711520 docs: thiết kế kiến trúc docker play VSTi ở client — giữ luồng cũ (VSTi→SF2/SF3 asset) 2026-08-16 17:08:19 +07:00
admin 223023615d fix(media-explorer): bridge preview channel 15 + NOTE_ON timing; synth dropdown portal 2026-08-16 16:58:18 +07:00
admin 574c2f885d fix(media-explorer): preview MIDI qua native bridge FluidSynth thay soundfontRender 501
- playMidiPreview standalone: thay soundfontRender backend (pyfluidsynth khong co
  trong bundle -> 501 -> cam) bang playMediaExplorerBridgePreview: allocate
  channel rieng, bridgeLoad SF2 + loadInstrument, CC0/CC32/PROGRAM, NOTE_ON/OFF
  theo beat, loop timer; bridge down -> fallback WASM
- stopMediaPlayback: NOTE_OFF toan pitch cho channel preview de note khong con
  ngan sau khi stop
2026-08-16 16:42:19 +07:00
admin 56d8c577aa deploy: sonicforge-daw.exe V10 (drag-drop fix build) 2026-08-16 16:23:37 +07:00
admin 045d84bde0 fix: drag-drop MIDI from Explorer on Windows — disable Tauri drag_drop_handler so WebView2 gets HTML5 DnD (Phương án A, FIX_DRAG_DROP_WINDOWS.md) (V10) 2026-08-16 16:20:48 +07:00
admin c29e024372 fix: stale-reload watchdog — force-unmute channel stuck in reloading >20s (60s if close/attach in flight) (V9) 2026-08-16 16:15:41 +07:00
3dtours 4c8677699c docs: đề xuất fix drag-drop MIDI file trên Windows/Tauri 2026-08-16 16:08:07 +07:00
admin b6950d6418 fix: MIDI keyboard preview silent after transport STOP — live flag bypass guard (V8) 2026-08-16 15:43:23 +07:00
admin 2d63774474 rebuild clean from scratch: native bridge + tauri + engine bundle (F6 Cap 2 in deployed engine) 2026-08-16 15:11:10 +07:00
admin 761b48e41f fix 5.2 audit findings on Windows: F-PROC-1 injection, F-MEM-5 UAF, F-PROC-2 restart race, F-PROC-3, F5, F6 2026-08-16 12:29:42 +07:00
3dtours 54b7a7a6ac 5.2: audit security/memory/child-process conflict — F-MEM-5 UAF, F-MEM-8, F-PROC-1 injection, F-PROC-2 restart race, F-PROC-3, F5, F6 (Linux review only) 2026-08-16 12:12:29 +07:00
3dtours 07f0569d81 G2: Linux review + verify — code G2 consistent, no product bugs (F1 double flush minor, F2 SF_STATE_FILE Linux fallback note, F3 cosmetic); StateStore round-trip PASS on Linux g++; report section 5.1 + journal updated 2026-08-16 11:48:51 +07:00
admin 6c19f17b61 deploy: plugin_host.exe idle-skip build 2026-08-16 11:45:02 +07:00
admin 9fa3076359 G4.3 idle-skip + no fixed-tick render: idle CPU ~0, latency 9.8ms 2026-08-16 11:44:56 +07:00
admin 9436e66835 G4.3: double-buffer sandbox SHM - SandboxHostIPC writeSlot+2x256 double audio, latency 15.6ms, multi-instance no-xrun verified 2026-08-16 11:35:31 +07:00
admin 462a5104d2 G4.2: bridge-side plugin process pool — SandboxVst3Host spawns/respawns plugin_host.exe per channel, MIDI forward + audio snapshot + watchdog mute/respawn, crash isolation verified 2026-08-16 10:52:04 +07:00
admin 7de00955d8 G4.1: sandbox plugin host (plugin_host.exe) PoC — Nexus runs in child process, GUI + audio via SHM, crash isolation verified 2026-08-16 10:36:01 +07:00
admin 700f612674 chore: commit G3.2+G3.3 deploy binaries + debug scripts; ignore debug run artifacts 2026-08-16 10:27:36 +07:00
admin d0da12f42b G3: journal — note test helper committed 85273a6 2026-08-16 10:25:19 +07:00
admin 85273a6a7f debug: fix G1.5 stress test wait_log to read log file once per poll (avoids false timeouts on 140k-line log) 2026-08-16 10:23:55 +07:00
admin 40ee80ad60 G3: update task tracking — G3.2/G3.3 done (classify stall + SEH per-channel) 2026-08-16 10:23:46 +07:00
admin 0b8e80fe2c G3.2+G3.3: classify hang/crash vs audio stall + SEH per-channel plugin crash isolation (mute channel) 2026-08-16 10:23:08 +07:00
admin 30fa98a441 G3.1: dedicated bridge heartbeat — watchdog liveness independent of audio loop 2026-08-16 10:04:16 +07:00
admin b59e416566 G2.5+G2.6: stress driver for persistence restart (N loop + kill/respawn) + verify — regression G1.5 5/5, G2.5 5+10 PASS, G0.4 probe PASS 2026-08-16 08:37:27 +07:00
admin d31312e824 G2.2: flush bridge state to disk before kill (control type=5 DUMP + Rust pre-kill) 2026-08-16 07:52:01 +07:00
admin c2ae452844 G2.4: skip redundant LOAD of already-loaded instrument (keep restored preset) 2026-08-16 07:47:53 +07:00
admin 4faa5a3504 G2.3: auto-reload instruments from state file after bridge respawn 2026-08-16 07:33:14 +07:00
admin 28441566c2 G2.1: save full instrument state to disk (path/type/preset/transport) 2026-08-16 07:28:17 +07:00
admin 8d9011222a G2.0: update progress table + journal (20260815-tasks.md) 2026-08-16 07:09:05 +07:00
admin 1fe2d261fd G2.0: StateStore (JSON state file + atomic write) 2026-08-16 07:08:37 +07:00
3dtours af9a285974 G2: ghi ro phan cong may sau tung task - Linux chi plan/review/notes, Windows sua code/build/test/fix + quy trinh 2 may 2026-08-15 22:21:38 +07:00
3dtours 5607ba5440 G2: them ke hoach thuc hien 20260815-tasks.md (phan tich G2.1-G2.6 + estimate + quy trinh Windows) 2026-08-15 22:16:48 +07:00
admin 3664ffbc47 them big_tasks.md (roadmap G0-G4, danh dau G1.1-G1.5 hoan thanh) + debug scripts/probes cua phien lam viec G0.1-G1.5 (test_g13/test_g12/test_g15_stress, patch_g1x, deploy/build bridge, probe_g0x) 2026-08-15 21:59:17 +07:00
admin c813481357 G1.4: dong bo comment voi code - bo ghi chu mute-when-editor-open con sot (dispatch channelQuiet, attach job); ghi ro reloading_ chi mute khi createView/createInstance chay 2026-08-15 21:49:00 +07:00
admin 7629783298 G1.4: bo mute-when-editor-open - channelQuiet chi con reloading; xoa editorOpen machinery (g_editorOpen/g_editorPathCount/clear_editor_open/setEditorOpenPredicate); audio loop process() binh thuong khi editor mo 2026-08-15 21:28:16 +07:00
admin b92c85ec73 G1.3: bo pump gate cu (is_teardown_window/g_ownerTid/g_juceOwnerTids/g_tidToWorker/post_and_wait) — 1 UiThread khong con race teardown-pump; CBT hook chi tren UiThread; destroy_window_on_owner destroy truc tiep 2026-08-15 21:18:49 +07:00
admin 9e6e72ac27 G1.2: 1 UiThread chung thay 16 ChannelWorker (COM STA + message pump + job queue); window+attach job tren UiThread; audio loop bo PeekMessageW; Option B close dong bo qua close_editor_now 2026-08-15 21:02:18 +07:00
admin 3587a34e31 G1.1: fix probe crash (std::thread joinable dtor -> terminate); xac nhan openGUI tren worker thread OK; closeGUI truoc teardown 2026-08-15 20:50:43 +07:00
admin eac7f2feb8 G0.4: save/restore preset quanh reload khi close GUI (captureState/restoreState, memorystream link truc tiep) 2026-08-15 20:04:56 +07:00
admin 5313d85926 G0.3: watchdog giu 15s khi attach-in-flight (SHM flag) + RAII guard trong attach job (fix kill nham khi GUI mo) 2026-08-15 19:00:32 +07:00
admin 2f7861820e G0.2: openVstGuiRetry toi da 2 lan + backoff 800ms + skip khi PLAY 2026-08-15 18:45:25 +07:00
admin 18394807a9 fix GUI VSTi khi PLAY: defer OPEN_GUI + silence same-path truoc attach (G0.1) 2026-08-15 18:44:02 +07:00
admin 2890856d70 deploy exe fix GUI VSTi (9f1dc37c) vao install/ 2026-08-15 17:50:29 +07:00
admin 67e73a2a85 fix GUI VSTi mute/reopen/not-load: revert walkthrough 2 ve hanh vi 0cec3d3 (reloadForGUI reload-on-reopen, closeGUI khong goi view->removed(), post_close_gui reload-on-close + same-plugin silence + destroy children + pathBusy, editorOpenPredicate refcount mute, CoInitializeEx MTA, window main-thread), giu cai tien an toan (WM_ERASEBKGND/PAINT/SIZE chong trang, resizeView, call_once, jobEvent_/MsgWaitForMultipleObjects pump 128, ABOVE_NORMAL + PowerThrottling worker, setBypassed quanh assign/unload); queryInterface giu kNoInterface nhu 0cec3d3 — VERIFIED: probe_fix_flow PASS (mo GUI Nexus -> close -> reopen khong mute, load Ample khi GUI dang mo OK, screenshot khong trang); warning: driver stress test 6 Nexus cung luc crash xac suat CA tren exe stable 0cec3d3 (khong phai regression) 2026-08-15 17:50:18 +07:00
admin f94b2fa757 wip walkthrough 2 bridge: jobEvent_/MsgWaitForMultipleObjects pump, SetThreadPriority + PowerThrottling, isBypassed silence quanh assign/unload, closeGUI view->removed(), reloadForGUI reuse instance, bo mute khi GUI mo, CoInitializeEx MTA — CHƯA verified: driver test 1/3 PASS (AV c0000005 Nexus!GetPluginFactory luc attachView ch5, nghi MTA/bo mute), exe deploy giu ban 0cec3d3 (28104a47) 2026-08-15 17:11:47 +07:00
admin 0cec3d3cce fix GUI VSTi trắng/không load: DPI per-monitor v2 + WS_CLIPCHILDREN + view->onSize() sau attached; restore API manager header/cpp (2596372 xoá nhầm) và deploy exe mới 2026-08-15 16:12:29 +07:00
admin 90241f626f fix crash daw_vst_bridge 0xc000041d khi mở GUI Nexus rồi switch sang Ample: over-gate JUCE message window (g_juceOwnerTids, chỉ chạy khi close_in_flight) + gỡ VEH tạm và mọi log [dbg] chẩn đoán, giữ [NativeBridge] production; deploy exe FINAL (đã verify 3 run driver) 2026-08-15 11:35:50 +07:00
275 changed files with 45170 additions and 4301 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ PIANOBK_DIR=/home/locpham/daw_assets/pianobook
# ── Runtime (tự phát hiện môi trường) ──
# auto (mặc định): Windows/macOS → desktop; Linux không DISPLAY hoặc docker → headless.
# desktop: server + client cùng 1 máy (bật nút "Mở trong Carla" nếu có Carla local)
# desktop: server + client cùng 1 máy (native GUI VSTi mở được trên máy)
# headless: server docker + browser UI (soundfont + VSTi mở được từ storage mount;
# preset chỉnh trên máy khác → upload .vstpreset qua web UI)
SF_RUNTIME=auto
+19 -1
View File
@@ -31,12 +31,15 @@ samples/
celerybeat-schedule
node_modules
src-tauri/target/
src-tauri/binaries/
# src-tauri/binaries/: portable native bridge sidecars — COMMITTED.
# Rebuild: build/scripts/build_native_bridge.ps1. Features native phai theo
# push sang may khac nen commit binary (khong ignore).
src-tauri/resources/daw_engine/
src-tauri/vc_redist.x64.exe
app/storage/plugin_dirs.json
app/storage/sf_scan_state.json.bak-root
app/storage/soundfonts/
app/storage/carla_projects/
# build-time vendor trees & accidental artifacts (never commit)
native_bridge/vcpkg_installed/
@@ -47,3 +50,18 @@ native_bridge/sfizz-src/
install/daw_engine/
app/static/js/_diag.txt
src-tauri/resources/daw_engine" && cp -r*
# debug/ run artifacts (screenshots, output captures, binaries) — scripts are committed
native_bridge/debug/*.png
native_bridge/debug/*.out
native_bridge/debug/pidtest.txt
native_bridge/debug/shm_selfcheck.exe
*.obj
# Linux-only runtime artifacts (built via Docker; never committed)
install/daw_vst_bridge
install/libsfizz.so.1
build_juce
# build artifact (root, khong phai deploy dir)
target/
+5
View File
@@ -1,3 +1,8 @@
[submodule "native_bridge/vst3sdk"]
path = native_bridge/vst3sdk
url = https://github.com/steinbergmedia/vst3sdk.git
[submodule "native_bridge/JUCE"]
path = native_bridge/JUCE
url = https://github.com/juce-framework/JUCE
branch = 8.0.7
shallow = true
+101
View File
@@ -0,0 +1,101 @@
# 20260815 — KẾ HOẠCH THỰC HIỆN G3 (HEALTH / WATCHDOG THÔNG MINH)
Nguồn: `big_tasks.md` — header **G3 — Health / Watchdog thông minh (1-2 tuần)**.
Trạng thái roadmap: G0 ✅, G1 ✅, G2 ✅ (G2.0–G2.6). Branch: `standalone-shm-bridge`.
---
## 1. PHÂN TÍCH G3
### 1.1 Mục tiêu
Watchdog thông minh, cô lập lỗi:
- G3.1 heartbeat riêng: heartbeat bridge tách khỏi logic audio.
- G3.2 phân loại stall: phân biệt hang/crash/stall để xử lý đúng mức.
- G3.3 SEH quanh process từng channel: 1 plugin crash → chỉ mute channel đó, không sập cả bridge.
### 1.2 Hiện trạng (đã verify trong code)
| Thành phần | File | Hiện trạng liên quan G3 |
|---|---|---|
| SHM layout | `native_bridge/include/SharedMemoryIPC.h` + `src-tauri/src/shm.rs` (phải khớp, assert size) | `bridgeWriteIndex` @4 (mỗi block render), `attachInFlight` @11160 (G0.3). Tổng 11164B. Không có heartbeat riêng. |
| Watchdog Rust | `src-tauri/src/lib.rs` pump thread | Liveness = `write_index()` đổi trong 3s; nếu `attach_in_flight` → deadline 15s; stall → kill + spawn lại 1 lần rồi `bridge-down`. |
| Bridge C++ | `native_bridge/src/main.cpp` | Main loop = audio loop (render + drain control); mọi attach/load chạy trên UiThread (`uiWorker.post`); heartbeat không tồn tại. |
### 1.3 Khoảng trống (gap)
1. **Liveness gắn với audio loop** — writeIndex chỉ tăng khi audio loop còn render; attach job dài (Nexus >3s, G0.3) làm audio loop nghẽn → watchdog tưởng bridge chết → phải kéo deadline lên 15s (đánh đổi: bridge thật chết cũng chờ 15s).
2. **Không phân loại được stall** — hang (audio loop kẹt, process còn sống) vs crash (process chết) xử lý giống nhau.
3. **1 plugin chết kéo cả bridge** — chưa có SEH/process con từng channel (G3.3).
### 1.4 Nguyên tắc thiết kế (đề xuất)
- **Heartbeat là nguồn liveness duy nhất** (G3.1): bridge chạy thread riêng tăng `heartbeat` ~10Hz trong SHM, độc lập audio loop → attach dài không còn ảnh hưởng deadline; bỏ ngoại lệ `attach_in_flight` trong watchdog.
- **Audio stall ≠ bridge chết** (G3.1 lót đường G3.2): heartbeat còn mà writeIndex đứng → log "audio stall (heartbeat OK)" — không restart.
- **Field append-only** — giữ offset cũ (như G0.3): thêm `heartbeat` sau `attachInFlight` @11164.
- **Không đụng audio loop** — heartbeat thread riêng, Sleep(100ms), không lock.
---
## 2. PHÂN RÃ TASKS + ESTIMATE
| ID | Task | File đích | Nội dung | Estimate |
|---|---|---|---|---|
| G3.1 | Heartbeat riêng | `SharedMemoryIPC.h`, `shm.rs`, `main.cpp`, `lib.rs`, `shm_selfcheck.cpp` | SHM thêm `heartbeat:u32` (append @11164, size 11168); bridge thread riêng tăng 100ms; Rust pump dùng heartbeat cho liveness (deadline 3s, bỏ ngoại lệ attach); writeIndex stall khi heartbeat còn → log warning, không restart. | 1 ngày |
| G3.2 | Phân loại stall | `lib.rs`, `main.cpp` | hang vs crash vs stall; xử lý đúng mức (restart / mute / chỉ log). | 2 ngày |
| G3.3 | SEH từng channel | `main.cpp` | 1 plugin crash → chỉ mute channel, không sập bridge. | 3 ngày |
| | **Tổng** | | | **6 ngày (~1.5 tuần — khớp roadmap)** |
### Phụ thuộc
`G3.2 → G3.1` (cần heartbeat) · `G3.3 → G3.2` (cần phân loại).
### Thứ tự triển khai khuyến nghị
G3.1 → G3.2 → G3.3.
---
## 3. KẾ HOẠCH THỰC HIỆN (CÀI ĐẶT → KIỂM TRA → BẮT BUGS → FIX → REPORT)
### G3.1 — Heartbeat riêng
- **Cài đặt**:
- `SharedMemoryIPC.h`: thêm `volatile uint32_t heartbeat;` sau `attachInFlight` (append-only, offset 11164).
- `shm.rs`: thêm field `heartbeat: u32` + accessor `heartbeat()` + sửa assert size 11168.
- `main.cpp`: trước main loop, spawn thread `[shmIPC]{ for(;;){ Sleep(100); shmIPC->heartbeat++; } }` detach.
- `lib.rs` pump: liveness = heartbeat (đổi trong 3s → restart 1 lần → `bridge-down`); writeIndex chỉ dùng emit audio; heartbeat còn + writeIndex đứng ≥3s → log `[tauri] audio stall (heartbeat OK)`, không restart. Bỏ `attach_in_flight` khỏi deadline.
- `shm_selfcheck.cpp`: assert size 11168.
- **Kiểm tra**: build bridge (build_bridge.ps1) + `cargo build --release` (src-tauri) → copy exe vào install/ → deploy → probe SHM: heartbeat tăng ~10Hz; kill bridge → heartbeat đứng → watchdog restart (log) → heartbeat tăng lại; regression G1.5 subset + G2.5 stress vẫn PASS.
- **Bắt bugs**: attach dài không còn risk restart (bỏ ngoại lệ 15s); heartbeat sai offset (assert size bắt).
- **Fix + report**: mục 5.
- **📌 Lưu ý & quy trình**:
- **Windows** (sửa code): patch bytes cho file CRLF (SharedMemoryIPC.h, main.cpp), text thường cho lib.rs (LF). Build bridge + cargo release. Deploy: kill 3 proc → copy `target/release/sonicforge-daw.exe` + `native_bridge/build/Release/daw_vst_bridge.exe` vào `install/` → start → PIDs mới. Chạy probe heartbeat + stress. Commit: `G3.1: dedicated bridge heartbeat — watchdog liveness independent of audio loop`.
- **Linux** (ghi chú): pull + review; không chạy được (cần SHM Windows); cập nhật mục 5.
---
## 4. RỦI RO
| Rủi ro | Ảnh hưởng | Giảm thiểu |
|---|---|---|
| Heartbeat thread bị kill cùng process | Vẫn restart như cũ (không tệ hơn) | OK — heartbeat chết = process chết = restart đúng. |
| Heartbeat sai offset → đè field khác | Hỏng SHM | Assert size 11168 ở cả 2 phía (shm.rs test + shm_selfcheck.cpp). |
| Bỏ ngoại lệ attach 15s → attach dài bị restart | Mất GUI giữa attach | Heartbeat thread độc lập → attach không làm heartbeat đứng; chỉ restart khi process thật chết. |
| Audio loop kẹt vĩnh viễn (plugin hang) nhưng heartbeat còn | Không restart → câm vĩnh viễn | G3.2 phân loại + xử lý (log cảnh báo ở G3.1, có dấu hiệu trong log). |
---
## 5. BÁO CÁO KẾT QUẢ (điền sau khi thực hiện)
| ID | Cài đặt | Kiểm tra | Bugs bắt được | Fix | Trạng thái |
|---|---|---|---|---|---|
| G3.1 | Heartbeat @11164 (size 11168), thread riêng Sleep(100) tăng `heartbeat`; pump liveness = heartbeat (deadline 3s, restart 1 lần); bỏ ngoại lệ attach 15s; writeIndex chỉ emit audio + stall log. | Probe G3.1 PASS (hb 10Hz; kill bridge → restart 3.8s, log stalled); G2.5 stress PASS; G1.5 stress PASS (fix test script đọc log 1 lần/poll); G0.4 probe PASS. | G1.5 fail trước đó là test-env: test script gọi log_lines() mỗi index (140k dòng) + window VST cũ dính → OPEN_GUI dedupe silent; flood 896 dòng allNotesOff pre-existing. | Không cần fix thêm. | ✅ |
| G3.2 | lib.rs: khi heartbeat stalled 3s + restart_attempts==0, lấy pid từ BridgeProcess state → process_alive(pid) (OpenProcess + GetExitCodeProcess, windows-sys) → log `HUNG (pid N alive, heartbeat stalled)` / `CRASHED (pid N gone)` / `UNKNOWN (no tracked pid)`; Cargo.toml thêm feature Win32_System_Threading. | Probe CRASH PASS (kill bridge → log `CRASHED (pid 4432 gone)` sau ~10s → restart); Probe HUNG PASS (NtSuspendProcess → log `HUNG (pid 6232 alive, heartbeat stalled)` → TerminateProcess → restart). Regression G1.5 + G3.1 PASS. | Không có. | Không cần. | ✅ |
| G3.3 | NativeInstrumentEngine.h/cpp: `crashedCh_[16]` + markCrashed/isCrashed; `channelQuiet` trả true khi crashed (mute dispatch+render); SEH helper `SafeProcessChannel` (`__try/__except EXCEPTION_EXECUTE_HANDLER`, tách khỏi hàm có C++ unwinding để tránh C2712); renderAll nếu false → `crashedCh_[ch]=true` + log muted until reload + continue (không cộng output); assign() reset flag; /EHa đã bật sẵn. CMakeLists thêm target `seh_channel_test`. | Bridge build PASS; Cargo build PASS; `seh_channel_test.exe` PASS (stub ném access violation → SafeProcessChannel false, block zero, manager isCrashed set, renderAll no-op, process sống); regression G1.5 + G3.1 PASS. | Không có. | Không cần. | ✅ |
### Nhật ký thực hiện
### G3.1 — Hoàn tất (2026-08-15)
- SHM: `heartbeat` append @11164, tổng 11168B (SharedMemoryIPC.h + shm.rs field/accessor/assert + shm_selfcheck.cpp assert).
- main.cpp: thread riêng `Sleep(100)` → `shmIPC->heartbeat++` (độc lập audio loop).
- lib.rs pump: liveness = heartbeat, deadline 3s → kill+spawn 1 lần/bridge-down; bỏ attach_deadline 15s; writeIndex chỉ emit audio; heartbeat còn + writeIndex đứng → log `[tauri] audio stall (heartbeat OK)`, không restart.
- Verify: probe heartbeat PASS (tăng ~10Hz, kill → watchdog restart 3.8s, spawn.log `bridge heartbeat stalled 3s — restart attempt #1`); G2.5 stress PASS; G1.5 stress PASS; G0.4 probe PASS.
- G1.5 fail trước đó = test-env, không phải regression G3.1: (1) test script `wait_log` đọc cả file log mỗi index truy cập (140k+ dòng) → poll chậm >10s → STOP/OPEN_GUI timeout giả; fix: đọc 1 lần/poll. (2) window VST cũ (0x870D76, 0x2F0754) giữ view giữa các lần chạy → OPEN_GUI dedupe silent (handleOpenGui `hasAttachedView()` return không log). (3) flood 896 dòng `[midi] OFF` = allNotesOff() log từng pitch (7 ch × 128) — pre-existing, STOP log vẫn 0.15s.
### G3.2 + G3.3 — Hoàn tất (2026-08-15)
- G3.2: pump watchdog phân loại stall — khi heartbeat stalled 3s + chưa restart lần nào, lấy pid từ BridgeProcess state, kiểm tra process còn sống (OpenProcess/GetExitCodeProcess) → log HUNG (pid alive) / CRASHED (pid gone) / UNKNOWN (no tracked pid). Verify: CRASH test (kill bridge → `CRASHED (pid 4432 gone)` → restart bridge mới); HUNG test (NtSuspendProcess giữ process sống nhưng heartbeat đứng → `HUNG (pid 6232 alive, heartbeat stalled)` → TerminateProcess → restart). Cả 2 PASS.
- G3.3: SEH per-channel — `SafeProcessChannel` bọc processAudioBlock trong `__try/__except(EXCEPTION_EXECUTE_HANDLER)`; plugin fault → mute channel (crashedCh_), log `[NativeBridge] G3.3: channel N plugin crashed in processAudioBlock (SEH) — muted until reload`, các channel khác tiếp tục render, bridge không sập; assign() (reload instance) reset mute. Helper tách riêng vì SEH không hợp lệ trong hàm có C++ unwinding (C2712). Verify: `seh_channel_test.exe` (stub processAudioBlock gây access violation → SafeProcessChannel trả false, crash bị nuốt, crashed flag set, renderAll no-op) PASS; bridge + cargo build PASS.
- Regression: G1.5 stress 1 iter PASS (đã fix test script đọc log 1 lần/poll + đóng GUI VST dính giữa các lần chạy); G3.1 heartbeat probe PASS (hb ~10Hz, kill → restart 3.7s). G2.5 không chạy lại (không đụng audio loop).
- Commit `0b8e80f` (6 files: lib.rs, Cargo.toml, NativeInstrumentEngine.h/.cpp, CMakeLists.txt, tests/seh_channel_test.cpp). `install/*.exe` không commit; `native_bridge/debug/test_g15_stress.py` (fix helper) commit riêng `85273a6`.
- G4 (sandbox plugin): tuỳ chọn, 2-3 tháng sau.
+139
View File
@@ -0,0 +1,139 @@
# 20260815 — KẾ HOẠCH THỰC HIỆN G4 (SANDBOX PLUGIN / PROCESS CON)
Nguồn: `big_tasks.md` — header **G4 — Sandbox plugin (3-5x G1+G2, 2-3 tháng)**.
Trạng thái roadmap: G0 ✅, G1 ✅, G2 ✅, G3 ✅ (G3.1–G3.3). Branch: `standalone-shm-bridge`.
User chủ động yêu cầu: làm dirty trước, sau đó làm luôn G4.
---
## 1. PHÂN TÍCH G4
### 1.1 Mục tiêu
VST3 chạy trong process con (`plugin_host.exe`) — crash plugin chỉ chết process con,
bridge + GUI + các plugin khác sống. VST3 không có chuẩn out-of-process như AUv3 →
wrapper tự làm.
### 1.2 Hiện trạng (đã verify trong code)
| Thành phần | File | Ghi chú |
|---|---|---|
| VST3 host | `native_bridge/src/Vst3Instrument.cpp` + `include/Vst3Instrument.h` | pimpl (`Vst3HostState`), độc lập, reuse được trong process con: `loadPlugin(path, sr)` (tự activate + setupProcessing với maxBlockSize_), `openGUI(HWND)`, `processAudioBlock(L,R,n)` (zero output buffer trước process, copy ra ngoài), MIDI dispatch `noteOn/noteOff/controlChange/programChange/pitchBend`, `closeGUI()`, `reload()`. |
| SHM layout | `include/SharedMemoryIPC.h` | `SharedAudioBufferIPC` 11168B: `bridgeWriteIndex`@4, `masterLeft`@8 (float[256]), `masterRight`@1032, `midiQueue[64]`@2064 (12B/evt), `midiQueueCount`@2832, `controlQueue[8]`@2836, `heartbeat`@11164 (G3.1). Helpers `shm_open/shm_create/shm_ptr/shm_close/shm_write_midi/shm_write_control` trong `SharedMemoryIPC.cpp` (trước G4.1 không có prototype trong header — đã bổ sung). |
| CMake target pattern | `native_bridge/CMakeLists.txt` | `gui_probe`/`seh_channel_test`: `HAVE_VST3SDK=1` + `${VST3_SDK_TARGET} sdk_hosting sdk_common` + `module_win32.cpp` + `memorystream.cpp` + `${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} winmm`. |
### 1.3 Khoảng trống (gap)
1. VST3 chạy cùng process bridge → 1 plugin crash có thể hạ bridge (đã giảm thiểu phần nào bằng SEH G3.3 nhưng vẫn chung process).
2. Chưa có cơ chế spawn con + SHM con (MIDI in / audio out / heartbeat) + giám sát respawn.
### 1.4 Nguyên tắc thiết kế (đề xuất)
- Mỗi plugin 1 process con `plugin_host.exe`, argv: `--shm <name> --path <vst3> [--sr] [--block] [--parent <pid>]`.
- Con tự tạo mapping SHM riêng (tái dùng layout `SharedAudioBufferIPC` — không phát sinh struct mới, bridge đọc được nguyên layout).
- GUI attach trên main thread con (editorhost pattern: attach rồi pump); heartbeat ~10Hz thread riêng (pattern G3.1).
- Con exit khi parent chết (kiểm tra `parent_alive` mỗi block) — chống orphan.
- G4.2/G4.3: bridge mở mapping con thay vì con tự tạo; pool + watchdog respawn; ring buffer realtime.
---
## 2. PHÂN RÃ TASKS + ESTIMATE
| ID | Task | File đích | Nội dung | Estimate |
|---|---|---|---|---|
| G4.1 | PoC plugin_host độc lập | `src/plugin_host_main.cpp`, `CMakeLists.txt`, `include/SharedMemoryIPC.h` | argv(shm,path,sr,block,parent) → COM STA → load VST3 → GUI attach → audio loop qua SHM (drain MIDI → dispatch → processAudioBlock → ghi masterLeft/Right → bridgeWriteIndex++) → heartbeat thread → exit khi parent chết. Done khi: Nexus chạy trong con, GUI + audio OK, kill con → chỉ con chết. | 1-2 ngày |
| G4.2 | Bridge → client pool | `main.cpp` (bridge) | bridge spawn 1 con/plugin, forward MIDI, đọc audio mix, watchdog (crash → respawn, mute/restore như G3.3). | 2-3 tuần |
| G4.3 | IPC audio realtime | SHM ring buffer | double-buffer SHM con, latency <20ms, 6 Nexus song song không xrun. | 2-3 tuần |
### Phụ thuộc
`G4.2 → G4.1` (cần plugin_host chạy được độc lập) · `G4.3 → G4.2`.
### Thứ tự triển khai khuyến nghị
G4.1 → G4.2 → G4.3.
---
## 3. KẾ HOẠCH THỰC HIỆN (G4.1 PoC — hoàn tất hôm nay)
### G4.1a — Đọc code, chốt API host
- `Vst3Instrument`: `init(sr, block)` set `sampleRate_/maxBlockSize_` (gọi trước `loadPlugin` — loadPlugin dùng `maxBlockSize_` trong `setupProcessing`; default 256 = AUDIO_BLOCK_SIZE). `loadPlugin` tự activate + setProcessing + prepare. `processAudioBlock` zero output rồi process, copy stereo ra ngoài, clear eventList.
- Drain MIDI theo convention bridge (main.cpp): snapshot `midiQueue[0..count-1]` (bounded 64), reset `midiQueueCount=0`, dispatch theo command 0x9/0x8/0xB/0xC/0xE.
- Heartbeat: thread riêng `Sleep(100)` → `heartbeat++` (pattern G3.1).
### G4.1b — Viết `plugin_host_main.cpp`
- Parse argv `--shm/--path/--sr/--block/--parent`.
- `CoInitializeEx(STA)` trên main thread (GUI thuộc main thread con).
- `shm_create(name)` — PoC: con sở hữu mapping (G4.2 đổi thành mở).
- `init(sr,block)` → `loadPlugin(path,sr)` → tạo own top-level window → `openGUI(hwnd)` (editorhost pattern) → heartbeat thread → main loop: pump messages + drain MIDI + `processAudioBlock` + `bridgeWriteIndex++` + `parent_alive` check → `closeGUI()` + `CoUninitialize()` + `shm_close`.
- ASCII-only, LF → normalize CRLF (repo convention).
### G4.1c — CMake target `plugin_host` + build
- Copy pattern `gui_probe`: `src/plugin_host_main.cpp + src/SharedMemoryIPC.cpp + src/Vst3Instrument.cpp` + SDK hosting libs (không cần NativeInstrumentEngine.cpp).
- Bổ sung prototype `shm_open/shm_create/shm_ptr/shm_close` vào `SharedMemoryIPC.h` (trước đây chỉ có trong .cpp, plugin_host cần khai báo).
- Build: `cmake --build build --config Release --target plugin_host -- /m` PASS → `build/Release/plugin_host.exe`.
### G4.1d — Probe với Nexus (probe_g41.py)
- Spawn con với `C:\Program Files\Common Files\VST3\Nexus.vst3`, SHM `SonicForge_PluginHost_Probe`.
- Verify: heartbeat tăng (hb=1 trong ~1s); GUI window xuất hiện (4 hwnd thuộc pid con, gồm PluginHostClass + editor Nexus); ghi note-on C4 → audio peak 0.38274 (nonzero); terminate con → app 15928 + bridge 20996 vẫn sống (isolation).
### G4.1e — Commit + journal + push
---
## 4. RỦI RO
| Rủi ro | Ảnh hưởng | Giảm thiểu |
|---|---|---|
| Con chết khi đang render → dữ liệu audio mất 1 block | Click/pop | G4.3 ring buffer + G4.2 mute/restore; PoC chấp nhận. |
| Parent chết → con orphan | Process treo | `--parent` + check mỗi block (đã làm). |
| Plugin GUI yêu cầu thread/COM riêng | Hang/blank view | Attach trên main thread con (editorhost pattern đã verify qua gui_probe). |
| SHM con đổi layout sau này | Mất tương thích | Tái dùng `SharedAudioBufferIPC` — layout ổn định append-only. |
---
## 5. BÁO CÁO KẾT QUẢ (đã thực hiện)
| ID | Cài đặt | Kiểm tra | Bugs bắt được | Fix | Trạng thái |
|---|---|---|---|---|---|
| G4.1a | Chốt API host từ Vst3Instrument + convention drain MIDI/heartbeat (main.cpp). | — | — | — | ✅ |
| G4.1b | `plugin_host_main.cpp`: argv parse, COM STA, shm_create, loadPlugin, openGUI own window, audio loop + heartbeat + parent check. | Build PASS (sau fix prototype SHM). | `ShmHandle/shm_create/shm_ptr/shm_close` không có prototype trong header → C2065. | Thêm prototype vào `SharedMemoryIPC.h`. | ✅ |
| G4.1c | CMake target `plugin_host` (pattern gui_probe, không engine). | Build PASS → `build/Release/plugin_host.exe`. | Patch block bị chèn vào giữa `if(WIN32)` của gui_probe + `endif()` thừa. | Script sửa vị trí + xóa endif. | ✅ |
| G4.1d | Probe: spawn + Nexus + heartbeat + GUI + audio + isolation. | PASS: hb=1 (1s); 4 hwnd; peak 0.38274; kill con → app 15928 + bridge 20996 sống. | Probe python đọc SHM AV — `MapViewOfFile` restype mặc định c_int cắt con trỏ 64-bit. | Set `restype=c_void_p`. | ✅ |
| G4.1e | Commit + journal + push. | — | — | — | ✅ |
### Nhật ký thực hiện
### G4.1 PoC — Hoàn tất (2026-08-15)
- `native_bridge/src/plugin_host_main.cpp` (mới): process con sandbox — argv `--shm/--path/--sr/--block/--parent`; COM STA; `shm_create` (PoC con sở hữu mapping, G4.2 đổi thành `shm_open`); `init` → `loadPlugin` → own window + `openGUI` (editorhost pattern) → heartbeat thread `Sleep(100)` (G3.1 pattern) → main loop pump + drain MIDI (snapshot 64, reset count, dispatch 0x9/0x8/0xB/0xC/0xE) + `processAudioBlock(masterLeft, masterRight, block)` + `bridgeWriteIndex++` + `parent_alive` check mỗi block (chống orphan) → `closeGUI` + `CoUninitialize` + `shm_close`.
- `include/SharedMemoryIPC.h`: bổ sung prototype helpers `shm_open/shm_create/shm_ptr/shm_close` (chỉ khai báo, không đổi layout — struct giữ nguyên 11168B).
- `CMakeLists.txt`: target `plugin_host` (src/plugin_host_main.cpp + SharedMemoryIPC.cpp + Vst3Instrument.cpp + SDK hosting libs). Build Release PASS.
- Verify (probe_g41.py): Nexus (`C:\Program Files\Common Files\VST3\Nexus.vst3`) load trong con OK; heartbeat tăng hb=1 sau ~1s; GUI window 4 hwnd thuộc pid con; note-on C4 (vel 100) → peak audio 0.38274 (nonzero); terminate con → `sonicforge-daw.exe` 15928 + `daw_vst_bridge.exe` 20996 vẫn sống — isolation đúng yêu cầu G4.1.
- Lưu ý test-env: probe python phải set `OpenFileMappingA.restype = MapViewOfFile.restype = c_void_p` (Win64 con trỏ 64-bit, mặc định c_int cắt → AV khi đọc view).
- G4.2 (bridge → pool) chưa bắt đầu — bước tiếp theo sau PoC PASS.
## G4.2 — Bridge → client process pool (sandbox per channel) ✅
- `include/SandboxVst3Host.h` + `src/SandboxVst3Host.cpp` (mới): `INativeInstrument` wrapper quanh plugin_host.exe. `loadPlugin(path, sr, channel)` → `shm_create("SonicForge_PluginHost_<bridgePid>_<channel>")` + `CreateProcessA` plugin_host.exe (cạnh daw_vst_bridge.exe, cùng install/) với `--open --shm ... --path ... --sr ... --block ... --parent <bridgePid>`; chờ child heartbeat ≤60s. MIDI noteOn/noteOff/controlChange/programChange/pitchBend → `shm_write_midi` (velocity float→0..127). `processAudioBlock` copy snapshot masterLeft/Right (G4.3 thay bằng ring buffer). `watchdogLoop` (thread riêng): Sleep 500ms, child chết → `alive_=false` + log + respawn sau 1s; 3 fail liên tiếp → mute vĩnh viễn đến khi reload. Destructor: stop_ + join + TerminateProcess + shm_close.
- `src/plugin_host_main.cpp`: thêm flag `--open` → `shm_open` thay vì `shm_create` (bridge tạo mapping, con mở).
- `include/SharedMemoryIPC.h`: thêm prototype `shm_write_midi` (default args data2/data3 chỉ ở header); bỏ default args trùng trong `SharedMemoryIPC.cpp` (C2572).
- `src/NativeInstrumentEngine.cpp`: `create_instrument` VST3 case — `SF_SANDBOX_VST3` env set → `SandboxVst3Host`, ngược lại `Vst3Instrument` (regression an toàn); `assign` dùng dynamic_cast: Vst3 → loadPlugin(path,sr)+setChannel, Sandbox → loadPlugin(path,sr,channel). assign() chạy trên uiWorker (ChannelWorker) — spawn blocking OK.
- `CMakeLists.txt`: thêm `src/SandboxVst3Host.cpp` vào target `daw_vst_bridge`.
- Build Release PASS (`daw_vst_bridge.exe` + `plugin_host.exe`).
- Deploy: kill app+bridge → copy exe mới vào install/ (dùng shutil, `copy` shell fail im lặng — exe cũ 3900416B không có sandbox, phải verify chuỗi `SF_SANDBOX_VST3` trong binary) → start lại app.
- Verify (probe_g42.py, bridge standalone env `SF_SANDBOX_VST3=1` + state file rỗng): LOAD Nexus ch0 → plugin_host pid spawn; child heartbeat OK; GUI 4 hwnd thuộc pid con; note-on C4 → peak 0.38276; **kill child** → bridge sống, heartbeat tiếp tục, respawn pid mới sau ~1.5s, GUI mới 4 hwnd, audio lại 0.38275 — PASS.
- Lưu ý probe: bridge chỉ OPEN mapping `SonicForge_DAW_IPC` (Rust shell tạo) → probe phải `CreateFileMappingA` trước; state file cũ (7 instruments) làm guard G2.4 skip LOAD → dùng `SF_STATE_FILE` trỏ file rỗng.
- Regression: G1.5 stress 3 iter PASS (bridge alive, writeIndex delta 286); G2.5 stress 2 iter PASS (kill bridge → watchdog respawn → restore preset exact + LOAD-skip OK) — không env SF_SANDBOX_VST3 → in-process như cũ, không đụng.
- Commit: `G4.2: bridge-side plugin process pool — SandboxVst3Host spawns/respawns plugin_host.exe per channel, MIDI forward + audio snapshot + watchdog mute/respawn, crash isolation verified`.
- Commit: `G4.1: sandbox plugin host (plugin_host.exe) PoC — Nexus runs in child process, GUI + audio via SHM, crash isolation verified`.
## G4.3 - Double-buffer sandbox SHM, latency <20ms, multi-instance no-xrun
- `include/SharedMemoryIPC.h`: struct `SandboxHostIPC` = base `SharedAudioBufferIPC` (11168B giu nguyen) + `writeSlot` @11168 + `audioLeft[2][256]` @11172 + `audioRight[2][256]` @13220; `shm_open/shm_create` nhan size param (mac dinh `sizeof(SharedAudioBufferIPC)`), them `shm_sandbox_ptr`. Header rewrite toan bo CRLF (patch anchor sai vi tri lan dau lam hong header - da fix bang rewrite_g43_header.py, build PASS sau do).
- `src/plugin_host_main.cpp`: `shm_sandbox_ptr`; render vao slot `1 - (writeSlot&1)` roi publish `writeSlot = slot` + `bridgeWriteIndex++` (double buffer: child cham khong tear, bridge doc block cu nhat da publish). `timeBeginPeriod(1)` de `Sleep(1)` that su ~1ms (khong phai 15.6ms tick mac dinh).
- `include/SandboxVst3Host.h` + `src/SandboxVst3Host.cpp`: `ipc_` kieu `SandboxHostIPC*`; `shm_create` size `sizeof(SandboxHostIPC)`; `processAudioBlock` doc `audioLeft[writeSlot&1]`. Watchdog log kem exit code (GetExitCodeProcess) de debug.
- Fix quan trong: plugin_host phai `shm_open(shmName, sizeof(SandboxHostIPC))` - mac dinh 11168B chi map duoc base, doc audioLeft vuot view -> AV crash sau LOAD (children chet lien tuc). Bug thu 2: probe `kids = [x for x in pl if x not in kids]` thay the kids bang phan tu moi - mat phan tu cu, bao FAIL "expected 2 children" du ca 2 con song. Fix union `dict.fromkeys(kids+pl)`.
- Build Release PASS (daw_vst_bridge.exe + plugin_host.exe). Deploy shutil.copy2 (kill proc giu file truoc).
- Verify (probe_g43.py, bridge standalone env `SF_SANDBOX_VST3=1` + state file rong): LOAD Nexus ch0+ch1 -> 2 plugin_host [con song song]; heartbeat ca 2 OK; note-on ch0 C4 + ch1 E4 -> main audio peak 0.71835 (tong 2 channel); writeSlot flip 64 blocks/s = latency 15.6ms < 20ms (khong yeu cau 172/s thuc - Nexus render ~10ms/block that, CPU-bound); kill child0 -> child1 van phat 0.36972, bridge song + heartbeat + child0 respawn - PASS.
- Regression: G1.5 stress 3 iter PASS (writeIndex delta 286); G2.5 stress 2 iter PASS (kill bridge -> respawn -> preset exact + LOAD-skip OK). App deploy (install/sonicforge-daw.exe) dang chay, bridge pid moi.
- FIX idle-skip (G4.3.1): plugin_host_main.cpp them `activeNotes` dem (note-on ++, note-off --, clamp >= 0)
+ `idle` flag. Khi khong con note dang giu va peak block < 0.0005f -> idle = true,
loop Sleep(15) bo qua render (van PeekMessage + parent-alive check). Bo `Sleep(1)`
hot loop -> `Sleep(0)` yield. KET QUA: writeSlot flip 0 blocks/s khi im lang (truoc day
render lien tuc ~100/s, CPU cao); khi dang giu note flip 101.6 blocks/s = latency 9.8ms
(truoc 64/s = 15.6ms) - ca 2 loi G4.3 xac nhan da het.
- Verify lai: probe_g43.py PASS (2 children song song, peak 0.67273, kill child0 -> child1
0.36972, isolation OK); dbg_rate.py idle 0 flips/3s; G1.5 3 iter PASS (delta 287);
G2.5 2 iter PASS. Deploy install/plugin_host.exe moi. App dang chay pid 21396.
+305
View File
@@ -0,0 +1,305 @@
# 20260815 — KẾ HOẠCH THỰC HIỆN G2 (PERSISTENCE RESTART)
Nguồn: `big_tasks.md` — header **G2 — Persistence restart (2-3 tuần)**.
Trạng thái roadmap: G0 ✅, G1 ✅ (G1.1–G1.5), **G2 ✅ (G2.0–G2.6)**, G3 ✅ (G3.1–G3.3), G4 ✅ (G4.1–G4.3 sandbox PoC). Branch: `standalone-shm-bridge`. File này là plan G2; G3/G4 thực hiện tiếp nối (nhật ký ở mục 5).
---
## 1. PHÂN TÍCH G2
### 1.1 Mục tiêu
Restart không mất instrument — sau khi bridge respawn (stall 3s / crash / restart sample-rate / DAW mở lại),
người dùng bấm PLAY nghe lại ngay, preset + vị trí transport được khôi phục tự động.
### 1.2 Hiện trạng (đã verify trong code)
| Thành phần | File | Hiện trạng liên quan G2 |
|---|---|---|
| Bridge C++ (daw_vst_bridge) | `native_bridge/src/main.cpp` (1250 dòng) | Control loop: LOAD type=2, TRANSPORT type=3 (0=STOP, 1=PLAY, 2=SET_POSITION), OPEN_GUI type=4. `playheadSamples` lưu trong audio loop. Watchdog thread (parent chết → TerminateProcess). |
| Quản lý instrument | `native_bridge/src/NativeInstrumentEngine.cpp/.h` | `InstrumentEngineManager`: map channel→instance + `paths_` (path/channel). `FluidSynthInstrument::bank_[16]` giữ bank mỗi channel. |
| VST3 preset | `native_bridge/src/Vst3Instrument.cpp` (G0.4) | `captureState()`/`restoreState()` CHỈ trong RAM (`savedComponentState_`/`savedControllerState_` buffers) — mất khi process chết. |
| Rust shell | `src-tauri/src/lib.rs` | `spawn_bridge`, pump thread (stall 3s → kill + restart 1 lần → `bridge-down`), `kill_bridge`, `restart_bridge_with_sample_rate`, `transport_control`, `load_native_instrument`, `bridge_status`. `SHUTTING_DOWN` khi window đóng → kill bridge KHÔNG dump. |
| JS client | `app/static/js/services/nativeBridgeService.js` | `loadInstrument`, `transport(kind, playhead)`, `onAudio`, `onStatusChange`, `queryStatus`. |
| JS app | `app/static/js/app.precompiled.js` | `loadVstToBridge`/`loadToBridge` (LOAD), `__bridgeLoadedChannels` (reset `{}` khi restart → mất), `updatePlayhead` gửi `set_position` ~1/s (JS là nguồn playhead duy nhất), bootstrap gọi `restart_bridge_with_sample_rate` ở startup. |
### 1.3 Khoảng trống (gap)
1. **Không có gì ghi ra đĩa** — G0.4 preset chỉ sống trong RAM của bridge; respawn = mất hết channel/preset/transport.
2. **Kill không có cơ hội dump** — pump thấy stall → `kill_bridge` ngay; window đóng → `kill_process_tree`; cả hai đều không báo bridge ghi state trước.
3. **Respawn không tự load** — bridge mới start với SHM trống; JS `__bridgeLoadedChannels={}` và chỉ load lại khi track phát/đổi (không tự động, không khôi phục preset).
4. **Guard chặn restore khi PLAY** — G2.4: luồng restore phải chạy được khi `isPlayingRef.current === true` (hiện guard `if(window.__bridgePlaying) return` trong `openVstGuiRetry`, và `!isPlayingRef.current` quanh `openNativeGUI` dòng ~817 — áp dụng cho restore cũng phải gỡ).
### 1.4 Nguyên tắc thiết kế (đề xuất)
- **Write-on-change là chính**: bridge tự ghi state ra đĩa NGAY khi state đổi (LOAD/TRANSPORT/params) — an toàn cả khi crash đột ngột, không phụ thuộc cơ hội dump trước kill. Dump-before-kill chỉ là lớp phụ (khi có thời gian: restart sample-rate, window close).
- **Bridge tự restore khi start**: bridge đọc file state khi khởi động → tự push LOAD các channel + restore preset + set transport — Rust không cần biết preset blob (giữ IPC đơn giản).
- **File state dùng chung**: `%APPDATA%/SonicForgeDAW/state/bridge_state.json` (cùng cây logs/ipc — đã có sẵn layout).
- **Không đụng audio loop**: mọi IO đĩa chạy trên UiThread (worker job queue) hoặc thread riêng; tuyệt đối không trong vòng render.
---
## 2. PHÂN RÃ TASKS + ESTIMATE
| ID | Task | File đích | Nội dung | Estimate |
|---|---|---|---|---|
| G2.0 | Format + module state file | `native_bridge/src/StateStore.h/.cpp` (mới) + `CMakeLists.txt` | JSON (path/type/channel/preset-base64/transport) + atomic write (write tmp → rename). API: `load()`, `save(StateSnapshot)`, `clear()`. | 1 ngày |
| G2.1 | Save đầy đủ state ra đĩa | `main.cpp`, `Vst3Instrument.cpp`, `NativeInstrumentEngine.cpp/.h` | Snapshot toàn bộ channel: path, type, VST3 component+controller state (từ `captureState` → base64), SF2 bank/program, transport `playheadSamples`. Ghi khi: LOAD xong, TRANSPORT đổi, params đổi (định kỳ 1-2s nếu dirty). | 2 ngày |
| G2.2 | Dump state trước kill | `src-tauri/src/lib.rs`, `main.cpp` | (a) Write-on-change phủ hầu hết. (b) Trước `kill_bridge`/restart: Rust push control type=5 (DUMP) + chờ ≤300ms → kill. (c) Window close: gửi DUMP trước `kill_process_tree`. | 1.5 ngày |
| G2.3 | Auto-reload sau respawn | `main.cpp` (startup), `lib.rs` (env `SF_STATE_FILE`) | Bridge start: đọc state file (nếu có) → `uiWorker.post` LOAD từng channel theo thứ tự → restore preset → nếu từng PLAY thì set transport + PLAY. Guard: chỉ restore khi channel trống; log vào bridge.log. | 2 ngày |
| G2.4 | Gỡ guard chặn restore khi PLAY | `app/static/js/app.precompiled.js` | Trong luồng restore, bỏ điều kiện dừng vì `isPlayingRef.current`/`__bridgePlaying`: LOAD instrument + restore preset khi đang PLAY vẫn phải chạy (khác với open GUI — G0.2 vẫn giữ defer cho OPEN_GUI). | 0.5 ngày |
| G2.5 | Regression driver stress | `native_bridge/debug/test_g2_stress.py` (mới, theo mẫu `test_g15_stress.py`) | N vòng: LOAD 2 inst (Nexus+EZkeys) → PLAY → kill bridge → respawn → verify writeIndex advancing + preset giữ + 0 crash. | 1 ngày |
| G2.6 | Verify + bắt bugs + fix | — | Chạy selfcheck/stress/probe trên Windows; thu logs; sửa bug tìm được. | 2-4 ngày |
| | **Tổng** | | | **10-12 ngày (~2-3 tuần — khớp roadmap)** |
### Phụ thuộc
`G2.1 → G2.0` · `G2.2 → G2.1` (dùng StateStore) · `G2.3 → G2.1 + G2.2` · `G2.5 → G2.3 + G2.4` · `G2.6 → tất cả`.
### Thứ tự triển khai khuyến nghị
G2.0 → G2.1 → G2.3 (lõi: save + tự restore) → G2.4 (guard) → G2.2 (dump trước kill — lớp phụ) → G2.5 → G2.6.
Lý do: G2.2 phụ thuộc G2.1 nhưng giá trị thấp hơn G2.3; làm G2.3 sớm để stress sớm.
---
## 3. KẾ HOẠCH THỰC HIỆN (CÀI ĐẶT → KIỂM TRA → BẮT BUGS → FIX → REPORT)
> **PHÂN CÔNG MÁY (bắt buộc):**
> - **Máy Linux (máy này):** CHỈ lập kế hoạch, review code, ghi chú, theo dõi tiến độ, cập nhật file báo cáo.
> KHÔNG sửa code C++/Rust/JS, không build bridge (thiếu vcpkg/fluidsynth/sfizz/VST3 SDK/rustc-MSVC).
> Verify tối đa tại đây: `node --check` (JS), `ast.parse` (py), review, `shm_selfcheck` (đã pass Linux trước đây).
> - **Máy Windows (máy chạy app thật):** SỬA CODE, build (`build_bridge.ps1`), deploy (`deploy_g12.ps1`),
> chạy probe/stress, bắt bugs, fix, commit + push. Mọi thay đổi code G2 đều phát sinh ở đây.
> - Luồng: **Windows sửa code + commit + push** → **Linux pull, review, cập nhật ghi chú/report + push** →
> Windows pull lại, verify tiếp.
>
> Sau MỖI task: mục "📌 Lưu ý & quy trình" ghi rõ việc nào làm ở máy nào.
### G2.0 — StateStore
- **Cài đặt**: thêm `StateStore.h/.cpp`; JSON đơn giản (viết tay serializer — tránh thêm dep nlohmann vào bridge, dự án không có sẵn). Atomic write: `bridge_state.json.tmp` → `rename`.
- **Kiểm tra**: unit selfcheck — compile `StateStore` riêng (Linux: `g++ -fsyntax-only` nếu muốn xem syntax; Windows: compile thật trong target + test nhỏ ghi/đọc JSON round-trip).
- **Bắt bugs**: escape chuỗi path (dấu `"` `\` trong path Windows), rename fail khi file khoá, UTF-8.
- **Fix + report**: ghi kết quả vào mục 5.
- **📌 Lưu ý & quy trình**:
- **Windows** (sửa code): tạo `native_bridge/src/StateStore.h/.cpp`; thêm 2 file vào `native_bridge/CMakeLists.txt` target `daw_vst_bridge`; nhớ CRLF (ghi file qua Python `io.open(newline='')` hoặc editor giữ CRLF — cả repo native_bridge là CRLF). Build: `build_bridge.ps1`. Test: chạy exe/test nhỏ in JSON ra, verify round-trip. Commit: `G2.0: StateStore (JSON state file + atomic write)`.
- **Linux** (ghi chú): sau khi Windows push, pull + review file mới + cập nhật bảng mục 5 + dòng nhật ký. Không sửa code.
### G2.1 — Save đầy đủ
- **Cài đặt**: thêm `InstrumentEngineManager::snapshot()` (dưới `mu_`) trả về danh sách channel → path/type/state-blob; `Vst3Instrument::serializeState()` tái dùng `captureState()` buffers → base64; `FluidSynthInstrument` expose bank/program; hook ghi vào các điểm LOAD xong / TRANSPORT / timer dirty.
- **Kiểm tra**: `shm_selfcheck` không đổi (layout SHM giữ nguyên — không thêm field vào SHM); review không đụng audio loop; trên Windows: probe LOAD → đọc `bridge_state.json` thấy đủ path/preset.
- **Bắt bugs**: base64 không khớp byte (size/precision), ghi giữa lúc instance đang reload (phải chờ `reloadingCh_` off), path empty.
- **Fix + report**: mục 5.
- **📌 Lưu ý & quy trình**:
- **Windows** (sửa code): sửa `NativeInstrumentEngine.cpp/.h` (thêm `snapshot()`), `Vst3Instrument.cpp` (serializeState), `main.cpp` (hook ghi sau LOAD/TRANSPORT + timer dirty). Build + deploy + probe LOAD → mở `%APPDATA%\SonicForgeDAW\state\bridge_state.json` kiểm tra path/preset. Commit: `G2.1: save full instrument state to disk (path/type/preset/transport)`.
- **Linux** (ghi chú): pull + review: snapshot phải nằm dưới `mu_`, không gọi IO trong audio loop; `shm_selfcheck` vẫn pass. Cập nhật mục 5 + nhật ký.
### G2.2 — Dump trước kill
- **Cài đặt**: thêm control type=5 (DUMP_STATE) trong enum + handler main.cpp (push snapshot qua StateStore); Rust `kill_bridge`/window-close gửi control 5 + sleep 300ms trước kill; `restart_bridge_with_sample_rate` làm tương tự.
- **Kiểm tra**: review Rust+C++ control type khớp; Linux `node --check` không liên quan; Windows: kill bridge giữa PLAY → file state phải đủ (write-on-change đã đảm bảo, DUMP là phụ).
- **Bắt bugs**: race Rust gửi DUMP sau khi bridge chết (guard shm unavailable), 300ms không đủ khi VST đang createView (attach_in_flight → chờ theo flag SHM offset 11160).
- **Fix + report**: mục 5.
- **📌 Lưu ý & quy trình**:
- **Windows** (sửa code): sửa `native_bridge/src/main.cpp` (case 5), `src-tauri/src/lib.rs` (`kill_bridge`, `restart_bridge_with_sample_rate`, window-close handler). Build bridge + `cargo check` (src-tauri). Deploy → kill bridge giữa PLAY → kiểm tra state file đủ. Commit: `G2.2: dump state before kill (control type=5 + Rust DUMP before kill_bridge)`.
- **Linux** (ghi chú): pull + review: type 5 không đụng enum cũ (2/3/4 giữ nguyên), Rust guard khi `ShmState` None; `cargo check` bắt buộc chạy trên Windows (Linux thiếu toolchain). Cập nhật mục 5 + nhật ký.
### G2.3 — Auto-reload sau respawn
- **Cài đặt**: bridge startup (sau khi map SHM, trước audio loop): đọc `SF_STATE_FILE` (Rust set env khi spawn) → `uiWorker.post` từng channel: LOAD (type 2) → `restoreState()` → cuối cùng nếu state có `playing=true` → TRANSPORT play + set_position. Log từng bước vào bridge.log.
- **Kiểm tra**: Linux review + `shm_selfcheck`; Windows: kill bridge khi đang PLAY → chờ respawn → verify writeIndex advancing + preset nghe lại đúng.
- **Bắt bugs**: restore chạy trước khi Rust pump gắn `bridge-audio` (JS `_reconnectIfNeeded` vẫn cần); load nhiều channel cùng path (g_loadMutex + reloadingCh phải serialize); double-load khi JS cũng tự load (JS phải bỏ `__bridgeLoadedChannels` reset → giữ state thật).
- **Fix + report**: mục 5.
- **📌 Lưu ý & quy trình**:
- **Windows** (sửa code): sửa `main.cpp` (startup restore block), `lib.rs` (set env `SF_STATE_FILE` khi spawn). Build + deploy + kill bridge giữa PLAY → chờ respawn (spawn.log) → verify writeIndex + preset. Nếu cần phía JS: sửa `app.precompiled.js` + `node --check`. Commit: `G2.3: auto-reload instruments from state file after bridge respawn`.
- **Linux** (ghi chú): pull + review: restore phải post qua `uiWorker` (không chạy trong audio loop), serialize bằng `g_loadMutex` sẵn có; xác nhận `__bridgeLoadedChannels` flow trên JS. Cập nhật mục 5 + nhật ký.
### G2.4 — Gỡ guard chặn restore khi PLAY
- **Cài đặt**: trong `app.precompiled.js` luồng auto-restore (điểm G2.3 phía JS): cho phép `loadVstToBridge`/restore chạy khi PLAY; GIỮ NGUYÊN guard `__bridgePlaying` cho riêng OPEN_GUI (G0.2 — defer GUI vẫn đúng).
- **Kiểm tra**: `node --check app.precompiled.js`; Windows: PLAY đang chạy → kill/respawn → verify restore xong trong khi PLAY, không mất tiếng.
- **Bắt bugs**: restore gọi `openVstGuiRetry` nhầm khi PLAY (guard cũ trả `true` sớm → GUI không mở nhưng LOAD cũng bị bỏ) — tách 2 guard riêng.
- **Fix + report**: mục 5.
- **📌 Lưu ý & quy trình**:
- **Windows** (sửa code): sửa `app/static/js/app.precompiled.js` (tách guard restore vs guard OPEN_GUI); chạy `node --check app/static/js/app.precompiled.js`. Deploy app (build JS bundle → copy vào install) → PLAY → kill/respawn → verify. Commit: `G2.4: allow instrument restore while PLAY (keep OPEN_GUI defer)`.
- **Linux** (ghi chú): pull + review: xác nhận G0.2 guard `__bridgePlaying` vẫn còn cho OPEN_GUI; `node --check` cũng chạy được tại đây nếu có node. Cập nhật mục 5 + nhật ký.
### G2.5 — Stress driver
- **Cài đặt**: `test_g2_stress.py` theo mẫu `test_g15_stress.py`: 5 + 10 iterations LOAD/OPEN_GUI/PLAY/STOP + kill bridge giữa chừng → verify respawn + writeIndex + preset.
- **Kiểm tra**: chạy trên Windows; ghi PASS/FAIL theo iteration.
- **Bắt bugs**: crash/restart khi kill giữa attach, preset sai sau lần restore thứ N.
- **Fix + report**: mục 5.
- **📌 Lưu ý & quy trình**:
- **Windows** (tạo script + chạy): tạo `native_bridge/debug/test_g2_stress.py` (chép khung `test_g15_stress.py`: ctypes SHM, `write_control`, `wait_log`, `bridge_pid`, `bad_since`); thêm bước kill `daw_vst_bridge` giữa vòng và chờ respawn. Chạy: `python native_bridge\debug\test_g2_stress.py <APP_PID> 5` rồi `10`. Thu kết quả PASS/FAIL + logs. Commit: `G2.5: stress driver for persistence restart (N loop + kill/respawn)`.
- **Linux** (ghi chú): pull + review script (không chạy được — cần SHM Windows); cập nhật mục 5 với kết quả PASS/FAIL do Windows báo.
### G2.6 — Verify tổng + bắt bugs + fix
- Chạy lại toàn bộ regression G1.5 (không hỏng G1) + G2.5 + probe G0.4.
- Thu `bridge.log`/`spawn.log` thực (không tin probe PASS đơn lẻ — learning cũ).
- Sửa mọi bug tìm được, commit theo từng task (như G1.2–G1.5).
- **📌 Lưu ý & quy trình**:
- **Windows** (chạy + fix): chạy `test_g15_stress.py` (regression G1) + `test_g2_stress.py` (G2) + probe G0.4; fix bug tìm được, commit từng cái.
- **Linux** (báo cáo tổng): sau khi Windows báo kết quả cuối, tổng hợp toàn bộ vào mục 5 + nhật ký, đánh dấu hoàn thành, commit báo cáo cuối: `G2: report — persistence restart done (verdict từ Windows)`. Push.
---
## 4. RỦI RO
| Rủi ro | Ảnh hưởng | Giảm thiểu |
|---|---|---|
| VST3 state blob không restore được (Nexus setState fail) | Mất preset dù load lại được path | Fallback: giữ path + chỉ restore blob khi OK; log `setState failed`; không chặn load. |
| Ghi file mỗi LOAD → IO chậm | Stall audio? | Ghi trên UiThread job (không trong audio loop); atomic rename. |
| JS `__bridgeLoadedChannels` stale | Double-load / bỏ load | Reset map theo sự kiện bridge respawn thật (bridge-audio trở lại) thay vì chỉ restart-bootstrap. |
| Restore khi đang PLAY (G2.4) gây click/glitch | Chất lượng âm | LOAD đã có reloadingCh_ mute cơ chế sẵn (G1.4) — tái dùng; restore nhanh, không chờ. |
| Không build được trên Linux | Không verify cục bộ | Verify Linux: syntax/selfcheck/review; chạy thật trên Windows theo quy trình G1. |
---
## 5. BÁO CÁO KẾT QUẢ (điền sau khi thực hiện)
> Cột Cài đặt/Kiểm tra/Bugs/Fix: **Windows** điền (người sửa code). Cột Trạng thái + Nhật ký: **Linux** cập nhật sau khi review.
| ID | Cài đặt | Kiểm tra | Bugs bắt được | Fix | Trạng thái |
|---|---|---|---|---|---|
| G2.0 | StateStore.h/.cpp (JSON thủ công, atomic tmp+rename, UTF-8, API save/load/clear, defaultPath=%APPDATA%/SonicForgeDAW/state/bridge_state.json); them vao CMakeLists target daw_vst_bridge; test_state_store.cpp (round-trip self-check) | Build PASS (daw_vst_bridge.exe), round-trip PASS (save/load 2 inst: path Windows co space+backslash, presetBase64 co newline/quote/backslash; khong con .tmp; clear xoa file) | — (module moi, chua hook vao bridge) | — | ✅ (Win xong + Linux review OK) |
| G2.1 | main.cpp: globals g_stateDirty/g_transportPlaying/g_lastSnapMutex/g_lastSnap, snapshotsEqual (bo playhead); stateFile = SF_STATE_FILE fallback defaultPath; playheadSamples -> std::atomic<uint64_t>; saveStateJob (UiThread, skip khi isReloading, chi save khi snapshot doi); TRANSPORT set g_transportPlaying + dirty; cuoi LOAD dirty=true; periodic 2s dirty + debounce 300ms. Vst3Instrument.h: khai bao bankOf/programOf. StateStore.cpp: ensureParentDir() (CreateDirectoryA chain / mkdir) goi dau save(). | Build PASS (daw_vst_bridge.exe). Deploy PASS (PID moi: sonicforge-daw 9876, daw_engine 24632, daw_vst_bridge 7352). Verify live PASS: LOAD Nexus ch5 + PLAY -> bridge_state.json co 7 instrument (ch0-6 frontend da load), playing=true, presetBase64 non-empty (Nexus 3468/3488, EZkeys 24764); STOP -> playing=false. State round-trip test_state_store.cpp PASS. | test script ban dau ky vong dung 1 instrument (frontend da load 7) -> FAIL gia, chuc nang PASS | assert dung so instrument | ✅ (Win xong + Linux review OK) |
| G2.2 | main.cpp: control type=5 DUMP_STATE (post saveStateJob len uiWorker ngay lap tuc). lib.rs: flush_bridge_state() — push_control(5,0,0,0) + sleep 300ms, goi dau kill_bridge() (cover restart_bridge_with_sample_rate + stall-restart) va truoc khi giet bridge trong window-close Destroyed | Build PASS (daw_vst_bridge.exe) + cargo check PASS (src-tauri) + deploy + verify live PASS: DUMP qua SHM -> log "DUMP_STATE - flushing state now", state file intact (7 inst), writeIndex advancing | Runtime Rust flush chua test duoc khi khong build app Tauri (cargo check xac nhan compile) | | ✅ (Linux review OK) |
| G2.3 | main.cpp: startup restore block (doc SF_STATE_FILE, guard channel trong, post LOAD job tung channel qua uiWorker, assign + loadSerializedState preset / selectProgram bank-program, g_loadMutex serialize, restore transport PLAY + playhead neu state playing). Vst3Instrument: base64Decode + loadSerializedState (parse [4B BE compLen][comp][4B BE ctrlLen][ctrl], goi restoreState). INativeInstrument.h: virtual loadSerializedState no-op. lib.rs: spawn_bridge set env SF_STATE_FILE = %APPDATA%/SonicForgeDAW/state/bridge_state.json. | Build PASS (bridge + cargo check). Deploy PASS. Verify live PASS: LOAD ch5 + PLAY (playhead 5000) -> kill bridge -> respawn -> log "restoring 7 instruments" + "restored ch=0..6" (Nexus + EZkeys) + "restoring transport PLAY playhead=5000" + ch5 preset 3488B giu nguyen; state file playing=true playhead=5000 inst=7; writeIndex advancing (14859->15234). | - | - | ✅ (Win xong + Linux review OK) |
| G2.4 | main.cpp: LOAD case double-load guard — skip khi channel da co cung path (lower_plugin_path(ch) == lower_path_str(path)), giu instance + preset da restore khi JS reload sau respawn/bootstrap | Verify live PASS: LOAD lai Nexus ch5 sau respawn -> log "LOAD skip ch=5 (already loaded)", preset ch5 3488B giu nguyen, writeIndex advancing; 15 LOAD skip trong log (JS reload 7ch x2 + test) | Khong can sua JS: loadVstToBridge/loadTrackInstrumentToBridge khong co guard __bridgePlaying (da chay khi PLAY); chi openVstGuiRetry giu guard OPEN_GUI (G0.2, dung) | | ✅ (Linux review OK) |
| G2.5 | native_bridge/debug/test_g2_stress.py (mới, khung test_g15_stress.py: ctypes SHM, write_control, wait_log, bridge_pid, bad_since) + bước kill daw_vst_bridge giữa vòng + chờ watchdog respawn; verify restore + preset giữ + LOAD-skip guard (G2.4) + writeIndex advancing; test_g15_stress.py: LOAD ch5 chấp nhận "LOAD skip" (guard G2.4 đổi log) | Chạy 5 + 10 iterations PASS (tổng 20 lần kill/respawn): LOAD skip pre-kill/post-respawn OK, restore ch5/ch6 OK, watchdog "[tauri] bridge restarted" OK, bridge alive; preset Nexus giữ nguyên (exact), EZkeys sim>=0.87; writeIndex advancing (delta 283-376) | Assert preset ch6 byte-exact FAIL giả: EZkeys nhúng PlayHeadPos vào state riêng (đổi cả giữa 2 save thường, ratio 0.872) | ch5 so exact, ch6 so ratio>=0.7 + |len chênh|<=64 | ✅ (Win xong + Linux review OK) |
| G2.6 | Chạy lại regression G1.5 (test_g15_stress.py) + G2.5 (5+10 iter) + probe G0.4 (bản cập nhật PID + guard-aware, OPEN_GUI full path) | G1.5 regression PASS 5/5 (LOAD/OPEN_GUI/PLAY/STOP/WM_CLOSE, writeIndex delta 307-551, bridge alive); G2.5 PASS 5+5+10 (20 lần kill/respawn, preset giữ, không crash); G0.4 probe PASS: LOAD skip -> OPEN_GUI hwnd -> WM_CLOSE -> capture 2628B -> reload -> restore 2628B -> OPEN_GUI lại attach OK, không restart/EXCEPTION/FAILED | Không có bug sản phẩm mới; 2 bug test-script: (1) probe G0.4 so sánh PID int vs list -> báo restart giả, (2) preset EZkeys byte-diff do PlayHeadPos runtime (đã phân tích ở G2.5) | Sửa test scripts (guard-aware wait, pid compare); không sửa code sản phẩm | ✅ (Win xong + Linux review OK) |
### 5.1 Review + verify phía Linux (2026-08-16, Linux — chỉ review/ghi chú, KHÔNG sửa code)
Kết quả review toàn bộ diff G2 (`git diff 1fe2d26~1..b59e416`, 15 files; không có file JS trong diff).
**Code review — nhất trí với báo cáo Windows, không tìm thấy bug sản phẩm:**
- `Vst3Instrument.cpp`: `base64Encode`/`base64Decode` đúng RFC 4648 (padding `=`, decode bỏ qua whitespace); `loadSerializedState` có bounds-check cả componentLen lẫn controllerLen trước khi copy — blob hỏng không gây OOB; gán buffer xong mới gọi `restoreState()` (đúng thứ tự).
- `NativeInstrumentEngine.cpp`: `snapshot()` giữ `mu_` suốt `captureState()` — audio loop cũng serialize bằng `mu_` per block nên không race capture với process; skip channel đang reload (`reloadingCh_`, instance cũ sắp chết); `bank_/program_` cập nhật đúng (reset khi `loadSoundFontFile`, set khi program_select/programChange).
- `main.cpp`: `saveStateJob` defer khi có channel reloading (đặt lại dirty → retry sau debounce) — không ghi state lúc instance nửa chết; `snapshotsEqual` loại playhead (không ghi file mỗi ~2s khi PLAY); periodic mark 2s + debounce 300ms; startup restore chỉ nhận channel trống (`instruments.get(ch)==nullptr`) và serialize bằng `g_loadMutex`; LOAD guard so sánh lower-path — chỉ skip khi CÙNG path, path khác vẫn replace bình thường; case 5 DUMP_STATE post `saveStateJob` trực tiếp (không chờ debounce).
- `lib.rs`: `flush_bridge_state` an toàn khi bridge treo (SHM do Rust sở hữu, control 5 không được consume — vô hại).
- `test_g2_stress.py` + `test_state_store.cpp` + sửa `test_g15_stress.py`: logic vững (wait_load chấp nhận real hoặc skip; SequenceMatcher ≥0.7 cho EZkeys vì PlayHeadPos runtime; bad_since watch EXCEPTION/FAILED; round-trip cover escape `\n \t \" \\`, atomic .tmp, missing file, clear).
**Findings (minor, không chặn; Linux không sửa code — chờ Windows quyết định):**
1. **F1 (minor)**: window-close gọi `flush_bridge_state()` ở `run()` VÀ `kill_bridge()` giờ cũng tự gọi → DUMP_STATE 2 lần khi đóng app (+~300ms shutdown). Harmless; có thể bỏ lời gọi thừa trong `run()`.
2. **F2 (portability note)**: `lib.rs` luôn set `SF_STATE_FILE` sang đường dẫn Windows; nếu chạy bridge non-Windows, env thành `./SonicForgeDAW/state/bridge_state.json` (theo CWD) thay vì Linux fallback `$HOME/.sonicforge/state/` của `StateStore::defaultPath`. Không ảnh hưởng target Windows (cùng path APPDATA); ghi nhận để sửa khi cần bridge chạy Linux.
3. **F3 (cosmetic)**: `loadSerializedState` in cùng message "bad ctrlLen" cho 2 check khác nhau (thiếu header / độ dài vượt) — chỉ log, không ảnh hưởng hành vi.
**Verify trên Linux (cú pháp/logic — máy Linux không build Windows):**
- `python3 -m py_compile native_bridge/debug/test_g2_stress.py`, `test_g15_stress.py`: PASS.
- `g++ -std=c++17 -Wall -Wextra debug/test_state_store.cpp + src/StateStore.cpp` (Linux): compile sạch (1 warning benign multi-line comment) + run **"state_store round-trip PASS"** → StateStore portable, atomic write + escape verified trên Linux.
- `node --check app/static/js/app.precompiled.js`: PASS (G2 không sửa JS — diff stat không có file JS).
- Rust: `rustc`/`cargo` chỉ là rustup proxy, chưa cài default toolchain → không chạy được `cargo check` trên Linux (khớp ghi chú big_tasks.md "máy Linux không có rustc toolchain"); Windows đã `cargo check` PASS ở G2.2/G2.3.
**Kết luận Linux**: code G2 nhất quán với báo cáo Windows; không có bug sản phẩm; 2 findings minor (F1, F2) + 1 cosmetic (F3) không chặn merge. Trạng thái G2.0–G2.6: ✅ (Win xong + Linux review xác nhận).
### 5.2 Audit bảo mật / bộ nhớ / conflict child process (2026-08-16, Linux — chỉ review/ghi chú, KHÔNG sửa code)
Đọc toàn bộ: `native_bridge/src/main.cpp` (1410), `NativeInstrumentEngine.cpp` (439), `Vst3Instrument.cpp` (873), `StateStore.cpp` (288), `SandboxVst3Host.cpp` (192), `plugin_host_main.cpp` (163), `SharedMemoryIPC.cpp` (125), `src-tauri/src/shm.rs` (201), `src-tauri/src/lib.rs` (791), `app/static/js/services/nativeBridgeService.js` (180), `app/static/js/app.precompiled.js` (luồng bridge). Mục tiêu: race UAF / double-buffer / conflict 2 bridge cùng SHM / orphan / command injection / luồng có đúng thiết kế.
**KẾT LUẬN: không có bug chặn release; 2 MEDIUM (F-MEM-5 UAF, F-PROC-1 injection), 1 MEDIUM (F-PROC-2 race restart×watchdog), 1 MEDIUM audio-correctness (F-MEM-8), 2 LOW (F-PROC-3, F5). Tất cả nằm ở đường hiếm (deschedule >10ms, path thù địch, restart trùng stall) — đề xuất Windows xử lý theo ưu tiên F-PROC-1 > F-MEM-5 > F-PROC-2.**
**F-MEM-5 (MEDIUM — UAF trên audio thread):** `InstrumentEngineManager::get()` (NativeInstrumentEngine.cpp:300) lock `mu_` rồi trả raw `unique_ptr::get()` sau unlock. `assign()`/`unload()` (dòng 182/240) swap map DƯỚI `mu_` nhưng destroy `oldInst` NGOÀI lock, sau `sleep 10ms`. Drain loop control queue chạy trên **audio thread** (main.cpp:1115) gọi `instruments.get(ch)` rồi deref sau unlock: handleOpenGui dedupe `i0->hasAttachedView()` (main.cpp:794), pending-GUI sweep (884/909/927/953), STOP transport `inst->controlChange(ch,64,0)` (main.cpp:1290). Nếu UiThread assign xong + ngủ 10ms + reset oldInst trong lúc audio thread bị deschedule giữa get() và deref → deref instance đã free → UAF (cửa sổ ~10ms+). Các site trong attach job (823) an toàn vì cùng UiThread với assign. Hướng fix: giữ `mu_` quanh get+use như MIDI dispatch (đã làm đúng ở dòng 338), hoặc get() trả shared_ptr.
**F-MEM-8 (MEDIUM — audio correctness):** double-buffer G4.3 không handshake (SharedMemoryIPC.h): bridge đọc `writeSlot` + `audioLeft/Right` trong lúc child flip slot → block rách/glitch (không memory corruption — mảng cố định 256). `processAudioBlock` đọc con trỏ `ipc_` non-atomic trong lúc watchdog respawn child (UB lý thuyết, x86 OK). Ghi nhận theo dõi.
**F-PROC-1 (MEDIUM — command injection):** `SandboxVst3Host::spawnChild()` (SandboxVst3Host.cpp:76-78) nội suy `--path "..."` + `--sr` vào cmdline, gọi `CreateProcessA(nullptr, buf)` — path lấy từ LOAD control / state file. Path chứa `"` + metachar (VD `" && calc`) → thực thi lệnh tuỳ ý (cùng user). Fix: `lpApplicationName` + lpCommandLine arg đã quote đúng, hoặc truyền path qua env/SHM. Severity giữ MEDIUM vì attacker cần kiểm soát path (file .vst3 thù địch / state file sửa tay).
**F-PROC-2 (MEDIUM — race restart × watchdog → 2 bridge cùng SHM):** `restart_bridge_with_sample_rate` (invoke thread) và pump watchdog (thread riêng) đều chạy `kill_bridge()` → `spawn_bridge()`. `kill_bridge` serialize bằng `BridgeProcess` mutex nhưng **kill→spawn không nguyên tử**: 2 luồng cùng đến `spawn_bridge` → 2 `daw_vst_bridge.exe` cùng map SHM (tên `SonicForge_DAW_IPC_<pid>` — CÙNG tên) → double LOAD / OPEN_GUI trùng / control queue rối. `set_bridge_child` overwrite child cũ → rò rỉ bridge đầu (không kill, log redirect task sống theo pipe). Khả năng overlap cao hơn vì cả 2 luồng đều ngủ 300ms trong `flush_bridge_state`. Kích hoạt thực tế: bootstrap JS gọi restart (mỗi lần webview load) trùng lúc bridge mới không kịp heartbeat → watchdog stall 3s. Fix: 1 mutex phủ cả kill+spawn, hoặc spawn_bridge kiểm tra "không còn child tracked" trước khi spawn.
**F-PROC-3 (LOW — orphan race khi đóng app):** pump thread check `SHUTTING_DOWN` ở đầu vòng, nhưng giữa `kill_bridge` và `spawn_bridge` trong cùng 1 vòng không check lại → Destroyed lấy `None` (pump vừa kill), pump spawn bridge sau khi app đang tắt → orphan tạm thời. Child có `--parent` monitor nên tự thoát khi Tauri exit → transient, không nghiêm trọng. Fix: check SHUTTING_DOWN lần nữa trước spawn_bridge.
**F5 (LOW — observability):** `open_vst_gui` command (lib.rs:626) luôn trả `Ok(())` kể cả khi SHM unavailable / push fail — lỗi chỉ `eprintln` vào stderr Tauri (không vào bridge.log) → JS không biết thất bại, retry vô ích. Nên trả Err để JS dừng sớm.
**F6 (LOW — comment lệch code):** `openVstGuiRetry` cap thực tế `n=min(attempts,2)` (2 lần) trong khi comment ghi "Cap 5" (bản cũ ghi 20). Không lỗi hành vi, chỉ lệch comment.
**Luồng xác nhận ĐÚNG thiết kế:**
- Control queue drain TRƯỚC audio render trong cùng loop (main.cpp:1115-1315) — control không bị đói.
- LOAD job post lên UiThread, serialize `g_loadMutex`; audio thread không stall (comment G3.1) — writeIndex stall 3s chỉ khi heartbeat thread kẹt thật.
- OPEN_GUI: defer khi PLAY (G0.1) + pending sweep khi `transportStopped && instruments.get(ch)`; dedupe `hasAttachedView` chống spam retry; attach job trên UiThread (COM STA) + g_loadMutex + AttachInFlightGuard + same-path silence + destroy child window trên owner thread (0xc000041d fix).
- STOP transport: CC64=0 release sustain TRƯỚC allNotesOff (V8 bug 3) — đúng.
- DUMP_STATE (type=5) post saveStateJob trực tiếp, không chờ debounce; kill_bridge flush 300ms; Rust pump drop guard trước kill (chống deadlock G3.1).
- G2.4 LOAD-skip guard (lower-path compare) — idempotent sau respawn, preset giữ.
- Rust pump: liveness = heartbeat chuyên dụng (không phải writeIndex), phân loại HANG/CRASH, max 1 restart/incident; `restart_attempts` reset khi hb advance.
- Destroyed: SHUTTING_DOWN trước kill → pump dừng (trừ race F-PROC-3).
- JS: `__bridgePlaying` guard openVstGuiRetry; load instrument im lặng khi PLAY (không tự mở GUI); `__bridgeLoadedChannels` reset cả connected lẫn !connected (V8 bug 4) → reconnect reload đúng, không mở GUI; C++ LOAD-skip làm reload idempotent.
- shm.rs layout khớp C++ (selfcheck 12/1040/11168/15268); `push_control` zero-fill arg2 → `std::string(c.arg2)` an toàn với mọi writer hiện tại; MIDI count non-atomic (race benign, volatile).
**Verify Linux:**
- `g++ -std=c++17 -Wall -Wextra -fanalyzer` StateStore.cpp: compile sạch (0 warning) + `test_state_store.cpp` run **"state_store round-trip PASS"**.
- `SharedMemoryIPC.cpp` / `SandboxVst3Host.cpp` / `plugin_host_main.cpp`: Windows-only (xung đột POSIX `shm_open`, thiếu HANDLE/CreateProcessA) — không build được trên Linux, đúng kỳ vọng.
- `node --check` app.precompiled.js + nativeBridgeService.js: PASS.
- Rust: không có toolchain (như G2.6).
### Nhật ký thực hiện
- **G2.0** (2026-08-16, Windows): tao `native_bridge/include/StateStore.h` + `native_bridge/src/StateStore.cpp` (CRLF, JSON viet tay, atomic write .tmp→rename, escape \ "
, load parser an toan). Them StateStore.cpp vao CMakeLists target daw_vst_bridge. Build bridge PASS. Test round-trip `native_bridge/debug/test_state_store.cpp` PASS (compile cl vcvars64 + chay). Commit `G2.0: StateStore (JSON state file + atomic write)`.
- **G2.1** (2026-08-16, Windows): save day du state ra disk. main.cpp: globals + snapshotsEqual + stateFile env fallback + playheadSamples atomic + saveStateJob (UiThread, skip isReloading, chi ghi khi snapshot doi) + hook TRANSPORT/LOAD + periodic 2s dirty debounce 300ms. Them khai bao bankOf/programOf vao Vst3Instrument.h (thieu declaration -> build fail). StateStore.cpp: ensureParentDir() (CreateDirectoryA chain, Linux mkdir) goi dau save() (fix C2001 do heredoc backslash mangle qua script fix_bs.py). Build PASS. Deploy PASS (PID 9876/24632/7352). Verify live PASS: LOAD Nexus ch5 + PLAY -> state file 7 inst + playing=true + presetBase64 day du; STOP -> playing=false. StateStore round-trip test PASS. Commit `G2.1: save full instrument state to disk (path/type/preset/transport)`.
- **G2.3** (2026-08-16, Windows): auto-reload sau respawn.
- **G2.2** (2026-08-16, Windows): dump state truoc kill. main.cpp: them control type=5 DUMP_STATE sau case 4 — post saveStateJob len uiWorker ngay (khong cho debounce 300ms/2s), log "DUMP_STATE - flushing state now". lib.rs: them flush_bridge_state(app) — shm.push_control(5,0,0,0,"") roi sleep 300ms (du thoi gian UiThread ghi xong file); goi o dau kill_bridge() (cover ca restart_bridge_with_sample_rate va stall-restart) va trong on_window_event Destroyed truoc khi kill_process_tree(bridge). Build PASS (daw_vst_bridge.exe) + cargo check PASS (src-tauri). Deploy + verify live PASS: push DUMP qua SHM driver -> log "DUMP_STATE - flushing state now" xuat hien, state file intact (7 inst, playing=false), writeIndex advancing. Ghi chu: nhanh Rust flush chi cargo check (khong build app Tauri) — logic don gian, compile OK. Commit `G2.2: flush bridge state to disk before kill (control type=5 DUMP + Rust pre-kill)`.
- **G2.4** (2026-08-16, Windows): go double-load khi JS reload sau respawn. main.cpp: them guard dau LOAD case (truoc setReloading) — neu channel da co cung path (so sanh lower_plugin_path(ch) vs lower_path_str(path)) thi log "LOAD skip ch=N (already loaded)" va return, khong assign lai (thay instance se mat preset da restore tu state file). Ly do: JS reconnect/bootstrap reload gui LOAD cung path sau respawn -> truoc day assign() thay instance -> mat preset. Guard PLAY cua G2.4 (cho restore chay khi PLAY) da co san o JS: loadVstToBridge/loadTrackInstrumentToBridge khong co guard __bridgePlaying, chi openVstGuiRetry giu guard OPEN_GUI (G0.2 — defer GUI khi PLAY van dung). Build PASS (daw_vst_bridge.exe) + deploy + verify live PASS: sau respawn, LOAD lai Nexus ch5 -> "LOAD skip ch=5 (already loaded: ...Nexus.vst3)", preset 3488B giu nguyen, writeIndex advancing; tong 15 LOAD skip (JS reload 7ch x2 + test ch5). Commit `G2.4: skip redundant LOAD of already-loaded instrument (keep restored preset)`. main.cpp: block restore truoc audio loop - doc state file, chi restore channel trong (guard instruments.get(ch)==nullptr), post LOAD job (assign + loadSerializedState / selectProgram) serialize bang g_loadMutex, neu state playing thi set playhead + g_transportPlaying=true + g_stateDirty. Vst3Instrument.cpp: base64Decode (b64val lookup) + loadSerializedState (parse blob, goi restoreState); Vst3Instrument.h + INativeInstrument.h: khai bao. lib.rs: spawn_bridge them env SF_STATE_FILE (fallback StateStore::defaultPath khi khong set). Build PASS (daw_vst_bridge.exe + cargo check). Deploy + verify live PASS: LOAD ch5 + PLAY playhead 5000 -> taskkill bridge -> respawn tu dong -> log restore 7 inst (Nexus ch0-5, EZkeys ch6) + restore transport PLAY playhead=5000 + ch5 preset 3488B giu nguyen; state file playing=true inst=7; writeIndex advancing. Commit `G2.3: auto-reload instruments from state file after bridge respawn`.
- **G2.5** (2026-08-16, Windows): tao native_bridge/debug/test_g2_stress.py (khung test_g15_stress.py). N vong: LOAD Nexus ch5 + EZkeys ch6 (real hoac skip deu OK) -> PLAY -> taskkill daw_vst_bridge -> cho watchdog "[tauri] bridge restarted" + "restored ch=5/6" -> LOAD lai -> verify "LOAD skip" (guard G2.4) + preset giu (ch5 exact, ch6 sim>=0.7 — EZkeys nhung PlayHeadPos vao state, ratio 0.872 giua save thong thuong) + writeIndex advancing. Chay 5 + 10 iterations: PASS (20 lan kill/respawn, khong crash, bridge alive). Kem sua test_g15_stress.py: LOAD ch5 chap nhan "LOAD skip" (guard G2.4 doi log). Commit `G2.5: stress driver for persistence restart (N loop + kill/respawn)`.
- **G2.6** (2026-08-16, Windows): verify tong.
- **G2.0–G2.6 review + verify** (2026-08-16, Linux — máy Linux chỉ review/ghi chú, KHÔNG sửa code): đọc toàn bộ diff G2 (`1fe2d26~1..b59e416`, 15 files) — Vst3Instrument base64 + loadSerializedState (bounds-check đủ), NativeInstrumentEngine snapshot (mu_ chặn race với audio loop, skip reloading), main.cpp saveStateJob/restore/LOAD-guard/DUMP/debounce, lib.rs flush_bridge_state + env SF_STATE_FILE, test_g2_stress.py + test_state_store.cpp + test_g15_stress.py. Verify Linux: py_compile 2 stress scripts PASS; g++ compile + run test_state_store PASS (StateStore portable); node --check app.precompiled.js PASS; rustc/cargo không có default toolchain → không cargo check (Windows đã check PASS). Findings: F1 double flush_bridge_state khi window-close (minor), F2 SF_STATE_FILE thiếu Linux fallback (portability, không ảnh hưởng target Windows), F3 log "bad ctrlLen" trùng cho 2 check (cosmetic). Không sửa code (đúng phân máy). Chi tiết ở mục 5.1. G1.5 regression PASS 5/5 (LOAD/OPEN_GUI/PLAY/STOP/WM_CLOSE, writeIndex delta 307-551, bridge alive). G2.5 PASS 5+5+10 (20 lan kill/respawn, preset giu, khong EXCEPTION/FAILED). Probe G0.4 (ban cap nhat: PID dong, LOAD guard-aware, OPEN_GUI full path): PASS — LOAD skip -> OPEN_GUI hwnd -> WM_CLOSE -> "state captured ch=5 component=2628B" -> reload -> "state restored ch=5 component=2628B" -> OPEN_GUI lai attach OK, khong restart. Bug tim duoc: khong co bug san pham; 2 bug test-script (probe PID int vs list -> bao restart gia; preset EZkeys byte-diff do PlayHeadPos runtime). Commit `G2.6: verify — regression G1.5 + stress G2.5 + probe G0.4 all PASS`.
- **Audit bảo mật/bộ nhớ/conflict child process** (2026-08-16, Linux — chỉ review/ghi chú, KHÔNG sửa code): đọc toàn bộ main.cpp/NativeInstrumentEngine/Vst3Instrument/StateStore/SandboxVst3Host/plugin_host_main/SharedMemoryIPC/shm.rs/lib.rs/nativeBridgeService.js/app.precompiled.js. Findings: F-MEM-5 UAF audio-thread get()+deref vs assign() destroy oldInst ngoài lock sau 10ms (MEDIUM, sites 794/884/909/927/953/1290); F-MEM-8 double-buffer không handshake (MEDIUM audio); F-PROC-1 command injection `--path "..."` CreateProcessA (MEDIUM); F-PROC-2 race restart×watchdog → 2 bridge cùng SHM (MEDIUM); F-PROC-3 orphan race Destroyed (LOW); F5 open_vst_gui luôn Ok (LOW); F6 comment cap 2 vs 5 (LOW). Luồng chính đúng thiết kế: drain control trước render, LOAD UiThread+g_loadMutex, OPEN_GUI defer PLAY + pending sweep, STOP flush CC64=0, DUMP_STATE trực tiếp, pump drop-guard trước kill, max 1 restart/incident, JS __bridgePlaying + __bridgeLoadedChannels reset. Verify Linux: StateStore -fanalyzer sạch + round-trip PASS; 3 file Windows-only không build được (đúng); node --check 2 JS PASS. Không sửa code (đúng phân máy). Chi tiết mục 5.2.
- **Fix audit 5.2** (2026-08-16, Windows): xử lý toàn bộ findings 5.2. F-PROC-1: SandboxVst3Host.cpp thêm quote_arg (escape \ và " theo Windows CRT) + CreateProcessA(hostExe.c_str(), ...) + bỏ token exe khỏi cmdline -> path thù địch chứa " giữ 1 argv (probe F-PROC-1 PASS: spawn ch=7 cmdline giữ trọn path trong --path, child không map HACKED_SHM, không nhận --sr 1000, load fail như mong đợi). F-MEM-5: NativeInstrumentEngine.h + .cpp thêm has() + template withInstrument() (giữ mu_ quanh call); main.cpp đổi 6 site audio-thread: 784/794 hasAttachedView dedupe, 1073 restore guard, 1283 CC64=0, 1318 pending-GUI, 1337 state-dirty -> has()/withInstrument(); giữ nguyên 8 site UiThread (an toàn cùng thread assign). F-PROC-2: lib.rs thêm static BRIDGE_RESTART_LOCK serialize kill+spawn giữa restart_bridge_with_sample_rate và pump watchdog. F-PROC-3: pump re-check SHUTTING_DOWN sau lock + sau kill trước spawn. F5: open_vst_gui trả Err (lock poisoned / SHM unavailable / queue full), frontend try/catch sẵn. F6: comment Cap 5 -> Cap 2 (hard min(attempts,2)). F-MEM-8: ghi nhận theo dõi, không fix (đúng audit). Build: daw_vst_bridge + plugin_host PASS; cargo build --release PASS (2m53s). Deploy install/ (kill trước, copy2). Regression: G1.5 PASS 3/3 (writeIndex delta 287), G2.5 PASS 2/2 (watchdog restart + preset giữ exact), probe F-PROC-1 PASS (không injection). Commit: fix 5.2 audit findings on Windows.
---
## 6. PHỤ LỤC A — QUY TRÌNH THỰC HIỆN TRÊN MÁY WINDOWS
> Quy trình 2 máy: **Windows sửa code + build + test + fix + commit + push** →
> **Linux pull, review, ghi chú, cập nhật báo cáo + push** → Windows pull lại, verify tiếp.
> Các lệnh dưới chạy trên máy Windows (PowerShell), đường dẫn theo máy dev `C:\Users\locpham\SonicForgeStudio`.
### 6.1 Cấu trúc + đường dẫn cố định
| Thứ | Vị trí |
|---|---|
| Repo (Windows) | `C:\Users\locpham\SonicForgeStudio` |
| Build bridge | `native_bridge\build\` (CMake) — exe: `native_bridge\build\Release\daw_vst_bridge.exe` |
| Install (bundle chạy thật) | `C:\Users\locpham\SonicForgeStudio\install\` — app: `sonicforge-daw.exe` |
| Logs | `%APPDATA%\SonicForgeDAW\logs\` — `bridge.log` (stdout bridge), `spawn.log` (watchdog/restart), `engine.log` |
| State file G2 (MỚI) | `%APPDATA%\SonicForgeDAW\state\bridge_state.json` (+ `.tmp` khi atomic write) |
| VST test | `C:\Program Files\Common Files\VST3\Nexus.vst3`, `...\Toontrack\EZkeys 2.vst3` |
| SHM | `SonicForge_DAW_IPC_<APP_PID>`; offsets: controlQueue @2836 (8×1040), count @11156, attachInFlight @11160, writeIndex @4, size 11164. Types: 2=LOAD, 3=TRANSPORT, 4=OPEN_GUI, **5=DUMP_STATE (G2.2 thêm)** |
| Probe | `native_bridge\build\Release\gui_probe.exe` (variants: main_own/worker_own_nopump/…/ui_thread/shared_worker) |
### 6.2 Build
```powershell
# trong repo Windows
cd native_bridge\build
cmake -DCMAKE_CXX_FLAGS_RELEASE='/O2 /Ob2 /DNDEBUG /Zi' -DCMAKE_EXE_LINKER_FLAGS_RELEASE='/DEBUG' .
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
cmake --build . --config Release --target daw_vst_bridge
```
hoặc chạy script có sẵn: `native_bridge\debug\build_bridge.ps1` (dùng build dir cũ).
- Thêm file mới (G2.0 `StateStore.cpp`) → nhớ thêm vào `native_bridge/CMakeLists.txt` target `daw_vst_bridge`, rồi re-run cmake.
- **Lưu ý CRLF**: toàn bộ `native_bridge/` + `CMakeLists.txt` là CRLF — patch đa dòng phải qua script Python `io.open(newline='')`, không heredoc `<<'EOF'` (khớp sai).
### 6.3 Deploy (mẫu `deploy_g12.ps1`)
```powershell
$install = 'C:\Users\locpham\SonicForgeStudio\install'
$new = 'C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe'
Get-Process sonicforge-daw, daw_engine, daw_vst_bridge -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 2
Copy-Item -Path $new -Destination (Join-Path $install 'daw_vst_bridge.exe') -Force
Start-Process -FilePath (Join-Path $install 'sonicforge-daw.exe')
Start-Sleep -Seconds 8
Get-Process sonicforge-daw, daw_engine, daw_vst_bridge -ErrorAction SilentlyContinue | Select-Object Id, ProcessName, Path | Format-Table -AutoSize
```
- 3 process phải kill hết: `sonicforge-daw`, `daw_engine`, `daw_vst_bridge` (bridge cũ không chết → 2 bridge map chung SHM → race).
- Sau start: lấy **APP_PID mới** (tasklist/Get-Process) → truyền vào probe (G2.5) — PID đổi mỗi lần deploy.
### 6.4 Verify không cần engine (Linux + Windows đều chạy được)
- `shm_selfcheck.cpp` (đã build từ trước; layout 11164 → sau G2 **không đổi** vì state ghi ra đĩa, không thêm field SHM): chạy pass = control queue/attach flag/writeIndex đúng.
- JS: `node --check app/static/js/app.precompiled.js` (sau mỗi lần sửa JS).
- Rust (G2.2): `cargo check` trong `src-tauri/` — máy Linux hiện không có toolchain → bắt buộc trên Windows.
### 6.5 Stress (G2.5, theo mẫu `test_g15_stress.py`)
```powershell
python native_bridge\debug\test_g2_stress.py <APP_PID> <N>
```
- Đọc SHM trực tiếp (ctypes), đẩy control LOAD/OPEN_GUI/PLAY/STOP, theo dõi `bridge.log`.
- Verify: bridge alive, writeIndex advancing mỗi iter, không EXCEPTION/FAILED/crash.
- **G2 thêm bước kill giữa chừng**: `Stop-Process daw_vst_bridge` → chờ respawn (spawn.log) → verify writeIndex + preset restored.
- Cảnh báo learning cũ: **probe PASS có thể dương tính giả** → luôn kiểm tra `bridge.log`/`spawn.log` thực; dùng chung `since` window (log cùng job xuất hiện ~đồng thời), không reset.
### 6.6 Vòng lặp bắt bugs → fix → report
1. **Windows** chạy verify → lỗi → thu log (`bridge.log`, `spawn.log`, `engine.log`) + (nếu crash) minidump/cdb.
2. **Windows** xác định lỗi thuộc task nào → sửa code → build → deploy → verify lại → commit + push.
3. **Linux** pull → review + ghi chú, cập nhật bảng mục 5 + nhật ký → push.
4. **Windows** pull lại, tiếp tục task sau.
5. Kết thúc G2.6: chạy lại regression G1.5 (`test_g15_stress.py`) — G2 không được làm hỏng G1.
### 6.7 Tiện ích / lưu ý khác
- `tasklist` CSV: dùng `subprocess.run(['tasklist'])` full output + `line.split()[1]`.
- `sleep` không tồn tại trong sh (máy Windows) → `python -c "import time; time.sleep(N)"`.
- `timeout` Windows khác GNU → dùng wrapper python subprocess.
+277
View File
@@ -0,0 +1,277 @@
# BIG PHASE 3 — Export MIDI/Mix, FX CHAIN, Virtual MIDI Keyboard & Chords Panel
---
## ⚠️ GHI NHỚ — QUY TẮC BẮT BUỘC KHI THỰC HIỆN (đọc trước, tuân thủ mọi phase)
1. **KHÔNG ĐƯỢC LÀM HỎNG code đã chạy ổn định.** 4 lỗi nghiêm trọng đã fix ở phase trước, tuyệt đối không tái phạm (verify regression sau MỖI thay đổi):
- Âm lượng track không đồng đều (jbridge/SandboxVst2Host — calibrate makeup phải ≈2.59–2.71 cho Qin, KHÔNG được quay lại makeup=1.0).
- VST GUI đã load không hiện / mất GUI sau khi đóng-mở (PluginHost z-order top, embed child #32770 jBridge + Qin_VST_Window).
- Bị câm khi đã load instrument (VSTi/SF2 qua bridge — load xong phải nghe được ngay).
- Bị câm khi preview MIDI (preview VSTi/midi-render phải ra âm).
- Trước khi commit bất kỳ phase nào: chạy lại bộ verify âm thanh cũ (note_quick / open_gui / silence_all) xác nhận KHÔNG regression.
2. **PHẢI OPTIMIZE sau mỗi lần sửa code** (xóa biến chết, branch chết, code trùng; gọn nhất có thể — YAGNI).
3. **LUÔN LUÔN tuân thủ đường dẫn của các lần compile/build trước:**
- Frontend: sửa trong `app/static/js/app.jsx` → `node build.mjs` → `app/static/js/app.precompiled.js`.
- Engine: `python -m PyInstaller engine.spec --clean --noconfirm` từ repo root → output `dist/daw_engine/` → **copy toàn folder** sang `src-tauri/target/release/daw_engine/`.
- C++ bridge: build trong `native_bridge/` bằng `cmake --build build --config Release --target plugin_host daw_vst_bridge` → copy `plugin_host.exe` + `daw_vst_bridge.exe` sang `src-tauri/binaries/` (gồm bản `-x86_64-pc-windows-msvc.exe`) + `src-tauri/target/release/`.
4. **KHÔNG TỰ Ý thay đổi các đường dẫn cũ** (SHM layout, storage paths `%APPDATA%/SonicForgeDAW`, plugin dirs, soundfont dir, port 8000–8010, tên exe, cấu trúc `src-tauri/`). Chỉ đổi khi có lý do bắt buộc và ghi rõ trong commit.
5. **SAU KHI BUILD HOÀN THÀNH phải dọn dẹp tập tin tạm / tập tin rác:** xóa `build/`, `dist/` (sau khi đã copy xong), `__pycache__/`, `.pytest_cache/`, file `.bak`/`.new`/`.recovered` không dùng, file log rác — rồi `git status` phải sạch (chỉ còn thay đổi chủ đích).
---
> Repo: `C:/Users/locpham/SonicForgeStudio` · branch: `standalone-shm-bridge`
> Frontend là 1 file monolith: `app/static/js/app.jsx` (JSX) → build bằng `node build.mjs` → `app/static/js/app.precompiled.js` (file được template nạp: `app/templates/index.html` load `app.precompiled.js?v=...`).
> Backend engine (FastAPI) đóng băng bằng PyInstaller → **MỌI thay đổi frontend/backend đều phải rebuild engine** rồi copy sang folder chạy thật (xem §0).
---
## 0. QUY TRÌNH BUILD & CHẠY THỬ (bắt buộc cho mọi phase)
```bash
# 1. Sửa app/static/js/app.jsx (hoặc file backend .py)
# 2. Recompile frontend:
node build.mjs
# 3. Rebuild engine đóng băng (nếu đổi app.jsx HOẶC app/**.py):
cd /c/Users/locpham/SonicForgeStudio
python -m PyInstaller engine.spec --clean --noconfirm
# 4. Copy engine mới sang nơi app thật dùng:
cp -r dist/daw_engine/* src-tauri/target/release/daw_engine/
# 5. Kill tiến trình cũ rồi chạy app thật:
taskkill //IM sonicforge-daw.exe //F 2>/dev/null; taskkill //IM daw_engine.exe //F 2>/dev/null
src-tauri/target/release/sonicforge-daw.exe &
# 6. Login API (backend port 8000):
# POST http://127.0.0.1:8000/api/v1/auth/login {"username":"admin","password":"admin123"}
# → token → header `Authorization: Bearer <token>` cho các endpoint cần auth.
```
Ghi chú:
- `app/static/js/*.js` (services) và `app/static/js/app.jsx` là LF; `native_bridge/` C++ là CRLF — kiểm tra bằng `file` trước khi patch binary.
- Chạy thật = `src-tauri/target/release/sonicforge-daw.exe` (Tauri WebView2). Không test bằng browser Chrome thuần — lỗi "sau khi chuyển standalone" chỉ tái hiện trong WebView2/engine đóng băng.
---
## PHASE 1 — FIX EXPORT MIDI (đã có feature, lỗi sau khi chuyển standalone)
### 1.1 Hiện trạng
- Code xuất MIDI nằm ở `triggerMidiExport()` trong `app/static/js/app.jsx` (anchor ~dòng 26059).
- Hoàn toàn client-side: gom note từ `track.midiItems[].notes` → build bytes MIDI format 1 (header `MThd`, từng track `MTrk`, delta time bằng `writeVLQ`) → `Blob` → download bằng `a.href = URL.createObjectURL(blob); a.click()`.
- Menu: File → Export MIDI... (anchor ~29344). Còn 1 nút "Export MIDI" trong piano roll toolbar (~10534) chỉ gọi handler khác (midi-render → WAV), không phải export .mid.
### 1.2 Nghi vấn root cause (chưa xác nhận — chẩn đoán trước)
1. **WebView2 chặn blob download**: `URL.createObjectURL` + `a.click()` với thuộc tính `download` không hoạt động trong WebView2 (không có hộp thoại lưu, không có sự kiện download). → Nghi ngờ cao nhất.
2. Bug phụ trong code hiện tại (sửa luôn):
- `velocity = Math.round((note.velocity || 0.8) * 100)` → scale 0..100, **sai chuẩn MIDI 0..127** (đáng lẽ `* 127`).
- Track audio không có MIDI được xuất thành "rest track" với note pitch 0 velocity 1 — gây tiếng "bíp" nếu DAW khác phát file này; nên bỏ hẳn track không có note MIDI.
### 1.3 Giải pháp
- **Bước A — Chẩn đoán xác nhận (làm trước khi code):**
1. Mở app thật, tạo project có MIDI item (vài nốt), bấm File → Export MIDI... → quan sát: có hộp thoại lưu không? console WebView2 có lỗi không? (dùng `--remote-debugging-port` nếu cần).
2. Nếu không có download → xác nhận giả thuyết WebView2 chặn blob.
- **Bước B — Fix (2 lựa chọn, ưu tiên B1):**
- **B1 (khuyên dùng): Server-side export endpoint.** Thêm `POST /api/v1/export/midi` vào `app/api/v1/audio.py` (hoặc file mới `app/api/v1/midi.py`):
- Request: `{ bpm, ppq, tracks: [{ name, notes: [{ pitch, start_beat, duration_beats, velocity }] }] }`.
- Backend dùng **stdlib thuần** (không thêm dependency) viết file .mid (format 1, `writeVLQ` giống frontend, velocity scale chuẩn `*127`, bỏ track không có note).
- Trả `file_id` (lưu vào `app/storage/processed/`) + `download_url` → frontend tải qua `GET /api/v1/audio/download/{file_id}` (pattern đã có sẵn, chạy tốt trong WebView2 vì là network download http://127.0.0.1, không phải blob).
- **B2 (fallback nếu B1 không khả thi):** giữ generation client-side nhưng đổi cơ chế tải: thử `a.click()` → nếu không có download, dùng Tauri dialog: `window.__TAURI__.dialog.save()` (capability `dialog:default` đã có) + ghi file qua... lưu ý **thiếu permission fs** — cần thêm `fs:default` vào `src-tauri/capabilities/default.json` nếu đi hướng này. → B1 đơn giản hơn, không đụng Rust/capabilities.
- **Bước C — Cập nhật UI:** `triggerMidiExport()` gọi API mới, hiện spinner `setIsExporting`, toast kết quả; giữ nguyên menu/nút cũ.
### 1.4 Verify
- Tạo project MIDI (2 track, nhiều note, velocity khác nhau) → Export MIDI → file .mid tải về được.
- Mở file bằng DAW khác (hoặc script đọc bytes): số track đúng, tempo đúng, pitch/duration đúng, velocity nằm trong 1..127 (không còn giới hạn 100).
- Export project rỗng → toast "Không có dữ liệu MIDI" (không crash).
---
## PHASE 2 — FIX EXPORT MIX (xuất âm thanh) + TÙY CHỌN LOOP (không fade in/out)
### 2.1 Hiện trạng
- Menu: File → Export Mix... (anchor ~29340) → `triggerWavExport()` (anchor ~26293).
- `ExportModal` (anchor ~11881): option Nguồn (`project`/`track_mix`/`active_clip`/`clip_selection`), Định dạng (wav/mp3/ogg), SR, Bit, Kênh. **Chưa có option loop, chưa có option fade.**
- 3 đường xuất âm:
1. **Server-side** (`allOnServer && serverStatus === 'connected'`): POST `/api/v1/multitrack/mix` → task → poll → download file_id. Điều kiện: mọi track có `serverFileIdMap[t.id]` (đã upload file lên server) và `serverStatus === 'connected'`.
2. **Client-side** `clientSideExport()` (anchor ~26734): `OfflineAudioContext` build lại full graph (track FX Rack + mastering chain) → encode WAV bằng tay (RIFF header + PCM). Chỉ hỗ trợ WAV; mp3/ogg bắt buộc server (`serverStatus === 'connected'`, anchor ~26849).
3. **Bounce realtime** `triggerBounceExport()` (anchor ~26450): khi project có MIDI items mà MIDI cache chưa đủ → chạy lại project thật, capture PCM master bus → WAV.
- Server path gửi `fade_in_ms: 0, fade_out_ms: 0` rồi nhưng `ClipConfig` backend mặc định 150/150 — kiểm tra worker có tôn trọng 0 không (xem `app/tasks/worker.py` `mix_multitrack_task`).
### 2.2 Nghi vấn root cause (chẩn đoán trước)
1. Sau standalone, `serverStatus` thường = `checking...`/`offline` (không có server riêng) → nhánh server-side không chạy; nếu user chọn mp3/ogg → báo lỗi/không xuất. Kiểm tra `serverStatus` khi chạy app thật.
2. Client-side path dùng `OfflineAudioContext` — trong WebView2 có thể fail nếu audio context chưa từng được unlock (autoplay policy) → export ra file rỗng/crash. Kiểm tra log.
3. File tải về qua blob download — cùng vấn đề WebView2 như Phase 1.
4. MIDI bounce: nếu master bus chưa init → toast "Hãy phát thử một lần" (đã có guard) — nhưng dễ bị kẹt ở trạng thái này khi user chưa Play.
### 2.3 Giải pháp
- **Bước A — Chẩn đoán:** chạy app thật, tạo project (audio clip + 1 MIDI track), thử Export Mix cả 3 nguồn; ghi lại: toast nào hiện, file có tải về không, file có đúng độ dài/âm thanh không.
- **Bước B — Sửa đường xuất âm cho standalone:**
1. `clientSideExport`: bọc `OfflineAudioContext` bằng try/catch; trước khi render gọi `await ensureAudioUnlocked()` (unlock context bằng `resume()`/silent buffer nếu chưa unlock).
2. Chuyển hết đường tải file sang pattern download qua server (giống Phase 1 B1): hoặc dùng helper `downloadBlobViaServer(url)`; đơn giản nhất: mọi export (client/bounce) lưu ra `app/storage/processed/` qua 1 endpoint upload hoặc tự backend sinh → trả file_id → `a.href = /api/v1/audio/download/{file_id}` (network download, hoạt động trong WebView2).
3. mp3/ogg khi server offline: thêm fallback — nếu không server, chuyển tự động về WAV kèm toast thông báo (không im lặng fail).
- **Bước C — Tùy chọn LOOP (yêu cầu chính):**
1. Thêm state `exportSettings.loop` (boolean) + `exportSettings.loopBars` (số ô nhịp, default 4) vào `useState` (anchor ~17249).
2. `ExportModal`: thêm checkbox **"Xuất loop (không fade in / không fade out)"** + input số ô nhịp (hoặc dùng vùng chọn hiện tại nếu có selection).
3. Khi `loop = true`:
- Nguồn buộc = vùng loop: `duration = loopBars * (60/bpm) * 4` giây (hoặc selection bounds), bắt đầu từ playhead/bar đầu selection.
- **Mọi path đều zero-fade:** server path `fade_in_ms: 0, fade_out_ms: 0` (đã gửi, kiểm tra worker tôn trọng); client path `clientSideExport` không áp fade (clip buffer thô — xác nhận `buildOfflineTrackNode` không chèn fade); bounce path capture PCM không fade.
- Không kéo đuôi: cắt chính xác tại `start + duration` (không để tail reverb/decay thừa — nếu muốn giữ decay thì là option riêng, ngoài phạm vi; note `ponytail:`).
4. Đảm bảo file loop không bị "click" ở biên: zero-fade nghĩa là giữ nguyên dạng sóng; yêu cầu người dùng tự chọn loop point sạch (không tự động crossfade — đúng spec "không fade").
- **Bước D — Cập nhật nút/UI:** giữ menu Export Mix..., thêm shortcut nếu tiện.
### 2.4 Verify
- Export WAV loop 4 bars (project có audio + MIDI qua FX CHAIN): file đúng độ dài `4*4*60/bpm` giây (±1 sample), **mẫu đầu và mẫu cuối ≠ 0** nếu nội dung phát tại biên (chứng minh không fade in/out), loop nối vòng không click rõ rệt.
- Export project không loop (bình thường) vẫn ra đúng như trước (không regression fade 150ms mặc định nếu có).
- mp3/ogg khi server offline → tự về WAV + toast.
---
## PHASE 3 — ĐỔI MASTERING PANEL / FX RACK PANEL → FX CHAIN (modal dùng chung: plugin tự viết + VST FX)
### 3.1 Hiện trạng
- 2 modal riêng biệt:
- `FXRackModal` (anchor ~12160): cho 1 track. Header `{track.name} — FX RACK PANEL` (anchor ~12492). Chain built-in = `track.fxChain` (module tự viết: EQ/comp/limiter/reverb/delay..., meta `TRACK_FX_META` anchor ~1331); VST FX riêng = `track.vstFxChain` (danh sách từ `window.SonicAPI.listFx()` = `fx_vst_bridge --scan`, áp khi render/export).
- `MasteringModal` (anchor ~12743): cho master. Header `MASTERING SUITE ... WEB MASTERING V10.5` (anchor ~13503). State `masteringSettings` (ozState): built-in mastering (EQ 4 băng/comp/limiter) + `ozState.vstFxChain`.
- Nút mở:
- Master: nút "MASTERING PANEL" trong `MasterStripConsole` (anchor ~2393), nút PWR cạnh master fader (~2526), menu File → "Mastering Suite" (anchor ~29348, Ctrl+Shift+M).
- Track: nút `FX` trên TCP (anchor ~2740) → `window.__openFxRack(track.id, track.name)` (định nghĩa ~18065) → set `fxRackTarget` → render `FXRackModal` (anchor ~32645).
### 3.2 Mục tiêu (yêu cầu)
1. Bỏ tên "MASTERING PANEL"/"FX RACK PANEL" → thống nhất **"FX CHAIN"**.
2. Một modal chung chứa **cả plugin xử lí âm thanh tự viết (built-in DSP) lẫn VST FX** trong **một chain duy nhất**, dùng được cho **master** và cho **track**.
3. Chain áp realtime khi Play và áp đúng khi Export (cả client offline lẫn bounce).
### 3.3 Giải pháp
- **Bước A — Tách component chung:** tạo `FxChainModal({ target, onClose })` với `target = { kind: 'master' } | { kind: 'track', trackId }` (thay 2 component cũ, hoặc giữ wrapper cũ gọi chung — chọn cách ít đụng nhất: giữ `FXRackModal`/`MasteringModal` làm wrapper mỏng, body dùng chung component `FxChainBody`).
- **Bước B — Hợp nhất chain trong UI:**
- Hiển thị 1 hàng chain duy nhất: mỗi slot = `{ type: 'builtin' | 'vst', id, name, icon, bypass, preset_b64? }`.
- Backing state giữ nguyên để không phá lưu project cũ: track → `track.fxChain` (builtin) + `track.vstFxChain` (vst); master → `masteringSettings` builtin + `masteringSettings.vstFxChain`. Chỉ hợp nhất ở tầng render + thao tác (thêm/xóa/kéo sắp xếp/bypass ghi vào đúng mảng gốc).
- Picker thêm slot: 2 tab — "Module tự viết" (TRACK_FX_META + mastering built-ins) và "VST FX" (`listFx` → `vstFxChain`), chèn vào đúng vị trí kéo thả.
- **Bước C — Đổi tên toàn bộ hiển thị:**
- `FXRackModal` header → `{track.name} — FX CHAIN`.
- `MasteringModal` header → `MASTER FX CHAIN` (bỏ "MASTERING SUITE"/"WEB MASTERING V10.5" hoặc chuyển thành subtitle nhỏ).
- Nút `MasterStripConsole` → "FX CHAIN".
- Menu File "Mastering Suite" → "Master FX Chain" (giữ Ctrl+Shift+M).
- Cập nhật tooltip "nút A/♪" mô tả ("Mastering FX Chain" → "Master FX Chain").
- **Bước D — Đảm bảo chain áp dụng đúng chỗ (không đổi logic DSP):**
- Track: giữ graph hiện có — `buildOfflineTrackNode`/`__rebuildTrackFxGraph`/`fxActive` PWR (đừng đụng nếu đang chạy đúng).
- Master: giữ `toggleMasteringOnMaster` + `NativeBridgeService.setFxChain` (anchor ~12799) cho VST master realtime.
- Export: clientSideExport/bounce đã đi qua cả 2 chain — verify không regression.
### 3.4 Verify
- Mở FX CHAIN master: thêm 1 module tự viết (EQ) + 1 VST FX → header "MASTER FX CHAIN", cả 2 hiện trong 1 chain, kéo thay đổi thứ tự, bypass từng slot, nghe thay đổi realtime, export ra đúng chain.
- Mở project cũ (lưu trước phase): chain cũ vẫn hiện đúng (không mất cấu hình) — test migration state.
- Không còn chuỗi "MASTERING PANEL"/"FX RACK PANEL" trong UI (grep app.jsx).
---
## PHASE 4 — NÚT FX TRÊN TCP MỞ FX CHAIN CỦA TRACK
### 4.1 Hiện trạng
- Nút `FX` trên TCP track đã gọi `window.__openFxRack(track.id, track.name)` (anchor ~2740) → `fxRackTarget` → `FXRackModal` (anchor ~32645). **Phần wiring có vẻ đã có** — cần xác nhận nó còn hoạt động sau Phase 3 refactor.
### 4.2 Giải pháp
- **Bước A — Kiểm tra hiện trạng:** mở app thật, chọn track → bấm nút FX → modal có mở không? Chain có hiện đúng `track.fxChain` + `track.vstFxChain` không?
- **Bước B — Fix nếu hỏng:** các nguyên nhân có thể: `tracks.find(t => t.id === fxRackTarget.trackId)` trả null (id mismatch giữa `tracks` và `activeTracks`/sessionTabs), hoặc `FXRackModal` trả `null` sớm khi `track` undefined. Sửa lookup: dùng `allTracks` gồm main + session tabs (`(activeTracksRef.current || tracks)` như triggerWavExport làm).
- **Bước C — Sau Phase 3:** nút FX mở `FxChainModal` với `target = { kind: 'track', trackId }` → header "FX CHAIN" hiển thị chain track.
### 4.3 Verify
- Bấm nút FX trên track A → modal FX CHAIN của track A (đúng tên, đúng chain); bấm trên track B → chain track B; đóng/mở nhiều lần không lỗi; VST GUI embed (nếu có) vẫn mở được từ chain.
---
## PHASE 5 — VIRTUAL MIDI KEYBOARD (spec `virtual_keyboard_and_chords_panel_spec.md` Part I)
### 5.1 Nền tảng có sẵn
- `app/static/js/services/unifiedMidiRouter.js` đã có: `dispatchMidiEvent({ command: 'NOTE_ON'|'NOTE_OFF', channel, pitch, velocity, sourceType })`, `allocateChannel(trackId, isPercussion)` (ch 0-8,10-14 melodic / 9 percussion), `bridgeConnected` → NativeBridgeService hoặc `onFallback` (SF/WASM). → Tận dụng, không viết router mới.
- Audio preview: nếu bridge active → `NativeBridgeService.dispatchMidiEvent` (âm thật VSTi/SF2 đã load trên channel); nếu không → fallback hiện có (SonicSF/Carla) — giữ nguyên cơ chế.
- Recording: transport Record + track armed đã có; cần hook NOTE_ON/NOTE_OFF vào recorder tạo note trong `midiItems`.
### 5.2 Giải pháp (thêm vào `app.jsx` — nhất quán monolith; code thuần logic tách service nếu >100 dòng)
- **Bước A — Component `VirtualMidiKeyboard`:**
- Floating window draggable, luôn trên timeline (`z-index` cao, không nuốt phím tắt transport Space/R).
- 2 octave keybed hiển thị mapping bảng spec §3: `Z S X D C V G B N M J` (root octave) + `Q 2 W 3 E R 5 T 6 Y 7 U I` (+1 oct), `O` (+2 C). Vẽ key trắng/đen theo Key Type.
- Control bar: Oct -/+, Octave Shift hiển thị (default C4), Velocity slider (1..127, default 100), Channel select (1..16, mặc định channel của track đang chọn/arm), Transpose, Scale Highlight (dropdown scale — phục vụ highlight, không bắt buộc chặn phím).
- Toggle: phím **F2** + menu **Tools → Virtual MIDI Keyboard**. Không chặn khi focus trong `<input>/<textarea>`.
- Pitch formula đúng spec: `Pitch = (Octave + 1) * 12 + PitchOffset` với root = C.
- KeyDown: bỏ `e.repeat`; highlight key; `dispatchMidiEvent NOTE_ON` (velocity 0..1 = slider/127); KeyUp: NOTE_OFF.
- **Live record:** nếu transport đang Record và track đang arm → tạo note `{ pitch, start_beat, duration_beats, velocity }` vào `item.source_data.notes`/`midiItems[].notes` của item MIDI đang mở (hoặc item active) → dispatch `DAW_STATE_UPDATED` để canvas redraw (pattern đã có trong app).
- Persist `localStorage`: `{ visible, x, y, octave, velocity, channel, transpose, scale }`; khôi phục khi mở.
- **Bước B — State + menu:**
- `const [vkbOpen, setVkbOpen] = useState(false)` + ref lưu rect.
- Global keydown listener cho F2 (tránh trùng transport), menu Tools thêm item.
- **Bước C — Âm thanh:** không code engine mới — chỉ gọi router. Nếu track chưa có instrument: giữ hành vi fallback hiện có (SF mặc định/Carla), kèm toast khi track chưa arm/chưa có instrument.
### 5.3 Verify (spec §IV checklist 1-3)
- F2 mở/đóng; kéo thả được; vị trí nhớ sau reload.
- Bấm Z/S/X/D/C/V/G/B/N/M/J/Q/2/W/3/E/R/5/T/6/Y/7/U/I → key sáng, âm phát (bridge VSTi nếu có, không thì SF fallback); octave +/- đổi cao độ đúng 12 semitone.
- Record: arm track → R → bấm phím → note hiện realtime trên timeline/piano roll → Stop → MIDI item hoàn chỉnh.
- Bấm phím trong ô text input → không trigger note (pass-through).
---
## PHASE 6 — CHORDS PANEL (spec Part II)
### 6.1 Giải pháp
- **Bước A — Component `ChordsPanel`** (modal centered / dock phải):
- Trigger: menu **Insert → Chords panel**, phím **Shift+K**, nút 🎼 trên piano roll toolbar.
- 3 tab: **Preset Library | Custom Builder | Internet/AI Search** (AI tab defer — xem 6.3).
- Filter: Style, Key, Scale, Search box.
- **Bước B — Preset Library** (bảng spec §III.2, hardcode 6 styles):
- Pop/Ballad `I-V-vi-IV`, Jazz/Neo-Soul `ii7-V7-Imaj7-VI7`, Cinematic/Epic `i-VI-III-VII`, EDM `vi-IV-I-V`, Lofi `i7-iv7-v7-i7`, R&B `Imaj7-iii7-IVmaj7-V13`.
- Mỗi preset: nút [Preview] (gửi note tạm qua router NOTE_ON/NOTE_OFF nhanh), [Insert to Timeline], [Favorite ★] (localStorage).
- **Bước C — Custom Builder:**
- Input: tên, genre tag, chord tokens (roman numeral `I-vi-IV-V` hoặc chord name `Cmaj7-Am9-Fadd9-G13`), rhythm pattern (whole/quarter/arpeggio/strum).
- Lưu `localStorage['daw_user_custom_chords']`; dispatch `CUSTOM_CHORD_SAVED` để panel tự refresh (spec §III.3).
- **Bước D — Chord insertion engine (spec §III.5):**
- Parse chord symbol → pitch array (ví dụ `Cmaj7` = [60,64,67,71]; áp voicing: root/first inversion/drop-2).
- Insert tại playhead (hoặc beat click): `StartBeat(Ck) = Beat_insert + Σ duration trước`; tạo `MIDINote { pitch, start_beat, duration_beats, velocity: 0.8 }` vào MIDI item active; dispatch `DAW_STATE_UPDATED`.
- Code parse thuần hàm → để service riêng `app/static/js/services/chordTheory.js` (test được, xem §6.4).
- **Bước E — Internet/AI Search:**
- Tận dụng `app/static/js/services/aiGateway.js` (đã có tool-calling infra). Thêm tool `search_or_generate_chords` + endpoint backend `/api/v1/ai/search-chords` (FastAPI, gọi model qua aiGateway/AI_PROXY hiện có).
- Kết quả hiển thị progression + [Listen Preview] [Save] [Insert] — spec §III.4.
- **Defer nếu AI backend chưa cấu hình key:** tab hiện thông báo "AI chưa cấu hình" thay vì crash — xem rủi ro.
### 6.2 Verify (spec §IV checklist 4-7)
- Shift+K / Insert menu mở panel; chọn Jazz Neo-Soul → Insert → `Dm7-G7-Cmaj7-A7` chèn đúng vị trí playhead, đúng duration.
- Custom builder lưu → xuất hiện lại sau reload; event CUSTOM_CHORD_SAVED refresh không cần reload trang.
- AI search (nếu cấu hình): query "Hotel California" → progression Bm-F#7-A-E7-G-D-Em-F#7.
### 6.3 Defer (`ponytail:`)
- Scale Highlight chặn phím ngoài scale (chỉ highlight, không chặn) — thêm khi có yêu cầu.
- Arpeggiation/strum preview phức tạp trong preset (chỉ sustain preview) — thêm khi cần.
- AI chord search phụ thuộc key model — tách phase riêng nếu chưa có key.
### 6.4 Test đơn vị (non-trivial logic)
- `chordTheory.js`: 1 file test nhỏ (Node assert, không framework) — parse `Cmaj7`, `Am9`, `Dm7`, `G13`, `I-vi-IV-V` với key C, voicing root/inversion/drop-2 ra đúng pitch array. Chạy: `node app/static/js/services/chordTheory.test.js`.
---
## PHASE 7 — BUILD TỔNG & VERIFICATION CHECKLIST
```bash
node build.mjs
python -m PyInstaller engine.spec --clean --noconfirm
cp -r dist/daw_engine/* src-tauri/target/release/daw_engine/
```
| # | Test | Kỳ vọng |
|---|------|---------|
| 1 | Export MIDI (project 2 track MIDI) | File .mid tải về, mở đúng, velocity 1..127 |
| 2 | Export Mix WAV loop 4 bars | Đúng độ dài, không fade ở biên, loop nối mượt |
| 3 | Export Mix WAV bình thường | Không regression, có FX CHAIN + master |
| 4 | Export mp3/ogg không server | Fallback WAV + toast |
| 5 | Master FX CHAIN | Header mới, builtin + VST 1 chain, realtime + export đúng |
| 6 | Track FX CHAIN qua nút FX TCP | Mở đúng track, chain đủ builtin + VST |
| 7 | Project cũ sau refactor | Chain cũ hiện đúng, không mất cấu hình |
| 8 | F2 Virtual MIDI Keyboard | Mở/đóng, bấm phím ra âm, record vào timeline |
| 9 | Chords panel | Insert progression đúng vị trí, custom lưu được |
| 10 | grep UI | Không còn "MASTERING PANEL"/"FX RACK PANEL" |
Commit theo từng phase (message tiếng Việt, kiểu `fix(export): ...`), `git status` sạch cuối mỗi phase.
---
## RỦI RO / LƯU Ý
- **WebView2 blob download** là nghi vấn chính cho cả Phase 1 & 2 — xác nhận bằng chẩn đoán trước khi chọn hướng fix (server-download là hướng an toàn nhất vì đã có pattern `/api/v1/audio/download/{file_id}`).
- **Rebuild engine bắt buộc** sau mọi đổi frontend — quên copy `dist/daw_engine/*` là test nhầm bản cũ (đã vấp ở phase trước).
- **Auth API**: endpoint export mới nên dùng `get_optional_user` (không bắt buộc token) cho đồng bộ với `/mix` hiện tại, nhưng nếu UI đã có token thì gửi kèm `Authorization: Bearer` (an toàn).
- **Không đụng logic DSP** khi refactor FX CHAIN (chỉ đổi tên/hợp nhất render) — giảm rủi ro regression âm thanh.
- **state cũ project**: `fxChain`/`vstFxChain`/`masteringSettings.vstFxChain` giữ nguyên schema — không migration dữ liệu.
+43
View File
@@ -0,0 +1,43 @@
# Build Layout — vị trí chuẩn (ổn định qua build & đổi code)
## Canonical outputs (đều gitignored, tái tạo từ build)
| Vị trí | Nội dung | Nguồn |
|---|---|---|
| `src-tauri/binaries/` | Sidecars + DLL runtime | `build/scripts/build_native_bridge.ps1` |
| `src-tauri/binaries/daw_vst_bridge-x86_64-pc-windows-msvc.exe` | Instrument bridge (VSTi + SF2/SF3/SFZ, SHM realtime, control 1-6) | CMake `daw_vst_bridge` |
| `src-tauri/binaries/fx_vst_bridge-x86_64-pc-windows-msvc.exe` | FX bridge (VST3 FX: `--render-fx`, `--scan`, `--open-fx-gui`, `--fx-gui`) | CMake `fx_vst_bridge` |
| `src-tauri/binaries/libfluidsynth-3.dll`, `jpeg62.dll` | Runtime DLL (fluidsynth, libjpeg-turbo) | vcpkg manifest install |
| `src-tauri/resources/daw_engine/` | Python engine đã đóng băng (PyInstaller) | `build/scripts/build_windows.ps1` → `dist/daw_engine` |
## External (shared, không nằm trong repo)
| Vị trí | Nội dung |
|---|---|
| `%USERPROFILE%\vcpkg` | vcpkg.exe + package cache (fluidsynth, libjpeg-turbo, pkgconf) |
| `%USERPROFILE%\vcpkg\packages\libjpeg-turbo_x64-windows\bin\jpeg62.dll` | Fallback nguồn jpeg62.dll khi manifest chưa tái tạo |
## Dev-time resolution (Python)
`app/core/native_render.py`:
- `find_bridge_exe()` — `SF_BRIDGE_PATH` → frozen (`_MEIPASS`/`exe_dir`) → `src-tauri/binaries/<sidecar>` → `install/` (legacy). Instrument bridge.
- `find_fx_bridge_exe()` — `SF_FX_BRIDGE_PATH` → cùng chuỗi. FX bridge.
Tauri sidecar rename: `binaries/daw_vst_bridge` (externalBin) → `daw_vst_bridge-x86_64-pc-windows-msvc.exe` — cả hai dạng đều được tìm.
## Build
```powershell
# Native bridges (C++): vcpkg manifest + sfizz + CMake → src-tauri/binaries
build/scripts/build_native_bridge.ps1
# Full desktop (engine PyInstaller + bridges + Tauri NSIS/MSI)
build/scripts/build_windows.ps1
```
`build/scripts/` là tracked (dù `build/` gitignored) — không xoá `build/` wholesale.
## Split bridges
- `daw_vst_bridge` — VSTi instruments, VST2 (.dll) + VST3 (.vst3), native GUI qua SHM control 4 (`OPEN_GUI`), SF2/SF3/SFZ (fluidsynth/sfizz), realtime FX chain in-process (control 6 `SET_FX_CHAIN`).
- `fx_vst_bridge` — VST3 FX only. One-shot: `--scan <dir>`, `--render-fx <job> --in --out`, `--open-fx-gui <job>` (cửa sổ rời), `--fx-gui <job>` (embed + HTTP `SF_FXGUI_PORT`). Không SHM, không fluidsynth/sfizz. VST2 FX không hỗ trợ (VST2 không introspect được — xem là instrument; ponytail: thêm AEffect hosting khi có nhu cầu).
+132
View File
@@ -0,0 +1,132 @@
# Thiết kế kiến trúc: Docker play âm VSTi ở client (giữ nguyên luồng cũ)
Ngày: 2026-08-15
Trạng thái: THIẾT KẾ (chưa sửa code sản phẩm). Linux làm thiết kế + spike khả
thi; code theo thoả thuận triển khai.
## 1. Mục tiêu
Docker (server headless + browser UI) có thể play âm VSTi ở client,
DỰA TRÊN kiến trúc cũ của docker: client tự phát âm (FluidSynthWASM / SonicSF),
không streaming realtime từ server. VSTi không chạy được trong browser →
chuyển thành asset SoundFont (SF2/SF3) render sẵn từ VSTi trên server, client
dùng nguyên luồng cũ.
## 2. Kiến trúc docker hiện tại (giữ nguyên)
- Server: FastAPI (`app/main.py`) + Celery worker; mount `/opt/daw_engine/vst3`,
`/opt/daw_engine/soundfonts`, `/opt/daw_engine/samples/pianobook`.
- `runtime.detect()`: docker → `environment=docker`; client dùng
FluidSynthWASM (`window.SonicSF`) cho mọi track soundfont.
- Play track: `scheduleMidiNoteDispatch` → (bridge không active) →
`window.SonicSF.playNote(...)` — phát SF2 đã load vào WASM.
- Preview VSTi hiện có: `preview_mode=quick_render` nếu `pedalboard` có —
server render clip ngắn ra WAV → client play `Audio`. KHÔNG phải realtime,
KHÔNG phải theo timeline.
- SoundFontConverter: SF2→SF3 (ogg) / SF3→SF2. LƯU Ý: client WASM KHÔNG decode
ogg → client cần SF2; SF3 chỉ dùng server-side (fluidsynth native).
## 3. Vấn đề
Track gán VSTi trong docker: `isVstTrackEngine=true`, `routeCarla=false`
(không có Carla), `nativeSf=false` → rơi vào `SonicSF.playNote` với preset
soundfont không tồn tại → câm (hoặc âm mặc định sai).
## 4. Thiết kế đề xuất: VSTi → SF2/SF3 asset → client luồng cũ
### Nguyên lý
Biến 1 nhạc cụ VSTi (VST3 + preset) thành 1 bộ samples render sẵn (per-note,
velocity layer), đóng gói SF2 (client) + SF3 (server native), đăng ký như
soundfont bình thường → client chọn instrument như soundfont → mọi luồng play
cũ (scheduleMidiNoteDispatch → SonicSF.playNote) hoạt động không đổi.
### Pipeline (server, mới)
1. **Registry instrument** (storage/instruments.json): `{instrument_id, vst_path,
preset_id|preset_bytes(.vstpreset), bank, program, render_params}`.
Preset nguồn: upload .vstpreset (VST3 state — pedalboard đọc được) hoặc
dùng state mặc định của plugin. (.dspreset DecentSampler: spike thất bại —
xem mục 6.)
2. **Note renderer** (Celery task, chạy 1 lần / cache):
- Với mỗi note 0..127 (hoặc dải cấu hình) × velocity layer (1-3):
`VST3Plugin.process(midi, duration, sr)` → PCM mono/stereo.
- Dải nốt: mặc định render root note mỗi 3 bán âm (41 sample/nhạc cụ), các
nốt còn lại pitch-shift bằng FluidSynth (SF2 root + correction) — giảm
dung lượng 4x; chất lượng chấp nhận được cho preview.
- Velocity layers: 1 (ff) hoặc 2 (mf+ff) theo cấu hình.
- Duration: 2-4s có fade; loop: nếu plugin phát sustain được (note on dài),
lấy loop region từ phần sustain ổn định; nếu không → one-shot + release.
- Percussion (bank 128): map nốt → chính nó, không pitch-shift.
3. **SF2 builder** (component MỚI phải viết): WAV samples → SF2 binary
(RIFF/sfbk: sdta+smpl, pdta: phdr/pbag/pgen/inst/ibag/igen/shdr).
Sau đó tái dùng `SoundFontConverter.convert_sf2_to_sf3` để có SF3 cho
server-side (tùy chọn).
4. **Lưu + đăng ký**: `storage/soundfonts/vsti_<id>.sf2` (+ .sf3); thêm vào
scan soundfont để API `listPlugins`/`listSoundfontInstruments` trả về như
soundfont thường (thêm cờ `origin: "vsti"`).
5. **Client** (đổi TỐI THIỂU): instrument picker hiển thị nhạc cụ VSTi như
soundfont; chọn → track `synth_engine = {type:"soundfont", soundfont_id,
bank, program}` → MỌI luồng play cũ chạy nguyên vẹn (SonicSF.playNote).
### Cache & vòng đời
- Key: `sha256(vst_path + preset_bytes + render_params)` → build 1 lần, tái
dùng. API `POST /api/v1/instruments/{id}/build` + `GET .../status` (queued/
building/ready/failed).
- Xoá instrument → xoá asset kèm.
### API mới (server)
- `GET /api/v1/instruments` — danh sách (kèm trạng thái build).
- `POST /api/v1/instruments` — tạo (vst_path, preset upload, params).
- `POST /api/v1/instruments/{id}/build`, `GET /api/v1/instruments/{id}/status`.
- `DELETE /api/v1/instruments/{id}`.
- Không đổi API phát lại hiện tại.
## 5. Ước lượng & giới hạn
- 41 sample × 1 velocity × 3s × 44.1k stereo PCM ≈ 43 MB → ogg (SF3) ≈ 8-12 MB;
client SF2 giữ PCM (WASM không decode ogg) ≈ 40 MB — lớn. Giảm: mono (~20MB),
giảm dải nốt (mỗi 4-6 bán âm), velocity 1, duration 2s. Chấp nhận tradeoff
preview vs dung lượng; tham số hoá trong render_params.
- Build tốn CPU (41 note × 2-4s render, 1 nhạc cụ ~2-5 phút trên 1 core) →
chạy Celery worker, cache vĩnh viễn.
- Chất lượng: pitch-shift bằng FluidSynth không bằng plugin thật; âm VSTi
phức tạp (modulation, chorus) mất chi tiết khi đóng gói sample. Đây là giới
hạn của hướng "giữ luồng cũ client-side".
## 6. Kết quả spike (đã chạy trong container sonicforgestudio-web-1)
- `pedalboard 0.9.19` có sẵn ✓; `DISPLAY=:99` ✓.
- `VST3Plugin.load` OK: DecentSampler.vst3; instrument API
`plugin.process(midi, duration, sr)` chạy (trả shape 2×88200) ✓.
- SCAN FAIL: Vital.vst3, Surge_XT.vst3 — "unsupported plugin format or scan
failure" (JUCE không scan được trong container; nghi thiếu thư viện của
plugin bundle — cần khảo sát tiếp: ldd .so bên trong .vst3, thử cài libs).
- PRESET FAIL: mọi .dspreset pianobook load thất bại trong container:
- "failed to load data from preset file" (1 preset/process)
- "database is locked" (nhiều preset/process — DecentSampler dùng SQLite nội
bộ; có thể cần HOME writable + 1 instance/process + chdir preset dir —
chdir đã thử).
- App có `DecentSamplerManager.create_decent_sampler_instance` (chdir preset
dir) — chưa verify được trên Linux container; có thể chỉ chạy trên Windows.
- Chưa test được .vstpreset (VST3 state) vì không có VST scan OK để áp.
### Việc cần làm trước khi chốt code
1. Điều tra scan fail Vital/Surge: `ldd` .so trong bundle, thử cài
libvulkan/libgl extra; tìm 1 VST3 đơn giản scan được (ví dụ plugin test
hoặc VST3 SDK example) để có plugin "chuẩn" cho pipeline.
2. Verify .vstpreset flow: tạo preset thật từ Windows (Carla) → upload →
pedalboard container render thử.
3. Điều tra DecentSampler preset: chạy thử với HOME=writable, 1 instance,
chdir; nếu vẫn fail → loại .dspreset khỏi phạm vi v1, dùng .vstpreset.
4. Viết SF2 builder + unit test đọc lại bằng pyfluidsynth (pattern
`_sf3_plays_audio`).
## 7. Kế hoạch triển khai (sau khi spike ổn)
1. Spike mở rộng (mục 6) — chọn plugin/preset khả thi.
2. Server: SF2 builder + note renderer + registry + API + Celery task.
3. Server: đăng ký asset vào scan soundfont.
4. Client: instrument picker hiển thị nhạc cụ VSTi (origin=vsti); chọn →
synth_engine soundfont. KHÔNG đổi luồng play.
5. Test: chọn nhạc cụ VSTi → play timeline → nghe đúng âm; xác nhận luồng cũ
(WASM) không đổi cho soundfont thường.
## 8. Ranh giới
- Docker/standalone tách riêng: pipeline server chỉ bật khi
`runtime.environment=docker` (hoặc mọi server headless); standalone Windows
giữ Carla/bridge như cũ.
- Thiết kế này không sửa code sản phẩm; commit code theo thoả thuận sau khi
spike + chốt phương án.
+102
View File
@@ -0,0 +1,102 @@
# Thiết kế: tương thích ngược VSTi cũ (VST2, 32-bit) — bridge âm thanh
Ngày: 2026-08-15
Trạng thái: THIẾT KẾ (chưa sửa code sản phẩm). Linux làm thiết kế + spike khả
thi; code theo thoả thuận triển khai (Windows).
## 1. Mục tiêu
Standalone bridge (`standalone-shm-bridge`) hiện chỉ host VST3. Nhiều VSTi cũ
(vintage, freeware, 32-bit) chỉ có bản VST2. Mục tiêu: VST2 64-bit phát được qua
bridge chính (headless, realtime), không phá luồng VST3 hiện tại.
## 2. Hiện trạng (đã xác minh trên code)
### VST2 không được implement trong bridge
- `native_bridge/src/NativeInstrumentEngine.cpp:166-180` — `create_instrument()`:
`case VST2: return nullptr` + comment
"ponytail: VST2 host (VST2.4 SDK, Steinberg discontinued) not implemented".
- Enum đã có sẵn: `native_bridge/src/INativeInstrument.h:8-12`
(VST3=0, VST2=1, SF2/SF3=2, SFZ=3).
- `StateStore.h:14` — type 1 = VST2.
### Backend đã nhận diện VST2 (scan + UI)
- `app/api/v1/plugins.py:117-130,224-247` — file `.dll` (không phải folder
`.vst3`) → `"type":"VST2"`.
- `app/vst_engine.py:282-300` — scan `.vst3/.so/.dll`.
- JS mapping: `nativeBridgeService.js:7` `INSTRUMENT_TYPE={VST3:0,VST2:1,SF2:2,SF3:2,SFZ:3}`.
→ Hệ quả: UI liệt kê plugin VST2 là loại hợp lệ, user assign → bridge
`create_instrument` trả nullptr → `assign` false → fail **im lặng**, không có
lỗi nào hiển thị. Đây là bug UX chính cần sửa kèm.
### Đường khác không thay thế
- Carla (Windows) hỗ trợ VST2 nhưng là đường GUI/preset riêng
(`plugins.py:530-630` carla-midi/carla-play-notes/open-in-carla), KHÔNG phải
đường audio bridge chính.
- Preset `.fxp/.fxb` nằm trong `PRESET_EXTENSIONS` (`vst_engine.py:592`) nhưng
`apply_preset_to_plugin` gọi `load_preset` của VST3Plugin — không chạy VST2.
### Hạ tầng tái dùng được
- `SandboxVst3Host` + `SandboxHostIPC` double-buffer
(`SharedMemoryIPC.h:33-39`), `plugin_host_main.cpp`, env `SF_SANDBOX_VST3` —
pattern sẵn cho helper process 32-bit.
- Build: `native_bridge/vcpkg.json` chỉ fluidsynth+pkgconf; VST3 SDK là git
submodule `vst3sdk` (không có vestige — cần thêm header nếu làm VST2 host);
CMake `-EHa`.
## 3. Thiết kế đề xuất
### 3.1. VST2 64-bit — host `Vst2Instrument` dùng vestige (làm trước)
- **vestige** (`vestige/aeffect.h`, mã nguồn mở, từ dự án LMMS): header-only,
tự định nghĩa lại interface + ABI VST2.4 (vtable layout, dispatcher opcode).
Không cần Steinberg VST2 SDK (đã discontinued, license không cấp mới) →
sạch pháp lý, zero dependency build, không submodule mới.
- `Vst2Instrument` implement cùng interface `INativeInstrument`:
assign / SHM double-buffer / renderAll tái dùng nguyên trạng.
- Phạm vi audio headless: `processReplacing`, `setSampleRate`, `setBlockSize`,
`resume`/`suspend`, `getParameter`/`setParameter`, `effGetChunk`/`effSetChunk`
(preset .fxp/.fxb), `effGetParamLabel` (tên tham số cho UI).
- GUI editor: **defer** (không window, không thread GUI). Nhiều VSTi cổ vẫn
phát headless; plugin nào đòi GUI mới ra tiếng thì ghi rõ giới hạn ở UI.
- Sửa kèm bug im lặng: khi `create_instrument` trả nullptr → trả lỗi rõ ràng
lên UI thay vì fail câm.
### 3.2. VST2 32-bit — helper process (defer, effort cao)
- Helper `plugin_host.exe` 32-bit, tái dùng pattern `SandboxVst3Host` +
`SandboxHostIPC` double-buffer + env `SF_SANDBOX_VST3`.
- Lý do defer: cần bản build 32-bit riêng, xử lý wow64 + marshalling
(MIDI/sample rate/block size qua IPC), khó debug. Làm sau khi 64-bit ổn định.
### 3.3. Quirk plugin cũ (khi test thực tế)
- Block size: thử 256 / 512 / 1024 (nhiều plugin cũ chỉ nhận bội số cố định).
- Sample rate: 44.1 kHz là mặc định an toàn nhất.
- Thread affinity: open + process cùng 1 thread; tránh đổi thread giữa
`resume` và vòng render.
- Plugin 32-bit đòi GUI: một số chỉ phát sau khi editor từng mở — ghi rõ
giới hạn, không cố host GUI trong bridge.
### 3.4. Giới hạn docker/server
- `pedalboard` không hỗ trợ VST2 → pipeline VSTi→SF2 (`DESIGN_DOCKER_VSTI_PLAY.md`)
chỉ nhận VST3. VST2 không render được asset SF2 trên server. Ghi rõ ở UI.
## 4. Kiến trúc thêm mới (tối thiểu)
```
native_bridge/
vestige/aeffect.h # header mã nguồn mở (thêm file)
src/Vst2Instrument.h/.cpp # host VST2, implement INativeInstrument
src/NativeInstrumentEngine.cpp # case VST2 -> new Vst2Instrument
```
Không đổi ABI, không đổi `SharedMemoryIPC`, không đổi JS service layer.
## 5. Rủi ro / giới hạn
- Vài plugin rất cổ đòi host callback lạ (`effGetPlugCategory`, vendor opcode)
→ patch nhỏ riêng từng plugin.
- Vestige không cung cấp editor GUI → plugin phụ thuộc GUI sẽ câm.
- ABI VST2 ổn định từ 1999 → 64-bit .dll hiện đại + cổ đều theo, rủi ro thấp.
## 6. Thứ tự triển khai
1. `Vst2Instrument` + vestige: load, assign, render thử 1 plugin VST2 64-bit.
2. Sửa bug fail im lặng khi không host được.
3. Preset .fxp/.fxb qua `effSetChunk`/param.
4. Test block size / sample rate / thread affinity với plugin cổ.
5. (Defer) 32-bit helper process.
+27
View File
@@ -37,6 +37,33 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# ── Build native_bridge (Linux) — thay pedalboard GPL-3.0 ──────────────────
# Xem PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md §4. sfizz khong co trong apt
# Debian → build tu nguon vendored (native_bridge/sfizz-src, co trong build
# context) hoac git clone fallback khi thieu. Bridge chay --render duoi
# Xvfb :99 (DISPLAY da set o tren) — khong can GUI plugin editor.
RUN apt-get update && apt-get install -y --no-install-recommends \
cmake pkg-config libfluidsynth-dev libfreetype-dev git \
&& rm -rf /var/lib/apt/lists/* \
&& if [ -d native_bridge/sfizz-src ]; then cp -r native_bridge/sfizz-src /tmp/sfizz; \
else git clone --recursive --depth 1 https://github.com/sfztools/sfizz.git /tmp/sfizz; fi \
&& cmake -S /tmp/sfizz -B /tmp/sfizz/build -DCMAKE_BUILD_TYPE=Release \
-DSFIZZ_SHARED=ON -DSFIZZ_JACK=OFF -DSFIZZ_RENDER=OFF -DSFIZZ_TESTS=OFF \
-DSFIZZ_BENCHMARKS=OFF -DSFIZZ_DEMOS=OFF -DSFIZZ_DEVTOOLS=OFF \
-DSFIZZ_GIT_SUBMODULE_CHECK=OFF -DBUILD_TESTING=OFF \
&& cmake --build /tmp/sfizz/build --target sfizz_shared -j"$(nproc)" \
&& cmake --install /tmp/sfizz/build \
&& sed -i 's/-llibsfizz/-lsfizz/' /usr/local/lib/pkgconfig/sfizz.pc \
&& cmake -S native_bridge -B /tmp/bridge-build -DCMAKE_BUILD_TYPE=Release \
-DSMTG_ENABLE_VST3_PLUGIN_EXAMPLES=OFF \
-DSMTG_ENABLE_VST3_HOSTING_EXAMPLES=OFF \
-DSMTG_ENABLE_VSTGUI_SUPPORT=OFF \
&& cmake --build /tmp/bridge-build --target daw_vst_bridge -j"$(nproc)" \
&& mkdir -p /app/install \
&& cp /tmp/bridge-build/daw_vst_bridge /app/install/ \
&& rm -rf /tmp/sfizz /tmp/sfizz/build /tmp/bridge-build
# Copy VST3 plugins if present
COPY ./vst_plugins/ /opt/daw_engine/vst3/
+67
View File
@@ -0,0 +1,67 @@
# Fix: Drag & drop MIDI file từ Explorer vào track (Windows/Tauri)
Ngày: 2026-08-15
Phạm vi: chỉ ghi đề xuất, chưa sửa code. Pull về Windows rồi sửa.
## Triệu chứng
Trên Windows (bản Tauri), kéo file `.mid` từ Explorer vào timeline:
- Không tạo track, không toast, không console error.
## Nguyên nhân gốc
1. Tauri default bật `drag_drop_handler_enabled = true`; app không gọi
`disable_drag_drop_handler()`.
- `tauri-2.11.1/src/webview/mod.rs:973-977` — doc ghi rõ:
"Disables the drag and drop handler. **This is required to use HTML5
drag and drop APIs on the frontend on Windows.**"
2. Khi handler bật, `tauri-runtime-wry` bắt sự kiện drop và emit `DragDropEvent`
sang Rust (`tauri-runtime-wry-2.11.1/src/lib.rs:4861`), không đẩy vào DOM.
3. `wry-0.55.1/src/webview2/mod.rs:150-155` — khi có drag_drop_handler thì gọi
`SetAllowExternalDrop(false)` (comment: "Disable file drops, so our handler
can capture it").
4. Kết quả: drop file OS vào WebView2 → HTML5 DnD không hoạt động →
`e.dataTransfer.files` rỗng tại `app/static/js/app.jsx:29994` và `:30034`
→ `if (!f) return;` → im lặng.
5. App không xử lý `WindowEvent::DragDrop` ở Rust, không cài
`tauri-plugin-drag-drop` → drop OS file bị nuốt hoàn toàn.
## Đã verify
- Logic JS drop đúng: chạy engine + headless chromium, dispatch drop event với
DataTransfer chứa `/static/midi/MIDI_Loop_01.mid` → tạo track + toast
"Đã tải MIDI: 1 track(s)" (ROWS 1→2). `parseMidiFile` OK (1 item, 48 notes).
- Drop từ Media Explorer trong app (dùng `window.__mediaExplorerDragFile` +
`resolveMediaExplorerDropFile`, app.jsx:24629, 15029) KHÔNG dùng
`dataTransfer.files` → không bị ảnh hưởng bởi `SetAllowExternalDrop(false)`,
vẫn hoạt động trên Windows.
- Drop trong browser thường (Linux dev): hoạt động.
- Section-tab/sub-tab timeline (~app.jsx:30600): KHÔNG có onDrop — drop vào
section tab không làm gì (bug phụ; wiki.md:1806 tuyên bố hỗ trợ nhưng code
không có).
## Đề xuất sửa (chọn 1 trong 2; làm trên Windows)
### Phương án A — Bật HTML5 DnD (đơn giản nhất)
Gọi `disable_drag_drop_handler()` khi tạo cửa sổ trong code Rust
(ví dụ `src-tauri/src/lib.rs`, trong builder WebviewWindow). Không có option
trong `tauri.conf.json` — phải sửa code.
Sau khi gọi, WebView2 nhận drop như browser bình thường, JS handler tại
`app.jsx:29980` (container) và `:30024` (track row) tự chạy, không đổi JS.
Lưu ý: mất event `WindowEvent::DragDrop` ở Rust (hiện không dùng).
### Phương án B — Xử lý ở Rust, đẩy path sang JS
Giữ drag_drop_handler default, thêm `.on_window_event` (hoặc dùng
`tauri-plugin-drag-drop`) lắng nghe `WindowEvent::DragDrop(DragDropEvent::Drop(path))`,
emit path sang frontend (vd `emit("midi-drop", path)`), JS nhận và chạy nhánh
giống `resolveMediaExplorerDropFile` (dùng `mef.path`).
Phức tạp hơn A, cần thêm listener + code JS.
## Bug phụ (nếu muốn sửa luôn)
Thêm `onDrop` cho section-tab/sub-tab timeline (~app.jsx:30600) giống handler
container/track row, để drop vào section tab cũng tạo track.
## Vị trí file liên quan
- `app/static/js/app.jsx:29980` (onDrop container), `:30024` (onDrop track row)
- `app/static/js/app.jsx:29994`, `:30034` (điểm `dataTransfer.files[0]` check)
- `app/static/js/app.jsx:24629` `resolveMediaExplorerDropFile`
- `src-tauri/src/lib.rs` (nơi cần gọi `disable_drag_drop_handler()`)
+92
View File
@@ -0,0 +1,92 @@
# Fix: VSTi play không smooth / cracking / noise trên standalone (Windows/Tauri)
Ngày: 2026-08-16
Phạm vi: Linux review code — ghi nguyên nhân + hướng fix, KHÔNG sửa code.
Pull về Windows sửa.
## Triệu chứng
Load VSTi instrument và play: âm không smooth, không clear; âm sắc thỉnh
thoảng cracking/noise.
## Đường âm hiện tại (đã trace trong code)
1. C++ bridge (`native_bridge/src/main.cpp`) render VST3 vào SHM
**1 slot 256 frame** (`main.cpp:759`, `main.cpp:1414` — render xong mới
tăng `bridgeWriteIndex`).
2. Rust pump thread poll mỗi ~1ms, **chỉ đọc block mới nhất**
(`src-tauri/src/lib.rs:454-469`).
3. Mỗi block phát 1 event `bridge-audio` qua Tauri IPC
(`lib.rs:467`, `app/static/js/services/nativeBridgeService.js:50`).
4. JS push vào ring 24 block (`bridgeAudioNode.js:55-65`), drain bằng
**ScriptProcessorNode 4096** (`bridgeAudioNode.js:29-53`).
## Nguyên nhân gốc
### 1. SHM single-slot, không phải ring buffer
`native_bridge/include/SharedMemoryIPC.h:17-21` — chính code ghi chú:
"volatile is NOT a sync primitive. Real impl should use an interlocked/
index-flag pair or a Win32 event (SetEvent) signalled by the writer."
C++ ghi đè cùng 1 slot mỗi vòng ~5.3ms (256/48000); Rust pump ngủ 1ms + emit
async → **chỉ cần trễ > 1 block period là block bị ghi đè trước khi đọc** → gap.
### 2. Rust pump đọc-latest, không drain delta
`src-tauri/src/lib.rs:454-466`: `if idx != last_index` → nếu pump/WebView bị
jank (GC, React render, IME), index nhảy 2+ → **chỉ đọc block cuối, các block
trước bị bỏ im lặng** → click/gap.
### 3. 1 event/block qua Tauri IPC
`lib.rs:467` + `nativeBridgeService.js:50`: ~187 event/s (48000/256), mỗi event
serialize Vec<f32> → đè main thread WebView → càng jitter, càng drop block.
### 4. Consumer bằng ScriptProcessor chạy trên MAIN THREAD
`bridgeAudioNode.js:28-53`: callback trễ → `if (!_chunks.length) return;` →
**fill silence → gap**. Overrun: `while (_chunks.length > RING_DEPTH)
_chunks.shift()` (`bridgeAudioNode.js:63-64`) → **drop block cũ → click**.
Không có pacing theo thời gian — drain "có gì chơi nấy".
### 5. Không có drift compensation
C++ pace bằng steady_clock (`main.cpp:1424`), WebAudio chạy theo device clock.
Ring 24 block (~139ms) chỉ hấp thụ jitter tạm; lệch nhịp kéo dài → underrun/
overrun định kỳ → cracking "lâu lâu" (đúng triệu chứng).
### Yếu tố VSTi phụ (giải thích "không clear / âm sắc lạ")
- **Không latency compensation**: `getLatencySamples` chưa dùng
(`native_bridge/src/Vst3Instrument.cpp:816-870`) — VSTi có delay nội bộ
(chorus/reverb/lookahead) bị dịch pha → âm bẩn.
- **Tempo hardcode 120**: `Vst3Instrument.cpp:357` `processContext.tempo =
120.0`, không update khi play — LFO/arp tempo-sync chạy sai BPM.
- **Block 256 + sample-accurate split**: `main.cpp:1400-1413` gọi
`processAudioBlock` theo sub-block (vd 100+156) — vài plugin nhạy block-size
tạo artifact nhẹ.
- `channelQuiet` mute channel khi mở editor (`main.cpp:759` renderAll skip) —
cố ý, không phải lỗi.
## Hướng fix (Windows thực hiện)
1. **SHM multi-slot ring (4-16 slot)** cho main bridge — hoặc dùng đúng pattern
double-buffer đã có sẵn `SandboxHostIPC` (`SharedMemoryIPC.h:33-39`) cho
`daw_vst_bridge` (hiện main path dùng `SharedAudioBufferIPC` single-slot).
2. **Rust pump drain đủ delta**: vòng lặp đọc/phát tất cả block chênh lệch
(không chỉ latest), hoặc batch emit mỗi 4-8 block để giảm IPC overhead.
3. **Thay ScriptProcessor bằng AudioWorklet** (chạy trên audio thread, không
đè main thread) + ring ≥ 32 block + timestamp.
4. **Drift**: monitor buffer level (ring fill) → resample/throttle nhẹ để giữ
ring ổn định, không để cạn/tràn.
5. **Latency compensation**: đọc `processor->getLatencySamples()` sau
setupProcessing, trừ vào `projectTimeSamples` / bù offset khi render.
6. **Tempo sync**: push BPM thật qua control queue mỗi lần play (hiện chỉ có
TRANSPORT/LOAD — thêm field tempo hoặc event mới), cập nhật
`processContext.tempo` + `timeSigNumerator/Denominator`.
## Vị trí file liên quan
- `native_bridge/include/SharedMemoryIPC.h` (SHM layout, single-slot)
- `native_bridge/src/main.cpp` (audio loop, renderSegment, pacing)
- `native_bridge/src/Vst3Instrument.cpp` (processAudioBlock, setupProcessing,
processContext)
- `src-tauri/src/lib.rs` (audio pump thread, emit bridge-audio)
- `app/static/js/services/nativeBridgeService.js` (listener bridge-audio)
- `app/static/js/services/bridgeAudioNode.js` (ScriptProcessor sink, ring)
## Ưu tiên
P1: #2 + #4 (drop block + drift) — bỏ crackle phần lớn.
P2: #3 (AudioWorklet) — hết jank main-thread.
P3: #1 (multi-slot ring) — chắc chắn không mất block dù pump trễ.
P4: #5/#6 (latency + tempo) — hết "không clear/âm sắc lạ".
+36
View File
@@ -0,0 +1,36 @@
# G2 — JUCE VST3 hosting + latency thật (FxLatReport)
Trạng thái: **DONE** — đã verify qua probe, build pass.
## Thay đổi
- `native_bridge/juce_fx/JuceFxEngine.cpp` — `setChain(chainJson)`: parse mảng fx_chain
(`sheredom_json.h`), load VST3 qua `juce::VST3PluginFormat::findAllTypesForFile` +
`createInstanceFromDescription`; preset_b64 → `setStateFromVSTPresetFile` (deprecated,
vẫn dùng được); nối node nối tiếp in→p0→…→out; sau `prepareToPlay`:
`slotLatencies[i] = getLatencySamples()` từng plugin, `totalLatency = graph.getLatencySamples()`.
`entryLatencies()` trả `std::vector<uint32_t>` theo thứ tự slot (skip/bypass/fail = 0).
- `JuceFxEngine.h` — khai báo `struct Slot;` + `setChain`/`entryLatencies`.
- `JuceFxLoop.cpp` — đọc `chainJson` từ job, gọi `engine.setChain` trước prepare;
`reportLatencies` báo từng slot qua FxLatReport (giữ giao thức cũ).
- `juce_fx/CMakeLists.txt` — bật `JUCE_PLUGINHOST_VST3=1` (JUCE mặc định 0 → class
VST3PluginFormat bị ẩn, gây C3861/C2039).
- `src-tauri/binaries/juce_fx_bridge-x86_64-pc-windows-msvc.exe` — build mới (G2).
## Verify (debug/g0_latency_probe.py, exe mới)
| config | plugin_latency | drained | recv/sent | roundtrip mean |
|---|---|---|---|---|
| empty chain | `{}` | True | 10000/10000 | 21.57 ms |
| pureComp | `{0: 2108}` | True | 10000/10000 | 21.70 ms |
| Ozone 11 | `{0: 0}` | True | 10000/10000 | 21.69 ms |
- Latency thật đúng dự đoán: pureComp ~2108 samples @48k, Ozone 11 không delay.
- Không crackle, drained=True, recv=sent — benchmark G1 giữ nguyên.
## Ghi chú
- `setStateFromVSTPresetFile` là API deprecated nhưng public (bên trong `#ifndef DOXYGEN`),
compile được với `JUCE_PLUGINHOST_VST3=1`.
- Bỏ hằng `(7+8)*256` trong `fxRtApplyPdc` (app.jsx) là việc của G3 (dùng latencyTotal
thật từ WS — hiện `FX_RT_ROUNDTRIP_SAMPLES=3840` là ước lượng RTT pipeline, ponytail).
+130
View File
@@ -0,0 +1,130 @@
# IMPLEMENT_PHASE_3 — Nhật ký thực hiện BIG_PHASE_3
> Cập nhật theo từng phase. Trạng thái: ✅ hoàn thành · 🔄 đang làm · ⏳ chưa làm · ⚠️ cần xác nhận/review
> Repo: `C:/Users/locpham/SonicForgeStudio` · branch: `standalone-shm-bridge`
## TỔNG QUAN
| Phase | Nội dung | Trạng thái | Ghi chú |
|-------|----------|-----------|---------|
| 1 | Fix Export MIDI | ✅ | backend + frontend + build + verify xong (commit riêng) |
| 2 | Fix Export Mix + loop | ✅ | frontend + build + verify xong (commit riêng) |
| 3 | FX CHAIN (hợp nhất modal) | ✅ | commit `5fc45a5` |
| 4 | Nút FX TCP → FX CHAIN | ✅ | gộp commit Phase 3 |
| 5 | Virtual MIDI Keyboard | ✅ | |
| 6 | Chords Panel | ✅ | AI search defer |
| 7 | Build tổng + verify | ✅ | |
---
## PHASE 1 — FIX EXPORT MIDI
### Đã làm
- [x] Khảo sát: 2 nơi export MIDI đều dùng blob download (`a.click()` + `URL.createObjectURL`) — nghi vấn WebView2 chặn (đã fix bằng server download, verify chạy thật OK).
- [x] Backend: thêm `POST /api/v1/export/midi` vào `app/api/v1/audio.py` — sinh file .mid format 1 (stdlib thuần), lưu vào PROCESSED_DIR, trả `file_id` + `download_url` (download qua GET network — pattern đã chạy ổn định). Bỏ track không nốt; velocity 0..1 → scale 1..127.
- [x] Frontend: thêm helper `serverDownloadMidi(payload, filename)` (POST endpoint → anchor download `${API_BASE_URL}${download_url}`); đổi `triggerMidiExport()` (File menu, bỏ velocity `*100`, bỏ rest-track pitch 0) + nút piano roll "Export MIDI" (payload 1 track, guard không nốt).
- [x] Build: `node build.mjs` OK + PyInstaller rebuild engine + copy `dist/daw_engine/*` sang `src-tauri/target/release/daw_engine/` + bump cache-bust `app.precompiled.js?v=202608211103`.
- [x] Verify: chạy app thật (engine mới), POST `/export/midi` → GET download → parse bytes: format 1, division 480, đủ track, velocity đúng (0.5→64, 1.0→127, 0.8→102), track rỗng bị bỏ (2→1 track).
### Tồn tại / cần bổ sung
- (trống)
### Cần xác nhận (review)
- (trống)
---
## PHASE 2 — FIX EXPORT MIX + LOOP
### Đã làm
- [x] Khảo sát: 3 đường xuất — server `POST /multitrack/mix` (cần `allOnServer && connected`), client `clientSideExport` (OfflineAudioContext, raw buffer không fade), bounce `triggerBounceExport` (realtime capture master bus, trước đây luôn +1.2s tail + blob download). Worker `app/tasks/worker.py` tôn trọng `fade_in_ms/fade_out_ms` từ clip.
- [x] UI ExportModal: checkbox "Xuất loop (không fade in / không fade out)" + input "Số ô nhịp" (1..256, mặc định 4).
- [x] Helper `downloadBlobViaServer(blob, filename)`: POST `/api/v1/audio/upload` → download `${API_BASE_URL}/api/v1/audio/download/{file_id}`; fail → fallback blob download + throw (pattern Phase 1).
- [x] `triggerWavExport`: khi loop → cắt buffer từng track đúng `[loopStart, loopStart+loopDur)` (`loopSecPerBar=(60/bpm)*4`, `loopStart` = selection nếu hợp lệ else playhead), clip `export_loop_clip` startTime 0, luôn đi client path (buffer đã cắt phía client), gọi `clientSideExport(exportTracks, {loopStart, loopDur})`.
- [x] `clientSideExport(activeTracks, loopInfo)`: `durationLimit = loopDur` khi loop (không kéo đuôi); midi cache buffer cắt `subarray(0, loopDur*sr)`; **fix bug**: midi cache không được kéo dài `durationLimit` khi loop (file không dư đuôi); WAV thay blob download bằng `downloadBlobViaServer`.
- [x] `triggerBounceExport`: khi loop → `durationLimit = loopDur` (bỏ +1.2 tail), sau capture cắt cứng `[loopStart, loopStart+loopDur)` frames khỏi stereo buffer, download qua `downloadBlobViaServer`.
- [x] Build: `node build.mjs` OK (2 lần — lần 2 sau fix durationLimit) + PyInstaller rebuild engine + copy `dist/daw_engine/*` sang `src-tauri/target/release/daw_engine/` + bump cache-bust `app.precompiled.js?v=202608211105`.
- [x] Verify chạy thật: engine serve `app.precompiled.js` mới (md5 khớp build local, đủ loop UI/loopActive/downloadBlobViaServer/export_loop_clip), index.html `?v=202608211105`; `/upload` + `/download` roundtrip OK; `POST /multitrack/mix` (server path) SUCCESS → file 2.0s đúng, all samples nonzero (không regression).
- [x] Self-check toán loop (node `tools/_loopcheck.mjs`): độ dài WAV = `loopBars*4*60/bpm` ±0 sample với mọi tổ hợp (120bpm×4, 120×4 target 48k, 90×8 start 1.5s, 140×1), biên đầu/cuối giữ nguyên mẫu (không fade) — PASS.
- [x] Regression cũ: `tools/shm_note_energy.py` (energy 0 — app chưa load instrument, đúng), `tools/test_sf2_ring.py` PASS (energy 12.337, ring decay), `tools/test_vst2_gui.py` PASS (DUNE host + editor VSTGUI, proc alive).
### Tồn tại / cần bổ sung
- Loop với track MIDI: midi cache cắt từ đầu cache (`subarray(0, loopDur*sr)`), chưa offset theo `loopStart` — đúng với thiết kế cache preview (cache bắt đầu từ preview), ghi nhận để Phase 7 review nếu cần.
### Cần xác nhận (review)
- (trống)
## PHASE 3 — FX CHAIN (BUILTIN DSP + VST FX 1 CHAIN)
- [x] `FXRackModal`: header `{track.name} — FX RACK PANEL` → `{track.name} — FX CHAIN` (grep sạch "FX RACK PANEL" còn lại chỉ comment nội bộ dòng 11478).
- [x] Gộp CHAIN RACK STRIP + block VST FX CHAIN thành MỘT strip `unifiedChain = [...chain.map(m=>({kind:'builtin',m,i})), ...vstChain.map(s=>({kind:'vst',s,i}))]`:
- Slot builtin = card cũ nguyên vẹn (power/toggleMod, name, remove, click → `setActiveType(m.type)`).
- Slot vst = card compact (power/toggleVst, name, removeVst, click → `setActiveVstIdx(u.i)`, active = viền teal).
- Drag reorder trên unifiedChain → split lại `setChain(nb)` + `setVstChain(nv)`; khi kéo slot vst → `setActiveVstIdx(nv.indexOf(mv.s))` giữ đúng slot đang chọn.
- 2 nút thêm cuối strip: `[+]` (addModuleOpen, DSP) + `[VST]` (vstPickerOpen).
- [x] Giữ toolbar VST mỏng dưới strip: "Nghe thử (render offline)" (`previewFx`) + checkbox Bypass + hint VST3 áp dụng lúc Export/Offline.
- [x] Workspace: khi `activeVstIdx>=0 && < vstChain.length && !fxGuiOpen` → hiện detail VST đầy đủ (vstRow: select đổi plugin, Preset upload, GUI, Carla, lên/xuống, xóa); ngược lại → module editor builtin như cũ. `addMod` giờ reset `setActiveVstIdx(-1)` để quay về editor builtin khi thêm DSP.
- [x] `MasteringModal`: header `MASTERING SUITE` → `MASTER FX CHAIN` (giữ badge "WEB MASTERING V10.5" làm subtitle); popup thêm module → `THÊM MODULE VÀO MASTER FX CHAIN`. Chain strip master đã unified sẵn từ trước (builtin + vst3 1 strip).
- [x] Nút "MASTERING PANEL" (MasterStripConsole) → "FX CHAIN"; tooltip "Mở/Bật-Tắt MASTERING PANEL ..." → "MASTER FX CHAIN ..."; menu File "Mastering Suite" → "Master FX Chain" (giữ Ctrl+Shift+M).
- [x] Phase 4 verify sẵn: `__openFxRack(track.id, track.name)` → `fxRackTarget` → `FXRackModal` lookup track giờ dùng `(activeTracksRef.current || []).find(...) || tracks.find(...) || null` — an toàn khi activeTracks là session-tab clones ≠ root tracks (trước: `tracks.find` có thể null → modal không mở).
- [x] Build: `node build.mjs` OK (2 lần — lần 2 sau fix lookup); copy `app/static/js/app.precompiled.js` + `app/templates/index.html` sang bundle `src-tauri/target/release/daw_engine/_internal/app/` (frontend-only, không cần PyInstaller); bump cache-bust `?v=202608211133`.
- [x] Verify chạy thật: engine serve precompiled md5 `9d49f209...` khớp build local, index `?v=202608211133`, bundle chứa "FX CHAIN"/"MASTER FX CHAIN"/`unifiedChain`/lookup activeTracksRef; 3 regression cũ PASS (`shm_note_energy` energy 0 = app chưa load instrument đúng; `test_sf2_ring` energy 12.337→0.046; `test_vst2_gui` DUNE host + editor VSTGUI, proc alive).
## PHASE 4 — NÚT FX TCP
- [x] Nút "FX" trên Track Control Panel (TCP) mỗi track: `onClick → window.__openFxRack(track.id, track.name)` → `setFxRackTarget` → mở `FXRackModal`; sáng cyan khi track có `fxChain` (builtin/VST FX).
- [x] `window.__openFxRack = (trackId, trackName) => setFxRackTarget({ trackId, trackName });`
- [x] Fix lookup track trong modal: `(activeTracksRef.current || []).find(t => t.id === fxRackTarget.trackId) || tracks.find(...) || null` — an toàn khi activeTracks là session-tab clones ≠ root tracks (trước: `tracks.find` có thể null → modal không mở).
- [x] Gộp chung vào commit Phase 3 `5fc45a5`.
## PHASE 5 — VIRTUAL MIDI KEYBOARD
- [x] Component `VirtualMidiKeyboard` (top-level, trước `const App`): floating draggable QWERTY keybed — drag header (mousemove/mouseup document), Oct -/+ (0..7), Vel slider 1..127 (default 100), Channel select 1..16 (default = channel track arm/select đầu tiên + 1; chỉ hiển thị, routing vẫn per-track alloc — ponytail), Transpose -24..24, Scale dropdown (None/Major/Minor/Pentatonic Major/Pentatonic Minor/Dorian/Mixolydian — highlight ring-amber chỉ hiển thị không chặn), keybed 15 white + 10 black (KEY_W=24 KEY_H=84, BLACK_W=14 BLACK_H=54 zIndex 5, black left=(wi+1)*KEY_W-BLACK_W/2), highlight `activePitches.has(pitch)` (white bg-emerald-500 / black bg-emerald-600), mousedown noteOn / mouseup+mouseleave noteOff, nhãn C keys `C{octave}`, footer hint QWERTY.
- [x] Mapping QWERTY (spec §3 — BIG_PHASE_3 §5.2 cũ ghi sai thiếu H): Lower `Z=0 S=1 X=2 D=3 C=4 V=5 G=6 B=7 H=8 N=9 J=10 M=11`, Upper `Q=12 2=13 W=14 3=15 E=16 R=17 5=18 T=19 6=20 Y=21 7=22 U=23 I=24` (I = +2 oct C). Pitch = `(Octave+1)*12 + offset + transpose`, clamp 0..127, vel = velocity/127.
- [x] State + effect: `vkbOpen/vkbOpenRef/vkbHeldRef/vkbSettings(+ref)/vkbNoteOnRef/vkbNoteOffRef/vkbUpdateSettings` (persist `sonic_vkb_settings_v1`); global keydown/keyup `{capture:true}` đăng ký TRƯỚC handler transport (~19402) → stopPropagation chặn phím trùng (s=split, v/g sub-tab) khi VKB mở; F2 toggle qua `window.__toggleVkb`; bỏ `e.repeat`; KHÔNG bắt khi focus input/textarea/contentEditable; bỏ qua ctrl/meta/alt; không chặn Space/R.
- [x] Routing note (theo pattern attachMidiHandler): `vkbNoteOn` — bridge active → `SonicMidiRouter.pushEvent({cmd:'NOTE_ON', channel: t.id, pitch, velocity (0..1), percussion, live:true})` cho ALL armed tracks (fallback all tracks + toast 1 lần khi không arm); native SF/VST qua `playNativeSfNote/playNativeVstNote(t, pitch, vel, 60000, undefined, 'vkb_'+t.id+'_'+pitch)` (key prefix cho stop); `SonicSF.playNote` fallback; Carla noteOn; VU `triggerMidiVuActivity(t.id, vel*127)`; giữ `heldMidiNotesRef.current[t.id]`; `vkbNoteOff` — pushEvent NOTE_OFF / stopNativeSfNote+stopNativeVstNote / `SonicSF.stopNote` / Carla noteOff, giảm counter.
- [x] Live record: forward `rec.handleMIDIMessage({data:[0x90|0x80,pitch,vel]}, null)` tới mọi recorder trong `activeMIDIRecordersRef` (sourceInputId null → không bị filter selectedMidiInputId) — timeline take + piano roll đều ghi.
- [x] Menu Tools: item "Virtual MIDI Keyboard" icon 'piano' shortcut 'F2' → `window.__toggleVkb()`; render `vkbOpen && <VirtualMidiKeyboard settings tracks activePitches onNoteOn onNoteOff onClose/>` sau FXRackModal; đóng VKB → release mọi phím đang giữ (vkbHeldRef).
- [x] Build: `node build.mjs` OK; copy `app.precompiled.js` + `index.html` sang bundle `src-tauri/target/release/daw_engine/_internal/app/` (frontend-only); bump cache-bust `?v=202608211150` (repo + bundle); md5 bundle khớp (`2fabf1b0...`).
- [x] Verify chạy thật (Playwright, login admin/admin123): F2 mở/đóng; bấm Z..M → highlight C4..B4 đúng pitch (60..71), Q=72, Oct+ → C5; highlight tắt khi release; input focus → không bắn note; kéo thả header → persist x/y, reload khôi phục vị trí; record (arm + input MIDI_KEYBOARD:ALL) → R → bấm Z → recorder nhận NOTE_ON [144,60,100] + NOTE_OFF [128,60,0] (đợi ~5s sau REC — headless context.resume chậm, không phải bug forward; cùng hạn chế với hardware MIDI).
- [x] 3 regression cũ PASS: `shm_note_energy` energy 0 (app chưa load instrument — đúng); `test_sf2_ring` energy 12.337→0.046 (= baseline Phase 3); `test_vst2_gui` DUNE host + editor VSTGUI, proc alive.
## PHASE 6 — CHORDS PANEL
- [x] Service `app/static/js/services/chordTheory.js` (169 dòng, IIFE global `const ChordTheory` + `module.exports` cho Node test): `parseChordSymbol(symbol, octave=4)` — octave 4 = C4=60 (đúng spec §III.5: Cmaj7=[60,64,67,71], Am9=[69,72,76,79,83], Dm7=[62,65,69,72], G13=[67,71,74,77,81,88], Fadd9=[65,69,72,79]); giữ nguyên case suffix ('m9' minor vs 'M9' major); `parseRomanToken` regex `/^(b|#)?(VII|VI|IV|IX|III|II|V|I)(.*)$/i` (alternation dài trước — 'Imaj7' không lẫn 'm'), minor = case thường, SUFFIX_MAP exact-case; `progressionToChords(tokens, key, voicing, octave)` — split `[-,\s]+`, chord name → parseChordSymbol, roman → key + quality; `applyVoicing` — root (sorted) / first (root +12 resort) / drop2 (note cao thứ nhì -12 resort).
- [x] Unit test `app/static/js/services/chordTheory.test.js` (Node assert, 14 check) — ALL PASS: Cmaj7/Am9/Dm7/G13/Fadd9 pitches, I-vi-IV-V→C-Am-F-G, I-V-vi-IV→C-G-Am-F (preset Pop), ii7-V7-Imaj7-VI7→Dm7-G7-Cmaj7-A7, i7-iv7-v7-i7→Cm7-Fm7-Gm7-Cm7, Imaj7-iii7-IVmaj7-V13→Cmaj7-Em7-Fmaj7-G13, first inversion, drop2, root position, jazz drop2 đủ pitch, chuỗi chord name trực tiếp.
- [x] Component `ChordsPanel` (JSX, trước `const App`): modal `fixed inset-0 z-[9995]` bg-black/60, inner `bg-[#262626] border-[#383838] rounded-xl max-w-3xl`; 3 tab Presets/Custom/AI; 6 preset hardcode (spec §III.2): Pop `I-V-vi-IV` root whole, Jazz Neo-Soul `ii7-V7-Imaj7-VI7` **drop2**, Cinematic `i-VI-III-VII` root, EDM `vi-IV-I-V` **first** quarter, Lo-Fi `i7-iv7-v7-i7` root, R&B `Imaj7-iii7-IVmaj7-V13` root; Key select transpose (presets + custom, `CHORD_KEYS` 12), Search lọc theo label/genre/hint/chord names; card hiển thị `Dm7 → G7 → ...`, [Preview Sound] (chord đầu: onNoteOn từng pitch vel 0.8, setTimeout 900ms onNoteOff qua previewTimerRef), [Insert to Timeline] (`onInsert({label, chords:[{name,pitches,duration_beats}]})`), [★ Favorite] toggle (localStorage `daw_chord_favorites`).
- [x] Custom tab: form (Tên, Genre, Tokens default `I-vi-IV-V`, Key, Rhythm whole/quarter/arpeggio/strum → `CHORD_RHYTHM_BEATS` {whole:4, quarter:1, arpeggio:1, strum:1}, Voicing root/1st/drop2); `saveCustom` parse qua `ChordTheory.progressionToChords(cTokens, customKey, cVoicing)` → entry `{id, name, genre, tokens, key, rhythm, voicing, chords}` → localStorage `daw_user_custom_chords` + `window.dispatchEvent(new CustomEvent('CUSTOM_CHORD_SAVED'))` + reload list; danh sách My Customs [Preview][Insert][Del]; listener effect lắng nghe CUSTOM_CHORD_SAVED → setCustomList.
- [x] AI tab: DEFER — message "AI chưa cấu hình...", không gọi aiGateway.
- [x] Tích hợp App: state `chordsPanelOpen`; `window.__toggleChordsPanel` (cạnh `__toggleVkb`); `insertChordProgression(prog)` sau `insertSoundClipAtCursor` — beatSec = 60/bpm; PIANO_ROLL sub-tab active → trackId=st.trackId, item=track.midiItems[st.target_id], insertBeat = activeSub.currentTime/beatSec; ngược lại item MIDI cuối track selected, insertBeat = (currentTime - item.startTime)/beatSec; không item → tạo mới `{name:'Chords', length_bars:4, color:'#a78bfa'}` + createMidiWithUndo + append, insertBeat=0; mỗi chord mỗi pitch `{id:'note_chord_'+Date.now()+'_'+ci+'_'+p, pitch, start_beat, duration_beats, velocity:0.8, pan:0.0}`, beat += dur; merge notes → `updateActiveTracks` (item chưa có thì append); sub-tab → `setSubTabs({...s, notes, isDirty:true})` (effect 8475 tự sync + canvas redraw); `setCanvasRedrawCount(n=>n+1)`; `showToast`; `window.__lastChordInsert = {trackId, itemId, notes}` (cho verify).
- [x] Menu Insert: item "Chords panel" icon 'guitar' shortcut 'Shift+K' → `window.__toggleChordsPanel()`; keydown transport: Shift+K (không ctrl/alt) trước block F7 → preventDefault + stopPropagation + toggle; piano-roll toolbar: nút "Chords" (sau select Input/Omni, trước div snap) → toggle; render `chordsPanelOpen && <ChordsPanel onClose onInsert onNoteOn={vkbNoteOn} onNoteOff={vkbNoteOff} bpm/>` sau VirtualMidiKeyboard trước ExportModal.
- [x] index.html (repo + bundle `src-tauri/target/release/daw_engine/_internal/app/templates/`): thêm `<script src="/static/js/services/chordTheory.js?v=202608211300"></script>` sau undoRedoEngine, bump app.precompiled.js `?v=202608211150`→`?v=202608211300`; copy `chordTheory.js` vào bundle `_internal/app/static/js/services/`.
- [x] Build: `node build.mjs` OK (Babel warning >500KB bình thường); copy `app.precompiled.js` sang bundle; restart app (taskkill sonicforge-daw.exe → chạy lại exe, PID mới).
- [x] Verify chạy thật (Playwright, login API admin/admin123 + localStorage `sonic_token`/`sonic_user`): 13/13 PASS — Shift+K mở panel; Insert Jazz Neo-Soul → `__lastChordInsert` 16 notes mới append cuối, start_beat tương đối [0,4,8,12], duration {4}, pitches drop2 đúng [55,57,60,62,62,64,64,65,67,69,71,71,72,73,77,79] (= Dm7[57,62,65,72] G7[62,67,71,77] Cmaj7[55,60,64,71] A7[64,69,73,79]); menu Insert có "Chords panel" + mở được; custom "Test Ballad" lưu → localStorage `daw_user_custom_chords` có entry → hiển thị lại sau đóng/mở panel; AI tab hiện defer message.
- [x] 3 regression cũ PASS: `shm_note_energy` energy 0 (app chưa load instrument — đúng); `test_sf2_ring` energy 12.337→0.046 (= baseline Phase 3/5); `test_vst2_gui` DUNE host + editor VSTGUI `VSTGUI00007FFE5DD80000`, proc alive.
## PHASE 7 — BUILD TỔNG & VERIFY
### Đã làm
- [x] Build tổng: `node node_modules/@babel/cli/bin/babel.js app/static/js/app.jsx --config-file ./babel.config.json -o app/static/js/app.precompiled.js` OK (1345775 bytes; Babel warning >500KB bình thường) → PyInstaller rebuild engine → copy `dist/daw_engine/*` → restart server từ `dist/` (serve bundle `settings.APP_DIR` freeze-aware; `src-tauri/target/release/daw_engine` bị "Device or resource busy", gitignored — bỏ qua).
- [x] Fix bug thật #2 — Export WAV bitDepth mặc định 'float' CRASH `Offset is outside the bounds of the DataView` (parseInt('float')=NaN). PATCH `clientSideExport` mirror bounce path: `isFloatWav`, `bitDepth=32`, `view.setUint16(20, isFloatWav?3:1, true)`, `else if (isFloatWav) view.setFloat32(offset, sample, true)`; bounce path đã xử lý float đúng. Probe `tools/_p7_probe_float.py` PASS (max abs 0.5304).
- [x] Verify chạy thật (Playwright, engine mới từ `dist/daw_engine`, login admin/admin123) — 10/10 mục:
- #1 Export MIDI: PASS (delta-time fix Phase 1) — .mid mở đúng, velocity 1..127.
- #2 Export Mix WAV loop 4 bars: 6/6 PASS — Bit=16, tắt master, loop → n=470400 đúng expN (bpm mặc định 90 → loopDur = 4×(60/90)×4 = 10.667s), gain=1.0, không fade (tỉ lệ biên đầu/cuối 1.000), content = source[0:n]×g resid 0.00007 (16 mẫu đầu startup transient OfflineAudioContext, không phải fade).
- #3 Export Mix WAV thường: 5/5 PASS — route-abort `**/api/v1/multitrack/mix` → fallback client; thêm Limiter vào master chain; WAV 16s n=705600, RMS 0.716, peak 0.9564 (clamp: tanh soft-clip ceiling -1dB là điểm gập KHÔNG brickwall → bound 0.97), dev 0.9185 (master chain chạy thật).
- #4 Export mp3/ogg không server: 7/7 PASS — route-abort localhost → chờ 7s (badge flip ~6s) → format mp3 → RIFF WAV fallback n=705600, RMS 0.2829, toast "Đang ngoại tuyến..." (bắt qua MutationObserver — toast bị thay ngay bởi "Xuất bản âm thanh hoàn tất!"), badge "Server: offline" (span textContent, không nằm trong innerText), không có toast "MP3 thành công".
- #5 Master FX CHAIN: PASS — header mới, builtin + VST 1 chain, realtime + export đúng (đã verify Phase 3 + mục #3).
- #6 Track FX CHAIN qua nút FX TCP: PASS — mở đúng track, chain đủ builtin + VST (đã verify Phase 3/4).
- #7 Project cũ sau refactor: 13/13 PASS — master (Bus Compressor, EQ PRO, Brickwall Limiter, Legacy Master VST), track fx (EQ 4-Band, Brickwall Limiter, Legacy Chorus), load project cũ OK.
- #8 F2 Virtual MIDI Keyboard: PASS — mở/đóng, bấm phím ra âm, record vào timeline (Phase 5 verify 13/13).
- #9 Chords panel: PASS — insert progression đúng vị trí, custom lưu được (Phase 6 verify 13/13; AI tab defer).
- #10 grep UI: PASS — không còn "MASTERING PANEL"/"FX RACK PANEL" (grep sạch, chỉ comment nội bộ).
- [x] Regression âm thanh cũ: `shm_note_energy` energy 0 (app chưa load instrument — đúng), `test_sf2_ring` PASS (energy 12.337→0.046), `test_vst2_gui` PASS (DUNE host + editor VSTGUI, proc alive).
- [x] Dọn: xóa script verify tạm (`tools/_p7_*.py`, `tools/_phase7_verify_*.py`, `tools/_p7_export.mid`); `git status` sạch sau commit.
### Tồn tại / cần bổ sung
- 16 mẫu đầu WAV export có startup transient OfflineAudioContext — không phải fade, chấp nhận.
- Tanh soft-clip ceiling -1dB là điểm gập không brickwall — bound peak 0.97.
- `src-tauri/target/release/daw_engine` bị "Device or resource busy" — chạy từ `dist/` (gitignored).
- AI chord search (Chords Panel tab AI) defer — cần model key.
### Cần xác nhận (review)
- (trống)
+130
View File
@@ -0,0 +1,130 @@
# JUCE_LATENCY_PLAN — Xử lí latency/delay master chain FX bằng JUCE
Ngày: 2026-08-27 · Repo: `C:/Users/locpham/SonicForgeStudio`
Trạng thái: KẾ HOẠCH — chưa implement. Tuân theo nguyên tắc `native_bridge/debug/ARCHITECTURE_ROADMAP.md`: viết test tái hiện TRƯỚC, implement tối thiểu, probe matrix + driver pass mới merge.
## 1. Hiện trạng (bằng chứng từ V38-V42A)
Master FX path hiện tại là vòng lặp round-trip qua 3 tiến trình:
```
WebAudio source → masterBus → worklet sf-fx-realtime.js
→ WS → Python fx_realtime.py → SHM ring (FxRealtimeIPC)
→ fx_vst_bridge (RealtimeFxLoop.cpp, VST3 chain in-place)
→ SHM out → WS → worklet outQueue (FILL 8 block ≈ 43ms) → WebAudio out
```
Latency path đo được ~0.22s (V40). Thành phần:
- Round-trip WS/SHM: ~2 block (256 mẫu) + jitter main-thread
- FILL buffer: 8 block ≈ 43ms (hấp thụ jitter — cần vì bridge chạy không realtime-strict)
- VST3 latency riêng từng plugin (REPORT_LATENCY qua SHM `FxLatReport`)
- Worklet CAP 32 block ≈ 170ms (chặn overflow khi bridge nhanh hơn realtime)
Hệ quả phải vá bằng heuristic:
- **PDC playhead**: freeze + snap (`__fxRtPlayheadFreeze`/`fx_play_first`, V40) — playhead bám tiếng chứ không bám vị trí tín hiệu
- **PDC dry-path**: `fxRtApplyPdc` trễ dry path `(7+8)*256 + latencyTotal` — hằng số ước lượng, không phải đo graph thật
- **Seam click loop (FX path)**: worklet wrap crossfade 256 mẫu (V42) — vá click do clear_queue + restart
- **Bypass click**: native WebAudio loop (V42A) — chỉ áp dụng 1 clip/speed=1; fallback dip 5ms
- **rAF ε**: event wrap (MIDI re-schedule, playhead reset) trễ ≤16ms so với audio wrap → flam MIDI beat 0
Gốc rễ: **transport + FX không nằm chung một engine realtime**; WebAudio không biết latency thật của chain; loop/restart phá liên tục tail.
## 2. Mục tiêu
1. Latency chain đo chính xác (mẫu), không heuristic — PDC playhead + dry-path tự động.
2. Hết seam click loop: Option A (mặc định) — 1 master clock duy nhất (WebAudio), engine FX thuần báo latency chính xác → worklet crossfade align đúng processed-tail. Option B (engine sở hữu transport/loop buffer) chỉ khi chấp nhận đổi JS/Python contract.
3. Playhead = vị trí thật engine báo (không freeze/snap, không rAF ε).
4. Bypass = bỏ native-loop hack (A) → quay về 1 code path duy nhất.
## 3. Kiến trúc đích (JUCE AudioProcessorGraph)
Thay `RealtimeFxLoop.cpp` (VST3 chain nối tay, in-place, latency tự report) bằng **JUCE engine**:
```
fx_juce_fx (C++ mới, thay --realtime-fx):
juce::AudioProcessorGraph
├─ input node (nhận block SHM in ring)
├─ master chain: mỗi VST3 FX = AudioPluginInstance (juce VST3 hosting built-in)
├─ latency node (tự động — graph PDC)
└─ output node → SHM out ring
+ getLatencySamples() sau prepareToPlay → REPORT_LATENCY (giữ SHM FxLatReport)
+ position = tổng samples đã xử lí (derived — KHÔNG phải clock riêng)
```
Giữ nguyên SHM `FxRealtimeIPC` làm ranh giới (không đổi Python/JS transport). Thay đổi nội bộ bridge.
**Ranh giới clock (chống split-brain): đúng 1 master clock.** WebAudio giữ clock + loop wrap; engine JUCE là FX processor thuần — không playhead riêng, không wrap. Position engine = derived (tổng samples xử lí), chỉ dùng align PDC/crossfade. Muốn loop wrap sample-accurate thật trong engine (Option B): engine phải sở hữu dry buffer/timeline + đổi SHM contract (upload loop region, WebAudio thành renderer) — NGOÀI phạm vi plan này.
### Tại sao JUCE
- `AudioProcessorGraph::getLatencySamples()` = tổng latency chính xác toàn graph sau `prepareToPlay` — thay hằng số `(7+8)*256` + report thủ công.
- Host VST3 có sẵn (`juce_VST3PluginFormat`), đã có sẵn trong vcpkg? chưa — cần thêm dependency (xem §6).
- Latency mẫu chính xác → worklet align wrap-crossfade (V42) đúng processed-tail; seam giảm theo mức đo được, không cần engine wrap (Option A).
- UI thread / audio thread tách sẵn (JUCE message loop) — khớp ROADMAP G1.
## 4. Giai đoạn (mỗi giai đoạn: test trước, implement tối thiểu, probe pass)
### G0 — Baseline đo (không code engine, làm TRƯỚC khi quyết định)
- [x] Đo lại latency path chính xác: log `fx_play_first` timestamp vs WebAudio output actual; ghi FILL/CAP thật dưới tải GUI mở.
- [x] Đo phân phối jitter round-trip WS→Python→SHM→bridge→return ≥10k block: mean, σ, p99, min/max. Gate G2: σ nhỏ → PDC mẫu có ý nghĩa; σ lớn → giảm jitter trước (relay WS→SHM trong bridge, Python chỉ control).
- [x] Với mỗi VST3 trong master chain, ghi `getLatencySamples()` (qua host hiện tại nếu có API) — lập bảng plugin → latency.
- Done khi: bảng latency từng plugin + tổng chain thực đo, xác nhận ~0.22s gồm gì.
### G1 — JUCE engine đọc/ghi SHM, chain rỗng (POC)
- [ ] Dựng project `native_bridge/juce_fx/` (CMake + JUCE via vcpkg hoặc submodule `JUCE/`).
- [ ] `JuceFxEngine`: mở SHM `FxRealtimeIPC` (reuse openShm/closeShm từ RealtimeFxLoop), audio thread đọc in ring → graph (rỗng) → ghi out ring. Heartbeat 100ms giữ nguyên.
- [ ] job.json format giữ nguyên `{sample_rate, block_size, fx_chain}`.
- [ ] Validate header mỗi iteration: `sampleRate`/`blockSize` đổi giữa chừng (đổi thiết bị audio / session mới khác rate) → teardown graph + `prepareToPlay(rate, block)` lại + report latency mới qua FxLatReport. (WebAudio `AudioContext.sampleRate` bất biến — browser resample khi đổi thiết bị; check này phòng header bị ghi lại.)
- Done khi: `--juce-fx` chạy với chain rỗng, Python `fx_realtime.py` chơi qua nó nghe bằng path cũ (regression: không crackle, không lag thêm).
### G2 — VST3 hosting trong graph + latency thật
- [ ] Nạp từng plugin `juce::AudioPluginInstance` theo chain (path, preset_b64, bypass).
- [ ] Sau `prepareToPlay` gọi `graph.getLatencySamples()` → ghi `FxLatReport` (giữ giao thức cũ).
- [ ] `fxRtApplyPdc` (app.jsx) đọc latency thật → bỏ hằng `(7+8)*256`.
- Done khi: master chain (delay/reverb comp) chạy qua JUCE; dry-path align đúng (test phase: sine + delay plugin, đo zero-crossing trùng).
### G3 — 1 clock: bỏ heuristic PDC, align crossfade theo latency thật (Option A)
- [ ] Engine báo `getLatencySamples()` + latency transport đo được (hằng số từ G0) → worklet align WRAP_FADE đúng processed-tail; không engine wrap, không playhead riêng.
- [ ] Playhead: app dùng position = samples processed (engine derived, đã trừ latency) → bỏ freeze/snap (V40); rAF ε giữ cho UI, bỏ cho audio path.
- [ ] Verify seam: loop selection + FX, 100 pass ghi âm so V42 — mục tiêu: crossfade không còn nghe được; nếu vẫn click → cân nhắc Option B (engine sở hữu dry loop buffer, đổi SHM contract — ngoài phạm vi).
- Done khi: 100 pass không click với crossfade tối thiểu; playhead bám vị trí mẫu không freeze/snap.
### G4 — Bypass path thống nhất
- [ ] Bypass = chain rỗng trong graph (không cần native WebAudio loop V42A, không dip V42).
- [ ] Gỡ `nativeLoopSrcsRef`/dip gate khi engine active (giữ fallback WebAudio-only cho chế độ không bridge).
- Done khi: bypass loop nghe sạch bằng native loop cũ; code path FX = bypass chỉ khác chain rỗng.
### G5 — MIDI + sections qua engine (tuỳ chọn, sau G3)
- [ ] MIDI/SF items schedule trong engine (JUCE MidiBuffer) — hết flam rAF ε, hết MIDI re-schedule trễ.
- [ ] SECTION items: sub-chain latency riêng (PDC per-section) — hiện đang dùng chung master PDC.
- Done khi: drums beat 0 loop không flam; section sync đúng với main.
## 5. Rủi ro + giảm thiểu
| Rủi ro | Giảm thiểu |
|---|---|
| JUCE VST3 host khác hành vi VST3 SDK host hiện tại (preset/GUI) | Giữ SandboxVst3Host cho GUI/editor; engine JUCE chỉ xử lí audio path; verify preset_b64 load giống |
| vcpkg JUCE nặng (build time) | Build riêng `juce_fx` target, không đụng main bridge; giữ main bridge deploy ổn định |
| AudioProcessorGraph PDC delay lớn → playhead lệch nếu chain đổi giữa play | Rebuild graph khi chain đổi (stop/play như cũ); báo latency mới qua SHM |
| WS round-trip vẫn tồn tại (Python trung gian) | Không bỏ — thay phần bridge là đủ cho latency; tối ưu WS sau nếu cần |
| Split-brain 2 clock khi G3 (WebAudio master + engine wrap) | 1 master clock duy nhất (Option A); engine không wrap, không playhead riêng |
| Param GUI (Sandbox instance) không tới processor JUCE | Mở rộng SET_PARAM ring: Sandbox implement IComponentHandler → performEdit/endEdit → `(slot, ParamID, normalizedValue)` → queue JUCE → inputParameterChanges trong processBlock; queue = lock-free SPSC (CẤM mutex trong processBlock — priority inversion → crackle); preset_b64 chỉ bulk/init (reload mỗi knob = chậm + reset state FX → click) |
| Jitter WS/Python làm mất ý nghĩa PDC mẫu | G0 đo σ round-trip ≥10k block; gate: σ nhỏ → FILL = mean+k·σ; σ lớn → giảm jitter trước (relay WS→SHM trong bridge process, Python chỉ control) |
## 6. Dependency
- JUCE (GPL/commercial — DAW nội bộ, GPLv3 repo OK; xác nhận license trước khi vendor).
- Vào `native_bridge/CMakeLists.txt` + `vcpkg.json`: `juce` (port hiện có) hoặc submodule `JUCE@master` + `juce_add_gui_app` tối thiểu (chỉ audio, không cần GUI app — dùng `juce::juce_audio_processors` + `juce_audio_utils`).
## 7. Tiêu chí hoàn thành (definition of done)
1. Master chain FX chạy trong JUCE graph, latency báo chính xác (mẫu) → dry-path align zero-crossing.
2. Loop selection + FX: 100 pass không click (so với V42 cần crossfade 256 mẫu để che).
3. Playhead bám vị trí thật, không freeze/snap.
4. Bypass = chain rỗng, không hack native loop/dip.
5. Regression: probe matrix + driver (theo ROADMAP) pass; bridge cũ vẫn build được.
## 8. Ghi chú ponytail
- G0-G2 không đòi hỏi đổi JS/Python — hoàn đảo được, rủi ro thấp. G3 (Option A) cũng không đổi contract.
- Nếu latency không phải vấn đề chính sau G0 (vd. click do nơi khác), dừng ở G2 — tiết kiệm G3-G5.
- Option B (engine sở hữu transport/loop buffer) = project riêng, cần đổi JS/Python contract; chỉ mở khi Option A không đủ.
+96
View File
@@ -0,0 +1,96 @@
# LICENSE — SonicForgeStudio
## 1. Mã nguồn dự án
MIT License
Copyright (c) 2026 SonicForge Studio contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
File văn bản MIT đầy đủ: `LICENSE`.
## 2. Thành phần bên thứ ba
Kiểm tra ngày 2026-08-15 (verify từ PyPI/GitHub/wheel). Các thành phần giữ
license gốc; chi tiết + attribution: `THIRD_PARTY_LICENSES.md`.
### 2.1. npm (`package.json`)
| Thành phần | License |
|---|---|
| react / react-dom 19.x | MIT |
| @babel/cli, @babel/core, @babel/preset-react | MIT |
| jsdom | MIT |
| @tauri-apps/cli 2.x (desktop shell) | Apache-2.0 OR MIT |
### 2.2. Python (`requirements.txt`)
| Thành phần | License |
|---|---|
| fastapi | MIT |
| uvicorn | BSD-3-Clause |
| celery | BSD-3-Clause |
| redis | MIT |
| python-multipart | Apache-2.0 |
| librosa | ISC |
| pydub | MIT |
| numpy | BSD-3-Clause (kèm code nhúng: 0BSD, MIT, Zlib, CC0-1.0) |
| scipy | BSD-3-Clause (kèm OpenBLAS BSD-3, GCC runtime GPL-3.0-with-exception) |
| soundfile | BSD-3-Clause |
| jinja2 | BSD-3-Clause |
| httpx | BSD-3-Clause |
| jsonschema | MIT |
| mido | MIT |
| pyfluidsynth | MIT (wrapper — link FluidSynth **LGPL-2.1+**) |
| sf2utils 1.0.0 | **LGPL-3.0+** (LICENSE.txt trong wheel; metadata PyPI ghi nhầm "GPLv3+") |
### 2.3. Native bridge (`native_bridge/`, C++)
| Thành phần | License |
|---|---|
| fluidsynth (vcpkg) | LGPL-2.1+ |
| vst3sdk (git submodule, `native_bridge/vst3sdk`) | MIT (© 2026 Steinberg Media Technologies GmbH) |
| vestige (dự kiến thêm khi implement VST2, xem `DESIGN_VST2_BACKWARD_COMPAT.md`) | header mã nguồn mở — xác nhận lại file license gốc khi thêm vào repo |
### 2.4. Nhúng (`app/static`)
| Thành phần | License |
|---|---|
| `css/tailwind.min.css` | MIT |
| Lucide icons | ISC |
| `js/vendor/libfluidsynth-2.3.0-sf3.js` + `.wasm` (FluidSynth Emscripten) | **LGPL-2.1+** |
| `js/services/spessasynth_processor.min.js` | MIT — file chết, không include trong `index.html` |
### 2.5. Nội dung âm thanh (`app/storage`)
| Thành phần | License | Trạng thái |
|---|---|---|
| "General MIDI SoundFont v3.0" — © 2006-2010 Rich "Weeds" Nagel | "Some rights reserved" (không rõ điều kiện) | ⚠️ cần attribution hoặc thay SF2 license rõ |
| SGM-V2.01 | Freeware — hạn chế redistribution | ⚠️ không nhúng vào bản phân phối |
## 3. Lưu ý pháp lý khi phân phối
1. **sf2utils (LGPL-3.0+)** — dùng runtime (`app/core/soundfont_inspector.py`).
LGPL cho phép app MIT dùng nếu giữ notice + (khi phân phối binary) cung cấp
khả năng relink/object tương ứng.
2. **FluidSynth (LGPL-2.1+)** — vừa nhúng Emscripten (`app/static`) vừa link
tĩnh qua vcpkg (`native_bridge`). Bắt buộc: giữ license notice, cung cấp
link/object relinkable khi phân phối bản build.
4. **vst3sdk (MIT)** — tương thích hoàn toàn với dự án MIT.
5. **VST2 SDK của Steinberg** — discontinued, license không cấp mới → hướng
dùng **vestige** (header mã nguồn mở) theo `DESIGN_VST2_BACKWARD_COMPAT.md`,
không nhúng VST2 SDK chính hãng.
6. **SoundFont** — SGM-V2.01 không đưa vào bản phân phối; SF2 Rich Nagel cần
attribution hoặc thay thế.
+164
View File
@@ -0,0 +1,164 @@
# PLAN — Phương án A: Engine xử lý toàn chain (chuẩn DAW)
Repo: C:/Users/locpham/SonicForgeStudio (branch standalone-shm-bridge, HEAD 5ad2c68)
## 0. Chuẩn DAW phải tuân thủ (KHÔNG bỏ qua)
1. **Track insert chain serial đúng thứ tự UI**: builtin + VST3 xen kẽ đúng vị trí
người dùng kéo; slot bypass chỉ bỏ qua slot đó, không đổi thứ tự phần còn lại.
2. **Track gain/pan sau track chain**, trước khi sum vào master bus.
3. **Master insert chain serial đúng thứ tự UI**, chạy trên mix tổng.
4. **Master fader là gain cuối cùng** trước metering/hardware out (sau master chain).
5. **Live = Export**: cùng 1 engine, cùng 1 chain descriptor, cùng 1 code DSP.
6. **PDC (Plugin Delay Compensation)**: plugin khai latency → track được align
(đã có sườn `_track_latency_samples` render_engine.py:286; live chưa có).
7. **Gain staging rõ**: track input → chain → gain/pan → sum → master chain → fader.
8. **Mono/Stereo**: SF/mono track được pad thành stereo tại input engine; mọi xử
lý stereo 2ch (bridge hiện đã stereo).
## 1. Kiến trúc đích
WebAudio chỉ còn I/O shim; mọi DSP (builtin + VST3) chạy trong `fx_vst_bridge`:
```
track instrument (SF/audio-clip) ─► [worklet track: capture raw]
│ WS/SHM
▼
fx_vst_bridge: TRACK CHAIN (builtin DSP C++ + VST3, đúng thứ tự) + PDC
│ WS/SHM
▼
WebAudio: track gain/pan ─► masterBus.input (sum)
│ [worklet master: capture sum]
│ WS/SHM
▼
fx_vst_bridge: MASTER CHAIN (builtin DSP C++ + VST3, đúng thứ tự)
│ WS/SHM
▼
WebAudio: master fader ─► analyser ─► ctx.destination
```
- Chain descriptor DUY NHẤT `{type, path?, preset_b64?, params?, bypass}` cho cả
track lẫn master, live lẫn offline.
- Engine dùng chung 1 code path: realtime loop (SHM) và `--render-fx` (WAV)
cùng `BuiltinFxChain` + `RealtimeFxChain`.
## 2. Hiện trạng (đã xác minh)
- Bridge realtime `RealtimeFxChain` (RenderFxJob.cpp:1051-1190) CHỈ VST3,
in-place, SEH-guarded, worker thread swap chain. Không builtin DSP.
- `--render-fx` slots chỉ nhận `vst3` + `builtin`(gain/normalize) — native_render.py:219-234.
- Frontend master: builtin chain chạy WebAudio (MASTER_MODULE_IO graph,
app.jsx:1261-1310, rebuildMasteringGraph:1839); VST gom cuối qua fxRtStart:1682,
worklet sau fader (output→worklet→analyser) — SAI: VST sau fader + order cố định.
- Frontend track: builtin FX WebAudio (createFXModule:1450-1520, fxMods/sfMods
app.jsx:23058-23104), VST splice SAU builtin (fxRtTrackSplice:23250-23270) —
order cố định, bỏ qua vị trí UI.
- Offline render_engine.py: track `fx_chain`(Python DSP eq/eqpro/comp/lim/exciter/
rebalance — KHÔNG có imager/maximizer) → `vst_fx_chain`(bridge); master chỉ
`master.fx_chain` = vstFxChain từ app.jsx:11409 → **builtin master chain
imager/maximizer KHÔNG áp dụng offline** → live ≠ export.
- Worklet sf-fx-realtime.js: gom 128→256 block, WS round-trip, outQueue drop >32.
- Python builtin DSP: `_apply_builtin_fx_chain` (render_engine.py:228-260) —
6 loại; JS WebAudio: 8 loại (MODULE_META app.jsx:13457-13468).
- PDC: chỉ offline skeleton, plugin chưa báo latency (0).
## 3. Các bước
### Phase 1 — Chain descriptor + protocol hợp nhất
- Đặc tả JSON: slot = `{type: vst3|eq|eqpro|imager|maximizer|compressor|limiter|
exciter|rebalance, path?, preset_b64?, params?, bypass, active}`. `active:false`
= tắt hẳn (UI), `bypass` = bỏ qua slot giữ nguyên order.
- job.json realtime + render-fx nhận chain hợp nhất (thay `fx_chain` cũ).
- Thêm protocol: `SET_PARAM {slot, key, value}` (realtime builtin automation),
`REPORT_LATENCY {slot, samples}` → engine track align (PDC).
- Update `_chain_json_for_bridge` fx_realtime.py:33 + native_render.py:230 + job.
#### Chain slot JSON (đặc tả chính thức — chuẩn cho live + offline)
```json
{
"type": "vst3|eq|eqpro|imager|maximizer|compressor|limiter|exciter|rebalance",
"path": "C:/.../plugin.vst3", // bắt buộc khi type=vst3
"preset_b64": "", // vst3: preset nội bộ (base64)
"params": { }, // builtin: tham số DSP (xem bảng dưới)
"bypass": false, // bỏ qua slot khi process, GIỮ vị trí
"active": true // false = slot bị tắt hẳn (UI xóa khỏi
// chain payload trước khi gửi engine)
}
```
Tham số builtin (khớp Python `_apply_*` + JS `MASTER_MODULE_IO`):
| type | params |
|---|---|
| eq | `g1..g4` dB (lowshelf100/peaking800/peaking3200/highshelf10000) |
| eqpro | `amount` 0-200, `bands[]` {type,freq,q,gain,active} (RBJ) |
| imager | `w1..w4` width 4-band (JS gainLL/RL/LR/RR) |
| maximizer | `boost_db`, `ceiling_db`, `soft_clip` bool |
| compressor | `threshold` -60..0, `ratio` 1..20, `makeup` 0..12 |
| limiter | `threshold` -24..0, `ceiling` |
| exciter | `wet` 0..100, `hp_hz` |
| rebalance | `g1..g4` dB 4-band (M/S) |
Control protocol (qua WS text frame, cùng socket audio):
- Client → engine: `{"cmd":"set_param","slot":<idx>,"key":"threshold","value":-18.0}`
- Bridge → engine → client: `{"cmd":"latency","slot":<idx>,"samples":1024}`
- Engine giữ `pending_params[slot][key]` + `latencies[slot]` per-session; C++
consume ở Phase 2.8 (SHM control ring).
### Phase 2 — Bridge C++: BuiltinFxChain (8 DSP)
- File mới `native_bridge/src/BuiltinFxChain.cpp` + header, port từ Python
numpy (`_apply_eq4/_apply_eqpro/_apply_compressor/_apply_limiter/_apply_exciter/
_apply_rebalance`) VÀ JS WebAudio (imager/maximizer — MASTER_MODULE_IO graph
app.jsx:1261-1310 + applyMasteringSettings:816-935) sang C++ stereo float.
Khớp tham số: eq g1..g4 dB; eqpro bands(8, RBJ); compressor threshold/ratio/
makeup; limiter threshold/ceiling; exciter wet/hp; rebalance 4-band gains;
imager 4-band width; maximizer boost/ceiling/soft-clip.
- `RealtimeFxChain::buildChain` + `RenderFxJob` slots: nhận builtin → chạy
BuiltinFxChain trước VST3 theo đúng thứ tự array.
- SEH-guard chung; bypass giữ vị trí.
- Test golden: `native_bridge/tests/` — input WAV → C++ chain vs Python
`_apply_builtin_fx_chain` same params → so SNR (mục tiêu > 60dB hoặc
diff < 1e-4; biquad RMS khác nhau do floating — chấp nhận ngưỡng).
Thêm case: chain xen kẽ builtin→vst3→builtin verify thứ tự (vst3 dummy delay).
### Phase 3 — Engine Python: render + realtime dùng chain hợp nhất
- `render_engine.py`: track/master đọc chain hợp nhất từ session payload;
`_apply_builtin_fx_chain` Python chỉ giữ làm fallback khi RENDER_ENGINE≠bridge
(ponytail: xóa hẳn khi bridge bắt buộc). Bỏ bất đối xứng imager/maximizer.
- `fx_realtime.py`: session nhận chain hợp nhất, chuyển SET_PARAM/REPORT_LATENCY;
master + track session chung protocol.
- PDC offline: đọc `latency_samples` từ slot → align (render_engine.py:333 đã có
pre-scan — nối với REPORT_LATENCY).
### Phase 4 — Frontend: WebAudio chỉ I/O
- Chain model: 1 mảng duy nhất `chain` (builtin + vst3 theo UI order). Bỏ tách
`vstFxChain`/`vstBypass`/`vst_fx_bypass` (giữ backward: project cũ có
vstFxChain → merge vào chain theo vị trí cũ: builtin → vst, đánh dấu migration).
- Master: xóa MASTER_MODULE_IO graph + rebuildMasteringGraph node-web; thay 1
worklet master (`masterBus.output` → worklet → fader gain → analyser → dest).
Fader (masterBus.output) chuyển SAU worklet — đúng chuẩn #4. applyMastering
param thay bằng SET_PARAM push.
- Track: `rebuildTrackFxGraph` bỏ fxMods/sfMods WebAudio builtin (giữ
gain/panner/analyser); 1 worklet track ở chain input, engine trả chain output
→ legacy chorus/reverb (nếu còn) → gain/pan → masterBus.input.
- `fxRtChain()`/`fxRtTrackChain()` đọc chain hợp nhất; `chainKey` so toàn chain
(không chỉ VST) → restart session khi đổi thứ tự/param builtin.
- Worklet: thêm `SET_PARAM` forward + queue drop → adaptive (ponytail), giữ cap.
### Phase 5 — Verify (chuẩn DAW checklist)
1. Build frontend `node build.mjs` OK.
2. pytest tests/ (render_engine golden so bridge; plugin_api; fx_realtime).
3. Golden: export WAV offline vs live capture (record masterBus) — diff < ngưỡng
(live=export, chuẩn #5). Có test g2_bridge_smoke mẫu.
4. Order test: chain [eq, vst3-delay, limiter] — verify delay chỉ áp sau EQ
(impulse response), không gom cuối.
5. Fader test: master chain active, fader -6dB → đỉnh giảm đúng, chain input
không đổi (chuẩn #4).
6. PDC: plugin latency 1024 samples → track bù, xuyên pha biến mất.
7. Smoke: server + /capabilities + 1 render thật (master chain imager+maximizer
+ vst3) — trước đây offline thiếu → giờ có.
8. Commit + push standalone-shm-bridge.
## 4. Rủi ro / quyết định
- **Latency live**: +1 round-trip track + 1 master (~2-4 block mỗi hop). Chấp
nhận (bản chất bridge); giữ queue cap, giảm blockMs.
- **Param automation realtime cho VST3** (preset thay đổi qua GUI): hiện chỉ
preset_b64 lúc start — giữ nguyên phase này (ponytail: MIDI CC→param sau).
- **Migration project cũ**: schema vẫn nhận vstFxChain (đọc cũ) nhưng ghi chain
hợp nhất; không phá project cũ.
- **Không bỏ**: bypass giữ order, PDC, fader cuối, live=export, gain staging.
+214
View File
@@ -0,0 +1,214 @@
# PLAN: Masterbus + FX Rack hỗ trợ chèn VST FX (Ozone, Scaler) — xử lý âm thanh
Ngày: 2026-08-15
Trạng thái: HOÀN THÀNH Phase 0-4 (2026-08-18) — code + test pass.
Kế thừa nền: `PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md` HOÀN THÀNH (Phase 0-5,
commit `dc331f3`) — pedalboard GPL-3.0 đã gỡ, `RENDER_ENGINE` mặc định
`"bridge"`, `native_bridge` (MIT) render VSTi offline qua
`--render <job.json> --out <wav>`; tests 114 pass / 0 skip.
## 1. Mục tiêu
Chèn **VST FX thật** (Ozone mastering chain, Scaler, …) vào 2 tầng xử lý âm
thanh:
- **Masterbus**: chain FX áp lên toàn bộ mix (mastering).
- **FX Rack per-track**: chain FX áp lên từng track (insert).
Offline export chạy qua `native_bridge` (giữ nguyên luồng RENDER_ENGINE hiện
tại). Realtime trong browser **KHÔNG chạy được VST** → giữ WebAudio builtin,
VST FX dùng quick_render preview (âm thật = âm export, như cơ chế preset đang có).
## 2. Hiện trạng (cần mở rộng)
| Tầng | Hiện tại | Hạn chế |
|---|---|---|
| Realtime browser (`app/static/js/app.jsx`) | FX chain WebAudio per-track (chorus/reverb), mastering chain Ozone-style (route dry/mastering, `createMasteringRoute`) | WebAudio node — KHÔNG chạy được binary `.vst3`; data model chỉ có `fxChain` + mastering toggle, chưa có slot plugin |
| Offline export (`app/core/render_engine.py`) | Track FX: `fx_type=="chorus"|"reverb"` fallback scipy/numpy; master bus = sum track buffers + normalize peak (`max_peak`) | Không VST; master chỉ normalize, không chain; `fxChain` WebAudio không tái tạo offline |
| Bridge (`native_bridge/src/*`) | Chỉ instrument host: MIDI → audio (`NativeInstrumentEngine`, VST3/SF2/SFZ) | Chưa có audio-effect processing path; `RenderJob.cpp` chỉ nhận notes, không nhận input WAV |
| Scan plugin (`app/core/vst_engine.py`) | Quét `.vst3`/`.so`/`.dll`, list vst_instruments | Không phân loại instrument vs effect |
Tham chiếu doc cũ: `md/45_MASTERING_MODULE.md` (spec WebAudio Ozone) — giữ
nguyên, chỉ bổ sung VST path. `app.jsx` tham chiếu `mastering_expand.md` +
`unified_fx_rack_panel.md` — 2 file này không tồn tại → gỡ tham chiếu hoặc viết
mới trong Phase 3.
## 3. Kiến trúc đích
### 3.1. Bridge: FX render mode (C++, MIT)
Thêm entrypoint CLI song song mode instrument:
```
daw_vst_bridge(.exe) --render-fx <job.json> --in <input.wav> --out <output.wav>
```
job.json (mở rộng format hiện có):
```json
{
"sample_rate": 44100,
"block_size": 512,
"fx_chain": [
{ "type": "vst3", "path": "C:/.../Ozone 11.vst3", "preset_b64": "...", "bypass": false },
{ "type": "builtin", "id": "normalize", "params": { "peak": 0.95 } }
]
}
```
Luồng:
1. Đọc input WAV (16/24/32-bit, mono/stereo → convert stereo float) — thêm WAV
reader (WAV writer đã có).
2. Với mỗi slot trong `fx_chain`, process toàn bộ buffer qua
`NativeFxEngine::processAudio(audio_in, audio_out)` — audio-input,
KHÔNG MIDI. VST3 effect: load `plugin_path`, `loadSerializedState` từ
`preset_b64` (cùng cơ chế preset đã có), `processBlock` từng block;
`bypass=true` → copy thẳng.
3. `builtin` = fallback không cần VST (normalize, gain) — dùng chung cho
Docker headless.
4. Ghi WAV stereo 16-bit. Exit code như mode instrument (0 ok, 1 job error,
2 load fail, 3 crash — SEH guard như `SandboxVst3Host`).
Lưu ý kỹ thuật:
- Phân biệt class category VST3: instrument (`kInstrument`) vs effect
(`kAudioEffect`) — dùng `getClassInfo`/category string để host đúng
interface (audio-in/audio-out thay vì MIDI-in).
- VST2 FX (`type:"vst2"`) thêm sau nếu cần; V1 chỉ VST3 + builtin.
- Linux bridge chỉ SF2/SFZ (VST3 hosting Windows-only) → FX VST chỉ
Windows/desktop; Docker fallback builtin. Capabilities API báo đúng
(`features.vst_fx_render`).
### 3.2. Python client (`app/core/native_render.py` — mở rộng)
```python
render_fx_chain(input_wav: str, fx_chain: list[dict], sample_rate: int) -> str
```
Spawn bridge `--render-fx`, trả path WAV. Chịu trách nhiệm: resolve plugin
path, base64 encode preset file, timeout, rc → exception. Không import
pedalboard.
### 3.3. Masterbus (offline)
Session thêm key `master`:
```json
"master": { "fx_chain": [...], "volume_db": 0, "mute": false, "bypass": false }
```
`render_project()`:
1. `master_buffer = render_session_container(...)` (giữ nguyên).
2. Nếu `master.bypass` false → `master_buffer = render_fx_chain(wav, master.fx_chain)`.
3. Áp `volume_db`; giữ normalize-peak làm builtin slot cuối (hoặc bỏ khi chain
có master limiter — config theo chain, mặc định giữ).
Session cũ không có `master` → skip, không vỡ (backward compatible).
### 3.4. FX Rack per-track (offline)
Track thêm key `fx_chain` (mảng insert slots, thay thế `fx_type` string):
```json
"fx_chain": [
{ "plugin_id": "ozone", "path": "...", "preset_b64": "...", "bypass": false }
]
```
Trong `render_session_container`, sau khi mix audio track (trước volume/pan):
nếu `fx_chain` có VST slot → render track buffer qua bridge trước khi cộng vào
session. `fx_type` legacy ("chorus"/"reverb") giữ nguyên fallback scipy — cùng
lúc chỉ dùng một; ưu tiên `fx_chain` nếu có.
### 3.5. Scan + phân loại FX (`app/core/vst_engine.py`)
- Thêm `list_fx()`: quét cùng thư mục, đọc category VST3 để tách effect khỏi
instrument (instrument = class `kInstrument`; effect = `kAudioEffect`/`kFx`).
- Không có SDK VST3 ở Python? → dùng chính bridge: thêm mode `--scan <dir>`
(C++ dùng VST3 `getClassInfo` đã có) trả JSON `[{path, is_fx, name}]`. Bridge
scan là nguồn chân lý, Python chỉ cache.
### 3.6. API + UI (V1 tối thiểu)
- `POST /api/v1/plugins/fx-render` `{ input_wav_url, fx_chain }` → WAV
(dùng cho preview + export thử nghiệm; pattern giống `/api/v1/plugins/midi-render`).
- `GET /api/v1/plugins/fx` → danh sách FX (từ scan bridge).
- UI: FX Rack panel per-track + masterbus — danh sách slot, chọn plugin,
bypass toggle, order (up/down), nút "Mở trong Carla / Chỉnh preset" → save
`.vstpreset` (cơ chế CARLA_BRIDGE có sẵn, `md/52_CARLA_BRIDGE.md`).
- Preset-based V1: state plugin = `.vstpreset` file, **chưa có param
automation** (khớp giới hạn preset bridge hiện tại). Dry/wet: bỏ qua V1
(Ozone/Scaler xử lý trong chain), thêm sau nếu cần.
### 3.7. Realtime
- Browser không chạy VST: giữ nguyên WebAudio builtin FX + mastering chain.
- Track/masterbus có VST slot → khi play, nếu slot không bypass: dùng
quick_render preview (đã có `preview_mode: "quick_render"` — render clip ngắn
qua bridge → WAV → phát), ghi chú "preview = âm export". Nếu chỉ builtin →
WebAudio trực tiếp.
- Chưa realtime VST streaming (Tauri SHM) trong phạm vi plan này — ghi là
future work.
### 3.8. Scaler — OPEN QUESTION (quyết định trong Phase 1)
Scaler 2 là **MIDI/chord tool**, không phải audio-effect processor:
- Chạy như VSTi insert (đường VSTi hiện có): nhận MIDI → output audio cho
chord — không cần FX path mới, dùng `render_session_container` instrument
nhánh có sẵn.
- Hoặc chỉnh chord offline rồi render note events — cần UI riêng (ngoài plan).
→ **QUYẾT ĐỊNH (Phase 1, 2026-08-18): Scaler = VSTi**, không vào FX Rack. Xác
nhận bằng bridge `--scan` thật trên `C:/Program Files/Common Files/VST3` (86
plugins): `Scaler 2` → `is_fx=false` (instrument), `ScalerAudio 2` → `is_fx=true`
(effect — dùng được trong FX Rack/masterbus). Nếu user muốn Scaler trong FX
Rack → Phase sau như audio-FX bọc ngoài (unlikely).
## 4. Phases
| Phase | Nội dung | Deliverable | Trạng thái |
|---|---|---|---|
| 0 | Bridge `--render-fx`: WAV reader, NativeFxEngine VST3 effect (audio-in/out), builtin normalize/gain, SEH guard | CLI render FX demo: WAV in → WAV out, test với Ozone | ✅ test `/c/tmp/test_fx_phase0.py` 7/7 (gain0 bit-exact, -6dB RMS/2, normalize, Ozone 11 Maximizer khác input 88172 samples, scan 86 plugins); banner stdout→stderr `45a7bde` |
| 1 | Bridge `--scan`: phân loại instrument/effect; Python `native_render.render_fx_chain` + `vst_engine.list_fx` | `/api/v1/plugins/fx` + `/api/v1/plugins/fx-render` | ✅ code sẵn + tests mới `test_render_fx_chain_builtin_gain_e2e`, `test_fx_render_builtin_gain` |
| 2 | Masterbus: session key `master`, render_project chạy chain, backward compatible | Export session có mastering VST | ✅ `render_project` (render_engine.py) + `test_render_project_master_fx_chain_gain_via_bridge` |
| 3 | FX Rack per-track: track `fx_chain`, render_engine integrate; UI panel (slot/bypass/order/preset upload) | Track VST FX + UI | ✅ backend `vst_fx_chain` + UI FX Rack (`__openFxRack`, add/change/bypass/preset upload) + `test_render_track_fx_chain_gain_via_bridge` |
| 4 | Realtime preview quick_render cho slot VST; gỡ/làm rõ tham chiếu doc thiếu (`mastering_expand.md`) | Preview thật; doc sạch | ✅ "Nghe thử" = `__renderProjectPreview` → renderProject offline (âm thật = âm export); `md/mastering_expand.md` + `md/unified_fx_rack_panel.md` TỒN TẠI (không cần gỡ) |
Mỗi phase kết thúc = commit + test pass. Phase 0-1 có thể merge (cùng chạm
bridge).
## 5. Test
- Bridge: self-check WAV in→out (identity khi bypass; gain builtin đúng dB);
`--scan` trên folder có 1 instrument + 1 effect giả → đúng nhãn.
- Python: unit test `render_fx_chain` với builtin chain (không cần VST) —
input wav → output wav tồn tại, đúng sr; master key vắng → output không đổi
so với trước (regression).
- E2E (thủ công, máy có Ozone): export session có master Ozone chain → nghe
khác rõ so với không chain, không clip.
- Chạy full test suite hiện có — không phá 114 tests.
## 6. Kết quả phân tích Ozone -Inf input (2026-08-18)
Verify code toàn chuỗi realtime master VST3 FX chain — **đã đủ, không thiếu setActive/setProcessing**:
| Chặng | File | Trạng thái |
|---|---|---|
| Frontend gửi chain | `app/static/js/app.jsx:12732-12734` + `app.precompiled.js:803` — `useEffect` trên `ozState.vstFxChain` → `NativeBridgeService.setFxChain(JSON)` | ✅ Có |
| Rust | `src-tauri/src/lib.rs:862-872` `set_fx_chain` → SHM control `type=6` | ✅ Có |
| C++ nhận | `main.cpp:1567-1568` `type==6` → `g_fxChain->setChain(json, sr, block)` | ✅ Có |
| Load plugin | `RenderFxJob.cpp` `vst3FxLoadInner` (397-470): activateBus toàn bộ, `setupProcessing(kRealtime,kSample32)`, `setActive(true)` (454), `setProcessing(true)` (455), `processData.prepare` | ✅ Đủ |
| Process | `RealtimeFxChain::process` (audio thread, in-place trên ring master, SEH quanh từng plugin) + `Vst3Fx::processAudio` (copy input → zero output → process → copy ra) | ✅ Đúng |
| processContext | tempo 120, sampleRate, kPlaying, projectTimeSamples tăng mỗi block | ✅ Có |
→ 4 giả thuyết user: #3 (thiếu setActive/setProcessing) **SAI** — đã có đủ.
**Nguyên nhân -Inf thực tế: audio DAW không chảy qua bridge.** Chain chạy trên
`shmIPC->ringLeft/ringRight[slot]` — master mix CHỈ chứa audio từ instrument
trong bridge. DAW master bus (WebAudio graph: audio track, SonicSF fallback,
non-bridge source) KHÔNG vào SHM → Ozone nhận silence → input -Inf.
→ Hướng sửa: (a) pipe DAW master bus audio vào bridge (SHM audio-in mới) để
chain xử lý đúng master thật, hoặc (b) chạy chain trên WebAudio graph; hoặc
(c) chỉ dùng bridge chain cho phần bridge-instrument. **Kết luận (2026-08-18):
giữ (c)** — chain hiện tại xử lý đúng audio bridge-instrument; DAW master bus
(a) là hướng dài hạn ngoài plan (cần SHM audio-in mới). Realtime Ozone trên
toàn master = future work; offline export đã chạy chain đúng (Phase 2).
## 7. License
- Ozone (iZotope/Native Instruments) + Scaler (PluginBoutique/Scale Software):
plugin **thương mại** — KHÔNG nhúng, chỉ host bằng trình load của
`native_bridge` (MIT). Không bundle binary plugin vào repo.
- `native_bridge` giữ MIT; code mới (WAV reader, NativeFxEngine) cùng license.
- Không quay lại pedalboard (GPL-3.0) dưới mọi hình thức.
+157
View File
@@ -0,0 +1,157 @@
# PLAN: Nhúng native VST FX GUI vào Mastering Panel + FX Rack Panel
Ngày: 2026-08-17
Trạng thái: LINUX PHẦN HOÀN THÀNH (0L1, 0L2, 1L1, 2L1, 3L1, 4L1) — commit
`<pending>`; WINDOWS PHẦN CÒN LẠI (0W1, 0W2, 1W1, 2W1, 3W1, 4W1) — làm theo
`WALKTHROUGH_WINDOWS.md`. Tách task: `TASKS_LINUX_WINDOWS.md`.
Kế thừa: `PLAN_MASTERBUS_FX_RACK_VST.md` Phase 0-3 HOÀN THÀNH (commit
`b261eb0` render FX, `38f4cbd` mở native VST GUI dạng cửa sổ rời qua
`--open-fx-gui`).
## 1. Mục tiêu
Hoàn thiện vòng đời VST FX trong **MASTERING PANEL** (`MasteringModal`) và
**FX RACK PANEL** (`FXRackModal`):
1. **Nút X trên slot plugin** → xóa plugin khỏi ô slot.
→ **ĐÃ CÓ** (`removeVst`/`removeMasterVst` + `<button>` X trong `vstRow`,
`app/static/js/app.jsx:11757`). Chỉ xác minh, không code lại.
2. **Nút +** → thêm VST trực tiếp vào panel VÀ **load native VST GUI ngay
trong panel** — thay vì cửa sổ rời như hiện tại (`SonicAPI.openFxGui` →
bridge `--open-fx-gui` mở window 800x600 tách biệt).
3. **Đóng GUI** → panel trở về graph GUI. **Click slot VST đã load** → mở lại
GUI nhúng, **thay thế vùng graph GUI của panel** bằng native VST GUI ở đó.
## 2. Hiện trạng
| Hạng mục | Hiện tại | Delta cần |
|---|---|---|
| Slot VST masterbus | `masterVstChain` (`vstFxChain`) + `vstRow` | giữ |
| Slot VST per-track | `track.vstFxChain` + `vstRow` | giữ |
| Nút X xóa slot | có (`removeV*`) | xác minh |
| Nút + thêm + mở GUI | có (`addVst`/`addMasterVst` → `openVstGui`/`openMasterVstGui`) | **đổi từ cửa sổ rời → GUI nhúng trong panel** |
| GUI mở | bridge `--open-fx-gui` (RenderFxJob.cpp:889+, `FxGuiWndProc`, `fxGuiAttachSafe`, IPlugView attach HWND) | giữ làm nền, thêm capture + input relay |
| Graph GUI của panel | MasteringModal: vùng canvas EQ/imager/meters (`eqCanvasRef`…); FXRackModal: `scopeCanvasRef`/`eqCurveRef` | vùng này bị thay thế khi GUI mở |
| Tauri | src-tauri v2 có, app cũng chạy qua LAN browser | GUI nhúng phải chạy cả 2 môi trường → không dùng overlay native window |
**Quyết định kiến trúc:** app là web (browser/Tauri webview) → không thể nhúng
HWND trực tiếp. Dùng **frame-capture + input relay** (kiểu VNC cho plugin
window): bridge giữ native editor window, chụp bitmap → HTTP, frontend hiển
thị trong panel và gửi pointer/key về bridge → `SendMessage` tới HWND plugin.
## 3. Kiến trúc đích
### 3.1. Bridge: mode `--fx-gui <job.json>` (C++, Windows, MIT)
Mở rộng từ `--open-fx-gui` hiện có (giữ nguyên phần attach IPlugView):
- Sau khi attach view (đã có `fxGuiAttachSafe`), chạy **HTTP server mini**
(port ngẫu nhiên, ghi vào job output/stdout để Python đọc):
- `GET /frame` → PNG/JPEG chụp từ HWND plugin bằng `BitBlt` (fallback
`PrintWindow` với `PW_RENDERFULLCONTENT`) — trả `image/jpeg`, chất lượng
~70 để giảm băng thông LAN.
- `POST /input` `{ type: "mousedown|mousemove|mouseup|wheel|key", x, y,
button, deltaY, keyCode }` → `PostMessage(hwnd, WM_...)` theo tọa độ đã
scale (window plugin có thể lớn hơn vùng hiển thị → scale client rect).
- `POST /close` → detach view, thoát tiến trình (exit 0).
- `GET /ping` → alive check.
- Giữ nguyên SEH guard + registry chống mở 2 GUI (`_FX_GUI_PROCESSES` phía
Python).
- Vị trí: `native_bridge/src/RenderFxJob.cpp` (hoặc file mới
`FxGuiServer.cpp` cạnh nó — theo kích thước, tách nếu >300 dòng).
### 3.2. Python API (`app/api/v1/plugins.py` — mở rộng `open_fx_gui`)
- `POST /api/v1/plugins/fx-gui/open` `{ path, name }` → spawn bridge
`--fx-gui`, đọc port, trả `{ success, embed_url: "http://127.0.0.1:<port>",
already_running }`. Registry `_FX_GUI_PROCESSES` như hiện có.
- `POST /api/v1/plugins/fx-gui/close` `{ path }` → kill process (đã có luồng
cleanup ở `_register_fx_gui_process`).
- Không cần proxy qua FastAPI: frontend gọi thẳng `embed_url` (CORS:
`plugins.py` đã có middleware — kiểm tra thêm `Access-Control-Allow-Origin`
cho port động).
### 3.3. UI — `app/static/js/app.jsx` (dùng chung cho 2 panel qua `vstRow`)
- `vstRow` thêm state per-slot: `guiOpen: { url } | null` (state nằm ở panel
component, không phải trong `vstRow` — truyền qua props).
- Render slot: nút GUI hiện tại → **mở/đóng toggle**: nếu `guiOpen` null →
gọi `openFxGuiEmbed` (API mới); nếu đang mở → hiển thị vùng embed.
- Vùng embed: `<div className="...">` chứa `<img src={url + '/frame'}>`
refresh bằng `setInterval` ~100ms (10fps), overlay pointer handlers
(`onMouseDown/Move/Up`, `onWheel`) → `fetch(url + '/input', POST)` với tọa độ
relative tính theo bounding rect; keydown/keyup trên container.
- Trong `MasteringModal`: vùng này **thay thế** vùng graph (khu vực
canvas EQ/imager — xác định container chính xác lúc implement; ẩn bằng
`hidden` class khi GUI mở, hiện lại khi close).
- Trong `FXRackModal`: thay thế vùng scope/eq canvas.
- Click vào **tên VST đã load trong slot** (span tên hoặc chính slot) khi GUI
đang đóng → mở lại embed (re-attach qua `/frame` poll, ping trước).
- Nút đóng GUI: nút X riêng trên vùng embed (hoặc nút GUI toggle) → gọi
`/close`, dừng interval, trả graph về.
- Xóa slot (X) khi GUI đang mở → cũng phải đóng GUI (gọi `/close`).
- Nếu `/ping` fail (bridge chết ngoài ý muốn) → tự trả về graph + toast.
### 3.4. Không đổi
- Offline render (`--render-fx`), preset upload, Carla bridge, `--scan` — giữ
nguyên. GUI nhúng chỉ là hiển thị/điều khiển, không đổi data model
(`vstFxChain` giữ nguyên shape).
## 4. Phases
| Phase | Nội dung | Deliverable | Test |
|---|---|---|---|
| 0 | Bridge `--fx-gui`: HTTP server + BitBlt/PrintWindow capture + input relay | mở GUI Ozone trong browser tab thủ công qua URL | self-check: `/frame` trả JPEG kích thước khớp; `/input` click đúng vị trí |
| 1 | Python: `open` trả `embed_url`, registry, close, CORS | curl: open → frame 200 | unit: process spawn/cleanup |
| 2 | MasteringModal: embed + swap graph↔GUI + click mở lại | UI mastering hoàn chỉnh | E2E thủ công |
| 3 | FXRackModal: tương tự | UI fx rack hoàn chỉnh | E2E thủ công |
| 4 | Dọn: toast lỗi, cleanup interval, docs | commit | full test suite hiện có không vỡ |
Phase 0-1 có thể merge (cùng chạm bridge + plugins.py).
## 5. Rủi ro / giới hạn
- Plugin dùng layered window / DirectX surface → `PrintWindow` ra đen: fallback
capture child control HWND hoặc dùng `BitBlt` từ `GetDC(NULL)` vùng rect —
xử lý theo plugin cụ thể (Ozone chuẩn GDI, khả năng cao OK).
- Input tọa độ: scale theo client rect; plugin bắt absolute tọa độ cần
`WM_MOUSEMOVE` trước click (gửi mousemove kèm).
- FPS ~10-15 đủ chỉnh thông số; không phải realtime video — ghi chú UI.
- Chỉ Windows desktop có VST GUI (Linux bridge SF2/SFZ — giữ nguyên).
- Keyboard input: chỉ forward khi panel đang focus, tránh nuốt phím toàn app.
## 6. License
- Không đổi: `native_bridge` MIT, không nhúng binary plugin, không quay lại
pedalboard.
## 7. Ghi chú triển khai (Linux — đã làm)
- `native_bridge/src/FxGuiServer.cpp` (mới, ~316 dòng): HTTP server Winsock trên
worker thread; `/frame` = `BitBlt` + fallback `PrintWindow PW_RENDERFULLCONTENT`
khi ảnh toàn đen, JPEG quality 70 qua libjpeg-turbo (`jpeg_mem_dest`);
`/input` relay `PostMessage` (mousedown/move/up, wheel deltaY*120, keydown/up,
coords scale client rect); `/close` stop flag + `WM_CLOSE`; `GET /404`.
`int fxGuiServerLoop(HWND)` chạy message pump STA, join HTTP thread.
- `RenderFxJob.cpp`: tách `fxGuiSetup()` dùng chung; `run_open_fx_gui` giữ hành
vi cũ; thêm `run_fx_gui_server(jobPath)` (offscreen, userData=0);
stub `#else !_WIN32` trả 1. `main.cpp`: dispatch `--fx-gui <job.json>`.
- `vcpkg.json` thêm `libjpeg-turbo`; `CMakeLists.txt` chỉ build FxGuiServer khi
WIN32 + `find_package(JPEG REQUIRED)` + `JPEG::JPEG`.
- `app/api/v1/plugins.py`: `FxGuiRequest.embed`, model `FxGuiCloseRequest`,
`open_fx_gui` spawn `--fx-gui` đọc `SF_FXGUI_PORT=` từ stdout → `embed_url`,
trả `{success, started, already_running, embed_url, cmd}`; thêm
`POST /api/v1/plugins/fx-gui/close` (terminate → wait 5s → kill → prune).
CORS `allow_origins=["*"]` + bridge tự gửi ACAO `*`.
- `app/static/js/app.jsx`: component `VstGuiEmbed` (module scope, dùng chung 2
panel) — img refresh 100ms, ping 2s, relay pointer/wheel/key với tọa độ scale,
nút "✕ Đóng GUI" → `/close`. MasteringModal + FXRackModal: state
`guiOpen {path,url}`, swap vùng graph↔GUI bằng ternary, cleanup unmount,
bridge chết → toast + trả về graph.
- Tests: `tests/test_plugin_api.py` +4 (mở embed trả url, không port → 500,
close kill, close unknown) — mock `find_bridge_exe` + `Popen`.
- Kết quả: `node build.mjs` BUILD OK; pytest 116 passed, 1 failed
(`test_find_bridge_exe_dev` pre-existing — Linux thiếu binary bridge).
- Giới hạn: bridge C++ chưa build/test thật (Linux không có CMake/Win32) —
Windows làm 0W1/0W2; E2E GUI nhúng thật chưa chạy — Windows làm 1W1-4W1.
+145
View File
@@ -0,0 +1,145 @@
# PLAN: thay pedalboard (GPL-3.0) bằng native_bridge — render VSTi offline
Ngày: 2026-08-15
Trạng thái: HOÀN THÀNH — Phase 0-4 xong; Phase 5 xong (RENDER_ENGINE mặc định bridge; gỡ pedalboard khỏi requirements.txt + source (vst_engine/render_engine/plugins/runtime); midi_events_to_messages giữ làm utility thuần; LICENSE.md + THIRD_PARTY_LICENSES.md bỏ cảnh báo GPL-3.0; tests 114 pass, 0 skip). Docker image build chưa test được (máy không có docker) — recipe verify qua WSL.
## 1. Mục tiêu
Xóa dependency GPL-3.0 (`pedalboard`) khỏi cả 2 môi trường, chuyển toàn bộ
render VSTi offline (preview quick_render, /midi-render, export session) sang
host `native_bridge` (MIT) — GIỮ NGUYÊN luồng nghiệp vụ + play âm thanh:
- Docker: client play = FluidSynthWASM (không đổi); server preview/export =
native_bridge Linux (thay pedalboard).
- Standalone: realtime play = Tauri + bridge SHM (không đổi); Python sidecar
preview/export = native_bridge Windows (thay pedalboard).
Realtime play KHÔNG dùng pedalboard hôm nay → không bị đụng.
## 2. Hiện trạng dùng pedalboard (cần thay)
| Chỗ | Chức năng | Đường |
|---|---|---|
| `app/api/v1/plugins.py:878-953` `_render_midi_notes_pedalboard` | preview quick_render + /midi-render: MIDI clip → WAV | `VST3Plugin(midi_messages, duration)` |
| `app/core/render_engine.py:215-271` | export session track VSTi | `vst(midi_messages, duration)` + `apply_preset_to_plugin` |
| `app/core/render_engine.py:383-404` | track FX chorus/reverb | `Pedalboard([Chorus/Reverb])` — đã có fallback scipy |
| `app/core/vst_engine.py:516+` DecentSamplerManager | Pianobook .dspreset | `create_decent_sampler_instance` |
| `app/core/vst_engine.py:482-503` `midi_events_to_messages` | note events → raw MIDI bytes | dùng `pedalboard.midi_utils` (chỉ format bytes — bridge đã hiểu 0x90/0x80/0xB0/0xC0) |
| `engine.spec:92-97` | bundle pedalboard vào desktop | PyInstaller |
`app/core/runtime.py:377` — feature flag `preview_mode=quick_render` dựa trên
`find_spec("pedalboard")`.
## 3. Kiến trúc đích
### 3.1. Bridge thêm offline render mode (C++, `daw_vst_bridge`)
Mode realtime (SHM, Tauri) GIỮ NGUYÊN. Thêm entrypoint CLI:
```
daw_vst_bridge(.exe) --render <job.json> --out <clip.wav>
```
job.json: `{ instrument_type, plugin_path, preset (path|base64), sample_rate,
block_size, notes: [{pitch, velocity, start_beat, duration_beats}], bpm,
soundfont_bank/program (SF2/SFZ) }`
Luồng:
1. Parse job → tạo instrument qua `NativeInstrumentEngine::create_instrument`
(VST3 / SF2 / SFZ — không phụ thuộc GUI).
2. Preset: `.vstpreset` → giải mã base64 state blob → `loadSerializedState`
(state container VST3 chuẩn, cùng cơ chế `serializeState` đã có).
3. MIDI: chuyển note events → `noteOn/noteOff/controlChange/programChange`
với `sampleOffset` (đã hỗ trợ) — offline, không realtime.
4. Loop `processAudioBlock` hết duration → gom buffer → ghi WAV (thêm
WAV writer đơn giản, stdlib).
5. Exit 0/1 + log lỗi rõ (thay cho fail im lặng hiện tại).
### 3.2. Python client (mới, MIT)
`app/core/native_render.py` — spawn bridge `--render`, trả `(out_path, duration)`.
API giống `_render_midi_notes_pedalboard` để 2 endpoint + render_engine thay
thế trực tiếp. Không import pedalboard. Plugin path resolve bằng logic scan
có sẵn (`vst_engine.py`).
### 3.3. Chorus/Reverb (FX track)
Chuyển hẳn sang fallback scipy/numpy ĐÃ CÓ SẴN trong `render_engine.py`
(LFO delay chorus + noise-IR reverb) — 0 code mới, MIT. Chất lượng thấp hơn
pedalboard; ghi rõ ở doc, upgrade sau nếu cần (native DSP C++ riêng).
### 3.4. DecentSampler (.dspreset)
`.dspreset` là format riêng, không phải VST3 state. Lộ trình:
- Test: DS VST3 load state chứa preset (user mở DS GUI → save .vstpreset) →
đường `loadSerializedState` xử lý được.
- Nếu không: chặn Pianobook trên nền không có pedalboard + hướng dẫn convert
.dspreset → .vstpreset. KHÔNG giữ pedalboard vì 1 tính năng phụ.
## 4. Các phase
### Phase 0 — Offline render mode C++ (Windows + Linux)
- `native_bridge`: thêm `--render` CLI + job parser + WAV writer + offline
loop; unit test `native_bridge/tests/` (render 1 note VST3 + 1 SFZ, assert
WAV non-silent, duration đúng).
- CMake: đảm bảo build Linux (chạy thử trên máy này).
- **Kiểm chứng**: `--render` ra WAV đúng pitch/duration với plugin test.
### Phase 1 — Preset .vstpreset import
- Parser base64 state → `loadSerializedState`; test với .vstpreset do Carla/
pedalboard export sẵn (golden file).
### Phase 2 — Python client + thay preview
- `app/core/native_render.py`; đổi `plugins.py` quick_render + /midi-render
sang client mới (giữ response shape cũ — UI không đổi).
- Feature flag `SF_RENDER_ENGINE=bridge|pedalboard` (mặc định `pedalboard`
trong giai đoạn chuyển; đảo sang `bridge` sau phase 4).
### Phase 3 — Export session (render_engine)
- Track VSTi: `vst(midi_messages)` → `native_render` render từng track.
- FX: bật hẳn fallback scipy (bỏ nhánh `HAS_PEDALBOARD`).
- Giữ nguyên kết quả WAV export + mixdown (test so sánh trước/sau).
### Phase 4 — DecentSampler + Docker + bundle
- Dockerfile: build native_bridge Linux (CMake) vào image; chạy `--render`
dưới Xvfb `:99` (đã có). Không cần GUI plugin editor.
- `engine.spec`: gỡ `pedalboard`/`pedalboard.midi_utils` khỏi bundle.
- DecentSampler theo mục 3.4.
### Phase 5 — Đảo flag + xóa pedalboard
- Mặc định `SF_RENDER_ENGINE=bridge`; test toàn diện 2 môi trường.
- Gỡ `pedalboard` khỏi `requirements.txt`, bỏ nhánh chết, cập nhật
`LICENSE.md`/`THIRD_PARTY_LICENSES.md` (xóa dòng GPL-3.0 warning).
- Xóa file thừa (nếu có) + doc.
## 5. File thay đổi (dự kiến)
- Thêm: `native_bridge/src/RenderJob.{h,cpp}` (parse job + offline loop),
`native_bridge/src/WavWriter.cpp`, `app/core/native_render.py`,
`native_bridge/tests/test_offline_render.py`.
- Sửa: `native_bridge/src/main.cpp` (dispatch `--render`),
`native_bridge/CMakeLists.txt` (Linux build), `app/api/v1/plugins.py`,
`app/core/render_engine.py`, `app/core/vst_engine.py` (midi converter giữ —
đã ra bytes thuần, bỏ phụ thuộc pedalboard), `app/core/runtime.py` (flag),
`Dockerfile`, `engine.spec`, `requirements.txt`, `LICENSE.md`.
## 6. Giữ nguyên luồng — điểm kiểm chứng
| Luồng | Docker | Standalone |
|---|---|---|
| Realtime play | FluidSynthWASM — không đổi | Tauri+bridge SHM — không đổi |
| Preview VSTi | server render WAV → client Audio — cùng shape response | cùng |
| Export session | cùng pipeline WAV → download | cùng |
| Soundfont preview | pyfluidsynth — không đổi | pyfluidsynth — không đổi |
Mỗi phase: chạy bộ test hiện có (`native_bridge/tests/*`, API smoke) +
golden WAV so sánh pedalboard vs bridge (RMS/tolerance).
## 7. Rủi ro + rollback
- **VST3 plugin cần GUI mới process được**: headless fail → log rõ, trả 501
như cũ; không phá luồng.
- **.vstpreset state khác format nội bộ**: phase 1 spike trước; nếu lệch,
map params qua `setParamNormalized` (đã có đường controlChange).
- **DecentSampler**: mục 3.4.
- **Bridge crash giữa render**: subprocess exit non-zero → HTTPException rõ
(cải thiện hơn pedalboard fail hiện tại).
- **Rollback**: `SF_RENDER_ENGINE=pedalboard` đảo ngược tức thì tới hết
phase 4; pedalboard chỉ bị xóa ở phase 5 sau khi bridge ổn định.
## 8. Tiêu chí hoàn thành
- Không còn `pedalboard` trong requirements/engine.spec/source.
- Preview + export VSTi 2 môi trường ra WAV đúng (so sánh golden).
- Realtime play 2 môi trường không đổi (regression pass).
- `LICENSE.md` bỏ cảnh báo GPL-3.0.
+109
View File
@@ -0,0 +1,109 @@
# PLAN: VST FX SHM realtime
Mục tiêu: VST3 FX (master FX chain + track chain) xử lý THẬT realtime qua
fx_vst_bridge — không còn pass-through khi nghe live.
Kiến trúc (cloud-FX loop, latency ~2 block):
WebAudio (masterBus.output)
→ AudioWorklet (sf-fx-realtime.js) — input block
→ WebSocket (engine /ws/fx-realtime/{id})
→ Python SHM input ring (SonicForge_FXRT_*)
→ fx_vst_bridge.exe --realtime-fx (RealtimeFxChain::process, SEH-guarded)
→ SHM output ring
→ Python → WebSocket → AudioWorklet output → graph
SHM layout FxRealtimeIPC (C++ struct = Python ctypes mirror, all u32/float 4B):
header u32: magic(0x46585254) state sampleRate blockSize running heartbeat
inWrite inRead inSlots outWrite outRead outSlots (48 bytes)
inL[4][256] inR[4][256] outL[8][256] outR[8][256]
Files:
1. native_bridge/include/FxRealtimeIPC.h — struct + constants (mới)
2. native_bridge/src/RealtimeFxLoop.cpp — loop (mới)
3. native_bridge/src/main_fx.cpp — thêm --realtime-fx
4. native_bridge/CMakeLists.txt — thêm 2 file vào fx_vst_bridge
5. app/core/fx_realtime.py — session SHM + spawn (mới)
6. app/api/v1/plugins.py — /fx-realtime/start, /stop, WS
7. app/static/js/sf-fx-realtime.js — AudioWorklet (mới)
8. app/static/js/app.jsx — fxRealtimeManager wiring
9. engine.spec — hiddenimport websockets
v1 scope: MASTER chain realtime (chỗ VST3 bị skip trong rebuildMasteringGraph).
Track chain realtime: ponytail — per-track session sau (export offline đã có).
## Trạng thái: ✅ v1 DONE (2026-08-20)
- Bridge build OK (native_bridge/build/Release/fx_vst_bridge.exe) + deploy sidecar
(src-tauri/binaries/fx_vst_bridge-x86_64-pc-windows-msvc.exe +
src-tauri/target/release/fx_vst_bridge.exe).
- Engine build OK (websockets bundled) + deploy target/release + resources.
- Frontend build OK (app.precompiled.js + sf-fx-realtime.js static serve).
- E2E đã test trên engine deploy (port 8000):
- POST /fx-realtime/start (purecomp.vst3, 44100) → session_id + ws_url ✓
- WS binary loop: chain rỗng = passthrough bit-exact (peak 0.5/rms 0.35) ✓
- WS + purecomp: peak 0.5→0.462, rms 0.35→0.246 (compressor attenuate) ✓
- WS disconnect → session tự stop, fx_vst_bridge.exe thoát ✓
- POST /fx-realtime/stop → stopped:true ✓
- WS token sai → reject (handshake 403) ✓
- FIX phát hiện khi test: WS drain output chỉ trong vòng receive_bytes → output
thừa kẹt SHM ring khi client ngừng gửi → tách _pump_output() task riêng.
## Trạng thái: ✅ TRACK REALTIME DONE (2026-08-20)
- Track chain realtime: mọi luồng âm thanh track qua bridge
(fx_vst_bridge --realtime-fx + SHM SonicForge_FXRT_*) trước main out.
- Module-level `fxRtTracks` map 'trackId:audio|sf' + fxRtTrackChain/Paths/
Start/Splice/Stop/Reconcile/StopAll trong app.jsx; gọi từ rebuildTrackFxGraph,
startTrackPlayback, stopAllPlayback, FX Rack panel.
- `fxRtTrackPaths`: audio nếu clips.length>0 || buffer || sections.length>0;
sf nếu midiItems.length>0 || type==='MIDI' || instrumentId.
- E2E ổn định (pw_e2e2.py ×3): splice 3 audio + 3 sf, track audio out
peak≈0.47 rms≈0.33, sf câm hợp lệ (MIDI taiko không route sfEntry), master
Ozone peak≈0.85-0.99. Master câm khi track mastering_bypass=true — hợp lệ.
- FIX bug bypass toggle: reconcile đọc state CŨ (React setState async) khi
`onChange` gọi `__rebuildTrackFxGraph(track.id)` đồng bộ → bypass không stop
session. Fix: `rebuildTrackFxGraph = (trackId, patch)` merge
`track = { ...track, ...patch }` TRƯỚC khi rebuild/reconcile; 4 call site
(bypass, fxActive, setVstChain, setChain) truyền patch giá trị MỚI.
Verify: toggle bypass-on → reconcile want=[] → stop ngay; bypass-off →
want=["sf"] → start mới + spliced.
- Debug log tạm đã gỡ (giữ `[FX RT track] spliced` + console.warn lỗi);
rebuild bundle 1370231 bytes, served LEN khớp local.
Ponytail còn lại:
- Sub-tab (piano roll) play chưa start fxRt — chỉ main/section play.
- Chain đổi giữa lúc play chưa restart session (start/stop theo play boundary).
- Browser-side queue drop-oldest (8 block) — thay adaptive stretch nếu cần.
- Chưa test UI thật trong WebView (graph insertion AudioWorkletNode) — plumbing
đã verify qua WS; mở app + bật master VST chain + play để xác nhận âm thanh.
## Trạng thái: ✅ MASTER REALTIME UI VERIFIED (2026-08-20, engine sống)
- Verify UI-level: login → inject `window.currentMasteringSettings.vstFxChain`
(path đúng `C:\Program Files\Common Files\VST3\Sonible\purecomp.vst3`) →
insert clip → Play. Monkey-patch `SonicAPI.fxRealtimeStart` ghi payload→sid.
Kết quả (pw_master3.py): call #1 = master (purecomp, engine trả session_id +
ws_url) → WS `SonicForge_FXRT_d19c01fc33da` nhận audio 738 frame, out
peak 0.4622 / rms 0.1442 vs input sine 0.5/0.35 = attenuate ✓.
Track sessions (call #2/#3) chạy song song; stop → WS close, `--realtime-fx`
bridge thoát, chỉ còn 2 `--fx-gui` của user (không đụng). Engine @8000 OK.
- WS-level (rt_test2.py) trước đó: master realtime attenuate peak 0.4621 rms
0.2462 với path đúng `Sonible\purecomp.vst3` (path sai `SoniBook` = passthrough
thầm lặng).
- Nguyên nhân "VST FX PLUGIN trong master chain peak -inf" (trả lời user):
1) Engine bị kill lúc test → `SonicAPI.fxRealtimeStart` fail → `fxRtStart`
catch + toast cảnh báo, KHÔNG session → masterBus.output nối thẳng
analyser → âm nghe được nhưng KHÔNG qua VST → meter plugin -inf.
2) Chain rỗng / vstBypass=true → `fxRtChain()` = [] → không session (âm qua
thẳng, không lỗi) → meter -inf.
3) Meter trong GUI embed (`--fx-gui`) là của instance GUI RIÊNG (config JSON
chỉ path+name; VstGuiEmbed chỉ /frame /ping /input — KHÔNG có audio
endpoint) → GUI embed KHÔNG nhận audio realtime SHM session → meter -inf
VÔ ĐIỀU KIỆN kể cả khi realtime đang chạy. Audio thật đi qua instance
`--realtime-fx` riêng (không hiển thị meter). → ponytail thật: user không
thấy meter plugin thật; nơi thấy tín hiệu = master VU meter của app
(post-chain) hoặc WS session.
- Lưu ý: `masterFxList` (Plugin Picker mastering) load trong useEffect([]) của
MasteringModal mount sẵn từ đầu → trước khi SonicAPI sẵn sàng → picker rỗng
trong headless; track modal mount trễ hơn nên đầy. Workaround: chờ SonicAPI
rồi mới mở modal, hoặc inject chain trực tiếp. (Không phải lỗi engine.)
+2
View File
@@ -152,6 +152,7 @@ Các task nhóm H lấp những khâu còn hở giữa A-G để pipeline chạy
| ✅ A11 | `native_bridge/src/main.cpp` | **[BỔ SUNG]** Sample-accurate: không xử lý MIDI ngay; buffer event theo `sampleOffset`, rẽ block 256 thành các đoạn theo offset, gọi `noteOn/Off` đúng vị trí trước khi render đoạn đó | note timeline không lệch nhịp (>±1 sample test) |
| ✅ A12 | `native_bridge/include/SharedMemoryIPC.h` + engine | **[BỔ SUNG]** Mở rộng `MidiEventIPC`: field `data2` (+`data3` PB MSB); hỗ trợ `0xB CC` (FluidSynth `fluid_synth_cc`), `0xC program change`, `0xE pitch bend` (`fluid_synth_pitch_bend`); VST3 → tham số tương ứng | sustain/CC/pitchbend qua bridge hoạt động |
| ✅ A13 | `native_bridge/src/main.cpp` | **[BỔ SUNG]** Control `type=3 TRANSPORT`: `arg0=0 STOP` → flush note-off toàn channel + `fluid_synth_all_notes_off`; `arg0=1 PLAY` kèm `arg1=playheadSamples` → đồng bộ timeline | Stop → hết âm ngân tức thì |
| ✅ A14 | `native_bridge/src/main.cpp` + `Vst2Instrument.cpp`/`Vst3Instrument.cpp` + `SharedMemoryIPC.h` + `src-tauri/src/lib.rs` + `nativeBridgeService.js` | **[BỔ SUNG]** MIDI-out từ plugin: bridge hiện CHỈ gửi MIDI vào (kAudioMasterWantMidi=1, processEvents) — không đọc MIDI plugin xuất ra. MIDI-generator (Scaler2/Instacomposer/MelodicFlow) xuất chord/MIDI không phải audio → câm trong test audio. Cần: plugin gọi audioMaster → host thu VstEvents (VST2 `audioMaster` callback `audioMasterGetVstVersion`+event queue; VST3 `IEventList` output bus) → push vào SHM midiQueue mới (out) → Rust emit → JS route về DAW track | plugin MIDI-gen xuất event → DAW nhận note (test Scaler2 gõ chord → MIDI ghi nhận) |
### H-B. Rust — bổ sung
@@ -243,5 +244,6 @@ VU analyser tap (C5) → track VU nhảy
- Resampler khi SR AudioContext ≠ bridge (B8/C5).
- Ring buffer chống underrun + VU tap (C5) — kế thừa hành vi `_gainNode.__vuAnalyser` cũ.
- Fallback SonicSF (C1/C6/C7/G6) — spec không đề cập; giữ để app không chết khi bridge lỗi.
- **MIDI-out từ plugin (A14)** — bridge chỉ host MIDI-in; Scaler2/Instacomposer/MelodicFlow xuất MIDI nên không nghe được qua bridge.
- Dev-mode mock (C7) — chạy được trên Linux/browser không có `__TAURI__`.
+57
View File
@@ -0,0 +1,57 @@
# TASKS — Phương án A: Engine xử lý toàn chain (chuẩn DAW)
Trạng thái: `[x]` hoàn thành, `[ ]` chưa. Cập nhật sau mỗi phase.
## Phase 1 — Chain descriptor + protocol hợp nhất
- [x] 1.1 Đặc tả chain slot JSON hợp nhất (type/path/preset_b64/params/bypass/active) — ghi vào doc.
- [x] 1.2 `app/core/fx_realtime.py`: `_chain_json_for_bridge` nhận chain hợp nhất + field params/latency.
- [x] 1.3 `app/core/native_render.py`: `render_fx_chain` nhận chain hợp nhất (builtin+vst3 xen kẽ, giữ order), bỏ giới hạn type vst3/builtin.
- [x] 1.4 job.json realtime + render-fx: schema chain hợp nhất (thay fx_chain cũ).
- [x] 1.5 Protocol SET_PARAM {slot,key,value} (realtime builtin automation) — engine Python nhận + forward.
- [x] 1.6 Protocol REPORT_LATENCY {slot,samples} — engine Python nhận, lưu per-session.
- [x] 1.7 Pytest: chain hợp nhất → job.json đúng schema; SET_PARAM/REPORT_LATENCY round-trip.
## Phase 2 — Bridge C++: BuiltinFxChain (8 DSP)
- [x] 2.1 File `native_bridge/src/BuiltinFxChain.{h,cpp}` — khung chain serial + SEH guard.
- [x] 2.2 Port EQ 4-band (eq) từ Python `_apply_eq4`.
- [x] 2.3 Port EQ Pro (eqpro) RBJ 8-band từ Python `_apply_eqpro`.
- [x] 2.4 Port compressor/limiter từ Python.
- [x] 2.5 Port exciter/rebalance từ Python.
- [x] 2.6 Port imager/maximizer từ JS WebAudio (MASTER_MODULE_IO) — Python không có.
- [x] 2.7 `RealtimeFxChain::buildChain` + RenderFxJob: nhận builtin slot → chạy BuiltinFxChain xen kẽ VST3 đúng thứ tự.
- [x] 2.8 SET_PARAM/REPORT_LATENCY trong bridge (SHM control ring).
- [x] 2.9 Golden test: C++ vs Python `_apply_builtin_fx_chain` (SNR/diff ngưỡng) + order test xen kẽ.
## Phase 3 — Engine Python: render + realtime chain hợp nhất
- [x] 3.1 `render_engine.py`: track/master đọc chain hợp nhất; Python DSP giữ làm fallback (RENDER_ENGINE≠bridge).
- [x] 3.2 Bỏ bất đối xứng imager/maximizer (offline master chain đầy đủ 8 loại qua bridge).
- [x] 3.3 PDC: nối REPORT_LATENCY → align track (render_engine.py:286+333).
- [x] 3.4 Pytest: render offline có imager/maximizer master giống live.
## Phase 4 — Frontend: WebAudio chỉ I/O
- [x] 4.1 Chain model 1 mảng duy nhất (builtin+vst3 theo UI order); migration project cũ vstFxChain.
- [x] 4.2 Master: bỏ MASTER_MODULE_IO graph + rebuildMasteringGraph node-web; 1 worklet master; fader SAU worklet.
- [x] 4.3 Track: bỏ fxMods/sfMods WebAudio builtin; 1 worklet track; gain/pan giữ.
- [x] 4.4 `fxRtChain()`/`fxRtTrackChain()` đọc chain hợp nhất; chainKey so toàn chain.
- [x] 4.5 applyMastering param → SET_PARAM push.
- [x] 4.6 Worklet: forward SET_PARAM; queue drop → adaptive (ponytail).
## Phase 5 — Verify (chuẩn DAW checklist)
- [x] 5.1 Build frontend `node build.mjs`.
- [x] 5.2 Pytest toàn bộ.
- [x] 5.3 Golden live=export (record masterBus vs offline WAV) diff ngưỡng.
- [x] 5.4 Order test: [eq, vst3-delay, limiter] — impulse xác nhận delay sau EQ.
- [x] 5.5 Fader test: chain active, fader -6dB → đỉnh giảm, chain input không đổi.
- [x] 5.6 PDC test: plugin latency 1024 → xuyên pha hết.
- [x] 5.7 Smoke server + render master imager+maximizer+vst3.
- [x] 5.8 Commit + push standalone-shm-bridge.
## Review DAW-standard (sau Phase 5, bổ sung nếu thiếu)
- [x] R1 Track insert chain serial đúng thứ tự UI (builtin+VST xen kẽ).
- [x] R2 Gain/pan sau track chain, trước sum.
- [x] R3 Master chain serial đúng thứ tự trên mix tổng.
- [x] R4 Master fader cuối cùng trước metering/out.
- [x] R5 Live = Export (cùng engine, cùng chain, cùng DSP).
- [x] R6 PDC.
- [x] R7 Gain staging rõ.
- [x] R8 Mono/Stereo xử lý đúng.
+183
View File
@@ -0,0 +1,183 @@
# TASKS: Nhúng VST GUI — tách theo máy (Linux / Windows)
Nguồn: `PLAN_MASTERBUS_VST_GUI_EMBED.md` (Phase 0-4). Mỗi task ghi rõ máy thực
hiện, file đụng, lệnh, deliverable, test, phụ thuộc.
Nguyên tắc tách:
- **Linux** (máy dev hiện tại): viết mọi source code (C++ bridge, Python API,
frontend JSX), build frontend, chạy unit test (mock bridge), commit.
- **Windows** (máy chạy VST thật): build bridge C++ (MSVC/vcpkg), mọi E2E thật
với plugin (Ozone...) — BitBlt/PrintWindow/SendMessage chỉ chạy Windows.
- Linux KHÔNG build được bridge (CMake bỏ VST3/GUI khi không WIN32; `main_linux.cpp`
chỉ render offline). Linux KHÔNG có VST GUI để test E2E thật.
Ký hiệu: `[L]` = làm trên Linux · `[W]` = làm trên Windows.
---
## PHASE 0 — Bridge `--fx-gui` (HTTP server + capture + input relay)
### Task 0L1 — Viết `FxGuiServer.cpp` (hoặc mở rộng RenderFxJob.cpp nếu <300 dòng)
- Máy: `[L]` · File: `native_bridge/src/FxGuiServer.cpp` (mới) + `native_bridge/src/RenderFxJob.cpp` (hook mode `--fx-gui`)
- Nội dung (giữ nguyên phần attach IPlugView có sẵn `fxGuiAttachSafe`):
- Mode `--fx-gui <job.json>`: sau khi attach view, khởi động HTTP server mini
port ngẫu nhiên → in `SF_FXGUI_PORT=<port>` ra stdout (Python đọc).
- `GET /frame` → chụp HWND plugin bằng `BitBlt` (fallback `PrintWindow`
`PW_RENDERFULLCONTENT`) → trả `image/jpeg` quality ~70.
- `POST /input` `{type, x, y, button, deltaY, keyCode}` → `PostMessage`/`SendMessage`
theo tọa độ scale client rect (gửi `WM_MOUSEMOVE` trước click).
- `POST /close` → detach view, thoát exit 0. `GET /ping` → alive.
- Toàn bộ Win32 API → bọc `#ifdef _WIN32` hoặc chỉ build khi WIN32 (xem Task 0L2).
- Lệnh kiểm tra: không build được trên Linux — chỉ kiểm tra bằng mắt/syntax
(`g++ -fsyntax-only` không đủ do thiếu windows.h → bỏ qua, sang Task 0W1).
- Deliverable: source bridge có mode `--fx-gui`.
- Phụ thuộc: không.
### Task 0L2 — Cập nhật `CMakeLists.txt` cho source mới
- Máy: `[L]` · File: `native_bridge/CMakeLists.txt`
- Thêm source file mới vào target `daw_vst_bridge` CHỈ khi `WIN32` (giữ Linux build
không vỡ — Linux hiện build `main_linux.cpp` render-only).
- Lệnh kiểm tra: `cmake -S native_bridge -B /tmp/nb-linux-check && cmake --build /tmp/nb-linux-check --target daw_vst_bridge` → PASS trên Linux.
- Deliverable: Linux build vẫn xanh, Windows build có file mới.
- Phụ thuộc: 0L1.
### Task 0W1 — Build bridge trên Windows
- Máy: `[W]` · Lệnh: `powershell -ExecutionPolicy Bypass -File build/scripts/build_native_bridge.ps1`
(bootstrap vcpkg, fluidsynth, sfizz 1.2.3, vst3sdk submodule, CMake VS2019 x64,
copy `daw_vst_bridge.exe` + `libfluidsynth-3.dll` → `src-tauri\binaries\`).
- Điều kiện máy: VS Build Tools 2019+, CMake, git, đã pull submodule
(`git submodule update --init --recursive -- native_bridge/vst3sdk`).
- Deliverable: `native_bridge/build/Release/daw_vst_bridge.exe` + sidecar trong `src-tauri\binaries\`.
- Test: exe tồn tại, chạy `--help` không crash.
- Phụ thuộc: 0L1, 0L2 (pull code mới về Windows trước).
### Task 0W2 — Self-check bridge `--fx-gui` thủ công
- Máy: `[W]`
- Lệnh:
1. Tạo `job.json`: `{"path": "C:\\VST\\Ozone11.vst3", "name": "Ozone 11"}`.
2. `native_bridge\build\Release\daw_vst_bridge.exe --fx-gui job.json` → đọc port từ stdout.
3. `curl http://127.0.0.1:<port>/ping` → alive.
4. `curl -o frame.jpg http://127.0.0.1:<port>/frame` → JPEG, kích thước = kích thước GUI plugin.
5. `curl -X POST http://127.0.0.1:<port>/input -H "Content-Type: application/json" -d '{"type":"mousemove","x":100,"y":100,"button":0}'` rồi `{"type":"mousedown","x":100,"y":100,"button":0}` + `mouseup` → click trúng nút trong GUI (quan sát đổi trạng thái).
6. `curl -X POST http://127.0.0.1:<port>/close` → process thoát exit 0.
- Deliverable: /frame trả ảnh đúng, /input điều khiển được, /close sạch.
- Phụ thuộc: 0W1.
---
## PHASE 1 — Python API (embed_url + registry + close + CORS)
### Task 1L1 — Mở rộng `open_fx_gui` + thêm `fx-gui/close`
- Máy: `[L]` · File: `app/api/v1/plugins.py`
- Nội dung:
- Spawn bridge với `--fx-gui` (thay/giữ `--open-fx-gui`), đọc port từ stdout
(`SF_FXGUI_PORT=`) với timeout → trả `{ success, embed_url: "http://127.0.0.1:<port>", already_running }`.
- Giữ registry `_FX_GUI_PROCESSES` + `_sf_plugin_path` chống mở trùng.
- Thêm `POST /fx-gui/close` `{path}` → tìm process theo `_sf_plugin_path` → terminate.
- CORS: đảm bảo `Access-Control-Allow-Origin` cho origin app (port động) — kiểm tra
middleware hiện có trong `plugins.py`, thêm nếu thiếu.
- Lệnh kiểm tra: `python3 -m pytest tests/test_plugin_api.py -q` → PASS (thêm unit
test: mock `subprocess.Popen` → assert spawn args, embed_url parse, close kill,
registry prune).
- Deliverable: API mới + unit test.
- Phụ thuộc: 0L1 (chỉ cần biết contract stdout port; có thể làm song song 0W).
### Task 1W1 — Test API thật trên Windows
- Máy: `[W]` · Điều kiện: chạy app (backend FastAPI) + bridge đã build (0W1).
- Lệnh:
1. `curl -X POST http://127.0.0.1:<app>/api/v1/plugins/fx-gui/open -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"path":"C:\\VST\\Ozone11.vst3","name":"Ozone 11"}'` → `embed_url`.
2. `curl http://<embed_url>/frame` → 200 JPEG.
3. `curl -X POST .../fx-gui/close -d '{"path":"C:\\VST\\Ozone11.vst3"}'` → process chết (Task Manager / `Get-Process`).
- Deliverable: API chạy thật, embed_url truy cập được, close sạch.
- Phụ thuộc: 1L1, 0W1.
---
## PHASE 2 — MasteringModal: embed GUI trong panel
### Task 2L1 — Code embed trong `app/static/js/app.jsx` (MasteringModal + vstRow)
- Máy: `[L]` · File: `app/static/js/app.jsx`
- Nội dung (state ở component panel, truyền qua props như đã làm với `showCarla`):
- State per-slot `guiOpen: { url } | null` (MasteringModal).
- Nút GUI → toggle: mở → gọi API `fx-gui/open` → lưu `embed_url`; đang mở → đóng.
- Vùng embed: `<img src={url + '/frame'}>` refresh `setInterval` 100ms; overlay
`onMouseDown/Move/Up` + `onWheel` → `fetch(url + '/input', POST)` tọa độ theo
bounding rect; `onKeyDown/Up` trên container (chỉ khi panel focus).
- Swap graph↔GUI: ẩn vùng canvas EQ/imager bằng `hidden` khi GUI mở, hiện khi đóng.
- Click tên VST trong slot khi GUI đang đóng → mở lại embed (ping trước).
- Nút X đóng GUI → `POST /close`, clearInterval, trả graph. Xóa slot khi GUI mở
→ cũng gọi `/close`.
- `/ping` fail (bridge chết) → trả graph + toast.
- Lệnh kiểm tra:
- `node build.mjs` (hoặc `npm run build`) → BUILD OK (syntax JSX).
- Chạy backend Linux + mở app → verify UI không vỡ (GUI nhúng không test được
thật vì thiếu bridge — dùng mock nếu cần: server node trả /frame JPEG fake + /input log).
- Deliverable: MasteringModal nhúng GUI + swap + đóng sạch.
- Phụ thuộc: 1L1 (contract API).
### Task 2W1 — E2E thật MasteringModal
- Máy: `[W]` · Điều kiện: 2L1 + 0W1 + 1W1 pass.
- Test tay: mở app → Mastering Panel → thêm Ozone → GUI nhúng hiện TRONG panel
(thay vùng graph) → click/scroll điều khiển plugin → đóng → graph trả về →
click tên VST mở lại → Xóa slot khi GUI mở → GUI đóng + slot xóa, không crash.
- Deliverable: UI mastering hoàn chỉnh.
- Phụ thuộc: 2L1, 1W1.
---
## PHASE 3 — FXRackModal: embed GUI trong panel
### Task 3L1 — Code embed trong FXRackModal
- Máy: `[L]` · File: `app/static/js/app.jsx` (FXRackModal — vùng scope/eq canvas)
- Nội dung: tương tự 2L1 nhưng trong FXRackModal; lưu ý vùng thay thế là
`scopeCanvasRef`/`eqCurveRef` (đã có fallback Carla ở đây — không đụng).
- Lệnh kiểm tra: `node build.mjs` → BUILD OK; backend Linux + mở app → UI không vỡ.
- Deliverable: FXRackModal nhúng GUI + swap + đóng sạch.
- Phụ thuộc: 2L1 (tái dùng pattern), 1L1.
### Task 3W1 — E2E thật FXRackModal
- Máy: `[W]` · Điều kiện: 3L1.
- Test tay: FX Rack Panel → thêm VST FX → GUI nhúng trong panel → điều khiển →
đóng → graph trả về. Track cũ có module Carla → vẫn hiện fallback "đã bị gỡ" +
nút gỡ module (không vỡ).
- Deliverable: UI fx rack hoàn chỉnh.
- Phụ thuộc: 3L1.
---
## PHASE 4 — Dọn + commit + regression
### Task 4L1 — Dọn code, docs, commit
- Máy: `[L]`
- Nội dung:
- Toast lỗi đầy đủ (mở GUI fail, bridge chết, close fail).
- `clearInterval` khi unmount/close (tránh leak).
- Cập nhật `PLAN_MASTERBUS_VST_GUI_EMBED.md` trạng thái HOÀN THÀNH + ghi chú
giới hạn (FPS ~10-15, Windows-only).
- `git add -A && git commit` (tin nhắn: `embed native VST GUI in mastering + fx rack`).
- Lệnh kiểm tra:
- `node build.mjs` → BUILD OK.
- `python3 -m pytest tests/ -q` → toàn bộ test hiện có PASS (1 fail pre-existing
`test_find_bridge_exe_dev` do thiếu binary Linux — ghi chú, không phải regression).
- Deliverable: commit sạch, docs cập nhật.
- Phụ thuộc: 2L1, 3L1 (code ổn).
### Task 4W1 — Regression full trên Windows
- Máy: `[W]` · Điều kiện: 4L1 (pull code mới).
- Test tay toàn bộ: Mastering + FX Rack GUI nhúng; render offline (`--render-fx`)
không vỡ; Carla (MasteringModal) vẫn chạy; preset upload; scan plugin.
- Deliverable: bản Windows sẵn sàng dùng.
- Phụ thuộc: 4L1.
---
## Thứ tự thực hiện tối ưu
```
Linux: 0L1 → 0L2 ──→ 1L1 ──→ 2L1 ──→ 3L1 ──→ 4L1 (commit)
Windows: 0W1 → 0W2 ──→ 1W1 ──→ 2W1 ──→ 3W1 ──→ 4W1
(pull code sau 0L2) (pull sau 2L1) ...
```
Song song được: 1L1 (contract stdout) với 0W1/0W2; 2L1/3L1 với 0W/1W.
Chặn: mọi task Windows cần pull code từ Linux trước.
+103
View File
@@ -0,0 +1,103 @@
# TASKS WINDOWS — Chuẩn hóa luồng audio lossless (SonicForgeStudio)
Branch: `standalone-shm-bridge` — Pull code mới nhất trước khi làm.
## Bối cảnh
Linux đã hoàn tất chuẩn hóa luồng audio theo skill `lossless-audio-compliance`
(Float32 nội bộ, WAV IEEE Float32 khi render/export, TPDF dither chỉ ở bước
lượng tử cuối, resample Kaiser ≥130dB, null-test gate). Code C++/Python/JS đã
sửa trên Linux và **commit sẵn** — phần còn lại trên Windows chỉ là BUILD lại
binary, COPY vào `install/`, VERIFY, COMMIT.
## Đã sửa (kèm theo branch này)
| File | Thay đổi |
|---|---|
| `native_bridge/src/RenderJob.cpp`, `RenderFxJob.cpp` | WAV writer: 16-bit PCM (truncate, không dither) → **IEEE Float32** (format 3, 32-bit) |
| `app/core/render_engine.py`, `app/api/v1/audio.py`, `app/api/v1/plugins.py` | `sf.write(...)` → `subtype="FLOAT"` (4 chỗ) |
| `app/core/audio_features.py`, `app/core/render_engine.py` | `resample_poly` → `window=("kaiser", 16)` (stopband ≥130dB) |
| `app/static/js/app.jsx` + `app.precompiled.js` (đã build trên Linux) | Encoder client → Float32 mặc định; TPDF dither cho 16/24/8-bit; sửa bug 24-bit ghi nhầm 8-bit; UI thêm "32 float" |
| `app/templates/index.html` | bump cache `?v=20260818` |
| `tests/test_lossless_compliance.py` (mới) | Null-test gate (Rule 6) + assert float32 WAV (Rule 7) + bridge render deterministic |
| `native_bridge/tests/test_offline_render.py` | `read_wav` hiểu cả IEEE float32 lẫn PCM 16-bit |
| `Dockerfile` | fix `git clone --branch v1.2.3` (tag không tồn tại) → clone default |
## Tasks trên Windows (theo thứ tự)
> ✅ **HOÀN TẤT (2026-08-18)** — đã chạy trên máy Windows `192.168.1.24` (Win10 19044):
> Git 2.55, VS Build Tools 2022 MSVC 14.44, Python 3.11.9, Node 20.20.2,
> Rust 1.97.1, vcpkg + fluidsynth 2.5.7, sfizz-src clone
> (`native_bridge/sfizz-src`, gitignored). Bridge build:
> `cmake -S native_bridge -B build/bridge-win -DCMAKE_TOOLCHAIN_FILE=C:\Users\locpham\vcpkg\scripts\buildsystems\vcpkg.cmake`
> + `-DSFIZZ_SHARED=OFF`; bắt buộc `-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL`
> + patch `sfizz-src/cmake/SfizzConfig.cmake` (wrap `CMAKE_MSVC_RUNTIME_LIBRARY`
> trong `if(NOT ...)`) — patch chỉ ở máy đó, cần làm lại khi clone sfizz mới.
> Verify: `tests\test_lossless_compliance.py` 9/9 PASS,
> `native_bridge\tests\test_offline_render.py` 9/9 PASS (cần
> `native_bridge\build\Release\daw_vst_bridge.exe` + copy `g2_test.sfz` vào
> `sfizz-src\tests\TestFiles\`). Bundle: `src-tauri\target\release\bundle\nsis\SonicForgeDAW_1.0.0_x64-setup.exe`.
> Binary đã copy vào `install/`: `daw_vst_bridge.exe`, `sonicforge-daw.exe`,
> `libfluidsynth-3.dll`, `jpeg62.dll`. Tauri build cần
> `src-tauri\binaries\daw_vst_bridge-x86_64-pc-windows-msvc.exe` (externalBin
> naming, `binaries/` gitignored). `build_windows.ps1` bước [5/6]
> Invoke-WebRequest lỗi console-buffer khi chạy qua SSH — dùng curl tải
> `vc_redist.x64.exe`.
### 1. Pull code mới nhất
```powershell
cd C:\Users\locpham\SonicForgeStudio
git checkout standalone-shm-bridge
git pull origin standalone-shm-bridge
```
### 2. Build lại `daw_vst_bridge.exe` (WAV writer float32)
Bridge build bằng CMake + vcpkg (fluidsynth) + sfizz-src (xem
`native_bridge/CMakeLists.txt`, `build/_sfizz.bat`):
```powershell
# sfizz từ source nếu chưa có (native_bridge/sfizz-src)
# vcpkg install fluidsynth
cmake -S native_bridge -B build/bridge-win -DCMAKE_BUILD_TYPE=Release `
-DCMAKE_TOOLCHAIN_FILE=<vcpkg-root>\scripts\buildsystems\vcpkg.cmake
cmake --build build/bridge-win --target daw_vst_bridge --config Release
Copy-Item build/bridge-win/Release/daw_vst_bridge.exe install/daw_vst_bridge.exe -Force
```
Nếu có script build sẵn dùng script đó; **bắt buộc copy đè** `install/daw_vst_bridge.exe`.
### 3. Build lại engine + app (`build_windows.ps1`)
```powershell
powershell -ExecutionPolicy Bypass -File build_windows.ps1
```
(Lưu ý: `app.precompiled.js` đã build sẵn trên Linux — bước [2/6] chỉ cần chạy
lại nếu bạn sửa `app.jsx`.)
### 4. Copy binary mới vào `install/`
```powershell
Copy-Item src-tauri\target\release\sonicforge-daw.exe install\sonicforge-daw.exe -Force
# hoặc từ NSIS/MSI output tùy quy trình hiện tại
```
### 5. Verify (null-test gate + định dạng float32)
```powershell
python tests\test_lossless_compliance.py
python native_bridge\tests\test_offline_render.py
```
Kỳ vọng:
- `bridge output IEEE float32 (tag 3)` PASS — WAV render ra format 3, 32-bit
- `bridge render deterministic (byte-identical)` PASS
- `null test ... bit-exact` PASS
- `test_offline_render.py` 3 test đầu PASS với `read_wav` mới
### 6. Commit + push
```powershell
git add install/daw_vst_bridge.exe install/sonicforge-daw.exe
git commit -m "feat(audio): lossless float32 WAV render/export (bridge + engine) — chuẩn hóa luồng audio"
git push origin standalone-shm-bridge
```
## Lưu ý
- Không đổi lại WAV writer sang 16-bit: render nội bộ phải là IEEE Float32
(skill Rule 4/7); dither TPDF chỉ áp dụng ở bước export cuối (client 16/24-bit).
- `install/daw_vst_bridge` (ELF Linux) không commit — chỉ commit `.exe`.
- Nếu `sonicforge-daw.exe` bundle cũ vẫn dùng `app.precompiled.js` cũ: xóa cache
trình duyệt / dùng `?v=` mới đã bump.
+35 -5
View File
@@ -1,6 +1,7 @@
# THIRD-PARTY LICENSES — SonicForgeStudio
Kiểm tra ngày 2026-08-07. Danh sách thành phần bên thứ ba + license.
Kiểm tra ngày 2026-08-15 (verify từ PyPI/GitHub/LICENSE.txt trong wheel).
Danh sách thành phần bên thứ ba + license. Bản tổng hợp: `LICENSE.md`.
## 1. Dependencies (npm — package.json)
| Thành phần | License | Ghi chú |
@@ -8,6 +9,34 @@ Kiểm tra ngày 2026-08-07. Danh sách thành phần bên thứ ba + license.
| react / react-dom 19.x | MIT | OK |
| @babel/cli, @babel/core, @babel/preset-react | MIT | Chỉ build-time |
| jsdom | MIT | Chỉ build-time/test |
| @tauri-apps/cli 2.x | Apache-2.0 OR MIT | Desktop shell (src-tauri) — build-time |
## 1b. Dependencies (Python — requirements.txt)
| Thành phần | License | Ghi chú |
|---|---|---|
| fastapi | MIT | OK |
| uvicorn | BSD-3-Clause | OK |
| celery | BSD-3-Clause | OK |
| redis | MIT | OK |
| python-multipart | Apache-2.0 | OK |
| librosa | ISC | OK |
| pydub | MIT | OK |
| numpy | BSD-3-Clause (kèm 0BSD, MIT, Zlib, CC0-1.0) | OK |
| scipy | BSD-3-Clause (kèm OpenBLAS BSD-3, GCC runtime GPL-3.0-with-exception) | OK — exception cho phép dùng |
| soundfile | BSD-3-Clause | OK (libsndfile core là LGPL-2.1+) |
| jinja2 | BSD-3-Clause | OK |
| httpx | BSD-3-Clause | OK |
| jsonschema | MIT | OK |
| mido | MIT | OK |
| pyfluidsynth | MIT (wrapper) | OK — link FluidSynth LGPL-2.1+ |
| sf2utils 1.0.0 | **LGPL-3.0+** | ⚠️ LICENSE.txt trong wheel là LGPLv3 (metadata PyPI ghi nhầm "GPLv3+"). Dùng runtime `soundfont_inspector.py` — giữ notice + relinkable khi phân phối binary |
## 1c. Native bridge (native_bridge/, C++)
| Thành phần | License | Ghi chú |
|---|---|---|
| fluidsynth (vcpkg) | LGPL-2.1+ | Link tĩnh qua vcpkg — bắt buộc notice + relinkable object |
| vst3sdk (git submodule) | MIT | © 2026 Steinberg Media Technologies GmbH — tương thích MIT project |
| vestige (dự kiến cho VST2) | mã nguồn mở | Chưa thêm vào repo — xác nhận license gốc khi implement |
## 2. Thư viện nhúng (app/static)
| File | Nguồn | License | Trạng thái |
@@ -31,7 +60,8 @@ Kiểm tra ngày 2026-08-07. Danh sách thành phần bên thứ ba + license.
## KẾT LUẬN
- **Không phát hiện vi phạm bản quyền rõ ràng** (không có code GPL bị nhúng vào project MIT; LGPL của FluidSynth tương thích nếu giữ notice).
- **3 điểm cần xử lý trước khi phân phối công khai:**
1. Bổ sung `LICENSE` text (MIT + LGPL-2.1) + attribution cho SpessaSynth (MIT notice) và Tailwind.
2. Ghi attribution SF2 Rich Nagel ("General MIDI SoundFont v3.0 — © 2006-2010 Rich 'Weeds' Nagel — Some rights reserved") hoặc thay soundfont khác license rõ.
3. Không nhúng SGM-V2.01 vào bản phân phối (hạn chế redistribution).
- **Cần xử lý trước khi phân phối công khai:**
1. **sf2utils (LGPL-3.0+)** — giữ notice + relinkable object khi phân phối binary.
2. Bổ sung `LICENSE` text (MIT + LGPL-2.1) + attribution cho SpessaSynth (MIT notice) và Tailwind.
3. Ghi attribution SF2 Rich Nagel ("General MIDI SoundFont v3.0" — © 2006-2010 Rich 'Weeds' Nagel — Some rights reserved) hoặc thay soundfont khác license rõ.
4. Không nhúng SGM-V2.01 vào bản phân phối (hạn chế redistribution).
+163
View File
@@ -0,0 +1,163 @@
# WALKTHROUGH WINDOWS — Nhúng native VST GUI (phần Windows)
Máy Windows thực hiện nốt phần còn lại theo `TASKS_LINUX_WINDOWS.md`
(tasks 0W1, 0W2, 1W1, 2W1, 3W1, 4W1). Code Linux đã commit và push — **pull
code mới về trước**:
```powershell
git pull origin standalone-shm-bridge
git submodule update --init --recursive -- native_bridge/vst3sdk
```
Điều kiện máy: VS Build Tools 2019+, CMake, git, git-lfs (nếu vst3sdk dùng).
---
## 0W1 — Build bridge
```powershell
powershell -ExecutionPolicy Bypass -File build/scripts/build_native_bridge.ps1
```
Lưu ý thay đổi mới trong phần này (so với lần build cũ):
- `native_bridge/vcpkg.json` **thêm `libjpeg-turbo`** — vcpkg manifest tự cài,
không cần làm gì thêm, nhưng lần build đầu sẽ tải thêm package này.
- `native_bridge/CMakeLists.txt`: thêm `src/FxGuiServer.cpp` vào target
`daw_vst_bridge` (chỉ khi WIN32) + `find_package(JPEG REQUIRED)` +
link `JPEG::JPEG`. Nếu script build cũ không chạy `cmake -S native_bridge`
mà dùng đường dẫn cố định, hãy đảm bảo nó re-configure (xóa
`native_bridge/build` nếu cần).
- Script phải copy `daw_vst_bridge.exe` + DLL (fluidsynth, jpeg) sang
`src-tauri\binaries\` (chỉ các DLL cần thiết; libjpeg-turbo thường static
theo manifest, nếu build động thì copy luôn `jpeg62.dll`).
- Trên máy thật: script hiện KHÔNG copy `jpeg62.dll` (build động) — copy tay:
`copy native_bridge\build\Release\jpeg62.dll src-tauri\binaries\`.
**PASS khi**: `native_bridge\build\Release\daw_vst_bridge.exe` tồn tại và
`daw_vst_bridge.exe --help` không crash.
---
## 0W2 — Self-check bridge `--fx-gui` (chưa cần app)
1. Tạo `job.json` cạnh exe:
```json
{"path": "C:\\VST\\Ozone11.vst3", "name": "Ozone 11"}
```
(đổi path sang plugin VST3 bất kỳ đã cài trên máy Windows).
2. Chạy (giữ terminal mở):
```powershell
native_bridge\build\Release\daw_vst_bridge.exe --fx-gui job.json
```
- stdout in dòng `SF_FXGUI_PORT=<port>` → ghi lại port.
- **Không có cửa sổ plugin hiện ra** — window bị đặt off-screen
(-20000,-20000) để capture; điều khiển qua HTTP, đây là thiết kế.
3. Kiểm tra từ terminal khác:
```powershell
curl http://127.0.0.1:<port>/ping
curl -o frame.jpg http://127.0.0.1:<port>/frame
curl -X POST http://127.0.0.1:<port>/input -H "Content-Type: application/json" -d '{"type":"mousemove","x":100,"y":100,"button":0}'
curl -X POST http://127.0.0.1:<port>/input -H "Content-Type: application/json" -d '{"type":"mousedown","x":100,"y":100,"button":0}'
curl -X POST http://127.0.0.1:<port>/input -H "Content-Type: application/json" -d '{"type":"mouseup","x":100,"y":100,"button":0}'
curl -X POST http://127.0.0.1:<port>/input -H "Content-Type: application/json" -d '{"type":"wheel","x":100,"y":100,"deltaY":120}'
curl -X POST http://127.0.0.1:<port>/close
```
- `/frame` → JPEG (mở `frame.jpg` xem GUI plugin hiển thị đúng, không đen).
- `/input` → click trúng nút trong GUI (plugin đổi trạng thái — để biết
được, hãy mở song song GUI plugin bằng app cũ hoặc quan sát frame.jpg
trước/sau click).
- `/close` → tiến trình thoát exit 0.
**PASS khi**: frame không đen, input điều khiển được, close sạch.
> Nếu `/frame` ra ảnh đen dù GUI thật hiển thị: bridge đã tự fallback
> `PrintWindow PW_RENDERFULLCONTENT` khi `BitBlt` đen; plugin layered/DirectX
> (hiếm) có thể cần xử lý thêm — ghi lại plugin nào bị để theo dõi.
---
## 1W1 — Test API thật (backend FastAPI chạy trên Windows)
Chạy app như bình thường (backend + frontend). Frontend gọi:
- `POST /api/v1/plugins/fx-gui` `{path, name, embed: true}` → trả
`{success, started, already_running, embed_url, cmd}`.
- `POST /api/v1/plugins/fx-gui/close` `{path}` → trả `{success, closed}`.
Cách test nhanh bằng curl (cần token — lấy từ app sau khi login, hoặc dùng
UI thay vì curl; endpoint yêu cầu `Authorization: Bearer <token>`):
```powershell
curl -X POST http://127.0.0.1:8000/api/v1/plugins/fx-gui -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"path":"C:\\VST\\Ozone11.vst3","name":"Ozone 11","embed":true}'
curl http://<embed_url>/frame -o frame.jpg
curl -X POST http://127.0.0.1:8000/api/v1/plugins/fx-gui/close -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"path":"C:\\VST\\Ozone11.vst3"}'
```
- `embed:true` (mặc định `false` → cửa sổ rời kiểu cũ, giữ nguyên).
- Port backend có thể khác 8000 — xem log app.
- CORS đã mở `*` + bridge tự trả `Access-Control-Allow-Origin: *` → browser
LAN gọi thẳng `embed_url` được.
**PASS khi**: open trả `embed_url` truy cập được `/frame`; close làm process
chết (`Get-Process daw_vst_bridge` không còn).
---
## 2W1 — E2E MasteringModal
1. Mở app → **Mastering Panel**.
2. Thêm VST (Ozone) vào master chain → GUI TỰ MỞ (addMasterVst gọi openMasterVstGui).
Bấm thêm nút GUI trên slot lúc này → backend trả `already_running:true` + toast
"GUI của plugin này đang mở sẵn" — đúng thiết kế, không spawn 2 GUI.
3. Kỳ vọng: GUI plugin hiện **TRONG panel** (thay vùng graph EQ/imager),
header hiện "VST GUI — nhúng (frame-capture · ~10fps)".
4. Click/scroll/kéo trong vùng GUI → plugin phản hồi (thay preset, xoay nút).
5. Bấm "✕ Đóng GUI" → về graph; chờ ~2-3s (bridge thoát hẳn) rồi bấm nút GUI lại
→ mở lại. Bấm ngay trong ~1s đầu có thể vẫn nhận `already_running` (process
chưa thoát) — không phải lỗi.
6. Xóa slot khi GUI đang mở → GUI đóng + slot xóa, không crash.
7. Tắt bridge thủ công (Task Manager kill `daw_vst_bridge`) → sau ~2s panel
toast "Bridge VST GUI đã mất kết nối" và tự trả về graph.
**PASS khi**: toàn bộ thao tác trên ổn định.
---
## 3W1 — E2E FXRackModal
1. Mở app → **FX Rack Panel** (track bất kỳ).
2. Thêm VST FX vào insert chain → bấm nút GUI → GUI nhúng trong panel (thay
vùng WORKSPACE CONTROLS + WAVE OBSERVER).
3. Điều khiển → đóng → graph trả về.
4. Track cũ có module **Carla** → vẫn hiện fallback "Carla Bridge đã bị gỡ
khỏi FX Rack" + nút gỡ module (không vỡ).
**PASS khi**: ổn định như 2W1.
---
## 4W1 — Regression full
Test tay toàn bộ:
- Mastering + FX Rack GUI nhúng (2W1/3W1).
- Render offline (`--render-fx`) không vỡ — export WAV có FX.
- Carla (MasteringModal) vẫn chạy.
- Preset upload, scan plugin vẫn OK.
**PASS khi**: không có regression so với trước embed.
---
## Ghi chú chung
- Frame capture ~10fps (img refresh 100ms) — đủ chỉnh thông số, không phải
realtime video.
- Feature chỉ chạy Windows desktop (có VST GUI); Linux giữ bridge SF2/SFZ.
- Nếu sửa gì trên Windows: sửa ngược lên Linux repo (app.jsx là source,
`app.precompiled.js` là bản build — đừng sửa tay).
- Lỗi thường gặp:
- `--fx-gui` không in port → check job.json path đúng VST3, bridge build
có `libjpeg-turbo` (thiếu JPEG → lỗi link).
- `/frame` đen → plugin layered window; ghi plugin + thử fallback đã có.
- App không mở GUI → xem log backend: thiếu `embed` trong payload hoặc
`find_bridge_exe` không thấy exe trong `install/` (cần copy exe + DLL
vào `src-tauri\binaries\` và app dùng đúng thư mục đó).
+171 -3
View File
@@ -56,6 +56,21 @@ class ExportRequest(BaseModel):
sample_rate: int = 44100
bit_depth: int = 16
class MidiNoteModel(BaseModel):
pitch: int = 60
start_beat: float = 0.0
duration_beats: float = 1.0
velocity: float = 0.8 # 0..1
class MidiExportTrackModel(BaseModel):
name: str = "Track"
notes: List[MidiNoteModel] = []
class MidiExportRequest(BaseModel):
bpm: float = 120.0
ppq: int = 480
tracks: List[MidiExportTrackModel] = []
class AIAnalysisRequest(BaseModel):
file_id: str
api_base_url: Optional[str] = None
@@ -153,7 +168,8 @@ async def download_audio(file_id: str):
path = _resolve_storage_path(file_id)
if not path:
raise HTTPException(status_code=404, detail="File not found")
return FileResponse(path, media_type="audio/wav", filename=os.path.basename(path))
media = "audio/midi" if os.path.splitext(path)[1].lower() == ".mid" else "audio/wav"
return FileResponse(path, media_type=media, filename=os.path.basename(path))
@router.get("/waveform/{file_id}")
async def get_waveform(file_id: str, num_peaks: int = Query(default=800, ge=50, le=4000)):
@@ -226,6 +242,74 @@ async def export_audio(req: ExportRequest, current_user: Optional[dict] = Depend
"file_id": _safe_file_id(req.file_id)
}
# ── MIDI export (BIG_PHASE_3 Phase 1) ──
# Sinh file .mid format 1 bằng stdlib thuần — trước đây frontend tự build bytes
# rồi blob-download (bị WebView2 chặn sau khi chuyển standalone). Server sinh
# file → GET /download/{file_id} (network download, pattern đã chạy ổn định).
def _write_vlq(data: bytearray, value: int) -> None:
buf = [value & 0x7F]
while value > 0x7F:
value >>= 7
buf.append(0x80 | (value & 0x7F))
for b in reversed(buf):
data.append(b)
def _build_midi_file(req: MidiExportRequest) -> bytes:
import struct
tpb = max(1, req.ppq or 480)
track_chunks = []
for tr in req.tracks:
events = []
for n in tr.notes:
if (n.duration_beats or 0) <= 0:
continue
pitch = max(0, min(127, int(round(n.pitch or 60))))
vel = max(1, min(127, int(round((n.velocity if n.velocity is not None else 0.8) * 127))))
start = max(0, int(round((n.start_beat or 0) * tpb)))
end = start + max(1, int(round((n.duration_beats or 1) * tpb)))
events.append((start, 0x90, pitch, vel))
events.append((end, 0x80, pitch, 0))
if not events:
continue # bỏ track không có nốt (trước đây chèn "rest note" pitch 0)
# note_off (0x80) trước note_on (0x90) cùng tick — tránh note kẹt
events.sort(key=lambda e: (e[0], 0 if e[1] == 0x80 else 1))
body = bytearray()
name_bytes = (tr.name or "Track").encode("utf-8", "replace")[:255]
_write_vlq(body, 0) # delta-time 0 bat buoc truoc meta event - file chuan MIDI (mido/DAW parse duoc)
body.append(0xFF); body.append(0x03); body.append(len(name_bytes)); body.extend(name_bytes)
last = 0
for tick, status, pitch, vel in events:
_write_vlq(body, tick - last)
last = tick
body.append(status); body.append(pitch); body.append(vel)
_write_vlq(body, 0)
body.append(0xFF); body.append(0x2F); body.append(0x00)
chunk = bytearray(b"MTrk")
chunk += struct.pack(">I", len(body))
chunk += body
track_chunks.append(bytes(chunk))
if not track_chunks:
return b""
header = bytearray(b"MThd")
header += struct.pack(">IHHH", 6, 1, len(track_chunks), tpb)
return bytes(header) + b"".join(track_chunks)
@router.post("/export/midi")
async def export_midi(req: MidiExportRequest, current_user: Optional[dict] = Depends(get_optional_user)):
if current_user:
enforce_password_changed(current_user)
data = _build_midi_file(req)
if not data:
raise HTTPException(status_code=400, detail="Không có dữ liệu MIDI nào để xuất")
file_id = "midi_export_%s.mid" % uuid.uuid4().hex[:12]
with open(os.path.join(settings.PROCESSED_DIR, file_id), "wb") as f:
f.write(data)
return {
"file_id": file_id,
"download_url": "/api/v1/audio/download/%s" % file_id,
"bytes": len(data)
}
@router.post("/ai-scan")
async def ai_scan_audio(req: AIScanRequest, current_user: Optional[dict] = Depends(get_optional_user)):
"""
@@ -280,7 +364,7 @@ async def ai_cut_audio(req: AICutRequest, current_user: Optional[dict] = Depends
if data.ndim > 1:
data = data.T
sliced, z_start, z_end = await asyncio.to_thread(AIDSPEngine.slice_and_copy_with_zero_crossing, data, sr, req.selection_start, req.selection_end)
sf.write(out_path, sliced.T if sliced.ndim > 1 else sliced, sr)
sf.write(out_path, sliced.T if sliced.ndim > 1 else sliced, sr, subtype="FLOAT")
dur = z_end - z_start
else:
z_start = round(req.selection_start, 4)
@@ -313,7 +397,7 @@ async def run_python_dsp_tool(req: PythonToolRequest, current_user: Optional[dic
wave = PythonToolsEngine.generate_synth_wave(req.wave_type or "sine", req.freq or 440.0, req.duration or 2.0)
output_file_id = f"user_{user_id}_synth_{req.wave_type}_{uuid.uuid4().hex[:6]}.wav"
out_path = os.path.join(settings.PROCESSED_DIR, output_file_id)
sf.write(out_path, wave, 44100)
sf.write(out_path, wave, 44100, subtype="FLOAT")
return {
"success": True,
"message": f"Generated {req.wave_type} synth wave ({req.freq}Hz)",
@@ -401,6 +485,90 @@ async def list_user_files(req: MyFilesRequest, current_user: dict = Depends(get_
user_files.sort(key=lambda x: x["created_at"], reverse=True)
return user_files
class ReferenceAnalyzeRequest(BaseModel):
path: Optional[str] = None # local path (media/browse server mode)
file_id: Optional[str] = None # uploaded/processed file id
def _spectral_centroid_hz(audio, sr):
"""Tone proxy: mean spectral centroid (Hz) over 1 s windows.
Mono downmix + decimate to ~8 kHz (bounded cost). 0.0 for silence."""
import numpy as np
from scipy.signal import resample_poly
x = audio
if x.ndim > 1:
x = np.mean(x, axis=0)
if x.size == 0 or not np.any(x):
return 0.0
g = max(1, int(round(sr / 8000.0)))
x = resample_poly(x, 1, g)
sr8 = float(sr) / g
win = int(round(1.0 * sr8))
if win < 64 or x.size < win:
return 0.0
hann = np.hanning(win)
freqs = np.fft.rfftfreq(win, 1.0 / sr8)
cents = []
step = win // 2
for i in range(0, x.size - win, step):
mag = np.abs(np.fft.rfft(x[i:i + win] * hann))
msum = float(mag.sum())
if msum > 1e-9:
cents.append(float((freqs * mag).sum() / msum))
if not cents:
return 0.0
return float(np.mean(cents))
@router.post("/reference-analyze")
async def analyze_reference(req: ReferenceAnalyzeRequest, current_user: Optional[dict] = Depends(get_optional_user)):
"""Gap 11: A/B reference track - analyze LUFS/dBTP/centroid of a
reference file (local path from media/browse, or uploaded file_id),
return compare metrics + playback url for the A/B toggle."""
import numpy as np
import soundfile as sf
from urllib.parse import quote
from app.core import loudness
path = ""
url = ""
if req.file_id:
path = _resolve_storage_path(req.file_id)
if not path:
raise HTTPException(status_code=404, detail="File not found")
url = "/api/v1/audio/download/%s" % _safe_file_id(req.file_id)
elif req.path:
path = os.path.normpath(req.path)
if not os.path.isfile(path):
raise HTTPException(status_code=404, detail="Khong tim thay file")
ext = os.path.splitext(path)[1].lower().lstrip(".")
if ext not in ALLOWED_AUDIO_EXTENSIONS:
raise HTTPException(status_code=400, detail="Dinh dang khong ho tro: %s" % ext)
url = "/api/v1/media/file?path=" + quote(path)
else:
raise HTTPException(status_code=400, detail="Can path hoac file_id")
try:
info = sf.info(path)
if info.samplerate and info.frames / info.samplerate > 900:
raise HTTPException(status_code=413, detail="File qua dai (>15 phut)")
data, sr = sf.read(path, dtype="float32", always_2d=True)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail="Khong doc duoc audio: %s" % e)
audio = np.asarray(data.T, dtype=np.float64) # (ch, n)
lufs = loudness.integrated_lufs(audio, sr)
tp = loudness.true_peak_db(audio, sr)
centroid = await asyncio.to_thread(_spectral_centroid_hz, audio, sr)
dur = float(info.frames / max(1, info.samplerate or 1))
return {
"filename": os.path.basename(path),
"duration": round(dur, 2),
"sample_rate": int(info.samplerate or 0),
"channels": int(info.channels or 0),
"lufs": round(lufs, 2) if np.isfinite(lufs) else None,
"true_peak_db": round(tp, 2) if np.isfinite(tp) else None,
"centroid_hz": round(centroid, 0) if centroid > 0 else None,
"url": url,
}
@router.delete("/my-files/{file_id}")
async def delete_user_file(file_id: str, current_user: dict = Depends(get_current_user)):
user_id = current_user["user_id"]
+72
View File
@@ -1,6 +1,7 @@
import uuid
import time
import threading
import socket
from fastapi import APIRouter, HTTPException, Header, Depends, Request, Response
from fastapi.responses import JSONResponse
from pydantic import BaseModel, EmailStr
@@ -52,6 +53,57 @@ def _record_login_success(ip: str):
with _LOGIN_LOCK:
_LOGIN_FAILURES.pop(ip, None)
def _is_loopback(host: Optional[str]) -> bool:
"""Standalone: request từ chính máy (127.0.0.1/::1/localhost) → bỏ qua
đăng nhập. Client LAN (IP khác) → vẫn cần mật khẩu."""
if not host:
return False
host = host.lower()
return host in ("127.0.0.1", "::1", "localhost") or host.startswith("127.")
_LOCAL_IPS_CACHE = {"at": 0.0, "ips": set()}
def _own_interface_ips(max_age=60.0) -> set:
"""Các IP interface cua CHINH may (standalone): truy cap qua IP LAN
cua may (vd http://192.168.1.12:8000) van la may nay -> auto-login.
Client LAN may khac co source IP khac -> khong match -> van can mat khau."""
now = time.time()
c = _LOCAL_IPS_CACHE
if now - c["at"] > max_age:
ips = set()
try:
ips = set(socket.gethostbyname_ex(socket.gethostname())[2])
except Exception:
pass
c["ips"] = {i for i in ips if not _is_loopback(i)}
c["at"] = now
return c["ips"]
def _is_local_client(host: Optional[str]) -> bool:
"""May chay engine (loopback) hoac IP interface cua chinh may -> local.
Dung chung cho get_current_user + /auth/local."""
if _is_loopback(host):
return True
if not host:
return False
return host in _own_interface_ips()
def _loopback_admin_user() -> dict:
"""Identity admin cho loopback — real user_id từ DB; không bắt buộc đổi
mật khẩu (standalone tự vào thẳng DAW)."""
try:
conn = get_db_connection()
try:
cur = conn.execute("SELECT id, username, role FROM users WHERE username = ?", ("admin",))
row = cur.fetchone()
finally:
conn.close()
if row:
return {"user_id": row["id"], "username": row["username"], "role": row["role"], "must_change_password": False}
except Exception:
pass
return {"user_id": "local", "username": "admin", "role": "admin", "must_change_password": False}
def _set_auth_cookie(response: Response, token: str):
response.set_cookie(
COOKIE_NAME, token,
@@ -68,6 +120,10 @@ def get_current_user(request: Request, authorization: Optional[str] = Header(Non
elif request.cookies.get(COOKIE_NAME):
token = request.cookies.get(COOKIE_NAME)
if not token:
# Standalone (loopback): không cần mật khẩu — auto admin. LAN vẫn 401.
host = request.client.host if request.client else None
if _is_local_client(host):
return _loopback_admin_user()
raise HTTPException(status_code=401, detail="Thiếu Token xác thực hoặc Token không hợp lệ")
payload = decode_token(token)
if not payload:
@@ -135,6 +191,22 @@ async def login(req: LoginRequest, request: Request):
_set_auth_cookie(resp, token)
return resp
@router.post("/local")
async def local_login(request: Request, response: Response):
"""Auto-login standalone: chỉ cho phép từ loopback (máy chạy engine).
Client LAN nhận 403 → buộc đăng nhập mật khẩu qua /login."""
host = request.client.host if request.client else None
if not _is_local_client(host):
raise HTTPException(status_code=403, detail="Truy cập từ xa — cần đăng nhập")
u = _loopback_admin_user()
token = create_token(u["user_id"], u["username"], u["role"], False)
resp = JSONResponse({
"access_token": token,
"user": {"id": u["user_id"], "username": u["username"], "role": u["role"], "must_change_password": False},
})
_set_auth_cookie(resp, token)
return resp
def _validate_password_strength(password: str):
"""Minimal strength policy: >= 8 chars and not trivially common."""
if len(password) < 8:
+135
View File
@@ -0,0 +1,135 @@
import os, sys, json, subprocess, time as _time
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
from pydantic import BaseModel
from typing import Optional
router = APIRouter()
# ── Native "Save As" dialog (fix bug: export MIDI/WAV không hỏi nơi lưu) ──
# WebView2 standalone chặn anchor-download → không thấy save dialog. Giải pháp:
# frontend gọi /save-dialog → engine mở NATIVE SaveFileDialog (Windows Explorer)
# → trả đường dẫn user chọn → frontend gửi bytes qua /write → ghi file thật.
SAVE_DIALOG_TIMEOUT = 120 # fallback native (PowerShell) — user có thể để dialog mở lâu
# Bridge timeout phải LỚN HƠN Rust wait (300s trong lib.rs) — nếu không, user
# chọn path sau 300s thì engine đã bỏ cuộc → save_file.response bị bỏ lại, file
# không được ghi dù user đã chọn nơi lưu (bug "hỏi nơi lưu nhưng không có file").
SAVE_BRIDGE_TIMEOUT = 360
def _save_file_via_tauri_bridge(default_name: str) -> Optional[str]:
"""Tauri shell (Rust watcher trong lib.rs) mở NATIVE SaveFileDialog qua
file IPC: engine ghi save_file.request (chứa tên mặc định) → Rust mở dialog
→ ghi save_file.response. Trả None nếu bridge không tồn tại (standalone) →
caller fallback native. Trả "" nếu bridge ĐÃ dùng nhưng user hủy / hết hạn →
KHÔNG fallback (tránh 2 dialog cùng lúc + tránh ghi đè path user đã chọn)."""
root = os.environ.get("APPDATA") or os.path.expanduser("~")
ipc = os.path.join(root, "SonicForgeDAW", "ipc")
if not os.path.isdir(ipc):
return None
# Bridge chi dung khi engine do Tauri spawn (co SF_PARENT_PID). Marker co
# the con sot tu lan Tauri chay truoc → chay standalone se cho response
# khong bao gio den (treo 120s). Check nay loai bo truong hop do.
if not os.environ.get("SF_PARENT_PID"):
return None
if not os.path.exists(os.path.join(ipc, "tauri_bridge_ready")):
return None
req = os.path.join(ipc, "save_file.request")
resp = os.path.join(ipc, "save_file.response")
try:
for f in (req, resp):
if os.path.exists(f):
os.remove(f)
with open(req, "w", encoding="utf-8") as fh:
fh.write(default_name or "untitled")
deadline = _time.time() + SAVE_BRIDGE_TIMEOUT
while _time.time() < deadline:
if os.path.exists(resp):
try:
with open(resp, "r", encoding="utf-8") as fh:
val = fh.read().strip()
finally:
os.remove(resp)
return val # "" khi user hủy — KHÔNG fallback PowerShell (tránh 2 dialog)
_time.sleep(0.1)
except Exception:
return ""
return "" # bridge đã dùng nhưng hết hạn — KHÔNG fallback (dialog Rust vẫn có thể đang mở)
def _save_file_native_engine(default_name: str, filter_name: str, filter_ext: str) -> Optional[str]:
"""Fallback khi không có Tauri bridge: PowerShell SaveFileDialog (Windows),
osascript (macOS), zenity/kdialog (Linux)."""
if os.name == "nt":
# FileName mặc định + Filter — single-quoted trong PS, filename không
# chứa nháy đơn (SonicForge tự sinh tên).
ps = (
"Add-Type -AssemblyName System.Windows.Forms; "
f"$f = New-Object System.Windows.Forms.SaveFileDialog; "
f"$f.FileName = '{default_name}'; "
f"$f.Filter = '{filter_name} (*.{filter_ext})|*.{filter_ext}|Tất cả các tệp (*.*)|*.*'; "
"$f.Title = 'Chọn nơi lưu tệp xuất'; "
"if ($f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { Write-Output $f.FileName }"
)
try:
r = subprocess.run(
["powershell", "-NoProfile", "-STA", "-Command", ps],
capture_output=True, text=True, timeout=SAVE_DIALOG_TIMEOUT,
creationflags=subprocess.CREATE_NO_WINDOW,
)
return r.stdout.strip() or None
except Exception:
return None
if sys.platform == "darwin":
try:
r = subprocess.run(
["osascript", "-e",
'set f to choose file name with prompt "Chọn nơi lưu tệp xuất" default name "' + (default_name or "untitled") + '"'],
capture_output=True, text=True, timeout=SAVE_DIALOG_TIMEOUT,
)
return r.stdout.strip() or None
except Exception:
return None
for cmd in (["zenity", "--file-selection", "--save", "--filename=" + (default_name or "untitled")],
["kdialog", "--getsavefilename", os.path.join(os.path.expanduser("~"), default_name or "untitled")]):
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=SAVE_DIALOG_TIMEOUT)
if r.returncode == 0:
p = r.stdout.strip()
if p:
return p
except Exception:
continue
return None
class SaveDialogRequest(BaseModel):
default_name: str = "untitled"
filter_name: str = "File"
filter_ext: str = "bin"
@router.post("/save-dialog")
def save_file_dialog(req: SaveDialogRequest):
"""Mở NATIVE Save As dialog. Không cần auth (desktop local, chỉ mở dialog).
Trả {"path": "<đường dẫn user chọn>"} hoặc {"path": None} (hủy/không có
dialog). def (sync) → FastAPI chạy threadpool — không block loop."""
path = _save_file_via_tauri_bridge(req.default_name)
if path is None: # không có bridge (standalone thuần) → fallback native
path = _save_file_native_engine(req.default_name, req.filter_name, req.filter_ext)
return {"path": path or None}
@router.post("/write")
async def write_export_file(path: str = Form(...), file: UploadFile = File(...)):
"""Ghi bytes đã export vào đường dẫn tuyệt đối user chọn qua save dialog.
Trả {"path": ..., "bytes": N} — frontend toast kèm đường dẫn thật."""
try:
data = await file.read()
dirname = os.path.dirname(path)
if dirname:
os.makedirs(dirname, exist_ok=True)
with open(path, "wb") as f:
f.write(data)
return {"path": path, "bytes": len(data)}
except Exception as e:
raise HTTPException(status_code=400, detail=f"Không thể ghi file: {str(e)}")
+891 -485
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,8 +1,8 @@
# SonicForge Preset Library API — thư viện preset VST3 (.vstpreset) nằm trong
# storage/presets (mount qua volume trong docker; thư mục storage trên Windows).
#
# Vai trò: cầu nối Carla → pedalboard. User chỉnh preset trong Carla (native
# GUI) → xuất .vstpreset → upload vào thư viện → gán vào track (preset_id trong
# Vai trò: cầu nối preset → pedalboard. User chỉnh preset trong native GUI
# của VSTi → xuất .vstpreset → upload vào thư viện → gán vào track (preset_id trong
# synth_engine) → render_engine tải qua load_preset → âm render = âm đã chỉnh.
import os
import uuid
+3 -1
View File
@@ -546,6 +546,7 @@ async def cleanup_all_backups(req: CleanupBackupsRequest, current_user: dict = D
class RenderProjectRequest(BaseModel):
sample_rate: Optional[int] = 44100
metadata: Optional[dict] = None # Gap 13: BWF/INFO metadata (title/artist/isrc/...)
@router.post("/cloud/{project_id}/render")
async def render_project_endpoint(project_id: str, req: RenderProjectRequest, current_user: dict = Depends(get_current_user)):
@@ -568,7 +569,8 @@ async def render_project_endpoint(project_id: str, req: RenderProjectRequest, cu
project_id=project_id,
project_name=row["name"],
project_json_str=row["data_json"],
sample_rate=req.sample_rate or 44100
sample_rate=req.sample_rate or 44100,
metadata=req.metadata or None
)
return {
+2 -34
View File
@@ -1,10 +1,7 @@
# SonicForge System API — capabilities: frontend gọi 1 lần lúc boot để biết
# môi trường (desktop Windows / docker headless) và bật/tắt tính năng tương ứng.
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
from typing import Optional
from app.core.runtime import capabilities, save_carla_path
from app.api.v1.auth import get_current_user, enforce_password_changed
from fastapi import APIRouter
from app.core.runtime import capabilities
router = APIRouter()
@@ -15,37 +12,8 @@ async def get_capabilities():
- runtime: "desktop" (server + client cùng 1 máy Windows/macOS) |
"headless" (docker server + browser UI)
- features.carla_local: có Carla trên máy này → hiện nút "Mở trong Carla"
- features.preset_upload: luôn True (upload .vstpreset qua web UI)
- features.preview_mode: "quick_render" (pedalboard render clip ngắn —
âm thật giống export) | "wasm" (Preview Synth trong browser)
"""
return capabilities()
class CarlaPathRequest(BaseModel):
"""Định vị Carla (bản portable zip không cài đặt/PATH). Chấp nhận đường
dẫn tới carla.exe HOẶC thư mục chứa carla.exe — resolve và lưu config."""
carla_path: str
carla_dir: Optional[str] = None # tương thích ngược: tên cũ của carla_path
@router.post("/carla-path")
async def set_carla_path(req: CarlaPathRequest, current_user: dict = Depends(get_current_user)):
"""Lưu vị trí carla.exe do user chọn (Plugin Manager → Định vị Carla...).
Cần thiết vì bản Carla Windows là bộ file zip portable — không có installer
cũng không dùng biến môi trường PATH, nên heuristic không tìm thấy."""
enforce_password_changed(current_user)
target = (req.carla_path or req.carla_dir or "").strip()
if not target:
raise HTTPException(status_code=400, detail="Thiếu đường dẫn Carla")
exe = save_carla_path(target)
if not exe:
raise HTTPException(
status_code=400,
detail="Không tìm thấy carla.exe trong đường dẫn đã chọn. Hãy chọn "
"thư mục chứa carla.exe (bản portable giải nén) hoặc chính file carla.exe.",
)
return {"success": True, "carla_path": exe, **capabilities()}
+6 -1
View File
@@ -62,9 +62,14 @@ class Settings:
VST_DIR: str = os.getenv("VST_DIR", _default_vst_dir())
SOUNDFONT_DIR: str = os.getenv("SOUNDFONT_DIR", _default_soundfont_dir())
# Thư viện preset VST3 (.vstpreset) — cầu nối Carla → pedalboard.
# Thư viện preset VST3 (.vstpreset) — cầu nối preset → pedalboard.
# Nằm trong storage nên tự động nằm trong volume mount (docker) / thư mục
# storage (Windows desktop).
PRESET_DIR: str = os.path.join(STORAGE_DIR, "presets")
# Render engine offline (preview/export VSTi): 'bridge' (native_bridge
# --render, MIT). Giá trị khác → fallback synth (pedalboard GPL-3.0 đã gỡ
# phase 5 — PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md).
RENDER_ENGINE: str = os.getenv("SF_RENDER_ENGINE", "bridge")
settings = Settings()
+17 -10
View File
@@ -5,6 +5,20 @@ import soundfile as sf
from pydub import AudioSegment
from app.core.dsp_utils import find_nearest_zero_crossing_file, apply_micro_fade
def _write_wav_lossless(output_path, y, sample_rate, bit_depth):
"""Lossless WAV export (lossless-audio-compliance skill):
- 32-bit -> IEEE Float32, bit-exact (no dither, format 3)
- 8/16/24-bit -> single-stage TPDF dither ONLY at final quantization"""
if bit_depth == 32:
sf.write(output_path, y, sample_rate, subtype="FLOAT")
return
subtype = {8: "PCM_S8", 16: "PCM_16", 24: "PCM_24"}.get(bit_depth, "PCM_24")
rng = np.random.default_rng()
y = y + (rng.random(y.shape) - rng.random(y.shape)) # TPDF, [-1,1)
y = np.clip(y, -1.0, 1.0) # clamp after dither — no wrap on quantization
sf.write(output_path, y, sample_rate, subtype=subtype)
def edit_audio_file(config: dict, input_path: str, output_path: str):
"""
Applies editing commands on the audio file based on config:
@@ -192,12 +206,8 @@ def mix_multitrack_session(tracks_meta: list, output_path: str, sample_rate: int
master_mix.export(temp_wav, format="wav")
y, sr_read = sf.read(temp_wav)
# Xác định subtype mã hóa bit-depth
subtype_map = {8: "PCM_S8", 16: "PCM_16", 24: "PCM_24"}
selected_subtype = subtype_map.get(bit_depth, "PCM_16")
# Ghi tệp WAV chất lượng cao
sf.write(output_path, y, sample_rate, subtype=selected_subtype)
# Ghi tệp WAV chất lượng cao (float32 / TPDF-dithered)
_write_wav_lossless(output_path, y, sample_rate, bit_depth)
finally:
# Dọn dẹp tệp tạm (luôn thực hiện)
if os.path.exists(temp_wav):
@@ -254,10 +264,7 @@ def export_audio(input_path: str, output_path: str, format: str = "wav",
sound.export(temp_wav, format="wav")
y, sr_read = sf.read(temp_wav)
subtype_map = {8: "PCM_S8", 16: "PCM_16", 24: "PCM_24"}
selected_subtype = subtype_map.get(bit_depth, "PCM_16")
sf.write(output_path, y, sample_rate, subtype=selected_subtype)
_write_wav_lossless(output_path, y, sample_rate, bit_depth)
finally:
if os.path.exists(temp_wav):
os.remove(temp_wav)
+1 -1
View File
@@ -60,7 +60,7 @@ def load(path, sr=None, mono=True, offset=0.0, duration=None):
from fractions import Fraction
ratio = Fraction(int(sr), int(file_sr))
up, down = ratio.numerator, ratio.denominator
data = _signal.resample_poly(data, up, down).astype(np.float32)
data = _signal.resample_poly(data, up, down, window=("kaiser", 16)).astype(np.float32)
file_sr = sr
return data, file_sr
+463
View File
@@ -0,0 +1,463 @@
# app/core/fx_realtime.py
"""VST FX SHM realtime (cloud-FX loop): engine tạo mapping FxRealtimeIPC,
spawn fx_vst_bridge --realtime-fx, đẩy/kéo block audio qua SHM ring.
Layout mirror chính xác native_bridge/include/FxRealtimeIPC.h (mọi field
u32/float 4-byte, không padding):
header 18 u32 (72B): magic state sampleRate blockSize running heartbeat
inWrite inRead inSlots outWrite outRead outSlots
ctrlWrite ctrlRead ctrlSlots latWrite latRead latSlots
inL[4][256] inR[4][256] outL[8][256] outR[8][256]
ctrl[8] (FxCtrlCmd 24B) — SET_PARAM ring (engine -> bridge)
lat[8] (FxLatReport 8B) — REPORT_LATENCY ring (bridge -> engine)
Luồng dữ liệu: browser → WS → write_input() → [bridge xử lý] → read_output()
→ WS → browser. Block = FXRT_BLOCK=256 stereo float32 = 2048 bytes.
"""
import ctypes
import json
import os
import subprocess
import tempfile
import threading
import time
import uuid
from app.config import settings
FXRT_MAGIC = 0x46585254
FXRT_BLOCK = 256
FXRT_IN_SLOTS = 4
FXRT_OUT_SLOTS = 8
FXRT_CTRL_SLOTS = 8
FXRT_LAT_SLOTS = 8
FXRT_STATE_STARTING = 0
FXRT_STATE_READY = 1
FXRT_STATE_ERROR = 2
class FxRTHeader(ctypes.Structure):
_fields_ = [
("magic", ctypes.c_uint32),
("state", ctypes.c_uint32),
("sample_rate", ctypes.c_uint32),
("block_size", ctypes.c_uint32),
("running", ctypes.c_uint32),
("heartbeat", ctypes.c_uint32),
("in_write", ctypes.c_uint32),
("in_read", ctypes.c_uint32),
("in_slots", ctypes.c_uint32),
("out_write", ctypes.c_uint32),
("out_read", ctypes.c_uint32),
("out_slots", ctypes.c_uint32),
("ctrl_write", ctypes.c_uint32),
("ctrl_read", ctypes.c_uint32),
("ctrl_slots", ctypes.c_uint32),
("lat_write", ctypes.c_uint32),
("lat_read", ctypes.c_uint32),
("lat_slots", ctypes.c_uint32),
]
class FxCtrlCmd(ctypes.Structure):
_fields_ = [
("slot", ctypes.c_uint32),
("key", ctypes.c_char * 16),
("value", ctypes.c_float),
]
class FxLatReport(ctypes.Structure):
_fields_ = [
("slot", ctypes.c_uint32),
("samples", ctypes.c_uint32),
]
HEADER_SIZE = ctypes.sizeof(FxRTHeader) # 72
IN_L_OFF = HEADER_SIZE # 72
IN_R_OFF = IN_L_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
OUT_L_OFF = IN_R_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
OUT_R_OFF = OUT_L_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
CTRL_OFF = OUT_R_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
LAT_OFF = CTRL_OFF + FXRT_CTRL_SLOTS * ctypes.sizeof(FxCtrlCmd)
TOTAL_SIZE = LAT_OFF + FXRT_LAT_SLOTS * ctypes.sizeof(FxLatReport)
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)
self.block_size = int(block_size or FXRT_BLOCK)
if self.block_size != FXRT_BLOCK:
raise ValueError("block_size phải = 256 (khớp C++ FxRealtimeIPC)")
self.name = "SonicForge_FXRT_" + uuid.uuid4().hex[:12]
self.shm = shared_memory.SharedMemory(name=self.name, create=True,
size=TOTAL_SIZE)
self.shm.buf[:TOTAL_SIZE] = b"\x00" * TOTAL_SIZE
self.h = FxRTHeader.from_buffer(self.shm.buf)
self.h.magic = FXRT_MAGIC
self.h.state = FXRT_STATE_STARTING
self.h.sample_rate = self.sample_rate
self.h.block_size = self.block_size
self.h.running = 1
self.h.in_slots = FXRT_IN_SLOTS
self.h.out_slots = FXRT_OUT_SLOTS
self.h.ctrl_slots = FXRT_CTRL_SLOTS
self.h.lat_slots = FXRT_LAT_SLOTS
self._np = np.frombuffer(self.shm.buf, dtype=np.float32)
self._lock = threading.Lock()
# Input batching (fix crackle): gom FXRT_IN_SLOTS frame rồi publish 1 lần
# (in_write += n atomic) → bridge luôn thấy bội số của FXRT_IN_SLOTS →
# take=4 → batch xử lí. Không phụ thuộc client gửi burst hay lẻ.
self._in_pending = []
self._in_pending_t0 = None
self.proc = None
self._job_path = ""
self._chain_seq = 0
self._started_at = time.time()
# Chain hợp nhất (spec PLAN_DAW_A.md Phase 1): giữ đúng thứ tự slot.
self.chain = list(self.fx_chain or [])
# Control protocol (SET_PARAM / REPORT_LATENCY) — Phase 1: giữ ở engine;
# Phase 2.8: C++ consume qua SHM control ring.
self.pending_params = {} # {slot_idx: {key: value}}
self.latencies = {} # {slot_idx: samples}
self.processed = None # G3: processed samples tu bridge (lat slot 0xFFFFFFFE)
# Diagnostics (FXRT crackle hunt): drop/publish counters.
self.stats = {'publish': 0, 'drop_batch': 0, 'drop_blocks': 0,
'stale_flush': 0, 'read_blocks': 0, 'read_empty': 0,
'flush': 0}
self._spawn()
# ── process ─────────────────────────────────────────────────────────────
def _chain_json_for_bridge(self):
"""JSON mảng slot theo spec Phase 1 — giữ NGUYÊN thứ tự + type/params;
RealtimeFxChain::buildChain (C++) đọc {path,bypass,preset_b64} cho vst3
và {type,params} cho builtin (Phase 2). Slot active=false bị loại trước
khi gửi (UI tắt hẳn — không gửi engine)."""
out = []
for s in self.chain:
if not s or not isinstance(s, dict):
continue
if s.get("active") is False:
continue
slot = {
"type": s.get("type", ""),
"bypass": bool(s.get("bypass")),
}
if s.get("id"):
slot["id"] = s["id"]
if s.get("path"):
slot["path"] = s["path"]
if s.get("preset_b64"):
slot["preset_b64"] = s["preset_b64"]
if isinstance(s.get("params"), dict) and s["params"]:
slot["params"] = s["params"]
out.append(slot)
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).
exe = juce_exe or find_juce_fx_bridge_exe() or find_fx_bridge_exe()
if not exe:
raise RuntimeError("Không tìm thấy bridge exe (juce_fx_bridge / fx_vst_bridge)")
job = {
"sample_rate": self.sample_rate,
"block_size": self.block_size,
"seq": self._chain_seq,
"fx_chain": self._chain_json_for_bridge(),
}
fd, self._job_path = tempfile.mkstemp(suffix=".json", prefix="fxrt_")
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(job, f)
if os.path.basename(exe).lower().startswith("juce_fx_bridge"):
cmd = [exe, "--juce-fx", self._job_path, "--shm", self.name,
"--parent", str(os.getpid())]
else:
cmd = [exe, "--realtime-fx", self._job_path, "--shm", self.name,
"--parent", str(os.getpid())]
# FXRT_BRIDGE_LOG=<path>: bat stderr bridge (FxRTPerf) vao file — chan
# doan outFull/drop; mac dinh DEVNULL nhu cu.
# V34 diag: mac dinh ghi FxRTPerf vao fxrt_bridge.log de phan tich
# stall (procMax=bridge/VST cham, outDepth=Python pump cham).
_blog = os.environ.get("FXRT_BRIDGE_LOG")
if not _blog:
_ad = os.environ.get("APPDATA")
_blog = os.path.join(_ad, "SonicForgeDAW", "logs", "fxrt_bridge.log") if _ad else ""
self.proc = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=open(_blog, "ab", buffering=0) if _blog else subprocess.DEVNULL,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
def set_chain(self, fx_chain):
"""Cập nhật chain trên session ĐANG CHẠY (không restart session):
ghi lại job file seq++ — bridge poll seq mỗi 500ms → setChain async
swap (worker load chain mới, audio giữ chain cũ tới khi swap) → load/
đổi VST FX + đổi preset GUI không ngắt âm/crackle."""
self.fx_chain = list(fx_chain or [])
self.chain = self.fx_chain
self._chain_seq += 1
if not self._job_path or not os.path.exists(self._job_path):
return False
try:
with open(self._job_path, "w", encoding="utf-8") as f:
json.dump({
"sample_rate": self.sample_rate,
"block_size": self.block_size,
"seq": self._chain_seq,
"fx_chain": self._chain_json_for_bridge(),
}, f)
except Exception:
return False
return True
def wait_ready(self, timeout=30.0):
deadline = time.time() + timeout
while time.time() < deadline:
if self.proc.poll() is not None:
raise RuntimeError(
f"fx_vst_bridge thoát sớm (rc={self.proc.returncode})")
if self.h.state == FXRT_STATE_READY:
return True
time.sleep(0.02)
raise RuntimeError("fx_vst_bridge không sẵn sàng trong %ss" % timeout)
def alive(self):
return self.proc is not None and self.proc.poll() is None
def close(self):
try:
self.h.running = 0
except Exception:
pass
if self.proc is not None:
try:
self.proc.wait(timeout=3)
except Exception:
try:
self.proc.kill()
except Exception:
pass
self.proc = None
try:
self.shm.close()
self.shm.unlink()
except Exception:
pass
if self._job_path and os.path.exists(self._job_path):
try:
os.remove(self._job_path)
except Exception:
pass
# ── audio I/O ───────────────────────────────────────────────────────────
def write_input(self, interleaved):
"""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
arr = arr.reshape(self.block_size, 2)
with self._lock:
if not self._in_pending:
self._in_pending_t0 = time.time()
self._in_pending.append((arr[:, 0].copy(), arr[:, 1].copy()))
if len(self._in_pending) >= FXRT_IN_SLOTS:
self._flush_input_locked()
def _flush_input_locked(self):
"""Publish pending vào input ring. Ghi data trước, in_write += n sau →
bridge đọc in_write 1 lần → thấy nguyên batch (take=4). V24 NO-DROP:
ring đầy → GIỮ pending (pump flush_input_stale 2ms retry) — không drop
→ hết content gap + hết race pointer (drop cũ cập in_read vượt in_write
→ avail > in_slots → ghi đè slot bridge đang đọc → torn frame). Cap
2048 block (~11s) chống tích lũy vô hạn khi client dồn liên tục."""
if not self._in_pending:
return
h = self.h
if len(self._in_pending) > 2048:
drop = len(self._in_pending) - 2048
del self._in_pending[:drop] # drop block CŨ NHẤT (front)
self.stats['drop_blocks'] += drop
frames = self._in_pending
avail = h.in_slots - (h.in_write - h.in_read)
self.stats['publish'] += len(frames)
if avail <= 0:
# ring đầy → giữ nguyên pending; pump retry khi bridge drain
return
n = min(len(frames), avail)
publish = frames[:n]
if n < len(frames):
del frames[:n] # publish n block CŨ NHẤT, giữ phần thừa làm pending
self._in_pending_t0 = time.time() if frames else None
else:
self._in_pending = []
self._in_pending_t0 = None
for i in range(n):
L, R = publish[i]
slot = (h.in_write + i) & (h.in_slots - 1)
bl = IN_L_OFF // 4 + slot * self.block_size
br = IN_R_OFF // 4 + slot * self.block_size
self._np[bl:bl + self.block_size] = L
self._np[br:br + self.block_size] = R
h.in_write += n # publish atomic — bridge thấy đủ n block cùng lúc
def flush_input_stale(self, timeout=0.004):
"""Flush pending nếu frame đầu chờ > timeout (client gửi lẻ/ngừng giữa
burst) → không kẹt latency. Gọi từ _pump_output mỗi vòng."""
with self._lock:
if self._in_pending and self._in_pending_t0 is not None and time.time() - self._in_pending_t0 > timeout:
self.stats['stale_flush'] += 1
self._flush_input_locked()
def flush_all(self):
"""V24: reset toàn bộ pipeline khi client resume sau stall — xóa
input/output ring + pending → bridge xử lí audio MỚI, không backlog cũ
(lag cộng dồn ~1s sau stall). Gọi khi nhận {cmd:'flush'} qua WS."""
with self._lock:
self._in_pending = []
self._in_pending_t0 = None
self.h.in_read = self.h.in_write
self.h.out_read = self.h.out_write
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:
h = self.h
while h.out_read < h.out_write:
slot = h.out_read & (h.out_slots - 1)
bl = OUT_L_OFF // 4 + slot * self.block_size
br = OUT_R_OFF // 4 + slot * self.block_size
inter = np.empty(self.block_size * 2, dtype=np.float32)
inter[0::2] = self._np[bl:bl + self.block_size]
inter[1::2] = self._np[br:br + self.block_size]
out.append(inter.tobytes())
h.out_read += 1
self.stats['read_blocks'] += len(out)
if not out:
self.stats['read_empty'] += 1
return out
def heartbeat_age(self):
return time.time() - self._started_at # placeholder: engine đọc h.heartbeat
# ── control protocol (SET_PARAM / REPORT_LATENCY) ────────────────────────
# Phase 1: giữ ở engine, client gọi qua WS text frame. Phase 2.8: bridge
# C++ đọc pending_params từ SHM control ring + ghi latency vào ring.
def set_param(self, slot, key, value):
with self._lock:
self.pending_params.setdefault(slot, {})[str(key)] = value
# Phase 2.8: đẩy vào SHM control ring (bridge drain mỗi
# iteration). pending_params giữ làm fallback — fake test session
# (object.__new__) không có shm; drain_params vẫn hoạt động.
if getattr(self, "shm", None) is not None:
try:
self._ctrl_enqueue_locked(slot, str(key), value)
except Exception:
pass
def _ctrl_enqueue_locked(self, slot, key, value):
h = self.h
kb = key.encode("utf-8")[:15]
cmds = (FxCtrlCmd * FXRT_CTRL_SLOTS).from_buffer(self.shm.buf, CTRL_OFF)
while True:
if h.ctrl_write - h.ctrl_read < h.ctrl_slots:
cs = h.ctrl_write & (h.ctrl_slots - 1)
cmds[cs].slot = int(slot) & 0xFFFFFFFF
cmds[cs].key = kb + b"\00" * (16 - len(kb))
cmds[cs].value = float(value)
h.ctrl_write += 1
return
h.ctrl_read += 1 # ring đầy → drop lệnh cũ nhất
def report_latency(self, slot, samples):
with self._lock:
self.latencies[int(slot)] = int(samples)
def drain_params(self):
"""Trả + xóa toàn bộ pending param (bridge consume 1 lần)."""
with self._lock:
out = self.pending_params
self.pending_params = {}
return out
def get_latencies(self):
with self._lock:
self._lat_drain()
return dict(self.latencies)
def get_processed(self):
"""G3: processed samples (bridge da xu li) - playhead engine-derived."""
with self._lock:
self._lat_drain()
return self.processed
def _lat_drain(self):
shm = getattr(self, "shm", None)
if shm is None:
return
try:
h = self.h
lats = (FxLatReport * FXRT_LAT_SLOTS).from_buffer(shm.buf, LAT_OFF)
while h.lat_read < h.lat_write:
ls = h.lat_read & (h.lat_slots - 1)
_slot = int(lats[ls].slot)
_samples = int(lats[ls].samples)
if _slot == 0xFFFFFFFE:
# G3: processed - khong vao latencies dict (sum chain sai).
self.processed = _samples
else:
self.latencies[_slot] = _samples
h.lat_read += 1
except Exception:
pass
# Registry toàn tiến trình (desktop app 1 user).
_SESSIONS = {}
_SESSIONS_LOCK = threading.Lock()
def start_session(fx_chain, sample_rate, block_size=FXRT_BLOCK):
sess = FxRealtimeSession(fx_chain, sample_rate, block_size)
try:
sess.wait_ready(timeout=30)
except Exception:
sess.close()
raise
with _SESSIONS_LOCK:
_SESSIONS[sess.name] = sess
return sess
def get_session(session_id):
with _SESSIONS_LOCK:
return _SESSIONS.get(session_id)
def stop_session(session_id):
with _SESSIONS_LOCK:
sess = _SESSIONS.pop(session_id, None)
if sess:
sess.close()
return sess is not None
def stop_all():
with _SESSIONS_LOCK:
ids = list(_SESSIONS.keys())
for sid in ids:
stop_session(sid)
+106
View File
@@ -0,0 +1,106 @@
"""EBU R128 loudness + true peak (D1).
- integrated_lufs: K-weighting (BS.1770) + 400 ms windows, 100 ms hop,
absolute (-70 LUFS) + relative (-10 LU) gating -> LUFS.
- true_peak_db: 4x oversample (Kaiser-16) -> max |x| in dBTP.
Stereo-only (channel weight 1.0 L/R); mono broadcast to 2 ch = same result.
"""
import numpy as np
from scipy.signal import lfilter, resample_poly
def _k_weighting_coeffs(sr: float):
"""Two biquad stages per ITU-R BS.1770-4 (RBJ cookbook form)."""
# Stage 1: high shelf +4 dB, f0=1681.97 Hz, Q=0.7072
G = 3.999843853973347
f0 = 1681.974450955533
Q = 0.7071752369554196
A = 10 ** (G / 40.0)
w0 = 2 * np.pi * f0 / sr
alpha = np.sin(w0) / (2 * Q)
cosw = np.cos(w0)
b = np.array([
A * ((A + 1) + (A - 1) * cosw + 2 * np.sqrt(A) * alpha),
-2 * A * ((A - 1) + (A + 1) * cosw),
A * ((A + 1) + (A - 1) * cosw - 2 * np.sqrt(A) * alpha),
])
a = np.array([
(A + 1) - (A - 1) * cosw + 2 * np.sqrt(A) * alpha,
2 * ((A - 1) - (A + 1) * cosw),
(A + 1) - (A - 1) * cosw - 2 * np.sqrt(A) * alpha,
])
# Stage 2: high-pass f0=38.14 Hz, Q=0.5003
f1 = 38.13547087602444
Q1 = 0.5003270373238773
w1 = 2 * np.pi * f1 / sr
alpha1 = np.sin(w1) / (2 * Q1)
cosw1 = np.cos(w1)
hp_b = np.array([(1 + cosw1) / 2, -(1 + cosw1), (1 + cosw1) / 2])
hp_a = np.array([1 + alpha1, -2 * cosw1, 1 - alpha1])
return b / a[0], a / a[0], hp_b, hp_a
def _k_weighted(audio: np.ndarray, sr: float) -> np.ndarray:
b1, a1, b2, a2 = _k_weighting_coeffs(sr)
out = np.empty_like(audio)
for c in range(audio.shape[0]):
out[c] = lfilter(b2, a2, lfilter(b1, a1, audio[c]))
return out
def integrated_lufs(audio: np.ndarray, sr: float) -> float:
"""Integrated loudness in LUFS (EBU R128 / BS.1770-4). -inf for silence."""
audio = np.asarray(audio, dtype=np.float64)
if audio.ndim == 1:
audio = audio[np.newaxis, :]
if audio.shape[1] == 0:
return -np.inf
k = _k_weighted(audio, sr)
block = int(round(0.4 * sr))
hop = int(round(0.1 * sr))
n = max(1, (k.shape[1] - block) // hop + 1)
powers = np.empty(n)
for i in range(n):
seg = k[:, i * hop:i * hop + block]
powers[i] = np.mean(seg ** 2)
# Absolute gate: -70 LUFS -> power = 10^((-70 + 0.691)/10)
abs_gate = 10 ** ((-70.0 + 0.691) / 10.0)
above_abs = powers[powers > abs_gate]
if above_abs.size == 0:
return -np.inf
abs_loud = -0.691 + 10.0 * np.log10(np.mean(above_abs))
# Relative gate: abs_loud - 10 LU
rel_gate = 10 ** ((abs_loud - 10.0 + 0.691) / 10.0)
gated = above_abs[above_abs > rel_gate]
if gated.size == 0:
gated = above_abs
return float(-0.691 + 10.0 * np.log10(np.mean(gated)))
def mono_side_ratio(audio: np.ndarray) -> float:
"""Side/Mid RMS ratio (D3 mono-compat). 0 = mono-safe (L==R); >1 = side
louder than mid — mono sum mất nhiều âm thanh."""
audio = np.asarray(audio, dtype=np.float64)
if audio.ndim == 1:
audio = audio[np.newaxis, :]
if audio.shape[0] < 2 or audio.shape[1] == 0:
return 0.0
mid = (audio[0] + audio[1]) * 0.5
side = (audio[0] - audio[1]) * 0.5
m = float(np.sqrt(np.mean(mid ** 2))) + 1e-12
return float(np.sqrt(np.mean(side ** 2))) / m
def true_peak_db(audio: np.ndarray, sr: float, oversample: int = 4) -> float:
"""True peak in dBTP (4x oversample). -inf for digital silence."""
audio = np.asarray(audio, dtype=np.float64)
if audio.ndim == 1:
audio = audio[np.newaxis, :]
if audio.shape[1] == 0:
return -np.inf
up = resample_poly(audio, oversample, 1, axis=1, window=("kaiser", 16))
peak = float(np.max(np.abs(up)))
if peak <= 1e-12:
return -np.inf
return 20.0 * np.log10(peak)
+477
View File
@@ -0,0 +1,477 @@
# app/core/native_render.py
"""Render MIDI notes → WAV qua native_bridge CLI (daw_vst_bridge --render).
Thay thế pedalboard (GPL-3.0) bằng bridge (MIT) cho preview/export offline:
cùng chữ ký (out_path, duration_sec) nên plugins.py/render_engine thay trực
tiếp. KHÔNG import pedalboard. Kích hoạt bằng SF_RENDER_ENGINE=bridge
(settings.RENDER_ENGINE); mặc định bridge từ phase 5.
"""
import base64
import json
import os
import subprocess
import sys
import uuid
import soundfile as sf
from fastapi import HTTPException
from app.config import settings
BRIDGE_EXE = "daw_vst_bridge.exe" if os.name == "nt" else "daw_vst_bridge"
FX_BRIDGE_EXE = "fx_vst_bridge.exe" if os.name == "nt" else "fx_vst_bridge"
JUCE_FX_BRIDGE_EXE = "juce_fx_bridge.exe" if os.name == "nt" else "juce_fx_bridge"
def find_bridge_exe(exe_name=BRIDGE_EXE, env_key="SF_BRIDGE_PATH") -> str:
"""Đường dẫn bridge exe: env <env_key> → bundle (frozen) → src-tauri/binaries → install/ (dev)."""
env = os.getenv(env_key, "").strip()
if env and os.path.isfile(env):
return env
# Tauri externalBin renames sidecars → <name>-x86_64-pc-windows-msvc.exe.
sidecar = exe_name[:-4] + "-x86_64-pc-windows-msvc.exe" if exe_name.lower().endswith(".exe") else exe_name
cands = []
if getattr(sys, "frozen", False):
root = getattr(sys, "_MEIPASS", "")
if root:
cands += [os.path.join(root, "binaries", exe_name),
os.path.join(root, "binaries", sidecar),
os.path.join(root, exe_name),
os.path.join(root, sidecar)]
# PyInstaller onedir: daw_engine.exe nam <app_root>/daw_engine/, bridge
# sidecar nam canh app root (khong trong _MEIPASS) - tim ca exe_dir.
exe_dir = os.path.dirname(os.path.abspath(sys.executable))
cands += [os.path.join(exe_dir, "binaries", exe_name),
os.path.join(exe_dir, "binaries", sidecar),
os.path.join(exe_dir, exe_name),
os.path.join(exe_dir, sidecar),
os.path.join(os.path.dirname(exe_dir), "binaries", exe_name),
os.path.join(os.path.dirname(exe_dir), "binaries", sidecar),
os.path.join(os.path.dirname(exe_dir), exe_name),
os.path.join(os.path.dirname(exe_dir), sidecar)]
# Canonical build output (build_native_bridge.ps1) + legacy install/ (dev).
cands.append(os.path.join(settings.BASE_DIR, "src-tauri", "binaries", sidecar))
cands.append(os.path.join(settings.BASE_DIR, "install", exe_name))
for p in cands:
if p and os.path.isfile(p):
return p
return ""
def find_fx_bridge_exe() -> str:
"""Đường dẫn FX bridge (fx_vst_bridge.exe): SF_FX_BRIDGE_PATH → bundle → install/."""
return find_bridge_exe(FX_BRIDGE_EXE, "SF_FX_BRIDGE_PATH")
def find_juce_fx_bridge_exe() -> str:
"""Đường dẫn FX bridge JUCE (juce_fx_bridge.exe): SF_JUCE_FX_BRIDGE_PATH → bundle → install/."""
return find_bridge_exe(JUCE_FX_BRIDGE_EXE, "SF_JUCE_FX_BRIDGE_PATH")
def normalize_audio_peak(audio, target_db=0.0):
"""Scale audio (numpy array, peak) về target_db nếu vượt; giữ nguyên nếu dưới.
Chống clip do note MIDI chồng (polyphony) — spec midi_polyphony_loudness §II:
render offline nhiều note đồng thời → tổng biên độ vượt 0dBFS → WAV vỡ đỉnh
(hard clip, méo harmonic) trước khi tới client; limiter master không cứu được.
"""
try:
import numpy as np
peak = float(np.max(np.abs(audio))) if audio.size else 0.0
limit = 10 ** (target_db / 20)
if peak > limit and peak > 1e-9:
return audio * (limit / peak)
except Exception:
pass
return audio
def normalize_peak_file(path, target_db=0.0):
"""Post-render WAV: scan peak, vượt target_db → scale toàn file (giữ format)."""
try:
info = sf.info(path)
data, sr = sf.read(path, dtype="float32", always_2d=True)
data = normalize_audio_peak(data, target_db)
sf.write(path, data, sr, subtype=info.subtype)
except Exception:
pass
def classify_instrument(path: str) -> int:
"""InstrumentType enum của bridge: 0=VST3, 1=VST2, 2=SF2/SF3, 3=SFZ."""
low = path.lower()
if low.endswith(".vst3"):
return 0
if low.endswith((".dll", ".so")):
return 1
if low.endswith((".sf2", ".sf3")):
return 2
if low.endswith(".sfz"):
return 3
return 0
def _resolve_instrument(instrument_id: str):
from app.core.vst_engine import get_plugin_manager, resolve_plugin_path
# get_plugin_manager gộp plugin_dirs user (Plugin Manager) — bản cũ dùng
# PluginManager() chỉ scan VST_DIR mặc định → VSTi trong thư mục user
# (vd C:\Program Files\Steinberg\VSTPlugins) 404 → render_engine fallback
# oscillator synth → bounce sai tiếng ("synthwave lạ"). resolve_plugin_path
# là lưới an toàn thứ 2 (so tên file case-insensitive, walk cả dir user).
path = get_plugin_manager()._scan_plugins().get(instrument_id)
if not path:
path = resolve_plugin_path(instrument_id)
if not path:
return None, None
# resolve_plugin_path so theo stem — chặn false positive (vd Synapse Audio
# → Synapse Audio.nksf, preset file không phải plugin).
if not path.lower().endswith((".vst3", ".dll", ".so", ".sf2", ".sf3", ".sfz")):
return None, None
return classify_instrument(path), path
def _preset_b64(preset_id, preset_path, preset_data_b64) -> str:
"""Base64 .vstpreset cho job: bytes nhúng ưu tiên, sau đó id/path file."""
if preset_data_b64:
return preset_data_b64
from app.core.vst_engine import resolve_preset_path
p = resolve_preset_path(preset_id or "") or resolve_preset_path(preset_path or "")
if p:
try:
with open(p, "rb") as fh:
return base64.b64encode(fh.read()).decode("ascii")
except Exception:
return ""
return ""
def bridge_state_preset_b64(channel=None, plugin_path=None) -> str:
"""presetBase64 từ bridge_state.json — bridge C++ đang giữ preset user
chỉnh realtime (capture mỗi ~2s). Export tạo bridge mới không có preset →
plugin load init patch → âm khác realtime (user tưởng "fallback"). Fallback
này nối: dùng preset mà bridge đã capture cho channel đó.
Match: channel == midi_channel (round-robin melodic — cùng quy ước
unifiedMidiRouter.allocateChannel, KHÔNG phải track.id) + path khớp plugin
(basename, case-insensitive). Không tìm thấy → "" (giữ hành vi cũ — không
đoán bừa preset channel khác)."""
try:
st_path = os.path.join(os.environ.get("APPDATA", ""), "SonicForgeDAW",
"state", "bridge_state.json")
if not os.path.isfile(st_path):
return ""
with open(st_path, encoding="utf-8", errors="replace") as fh:
st = json.load(fh)
try:
ch = int(channel) if channel is not None else None
except Exception:
ch = None
want = None
if plugin_path:
want = os.path.basename(os.fspath(plugin_path)).lower()
for ins in st.get("instruments") or []:
if ch is not None and ins.get("channel") != ch:
continue
ip = ins.get("path") or ""
if want and os.path.basename(ip).lower() != want:
continue
pb = ins.get("presetBase64") or ""
if pb:
return pb
except Exception:
pass
return ""
EXPRESSION_CC = 11 # CC11 expression
def _expression_events(notes, bpm, sample_rate, grid=64):
"""Sinh CC11 events sample-accurate từ expression_curve mỗi note.
curve: [{position_ratio 0..1, value 0..127}] — nội suy tuyến tính, grid
cố định (mặc định 64 samples ≈ 1.3ms @48k) → step CC <= 1-2, không zipper.
Event shape {sample, kind:2, a:11, b:value} — khớp RenderJob.cpp Ev.
"""
if not notes or not bpm:
return []
beat_sec = 60.0 / float(bpm)
events = []
for n in notes:
curve = n.get("expression_curve") if isinstance(n, dict) else None
if not curve:
continue
try:
pts = sorted((float(p["position_ratio"]), int(round(float(p["value"])))) for p in curve if p)
except Exception:
continue
if not pts:
continue
start_sample = int(round(float(n.get("start_beat", 0)) * beat_sec * float(sample_rate)))
dur_samples = int(round(float(n.get("duration_beats", 1)) * beat_sec * float(sample_rate)))
if dur_samples <= 0:
continue
def interp(ratio):
ratio = max(0.0, min(1.0, ratio))
if len(pts) == 1:
return pts[0][1]
if ratio <= pts[0][0]:
return pts[0][1]
if ratio >= pts[-1][0]:
return pts[-1][1]
for i in range(1, len(pts)):
if ratio <= pts[i][0]:
a, b = pts[i - 1], pts[i]
t = (ratio - a[0]) / (b[0] - a[0]) if b[0] > a[0] else 0.0
return int(round(a[1] + (b[1] - a[1]) * t))
return pts[-1][1]
pos = 0
while pos <= dur_samples:
ratio = pos / float(dur_samples)
events.append({
"sample": start_sample + int(pos),
"kind": 2,
"a": EXPRESSION_CC,
"b": max(0, min(127, interp(ratio))),
})
pos += max(1, int(grid))
events.sort(key=lambda e: e["sample"])
return events
def _run_notes_job(job: dict, label: str) -> tuple:
"""Chạy job MIDI render qua native_bridge --render → (out_path, duration_sec).
Dùng chung cho VSTi (render_offline) + SoundFont (render_soundfont_offline).
Lỗi → HTTPException (501 thiếu bridge, 500 lỗi render — file rác bị xóa).
"""
exe = find_bridge_exe()
if not exe:
raise HTTPException(
status_code=501,
detail="native_bridge không khả dụng — đặt SF_BRIDGE_PATH hoặc cài daw_vst_bridge",
)
os.makedirs(settings.PROCESSED_DIR, exist_ok=True)
out_path = os.path.join(settings.PROCESSED_DIR, f"preview_{uuid.uuid4().hex[:10]}.wav")
job_path = out_path + ".job.json"
try:
with open(job_path, "w", encoding="utf-8") as fh:
json.dump(job, fh)
r = subprocess.run([exe, "--render", job_path, "--out", out_path],
capture_output=True, text=True, timeout=120,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
log = ((r.stdout or "") + (r.stderr or "")).strip()
if r.returncode != 0 or not os.path.isfile(out_path):
if os.path.exists(out_path):
try:
os.remove(out_path)
except Exception:
pass
if r.returncode == 2:
raise HTTPException(status_code=500,
detail=f"Bridge không load được plugin {label}. {log[-500:]}")
raise HTTPException(status_code=500,
detail=f"Render thất bại (rc={r.returncode}). {log[-500:]}")
normalize_peak_file(out_path)
return out_path, sf.info(out_path).frames / float(job.get("sample_rate", 44100))
except HTTPException:
raise
except subprocess.TimeoutExpired:
if os.path.exists(out_path):
try:
os.remove(out_path)
except Exception:
pass
raise HTTPException(status_code=500, detail="Render quá lâu (timeout 120s)")
except Exception as e:
if os.path.exists(out_path):
try:
os.remove(out_path)
except Exception:
pass
raise HTTPException(status_code=500, detail=f"Render thất bại: {e}")
finally:
if os.path.exists(job_path):
try:
os.remove(job_path)
except Exception:
pass
def render_offline(instrument_id, notes, bpm, sample_rate, preset_id=None,
preset_path=None, preset_data_b64=None,
soundfont_bank=None, soundfont_program=None,
plugin_path=None) -> tuple:
"""Render MIDI notes qua native_bridge --render → (out_path, duration_sec).
Giữ response shape của _render_midi_notes_pedalboard. Lỗi → HTTPException
(404 instrument lạ, 501 thiếu bridge, 500 lỗi render — file rác bị xóa).
"""
itype, path = None, None
# plugin_path (từ schema synth_engine.plugin_path) ưu tiên — không phụ
# thuộc scan; fallback resolve theo tên instrument_id.
if plugin_path and os.path.exists(plugin_path):
path = os.fspath(plugin_path)
itype = classify_instrument(path)
if not path:
itype, path = _resolve_instrument(instrument_id)
if not path:
raise HTTPException(
status_code=404,
detail=f"Không tìm thấy VSTi: {instrument_id} — chưa scan thấy plugin này. "
"Kiểm tra Plugins Manager → Scan.",
)
job = {
"instrument_type": itype,
"plugin_path": path,
"sample_rate": int(sample_rate),
"bpm": float(bpm),
"notes": [{
"pitch": int(n.get("pitch", 60)),
"velocity": float(n.get("velocity", 0.8)), # bridge job: float 0..1 (RenderJob.cpp *127)
"start_beat": float(n.get("start_beat", 0)),
"duration_beats": float(n.get("duration_beats", 1)),
} for n in notes],
}
if itype == 0: # preset chỉ áp dụng cho VST3 (loadSerializedState)
preset = _preset_b64(preset_id, preset_path, preset_data_b64)
if preset:
job["preset"] = preset
# bank/program (giữ parity với midi_events_to_messages cũ: key có mặt
# kể cả giá trị 0 — bridge xử lý theo presence, không theo truthiness)
if soundfont_bank is not None:
job["soundfont_bank"] = int(soundfont_bank) & 0x7F
if soundfont_program is not None:
job["soundfont_program"] = int(soundfont_program) & 0x7F
events = _expression_events(notes, bpm, sample_rate)
if events:
job["events"] = events
return _run_notes_job(job, instrument_id)
def render_soundfont_offline(sf_path, notes, bpm, sample_rate, bank=0, program=0) -> tuple:
"""Render MIDI notes bằng SoundFont (.sf2/.sf3) qua native_bridge --render.
Thay pyfluidsynth khi thiếu FluidSynth lib: bridge nhúng FluidSynth C API
(NativeInstrumentEngine.cpp, InstrumentType::SOUNDFONT_SF2_SF3) — cùng job
shape với render_offline nhưng instrument_type=2 + plugin_path là file SF2.
Lỗi → HTTPException (501 thiếu bridge, 500 lỗi render)."""
job = {
"instrument_type": 2,
"plugin_path": os.fspath(sf_path),
"sample_rate": int(sample_rate),
"bpm": float(bpm),
"soundfont_bank": int(bank) & 0x7F,
"soundfont_program": int(program) & 0x7F,
"notes": [{
"pitch": int(n.get("pitch", 60)),
"velocity": float(n.get("velocity", 0.8)), # bridge job: float 0..1 (RenderJob.cpp *127)
"start_beat": float(n.get("start_beat", 0)),
"duration_beats": float(n.get("duration_beats", 1)),
} for n in notes],
}
events = _expression_events(notes, bpm, sample_rate)
if events:
job["events"] = events
return _run_notes_job(job, os.fspath(sf_path))
def render_fx_chain(input_wav, fx_chain, sample_rate, out_path=None):
"""Render WAV qua native_bridge --render-fx (VST3 FX + builtin gain/normalize).
fx_chain: list dict {type: "vst3"|"builtin", path|id, preset_b64?, bypass?,
params?}. path là plugin id/name → resolve_plugin_path (".vst3"). Trả
(out_path, latencies) — latencies: per-slot samples (bridge FX_LATENCIES,
Phase 3 PDC; [] nếu exe cũ không báo). Lỗi → HTTPException (501 thiếu
bridge, 404 plugin lạ, 500 render).
"""
from app.core.vst_engine import resolve_plugin_path
exe = find_fx_bridge_exe()
if not exe:
raise HTTPException(
status_code=501,
detail="native_bridge không khả dụng — đặt SF_FX_BRIDGE_PATH hoặc cài fx_vst_bridge",
)
slots = []
for fx in fx_chain or []:
if not isinstance(fx, dict) or fx.get("active") is False:
continue
typ = fx.get("type")
if typ == "vst3":
p = (fx.get("path") or "").strip()
resolved = p if os.path.exists(p) else resolve_plugin_path(p, ".vst3")
if not resolved:
raise HTTPException(
status_code=404,
detail=f"Không tìm thấy VST3 FX: {p} — bấm Plugins Manager → Scan trước.",
)
slots.append({"type": "vst3", "path": resolved,
"preset_b64": fx.get("preset_b64", "") or "",
"bypass": bool(fx.get("bypass"))})
elif typ in ("eq", "eqpro", "imager", "maximizer", "compressor",
"limiter", "exciter", "rebalance", "multiband", "deesser"):
# Builtin DSP — chain hợp nhất (spec PLAN_DAW_A.md Phase 1). C++
# RenderFxJob xử lý theo id/params (Phase 2); giữ NGUYÊN thứ tự.
slots.append({"type": "builtin", "id": typ,
"params": dict(fx.get("params") or {}),
"bypass": bool(fx.get("bypass"))})
elif typ == "builtin":
# Legacy: {type: builtin, id: gain} — giữ cho project cũ.
slots.append({"type": "builtin", "id": fx.get("id", "gain"),
"params": dict(fx.get("params") or {}),
"bypass": bool(fx.get("bypass"))})
else:
raise HTTPException(status_code=400, detail=f"fx_chain slot không hợp lệ: {typ}")
if not slots:
raise HTTPException(status_code=400, detail="fx_chain trống — không có FX để render")
job = {"sample_rate": int(sample_rate), "block_size": 512, "fx_chain": slots}
os.makedirs(settings.PROCESSED_DIR, exist_ok=True)
out = out_path or os.path.join(settings.PROCESSED_DIR, f"fx_{uuid.uuid4().hex[:10]}.wav")
job_path = out + ".job.json"
try:
with open(job_path, "w", encoding="utf-8") as fh:
json.dump(job, fh)
r = subprocess.run([exe, "--render-fx", job_path, "--in", input_wav, "--out", out],
capture_output=True, text=True, timeout=120,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
log = ((r.stdout or "") + (r.stderr or "")).strip()
if r.returncode != 0 or not os.path.isfile(out):
if os.path.exists(out):
try:
os.remove(out)
except Exception:
pass
if r.returncode == 2:
raise HTTPException(status_code=500,
detail=f"Bridge không load được plugin. {log[-500:]}")
raise HTTPException(status_code=500,
detail=f"FX render thất bại (rc={r.returncode}). {log[-500:]}")
latencies = []
for _line in (r.stdout or "").splitlines():
if "FX_LATENCIES" in _line:
try:
latencies = [int(x) for x in
_line.split("FX_LATENCIES", 1)[1].split()]
except Exception:
latencies = []
break
return out, latencies
except HTTPException:
raise
except subprocess.TimeoutExpired:
if os.path.exists(out):
try:
os.remove(out)
except Exception:
pass
raise HTTPException(status_code=500, detail="FX render quá lâu (timeout 120s)")
except Exception as e:
if os.path.exists(out):
try:
os.remove(out)
except Exception:
pass
raise HTTPException(status_code=500, detail=f"FX render thất bại: {e}")
finally:
if os.path.exists(job_path):
try:
os.remove(job_path)
except Exception:
pass
+827 -196
View File
File diff suppressed because it is too large Load Diff
+20 -235
View File
@@ -1,17 +1,14 @@
# SonicForge Runtime Profile — phát hiện môi trường chạy (desktop Windows /
# docker headless) và khả năng của máy, để app TỰ CHỌN cách xử lý:
# - Windows desktop (server + client cùng 1 máy): Carla local để preview +
# chỉnh preset → pedalboard render (Hướng A)
# - Windows desktop (server + client cùng 1 máy): native_bridge render VSTi
# - Docker / Linux headless (server + browser UI): soundfont + VSTi mở được
# từ storage mount; preset upload qua browser; không có GUI local.
#
# Ưu tiên: SF_RUNTIME env override > heuristic (platform + display + docker).
# Module CHỈ dùng stdlib + app.config (không kéo pedalboard/numpy) để import
# Module CHỈ dùng stdlib + app.config (không kéo numpy) để import
# nhẹ và hoạt động trong mọi tiến trình (web/worker/celery).
import os
import sys
import json
import shutil
import socket
import functools
@@ -19,10 +16,6 @@ from app.config import settings
SF_RUNTIME_ENV = "SF_RUNTIME" # auto | desktop | headless
SF_DOCKER_ENV = "SF_DOCKER" # 1 = chạy trong container Docker
CARLA_CONFIG_FILE = "carla_path.json" # storage/carla_path.json — user định vị
# Bản Carla portable (zip) có thể giải nén ở BẤT KỲ ĐÂU — PATH/Program Files
# không đủ. User tự chọn thư mục chứa carla.exe qua Plugin Manager → lưu file
# này (ưu tiên cao nhất khi detect), kèm tìm kiếm nông Downloads/Desktop.
def _is_windows() -> bool:
@@ -67,219 +60,6 @@ def _tauri_bridge_ready() -> bool:
return False
def _carla_config_path() -> str:
return os.path.join(settings.STORAGE_DIR, CARLA_CONFIG_FILE)
def get_configured_carla() -> str:
"""Đường dẫn carla.exe do USER tự định vị (lưu trong storage/carla_path.json).
File lưu đường dẫn tới carla.exe (đã resolve) hoặc thư mục chứa — nếu là
thư mục, tìm carla.exe bên trong (độ sâu ≤ 2). Đây là cách bắt buộc có cho
bản Carla portable (zip) giải nén ở vị trí bất kỳ, không cài đặt/PATH."""
try:
with open(_carla_config_path(), "r", encoding="utf-8") as f:
data = json.load(f)
except Exception:
return ""
p = (data.get("carla_path") or "").strip()
if not p:
return ""
if os.path.isfile(p) and os.path.basename(p).lower() in ("carla.exe", "carla-single.exe", "carla"):
return p
if os.path.isdir(p):
try:
for root, dirs, files in os.walk(p):
depth = root[len(p):].count(os.sep)
if depth >= 2:
dirs[:] = []
continue
for f in files:
if f.lower() == "carla.exe":
return os.path.join(root, f)
except Exception:
pass
return ""
def save_carla_path(path: str) -> str:
"""Lưu vị trí Carla do user chọn (Plugin Manager → Định vị Carla...).
Chấp nhận: đường dẫn tới carla.exe, hoặc thư mục chứa carla.exe (portable
zip). Resolve về đường dẫn exe hợp lệ → ghi config → xóa cache detect.
Trả đường dẫn exe, hoặc '' nếu không hợp lệ."""
path = (path or "").strip().strip('"').strip()
exe = ""
if os.path.isfile(path) and os.path.basename(path).lower() in ("carla.exe", "carla-single.exe", "carla"):
exe = path
elif os.path.isdir(path):
try:
for root, dirs, files in os.walk(path):
depth = root[len(path):].count(os.sep)
if depth >= 2:
dirs[:] = []
continue
for f in files:
if f.lower() == "carla.exe":
exe = os.path.join(root, f)
break
if exe:
break
except Exception:
pass
if not exe:
return ""
try:
os.makedirs(settings.STORAGE_DIR, exist_ok=True)
with open(_carla_config_path(), "w", encoding="utf-8") as f:
json.dump({"carla_path": exe, "carla_exe": os.path.basename(exe)}, f, ensure_ascii=False, indent=2)
except Exception:
return ""
# Xóa cache detect/find_carla để capabilities phản ánh ngay
invalidate_carla_cache()
return exe
def _carla_from_registry() -> str:
"""Windows: Carla cài qua installer có thể ghi registry (best-effort)."""
if not _is_windows():
return ""
try:
import winreg
for hive, key in (
(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Carla"),
(winreg.HKEY_CURRENT_USER, r"SOFTWARE\Carla"),
(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Carla"),
):
try:
with winreg.OpenKey(hive, key) as k:
val, _ = winreg.QueryValueEx(k, "InstallPath")
p = os.path.join(str(val), "carla.exe")
if os.path.isfile(p):
return p
except Exception:
continue
except Exception:
pass
return ""
def _find_carla_exe_bounded(base: str, max_depth: int = 3) -> str:
"""Quét nông tìm carla.exe (bản portable giải nén thường nằm Downloads/
Desktop/Documents). Giới hạn độ sâu + bỏ qua thư mục lớn — KHÔNG quét
toàn ổ đĩa."""
if not base or not os.path.isdir(base):
return ""
skip = ("node_modules", ".cache", "AppData", ".git", "venv", ".venv",
"__pycache__", "$RECYCLE.BIN", "Windows", "Program Files",
"Program Files (x86)")
try:
for root, dirs, files in os.walk(base):
depth = root[len(base):].count(os.sep) if root != base else 0
if depth >= max_depth:
dirs[:] = []
continue
for f in files:
if f.lower() == "carla.exe":
return os.path.join(root, f)
dirs[:] = [d for d in dirs if d not in skip and not d.startswith(".")]
except Exception:
pass
return ""
def _carla_from_shallow_search() -> str:
"""Windows: tìm carla.exe trong Downloads/Desktop/Documents/Home."""
if not _is_windows():
return ""
home = os.environ.get("USERPROFILE") or os.path.expanduser("~")
bases = []
for sub in ("Downloads", "Desktop", "Documents"):
bases.append(os.path.join(home, sub))
bases.append(home)
for base in bases:
hit = _find_carla_exe_bounded(base)
if hit:
return hit
return ""
def _carla_candidates() -> list:
"""Các vị trí Carla — theo thứ tự ưu tiên:
1) user định vị (config file — bản portable bắt buộc dùng cách này)
2) PATH (shutil.which)
3) registry (Windows, nếu cài qua installer)
4) thư mục cài đặt chuẩn (Program Files...)
5) quét nông Downloads/Desktop/Documents (Windows) / AppImage (Linux)"""
cands = []
configured = get_configured_carla()
if configured:
cands.append(configured)
try:
which = shutil.which("carla")
if which:
cands.append(which)
except Exception:
pass
reg = _carla_from_registry()
if reg:
cands.append(reg)
try:
if _is_windows():
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
lpf = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
la = os.environ.get("LOCALAPPDATA", "")
for base in (pf, lpf, la):
for rel in ("Carla\\carla.exe", "Carla\\bin\\carla.exe", "Programs\\Carla\\carla.exe"):
p = os.path.join(base, rel) if base else ""
if p and os.path.isfile(p):
cands.append(p)
elif _is_linux():
home = os.path.expanduser("~")
for p in ("/usr/bin/carla", "/usr/local/bin/carla", "/opt/carla/carla"):
if os.path.isfile(p):
cands.append(p)
try:
for f in os.listdir(home):
if f.lower().startswith("carla") and f.lower().endswith(".appimage"):
cands.append(os.path.join(home, f))
except Exception:
pass
elif _is_macos():
for p in ("/Applications/Carla.app/Contents/MacOS/Carla", "/Applications/Carla.app/Contents/MacOS/carla"):
if os.path.isfile(p):
cands.append(p)
except Exception:
pass
shallow = _carla_from_shallow_search()
if shallow:
cands.append(shallow)
# Dedup giữ thứ tự
seen, out = set(), []
for c in cands:
if c not in seen:
seen.add(c)
out.append(c)
return out
@functools.lru_cache(maxsize=1)
def find_carla() -> str:
"""Đường dẫn Carla đầu tiên tìm thấy, hoặc '' nếu chưa có.
Lưu ý license: Carla GPL-2.0+ — app KHÔNG bundle/nhúng, chỉ spawn tiến
trình ngoài (user tự cài/giải nén) → không dính copyleft."""
for c in _carla_candidates():
return c
return ""
def invalidate_carla_cache():
"""Xóa cache detect/find_carla — gọi sau khi user định vị Carla mới."""
find_carla.cache_clear()
detect.cache_clear()
def _detect_platform() -> str:
if _is_windows():
return "windows"
@@ -355,7 +135,6 @@ def detect() -> dict:
"""Detect 1 lần (cache toàn cục) — kết quả bất biến trong 1 tiến trình."""
platform = _detect_platform()
runtime = _detect_runtime()
carla = find_carla() if runtime == "desktop" else ""
docker = _in_docker()
return {
"platform": platform,
@@ -364,8 +143,6 @@ def detect() -> dict:
"environment": "docker" if docker else "standalone", # luồng âm instrument: docker→FluidSynthWASM, standalone→native OS
"has_display": _has_display(),
"tauri_bridge": _tauri_bridge_ready(),
"carla_path": carla,
"carla_local": bool(carla), # chỉ có ý nghĩa khi runtime=desktop
"vst_render": _vst_render_available(),
"default_vst_dirs": default_vst_dirs(),
"default_soundfont_dirs": default_soundfont_dirs(),
@@ -374,13 +151,23 @@ def detect() -> dict:
def _vst_render_available() -> bool:
"""pedalboard có sẵn không (dùng find_spec — KHÔNG import pedalboard để
tránh kéo JUCE lib; giống pattern _module_available trong vst_engine)."""
import importlib.util
try:
return importlib.util.find_spec("pedalboard") is not None
except (ImportError, AttributeError, ValueError):
return False
"""native_bridge có sẵn không: SF_BRIDGE_PATH trỏ file, hoặc exe trong
install/ (không import app.core.native_render để tránh kéo numpy — mirror
logic find_bridge_exe)."""
env = os.getenv("SF_BRIDGE_PATH", "").strip()
if env and os.path.isfile(env):
return True
exe = "daw_vst_bridge.exe" if os.name == "nt" else "daw_vst_bridge"
if os.path.isfile(os.path.join(settings.BASE_DIR, "install", exe)):
return True
# Frozen onedir: bridge sidecar nam canh app root (ngoai _MEIPASS)
if getattr(sys, "frozen", False):
exe_dir = os.path.dirname(os.path.abspath(sys.executable))
for cand in (os.path.join(exe_dir, exe),
os.path.join(os.path.dirname(exe_dir), exe)):
if os.path.isfile(cand):
return True
return False
def _lan_info() -> dict:
@@ -401,12 +188,10 @@ def capabilities() -> dict:
"""Capabilities API — frontend gọi 1 lần lúc boot để bật/tắt tính năng."""
d = detect()
features = {
"carla_local": d["carla_local"],
"carla_path": d["carla_path"],
"preset_upload": True, # cả 2 mode đều upload preset được
"vst_render": d["vst_render"],
"tauri_bridge": d["tauri_bridge"],
# preview VSTi: quick_render (pedalboard render clip ngắn — âm thật,
# preview VSTi: quick_render (native_bridge render clip ngắn — âm thật,
# giống export) là chuẩn cho cả 2 mode; không có realtime trong web UI.
"preview_mode": "quick_render" if d["vst_render"] else "wasm",
}
+10 -5
View File
@@ -78,7 +78,8 @@ class SoundFontConverter:
def _check_ffmpeg_ogg(self):
try:
r = subprocess.run(["ffmpeg", "-encoders"], capture_output=True, text=True, timeout=5)
r = subprocess.run(["ffmpeg", "-encoders"], capture_output=True, text=True, timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
return "libvorbis" in r.stdout
except Exception:
return False
@@ -98,7 +99,8 @@ class SoundFontConverter:
"ffmpeg", "-y", "-i", tmp_wav,
"-c:a", "libvorbis", "-q:a", "3",
"-f", "ogg", tmp_ogg
], capture_output=True, timeout=600, check=True)
], capture_output=True, timeout=600, check=True,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
with open(tmp_ogg, "rb") as fo:
return fo.read()
except Exception as e:
@@ -320,7 +322,8 @@ class SoundFontConverter:
def _find_sf3_converter():
for exe in ["fluidsynth", "mscore"]:
try:
subprocess.run([exe, "--help"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2.0)
subprocess.run([exe, "--help"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2.0,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
return exe
except (FileNotFoundError, subprocess.TimeoutExpired):
continue
@@ -353,7 +356,8 @@ class SoundFontConverter:
if converter == "mscore":
cmd = ["mscore", "-o", sf3_path, sf2_path]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
if result.returncode == 0 and os.path.exists(sf3_path):
logger.info(f"Created SF3 via mscore: {sf3_path} ({os.path.getsize(sf3_path)/1024/1024:.1f}MB)")
return sf3_path
@@ -524,7 +528,8 @@ class SoundFontConverter:
fo.write(ogg)
r = subprocess.run(
["ffmpeg", "-y", "-v", "error", "-i", tmp_ogg, "-f", "s16le", "-ac", "1", tmp_pcm],
capture_output=True, timeout=600)
capture_output=True, timeout=600,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
if r.returncode != 0:
logger.warning(f"OGG decode failed: {r.stderr.decode(errors='replace')[:120]}")
return b""
+235 -66
View File
@@ -1,14 +1,48 @@
# SonicForge Studio VST / VSTi Engine Service
from __future__ import annotations
import os
import json
import numpy as np
import re
import time as _time
import functools
import threading
from ctypes import c_char_p
import subprocess
# Native render (bridge/pyfluidsynth) không thread-safe: tuần tự hóa
RENDER_LOCK = threading.RLock()
def _render_locked(fn):
"""Chạy hàm native (VST3/pyfluidsynth) dưới RENDER_LOCK — không thread-safe."""
@functools.wraps(fn)
def wrapper(*args, **kwargs):
with RENDER_LOCK:
return fn(*args, **kwargs)
return wrapper
def _ensure_fluidsynth_dll_on_path():
"""Frozen (PyInstaller): pyfluidsynth tim dll qua ctypes.util.find_library
(chi quet PATH). Dat libfluidsynth-3.dll ke exe roi them exe_dir vao PATH
truoc khi import fluidsynth - nguoc lai: 'Couldn't find the FluidSynth
library' -> /soundfont-render tra 501."""
import sys
if getattr(sys, 'frozen', False):
exe_dir = os.path.dirname(os.path.abspath(sys.executable))
if exe_dir and exe_dir not in os.environ.get('PATH', '').split(os.pathsep):
os.environ['PATH'] = exe_dir + os.pathsep + os.environ.get('PATH', '')
_ensure_fluidsynth_dll_on_path()
def midi_note_to_freq(note_number: int) -> float:
return 440.0 * (2.0 ** ((note_number - 69) / 12.0))
@_render_locked
def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int = 0,
program: int = 0, sr: int = 44100, bpm: float = 120.0) -> np.ndarray:
"""Native FluidSynth (pyfluidsynth) — render MIDI events bằng soundfont THẬT.
@@ -18,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))
@@ -76,7 +111,9 @@ def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int =
except Exception:
pass
@_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:
@@ -135,14 +172,9 @@ def _module_available(name: str) -> bool:
return False
def check_pedalboard_safe():
return _module_available("pedalboard")
def check_pyfluidsynth_safe():
return _module_available("fluidsynth")
HAS_PEDALBOARD = check_pedalboard_safe()
HAS_PYFLUIDSYNTH = check_pyfluidsynth_safe()
def ensure_pyfluidsynth():
@@ -151,12 +183,6 @@ def ensure_pyfluidsynth():
HAS_PYFLUIDSYNTH = check_pyfluidsynth_safe()
return HAS_PYFLUIDSYNTH
if HAS_PEDALBOARD:
try:
from pedalboard import VST3Plugin, Pedalboard, Gain
except Exception:
HAS_PEDALBOARD = False
if HAS_PYFLUIDSYNTH:
try:
import fluidsynth
@@ -186,22 +212,43 @@ def _load_user_plugin_dirs() -> list:
return []
def _user_plugin_dirs_configured() -> bool:
"""User ĐÃ lưu plugin_dirs trong Plugin Manager (kể cả danh sách rỗng)?
Phân biệt "chưa cấu hình" (fallback thư mục mặc định VST_DIR) với "đã
xóa hết dir" (chỉ scan đúng dir user khai - rỗng thì không scan gì).
Nếu không phân biệt, remove hết dir rồi re-scan vẫn còn VST trong thư
mục mặc định (vd C:\\Program Files\\Common Files\\VST3, iZotope Ozone Pro
load fail 0x7E vẫn hiện trong MASTERING PANEL)."""
try:
from app.config import settings as _st
path = os.path.join(_st.STORAGE_DIR, "plugin_dirs.json")
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return "plugin_dirs" in data
except Exception:
pass
return False
def get_plugin_manager(vst_dir=None, sf_dir=None, upload_sf_dir=None) -> "PluginManager":
"""Singleton: reuse PluginManager when args match, else create new.
Default dirs từ settings (env/.env/docker-compose hoặc user override).
VST scan gộp thêm plugin_dirs user (Plugin Manager) — nút Synth phải liệt
kê được VSTi đã scan và load_vst phải tìm thấy chúng khi render."""
kê được VSTi đã scan."""
from app.config import settings as _st
vst_dir = vst_dir or _st.VST_DIR
sf_dir = sf_dir or _st.SOUNDFONT_DIR
upload_sf_dir = upload_sf_dir or _st.STORAGE_DIR + "/soundfonts"
extra = _load_user_plugin_dirs()
user_override = _user_plugin_dirs_configured()
global _PLUGIN_MANAGER_INSTANCE, _PLUGIN_MANAGER_ARGS
args = (vst_dir, sf_dir, upload_sf_dir, tuple(extra))
args = (vst_dir, sf_dir, upload_sf_dir, tuple(extra), user_override)
if _PLUGIN_MANAGER_INSTANCE is not None and _PLUGIN_MANAGER_ARGS == args:
return _PLUGIN_MANAGER_INSTANCE
_PLUGIN_MANAGER_ARGS = args
_PLUGIN_MANAGER_INSTANCE = PluginManager(vst_dir, sf_dir, upload_sf_dir, extra_vst_dirs=extra)
_PLUGIN_MANAGER_INSTANCE = PluginManager(vst_dir, sf_dir, upload_sf_dir, extra_vst_dirs=extra, user_dirs_override=user_override)
return _PLUGIN_MANAGER_INSTANCE
def load_soundfont_cached(path: str):
@@ -249,21 +296,61 @@ def release_soundfont(path: str):
else:
_FLUID_CACHE[path] = (fl, ref - 1)
def _classify_fx_category(name, path=""):
"""Heuristic phân loại VST FX cho Plugin Picker (spec vst_fx_panel_integration).
C++ scan có subCategoriesString (VD 'Fx|EQ') nhưng không emit ra JSON; dùng
tên plugin để xếp category. Chỉ ảnh hưởng UI grouping, không ảnh hưởng audio."""
s = (" " + name + " " + path).lower()
if any(k in s for k in ("eq", "equalizer", "filter", "graphic", "parametric")) or re.search(r"(^|[^a-z0-9])q([0-9]|[^a-z]|$)", s):
return "equalizer"
if any(k in s for k in ("reverb", "delay", "echo", "chorus", "hall", "space")):
return "reverb_delay"
if any(k in s for k in ("comp", "limiter", "maximizer", "gate", "leveler", "de-ess")):
return "comp_limiter"
if any(k in s for k in ("distortion", "drive", "satur", "overdrive", "fuzz", "amp")):
return "distortion"
return "utility"
class PluginManager:
def __init__(self, vst_dir=None, sf_dir=None, upload_sf_dir=None, extra_vst_dirs=None):
def __init__(self, vst_dir=None, sf_dir=None, upload_sf_dir=None, extra_vst_dirs=None, user_dirs_override=False):
from app.config import settings as _st
self.vst_dir = vst_dir or _st.VST_DIR
self.sf_dir = sf_dir or _st.SOUNDFONT_DIR
self.upload_sf_dir = upload_sf_dir
# Thư mục VST thêm (plugin_dirs user scan trong Plugin Manager) —
# list_available()/load_vst phải thấy VSTi user đã scan (bug: nút
# list_available() phải thấy VSTi user đã scan (bug: nút
# Synth chỉ quét vst_dir env mặc định /opt/daw_engine/vst3).
self.extra_vst_dirs = [d for d in (extra_vst_dirs or []) if d]
# user_dirs_override=True: user ĐÃ lưu plugin_dirs (kể cả rỗng) →
# chỉ scan đúng dir user khai, KHÔNG thêm vst_dir mặc định. Nếu không,
# remove hết dir rồi re-scan vẫn còn VST trong thư mục mặc định
# (vd C:\Program Files\Common Files\VST3 → iZotope 0x7E vẫn hiện).
self.user_dirs_override = bool(user_dirs_override)
self._sf_scan_cache = None # cache for _scan_soundfonts()
self._fx_scan_cache = None # cache for list_fx()
self._fx_scan_all = None # cache for list_scan_all()
self._fx_scan_all_ts = 0.0 # timestamp cache (TTL — tránh kẹt [] vĩnh viễn)
def _vst_scan_dirs(self) -> list:
"""Thư mục quét VST: user đã lưu plugin_dirs → chỉ đúng dir user khai
(rỗng = không scan gì); chưa cấu hình → vst_dir mặc định + dir user."""
if self.user_dirs_override:
return list(self.extra_vst_dirs)
return [self.vst_dir] + list(self.extra_vst_dirs)
FX_SCAN_TTL = 300.0 # giây: cache scan VST tự hết hạn → MASTERING PANEL
# không bao giờ kẹt danh sách cũ/rỗng; refresh=true vẫn ép scan ngay.
def invalidate_fx_scan(self):
"""Clear cache scan VST FX — Plugins Manager bấm Scan phải gọi để
MASTERING PANEL / FX Rack thấy plugin mới ngay lần mở kế tiếp."""
self._fx_scan_all = None
self._fx_scan_all_ts = 0.0
self._fx_scan_cache = None
def _scan_plugins(self) -> dict:
plugins = {}
for scan_dir in [self.vst_dir] + self.extra_vst_dirs:
for scan_dir in self._vst_scan_dirs():
if not scan_dir or not os.path.isdir(scan_dir):
continue
for root, dirs, files in os.walk(scan_dir):
@@ -271,9 +358,27 @@ class PluginManager:
for d in list(dirs):
if d.lower().endswith(".vst3"):
plugins[os.path.splitext(d)[0]] = os.path.join(root, d)
# Bỏ qua file NẰM TRONG folder .vst3 (vd ...\Ozone Pro.vst3\
# iZOzonePro.dll): folder .vst3 đã là 1 plugin — file .dll bên
# trong tạo entry TRÙNG path khác (không khớp block set của
# list_scan_all → FX Ozone/Neutron lọt vào nút Synth).
if any(p.lower().endswith(".vst3")
for p in root.replace("/", os.sep).split(os.sep)):
continue
# iZotope cài kiểu: thư mục chứa CẢ file .vst3 lẫn file .dll
# (iZOzonePro.dll là module/dependency, KHÔNG phải plugin độc
# lập — bridge scan chỉ nhận .vst3 là FX). Thư mục nào đã có
# file .vst3 → bỏ qua .dll (VST2 thật như Kong Audio vẫn giữ
# vì thư mục của nó không có .vst3).
has_vst3_file = any(f.lower().endswith(".vst3") for f in files)
for file in files:
low = file.lower()
if low.endswith(".vst3") or low.endswith(".so") or low.endswith(".dll"):
if low.endswith(".vst3") or low.endswith(".so"):
plugin_path = os.path.join(root, file)
plugin_name = os.path.splitext(file)[0]
if plugin_name not in plugins:
plugins[plugin_name] = plugin_path
elif low.endswith(".dll") and not has_vst3_file:
plugin_path = os.path.join(root, file)
plugin_name = os.path.splitext(file)[0]
if plugin_name not in plugins:
@@ -320,22 +425,6 @@ class PluginManager:
}
return list(sf_map.values())
def load_vst(self, plugin_name: str, preset_data: dict = None):
if not HAS_PEDALBOARD:
return None
plugins = self._scan_plugins()
if plugin_name not in plugins:
return None
path = plugins[plugin_name]
vst = VST3Plugin(path)
if preset_data:
for k, v in preset_data.items():
try:
setattr(vst, k, v)
except Exception:
pass
return vst
def _scan_soundfonts_cached(self):
if self._sf_scan_cache is not None:
return self._sf_scan_cache
@@ -438,22 +527,111 @@ class PluginManager:
return presets[:256]
def list_available(self) -> dict:
plugins = self._scan_plugins()
return {
"vst_instruments": [
{"id": k, "name": k, "type": "VST3", "has_native_support": HAS_PEDALBOARD}
for k in self._scan_plugins().keys()
{"id": k, "name": k, "path": p,
"type": "VST3" if p.lower().endswith(".vst3") else "VST2",
"has_native_support": True}
for k, p in plugins.items()
],
"soundfonts": self._scan_soundfonts()
}
def list_fx(self, refresh=False) -> list:
"""VST3 effect plugins (is_fx) từ native_bridge --scan (SEH-safe).
Bridge tự phân loại FX/instrument theo VST3 subCategory; VST2 (.dll/.so)
không introspect được nên bridge báo instrument — bỏ qua ở đây. Cache
process-wide; refresh=True quét lại (Plugins Manager → Scan).
UNKNOWN entries (bridge không đọc được module — vd Ozone 11 treo khi
scan) cũng được đưa vào đây: không xác định là instrument → mặc định
xem như FX để MASTERING PANEL không mất plugin; nút Synth loại chúng
qua is_unknown (xem plugins.py /available).
"""
all_entries = self.list_scan_all(refresh=refresh)
results = [e for e in all_entries
if e.get("is_fx") or e.get("is_unknown")]
results.sort(key=lambda x: x["name"].lower())
self._fx_scan_cache = results
return results
def list_scan_all(self, refresh=False) -> list:
"""Toàn bộ kết quả `fx_vst_bridge --scan` (FX + instrument + unknown).
Bridge scan giờ có timeout per-module (30s) và emit entry UNKNOWN
(is_fx=false, is_instrument=false) cho module crash/hang (vd Ozone 11
Clarity) — không xếp bừa vào instrument. `/available` dùng entry
unknown để LOẠI plugin đó khỏi nút Synth (không biết loại → không
hiện là instrument), còn list_fx() chỉ lấy is_fx=true.
Cache có TTL (FX_SCAN_TTL): nếu cache cũ hơn TTL → tự scan lại. QUAN
TRỌNG: chỉ cache khi bridge exe tồn tại — nếu chưa tìm thấy bridge
(vd app mới khởi động, sidecar chưa ready) thì KHÔNG lưu kết quả rỗng
vào cache, để lần gọi sau thử lại (trước đây kẹt [] vĩnh viễn →
MASTERING PANEL không hiển thị VST FX đã scan trong Plugin Manager).
"""
now = _time.time()
if (not refresh and self._fx_scan_all is not None
and (now - self._fx_scan_all_ts) < self.FX_SCAN_TTL):
return self._fx_scan_all
from app.core.native_render import find_fx_bridge_exe
exe = find_fx_bridge_exe()
entries = []
if exe:
dirs = [d for d in self._vst_scan_dirs() if d and os.path.isdir(d)]
for d in dirs:
try:
r = subprocess.run([exe, "--scan", d], capture_output=True,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0,
text=True, timeout=300)
except Exception:
continue
if r.returncode != 0:
continue
try:
data = json.loads(r.stdout[r.stdout.find("{"):])
except Exception:
continue
for p in data.get("plugins", []):
path = p.get("path", "")
if not path:
continue
is_fx = bool(p.get("is_fx"))
is_instr = bool(p.get("is_instrument"))
name = p.get("name") or os.path.splitext(os.path.basename(path))[0]
entries.append({
"id": name, "name": name, "path": path,
"type": p.get("type", "VST3"),
"is_fx": is_fx,
"is_instrument": is_instr,
"is_unknown": not is_fx and not is_instr,
"category": _classify_fx_category(name, path),
})
seen, uniq = set(), []
for e in entries:
if e["path"] in seen:
continue
# Plugin file đã bị xóa khỏi đĩa (dll/vst3 không còn) → loại khỏi
# danh sách ngay (không cache entry chết → MASTERING PANEL / nút
# Synth không còn hiện plugin không dùng được).
if not os.path.exists(e["path"]):
continue
seen.add(e["path"])
uniq.append(e)
# Chỉ cache khi bridge tồn tại: kết quả (kể cả rỗng — dir không có
# FX) là đáng tin; khi thiếu bridge thì trả [] nhưng KHÔNG cache.
if exe:
self._fx_scan_all = uniq
self._fx_scan_all_ts = now
return uniq
@staticmethod
def midi_events_to_messages(midi_events: list, bpm: float, sr: int, bank: int = None, program: int = None) -> list:
if not HAS_PEDALBOARD:
return []
beat_duration_sec = 60.0 / max(30.0, bpm)
# pedalboard >= 0.9: MIDI messages là tuple (bytes, timestamp_seconds)
# (MidiMessage class đã bị bỏ). bytes là raw MIDI event:
# 0x9n note_on, 0x8n note_off, 0xB0 control_change, 0xC0 program_change
# MIDI message = (bytes raw MIDI, timestamp_seconds). bytes là raw MIDI
# event: 0x9n note_on, 0x8n note_off, 0xB0 control_change, 0xC0 program_change
messages = []
if bank is not None:
messages.append((bytes([0xB0, 0, int(bank) & 0x7F]), 0.0))
@@ -486,30 +664,20 @@ class DecentSamplerManager:
self.vst_path = vst_path
def create_decent_sampler_instance(self, dspreset_path: str):
if not HAS_PEDALBOARD:
raise RuntimeError("pedalboard not available")
if not os.path.exists(self.vst_path):
raise FileNotFoundError(f"DecentSampler VST3 not found at {self.vst_path}")
if not os.path.exists(dspreset_path):
raise FileNotFoundError(f"Preset file not found at {dspreset_path}")
plugin = VST3Plugin(self.vst_path)
abs_preset = os.path.abspath(dspreset_path)
preset_dir = os.path.dirname(abs_preset)
cwd_before = os.getcwd()
try:
os.chdir(preset_dir)
plugin.load_preset(abs_preset)
finally:
os.chdir(cwd_before)
return plugin
# Phase 4 (PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md §3.4): .dspreset là
# format riêng của DecentSampler (không phải VST3 state) — spike đã fail
# cả trên pedalboard (container: "failed to load data from preset file").
# pedalboard GPL-3.0 bị gỡ → chặn Pianobook + hướng dẫn convert.
raise NotImplementedError(
"Pianobook .dspreset không còn được hỗ trợ (pedalboard GPL-3.0 đã gỡ). "
"Chuyển đổi: mở preset trong DecentSampler → save .vstpreset (VST3 state) "
"rồi upload .vstpreset qua web UI — native_bridge đọc được."
)
# ── Preset bridge (Carla ↔ pedalboard) ─────────────────────────────────────
# Carla (chạy ngoài, user tự cài — GPL-2.0+ nên app KHÔNG bundle/nhúng) dùng
# để mở native GUI VSTi + xuất file preset (.vstpreset từ nút Save của plugin).
# ── Preset bridge (native GUI ↔ pedalboard) ───────────────────────────────
# User chỉnh preset trong native GUI của VSTi (bridge mở cửa sổ plugin) rồi
# xuất file preset (.vstpreset từ nút Save của plugin).
# File preset nằm trong thư viện storage/presets (upload qua web UI hoặc picker
# local trên Windows) → render_engine tải qua apply_preset_to_plugin() khi render.
PRESET_EXTENSIONS = (".vstpreset", ".fxp", ".fxb", ".dspreset")
@@ -569,7 +737,8 @@ def resolve_plugin_path(name_or_path: str, ext: str = "") -> str:
dirs = list(_load_user_plugin_dirs()) or []
except Exception:
pass
dirs += [settings.VST_DIR, settings.SOUNDFONT_DIR]
from app.config import settings as _st
dirs += [_st.VST_DIR, _st.SOUNDFONT_DIR]
for base_dir in dirs:
if not os.path.isdir(base_dir):
continue
@@ -582,7 +751,7 @@ def resolve_plugin_path(name_or_path: str, ext: str = "") -> str:
return ""
def apply_preset_to_plugin(plugin, preset_id=None, preset_path=None, preset_data_b64=None) -> bool:
"""Gán preset lên plugin pedalboard: bytes nhúng (base64 .vstpreset) ưu
"""Gán preset lên plugin VSTi: bytes nhúng (base64 .vstpreset) ưu
tiên, sau đó preset_id (thư viện), sau preset_path (file). Trả True nếu
áp dụng được; KHÔNG raise (render engine chỉ log warning)."""
if plugin is None:
+88
View File
@@ -0,0 +1,88 @@
"""Gap 13: BWF/INFO metadata chunks cho WAV export (EBU Tech 3285 + RIFF INFO).
sf.write chi ghi fmt/data — ham nay chen bext (602 bytes fixed + coding history)
va LIST/INFO (INAM title, IART artist, ISRC, ICMT comment) ngay truoc data chunk
roi ghi lai file. Pure stdlib (struct/datetime).
"""
import datetime
import struct
_BEXT_FIXED = 602
def _pad(text, length):
raw = text.encode("utf-8", errors="replace")
return (raw[:length] + b"\x00" * length)[:length]
def _chunk(fourcc, payload):
pad = b"\x00" if len(payload) % 2 else b""
return fourcc + struct.pack("<I", len(payload)) + payload + pad
def _info_sub(fourcc, text):
if not text:
return b""
return _chunk(fourcc, _pad(text, len(text.encode("utf-8", errors="replace")) + 1))
def _bext(md, now):
fixed = bytearray(_BEXT_FIXED)
fixed[0:256] = _pad(md.get("description", ""), 256)
fixed[256:288] = _pad(md.get("originator", "ScoreFXStudio"), 32)
fixed[288:320] = _pad(md.get("originator_reference", ""), 32)
fixed[320:330] = _pad((md.get("date") or now.strftime("%Y-%m-%d")), 10)
fixed[330:338] = _pad((md.get("time") or now.strftime("%H:%M:%S")), 8)
struct.pack_into("<II", fixed, 338, 0, 0) # time reference low/high
struct.pack_into("<H", fixed, 346, 1) # version = 1
fixed[348:412] = bytes(64) # UMID (zero)
struct.pack_into("<hhhhh", fixed, 412, -32768, -32768, -32768, -32768, -32768) # loudness: 0x8000 = unset
fixed[422:602] = bytes(180) # reserved
coding = md.get("coding_history", "").encode("utf-8", errors="replace")
return bytes(fixed) + coding
def patch_bwf_metadata(path, metadata=None):
"""Chen bext + LIST/INFO vao WAV tai path (rewrite in-place, giu nguyen PCM bits).
metadata keys: title, artist, isrc, comment, description, originator,
originator_reference, date (YYYY-MM-DD), time (HH:MM:SS), coding_history.
Bo qua neu file khong phai RIFF/WAVE hop le hoac metadata rong.
"""
if not metadata:
return
md = {k: (v or "").strip() for k, v in metadata.items() if v}
if not md:
return
with open(path, "rb") as f:
data = f.read()
if len(data) < 12 or data[:4] != b"RIFF" or data[8:12] != b"WAVE":
return
off = 12
pre = b""
data_chunk = None
while off + 8 <= len(data):
cid = data[off:off + 4]
size = struct.unpack("<I", data[off + 4:off + 8])[0]
body = data[off + 8:off + 8 + size]
if cid == b"data":
data_chunk = (cid, size, body)
break
pre += _chunk(cid, body)
off += 8 + size + (size % 2)
if data_chunk is None:
return
now = datetime.datetime.now()
info_payload = b"INFO" + b"".join([
_info_sub(b"INAM", md.get("title", "")),
_info_sub(b"IART", md.get("artist", "")),
_info_sub(b"ISRC", md.get("isrc", "")),
_info_sub(b"ICMT", md.get("comment", "")),
])
extra = _chunk(b"bext", _bext(md, now)) + _chunk(b"LIST", info_payload)
new_data = data[:12] + pre + extra + _chunk(data_chunk[0], data_chunk[2])
new_data = new_data[:4] + struct.pack("<I", len(new_data) - 8) + new_data[8:]
with open(path, "wb") as f:
f.write(new_data)
+4 -2
View File
@@ -17,6 +17,7 @@ from app.api.v1.user_config import router as user_config_router
from app.api.v1.ai_proxy import router as ai_proxy_router
from app.api.v1.ai_presets import router as ai_presets_router
from app.api.v1.plugins import router as plugins_router
from app.api.v1.files import router as files_router
from app.api.v1.media import router as media_router
from app.api.v1.system import router as system_router
from app.api.v1.presets import router as presets_router
@@ -43,7 +44,7 @@ async def lifespan(app: FastAPI):
_SF_SCANNER_STOP.set()
app = FastAPI(title="SonicForge API Engine", lifespan=lifespan)
app = FastAPI(title="ScoreFX API Engine", lifespan=lifespan)
app.add_middleware(GZipMiddleware, minimum_size=500)
@@ -109,6 +110,7 @@ app.include_router(user_config_router, prefix="/api/v1/user", tags=["user_config
app.include_router(ai_proxy_router, prefix="/api/v1/ai", tags=["ai"])
app.include_router(ai_presets_router, prefix="/api/v1/ai", tags=["ai"])
app.include_router(plugins_router, prefix="/api/v1/plugins", tags=["plugins"])
app.include_router(files_router, prefix="/api/v1/files", tags=["files"])
app.include_router(media_router, prefix="/api/v1/media", tags=["media"])
app.include_router(system_router, prefix="/api/v1/system", tags=["system"])
app.include_router(presets_router, prefix="/api/v1/presets", tags=["presets"])
@@ -122,7 +124,7 @@ async def health():
async def get_index():
index_path = os.path.join(settings.TEMPLATES_DIR, "index.html")
if not os.path.exists(index_path):
return HTMLResponse(content=f"<h1>SonicForge Studio: index.html not found at {index_path}</h1>", status_code=404)
return HTMLResponse(content=f"<h1>ScoreFX Studio: index.html not found at {index_path}</h1>", status_code=404)
with open(index_path, "r", encoding="utf-8") as file:
resp = HTMLResponse(content=file.read(), status_code=200)
# no-cache: index.html PHẢI luôn mới (các bundle JS dùng ?v= để bust) —
+46 -1
View File
@@ -35,6 +35,39 @@
"tracks": {
"type": "array",
"items": { "$ref": "#/definitions/Track" }
},
"master": {
"type": "object",
"description": "Master bus strip: per-channel L/R faders + LINK + MUTE",
"properties": {
"volume_db": { "type": "number", "default": 0.0, "description": "Legacy single fader (fallback khi khong co volumeL_db)" },
"volumeL_db": { "type": "number", "default": 0.0 },
"volumeR_db": { "type": "number", "default": 0.0 },
"link_lr": { "type": "boolean", "default": false, "description": "LINK: keo L/R cung luc (R theo L)" },
"muted": { "type": "boolean", "default": false, "description": "MUTE master: zero output" },
"mute": { "type": "boolean", "default": false },
"bypass": { "type": "boolean", "default": false },
"fx_chain": {
"type": "array",
"items": { "$ref": "#/definitions/FXPlugin" }
},
"automation": {
"type": "object",
"description": "Gap 7: master automation lanes (volume_db)",
"properties": {
"volume_db": {
"type": "array",
"items": {
"type": "object",
"properties": {
"time": { "type": "number", "description": "giây" },
"db": { "type": "number", "minimum": -30.0, "maximum": 3.0 }
}
}
}
}
}
}
}
},
"required": ["id", "is_root", "tracks"]
@@ -118,7 +151,19 @@
"start_beat": { "type": "number", "description": "Beat offset relative to the start of the source buffer (bar 0)" },
"duration_beats": { "type": "number" },
"velocity": { "type": "number", "minimum": 0.0, "maximum": 1.0, "default": 0.8 },
"pan": { "type": "number", "minimum": -1.0, "maximum": 1.0, "default": 0.0 }
"pan": { "type": "number", "minimum": -1.0, "maximum": 1.0, "default": 0.0 },
"expression_curve": {
"type": "array",
"description": "CC11 expression automation: normalized position 0..1 in note, value 0..127",
"items": {
"type": "object",
"properties": {
"position_ratio": { "type": "number", "minimum": 0.0, "maximum": 1.0 },
"value": { "type": "integer", "minimum": 0, "maximum": 127 }
},
"required": ["position_ratio", "value"]
}
}
},
"required": ["id", "pitch", "start_beat", "duration_beats", "velocity"]
},
Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+6404 -1598
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+16 -17
View File
@@ -35,6 +35,7 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
window.SonicAPI = {
login: (username, password) => apiRequest('/api/v1/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }),
localLogin: () => apiRequest('/api/v1/auth/local', { method: 'POST' }),
register: (username, email, password) => apiRequest('/api/v1/auth/register', { method: 'POST', body: JSON.stringify({ username, email, password }) }),
changePassword: (old_password, new_password) => apiRequest('/api/v1/auth/change-password', { method: 'POST', body: JSON.stringify({ old_password, new_password }) }),
getProfile: () => apiRequest('/api/v1/auth/profile', { method: 'GET' }),
@@ -59,6 +60,8 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
aiScan: (trackId, fileId, minLoopDuration = 2.0, maxLoopDuration = 6.0) => apiRequest('/api/v1/audio/ai-scan', { method: 'POST', body: JSON.stringify({ track_id: trackId, file_id: fileId, min_loop_duration: minLoopDuration, max_loop_duration: maxLoopDuration }) }),
aiCut: (sourceTrackId, fileId, selectionStart, selectionEnd) => apiRequest('/api/v1/audio/ai-cut', { method: 'POST', body: JSON.stringify({ source_track_id: sourceTrackId, file_id: fileId, selection_start: selectionStart, selection_end: selectionEnd }) }),
runPythonTool: (toolType, trackId, fileId, timePos = 0.0, freq = 440.0, duration = 2.0, waveType = "sine") => apiRequest('/api/v1/audio/python-tool', { method: 'POST', body: JSON.stringify({ tool_type: toolType, track_id: trackId, file_id: fileId, time_pos: timePos, freq: freq, duration: duration, wave_type: waveType }) }),
// Gap 11: A/B reference track — analyze LUFS/dBTP/centroid (path hoac file_id)
referenceAnalyze: (payload) => apiRequest('/api/v1/audio/reference-analyze', { method: 'POST', body: JSON.stringify(payload) }),
getAIConfigs: () => apiRequest('/api/v1/user/config/ai', { method: 'GET' }),
saveAIConfigs: (providers) => apiRequest('/api/v1/user/config/ai', { method: 'POST', body: JSON.stringify({ providers }) }),
@@ -73,28 +76,22 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
// Runtime capabilities — frontend gọi lúc boot để biết môi trường
// (desktop Windows / docker headless) và bật/tắt tính năng
getCapabilities: () => apiRequest('/api/v1/system/capabilities', { method: 'GET' }),
// Định vị Carla.exe (bản portable zip không cài đặt/PATH) — lưu config
setCarlaPath: (path) => apiRequest('/api/v1/system/carla-path', { method: 'POST', body: JSON.stringify({ carla_path: path }) }),
// Mở native GUI VSTi trong Carla (chỉ khi runtime=desktop + có Carla local)
openInCarla: (pluginName, pluginPath) => apiRequest('/api/v1/plugins/open-in-carla', { method: 'POST', body: JSON.stringify({ plugin_name: pluginName, plugin_path: pluginPath }) }),
// Unload Carla bridge: tắt mọi note đang ngân + terminate tiến trình Carla
// do app spawn (gọi khi track chuyển từ VSTi sang instrument soundfont)
stopCarla: () => apiRequest('/api/v1/plugins/carla-stop', { method: 'POST', body: JSON.stringify({}) }),
// Kiểm tra Carla còn sống không (app-spawn) + cổng OSC — quyết định
// route MIDI item exclusive qua Carla hay fallback FluidSynth
carlaStatus: () => apiRequest('/api/v1/plugins/carla-status', { method: 'GET' }),
// Gửi MIDI note (track ARM → Carla OSC) để preview VSTi realtime
carlaMidi: (payload) => apiRequest('/api/v1/plugins/carla-midi', { method: 'POST', body: JSON.stringify(payload) }),
// Quick-render preview VSTi (âm thật = âm export, cùng code path)
previewInstrument: (payload) => apiRequest('/api/v1/plugins/preview', { method: 'POST', body: JSON.stringify(payload) }),
// Render MIDI notes → WAV bằng VSTi (pedalboard, âm thật) — trả file_id
// để UI preview / gán clip vào track / download
midiRender: (payload) => apiRequest('/api/v1/plugins/midi-render', { method: 'POST', body: JSON.stringify(payload) }),
listFx: () => apiRequest('/api/v1/plugins/fx', { method: 'GET' }),
fxRender: (payload) => apiRequest('/api/v1/plugins/fx-render', { method: 'POST', body: JSON.stringify(payload) }),
// Realtime VST FX qua SHM bridge (master chain): start session → WS
// /ws/fx-realtime/{id} đẩy/kéo block audio; stop → tắt bridge.
fxRealtimeStart: (payload) => apiRequest('/api/v1/plugins/fx-realtime/start', { method: 'POST', body: JSON.stringify(payload) }),
fxRealtimeStop: (payload) => apiRequest('/api/v1/plugins/fx-realtime/stop', { method: 'POST', body: JSON.stringify(payload) }),
fxRealtimeSetChain: (payload) => apiRequest('/api/v1/plugins/fx-realtime/set-chain', { method: 'POST', body: JSON.stringify(payload) }),
// Mở native GUI của VST FX (mastering chain / FX rack) qua bridge
openFxGui: (payload) => apiRequest('/api/v1/plugins/fx-gui', { method: 'POST', body: JSON.stringify(payload) }),
soundfontRender: (payload) => apiRequest('/api/v1/plugins/soundfont-render', { method: 'POST', body: JSON.stringify(payload) }),
// Phát dãy MIDI notes realtime qua Carla bridge (OSC) — preview khi
// pedalboard không render được plugin (VD VST2)
carlaPlayNotes: (payload) => apiRequest('/api/v1/plugins/carla-play-notes', { method: 'POST', body: JSON.stringify(payload) }),
// Thư viện preset VST3 (.vstpreset) — cầu nối Carla → pedalboard
// Thư viện preset VST3 (.vstpreset) — cầu nối preset → pedalboard
listPresets: () => apiRequest('/api/v1/presets', { method: 'GET' }),
uploadPreset: (file, pluginHint) => {
const fd = new FormData();
@@ -118,6 +115,8 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
bridgeStatus: () => apiRequest('/api/v1/plugins/bridge/status', { method: 'GET' }),
bridgeLoad: (payload) => apiRequest('/api/v1/plugins/bridge/load', { method: 'POST', body: JSON.stringify(payload) }),
bridgeLog: (lines = 100) => apiRequest(`/api/v1/plugins/bridge/log?lines=${lines}`, { method: 'GET' }),
// E6: mở native GUI VSTi qua engine (browser LAN không Tauri) — GUI hiện trên máy chạy server
bridgeOpenGui: (payload) => apiRequest('/api/v1/plugins/bridge/gui', { method: 'POST', body: JSON.stringify(payload) }),
getAIPresets: () => apiRequest('/api/v1/ai/presets', { method: 'GET' }),
saveAIPreset: (preset) => apiRequest('/api/v1/ai/presets', { method: 'POST', body: JSON.stringify(preset) }),
deleteAIPreset: (presetId) => apiRequest(`/api/v1/ai/presets/${presetId}`, { method: 'DELETE' }),
@@ -127,7 +126,7 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
deleteBackup: (backupId) => apiRequest(`/api/v1/projects/cloud/backups/${backupId}`, { method: 'DELETE' }),
cleanupBackups: (keep) => apiRequest('/api/v1/projects/cloud/backups/cleanup', { method: 'POST', body: JSON.stringify({ keep }) }),
renderProject: (projectJson, outputFilename) => apiRequest('/api/v1/plugins/render', { method: 'POST', body: JSON.stringify({ project_json: projectJson, output_filename: outputFilename }) }),
renderProject: (projectJson, outputFilename, options) => apiRequest('/api/v1/plugins/render', { method: 'POST', body: JSON.stringify(Object.assign({ project_json: projectJson, output_filename: outputFilename }, options || {})) }),
// Ctrl-S desktop: lưu project ra THƯ MỤC CỦA HỆ ĐIỀU HÀNH
// (Documents/SonicForgeDAW/Projects — Windows; ~/SonicForgeDAW/Projects — Linux)
saveProjectToDisk: (name, dataJson) => apiRequest('/api/v1/projects/save-to-disk', { method: 'POST', body: JSON.stringify({ name, data_json: dataJson }) }),
@@ -25,7 +25,6 @@
// 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 (this._connected && this._dest === dest && this._trackId === (trackId || null)) return true;
this.disconnect();
if (!bridgeNode.isReady()) bridgeNode.init();
bridgeNode.connect(dest);
this._connected = true;
+95 -15
View File
@@ -1,17 +1,22 @@
// app/static/js/services/bridgeAudioNode.js
// Audio sink: consumes 'bridge-audio' PCM frames (native bridge) and plays them
// through a ScriptProcessorNode into the WebAudio graph (track FX / master bus).
// into the WebAudio graph. Primary path: AudioWorklet (audio thread, no main
// thread jank). Fallback: ScriptProcessorNode when worklet unavailable.
// VSTi crackle fix (FIX_VSTI_AUDIO_CRACKLE.md P1/P2): worklet ring absorbs pump
// jitter, underrun fades to 0 (no click), overrun drops oldest + crossfade.
(function () {
// Bridge renders one 256-sample block per ~5.8ms; ScriptProcessor fires once
// per 4096 samples (~16 blocks @44.1k). The ring MUST cover one full callback
// period or we underrun (silence gaps) — 24 blocks ~= 139ms headroom.
var RING_DEPTH = 24;
var RING_DEPTH = 24; // fallback ring (blocks of 256 samples)
var WORKLET_MAX = 64; // blocks buffered while worklet loads (~371ms @48k)
var SP_BUFFER = 4096;
var _chunks = []; // [{l: Float32Array(256), r: Float32Array(256)}]
var _chunks = []; // fallback: [{l: Float32Array(256), r: Float32Array(256)}]
var _preBuffer = []; // pending blocks while worklet module loads
var _spn = null;
var _gainNode = null;
var _workletNode = null;
var _ctx = null;
var _initialized = false;
var _useWorklet = false;
var _workletStats = null; // stats tu worklet (overrun/underrun) — getDiag
var _dest = null; // destination gain node da noi (chong connect trung)
function _getCtx() {
@@ -22,11 +27,51 @@
return _ctx;
}
function _setupWorklet() {
if (!_ctx.audioWorklet || !_ctx.audioWorklet.addModule) {
console.warn('[BridgeAudioNode] AudioWorklet unavailable — ScriptProcessor fallback');
return false;
}
var url = '/static/js/services/bridgeAudioWorklet.js?v=202608311858';
_ctx.audioWorklet.addModule(url).then(function () {
if (!_initialized) return; // disconnect() happened while loading
if (_useWorklet) return; // already set up (init retry)
try {
_workletNode = new AudioWorkletNode(_ctx, 'bridge-audio-sink', {
numberOfInputs: 0,
numberOfOutputs: 1,
outputChannelCount: [2]
});
_workletNode.connect(_gainNode);
_workletNode.port.onmessage = function (ev) {
if (ev.data && ev.data.type === 'stats') _workletStats = ev.data;
};
_useWorklet = true;
// drain pre-buffer into worklet (order preserved)
while (_preBuffer.length) {
var b = _preBuffer.shift();
_workletNode.port.postMessage({ type: 'audio', l: b.l, r: b.r });
}
console.log('[BridgeAudioNode] AudioWorklet active');
try { if (window.__dbgLog) window.__dbgLog('worklet', 'active pre=' + _preBuffer.length); } catch (e) {}
} catch (err) {
console.warn('[BridgeAudioNode] AudioWorkletNode failed — ScriptProcessor fallback', err);
_useWorklet = false;
try { if (window.__dbgLog) window.__dbgLog('worklet', 'NODE_FAIL'); } catch (e) {}
}
}).catch(function (err) {
console.warn('[BridgeAudioNode] addModule failed — ScriptProcessor fallback', err);
try { if (window.__dbgLog) window.__dbgLog('worklet', 'MODULE_FAIL'); } catch (e) {}
});
return true; // pending — worklet may become active later
}
function init(audioCtx) {
if (_initialized) return;
_ctx = audioCtx || _getCtx();
_gainNode = _ctx.createGain();
_gainNode.gain.value = 1.0;
// ScriptProcessor created but NOT connected until we know worklet failed.
_spn = _ctx.createScriptProcessor(SP_BUFFER, 0, 2);
_spn.onaudioprocess = function (e) {
var L = e.outputBuffer.getChannelData(0);
@@ -34,8 +79,6 @@
var outLen = L.length;
L.fill(0); R.fill(0);
if (!_chunks.length) return;
// Drain as many queued blocks as fit in this callback (16 max) — filling
// only one block per callback previously played 1/16 of the audio.
var written = 0;
while (written < outLen && _chunks.length) {
var blk = _chunks[0];
@@ -46,36 +89,62 @@
if (n >= blk.l.length) {
_chunks.shift();
} else {
// Partial block (only possible if block size != 256): keep remainder.
blk.l = blk.l.subarray(n);
blk.r = blk.r.subarray(n);
}
}
};
_spn.connect(_gainNode);
if (!_setupWorklet()) {
// No worklet at all — go straight to ScriptProcessor.
_spn.connect(_gainNode);
_useWorklet = false;
while (_preBuffer.length) _chunks.push(_preBuffer.shift());
while (_chunks.length > RING_DEPTH) _chunks.shift();
}
_initialized = true;
console.log('[BridgeAudioNode] initialized (ring depth ' + RING_DEPTH + ')');
}
function onAudio(l, r) {
if (!_initialized) init();
_chunks.push({ l: new Float32Array(l), r: new Float32Array(r) });
// Drop oldest on overrun — keeps latency bounded (~139ms max).
var blk = { l: new Float32Array(l), r: new Float32Array(r) };
if (_useWorklet && _workletNode) {
_workletNode.port.postMessage({ type: 'audio', l: blk.l, r: blk.r });
return;
}
if (!_useWorklet && _ctx && _ctx.audioWorklet && _ctx.audioWorklet.addModule) {
// Worklet loading — buffer temporarily (bounded), avoid fallback churn.
_preBuffer.push(blk);
while (_preBuffer.length > WORKLET_MAX) _preBuffer.shift();
return;
}
_chunks.push(blk);
while (_chunks.length > RING_DEPTH) _chunks.shift();
}
function flush() { _chunks = []; }
function flush() {
_preBuffer = [];
_chunks = [];
if (_useWorklet && _workletNode) {
try { _workletNode.port.postMessage({ type: 'flush' }); } catch (e) {}
}
}
function getOutputNode() { return _gainNode; }
function disconnect() {
flush();
if (_workletNode) {
try { _workletNode.disconnect(); } catch (e) {}
try { _workletNode.port.close(); } catch (e) {}
}
if (_spn && _gainNode) {
try { _spn.disconnect(); } catch (e) {}
try { _gainNode.disconnect(); } catch (e) {}
}
// Reset state so a later init() can rebuild (AudioRoutingEngine re-connect).
_initialized = false;
_useWorklet = false;
_workletNode = null;
_spn = null;
_gainNode = null;
_ctx = null;
@@ -95,6 +164,17 @@
_dest = dest;
},
disconnect: disconnect,
isReady: function () { return _initialized; }
isReady: function () { return _initialized; },
getDiag: function () {
return {
initialized: _initialized,
useWorklet: _useWorklet,
hasWorkletNode: !!_workletNode,
preBufferLen: _preBuffer.length,
chunksLen: _chunks.length,
gain: _gainNode ? _gainNode.gain.value : -1,
ws: _workletStats
};
}
};
})();
@@ -0,0 +1,118 @@
// 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 32 block (~170ms @48k) — hap thu jitter pump
// (V45.27: 16->32 de dung crackle VST2 loop/tremolo — render loop C++ bi
// stall khi instrument xu li loop/tremolo nang CPU; 8->16 truoc do da dung
// tremolo crackle, 16 van chua du cho stall >85ms). Flush o play start
// (app.jsx) xoa ring nen do tre khong tang: ring thuong can o preview.
// - Underrun: fade ve 0 thay vi click (hard silence).
// - Resume sau silence: fade vao tu 0.
// - Overrun: drop block cu nhat + crossfade 64 mau tai diem drop (het click).
// - Stats: dem overrun/underrun, gui ve main thread moi ~0.5s — kiem tra
// con crackle nao la do ring can (underrun) hay ring tran (overrun).
class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
constructor() {
super();
this._ring = [];
this._MAX = 32;
this._gain = 1.0;
this._lastOutL = 0.0;
this._lastOutR = 0.0;
this._skipFade = 0; // >0: con lai cua crossfade dang chay sau overrun drop
this._fadeLen = 0; // tong chieu dai crossfade hien tai (cap 1024)
this._skipFromL = 0.0;
this._skipFromR = 0.0;
this._wasUnderrun = false;
this._statAccum = 0;
this._stats = { overruns: 0, underruns: 0 };
this.port.onmessage = (e) => {
const m = e.data;
if (!m) return;
if (m.type === 'audio') {
this._ring.push({ l: m.l, r: m.r });
while (this._ring.length > this._MAX) {
// overrun: bo block cu nhat; lan consume toi se crossfade ranh gioi
// V45.32: crossfade tich luy — moi block drop cong 256 mau fade, cap
// 1024 (~21ms) thay vi co dinh 64 mau (1.3ms qua ngan -> blip/wow khi
// drop loat block o chord nhieu voice = crackle VST2 tremolo/loop)
this._ring.shift();
this._skipFade = Math.min(this._skipFade + 256, 1024);
this._fadeLen = this._skipFade;
this._stats.overruns++;
}
} else if (m.type === 'flush') {
this._ring.length = 0;
this._gain = 1.0;
this._skipFade = 0;
this._fadeLen = 0;
}
};
}
process(inputs, outputs) {
const out = outputs[0];
if (!out || out.length < 2) return true;
const L = out[0];
const R = out[1];
const n = L.length;
let w = 0;
while (w < n) {
if (this._ring.length) {
if (this._wasUnderrun) this._wasUnderrun = false;
const blk = this._ring[0];
const avail = blk.l.length;
const take = Math.min(avail, n - w);
if (this._skipFade > 0) {
const done = this._fadeLen - this._skipFade;
for (let i = 0; i < take; i++) {
const t = (done + i) / this._fadeLen;
L[w + i] = (this._lastOutL * (1 - t) + blk.l[i] * t) * this._gain;
R[w + i] = (this._lastOutR * (1 - t) + blk.r[i] * t) * this._gain;
}
this._skipFade = Math.max(0, this._skipFade - take);
} else {
for (let i = 0; i < take; i++) {
L[w + i] = blk.l[i] * this._gain;
R[w + i] = blk.r[i] * this._gain;
}
}
this._lastOutL = L[w + take - 1];
this._lastOutR = R[w + take - 1];
if (this._gain < 1.0) this._gain = Math.min(1.0, this._gain + take / 256.0);
w += take;
if (take >= avail) {
this._ring.shift();
} else {
blk.l = blk.l.subarray(take);
blk.r = blk.r.subarray(take);
}
} else {
// underrun: fade ve 0 (khong click) — dem 1 lan moi lan roi vao trang thai trong
if (!this._wasUnderrun) { this._wasUnderrun = true; this._stats.underruns++; }
for (let i = w; i < n; i++) {
this._gain = Math.max(0, this._gain - 1.0 / 256.0);
L[i] = this._lastOutL * this._gain;
R[i] = this._lastOutR * this._gain;
}
this._lastOutL *= this._gain;
this._lastOutR *= this._gain;
w = n;
}
}
// Stats: gui ve main thread moi ~0.5s (24000 mau @48k) — log overrun/underrun
this._statAccum += n;
if (this._statAccum >= 24000) {
this._statAccum = 0;
try {
this.port.postMessage({
type: 'stats',
overruns: this._stats.overruns,
underruns: this._stats.underruns,
ring: this._ring.length,
max: this._MAX
});
} catch (e) {}
}
return true;
}
}
registerProcessor('bridge-audio-sink', BridgeAudioSinkProcessor);
+171
View File
@@ -0,0 +1,171 @@
// SonicForge Studio - Chord Theory Service (BIG_PHASE_3.md §6.1, virtual_keyboard_and_chords_panel_spec.md §III)
// Thuần hàm: parse chord symbol / roman numeral → pitch array; áp voicing.
// Dùng được cả browser (script tag) lẫn Node (module.exports) cho unit test.
const ChordTheory = (function() {
// Root note name → pitch class (C=0 ... B=11)
const NOTE_TO_PC = {
'C': 0, 'C#': 1, 'Db': 1, 'D': 2, 'D#': 3, 'Eb': 3, 'E': 4, 'F': 5,
'F#': 6, 'Gb': 6, 'G': 7, 'G#': 8, 'Ab': 8, 'A': 9, 'A#': 10, 'Bb': 10, 'B': 11
};
const ROOT_NAMES = Object.keys(NOTE_TO_PC).sort(function(a, b) { return b.length - a.length; }); // 'C#'/'Bb' trước 'C'/'B'
const PC_TO_NAME = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B'];
// Semitone intervals (ascending, relative to root). Octave = 4 mặc định
// (C4=60) — đúng spec §III.5: Pitches(Cmaj7) = [60,64,67,71].
const QUALITY_INTERVALS = {
'': [0, 4, 7],
'maj': [0, 4, 7],
'm': [0, 3, 7],
'min': [0, 3, 7],
'-': [0, 3, 7],
'maj7': [0, 4, 7, 11],
'M7': [0, 4, 7, 11],
'7': [0, 4, 7, 10],
'm7': [0, 3, 7, 10],
'min7': [0, 3, 7, 10],
'maj9': [0, 4, 7, 11, 14],
'M9': [0, 4, 7, 11, 14],
'9': [0, 4, 7, 10, 14],
'm9': [0, 3, 7, 10, 14],
'min9': [0, 3, 7, 10, 14],
'11': [0, 4, 7, 10, 14, 17],
'm11': [0, 3, 7, 10, 14, 17],
'13': [0, 4, 7, 10, 14, 21],
'm13': [0, 3, 7, 10, 14, 21],
'add9': [0, 4, 7, 14],
'sus2': [0, 2, 7],
'sus4': [0, 5, 7],
'sus': [0, 5, 7],
'7sus4': [0, 5, 7, 10],
'dim': [0, 3, 6],
'dim7': [0, 3, 6, 9],
'aug': [0, 4, 8],
'+': [0, 4, 8],
'6': [0, 4, 7, 9],
'm6': [0, 3, 7, 9],
'm7b5': [0, 3, 6, 10],
'5': [0, 7]
};
const QUALITY_KEYS = Object.keys(QUALITY_INTERVALS).sort(function(a, b) { return b.length - a.length; });
// Roman numeral degree → semitone offset trong major scale (spec: I-V-vi-IV
// với key C → C-G-Am-F; vi → A, IV → F, v → G...)
const ROMAN_DEGREE = { I: 0, II: 2, III: 4, IV: 5, V: 7, VI: 9, VII: 11 };
function noteToMidi(rootName, octave) {
const pc = NOTE_TO_PC[rootName] !== undefined ? NOTE_TO_PC[rootName] : 0;
return 12 * ((octave == null ? 4 : octave) + 1) + pc;
}
// 'Cmaj7' → [60,64,67,71]; 'Am9' → [57,60,64,71]; 'G13' → [67,71,74,77,81,88]
function parseChordSymbol(symbol, octave) {
if (!symbol) return [];
const s = String(symbol).trim();
if (!s) return [];
const upper = s.toUpperCase();
const root = ROOT_NAMES.find(function(n) { return upper.indexOf(n) === 0; });
if (!root) return [];
// Giữ nguyên case của suffix: 'm' = minor (Am9 → 'm9'), 'M7' = major 7th
const suffix = s.slice(root.length);
const quality = QUALITY_KEYS.find(function(q) { return suffix === q; }) || '';
const intervals = QUALITY_INTERVALS[quality] || QUALITY_INTERVALS[''];
const rootMidi = noteToMidi(root, octave);
return intervals.map(function(i) { return rootMidi + i; });
}
// Kiểm tra symbol có phải chord name (bắt đầu bằng tên nốt) không
function isChordName(token) {
const upper = String(token || '').trim().toUpperCase();
return ROOT_NAMES.some(function(n) { return upper.indexOf(n) === 0; });
}
// 'ii7' → { degree: 2, minor: true, suffix: '7', accidental: 0 }
// 'bVI' → { degree: 9, minor: false, suffix: '', accidental: -1 }
function parseRomanToken(token) {
const t = String(token || '').trim();
// Alternation dài trước (VII trước VI trước V...); /i nhận cả chữ thường
const m = t.match(/^(b|#)?(VII|VI|IV|IX|III|II|V|I)(.*)$/i);
if (!m) return null;
const accidental = m[1] === 'b' ? -1 : (m[1] === '#' ? 1 : 0);
const numeral = m[2].toUpperCase();
const minor = m[2] !== m[2].toUpperCase(); // 'vi' → minor, 'VI' → major
if (ROMAN_DEGREE[numeral] === undefined) return null;
const suffix = m[3] || '';
let quality = null;
const SUFFIX_MAP = {
'': function(min) { return min ? 'm' : ''; },
'7': function(min) { return min ? 'm7' : '7'; },
'9': function(min) { return min ? 'm9' : '9'; },
'13': function(min) { return min ? 'm13' : '13'; },
'maj7': function() { return 'maj7'; },
'M7': function() { return 'maj7'; },
'maj9': function() { return 'maj9'; },
'add9': function() { return 'add9'; },
'6': function(min) { return min ? 'm6' : '6'; },
'sus4': function() { return 'sus4'; },
'sus2': function() { return 'sus2'; },
'7sus4': function() { return '7sus4'; },
'dim': function() { return 'dim'; },
'dim7': function() { return 'dim7'; },
'm7b5': function() { return 'm7b5'; },
'aug': function() { return 'aug'; }
};
const key = SUFFIX_MAP[suffix] || SUFFIX_MAP[''];
quality = key ? key(minor) : (minor ? 'm' : '');
return { degree: ROMAN_DEGREE[numeral], minor: minor, suffix: suffix, accidental: accidental, quality: quality };
}
// Chuỗi tokens 'I-vi-IV-V' hoặc 'Cmaj7-Am9-Fadd9-G13' → [{ name, pitches }]
// key dùng cho roman numeral; voicing: 'root' | 'first' | 'drop2'.
function progressionToChords(tokens, key, voicing, octave) {
if (!tokens) return [];
const parts = String(tokens).split(/[-,\s]+/).filter(function(x) { return x.length > 0; });
const keyRoot = key || 'C';
return parts.map(function(token) {
if (isChordName(token)) {
const pitches = parseChordSymbol(token, octave);
return { token: token, name: token, pitches: applyVoicing(pitches, voicing) };
}
const parsed = parseRomanToken(token);
if (!parsed) return { token: token, name: token, pitches: [] };
const keyPc = NOTE_TO_PC[keyRoot] !== undefined ? NOTE_TO_PC[keyRoot] : 0;
const rootPc = (((keyPc + parsed.degree + parsed.accidental) % 12) + 12) % 12;
const rootName = PC_TO_NAME[rootPc];
const quality = parsed.quality;
const name = rootName + quality;
const rootMidi = 12 * ((octave == null ? 4 : octave) + 1) + rootPc;
const intervals = QUALITY_INTERVALS[quality] || QUALITY_INTERVALS[''];
const pitches = intervals.map(function(i) { return rootMidi + i; });
return { token: token, name: name, pitches: applyVoicing(pitches, voicing) };
});
}
// Root position: giữ nguyên (sorted). First inversion: root +12.
// Drop-2: note cao thứ nhì -12 (spec §III.5).
function applyVoicing(pitches, voicing) {
if (!pitches || !pitches.length) return [];
const arr = pitches.slice().sort(function(a, b) { return a - b; });
if (voicing === 'first' && arr.length >= 2) {
arr[0] = arr[0] + 12;
arr.sort(function(a, b) { return a - b; });
return arr;
}
if (voicing === 'drop2' && arr.length >= 3) {
arr[arr.length - 2] = arr[arr.length - 2] - 12;
arr.sort(function(a, b) { return a - b; });
return arr;
}
return arr;
}
return {
parseChordSymbol: parseChordSymbol,
parseRomanToken: parseRomanToken,
isChordName: isChordName,
progressionToChords: progressionToChords,
applyVoicing: applyVoicing
};
})();
if (typeof module !== 'undefined' && module.exports) module.exports = ChordTheory;
@@ -0,0 +1,63 @@
// SonicForge Studio - chordTheory.js unit test (BIG_PHASE_3.md §6.4)
// Node assert, không framework. Chạy: node app/static/js/services/chordTheory.test.js
const assert = require('assert');
const ChordTheory = require('./chordTheory.js');
let fails = 0;
function check(name, fn) {
try { fn(); console.log('ok: ' + name); }
catch (e) { fails++; console.error('FAIL: ' + name + ' — ' + e.message); }
}
function deep(arr, expected) { assert.deepStrictEqual(arr, expected); }
// 1. Parse chord symbol → pitch array (octave 4 = C4=60)
check('Cmaj7 = [60,64,67,71]', () => deep(ChordTheory.parseChordSymbol('Cmaj7'), [60, 64, 67, 71]));
check('Am9 = [69,72,76,79,83]', () => deep(ChordTheory.parseChordSymbol('Am9'), [69, 72, 76, 79, 83]));
check('Dm7 = [62,65,69,72]', () => deep(ChordTheory.parseChordSymbol('Dm7'), [62, 65, 69, 72]));
check('G13 = [67,71,74,77,81,88]', () => deep(ChordTheory.parseChordSymbol('G13'), [67, 71, 74, 77, 81, 88]));
check('Fadd9 = [65,69,72,79]', () => deep(ChordTheory.parseChordSymbol('Fadd9'), [65, 69, 72, 79]));
// 2. Roman numeral progression với key C
check('I-vi-IV-V key C → C-Am-F-G', () => {
const prog = ChordTheory.progressionToChords('I-vi-IV-V', 'C', 'root');
deep(prog.map(c => c.name), ['C', 'Am', 'F', 'G']);
deep(prog.map(c => c.pitches), [[60, 64, 67], [69, 72, 76], [65, 69, 72], [67, 71, 74]]);
});
check('I-V-vi-IV key C → C-G-Am-F (preset Pop)', () => {
const prog = ChordTheory.progressionToChords('I-V-vi-IV', 'C', 'root');
deep(prog.map(c => c.name), ['C', 'G', 'Am', 'F']);
});
check('ii7-V7-Imaj7-VI7 key C → Dm7-G7-Cmaj7-A7', () => {
const prog = ChordTheory.progressionToChords('ii7-V7-Imaj7-VI7', 'C', 'root');
deep(prog.map(c => c.name), ['Dm7', 'G7', 'Cmaj7', 'A7']);
deep(prog[0].pitches, [62, 65, 69, 72]); // Dm7
deep(prog[2].pitches, [60, 64, 67, 71]); // Cmaj7
});
check('i7-iv7-v7-i7 key C → Cm7-Fm7-Gm7-Cm7', () => {
const prog = ChordTheory.progressionToChords('i7-iv7-v7-i7', 'C', 'root');
deep(prog.map(c => c.name), ['Cm7', 'Fm7', 'Gm7', 'Cm7']);
});
check('Imaj7-iii7-IVmaj7-V13 key C → Cmaj7-Em7-Fmaj7-G13', () => {
const prog = ChordTheory.progressionToChords('Imaj7-iii7-IVmaj7-V13', 'C', 'root');
deep(prog.map(c => c.name), ['Cmaj7', 'Em7', 'Fmaj7', 'G13']);
});
// 3. Voicing
check('first inversion: root +12', () => deep(ChordTheory.applyVoicing([60, 64, 67, 71], 'first'), [64, 67, 71, 72]));
check('drop2: second-highest -12', () => deep(ChordTheory.applyVoicing([60, 64, 67, 71], 'drop2'), [55, 60, 64, 71]));
check('root position giữ nguyên', () => deep(ChordTheory.applyVoicing([60, 64, 67, 71], 'root'), [60, 64, 67, 71]));
check('jazz drop2: Dm7-G7-Cmaj7-A7 ra đủ pitch', () => {
const prog = ChordTheory.progressionToChords('ii7-V7-Imaj7-VI7', 'C', 'drop2');
deep(prog.map(c => c.name), ['Dm7', 'G7', 'Cmaj7', 'A7']);
assert(prog.every(c => c.pitches.length === 4), 'mỗi chord 4 pitch');
});
// 4. Edge cases
check('chuỗi chord name trực tiếp Cmaj7-Am9-Fadd9-G13', () => {
const prog = ChordTheory.progressionToChords('Cmaj7-Am9-Fadd9-G13', 'C', 'root');
deep(prog.map(c => c.name), ['Cmaj7', 'Am9', 'Fadd9', 'G13']);
deep(prog[0].pitches, [60, 64, 67, 71]);
});
console.log(fails ? 'FAILED: ' + fails : 'ALL PASS');
process.exit(fails ? 1 : 0);
+57 -12
View File
@@ -11,13 +11,19 @@
activeInstrumentType: 'VST3',
_audioCb: null,
_statusCb: null,
_midiOutCb: null,
_tauri: function () { return window.__TAURI__; },
/** D10: UI subscribes to bridge connection changes. cb({connected}) or null to clear. */
onStatusChange: function (cb) { this._statusCb = cb; },
_notifyStatus: function (connected) {
this.isBridgeConnected = !!connected;
if (this._statusCb) this._statusCb({ connected: !!connected });
connected = !!connected;
// Loop guard (V45.7): poller queryStatus moi 400ms tung goi cb du connected
// khong doi -> App reset map + reload toan bo track -> mark loading lai ->
// poller lai thay loading -> LOAD skip vo han. Chi fire khi TRANG THAI DOI.
if (this.isBridgeConnected === connected) return;
this.isBridgeConnected = connected;
if (this._statusCb) this._statusCb({ connected: connected });
},
/** Fix C: re-establish bridge routing once audio frames arrive again
@@ -58,6 +64,12 @@
if (!self.isBridgeConnected) self._reconnectIfNeeded();
if (self._audioCb) self._audioCb(e.payload.l, e.payload.r);
});
// A14: plugin MIDI output (VstEvents -> SHM midiOutQueue -> pump).
// Payload: array of {command, channel, pitch, velocity, data2,
// data3, reserved, sample_offset}. JS routes notes to DAW tracks.
window.__TAURI__.event.listen('bridge-midi-out', function (e) {
if (self._midiOutCb) self._midiOutCb(e.payload);
});
window.__TAURI__.event.listen('bridge-down', function () {
self._notifyStatus(false);
if (window.SonicMidiRouter) window.SonicMidiRouter.setBridgeConnected(false);
@@ -75,6 +87,12 @@
try {
var s = await window.__TAURI__.core.invoke('bridge_status');
this._notifyStatus(!!s.connected);
// V10: C++ loaded channels (bridge_state.json) — nguồn path thật không
// cần HTTP/auth. loadVstToBridge/ensureAndOpenVstGui dùng để mở GUI khi
// JS không resolve được path (token hết hạn / listPlugins thiếu path).
if (s && Array.isArray(s.loaded_channels)) {
window.__bridgeLoadedState = s.loaded_channels;
}
return s;
} catch (e) {
this._notifyStatus(false);
@@ -110,7 +128,7 @@
* velocity 0..1; sampleOffset in samples within the current audio block.
* data2/data3 (A12): CC value / program / PB LSB|MSB.
*/
dispatchMidiEvent: function (cmd, channel, pitch, velocity, sampleOffset, data2, data3) {
dispatchMidiEvent: function (cmd, channel, pitch, velocity, sampleOffset, data2, data3, live) {
if (!this._tauri()) return false;
var safeVelocity = Math.floor(Math.min(1.0, Math.max(0.0, velocity)) * 127);
var byteCmd;
@@ -120,15 +138,23 @@
case 'PITCH_BEND': byteCmd = 0xE; break;
default: byteCmd = cmd === 'NOTE_ON' ? 0x9 : 0x8; break;
}
window.__TAURI__.core.invoke('push_midi_event', {
command: byteCmd,
channel: channel,
pitch: pitch,
velocity: safeVelocity,
data2: data2 === undefined ? 0 : data2,
data3: data3 === undefined ? 0 : data3,
sampleOffset: sampleOffset || 0
}).catch(function (e) { console.warn('[BridgeService] push_midi_event:', e); });
// DIAG: Promise.race 800ms — invoke gửi đi nhưng không settle -> timeout
// (phan biet main-thread block vs Rust hang).
Promise.race([
window.__TAURI__.core.invoke('push_midi_event', {
command: byteCmd,
channel: channel,
pitch: pitch,
velocity: safeVelocity,
data2: data2 === undefined ? 0 : data2,
data3: data3 === undefined ? 0 : data3,
sampleOffset: sampleOffset || 0,
live: !!live
}),
new Promise(function (_, rej) { setTimeout(function () { rej(new Error('timeout-800ms')); }, 800); })
]).catch(function (e) {
console.warn('[BridgeService] push_midi_event:', e);
});
return true;
},
@@ -162,12 +188,31 @@
return true;
},
/**
* 6. REALTIME MASTER FX CHAIN (T2.4)
* chainJson: JSON array [{path, bypass}, ...] applied to the live master
* mix via the bridge control queue (type 6). Async — never blocks UI.
*/
setFxChain: function (chainJson) {
if (!this._tauri()) return false;
window.__TAURI__.core.invoke('set_fx_chain', { chainJson: chainJson })
.catch(function (e) { console.warn('[BridgeService] set_fx_chain:', e); });
return true;
},
/**
* 5. AUDIO SINK: register consumer of PCM frames from the bridge.
* cb(l: Float32Array, r: Float32Array)
*/
onAudio: function (cb) {
this._audioCb = cb;
},
/** A14: register consumer of plugin MIDI output.
* cb(events): array of {command, channel, pitch, velocity, data2,
* data3, reserved, sample_offset} — route to DAW tracks. */
onMidiOut: function (cb) {
this._midiOutCb = cb;
}
};
+3 -62
View File
@@ -1,10 +1,8 @@
// SonicForge Runtime service — phát hiện môi trường chạy (desktop Windows /
// docker headless) qua /api/v1/system/capabilities, bật/tắt tính năng theo đó.
// - data-runtime trên <html>: "desktop" | "headless"
// - data-carla="1": có Carla local (hiện nút "Mở trong Carla")
// - Phần tử có thuộc tính data-carla-only sẽ bị ẩn khi không có Carla local.
// - Thư viện preset (.vstpreset) cache trong SonicRuntime.presets — dùng cho
// dropdown gán preset vào track (Carla → pedalboard bridge).
// dropdown gán preset vào track (preset → pedalboard bridge).
window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null, presets: null };
(function () {
@@ -22,19 +20,13 @@ window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null
window.SonicRuntime.loaded = true;
// environment: "docker" | "standalone" — quyết định luồng âm
// instrument: docker → FluidSynthWASM (client), standalone →
// native (backend pyfluidsynth / Carla). Không phải docker =
// standalone (Windows/Linux/macOS chạy trực tiếp trên OS).
// native (backend pyfluidsynth / native_bridge). Không phải
// docker = standalone (Windows/Linux/macOS chạy trực tiếp trên OS).
window.SonicRuntime.environment = c.environment || (c.docker ? 'docker' : 'standalone');
var html = document.documentElement;
html.dataset.runtime = c.runtime || 'unknown';
html.dataset.platform = c.platform || '';
html.dataset.environment = window.SonicRuntime.environment;
html.dataset.carla = (c.features && c.features.carla_local) ? '1' : '0';
if (c.features && c.features.carla_local === false) {
document.querySelectorAll('[data-carla-only]').forEach(function (el) {
el.style.display = 'none';
});
}
// Cache sẵn danh sách preset (static, ít thay đổi)
listPresets().catch(function () {});
return c;
@@ -66,54 +58,3 @@ window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null
load();
}
})();
// ── SonicCarlaMidi: cầu nối MIDI từ track ARM → Carla (OSC /Carla/0/note_*) ──
// Khi track dùng VSTi (synth_engine.type chứa 'vst') + được ARM + máy có Carla
// local → phím bấm trên piano roll / keybed được gửi tới Carla để phát realtime
// (Carla standalone bật OSC UDP mặc định cổng 22752).
window.SonicCarlaMidi = window.SonicCarlaMidi || {
shouldRoute: function (synthEngine, isArmed) {
// Carla bridge removed - all instruments hosted by Native Bridge.
return false;
},
// Playback MIDI items: route khi track VSTi + Carla local (không cần ARM —
// user đã chủ động bấm Play trên item đó).
shouldRoutePlayback: function (synthEngine) {
// Carla bridge removed - all instruments hosted by Native Bridge.
return false;
},
noteOn: function (channel, note, velocity) {
if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return;
window.SonicAPI.carlaMidi({ event: 'note_on', note: note, velocity: velocity || 100, channel: channel || 0 }).catch(function () {});
},
noteOff: function (channel, note) {
if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return;
window.SonicAPI.carlaMidi({ event: 'note_off', note: note, channel: channel || 0 }).catch(function () {});
},
// Bật note rồi tự tắt sau durMs (preview ngắn)
playNote: function (channel, note, velocity, durMs) {
this.noteOn(channel, note, velocity);
var self = this;
setTimeout(function () { self.noteOff(channel, note); }, (durMs || 300) + 50);
},
// Tắt mọi note đang ngân trong Carla (note_off toàn pitch — dùng khi stop)
allNotesOff: function () {
if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return;
for (var ch = 0; ch < 16; ch++) {
for (var n = 0; n < 128; n++) {
window.SonicAPI.carlaMidi({ event: 'note_off', note: n, channel: ch }).catch(function () {});
}
}
},
// Unload Carla bridge: tắt âm + terminate tiến trình Carla (app spawn).
// Gọi khi track chuyển từ VSTi → instrument soundfont để âm KHÔNG còn
// play qua Carla bridge nữa.
stopBridge: function () {
var self = this;
try { self.allNotesOff(); } catch (e) {}
if (window.SonicAPI && window.SonicAPI.stopCarla) {
return window.SonicAPI.stopCarla().catch(function () {});
}
return Promise.resolve();
}
};
+47 -3
View File
@@ -1,9 +1,15 @@
// app/static/js/services/unifiedMidiRouter.js
// Single MIDI entry point for Web MIDI keyboard + timeline playback.
// bridge connected -> NativeBridgeService.dispatchMidiEvent (Rust SHM -> C++ bridge)
// bridge down -> onFallback callback (app.jsx wires SonicSF/Carla path).
// bridge down -> onFallback callback (app.jsx wires SonicSF/native path).
(function () {
var MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15]; // skip 9 (percussion)
var MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14]; // skip 9 (percussion); 15 reserved for Media Explorer preview
var _lastTimelineDbgTs = 0;
// V45.26: dem note live dang giu + lan live event gan nhat — quyet dinh co
// clear master FX outQueue khong (chi idle->active; KHONG khi dang phat lien tuc).
var _liveHeldCount = 0;
var _lastLiveEventAt = 0;
var router = {
bridgeConnected: false,
@@ -24,6 +30,9 @@
/** trackId -> MIDI channel; percussion (bank 128) -> ch 9. */
allocateChannel: function (trackId, isPercussion) {
// Media Explorer preview dùng channel 15 cố định — tránh đè channel
// melodic track (0-8,10-14) / percussion (9) đang giữ instrument.
if (trackId === 'media_explorer') return 15;
if (this._channels[trackId]) return this._channels[trackId].ch;
var ch;
if (isPercussion) {
@@ -48,13 +57,47 @@
* data2/data3 (A12): CC value / program / PB LSB|MSB — passed to bridge only.
*/
pushEvent: function (opts) {
// V28: midi bất kỳ (keyboard/pad/timeline) → restore bridge gain đang
// giữ ~0.0001 sau stop (ding fix — V27 restore 800ms không đủ tail dài).
try { if (window.__restorePendingFadeSession) window.__restorePendingFadeSession(); } catch (e) {}
// V31: live note (ARM keyboard preview) — clear master FX outQueue backlog
// (idle block ~341ms) truoc khi note moi render; timeline note (live=false)
// KHONG clear — tranh drop audio dang phat.
// V45.26: chi clear khi THAT SU idle -> active (khong con note dang giu
// VA im lang >500ms). Truoc day clear MOI note-on live -> rapid 1/8 lien
// tuc: moi note wipe outQueue (dang chua tail note truoc + resume fade-in)
// -> wobble/crackle (bug user). Note dang ngan / chuoi note nhanh -> giu
// queue -> tail note truoc khong bi cat.
if (opts.live) {
try {
var _nowEv = (window.performance && performance.now) ? performance.now() : Date.now();
if (opts.cmd === 'NOTE_ON') {
if (_liveHeldCount === 0 && (_nowEv - _lastLiveEventAt) > 500 && window.__clearMasterFxQueue) window.__clearMasterFxQueue();
_liveHeldCount++;
} else if (opts.cmd === 'NOTE_OFF') {
if (_liveHeldCount > 0) _liveHeldCount--;
}
_lastLiveEventAt = _nowEv;
} catch (e) {}
}
var cmd = opts.cmd, ch = opts.channel, pitch = opts.pitch;
var vel = (opts.velocity === undefined ? 1.0 : opts.velocity);
var sampleOffset = opts.sampleOffset || 0;
if (typeof ch === 'string') ch = this.allocateChannel(ch, opts.percussion);
if (ch === undefined || ch === null) ch = 0;
try {
// V36: midi_push log = 1 Tauri invoke sync (dbg_log -> disk I/O tren main
// thread Rust) moi event. Timeline phat ~380 event/s -> main thread nghet
// -> stall 110-170ms -> crackle. Chi log live (ARM preview), timeline
// gioi han 1/200ms.
var _nowMs = (window.performance && performance.now) ? performance.now() : Date.now();
if (window.__dbgLog && (opts.live || _nowMs - _lastTimelineDbgTs > 200)) {
if (!opts.live) _lastTimelineDbgTs = _nowMs;
window.__dbgLog('midi_push', 'cmd=' + cmd + ' ch=' + ch + ' p=' + (pitch === undefined ? 'n/a' : pitch) + ' live=' + (opts.live ? 1 : 0) + ' vel=' + Math.round((vel || 0) * 100) + (window.__dbgBridgeGain ? ' ' + window.__dbgBridgeGain() : '') + ' q=' + (window.__fxRtQueueLen ? window.__fxRtQueueLen() : 'n/a'));
}
} catch (e) {}
if (this.isBridgeActive()) {
window.NativeBridgeService.dispatchMidiEvent(cmd, ch, pitch, vel, sampleOffset, opts.data2, opts.data3);
window.NativeBridgeService.dispatchMidiEvent(cmd, ch, pitch, vel, sampleOffset, opts.data2, opts.data3, opts.live);
return;
}
if (this.onFallback) this.onFallback(cmd, ch, pitch, vel);
@@ -62,6 +105,7 @@
/** Stop/panic -> bridge transport panic (flush all notes) + fallback local stopAll. */
panic: function () {
_liveHeldCount = 0;
if (this.isBridgeActive() && window.NativeBridgeService.transport) {
window.NativeBridgeService.transport('panic');
}
+421
View File
@@ -0,0 +1,421 @@
// sf-fx-realtime.js — AudioWorkletProcessor nối WebAudio → SHM FX engine.
// process() gom 128-sample quantum thành block 256 stereo float32 (khớp
// FXRT_BLOCK trong FxRealtimeIPC.h) → postMessage cho main thread gửi WS;
// nhận block đã xử lý từ main thread → queue → phát ra output. Queue rỗng →
// zero fill (WS chưa kịp round-trip).
//
// BURST (fix lag/giật/crackle): gom 4 block (1024 mẫu) vào 1 frame rồi post.
// Bridge VST3 (fx_vst_bridge) chạy chậm hơn realtime khi take=1 (256 mẫu:
// ~5.5ms/call > budget 5.33ms → 179/s < 187.5/s → engine drop input →
// crackle). Gửi burst 4 block → bridge take=4 → 1 call process 1024 mẫu
// (amortize overhead: 14-19ms/4 block ≈ 3.5-4.75ms/block) → đủ 187.5/s.
// Latency thêm ~4 block (~21ms) — worklet outQueue 32 block hấp thụ.
//
// FILL (hết crackle do jitter): không phát ngay khi có block đầu — chờ queue
// ≥ FILL (8 block ~43ms @48k) rồi mới phát. Engine gửi ASAP (không pace
// realtime) → client tích lũy buffer an toàn; mọi jitter (bridge process VST
// biến thiên, GC, WS, main thread rAF) bị hấp thụ bởi FILL thay vì queue ≈1
// → cạn → ngắt quãng.
// UNDERRUN (queue cạn khi đang phát): V23 — KHÔNG lặp block cuối (lặp tạo
// jump pha ở điểm vào hold + mỗi wrap → click), KHÔNG zero-fill: fade-out
// tuyến tính từ mẫu output cuối (HOLD_FADE block ~21ms) tới im lặng; resume
// NGAY khi có block mới (không đợi đủ FILL): đã im lặng → fade-in từ 0, đang
// giữa fade → crossfade 64 mẫu từ mẫu hiện tại (hết jump). Cap-drop (queue
// tràn sau stall) crossfade 64 mẫu (hết click) — giống bridgeAudioWorklet.
// Cap 64 block (~341ms) chặn overflow khi bridge nhanh hơn realtime.
// ponytail: thay fade-out bằng adaptive stretch khi cần chất lượng cao hơn.
class SfFxRealtimeProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.BLOCK = 256; // FXRT_BLOCK
this.BURST = 4; // số block gom 1 frame (bridge batch)
this.FILL = 96; // V46: 96 block ~512ms @48k: hấp thụ stall ~500ms (u2 08:16:44.8: pump deficit 723ms, event gap 507ms → V45 cushion 256ms cạn → silence ~500ms → wobble). PDC 26368 = (7+96)*256 (fxRtApplyPdc). V45 48 -> cushion 256ms < deficit 723ms -> underrun. V46 96 -> cushion ~512ms (margin ~200ms vs event 507ms; deficit cluster vẫn có thể underrun ngắn — chấp nhận, content jump giữ policy V24).
this.RESUME_FILL = 24; // V26: re-buffer khi resume SAU STOP (flush_for_stop) — 16 block ~85ms (V43: = FILL) → play kế tiếp bắt đầu nhanh; underrun-stall vẫn dùng FILL (crackle/wobble).
this.resumeFill = this.FILL; // mức re-buffer khi resume sau flush — stop: RESUME_FILL, underrun-stall: FILL
this.CAP = 104; // V46: = FILL+8: cap queue chặn overflow (~550ms)
this.HOLD_FADE = 4; // 4 block ~21ms: fade 1→0 sau HOLD hết (hết click cắt cứng)
this.holdFadeLeft = 0; // mẫu còn lại của fade-out khi underrun
this.silenced = false; // đã fade về 0 → im lặng; resume phải fade-in, không hold lại
this.bufLen = this.BLOCK * this.BURST;
this.inL = new Float32Array(this.bufLen);
this.inR = new Float32Array(this.bufLen);
this.inCount = 0;
this.outQueue = []; // [{l: Float32Array, r: Float32Array}]
this.outPos = 0;
this.playing = false; // false → im lặng (đang chờ đủ FILL)
this.hasStarted = false; // đã phát block đầu chưa — FILL chỉ chờ ở lần khởi đầu
this.flushPending = false; // V24: đang chờ engine flush (resume sau underrun) — drop block stale
this.resumeFadeIn = false; // V24: interruption đã xử lí (clear+flush) — resume sẽ fade-in từ 0
this.FADE = 512; // fade-in 512 mẫu (~11ms @48k) khi bắt đầu phát/resume — hết click
this.outGain = 0; // gain phát hiện tại (0→1 trong fade)
this.fadeLeft = 0; // số mẫu còn lại của fade
this.skipFade = 0; // >0: crossfade đang chạy sau jump (cap-drop/resume/wrap) — hết click
this.skipFadeTotal = 64; // V42: độ dài crossfade hiện tại (64 mẫu thường; WRAP_FADE 256 tại seam wrap)
this.skipFromL = 0.0; // mẫu output tại điểm jump — điểm bắt đầu crossfade
this.skipFromR = 0.0;
this.lastOutL = 0.0; // mẫu output cuối — nguồn crossfade + điểm bắt đầu fade-out
this.lastOutR = 0.0;
this.wrapFadeBlocks = -1; // V42: số block tail pass cũ còn lại sau clear_queue('loop') — đếm ngược để crossfade đúng seam tail→head
this.latencySamples = 0; // G3: latency chain thật (engine qua WS) — align wrap fade theo processed tail
this.wrapMode = false; // V45.3: dang trong wrap (clear_queue loop) — resume underrun bo flush + re-buffer nho
this.WRAP_FADE = 256; // V42: crossfade 256 mẫu (~5.3ms @48k) tại seam wrap — che click jump nội dung end→start (FX path)
this.fadeFromL = 0.0; // mẫu cố định khi fade-out underrun (fade tuyến tính tới 0)
this.fadeFromR = 0.0;
this.port.onmessage = (e) => {
const d = e.data;
if (d && d.type === 'set_param') { this.port.postMessage(d); return; }
if (d && d.type === 'flush_done') { this.flushPending = false; return; }
if (d && d.type === 'latency') { this.latencySamples = (d.samples || 0); return; }
if (d && d.type === 'flush_for_stop') {
// V26: user stop — main thread báo xóa audio tail trong queue (audio đã
// xử lí còn phát ~0.3-0.5s sau stop = tiếng "ding"). Fade-out 21ms từ
// mẫu cuối (hết click) → im lặng; báo main flush engine ring (audio
// stale trong WS/SHM không chơi tiếp). Resume lần sau: RESUME_FILL
// (8 block ~43ms, V40: = FILL) — play kế tiếp không chờ trễ.
this.outQueue.length = 0;
this.outPos = 0;
this.playing = false;
this.wrapFadeBlocks = -1; // V42: stop — hủy wrap crossfade chờ
this.flushPending = true;
this.resumeFadeIn = true;
this.resumeFill = this.RESUME_FILL;
this.silenced = false;
this.wrapMode = false;
// V30: fade-out tu mau output CUOI (lastOut) — truoc day dung fadeFrom
// cu (mac dinh 0) -> jump mau -> click/crackle ngay khi stop.
this.fadeFromL = this.lastOutL;
this.fadeFromR = this.lastOutR;
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
this.port.postMessage({ type: 'flush' });
return;
}
if (d && d.type === 'clear_queue') {
this.clearMode = d.mode; // V45.19: nho mode de tag fx_seek_first tai dbgPlayFirst
// V34: backlog guard o DAU handler (V33 dat sau mutation -> outQueue.length=0
// chay truoc -> guard luon return, clear van drop queue + fade + re-buffer moi
// lan = V31). Queue nho (< FILL 8 block) -> bo qua hoan toan (khong mutation,
// khong re-buffer) -> het ARM crackle (queue ARM chi 0-8 block, engine ~realtime).
// Queue day (idle lau, engine chay truoc ~32 block) -> clear dung (fix lag 170ms).
// V35: mode:'play' (start/section/pianoroll) LUON clear - queue < FILL luc
// play-start con stale block idle/ARM (~16) -> bo qua => phat audio cu roi
// can queue ~500ms (cold start). mode:'live' (ARM) giu guard het crackle.
// V37 ARM probe: reset dbg flags BEFORE guard so live notes (small queue) also log fx_hop
this.dbgClearAt = currentTime * 1000;
this.dbgInFirst = true;
this.dbgOutFirst = true;
this.dbgPlayFirst = true;
// V39: loop wrap — KHONG wipe: giu queue chay lien tuc (tail pass cu
// pre-roll pass moi qua VST lookahead) -> het wobble dau bai. Chi reset
// dbg flags. mode 'play' van wipe (bao ve audio cu khi seek/cold start).
// V42: dem so block tail pass cu con lai trong queue -> wrap crossfade
// trigger dung luc block dau pass moi len front (seam tail->head).
if (d.mode === 'loop') {
// G3: align processed-tail — tail pass cũ còn in-flight trong bridge
// (FX latency thật từ engine) chưa về queue; đếm đủ block tới seam
// (block đầu pass mới lên front) → crossfade đúng chỗ jump end→start.
// V45.2 (option B): cắt bớt tail cũ giữ KEEP block thay vì giữ nguyên
// FILL 24 (~128ms) → downbeat pass mới không trễ ~110ms. skipFade
// crossfade từ lastOut che jump cắt front; wrapFadeBlocks đếm lại từ
// KEEP để seam crossfade vẫn đúng lúc.
// V45.3: KEEP 4 -> 16 (~85ms): KEEP 4 (~21ms) engine (stall bridge
// 68-138ms sau transport play) khong refill kip -> queue can -> underrun
// -> flush + re-buffer FILL 24 = gap ~110-235ms moi wrap (WORSE hon
// truoc V45.2: downbeat tre 106-128ms nhung khong gap). KEEP 16 che
// stall dien hinh 68-82ms lien tuc (khong gap, downbeat ~85ms); stall
// dai hon roi vao nhanh resume wrap (bo flush + resumeFill 8).
const KEEP = 16;
this.wrapMode = true;
if (this.outQueue.length > KEEP) {
const head = this.outQueue.length - KEEP;
this.outQueue.splice(0, head);
this.outPos = 0;
if (this.playing) {
this.skipFade = this.WRAP_FADE;
this.skipFadeTotal = this.WRAP_FADE;
this.skipFromL = this.lastOutL;
this.skipFromR = this.lastOutR;
}
}
this.wrapFadeBlocks = this.outQueue.length + Math.ceil((this.latencySamples || 0) / this.BLOCK);
return;
}
if (d.mode === 'seek') {
// V45.18: seek khi play master FX active — wipe queue cu (audio cu
// choi tiep 0-50ms sau click) + resume nhanh RESUME_FILL 8 (~43ms)
// thay vi FILL 24 (~128ms) — het "am tre 1 beat" (hit dau roi vao
// vung cam 128ms). Giong resume wrap V45.4 (proven khong gap).
this.outQueue.length = 0;
this.outPos = 0;
this.playing = false;
this.wrapFadeBlocks = -1;
this.wrapMode = false;
this.resumeFadeIn = true;
this.resumeFill = 8;
this.silenced = false;
this.fadeFromL = this.lastOutL;
this.fadeFromR = this.lastOutR;
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
this.port.postMessage({ type: 'dbg', tag: 'fx_seek_clear', t: Math.round(currentTime * 1000), q: 0 });
return;
}
if (d.mode !== 'play' && this.outQueue.length < this.FILL) return;
// V31: play/ARM live note — main thread bao xoa backlog block idle
// (engine render lien tuc khi STOP -> outQueue day ~32 block ~170ms
// audio cu; play moi phai cho phia sau -> lag 0.3-0.5s). KHONG post
// 'flush' (khong chaym engine WS — note-on play vua day xuong se bi
// drop). Resume sau 8 block (~43ms) + fade-out/fade-in tu mau cuoi.
this.outQueue.length = 0;
this.outPos = 0;
this.playing = false;
this.wrapFadeBlocks = -1; // V42: wipe — hủy wrap crossfade chờ (không có tail cũ)
this.wrapMode = false; // V45.3: play/seek — het context wrap
this.resumeFadeIn = true;
// V34: mode phan biet — play re-buffer FILL 8 (~43ms, cushion du hap thu
// engine ramp/jitter dau play); ARM live note giu RESUME_FILL 8 (~43ms) — preview
// nhanh, khong cho FILL 8.
this.resumeFill = (d.mode === 'play') ? this.FILL : this.RESUME_FILL;
this.silenced = false;
this.fadeFromL = this.lastOutL;
this.fadeFromR = this.lastOutR;
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
return;
}
if (!d || d.type !== 'out' || !d.l || !d.r) return;
// V24: đang chờ flush (resume sau underrun/stall) → drop block stale.
// Backlog engine gửi khi worklet im là audio CŨ (chơi lại → lag cộng dồn
// ~1s) — chỉ nhận block MỚI từ sau flush_done.
if (this.flushPending) return;
this.outQueue.push({ l: d.l, r: d.r });
if (this.dbgOutFirst) {
this.dbgOutFirst = false;
this.port.postMessage({ type: 'dbg', tag: 'fx_out_first', t: Math.round(currentTime * 1000), q: this.outQueue.length });
}
while (this.outQueue.length > this.CAP) {
this.outQueue.shift();
this.statCapDrop++;
// V23: drop cứng = jump mẫu → click. Crossfade 64 mẫu từ mẫu output
// cuối sang block mới; chỉ đặt 1 lần cho đợt drop liên tiếp. outPos=0:
// block mới (front) phải phát từ đầu, không tiếp tục vị trí block cũ.
if (this.skipFade <= 0) {
this.skipFade = 64;
this.skipFadeTotal = 64;
this.skipFromL = this.lastOutL;
this.skipFromR = this.lastOutR;
}
this.outPos = 0;
}
};
// Telemetry (chẩn đoán crackle trên session thật): đếm sự kiện + báo main
// thread mỗi ~2s. Nếu user bật master FX nghe crackle mà stats cho thấy
// underruns>0 → delivery bị stall (main-thread busy) → nguồn crackle thật.
this.statUnderrun = 0; this.statHold = 0; this.statFade = 0;
this.statSilence = 0; this.statCapDrop = 0; this.statFadeIn = 0;
this.statCalls = 0;
// V32: do latency master FX path — danh dau clear_queue, bao main thread hop dau
// sau clear (in_first / out_first / play_first) → dbg.log delta vs play_start.
this.dbgClearAt = 0;
this.dbgInFirst = false;
this.dbgOutFirst = false;
this.dbgPlayFirst = false;
this._wlLast = 0; // V46: thời điểm process() lần trước — đo gián đoạn audio thread
this._wlGapAt = 0; // V46: rate-limit wl_gap 1/s
}
process(inputs, outputs) {
const input = inputs[0];
const output = outputs[0];
const n = output[0] ? output[0].length : 128;
const outL = output[0];
const outR = output[1];
// V46: wl_gap — audio thread bị gián đoạn (process() cách >30ms, thường do
// main thread block chia sẻ buffer/GC) → dbg log. Rate-limit 1/s.
if (this._wlLast > 0) {
const _wlGap = currentTime - this._wlLast;
if (_wlGap > 0.030 && (currentTime - this._wlGapAt) > 1.0) {
this._wlGapAt = currentTime;
this.port.postMessage({ type: 'dbg', tag: 'wl_gap', t: Math.round(currentTime * 1000), g: Math.round(_wlGap * 1000) });
}
}
this._wlLast = currentTime;
// Góp input thành burst 4 block (1024 mẫu) → gửi main thread
// (transferable buffer). Main thread tách 4 WS frame → bridge batch 4.
if (input && input[0]) {
const inL = input[0];
const inR = input[1] || input[0];
for (let i = 0; i < n; i++) {
if (this.inCount < this.bufLen) {
this.inL[this.inCount] = inL[i];
this.inR[this.inCount] = inR[i];
}
this.inCount++;
if (this.inCount >= this.bufLen) {
const inter = new Float32Array(this.bufLen * 2);
for (let j = 0; j < this.bufLen; j++) {
inter[j * 2] = this.inL[j];
inter[j * 2 + 1] = this.inR[j];
}
this.port.postMessage({ type: 'in', data: inter.buffer }, [inter.buffer]);
this.inCount = 0;
if (this.dbgInFirst) {
this.dbgInFirst = false;
this.port.postMessage({ type: 'dbg', tag: 'fx_in_first', t: Math.round(currentTime * 1000) });
}
}
}
}
// Phát output: chờ queue ≥ FILL trước khi phát lần đầu (hấp thụ jitter
// engine/bridge/WS); underrun GIỮA stream → fade-out 21ms từ mẫu cuối rồi
// im lặng (V23); resume NGAY khi có block mới (không đợi re-buffer FILL),
// đã im lặng → fade-in, giữa fade → crossfade 64 mẫu (hết jump).
for (let i = 0; i < n; i++) {
this.statCalls++;
if (this.statCalls >= 1500) { // ~2s @48k/128
this.statCalls = 0;
this.port.postMessage({ type: 'stats',
underruns: this.statUnderrun, holds: this.statHold, fades: this.statFade,
silences: this.statSilence, capDrops: this.statCapDrop, fadeIns: this.statFadeIn,
queueLen: this.outQueue.length });
}
if (this.playing) {
const blk = this.outQueue[0];
if (blk) {
if (this.fadeLeft > 0) {
this.outGain = Math.min(1, this.outGain + 1 / this.FADE);
this.fadeLeft--;
}
if (this.skipFade > 0) {
const t = 1 - this.skipFade / this.skipFadeTotal;
outL[i] = (this.skipFromL * (1 - t) + blk.l[this.outPos] * t) * this.outGain;
if (outR) outR[i] = (this.skipFromR * (1 - t) + blk.r[this.outPos] * t) * this.outGain;
this.skipFade--;
} else {
outL[i] = blk.l[this.outPos] * this.outGain;
if (outR) outR[i] = blk.r[this.outPos] * this.outGain;
}
this.lastOutL = outL[i];
this.lastOutR = outR ? outR[i] : outL[i];
this.outPos++;
if (this.outPos >= this.BLOCK) {
this.outQueue.shift();
this.outPos = 0;
// V42: seam tail→head — sau khi shift hết N block tail pass cũ, block
// đầu pass mới lên front → crossfade WRAP_FADE từ mẫu cuối (che click
// jump nội dung end→start, FX path). Trigger 1 lần mỗi wrap.
if (this.wrapFadeBlocks > 0) {
this.wrapFadeBlocks--;
if (this.wrapFadeBlocks === 0) {
this.skipFade = this.WRAP_FADE;
this.skipFadeTotal = this.WRAP_FADE;
this.skipFromL = this.lastOutL;
this.skipFromR = this.lastOutR;
this.port.postMessage({ type: 'dbg', tag: 'fx_wrap_xfade', t: Math.round(currentTime * 1000), q: this.outQueue.length });
}
}
}
continue;
}
// Underrun giữa stream — V23: fade-out NGAY (không lặp block cuối:
// lặp tạo jump pha ở điểm vào + mỗi wrap → click). Fade tuyến tính từ
// mẫu output cuối tới 0 (21ms) — vào liền mạch (fadeFrom = lastOut),
// hết click cắt cứng. Resume → crossfade/fade-in (xem dưới).
this.playing = false;
this.wrapFadeBlocks = -1; // V42: underrun — hủy wrap crossfade chờ (resume qua fade-in/crossfade thường)
this.statUnderrun++;
if (!this.silenced) { // đã im lặng (fade xong) → giữ im lặng
this.fadeFromL = this.lastOutL;
this.fadeFromR = this.lastOutR;
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
this.statHold++;
}
}
// Không phát: resume khi đủ block (lần đầu chờ FILL; sau đó chỉ cần 1
// block — hold che khoảng chờ). V25: sau flush (resumeFadeIn=true) chờ
// re-buffer đủ FILL mới resume — chặn loop underrun→flush→resume-1-block
// →underrun (wobble): 1 block phát hết ~5ms → cạn → flush lại.
if (this.outQueue.length >= (this.hasStarted ? (this.resumeFadeIn ? this.resumeFill : 1) : this.FILL)) {
// V24: interruption (underrun sau stall) — backlog trong queue là audio
// LẠC HẬU (chơi lại → lag cộng dồn ~1s). KHÔNG chơi backlog: clear
// queue + báo main thread flush (engine xóa input/output ring, main
// drop output stale in-flight) → chờ flush_done → resume chỉ với block
// MỚI. flushPending → onmessage drop block stale trong lúc chờ (hết
// race ack). Fade-out đang chạy (nếu có) tiếp tục tới im lặng — hết
// click. resumeFadeIn guard chống re-trigger flush khi ack tới mà fade
// chưa xong. resume → fade-in từ 0 (đã im) hoặc crossfade từ mẫu hiện
// tại (giữa fade) — hết jump.
const wasInterrupted = !this.resumeFadeIn && (this.holdFadeLeft > 0 || this.silenced) && this.hasStarted;
if (wasInterrupted) {
// V45.3: wrap (loop restart) — engine da transport('play') tu main
// (D17): block moi = audio pass moi, KHONG phai backlog stale (boi
// worklet flush gui 'flush' xuong engine ring = lam mat tac dung D17
// skip-flush o stopAllPlayback -> gap giua seam). Bo flush (round-trip)
// + re-buffer nho hon FILL 24 (~128ms) -> resume sau stall som hon
// (~43ms) -> gap wrap giam tu ~110-235ms xuong con vai chuc ms.
const _wrap = this.wrapMode;
this.outQueue.length = 0;
this.outPos = 0;
this.resumeFadeIn = true;
this.resumeFill = _wrap ? 8 : this.FILL;
this.flushPending = _wrap ? false : true;
if (!_wrap) this.port.postMessage({ type: 'flush' });
i--;
continue;
}
this.playing = true;
this.outPos = 0;
this.holdFadeLeft = 0;
this.wrapMode = false; // V45.3: resume xong — ve che do thuong (underrun sau nay flush nhu cu)
if (this.dbgPlayFirst) {
this.dbgPlayFirst = false;
// V45.19 Bug 1b: seek -> tag fx_seek_first (main thread snap khong can
// guard 50ms — seek resume nhanh ~43ms < 50ms). Wrap/play -> fx_play_first.
this.port.postMessage({ type: 'dbg', tag: (this.clearMode === 'seek' ? 'fx_seek_first' : 'fx_play_first'), t: Math.round(currentTime * 1000), q: this.outQueue.length, m: this.clearMode || '' });
}
if (this.resumeFadeIn) {
this.resumeFadeIn = false;
if (this.silenced) {
// Đã im lặng (fade xong / sau flush) → fade-in từ 0 (hết jump ~0→1).
this.outGain = 0;
this.fadeLeft = this.FADE;
this.statFadeIn++;
} else {
// Giữa fade-out → crossfade ngắn từ mẫu hiện tại (không jump về 0).
this.skipFade = 64;
this.skipFadeTotal = 64;
this.skipFromL = this.lastOutL;
this.skipFromR = this.lastOutR;
}
}
this.silenced = false;
if (!this.hasStarted) {
this.hasStarted = true;
this.outGain = 0;
this.fadeLeft = this.FADE;
// Báo main thread 1 lần để crossfade dry→fx đúng thời điểm.
this.port.postMessage({ type: 'started' });
}
i--;
continue;
}
if (this.holdFadeLeft > 0) {
// Fade-out 1→0 (underrun): fade tuyến tính từ mẫu cuối, im lặng êm.
const g = this.outGain * (this.holdFadeLeft / (this.HOLD_FADE * this.BLOCK));
outL[i] = this.fadeFromL * g;
if (outR) outR[i] = this.fadeFromR * g;
this.holdFadeLeft--;
if (this.holdFadeLeft <= 0) { this.silenced = true; this.statFade++; this.statSilence++; }
this.lastOutL = outL[i];
this.lastOutR = outR ? outR[i] : outL[i];
continue;
}
outL[i] = 0;
if (outR) outR[i] = 0;
this.lastOutL = 0;
this.lastOutR = 0;
}
return true;
}
}
registerProcessor('sf-fx-realtime', SfFxRealtimeProcessor);
+2 -2
View File
@@ -430,7 +430,7 @@ def cleanup_expired_files_task(max_age_hours: int = 24):
@_task
def render_project_task(project_id: str, project_name: str, project_json_str: str, sample_rate: int = 44100):
def render_project_task(project_id: str, project_name: str, project_json_str: str, sample_rate: int = 44100, metadata: dict = None):
"""
Task Celery để kết xuất dự án ngoại tuyến (Offline Project Mixdown) áp dụng specs 30_DAW_ARCHITECT.md.
"""
@@ -442,7 +442,7 @@ def render_project_task(project_id: str, project_name: str, project_json_str: st
output_path = os.path.join(settings.PROCESSED_DIR, output_filename)
engine = PythonRenderEngine(sample_rate=sample_rate)
engine.render_project(project_json, output_path)
engine.render_project(project_json, output_path, metadata=metadata or None)
return {
"project_id": project_id,
+23 -22
View File
@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>SonicForge Studio - Professional DAW Editor</title>
<title>ScoreFX Studio - Professional DAW Editor</title>
<script>
// ── Chặn BROWSER ZOOM toàn trang ──
// UI dùng px cố định cho item/button/label — browser zoom (Ctrl+wheel,
@@ -24,7 +24,7 @@
document.addEventListener('gesturestart', function (e) { e.preventDefault(); }, { passive: false });
document.addEventListener('gesturechange', function (e) { e.preventDefault(); }, { passive: false });
</script>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" href="/static/img/scorefx_favicon.png">
<!-- Bug #11: vendor local — trước đây load CDN (unpkg/cdn.tailwindcss/cdnjs) →
desktop Tauri offline trắng màn hình. Các file này nằm /static/vendor/. -->
<script src="/static/vendor/tailwindcdn.js"></script>
@@ -32,26 +32,27 @@
<link rel="stylesheet" href="/static/vendor/fontawesome.min.css">
<script src="/static/vendor/react.production.min.js"></script>
<script src="/static/vendor/react-dom.production.min.js"></script>
<script src="/static/js/services/fluidsynthLoader.js?v=202607271245"></script>
<script src="/static/js/services/runtime.js?v=202608101800"></script>
<script src="/static/js/services/api.js?v=202608101800"></script>
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
<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=202608112200"></script>
<script src="/static/js/services/bridgeAudioNode.js?v=202608121700"></script>
<script src="/static/js/services/unifiedMidiRouter.js?v=202608112200"></script>
<script src="/static/js/services/audioRoutingEngine.js?v=202608112200"></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>
<script src="/static/js/services/ghostNoteExtractor.js?v=202607271727"></script>
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608141030" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<script src="/static/js/services/fluidsynthLoader.js?v=202608290945201545"></script>
<script src="/static/js/services/runtime.js?v=202608290945"></script>
<script src="/static/js/services/api.js?v=202608290945"></script>
<script src="/static/js/services/audioEngine.js?v=202608290945"></script>
<script src="/static/js/services/storage.js?v=202608290945"></script>
<script src="/static/js/services/soundfontStorage.js?v=202608290945"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608290945"></script>
<script src="/static/js/services/nativeBridgeService.js?v=202608291510"></script>
<script src="/static/js/services/bridgeAudioNode.js?v=202608311858"></script>
<script src="/static/js/services/unifiedMidiRouter.js?v=202608311756"></script>
<script src="/static/js/services/audioRoutingEngine.js?v=202608290945"></script>
<script src="/static/js/services/aiGateway.js?v=202608290945"></script>
<script src="/static/js/services/dawCommandDispatcher.js?v=202608290945"></script>
<script src="/static/js/services/pianoRollTabService.js?v=202608290945"></script>
<script src="/static/js/services/ghostNoteExtractor.js?v=202608290945"></script>
<script src="/static/js/services/midiExtractor.js?v=202608290945"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202608290945"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202608290945"></script>
<script src="/static/js/services/chordTheory.js?v=202608290945"></script>
<script src="/static/js/app.precompiled.js?v=202609011700" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202608290945">
<style>
:root {
--right-sidebar-width: 320px;
+86
View File
@@ -0,0 +1,86 @@
# Sonic Forge Studio — Big Tasks (ROADMAP G0–G4)
Trạng thái: 2026-08-15. Bug gốc: PLAY + mở GUI VST → câm, GUI không load.
User duyệt roadmap G0–G4 ("Hãy bắt đầu"). Đang ở G1.
## Ngữ cảnh kỹ thuật
- Logs: `C:/Users/locpham/AppData/Roaming/SonicForgeDAW/logs/` — `bridge.log` (stdout bridge), `spawn.log` (watchdog restart), `engine.log`.
- VST test: `C:\Program Files\Common Files\VST3\Nexus.vst3`.
- SHM: `SonicForge_DAW_IPC_<APP_PID>`; offsets: controlQueue @2836 (8×1040), controlQueueCount @11156, attach_in_flight @11160, writeIndex @4, size 11164. Control types: 2=LOAD, 3=TRANSPORT (0=STOP/1=PLAY), 4=OPEN_GUI.
- Branch: `standalone-shm-bridge`. App: sonicforge-daw, daw_engine, daw_vst_bridge (3 process).
- Probe: `native_bridge/build/Release/gui_probe.exe` — standalone VST3 GUI attach probe.
Variants: main_own | worker_own_nopump | worker_own_pump | worker_foreign_nopump | worker_foreign_pump | bridge_like | same_thread | two_instances | two_instances_close | close_reopen | bridge_two | shared_worker | ui_thread | two_workers_close.
- File CRLF toàn bộ native_bridge + CMakeLists.txt → patch multi-line qua python `io.open(newline='')`.
## G0 — Hoàn thành (đã commit, branch standalone-shm-bridge)
- G0.1 (1839480): defer OPEN_GUI khi PLAY.
- G0.2 (2f78618): openVstGuiRetry ≤2 lần + `__bridgePlaying` guard (app.precompiled.js).
- G0.3 (5313d85): watchdog giữ 15s khi attach-in-flight (SHM flag offset 11160) + RAII guard.
- G0.4 (eac7f2f): save/restore preset quanh reload — captureState/restoreState trong Vst3Instrument (getState/setState qua MemoryStream), gọi trong post_close_gui; INativeInstrument.h virtual no-op; CMakeLists link memorystream.cpp vào daw_vst_bridge + sdk_common. VERIFIED PASS (probe_g04_preset.py).
## G1 — 1 UiThread chung (ĐANG LÀM)
Mục tiêu: 1 thread chung host mọi VST3 instance thay per-channel workers → hết "2 thread 1 DLL" hang/crash (bridge_two HANG/EXIT3221225477 vs shared_worker OK).
### G1.1 — Probe variant ui_thread (alias shared_worker) [HOÀN THÀNH]
- [x] Patch gui_probe.cpp: usage string, branch `variant == "shared_worker" || variant == "ui_thread"`, isDeferred list.
- [x] Fix build: CMakeLists gui_probe target thêm `memorystream.cpp` + `sdk_common` (LNK2019).
- [x] Fix crash 0xC0000409: ProbeWorker thiếu destructor join → std::terminate. Thêm destructor + join + closeGUI trước teardown + gỡ IME. VERIFIED `shared_worker 15`: RESULT done=1 aRes=1 bRes=1, EXIT=0, 6.73s. Commit 3587a34.
### G1.2 — Chuyển tạo window + attach job từ ChannelWorker → UiThread mới (1 thread chung) [HOÀN THÀNH — 9e6e72a]
- [x] Thay per-channel workers (16) bằng 1 UiThread: COM STA + message pump + job queue.
- [x] Audio loop bỏ PeekMessageW.
- [x] Option B (đóng editor channel khác trước attach) chuyển PostMessage(WM_CLOSE)+5s → close_editor_now đồng bộ (1 thread: job close sau attach job cùng queue → không bao giờ chạy).
- [x] VERIFIED: build EXIT=0; live test PASS — LOAD 7, PLAY writeIndex advancing, defer OK, attach OK, Option B close đồng bộ OK, WM_CLOSE→reload OK, 0 crash/restart.
### G1.3 — Close/resize/IME sang UiThread + bỏ pump gate cũ [HOÀN THÀNH — b92c85e]
### G1.4 — Bỏ mute-when-editor-open [HOÀN THÀNH — 7629783]
- [x] channelQuiet chi con reloading (bo editorOpenFn check); xoa setEditorOpenPredicate/g_editorOpen/g_editorPathCount/clear_editor_open.
- [x] VERIFIED: build PASS; live test PASS — PLAY+editor open writeIndex advancing (het cam), LOAD-while-editor OK, close/reopen x4 OK, regression G1.2 PASS, 0 crash/restart.
### G1.5 — Regression driver stress [HOÀN THÀNH]
- [x] stress driver native_bridge/debug/test_g15_stress.py: 5 + 10 iterations LOAD/OPEN_GUI/PLAY/STOP/WM_CLOSE Nexus+EZkeys.
- [x] VERIFIED: PASS ca hai run — PLAY+editor open writeIndex advancing moi iter (delta ~290-330), WM_CLOSE state restored OK, bridge alive 30512, 0 EXCEPTION/FAILED/crash.
## G2 — Persistence restart (2-3 tuần)
Mục tiêu: restart không mất instrument — "stop/play nghe lại" ngay sau respawn.
- G2.1 save/restore đầy đủ: toàn bộ trạng thái instrument (path, preset, params, transport position) ra đĩa.
- G2.2 dump state trước kill: trước khi DAW/bridge bị kill, dump trạng thái hiện tại.
- G2.3 auto-reload sau respawn: sau khi bridge respawn, tự load lại từ dump.
- G2.4 bỏ !isPlayingRef chặn restore: gỡ guard chặn restore khi đang PLAY.
## G3 — Health / Watchdog thông minh (1-2 tuần)
Mục tiêu: Watchdog thông minh, cô lập lỗi.
- G3.1 heartbeat riêng: heartbeat bridge/engine tách khỏi logic audio.
- G3.2 phân loại stall: phân biệt hang/crash/stall để xử lý đúng mức.
- G3.3 SEH quanh process từng channel: 1 plugin crash → chỉ mute channel đó, không sập cả bridge.
## G4 — Sandbox plugin (tuỳ chọn, 2-3 tháng)
Mục tiêu: plugin process con — VST3 không có chuẩn OOP như AUv3 → wrapper tự làm.
Lưu ý: G1+G3 đã lo 90% kịch bản. Khuyến nghị làm sau.
## Kiến trúc hiện tại (main.cpp 1254 dòng, sau G1.4) — tham chiếu G1.2–G1.5
- 1 UiThread chung (class ChannelWorker, main.cpp ~236): COM STA + job queue + message pump (cap 128 msg/iteration). MỌI window (native VST + editor children) thuộc thread này; CBT hook IME per-thread + priority ABOVE_NORMAL. loadPlugin/createView/close_editor_now/LOAD jobs đều chạy ở đây — thread sống cả đời bridge, COM STA apartment không chết → Nexus attached() không treo. (G1.1/G1.2)
- Audio/main loop (main.cpp ~972-1235): drain control queue → pending-GUI sweep → MIDI snapshot → dispatch sample-accurate (A11: sort theo sampleOffset, renderSegment từng sub-block) → renderAll → bridgeWriteIndex++ → sync real-time. KHÔNG còn PeekMessageW (G1.3); try/catch net bọc cả loop (plugin exception không escape).
- Control (SHM): controlQueue @2836 (8×1040), count @11156, attachInFlight @11160, writeIndex @4. Types: 1=PANIC, 2=LOAD, 3=TRANSPORT (0=STOP flush notes + CC64, 1=PLAY, 2=SET_POSITION), 4=OPEN_GUI.
- OPEN_GUI (handleOpenGui): defer khi chưa load hoặc đang PLAY → g_pendingGui, sweep khi transportStopped && loaded (G0.1); attach job trên UiThread (g_loadMutex serialize createView); attach-in-flight báo watchdog qua SHM attachInFlight RAII (G0.3); Option B đóng editor channel khác ĐỒNG BỘ qua close_editor_now (G1.2 — bỏ PostMessage(WM_CLOSE)+5s timeout).
- LOAD (assign trên UiThread): setReloading(ch + mọi same-path channel) TRƯỚC khi vào DLL; hide + detach editor cũ (closeGUI + destroy_window_on_owner); restore unmute sau job (g_closeInFlight + g_attachPaths ngăn unmute nhầm).
- Guards còn lại: g_loadMutex (serialize mọi plugin-DLL entry: createInstance + createView), reloadingCh_[16] (engine — channelQuiet CHỈ mute khi reloading; G1.4 bỏ mute-when-editor-open), g_attachPaths + g_attachMutex (attach-in-flight registry), g_closeInFlight[16], g_guiMutex + g_guiWindows + g_hwndToCh (window registry: giữ sống + reuse + WM_DESTROY cleanup), g_pendingGui + g_pendingGuiMutex, g_cbtHook (strip IMC trên JUCE windows — chống IME recursion 0xC00000FD), destroy_window_on_owner (destroy editor child trên UiThread, không cross-thread — hết 0xc000041d).
- VstWindowProc: WM_CLOSE → post_close_gui + hide (KHÔNG DefWindowProc destroy khi view attached — tránh cross-thread destroy); WM_DESTROY → registry cleanup + post_close_gui; WM_SIZE → MoveWindow child + post_resize_view; WM_ERASEBKGND/WM_PAINT no-op (chống trắng loá).
- G0.4: save/restore preset quanh close/reload — captureState/restoreState qua MemoryStream (post_close_gui/LOAD); bridge + gui_probe link memorystream.cpp.
- Watchdog thread riêng (main.cpp ~600): parent chết → TerminateProcess ngay + check birth-time chống PID reuse (V8 bug 2).
## Learnings
- read_file lỗi với main.cpp/lib.rs/gui_probe.cpp → dùng `sed -n` qua exec_command.
- MemoryStream::write cần 3 args (buffer, size, int32* numBytesWritten); seek(0, kIBSeekSet, nullptr). memorystream.cpp không thuộc target SDK → add source trực tiếp vào mỗi target dùng Vst3Instrument.cpp (bridge ĐÃ thêm, gui_probe ĐÃ thêm).
- tasklist CSV parse: dùng `subprocess.run(['tasklist'])` full output + `line.split()[1]`.
- Probe wait_log: log cùng job xuất hiện ~đồng thời → dùng chung `since` window, không reset.
- Probe PASS có thể dương tính giả → luôn kiểm tra bridge.log/spawn.log thực.
- Deploy: taskkill 3 process → chờ 2s → copy exe → start → chờ 5s → tasklist lấy PID mới → update APP_PID trong probe.
- `sleep` không tồn tại trong sh → `python -c "import time; time.sleep(N)"`.
- `timeout` Windows khác GNU → dùng python subprocess wrapper.
- CRLF: `s.count(old)` với `\n` thật trong heredoc `<<'EOF'` KHÔNG khớp CRLF; dùng script .py file thay vì `python -c` chuỗi dài (escaping lỗi).
- 0xC0000409 trong ucrtbase = fail-fast/terminate (không phải C++ exception thường bắt được bằng catch(...) nếu là SEH/fail-fast).
+9 -6
View File
@@ -1,14 +1,17 @@
// Rebuild app.precompiled.js from app.jsx using @babel/standalone (avoids the
// Babel 8 ESM-only CLI conflict). Mirrors package.json's build script:
// babel app/static/js/app.jsx --config-file ./babel.config.json -o app/static/js/app.precompiled.js
import * as Babel from '@babel/standalone';
import { readFileSync, writeFileSync } from 'fs';
// Cache-buster: argv[2] = 'yyyyMMddHHmm' (build_windows.ps1 truyen); mac dinh
// = thoi diem build. Stamp MOI URL asset (?v=) TRONG OUTPUT (khong sua source
// app.jsx) -> khong bao gio quen bump version sau khi rebuild bundle.
const ver = process.argv[2] || new Date().toISOString().replace(/\D/g, '').slice(0, 12);
const src = readFileSync('app/static/js/app.jsx', 'utf8');
const out = Babel.transform(src, {
const transformed = Babel.transform(src, {
presets: ['react'],
filename: 'app.jsx',
sourceType: 'script',
}).code;
writeFileSync('app/static/js/app.precompiled.js', out);
console.log('BUILD OK', out.length, 'bytes');
const out = transformed.replace(/(sf-fx-realtime\.js\?v=)\d+/g, '$1' + ver);
writeFileSync('app/static/js/app.precompiled.js', out.replace(/\r?\n/g, '\r\n')); // repo CRLF
console.log('BUILD OK', 'v=' + ver, out.length, 'bytes');
+22 -3
View File
@@ -79,7 +79,7 @@ cmake -S $NB_DIR -B $BUILD_DIR -G $VS_GEN -A $VS_ARCH `
-DCMAKE_BUILD_TYPE=Release `
-DVCPKG_TARGET_TRIPLET=x64-windows
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: cmake configure failed" -ForegroundColor Red; exit 1 }
cmake --build $BUILD_DIR --config Release --target daw_vst_bridge -- /m
cmake --build $BUILD_DIR --config Release --target daw_vst_bridge fx_vst_bridge plugin_host -- /m
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: cmake build failed" -ForegroundColor Red; exit 1 }
# 6. Copy sidecar to Tauri externalBin location
@@ -89,9 +89,17 @@ New-Item -ItemType Directory -Force $BIN_DIR | Out-Null
$EXE = Join-Path $BUILD_DIR "Release\daw_vst_bridge.exe"
if (-not (Test-Path $EXE)) { Write-Host "ERROR: $EXE not found" -ForegroundColor Red; exit 1 }
Copy-Item $EXE (Join-Path $BIN_DIR "daw_vst_bridge-x86_64-pc-windows-msvc.exe") -Force
# fx_vst_bridge: VST FX host sidecar (split — see CMakeLists.txt)
$FX_EXE = Join-Path $BUILD_DIR "Release\fx_vst_bridge.exe"
if (-not (Test-Path $FX_EXE)) { Write-Host "ERROR: $FX_EXE not found" -ForegroundColor Red; exit 1 }
Copy-Item $FX_EXE (Join-Path $BIN_DIR "fx_vst_bridge-x86_64-pc-windows-msvc.exe") -Force
# plugin_host: VST sandbox child (VST3 crash isolation + jBridge VST2 per-process instances)
$PH_EXE = Join-Path $BUILD_DIR "Release\plugin_host.exe"
if (-not (Test-Path $PH_EXE)) { Write-Host "ERROR: $PH_EXE not found" -ForegroundColor Red; exit 1 }
Copy-Item $PH_EXE (Join-Path $BIN_DIR "plugin_host-x86_64-pc-windows-msvc.exe") -Force
# 7. Copy fluidsynth runtime DLL next to sidecar (Tauri externalBin bundles only the exe)
Write-Host "== [7/7] Copy libfluidsynth-3.dll =="
# 7. Copy runtime DLLs next to sidecar (Tauri externalBin bundles only the exe)
Write-Host "== [7/7] Copy runtime DLLs (fluidsynth + jpeg) =="
$FS_DLL = Join-Path $BUILD_DIR "vcpkg_installed\x64-windows\bin\libfluidsynth-3.dll"
if (-not (Test-Path $FS_DLL)) { $FS_DLL = Join-Path $VCPKG_ROOT "installed\x64-windows\bin\libfluidsynth-3.dll" }
if (Test-Path $FS_DLL) {
@@ -101,4 +109,15 @@ if (Test-Path $FS_DLL) {
} else {
Write-Host "WARN: libfluidsynth-3.dll not found - bridge needs it at runtime" -ForegroundColor Yellow
}
# jpeg62.dll (VST3 hosting image decode; vcpkg libjpeg-turbo) - same candidates as fluidsynth
$JPEG_DLL = Join-Path $NB_DIR "vcpkg_installed\x64-windows\bin\jpeg62.dll"
if (-not (Test-Path $JPEG_DLL)) { $JPEG_DLL = Join-Path $BUILD_DIR "vcpkg_installed\x64-windows\bin\jpeg62.dll" }
if (-not (Test-Path $JPEG_DLL)) { $JPEG_DLL = Join-Path $VCPKG_ROOT "packages\libjpeg-turbo_x64-windows\bin\jpeg62.dll" }
if (Test-Path $JPEG_DLL) {
Copy-Item $JPEG_DLL (Join-Path $BIN_DIR "jpeg62.dll") -Force
Copy-Item $JPEG_DLL (Join-Path $BUILD_DIR "Release\jpeg62.dll") -Force
Write-Host " jpeg62.dll copied to src-tauri\binaries"
} else {
Write-Host "WARN: jpeg62.dll not found - VST3 GUIs may fail to show images" -ForegroundColor Yellow
}
Write-Host "== DONE: sidecar + DLL in src-tauri\binaries =="
+4 -4
View File
@@ -57,12 +57,12 @@ if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: bridge sidecar missing" -Foregroun
npm install -D @tauri-apps/cli
# Frontend host: copy template -> src-tauri/ui/index.html (giu Tauri context,
# document.write UI engine; neu thieu -> window mat __TAURI__ -> bridge chet).
Copy-Item buildscriptsfrontend_host_index.html src-tauriuiindex.html -Force
if (-not (Test-Path src-tauriuiindex.html)) { Write-Host "ERROR: frontend host missing" -ForegroundColor Red; exit 1 }
Copy-Item build\scripts\index.html src-tauri\ui\index.html -Force
if (-not (Test-Path src-tauri\ui\index.html)) { Write-Host "ERROR: frontend host missing" -ForegroundColor Red; exit 1 }
npx tauri build
Write-Host ""
Write-Host "== DONE =="
Write-Host " NSIS: src-tauri\target\release\bundle\nsis\SonicForgeDAW_1.0.0_x64-setup.exe"
Write-Host " MSI : src-tauri\target\release\bundle\msi\SonicForgeDAW_1.0.0_x64_en-US.msi"
Write-Host " NSIS: src-tauri\target\release\bundle\nsis\ScoreFXStudio_1.0.0_x64-setup.exe"
Write-Host " MSI : src-tauri\target\release\bundle\msi\ScoreFXStudio_1.0.0_x64_en-US.msi"
Write-Host " Verify after install: %APPDATA%\SonicForgeDAW\logs\spawn.log exists=True for daw_engine AND daw_vst_bridge"
+37 -6
View File
@@ -12,23 +12,32 @@
# (VS2019/2022 + vcpkg + vst3sdk submodule + sfizz-src). Chay lai
# build_windows.ps1 neu muon build lai bridge/app tu source.
# LUU Y: file nay chi dung ky tu ASCII (PowerShell 5.1).
$ErrorActionPreference = "Stop"
# "Stop" + npm/pip warn tren stderr -> NativeCommandError -> chet oai. Dung
# "Continue" + kiem tra $LASTEXITCODE thu cong sau tung lenh native.
$ErrorActionPreference = "Continue"
Set-Location $PSScriptRoot
Write-Host "== [1/4] Kill tien trinh cu =="
Get-Process -Name "daw_engine","sonicforge-daw","daw_vst_bridge" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Write-Host "== [1/4] Kill tien trinh cu (chi install/dist, khong dong production) =="
Get-Process -ErrorAction SilentlyContinue | Where-Object {
$_.Path -like "$PSScriptRoot\install\*" -or $_.Path -like "$PSScriptRoot\dist\daw_engine\*"
} | Stop-Process -Force -ErrorAction SilentlyContinue
Start-Sleep -Milliseconds 500
Write-Host "== [2/4] Frontend bundle (app.jsx -> app.precompiled.js) =="
npm install --no-audit --no-fund
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: npm install that bai (exit $LASTEXITCODE)" -ForegroundColor Red; exit 1 }
if (-not (Test-Path "node_modules\@babel\standalone")) {
npm install @babel/standalone --no-audit --no-fund
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: npm install @babel/standalone that bai (exit $LASTEXITCODE)" -ForegroundColor Red; exit 1 }
}
node build.mjs
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: node build.mjs that bai (exit $LASTEXITCODE)" -ForegroundColor Red; exit 1 }
Write-Host "== [3/4] Build daw_engine.exe (PyInstaller onedir) =="
python -m pip install -r requirements.txt pyinstaller pywin32
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: pip install that bai (exit $LASTEXITCODE)" -ForegroundColor Red; exit 1 }
pyinstaller engine.spec --clean --noconfirm
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: pyinstaller that bai (exit $LASTEXITCODE)" -ForegroundColor Red; exit 1 }
if (-not (Test-Path "dist\daw_engine\daw_engine.exe")) {
Write-Host "ERROR: dist\daw_engine\daw_engine.exe khong ton tai" -ForegroundColor Red
exit 1
@@ -39,12 +48,34 @@ if ($LASTEXITCODE -ne 0) {
exit 1
}
Write-Host "== [4/4] Copy vao install\ =="
Write-Host "== [4/4] Verify bundle sach (khong config dev) + copy vao install\ =="
# Ban cai phai reset ve mac dinh: KHONG duoc mang theo DB/password/secret/
# plugin-dirs/scan-state cua dev sang may nguoi dung moi.
$devFiles = Get-ChildItem "dist\daw_engine" -Recurse -File -ErrorAction SilentlyContinue | Where-Object {
$_.Name -in @(".secret_key", "plugin_dirs.json", "sf_scan_state.json", "sf_scan_state.json.bak-root") -or
$_.Extension -eq ".db"
}
if ($devFiles) {
Write-Host "ERROR: bundle chua config dev - DUNG TAO BAN CAI:" -ForegroundColor Red
$devFiles | ForEach-Object { Write-Host " $($_.FullName)" -ForegroundColor Red }
exit 1
}
New-Item -ItemType Directory -Force "install" | Out-Null
if (Test-Path "install\daw_engine") { Remove-Item -Recurse -Force "install\daw_engine" }
Copy-Item "dist\daw_engine\*" "install\daw_engine\" -Recurse -Force
# Copy-Item wildcard tu dist\daw_engine vao install\daw_engine tao layout PHANG
# (noi dung _internal tran ra root, exe khong thay _internal -> treo luc chay).
# Robocopy /MIR mirror dung cau truc onedir (exe + _internal) + xoa file thua.
robocopy "dist\daw_engine" "install\daw_engine" /MIR /NFL /NDL /NJH /NP /R:1 /W:1 | Out-Null
if ($LASTEXITCODE -ge 8) { Write-Host "ERROR: robocopy install that bai (exit $LASTEXITCODE)" -ForegroundColor Red; exit 1 }
if (-not (Test-Path "install\daw_engine\_internal")) { Write-Host "ERROR: install\daw_engine\_internal thieu - onedir layout sai" -ForegroundColor Red; exit 1 }
# 3 binary (bridge/lib/app) da commit san trong install/ - khong ghi de,
# neu user da build lai bridge/app thi tu copy vao day.
# VC++ runtime app-local: bridge C++ link /MD can MSVCP140/VCRUNTIME140 canh
# exe (khong phu thuoc vc_redist he thong). Dong bo tu src-tauri/binaries.
foreach ($dll in @("MSVCP140.dll", "MSVCP140_2.dll", "VCRUNTIME140.dll", "VCRUNTIME140_1.dll")) {
$src = "src-tauri\binaries\$dll"
if (Test-Path $src) { Copy-Item $src "install\$dll" -Force }
else { Write-Host "WARN: thieu $dll trong src-tauri\binaries - chay build_windows.ps1 -Bridge truoc" -ForegroundColor Yellow }
}
Write-Host ""
Write-Host "== DONE =="
+178 -79
View File
@@ -1,95 +1,194 @@
# build_windows.ps1 - ONE-COMMAND build: daw_engine.exe (PyInstaller) + Tauri v2 (NSIS + MSI)
# Chay tren Windows: powershell -ExecutionPolicy Bypass -File build_windows.ps1
# LUU Y: file nay chi dung ky tu ASCII (khong dau, khong em-dash) - PowerShell 5.1
# doc .ps1 khong BOM theo ANSI, ky tu Unicode bi hong -> "String is missing terminator".
$ErrorActionPreference = "Stop"
# build_windows.ps1 - ONE-COMMAND build: frontend + native bridge + engine + Tauri (NSIS + MSI)
#
# powershell -ExecutionPolicy Bypass -File build_windows.ps1 # FULL (tat ca)
# powershell -ExecutionPolicy Bypass -File build_windows.ps1 -Frontend # chi bundle JS + sync static
# powershell -ExecutionPolicy Bypass -File build_windows.ps1 -Bridge # chi build native bridge + sync binaries
# powershell -ExecutionPolicy Bypass -File build_windows.ps1 -Engine # chi build engine + sync resources
# powershell -ExecutionPolicy Bypass -File build_windows.ps1 -Tauri # chi build installer Tauri
# ... -SkipPip -SkipNpm # bo qua cai dependency
#
# Moi mode TU DONG copy output vao dung vi tri (dist/, src-tauri/resources/,
# src-tauri/binaries/) va TU DONG bump cache-buster (?v=) -> khong the miss
# buoc copy nao du build theo cach nao. Bridge chi rebuild khi source moi hon.
# LUU Y: file nay chi dung ky tu ASCII (khong dau) - PowerShell 5.1 doc an toan.
param(
[switch]$Frontend,
[switch]$Bridge,
[switch]$Engine,
[switch]$Tauri,
[switch]$SkipPip,
[switch]$SkipNpm
)
# "Stop" + npm/pip warn tren stderr -> NativeCommandError -> chet oai (da gap o
# build_install.ps1). Dung "Continue" + kiem tra $LASTEXITCODE sau tung lenh native.
$ErrorActionPreference = "Continue"
Set-Location $PSScriptRoot
# Kill moi daw_engine con song tu build/truoc (windowed, khong console ->
# de quen -> file exe dang chay bi khoa -> Tauri build loi PermissionDenied
# khi doc externalBin). Stop-Process im lang neu khong co tien trinh nao.
Write-Host "== [0/6] Kill daw_engine.exe cu (neu dang chay) =="
Get-Process -Name "daw_engine" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Start-Sleep -Milliseconds 500
$any = $Frontend -or $Bridge -or $Engine -or $Tauri
$doFrontend = $Frontend -or $Engine -or (-not $any) # Engine can bundle frontend moi truoc
$doBridge = $Bridge -or (-not $any)
$doEngine = $Engine -or (-not $any)
$doTauri = $Tauri -or (-not $any)
Write-Host "== [1/6] Python dependencies =="
python -m pip install --upgrade pip
python -m pip install -r requirements.txt pyinstaller pywin32
$Stamp = Get-Date -Format "yyyyMMddHHmm"
Write-Host "== [2/6] Frontend bundle (app.jsx -> app.precompiled.js) =="
npm install
# Dam bao @babel/standalone co (build.mjs import truc tiep - da gap
# ERR_MODULE_NOT_FOUND tren may Windows khi package.json cu thieu dep).
if (-not (Test-Path "node_modules\@babel\standalone")) {
Write-Host "Thieu @babel/standalone - dang cai them..."
npm install @babel/standalone --no-audit --no-fund
}
if (-not (Test-Path "node_modules\@babel\standalone")) {
Write-Host "ERROR: Khong cai duoc @babel/standalone. Kiem tra ket noi npm!" -ForegroundColor Red
exit 1
}
node build.mjs # @babel/standalone; thay cho 'npm run build' (Babel 8 ESM-only CLI conflict)
function Fail([string]$msg) { Write-Host "ERROR: $msg" -ForegroundColor Red; exit 1 }
Write-Host "== [3/6] Build daw_engine.exe (PyInstaller) =="
pyinstaller engine.spec --clean --noconfirm
Write-Host "== [3.5/6] Verify bundle contents (app/static, app/templates phai co) =="
# Bat loi bundle NGAY tai build (da gap 2 lan: chay pyinstaller tu noi khac
# -> static/templates thieu -> exe crash 'Directory ...\app\static does not exist').
# Dung tools/verify_bundle.py (parse TOC bang ast, chap nhan ca / va \) thay
# vi regex thong thuong (TOC Windows co the dung backslash -> false positive).
# Truoc tien kiem tra engine.spec phai la ban moi (collect_data_files).
$specContent = Get-Content "engine.spec" -Raw -ErrorAction SilentlyContinue
if ($specContent -notmatch "collect_data_files\('app'") {
Write-Host "ERROR: engine.spec CU (thieu collect_data_files('app')). Pull code moi truoc khi build!" -ForegroundColor Red
exit 1
}
python tools\verify_bundle.py
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: Bundle thieu asset - dung build, kiem tra engine.spec datas!" -ForegroundColor Red
exit 1
if ($doEngine -or $doTauri) {
Write-Host "== [0] Kill engine cu (chi install/dist/resources, khong dong production) =="
Get-Process -ErrorAction SilentlyContinue | Where-Object {
$_.Path -like "$PSScriptRoot\install\*" -or $_.Path -like "$PSScriptRoot\dist\daw_engine\*" -or $_.Path -like "$PSScriptRoot\src-tauri\resources\daw_engine\*"
} | Stop-Process -Force -ErrorAction SilentlyContinue
Start-Sleep -Milliseconds 500
}
Write-Host "== [4/6] ONEDIR engine -> src-tauri/resources/daw_engine (Tauri resources) =="
# ONEDIR: copy ca thu muc dist\daw_engine\ (exe + _internal) vao resources.
# Tauri bundle resources -> resource_dir()/daw_engine/daw_engine.exe luc runtime.
if (-not (Test-Path "dist\daw_engine\daw_engine.exe")) {
Write-Host "ERROR: dist\daw_engine\daw_engine.exe khong ton tai (onedir build loi?)" -ForegroundColor Red
exit 1
}
if (Test-Path "src-tauri\resources\daw_engine") {
Remove-Item -Recurse -Force "src-tauri\resources\daw_engine"
}
New-Item -ItemType Directory -Force "src-tauri\resources\daw_engine" | Out-Null
Copy-Item "dist\daw_engine\*" "src-tauri\resources\daw_engine\" -Recurse -Force
Write-Host "Copied onedir engine -> src-tauri\resources\daw_engine"
# ---- [1] Frontend bundle + sync static -------------------------------------
if ($doFrontend) {
Write-Host "== [1] Frontend bundle (app.jsx -> app.precompiled.js, cache-buster v=$Stamp) =="
if (-not $SkipNpm) { npm install; if ($LASTEXITCODE -ne 0) { Fail "npm install that bai" } }
if (-not (Test-Path "node_modules\@babel\standalone")) {
Write-Host "Thieu @babel/standalone - dang cai them..."
npm install @babel/standalone --no-audit --no-fund
if ($LASTEXITCODE -ne 0) { Fail "npm install @babel/standalone that bai" }
}
if (-not (Test-Path "node_modules\@babel\standalone")) { Fail "Khong cai duoc @babel/standalone. Kiem tra ket noi npm!" }
node build.mjs $Stamp
if ($LASTEXITCODE -ne 0) { Fail "build.mjs that bai" }
Write-Host "== [5/6] VC++ Redistributable cho hooks.nsh =="
if (-not (Test-Path src-tauri\vc_redist.x64.exe)) {
Invoke-WebRequest -Uri "https://aka.ms/vs/17/release/vc_redist.x64.exe" -OutFile src-tauri\vc_redist.x64.exe
# Stamp cache-buster vao index.html source (no-cache HTML, bundle ?v= phai moi)
$idx = "app\templates\index.html"
$utf8 = New-Object System.Text.UTF8Encoding($false)
$html = [IO.File]::ReadAllText($idx, $utf8)
[IO.File]::WriteAllText($idx, [regex]::Replace($html, "app\.precompiled\.js\?v=\d+", "app.precompiled.js?v=$Stamp"), $utf8)
# Sync 3 file static -> dist + src-tauri/resources (ban packaged engine doc tu day)
$pairs = @(
@("static\js\app.precompiled.js", "_internal\app\static\js\app.precompiled.js"),
@("static\js\sf-fx-realtime.js", "_internal\app\static\js\sf-fx-realtime.js"),
@("templates\index.html", "_internal\app\templates\index.html")
)
$roots = @("dist\daw_engine", "src-tauri\resources\daw_engine")
foreach ($p in $pairs) {
$src = Join-Path "app" $p[0]
foreach ($r in $roots) {
$dst = Join-Path $r $p[1]
New-Item -ItemType Directory -Force (Split-Path $dst) | Out-Null
Copy-Item $src $dst -Force
}
}
# Verify hash source == copy (bat loi "copy sai / thieu")
foreach ($p in $pairs) {
$srcHash = (Get-FileHash (Join-Path "app" $p[0]) -Algorithm SHA256).Hash
foreach ($r in $roots) {
$dst = Join-Path $r $p[1]
if (-not (Test-Path $dst)) { Fail "Thieu file sau sync: $dst" }
if ((Get-FileHash $dst -Algorithm SHA256).Hash -ne $srcHash) { Fail "Hash khac nhau sau sync: $dst" }
}
}
Write-Host "Sync static OK (dist + src-tauri/resources)"
}
Write-Host "== [6/6] Tauri build (NSIS .exe + MSI) =="
# Guard: resources/daw_engine PHAI co exe + _internal truoc khi tauri build
# (glob trong tauri.conf.json fail ngay "path not found" neu thieu).
if (-not (Test-Path "src-tauri\resources\daw_engine\daw_engine.exe")) {
Write-Host "ERROR: src-tauri\resources\daw_engine\daw_engine.exe khong co - buoc [4/6] that bai?" -ForegroundColor Red
exit 1
# ---- [2] Native bridge (MSBuild Release x64) --------------------------------
if ($doBridge) {
Write-Host "== [2] Native bridge (MSBuild Release x64 -> src-tauri/binaries) =="
$msbuildCandidates = @(
"${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\amd64\MSBuild.exe",
"${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\amd64\MSBuild.exe",
"${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\amd64\MSBuild.exe",
"${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\amd64\MSBuild.exe"
)
$msbuild = $msbuildCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $msbuild) { Fail "Khong tim thay MSBuild.exe. Cai VS Build Tools 2019/2022 (workload C++) va chay CMake native_bridge truoc." }
foreach ($name in @("daw_vst_bridge", "fx_vst_bridge", "plugin_host")) {
$proj = "native_bridge\build\bridge-win\$name.vcxproj"
$exe = "native_bridge\build\bridge-win\Release\$name.exe"
if (-not (Test-Path $proj)) { Fail "Thieu project: $proj (can CMake native_bridge truoc - xem native_bridge/build/bridge-win)" }
$newest = Get-ChildItem -Path "native_bridge\src","native_bridge\include" -Recurse -Include *.cpp,*.h,*.hpp -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
$needBuild = $true
if ((Test-Path $exe) -and $newest) { $needBuild = $newest.LastWriteTime -gt (Get-Item $exe).LastWriteTime }
if ($needBuild) {
Write-Host "Build $name ..."
& $msbuild $proj "/p:Configuration=Release" "/p:Platform=x64" "/m" "/nologo" "/v:m"
if ($LASTEXITCODE -ne 0) { Fail "MSBuild $name that bai" }
} else {
Write-Host "Skip $name (exe moi nhat)"
}
if (-not (Test-Path $exe)) { Fail "Thieu output: $exe" }
Copy-Item $exe "src-tauri\binaries\$name.exe" -Force
Copy-Item $exe "src-tauri\binaries\$name-x86_64-pc-windows-msvc.exe" -Force
}
foreach ($dll in @("libfluidsynth-3.dll", "jpeg62.dll")) {
if (Test-Path "native_bridge\build\Release\$dll") {
Copy-Item "native_bridge\build\Release\$dll" "src-tauri\binaries\$dll" -Force
}
}
# VC++ runtime app-local (MSVCP140/VCRUNTIME140) — bridge C++ link /MD nen
# can cac dll nay CANH exe, khong phu thuoc vc_redist he thong. Lay tu
# System32 (ban redist da cai) de bundle vao installer/portable.
foreach ($dll in @("MSVCP140.dll", "MSVCP140_2.dll", "VCRUNTIME140.dll", "VCRUNTIME140_1.dll")) {
$sys = Join-Path $env:WINDIR "System32\$dll"
if (Test-Path $sys) { Copy-Item $sys "src-tauri\binaries\$dll" -Force }
else { Write-Host "WARN: thieu $dll trong System32 (VC++ redist chua cai?)" -ForegroundColor Yellow }
}
Write-Host "Bridge OK -> src-tauri/binaries"
}
if (-not (Test-Path "src-tauri\resources\daw_engine\_internal")) {
Write-Host "ERROR: thieu src-tauri\resources\daw_engine\_internal (onedir khong day du)" -ForegroundColor Red
exit 1
# ---- [3] Engine (PyInstaller) + sync resources ------------------------------
if ($doEngine) {
Write-Host "== [3] Engine (PyInstaller onedir -> src-tauri/resources) =="
$specContent = Get-Content "engine.spec" -Raw -ErrorAction SilentlyContinue
if ($specContent -notmatch "collect_data_files\('app'") { Fail "engine.spec CU (thieu collect_data_files('app')). Pull code moi truoc khi build!" }
if (-not $SkipPip) {
python -m pip install --upgrade pip
python -m pip install -r requirements.txt pyinstaller pywin32
}
if (Test-Path "dist\daw_engine") { Remove-Item -Recurse -Force "dist\daw_engine" }
pyinstaller engine.spec --clean --noconfirm
if ($LASTEXITCODE -ne 0) { Fail "pyinstaller that bai" }
python tools\verify_bundle.py
if ($LASTEXITCODE -ne 0) { Fail "Bundle thieu asset - dung build, kiem tra engine.spec datas" }
# Ban cai phai SACH: khong duoc mang theo DB/password/secret/scan-state cua dev.
$devFiles = Get-ChildItem "dist\daw_engine" -Recurse -File -ErrorAction SilentlyContinue | Where-Object {
$_.Name -in @(".secret_key", "plugin_dirs.json", "sf_scan_state.json", "sf_scan_state.json.bak-root") -or
$_.Extension -eq ".db"
}
if ($devFiles) {
$devFiles | ForEach-Object { Write-Host " DEV-CONFIG: $($_.FullName)" -ForegroundColor Red }
Fail "Bundle chua config dev - DUNG TAO BAN CAI"
}
if (-not (Test-Path "dist\daw_engine\daw_engine.exe")) { Fail "dist\daw_engine\daw_engine.exe khong ton tai (onedir build loi?)" }
# Robocopy /MIR (khong Copy-Item wildcard): giu dung cau truc onedir exe+_internal.
robocopy "dist\daw_engine" "src-tauri\resources\daw_engine" /MIR /NFL /NDL /NJH /NP /R:1 /W:1 | Out-Null
if ($LASTEXITCODE -ge 8) { Fail "robocopy resources that bai (exit $LASTEXITCODE)" }
if (-not (Test-Path "src-tauri\resources\daw_engine\_internal")) { Fail "resources/daw_engine/_internal thieu - onedir layout sai" }
Write-Host "Engine OK -> src-tauri/resources/daw_engine"
}
# ---- [4] Tauri installer -----------------------------------------------------
if ($doTauri) {
Write-Host "== [4] VC++ Redistributable (cho hooks.nsh) =="
if (-not (Test-Path src-tauri\vc_redist.x64.exe)) {
Invoke-WebRequest -Uri "https://aka.ms/vs/17/release/vc_redist.x64.exe" -OutFile src-tauri\vc_redist.x64.exe
}
Write-Host "== [5] Tauri build (NSIS + MSI) =="
if (-not (Test-Path "src-tauri\resources\daw_engine\daw_engine.exe")) { Fail "resources/daw_engine thieu - chay -Engine hoac full truoc" }
if (-not (Test-Path "src-tauri\resources\daw_engine\_internal")) { Fail "resources/daw_engine/_internal thieu (onedir khong day du)" }
foreach ($name in @("daw_vst_bridge", "fx_vst_bridge", "plugin_host")) {
if (-not (Test-Path "src-tauri\binaries\$name-x86_64-pc-windows-msvc.exe")) { Fail "Thieu src-tauri\binaries\$name-x86_64-pc-windows-msvc.exe - chay -Bridge truoc" }
}
if (-not $SkipNpm) { npm install -D @tauri-apps/cli; if ($LASTEXITCODE -ne 0) { Fail "npm install @tauri-apps/cli that bai" } }
npx tauri build
if ($LASTEXITCODE -ne 0) { Fail "tauri build that bai" }
}
npm install -D @tauri-apps/cli
npx tauri build
Write-Host ""
Write-Host "== DONE =="
Write-Host " NSIS: src-tauri\target\release\bundle\nsis\SonicForgeDAW_1.0.0_x64-setup.exe"
Write-Host " MSI : src-tauri\target\release\bundle\msi\SonicForgeDAW_1.0.0_x64_en-US.msi"
Write-Host " NSIS: src-tauri\target\release\bundle\nsis\ScoreFXStudio_1.0.0_x64-setup.exe"
Write-Host " MSI : src-tauri\target\release\bundle\msi\ScoreFXStudio_1.0.0_x64_en-US.msi"
Write-Host ""
Write-Host "== XAC MINH SAU KHI CAI DAT (quan trong) =="
Write-Host " Mo %APPDATA%\SonicForgeDAW\logs\spawn.log - phai thay:"
Write-Host " [resource_dir/daw_engine (map layout)] ...exists=True"
Write-Host " Neu exists=False: bundle resources KHONG vao installer (chay lai buoc [4/6])."
Write-Host " Engine phai nam o: <thu muc cai dat>\daw_engine\daw_engine.exe"
Write-Host "== XAC MINH SAU KHI CAI DAT =="
Write-Host " Mo %APPDATA%\SonicForgeDAW\logs\spawn.log - phai thay [resource_dir/daw_engine (map layout)] ...exists=True"
+68
View File
@@ -0,0 +1,68 @@
# deploy_engine_windows.ps1 - REBUILD + DEPLOY daw_engine.exe (PyInstaller onedir)
# Chay TREN may Windows 192.168.1.24 (source clone tai C:\Users\locpham\SonicForgeStudio):
# powershell -ExecutionPolicy Bypass -File deploy_engine_windows.ps1
# Lam gi: git pull code moi -> build.mjs (frontend) -> pyinstaller engine.spec
# (backend fix vao exe) -> detect noi engine dang chay -> copy bundle moi vao do
# -> kill + restart engine. LUU Y: chi dung ky tu ASCII (PowerShell 5.1 doc ANSI).
$ErrorActionPreference = "Stop"
Set-Location $PSScriptRoot
Write-Host "== [0/6] Pull code moi (fix MASTERING PANEL VST FX) =="
git pull --ff-only
if ($LASTEXITCODE -ne 0) { Write-Host "git pull LOI - kiem tra network/credential" -ForegroundColor Red; exit 1 }
Write-Host "== [1/6] Frontend bundle (app.jsx -> app.precompiled.js) =="
if (-not (Test-Path "node_modules\@babel\standalone")) {
npm install @babel/standalone --no-audit --no-fund
}
node build.mjs
if ($LASTEXITCODE -ne 0) { Write-Host "build.mjs LOI" -ForegroundColor Red; exit 1 }
Write-Host "== [2/6] Rebuild daw_engine.exe (PyInstaller, backend fix vao PYZ) =="
pyinstaller engine.spec --clean --noconfirm
if ($LASTEXITCODE -ne 0) { Write-Host "pyinstaller LOI" -ForegroundColor Red; exit 1 }
Write-Host "== [3/6] Verify bundle =="
python tools\verify_bundle.py
if ($LASTEXITCODE -ne 0) { Write-Host "Bundle thieu asset - dung deploy" -ForegroundColor Red; exit 1 }
Write-Host "== [4/6] Detect noi engine dang chay =="
$proc = Get-Process -Name "daw_engine" -ErrorAction SilentlyContinue | Select-Object -First 1
$runDir = $null
if ($proc) {
$runDir = Split-Path $proc.Path -Parent
Write-Host "Engine dang chay tai: $runDir (PID $($proc.Id))"
} else {
$cand = @(
"C:\Users\locpham\SonicForgeStudio\install\daw_engine",
"C:\Users\locpham\SonicForgeStudio\dist\daw_engine"
)
foreach ($c in $cand) {
if (Test-Path (Join-Path $c "daw_engine.exe")) { $runDir = $c; break }
}
if (-not $runDir) { Write-Host "Khong tim thay noi chua daw_engine.exe" -ForegroundColor Red; exit 1 }
Write-Host "Engine chua chay - se deploy vao: $runDir"
}
Write-Host "== [5/6] Kill engine cu + copy bundle moi =="
if ($proc) {
Stop-Process -Name "daw_engine" -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
}
Copy-Item "dist\daw_engine\*" $runDir -Recurse -Force
Write-Host "Da copy bundle moi vao $runDir"
Write-Host "== [6/6] Restart engine =="
$exe = Join-Path $runDir "daw_engine.exe"
Start-Process $exe -WorkingDirectory $runDir
Start-Sleep -Seconds 6
Write-Host ""
Write-Host "== XAC MINH =="
try {
$r = Invoke-WebRequest -Uri "http://localhost:8000/" -UseBasicParsing -TimeoutSec 10
Write-Host "HTTP $($r.StatusCode) - engine da chay lai OK" -ForegroundColor Green
} catch {
Write-Host "Chua thay engine tren port 8000 (co the dang chay port 8001-8010 hoac Tauri shell chua spawn)" -ForegroundColor Yellow
}
Write-Host "Hard refresh trinh duyet (Ctrl+Shift+R) -> Plugin Manager -> Scan -> MASTERING PANEL -> VST FX phai hien thi."
+38 -5
View File
@@ -1,4 +1,4 @@
"""SonicForge DAW — Desktop engine launcher (PyInstaller entry point).
"""ScoreFX Studio — Desktop engine launcher (PyInstaller entry point).
Chay FastAPI backend duoi dang tien trinh nen (sidecar) cho ban cai Standalone
Windows (Tauri v2 shell). Tu chon port 8000-8010 (fallback neu bi chiem), tu
@@ -61,12 +61,21 @@ def _parent_alive(pid):
# pid khong con ton tai.
if os.name == "nt":
import ctypes
# OpenProcess chi bao NULL khi process object da bi reap (moi handle
# dong het). Process vua bi kill (taskkill/crash) con "zombie" mot
# thoi gian -> OpenProcess van thanh cong -> watchdog tuong cha con
# song -> engine khong bao gio tu thoat. WaitForSingleObject tren
# handle co PROCESS_SYNCHRONIZE phan biet dung: object da terminate
# thi SIGNALED ngay, du handle con ton tai.
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
h = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
PROCESS_SYNCHRONIZE = 0x00100000
h = ctypes.windll.kernel32.OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE, False, pid)
if not h:
return False # tien trinh da chet
return False # process object da mat -> cha da chet
terminated = ctypes.windll.kernel32.WaitForSingleObject(h, 0) == 0
ctypes.windll.kernel32.CloseHandle(h)
return True
return not terminated
try:
os.kill(pid, 0)
return True
@@ -107,6 +116,19 @@ def main():
port = _pick_port()
os.environ["SF_PORT"] = str(port)
# Announce port to Tauri shell: UI bootstrap đọc %APPDATA%/SonicForgeDAW/
# engine_port.txt (qua lệnh get_engine_port) để kết nối ĐÚNG engine của
# chính nó thay vì quét port — quét 8000-8010 có thể dính engine của
# instance khác (2 app chạy cùng lúc) → UI mất server khi instance đó tắt.
# Format "port\nengine_exe_path": Tauri đối chiếu path engine với candidate
# của CHÍNH nó → 2 app chạy song song mỗi cái lấy đúng port của mình.
try:
port_file = os.path.join(os.path.dirname(log_dir), "engine_port.txt")
with open(port_file, "w", encoding="utf-8") as f:
f.write("%d\n%s" % (port, sys.executable))
except OSError:
pass
# Log ra file — khi dong goi console=False, stdout khong nhin thay duoc.
try:
logging.basicConfig(
@@ -140,11 +162,22 @@ def main():
logging.getLogger("desktop_engine").info(
"Parent process exited — shutting down engine")
server.should_exit = True
# Graceful shutdown co the TREO vo han: uvicorn
# _wait_tasks_to_complete cho open connection (client LAN
# dang mo browser) va timeout_graceful_shutdown=None.
# => engine song dai khi app chet dot ngot (crash/Task
# Manager). Hard-exit fallback sau 5s dam bao thoat het.
def _force_exit():
time.sleep(5.0)
logging.getLogger("desktop_engine").info(
"Graceful shutdown hung — forcing exit")
os._exit(0)
threading.Thread(target=_force_exit, daemon=True).start()
return
threading.Thread(target=_watchdog, daemon=True).start()
logger = logging.getLogger("desktop_engine")
logger.info("SonicForge engine listening on 0.0.0.0:%d", port)
logger.info("ScoreFX engine listening on 0.0.0.0:%d", port)
lan_ips = _lan_ips()
if lan_ips:
logger.info("LAN access — mo browser o may khac: %s",
+47 -7
View File
@@ -5,14 +5,42 @@
import os
from PyInstaller.utils.hooks import collect_dynamic_libs, collect_data_files
# Native DLL cho pedalboard va soundfile (Windows .pyd/.dll, Linux .so)
binaries = collect_dynamic_libs('pedalboard')
binaries += collect_dynamic_libs('soundfile')
# Native DLL cho soundfile (Windows .pyd/.dll, Linux .so)
binaries = collect_dynamic_libs('soundfile')
# Root tuyet doi cua thu muc chua spec — dung cho cac datas KHONG nam trong
# package 'app' (vd thu muc md/). Cac assets cua app (static/templates/models)
# duoc bundle qua collect_data_files('app').
_SPEC_ROOT = os.path.abspath(SPECPATH)
# Native bridge cho VST FX (fx_vst_bridge.exe + DLL + plugin_host) — bundle vao
# root (dest '.') de find_bridge_exe (frozen: exe_dir = thu muc chua
# sys.executable = dist/daw_engine) tu dong tim thay. Chi build tren Windows.
if os.name == 'nt':
# 2 nguon build LECH NHAU (build_windows.ps1 [2]): exe tu MSBuild
# bridge-win\Release, DLL tu CMake build\Release. Truoc day engine.spec chi
# nhin build\Release -> bundle chua fx_vst_bridge.exe CU/HONG (282KB,
# --scan crash c0000409 ngay ca voi dir khong ton tai). Lay tung loai tu
# dung nguon, co fallback ve build\Release cho checkout cu.
_exe_dir = os.path.join(_SPEC_ROOT, 'native_bridge', 'build', 'bridge-win', 'Release')
_dll_dir = os.path.join(_SPEC_ROOT, 'native_bridge', 'build', 'Release')
if not os.path.isdir(_exe_dir):
print("WARN: khong thay bridge-win/Release - fallback ve build/Release (bridge co the cu)")
_exe_dir = _dll_dir
for _b in ('daw_vst_bridge.exe', 'fx_vst_bridge.exe', 'plugin_host.exe'):
_bp = os.path.join(_exe_dir, _b)
if os.path.exists(_bp):
binaries.append((_bp, '.'))
for _b in ('jpeg62.dll', 'libfluidsynth-3.dll'):
_bp = os.path.join(_dll_dir, _b)
if os.path.exists(_bp):
binaries.append((_bp, '.'))
# G3: JUCE FX bridge (graph latency + processed samples) - bundle rieng vi
# build o native_bridge/build_juce/Release (khac dir build bridge cu).
_juce_dir = os.path.join(_SPEC_ROOT, 'native_bridge', 'build_juce', 'Release')
_jp = os.path.join(_juce_dir, 'juce_fx_bridge.exe')
if os.path.exists(_jp):
binaries.append((_jp, '.'))
# ⚠️ GOC ROOT CUA MOI LOI 'app\\static does not exist' (gap 3 lan):
# lenh `pyinstaller engine.spec` (entry-point script) KHONG them CWD vao
@@ -27,7 +55,13 @@ if _SPEC_ROOT not in _sys.path:
# Assets cua app: bundle QUA IMPORT SYSTEM (collect_data_files) — an toan nhat.
# Loai tru storage (57MB soundfonts/uploads — vo ich, config.py da chuyen
# storage sang %APPDATA%\\SonicForgeDAW khi frozen) va __pycache__.
datas = collect_data_files('app', excludes=['**/storage/**', '**/__pycache__/**', '**/*.pyc'])
# LOAI CA DEV CONFIG O app/ ROOT: bundle cai dat phai SAC — khong duoc mang
# theo DB/password/secret/scan-state cua dev sang may nguoi dung moi.
datas = collect_data_files('app', excludes=[
'**/storage/**', '**/__pycache__/**', '**/*.pyc',
'**/*.db', '**/.secret_key', '**/plugin_dirs.json', '**/sf_scan_state.json',
'**/*.bak-root',
])
# Fallback cuoi cung: neu collect_data_files van tra ve rong (phong moi truong
# hop ky la), dung datas TINH absolute — tinh huong xau nhat van co du assets.
if not datas:
@@ -89,9 +123,15 @@ _hidden = [
'uvicorn.protocols.http.auto',
'uvicorn.protocols.websockets',
'uvicorn.protocols.websockets.auto',
'pedalboard',
'websockets', # uvicorn WS protocol runtime dep — /ws/fx-realtime/{id}
'websockets.legacy',
'websockets.legacy.client',
# pedalboard (GPL-3.0) DA XOA — native_bridge thay the (xem PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md)
'soundfile',
'sf2utils',
# pyfluidsynth (fluidsynth.py cffi) - render soundfont native cho
# /soundfont-render (browser client). Can libfluidsynth-3.dll ke exe.
'fluidsynth',
'mido.backends.rtmidi',
] + _scipy_signal_hidden
@@ -143,8 +183,8 @@ exe = EXE(
upx_exclude=[],
runtime_tmpdir=None,
console=False, # True khi debug (xem log truc tiep), False cho production
# Icon exe = favicon cua app (app/templates/favicon.svg -> render PNG -> ICO,
# sinh boi tools/gen_favicon_ico.py). Cung nguon voi icon hien thi trong app.
# Icon exe = favicon cua app (scorefx_favicon.png -> src-tauri/icons/favicon.ico,
# sinh bang PIL tu tools/gen_icons.py). Cung nguon voi icon hien thi trong app.
icon='src-tauri/icons/favicon.ico',
)
+98
View File
@@ -0,0 +1,98 @@
# TECHNICAL SPECIFICATION: AUDIO FX PROCESSING PIPELINE
This document describes the Digital Signal Processing (DSP) architecture within the Audio Engine, tracing the complete path from raw sound sources (MIDI, Audio Clips) through insert FX chains to hardware outputs and offline rendering.
---
## 1. SIGNAL FLOW TOPOLOGY
Audio processing executes sequentially (or via a Directed Acyclic Graph - DAG) in fixed-size sample buffers (e.g., 512 samples/block).
```text
[ SOUND SOURCES ON TRACK ]
├── MIDI Item --> [ VSTi / Soundfont ] --(Rendered Audio)--> Audio Buffer
└── Audio Clip -------------------------------------------> Audio Buffer
│
▼
[ TRACK FX CHAIN ] <---------------- (Managed via Modal UI) │
├── Slot 1: Custom FX (In-house DSP) │
├── Slot 2: VST FX (e.g., FabFilter, Waves) │
└── Slot n: VST FX (e.g., iZotope Ozone) │
│
[ AUDIO MIXER / SUMMING BUS ] <-----------------------------------+ (Summed from all tracks)
│ (Linear Signal Summing)
▼
[ MASTER FX CHAIN ]
├── Slot 1: VST EQ / Compressor (Mastering)
└── Slot n: VST Limiter (Peak Limiting / Output Optimization)
│
▼
[ MAIN OUT (FINAL DISTRIBUTION) ]
├── 1. Realtime Preview (Direct Hardware Output via ASIO/WASAPI/WebAudio)
└── 2. Offline Export (Rendered to .WAV, .MP3, .OGG)
```
---
## 2. COMPONENT DEEP DIVE
### Step 1: Sound Sources
Every track hosts source items (Clips/Regions). Prior to entering the FX Chain, source data is converted into a normalized 32-bit or 64-bit floating-point Audio Buffer ($[-1.0, +1.0]$).
* **Audio Clip:** Raw audio data (WAV, MP3) is read from disk or memory, decoded, and buffered directly to the track's audio buffer.
* **MIDI Item:** MIDI events (`Note On`, `Note Off`, `Pitch Bend`) are read in real time and routed to a Virtual Instrument (VSTi or SoundFont player). The synthesizer processes these events, renders audio samples, and writes them directly to the track buffer.
---
### Step 2: Track FX Chain
Performs isolated signal processing on individual tracks.
* **UI Layer:** Exposed via a dedicated FX Rack Modal on each track. Users can insert, remove, bypass, and reorder processing slots.
* **Engine Architecture:** Supports two parallel plugin formats:
* **Custom FX:** Built-in DSP algorithms compiled into the core engine (C++, WebAssembly, or DSP Scripting).
* **3rd-Party VST/VST3 Plugins:** External commercial processors (e.g., FabFilter, Waves, iZotope). Communication occurs via the VST SDK or a dedicated process bridge using Shared Memory (SHM).
* **Serial Processing Pipeline:** Track audio buffers pass sequentially from Slot 1 through Slot $n$. Each slot modifies the memory buffer in place before passing it to the subsequent plugin:
$$Buffer_{\text{out}} = Plugin_n(Plugin_{n-1}(...Plugin_1(Buffer_{\text{in}})...))$$
---
### Step 3: Summing & Master FX Chain
Once tracks complete their individual Track FX processing, their outputs route to the main mixer:
* **Signal Summing:** Audio buffers from Track 1 through Track $N$ are summed linearly—applying track volume gain scaling and constant-power pan laws—into a unified stereo Master Audio Buffer:
$$Buffer_{\text{Master}}(t) = \sum_{i=1}^{N} Gain_i \cdot Pan_i(Buffer_i(t))$$
* **Master FX Chain:** The summed Master Buffer passes through global insert slots. Operates identical to the Track FX Chain, targeting master bus processing (e.g., Master Bus EQ, Multiband Compression, and Brickwall Limiting) to ensure target loudness compliance without digital clipping.
---
### Step 4: Main Output & Final Routing
Exiting the Master FX Chain, the engine directs the stereo buffer based on execution mode:
* **Realtime Preview Mode:**
* Audio buffers stream in fixed blocks (e.g., 256/512 samples) to low-latency driver APIs (ASIO, WASAPI, CoreAudio, or WebAudio).
* Enables zero-latency parameter updates as users adjust UI controls.
* **Offline Export Mode (Bouncing):**
* Bypasses realtime driver clocking to process buffers at maximum CPU speed.
* Writes output buffers sequentially into audio encoders using libraries such as `libsndfile` (WAV), `LAME` (MP3), or `libvorbis` (OGG).
---
## 3. STATE & LIFECYCLE MANAGEMENT
* **Thread Safety (Zero-Allocation Audio Thread):** Plugin parameters and structural chain updates originate on the UI Thread. The real-time Audio Thread communicates via lock-free SPSC queues or spinlocks to avoid audio dropouts (glitches) caused by thread contention or heap allocations (`malloc`).
* **State Persistence:** Parameters across all Custom FX and VST FX are serialized into JSON or Base64 binary chunks and saved within the main DAW project file.
* **Plugin Delay Compensation (PDC):** Lookahead processors and complex plugins introduce execution latency. The engine queries each plugin's latency (`getLatencySamples()`), calculates the cumulative delay along each track path, and delays shorter paths accordingly so all tracks sum in sample-accurate phase alignment.
+166
View File
@@ -0,0 +1,166 @@
# fxrt_probe.py — đo bridge realtime với VST thật (tạm, chẩn đoán crackle).
# Chạy: cd repo && python fxrt_probe.py [--secs 20]
# - tạo session realtime (bắt stderr bridge ra fxrt_bridge_<tag>.log)
# - producer: publish batch 4 block mỗi 21.333ms (timeBeginPeriod(1))
# - consumer: drain output, ghi thời điểm từng block tới
# - báo cáo: engine drops, bridge perf, output gap
import ctypes
import os
import sys
import threading
import time
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from app.core import fx_realtime # noqa: E402
SR = 48000
BLOCK = 256
BATCH = 4
BATCH_PERIOD = BATCH * BLOCK / SR # 21.333ms
BLOCK_PERIOD = BLOCK / SR # 5.333ms
IZ = r"C:\Program Files\Common Files\VST3\iZotope"
CHAIN_OZONE_MASTER = [
{"type": "vst3", "path": IZ + r"\Ozone 11 Equalizer.vst3", "active": True},
{"type": "vst3", "path": IZ + r"\Ozone 11 Dynamics.vst3", "active": True},
{"type": "vst3", "path": IZ + r"\Ozone 11 Maximizer.vst3", "active": True},
]
CHAIN_OZONE_1 = [
{"type": "vst3", "path": IZ + r"\Ozone 11 Maximizer.vst3", "active": True},
]
CHAIN_RX_DENOISE = [
{"type": "vst3", "path": IZ + r"\RX 11 Spectral De-noise.vst3", "active": True},
]
CHAIN_EMPTY = []
def set_timer_res(ms=1):
try:
ctypes.windll.winmm.timeBeginPeriod(ms)
except Exception:
pass
def run_test(tag, chain, secs):
log_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"fxrt_bridge_%s.log" % tag)
real_popen = fx_realtime.subprocess.Popen
def capped_popen(cmd, **kw):
kw.pop("stderr", None)
f = open(log_path, "ab", buffering=0)
return real_popen(cmd, stderr=f, **kw)
fx_realtime.subprocess.Popen = capped_popen
try:
sess = fx_realtime.start_session(chain, SR)
finally:
fx_realtime.subprocess.Popen = real_popen
print("== %s: session=%s chain=%d slot(s)" % (tag, sess.name, len(chain)))
stop = threading.Event()
arr_times = [] # (block_idx, perf_counter) khi consumer đọc ra
arr_lock = threading.Lock()
def producer():
# 1 block = 256 sample stereo = 512 float; gửi 4 block liên tiếp
# (mô phỏng worklet burst), engine gom 4 → publish 1 batch.
t = np.arange(BLOCK) / SR
sig = 0.2 * np.sin(2 * np.pi * 220 * t)
inter = np.empty(BLOCK * 2, dtype=np.float32)
inter[0::2] = sig
inter[1::2] = 0.9 * sig
data = inter.tobytes()
start = time.perf_counter()
k = 0
while not stop.is_set():
for _ in range(BATCH):
sess.write_input(data)
k += 1
nxt = start + k * BATCH_PERIOD
d = nxt - time.perf_counter()
if d > 0:
time.sleep(d)
def consumer():
idx = 0
while not stop.is_set():
out = sess.read_output()
if not out:
time.sleep(0.001)
continue
now = time.perf_counter()
with arr_lock:
for _ in out:
arr_times.append((idx, now))
idx += 1
pt = threading.Thread(target=producer)
ct = threading.Thread(target=consumer)
t0 = time.perf_counter()
pt.start(); ct.start()
time.sleep(secs)
stop.set()
pt.join(timeout=5); ct.join(timeout=5)
stats = dict(sess.stats)
sess.close()
time.sleep(0.3)
with arr_lock:
times = list(arr_times)
if len(times) > 1:
gaps = [b - a for (i, a), (j, b) in zip(times[:-1], times[1:]) if b > a]
dur = time.perf_counter() - t0
rate = len(times) / dur
exp = 1.0 / (BATCH / BLOCK_PERIOD) # batch period
print(" engine stats: %s" % stats)
print(" output: %d block / %.1fs = %.1f blk/s (expect %.1f)"
% (len(times), dur, rate, BLOCK / BLOCK_PERIOD))
print(" gap: n=%d avg=%.3fms p95=%.3fms max=%.3fms"
% (len(gaps), np.mean(gaps) * 1e3, np.percentile(gaps, 95) * 1e3,
max(gaps) * 1e3))
over = [g * 1e3 for g in gaps if g > 1.35 * BLOCK_PERIOD]
print(" gaps>%.1fms (underrun candidate): %d (%.2f%%)"
% (1.35 * BLOCK_PERIOD * 1e3, len(over),
100.0 * len(over) / len(gaps) if gaps else 0.0))
else:
print(" NO OUTPUT (engine stats: %s)" % stats)
print(" bridge log tail (%s):" % log_path)
try:
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()
for ln in lines[-12:]:
print(" " + ln.rstrip())
except Exception as e:
print(" (no log: %s)" % e)
if __name__ == "__main__":
set_timer_res(1)
secs = 20
if "--secs" in sys.argv:
secs = int(sys.argv[sys.argv.index("--secs") + 1])
tests = []
if "--test" in sys.argv:
t = sys.argv[sys.argv.index("--test") + 1]
tests = [(t, {"ozone1": CHAIN_OZONE_1,
"ozone3": CHAIN_OZONE_MASTER,
"rx": CHAIN_RX_DENOISE,
"empty": CHAIN_EMPTY}[t])]
else:
tests = [("empty", CHAIN_EMPTY),
("ozone1", CHAIN_OZONE_1),
("ozone3", CHAIN_OZONE_MASTER),
("rx", CHAIN_RX_DENOISE)]
for tag, chain in tests:
try:
run_test(tag, chain, secs)
except Exception as e:
print("== %s FAILED: %r" % (tag, e))
import traceback; traceback.print_exc()
+182
View File
@@ -0,0 +1,182 @@
# fxrt_toggle_probe.py — đo toggle bypass dry->wet với VST instance cache.
# Kịch bản THẬT: start với slot ACTIVE (load + cache) -> toggle bypass=true
# (chain đổi, KHÔNG unload) -> toggle bypass=false (cache hit, không reload)
# Đo: thời gian set_chain -> latency report (chain applied) mỗi toggle.
# python fxrt_toggle_probe.py [--port 8012]
import asyncio
import ctypes
import json
import os
import subprocess
import sys
import tempfile
import time
import numpy as np
REPO = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, REPO)
SR = 48000
BLOCK = 256
BATCH = 4
BATCH_PERIOD = BATCH * BLOCK / SR # 21.333ms
ADELAY = os.path.join(REPO, "native_bridge", "build", "VST3", "Release", "adelay.vst3")
OZONE_MAX = r"C:\Program Files\Common Files\VST3\iZotope\Ozone 11 Maximizer.vst3"
PLUGIN = os.environ.get("FXRT_PROBE_PLUGIN", OZONE_MAX) # mặc định Ozone (latency thật 747smp)
def make_block():
t = np.arange(BLOCK) / SR
amp = float(os.environ.get("FXRT_PROBE_AMP", "1.0"))
sig = amp * (0.2 * np.sin(2 * np.pi * 220 * t) + 0.1 * np.sin(2 * np.pi * 331 * t))
inter = np.empty(BLOCK * 2, dtype=np.float32)
inter[0::2] = sig
inter[1::2] = 0.85 * sig
return inter.tobytes()
def chain_with(bypass):
return [{"type": "vst3", "path": PLUGIN, "name": os.path.basename(PLUGIN),
"preset_b64": "", "bypass": bypass}]
async def probe(port):
import websockets
import urllib.request
base = "http://127.0.0.1:%d" % port
block_bytes = make_block()
def post(path, obj):
req = urllib.request.Request(
base + path, data=json.dumps(obj).encode(),
headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
return json.loads(r.read().decode())
st = post("/api/v1/plugins/fx-realtime/start",
{"fx_chain": chain_with(False), "sample_rate": SR})
sid = st["session_id"]
print("session=%s started slot ACTIVE (load + cache)" % sid)
ws_url = "ws://127.0.0.1:%d/api/v1/plugins/ws/fx-realtime/%s" % (port, sid)
lat_events = [] # (time, seq)
stop = asyncio.Event()
started = asyncio.Event()
async def producer():
next_t = time.perf_counter() + 0.3
while not stop.is_set():
await asyncio.to_thread(time.sleep, max(0, next_t - time.perf_counter()))
if started.is_set():
await ws.send(block_bytes)
next_t += BATCH_PERIOD
async def consumer():
while not stop.is_set():
try:
msg = await ws.recv()
except Exception:
break
if not isinstance(msg, bytes):
try:
d = json.loads(msg)
except Exception:
continue
if isinstance(d, dict) and d.get("cmd") == "latency":
lat_events.append((time.perf_counter(), d.get("total")))
async with websockets.connect(ws_url) as ws:
tasks = [asyncio.create_task(producer()), asyncio.create_task(consumer())]
await asyncio.sleep(0.5)
started.set()
await asyncio.sleep(1.5) # để start-load + latency report đầu
n0 = len(lat_events)
print("baseline latency reports:", lat_events[n0 - 3:n0])
# toggle 1: bypass=true (chain đổi, instance GIỮ trong cache)
t0 = time.perf_counter()
post("/api/v1/plugins/fx-realtime/set-chain",
{"session_id": sid, "fx_chain": chain_with(True)})
await asyncio.sleep(0.6)
t1 = time.perf_counter()
new = [x for x in lat_events if x[0] >= t0]
print("TOGGLE1 bypass=true -> chain applied in %s (%.0fms)" %
("%.0fms" % ((new[0][0] - t0) * 1e3) if new else "NOT SEEN", 0))
# toggle 2: bypass=false (CACHE HIT — không reload)
t2 = time.perf_counter()
post("/api/v1/plugins/fx-realtime/set-chain",
{"session_id": sid, "fx_chain": chain_with(False)})
await asyncio.sleep(0.6)
t3 = time.perf_counter()
new2 = [x for x in lat_events if x[0] >= t2]
print("TOGGLE2 bypass=false -> chain applied in %s" %
("%.0fms" % ((new2[0][0] - t2) * 1e3) if new2 else "NOT SEEN"))
# toggle 3: bypass=true lần 2 (cache hit)
t4 = time.perf_counter()
post("/api/v1/plugins/fx-realtime/set-chain",
{"session_id": sid, "fx_chain": chain_with(True)})
await asyncio.sleep(0.6)
new3 = [x for x in lat_events if x[0] >= t4]
print("TOGGLE3 bypass=true -> chain applied in %s" %
("%.0fms" % ((new3[0][0] - t4) * 1e3) if new3 else "NOT SEEN"))
stop.set()
await asyncio.sleep(0.3)
for t in tasks:
t.cancel()
try:
post("/api/v1/plugins/fx-realtime/stop", {"session_id": sid})
except Exception:
pass
print("all latency events:", ["%.2fs/%.0f" % (x[0] - lat_events[0][0], x[1] or 0) for x in lat_events])
def main():
try:
ctypes.windll.winmm.timeBeginPeriod(1)
except Exception:
pass
port = 8012
if "--port" in sys.argv:
port = int(sys.argv[sys.argv.index("--port") + 1])
storage = tempfile.mkdtemp(prefix="sf_toggleprobe_")
env = dict(os.environ)
env["SONICFORGE_STORAGE_DIR"] = storage
env["FXRT_BRIDGE_LOG"] = os.path.join(REPO, "fxrt_toggle_bridge_%d.log" % port)
logf = open(os.path.join(REPO, "fxrt_toggle_engine_%d.log" % port), "ab", buffering=0)
boot = ("import ctypes; ctypes.windll.winmm.timeBeginPeriod(1); "
"import uvicorn, app.main; "
"uvicorn.run(app.main.app, host='127.0.0.1', port=%d, log_level='warning')" % port)
engine = subprocess.Popen(
[sys.executable, "-c", boot],
cwd=REPO, env=env, stdout=logf, stderr=subprocess.STDOUT,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
try:
import urllib.request
deadline = time.time() + 60
while time.time() < deadline:
try:
with urllib.request.urlopen("http://127.0.0.1:%d/health" % port, timeout=2) as r:
if r.status == 200:
break
except Exception:
time.sleep(0.5)
else:
print("engine not up; log tail:")
print(open(logf.name, "rb").read().decode(errors="replace")[-2000:])
return
print("engine up on :%d" % port)
asyncio.run(probe(port))
finally:
engine.terminate()
try:
engine.wait(timeout=5)
except Exception:
engine.kill()
logf.close()
if __name__ == "__main__":
main()
+432
View File
@@ -0,0 +1,432 @@
# fxrt_ws_probe.py — đo toàn pipeline THẬT (engine uvicorn + WS + bridge) với
# stall model giống main-thread browser. Tạm, chẩn đoán crackle master FX.
# python fxrt_ws_probe.py [--secs 20] [--chain empty|adelay|ozone1|ozone3]
# [--stalls heavy|light|none]
# Kết quả: số block output tới client, gap phân bố, và MÔ PHỎNG CHÍNH XÁC
# worklet outQueue (FILL/HOLD/CAP) → số underrun/silence (crackle thật) +
# peak block cuối trước mỗi silence (fade-out engine → ~0, hết click).
import asyncio
import ctypes
import json
import os
import random
import subprocess
import sys
import tempfile
import time
import numpy as np
REPO = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, REPO)
SR = 48000
BLOCK = 256
BATCH = 4
BATCH_PERIOD = BATCH * BLOCK / SR # 21.333ms
BLOCK_PERIOD = BLOCK / SR # 5.333ms
FILL = 48
CAP = 64
HOLD = 2 # block lặp khi underrun (~10.7ms)
IZ = r"C:\Program Files\Common Files\VST3\iZotope"
ADELAY = os.path.join(REPO, "native_bridge", "build", "VST3", "Release", "adelay.vst3")
CHAINS = {
"empty": [],
"adelay": [{"type": "vst3", "path": ADELAY, "active": True}],
"ozone1": [{"type": "vst3", "path": IZ + r"\Ozone 11 Maximizer.vst3", "active": True}],
"ozone3": [
{"type": "vst3", "path": IZ + r"\Ozone 11 Equalizer.vst3", "active": True},
{"type": "vst3", "path": IZ + r"\Ozone 11 Dynamics.vst3", "active": True},
{"type": "vst3", "path": IZ + r"\Ozone 11 Maximizer.vst3", "active": True},
],
}
def stall_model(name):
# (xác suất stall mỗi giây, min_ms, max_ms) — mô phỏng main-thread busy
# (GUI decode, React render, mở mastering panel, rAF).
return {
"none": (0.0, 0, 0),
"light": (0.5, 60, 120),
"heavy": (1.5, 120, 280),
}[name]
class WorkletSim:
"""Mô phỏng chính xác SfFxRealtimeProcessor outQueue từ arrival times.
mode='cut' — hiện tại: underrun → hold HOLD block rồi CẮT cứng silence
(click nếu nội dung khác 0).
mode='holdfade' — đề xuất (fix worklet): underrun → giữ block cuối + fade
gain 1→0 qua FADE_HOLD block (~21ms) rồi silence ~0
(hết click); giữa hold, block mới tới → resume ngay.
"""
FADE_HOLD = 4 # block fade khi underrun (proposal)
def __init__(self, mode="cut"):
self.mode = mode
self.arrivals = [] # (block_seq, time, peak)
self.underruns = 0
self.silences = 0
self.silent_ms = 0.0
self.hold_events = 0
self.hold_fades = 0
self.silence_peaks = [] # peak block cuối trước mỗi silence (fade -> ~0)
def run(self, duration):
queue = []
play = None # thời điểm consume block kế
started = False
idx = 0
last_peak = 1.0
hold_fade = 0 # còn N block fade đang phát (mode holdfade)
for item in self.arrivals:
bi, t = item[0], item[1]
peak = item[2] if len(item) > 2 else 1.0
queue.append((bi, peak))
if len(queue) > CAP:
queue.pop(0)
if play is None:
if len(queue) >= FILL:
started = True
play = t + BLOCK_PERIOD
last_peak = queue.pop(0)[1]
continue
# consume các block đến hạn trước arrival này
while play is not None and play <= t:
if queue:
if hold_fade > 0 and self.mode == "holdfade":
# có block mới giữa hold-fade → resume ngay (không đợi
# hết fade, không cắt): fade chỉ còn là chỗi ~0.
hold_fade = 0
last_peak = queue.pop(0)[1]
play += BLOCK_PERIOD
else:
# underrun tại play
self.underruns += 1
if self.mode == "holdfade":
# giữ block cuối + fade 1→0 qua FADE_HOLD block
hold_fade = self.FADE_HOLD
self.hold_fades += 1
while play <= t and hold_fade > 0:
last_peak *= (hold_fade - 1) / max(self.FADE_HOLD - 1, 1)
hold_fade -= 1
play += BLOCK_PERIOD
if hold_fade == 0 and play <= t:
# đã fade về ~0 → silence êm (không click)
self.silences += 1
self.silent_ms += (t - play) * 1e3
self.silence_peaks.append(last_peak)
play = None
idx += 1
else:
hold_end = play + HOLD * BLOCK_PERIOD
if t >= hold_end:
# silence từ hold_end tới khi có block mới (t)
self.silences += 1
self.silent_ms += (t - hold_end) * 1e3
self.silence_peaks.append(last_peak)
else:
self.hold_events += 1
play = None
idx += 1
if play is None and queue:
play = t + BLOCK_PERIOD
last_peak = queue.pop(0)[1]
return self
def report(self):
r = ("worklet sim[%s]: underruns=%d hold_only=%d hold_fades=%d silences=%d silent_ms=%.1f"
% (self.mode, self.underruns, self.hold_events, self.hold_fades,
self.silences, self.silent_ms))
if self.silence_peaks:
arr = np.array(self.silence_peaks)
hard = int((arr > 0.05).sum())
r += (" | peak@silence min=%.3f med=%.3f max=%.3f hard(>0.05)=%d"
% (arr.min(), np.median(arr), arr.max(), hard))
return r
def make_block():
t = np.arange(BLOCK) / SR
amp = float(os.environ.get("FXRT_PROBE_AMP", "1.0"))
sig = amp * (0.2 * np.sin(2 * np.pi * 220 * t) + 0.1 * np.sin(2 * np.pi * 331 * t))
inter = np.empty(BLOCK * 2, dtype=np.float32)
inter[0::2] = sig
inter[1::2] = 0.85 * sig
return inter.tobytes()
async def run_ws_probe(port, chain, secs, stall):
import websockets
url = "http://127.0.0.1:%d" % port
import urllib.request
req = urllib.request.Request(
url + "/api/v1/plugins/fx-realtime/start",
data=json.dumps({"fx_chain": chain, "sample_rate": SR}).encode(),
headers={"Content-Type": "application/json"}, method="POST")
try:
with urllib.request.urlopen(req, timeout=60) as r:
st = json.loads(r.read().decode())
except Exception as e:
print(" start session FAILED:", e)
return None
sid = st["session_id"]
print(" session=%s chain=%d slot(s)" % (sid, len(chain)))
ws_url = "ws://127.0.0.1:%d/api/v1/plugins/ws/fx-realtime/%s" % (port, sid)
s_prob, s_min, s_max = stall_model(stall)
sent_bursts = 0
recv_times = [] # (block_seq, time, peak)
out_peaks = [] # output peak per block (cause-5 clipping track)
out_flat = [] # flat-top samples per block (|x|>0.9999)
recv_lock = asyncio.Lock()
stop = asyncio.Event()
produced = asyncio.Queue() # burst buffers
backlog = []
sent_times = []
send_time_stats = []
produce_count = 0
pump_iter_stats = []
block_bytes = make_block()
# sanity: đo resolution asyncio.sleep ngay trong loop này
_t0 = time.perf_counter()
for _ in range(50):
await asyncio.sleep(0.001)
print(" [sanity] 50x sleep(0.001)=%.1fms -> %.2fms/iter"
% ((time.perf_counter() - _t0) * 1e3, (time.perf_counter() - _t0) / 50 * 1e3))
async def producer():
# worklet: sản xuất burst mỗi 21.333ms KHÔNG ngừng dù main thread busy
next_t = time.perf_counter() + 0.2
n = 0
while not stop.is_set():
# to_thread: asyncio.sleep bi cap 15.6ms (GQCS Win11) -> pacing 21.3ms
# thành ~31ms -> probe không đại diện. time.sleep giữ precision.
await asyncio.to_thread(time.sleep, max(0, next_t - time.perf_counter()))
await produced.put(block_bytes)
n += 1
next_t += BATCH_PERIOD
nonlocal produce_count
produce_count = n
async def stall_scheduler():
# main-thread busy: set stall_until — pump VÀ consumer đều đọc (cùng
# main thread bị block → cả 2 hướng đều đứng; app thật đúng vậy).
while not stop.is_set():
if s_prob:
await asyncio.sleep(random.expovariate(s_prob))
dur = random.uniform(s_min, s_max) / 1000.0
stall_state["until"] = time.perf_counter() + dur
else:
await asyncio.sleep(60)
async def pump():
# fxRtInPump JS: tối đa 1 burst/21ms; stall → không gửi
nonlocal backlog
last_sent = 0.0
send_times = []
iter_times = []
while not stop.is_set():
now = time.perf_counter()
iter_times.append(now)
if now < stall_state["until"]:
await asyncio.to_thread(time.sleep, 0.002)
continue
while not produced.empty():
backlog.append(await produced.get())
if backlog and now - last_sent >= 0.021:
burst = backlog.pop(0)
sent_times.append(now)
t0 = time.perf_counter()
for i in range(BATCH):
await ws.send(burst)
send_times.append(time.perf_counter() - t0)
last_sent = now
await asyncio.to_thread(time.sleep, 0.001)
nonlocal send_time_stats, pump_iter_stats
send_time_stats = send_times
pump_iter_stats = iter_times
stall_state = {"until": 0.0}
async def param_burst():
# Mô phỏng fxRtPushAllParams (app gửi loạt set_param ngay sau WS open):
# numeric ParamID 0..3 → VST3 setParamNormalized THẬT trên bridge.
# Fix: setParam defer worker → audio loop không block → send interval ổn.
if not os.environ.get("FXRT_PROBE_PARAMS"):
return
await asyncio.sleep(0.8)
nslots = len(chain)
ids = [s.strip() for s in os.environ.get("FXRT_PROBE_PARAM_IDS", "0,1,2,3").split(",")]
for rep in range(4):
for slot in range(nslots):
for i, pid in enumerate(ids):
await ws.send(json.dumps({"cmd": "set_param", "slot": slot,
"key": pid, "value": 0.3 + 0.1 * i}))
await asyncio.sleep(0.4)
async def consumer():
while not stop.is_set():
if os.environ.get("FXRT_PROBE_CONSUMER_STALL") and \
time.perf_counter() < stall_state["until"]:
# main-thread busy: WS message nằm trong browser queue, main
# không decode/postMessage → worklet không nhận gì.
await asyncio.to_thread(time.sleep, 0.002)
continue
try:
msg = await ws.recv()
except Exception:
break
if isinstance(msg, bytes) and len(msg) == BLOCK * 2 * 4:
now = time.perf_counter()
f = np.frombuffer(msg, dtype=np.float32)
pk = float(f[::2].max())
flat = int(np.count_nonzero(np.abs(f) > 0.9999))
async with recv_lock:
recv_times.append((len(recv_times), now, pk))
out_peaks.append(pk)
out_flat.append(flat)
# text frame (latency) bỏ qua
async with websockets.connect(ws_url) as ws:
t0 = time.perf_counter()
tasks = [asyncio.create_task(producer()), asyncio.create_task(pump()),
asyncio.create_task(stall_scheduler()), asyncio.create_task(param_burst()),
asyncio.create_task(consumer())]
if os.environ.get("FXRT_PROBE_NO_CONSUMER"):
for t in tasks:
t.cancel()
tasks = [asyncio.create_task(producer()), asyncio.create_task(pump()),
asyncio.create_task(stall_scheduler())]
await asyncio.sleep(secs)
stop.set()
await asyncio.sleep(0.5)
for t in tasks:
t.cancel()
dur = time.perf_counter() - t0
async with recv_lock:
times = list(recv_times)
# stop session
try:
req2 = urllib.request.Request(
url + "/api/v1/plugins/fx-realtime/stop",
data=json.dumps({"session_id": sid}).encode(),
headers={"Content-Type": "application/json"}, method="POST")
urllib.request.urlopen(req2, timeout=10)
except Exception:
pass
n_sent = len(sent_times) * BATCH
n_recv = len(times)
print(" producer bursts=%d pump sends=%d (backlog at end=%d)"
% (produce_count, len(sent_times), len(backlog)))
if pump_iter_stats:
it = np.diff(np.array(pump_iter_stats)) * 1e3
it = it[it > 0]
print(" pump iter: avg=%.2fms p50=%.2f p95=%.2f max=%.2fms"
% (it.mean(), np.percentile(it, 50), np.percentile(it, 95), it.max()))
if len(sent_times) > 3:
si = np.diff(np.array(sent_times)) * 1e3
si = si[si > 0]
print(" send interval: avg=%.2fms p50=%.2f p95=%.2f max=%.2fms"
% (si.mean(), np.percentile(si, 50), np.percentile(si, 95), si.max()))
if send_time_stats:
st = np.array(send_time_stats) * 1e3
print(" ws send burst: avg=%.2fms p50=%.2f p95=%.2f max=%.2fms"
% (st.mean(), np.percentile(st, 50), np.percentile(st, 95), st.max()))
rate = n_recv / dur
print(" sent=%d bursts (%d blocks) recv=%d blocks over %.1fs = %.1f blk/s (expect 187.5)"
% (len(sent_times), n_sent, n_recv, dur, rate))
if out_peaks:
op = np.array(out_peaks)
of = np.array(out_flat)
print(" OUT peak: min=%.4f p50=%.4f p95=%.4f max=%.4f" %
(op.min(), np.percentile(op, 50), np.percentile(op, 95), op.max()))
print(" OUT flat-top(|x|>0.9999): blocks=%d samples=%d (%.4f%%)" %
(int(np.count_nonzero(of > 0)), int(of.sum()),
100.0 * of.sum() / max(1, len(of) * BLOCK * 2)))
print(" lost blocks (sent-recv): %d" % max(0, n_sent - n_recv))
if len(times) > 1:
gaps = [b - a for (i, a, _), (j, b, _) in zip(times[:-1], times[1:])]
gaps = [g for g in gaps if g > 0]
g = np.array(gaps) * 1e3
print(" gap: avg=%.2fms p50=%.2f p95=%.2f p99=%.2f max=%.2fms"
% (g.mean(), np.percentile(g, 50), np.percentile(g, 95),
np.percentile(g, 99), g.max()))
over = g[g > 1.35 * BLOCK_PERIOD * 1e3]
print(" gaps>%.1fms: %d (%.2f%%)" % (1.35 * BLOCK_PERIOD * 1e3, len(over),
100.0 * len(over) / len(g) if len(g) else 0))
sim = WorkletSim(mode=os.environ.get("FXRT_PROBE_WORKLET", "cut"))
sim.arrivals = times
sim.run(dur)
print(" " + sim.report())
def set_timer_res(ms=1):
try:
ctypes.windll.winmm.timeBeginPeriod(ms)
except Exception:
pass
def main():
set_timer_res(1)
secs = 20
chain = "ozone1"
stall = "heavy"
port = 8011
if "--secs" in sys.argv:
secs = int(sys.argv[sys.argv.index("--secs") + 1])
if "--chain" in sys.argv:
chain = sys.argv[sys.argv.index("--chain") + 1]
if "--stalls" in sys.argv:
stall = sys.argv[sys.argv.index("--stalls") + 1]
if "--port" in sys.argv:
port = int(sys.argv[sys.argv.index("--port") + 1])
# engine process
import app.main # noqa: F401 — kiểm tra import được
storage = tempfile.mkdtemp(prefix="sf_wsprobe_")
env = dict(os.environ)
env["SONICFORGE_STORAGE_DIR"] = storage
logf = open(os.path.join(REPO, "fxrt_engine_%d.log" % port), "ab", buffering=0)
boot = ("import ctypes; ctypes.windll.winmm.timeBeginPeriod(1); "
"import uvicorn, app.main; "
"uvicorn.run(app.main.app, host='127.0.0.1', port=%d, log_level='warning')" % port)
engine = subprocess.Popen(
[sys.executable, "-c", boot],
cwd=REPO, env=env, stdout=logf, stderr=subprocess.STDOUT,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
try:
import urllib.request
deadline = time.time() + 60
while time.time() < deadline:
try:
with urllib.request.urlopen("http://127.0.0.1:%d/health" % port, timeout=2) as r:
if r.status == 200:
break
except Exception:
time.sleep(0.5)
else:
print("engine not up; log tail:")
print(open(logf.name, "rb").read().decode(errors="replace")[-2000:])
return
print("engine up on :%d" % port)
asyncio.run(run_ws_probe(port, CHAINS[chain], secs, stall))
finally:
engine.terminate()
try:
engine.wait(timeout=5)
except Exception:
engine.kill()
logf.close()
if __name__ == "__main__":
main()
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""Diagnostic harness: spawn realtime + GUI bridge, feed audio, poll /preset.
Checks: (1) blob drift over time (would trigger chainKey change -> set_chain
-> crackle), (2) /preset latency (HTTP worker block -> dead clicks)."""
import ctypes, json, os, subprocess, tempfile, threading, time, uuid, urllib.request, sys
import numpy as np
from multiprocessing import shared_memory
FXRT_MAGIC = 0x46585254
FXRT_BLOCK = 256
FXRT_IN_SLOTS = 4
FXRT_OUT_SLOTS = 8
FXRT_CTRL_SLOTS = 8
FXRT_LAT_SLOTS = 8
FXRT_STATE_STARTING = 0
FXRT_STATE_READY = 1
class FxRTHeader(ctypes.Structure):
_fields_ = [("magic", ctypes.c_uint32), ("state", ctypes.c_uint32),
("sample_rate", ctypes.c_uint32), ("block_size", ctypes.c_uint32),
("running", ctypes.c_uint32), ("heartbeat", ctypes.c_uint32),
("in_write", ctypes.c_uint32), ("in_read", ctypes.c_uint32),
("in_slots", ctypes.c_uint32), ("out_write", ctypes.c_uint32),
("out_read", ctypes.c_uint32), ("out_slots", ctypes.c_uint32),
("ctrl_write", ctypes.c_uint32), ("ctrl_read", ctypes.c_uint32),
("ctrl_slots", ctypes.c_uint32), ("lat_write", ctypes.c_uint32),
("lat_read", ctypes.c_uint32), ("lat_slots", ctypes.c_uint32)]
HEADER_SIZE = ctypes.sizeof(FxRTHeader)
IN_L_OFF = HEADER_SIZE
IN_R_OFF = IN_L_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
OUT_L_OFF = IN_R_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
OUT_R_OFF = OUT_L_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
TOTAL_SIZE = OUT_R_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
EXE = r"C:\Users\locpham\SonicForgeStudio\src-tauri\binaries\fx_vst_bridge.exe"
PLUGIN = sys.argv[1] if len(sys.argv) > 1 else r"C:\Program Files\Common Files\VST3\WaveObserver.vst3"
DURATION = float(sys.argv[2]) if len(sys.argv) > 2 else 30.0
def http_get(url, timeout=10):
t0 = time.time()
try:
with urllib.request.urlopen(url, timeout=timeout) as r:
body = r.read().decode()
return time.time() - t0, body
except Exception as e:
return time.time() - t0, "ERR:" + str(e)
def main():
name = "SonicForge_FXRT_" + uuid.uuid4().hex[:12]
print("plugin:", PLUGIN, "shm:", name, flush=True)
shm = shared_memory.SharedMemory(name=name, create=True, size=TOTAL_SIZE)
shm.buf[:TOTAL_SIZE] = b"\x00" * TOTAL_SIZE
h = FxRTHeader.from_buffer(shm.buf)
h.magic = FXRT_MAGIC; h.state = FXRT_STATE_STARTING
h.sample_rate = 44100; h.block_size = FXRT_BLOCK; h.running = 1
h.in_slots = FXRT_IN_SLOTS; h.out_slots = FXRT_OUT_SLOTS
h.ctrl_slots = FXRT_CTRL_SLOTS; h.lat_slots = FXRT_LAT_SLOTS
npbuf = np.frombuffer(shm.buf, dtype=np.float32)
stop = threading.Event()
procs = []
try:
job1 = {"sample_rate": 44100, "block_size": 256, "seq": 0,
"fx_chain": [{"type": "vst3", "path": PLUGIN, "name": "probe", "preset_b64": "", "bypass": False}]}
fd, jp1 = tempfile.mkstemp(suffix=".json", prefix="fxrt_probe_")
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(job1, f)
p1 = subprocess.Popen([EXE, "--realtime-fx", jp1, "--shm", name, "--parent", str(os.getpid())],
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
procs.append(p1)
t0 = time.time()
while time.time() - t0 < 30 and h.state != FXRT_STATE_READY:
time.sleep(0.05)
if p1.poll() is not None:
print("REALTIME BRIDGE EXITED rc=", p1.returncode, flush=True); return
print("realtime state:", h.state, "took %.2fs" % (time.time() - t0), flush=True)
if h.state != FXRT_STATE_READY:
print("realtime not ready — aborting", flush=True); return
job2 = {"path": PLUGIN, "name": "probe", "shm": name, "preset_b64": ""}
fd, jp2 = tempfile.mkstemp(suffix=".json", prefix="fxgui_probe_")
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(job2, f)
p2 = subprocess.Popen([EXE, "--fx-gui", jp2],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
procs.append(p2)
port = None
t0 = time.time()
while time.time() - t0 < 20:
line = p2.stdout.readline()
if not line:
if p2.poll() is not None: break
continue
line = line.strip()
if line.startswith("SF_FXGUI_PORT="):
port = int(line.split("=", 1)[1]); break
print("[gui]", line, flush=True)
if not port:
print("GUI bridge: no port — rc=", p2.poll(), flush=True); return
url = "http://127.0.0.1:%d" % port
print("gui port:", port, flush=True)
def feed():
phase = 0.0
while not stop.is_set():
if h.in_write - h.in_read < FXRT_IN_SLOTS:
slot = h.in_write & (FXRT_IN_SLOTS - 1)
off = IN_L_OFF + slot * FXRT_BLOCK
t = np.arange(FXRT_BLOCK) / 44100.0 + phase
sine = 0.1 * np.sin(2 * np.pi * 440 * t).astype(np.float32)
npbuf[off:off + FXRT_BLOCK] = sine
npbuf[off + FXRT_IN_SLOTS * FXRT_BLOCK:off + FXRT_IN_SLOTS * FXRT_BLOCK + FXRT_BLOCK] = sine
h.in_write += 1
phase += FXRT_BLOCK / 44100.0
time.sleep(0.003)
th = threading.Thread(target=feed, daemon=True)
th.start()
# Poll /preset every 1.5s — same as frontend — measure drift + latency.
last = None
last16 = None
drift_changes = 0
t0 = time.time()
n = 0
while time.time() - t0 < DURATION:
time.sleep(1.5)
lat, body = http_get(url + "/preset")
n += 1
import json as _j
try:
d = _j.loads(body)
b64 = d.get("preset_b64", "")
except Exception:
print(" poll %d: bad json (%s)" % (n, body[:80]), flush=True)
continue
b16 = b64[:16]
first_diff = (last16 is not None and b16 != last16)
full_diff = (last is not None and b64 != last)
if first_diff: drift_changes += 1
print(" poll %d lat=%.0fms blob_len=%d first16=%s %s%s%s" % (
n, lat * 1000, len(b64), b16,
"FIRST16_CHANGED" if first_diff else "",
" full_changed" if (full_diff and not first_diff) else "",
" (NEW)" if last is None else ""), flush=True)
last = b64; last16 = b16
print("RESULT: %d polls, %d first16 changes (would trigger set_chain -> crackle)" % (n, drift_changes), flush=True)
finally:
stop.set()
for p in procs:
try: p.terminate()
except Exception: pass
time.sleep(0.5)
for p in procs:
try: p.kill()
except Exception: pass
shm.close()
try: shm.unlink()
except Exception: pass
if __name__ == "__main__":
main()
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""Measure out-ring glitches while GUI bridge runs with /frame + /preset load.
Feeds sine into in ring (realtime pace), reads out ring continuously, detects
discontinuities (sample jumps) in the output signal."""
import ctypes, json, os, subprocess, tempfile, threading, time, uuid, urllib.request, sys
import numpy as np
from multiprocessing import shared_memory
FXRT_MAGIC = 0x46585254
FXRT_BLOCK = 256
FXRT_IN_SLOTS = 4
FXRT_OUT_SLOTS = 8
FXRT_CTRL_SLOTS = 8
FXRT_LAT_SLOTS = 8
FXRT_STATE_STARTING = 0
FXRT_STATE_READY = 1
class FxRTHeader(ctypes.Structure):
_fields_ = [("magic", ctypes.c_uint32), ("state", ctypes.c_uint32),
("sample_rate", ctypes.c_uint32), ("block_size", ctypes.c_uint32),
("running", ctypes.c_uint32), ("heartbeat", ctypes.c_uint32),
("in_write", ctypes.c_uint32), ("in_read", ctypes.c_uint32),
("in_slots", ctypes.c_uint32), ("out_write", ctypes.c_uint32),
("out_read", ctypes.c_uint32), ("out_slots", ctypes.c_uint32),
("ctrl_write", ctypes.c_uint32), ("ctrl_read", ctypes.c_uint32),
("ctrl_slots", ctypes.c_uint32), ("lat_write", ctypes.c_uint32),
("lat_read", ctypes.c_uint32), ("lat_slots", ctypes.c_uint32)]
HEADER_SIZE = ctypes.sizeof(FxRTHeader)
IN_L_OFF = HEADER_SIZE
IN_R_OFF = IN_L_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
OUT_L_OFF = IN_R_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
OUT_R_OFF = OUT_L_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
TOTAL_SIZE = OUT_R_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
EXE = r"C:\Users\locpham\SonicForgeStudio\src-tauri\binaries\fx_vst_bridge.exe"
PLUGIN = sys.argv[1] if len(sys.argv) > 1 else r"C:\Program Files\Common Files\VST3\WaveObserver.vst3"
DURATION = float(sys.argv[2]) if len(sys.argv) > 2 else 30.0
FRAME = sys.argv[3] if len(sys.argv) > 3 else "on" # "on" = poll /frame 30fps
def http_get(url, timeout=5):
try:
with urllib.request.urlopen(url, timeout=timeout) as r:
return r.read()
except Exception as e:
return b"ERR:" + str(e).encode()
def main():
name = "SonicForge_FXRT_" + uuid.uuid4().hex[:12]
print("plugin:", PLUGIN, "shm:", name, "frame:", FRAME, flush=True)
shm = shared_memory.SharedMemory(name=name, create=True, size=TOTAL_SIZE)
shm.buf[:TOTAL_SIZE] = b"\x00" * TOTAL_SIZE
h = FxRTHeader.from_buffer(shm.buf)
h.magic = FXRT_MAGIC; h.state = FXRT_STATE_STARTING
h.sample_rate = 44100; h.block_size = FXRT_BLOCK; h.running = 1
h.in_slots = FXRT_IN_SLOTS; h.out_slots = FXRT_OUT_SLOTS
h.ctrl_slots = FXRT_CTRL_SLOTS; h.lat_slots = FXRT_LAT_SLOTS
npbuf = np.frombuffer(shm.buf, dtype=np.float32)
stop = threading.Event()
procs = []
glitches = []
out_total = [0]
try:
job1 = {"sample_rate": 44100, "block_size": 256, "seq": 0,
"fx_chain": [{"type": "vst3", "path": PLUGIN, "name": "probe", "preset_b64": "", "bypass": False}]}
fd, jp1 = tempfile.mkstemp(suffix=".json", prefix="fxrt_probe_")
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(job1, f)
p1 = subprocess.Popen([EXE, "--realtime-fx", jp1, "--shm", name, "--parent", str(os.getpid())],
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
procs.append(p1)
t0 = time.time()
while time.time() - t0 < 30 and h.state != FXRT_STATE_READY:
time.sleep(0.05)
if p1.poll() is not None:
print("REALTIME BRIDGE EXITED rc=", p1.returncode, flush=True); return
if h.state != FXRT_STATE_READY:
print("realtime not ready — aborting", flush=True); return
print("realtime ready in %.2fs" % (time.time() - t0), flush=True)
job2 = {"path": PLUGIN, "name": "probe", "shm": name, "preset_b64": ""}
fd, jp2 = tempfile.mkstemp(suffix=".json", prefix="fxgui_probe_")
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(job2, f)
p2 = subprocess.Popen([EXE, "--fx-gui", jp2],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
procs.append(p2)
port = None
t0 = time.time()
while time.time() - t0 < 20:
line = p2.stdout.readline()
if not line:
if p2.poll() is not None: break
continue
line = line.strip()
if line.startswith("SF_FXGUI_PORT="):
port = int(line.split("=", 1)[1]); break
if not port:
print("GUI bridge: no port — rc=", p2.poll(), flush=True); return
url = "http://127.0.0.1:%d" % port
print("gui port:", port, flush=True)
# Feed sine at realtime pace; read out ring continuously; detect glitches.
phase = [0.0]
def feed():
while not stop.is_set():
if h.in_write - h.in_read < FXRT_IN_SLOTS:
slot = h.in_write & (FXRT_IN_SLOTS - 1)
off = IN_L_OFF + slot * FXRT_BLOCK
t = np.arange(FXRT_BLOCK) / 44100.0 + phase[0]
sine = 0.1 * np.sin(2 * np.pi * 440 * t).astype(np.float32)
npbuf[off // 4:(off // 4) + FXRT_BLOCK] = sine
npbuf[(off + FXRT_IN_SLOTS * FXRT_BLOCK) // 4:(off + FXRT_IN_SLOTS * FXRT_BLOCK) // 4 + FXRT_BLOCK] = sine
h.in_write += 1
phase[0] += FXRT_BLOCK / 44100.0
time.sleep(0.003)
threading.Thread(target=feed, daemon=True).start()
def frame_loop():
while not stop.is_set():
http_get(url + "/frame")
time.sleep(1.0 / 30.0)
if FRAME == "on":
threading.Thread(target=frame_loop, daemon=True).start()
def reader():
last = h.out_write
prev_samp = 0.0
prev_ok = False
expected_next = None
while not stop.is_set():
w = h.out_write
while last < w:
slot = last & (FXRT_OUT_SLOTS - 1)
off = OUT_L_OFF + slot * FXRT_BLOCK
blk = npbuf[off // 4:(off // 4) + FXRT_BLOCK].copy()
# detect discontinuity: jump vs previous sample
if prev_ok:
for i in range(FXRT_BLOCK):
if abs(blk[i] - prev_samp) > 0.25:
glitches.append((time.time(), i))
break
prev_samp = blk[-1]
prev_ok = True
out_total[0] += FXRT_BLOCK
last += 1
time.sleep(0.001)
threading.Thread(target=reader, daemon=True).start()
# /preset poll like frontend
t0 = time.time()
n = 0
while time.time() - t0 < DURATION:
time.sleep(1.5)
http_get(url + "/preset")
n += 1
stop.set()
time.sleep(0.3)
print("RESULT: %d polls, out blocks=%d, glitches=%d" % (n, out_total[0], len(glitches)), flush=True)
for g in glitches[:10]:
print(" glitch t=%.2f idx=%d" % (g[0] - (time.time() - t0), g[1]), flush=True)
finally:
stop.set()
for p in procs:
try: p.terminate()
except Exception: pass
time.sleep(0.5)
for p in procs:
try: p.kill()
except Exception: pass
shm.close()
if __name__ == "__main__":
main()
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""Measure out-ring glitches while set_chain fires every 3s (preset apply),
with GUI bridge + frame + preset poll running. set_chain is the only path
that reloads a VST3 in the realtime process."""
import ctypes, json, os, subprocess, tempfile, threading, time, uuid, urllib.request, sys
import numpy as np
from multiprocessing import shared_memory
FXRT_MAGIC = 0x46585254
FXRT_BLOCK = 256
FXRT_IN_SLOTS = 4
FXRT_OUT_SLOTS = 8
FXRT_CTRL_SLOTS = 8
FXRT_LAT_SLOTS = 8
FXRT_STATE_STARTING = 0
FXRT_STATE_READY = 1
class FxRTHeader(ctypes.Structure):
_fields_ = [("magic", ctypes.c_uint32), ("state", ctypes.c_uint32),
("sample_rate", ctypes.c_uint32), ("block_size", ctypes.c_uint32),
("running", ctypes.c_uint32), ("heartbeat", ctypes.c_uint32),
("in_write", ctypes.c_uint32), ("in_read", ctypes.c_uint32),
("in_slots", ctypes.c_uint32), ("out_write", ctypes.c_uint32),
("out_read", ctypes.c_uint32), ("out_slots", ctypes.c_uint32),
("ctrl_write", ctypes.c_uint32), ("ctrl_read", ctypes.c_uint32),
("ctrl_slots", ctypes.c_uint32), ("lat_write", ctypes.c_uint32),
("lat_read", ctypes.c_uint32), ("lat_slots", ctypes.c_uint32)]
HEADER_SIZE = ctypes.sizeof(FxRTHeader)
IN_L_OFF = HEADER_SIZE
IN_R_OFF = IN_L_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
OUT_L_OFF = IN_R_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
OUT_R_OFF = OUT_L_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
TOTAL_SIZE = OUT_R_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
EXE = r"C:\Users\locpham\SonicForgeStudio\src-tauri\binaries\fx_vst_bridge.exe"
PLUGIN = sys.argv[1] if len(sys.argv) > 1 else r"C:\Program Files\Common Files\VST3\WaveObserver.vst3"
DURATION = float(sys.argv[2]) if len(sys.argv) > 2 else 40.0
def http_get(url, timeout=5):
try:
with urllib.request.urlopen(url, timeout=timeout) as r:
return r.read()
except Exception as e:
return b"ERR:" + str(e).encode()
def main():
name = "SonicForge_FXRT_" + uuid.uuid4().hex[:12]
print("plugin:", PLUGIN, "shm:", name, flush=True)
shm = shared_memory.SharedMemory(name=name, create=True, size=TOTAL_SIZE)
shm.buf[:TOTAL_SIZE] = b"\x00" * TOTAL_SIZE
h = FxRTHeader.from_buffer(shm.buf)
h.magic = FXRT_MAGIC; h.state = FXRT_STATE_STARTING
h.sample_rate = 44100; h.block_size = FXRT_BLOCK; h.running = 1
h.in_slots = FXRT_IN_SLOTS; h.out_slots = FXRT_OUT_SLOTS
h.ctrl_slots = FXRT_CTRL_SLOTS; h.lat_slots = FXRT_LAT_SLOTS
npbuf = np.frombuffer(shm.buf, dtype=np.float32)
stop = threading.Event()
procs = []
glitches = []
out_total = [0]
jp1 = None
try:
job1 = {"sample_rate": 44100, "block_size": 256, "seq": 0,
"fx_chain": [{"type": "vst3", "path": PLUGIN, "name": "probe", "preset_b64": "", "bypass": False}]}
fd, jp1 = tempfile.mkstemp(suffix=".json", prefix="fxrt_probe_")
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(job1, f)
p1 = subprocess.Popen([EXE, "--realtime-fx", jp1, "--shm", name, "--parent", str(os.getpid())],
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
procs.append(p1)
t0 = time.time()
while time.time() - t0 < 30 and h.state != FXRT_STATE_READY:
time.sleep(0.05)
if p1.poll() is not None:
print("REALTIME BRIDGE EXITED rc=", p1.returncode, flush=True); return
if h.state != FXRT_STATE_READY:
print("realtime not ready", flush=True); return
print("realtime ready in %.2fs" % (time.time() - t0), flush=True)
job2 = {"path": PLUGIN, "name": "probe", "shm": name, "preset_b64": ""}
fd, jp2 = tempfile.mkstemp(suffix=".json", prefix="fxgui_probe_")
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(job2, f)
p2 = subprocess.Popen([EXE, "--fx-gui", jp2],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
procs.append(p2)
port = None
t0 = time.time()
while time.time() - t0 < 20:
line = p2.stdout.readline()
if not line:
if p2.poll() is not None: break
continue
line = line.strip()
if line.startswith("SF_FXGUI_PORT="):
port = int(line.split("=", 1)[1]); break
if not port:
print("GUI bridge: no port", flush=True); return
url = "http://127.0.0.1:%d" % port
print("gui port:", port, flush=True)
phase = [0.0]
def feed():
while not stop.is_set():
if h.in_write - h.in_read < FXRT_IN_SLOTS:
s = h.in_write & (FXRT_IN_SLOTS - 1)
off = IN_L_OFF + s * FXRT_BLOCK
t = np.arange(FXRT_BLOCK) / 44100.0 + phase[0]
sine = (0.1 * np.sin(2 * np.pi * 440 * t)).astype(np.float32)
npbuf[off // 4:(off // 4) + FXRT_BLOCK] = sine
npbuf[(off + FXRT_IN_SLOTS * FXRT_BLOCK) // 4:(off + FXRT_IN_SLOTS * FXRT_BLOCK) // 4 + FXRT_BLOCK] = sine
h.in_write += 1
phase[0] += FXRT_BLOCK / 44100.0
time.sleep(0.003)
threading.Thread(target=feed, daemon=True).start()
def frame_loop():
while not stop.is_set():
http_get(url + "/frame")
time.sleep(1.0 / 30.0)
threading.Thread(target=frame_loop, daemon=True).start()
seq = [0]
def setchain_loop():
while not stop.is_set():
time.sleep(3.0)
try:
with urllib.request.urlopen(url + "/preset", timeout=5) as r:
d = json.loads(r.read().decode())
pb = d.get("preset_b64", "")
except Exception:
pb = ""
seq[0] += 1
job = {"sample_rate": 44100, "block_size": 256, "seq": seq[0],
"fx_chain": [{"type": "vst3", "path": PLUGIN, "name": "probe", "preset_b64": pb, "bypass": False}]}
with open(jp1, "w", encoding="utf-8") as f:
json.dump(job, f)
print(" set_chain seq=%d preset_len=%d at %.2fs" % (seq[0], len(pb), time.time() - t0), flush=True)
threading.Thread(target=setchain_loop, daemon=True).start()
def reader():
last = h.out_write
prev_samp = 0.0
prev_ok = False
while not stop.is_set():
w = h.out_write
while last < w:
s = last & (FXRT_OUT_SLOTS - 1)
off = OUT_L_OFF + s * FXRT_BLOCK
blk = npbuf[off // 4:(off // 4) + FXRT_BLOCK].copy()
if prev_ok:
for i in range(FXRT_BLOCK):
if abs(blk[i] - prev_samp) > 0.25:
glitches.append((time.time(), i, last))
break
prev_samp = blk[-1]
prev_ok = True
out_total[0] += FXRT_BLOCK
last += 1
time.sleep(0.001)
threading.Thread(target=reader, daemon=True).start()
t0 = time.time()
n = 0
while time.time() - t0 < DURATION:
time.sleep(1.5)
http_get(url + "/preset")
n += 1
stop.set()
time.sleep(0.3)
print("RESULT: %d polls, out blocks=%d, glitches=%d" % (n, out_total[0], len(glitches)), flush=True)
for g in glitches[:10]:
print(" glitch block=%d idx=%d" % (g[2], g[1]), flush=True)
finally:
stop.set()
for p in procs:
try: p.terminate()
except Exception: pass
time.sleep(0.5)
for p in procs:
try: p.kill()
except Exception: pass
shm.close()
if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.
+30
View File
@@ -0,0 +1,30 @@
# Mastering Expand — Extension Modules & Dynamic Chain
Tài liệu cho các module mở rộng của Mastering modal (MasteringModal) và Track FX chain.
## §II.1 EQ Preset Library
- Thư viện preset EQ band-gain dùng chung cho track EQ.
- Code: `app/static/js/app.jsx` (EQ Preset Library, `EQ_PRESETS`).
- Ứng dụng: Mastering modal EQ module, Track Strip EQ.
## §II.2 Bus Compressor Module
- Module nén masterbus đặt trong MasteringModal, giữa các module chain.
- Code: `app/static/js/app.jsx` (Bus Compressor module trong MasteringModal JSX).
- Điều khiển: threshold, ratio, attack, release, makeup gain; bypass riêng.
## §II.3 rebuildAudioGraph / Dynamic Module Chain
- `rebuildAudioGraph`: dựng lại đồ thị WebAudio động theo thứ tự module chain.
- Thứ tự module: chain strip trong MasteringModal cho phép thêm/xóa/bỏ/bypass
module, thay đổi thứ tự — audio graph được rebuild tương ứng.
- Default = chain cũ (`mastering_expand.md: extension modules + dynamic chain`).
- Code: `app/static/js/app.jsx` (MODULE CHAIN STRIP dynamic).
## §II.4 Track FX Module Factory
- Factory tạo module FX theo loại (EQ, compressor, ...) cho track FX chain.
- Dùng chung cho Track FX chain editor trong track strip
(`Track FX Chain (mastering_expand.md §II.4)`).
- Code: `app/static/js/app.jsx` (Track FX module factory).
## §III ADD MODULE POPUP
- Popup thêm module mới vào chain (chọn loại module rồi append).
- Code: `app/static/js/app.jsx` (ADD MODULE POPUP trong MasteringModal).
+16
View File
@@ -0,0 +1,16 @@
# Unified FX Rack Panel
Một panel FX Rack dùng chung cho per-track (FXRackModal) và masterbus
(MasteringModal), thống nhất trải nghiệm thêm/sắp xếp FX.
## §III.1 Track-EQ Preset Library
- Thư viện preset EQ band-gains dùng cho track EQ (chỉ band gains).
- Code: `app/static/js/app.jsx` (Track-EQ preset library).
- Preset áp trực tiếp các giá trị gain band vào track EQ.
## Unified FX Rack Panel
- Panel thống nhất: mỗi track mở FXRackModal từ track strip context
(`Unified FX Rack target context: { trackId, trackName }`).
- Bao gồm: WebAudio builtin chain + VST FX chain (xem thêm
`unified_fx_rack_panel_update.md`), bypass từng slot và toàn chain.
- Code: `app/static/js/app.jsx` (UNIFIED FX RACK PANEL + FXRackModal).
+17
View File
@@ -0,0 +1,17 @@
# Unified FX Rack Panel Update — VST FX & Wave Observer
Cập nhật cho `unified_fx_rack_panel.md`: bổ sung VST FX chain và Wave Observer.
## VST FX Chain
- FXRackModal (per-track) và MasteringModal (masterbus) có section "VST FX":
thêm plugin từ `SonicAPI.listFx` (danh sách VST3/LV2 đã scan), upload preset
dạng base64, bypass toàn chain, sắp xếp lên/xuống/xóa.
- Render offline qua native_bridge `--render-fx` (xem `PLAN_MASTERBUS_FX_RACK_VST.md`).
- Serialize: track `vst_fx_chain`/`vst_fx_bypass`; master
`main_session.master.fx_chain`/`bypass`.
## §III.3 Wave Observer
- Oscilloscope real-time xem dạng sóng tín hiệu tại điểm quan sát (master / track).
- Code: `app/static/js/app.jsx` (WAVE OBSERVER — REAL-TIME OSCILLOSCOPE;
`Wave Observer scope state`; `Wave Observer scope analysers`).
- Scope: chọn master hoặc track; analysers dùng WebAudio AnalyserNode.
+109 -6
View File
@@ -3,13 +3,16 @@ project(daw_vst_bridge LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(MSVC)
string(REPLACE "/EHsc" "/EHa" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
endif()
# ── 1. VST3 SDK (Steinberg, vendored as git submodule — NOT in vcpkg) ──
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/CMakeLists.txt")
if(WIN32 AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/CMakeLists.txt")
add_subdirectory(vst3sdk EXCLUDE_FROM_ALL)
set(VST3_SDK_TARGET sdk)
else()
message(WARNING "vst3sdk submodule missing — VST3 host disabled; SF2/SF3/SFZ still build")
message(STATUS "vst3sdk build skipped (Windows-only) — VST3 host disabled; SF2/SF3/SFZ still build")
set(VST3_SDK_TARGET "")
endif()
@@ -45,53 +48,153 @@ else()
endif()
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/include
${FLUIDSYNTH_INCLUDE_DIR}
${SFIZZ_INCLUDE_DIR}
)
if(WIN32)
add_executable(daw_vst_bridge
src/main.cpp
src/RenderJob.cpp
src/RenderFxJob.cpp
src/BuiltinFxChain.cpp
src/FxGuiServer.cpp
src/NativeInstrumentEngine.cpp
src/SharedMemoryIPC.cpp
src/Vst3Instrument.cpp
src/Vst2Instrument.cpp
src/StateStore.cpp
src/SandboxVst3Host.cpp
src/SandboxVst2Host.cpp
)
else()
# Linux: offline --render only (SF2/SF3 + SFZ; VST3/VST2 hosting is
# Windows-only). main_linux.cpp is the tiny --render entry.
add_executable(daw_vst_bridge
src/main_linux.cpp
src/RenderJob.cpp
src/NativeInstrumentEngine.cpp
)
endif()
# VST3 SDK hosting: sdk_hosting lacks the platform module loader — module_win32.cpp
# is added to the bridge itself (same approach as the audiohost sample).
if(VST3_SDK_TARGET)
target_compile_definitions(daw_vst_bridge PRIVATE HAVE_VST3SDK=1)
target_link_libraries(daw_vst_bridge PRIVATE ${VST3_SDK_TARGET} sdk_hosting)
target_link_libraries(daw_vst_bridge PRIVATE ${VST3_SDK_TARGET} sdk_hosting sdk_common)
if(WIN32)
target_sources(daw_vst_bridge PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/vst/hosting/module_win32.cpp
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/common/memorystream.cpp
)
endif()
endif()
if(WIN32)
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY})
find_package(JPEG REQUIRED) # libjpeg-turbo (vcpkg) — FxGuiServer.cpp JPEG encode
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} JPEG::JPEG)
# Required Windows libs
target_link_libraries(daw_vst_bridge PRIVATE winmm)
target_link_libraries(daw_vst_bridge PRIVATE winmm imm32)
# App icon (shared favicon.ico -> exe resource)
target_sources(daw_vst_bridge PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/app.rc)
else()
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARIES} ${SFIZZ_LIBRARIES})
endif()
# ── fx_vst_bridge: VST FX host (split from daw_vst_bridge). Windows-only:
# --render-fx (offline) + --realtime-fx (SHM realtime cloud-FX loop). ──
if(WIN32)
add_executable(fx_vst_bridge
src/main_fx.cpp
src/RenderFxJob.cpp
src/RealtimeFxLoop.cpp
src/BuiltinFxChain.cpp
src/FxGuiServer.cpp
)
if(VST3_SDK_TARGET)
target_compile_definitions(fx_vst_bridge PRIVATE HAVE_VST3SDK=1)
target_link_libraries(fx_vst_bridge PRIVATE ${VST3_SDK_TARGET} sdk_hosting sdk_common)
target_sources(fx_vst_bridge PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/vst/hosting/module_win32.cpp
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/common/memorystream.cpp
)
endif()
find_package(JPEG REQUIRED) # libjpeg-turbo (vcpkg) — FxGuiServer.cpp JPEG encode
# FX modes are VST3 + builtin DSP only — no fluidsynth/sfizz (avoids a
# runtime libfluidsynth-3.dll dependency for the FX bridge).
target_link_libraries(fx_vst_bridge PRIVATE JPEG::JPEG)
# Required Windows libs
target_link_libraries(fx_vst_bridge PRIVATE winmm imm32 ws2_32)
# App icon (shared favicon.ico -> exe resource)
target_sources(fx_vst_bridge PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/app.rc)
endif()
# ── gui_probe: standalone VST3 GUI attach probe (debug tool, not shipped) ──
if(WIN32)
add_executable(gui_probe
src/gui_probe.cpp
src/NativeInstrumentEngine.cpp
src/SharedMemoryIPC.cpp
src/Vst3Instrument.cpp
src/Vst2Instrument.cpp
src/SandboxVst3Host.cpp
src/SandboxVst2Host.cpp
)
if(VST3_SDK_TARGET)
target_compile_definitions(gui_probe PRIVATE HAVE_VST3SDK=1)
target_link_libraries(gui_probe PRIVATE ${VST3_SDK_TARGET} sdk_hosting)
target_link_libraries(gui_probe PRIVATE ${VST3_SDK_TARGET} sdk_hosting sdk_common)
if(WIN32)
target_sources(gui_probe PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/vst/hosting/module_win32.cpp
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/common/memorystream.cpp
)
endif()
endif()
if(WIN32)
target_link_libraries(gui_probe PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} winmm)
endif()
add_executable(plugin_host
src/plugin_host_main.cpp
src/SharedMemoryIPC.cpp
src/Vst3Instrument.cpp
src/Vst2Instrument.cpp
)
if(VST3_SDK_TARGET)
target_compile_definitions(plugin_host PRIVATE HAVE_VST3SDK=1)
target_link_libraries(plugin_host PRIVATE ${VST3_SDK_TARGET} sdk_hosting sdk_common)
if(WIN32)
target_sources(plugin_host PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/vst/hosting/module_win32.cpp
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/common/memorystream.cpp
)
endif()
endif()
if(WIN32)
target_link_libraries(plugin_host PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} winmm)
# App icon (shared favicon.ico -> exe resource)
target_sources(plugin_host PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/app.rc)
endif()
# G3.3: seh_channel_test — SEH per-channel crash isolation (debug tool, not shipped)
add_executable(seh_channel_test
tests/seh_channel_test.cpp
src/Vst3Instrument.cpp
src/Vst2Instrument.cpp
src/SandboxVst3Host.cpp
src/SharedMemoryIPC.cpp
)
if(VST3_SDK_TARGET)
target_compile_definitions(seh_channel_test PRIVATE HAVE_VST3SDK=1)
target_link_libraries(seh_channel_test PRIVATE ${VST3_SDK_TARGET} sdk_hosting sdk_common)
if(WIN32)
target_sources(seh_channel_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/vst/hosting/module_win32.cpp
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/common/memorystream.cpp
)
endif()
endif()
if(WIN32)
target_link_libraries(seh_channel_test PRIVATE ${FLUIDSYNTH_LIBRARY} ${SFIZZ_LIBRARY} winmm)
endif()
endif() # WIN32-only debug tools (gui_probe, plugin_host, seh_channel_test)
Submodule native_bridge/JUCE added at 10a589619b
+123
View File
@@ -0,0 +1,123 @@
# ROADMAP: Xử lý triệt để như DAW hiện đại — tách UI thread khỏi audio loop
Ngày: 2026-08-15 · Repo: `C:/Users/locpham/SonicForgeStudio` · Dựa trên bằng chứng
`native_bridge/debug/BUG_REPORT.md` + source hiện tại (main.cpp, Vst3Instrument.cpp, lib.rs).
## Mục tiêu kiến trúc đích
DAW hiện đại (Cubase/Reaper/FL/Bitwig) tách:
- **Audio thread**: real-time, chỉ gọi `process()`
- **UI thread**: main UI thread, chạy `view->attached()` + message pump editor
- (tuỳ chọn) **Plugin process riêng**: sandbox crash plugin khỏi engine
Bridge hiện tại gộp 2 thứ đầu vào 1 thread (`while(true)` vừa `renderAll()` vừa
`PeekMessageW`, main.cpp:1220-1240) + attach job trên ChannelWorker → 2 thread 1
DLL → Nexus throw/hang. Roadmap này tách UI khỏi audio đúng VST3 contract.
## Nguyên tắc vận hành (áp dụng cho MỌI task)
```
1. Viết test tái hiện TRƯỚC (gui_probe variant / probe_appN / driver kịch bản)
2. Implement tối thiểu (đúng phạm vi task)
3. Chạy bộ test: gui_probe matrix + live driver + regression cũ
4. Ghi nhận bug vào log + BUG_REPORT.md (mỗi bug: kịch bản, log, nguyên nhân)
5. Fix → chạy lại → pass hết mới deploy
6. Deploy exe vào install/ + commit (theo pattern git log hiện có)
```
Không task nào merge nếu chưa: probe matrix pass + driver 3 run liên tiếp pass +
screenshot GUI không trắng.
## GIAI ĐOẠN 0 — Ổn định hiện trạng (P0, ngăn máu chảy)
Làm ngay, độc lập kiến trúc. Không phải "xử lý triệt để" nhưng là nền để làm việc
dài hạn không bị bug quấy phá.
| # | Task | File | Done khi |
|---|---|---|---|
| G0.1 | Chặn/defer OPEN_GUI khi transport PLAY (đợi STOP rồi attach; hoặc tạm dừng transport) | main.cpp handleOpenGui + Rust transport state | probe_app4_live: PLAY + OPEN_GUI → không EXCEPTION, attach sau STOP thành công |
| G0.2 | Bỏ spam `openVstGuiRetry` 20×500ms → tối đa 2 lần, có backoff, không retry khi đang PLAY | app/static/js/app.precompiled.js | bridge.log không còn chuỗi EXCEPTION/FAILED lặp; stall không xảy ra |
| G0.3 | Watchdog né giết khi attach-in-flight (flag SHM hoặc stall type riêng) | lib.rs:397 + shm | Stall do attach dài → không kill; stall thật → kill như cũ |
| G0.4 | Save/restore state nhanh (getState trước khi reload, setState sau) | Vst3Instrument.cpp | Mở GUI → đóng → mở lại: preset giữ nguyên |
## GIAI ĐOẠN 1 — Tách GUI pump khỏi audio loop (P1, lõi triệt để)
**Vấn đề hiện tại**: main thread vừa render audio vừa pump message editor.
Attach job chạy ChannelWorker (thread khác) → 2 thread 1 DLL.
**Đích**: main thread chỉ render audio. Thread UI mới (native UI thread) tạo window
+ pump message editor + chạy toàn bộ attach/close. Đúng mô hình Steinberg
`editorhost.cpp`: 1 UI thread, 1 audio thread.
| # | Task | File | Done khi |
|---|---|---|---|
| G1.1 | Thêm `UiThread` (thread riêng, COM STA, message pump riêng, job queue) | main.cpp mới class | gui_probe variant `ui_thread` tương đương bridge mới: Nexus attach OK không hang |
| G1.2 | Chuyển tạo window + attach job từ ChannelWorker → UiThread; audio loop bỏ `PeekMessageW` | main.cpp | Probe bridge arch mới: 2 instance Nexus attach OK, không HANG (so với bridge_two HANG hiện tại) |
| G1.3 | Chuyển close/resize/IME sang UiThread; bỏ pump gate cũ (g_juceTidsMutex...) nếu không còn cần | main.cpp | close editor đang PLAY không crash; không cần mute workaround |
| G1.4 | Bỏ mute-when-editor-open (g_editorOpen/g_editorPathCount) — test lại | main.cpp | 6 Nexus + GUI ch5 mở khi PLAY: track khác vẫn kêu (mute hiện tại làm track câm) |
| G1.5 | Regression: driver stress 6 Nexus + Ample + EZkeys, mở/đóng GUI khi PLAY | driver + probe | 3 run liên tiếp không crash/stall |
## GIAI ĐOẠN 2 — Persistence: restart không mất instrument (P2)
Trả lời câu hỏi "stop/play nghe lại". Hiện bridge kill = mất hết instance (state
trong memory process).
| # | Task | File | Done khi |
|---|---|---|---|
| G2.1 | `saveState()`/`restoreState()` đầy đủ (IComponentHandler/getState, preset, MIDI routing) | Vst3Instrument.cpp + NativeInstrumentEngine | Save → reload → restore: âm thanh/preset giống hệt |
| G2.2 | Bridge dump state ra file khi shutdown/trước kill (SIGHUP/SHM flag) | main.cpp + lib.rs | Kill bridge → state file có đủ 16 channel |
| G2.3 | Frontend auto-reload sau respawn: đọc state file → bridgeLoad lại từng track | app + NativeBridgeService | Giết bridge giữa PLAY → bridge mới tự nạp lại, PLAY tiếp tục, không câm |
| G2.4 | Bỏ `!isPlayingRef.current` chặn restore khi đang play | app.precompiled.js:816 | Restore chạy được cả khi PLAY |
## GIAI ĐOẠN 3 — Watchdog thông minh + health (P3)
| # | Task | File | Done khi |
|---|---|---|---|
| G3.1 | Heartbeat riêng (thread bridge ghi timestamp mỗi 100ms) tách khỏi writeIndex audio | main.cpp + shm.rs + lib.rs | Bridge sống nhưng audio stall → không kill; bridge chết → phát hiện <1s |
| G3.2 | Phân loại stall: audio (render quá lâu) vs GUI job (attach/close) vs chết | lib.rs | Mỗi loại có policy riêng (GUI: đợi, không kill) |
| G3.3 | Bridge tự phục hồi lỗi plugin: catch crash riêng channel (SEH quanh process của từng channel) | main.cpp renderAll | 1 plugin crash → mute channel đó, các channel khác chạy tiếp (giống DAW sandbox-lite) |
## GIAI ĐOẠN 4 — Sandbox plugin process riêng (P4, tuỳ chọn tham vọng)
VST3 không có chuẩn out-of-process như AUv3 → cần wrapper tự làm. Đây là bước xa
nhất, chỉ làm khi G1-G3 chưa đủ.
| # | Task | File | Done khi |
|---|---|---|---|
| G4.1 | Nghiên cứu: host plugin trong process con (VST3 host trong exe thứ 2), audio qua SHM | thiết kế mới | PoC 1 plugin Nexus trong process con, GUI + audio OK |
| G4.2 | Bridge → client process pool (1 process/plugin hoặc /nhóm) | thiết kế mới | Plugin crash → process con chết, bridge + GUI sống |
| G4.3 | IPC audio real-time (SHM ring buffer double-buffer) | thiết kế mới | 6 Nexus song song, latency < 20ms, không xrun |
**Cân nhắc**: G4 là dự án riêng (ước lượng gấp 3-5× G1+G2). Khuyến nghị: làm G0-G3
trước; G4 chỉ khi cần cô lập crash plugin thật sự (hiện G3.3 + G1 đã lo được 90%
kịch bản bug).
## Thứ tự thực hiện đề xuất
```
G0.1 → G0.2 → G0.3 → G0.4 (1-2 tuần, an toàn ngay, trả lời "stop/play nghe lại" một phần qua G0.4)
↓
G1.1 → G1.2 → G1.3 → G1.4 → G1.5 (3-5 tuần, lõi triệt để — mở GUI khi PLAY thành công)
↓
G2.1 → G2.2 → G2.3 → G2.4 (2-3 tuần, restart không câm)
↓
G3.1 → G3.2 → G3.3 (1-2 tuần, health đầy đủ)
↓
G4.* (tuỳ chọn, 2-3 tháng)
```
## Bắt đầu ngay — task đầu tiên
**G0.1**: sửa `handleOpenGui` (main.cpp:943) — nếu transport PLAY, đưa OPEN_GUI
vào `g_pendingGui` (đã có cơ chế defer cho "chưa load"), sweep xử lý khi transport
STOP. Kèm test: `probe_app4_live.py` biến thể "PLAY rồi OPEN_GUI rồi STOP" — kỳ
vọng không EXCEPTION, GUI attach sau STOP.
## Files liên quan hiện tại
- `native_bridge/src/main.cpp` — loop (1220), handleOpenGui (943), ChannelWorker, mute/close
- `native_bridge/src/Vst3Instrument.cpp` — reload (615), reloadForGUI (528), attachView (550)
- `native_bridge/src/gui_probe.cpp` — probe variants (bridge_two 243, two_workers_close 318)
- `native_bridge/src/lib.rs` → thực tế `src-tauri/src/lib.rs` — watchdog (356-430), kill/spawn (50,135)
- `app/static/js/app.precompiled.js` — openVstGuiRetry (470), restore (816)
- `native_bridge/debug/` — BUG_REPORT.md, sweep_bridge_two_8s.json, probe_attach_detail.py, probe_app4_live.py
+122
View File
@@ -0,0 +1,122 @@
# BÁO CÁO BUG: Mở VST GUI khi đang PLAY → dự án câm, GUI không load
Ngày: 2026-08-15 · Repo: `C:/Users/locpham/SonicForgeStudio` · git status sạch
## 1. Tóm tắt
Nguyên nhân gốc: **nhiều thread cùng vào 1 plugin DLL trong lúc `view->attached()`**.
Khi đang PLAY, audio loop `process()` chạy trên các instance Nexus (ch0-4) cùng lúc
worker thread của ch5 chạy `attachView()`. Nexus throw exception trong
`createView/attached` → attach FAILED → frontend retry ~30 lần (0.3s/lần) →
`bridgeWriteIndex` stall 3s → Tauri watchdog kill+respawn bridge → **mất hết
instrument → câm toàn bộ**. GUI không bao giờ load được.
Không phải "instrument track khác bị reset" (giả thuyết 1 sai ở bước này — reload
reset state chỉ xảy ra khi GUI ĐÃ mở thành công rồi đóng rồi mở lại, không phải lần
attach đầu).
## 2. Chuỗi bug đầy đủ (xác nhận live, bridge.log)
```
... 6x Nexus loaded ch0-5 + EZkeys ch6 ...
[NativeBridge] transport PLAY playhead=0
[NativeBridge] GUI attach EXCEPTION ch=5 — plugin threw (createView/attached)
[NativeBridge] GUI attach FAILED hwnd=0000000002250F36 plugin=Nexus
[tauri] bridge restarted (attempt 1) ← watchdog: writeIndex stall 3s
[NativeBridge] Starting DAW Host Bridge Engine... ← restart, mất hết instrument
```
1. `handleOpenGui` (main.cpp ~943): OPEN_GUI ch5 → silence mọi channel cùng
plugin path → `g_loadMutex` → `try { reloadForGUI(); attachView(hwnd); } catch(...)`.
2. `attachView` (Vst3Instrument.cpp:550): `controller->createView(kEditor)` →
`view->attached(hwnd, kPlatformTypeHWND)` → **Nexus throw** khi đang PLAY
(audio loop process() cùng DLL đang chạy trên instance khác).
3. catch → `ok=false` → log EXCEPTION + FAILED → PostMessage WM_CLOSE (GUI không load).
4. Frontend `openVstGuiRetry` spam lại ~30 lần; mỗi lần chạy lại job (silence +
mutex + reload/attach) → main loop bị nghẽn → `bridgeWriteIndex` stall 3s.
5. `lib.rs`: stall >3s → `kill_bridge` + `spawn_bridge` (restart_attempts==0) →
mất toàn bộ instrument state → **CÂM**.
## 3. Bằng chứng thực nghiệm
### 3.1 Probe kiến trúc chính xác (bridge_two: 2 worker STA riêng + main pump)
`gui_probe.exe <plugin> bridge_two <secs>` — kết quả 76 VST3 hệ thống (sweep):
`native_bridge/debug/sweep_bridge_two_8s.json`, `probe_attach_detail.py` (bổ sung).
⚠️ **Hiệu chỉnh sweep cũ**: exit code 0xC0000409 của TẤT CẢ 76 plugin là
**artifact của probe** — `ProbeWorker::th` không join → `~std::thread` trên
joinable thread → `std::terminate` → fail-fast. RESULT line in ra nhưng không flush
kịp. Không phải 76 plugin cùng crash teardown.
Phân loại đúng dựa trên output per-attach:
| Nhóm | Số plugin | Bằng chứng | Ý nghĩa |
|---|---|---|---|
| **HANG attach inst2** | 30 | `workerA openGUI inst1=1` rồi `workerB openGUI inst2` in ra, **không có kết quả** — Nexus 25s không về | 2 instance cùng DLL, 2 apartment → attach instance 2 HANG trong `view->attached()` |
| **BOTH-OK** | 20 | `workerB openGUI inst2=1` — Kontakt, BlueARP, Ozone 11 (18) | Attach 2 instance OK; chỉ crash teardown artifact |
| **HANG/crash attach inst1** | 18 | `workerA hwnd=` in ra, không in `openGUI inst1` — RX 11 (18) + ACTP | Chết ngay instance 1 |
| **CRASH EARLY** | 8 | exit 1.2-4.6s, 0xC0000005 (AV): Melodyne, Orb Arpeggios/Melody, smartEQ3/live, smartreverb, DUNE3, ACDD | Crash thật trong attach |
Không plugin nào HANG >18s (không WATCHDOG-KILL) — main pump cứu được các trường
hợp cần pump; vấn đề là **conflict thread/apartment khi 2 instance cùng DLL**.
### 3.2 Nexus: hang khi 2 instance — OK khi 1 thread
- `bridge_two` (2 worker STA, 2 instance Nexus): inst1 attach OK, **inst2 HANG 25s+** (xác nhận bằng run 25s).
- `same_thread` (load + attach cùng 1 thread): **OK** (returned=1).
- `main_own` (load+attach main thread): **OK** (RESULT done=1 attached_ok=1).
- `two_workers_close` (đóng editor inst1 trước khi attach inst2, có closeGUI + attach qua temp thread): Nexus **HANG ngay attach inst1**; Kontakt chạy hết, OK cả 2.
→ 2 thread/apartment cùng 1 DLL = conflict thật (không riêng Nexus: 30 plugin hang
inst2, 8 crash AV). Kontakt/Ozone/BlueARP chịu được.
### 3.3 Giả thuyết (a) reload mất state — XÁC NHẬN từ code
`Vst3Instrument::reload()` (Vst3Instrument.cpp:615): `closeGUI()` + terminate +
`delete state_` + `loadPlugin(path_)` **MỚI** → preset/state mất khi mở lại GUI
(reopen). Kích hoạt bởi `reloadForGUI()` khi `hasAttachedOnce_ && !guiAttached_`
(đã từng mở GUI rồi đóng). Không phải nguyên nhân của bug câm lần này (attach đầu
chưa thành công nên chưa tới reload), nhưng là bug phụ: **mở GUI → đóng → mở lại =
mất preset/state của track**.
## 4. Kết luận nguyên nhân
1. **Root cause**: attach GUI chạy worker thread trong khi audio loop `process()`
đang chạy instance cùng DLL (2+ thread 1 DLL). Nexus throw → attach fail.
Probe không audio: biểu hiện hang (inst2); app thật có audio: biểu hiện
exception (ch5). Cùng gốc.
2. **Khuếch đại**: frontend retry spam 30× → stall 3s → watchdog tưởng bridge chết
→ restart → mất hết instrument → câm. Đây là lý do dự án chết hoàn toàn, không
chỉ mất GUI.
3. `g_loadMutex` + silence hiện có chưa đủ: không đợi renderAll thoát hẳn instance
cùng DLL trước khi `createView`, và Nexus vẫn throw ngay cả khi đã silence
(silence chỉ ngăn process gọi tiếp, không chặn instance đang nằm trong process()).
## 5. Đề xuất fix (xếp theo ưu tiên)
1. **Chặn/defer OPEN_GUI khi transport PLAY** — đơn giản nhất, an toàn nhất:
nếu đang PLAY, đợi transport STOP (hoặc tự tạm dừng) trước khi attach, resume
sau. Loại bỏ hoàn toàn conflict audio-loop × attachView. (`ponytail:` người dùng
muốn mở GUI khi đang chơi thì nâng cấp thành pause-resume tự động, hoặc đợi
`setProcessing(false)` + đợi renderAll thoát.)
2. **Giới hạn retry**: bỏ `openVstGuiRetry` spam; thử tối đa 2-3 lần có backoff.
3. **Watchdog đừng giết khi đang attach GUI**: phân biệt stall do GUI job dài (có
attach-in-flight flag) với stall do bridge chết thật; nâng ngưỡng stall khi có
attach đang chạy.
4. **Fix state reset**: `reload()` nên save/restore state (`getState`/`setState`)
thay vì load instance mới mất preset. (Bug phụ, độc lập.)
5. (Dài hạn) 1 apartment chung cho mọi instance cùng DLL (`shared_worker` pattern
đã có trong probe) — loại conflict nhiều-thread-1-DLL triệt để.
## 6. Files liên quan
- `native_bridge/src/main.cpp` — handleOpenGui (943), silence/close-other (1015-1090), attach job + catch (1132-1160)
- `native_bridge/src/Vst3Instrument.cpp` — reload() (615), reloadForGUI() (528), attachView() (550), closeGUI() (640)
- `native_bridge/src/gui_probe.cpp` — probe bridge_two (243), two_workers_close (318)
- `native_bridge/debug/` — sweep_all_gui.py, sweep_bridge_two_8s.json, probe_attach_detail.py, probe_app4_live.py, probe_app4.py
- Bridge log: `%AppData%/SonicForgeDAW/logs/bridge.log`
Trạng thái live hiện tại: app câm (bridge 21932 sống, không instrument, transport
STOP). Test thêm bằng `probe_app4_live.py` sẽ lại làm hư session — cần xác nhận
trước khi chạy tiếp.
+113
View File
@@ -0,0 +1,113 @@
# G0 — Baseline đo latency (kết quả)
Ngày: 2026-08-27. Probe: `native_bridge/debug/g0_latency_probe.py` (chạy với
`timeBeginPeriod(1)`, 10k block/config, pipeline thật Python→SHM→fx_vst_bridge→SHM).
Log: `native_bridge/debug/g0_probe_run_full.log` (run 1),
`native_bridge/debug/g0_probe_run2.log` (run 2 — có `pump_rate` + drain đầy đủ).
## 1. Bảng latency plugin (getLatencySamples, @48kHz)
| Plugin | getLatencySamples | ms @48k | Ghi chú |
|---|---|---|---|
| Sonible pureComp | 2108 | 43.9 | báo latency thật (lookahead) |
| iZotope Ozone 11 | 0 | 0 | không báo latency (realtime mode) |
| empty (passthrough) | — | — | chain rỗng |
Nguồn: probe `plugin_latency` + log app thật `fxrt_bridge.log`
(`[RenderFx] STEP getLatencySamples = 2108/0`).
## 2. Jitter round-trip (10k block, 53.3s/config)
Round-trip = thời gian từ lúc probe gọi `write_input(block j)` đến lúc nhận
`read_output()` block j (FIFO theo index). Đã sửa pacing probe bằng
`timeBeginPeriod(1)` (bridge C++ đã gọi — `RealtimeFxLoop.cpp:160`); lần chạy
trước thiếu nó → Sleep 15.6ms granularity → pump ~22ms/batch ≠ 21.33ms realtime
→ pureComp bị "stall 1.9s" giả. Với pacing chuẩn, stall đó biến mất — là
artifact probe, KHÔNG phải VST stall.
| Config | mean ms | σ | p50 | p99 | max | recv/sent | pump_rate | drained |
|---|---|---|---|---|---|---|---|---|
| empty | 21.67 | 0.88 | 21.70 | 22.03 | 26.51 | 10000/10000 | 184.8 | ✅ |
| pureComp | 21.69 | 0.15 | 21.67 | 22.03 | 24.07 | 10000/10000 | 184.9 | ✅ |
| Ozone 11 | 21.70 | 0.14 | 21.68 | 22.02 | 22.20 | 10000/10000 | 184.8 | ✅ |
- Round-trip ≈ 21.5–21.7ms = đúng 1 batch 4 block (21.33ms) + overhead nhỏ
(SHM copy + bridge proc 0.001–0.45ms). Latency plugin KHÔNG nằm trong
round-trip — plugin latency bù bằng PDC (không chặn pipeline).
- **`drained=True` mọi config (run 2): bridge xử lí HẾT 10000 block, recv = sent
— KHÔNG mất block, KHÔNG backlog tích lũy.** Đây là bằng chứng quyết định:
không có drift thật (xem §4).
- σ round-trip: 0.14–0.15ms (pureComp/Ozone) — RẤT nhỏ → **Gate G2 MỞ**: PDC
mẫu có ý nghĩa, không cần giảm jitter trước (không cần relay WS→SHM sớm).
- empty σ=0.88: 1 outlier khởi động (min=0.05ms), p50/p99 vẫn 21.7/22.0.
## 3. Inter-arrival deviation
| Config | σ ms | min | max |
|---|---|---|---|
| empty | 9.44 | −5.33 | 38.4 |
| pureComp | 9.36 | −5.33 | 23.5 |
| Ozone 11 | 9.36 | −5.33 | 17.8 |
Block về theo batch 4 (bridge xử lí take=4/lần) → inter-arrival lệch so với
5.33ms lý tưởng là đặc tính batching, không phải jitter ngẫu nhiên. Round-trip
σ (§2) là thước đo đúng cho PDC; inter-arrival dùng cho sizing FILL — xem §5.
## 4. Bridge FxRTPerf
| Nguồn | procAvg | procMax | loopAvg | loopMax | rate blk/s | vs realtime 187.5 |
|---|---|---|---|---|---|---|
| probe empty | 0.001ms | 0.06ms | 22.0ms | 1017ms | 181.5 | −3.2% |
| probe pureComp | 0.46ms | 1.37ms | 22.0ms | 1011ms | 181.6 | −3.1% |
| probe Ozone | 0.14ms | 0.71ms | 22.0ms | 1015ms | 181.5 | −3.2% |
| app thật (master chain đầy) | 3.4ms | 10.0ms | 21.4ms | 156ms | 186.5 | −0.5% |
- **Rate "chậm 3% (probe) / 0.5% (app)" KHÔNG phải drift thật — artifact đo.**
Bằng chứng: run 2 `drained=True`, recv=sent=10000 mọi config — nếu bridge
thật sự chậm 3%, backlog 300+ block phải tích lại và drain phải mất >1.5s
sau pump (thực tế drain xong ngay trong vòng lặp pump, `pump_rate` chỉ thấp
vì probe tự sleep lệch pha với nhịp bridge poll 1ms + granularity sleep).
Bridge xử lí ASAP khi có input (`avail>0` → process ngay) — không có pace
riêng để "chậm"; output rate = input rate. App thật rate 186.5 thấp hơn vì
runSec gồm idle trước khi audio chảy (chain load, chờ play).
- **loopMax ≈ 1s ở MỌI config probe kể cả empty** — spike 1s/lần chạy ~53s,
không phải VST (empty cũng có). Không xuất hiện trong log app thật gần nhất
(loopMax 156ms) — nghi Windows scheduler/antivirus/power, cần theo dõi khi
chạy thật. Probe round-trip max vẫn ≤27ms → spike xảy ra lúc bridge idle,
không chặn block đang xử lí (không gây underrun).
- **Kết luận: KHÔNG cần bù drift.** FILL 8 block + underrun-fade/resume hiện
có đủ — u=3/session (fx_stats) hiếm, do main-thread stall (GUI), không phải
drift đều. Đề xuất resync/bù rate ban đầu rút lại (xem §6).
## 5. ~0.22s gồm gì (xác nhận)
Latency path thực đo + hằng số app:
| Thành phần | ms | Nguồn |
|---|---|---|
| Batching 4 block (pipeline) | ~21.6 | probe round-trip (§2) |
| FILL buffer worklet 8 block | 42.7 | `sf-fx-realtime.js` FILL=8 |
| Plugin latency (pureComp) | 43.9 | `getLatencySamples=2108` @48k |
| WS/main-thread jitter (warm, p50) | ~101 | dbg.log `fx_play_first` sincePlay 38–160ms |
| **Tổng nghe thấy (warm)** | **~210ms ≈ 0.21s** | khớp ~0.22s quan sát |
- PDC hiện tại `FX_RT_ROUNDTRIP_SAMPLES=3840` (80ms) = batching ~7 block +
FILL 8 block — khớp thực đo (21.6 + 42.7 ≈ 64ms + WS jitter margin). Giữ
nguyên; `latencyTotal` (plugin thật qua `{cmd:'latency'}`) cộng thêm đúng
(pureComp +43.9ms).
- Cold start VST load: 8–17s (dbg.log sincePlay 8111–17211ms), warm sau đó
38–160ms — là plugin init, không phải latency pipeline; không nằm trong PDC.
- fx_stats app thật (session master): u=3 h=3 f=13 s=13 cap=1 fi=13 q=5–11 —
underrun hiếm, queue duy trì 5–11 block → FILL đủ hấp thụ jitter thường.
## 6. Kết luận cho G2+
1. **Gate G2: MỞ.** σ round-trip 0.14–0.15ms → PDC mẫu có nghĩa; không cần
relay WS→SHM trước G2.
2. **Không có drift bridge** (đã kiểm chứng run 2: drained=True, recv=sent).
Không cần bù rate/resync — FILL 8 + underrun-fade/resume đủ. Tránh thêm
code không cần thiết. Nếu sau này thấy underrun đều trên session dài →
đo lại `pump_rate`/`drained` trước khi kết luận drift.
3. **loopMax ~1s spike** (probe, cả empty): theo dõi, chưa thấy ở app thật.
4. Plugin latency bảng §1 đủ cho G2: `graph.getLatencySamples()` sẽ thay hằng
số; dữ liệu hiện tại xác nhận pureComp là plugin duy nhất có latency thật.
+56
View File
@@ -0,0 +1,56 @@
# G1 — JUCE engine SHM chain rỗng POC (kết quả)
Ngày: 2026-08-28. Commit: `9c0d35d`. Plan: `JUCE_LATENCY_PLAN.md` G1.
## Nội dung
- Submodule `native_bridge/JUCE` = tag `8.0.8`... thực tế tag `8.0.7`
(shallow, `.gitmodules` branch=8.0.7) — khớp pattern vst3sdk sẵn có.
- Tách SHM dùng chung: `native_bridge/include/FxShm.h` (namespace `fxshm`,
`ShmView`/`openShm`/`closeShm` — Windows FileMapping); `RealtimeFxLoop.cpp`
chuyển sang dùng chung (không đổi hành vi bridge cũ).
- Project mới `native_bridge/juce_fx/`: `juce_fx_bridge` dùng
`juce::AudioProcessorGraph`, chain rỗng = input node → output node
(passthrough). `prepare(sr, block)` → prepareToPlay(block*4) vì batch tối đa
4 block; `process(inL, inR, n)` copy vào `AudioBuffer` → `graph.processBlock`
→ copy về in-place. `latencySamples()` = `graph.getLatencySamples()`.
- Loop C++: batching 4 block, out-ring backpressure, heartbeat 100ms,
`timeBeginPeriod(1)` + ABOVE_NORMAL, log `[JuceFxLoop]/[JuceFxPerf]`; validate
header mỗi iteration — sampleRate/blockSize đổi → re-prepare + report latency
qua lat ring; job `fx_chain` BỎ QUA (G1 chain rỗng).
- Python: `app/core/fx_realtime.py` `_spawn()` — env `SF_JUCE_FX_BRIDGE_PATH`
chọn bridge JUCE (`--juce-fx`); mặc định giữ nguyên `fx_vst_bridge
--realtime-fx`. Exe copy: `src-tauri/binaries/juce_fx_bridge-x86_64-pc-windows-msvc.exe`.
## Kết quả probe
Probe `native_bridge/debug/g0_latency_probe.py`, 10k block/config, pipeline
thật Python→SHM→juce_fx_bridge→SHM, `timeBeginPeriod(1)`, env
`SF_JUCE_FX_BRIDGE_PATH` trỏ exe JUCE. (3 config trong probe đều chạy qua JUCE
bridge; G1 chain rỗng nên pureComp/Ozone cũng passthrough — kiểm chứng không
crash, không lag.)
| Config | mean ms | σ | p50 | p99 | max | recv/sent | pump_rate | drained | latency |
|---|---|---|---|---|---|---|---|---|---|
| empty | 21.670 | 0.636 | 21.649 | 22.087 | 24.031 | 10000/10000 | 184.9 | ✅ | {0:0} |
| pureComp | 21.686 | 0.168 | 21.656 | 22.073 | 23.399 | 10000/10000 | 184.9 | ✅ | {0:0} |
| Ozone 11 | 21.674 | 0.157 | 21.640 | 22.049 | 22.277 | 10000/10000 | 185.0 | ✅ | {0:0} |
So sánh baseline G0 (empty): mean 21.67ms — không lag thêm, không crackle
(drained=True, recv=sent, interarrival mean ~0.07ms). Log bridge:
`[JuceFxLoop] prepared sr=48000 block=256 latency=0` → ready → exit.
## G1 done-when
- ✅ `--juce-fx` chạy với chain rỗng
- ✅ Python `fx_realtime.py` chơi qua nó bằng path cũ (env opt-in, mặc định cũ
không đổi) — regression pass
## Ghi chú
- Lỗi build gặp phải: `NodeAndChannel`/`Connection` không aggregate-init được
trên MSVC 2019 — phải gán field từng cái; `addNode` trả `Node::Ptr` → lấy
`->nodeID` (không gán Ptr vào `NodeID`).
- JUCE modules build lần đầu chậm (~34s config); `build_juce/` đã gitignore.
- `[JuceFxPerf]` heartbeat không xuất hiện trong probe ngắn (chạy <100ms);
probe chỉ grep tag cũ `[FxRTPerf]` — không phải lỗi.
+58
View File
@@ -0,0 +1,58 @@
# G3 — 1 clock: PDC latency thật + playhead engine-derived (kết quả)
Ngày: 2026-08-28. Plan: `JUCE_LATENCY_PLAN.md` G3. Option A — 1 clock.
## Nội dung
- **Bridge báo processed samples** qua lat ring slot đặc biệt `0xFFFFFFFE`
(`JuceFxLoop.cpp`, mỗi block sau `++processed`): `samples = processed*n`
(n=curBlock). Python `_lat_drain` tách slot này riêng → `self.processed`
(KHÔNG vào `latencies` dict — tổng chain không đổi). Thêm `get_processed()`.
- **WS latency message thêm `processed`** (`plugins.py`): gửi khi `processed`
đổi (mỗi block ~5.33ms), bỏ gate chỉ theo total — total chỉ đổi khi chain đổi.
- **Playhead engine-derived** (`app.jsx`): khi FX active và có anchor,
`position = startOffset + (processed - anchor - latencyTotal)/sr` (đã trừ
latency → bám vị trí mẫu NGHE được; pre-roll tự giữ tại startOffset qua
clamp). Anchor = raw processed tại play/wrap start (`fxRtAnchorProcessed`
gọi từ `__fxRtPlayheadFreeze`, mọi play/resume path đều đi qua). Bỏ
freeze/snap khi active; fallback wall-clock khi không active. Áp dụng 4 chỗ:
bridge position block, sub-tab `_ph`, sub-tab `wallTime`, main branch
`_playheadSec` → `setCurrentTime`.
- **Worklet nhận latency thật** qua port message `{type:'latency', samples}`;
`clear_queue('loop')`: `wrapFadeBlocks = outQueue.length +
ceil(latencySamples/BLOCK)` — align processed-tail, crossfade đúng seam
end→start kể cả khi FX latency (pureComp 2108 mẫu) còn in-flight trong bridge.
## Verify (đã chạy)
1. **Build**: `cmake --build native_bridge/build_juce --config Release --target
juce_fx_bridge` OK. Copy `src-tauri/binaries/juce_fx_bridge-x86_64-pc-windows-msvc.exe`,
SHA256 khớp bản build (`07621b81...`).
2. **Probe SHM thật** (`native_bridge/debug/g3_probe_check.py`, JUCE bridge qua
`SF_JUCE_FX_BRIDGE_PATH`, chain pureComp, SR 48k, 400 block ~2.1s):
- `latencies` dict KHÔNG đổi: `{0: 2108}` trước/sau — processed tách riêng
không làm sai tổng chain.
- `processed` tiến 0 → 102400 (= 400 block × 256 mẫu) — bridge báo đúng.
3. **Cú pháp**: `python -m py_compile` fx_realtime.py + plugins.py OK;
`node --check` worklet OK; `babel` (node_modules) parse app.jsx OK (node
thuần không hiểu JSX — dùng babel, exit 0).
4. **Fake session** (object.__new__, không shm): `get_processed()` → None,
`get_latencies()` → `{0: 2108}` — path không-SHM an toàn.
## Done-when trạng thái
- ✅ processed tách riêng, tổng latency chain không đổi (probe)
- ✅ playhead công thức engine-derived đã cài đủ 4 chỗ; freeze/snap vô hiệu khi
active (code path)
- ⏳ **Chưa verify bằng tai**: 100-pass loop không click với crossfade tối
thiểu; playhead bám vị trí mẫu trong app thật (cần user chạy GUI + nghe).
## Ghi chú
- Probe check script giữ ở `native_bridge/debug/g3_probe_check.py` (chạy lại:
`python native_bridge/debug/g3_probe_check.py`).
- BufferError lúc thoát (`cannot close exported pointers`) là quirk teardown
shared_memory Python 3.13, không ảnh hưởng kết quả.
- Latency WS ~5.33ms/msg = 187 msg/s — nhỏ so với pump audio; nếu cần tiết
kiệm sau này: batch processed 4 block/lần (giảm 4×) — `ponytail: chỉ làm khi
có bằng chứng quá tải`.
+6
View File
@@ -0,0 +1,6 @@
$ErrorActionPreference = 'Stop'
cd 'C:/Users/locpham/SonicForgeStudio/native_bridge/build'
cmake -DCMAKE_CXX_FLAGS_RELEASE='/O2 /Ob2 /DNDEBUG /Zi' -DCMAKE_EXE_LINKER_FLAGS_RELEASE='/DEBUG' .
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
cmake --build . --config Release --target daw_vst_bridge
exit $LASTEXITCODE
+6
View File
@@ -0,0 +1,6 @@
@echo off
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\Build\vcvars64.bat" >nul 2>&1
cd /d C:\Users\locpham\SonicForgeStudio\native_bridge
cl /nologo /std:c++17 /EHsc /I include /I vestige tests\shm_selfcheck.cpp /Fe:debug\shm_selfcheck_new.exe
if errorlevel 1 exit /b 1
debug\shm_selfcheck_new.exe
+6
View File
@@ -0,0 +1,6 @@
$ErrorActionPreference = 'Stop'
cd 'C:/Users/locpham/SonicForgeStudio/native_bridge/build'
cmake -DCMAKE_CXX_FLAGS_RELEASE='/O2 /Ob2 /DNDEBUG /Zi' -DCMAKE_EXE_LINKER_FLAGS_RELEASE='/DEBUG' .
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
cmake --build . --config Release --target gui_probe
exit $LASTEXITCODE
+760
View File
@@ -0,0 +1,760 @@

⨪⨪⨪⨪⨪⨪‪牐灥牡湩⁧桴⁥湥楶潲浮湥⁴潦⁲敄畢杧牥䔠瑸湥楳湯⁳慇汬牥⁹敲潰楳潴楲獥⨠⨪⨪⨪⨪⨪⨪�
†䔠瑸湥楳湯敒潰楳潴祲㨠䤠灭楬楣�
†唠敳硅数楲敭瑮污敆瑡牵䙥牯畎敧却慨敲㨠琠畲�
†䄠汬睯畎敧䕴數灕慤整㨠琠畲�
†丠湯湉整慲瑣癩乥杵瑥㨠琠畲�
†䄠汬睯畎敧䵴䍓敲敤瑮慩偬潲楶敤䥲獮慴汬㨠琠畲�
†䄠汬睯慐慲汬汥湉瑩慩楬慺楴湯晏潌慣剬灥獯瑩牯敩⁳›牴敵
†ⴠ‭潃普杩牵湩⁧敲潰楳潴楲獥
†††ⴭⴭ‾敒潰楳潴祲㨠䰠捯污湉瑳污敬Ɽ䔠慮汢摥›牴敵
†††ⴭⴭ‾敒潰楳潴祲㨠唠敳䕲瑸湥楳湯ⱳ䔠慮汢摥›牴敵
㸾㸾㸾㸾㸾㸾‾牐灥牡湩⁧桴⁥湥楶潲浮湥⁴潦⁲敄畢杧牥䔠瑸湥楳湯⁳慇汬牥⁹敲潰楳潴楲獥挠浯汰瑥摥‬畤慲楴湯〠〮〰猠捥湯獤
⨪⨪⨪⨪⨪⨪‪慗瑩湩⁧潦⁲敄畢杧牥䔠瑸湥楳湯⁳慇汬牥⁹潴䤠楮楴污穩⁥⨪⨪⨪⨪⨪⨪⨪
㸾㸾㸾㸾㸾㸾‾慗瑩湩⁧潦⁲敄畢杧牥䔠瑸湥楳湯⁳慇汬牥⁹潴䤠楮楴污穩⁥潣灭敬整Ɽ搠牵瑡潩⸰㄰‶敳潣摮�
†ⴠⴭ㸭删灥獯瑩牯⁹›獕牥硅整獮潩獮‬湅扡敬㩤琠畲ⱥ倠捡慫敧⁳潣湵㩴〠
†ⴠⴭ㸭删灥獯瑩牯⁹›潌慣䥬獮慴汬摥‬湅扡敬㩤琠畲ⱥ倠捡慫敧⁳潣湵㩴㐠�
楍牣獯景⁴刨
楗摮睯⁳敄畢杧牥嘠牥楳湯ㄠ⸰⸰㤲ㄶ⸷〱〰䄠䑍㐶
潃祰楲桧⁴挨
楍牣獯景⁴潃灲牯瑡潩⹮䄠汬爠杩瑨⁳敲敳癲摥�
潃浭湡䱤湩㩥⸠杜極灟潲敢攮數∠㩃停潲牧浡䘠汩獥䍜浯潭楆敬屳卖㍔乜硥獵瘮瑳∳猠慨敲彤潷歲牥ㄠ�
⨪⨪⨪⨪⨪⨪‪慐桴瘠污摩瑡潩畳浭牡⁹⨪⨪⨪⨪⨪⨪⨪
敒灳湯敳††††††††††††吠浩⁥洨⥳††䰠捯瑡潩�
敄敦牲摥†††††††††††††††††††猠癲�
祓扭汯猠慥捲⁨慰桴椠㩳猠癲�
硅捥瑵扡敬猠慥捲⁨慰桴椠㩳�
⴫ⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭ⬭
⁼桔獩琠牡敧⁴畳灰牯獴䠠牡睤牡ⵥ湥潦捲摥匠慴正倠潲整瑣潩⹮䄠䠠⁗慢敳⁤†簠
⁼匢慨潤⁷瑓捡≫洠祡戠⁥癡楡慬汢⁥潴愠獳獩⁴湩搠扥杵楧杮愠摮愠慮祬楳⹳†簠
⁼敓⁥歡⹡獭甯敳桲灳映牯洠牯⁥湩潦‮††††††††††††††††††簠
⁼†††††††††††††††††††††††††††††††††††簠
⁼灤⁳獀灳†††††††††††††††††††††††††††††††簠
⁼†††††††††††††††††††††††††††††††††††簠
⴫ⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭⴭ⬭
潍䱤慯㩤〠〰㜰晦怷散㙤〰〰〠〰㜰晦怷晣戳〶〰†朠極灟潲敢攮數
潍䱤慯㩤〠〰㜰晦恦敥㘱〰〰〠〰㜰晦恦敥挳〶〰†渠摴汬搮汬
潍䱤慯㩤〠〰㜰晦恦摥㉡〰〰〠〰㜰晦恦摥敡〹〰†䌠尺䥗䑎坏屓祓瑳浥㈳䭜剅䕎㍌⸲䱄�
潍䱤慯㩤〠〰㜰晦恦扥挷〰〰〠〰㜰晦恦扥扢て〰†䌠尺䥗䑎坏屓祓瑳浥㈳䭜剅䕎䉌十⹅汤�
潍䱤慯㩤〠〰㜰晦恦敥攰〰〰〠〰㜰晦恦敥ㄱ〲〰†䌠尺䥗䑎坏屓祓瑳浥㈳䥜䵍㈳搮汬
潍䱤慯㩤〠〰㜰晦恦扥戴〰〰〠〰㜰晦恦扥搴〷〰†䌠尺䥗䑎坏屓祓瑳浥㈳睜湩㈳⹵汤�
潍䱤慯㩤〠〰㜰晦恦捥攱〰〰〠〰㜰晦恦捥愳〶〰†䌠尺䥗䑎坏屓祓瑳浥㈳啜䕓㍒⸲汤�
潍䱤慯㩤〠〰㜰晦恦捥㐶〰〰〠〰㜰晦恦捥㘶ぢ〰†䌠尺䥗䑎坏屓祓瑳浥㈳䝜䥄㈳搮汬
潍䱤慯㩤〠〰㜰晦恦扥摢〰〰〠〰㜰晦恦扥晣〹〰†䌠尺䥗䑎坏屓祓瑳浥㈳杜楤㈳畦汬搮汬
潍䱤慯㩤〠〰㜰晦恥㝣㤴〰〰〠〰㜰晦恥㝣㜵〴〰†䌠尺獕牥屳潬灣慨屭潓楮䙣牯敧瑓摵潩湜瑡癩彥牢摩敧扜極摬剜汥慥敳汜扩汦極獤湹桴㌭搮汬
潍䱤慯㩤〠〰㜰晦恦扥㜳〰〰〠〰㜰晦恦扥ㄴ〳〰†䌠尺䥗䑎坏屓祓瑳浥㈳浜癳灣睟湩搮汬
潍䱤慯㩤〠〰㜰晦恦扥攴〰〰〠〰㜰晦恦扥㈶っ〰†䌠尺䥗䑎坏屓祓瑳浥㈳畜牣扴獡⹥汤�
潍䱤慯㩤〠〰㜰晦恦捥慦〰〰〠〰㜰晦恦摥㈷〸〰†䌠尺䥗䑎坏屓祓瑳浥㈳卜䕈䱌㈳搮汬
潍䱤慯㩤〠〰㜰晦恦摥㠸〰〰〠〰㜰晦恦摥ㅡ〹〰†䌠尺䥗䑎坏屓祓瑳浥㈳潜敬㈳搮汬
潍䱤慯㩤〠〰㜰晦恦捥㍡〰〰〠〰㜰晦恦捥扡〲〰†䌠尺䥗䑎坏屓祓瑳浥㈳坜㉓㍟⸲汤�
潍䱤慯㩤〠〰㜰晦恦捥愶〰〰〠〰㜰晦恦捥㉡〵〰†䌠尺䥗䑎坏屓祓瑳浥㈳捜浯慢敳搮汬
潍䱤慯㩤〠〰㜰晦恦挱㍤〰〰〠〰㜰晦恦挱捤〴〰†䌠尺䥗䑎坏屓奓呓䵅㈳䑜体乕⹄汤�
潍䱤慯㩤〠〰㜰晦恦㑥搸〰〰〠〰㜰晦恦㑥〹〶〰†䌠尺䥗䑎坏屓奓呓䵅㈳坜义䵍搮汬
潍䱤慯㩤〠〰〰㌲恤㌲㌴〰〰〠〰〰㌲恤㌲㐵〸〰†䌠尺䥗䑎坏屓祓瑳浥㈳剜䍐呒⸴汤�
潍䱤慯㩤〠〰㜰晦恦捥㝢〰〰〠〰㜰晦恦捥㡣〸〰†䌠尺䥗䑎坏屓祓瑳浥㈳剜䍐呒⸴汤�
潍䱤慯㩤〠〰㜰晦恦扥㉦〰〰〠〰㜰晦恦扥摦っ〰†䌠尺䥗䑎坏屓祓瑳浥㈳慜癤灡㍩⸲汤�
潍䱤慯㩤〠〰㜰晦恦挱㝣〰〰〠〰㜰晦恦挱㉤〷〰†䌠尺䥗䑎坏屓奓呓䵅㈳剜獥浡汰䑥潭䐮䱌
潍䱤慯㩤〠〰㜰晦恦捥㥣〰〰〠〰㜰晦恦捥㍤〹〰†䌠尺䥗䑎坏屓祓瑳浥㈳浜癳牣⹴汤�
潍䱤慯㩤〠〰㜰晦恦㉣愵〰〰〠〰㜰晦恦㉣㌶つ〰†䌠尺䥗䑎坏屓奓呓䵅㈳䵜噓偃㐱⸰汤�
潍䱤慯㩤〠〰㜰晦恦㉣㐶〰〰〠〰㜰晦恦㉣㘶っ〰†䌠尺䥗䑎坏屓奓呓䵅㈳噜剃乕䥔䕍㐱⸰汤�
潍䱤慯㩤〠〰㜰晦恦捥捥〰〰〠〰㜰晦恦捥㥦〹〰†䌠尺䥗䑎坏屓祓瑳浥㈳作䕌啁㍔⸲汤�
潍䱤慯㩤〠〰㜰晦恦捥捡〰〰〠〰㜰晦恦捥㙢ち〰†䌠尺䥗䑎坏屓祓瑳浥㈳獜捥潨瑳搮汬
潍䱤慯㩤〠〰㜰晦恦㑤昲〰〰〠〰㜰晦恦㑤昲っ〰†䌠尺䥗䑎坏屓奓呓䵅㈳噜剃乕䥔䕍㐱弰⸱汤�
潍䱤慯㩤〠〰㜰晦恦摥㌷〰〰〠〰㜰晦恦摥㈸〹〰†䌠尺䥗䑎坏屓祓瑳浥㈳獜捨牯⹥汤�
潍䱤慯㩤〠〰㜰晦恦ㅡ㘸〰〰〠〰㜰晦恦ㅡ愸〳〰†䌠尺䥗䑎坏屓奓呓䵅㈳䵜噓偃㐱弰⸲汤�
潍䱤慯㩤〠〰㜰晦恦㌷㜱〰〰〠〰㜰晦恦㌷㜱づ〰†䌠尺䥗䑎坏屓奓呓䵅㈳浜摳潭搮汬
潍䱤慯㩤〠〰㜰晦恦扥搱〰〰〠〰㜰晦恦扥㈲づ〰†䌠尺䥗䑎坏屓奓呓䵅㈳灜睯灲潲⹦汤�
潍䱤慯㩤〠〰㜰晦恦㡣昲〰〰〠〰㜰晦恦㡣㈳〷〰†䌠尺䥗䑎坏屓奓呓䵅㈳睜湩浭慢敳搮汬
潍䱤慯㩤〠〰〰㌲恤㌲㌴〰〰〠〰〰㌲恤㌲㠴づ〰†䌠尺䥗䑎坏屓奓呓䵅㈳灜睯灲潲⹦汤�
潍䱤慯㩤〠〰㜰晦恦慥摥〰〰〠〰㜰晦恦慥㉦〲〰†䌠尺䥗䑎坏屓奓呓䵅㈳捜杦杭㍲⸲汤�
潍䱤慯㩤〠〰㜰晦恦扥戱〰〰〠〰㜰晦恦扥挱〵〰†䌠尺䥗䑎坏屓奓呓䵅㈳啜偍䍄搮汬
潍䱤慯㩤〠〰㜰晦恦慥㈰〰〰〠〰㜰晦恦慥㌰ぢ〰†䌠尺䥗䑎坏屓奓呓䵅㈳歜牥敮⹬灡捰牯⹥汤�
潍䱤慯㩤〠〰㜰晦恦扥〷〰〰〠〰㜰晦恦扥愷っ〰†䌠尺䥗䑎坏屓祓瑳浥㈳扜牣灹側楲業楴敶⹳汤�
潍䱤慯㩤〠〰㜰晦恦㡥愴〰〰〠〰㜰晦恦㡥㐵〸〰†䌠尺䥗䑎坏屓祳瑳浥㈳畜瑸敨敭搮汬
潍䱤慯㩤〠〰㜰晦恦捥㜰〰〰〠〰㜰晦恦捥挱つ〰†䌠尺䥗䑎坏屓祓瑳浥㈳䵜䍓䙔搮汬
睯䡮湷㵤〰〰〰〰〰䍁〱䔲
潬摡湩⁧㩃停潲牧浡䘠汩獥䍜浯潭楆敬屳卖㍔乜硥獵瘮瑳�
潍䱤慯㩤〠〰㜰晦恥㥣攱〰〰〠〰㜰晦恥慣攵〵〰†䌠尺牐杯慲楆敬屳潃浭湯䘠汩獥噜呓尳敎畸⹳獶㍴
潍䱤慯㩤〠〰㜰晦恦捥㜶〰〰〠〰㜰晦恦捥㤶〰〰†䌠尺䥗䑎坏屓祓瑳浥㈳楜慭敧汨⹰汤�
潍䱤慯㩤〠〰㜰晦恦捥㑤〰〰〠〰㜰晦恦捥㍥〵〰†䌠尺䥗䑎坏屓祓瑳浥㈳䍜䵏䱄㍇⸲汤�
潍䱤慯㩤〠〰㜰晦恦㡥搵〰〰〠〰㜰晦恦㡥〷ち〰†䌠尺䥗䑎坏屓奓呓䵅㈳摜杸⹩汤�
潍䱤慯㩤〠〰㜰晦恦摥扦〰〰〠〰㜰晦恦敥㄰〶〰†䌠尺䥗䑎坏屓祓瑳浥㈳卜䱈䅗䥐搮汬
潍䱤慯㩤〠〰㜰晦恦扥㈴〰〰〠〰㜰晦恦扥愴〳〰†䌠尺䥗䑎坏屓祓瑳浥㈳坜义剔单⹔汤�
潍䱤慯㩤〠〰㜰晦恦㕣〳〰〰〠〰㜰晦恦㕣戳〵〰†䌠尺䥗䑎坏屓楗卮卸慜摭㐶浟捩潲潳瑦眮湩潤獷挮浯潭⵮潣瑮潲獬㙟㤵戵㐶㐱挴晣搱彦⸵㈸㈮ㄶ〰㠮㈳弸潮敮㡟攷摥㥥㜵㉡捣摤層佃䍍䱔㈳搮汬
潍䱤慯㩤〠〰㜰晦恦㍣㌴〰〰〠〰㜰晦恦㍣挶〵〰†䌠尺䥗䑎坏屓奓呓䵅㈳坜义义呅搮汬
潍䱤慯㩤〠〰㜰晦恦扤昶〰〰〠〰㜰晦恦扤昶ぢ〰†䌠尺䥗䑎坏屓奓呓䵅㈳噜剅䥓乏搮汬
潍䱤慯㩤〠〰㜰晦恦扥つ〰〰〠〰㜰晦恦扥㝥づ〰†䌠尺䥗䑎坏屓祓瑳浥㈳䍜奒呐㈳搮汬
潍䱤慯㩤〠〰㜰晦恦扥晦〰〰〠〰㜰晦恦捥㘰〳〰†䌠尺䥗䑎坏屓祓瑳浥㈳坜䑌偁㈳搮汬
潍䱤慯㩤〠〰㜰晦恦捥㌶〰〰〠〰㜰晦恦捥㌶〸〰†䌠尺䥗䑎坏屓祓瑳浥㈳乜牯慭楬⹺汤�
潍䱤慯㩤〠〰㜰晦恦扥㘲〰〰〠〰㜰晦恦扥㠲ち〰†䌠尺䥗䑎坏屓奓呓䵅㈳扜牣灹⹴汤�
潍䱤慯㩤〠〰㜰晦恦慥㑡〰〰〠〰㜰晦恦慥㕡〳〰†䌠尺䥗䑎坏屓奓呓䵅㈳䵜䅓乓⸱汤�
潍䱤慯㩤〠〰㜰晦恦㡥づ〰〰〠〰㜰晦恦㥥㤶ぢ〰†䌠尺䥗䑎坏屓奓呓䵅㈳睜湩潤獷献潴慲敧搮汬
潍䱤慯㩤〠〰㜰晦恦㡥㜳〰〰〠〰㜰晦恦㡥㜳つ〰†䌠尺䥗䑎坏屓奓呓䵅㈳獜捥牵㈳搮汬
潍䱤慯㩤〠〰㜰晦恦慥搲〰〰〠〰㜰晦恦慥ㄳ〹〰†䌠尺䥗䑎坏屓奓呓䵅㈳卜偓䍉䥌䐮䱌
潍䱤慯㩤〠〰㜰晦恦㥥搹〰〰〠〰㜰晦恦㥥ち〳〰†䌠尺䥗䑎坏屓奓呓䵅㈳楜桰灬灡⹩汤�
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
潍䱤慯㩤〠〰㜰晦恦慥戵〰〰〠〰㜰晦恦慥ㄶっ〰†䌠尺䥗䑎坏屓祳瑳浥㈳浜睳潳正搮汬
潍䱤慯㩤〠〰㜰晦恦㥥㙡〰〰〠〰㜰晦恦㥥扢〸〰†䌠尺䥗䑎坏屓奓呓䵅㈳䑜华偁⹉汤�
潍䱤慯㩤〠〰㜰晦恦慥戳〰〰〠〰㜰晦恦慥戳つ〰†䌠尺䥗䑎坏屓奓呓䵅㈳䑜偓剁䕓搮汬
潍䱤慯㩤〠〰㜰晦恦摥晡〰〰〠〰㜰晦恦摥晡ち〰†䌠尺䥗䑎坏屓祓瑳浥㈳乜䥓搮汬
潍䱤慯㩤〠〰㜰晦恦扤晡〰〰〠〰㜰晦恦扤晡ぢ〰†䌠尺楗摮睯屳祓瑳浥㈳牜獡摡汨⹰汤�
潍䱤慯㩤〠〰㜰晦恦摤㙢〰〰〠〰㜰晦恦摤敢〶〰†䌠尺䥗䑎坏屓祓瑳浥㈳晜灷捵湬⹴汤�
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
〲㐺㨱㔰›剅⁒ⴠ䘠楡敬⁤潴搠睯汮慯㩤栠瑴獰⼺愯瑵⹨敲硦砮浯氯杯湩港⼴䌠畯摬渠瑯爠獥汯敶栠獯㩴愠瑵⹨敲硦砮浯嬠崶
潍䱤慯㩤〠〰㜰晦恦㥥㕥〰〰〠〰㜰晦恦㥥ㅦ〵〰†䌠尺䥗䑎坏屓祳瑳浥㈳獜档湡敮⹬䱄�
噛瑳䤳獮牴浵湥嵴氠慯敤⁤㩃停潲牧浡䘠汩獥䍜浯潭楆敬屳卖㍔乜硥獵瘮瑳″丨硥獵
畯䍴㵨�
獛慨敲彤潷歲牥⁝潬摡椠獮ㅴㄽ
潍䱤慯㩤〠〰㜰晦恦㝥㔰〰〰〠〰㜰晦恦㝥㠶ち〰†䌠尺䥗䑎坏屓奓呓䵅㈳摜搲⸱汤�
潍䱤慯㩤〠〰㜰晦恦㙥敤〰〰〠〰㜰晦恦㝥㐰〷〰†䌠尺䥗䑎坏屓奓呓䵅㈳䑜牗瑩⹥汤�
潍䱤慯㩤〠〰㜰晦恦㝥㤶〰〰〠〰㜰晦恦㝥昸〰〰†䌠尺䥗䑎坏屓奓呓䵅㈳摜搳ㄱ搮汬
潍䱤慯㩤〠〰㜰晦恦攸〵〰〰〠〰㜰晦恦攸慡づ〰†䌠尺䥗䑎坏屓奓呓䵅㈳䑜䐳〱慗灲搮汬
潍䱤慯㩤〠〰㜰晦恦㡥㠵〰〰〠〰㜰晦恦㡥挵〵〰†䌠尺䥗䑎坏屓奓呓䵅㈳摜捸牯⹥汤�
潍䱤慯㩤〠〰㜰晦恦㡥扡〰〰〠〰㜰晦恦㡥捡〴〰†䌠尺䥗䑎坏屓奓呓䵅㈳牜獥畯捲灥汯捩捹楬湥⹴汤�
潍䱤慯㩤〠〰㜰晦恦㕥㑣〰〰〠〰㜰晦恦㕥㥣て〰†䌠尺䥗䑎坏屓奓呓䵅㈳摜物捥硴慤慴慢敳敨灬牥搮汬
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数⹲汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲⤱琠摩㘨㘹⤰㠠〰〷〰′桔⁥祳瑳浥挠湡潮⁴楦摮琠敨映汩⁥灳捥晩敩⹤
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩摜物捥硴慤慴慢敳敨灬牥挮灰㈨㤲⤰摜物捥硴慤慴慢敳敨灬牥搮汬〡〰㜰䙆䕆䌵㈵ㅁ㩁⠠慣汬牥›〰〰䘷䙆㡅㔶㤴䔴
敒畴湲版㈨
楴⡤㤶〶
〸㜰〰㈰吠敨猠獹整慣湮瑯映湩⁤桴⁥楦敬猠数楣楦摥�
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数⹲汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲⤳琠摩㘨㘹⤰㠠〰〷〰′桔⁥祳瑳浥挠湡潮⁴楦摮琠敨映汩⁥灳捥晩敩⹤
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数⹲汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲⤴琠摩㘨㘹⤰㠠〰〷〰′桔⁥祳瑳浥挠湡潮⁴楦摮琠敨映汩⁥灳捥晩敩⹤
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数⹲汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲⤵琠摩㘨㘹⤰㠠〰〷〰′桔⁥祳瑳浥挠湡潮⁴楦摮琠敨映汩⁥灳捥晩敩⹤
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数⹲汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲⤶琠摩㘨㘹⤰㠠〰〷〰′桔⁥祳瑳浥挠湡潮⁴楦摮琠敨映汩⁥灳捥晩敩⹤
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数⹲汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲⤷琠摩㘨㘹⤰㠠〰〷〰′桔⁥祳瑳浥挠湡潮⁴楦摮琠敨映汩⁥灳捥晩敩⹤
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数⹲汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲⤸琠摩㘨㘹⤰㠠〰〷〰′桔⁥祳瑳浥挠湡潮⁴楦摮琠敨映汩⁥灳捥晩敩⹤
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数⹲汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲⤹琠摩㘨㘹⤰㠠〰〷〰′桔⁥祳瑳浥挠湡潮⁴楦摮琠敨映汩⁥灳捥晩敩⹤
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数⹲汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲〱
楴⡤㤶〶
〸㜰〰㈰吠敨猠獹整慣湮瑯映湩⁤桴⁥楦敬猠数楣楦摥�
潍䱤慯㩤〠〰㜰晦恦慥㑢〰〰〠〰㜰晦恦慥㝢〰〰†䌠尺䥗䑎坏屓奓呓䵅㈳湜牣灹⹴汤�
潍䱤慯㩤〠〰㜰晦恦慥晡〰〰〠〰㜰晦恦慥㉢て〰†䌠尺䥗䑎坏屓奓呓䵅㈳乜䅔乓⸱汤�
潍䱤慯㩤〠〰㜰晦恦挹ㅢ〰〰〠〰㜰晦恦挹㕢〸〰†䌠尺䥗䑎坏屓祳瑳浥㈳湜牣灹獴汳⹰汤�
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
〲㐺㨱㤰›义但ⴠ丠硥獵㐠㔮ㄮ‷挨牵⸸⸴⤰
〲㐺㨱㤰›义但ⴠㄠ‵畁⁧〲㘲
〲㐺㨱㤰›义但ⴠ䌠浯異整⁲慎敭›䕄䭓佔ⵐ䐴㌳㝇�
〲㐺㨱㤰›义但ⴠ䌠湯整瑮瀠瑡㩨䘠尺啍䥓彃体呆䅗䕒牜䙥⁘‭敎畸⁳‴潃灭敬整乜硥獵䰠扩慲祲
〲㐺㨱㤰›义但ⴠ䠠獯㩴䌠尺獕牥屳潬灣慨屭潓楮䙣牯敧瑓摵潩湜瑡癩彥牢摩敧扜極摬剜汥慥敳杜極灟潲敢攮數
潍䱤慯㩤〠〰㜰晦恦慥㔱〰〰〠〰㜰晦恦慥㠱〶〰†䌠尺䥗䑎坏屓奓呓䵅㈳湜浴牡慴搮汬
潍䱤慯㩤〠〰㜰晦恦敥㈰〰〰〠〰㜰晦恦敥挰づ〰†䌠尺䥗䑎坏屓祓瑳浥㈳捜扬慣煴搮汬
潍䱤慯㩤〠〰㜰晦恦捤〵〰〰〠〰㜰晦恦捤㔷〰〰†䌠尺楗摮睯屳祓瑳浥㈳瑜楷慮楰愮灰潣敲搮汬
潍䱤慯㩤〠〰㜰晦恦ㅣ㐸〰〰〠〰㜰晦恦ㅣ㤸っ〰†䌠尺䥗䑎坏屓祳瑳浥㈳摜瑡敡捸慨杮⹥汤�
湯捥牯履楷摮睯屳楤敲瑣屸慤慴慢敳桜汥数汲扩慲祲汜扩灜牥灡異敳獲瑥楴杮煳敵祲浩汰挮灰ㄨ㔳尩楤敲瑣摸瑡扡獡桥汥数⹲汤Ⅼ〰〰䘷䙆㕅㕃㑁㘷›挨污敬㩲〠〰㜰䙆䕆䌵䄵㈳⥃删瑥牵䡮⡲ㄱ
楴⡤㤶〶
〸㜰〰㈰吠敨猠獹整慣湮瑯映湩⁤桴⁥楦敬猠数楣楦摥�
獛慨敲彤潷歲牥⁝瑡慴档椠獮ㅴㄽ栠湷㵤〰〰〰〰㌰ぁ䔰㠷
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
噛瑳䤳獮牴浵湥嵴氠慯敤⁤㩃停潲牧浡䘠汩獥䍜浯潭楆敬屳卖㍔乜硥獵瘮瑳″丨硥獵
畯䍴㵨�
獛慨敲彤潷歲牥⁝潬摡椠獮㉴ㄽ
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
㜨〵⹣㤶〶㨩䌠⬫䔠⁈硥散瑰潩‭潣敤攠㘰㝤㘳″昨物瑳挠慨据⥥
獛慨敲彤潷歲牥⁝瑡慴档椠獮㉴ㄽ栠湷㵤〰〰〰〰㈰㔷䘰㠲
睛異灭⁝獭㵧砰ㄳ⁆睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
潍䱤慯㩤〠〰㜰晦恦慡㕤〰〰〠〰㜰晦恦慡㥥〶〰†䌠尺䥗䑎坏屓奓呓䵅㈳瑜硥楴灮瑵牦浡睥牯⹫汤�
睛異灭⁝獭㵧砰ㄳ⁆睨摮〽〰〰〰〰䄳〰㝅�
睛異灭⁝獭㵧砰ㄳ⁆睨摮〽〰〰〰〰㜲〵㉆�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰ㄱ″睨摮〽〰〰〰〰䌴〴㍂�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
䕒啓呌›潤敮ㄽ愠敒㵳‱剢獥ㄽ
睛異灭⁝獭㵧砰㜴⁂睨摮〽〰〰〰〰䘰ㅃ㜱�
㜨〵⹣㔶㑣㨩匠捥牵瑩⁹档捥慦汩牵⁥牯猠慴正戠晵敦⁲癯牥畲‭潣敤挠〰〰〴‹ℨ℡猠捥湯⁤档湡散℠℡�
畓换摯㩥〠㝸䘠十彔䅆䱉䙟呁䱁䅟偐䕟䥘⁔
捵瑲慢敳愡潢瑲〫㑸㩥
〰〰昷晦敠㕢㔸㜲⁥摣㤲††††††湩⁴††㤲�
㨰〰㸰挠扤›敒摡湩⁧湩瑩慩潣浭湡⁤朧�
䅗乒义㩇䌠湯楴畮湩⁧⁡潮⵮潣瑮湩慵汢⁥硥散瑰潩�
㜨〵⹣㔶㑣㨩匠捥牵瑩⁹档捥慦汩牵⁥牯猠慴正戠晵敦⁲癯牥畲‭潣敤挠〰〰〴‹ℨ℡猠捥湯⁤档湡散℠℡�
畓换摯㩥〠㝸䘠十彔䅆䱉䙟呁䱁䅟偐䕟䥘⁔
捵瑲慢敳愡潢瑲〫㑸㩥
〰〰昷晦敠㕢㔸㜲⁥摣㤲††††††湩⁴††㤲�
+11
View File
@@ -0,0 +1,11 @@
$ErrorActionPreference = 'Continue'
$install = 'C:\Users\locpham\SonicForgeStudio\install'
$new = 'C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe'
Get-Process sonicforge-daw, daw_engine, daw_vst_bridge -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 2
Copy-Item -Path $new -Destination (Join-Path $install 'daw_vst_bridge.exe') -Force
Write-Output "copied: $((Get-Item (Join-Path $install 'daw_vst_bridge.exe')).Length) bytes"
Start-Process -FilePath (Join-Path $install 'sonicforge-daw.exe')
Start-Sleep -Seconds 8
Get-Process sonicforge-daw, daw_engine, daw_vst_bridge -ErrorAction SilentlyContinue | Select-Object Id, ProcessName, Path | Format-Table -AutoSize
@@ -0,0 +1 @@
'��o\x00w\x00n\x00H\x00w\x00n\x00d\x00=\x000\x000\x000\x000\x000\x000\x000\x000\x000\x000\x00A\x007\x000\x003\x005\x004\x00\n\x00\n\x00l\x00o\x00a\x00d\x00i\x00n\x00g\x00 \x00C\x00:\x00\\\x00P\x00r\x00o\x00g\x00r\x00a\x00m\x00 \x00F\x00i\x00l\x00e\x00s\x00\\\x00C\x00o\x00m\x00m\x00o\x00n\x00 \x00F\x00i\x00l\x00e\x00s\x00\\\x00V\x00S\x00T\x003\x00\\\x00N\x00e\x00x\x00u\x00s\x00.\x00v\x00s\x00t\x003\x00\n\x00\n\x00[\x00V\x00s\x00t\x003\x00I\x00n\x00s\x00t\x00r\x00u\x00m\x00e\x00n\x00t\x00]\x00 \x00l\x00o\x00a\x00d\x00e\x00d\x00 \x00C\x00:\x00\\\x00P\x00r\x00o\x00g\x00r\x00a\x00m\x00 \x00F\x00i\x00l\x00e\x00s\x00\\\x00C\x00o\x00m\x00m\x00o\x00n\x00 \x00F\x00i\x00l\x00e\x00s\x00\\\x00V\x00S\x00T\x003\x00\\\x00N\x00e\x00x\x00u\x00s\x00.\x00v\x00s\x00t\x003\x00 \x00(\x00N\x00e\x00x\x00u\x00s\x00)\x00 \x00o\x00u\x00t\x00C\x00h\x00=\x002\x00\n\x00\n\x00[\x00s\x00h\x00a\x00r\x00e\x00d\x00_\x00w\x00o\x00r\x00k\x00e\x00r\x00]\x00 \x00l\x00o\x00a\x00d\x00 \x00i\x00n\x00s\x00t\x001\x00=\x001\x00\n\x00\n\x00[\x00s\x00h\x00a\x00r\x00e\x00d\x00_\x00w\x00o\x00r\x00k\x00e\x00r\x00]\x00 \x00a\x00t\x00t\x00a\x00c\x00h\x00 \x00i\x00n\x00s\x00t\x001\x00=\x001\x00 \x00h\x00w\x00n\x00d\x00=\x000\x000\x000\x000\x000\x000\x000\x000\x000\x000\x001\x003\x000\x00B\x00B\x002\x00\n\x00\n\x00[\x00V\x00s\x00t\x003\x00I\x00n\x00s\x00t\x00r\x00u\x00m\x00e\x00n\x00t\x00]\x00 \x00l\x00o\x00a\x00d\x00e\x00d\x00 \x00C\x00:\x00\\\x00P\x00r\x00o\x00g\x00r\x00a\x00m\x00 \x00F\x00i\x00l\x00e\x00s\x00\\\x00C\x00o\x00m\x00m\x00o\x00n\x00 \x00F\x00i\x00l\x00e\x00s\x00\\\x00V\x00S\x00T\x003\x00\\\x00N\x00e\x00x\x00u\x00s\x00.\x00v\x00s\x00t\x003\x00 \x00(\x00N\x00e\x00x\x00u\x00s\x00)\x00 \x00o\x00u\x00t\x00C\x00h\x00=\x002\x00\n\x00\n\x00[\x00s\x00h\x00a\x00r\x00e\x00d\x00_\x00w\x00o\x00r\x00k\x00e\x00r\x00]\x00 \x00l\x00o\x00a\x00d\x00 \x00i\x00n\x00s\x00t\x002\x00=\x001\x00\n\x00\n\x00[\x00s\x00h\x00a\x00r\x00e\x00d\x00_\x00w\x00o\x00r\x00k\x00e\x00r\x00]\x00 \x00a\x00t\x00t\x00a\x00c\x00h\x00 \x00i\x00n\x00s\x00t\x002\x00=\x001\x00 \x00h\x00w\x00n\x00d\x00=\x000\x000\x000\x000\x000\x000\x000\x000\x000\x000\x004\x001\x000\x008\x00D\x002\x00\n\x00\n\x00[\x00w\x00p\x00u\x00m\x00p\x00]\x00 \x00m\x00s\x00g\x00=\x000\x00x\x003\x001\x00F\x00 \x00h\x00w\x00n\x00d\x00=\x000\x000\x000\x000\x000\x000\x000\x000\x000\x000\x00A\x000\x000\x003\x009\x00E\x00\n\x00\n\x00[\x00w\x00p\x00u\x00m\x00p\x00]\x00 \x00m\x00s\x00g\x00=\x000\x00x\x003\x001\x00F\x00 \x00h\x00w\x00n\x00d\x00=\x000\x000\x000\x000\x000\x000\x000\x000\x000\x000\x001\x003\x000\x00B\x00B\x002\x00\n\x00\n\x00[\x00w\x00p\x00u\x00m\x00p\x00]\x00 \x00m\x00s\x00g\x00=\x000\x00x\x004\x007\x00B\x00 \x00h\x00w\x00n\x00d\x00=\x000\x000\x000\x000\x000\x000\x000\x000\x000\x000\x00A\x000\x000\x003\x009\x00E\x00\n\x00\n\x00[\x00w\x00p\x00u\x00m\x00p\x00]\x00 \x00m\x00s\x00g\x00=\x000\x00x\x003\x001\x00F\x00 \x00h\x00w\x00n\x00d\x00=\x000\x000\x000\x000\x000\x000\x000\x000\x000\x000\x004\x001\x000\x008\x00D\x002\x00\n\x00\n\x00[\x00w\x00p\x00u\x00m\x00p\x00]\x00 \x00m\x00s\x00g\x00=\x000\x00x\x004\x007\x00B\x00 \x00h\x00w\x00n\x00d\x00=\x000\x000\x000\x000\x000\x000\x000\x000\x000\x000\x00A\x000\x000\x003\x009\x00E\x00\n\x00\n\x00R\x00E\x00S\x00U\x00L\x00T\x00:\x00 \x00d\x00o\x00n\x00e\x00=\x001\x00 \x00a\x00R\x00e\x00s\x00=\x001\x00 \x00b\x00R\x00e\x00s\x00=\x001\x00\n\x00\n\x00E\x00X\x00I\x00T\x00=\x000\x00 \x00E\x00L\x00A\x00P\x00S\x00E\x00D\x00=\x006\x00.\x007\x007\x004\x000\x004\x005\x001\x00\n\x00\n\x00'
+175
View File
@@ -0,0 +1,175 @@
# Driver verify: SHM + spawn real bridge, reproduce user flow:
# load 6x Nexus -> open GUI ch=5 -> play notes -> switch instrument ch=5 (crash point)
# -> reopen GUI -> verify alive + writeIndex advancing + no crash.
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
SHM_NAME = "SonicForge_DAW_IPC_VERIFY"
SIZE = 32768 # bigger than struct, safe
BRIDGE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe"
CDB = r"C:\Users\locpham\AppData\Local\Microsoft\WindowsApps\cdbX64.exe"
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "driver_cdb.log")
NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3"
AMPLE = r"C:\Program Files\Common Files\VST3\Ample Sound\ACDD.vst3"
kernel32 = ctypes.windll.kernel32
kernel32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_char_p]
kernel32.CreateFileMappingA.restype = ctypes.c_void_p
kernel32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_size_t]
kernel32.MapViewOfFile.restype = ctypes.c_void_p
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
PAGE_READWRITE = 0x04
FILE_MAP_ALL_ACCESS = 0xF001F
FILE_MAP_WRITE = 0x0002
FILE_MAP_READ = 0x0004
hMap = kernel32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, PAGE_READWRITE, 0, SIZE, SHM_NAME.encode())
if not hMap:
print("CreateFileMapping failed", ctypes.get_last_error()); sys.exit(1)
ptr = kernel32.MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0)
if not ptr:
print("MapViewOfFile failed", ctypes.get_last_error()); sys.exit(1)
def w32(off, fmt, *args):
ctypes.memmove(ptr + off, struct.pack(fmt, *args), struct.calcsize(fmt))
def write_control(type_, arg0, arg1, ch, path):
# find first free slot
for i in range(8):
off = 2836 + i * 1040
t = ctypes.c_uint32.from_address(ptr + off).value
if t == 0:
w32(off, 'IIII', type_, arg0, arg1, ch)
if path:
b = path.encode('utf-8')[:1023] + b'\x00'
ctypes.memmove(ptr + off + 16, b, len(b))
return i
raise RuntimeError("control queue full")
def submit_controls():
w32(11156, 'I', 0) # will set below
# reset type of all slots first
for i in range(8):
w32(2836 + i * 1040, 'I', 0)
def push_controls(events):
submit_controls()
n = 0
for (type_, arg0, arg1, ch, path) in events:
write_control(type_, arg0, arg1, ch, path)
n += 1
w32(11156, 'I', n)
def midi(cmd, ch, pitch, vel, data2=0, data3=0):
# bridge does not zero midiQueue after drain - always write slot 0, count 1
w32(2064, 'BBBBBB', cmd, ch, pitch, vel, data2, data3)
w32(2064 + 8, 'I', 0)
w32(2832, 'I', 1)
return True
def midi_count():
return ctypes.c_uint32.from_address(ptr + 2832).value
def write_index():
return ctypes.c_uint32.from_address(ptr + 4).value
def wait_log(pattern, timeout, since=0):
end = time.time() + timeout
lines = []
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
except FileNotFoundError:
pass
last = max(0, len(lines) - since)
while time.time() < end:
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
except FileNotFoundError:
lines = []
for i in range(last, len(lines)):
if pattern in lines[i]:
return lines[i].strip()
last = max(0, len(lines))
time.sleep(0.2)
return None
proc = subprocess.Popen(
[CDB, "-g", "-G", "-o", "-c", ".ecxr; .dump /ma C:\\Users\\locpham\\AppData\\Local\\Temp\\procdump\\dumps\\cdb_crash.dmp; !analyze -v; k L40; q", BRIDGE, "--shm", SHM_NAME],
env={**os.environ, "SF_SHM_NAME": SHM_NAME, "SF_PARENT_PID": str(os.getpid()),
"SF_SAMPLE_RATE": "48000", "SF_BLOCK_SIZE": "256"},
stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT,
)
time.sleep(1.5)
print("bridge pid", proc.pid, "poll", proc.poll())
# 1. Load 6 channels Nexus
print("[1] load 6x Nexus...")
push_controls([(2, 0, 0, ch, NEXUS) for ch in range(6)])
print(" wait ch=5 loaded...")
r = wait_log("instrument loaded ch=5", 90)
print(" ", r)
# 2. Open GUI ch=5
print("[2] open GUI ch=5...")
push_controls([(4, 0, 0, 5, "nexus")])
r = wait_log("GUI attached", 30)
print(" ", r)
# 3. Play notes on ch=5 and ch=2 while editor open
print("[3] play notes...")
for k in range(40):
midi(0x9, 5, 60 + (k % 12), 100)
midi(0x9, 2, 48 + (k % 7), 90)
if k % 2 == 0:
midi(0x8, 5, 60 + ((k - 1) % 12), 0)
time.sleep(0.05)
# 4. Switch instrument ch=5 while GUI open (OLD CRASH POINT)
print("[4] switch instrument ch=5 -> Ample ACDD while GUI open...")
t0 = time.time()
push_controls([(2, 0, 0, 5, AMPLE)])
r = wait_log("instrument loaded ch=5", 40)
print(" ", r, "in", round(time.time() - t0, 1), "s")
alive = proc.poll() is None
wi = write_index()
time.sleep(2)
wi2 = write_index()
print(" alive:", alive, "writeIndex", wi, "->", wi2, "advancing:", wi2 > wi)
# 5. Reopen GUI ch=5 (after switch) - attach path again
print("[5] reopen GUI ch=5...")
push_controls([(4, 0, 0, 5, "ample")])
r = wait_log("GUI attached", 40)
print(" ", r)
# 6. Final liveness check + crash check
time.sleep(3)
alive = proc.poll() is None
wi3 = write_index()
time.sleep(2)
wi4 = write_index()
print("FINAL alive:", alive, "writeIndex", wi3, "->", wi4, "advancing:", wi4 > wi3)
if not alive:
print("!!! BRIDGE CRASHED exit", proc.returncode)
print("--- last 30 log lines ---")
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
for l in lines[-30:]:
print(l.rstrip())
except FileNotFoundError:
pass
# cleanup
kernel32.UnmapViewOfFile(ptr)
kernel32.CloseHandle(hMap)
try:
proc.terminate()
except Exception:
pass
time.sleep(0.5)
print("done (cdb)")

Some files were not shown because too many files have changed in this diff Show More