diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 8f67ebb..f00c3a2 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -218,7 +218,7 @@ const _sfWasmFallbackNote = (track, pitch, velocity, durationMs, startTime, key, const bank = (eng && eng.soundfont_bank) || 0; const program = (eng && eng.soundfont_program) || (track && track.instrumentProgram !== undefined ? track.instrumentProgram : 0); const k = key || (track ? track.id : 'global'); - if (_nativeSfPreviews[k] && token != null && _nativeSfPreviews[k].token !== token) return; // stale + if (_nativeSfPreviews[k] && token != null && _nativeSfPreviews[k].token > token) return; // stale (claim-then-compare: chi bo neu co note MOI HON) const S = window.SonicSF; if (!S) return; const ch = (track && track.midiChannel != null) ? track.midiChannel : 0; @@ -264,7 +264,7 @@ const playNativeSfNote = (track, pitch, velocity, durationMs, startTime, key) => _sfWasmFallbackNote(track, pitch, velocity, durationMs, startTime, k, token); return; } - if (_nativeSfPreviews[k] && _nativeSfPreviews[k].token !== token) return; // stale + if (_nativeSfPreviews[k] && _nativeSfPreviews[k].token > token) return; // stale (claim-then-compare: chi bo neu co note MOI HON) window.__nativeSfOk = true; const audio = new Audio(API_BASE_URL + res.url); _nativeSfPreviews[k] = { audio: audio, token: token }; diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 2099619..224e639 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -49,9 +49,9 @@ const _nativeSfPreviews={};// WASM fallback: backend pyfluidsynth render HONG tr // native path cam lang. Fallback choi note TRUC TIEP qua SonicSF (Web Audio // + libfluidsynth WASM da bundle) - am ra _gainNode -> masterBus.input. // window.__nativeSfOk: undefined (chua biet) | true (native OK) | false (dung WASM). -const _sfWasmFallbackNote=(track,pitch,velocity,durationMs,startTime,key,token)=>{try{const eng=track&&track.synth_engine;const sfId=eng&&eng.soundfont_id||(track&&track.instrumentId&&String(track.instrumentId).startsWith('sf_')?track.instrumentId:null);if(!sfId)return;const bank=eng&&eng.soundfont_bank||0;const program=eng&&eng.soundfont_program||(track&&track.instrumentProgram!==undefined?track.instrumentProgram:0);const k=key||(track?track.id:'global');if(_nativeSfPreviews[k]&&token!=null&&_nativeSfPreviews[k].token!==token)return;// stale +const _sfWasmFallbackNote=(track,pitch,velocity,durationMs,startTime,key,token)=>{try{const eng=track&&track.synth_engine;const sfId=eng&&eng.soundfont_id||(track&&track.instrumentId&&String(track.instrumentId).startsWith('sf_')?track.instrumentId:null);if(!sfId)return;const bank=eng&&eng.soundfont_bank||0;const program=eng&&eng.soundfont_program||(track&&track.instrumentProgram!==undefined?track.instrumentProgram:0);const k=key||(track?track.id:'global');if(_nativeSfPreviews[k]&&token!=null&&_nativeSfPreviews[k].token>token)return;// stale (claim-then-compare: chi bo neu co note MOI HON) const S=window.SonicSF;if(!S)return;const ch=track&&track.midiChannel!=null?track.midiChannel:0;const durMs=Math.max(200,durationMs||500);const ctx=getAudioContext();const delaySec=startTime?Math.max(0,startTime-ctx.currentTime):0;_nativeSfPreviews[k]={wasm:{ch:ch,pitch:pitch},token:token};const fire=function(){Promise.resolve(S.selectInstrument(ch,bank,program,sfId)).then(function(){S.playNote(pitch,velocity!=null?velocity:0.8,durMs,undefined,eng?undefined:program,null,ch,eng||undefined);}).catch(function(){try{S.playNote(pitch,velocity!=null?velocity:0.8,durMs,undefined,eng?undefined:program,null,ch,eng||undefined);}catch(e2){}});};if(delaySec>0){setTimeout(fire,delaySec*1000);}else{fire();}}catch(e){console.warn('[NativeSF] wasm fallback note error:',e);}};const playNativeSfNote=(track,pitch,velocity,durationMs,startTime,key)=>{try{const eng=track&&track.synth_engine;const sfId=eng&&eng.soundfont_id||(track&&track.instrumentId&&String(track.instrumentId).startsWith('sf_')?track.instrumentId:null);if(!sfId)return;const bank=eng&&eng.soundfont_bank||0;const program=eng&&eng.soundfont_program||(track&&track.instrumentProgram!==undefined?track.instrumentProgram:0);const k=key||(track?track.id:'global');const prev=_nativeSfPreviews[k];const token=(prev?prev.token:0)+1;// Native hong da biet (501) -> di thang WASM fallback, khong goi API. -if(window.__nativeSfOk===false){_sfWasmFallbackNote(track,pitch,velocity,durationMs,startTime,k,token);return;}const durationSec=Math.max(0.2,(durationMs||500)/1000);window.SonicAPI.soundfontRender({soundfont_id:sfId,bank:bank,program:program,bpm:120,notes:[{pitch:pitch,start_beat:0,duration_beats:durationSec*2,velocity:velocity!=null?velocity:0.8}]}).then(function(res){if(!res||!res.success||!res.url){window.__nativeSfOk=false;_sfWasmFallbackNote(track,pitch,velocity,durationMs,startTime,k,token);return;}if(_nativeSfPreviews[k]&&_nativeSfPreviews[k].token!==token)return;// stale +if(window.__nativeSfOk===false){_sfWasmFallbackNote(track,pitch,velocity,durationMs,startTime,k,token);return;}const durationSec=Math.max(0.2,(durationMs||500)/1000);window.SonicAPI.soundfontRender({soundfont_id:sfId,bank:bank,program:program,bpm:120,notes:[{pitch:pitch,start_beat:0,duration_beats:durationSec*2,velocity:velocity!=null?velocity:0.8}]}).then(function(res){if(!res||!res.success||!res.url){window.__nativeSfOk=false;_sfWasmFallbackNote(track,pitch,velocity,durationMs,startTime,k,token);return;}if(_nativeSfPreviews[k]&&_nativeSfPreviews[k].token>token)return;// stale (claim-then-compare: chi bo neu co note MOI HON) window.__nativeSfOk=true;const audio=new Audio(API_BASE_URL+res.url);_nativeSfPreviews[k]={audio:audio,token:token};const ctx=getAudioContext();const delay=startTime?Math.max(0,(startTime-ctx.currentTime)*1000):0;setTimeout(function(){if(!_nativeSfPreviews[k]||_nativeSfPreviews[k].audio!==audio)return;audio.play().catch(function(){});audio._sfStopTimer&&clearTimeout(audio._sfStopTimer);audio._sfStopTimer=setTimeout(function(){try{audio.pause();}catch(e){}},durationSec*1000+400);},delay);}).catch(function(){window.__nativeSfOk=false;_sfWasmFallbackNote(track,pitch,velocity,durationMs,startTime,k,token);});}catch(e){console.warn('[NativeSF] playNativeSfNote error:',e);}};const stopNativeSfNote=key=>{try{const k=key||'global';const prev=_nativeSfPreviews[k];if(!prev)return;prev.token++;try{if(prev.audio){prev.audio.pause();prev.audio.currentTime=0;}}catch(e){}try{if(prev.wasm&&window.SonicSF)window.SonicSF.stopNote(prev.wasm.ch,prev.wasm.pitch);}catch(e){}}catch(e){}};const stopAllNativeSfNotes=()=>{try{Object.keys(_nativeSfPreviews).forEach(function(k){stopNativeSfNote(k);});}catch(e){}};// ── Native soundfont item render (standalone, transport) ────────────────── // Render TOÀN BỘ MIDI item bằng native FluidSynth → decode AudioBuffer → // schedule nguồn audio đúng vị trí item (giống audio clip). opts: diff --git a/plans/PLAN_STANDALONE_WASM_PLAYBACK.md b/plans/PLAN_STANDALONE_WASM_PLAYBACK.md new file mode 100644 index 0000000..81d4fa9 --- /dev/null +++ b/plans/PLAN_STANDALONE_WASM_PLAYBACK.md @@ -0,0 +1,66 @@ +# PLAN — Standalone playback 100% client-side WASM (no Carla dependency) + +Goal: fix "mastering fx chain không xử lí" + "vsti không qua main out" bằng cách +chuyển live playback/preview của VSTi sang WASM (autosampled SF2 qua FluidSynth +WASM → track sfEntry → FX chain → masterBus mastering → main out). Export WAV +giữ nguyên backend Python (pedalboard VST3 + FluidSynth C++). + +## 1. Server — autosample endpoint (VSTi → SF2) +- `tools/autosample_vsti.py`: tách core thành `autosample_sf2(...)` callable + (giữ `main()` argparse cho CLI). +- `app/api/v1/plugins.py`: `AutosampleRequest` + `POST /autosample` — render + preset qua từng note bằng pedalboard (đúng plugin+preset như export) → SF2 + 16-bit mono vào `app/storage/soundfonts/{uuid}.sf2` + `.meta` → trả `sf_id`. +- Chỉ SF2 (không SF3): client WASM load SF2 trực tiếp, endpoint download ưu + tiên SF2 sẵn có. 30-60 note ≈ 10-20MB — chấp nhận, IndexedDB cache. + ponytail: SF3 (OGG nhỏ hơn) khi cần size nhỏ — thêm `--sf3` vào endpoint. + +## 2. Server — mastering FX chain trong render_project +- `app/core/mastering_engine.py` (mới): `apply_mastering(buffer, settings, fs)` + tái hiện chain WebAudio client (initMasterBus + applyMasteringSettings): + - `eq` (lowshelf 100 / peaking 822 Q0.7 / peaking 3200 Q1.2 / highshelf 10k) + - `imager` (M/S width 4 band: 20-100 / 100-1k / 1k-6k / 6k-20k) + - `maximizer` (boost → softclip atan → +upward comp → ceiling hard clip) + - `compressor` (threshold/ratio/knee/makeup) + - `limiter` (tanh soft clip) + - `exciter` (hp 2k + saturator, dry/wet) + - `rebalance` (M/S gains qua L/R crossfeed) + - chain order từ `settings['chain']`, gate `masterConnected && !isBypassed`. +- `app/core/render_engine.py`: `render_project` đọc `project_json['mastering_settings']` + → áp lên master_buffer trước `sf.write`; khi có mastering → clip [-1,1] + (giống WAV encoder client), không mastering → normalize như cũ. + +## 3. Client — VSTi live playback qua WASM autosample +- `app/static/js/services/api.js`: thêm `autosampleVsti(payload)`. +- `app/static/js/services/vstiAutosample.js` (mới): `SonicVstiAutosample` — + `ensure(synthEngine)` POST autosample 1 lần, cache map plugin_id+preset → + sf_id (localStorage), `sfIdFor` sync lookup. +- `app/static/js/services/soundfontPlayer.js`: `_playNoteFluid` + + `applyAITrackInstrument` fallback `synthEngine.autosampled_sf_id` khi thiếu + `soundfont_id`; VSTi chưa sample → ensure rồi defer note (pendingNoteOns có sẵn). +- `app/static/js/services/runtime.js`: `SonicCarlaMidi.shouldRoute` / + `shouldRoutePlayback` trả false trừ khi `window.__enableCarlaLivePlayback` + (mặc định off) — playback không phụ thuộc Carla nữa. +- `app/static/js/app.jsx`: + - `shouldRouteCarla` → false (VSTi đi qua SonicSF → masterBus → main out). + - chọn instrument VSTi (applyTrackInstrument) → fire-and-forget + `SonicVstiAutosample.ensure(synth_engine)`. + - timeline/local-loop/ghost/preview: VSTi đã chạy nhánh `SonicSF.playNote` + sẵn (isSfTrackEngine false → else-if); autosampled_sf_id làm nó kêu đúng âm. + - `ensureMidiCapture` đã capture `node.sfEntry` → VSTi vào midiCache → + clientSideExport (offline mix qua track FX + mastering) hoạt động cho VSTi. +- `app/templates/index.html`: script tag `vstiAutosample.js`. +- Build: `npm run build` (app.jsx → app.precompiled.js). + +## 4. Verify +- pytest `tests/` (kỳ vọng 108 pass / 1 skip / 1 env-specific fail như cũ). +- Test mastering: render project JSON có mastering_settings (chain eq+imager+ + maximizer) → assert output peak ≤ ceiling & khác bản không mastering. +- Test autosample: gọi `autosample_sf2` với VSTi (Nexus.vst3) → sf2utils parse + sạch; endpoint return sf_id + file tồn tại. +- Manual (browser): track VSTi → Preview/Play → âm qua masterBus (VU master + nhảy), Export WAV server ra file có mastering. + +## 5. Commit/push +- Commit trên branch `standalone`. Push vẫn chờ user cấu hình git auth + (credential.helper / token URL) — không retry khi chưa có. diff --git a/plans/WALKTHROUGH.md b/plans/WALKTHROUGH.md new file mode 100644 index 0000000..e1981f7 --- /dev/null +++ b/plans/WALKTHROUGH.md @@ -0,0 +1,176 @@ +# WALKTHROUGH — Thực thi tuần tự (SonicForgeStudio standalone) + +> Nạp file này mỗi session mới. Làm ĐÚNG 1 task, cập nhật bảng trạng thái, commit, dừng. +> Kiến trúc/lý do: `GIAI_PHAP.md`. File này chỉ chứa thao tác + trạng thái → context nhỏ. + +## 1. Thông tin cố định (mọi session) + +- Repo: `C:/Users/locpham/SonicForgeStudio` — branch: `standalone` — HEAD: `cadb540` (đã commit; **KHÔNG push**) +- Build JS: `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` +- Test: `python -m pytest tests/` → 110 passed, 1 skipped, 1 failed (`test_vst_engine.py::TestPluginManager::test_init` — env-fail Linux path, **bỏ qua, không sửa**) +- Rust check: `cd src-tauri && cargo check` (nếu task C) +- **CRLF rule**: file repo dùng CRLF. Đọc = `sed -n`/`grep` (không dùng read_file). Sửa = python: `s = open(p, encoding='utf-8', newline='').read()` → thay chuỗi `\n` → `s.replace('\n','\r\n')` → `open(p,'w',encoding='utf-8',newline='').write(s)`. Verify lại bằng grep/repr. +- Commit rule: sau mỗi task xanh (build+test OK) → `git add` + commit `standalone`. Trước mỗi task: `git status` phải sạch. +- Trình tự: A (T1–T3) → B (T4–T7) → C (T8–T15) → D (T16). + +## 2. Trạng thái + +| ID | Phase | Task | Trạng thái | +| --- | --- | --- | --- | +| T1 | A | Bug 1: token fix | [ ] | +| T2 | A | Bug 2: chain re-apply + sig order + neutral add | [ ] | +| T3 | A | Bug 3: autosample pre-warm + toast + re-sample | [ ] | +| T4 | B | `unifiedMidiRouter.js` | [ ] | +| T5 | B | `TrackInstrument` + keybed → router; xóa native preview/token | [ ] | +| T6 | B | Scheduler items → router | [ ] | +| T7 | B | Hardware MIDI → router | [ ] | +| T8 | C | Rust `vst_gui.rs` + `raw-window-handle` | [ ] | +| T9 | C | VST3 GUI bridge (`vst3_host_bridge.cpp` + DLL) | [ ] | +| T10 | C | VST2 engine (`VST2AudioEngine.cpp`, vestige.h) | [ ] | +| T11 | C | IPC param sync 2 chiều | [ ] | +| T12 | C | Native audio loop: SPSC + driver WASAPI/ASIO | [ ] | +| T13 | C | `NativeMixer` mirror `mastering_engine.py` | [ ] | +| T14 | C | Port FluidSynth native (hoặc loopback) | [ ] | +| T15 | C | Bỏ WebAudio master; IPC gain/pan; test matrix | [ ] | +| T16 | D | Autosample hoàn thiện (VST2 `.sf3`, preset, re-sample) | [ ] | + +Trạng thái dùng: `[ ]` chưa làm, `[▶]` đang làm/dang dở, `[x]` xong, `[!]` fail — ghi lỗi vào Ghi chú. + +## 3. Tiếp tục sau fail + +1. Đọc file này. Tìm task đầu tiên có `[ ]` hoặc `[▶]`. +2. Nếu `[▶]`: đọc Ghi chú (lỗi đã ghi), sửa tiếp. Nếu `[ ]`: nạp đúng mục "Context tối thiểu" của task (chỉ đọc các file/line đó — không đọc cả app.jsx). +3. Làm xong → Verify → build → pytest → cập nhật bảng `[x]` + Ghi chú 1 dòng → commit. +4. Fail không gỡ được: cập nhật `[!]` + Ghi chú lỗi đầy đủ (command, output, đoạn code), dừng, báo user. + +## 4. Các task + +### T1 — Bug 1: token fix (Phase A) +- **Mục tiêu**: bấm phím keyboard preview lần 2+ không câm. +- **File**: `app/static/js/app.jsx` +- **Context tối thiểu**: `grep -n '_sfWasmFallbackNote\|playNativeSfNote\|_nativeSfPreviews' app/static/js/app.jsx` → `_sfWasmFallbackNote` L213-236, `playNativeSfNote` L239-283. +- **Bước**: + 1. Trong `_sfWasmFallbackNote`: stale check `_nativeSfPreviews[k].token !== token` → đổi thành `> token` (claim-then-compare) và LUÔN ghi đè `_nativeSfPreviews[k] = { wasm, token }`. + 2. Cùng pattern `!== token` ở path API-success (~L257): sửa giống hệt. +- **Verify**: build babel; mở app; bấm cùng phím 2–5 lần liên tiếp → mọi lần đều có tiếng; bấm nhanh nhiều phím → không câm. +- **Done khi**: không còn câm; không đổi hành vi khác. +- **Rollback**: `git checkout -- app/static/js/app.jsx` +- **Ghi chú**: Nếu T4–T5 đã xong (router xóa cơ chế này) → bỏ qua task này, đánh `[x]` kèm "đã xóa tận gốc ở T5". + +### T2 — Bug 2: chain mastering (Phase A) +- **Mục tiêu**: thêm/sắp xếp module mastering không làm volume nhỏ đi; reorder đúng thứ tự. +- **File**: `app/static/js/app.jsx` +- **Context tối thiểu**: `grep -n 'applyMasteringSettings\|_lastMasteringSig\|chainSignature\|addModuleToChain\|_buildLimCurve\|_buildMaxCurve\|toggleMasteringOnMaster' app/static/js/app.jsx` (L635+, L1097, L1486, L913-935, L12446). +- **Bước**: + 1. `chainSignature` (L1097): order-sensitive — signature phải gồm thứ tự module (không phải `(type+active).join(',')` set-insensitive). + 2. `applyMasteringSettings` (L635+, early-return L642): bỏ phụ thuộc sig; mọi thay đổi chain → rebuild + re-apply TOÀN BỘ tham số (debounce ~50ms thay vì skip). + 3. Thêm module (limiter/compressor) qua `addModuleToChain`: neutral-then-apply — limiter threshold mặc định 0dB (không clamp −1dB), compressor makeup bù loss (hoặc threshold cao hơn), để "thêm module" không tự cắt volume. +- **Verify**: build; thêm limiter → volume không giảm rõ rệt; thêm compressor → không giảm; reorder limiter↔compressor → nghe đổi thứ tự; master fader vẫn hoạt động đúng. +- **Done khi**: 3 bước trên đúng; `python -m pytest tests/` xanh. +- **Rollback**: `git checkout -- app/static/js/app.jsx` + +### T3 — Bug 3: autosample UX (Phase A) +- **Mục tiêu**: VSTi không autosample được phải hiện rõ, không âm thầm ra oscillator default. +- **File**: `app/static/js/services/vstiAutosample.js`, `app/static/js/app.jsx` (nơi chọn instrument), util toast hiện có. +- **Context tối thiểu**: `grep -rn 'SonicVstiAutosample\|ensure' app/static/js/` + `grep -n '_playNoteFallback' app/static/js/soundfontPlayer.js` (L787+). +- **Bước**: + 1. `ensure()`: bắt lỗi (404/501/import fail) → trả về object `{ ok:false, reason }`, không throw âm thầm. + 2. UI: khi chọn instrument VSTi → gọi ensure NGAY (pre-warm) + progress indicator; note đầu tiên chờ ready (hoặc disable tới ready) — không rơi vào oscillator. + 3. Khi fail → toast "không autosample được VSTi: ". + 4. Nút re-sample trong panel instrument. +- **Verify**: chọn VSTi path sai → toast hiện reason, không ra âm oscillator; chọn VSTi hợp lệ → pre-warm xong, note đầu tiên ra đúng âm; bấm re-sample → chạy lại. +- **Done khi**: không còn fail âm thầm; pre-warm hoạt động. +- **Rollback**: `git checkout -- app/static/js/services/vstiAutosample.js app/static/js/app.jsx` + +### T4 — `unifiedMidiRouter.js` (Phase B) +- **Mục tiêu**: một điểm dispatch MIDI cho mọi nguồn (spec Part II). +- **File mới**: `app/static/js/services/unifiedMidiRouter.js` +- **Nội dung**: `UnifiedMidiEvent` (trackId, channel, command, pitch int 0-127, velocity int 1-127, sourceType, timestampNs) + `UnifiedMidiRouter` với `engineRegistry: Map` (thay "1 engine toàn cục" của spec — route theo trackId); `dispatchMidiEvent`; `activeVoiceTracker` key `${channel}_${pitch}` chống stuck notes; `panicAllNotesOff()`. +- **Verify**: `node --check app/static/js/services/unifiedMidiRouter.js`; viết 1 test assert nhỏ (pitch clamp, velocity normalize float→int, tracker đếm đúng, panic clear) — chạy bằng node. +- **Done khi**: file + test chạy xanh; chưa nối vào app. +- **Rollback**: xóa file. + +### T5 — `TrackInstrument` + keybed → router (Phase B) +- **Mục tiêu**: keyboard preview đi router; xóa cơ chế token/native. +- **File**: `app/static/js/app.jsx`, có thể thêm `app/static/js/services/trackInstrument.js` +- **Bước**: + 1. `TrackInstrument`: bọc FluidSynth channel per-track — `playNote(pitch, vel, dur)`; dùng cho cả preview lẫn items. + 2. Keybed `onMouseDown`/`onMouseUp` → router.dispatchMidiEvent (sourceType VIRTUAL_KEYBOARD). + 3. XÓA: `playNativeSfNote`, `_sfWasmFallbackNote`, `_nativeSfPreviews`, mọi `__nativeSfOk`/token logic. +- **Verify**: bấm phím liên tiếp 10 lần → không câm; không còn `_nativeSfPreviews` trong code (`grep`); build OK. +- **Done khi**: Bug 1 hết tận gốc; preview vẫn ra tiếng đúng track/channel. +- **Rollback**: `git checkout -- app/static/js/app.jsx` (xóa file mới nếu thêm). + +### T6 — Scheduler items → router (Phase B) +- **Mục tiêu**: MIDI items khi chạy timeline đi cùng router. +- **File**: `app/static/js/app.jsx` (scheduler/playhead) +- **Bước**: note-on/note-off của scheduler → `router.dispatchMidiEvent` (TIMELINE_SCHEDULER); note-off dùng tracker đối xứng. +- **Verify**: chạy timeline → items phát đúng pitch/vel; dừng giữa chừng → `panicAllNotesOff` không stuck note. +- **Done khi**: preview + timeline cùng engine, không nuốt voice. + +### T7 — Hardware MIDI → router (Phase B) +- **File**: `app/static/js/app.jsx` (L15689 `navigator.requestMIDIAccess`/`onmidimessage`) +- **Bước**: `onmidimessage` → `router.handleHardwareKeyboardMessage(...)` → dispatch (HARDWARE_KEYBOARD); xóa nhánh xử lý cũ. +- **Verify**: bấm phím MIDI hardware → ra tiếng qua router đúng track active; log sourceType. +- **Done khi**: 3 nguồn (keybed/scheduler/hardware) cùng một router. + +### T8 — Rust `vst_gui.rs` (Phase C) +- **Mục tiêu**: command `open_vst_gui(plugin_id, track_id)` tạo floating child window. +- **File**: `src-tauri/Cargo.toml` (+ `raw-window-handle`), `src-tauri/src/vst_gui.rs`, `src-tauri/src/lib.rs` (register command). +- **Nội dung** (spec vsti_gui): `WebviewWindowBuilder` label `vst_gui_{track}_{plugin}`, 800×600, always_on_top, mở lại thì focus; `raw_window_handle()` lấy HWND. +- **Verify**: `cargo check`; gọi từ JS `invoke('open_vst_gui', {...})` → cửa sổ nổi mở/đóng không crash. +- **Done khi**: window nổi mở được; chưa gắn plugin GUI (T9). + +### T9 — VST3 GUI bridge (Phase C) +- **File mới**: `native_host/vst3_host_bridge.cpp` + build DLL (MSVC/CMake) +- **Nội dung** (spec): `attach_vst3_editor_to_handle(plugin_id, parent_handle)` → `createView(kEditor)` → `attached(hwnd, kPlatformTypeHWND)` → `getSize()` → resize. Đóng: `removed()` + `release()`. +- **Quyết định trước khi làm**: VST3 SDK thuần (nhẹ) HAY JUCE (gộp luôn VST2 — xem T10). Khuyến nghị: JUCE nếu muốn 1 engine cho cả 2; SDK thuần nếu chỉ VST3. +- **Verify**: mở GUI VST3 trong cửa sổ nổi, chỉnh param, đóng → không leak/crash. +- **Done khi**: GUI plugin hiển thị + đóng sạch. + +### T10 — VST2 engine (Phase C) +- **File mới**: `native_host/VST2AudioEngine.cpp` (dùng `vestige.h` — clean-room, KHÔNG dùng aeffect.h chính thức; legal note spec III) +- **Nội dung** (spec VST2): `loadPlugin` (LoadLibraryW + `VSTPluginMain`/`main` + dispatcher init), `sendMidiNoteOn` (`effProcessEvents`, `deltaFrames`), `attachGUI` (`effEditOpen` + `effEditGetRect`), `processAudioBlock` (`processReplacing`), destructor teardown. `HostAudioMaster` phải trả lời opcode version/sampleRate/blockSize. +- **Verify**: load .dll VST2 → mở GUI → processReplacing ra tín hiệu. +- **Done khi**: VST2 GUI + audio thô chạy (chưa vào mixer). + +### T11 — IPC param sync (Phase C) +- **File**: `src-tauri/src/vst_gui.rs` + JS side +- **Nội dung**: native param đổi → `vst_param_changed` → JS cập nhật state DAW; JS automation → `set_param`/`setParamNormalized` → native. +- **Verify**: xoay knob plugin → JS state đổi; đổi tham số từ DAW → plugin đổi. +- **Done khi**: sync 2 chiều không lag, không vòng lặp vô hạn (guard flag). + +### T12 — Native audio loop (Phase C) +- **File mới**: `native_host/VST3AudioEngine.cpp` (hoặc gộp với T9/T10 theo quyết định JUCE), SPSC ring buffer +- **Nội dung** (spec audio): UI thread → SPSC queue → audio thread build EventList/VstEvents → `process()`/`processReplacing` → pre-alloc buffers; zero-allocation trong audio thread; driver WASAPI Exclusive/ASIO 128–256 samples; `setProcessing(true)` 1 lần lúc init; sampleRate từ negotiate. +- **Verify**: phát preview qua native driver, 0 underrun; đo latency ≤ ~5ms. +- **Done khi**: VSTi ra loa native, không qua WebAudio. + +### T13 — `NativeMixer` (Phase C) +- **File mới**: `native_host/NativeMixer.cpp` +- **Nội dung** (spec audio tier 3): track gain `10^(dB/20)`, pan constant-power, master sum, brickwall limiter — MIRROR `app/core/mastering_engine.py` (đọc file đó trước). +- **Verify**: golden test — render cùng 1 đoạn bằng native mixer và server `render_project` (cùng mastering settings) → so RMS/peak sai lệch < 0.1dB. +- **Done khi**: native mixer khớp server; track gain/pan áp cho cả VST2/VST3. + +### T14 — Port FluidSynth native (Phase C) +- **Mục tiêu**: SF track vào CÙNG native mixer (một master duy nhất). +- **Bước**: tích hợp libfluidsynth C++ vào native host; track SF → FluidSynth native → NativeMixer. (Nếu không port được: loopback WASM→native — phức tạp, ghi quyết định vào Ghi chú.) +- **Verify**: SF track + VSTi track cùng master; gain/pan/mastering áp đúng cả 2; preview + items + offline nhất quán. +- **Done khi**: một mixer duy nhất, hết WebAudio cho track instrument. + +### T15 — Bỏ WebAudio master (Phase C) +- **Bước**: track instrument không còn đi `masterBus` WebAudio; IPC `set_master_gain`, `set_track_gain_pan` từ JS → native; giữ WebAudio chỉ cho UI/aux nếu còn. +- **Verify**: test matrix spec V: VST3/VST2/SF × preview/live/offline đều đúng âm lượng và mastering. +- **Done khi**: Bug 2 không còn drift (một engine duy nhất); checklist spec 2 (negotiate SR/buffer, jitter <0.1ms, 0% underrun) đạt. + +### T16 — Autosample hoàn thiện (Phase D) +- **File**: `tools/autosample_vsti.py`, `app/static/js/services/vstiAutosample.js` +- **Nội dung**: autosample VST2 → `.sf3` cho offline (Pedalboard không hỗ trợ VST2 → giữ con đường này cho export); tôn trọng preset_path/preset_data; hiển thị note_count/size; re-sample khi đổi preset. +- **Verify**: export track VST2 ra file đúng âm; đổi preset → re-sample tự động; UI hiện size. +- **Done khi**: matrix hoàn chỉnh; toàn bộ test xanh; commit cuối. + +## 5. Checkpoint tổng +- Sau T3: 3 bug đã vá (tạm) — bản phát hành an toàn. +- Sau T7: unified MIDI pipeline xong phía client — Bug 1 hết tận gốc. +- Sau T15: native audio hoàn chỉnh — Bug 2 hết drift, Bug 3 hết fallback mặc định. +- Sau T16: matrix spec V đầy đủ.