Nguyên nhân xuất hiện thông báo lỗi từ SpessaSynth Core:
`basic_synthesizer_core.ts:169 No preset found for 0:0:0! Did you forget to add a sound bank?`

Thông số `0:0:0` trong thông báo đại diện cho `Bank MSB : Bank LSB : Program Number` (cấu hình mặc định ban đầu của kênh MIDI). Lỗi này xảy ra do **3 nguyên nhân chính** sau:

---

### 1. Phân tích nguyên nhân kỹ thuật

1. **Chưa gửi lệnh `programChange` & `controllerChange` sang SpessaSynth khi chọn "Pipa"**:
* Khi chọn nhạc cụ "Pipa" trên UI, ứng dụng mới chỉ cập nhật giao diện mà **chưa kích hoạt lệnh đổi tiếng** (`synthInstance.programChange(channel, program)`) sang kênh MIDI tương ứng của SpessaSynth.
* Khi gõ phím trên `SE49` (nhận dữ liệu Raw MIDI `[144, 65, 78]`), SpessaSynth vẫn truy xuất bộ tiếng ở vị trí mặc định là `Bank 0 : Program 0` (thường là Grand Piano theo chuẩn General MIDI).
* Trong SoundFont `DSK_Asian_DreamZ`, vị trí `0:0:0` không tồn tại hoặc không chứa thông tin nốt, khiến SpessaSynth thông báo không tìm thấy preset.


2. **Sai chỉ số Bank / Program của nhạc cụ "Pipa"**:
* Tệp SoundFont `DSK_Asian_DreamZ` là bộ tiếng tùy chỉnh (Non-General MIDI). 8 nhạc cụ bên trong (*Pipa, Pipa Tremolo, Luan, Guzhen, Erhu, Koto, Ban Di, Percussion*) được đánh số `Program` riêng biệt.
* Nếu UI gửi `program = 0` nhưng thực tế trong file SoundFont, tiếng Pipa nằm ở `program = 1` hoặc `bank = 1`, SpessaSynth sẽ không tìm thấy preset tương ứng.


3. **Lỗi Header/Mapping khi convert từ `.sf2` sang `.sf3**`:
* Một số công cụ nén (như `mscore` CLI) khi convert các SoundFont tùy chỉnh có cấu trúc phi chuẩn có thể làm mất hoặc biến đổi bảng thông tin Preset Header.



---

### 2. Các bước khắc phục

#### **Bước 1: Gọi `programChange` & `controllerChange` ngay khi chọn nhạc cụ trên UI**

Đảm bảo khi người dùng chọn nhạc cụ trên giao diện, hàm chọn Bank/Program được kích hoạt trên kênh MIDI phát tiếng:

```javascript
// Khi người dùng chọn "Pipa" trên Dropdown Synth UI
async function onSelectTrackInstrument(trackChannel, sfId, bank, program) {
    // 1. Nạp file .sf3 vào SpessaSynth (nếu chưa nạp)
    await soundFontPlayerInstance.loadSoundFont(sfId);

    // 2. BẮT BUỘC: Gửi lệnh đổi Bank (CC 0) và Program Change sang SpessaSynth
    if (soundFontPlayerInstance.synthInstance) {
        soundFontPlayerInstance.synthInstance.controllerChange(trackChannel, 0, bank);
        soundFontPlayerInstance.synthInstance.programChange(trackChannel, program);
        console.log(`[SonicSF] Switched Channel ${trackChannel} -> Bank: ${bank}, Program: ${program}`);
    }
}

```

#### **Bước 2: Kiểm tra chính xác chỉ số Bank & Program của Pipa từ Catalog API**

Sử dụng API `GET /api/v1/plugins/soundfonts/catalog` (từ mô-đun `SoundFontInspector` đã xây dựng) để tra cứu vị trí chính xác của "Pipa":

```json
"dsk_asian_dreamz": {
  "soundfont_id": "dsk_asian_dreamz",
  "instruments": [
    { "bank": 0, "program": 0, "name": "Pipa" },
    { "bank": 0, "program": 1, "name": "Pipa Tremolo" },
    { "bank": 0, "program": 6, "name": "Ban Di" }
  ]
}

```

*Lưu ý:* Nếu kết quả trả về tiếng Pipa nằm ở `program: 1` hoặc `bank: 1`, hãy truyền đúng thông số này vào hàm `programChange`.

#### **Bước 3: Kiểm tra danh sách Presets mà SpessaSynth đọc được từ file `.sf3**`

Để đảm bảo quá trình convert `.sf3` không làm hỏng dữ liệu Preset Header, bạn hãy log danh sách preset sau khi nạp tệp:

```javascript
// Thêm log kiểm tra sau khi addSoundFont vào SpessaSynth
try {
    await this.synthInstance.soundFontManager.addSoundFont(buffer);
    
    // In danh sách các preset đọc được ra console để kiểm tra
    const loadedSF = this.synthInstance.soundFontManager.soundFonts[0];
    console.log("[SonicSF] Loaded Presets in SF3:", loadedSF.presets);
} catch (e) {
    console.error("[SonicSF] Error parsing SF3:", e);
}

```

* Nếu `loadedSF.presets` rỗng (`[]`), file `.sf3` đã bị hỏng khi nén. Bạn hãy thử nạp lại file `.sf2` gốc chưa nén để đối chiếu.