Files
SonicForgeStudio/plans/WALKTHROUGH.md
T

30 KiB
Raw Blame History

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 \ns.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 (T1T3) → B (T4T7) → C (T8T15) → 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 25 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 T4T5 đã 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<trackId, TrackInstrument> (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: onmidimessagerouter.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); CloseaudioRunning=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 ở T13T16.

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): NativeMixerTrackGainPan::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): gainpan rồi nếu limiter active → tanh(xk)/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 pan0.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/lim1), (3dB/pan0.8/lim6). 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 đủ.