docs: phân tích + hướng fix VSTi play crackle/noise (Linux review)
This commit is contained in:
@@ -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ạ".
|
||||||
Reference in New Issue
Block a user