# CARLA BRIDGE — NATIVE GUI VSTi (Windows) + DUAL-MODE RUNTIME (Desktop / Docker Headless)

> Tài liệu kỹ thuật & vận hành: tích hợp Carla làm host native GUI cho VSTi trên
> Windows, tự phát hiện môi trường (desktop / headless), thư viện preset
> (.vstpreset) làm cầu nối Carla → pedalboard, và Plugin Manager quản lý
> soundfont + instrument.
>
> Phạm vi code: `app/core/runtime.py`, `app/api/v1/system.py`,
> `app/api/v1/presets.py`, `app/api/v1/plugins.py`, `app/core/vst_engine.py`,
> `app/core/render_engine.py`, `app/static/js/services/runtime.js`, `app.jsx`.

---

## 1. Bối cảnh & quyết định kiến trúc

| Vấn đề | Quyết định |
|---|---|
| `pedalboard` **cố tình headless** — không mở được native GUI VSTi | Không thay thế pedalboard; dùng **Carla làm host GUI ngoài** (preview + chỉnh preset), pedalboard giữ nguyên làm **render engine** (offline, cùng code path cho preview & export) |
| Browser không chạy được binary `.vst3` (Windows/Linux) | Preview realtime thật chỉ có trong **cửa sổ Carla**; trong web UI dùng **quick-render preview** (pedalboard render clip ngắn → wav — **âm thật = âm export**) |
| App chạy 2 môi trường: **Windows desktop** (server+client 1 máy) và **Docker headless** (server + browser UI) | **Runtime profile tự phát hiện** → bật/tắt tính năng theo môi trường (nút Carla Bridge chỉ hiện trên desktop có Carla) |
| Carla Windows là **bộ zip portable** — không installer, không PATH | **User tự định vị** thư mục chứa `carla.exe` (Plugin Manager → Định vị Carla...) + tìm kiếm dự phòng (registry, Program Files, quét nông Downloads/Desktop) |

```
┌────────────────────────── Windows Desktop (1 máy) ──────────────────────────┐
│  Browser/Tauri UI ──► FastAPI (localhost:8000) ──► pedalboard (render VST3) │
│        │                        │                        ▲                  │
│        ▼                        ▼                        │ load_preset      │
│  Nút "Carla Bridge" ──► spawn carla.exe (native GUI)     │                  │
│        │               chọn VSTi, chỉnh âm, Save .vstpreset                 │
│        └──────────────► Upload preset ──► storage/presets ─┘                 │
└─────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────── Docker Headless (server) ─────────────────────────┐
│  Browser UI ──► FastAPI (container :8000) ──► pedalboard (VSTi Linux)       │
│  Soundfont + VSTi mở được: mount volumes (docker-compose.prod.yml)          │
│  Preset .vstpreset: user chỉnh ở máy desktop → upload qua web UI            │
└─────────────────────────────────────────────────────────────────────────────┘
```

---

## 2. Runtime tự phát hiện môi trường (`app/core/runtime.py`)

### 2.1 Nguyên tắc

- **Heuristic + override**: `SF_RUNTIME=auto|desktop|headless` **thắng tuyệt đối**
  (WSL / docker-in-docker / remote desktop có thể làm heuristic sai).
- `SF_DOCKER=1` hoặc tồn tại `/.dockerenv` → nhận diện container.
- Linux: không có `DISPLAY` hoặc trong docker → `headless`; ngược lại `desktop`.
- Windows/macOS → luôn `desktop`.

### 2.2 Capabilities API (frontend gọi 1 lần lúc boot)

```
GET /api/v1/system/capabilities        (public — cần trước khi đăng nhập)
```

