# 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 | [x] | | T2 | A | Bug 2: chain re-apply + sig order + neutral add | [x] | | T3 | A | Bug 3: autosample pre-warm + toast + re-sample | [x] | | T4 | B | `unifiedMidiRouter.js` | [x] | | T5 | B | `TrackInstrument` + keybed → router; xóa native preview/token | [x] | | T6 | B | Scheduler items → router | [x] | | T7 | B | Hardware MIDI → router | [x] | | T8 | C | Rust `vst_gui.rs` + `raw-window-handle` | [x] | | T9 | C | VST3 GUI bridge (`vst3_host_bridge.cpp` + DLL) | [x] | | T10 | C | VST2 engine (`VST2AudioEngine.cpp`, vestige.h) | [x] | | T11 | C | IPC param sync 2 chiều | [x] | | T12 | C | Native audio loop: SPSC + driver WASAPI/ASIO | [x] | | T13 | C | `NativeMixer` mirror `mastering_engine.py` | [x] | | T14 | C | Port FluidSynth native (hoặc loopback) | [x] | | T15 | C | Bỏ WebAudio master; IPC gain/pan; test matrix | [x] | | T16 | D | Autosample hoàn thiện (VST2 `.sf3`, preset, re-sample) | [x] | 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**: VST3 SDK thuần (KHÔNG JUCE) — nhẹ, license BSD-3; T10 VST2 dùng vestige.h riêng; T12 cũng SDK thuần. - **Verify**: DLL build OK + 4 export `SF_VST3_Attach/Close/GetSize/Resize` load qua ctypes. Chưa có plugin VST3 thật để test GUI — giới hạn ghi nhận. - **Done khi**: DLL build + export OK. ### 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). - **Đã làm (commit `aaf21dd`)**: `native_host/vestige.h` (clean-room AEffect: struct đúng layout công khai; numInputs/numOutputs/flags là function pointer; opcode effOpen..effSetProcessPrecision; audioMaster opcodes; VstEvents/VstMidiEvent; ERect `short`), `native_host/VST2AudioEngine.cpp` (export `SF_VST2_Load/Close/GetSize/Resize/SendNoteOn/SendNoteOff/Process`; LoadLibraryW → `VSTPluginMain`/`main` → magic check → effOpen→SetSampleRate→SetBlockSize→MainsChanged(1)→StartProcess; editor effEditGetRect→effEditOpen khi có parent; Close teardown ngược; MIDI qua effProcessEvents; processReplacing với interleaved buffers `ptrs[i]=buffers+i*sampleFrames`; `tls_loading` thread_local cho hostAudioMasterImpl trả version 2400/sampleRate/blockSize), `native_host/tests/fake_vst2.cpp` (plugin giả: sine 440Hz stereo, editor rect 400x300), CMakeLists thêm `vst2_host_bridge` target. - **Verify đã chạy**: `fake_vst2.dll` build OK (MSVC); `vst2_host_bridge.dll` build OK; ctypes: 7 export SF_VST2_* OK; Load(parent=NULL) → handle, GetSize → -2 (không editor — đúng), Process 512×2 stereo → sine peak 0.25; Load(parent=giả) → GetSize 400x300 → Close OK. ### 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). - **Đã làm (commit `9eec33c`, chưa push)**: `native_host/vst3_host_bridge.cpp` + `native_host/VST2AudioEngine.cpp` thêm param API đồng bộ: export `SF_*_SetParamCallback(handle, cb, userdata)`, `SF_*_SetParam(handle, id, valueNormalized)` → `controller->setParamNormalized`/`effect->setParameter`, `SF_*_GetParamCount`, `SF_VST2_GetParam(handle, id, &value)`, `SF_VST3_GetParamInfo(handle, index, &id, title, cap, &value)` (title String128 UTF-16 → `StringConvert::convert`); VST2 callback chạy trong `audioMasterAutomate` (instance tra qua `g_effect_map` + mutex riêng để tránh deadlock với audio thread), VST3 qua `ComponentHandler::performEdit`; `native_host/tests/fake_vst2.cpp` thêm param 0 (gain, mặc định 0.5) + `setParameter`/`getParameter` + gọi `audioMasterAutomate` khi đổi từ editor. - **Rust (`src-tauri/src/vst_gui.rs`)**: `open_vst_gui(plugin_id, track_id, plugin_path, plugin_kind)` mở WebviewWindow `vst_gui.html?track=..&plugin=..`, tìm bridge DLL (env `SF_NATIVE_HOST_DIR` → resource_dir → exe_dir → cwd → `native_host/build/Release/`), Load/Attach → handle, đăng ký callback; `set_vst_param`, `get_vst_params`, `close_vst_editor` (kind lưu trong HANDLE_MAP để close/set đúng DLL); callback native → `emit_to("vst_gui_{track}_{plugin}", "vst_param_changed", {track_id, plugin_id, param_id, value})`; `lib.rs` đăng ký 4 commands + `vst_gui::init` trong setup. - **JS (`src-tauri/ui/vst_gui.html`)**: panel param trái — `get_vst_params` → slider 0..1 per param; slider change → `set_vst_param`; listen `vst_param_changed` cập nhật UI, guard chống loop: `pendingSet` chứa param_id JS vừa set, event echo của chính mình bị bỏ qua. - **Verify đã chạy**: `cargo check` OK (rustc 1.97, raw-window-handle 0.6: `hwnd.get()`); build lại 2 bridge DLL + fake (MSVC) OK; ctypes VST2: Load→1, SetParamCallback→0, GetParamCount→1, GetParam(0)=0.5, SetParam(0, 0.75)→0, callback nhận đúng 1 event `(handle, 0, 0.75)` — không loop, GetParam(0)=0.75 sau đó, Close→0; VST3 bridge: 8 export OK (Attach/Close/GetSize/Resize + 4 param; SendNote/Process thuộc T12+). Chưa có plugin VST3 thật để test GUI — giới hạn T9 giữ nguyên. ### T12 — Native audio loop (Phase C) - **File mới**: `native_host/AudioEngine.h` + `AudioEngine.cpp` — QUYẾT ĐỊNH: gộp "VST3AudioEngine" thành `AudioEngine` dùng chung cho cả 2 plugin kind (spec cho phép); WASAPI đủ cho milestone, ASIO bỏ qua (ghi chú — thêm khi có yêu cầu rõ ràng). - **AudioEngine** (zero-lock audio thread): SPSC ring MIDI (capacity 4096, tối đa 256 event/block; producer có mutex riêng — audio thread KHÔNG lock, KHÔNG cấp phát); WASAPI render thread event-driven, exclusive trước → fallback shared (shared bắt buộc `GetMixFormat` của device — format tự build IEEE_FLOAT 2ch bị `AUDCLNT_E_UNSUPPORTED_FORMAT` 0x88890008); device default trước → fallback enumerate endpoint ACTIVE; MMCSS "Audio"; API: `start/stop/pushMidi/underruns/latencySamples/blocksRendered/sampleRate`, `ProcessFn` = `bool(*)(const MidiEvent*, int32, float* outL, float* outR, int32 frames, void*)`; audio thread: drain SPSC → build `VstEvents` (VST2, buffer 256 cố định flexible-array) / `FixedEventList` (VST3, Event[512] + IEventList no-alloc) → `processReplacing`/`process` → interleave ra WASAPI. - **VST2 bridge**: Instance thêm `audio` + `audioRunning`; `SF_VST2_AudioStart(handle, sr, block, err, cap)` (set `audioRunning=true` TRƯỚC `audio.start` để SendNote route vào SPSC), `SF_VST2_AudioStop`, `SF_VST2_AudioUnderruns`, `SF_VST2_AudioLatency`, `SF_VST2_AudioBlocks`; `SendNoteOn/Off` — nếu `audioRunning` → push SPSC, else giữ đường direct `effProcessEvents` cũ (offline/không audio); `Close` → `audioRunning=false; audio.stop()` trước teardown. - **VST3 bridge**: Instance thêm `component` (từ `plugProvider->getComponent()`), `processor` (queryInterface `IAudioProcessor::iid`), `audio`, `audioRunning`; export `SF_VST3_AudioStart/Stop/Underruns/Latency/Blocks` + `SF_VST3_SendNoteOn/Off` (LUÔN push SPSC — không có đường direct); AudioStart: `setupProcessing` (`ProcessSetup`: `symbolicSampleSize=kSample32`, `maxSamplesPerBlock`, `sampleRate`), `setProcessing(true)`, rồi `audio.start`; Close/AudioStop → stop audio + `setProcessing(false)`; ProcessData: `symbolicSampleSize=kSample32`, outBus 2ch `channelBuffers32={outL,outR}`, `inputEvents=&FixedEventList`; cần include `ivstevents.h` (IEventList/Event) + `ivstaudioprocessor.h` đã có. - **Verify đã chạy (ctypes VST2 + fake_vst2)**: Load→1, AudioStart(44100,128)→0, blocks render sau 300ms = 30 → callback chạy; SendNoteOn(60)/Off qua SPSC → 0, blocks tăng (30→110); `SF_VST2_AudioUnderruns` = 0; `SF_VST2_AudioLatency` = 1056 mẫu (shared-mode buffer thực tế của device — KHÔNG phải 128; exclusive không được device chấp nhận → fallback shared, ghi nhận trung thực); AudioStop→0, Close→0. VST3: 15 export OK (8 cũ + AudioStart/Stop/Underruns/Latency/Blocks + SendNoteOn/Off). Chưa có plugin VST3 thật để test audio — giới hạn giữ nguyên (như T9/T11). - **Cảnh báo**: KHÔNG chạy `SF_VST2_Process` (offline render) song song với live audio trên cùng instance — render thread và caller cùng gọi dispatcher; T16 offline autosample sẽ dùng instance riêng. - **Done khi**: VSTi ra loa native, không qua WebAudio — đạt cho đường live preview; offline qua native vẫn ở T13–T16. ### T13 — `NativeMixer` (Phase C) - **File mới**: `native_host/NativeMixer.h` + `NativeMixer.cpp` (static lib `native_mixer`), `native_host/tests/native_mixer_test.cpp` + `test_native_mixer_golden.py` - **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. - **Đã làm (commit `8f1e89c` tiếp theo — T13)**: `NativeMixer` — `TrackGainPan::fromDbPan(gainDb, pan)`: gainLin=10^(dB/20); pan==0 → unity cả 2 kênh (đúng render_engine.py — KHÔNG nhân 0.7071); pan≠0 → constant-power theta=((pan+1)/2)*π/2, L*=cos(theta), R*=sin(theta). `setLimiter(active, thresholdDb)`: clamp threshold −24..0, NaN→−1 (mirror `_clamp`); k=1/max(0.02,t_lin), tk=tanh(k). `processInPlace(outL,outR,frames)`: gain*pan rồi nếu limiter active → tanh(x*k)/tk + hard clip [−1,1] (clip CHỈ khi limiter active — mirror render_project: clip sau apply_mastering; limiter off không clip). Audio-thread an toàn: setTrack/setLimiter từ UI thread trước AudioStart, processInPlace không lock/cấp phát. Wired vào cả 2 bridge: Instance thêm `mixer`; `vst2AudioProcess`/`vst3AudioProcess` gọi `mixer.processInPlace` sau process; export mới `SF_VST2_/SF_VST3_SetTrackGainPan(handle, gainDb, pan)` + `SF_VST2_/SF_VST3_SetMasterLimiter(handle, active, thresholdDb)` → VST2 18 export, VST3 17. - **Verify đã chạy**: golden test `python native_host/tests/test_native_mixer_golden.py` — 5 case (identity; gain/pan 0dB·pan0; +6dB pan−0.5 limiter −3dB; −9dB pan0.8 limiter −6dB; +12dB limiter 0dB) so với reference dùng CHÍNH code server: gain/pan theo render_engine.py + `apply_mastering` (mastering_engine) module limiter + clip — worst diff RMS/peak = 0.0001 dB (< 0.1 → PASS). Ctypes VST2: Load→1, SetTrackGainPan/SetMasterLimiter→0, AudioStart→0, 30 blocks/300ms, SendNoteOn/Off→0, underruns 0, Stop/Close→0. Build: cmake Release 2 bridge OK; native_mixer_test.exe build OK. - **Ghi chú**: golden test so với reference = đúng code path render_project (gain/pan render_engine + apply_mastering + clip) vì chưa có FluidSynth native (T14) để render SF trực tiếp — end-to-end qua render_project sẽ verify tiếp ở T14. - **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. - **Đã làm (commit `500384a` — T14)**: QUYẾT ĐỊNH — port được native, KHÔNG loopback. `native_host/SFHost.cpp` (DLL `sf_host_bridge`, link audio_engine + native_mixer): runtime-load libfluidsynth-3.dll từ `native_host/fluidsynth_runtime/` (gitignore; 22 DLL 12.3MB closure bằng dumpbin BFS; tái tạo bằng `scripts/dl_fluidsynth_runtime.py` — tải 22 MSYS2 mingw64 packages, giải nén zstandard, copy NEEDED list; ponytail: chạy lại BFS khi upgrade). Load bằng `GetModuleHandleW(L"sf_host_bridge")` + `GetModuleFileNameW` (KHÔNG nullptr — host có thể là python.exe) + `LoadLibraryExW(LOAD_WITH_ALTERED_SEARCH_PATH)`. **Export prefix FluidSynth 2.5.x**: `new_fluid_settings/new_fluid_synth/delete_fluid_settings/delete_fluid_synth` (KHÔNG phải `fluid_settings_new` — verify dumpbin). Bỏ setting `synth.lock-memory` (không tồn tại 2.5.6); giữ `audio.driver=file`, `synth_set_gain(1.0)` (mirror WASM client; default 0.2 = −14 dB). 15 export: `SF_FS_Create/LoadSF2/SelectInstrument(handle, channel, sfontId, bank, program)/NoteOn/NoteOff/AllNotesOff/RenderBlock/AudioStart/Stop/Underruns/Latency/Blocks/SetTrackGainPan/SetMasterLimiter/Close`. **BUG ĐÃ SỬA**: `fluid_synth_program_select` tham số 3 là **sfont_id** (không phải bank); `sfontId<0` → dùng `inst->sfid` lưu từ sfload (sfid thật của SF2 test là 1, không phải 0). RenderBlock: offline `write_float` + mixer, không qua WASAPI. - **Verify đã chạy**: `test_sf_host_bridge.py` PASS (Create→LoadSF2→SelectInstrument→AudioStart→NoteOn→blocks 0→40, underruns 0→Stop→Close; preset SF2 test là bank128/prog0 'Standard Kit' — không có 0/0). `test_sf_host_golden.py` PASS — 3 case so reference = libfluidsynth-3.dll ctypes (write_float) + mixer math (render_engine gain/pan + apply_mastering + clip), ngưỡng 0.1 dB, sai lệch thực tế 0.0001 dB (bit-exact): (0dB/pan0/lim-off), (−6dB/lim−1), (−3dB/pan0.8/lim−6). Export count SFHost 15 (dumpbin). - **Ghi chú**: SF2 test sfload trả sfid=1 — test truyền sfontId=-1 (dùng inst->sfid). T13 ghi chú "chưa có FluidSynth native" giờ đã hết — golden T14 dùng reference đúng code path server. ### 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. - **Đã làm (commit — T15)**: 1. C++: `NativeMixer` thêm master fader — `setMasterGain(float linear)` (m_masterGain=1.0), áp SAU limiter+clip trong `processInPlace` (mirror `masterBus.output.gain` sau mastering chain; server render_project không có master fader — WebAudio áp sau mastering). Export mới `SF_FS_/SF_VST2_/SF_VST3_SetMasterGain` (SFHost.cpp, VST2AudioEngine.cpp, vst3_host_bridge.cpp). Export count: SF 16, VST2 19, VST3 18. 2. `app/core/native_audio_service.py` (mới): singleton `NativeAudioService` ctypes wrapper — SF (ensure_sf/note_on/note_off/set_track_gain_pan/stats/audio_stop/render_sf_offline — instance TẠM, RenderBlock loop 256, note events theo start_beat/duration_beats), VST2 (live + offline qua `SF_VST2_Process` raw + `apply_track_mixer` Python mirror NativeMixer/mastering_engine; ponytail: giả định synth 0-input → output ở buf[0:2] vì bridge xếp outputs tại offset numInputs), VST3 (live; offline limitation — bridge chưa có Process export); `set_master_gain(db)` (db<=-50 → 0) áp linear mọi instance; `close_track/close_all/status`; DLL dir = `settings.BASE_DIR/native_host/build/Release` (env `SONICFORGE_NATIVE_DIR` override). BUG ĐÃ SỬA: argtypes SetMasterGain phải 2 tham số (i32, float) — không dùng chung loop 3 tham số với SetTrackGainPan. 3. `app/api/v1/native.py` (mới, đăng ký main.py prefix `/api/v1/native`): status, set_master_gain, track_gain_pan, sf/ensure (nhận sf_id → resolve `_find_sf2_path` server-side), sf/note_on, sf/note_off, sf/audio_stop, vst2/ensure, vst2/note_on, vst2/note_off, render (trả peak/rms cho test matrix). 4. JS: `app/static/js/services/nativeAudioClient.js` (mới, `window.SonicNativeAudio` — setMasterGain debounce 40ms, setTrackGainPan, ensureSf, noteOn/noteOff; include vào index.html); app.jsx — `handleFaderChange` → native set_master_gain, `updateTrackVolumeDb`/`updateTrackPan` → native track_gain_pan (pan/100; dùng tracks state lấy pan/volume hiện tại); `trackInstrument.js` — playNote/noteOff native-first (SF track có sfId → ensureSf + note_on + setTimeout note_off theo durationMs; ponytail: bỏ startTime offset — TrackInstrument không biết audioCtx), SonicSF WASM chỉ fallback. - **Verify đã chạy**: smoke python service (SF live blocks 50→70 underruns 0; master -6dB ratio 0.5012 đúng 10^(-6/20); VST2 live blocks 40→60 underruns 0; VST2 offline peak 0.25 sine, ratio -6/-6 = 0.2512 đúng 10^(-12/20)); API TestClient 10 endpoints OK; `python -m pytest tests/test_native_matrix.py` — 8 passed (SF live blocks/underruns 0, SF offline master gain + track gain pan, VST2 live, VST2 offline sine+mixer+limiter, VST3 bridge export SetMasterGain — limitation, API render); pytest toàn bộ 118 passed 1 skipped 1 env-fail (test_vst_engine Linux path — baseline). Build babel OK; node --check 2 service JS OK. - **Ghi chú**: VST3 live test không có plugin thật — bridge export verify (như T9/T11/T12). AudioEngine mỗi instance WASAPI riêng — chưa có master summing chung giữa track (giới hạn T15). CRLF rule: main.py 2 dòng LF do edit_file → convert lại CRLF toàn file. ### 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. - **Đã làm**: `tools/autosample_vsti.py` — VST2 branch: `_is_vst2_path` (.dll/.so không .vst3), `_render_note_native` qua `get_service().render_vst2_offline` (tail_sec=release), `autosample_sf2` nhận `extra_vst_dirs` + bỏ qua preset VST2 (bridge chưa hỗ trợ chunk); `write_sf2` sửa gen IDs chuẩn SF2: keyRange=43, overridingRootKey=58, sampleID=53 (gen cuối zone) — trước đó gen 60/69/74 bị FluidSynth "Discarding invalid global zone" (rms 1.5e-5 → 0.041). `app/core/soundfont_converter.py` — `_ensure_fluidsynth_runtime()` prepend PATH DLL closure (native_host/build/Release/fluidsynth_runtime) để pyfluidsynth import được; FLUID_SAMPLETYPE_OGG_VORBIS = 0x10 (không phải 0x20 — 0x20 làm sample bị ignore); `_sf3_plays_audio` dùng `Synth().sfload(str)`/get_samples (không có fluid_synth_write_float trong binding); reverse `_sf3_to_sf2` clear flag `& ~0x10`. `plugins.py` `POST /autosample` — truyền `extra_vst_dirs=_dirs` (VST2 scan riêng), không còn 501 cứng VST2. `vstiAutosample.js` — payload preset_path/preset_data, keyFor = plugin_id|preset_id (re-sample khi đổi preset), notify `sf:autosample-update`; `app.jsx` 2 chip badge size/note_count (PianoRollTabEditor + track strip). Babel rebuild `app.precompiled.js`. - **Verify đã chạy**: pipeline thật fake_vst2 → SF2 3 notes, FluidSynth SF2 rms 0.041; convert → SF3 rms 0.044 (cả 2 > 1e-4, warning "invalid sample loops sanitized" vô hại — loopstart/end=0); `python -m pytest tests/` — 119 passed 1 skipped 1 env-fail (test_vst_engine Linux path — baseline); test mới `test_autosample_vst2_native_bridge` (fake_vst2 → 3 notes). - **Ghi chú**: pyfluidsynth cần DLL trên PATH — `_ensure_fluidsynth_runtime()` ở module-level helper; SF3 verify dùng high-level Synth (sfload nhận str, không encode bytes); `write_sf2` inst terminator bagNdx = n (record terminal zone, không phải n+1). - **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 đủ.