```json
{
  "success": true,
  "runtime": "desktop",        // "desktop" | "headless"
  "platform": "windows",       // "windows" | "linux" | "darwin"
  "docker": false,
  "features": {
    "carla_local": true,       // có Carla trên máy → hiện nút "Carla Bridge"
    "carla_path": "D:/Tools/Carla/carla.exe",
    "preset_upload": true,     // luôn true (upload .vstpreset qua web UI)
    "vst_render": true,        // pedalboard khả dụng
    "tauri_bridge": false,
    "preview_mode": "quick_render"   // "quick_render" | "wasm"
  },
  "default_dirs": { "vst": [...], "soundfont": [...], "preset": "..." }
}
```

### 2.3 Bảng hành vi theo môi trường

| Tính năng | Windows desktop | Docker headless |
|---|---|---|
| Render VSTi | pedalboard `VST3Plugin` + `load_preset` | giống hệt (VSTi **bản Linux**) |
| Scan VST/SF | thư mục chuẩn Windows + `plugin_dirs.json` | mount volumes + `plugin_dirs.json` |
| Preview VSTi | quick-render (âm thật); realtime trong Carla | quick-render |
| Nút "Carla Bridge" (nút Synth) | **Hiện** → spawn `carla.exe` | **Ẩn** (không có GUI local) |
| Preset | picker local / upload → thư viện | upload → thư viện |
| SoundFont preview | FluidSynth WASM (browser) | giống hệt |

---

## 3. Cài đặt Carla trên Windows (bản portable zip)

Carla phát hành dạng **zip** (`Carla-2.5.x-win64.zip`) — **không có installer,
không ghi PATH**. Không bundle Carla vào installer của app (license GPL-2.0+,
xem §8).

1. Tải: <https://github.com/falkTX/Carla/releases> (bản `win64`).
2. Giải nén ra bất kỳ đâu (VD `D:\Tools\Carla\`, chứa `carla.exe`).
3. Khai báo 1 lần (2 cách, tùy chọn 1):
   - **Nút bấm**: Plugin Manager → section **"Carla Bridge (VSTi native GUI)"** →
     **"Định vị Carla..."** → chọn thư mục chứa `carla.exe` (hoặc chính file).
   - **Nhập tay**: ô text trong section đó → nhập `D:/Tools/Carla` → **Lưu**.
4. App lưu vào `storage/carla_path.json` → cache detect bị xóa → nút
   **"Carla Bridge"** hiện trong dropdown nút Synth + **tự động mở Carla**
   khi chọn VSTi.

### 3.1 Thứ tự phát hiện `carla_local`

| Ưu tiên | Nguồn |
|---|---|
| 1 | **Config user** (`storage/carla_path.json`) — kênh chính cho bản portable |
| 2 | PATH (`shutil.which`) |
| 3 | Registry (`HKLM/HKCU\SOFTWARE\Carla\InstallPath`) — nếu cài qua installer |
| 4 | Thư mục chuẩn (`Program Files\Carla`, `%LOCALAPPDATA%\Programs\Carla`) |
| 5 | **Quét nông** Downloads/Desktop/Documents (độ sâu ≤ 3, bỏ qua `node_modules`, `AppData`, `Windows`...) — không bao giờ quét toàn ổ đĩa |

```
POST /api/v1/system/carla-path       (auth) — lưu vị trí Carla
body: { "carla_path": "D:/Tools/Carla" }      (thư mục HOẶC file exe)
→ trả { success, carla_path, ...capabilities }
```

---

## 4. Luồng sử dụng end-to-end (Windows)

1. **Nút Synth** → chọn **VSTi** trong danh sách → app **TỰ ĐỘNG gọi Carla**:
   sinh file project `.carxs` (định dạng XML chính thức của Carla —
   `carla.exe [FILE]` nhận project file) chứa node
   `<Plugin><Info><Type>VST3</Type><Binary>path</Binary>...</Info></Plugin>`
   → `carla.exe <project.carxs>` → Carla mở lên **plugin đã load sẵn** kèm
   **on-screen MIDI keyboard** (`PixmapKeyboard`).
   - VST3 (file `.vst3` hoặc folder `X.vst3` Windows): auto-load qua `.carxs`.
   - VST2 (`.dll`/`.so` ngoài `.vst3`): không auto-load tin cậy (cần uniqueID)
     → mở Carla trống để user **Add Plugin** thủ công.
   - Project `.carxs` nằm `{STORAGE_DIR}/carla_projects/`, tự dọn sau 1 ngày.
2. Trong Carla: native GUI của VSTi hiện ra → **chọn instrument/preset** của
   plugin → **bật ARM** trên track trong app (tùy chọn) → **bấm phím trên
   keyboard ảo của Carla** để preview realtime (âm thật qua audio device).
3. **Save preset** bằng nút của CHÍNH plugin (không dùng project save của Carla)
   → file `.vstpreset`.
4. Trong app: dropdown Synth → **"⬆ Upload preset (từ Carla...)"** → chọn file
   `.vstpreset` → nằm trong **thư viện preset** → chọn preset trong danh sách
   **"VST Presets"** → gán vào track (`synth_engine.preset_id`).
5. **Render**: `render_engine.py` nạp plugin qua `load_vst()` rồi
   `apply_preset_to_plugin()` → **âm render = âm đã chỉnh trong Carla**
   (điều kiện: cùng sample rate — xem §7).

Lưu ý: danh sách VSTi trong dropdown vẫn cần thiết — dùng để **map tên →
đường dẫn khi pedalboard nạp plugin lúc render** (không phải để mở GUI).

---

## 5. Thư viện preset — cầu nối Carla ↔ pedalboard

- Thư mục: `{STORAGE_DIR}/presets` (Windows: `%APPDATA%\SonicForgeDAW\storage\presets`;
  Docker: nằm trong volume `sf_db` → `/app/app/storage/presets`).
- Định dạng hỗ trợ: `.vstpreset` (VST3 — chuẩn), `.fxp`/`.fxb` (VST2, chỉ
  preview trong Carla), `.dspreset` (DecentSampler/Pianobook).
- `synth_engine` của track có thể chứa 1 trong 3 dạng (ưu tiên giảm dần):

```json
{ "type": "vst3", "plugin_id": "Kontakt 7",
  "preset_data": "<base64 bytes .vstpreset nhúng — project tự chứa state, portable>",
  "preset_id": "a1b2c3....vstpreset",      // thư viện storage/presets
  "preset_path": "D:/presets/Piano.vstpreset" }
```

### API

```
GET    /api/v1/presets                        (public) — danh sách
POST   /api/v1/presets/upload                 (auth)   — multipart file + plugin_hint
GET    /api/v1/presets/{id}/download
DELETE /api/v1/presets/{id}                   (auth)
```

### Quick-render preview (âm thật = âm export)

```
POST /api/v1/plugins/preview
body: { instrument_id, notes:[{pitch,start_beat,duration_beats,velocity}],
        bpm, sample_rate, preset_id?, preset_path?, preset_data? }
→ { success, url: "/static/audio/processed/preview_xxx.wav", duration_sec }
```

Cùng code path với export (pedalboard + `load_preset`) → **preview nghe đúng
plugin/preset** (khác "Preview Synth WASM" cũ — âm giả).

---

## 6. Plugin Manager — SoundFont: Add Directory → Scan → Instrument → Synth

1. **Add Directory** (folder picker native / in-app browser) → thư mục vào
   danh sách (lưu `plugin_dirs.json`).
2. **Scan** → quét soundfont trong các thư mục (catalog qua
   `SoundFontAutoScanner`) + liệt kê VST.
3. Bấm vào **một soundfont** (▸) → expand danh sách **instrument bên trong**
   (Bank/Program/Tên) qua `GET /api/v1/plugins/soundfont-instruments/{sf_id}`.
4. Nút **"Chèn vào Synth"** → gán instrument (bank/program) vào **track đang
   chọn** (`setTrackInstrumentWithProgram`) và đóng Plugin Manager.

> SF3: instrument đọc sau khi chuyển đổi SF3→SF2 (endpoint download tự chuyển
> đổi khi cần). Nếu thiếu libfluidsynth, danh sách trả `[]` (graceful).

---

## 7. Lưu ý kỹ thuật & hạn chế (đã xác minh)

1. **Sample rate**: Carla chạy theo audio device (thường 48 kHz), pedalboard
   render mặc định 44.1 kHz → plugin phụ thuộc SR (delay/chorus/oversampling)
   nghe khác. **Render đúng SR của thiết bị Carla** để preview = export.
2. **VST2**: pedalboard 0.10+ **đã gỡ hỗ trợ VST2** (chỉ còn VST3) → chỉ preset
   VST3 (`.vstpreset`) round-trip được. Plugin VST2 cũ: preview được trong
   Carla nhưng **không render** được qua pedalboard.
3. **VSTi trên Docker**: chỉ chạy được plugin có bản **Linux** (`.vst3`/`.so`);
   plugin Windows-only (`.dll`) không chạy trên server Linux (không khuyến nghị
   Wine bridge trong container).
4. **License Carla GPL-2.0+**: app **không bundle/nhúng** Carla — chỉ spawn
   tiến trình ngoài + trao đổi file preset (không link code) → không dính
   copyleft. User tự tải zip.
5. **Preview WASM ≠ âm thật**: Preview Synth trong browser không phải plugin
   thật — dùng quick-render (`/plugins/preview`) nếu cần nghe đúng âm.

---

## 8. Checklist QA

- [ ] Windows: cài Carla zip → Định vị Carla → nút "Carla Bridge" hiện ở nút Synth
- [ ] Bấm Carla Bridge → `carla.exe` chạy, mở được VSTi + native GUI
- [ ] Save `.vstpreset` từ GUI plugin → Upload trong app → gán vào track → render ra âm đúng preset
- [ ] Render cùng SR với Carla → preview (Carla) nghe = âm export
- [ ] Plugin Manager: Add Directory → Scan → expand soundfont → "Chèn vào Synth" gán đúng bank/program vào track đang chọn
- [ ] Docker (`SF_DOCKER=1`): capabilities `runtime=headless`, không hiện Carla Bridge, preset upload hoạt động, render VSTi Linux + soundfont từ mount OK
- [ ] `SF_RUNTIME=desktop` trên Linux có DISPLAY → chạy như desktop
- [ ] pytest: `86 passed, 7 skipped`

## 9. Flow MIDI Keyboard hardware (Web MIDI) — route đầy đủ

Hardware MIDI keyboard (Web MIDI API) đi qua ĐÚNG flow như keybed ảo:

```
MIDI Keyboard (hardware, Web MIDI)
   ├─ note-on  → ensureMasteringRouting()  (ép âm qua Mastering FX Chain khi bật)
   │            → SonicSF.playNote()       (track soundfont/GM — qua track node → mastering)
   │            → SonicCarlaMidi.noteOn()  (track VSTi + ARM + Carla local — OSC /Carla/0/note_on)
   ├─ note-off → SonicSF.stopNote()        (dừng soundfont)
   │            → SonicCarlaMidi.noteOff() (dừng VSTi trong Carla — tránh kẹt âm)
   └─ CC64/CC1/Pitch Bend → SonicSF.controllerChange()/pitchBend() (armed tracks)
```

Trước đây chỉ keybed ảo (piano roll) gọi `SonicCarlaMidi` + `__ensureMasteringRouting`;
keyboard hardware bị bỏ sót → track VSTi không kêu khi bấm đàn thật, và âm soundfont
không qua mastering cho tới khi bật/tắt power. Đã sửa tại `onmidimessage` handler
(`app.jsx`): note-on/note-off route qua Carla bridge + ép đồng bộ mastering routing.

Export offline (`clientSideExport`) cũng áp **Main out volume** (master fader) vào
offline master bus — file WAV đúng độ lớn nghe được (trước đây luôn 1.0).
